mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 18:21:09 +00:00
* fix: clear package manager cache during update to ensure fresh downloads (v4.3.10) Resolves issue where 'ccs update' would report success but serve cached package versions, requiring manual 'npm cache clean --force'. Changes: - Node.js (bin/ccs.js): Added cache clearing for npm, yarn, pnpm before update - Bash (lib/ccs): Added 'npm cache clean --force' before npm update - PowerShell (lib/ccs.ps1): Added 'npm cache clean --force' before npm update - Updated manual fallback commands to include cache clearing - Non-blocking: continues with update even if cache clearing fails Version bumped to 4.3.10 * docs: update CHANGELOG for v4.3.10
1034 lines
34 KiB
JavaScript
Executable File
1034 lines
34 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
const { spawn } = require('child_process');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const { error, colored } = require('./utils/helpers');
|
|
const { detectClaudeCli, showClaudeNotFoundError } = require('./utils/claude-detector');
|
|
const { getSettingsPath, getConfigPath } = require('./utils/config-manager');
|
|
const { ErrorManager } = require('./utils/error-manager');
|
|
const RecoveryManager = require('./management/recovery-manager');
|
|
|
|
// Version (sync with package.json)
|
|
const CCS_VERSION = require('../package.json').version;
|
|
|
|
// Helper: Escape arguments for shell execution
|
|
function escapeShellArg(arg) {
|
|
// Windows: escape double quotes and wrap in double quotes
|
|
return '"' + String(arg).replace(/"/g, '""') + '"';
|
|
}
|
|
|
|
// Execute Claude CLI with unified spawn logic
|
|
function execClaude(claudeCli, args, envVars = null) {
|
|
const isWindows = process.platform === 'win32';
|
|
const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli);
|
|
|
|
// Prepare environment (merge with process.env if envVars provided)
|
|
const env = envVars ? { ...process.env, ...envVars } : process.env;
|
|
|
|
let child;
|
|
if (needsShell) {
|
|
// When shell needed: concatenate into string to avoid DEP0190 warning
|
|
const cmdString = [claudeCli, ...args].map(escapeShellArg).join(' ');
|
|
child = spawn(cmdString, {
|
|
stdio: 'inherit',
|
|
windowsHide: true,
|
|
shell: true,
|
|
env
|
|
});
|
|
} else {
|
|
// When no shell needed: use array form (faster, no shell overhead)
|
|
child = spawn(claudeCli, args, {
|
|
stdio: 'inherit',
|
|
windowsHide: true,
|
|
env
|
|
});
|
|
}
|
|
|
|
child.on('exit', (code, signal) => {
|
|
if (signal) process.kill(process.pid, signal);
|
|
else process.exit(code || 0);
|
|
});
|
|
child.on('error', () => {
|
|
showClaudeNotFoundError();
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
// Special command handlers
|
|
function handleVersionCommand() {
|
|
// Title
|
|
console.log(colored(`CCS (Claude Code Switch) v${CCS_VERSION}`, 'bold'));
|
|
console.log('');
|
|
|
|
// Installation section with table-like formatting
|
|
console.log(colored('Installation:', 'cyan'));
|
|
|
|
// Location
|
|
const installLocation = process.argv[1] || '(not found)';
|
|
console.log(` ${colored('Location:'.padEnd(17), 'cyan')} ${installLocation}`);
|
|
|
|
// .ccs/ directory location
|
|
const ccsDir = path.join(os.homedir(), '.ccs');
|
|
console.log(` ${colored('CCS Directory:'.padEnd(17), 'cyan')} ${ccsDir}`);
|
|
|
|
// Config path
|
|
const configPath = getConfigPath();
|
|
console.log(` ${colored('Config:'.padEnd(17), 'cyan')} ${configPath}`);
|
|
|
|
// Profiles.json location
|
|
const profilesJson = path.join(os.homedir(), '.ccs', 'profiles.json');
|
|
console.log(` ${colored('Profiles:'.padEnd(17), 'cyan')} ${profilesJson}`);
|
|
|
|
// Delegation status - check multiple indicators
|
|
const delegationSessionsPath = path.join(os.homedir(), '.ccs', 'delegation-sessions.json');
|
|
const delegationConfigured = fs.existsSync(delegationSessionsPath);
|
|
|
|
let readyProfiles = [];
|
|
|
|
// Check for profiles with valid API keys
|
|
for (const profile of ['glm', 'kimi']) {
|
|
const settingsPath = path.join(os.homedir(), '.ccs', `${profile}.settings.json`);
|
|
if (fs.existsSync(settingsPath)) {
|
|
try {
|
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
const apiKey = settings.env?.ANTHROPIC_AUTH_TOKEN;
|
|
if (apiKey && !apiKey.match(/YOUR_.*_API_KEY_HERE/) && !apiKey.match(/sk-test.*/)) {
|
|
readyProfiles.push(profile);
|
|
}
|
|
} catch (error) {
|
|
// Invalid JSON, skip
|
|
}
|
|
}
|
|
}
|
|
|
|
const hasValidApiKeys = readyProfiles.length > 0;
|
|
const delegationEnabled = delegationConfigured || hasValidApiKeys;
|
|
|
|
if (delegationEnabled) {
|
|
console.log(` ${colored('Delegation:'.padEnd(17), 'cyan')} Enabled`);
|
|
} else {
|
|
console.log(` ${colored('Delegation:'.padEnd(17), 'cyan')} Not configured`);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
// Ready Profiles section - make it more prominent
|
|
if (readyProfiles.length > 0) {
|
|
console.log(colored('Delegation Ready:', 'cyan'));
|
|
console.log(` ${colored('✓', 'yellow')} ${readyProfiles.join(', ')} profiles are ready for delegation`);
|
|
console.log('');
|
|
} else if (delegationEnabled) {
|
|
console.log(colored('Delegation Ready:', 'cyan'));
|
|
console.log(` ${colored('!', 'yellow')} Delegation configured but no valid API keys found`);
|
|
console.log('');
|
|
}
|
|
|
|
// Documentation
|
|
console.log(`${colored('Documentation:', 'cyan')} https://github.com/kaitranntt/ccs`);
|
|
console.log(`${colored('License:', 'cyan')} MIT`);
|
|
console.log('');
|
|
|
|
// Help hint
|
|
console.log(colored('Run \'ccs --help\' for usage information', 'yellow'));
|
|
|
|
process.exit(0);
|
|
}
|
|
|
|
function handleHelpCommand() {
|
|
// Title
|
|
console.log(colored('CCS (Claude Code Switch) - Instant profile switching for Claude CLI', 'bold'));
|
|
console.log('');
|
|
|
|
// Usage
|
|
console.log(colored('Usage:', 'cyan'));
|
|
console.log(` ${colored('ccs', 'yellow')} [profile] [claude-args...]`);
|
|
console.log(` ${colored('ccs', 'yellow')} [flags]`);
|
|
console.log('');
|
|
|
|
// Description
|
|
console.log(colored('Description:', 'cyan'));
|
|
console.log(' Switch between multiple Claude accounts and alternative models');
|
|
console.log(' (GLM, Kimi) instantly. Run different Claude CLI sessions concurrently');
|
|
console.log(' with auto-recovery. Zero downtime.');
|
|
console.log('');
|
|
|
|
// Model Switching
|
|
console.log(colored('Model Switching:', 'cyan'));
|
|
console.log(` ${colored('ccs', 'yellow')} Use default Claude account`);
|
|
console.log(` ${colored('ccs glm', 'yellow')} Switch to GLM 4.6 model`);
|
|
console.log(` ${colored('ccs glmt', 'yellow')} Switch to GLM with thinking mode`);
|
|
console.log(` ${colored('ccs glmt --verbose', 'yellow')} Enable debug logging`);
|
|
console.log(` ${colored('ccs kimi', 'yellow')} Switch to Kimi for Coding`);
|
|
console.log(` ${colored('ccs glm', 'yellow')} "debug this code" Use GLM and run command`);
|
|
console.log('');
|
|
|
|
// Account Management
|
|
console.log(colored('Account Management:', 'cyan'));
|
|
console.log(` ${colored('ccs auth --help', 'yellow')} Run multiple Claude accounts concurrently`);
|
|
console.log('');
|
|
|
|
// Delegation (inside Claude Code CLI)
|
|
console.log(colored('Delegation (inside Claude Code CLI):', 'cyan'));
|
|
console.log(` ${colored('/ccs "task"', 'yellow')} Delegate task (auto-selects best profile)`);
|
|
console.log(` ${colored('/ccs --glm "task"', 'yellow')} Force GLM-4.6 for simple tasks`);
|
|
console.log(` ${colored('/ccs --kimi "task"', 'yellow')} Force Kimi for long context`);
|
|
console.log(` ${colored('/ccs:continue "follow-up"', 'yellow')} Continue last delegation session`);
|
|
console.log(' Save tokens by delegating simple tasks to cost-optimized models');
|
|
console.log('');
|
|
|
|
// Diagnostics
|
|
console.log(colored('Diagnostics:', 'cyan'));
|
|
console.log(` ${colored('ccs doctor', 'yellow')} Run health check and diagnostics`);
|
|
console.log(` ${colored('ccs sync', 'yellow')} Sync delegation commands and skills`);
|
|
console.log(` ${colored('ccs update', 'yellow')} Update CCS to latest version`);
|
|
console.log('');
|
|
|
|
// Flags
|
|
console.log(colored('Flags:', 'cyan'));
|
|
console.log(` ${colored('-h, --help', 'yellow')} Show this help message`);
|
|
console.log(` ${colored('-v, --version', 'yellow')} Show version and installation info`);
|
|
console.log(` ${colored('-sc, --shell-completion', 'yellow')} Install shell auto-completion`);
|
|
console.log('');
|
|
|
|
// Configuration
|
|
console.log(colored('Configuration:', 'cyan'));
|
|
console.log(' Config File: ~/.ccs/config.json');
|
|
console.log(' Profiles: ~/.ccs/profiles.json');
|
|
console.log(' Instances: ~/.ccs/instances/');
|
|
console.log(' Settings: ~/.ccs/*.settings.json');
|
|
console.log(' Environment: CCS_CONFIG (override config path)');
|
|
console.log('');
|
|
|
|
// Shared Data
|
|
console.log(colored('Shared Data:', 'cyan'));
|
|
console.log(' Commands: ~/.ccs/shared/commands/');
|
|
console.log(' Skills: ~/.ccs/shared/skills/');
|
|
console.log(' Agents: ~/.ccs/shared/agents/');
|
|
console.log(' Plugins: ~/.ccs/shared/plugins/');
|
|
console.log(' Note: Commands, skills, agents, and plugins are symlinked across all profiles');
|
|
console.log('');
|
|
|
|
// Examples
|
|
console.log(colored('Examples:', 'cyan'));
|
|
console.log(` ${colored('$ ccs', 'yellow')} # Use default account`);
|
|
console.log(` ${colored('$ ccs glm "implement API"', 'yellow')} # Cost-optimized model`);
|
|
console.log('');
|
|
console.log(` For more: ${colored('https://github.com/kaitranntt/ccs/blob/main/README.md', 'cyan')}`);
|
|
console.log('');
|
|
|
|
// Uninstall
|
|
console.log(colored('Uninstall:', 'yellow'));
|
|
console.log(' npm: npm uninstall -g @kaitranntt/ccs');
|
|
console.log(' macOS/Linux: curl -fsSL ccs.kaitran.ca/uninstall | bash');
|
|
console.log(' Windows: irm ccs.kaitran.ca/uninstall | iex');
|
|
console.log('');
|
|
|
|
// Documentation
|
|
console.log(colored('Documentation:', 'cyan'));
|
|
console.log(` GitHub: ${colored('https://github.com/kaitranntt/ccs', 'cyan')}`);
|
|
console.log(' Docs: https://github.com/kaitranntt/ccs/blob/main/README.md');
|
|
console.log(' Issues: https://github.com/kaitranntt/ccs/issues');
|
|
console.log('');
|
|
|
|
// License
|
|
console.log(`${colored('License:', 'cyan')} MIT`);
|
|
|
|
process.exit(0);
|
|
}
|
|
|
|
function handleInstallCommand() {
|
|
console.log('');
|
|
console.log('Feature not available');
|
|
console.log('');
|
|
console.log('The --install flag is currently under development.');
|
|
console.log('.claude/ integration testing is not complete.');
|
|
console.log('');
|
|
console.log('For updates: https://github.com/kaitranntt/ccs/issues');
|
|
console.log('');
|
|
process.exit(0);
|
|
}
|
|
|
|
function handleUninstallCommand() {
|
|
console.log('');
|
|
console.log('Feature not available');
|
|
console.log('');
|
|
console.log('The --uninstall flag is currently under development.');
|
|
console.log('.claude/ integration testing is not complete.');
|
|
console.log('');
|
|
console.log('For updates: https://github.com/kaitranntt/ccs/issues');
|
|
console.log('');
|
|
process.exit(0);
|
|
}
|
|
|
|
async function handleDoctorCommand() {
|
|
const Doctor = require('./management/doctor');
|
|
const doctor = new Doctor();
|
|
|
|
await doctor.runAllChecks();
|
|
|
|
// Exit with error code if unhealthy
|
|
process.exit(doctor.results.isHealthy() ? 0 : 1);
|
|
}
|
|
|
|
async function handleSyncCommand() {
|
|
const { colored } = require('./utils/helpers');
|
|
|
|
console.log('');
|
|
console.log(colored('Syncing CCS Components...', 'cyan'));
|
|
console.log('');
|
|
|
|
// First, copy .claude/ directory from package to ~/.ccs/.claude/
|
|
const ClaudeDirInstaller = require('./utils/claude-dir-installer');
|
|
const installer = new ClaudeDirInstaller();
|
|
installer.install();
|
|
|
|
console.log('');
|
|
|
|
const cleanupResult = installer.cleanupDeprecated();
|
|
if (cleanupResult.success && cleanupResult.cleanedFiles.length > 0) {
|
|
console.log('');
|
|
}
|
|
|
|
// Then, create symlinks from ~/.ccs/.claude/ to ~/.claude/
|
|
const ClaudeSymlinkManager = require('./utils/claude-symlink-manager');
|
|
const manager = new ClaudeSymlinkManager();
|
|
manager.install(false);
|
|
|
|
console.log('');
|
|
console.log(colored('[OK] Sync complete!', 'green'));
|
|
console.log('');
|
|
|
|
process.exit(0);
|
|
}
|
|
|
|
/**
|
|
* Detect installation method
|
|
* @returns {'npm'|'direct'} - Installation method
|
|
*/
|
|
function detectInstallationMethod() {
|
|
const scriptPath = process.argv[1];
|
|
|
|
// Method 1: Check if script is inside node_modules
|
|
if (scriptPath.includes('node_modules')) {
|
|
return 'npm';
|
|
}
|
|
|
|
// Method 2: Check if script is in npm global bin directory
|
|
// Common patterns for npm global installations
|
|
const npmGlobalBinPatterns = [
|
|
/\.npm\/global\/bin\//, // ~/.npm/global/bin/ccs
|
|
/\/\.nvm\/versions\/node\/[^\/]+\/bin\//, // ~/.nvm/versions/node/v22.19.0/bin/ccs
|
|
/\/usr\/local\/bin\//, // /usr/local/bin/ccs (if npm global prefix is /usr/local)
|
|
/\/usr\/bin\// // /usr/bin/ccs (if npm global prefix is /usr)
|
|
];
|
|
|
|
for (const pattern of npmGlobalBinPatterns) {
|
|
if (pattern.test(scriptPath)) {
|
|
// Verify this is actually CCS by checking the linked target
|
|
try {
|
|
const binDir = path.dirname(scriptPath);
|
|
const nodeModulesDir = path.join(binDir, '..', 'lib', 'node_modules', '@kaitranntt', 'ccs');
|
|
const globalModulesDir = path.join(binDir, '..', 'node_modules', '@kaitranntt', 'ccs');
|
|
|
|
if (fs.existsSync(nodeModulesDir) || fs.existsSync(globalModulesDir)) {
|
|
return 'npm';
|
|
}
|
|
} catch (err) {
|
|
// Continue checking other patterns
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 3: Check if package.json exists in parent directory (development mode)
|
|
const packageJsonPath = path.join(__dirname, '..', 'package.json');
|
|
|
|
if (fs.existsSync(packageJsonPath)) {
|
|
try {
|
|
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
// If package.json has name "@kaitranntt/ccs", it's npm install
|
|
if (pkg.name === '@kaitranntt/ccs') {
|
|
return 'npm';
|
|
}
|
|
} catch (err) {
|
|
// Ignore parse errors
|
|
}
|
|
}
|
|
|
|
// Method 4: Check if script is a symlink pointing to node_modules
|
|
try {
|
|
const stats = fs.lstatSync(scriptPath);
|
|
if (stats.isSymbolicLink()) {
|
|
const targetPath = fs.readlinkSync(scriptPath);
|
|
if (targetPath.includes('node_modules') || targetPath.includes('@kaitranntt/ccs')) {
|
|
return 'npm';
|
|
}
|
|
}
|
|
} catch (err) {
|
|
// Continue to default
|
|
}
|
|
|
|
// Default to direct installation
|
|
return 'direct';
|
|
}
|
|
|
|
/**
|
|
* Detect which package manager was used for installation
|
|
* @returns {'npm'|'yarn'|'pnpm'|'bun'|'unknown'}
|
|
*/
|
|
function detectPackageManager() {
|
|
const scriptPath = process.argv[1];
|
|
|
|
// Check if script path contains package manager indicators
|
|
if (scriptPath.includes('.pnpm')) return 'pnpm';
|
|
if (scriptPath.includes('yarn')) return 'yarn';
|
|
if (scriptPath.includes('bun')) return 'bun';
|
|
|
|
// Check parent directories for lock files
|
|
const binDir = path.dirname(scriptPath);
|
|
const fs = require('fs');
|
|
|
|
// Check global node_modules parent for lock files
|
|
let checkDir = binDir;
|
|
for (let i = 0; i < 5; i++) {
|
|
if (fs.existsSync(path.join(checkDir, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
if (fs.existsSync(path.join(checkDir, 'yarn.lock'))) return 'yarn';
|
|
if (fs.existsSync(path.join(checkDir, 'bun.lockb'))) return 'bun';
|
|
checkDir = path.dirname(checkDir);
|
|
}
|
|
|
|
// Check if package managers are available on the system
|
|
const { spawnSync } = require('child_process');
|
|
|
|
// Try yarn global list to see if CCS is installed via yarn
|
|
try {
|
|
const yarnResult = spawnSync('yarn', ['global', 'list', '--pattern', '@kaitranntt/ccs'], {
|
|
encoding: 'utf8',
|
|
shell: true,
|
|
timeout: 5000
|
|
});
|
|
if (yarnResult.status === 0 && yarnResult.stdout.includes('@kaitranntt/ccs')) {
|
|
return 'yarn';
|
|
}
|
|
} catch (err) {
|
|
// Continue to next check
|
|
}
|
|
|
|
// Try pnpm list -g to see if CCS is installed via pnpm
|
|
try {
|
|
const pnpmResult = spawnSync('pnpm', ['list', '-g', '--pattern', '@kaitranntt/ccs'], {
|
|
encoding: 'utf8',
|
|
shell: true,
|
|
timeout: 5000
|
|
});
|
|
if (pnpmResult.status === 0 && pnpmResult.stdout.includes('@kaitranntt/ccs')) {
|
|
return 'pnpm';
|
|
}
|
|
} catch (err) {
|
|
// Continue to next check
|
|
}
|
|
|
|
// Try bun pm ls -g to see if CCS is installed via bun
|
|
try {
|
|
const bunResult = spawnSync('bun', ['pm', 'ls', '-g', '--pattern', '@kaitranntt/ccs'], {
|
|
encoding: 'utf8',
|
|
shell: true,
|
|
timeout: 5000
|
|
});
|
|
if (bunResult.status === 0 && bunResult.stdout.includes('@kaitranntt/ccs')) {
|
|
return 'bun';
|
|
}
|
|
} catch (err) {
|
|
// Continue to default
|
|
}
|
|
|
|
// Default to npm
|
|
return 'npm';
|
|
}
|
|
|
|
async function handleUpdateCommand() {
|
|
const { checkForUpdates } = require('./utils/update-checker');
|
|
const { spawn } = require('child_process');
|
|
|
|
console.log('');
|
|
console.log(colored('Checking for updates...', 'cyan'));
|
|
console.log('');
|
|
|
|
// Detect installation method for proper update source
|
|
const installMethod = detectInstallationMethod();
|
|
const isNpmInstall = installMethod === 'npm';
|
|
|
|
// Check for updates (force check)
|
|
const updateResult = await checkForUpdates(CCS_VERSION, true, installMethod);
|
|
|
|
if (updateResult.status === 'check_failed') {
|
|
console.log(colored(`[X] ${updateResult.message}`, 'red'));
|
|
console.log('');
|
|
console.log(colored('[i] Possible causes:', 'yellow'));
|
|
console.log(' • Network connection issues');
|
|
console.log(' • Firewall blocking requests');
|
|
console.log(' • GitHub/npm API temporarily unavailable');
|
|
console.log('');
|
|
console.log('Try again later or update manually:');
|
|
if (isNpmInstall) {
|
|
const packageManager = detectPackageManager();
|
|
let manualCommand;
|
|
|
|
switch (packageManager) {
|
|
case 'npm':
|
|
manualCommand = 'npm install -g @kaitranntt/ccs@latest';
|
|
break;
|
|
case 'yarn':
|
|
manualCommand = 'yarn global add @kaitranntt/ccs@latest';
|
|
break;
|
|
case 'pnpm':
|
|
manualCommand = 'pnpm add -g @kaitranntt/ccs@latest';
|
|
break;
|
|
case 'bun':
|
|
manualCommand = 'bun add -g @kaitranntt/ccs@latest';
|
|
break;
|
|
default:
|
|
manualCommand = 'npm install -g @kaitranntt/ccs@latest';
|
|
}
|
|
|
|
console.log(colored(` ${manualCommand}`, 'yellow'));
|
|
} else {
|
|
const isWindows = process.platform === 'win32';
|
|
if (isWindows) {
|
|
console.log(colored(' irm ccs.kaitran.ca/install | iex', 'yellow'));
|
|
} else {
|
|
console.log(colored(' curl -fsSL ccs.kaitran.ca/install | bash', 'yellow'));
|
|
}
|
|
}
|
|
console.log('');
|
|
process.exit(1);
|
|
}
|
|
|
|
if (updateResult.status === 'no_update') {
|
|
let message = `You are already on the latest version (${CCS_VERSION})`;
|
|
|
|
// Add context for why no update is shown
|
|
switch (updateResult.reason) {
|
|
case 'dismissed':
|
|
message = `Update dismissed. You are on version ${CCS_VERSION}`;
|
|
console.log(colored(`[i] ${message}`, 'yellow'));
|
|
break;
|
|
case 'cached':
|
|
message = `No updates available (cached result). You are on version ${CCS_VERSION}`;
|
|
console.log(colored(`[i] ${message}`, 'cyan'));
|
|
break;
|
|
default:
|
|
console.log(colored(`[OK] ${message}`, 'green'));
|
|
}
|
|
console.log('');
|
|
process.exit(0);
|
|
}
|
|
|
|
// Update available
|
|
console.log(colored(`[i] Update available: ${updateResult.current} → ${updateResult.latest}`, 'yellow'));
|
|
console.log('');
|
|
|
|
if (isNpmInstall) {
|
|
// npm installation - detect package manager and update
|
|
const packageManager = detectPackageManager();
|
|
let updateCommand, updateArgs, cacheCommand, cacheArgs;
|
|
|
|
switch (packageManager) {
|
|
case 'npm':
|
|
updateCommand = 'npm';
|
|
updateArgs = ['install', '-g', '@kaitranntt/ccs@latest'];
|
|
cacheCommand = 'npm';
|
|
cacheArgs = ['cache', 'clean', '--force'];
|
|
break;
|
|
case 'yarn':
|
|
updateCommand = 'yarn';
|
|
updateArgs = ['global', 'add', '@kaitranntt/ccs@latest'];
|
|
cacheCommand = 'yarn';
|
|
cacheArgs = ['cache', 'clean'];
|
|
break;
|
|
case 'pnpm':
|
|
updateCommand = 'pnpm';
|
|
updateArgs = ['add', '-g', '@kaitranntt/ccs@latest'];
|
|
cacheCommand = 'pnpm';
|
|
cacheArgs = ['store', 'prune'];
|
|
break;
|
|
case 'bun':
|
|
updateCommand = 'bun';
|
|
updateArgs = ['add', '-g', '@kaitranntt/ccs@latest'];
|
|
cacheCommand = null; // bun doesn't need explicit cache clearing
|
|
cacheArgs = null;
|
|
break;
|
|
default:
|
|
updateCommand = 'npm';
|
|
updateArgs = ['install', '-g', '@kaitranntt/ccs@latest'];
|
|
cacheCommand = 'npm';
|
|
cacheArgs = ['cache', 'clean', '--force'];
|
|
}
|
|
|
|
console.log(colored(`Updating via ${packageManager}...`, 'cyan'));
|
|
console.log('');
|
|
|
|
// Clear package manager cache first to ensure fresh download
|
|
if (cacheCommand) {
|
|
console.log(colored('Clearing package cache...', 'cyan'));
|
|
const cacheChild = spawn(cacheCommand, cacheArgs, {
|
|
stdio: 'inherit'
|
|
});
|
|
|
|
cacheChild.on('exit', (code) => {
|
|
if (code !== 0) {
|
|
console.log(colored('[!] Cache clearing failed, proceeding anyway...', 'yellow'));
|
|
}
|
|
// Continue with update regardless of cache clearing result
|
|
performUpdate();
|
|
});
|
|
|
|
cacheChild.on('error', (err) => {
|
|
console.log(colored('[!] Cache clearing failed, proceeding anyway...', 'yellow'));
|
|
// Continue with update regardless of cache clearing result
|
|
performUpdate();
|
|
});
|
|
} else {
|
|
// No cache clearing needed, proceed directly
|
|
performUpdate();
|
|
}
|
|
|
|
function performUpdate() {
|
|
const child = spawn(updateCommand, updateArgs, {
|
|
stdio: 'inherit'
|
|
// No shell needed for direct commands
|
|
});
|
|
|
|
child.on('exit', (code) => {
|
|
if (code === 0) {
|
|
console.log('');
|
|
console.log(colored('[OK] Update successful!', 'green'));
|
|
console.log('');
|
|
console.log(`Run ${colored('ccs --version', 'yellow')} to verify`);
|
|
console.log('');
|
|
} else {
|
|
console.log('');
|
|
console.log(colored('[X] Update failed', 'red'));
|
|
console.log('');
|
|
console.log('Try manually:');
|
|
console.log(colored(` ${updateCommand} ${updateArgs.join(' ')}`, 'yellow'));
|
|
console.log('');
|
|
}
|
|
process.exit(code || 0);
|
|
});
|
|
|
|
child.on('error', (err) => {
|
|
console.log('');
|
|
console.log(colored(`[X] Failed to run ${packageManager} update`, 'red'));
|
|
console.log('');
|
|
console.log('Try manually:');
|
|
console.log(colored(` ${updateCommand} ${updateArgs.join(' ')}`, 'yellow'));
|
|
console.log('');
|
|
process.exit(1);
|
|
});
|
|
}
|
|
} else {
|
|
// Direct installation - re-run installer
|
|
console.log(colored('Updating via installer...', 'cyan'));
|
|
console.log('');
|
|
|
|
const isWindows = process.platform === 'win32';
|
|
let command, args;
|
|
|
|
if (isWindows) {
|
|
// PowerShell
|
|
command = 'powershell.exe';
|
|
args = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command',
|
|
'irm ccs.kaitran.ca/install | iex'];
|
|
} else {
|
|
// Unix (bash with proper shell invocation)
|
|
command = '/bin/bash';
|
|
args = ['-c', 'curl -fsSL ccs.kaitran.ca/install | bash'];
|
|
}
|
|
|
|
const child = spawn(command, args, {
|
|
stdio: 'inherit'
|
|
// Do NOT use shell: true
|
|
});
|
|
|
|
child.on('exit', (code) => {
|
|
if (code === 0) {
|
|
console.log('');
|
|
console.log(colored('[OK] Update successful!', 'green'));
|
|
console.log('');
|
|
console.log(`Run ${colored('ccs --version', 'yellow')} to verify`);
|
|
console.log('');
|
|
} else {
|
|
console.log('');
|
|
console.log(colored('[X] Update failed', 'red'));
|
|
console.log('');
|
|
console.log('Try manually:');
|
|
if (isWindows) {
|
|
console.log(colored(' irm ccs.kaitran.ca/install | iex', 'yellow'));
|
|
} else {
|
|
console.log(colored(' curl -fsSL ccs.kaitran.ca/install | bash', 'yellow'));
|
|
}
|
|
console.log('');
|
|
}
|
|
process.exit(code || 0);
|
|
});
|
|
|
|
child.on('error', (err) => {
|
|
console.log('');
|
|
console.log(colored('[X] Failed to run installer', 'red'));
|
|
console.log('');
|
|
console.log('Try manually:');
|
|
if (isWindows) {
|
|
console.log(colored(' irm ccs.kaitran.ca/install | iex', 'yellow'));
|
|
} else {
|
|
console.log(colored(' curl -fsSL ccs.kaitran.ca/install | bash', 'yellow'));
|
|
}
|
|
console.log('');
|
|
process.exit(1);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Smart profile detection
|
|
function detectProfile(args) {
|
|
if (args.length === 0 || args[0].startsWith('-')) {
|
|
// No args or first arg is a flag → use default profile
|
|
return { profile: 'default', remainingArgs: args };
|
|
} else {
|
|
// First arg doesn't start with '-' → treat as profile name
|
|
return { profile: args[0], remainingArgs: args.slice(1) };
|
|
}
|
|
}
|
|
|
|
// Execute Claude CLI with embedded proxy (for GLMT profile)
|
|
async function execClaudeWithProxy(claudeCli, profileName, args) {
|
|
const { getSettingsPath } = require('./utils/config-manager');
|
|
|
|
// 1. Read settings to get API key
|
|
const settingsPath = getSettingsPath(profileName);
|
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
const apiKey = settings.env.ANTHROPIC_AUTH_TOKEN;
|
|
|
|
if (!apiKey || apiKey === 'YOUR_GLM_API_KEY_HERE') {
|
|
console.error('[X] GLMT profile requires Z.AI API key');
|
|
console.error(' Edit ~/.ccs/glmt.settings.json and set ANTHROPIC_AUTH_TOKEN');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Detect verbose flag
|
|
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
|
|
// 2. Spawn embedded proxy with verbose flag
|
|
const proxyPath = path.join(__dirname, 'glmt', 'glmt-proxy.js');
|
|
const proxyArgs = verbose ? ['--verbose'] : [];
|
|
// Use process.execPath for Windows compatibility (CVE-2024-27980)
|
|
const proxy = spawn(process.execPath, [proxyPath, ...proxyArgs], {
|
|
stdio: ['ignore', 'pipe', verbose ? 'pipe' : 'inherit']
|
|
});
|
|
|
|
// 3. Wait for proxy ready signal (with timeout)
|
|
const { ProgressIndicator } = require('./utils/progress-indicator');
|
|
const spinner = new ProgressIndicator('Starting GLMT proxy');
|
|
spinner.start();
|
|
|
|
let port;
|
|
try {
|
|
port = await new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => {
|
|
reject(new Error('Proxy startup timeout (5s)'));
|
|
}, 5000);
|
|
|
|
proxy.stdout.on('data', (data) => {
|
|
const match = data.toString().match(/PROXY_READY:(\d+)/);
|
|
if (match) {
|
|
clearTimeout(timeout);
|
|
resolve(parseInt(match[1]));
|
|
}
|
|
});
|
|
|
|
proxy.on('error', (error) => {
|
|
clearTimeout(timeout);
|
|
reject(error);
|
|
});
|
|
|
|
proxy.on('exit', (code) => {
|
|
if (code !== 0 && code !== null) {
|
|
clearTimeout(timeout);
|
|
reject(new Error(`Proxy exited with code ${code}`));
|
|
}
|
|
});
|
|
});
|
|
|
|
spinner.succeed(`GLMT proxy ready on port ${port}`);
|
|
} catch (error) {
|
|
spinner.fail('Failed to start GLMT proxy');
|
|
console.error('[X] Error:', error.message);
|
|
console.error('');
|
|
console.error('Possible causes:');
|
|
console.error(' 1. Port conflict (unlikely with random port)');
|
|
console.error(' 2. Node.js permission issue');
|
|
console.error(' 3. Firewall blocking localhost');
|
|
console.error('');
|
|
console.error('Workarounds:');
|
|
console.error(' - Use non-thinking mode: ccs glm "prompt"');
|
|
console.error(' - Enable verbose logging: ccs glmt --verbose "prompt"');
|
|
console.error(' - Check proxy logs in ~/.ccs/logs/ (if debug enabled)');
|
|
console.error('');
|
|
proxy.kill();
|
|
process.exit(1);
|
|
}
|
|
|
|
// 4. Spawn Claude CLI with proxy URL
|
|
const envVars = {
|
|
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,
|
|
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
ANTHROPIC_MODEL: 'glm-4.6'
|
|
};
|
|
|
|
// Use existing execClaude helper for consistent Windows handling
|
|
const isWindows = process.platform === 'win32';
|
|
const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli);
|
|
const env = { ...process.env, ...envVars };
|
|
|
|
let claude;
|
|
if (needsShell) {
|
|
// When shell needed: concatenate into string to avoid DEP0190 warning
|
|
const cmdString = [claudeCli, ...args].map(escapeShellArg).join(' ');
|
|
claude = spawn(cmdString, {
|
|
stdio: 'inherit',
|
|
windowsHide: true,
|
|
shell: true,
|
|
env
|
|
});
|
|
} else {
|
|
// When no shell needed: use array form (faster, no shell overhead)
|
|
claude = spawn(claudeCli, args, {
|
|
stdio: 'inherit',
|
|
windowsHide: true,
|
|
env
|
|
});
|
|
}
|
|
|
|
// 5. Cleanup: kill proxy when Claude exits
|
|
claude.on('exit', (code, signal) => {
|
|
proxy.kill('SIGTERM');
|
|
if (signal) process.kill(process.pid, signal);
|
|
else process.exit(code || 0);
|
|
});
|
|
|
|
claude.on('error', (error) => {
|
|
console.error('[X] Claude CLI error:', error);
|
|
proxy.kill('SIGTERM');
|
|
process.exit(1);
|
|
});
|
|
|
|
// Also handle parent process termination (use .once to avoid duplicates)
|
|
process.once('SIGTERM', () => {
|
|
proxy.kill('SIGTERM');
|
|
claude.kill('SIGTERM');
|
|
});
|
|
|
|
process.once('SIGINT', () => {
|
|
proxy.kill('SIGTERM');
|
|
claude.kill('SIGTERM');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle shell completion installation
|
|
*/
|
|
async function handleShellCompletionCommand(args) {
|
|
const { ShellCompletionInstaller } = require('./utils/shell-completion');
|
|
const { colored } = require('./utils/helpers');
|
|
|
|
console.log(colored('Shell Completion Installer', 'bold'));
|
|
console.log('');
|
|
|
|
// Parse flags
|
|
let targetShell = null;
|
|
if (args.includes('--bash')) targetShell = 'bash';
|
|
else if (args.includes('--zsh')) targetShell = 'zsh';
|
|
else if (args.includes('--fish')) targetShell = 'fish';
|
|
else if (args.includes('--powershell')) targetShell = 'powershell';
|
|
|
|
try {
|
|
const installer = new ShellCompletionInstaller();
|
|
const result = installer.install(targetShell);
|
|
|
|
if (result.alreadyInstalled) {
|
|
console.log(colored('[OK] Shell completion already installed', 'green'));
|
|
console.log('');
|
|
return;
|
|
}
|
|
|
|
console.log(colored('[OK] Shell completion installed successfully!', 'green'));
|
|
console.log('');
|
|
console.log(result.message);
|
|
console.log('');
|
|
console.log(colored('To activate:', 'cyan'));
|
|
console.log(` ${result.reload}`);
|
|
console.log('');
|
|
console.log(colored('Then test:', 'cyan'));
|
|
console.log(' ccs <TAB> # See available profiles');
|
|
console.log(' ccs auth <TAB> # See auth subcommands');
|
|
console.log('');
|
|
} catch (error) {
|
|
console.error(colored('[X] Error:', 'red'), error.message);
|
|
console.error('');
|
|
console.error(colored('Usage:', 'yellow'));
|
|
console.error(' ccs --shell-completion # Auto-detect shell');
|
|
console.error(' ccs --shell-completion --bash # Install for bash');
|
|
console.error(' ccs --shell-completion --zsh # Install for zsh');
|
|
console.error(' ccs --shell-completion --fish # Install for fish');
|
|
console.error(' ccs --shell-completion --powershell # Install for PowerShell');
|
|
console.error('');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Main execution
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
|
|
// Special case: version command (check BEFORE profile detection)
|
|
const firstArg = args[0];
|
|
if (firstArg === 'version' || firstArg === '--version' || firstArg === '-v') {
|
|
handleVersionCommand();
|
|
}
|
|
|
|
// Special case: help command
|
|
if (firstArg === '--help' || firstArg === '-h' || firstArg === 'help') {
|
|
handleHelpCommand();
|
|
return;
|
|
}
|
|
|
|
// Special case: install command
|
|
if (firstArg === '--install') {
|
|
handleInstallCommand();
|
|
return;
|
|
}
|
|
|
|
// Special case: uninstall command
|
|
if (firstArg === '--uninstall') {
|
|
handleUninstallCommand();
|
|
return;
|
|
}
|
|
|
|
// Special case: shell completion installer
|
|
if (firstArg === '--shell-completion' || firstArg === '-sc') {
|
|
await handleShellCompletionCommand(args.slice(1));
|
|
return;
|
|
}
|
|
|
|
// Special case: doctor command
|
|
if (firstArg === 'doctor' || firstArg === '--doctor') {
|
|
await handleDoctorCommand();
|
|
return;
|
|
}
|
|
|
|
// Special case: sync command (sync delegation commands and skills to ~/.claude/)
|
|
if (firstArg === 'sync' || firstArg === '--sync') {
|
|
await handleSyncCommand();
|
|
return;
|
|
}
|
|
|
|
// Special case: update command (update CCS to latest version)
|
|
if (firstArg === 'update' || firstArg === '--update') {
|
|
await handleUpdateCommand();
|
|
return;
|
|
}
|
|
|
|
// Special case: auth command (multi-account management)
|
|
if (firstArg === 'auth') {
|
|
const AuthCommands = require('./auth/auth-commands');
|
|
const authCommands = new AuthCommands();
|
|
await authCommands.route(args.slice(1));
|
|
return;
|
|
}
|
|
|
|
// Special case: headless delegation (-p flag)
|
|
if (args.includes('-p') || args.includes('--prompt')) {
|
|
const DelegationHandler = require('./delegation/delegation-handler');
|
|
const handler = new DelegationHandler();
|
|
await handler.route(args);
|
|
return;
|
|
}
|
|
|
|
// Auto-recovery for missing configuration
|
|
const recovery = new RecoveryManager();
|
|
const recovered = recovery.recoverAll();
|
|
|
|
if (recovered) {
|
|
recovery.showRecoveryHints();
|
|
}
|
|
|
|
// Detect profile
|
|
const { profile, remainingArgs } = detectProfile(args);
|
|
|
|
// Detect Claude CLI first (needed for all paths)
|
|
const claudeCli = detectClaudeCli();
|
|
if (!claudeCli) {
|
|
ErrorManager.showClaudeNotFound();
|
|
process.exit(1);
|
|
}
|
|
|
|
// Use ProfileDetector to determine profile type
|
|
const ProfileDetector = require('./auth/profile-detector');
|
|
const InstanceManager = require('./management/instance-manager');
|
|
const ProfileRegistry = require('./auth/profile-registry');
|
|
const { getSettingsPath } = require('./utils/config-manager');
|
|
|
|
const detector = new ProfileDetector();
|
|
|
|
try {
|
|
const profileInfo = detector.detectProfileType(profile);
|
|
|
|
if (profileInfo.type === 'settings') {
|
|
// Check if this is GLMT profile (requires proxy)
|
|
if (profileInfo.name === 'glmt') {
|
|
// GLMT FLOW: Settings-based with embedded proxy for thinking support
|
|
await execClaudeWithProxy(claudeCli, profileInfo.name, remainingArgs);
|
|
} else {
|
|
// EXISTING FLOW: Settings-based profile (glm, kimi)
|
|
// Use --settings flag (backward compatible)
|
|
const expandedSettingsPath = getSettingsPath(profileInfo.name);
|
|
execClaude(claudeCli, ['--settings', expandedSettingsPath, ...remainingArgs]);
|
|
}
|
|
} else if (profileInfo.type === 'account') {
|
|
// NEW FLOW: Account-based profile (work, personal)
|
|
// All platforms: Use instance isolation with CLAUDE_CONFIG_DIR
|
|
const registry = new ProfileRegistry();
|
|
const instanceMgr = new InstanceManager();
|
|
|
|
// Ensure instance exists (lazy init if needed)
|
|
const instancePath = instanceMgr.ensureInstance(profileInfo.name);
|
|
|
|
// Update last_used timestamp
|
|
registry.touchProfile(profileInfo.name);
|
|
|
|
// Execute Claude with instance isolation
|
|
const envVars = { CLAUDE_CONFIG_DIR: instancePath };
|
|
execClaude(claudeCli, remainingArgs, envVars);
|
|
} else {
|
|
// DEFAULT: No profile configured, use Claude's own defaults
|
|
execClaude(claudeCli, remainingArgs);
|
|
}
|
|
} catch (error) {
|
|
// Check if this is a profile not found error with suggestions
|
|
if (error.profileName && error.availableProfiles !== undefined) {
|
|
const allProfiles = error.availableProfiles.split('\n');
|
|
ErrorManager.showProfileNotFound(error.profileName, allProfiles, error.suggestions);
|
|
} else {
|
|
console.error(`[X] ${error.message}`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Run main
|
|
main().catch(error => {
|
|
console.error('Fatal error:', error.message);
|
|
process.exit(1);
|
|
}); |