mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat: Add Notifications Component with WebSocket and Auto Refresh
- Implemented a Notifications component for real-time updates using WebSocket. - Added silent refresh functionality to update data without notification bubbles. - Introduced auto-refresh mechanism to periodically check for changes in workflow data. - Enhanced data handling with session and task updates, ensuring UI reflects the latest state. feat: Create Hook Manager View for Managing Hooks - Developed a Hook Manager view to manage project and global hooks. - Added functionality to create, edit, and delete hooks with a user-friendly interface. - Implemented quick install templates for common hooks to streamline user experience. - Included environment variables reference for hooks to assist users in configuration. feat: Implement MCP Manager View for Server Management - Created an MCP Manager view for managing MCP servers within projects. - Enabled adding and removing servers from projects with a clear UI. - Displayed available servers from other projects for easy access and management. - Provided an overview of all projects and their associated MCP servers. feat: Add Version Fetcher Utility for GitHub Releases - Implemented a version fetcher utility to retrieve release information from GitHub. - Added functions to fetch the latest release, recent releases, and latest commit details. - Included functionality to download and extract repository zip files. - Ensured cleanup of temporary directories after downloads to maintain system hygiene.
This commit is contained in:
@@ -7,7 +7,7 @@ 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';
|
||||
import { fetchLatestRelease, fetchLatestCommit, downloadAndExtract, REPO_URL } from '../utils/version-fetcher.js';
|
||||
import { fetchLatestRelease, fetchLatestCommit, fetchRecentReleases, downloadAndExtract, cleanupTemp, REPO_URL } from '../utils/version-fetcher.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -59,6 +59,29 @@ export async function installCommand(options) {
|
||||
}
|
||||
}
|
||||
|
||||
// Determine source directory based on version option
|
||||
let sourceDir;
|
||||
let tempDir = null;
|
||||
let versionInfo = { version: getVersion(), branch: 'local', commit: '' };
|
||||
|
||||
if (options.version && options.version !== 'local') {
|
||||
// Remote installation - download from GitHub
|
||||
const downloadResult = await selectAndDownloadVersion(options);
|
||||
if (!downloadResult) {
|
||||
return; // User cancelled or error occurred
|
||||
}
|
||||
sourceDir = downloadResult.repoDir;
|
||||
tempDir = downloadResult.tempDir;
|
||||
versionInfo = {
|
||||
version: downloadResult.version,
|
||||
branch: downloadResult.branch,
|
||||
commit: downloadResult.commit
|
||||
};
|
||||
} else {
|
||||
// Local installation from package source
|
||||
sourceDir = getSourceDir();
|
||||
}
|
||||
|
||||
// Interactive mode selection
|
||||
const mode = options.mode || await selectMode();
|
||||
|
||||
@@ -73,6 +96,7 @@ export async function installCommand(options) {
|
||||
const pathValidation = validatePath(inputPath, { mustExist: true });
|
||||
if (!pathValidation.valid) {
|
||||
error(`Invalid installation path: ${pathValidation.error}`);
|
||||
if (tempDir) cleanupTemp(tempDir);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -81,12 +105,12 @@ export async function installCommand(options) {
|
||||
}
|
||||
|
||||
// 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}`);
|
||||
if (tempDir) cleanupTemp(tempDir);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -135,13 +159,15 @@ export async function installCommand(options) {
|
||||
// Create version.json
|
||||
const versionPath = join(installPath, '.claude', 'version.json');
|
||||
if (existsSync(dirname(versionPath))) {
|
||||
const versionInfo = {
|
||||
version: version,
|
||||
const versionData = {
|
||||
version: versionInfo.version,
|
||||
branch: versionInfo.branch,
|
||||
commit: versionInfo.commit,
|
||||
installedAt: new Date().toISOString(),
|
||||
mode: mode,
|
||||
installer: 'ccw'
|
||||
};
|
||||
writeFileSync(versionPath, JSON.stringify(versionInfo, null, 2), 'utf8');
|
||||
writeFileSync(versionPath, JSON.stringify(versionData, null, 2), 'utf8');
|
||||
addFileEntry(manifest, versionPath);
|
||||
totalFiles++;
|
||||
}
|
||||
@@ -151,27 +177,46 @@ export async function installCommand(options) {
|
||||
} catch (err) {
|
||||
spinner.fail('Installation failed');
|
||||
error(err.message);
|
||||
if (tempDir) cleanupTemp(tempDir);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Cleanup temp directory if used
|
||||
if (tempDir) {
|
||||
cleanupTemp(tempDir);
|
||||
}
|
||||
|
||||
// Save manifest
|
||||
const manifestPath = saveManifest(manifest);
|
||||
|
||||
// Show summary
|
||||
console.log('');
|
||||
const summaryLines = [
|
||||
chalk.green.bold('✓ Installation Successful'),
|
||||
'',
|
||||
chalk.white(`Mode: ${chalk.cyan(mode)}`),
|
||||
chalk.white(`Path: ${chalk.cyan(installPath)}`),
|
||||
chalk.white(`Version: ${chalk.cyan(versionInfo.version)}`),
|
||||
];
|
||||
|
||||
if (versionInfo.branch && versionInfo.branch !== 'local') {
|
||||
summaryLines.push(chalk.white(`Branch: ${chalk.cyan(versionInfo.branch)}`));
|
||||
}
|
||||
if (versionInfo.commit) {
|
||||
summaryLines.push(chalk.white(`Commit: ${chalk.cyan(versionInfo.commit)}`));
|
||||
}
|
||||
|
||||
summaryLines.push(
|
||||
'',
|
||||
chalk.gray(`Files installed: ${totalFiles}`),
|
||||
chalk.gray(`Directories created: ${totalDirs}`),
|
||||
'',
|
||||
chalk.gray(`Manifest: ${basename(manifestPath)}`)
|
||||
);
|
||||
|
||||
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)}`),
|
||||
],
|
||||
lines: summaryLines,
|
||||
borderColor: 'green'
|
||||
});
|
||||
|
||||
@@ -184,6 +229,177 @@ export async function installCommand(options) {
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Select version and download from GitHub
|
||||
* @param {Object} options - Command options
|
||||
* @returns {Promise<Object|null>} - Download result or null if cancelled
|
||||
*/
|
||||
async function selectAndDownloadVersion(options) {
|
||||
console.log('');
|
||||
divider();
|
||||
info('Version Selection');
|
||||
divider();
|
||||
console.log('');
|
||||
|
||||
// Fetch version information
|
||||
const spinner = createSpinner('Fetching version information...').start();
|
||||
|
||||
let latestRelease = null;
|
||||
let latestCommit = null;
|
||||
let recentReleases = [];
|
||||
|
||||
try {
|
||||
[latestRelease, latestCommit, recentReleases] = await Promise.all([
|
||||
fetchLatestRelease().catch(() => null),
|
||||
fetchLatestCommit('main').catch(() => null),
|
||||
fetchRecentReleases(5).catch(() => [])
|
||||
]);
|
||||
spinner.succeed('Version information loaded');
|
||||
} catch (err) {
|
||||
spinner.warn('Could not fetch all version info');
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// Build version choices
|
||||
const choices = [];
|
||||
|
||||
// Option 1: Latest Stable
|
||||
if (latestRelease) {
|
||||
choices.push({
|
||||
name: `${chalk.green('1)')} ${chalk.green.bold('Latest Stable')} ${chalk.cyan(latestRelease.tag)} ${chalk.gray(`(${latestRelease.date})`)} ${chalk.green('← Recommended')}`,
|
||||
value: { type: 'stable', tag: '' }
|
||||
});
|
||||
} else {
|
||||
choices.push({
|
||||
name: `${chalk.green('1)')} ${chalk.green.bold('Latest Stable')} ${chalk.gray('(auto-detect)')} ${chalk.green('← Recommended')}`,
|
||||
value: { type: 'stable', tag: '' }
|
||||
});
|
||||
}
|
||||
|
||||
// Option 2: Latest Development
|
||||
if (latestCommit) {
|
||||
choices.push({
|
||||
name: `${chalk.yellow('2)')} ${chalk.yellow.bold('Latest Development')} ${chalk.gray(`main @ ${latestCommit.shortSha}`)} ${chalk.gray(`(${latestCommit.date})`)}`,
|
||||
value: { type: 'latest', branch: 'main' }
|
||||
});
|
||||
} else {
|
||||
choices.push({
|
||||
name: `${chalk.yellow('2)')} ${chalk.yellow.bold('Latest Development')} ${chalk.gray('(main branch)')}`,
|
||||
value: { type: 'latest', branch: 'main' }
|
||||
});
|
||||
}
|
||||
|
||||
// Option 3: Specific Version
|
||||
choices.push({
|
||||
name: `${chalk.cyan('3)')} ${chalk.cyan.bold('Specific Version')} ${chalk.gray('- Choose from available releases')}`,
|
||||
value: { type: 'specific' }
|
||||
});
|
||||
|
||||
// Option 4: Custom Branch
|
||||
choices.push({
|
||||
name: `${chalk.magenta('4)')} ${chalk.magenta.bold('Custom Branch')} ${chalk.gray('- Install from a specific branch')}`,
|
||||
value: { type: 'branch' }
|
||||
});
|
||||
|
||||
// Check if version was specified via CLI
|
||||
if (options.version === 'stable') {
|
||||
return await downloadVersion({ type: 'stable', tag: options.tag || '' });
|
||||
} else if (options.version === 'latest') {
|
||||
return await downloadVersion({ type: 'latest', branch: 'main' });
|
||||
} else if (options.version === 'branch' && options.branch) {
|
||||
return await downloadVersion({ type: 'branch', branch: options.branch });
|
||||
}
|
||||
|
||||
// Interactive selection
|
||||
const { versionChoice } = await inquirer.prompt([{
|
||||
type: 'list',
|
||||
name: 'versionChoice',
|
||||
message: 'Select version to install:',
|
||||
choices
|
||||
}]);
|
||||
|
||||
// Handle specific version selection
|
||||
if (versionChoice.type === 'specific') {
|
||||
const tagChoices = recentReleases.length > 0
|
||||
? recentReleases.map(r => ({
|
||||
name: `${r.tag} ${chalk.gray(`(${r.date})`)}`,
|
||||
value: r.tag
|
||||
}))
|
||||
: [
|
||||
{ name: 'v3.2.0', value: 'v3.2.0' },
|
||||
{ name: 'v3.1.0', value: 'v3.1.0' },
|
||||
{ name: 'v3.0.1', value: 'v3.0.1' }
|
||||
];
|
||||
|
||||
tagChoices.push({
|
||||
name: chalk.gray('Enter custom tag...'),
|
||||
value: 'custom'
|
||||
});
|
||||
|
||||
const { selectedTag } = await inquirer.prompt([{
|
||||
type: 'list',
|
||||
name: 'selectedTag',
|
||||
message: 'Select release version:',
|
||||
choices: tagChoices
|
||||
}]);
|
||||
|
||||
let tag = selectedTag;
|
||||
if (selectedTag === 'custom') {
|
||||
const { customTag } = await inquirer.prompt([{
|
||||
type: 'input',
|
||||
name: 'customTag',
|
||||
message: 'Enter version tag (e.g., v3.2.0):',
|
||||
validate: (input) => input ? true : 'Tag is required'
|
||||
}]);
|
||||
tag = customTag;
|
||||
}
|
||||
|
||||
return await downloadVersion({ type: 'stable', tag });
|
||||
}
|
||||
|
||||
// Handle custom branch selection
|
||||
if (versionChoice.type === 'branch') {
|
||||
const { branchName } = await inquirer.prompt([{
|
||||
type: 'input',
|
||||
name: 'branchName',
|
||||
message: 'Enter branch name:',
|
||||
default: 'main',
|
||||
validate: (input) => input ? true : 'Branch name is required'
|
||||
}]);
|
||||
|
||||
return await downloadVersion({ type: 'branch', branch: branchName });
|
||||
}
|
||||
|
||||
return await downloadVersion(versionChoice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download specified version
|
||||
* @param {Object} versionChoice - Version selection
|
||||
* @returns {Promise<Object|null>} - Download result
|
||||
*/
|
||||
async function downloadVersion(versionChoice) {
|
||||
console.log('');
|
||||
const spinner = createSpinner('Downloading from GitHub...').start();
|
||||
|
||||
try {
|
||||
const result = await downloadAndExtract(versionChoice);
|
||||
spinner.succeed(`Downloaded: ${result.version} (${result.branch})`);
|
||||
return result;
|
||||
} catch (err) {
|
||||
spinner.fail('Download failed');
|
||||
error(err.message);
|
||||
console.log('');
|
||||
warning('Common causes:');
|
||||
console.log(chalk.gray(' • Network connection issues'));
|
||||
console.log(chalk.gray(' • Invalid version tag or branch name'));
|
||||
console.log(chalk.gray(' • GitHub API rate limit exceeded'));
|
||||
console.log('');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactive mode selection
|
||||
* @returns {Promise<string>} - Selected mode
|
||||
|
||||
443
ccw/src/commands/upgrade.js
Normal file
443
ccw/src/commands/upgrade.js
Normal file
@@ -0,0 +1,443 @@
|
||||
import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync, rmSync } from 'fs';
|
||||
import { join, dirname, basename } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import inquirer from 'inquirer';
|
||||
import chalk from 'chalk';
|
||||
import { showBanner, createSpinner, success, info, warning, error, summaryBox, divider } from '../utils/ui.js';
|
||||
import { getAllManifests, createManifest, addFileEntry, addDirectoryEntry, saveManifest, deleteManifest } from '../core/manifest.js';
|
||||
import { fetchLatestRelease, fetchLatestCommit, fetchRecentReleases, downloadAndExtract, cleanupTemp, REPO_URL } from '../utils/version-fetcher.js';
|
||||
|
||||
// Source directories to install
|
||||
const SOURCE_DIRS = ['.claude', '.codex', '.gemini', '.qwen'];
|
||||
|
||||
/**
|
||||
* Upgrade command handler
|
||||
* @param {Object} options - Command options
|
||||
*/
|
||||
export async function upgradeCommand(options) {
|
||||
showBanner();
|
||||
console.log(chalk.cyan.bold(' Upgrade Claude Code Workflow\n'));
|
||||
|
||||
// Get all manifests
|
||||
const manifests = getAllManifests();
|
||||
|
||||
if (manifests.length === 0) {
|
||||
warning('No installations found.');
|
||||
info('Run "ccw install" to install first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch latest version info
|
||||
const spinner = createSpinner('Checking for updates...').start();
|
||||
|
||||
let latestRelease = null;
|
||||
let latestCommit = null;
|
||||
|
||||
try {
|
||||
[latestRelease, latestCommit] = await Promise.all([
|
||||
fetchLatestRelease().catch(() => null),
|
||||
fetchLatestCommit('main').catch(() => null)
|
||||
]);
|
||||
spinner.succeed('Version information loaded');
|
||||
} catch (err) {
|
||||
spinner.fail('Could not fetch version info');
|
||||
error(err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// Display current installations with version comparison
|
||||
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 current version
|
||||
const versionFile = join(m.installation_path, '.claude', 'version.json');
|
||||
let currentVersion = 'unknown';
|
||||
let currentBranch = '';
|
||||
|
||||
if (existsSync(versionFile)) {
|
||||
try {
|
||||
const versionData = JSON.parse(readFileSync(versionFile, 'utf8'));
|
||||
currentVersion = versionData.version || 'unknown';
|
||||
currentBranch = versionData.branch || '';
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if upgrade is available
|
||||
let upgradeAvailable = false;
|
||||
let targetVersion = '';
|
||||
|
||||
if (latestRelease) {
|
||||
const latestVer = latestRelease.version;
|
||||
if (currentVersion !== latestVer && !currentVersion.startsWith('dev-')) {
|
||||
upgradeAvailable = true;
|
||||
targetVersion = latestVer;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.white(` ${i + 1}. `) + modeColor.bold(m.installation_mode));
|
||||
console.log(chalk.gray(` Path: ${m.installation_path}`));
|
||||
console.log(chalk.gray(` Current: ${currentVersion}${currentBranch ? ` (${currentBranch})` : ''}`));
|
||||
|
||||
if (upgradeAvailable && latestRelease) {
|
||||
console.log(chalk.green(` Available: ${latestRelease.tag} `) + chalk.green('← Update available'));
|
||||
upgradeTargets.push({
|
||||
manifest: m,
|
||||
currentVersion,
|
||||
targetVersion: latestRelease.tag,
|
||||
index: i
|
||||
});
|
||||
} else if (currentVersion.startsWith('dev-')) {
|
||||
console.log(chalk.yellow(` Development version - use --latest to update`));
|
||||
} else {
|
||||
console.log(chalk.gray(` Up to date ✓`));
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
divider();
|
||||
|
||||
// Version selection
|
||||
let versionChoice = { type: 'stable', tag: '' };
|
||||
|
||||
if (options.latest) {
|
||||
versionChoice = { type: 'latest', branch: 'main' };
|
||||
info('Upgrading to latest development version (main branch)');
|
||||
} else if (options.tag) {
|
||||
versionChoice = { type: 'stable', tag: options.tag };
|
||||
info(`Upgrading to specific version: ${options.tag}`);
|
||||
} else if (options.branch) {
|
||||
versionChoice = { type: 'branch', branch: options.branch };
|
||||
info(`Upgrading to branch: ${options.branch}`);
|
||||
} else {
|
||||
// Interactive version selection if no targets or --select specified
|
||||
if (upgradeTargets.length === 0 || options.select) {
|
||||
const { selectVersion } = await inquirer.prompt([{
|
||||
type: 'confirm',
|
||||
name: 'selectVersion',
|
||||
message: 'Select a specific version to install?',
|
||||
default: false
|
||||
}]);
|
||||
|
||||
if (selectVersion) {
|
||||
versionChoice = await selectVersionInteractive(latestRelease, latestCommit);
|
||||
if (!versionChoice) return;
|
||||
} else if (upgradeTargets.length === 0) {
|
||||
info('All installations are up to date.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Select which installations to upgrade
|
||||
let selectedManifests = [];
|
||||
|
||||
if (options.all) {
|
||||
selectedManifests = manifests;
|
||||
} else if (manifests.length === 1) {
|
||||
const { confirm } = await inquirer.prompt([{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: `Upgrade ${manifests[0].installation_mode} installation at ${manifests[0].installation_path}?`,
|
||||
default: true
|
||||
}]);
|
||||
|
||||
if (!confirm) {
|
||||
info('Upgrade cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
selectedManifests = [manifests[0]];
|
||||
} else {
|
||||
const choices = manifests.map((m, i) => {
|
||||
const target = upgradeTargets.find(t => t.index === i);
|
||||
const label = target
|
||||
? `${m.installation_mode} - ${m.installation_path} ${chalk.green('(update available)')}`
|
||||
: `${m.installation_mode} - ${m.installation_path}`;
|
||||
return {
|
||||
name: label,
|
||||
value: i,
|
||||
checked: !!target
|
||||
};
|
||||
});
|
||||
|
||||
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 => manifests[i]);
|
||||
}
|
||||
|
||||
// Download new version
|
||||
console.log('');
|
||||
const downloadSpinner = createSpinner('Downloading update...').start();
|
||||
|
||||
let downloadResult;
|
||||
try {
|
||||
downloadResult = await downloadAndExtract(versionChoice);
|
||||
downloadSpinner.succeed(`Downloaded: ${downloadResult.version} (${downloadResult.branch})`);
|
||||
} catch (err) {
|
||||
downloadSpinner.fail('Download failed');
|
||||
error(err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform upgrades
|
||||
console.log('');
|
||||
const results = [];
|
||||
|
||||
for (const manifest of selectedManifests) {
|
||||
const upgradeSpinner = createSpinner(`Upgrading ${manifest.installation_mode} at ${manifest.installation_path}...`).start();
|
||||
|
||||
try {
|
||||
const result = await performUpgrade(manifest, downloadResult);
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
cleanupTemp(downloadResult.tempDir);
|
||||
|
||||
// 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(downloadResult.version)}`),
|
||||
chalk.white(`Branch: ${chalk.cyan(downloadResult.branch)}`),
|
||||
''
|
||||
];
|
||||
|
||||
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('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactive version selection
|
||||
* @param {Object} latestRelease - Latest release info
|
||||
* @param {Object} latestCommit - Latest commit info
|
||||
* @returns {Promise<Object|null>} - Version choice
|
||||
*/
|
||||
async function selectVersionInteractive(latestRelease, latestCommit) {
|
||||
const choices = [];
|
||||
|
||||
// Option 1: Latest Stable
|
||||
if (latestRelease) {
|
||||
choices.push({
|
||||
name: `${chalk.green.bold('Latest Stable')} ${chalk.cyan(latestRelease.tag)} ${chalk.gray(`(${latestRelease.date})`)}`,
|
||||
value: { type: 'stable', tag: '' }
|
||||
});
|
||||
}
|
||||
|
||||
// Option 2: Latest Development
|
||||
if (latestCommit) {
|
||||
choices.push({
|
||||
name: `${chalk.yellow.bold('Latest Development')} ${chalk.gray(`main @ ${latestCommit.shortSha}`)}`,
|
||||
value: { type: 'latest', branch: 'main' }
|
||||
});
|
||||
}
|
||||
|
||||
// Option 3: Specific Version
|
||||
choices.push({
|
||||
name: `${chalk.cyan.bold('Specific Version')} ${chalk.gray('- Enter a release tag')}`,
|
||||
value: { type: 'specific' }
|
||||
});
|
||||
|
||||
// Option 4: Cancel
|
||||
choices.push({
|
||||
name: chalk.gray('Cancel'),
|
||||
value: null
|
||||
});
|
||||
|
||||
const { versionChoice } = await inquirer.prompt([{
|
||||
type: 'list',
|
||||
name: 'versionChoice',
|
||||
message: 'Select version to upgrade to:',
|
||||
choices
|
||||
}]);
|
||||
|
||||
if (!versionChoice) {
|
||||
info('Upgrade cancelled');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (versionChoice.type === 'specific') {
|
||||
const recentReleases = await fetchRecentReleases(5).catch(() => []);
|
||||
|
||||
const tagChoices = recentReleases.length > 0
|
||||
? recentReleases.map(r => ({
|
||||
name: `${r.tag} ${chalk.gray(`(${r.date})`)}`,
|
||||
value: r.tag
|
||||
}))
|
||||
: [];
|
||||
|
||||
tagChoices.push({
|
||||
name: chalk.gray('Enter custom tag...'),
|
||||
value: 'custom'
|
||||
});
|
||||
|
||||
const { selectedTag } = await inquirer.prompt([{
|
||||
type: 'list',
|
||||
name: 'selectedTag',
|
||||
message: 'Select release version:',
|
||||
choices: tagChoices
|
||||
}]);
|
||||
|
||||
let tag = selectedTag;
|
||||
if (selectedTag === 'custom') {
|
||||
const { customTag } = await inquirer.prompt([{
|
||||
type: 'input',
|
||||
name: 'customTag',
|
||||
message: 'Enter version tag (e.g., v3.2.0):',
|
||||
validate: (input) => input ? true : 'Tag is required'
|
||||
}]);
|
||||
tag = customTag;
|
||||
}
|
||||
|
||||
return { type: 'stable', tag };
|
||||
}
|
||||
|
||||
return versionChoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform upgrade for a single installation
|
||||
* @param {Object} manifest - Installation manifest
|
||||
* @param {Object} downloadResult - Download result with repoDir
|
||||
* @returns {Promise<Object>} - Upgrade result
|
||||
*/
|
||||
async function performUpgrade(manifest, downloadResult) {
|
||||
const installPath = manifest.installation_path;
|
||||
const sourceDir = downloadResult.repoDir;
|
||||
|
||||
// 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 in download');
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Update version.json
|
||||
const versionPath = join(installPath, '.claude', 'version.json');
|
||||
if (existsSync(dirname(versionPath))) {
|
||||
const versionData = {
|
||||
version: downloadResult.version,
|
||||
branch: downloadResult.branch,
|
||||
commit: downloadResult.commit,
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user