mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
feat: Add workflow dashboard template and utility functions
- Implemented a new HTML template for the workflow dashboard, featuring a responsive design with dark/light theme support, session statistics, and task management UI. - Created a browser launcher utility to open HTML files in the default browser across platforms. - Developed file utility functions for safe reading and writing of JSON and text files. - Added path resolver utilities to validate and resolve file paths, ensuring security against path traversal attacks. - Introduced UI utilities for displaying styled messages and banners in the console.
This commit is contained in:
309
ccw/src/commands/install.js
Normal file
309
ccw/src/commands/install.js
Normal file
@@ -0,0 +1,309 @@
|
||||
import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { join, dirname, basename, relative } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { fileURLToPath } from 'url';
|
||||
import inquirer from 'inquirer';
|
||||
import chalk from 'chalk';
|
||||
import { showHeader, showBanner, createSpinner, success, info, warning, error, summaryBox, step, divider } from '../utils/ui.js';
|
||||
import { createManifest, addFileEntry, addDirectoryEntry, saveManifest, findManifest, getAllManifests } from '../core/manifest.js';
|
||||
import { validatePath } from '../utils/path-resolver.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Source directories to install
|
||||
const SOURCE_DIRS = ['.claude', '.codex', '.gemini', '.qwen'];
|
||||
|
||||
// Get package root directory (ccw/src/commands -> ccw)
|
||||
function getPackageRoot() {
|
||||
return join(__dirname, '..', '..');
|
||||
}
|
||||
|
||||
// Get source installation directory (parent of ccw)
|
||||
function getSourceDir() {
|
||||
return join(getPackageRoot(), '..');
|
||||
}
|
||||
|
||||
/**
|
||||
* Install command handler
|
||||
* @param {Object} options - Command options
|
||||
*/
|
||||
export async function installCommand(options) {
|
||||
const version = getVersion();
|
||||
|
||||
// Show beautiful header
|
||||
showHeader(version);
|
||||
|
||||
// Check for existing installations
|
||||
const existingManifests = getAllManifests();
|
||||
if (existingManifests.length > 0 && !options.force) {
|
||||
info('Existing installations detected:');
|
||||
console.log('');
|
||||
existingManifests.forEach((m, i) => {
|
||||
console.log(chalk.gray(` ${i + 1}. ${m.installation_mode} - ${m.installation_path}`));
|
||||
console.log(chalk.gray(` Installed: ${new Date(m.installation_date).toLocaleDateString()}`));
|
||||
});
|
||||
console.log('');
|
||||
|
||||
const { proceed } = await inquirer.prompt([{
|
||||
type: 'confirm',
|
||||
name: 'proceed',
|
||||
message: 'Continue with new installation?',
|
||||
default: true
|
||||
}]);
|
||||
|
||||
if (!proceed) {
|
||||
info('Installation cancelled');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Interactive mode selection
|
||||
const mode = options.mode || await selectMode();
|
||||
|
||||
let installPath;
|
||||
if (mode === 'Global') {
|
||||
installPath = homedir();
|
||||
info(`Global installation to: ${installPath}`);
|
||||
} else {
|
||||
const inputPath = options.path || await selectPath();
|
||||
|
||||
// Validate the installation path
|
||||
const pathValidation = validatePath(inputPath, { mustExist: true });
|
||||
if (!pathValidation.valid) {
|
||||
error(`Invalid installation path: ${pathValidation.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
installPath = pathValidation.path;
|
||||
info(`Path installation to: ${installPath}`);
|
||||
}
|
||||
|
||||
// Validate source directories exist
|
||||
const sourceDir = getSourceDir();
|
||||
const availableDirs = SOURCE_DIRS.filter(dir => existsSync(join(sourceDir, dir)));
|
||||
|
||||
if (availableDirs.length === 0) {
|
||||
error('No source directories found to install.');
|
||||
error(`Expected directories in: ${sourceDir}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
info(`Found ${availableDirs.length} directories to install: ${availableDirs.join(', ')}`);
|
||||
divider();
|
||||
|
||||
// Check for existing installation at target path
|
||||
const existingManifest = findManifest(installPath, mode);
|
||||
if (existingManifest) {
|
||||
warning('Existing installation found at this location');
|
||||
const { backup } = await inquirer.prompt([{
|
||||
type: 'confirm',
|
||||
name: 'backup',
|
||||
message: 'Create backup before reinstalling?',
|
||||
default: true
|
||||
}]);
|
||||
|
||||
if (backup) {
|
||||
await createBackup(installPath, existingManifest);
|
||||
}
|
||||
}
|
||||
|
||||
// Create manifest
|
||||
const manifest = createManifest(mode, installPath);
|
||||
|
||||
// Perform installation
|
||||
console.log('');
|
||||
const spinner = createSpinner('Installing files...').start();
|
||||
|
||||
let totalFiles = 0;
|
||||
let totalDirs = 0;
|
||||
|
||||
try {
|
||||
for (const dir of availableDirs) {
|
||||
const srcPath = join(sourceDir, dir);
|
||||
const destPath = join(installPath, dir);
|
||||
|
||||
spinner.text = `Installing ${dir}...`;
|
||||
|
||||
const { files, directories } = await copyDirectory(srcPath, destPath, manifest);
|
||||
totalFiles += files;
|
||||
totalDirs += directories;
|
||||
}
|
||||
|
||||
// Create version.json
|
||||
const versionPath = join(installPath, '.claude', 'version.json');
|
||||
if (existsSync(dirname(versionPath))) {
|
||||
const versionInfo = {
|
||||
version: version,
|
||||
installedAt: new Date().toISOString(),
|
||||
mode: mode,
|
||||
installer: 'ccw'
|
||||
};
|
||||
writeFileSync(versionPath, JSON.stringify(versionInfo, null, 2), 'utf8');
|
||||
addFileEntry(manifest, versionPath);
|
||||
totalFiles++;
|
||||
}
|
||||
|
||||
spinner.succeed('Installation complete!');
|
||||
|
||||
} catch (err) {
|
||||
spinner.fail('Installation failed');
|
||||
error(err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Save manifest
|
||||
const manifestPath = saveManifest(manifest);
|
||||
|
||||
// Show summary
|
||||
console.log('');
|
||||
summaryBox({
|
||||
title: ' Installation Summary ',
|
||||
lines: [
|
||||
chalk.green.bold('✓ Installation Successful'),
|
||||
'',
|
||||
chalk.white(`Mode: ${chalk.cyan(mode)}`),
|
||||
chalk.white(`Path: ${chalk.cyan(installPath)}`),
|
||||
'',
|
||||
chalk.gray(`Files installed: ${totalFiles}`),
|
||||
chalk.gray(`Directories created: ${totalDirs}`),
|
||||
'',
|
||||
chalk.gray(`Manifest: ${basename(manifestPath)}`),
|
||||
],
|
||||
borderColor: 'green'
|
||||
});
|
||||
|
||||
// Show next steps
|
||||
console.log('');
|
||||
info('Next steps:');
|
||||
console.log(chalk.gray(' 1. Restart Claude Code or your IDE'));
|
||||
console.log(chalk.gray(' 2. Run: ccw view - to open the workflow dashboard'));
|
||||
console.log(chalk.gray(' 3. Run: ccw uninstall - to remove this installation'));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactive mode selection
|
||||
* @returns {Promise<string>} - Selected mode
|
||||
*/
|
||||
async function selectMode() {
|
||||
const { mode } = await inquirer.prompt([{
|
||||
type: 'list',
|
||||
name: 'mode',
|
||||
message: 'Select installation mode:',
|
||||
choices: [
|
||||
{
|
||||
name: `${chalk.cyan('Global')} - Install to home directory (recommended)`,
|
||||
value: 'Global'
|
||||
},
|
||||
{
|
||||
name: `${chalk.yellow('Path')} - Install to specific project path`,
|
||||
value: 'Path'
|
||||
}
|
||||
]
|
||||
}]);
|
||||
|
||||
return mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactive path selection
|
||||
* @returns {Promise<string>} - Selected path
|
||||
*/
|
||||
async function selectPath() {
|
||||
const { path } = await inquirer.prompt([{
|
||||
type: 'input',
|
||||
name: 'path',
|
||||
message: 'Enter installation path:',
|
||||
default: process.cwd(),
|
||||
validate: (input) => {
|
||||
if (!input) return 'Path is required';
|
||||
if (!existsSync(input)) {
|
||||
return `Path does not exist: ${input}`;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}]);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create backup of existing installation
|
||||
* @param {string} installPath - Installation path
|
||||
* @param {Object} manifest - Existing manifest
|
||||
*/
|
||||
async function createBackup(installPath, manifest) {
|
||||
const spinner = createSpinner('Creating backup...').start();
|
||||
|
||||
try {
|
||||
const timestamp = new Date().toISOString().replace(/[-:]/g, '').replace('T', '-').split('.')[0];
|
||||
const backupDir = join(installPath, `.claude-backup-${timestamp}`);
|
||||
|
||||
mkdirSync(backupDir, { recursive: true });
|
||||
|
||||
// Copy existing .claude directory
|
||||
const claudeDir = join(installPath, '.claude');
|
||||
if (existsSync(claudeDir)) {
|
||||
await copyDirectory(claudeDir, join(backupDir, '.claude'));
|
||||
}
|
||||
|
||||
spinner.succeed(`Backup created: ${backupDir}`);
|
||||
} catch (err) {
|
||||
spinner.warn(`Backup failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy directory recursively
|
||||
* @param {string} src - Source directory
|
||||
* @param {string} dest - Destination directory
|
||||
* @param {Object} manifest - Manifest to track files (optional)
|
||||
* @returns {Object} - Count of files and directories
|
||||
*/
|
||||
async function copyDirectory(src, dest, manifest = null) {
|
||||
let files = 0;
|
||||
let directories = 0;
|
||||
|
||||
// Create destination directory
|
||||
if (!existsSync(dest)) {
|
||||
mkdirSync(dest, { recursive: true });
|
||||
directories++;
|
||||
if (manifest) addDirectoryEntry(manifest, dest);
|
||||
}
|
||||
|
||||
const entries = readdirSync(src);
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = join(src, entry);
|
||||
const destPath = join(dest, entry);
|
||||
const stat = statSync(srcPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
const result = await copyDirectory(srcPath, destPath, manifest);
|
||||
files += result.files;
|
||||
directories += result.directories;
|
||||
} else {
|
||||
copyFileSync(srcPath, destPath);
|
||||
files++;
|
||||
if (manifest) addFileEntry(manifest, destPath);
|
||||
}
|
||||
}
|
||||
|
||||
return { files, directories };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get package version
|
||||
* @returns {string} - Version string
|
||||
*/
|
||||
function getVersion() {
|
||||
try {
|
||||
const pkgPath = join(getPackageRoot(), 'package.json');
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
||||
return pkg.version || '1.0.0';
|
||||
} catch {
|
||||
return '1.0.0';
|
||||
}
|
||||
}
|
||||
37
ccw/src/commands/list.js
Normal file
37
ccw/src/commands/list.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import chalk from 'chalk';
|
||||
import { showBanner, divider, info } from '../utils/ui.js';
|
||||
import { getAllManifests } from '../core/manifest.js';
|
||||
|
||||
/**
|
||||
* List command handler - shows all installations
|
||||
*/
|
||||
export async function listCommand() {
|
||||
showBanner();
|
||||
console.log(chalk.cyan.bold(' Installed Claude Code Workflow Instances\n'));
|
||||
|
||||
const manifests = getAllManifests();
|
||||
|
||||
if (manifests.length === 0) {
|
||||
info('No installations found.');
|
||||
console.log('');
|
||||
console.log(chalk.gray(' Run: ccw install - to install Claude Code Workflow'));
|
||||
console.log('');
|
||||
return;
|
||||
}
|
||||
|
||||
manifests.forEach((m, i) => {
|
||||
const modeColor = m.installation_mode === 'Global' ? chalk.cyan : chalk.yellow;
|
||||
|
||||
console.log(chalk.white.bold(` ${i + 1}. `) + modeColor.bold(m.installation_mode));
|
||||
console.log(chalk.gray(` Path: ${m.installation_path}`));
|
||||
console.log(chalk.gray(` Date: ${new Date(m.installation_date).toLocaleDateString()}`));
|
||||
console.log(chalk.gray(` Version: ${m.application_version}`));
|
||||
console.log(chalk.gray(` Files: ${m.files_count}`));
|
||||
console.log(chalk.gray(` Dirs: ${m.directories_count}`));
|
||||
console.log('');
|
||||
});
|
||||
|
||||
divider();
|
||||
console.log(chalk.gray(' Run: ccw uninstall - to remove an installation'));
|
||||
console.log('');
|
||||
}
|
||||
238
ccw/src/commands/uninstall.js
Normal file
238
ccw/src/commands/uninstall.js
Normal file
@@ -0,0 +1,238 @@
|
||||
import { existsSync, unlinkSync, rmdirSync, readdirSync, statSync } from 'fs';
|
||||
import { join, dirname, basename } from 'path';
|
||||
import inquirer from 'inquirer';
|
||||
import chalk from 'chalk';
|
||||
import { showBanner, createSpinner, success, info, warning, error, summaryBox, divider } from '../utils/ui.js';
|
||||
import { getAllManifests, deleteManifest } from '../core/manifest.js';
|
||||
|
||||
/**
|
||||
* Uninstall command handler
|
||||
* @param {Object} options - Command options
|
||||
*/
|
||||
export async function uninstallCommand(options) {
|
||||
showBanner();
|
||||
console.log(chalk.cyan.bold(' Uninstall Claude Code Workflow\n'));
|
||||
|
||||
// Get all manifests
|
||||
const manifests = getAllManifests();
|
||||
|
||||
if (manifests.length === 0) {
|
||||
warning('No installations found.');
|
||||
info('Nothing to uninstall.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Display installations
|
||||
console.log(chalk.white.bold(' Found installations:\n'));
|
||||
|
||||
manifests.forEach((m, i) => {
|
||||
const modeColor = m.installation_mode === 'Global' ? chalk.cyan : chalk.yellow;
|
||||
console.log(chalk.white(` ${i + 1}. `) + modeColor.bold(m.installation_mode));
|
||||
console.log(chalk.gray(` Path: ${m.installation_path}`));
|
||||
console.log(chalk.gray(` Date: ${new Date(m.installation_date).toLocaleDateString()}`));
|
||||
console.log(chalk.gray(` Version: ${m.application_version}`));
|
||||
console.log(chalk.gray(` Files: ${m.files_count} | Dirs: ${m.directories_count}`));
|
||||
console.log('');
|
||||
});
|
||||
|
||||
divider();
|
||||
|
||||
// Select installation to uninstall
|
||||
let selectedManifest;
|
||||
|
||||
if (manifests.length === 1) {
|
||||
const { confirm } = await inquirer.prompt([{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: `Uninstall ${manifests[0].installation_mode} installation at ${manifests[0].installation_path}?`,
|
||||
default: false
|
||||
}]);
|
||||
|
||||
if (!confirm) {
|
||||
info('Uninstall cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
selectedManifest = manifests[0];
|
||||
} else {
|
||||
const choices = manifests.map((m, i) => ({
|
||||
name: `${m.installation_mode} - ${m.installation_path}`,
|
||||
value: i
|
||||
}));
|
||||
|
||||
choices.push({ name: chalk.gray('Cancel'), value: -1 });
|
||||
|
||||
const { selection } = await inquirer.prompt([{
|
||||
type: 'list',
|
||||
name: 'selection',
|
||||
message: 'Select installation to uninstall:',
|
||||
choices
|
||||
}]);
|
||||
|
||||
if (selection === -1) {
|
||||
info('Uninstall cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
selectedManifest = manifests[selection];
|
||||
|
||||
// Confirm selection
|
||||
const { confirm } = await inquirer.prompt([{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: `Are you sure you want to uninstall ${selectedManifest.installation_mode} installation?`,
|
||||
default: false
|
||||
}]);
|
||||
|
||||
if (!confirm) {
|
||||
info('Uninstall cancelled');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// Perform uninstallation
|
||||
const spinner = createSpinner('Removing files...').start();
|
||||
|
||||
let removedFiles = 0;
|
||||
let removedDirs = 0;
|
||||
let failedFiles = [];
|
||||
|
||||
try {
|
||||
// Remove files first (in reverse order to handle nested files)
|
||||
const files = [...(selectedManifest.files || [])].reverse();
|
||||
|
||||
for (const fileEntry of files) {
|
||||
const filePath = fileEntry.path;
|
||||
spinner.text = `Removing: ${basename(filePath)}`;
|
||||
|
||||
try {
|
||||
if (existsSync(filePath)) {
|
||||
unlinkSync(filePath);
|
||||
removedFiles++;
|
||||
}
|
||||
} catch (err) {
|
||||
failedFiles.push({ path: filePath, error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
// Remove directories (in reverse order to remove nested dirs first)
|
||||
const directories = [...(selectedManifest.directories || [])].reverse();
|
||||
|
||||
// Sort by path length (deepest first)
|
||||
directories.sort((a, b) => b.path.length - a.path.length);
|
||||
|
||||
for (const dirEntry of directories) {
|
||||
const dirPath = dirEntry.path;
|
||||
spinner.text = `Removing directory: ${basename(dirPath)}`;
|
||||
|
||||
try {
|
||||
if (existsSync(dirPath)) {
|
||||
// Only remove if empty
|
||||
const contents = readdirSync(dirPath);
|
||||
if (contents.length === 0) {
|
||||
rmdirSync(dirPath);
|
||||
removedDirs++;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore directory removal errors (might not be empty)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to clean up parent directories if empty
|
||||
const installPath = selectedManifest.installation_path;
|
||||
for (const dir of ['.claude', '.codex', '.gemini', '.qwen']) {
|
||||
const dirPath = join(installPath, dir);
|
||||
try {
|
||||
if (existsSync(dirPath)) {
|
||||
await removeEmptyDirs(dirPath);
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
spinner.succeed('Uninstall complete!');
|
||||
|
||||
} catch (err) {
|
||||
spinner.fail('Uninstall failed');
|
||||
error(err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete manifest
|
||||
deleteManifest(selectedManifest.manifest_file);
|
||||
|
||||
// Show summary
|
||||
console.log('');
|
||||
|
||||
if (failedFiles.length > 0) {
|
||||
summaryBox({
|
||||
title: ' Uninstall Summary ',
|
||||
lines: [
|
||||
chalk.yellow.bold('⚠ Partially Completed'),
|
||||
'',
|
||||
chalk.white(`Files removed: ${chalk.green(removedFiles)}`),
|
||||
chalk.white(`Directories removed: ${chalk.green(removedDirs)}`),
|
||||
chalk.white(`Failed: ${chalk.red(failedFiles.length)}`),
|
||||
'',
|
||||
chalk.gray('Some files could not be removed.'),
|
||||
chalk.gray('They may be in use or require elevated permissions.'),
|
||||
],
|
||||
borderColor: 'yellow'
|
||||
});
|
||||
|
||||
if (process.env.DEBUG) {
|
||||
console.log('');
|
||||
console.log(chalk.gray('Failed files:'));
|
||||
failedFiles.forEach(f => {
|
||||
console.log(chalk.red(` ${f.path}: ${f.error}`));
|
||||
});
|
||||
}
|
||||
} else {
|
||||
summaryBox({
|
||||
title: ' Uninstall Summary ',
|
||||
lines: [
|
||||
chalk.green.bold('✓ Successfully Uninstalled'),
|
||||
'',
|
||||
chalk.white(`Files removed: ${chalk.green(removedFiles)}`),
|
||||
chalk.white(`Directories removed: ${chalk.green(removedDirs)}`),
|
||||
'',
|
||||
chalk.gray('Manifest removed'),
|
||||
],
|
||||
borderColor: 'green'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively remove empty directories
|
||||
* @param {string} dirPath - Directory path
|
||||
*/
|
||||
async function removeEmptyDirs(dirPath) {
|
||||
if (!existsSync(dirPath)) return;
|
||||
|
||||
const stat = statSync(dirPath);
|
||||
if (!stat.isDirectory()) return;
|
||||
|
||||
let files = readdirSync(dirPath);
|
||||
|
||||
// Recursively check subdirectories
|
||||
for (const file of files) {
|
||||
const filePath = join(dirPath, file);
|
||||
if (statSync(filePath).isDirectory()) {
|
||||
await removeEmptyDirs(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-check after processing subdirectories
|
||||
files = readdirSync(dirPath);
|
||||
if (files.length === 0) {
|
||||
rmdirSync(dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
132
ccw/src/commands/view.js
Normal file
132
ccw/src/commands/view.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import { scanSessions } from '../core/session-scanner.js';
|
||||
import { aggregateData } from '../core/data-aggregator.js';
|
||||
import { generateDashboard } from '../core/dashboard-generator.js';
|
||||
import { launchBrowser, isHeadlessEnvironment } from '../utils/browser-launcher.js';
|
||||
import { resolvePath, ensureDir, getWorkflowDir, validatePath, validateOutputPath } from '../utils/path-resolver.js';
|
||||
import chalk from 'chalk';
|
||||
import { writeFileSync, existsSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
|
||||
/**
|
||||
* View command handler - generates and opens workflow dashboard
|
||||
* @param {Object} options - Command options
|
||||
*/
|
||||
export async function viewCommand(options) {
|
||||
// Validate project path
|
||||
const pathValidation = validatePath(options.path, { mustExist: true });
|
||||
if (!pathValidation.valid) {
|
||||
console.error(chalk.red(`\n Error: ${pathValidation.error}\n`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const workingDir = pathValidation.path;
|
||||
const workflowDir = join(workingDir, '.workflow');
|
||||
|
||||
console.log(chalk.blue.bold('\n CCW Dashboard Generator\n'));
|
||||
console.log(chalk.gray(` Project: ${workingDir}`));
|
||||
console.log(chalk.gray(` Workflow: ${workflowDir}\n`));
|
||||
|
||||
// Check if .workflow directory exists
|
||||
if (!existsSync(workflowDir)) {
|
||||
console.log(chalk.yellow(' No .workflow directory found.'));
|
||||
console.log(chalk.gray(' This project may not have any workflow sessions yet.\n'));
|
||||
|
||||
// Still generate an empty dashboard
|
||||
const emptyData = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
activeSessions: [],
|
||||
archivedSessions: [],
|
||||
reviewData: null,
|
||||
statistics: {
|
||||
totalSessions: 0,
|
||||
activeSessions: 0,
|
||||
totalTasks: 0,
|
||||
completedTasks: 0,
|
||||
reviewFindings: 0
|
||||
}
|
||||
};
|
||||
|
||||
await generateAndOpen(emptyData, workflowDir, options);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: Scan for sessions
|
||||
console.log(chalk.cyan(' Scanning sessions...'));
|
||||
const sessions = await scanSessions(workflowDir);
|
||||
console.log(chalk.green(` Found ${sessions.active.length} active, ${sessions.archived.length} archived sessions`));
|
||||
|
||||
if (sessions.hasReviewData) {
|
||||
console.log(chalk.magenta(' Review data detected - will include Reviews tab'));
|
||||
}
|
||||
|
||||
// Step 2: Aggregate all data
|
||||
console.log(chalk.cyan(' Aggregating data...'));
|
||||
const dashboardData = await aggregateData(sessions, workflowDir);
|
||||
|
||||
// Log statistics
|
||||
const stats = dashboardData.statistics;
|
||||
console.log(chalk.gray(` Tasks: ${stats.completedTasks}/${stats.totalTasks} completed`));
|
||||
if (stats.reviewFindings > 0) {
|
||||
console.log(chalk.gray(` Review findings: ${stats.reviewFindings}`));
|
||||
}
|
||||
|
||||
// Step 3 & 4: Generate and open
|
||||
await generateAndOpen(dashboardData, workflowDir, options);
|
||||
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`\n Error: ${error.message}\n`));
|
||||
if (process.env.DEBUG) {
|
||||
console.error(error.stack);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate dashboard and optionally open in browser
|
||||
* @param {Object} data - Dashboard data
|
||||
* @param {string} workflowDir - Path to .workflow
|
||||
* @param {Object} options - Command options
|
||||
*/
|
||||
async function generateAndOpen(data, workflowDir, options) {
|
||||
// Step 3: Generate dashboard HTML
|
||||
console.log(chalk.cyan(' Generating dashboard...'));
|
||||
const html = await generateDashboard(data);
|
||||
|
||||
// Step 4: Validate and write dashboard file
|
||||
let outputPath;
|
||||
if (options.output) {
|
||||
const outputValidation = validateOutputPath(options.output, workflowDir);
|
||||
if (!outputValidation.valid) {
|
||||
console.error(chalk.red(`\n Error: ${outputValidation.error}\n`));
|
||||
process.exit(1);
|
||||
}
|
||||
outputPath = outputValidation.path;
|
||||
} else {
|
||||
outputPath = join(workflowDir, 'dashboard.html');
|
||||
}
|
||||
|
||||
ensureDir(dirname(outputPath));
|
||||
writeFileSync(outputPath, html, 'utf8');
|
||||
console.log(chalk.green(` Dashboard saved: ${outputPath}`));
|
||||
|
||||
// Step 5: Open in browser (unless --no-browser or headless environment)
|
||||
if (options.browser !== false) {
|
||||
if (isHeadlessEnvironment()) {
|
||||
console.log(chalk.yellow('\n Running in CI/headless environment - skipping browser launch'));
|
||||
console.log(chalk.gray(` Open manually: file://${outputPath.replace(/\\/g, '/')}\n`));
|
||||
} else {
|
||||
console.log(chalk.cyan(' Opening in browser...'));
|
||||
try {
|
||||
await launchBrowser(outputPath);
|
||||
console.log(chalk.green.bold('\n Dashboard opened in browser!\n'));
|
||||
} catch (error) {
|
||||
console.log(chalk.yellow(`\n Could not open browser: ${error.message}`));
|
||||
console.log(chalk.gray(` Open manually: file://${outputPath.replace(/\\/g, '/')}\n`));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.gray(`\n Open in browser: file://${outputPath.replace(/\\/g, '/')}\n`));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user