diff --git a/ccw/src/cli.js b/ccw/src/cli.js index 679106e7..77050833 100644 --- a/ccw/src/cli.js +++ b/ccw/src/cli.js @@ -74,9 +74,6 @@ export function run(argv) { .description('Install Claude Code Workflow to your system') .option('-m, --mode ', 'Installation mode: Global or Path') .option('-p, --path ', 'Installation path (for Path mode)') - .option('-v, --version ', 'Version type: local, stable, latest, or branch', 'local') - .option('-t, --tag ', 'Specific release tag (e.g., v3.2.0) for stable version') - .option('-b, --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 ', 'Upgrade to specific release tag (e.g., v3.2.0)') - .option('-b, --branch ', 'Upgrade to specific branch') - .option('-s, --select', 'Force interactive version selection') .action(upgradeCommand); // List command diff --git a/ccw/src/commands/install.js b/ccw/src/commands/install.js index ba505d6a..bed8a58b 100644 --- a/ccw/src/commands/install.js +++ b/ccw/src/commands/install.js @@ -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} - 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} - 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} - Selected mode diff --git a/ccw/src/commands/upgrade.js b/ccw/src/commands/upgrade.js index ee710655..25102840 100644 --- a/ccw/src/commands/upgrade.js +++ b/ccw/src/commands/upgrade.js @@ -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} - 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} - 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, diff --git a/ccw/src/utils/version-fetcher.js b/ccw/src/utils/version-fetcher.js deleted file mode 100644 index a0e46be9..00000000 --- a/ccw/src/utils/version-fetcher.js +++ /dev/null @@ -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} - */ -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>} - */ -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} - */ -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} - 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 - } - } -}