Files
Claude-Code-Workflow/ccw/src/commands/upgrade.js
catlog22 f459061ad5 refactor: 简化 ccw 安装流程,移除远程下载功能
- 删除 version-fetcher.js,移除 GitHub API 依赖
- install.js: 移除远程版本选择,只保留本地安装
- upgrade.js: 重写为本地升级,比对包版本与已安装版本
- cli.js: 移除 -v/-t/-b 等版本相关选项
- 添加 CLAUDE.md 复制到 .claude 目录的逻辑

版本管理统一到 npm:npm install -g ccw@版本号

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-07 20:03:10 +08:00

308 lines
9.0 KiB
JavaScript

import { existsSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
import { join, dirname, basename } from 'path';
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'];
// 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(), '..');
}
/**
* 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';
}
}
/**
* Upgrade command handler
* @param {Object} options - Command options
*/
export async function upgradeCommand(options) {
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 = [];
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 = [];
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 => upgradeTargets[i].manifest);
}
// Perform upgrades
console.log('');
const results = [];
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) {
upgradeSpinner.fail(`Failed to upgrade ${manifest.installation_mode}`);
error(err.message);
results.push({ manifest, success: false, error: err.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, sourceDir, version) {
const installPath = manifest.installation_path;
// 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(manifest.installation_mode, installPath);
let totalFiles = 0;
let totalDirs = 0;
// Copy each directory
for (const dir of availableDirs) {
const srcPath = join(sourceDir, dir);
const destPath = join(installPath, dir);
const { files, directories } = await copyDirectory(srcPath, destPath, newManifest);
totalFiles += files;
totalDirs += directories;
}
// Copy CLAUDE.md to .claude directory
const claudeMdSrc = join(sourceDir, 'CLAUDE.md');
const claudeMdDest = join(installPath, '.claude', 'CLAUDE.md');
if (existsSync(claudeMdSrc) && existsSync(dirname(claudeMdDest))) {
copyFileSync(claudeMdSrc, claudeMdDest);
addFileEntry(newManifest, claudeMdDest);
totalFiles++;
}
// 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
* @returns {Object} - Count of files and directories
*/
async function copyDirectory(src, dest, manifest) {
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) {
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 };
}