diff --git a/README.md b/README.md index e3e519f1a..8c71f7154 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ npm i -g claude-code-best ccb # 以 nodejs 打开 claude code ccb-bun # 以 bun 形态打开 +ccb update # 更新到最新版本 CLAUDE_BRIDGE_BASE_URL=https://remote-control.claude-code-best.win/ CLAUDE_BRIDGE_OAUTH_TOKEN=test-my-key ccb --remote-control # 我们有自部署的远程控制 ``` diff --git a/src/cli/updateCCB.ts b/src/cli/updateCCB.ts new file mode 100644 index 000000000..83731c59f --- /dev/null +++ b/src/cli/updateCCB.ts @@ -0,0 +1,166 @@ +/** + * `ccb update` — Check and install the latest version of claude-code-best. + * + * Detection strategy: + * 1. If `bun` is available and the current installation was done via bun → use `bun update -g` + * 2. Otherwise → use `npm install -g` + */ +import chalk from 'chalk' +import { execSync } from 'node:child_process' +import { existsSync, readFileSync } from 'node:fs' +import { homedir } from 'node:os' +import { join, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { logForDebugging } from '../utils/debug.js' +import { execFileNoThrowWithCwd } from '../utils/execFileNoThrow.js' +import { gracefulShutdown } from '../utils/gracefulShutdown.js' +import { writeToStdout } from '../utils/process.js' + +const PACKAGE_NAME = 'claude-code-best' + +function getCurrentVersion(): string { + // Read version from the nearest package.json (walks up from this file) + try { + const __dirname = dirname(fileURLToPath(import.meta.url)) + // In dev: src/cli/updateCCB.ts → ../../package.json + // In build: dist/chunks/xxx.js → ../../package.json (may not exist) + const pkgPath = join(__dirname, '..', '..', 'package.json') + if (existsSync(pkgPath)) { + const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) + if (pkg.version) return pkg.version + } + } catch { + // fallback + } + return MACRO.VERSION +} + +function isCommandAvailable(cmd: string): boolean { + try { + execSync(`which ${cmd} 2>/dev/null`, { stdio: 'pipe' }) + return true + } catch { + return false + } +} + +/** + * Detect whether the current installation was done via bun. + * Checks if the binary path contains "bun" or if bun's global install dir has our package. + */ +function isBunInstallation(): boolean { + // Check if the running binary is under bun's global install path + const execPath = process.execPath + if (execPath.includes('bun')) { + return true + } + + // Check bun's global install directory + const bunGlobalDir = join(homedir(), '.bun', 'install', 'global') + if (existsSync(join(bunGlobalDir, 'node_modules', PACKAGE_NAME))) { + return true + } + + return false +} + +/** + * Get the latest version from npm registry. + */ +async function getLatestVersion(): Promise { + const result = await execFileNoThrowWithCwd( + 'npm', + ['view', `${PACKAGE_NAME}@latest`, 'version', '--prefer-online'], + { abortSignal: AbortSignal.timeout(10_000), cwd: homedir() }, + ) + if (result.code !== 0) { + logForDebugging(`npm view failed: ${result.stderr}`) + return null + } + return result.stdout.trim() +} + +/** + * Compare two semver strings. Returns true if a >= b. + */ +function gte(a: string, b: string): boolean { + const parseVer = (v: string) => v.replace(/^\D/, '').split('.').map(Number) + const pa = parseVer(a) + const pb = parseVer(b) + for (let i = 0; i < 3; i++) { + if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true + if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false + } + return true +} + +export async function updateCCB(): Promise { + const currentVersion = getCurrentVersion() + writeToStdout(`Current version: ${currentVersion}\n`) + + // Determine package manager + const hasBun = isCommandAvailable('bun') + const useBun = isBunInstallation() + const pkgManager = useBun && hasBun ? 'bun' : 'npm' + + writeToStdout(`Package manager: ${pkgManager}\n`) + writeToStdout('Checking for updates...\n') + + // Get latest version + const latestVersion = await getLatestVersion() + if (!latestVersion) { + process.stderr.write(chalk.red('Failed to check for updates') + '\n') + process.stderr.write('Unable to fetch latest version from npm registry.\n') + await gracefulShutdown(1) + return + } + + // Already up to date? + if (latestVersion === currentVersion || gte(currentVersion, latestVersion)) { + writeToStdout(chalk.green(`ccb is up to date (${currentVersion})`) + '\n') + await gracefulShutdown(0) + return + } + + writeToStdout( + `New version available: ${latestVersion} (current: ${currentVersion})\n`, + ) + writeToStdout(`Installing update via ${pkgManager}...\n`) + + try { + if (pkgManager === 'bun') { + execSync(`bun update -g ${PACKAGE_NAME}`, { + stdio: 'inherit', + cwd: homedir(), + timeout: 120_000, + }) + } else { + execSync(`npm install -g ${PACKAGE_NAME}@latest`, { + stdio: 'inherit', + cwd: homedir(), + timeout: 120_000, + }) + } + + writeToStdout( + chalk.green( + `Successfully updated from ${currentVersion} to ${latestVersion}`, + ) + '\n', + ) + } catch (error) { + process.stderr.write(chalk.red('Update failed') + '\n') + process.stderr.write(`${error}\n`) + process.stderr.write('\n') + process.stderr.write('Try manually updating with:\n') + if (pkgManager === 'bun') { + process.stderr.write(chalk.bold(` bun update -g ${PACKAGE_NAME}`) + '\n') + } else { + process.stderr.write( + chalk.bold(` npm install -g ${PACKAGE_NAME}@latest`) + '\n', + ) + } + await gracefulShutdown(1) + } + + await gracefulShutdown(0) +} diff --git a/src/main.tsx b/src/main.tsx index 9d9cf56de..06a05cabf 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6551,6 +6551,15 @@ async function run(): Promise { }, ); + // claude update — update ccb to the latest version via npm or bun + program + .command("update") + .description("Update claude-code-best (ccb) to the latest version") + .action(async () => { + const { updateCCB } = await import("./cli/updateCCB.js"); + await updateCCB(); + }); + // ant-only commands if (process.env.USER_TYPE === "ant") { const validateLogId = (value: string) => {