mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-13 02:41:50 +08:00
feat(ccw): migrate backend to TypeScript
- Convert 40 JS files to TypeScript (CLI, tools, core, MCP server) - Add Zod for runtime parameter validation - Add type definitions in src/types/ - Keep src/templates/ as JavaScript (dashboard frontend) - Update bin entries to use dist/ - Add tsconfig.json with strict mode - Add backward-compatible exports for tests - All 39 tests passing Breaking changes: None (backward compatible) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
352
ccw/src/commands/upgrade.ts
Normal file
352
ccw/src/commands/upgrade.ts
Normal file
@@ -0,0 +1,352 @@
|
||||
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<void> {
|
||||
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<Object>} - Upgrade result
|
||||
*/
|
||||
async function performUpgrade(manifest: any, sourceDir: string, version: string): Promise<UpgradeResult> {
|
||||
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<CopyResult> {
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user