import { existsSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; import { join, dirname, basename } from 'path'; import { homedir } from 'os'; import { fileURLToPath } from 'url'; import inquirer from 'inquirer'; import chalk from 'chalk'; import { showBanner, createSpinner, info, warning, error, summaryBox, divider } from '../utils/ui.js'; import { getAllManifests, createManifest, addFileEntry, addDirectoryEntry, saveManifest, deleteManifest } from '../core/manifest.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Source directories to install const SOURCE_DIRS = ['.claude', '.codex', '.gemini', '.qwen']; // Subdirectories that should always be installed to global (~/.claude/) const GLOBAL_SUBDIRS = ['workflows', 'scripts', 'templates']; interface UpgradeOptions { all?: boolean; } interface UpgradeResult { files: number; directories: number; } interface CopyResult { files: number; directories: number; } // Get package root directory (ccw/src/commands -> ccw) function getPackageRoot(): string { return join(__dirname, '..', '..'); } // Get source installation directory (parent of ccw) function getSourceDir(): string { return join(getPackageRoot(), '..'); } /** * Get package version * @returns {string} - Version string */ function getVersion(): string { try { // First try root package.json (parent of ccw) const rootPkgPath = join(getSourceDir(), 'package.json'); if (existsSync(rootPkgPath)) { const pkg = JSON.parse(readFileSync(rootPkgPath, 'utf8')); if (pkg.version) return pkg.version; } // Fallback to ccw package.json const pkgPath = join(getPackageRoot(), 'package.json'); const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); return pkg.version || '1.0.0'; } catch { return '1.0.0'; } } /** * Upgrade command handler * @param {Object} options - Command options */ export async function upgradeCommand(options: UpgradeOptions): Promise { showBanner(); console.log(chalk.cyan.bold(' Upgrade Claude Code Workflow\n')); const currentVersion = getVersion(); // Get all manifests const manifests = getAllManifests(); if (manifests.length === 0) { warning('No installations found.'); info('Run "ccw install" to install first.'); return; } // Display current installations console.log(chalk.white.bold(' Current installations:\n')); const upgradeTargets: any[] = []; for (let i = 0; i < manifests.length; i++) { const m = manifests[i]; const modeColor = m.installation_mode === 'Global' ? chalk.cyan : chalk.yellow; // Read installed version const versionFile = join(m.installation_path, '.claude', 'version.json'); let installedVersion = 'unknown'; if (existsSync(versionFile)) { try { const versionData = JSON.parse(readFileSync(versionFile, 'utf8')); installedVersion = versionData.version || 'unknown'; } catch { // Ignore parse errors } } // Check if upgrade needed const needsUpgrade = installedVersion !== currentVersion; console.log(chalk.white(` ${i + 1}. `) + modeColor.bold(m.installation_mode)); console.log(chalk.gray(` Path: ${m.installation_path}`)); console.log(chalk.gray(` Installed: ${installedVersion}`)); if (needsUpgrade) { console.log(chalk.green(` Package: ${currentVersion} `) + chalk.green('← Update available')); upgradeTargets.push({ manifest: m, installedVersion, index: i }); } else { console.log(chalk.gray(` Up to date ✓`)); } console.log(''); } divider(); if (upgradeTargets.length === 0) { info('All installations are up to date.'); console.log(''); info('To upgrade ccw itself, run:'); console.log(chalk.cyan(' npm update -g ccw')); console.log(''); return; } // Select which installations to upgrade let selectedManifests: any[] = []; if (options.all) { selectedManifests = upgradeTargets.map(t => t.manifest); } else if (upgradeTargets.length === 1) { const target = upgradeTargets[0]; const { confirm } = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message: `Upgrade ${target.manifest.installation_mode} installation (${target.installedVersion} → ${currentVersion})?`, default: true }]); if (!confirm) { info('Upgrade cancelled'); return; } selectedManifests = [target.manifest]; } else { const choices = upgradeTargets.map((t, i) => ({ name: `${t.manifest.installation_mode} - ${t.manifest.installation_path} (${t.installedVersion} → ${currentVersion})`, value: i, checked: true })); const { selections } = await inquirer.prompt([{ type: 'checkbox', name: 'selections', message: 'Select installations to upgrade:', choices }]); if (selections.length === 0) { info('No installations selected'); return; } selectedManifests = selections.map((i: number) => upgradeTargets[i].manifest); } // Perform upgrades console.log(''); const results: any[] = []; const sourceDir = getSourceDir(); for (const manifest of selectedManifests) { const upgradeSpinner = createSpinner(`Upgrading ${manifest.installation_mode} at ${manifest.installation_path}...`).start(); try { const result = await performUpgrade(manifest, sourceDir, currentVersion); upgradeSpinner.succeed(`Upgraded ${manifest.installation_mode}: ${result.files} files`); results.push({ manifest, success: true, ...result }); } catch (err) { const errMsg = err as Error; upgradeSpinner.fail(`Failed to upgrade ${manifest.installation_mode}`); error(errMsg.message); results.push({ manifest, success: false, error: errMsg.message }); } } // Show summary console.log(''); const successCount = results.filter(r => r.success).length; const failCount = results.filter(r => !r.success).length; const summaryLines = [ successCount === results.length ? chalk.green.bold('✓ Upgrade Successful') : chalk.yellow.bold('⚠ Upgrade Completed with Issues'), '', chalk.white(`Version: ${chalk.cyan(currentVersion)}`), '' ]; if (successCount > 0) { summaryLines.push(chalk.green(`Upgraded: ${successCount} installation(s)`)); } if (failCount > 0) { summaryLines.push(chalk.red(`Failed: ${failCount} installation(s)`)); } summaryBox({ title: ' Upgrade Summary ', lines: summaryLines, borderColor: failCount > 0 ? 'yellow' : '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(''); } /** * Perform upgrade for a single installation * @param {Object} manifest - Installation manifest * @param {string} sourceDir - Source directory * @param {string} version - Version string * @returns {Promise} - Upgrade result */ async function performUpgrade(manifest: any, sourceDir: string, version: string): Promise { const installPath = manifest.installation_path; const mode = manifest.installation_mode; // Get available source directories const availableDirs = SOURCE_DIRS.filter(dir => existsSync(join(sourceDir, dir))); if (availableDirs.length === 0) { throw new Error('No source directories found'); } // Create new manifest const newManifest = createManifest(mode, installPath); let totalFiles = 0; let totalDirs = 0; // For Path mode, upgrade workflows to global first if (mode === 'Path') { const globalPath = homedir(); for (const subdir of GLOBAL_SUBDIRS) { const srcWorkflows = join(sourceDir, '.claude', subdir); if (existsSync(srcWorkflows)) { const destWorkflows = join(globalPath, '.claude', subdir); const { files, directories } = await copyDirectory(srcWorkflows, destWorkflows, newManifest); totalFiles += files; totalDirs += directories; } } } // Copy each directory for (const dir of availableDirs) { const srcPath = join(sourceDir, dir); const destPath = join(installPath, dir); // For Path mode on .claude, exclude global subdirs (they're already installed to global) const excludeDirs = (mode === 'Path' && dir === '.claude') ? GLOBAL_SUBDIRS : []; const { files, directories } = await copyDirectory(srcPath, destPath, newManifest, excludeDirs); totalFiles += files; totalDirs += directories; } // Update version.json const versionPath = join(installPath, '.claude', 'version.json'); if (existsSync(dirname(versionPath))) { const versionData = { version: version, installedAt: new Date().toISOString(), upgradedAt: new Date().toISOString(), mode: manifest.installation_mode, installer: 'ccw' }; writeFileSync(versionPath, JSON.stringify(versionData, null, 2), 'utf8'); addFileEntry(newManifest, versionPath); totalFiles++; } // Delete old manifest and save new one if (manifest.manifest_file) { deleteManifest(manifest.manifest_file); } saveManifest(newManifest); return { files: totalFiles, directories: totalDirs }; } /** * Copy directory recursively * @param {string} src - Source directory * @param {string} dest - Destination directory * @param {Object} manifest - Manifest to track files * @param {string[]} excludeDirs - Directory names to exclude (optional) * @returns {Object} - Count of files and directories */ async function copyDirectory( src: string, dest: string, manifest: any, excludeDirs: string[] = [] ): Promise { let files = 0; let directories = 0; // Create destination directory if (!existsSync(dest)) { mkdirSync(dest, { recursive: true }); directories++; addDirectoryEntry(manifest, dest); } const entries = readdirSync(src); for (const entry of entries) { // Skip excluded directories if (excludeDirs.includes(entry)) { continue; } 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++; addFileEntry(manifest, destPath); } } return { files, directories }; }