mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
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>
This commit is contained in:
@@ -74,9 +74,6 @@ export function run(argv) {
|
||||
.description('Install Claude Code Workflow to your system')
|
||||
.option('-m, --mode <mode>', 'Installation mode: Global or Path')
|
||||
.option('-p, --path <path>', 'Installation path (for Path mode)')
|
||||
.option('-v, --version <version>', 'Version type: local, stable, latest, or branch', 'local')
|
||||
.option('-t, --tag <tag>', 'Specific release tag (e.g., v3.2.0) for stable version')
|
||||
.option('-b, --branch <branch>', 'Branch name for branch version type', 'main')
|
||||
.option('-f, --force', 'Force installation without prompts')
|
||||
.action(installCommand);
|
||||
|
||||
@@ -89,12 +86,8 @@ export function run(argv) {
|
||||
// Upgrade command
|
||||
program
|
||||
.command('upgrade')
|
||||
.description('Upgrade Claude Code Workflow installations to latest version')
|
||||
.description('Upgrade Claude Code Workflow installations')
|
||||
.option('-a, --all', 'Upgrade all installations without prompting')
|
||||
.option('-l, --latest', 'Upgrade to latest development version (main branch)')
|
||||
.option('-t, --tag <tag>', 'Upgrade to specific release tag (e.g., v3.2.0)')
|
||||
.option('-b, --branch <branch>', 'Upgrade to specific branch')
|
||||
.option('-s, --select', 'Force interactive version selection')
|
||||
.action(upgradeCommand);
|
||||
|
||||
// List command
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync, rmSync } from 'fs';
|
||||
import { join, dirname, basename, relative } from 'path';
|
||||
import { homedir, tmpdir } from 'os';
|
||||
import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync } 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 { showHeader, showBanner, createSpinner, success, info, warning, error, summaryBox, step, divider } from '../utils/ui.js';
|
||||
import { showHeader, createSpinner, info, warning, error, summaryBox, 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, fetchRecentReleases, downloadAndExtract, cleanupTemp, REPO_URL } from '../utils/version-fetcher.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -59,28 +58,8 @@ 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();
|
||||
}
|
||||
// Local installation from package source
|
||||
const sourceDir = getSourceDir();
|
||||
|
||||
// Interactive mode selection
|
||||
const mode = options.mode || await selectMode();
|
||||
@@ -96,7 +75,6 @@ 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);
|
||||
}
|
||||
|
||||
@@ -110,7 +88,6 @@ export async function installCommand(options) {
|
||||
if (availableDirs.length === 0) {
|
||||
error('No source directories found to install.');
|
||||
error(`Expected directories in: ${sourceDir}`);
|
||||
if (tempDir) cleanupTemp(tempDir);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -156,13 +133,21 @@ export async function installCommand(options) {
|
||||
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))) {
|
||||
spinner.text = 'Installing CLAUDE.md...';
|
||||
copyFileSync(claudeMdSrc, claudeMdDest);
|
||||
addFileEntry(manifest, claudeMdDest);
|
||||
totalFiles++;
|
||||
}
|
||||
|
||||
// Create version.json
|
||||
const versionPath = join(installPath, '.claude', 'version.json');
|
||||
if (existsSync(dirname(versionPath))) {
|
||||
const versionData = {
|
||||
version: versionInfo.version,
|
||||
branch: versionInfo.branch,
|
||||
commit: versionInfo.commit,
|
||||
version: version,
|
||||
installedAt: new Date().toISOString(),
|
||||
mode: mode,
|
||||
installer: 'ccw'
|
||||
@@ -177,15 +162,9 @@ 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);
|
||||
|
||||
@@ -196,23 +175,13 @@ export async function installCommand(options) {
|
||||
'',
|
||||
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.white(`Version: ${chalk.cyan(version)}`),
|
||||
'',
|
||||
chalk.gray(`Files installed: ${totalFiles}`),
|
||||
chalk.gray(`Directories created: ${totalDirs}`),
|
||||
'',
|
||||
chalk.gray(`Manifest: ${basename(manifestPath)}`)
|
||||
);
|
||||
];
|
||||
|
||||
summaryBox({
|
||||
title: ' Installation Summary ',
|
||||
@@ -229,177 +198,6 @@ 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
|
||||
|
||||
@@ -1,15 +1,41 @@
|
||||
import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync, rmSync } from 'fs';
|
||||
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, success, info, warning, error, summaryBox, divider } from '../utils/ui.js';
|
||||
import { showBanner, createSpinner, 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';
|
||||
|
||||
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
|
||||
@@ -18,6 +44,8 @@ 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();
|
||||
|
||||
@@ -27,27 +55,7 @@ export async function upgradeCommand(options) {
|
||||
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
|
||||
// Display current installations
|
||||
console.log(chalk.white.bold(' Current installations:\n'));
|
||||
|
||||
const upgradeTargets = [];
|
||||
@@ -56,47 +64,29 @@ export async function upgradeCommand(options) {
|
||||
const m = manifests[i];
|
||||
const modeColor = m.installation_mode === 'Global' ? chalk.cyan : chalk.yellow;
|
||||
|
||||
// Read current version
|
||||
// Read installed version
|
||||
const versionFile = join(m.installation_path, '.claude', 'version.json');
|
||||
let currentVersion = 'unknown';
|
||||
let currentBranch = '';
|
||||
let installedVersion = 'unknown';
|
||||
|
||||
if (existsSync(versionFile)) {
|
||||
try {
|
||||
const versionData = JSON.parse(readFileSync(versionFile, 'utf8'));
|
||||
currentVersion = versionData.version || 'unknown';
|
||||
currentBranch = versionData.branch || '';
|
||||
installedVersion = versionData.version || 'unknown';
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
// 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(` Current: ${currentVersion}${currentBranch ? ` (${currentBranch})` : ''}`));
|
||||
console.log(chalk.gray(` Installed: ${installedVersion}`));
|
||||
|
||||
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`));
|
||||
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 ✓`));
|
||||
}
|
||||
@@ -105,48 +95,26 @@ export async function upgradeCommand(options) {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
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 = manifests;
|
||||
} else if (manifests.length === 1) {
|
||||
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 ${manifests[0].installation_mode} installation at ${manifests[0].installation_path}?`,
|
||||
message: `Upgrade ${target.manifest.installation_mode} installation (${target.installedVersion} → ${currentVersion})?`,
|
||||
default: true
|
||||
}]);
|
||||
|
||||
@@ -155,19 +123,13 @@ export async function upgradeCommand(options) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectedManifests = [manifests[0]];
|
||||
selectedManifests = [target.manifest];
|
||||
} 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 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',
|
||||
@@ -181,32 +143,19 @@ export async function upgradeCommand(options) {
|
||||
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;
|
||||
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, downloadResult);
|
||||
const result = await performUpgrade(manifest, sourceDir, currentVersion);
|
||||
upgradeSpinner.succeed(`Upgraded ${manifest.installation_mode}: ${result.files} files`);
|
||||
results.push({ manifest, success: true, ...result });
|
||||
} catch (err) {
|
||||
@@ -216,9 +165,6 @@ export async function upgradeCommand(options) {
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
cleanupTemp(downloadResult.tempDir);
|
||||
|
||||
// Show summary
|
||||
console.log('');
|
||||
|
||||
@@ -230,8 +176,7 @@ export async function upgradeCommand(options) {
|
||||
? 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)}`),
|
||||
chalk.white(`Version: ${chalk.cyan(currentVersion)}`),
|
||||
''
|
||||
];
|
||||
|
||||
@@ -256,109 +201,21 @@ export async function upgradeCommand(options) {
|
||||
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
|
||||
* @param {string} sourceDir - Source directory
|
||||
* @param {string} version - Version string
|
||||
* @returns {Promise<Object>} - Upgrade result
|
||||
*/
|
||||
async function performUpgrade(manifest, downloadResult) {
|
||||
async function performUpgrade(manifest, sourceDir, version) {
|
||||
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');
|
||||
throw new Error('No source directories found');
|
||||
}
|
||||
|
||||
// Create new manifest
|
||||
@@ -377,13 +234,20 @@ async function performUpgrade(manifest, downloadResult) {
|
||||
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: downloadResult.version,
|
||||
branch: downloadResult.branch,
|
||||
commit: downloadResult.commit,
|
||||
version: version,
|
||||
installedAt: new Date().toISOString(),
|
||||
upgradedAt: new Date().toISOString(),
|
||||
mode: manifest.installation_mode,
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
import https from 'https';
|
||||
import { existsSync, mkdirSync, createWriteStream, rmSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { createUnzip } from 'zlib';
|
||||
import { pipeline } from 'stream/promises';
|
||||
|
||||
// GitHub repository URL
|
||||
export const REPO_URL = 'https://github.com/catlog22/Claude-Code-Workflow';
|
||||
const API_BASE = 'https://api.github.com/repos/catlog22/Claude-Code-Workflow';
|
||||
|
||||
/**
|
||||
* Make HTTPS request with JSON response
|
||||
* @param {string} url - URL to fetch
|
||||
* @param {number} timeout - Timeout in ms (default: 10000)
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
function fetchJson(url, timeout = 10000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.get(url, {
|
||||
headers: {
|
||||
'User-Agent': 'ccw-installer',
|
||||
'Accept': 'application/vnd.github.v3+json'
|
||||
},
|
||||
timeout
|
||||
}, (res) => {
|
||||
// Handle redirects
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
return fetchJson(res.headers.location, timeout).then(resolve).catch(reject);
|
||||
}
|
||||
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
|
||||
return;
|
||||
}
|
||||
|
||||
let data = '';
|
||||
res.on('data', chunk => { data += chunk; });
|
||||
res.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch (err) {
|
||||
reject(new Error('Invalid JSON response'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('Request timeout'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch latest stable release info
|
||||
* @returns {Promise<{tag: string, date: string, url: string}>}
|
||||
*/
|
||||
export async function fetchLatestRelease() {
|
||||
const data = await fetchJson(`${API_BASE}/releases/latest`);
|
||||
|
||||
return {
|
||||
tag: data.tag_name,
|
||||
version: data.tag_name.replace(/^v/, ''),
|
||||
date: data.published_at ? new Date(data.published_at).toLocaleDateString() : '',
|
||||
url: data.zipball_url,
|
||||
htmlUrl: data.html_url
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch recent releases list
|
||||
* @param {number} limit - Number of releases to fetch
|
||||
* @returns {Promise<Array<{tag: string, date: string}>>}
|
||||
*/
|
||||
export async function fetchRecentReleases(limit = 5) {
|
||||
const data = await fetchJson(`${API_BASE}/releases?per_page=${limit}`);
|
||||
|
||||
return data.map(release => ({
|
||||
tag: release.tag_name,
|
||||
version: release.tag_name.replace(/^v/, ''),
|
||||
date: release.published_at ? new Date(release.published_at).toLocaleDateString() : '',
|
||||
url: release.zipball_url
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch latest commit from a branch
|
||||
* @param {string} branch - Branch name (default: main)
|
||||
* @returns {Promise<{sha: string, shortSha: string, date: string, message: string}>}
|
||||
*/
|
||||
export async function fetchLatestCommit(branch = 'main') {
|
||||
const data = await fetchJson(`${API_BASE}/commits/${branch}`);
|
||||
|
||||
return {
|
||||
sha: data.sha,
|
||||
shortSha: data.sha.substring(0, 7),
|
||||
date: data.commit.committer.date ? new Date(data.commit.committer.date).toLocaleDateString() : '',
|
||||
message: data.commit.message.split('\n')[0]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Download file from URL
|
||||
* @param {string} url - URL to download
|
||||
* @param {string} destPath - Destination file path
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function downloadFile(url, destPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = createWriteStream(destPath);
|
||||
|
||||
https.get(url, {
|
||||
headers: {
|
||||
'User-Agent': 'ccw-installer',
|
||||
'Accept': 'application/octet-stream'
|
||||
}
|
||||
}, (res) => {
|
||||
// Handle redirects
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
file.close();
|
||||
return downloadFile(res.headers.location, destPath).then(resolve).catch(reject);
|
||||
}
|
||||
|
||||
if (res.statusCode !== 200) {
|
||||
file.close();
|
||||
reject(new Error(`Download failed: HTTP ${res.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
res.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
resolve();
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
file.close();
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract zip file using native unzip command or built-in
|
||||
* @param {string} zipPath - Path to zip file
|
||||
* @param {string} destDir - Destination directory
|
||||
* @returns {Promise<string>} - Extracted directory path
|
||||
*/
|
||||
async function extractZip(zipPath, destDir) {
|
||||
const { execSync } = await import('child_process');
|
||||
|
||||
// Try using native unzip commands
|
||||
try {
|
||||
// Try PowerShell Expand-Archive (Windows)
|
||||
execSync(`powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`, {
|
||||
stdio: 'pipe'
|
||||
});
|
||||
} catch {
|
||||
try {
|
||||
// Try unzip command (Unix/Git Bash)
|
||||
execSync(`unzip -o "${zipPath}" -d "${destDir}"`, { stdio: 'pipe' });
|
||||
} catch {
|
||||
throw new Error('No unzip utility available. Please install unzip or use PowerShell.');
|
||||
}
|
||||
}
|
||||
|
||||
// Find extracted directory
|
||||
const { readdirSync } = await import('fs');
|
||||
const entries = readdirSync(destDir);
|
||||
const repoDir = entries.find(e => e.startsWith('Claude-Code-Workflow-') || e.startsWith('catlog22-Claude-Code-Workflow-'));
|
||||
|
||||
if (!repoDir) {
|
||||
throw new Error('Could not find extracted repository directory');
|
||||
}
|
||||
|
||||
return join(destDir, repoDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and extract repository
|
||||
* @param {Object} options
|
||||
* @param {'stable'|'latest'|'branch'} options.type - Version type
|
||||
* @param {string} options.tag - Specific tag (for stable)
|
||||
* @param {string} options.branch - Branch name (for branch type)
|
||||
* @returns {Promise<{repoDir: string, version: string, branch: string, commit: string}>}
|
||||
*/
|
||||
export async function downloadAndExtract(options = {}) {
|
||||
const { type = 'stable', tag = '', branch = 'main' } = options;
|
||||
|
||||
// Create temp directory
|
||||
const tempDir = join(tmpdir(), `ccw-install-${Date.now()}`);
|
||||
if (!existsSync(tempDir)) {
|
||||
mkdirSync(tempDir, { recursive: true });
|
||||
}
|
||||
|
||||
let zipUrl;
|
||||
let versionInfo = { version: '', branch: '', commit: '' };
|
||||
|
||||
// Determine download URL based on version type
|
||||
if (type === 'stable') {
|
||||
if (tag) {
|
||||
zipUrl = `${REPO_URL}/archive/refs/tags/${tag}.zip`;
|
||||
versionInfo.version = tag.replace(/^v/, '');
|
||||
versionInfo.branch = tag;
|
||||
} else {
|
||||
const release = await fetchLatestRelease();
|
||||
zipUrl = `${REPO_URL}/archive/refs/tags/${release.tag}.zip`;
|
||||
versionInfo.version = release.version;
|
||||
versionInfo.branch = release.tag;
|
||||
}
|
||||
} else if (type === 'latest') {
|
||||
zipUrl = `${REPO_URL}/archive/refs/heads/main.zip`;
|
||||
const commit = await fetchLatestCommit('main');
|
||||
versionInfo.version = `dev-${commit.shortSha}`;
|
||||
versionInfo.branch = 'main';
|
||||
versionInfo.commit = commit.shortSha;
|
||||
} else {
|
||||
zipUrl = `${REPO_URL}/archive/refs/heads/${branch}.zip`;
|
||||
const commit = await fetchLatestCommit(branch);
|
||||
versionInfo.version = `dev-${commit.shortSha}`;
|
||||
versionInfo.branch = branch;
|
||||
versionInfo.commit = commit.shortSha;
|
||||
}
|
||||
|
||||
// Download zip file
|
||||
const zipPath = join(tempDir, 'repo.zip');
|
||||
await downloadFile(zipUrl, zipPath);
|
||||
|
||||
// Extract zip
|
||||
const repoDir = await extractZip(zipPath, tempDir);
|
||||
|
||||
return {
|
||||
repoDir,
|
||||
tempDir,
|
||||
...versionInfo
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup temporary directory
|
||||
* @param {string} tempDir - Temp directory to remove
|
||||
*/
|
||||
export function cleanupTemp(tempDir) {
|
||||
if (existsSync(tempDir)) {
|
||||
try {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user