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:
catlog22
2025-12-07 20:03:10 +08:00
parent a6f9701679
commit f459061ad5
4 changed files with 97 additions and 694 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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
}
}
}