mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 22:16:41 +00:00
Fixed:
- Migration now runs during installation across all methods (npm, bash, PowerShell)
- Guarantees ~/.ccs/shared/ populated immediately with ~/.claude/ content
- Users no longer need to run ccs command to trigger migration
Changed:
- Refactored SharedManager with _needsMigration() and _performMigration() methods
- _copyDirectory() returns {copied, skipped} stats and preserves existing files
- Shows detailed migration output: '[OK] Migrated 5 commands, 19 skills'
- Removed lazy migration from bin/ccs.js, lib/ccs, lib/ccs.ps1
Implementation:
- npm: Migration in scripts/postinstall.js
- bash: Migration in installers/install.sh (migrate_shared_data function)
- PowerShell: Migration in installers/install.ps1 (Invoke-SharedDataMigration)
- Fixed arithmetic expansion with set -e (changed ((var++)) to var=$((var + 1)))
Cross-platform parity maintained across all installation methods.
320 lines
10 KiB
JavaScript
Executable File
320 lines
10 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('./helpers');
|
|
const { detectClaudeCli, showClaudeNotFoundError } = require('./claude-detector');
|
|
const { getSettingsPath, getConfigPath } = require('./config-manager');
|
|
const { ErrorManager } = require('./error-manager');
|
|
const RecoveryManager = require('./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
|
|
console.log(colored('Installation:', 'cyan'));
|
|
|
|
// Location
|
|
const installLocation = process.argv[1] || '(not found)';
|
|
console.log(` ${colored('Location:', 'cyan')} ${installLocation}`);
|
|
|
|
// Config path
|
|
const configPath = getConfigPath();
|
|
console.log(` ${colored('Config:', 'cyan')} ${configPath}`);
|
|
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 (work, personal, team) and');
|
|
console.log(' alternative models (GLM, Kimi) instantly. Concurrent sessions with');
|
|
console.log(' 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 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')} Manage multiple Claude accounts`);
|
|
console.log(` ${colored('ccs work', 'yellow')} Switch to work account`);
|
|
console.log(` ${colored('ccs personal', 'yellow')} Switch to personal account`);
|
|
console.log('');
|
|
|
|
// Diagnostics
|
|
console.log(colored('Diagnostics:', 'cyan'));
|
|
console.log(` ${colored('ccs doctor', 'yellow')} Run health check and diagnostics`);
|
|
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('');
|
|
|
|
// 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(' Note: Commands, skills, and agents are symlinked across all profiles');
|
|
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('./doctor');
|
|
const doctor = new Doctor();
|
|
|
|
await doctor.runAllChecks();
|
|
|
|
// Exit with error code if unhealthy
|
|
process.exit(doctor.results.isHealthy() ? 0 : 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) };
|
|
}
|
|
}
|
|
|
|
// 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: doctor command
|
|
if (firstArg === 'doctor' || firstArg === '--doctor') {
|
|
await handleDoctorCommand();
|
|
return;
|
|
}
|
|
|
|
// Special case: auth command (multi-account management)
|
|
if (firstArg === 'auth') {
|
|
const AuthCommands = require('./auth-commands');
|
|
const authCommands = new AuthCommands();
|
|
await authCommands.route(args.slice(1));
|
|
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('./profile-detector');
|
|
const InstanceManager = require('./instance-manager');
|
|
const ProfileRegistry = require('./profile-registry');
|
|
const { getSettingsPath } = require('./config-manager');
|
|
|
|
const detector = new ProfileDetector();
|
|
|
|
try {
|
|
const profileInfo = detector.detectProfileType(profile);
|
|
|
|
if (profileInfo.type === 'settings') {
|
|
// 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) {
|
|
console.error(`[X] ${error.message}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Run main
|
|
main().catch(error => {
|
|
console.error('Fatal error:', error.message);
|
|
process.exit(1);
|
|
}); |