diff --git a/CLAUDE.md b/CLAUDE.md index 9e299699..0b69e40c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,9 +16,19 @@ CCS (Claude Code Switch) is a lightweight CLI wrapper enabling instant switching **YAGNI** (You Aren't Gonna Need It): No features "just in case" **KISS** (Keep It Simple): Simple bash/PowerShell/Node.js, no complexity **DRY** (Don't Repeat Yourself): One source of truth (config.json) +**CLI-First UX**: The primary user experience is through the command-line interface The tool does ONE thing: enable instant switching between Claude accounts and alternative models. Never add features that violate these principles. +### CLI Documentation Requirement (CRITICAL) + +**All functionality changes MUST update `--help` output across all implementations:** +- `bin/ccs.js` - handleHelpCommand() function +- `lib/ccs` - show_help() function (bash) +- `lib/ccs.ps1` - Show-Help function (PowerShell) + +This is non-negotiable. The CLI help is the primary documentation users reference. If a feature exists but isn't documented in `--help`, it effectively doesn't exist for users. + ## Key Constraints 1. **NO EMOJIS in terminal output** - Use ASCII symbols ([OK], [!], [X], [i]) for compatibility @@ -281,9 +291,11 @@ All values must be strings (not booleans/objects) to prevent PowerShell crashes. ### Adding a New Feature 1. Verify it aligns with YAGNI/KISS/DRY principles 2. Implement for both bash/PowerShell and Node.js if applicable -3. Test on all platforms (macOS, Linux, Windows) -4. Update tests in `tests/edge-cases.sh` and `tests/edge-cases.ps1` -5. Update CONTRIBUTING.md if it affects contributors +3. **Update `--help` output in ALL three implementations** (bin/ccs.js, lib/ccs, lib/ccs.ps1) - REQUIRED +4. Test on all platforms (macOS, Linux, Windows) +5. Update tests in `tests/edge-cases.sh` and `tests/edge-cases.ps1` +6. Update CONTRIBUTING.md if it affects contributors +7. Update README.md examples if user-facing ### Fixing Bugs 1. Add test case reproducing the bug @@ -318,6 +330,8 @@ Before any PR, verify: - [ ] Manual PATH setup instructions clear if auto fails - [ ] v3.0 concurrent sessions work correctly - [ ] Instance isolation works (no cross-profile contamination) +- [ ] **`--help` output updated in bin/ccs.js, lib/ccs, and lib/ccs.ps1** (if feature added/changed) +- [ ] **`--help` output consistent across all three implementations** ## Integration with Claude Code diff --git a/VERSION b/VERSION index 4a36342f..cb2b00e4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.0 +3.0.1 diff --git a/bin/ccs.js b/bin/ccs.js index a2e1fd1e..467047c9 100755 --- a/bin/ccs.js +++ b/bin/ccs.js @@ -8,6 +8,8 @@ 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; @@ -97,25 +99,34 @@ function handleHelpCommand() { // Description console.log(colored('Description:', 'cyan')); - console.log(' Switch between Claude models instantly. Stop hitting rate limits.'); - console.log(' Maps profile names to Claude settings files via ~/.ccs/config.json'); + 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(''); - // Profile Switching - console.log(colored('Profile Switching:', 'cyan')); - console.log(` ${colored('ccs', 'yellow')} Use default profile`); - console.log(` ${colored('ccs glm', 'yellow')} Switch to GLM profile`); - console.log(` ${colored('ccs kimi', 'yellow')} Switch to Kimi profile`); - console.log(` ${colored('ccs work', 'yellow')} Use work account (saved profile)`); - console.log(` ${colored('ccs glm', 'yellow')} "debug this code" Switch to GLM and run command`); - console.log(` ${colored('ccs work', 'yellow')} "review code" Use work account and run command`); + // 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 create ', 'yellow')} Create new profile and login`); console.log(` ${colored('ccs auth list', 'yellow')} List all saved profiles`); - console.log(` ${colored('ccs auth --help', 'yellow')} Show account management help`); + console.log(` ${colored('ccs auth show ', 'yellow')} Show profile details`); + console.log(` ${colored('ccs auth remove ', 'yellow')} Remove profile (requires --force)`); + console.log(` ${colored('ccs auth default ', 'yellow')} Set default profile`); + console.log(` ${colored('ccs work', 'yellow')} Switch to work account`); + console.log(` ${colored('ccs personal', 'yellow')} Switch to personal account`); + console.log(` ${colored('ccs work', 'yellow')} "review code" Run command with work account`); + console.log(''); + + // Diagnostics + console.log(colored('Diagnostics:', 'cyan')); + console.log(` ${colored('ccs doctor', 'yellow')} Run health check and diagnostics`); console.log(''); // Flags @@ -195,6 +206,16 @@ function handleUninstallCommand() { 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('-')) { @@ -234,6 +255,12 @@ async function main() { 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'); @@ -242,13 +269,21 @@ async function main() { 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) { - showClaudeNotFoundError(); + ErrorManager.showClaudeNotFound(); process.exit(1); } diff --git a/bin/config-manager.js b/bin/config-manager.js index b3f65bbd..d4371309 100644 --- a/bin/config-manager.js +++ b/bin/config-manager.js @@ -4,6 +4,7 @@ const fs = require('fs'); const path = require('path'); const os = require('os'); const { error, expandPath } = require('./helpers'); +const { ErrorManager } = require('./error-manager'); // Get config file path function getConfigPath() { @@ -16,7 +17,15 @@ function readConfig() { // Check config exists if (!fs.existsSync(configPath)) { - error(`Config file not found: ${configPath}`); + // Attempt recovery + const RecoveryManager = require('./recovery-manager'); + const recovery = new RecoveryManager(); + recovery.ensureConfigJson(); + + if (!fs.existsSync(configPath)) { + ErrorManager.showInvalidConfig(configPath, 'File not found'); + process.exit(1); + } } // Read and parse JSON @@ -25,12 +34,14 @@ function readConfig() { const configContent = fs.readFileSync(configPath, 'utf8'); config = JSON.parse(configContent); } catch (e) { - error(`Invalid JSON in ${configPath}: ${e.message}`); + ErrorManager.showInvalidConfig(configPath, `Invalid JSON: ${e.message}`); + process.exit(1); } // Validate config has profiles object if (!config.profiles || typeof config.profiles !== 'object') { - error(`Config must have 'profiles' object in ${configPath}`); + ErrorManager.showInvalidConfig(configPath, "Missing 'profiles' object"); + process.exit(1); } return config; @@ -44,8 +55,10 @@ function getSettingsPath(profile) { const settingsPath = config.profiles[profile]; if (!settingsPath) { - const availableProfiles = Object.keys(config.profiles).join(', '); - error(`Profile '${profile}' not found. Available: ${availableProfiles}`); + const availableProfiles = Object.keys(config.profiles); + const profileList = availableProfiles.map(p => ` - ${p}`); + ErrorManager.showProfileNotFound(profile, profileList); + process.exit(1); } // Expand path @@ -53,7 +66,22 @@ function getSettingsPath(profile) { // Validate settings file exists if (!fs.existsSync(expandedPath)) { - error(`Settings file not found: ${expandedPath}`); + // Auto-create if it's ~/.claude/settings.json + if (expandedPath.includes('.claude') && expandedPath.endsWith('settings.json')) { + const RecoveryManager = require('./recovery-manager'); + const recovery = new RecoveryManager(); + recovery.ensureClaudeSettings(); + + if (!fs.existsSync(expandedPath)) { + ErrorManager.showSettingsNotFound(expandedPath); + process.exit(1); + } + + console.log('[i] Auto-created missing settings file'); + } else { + ErrorManager.showSettingsNotFound(expandedPath); + process.exit(1); + } } // Validate settings file is valid JSON @@ -61,7 +89,8 @@ function getSettingsPath(profile) { const settingsContent = fs.readFileSync(expandedPath, 'utf8'); JSON.parse(settingsContent); } catch (e) { - error(`Invalid JSON in ${expandedPath}: ${e.message}`); + ErrorManager.showInvalidConfig(expandedPath, `Invalid JSON: ${e.message}`); + process.exit(1); } return expandedPath; diff --git a/bin/doctor.js b/bin/doctor.js new file mode 100644 index 00000000..b3f9dbe2 --- /dev/null +++ b/bin/doctor.js @@ -0,0 +1,365 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { spawn } = require('child_process'); +const { colored } = require('./helpers'); +const { detectClaudeCli } = require('./claude-detector'); + +/** + * Health check results + */ +class HealthCheck { + constructor() { + this.checks = []; + this.warnings = []; + this.errors = []; + } + + addCheck(name, status, message = '', fix = null) { + this.checks.push({ name, status, message, fix }); + + if (status === 'error') this.errors.push({ name, message, fix }); + if (status === 'warning') this.warnings.push({ name, message, fix }); + } + + hasErrors() { + return this.errors.length > 0; + } + + hasWarnings() { + return this.warnings.length > 0; + } + + isHealthy() { + return !this.hasErrors(); + } +} + +/** + * CCS Health Check and Diagnostics + */ +class Doctor { + constructor() { + this.homedir = os.homedir(); + this.ccsDir = path.join(this.homedir, '.ccs'); + this.claudeDir = path.join(this.homedir, '.claude'); + this.results = new HealthCheck(); + } + + /** + * Run all health checks + */ + async runAllChecks() { + console.log(colored('Running CCS Health Check...', 'cyan')); + console.log(''); + + await this.checkClaudeCli(); + this.checkCcsDirectory(); + this.checkConfigFiles(); + this.checkClaudeSettings(); + this.checkProfiles(); + this.checkInstances(); + this.checkPermissions(); + + this.showReport(); + return this.results; + } + + /** + * Check 1: Claude CLI availability + */ + async checkClaudeCli() { + process.stdout.write('[?] Checking Claude CLI... '); + + const claudeCli = detectClaudeCli(); + + // Try to execute claude --version + try { + const result = await new Promise((resolve, reject) => { + const child = spawn(claudeCli, ['--version'], { + stdio: 'pipe', + timeout: 5000 + }); + + let output = ''; + child.stdout.on('data', data => output += data); + child.stderr.on('data', data => output += data); + + child.on('close', code => { + if (code === 0) resolve(output); + else reject(new Error('Exit code ' + code)); + }); + + child.on('error', reject); + }); + + console.log(colored('[OK]', 'green')); + this.results.addCheck('Claude CLI', 'success', `Found: ${claudeCli}`); + } catch (err) { + console.log(colored('[X]', 'red')); + this.results.addCheck( + 'Claude CLI', + 'error', + 'Claude CLI not found or not working', + 'Install from: https://docs.claude.com/en/docs/claude-code/installation' + ); + } + } + + /** + * Check 2: ~/.ccs/ directory + */ + checkCcsDirectory() { + process.stdout.write('[?] Checking ~/.ccs/ directory... '); + + if (fs.existsSync(this.ccsDir)) { + console.log(colored('[OK]', 'green')); + this.results.addCheck('CCS Directory', 'success'); + } else { + console.log(colored('[X]', 'red')); + this.results.addCheck( + 'CCS Directory', + 'error', + '~/.ccs/ directory not found', + 'Run: npm install -g @kaitranntt/ccs --force' + ); + } + } + + /** + * Check 3: Config files + */ + checkConfigFiles() { + const files = [ + { path: path.join(this.ccsDir, 'config.json'), name: 'config.json' }, + { path: path.join(this.ccsDir, 'glm.settings.json'), name: 'glm.settings.json' }, + { path: path.join(this.ccsDir, 'kimi.settings.json'), name: 'kimi.settings.json' } + ]; + + for (const file of files) { + process.stdout.write(`[?] Checking ${file.name}... `); + + if (!fs.existsSync(file.path)) { + console.log(colored('[X]', 'red')); + this.results.addCheck( + file.name, + 'error', + `${file.name} not found`, + 'Run: npm install -g @kaitranntt/ccs --force' + ); + continue; + } + + // Validate JSON + try { + const content = fs.readFileSync(file.path, 'utf8'); + JSON.parse(content); + console.log(colored('[OK]', 'green')); + this.results.addCheck(file.name, 'success'); + } catch (e) { + console.log(colored('[X]', 'red')); + this.results.addCheck( + file.name, + 'error', + `Invalid JSON: ${e.message}`, + `Backup and recreate: mv ${file.path} ${file.path}.backup && npm install -g @kaitranntt/ccs --force` + ); + } + } + } + + /** + * Check 4: Claude settings + */ + checkClaudeSettings() { + process.stdout.write('[?] Checking ~/.claude/settings.json... '); + + const settingsPath = path.join(this.claudeDir, 'settings.json'); + + if (!fs.existsSync(settingsPath)) { + console.log(colored('[!]', 'yellow')); + this.results.addCheck( + 'Claude Settings', + 'warning', + '~/.claude/settings.json not found', + 'Run: claude /login' + ); + return; + } + + // Validate JSON + try { + const content = fs.readFileSync(settingsPath, 'utf8'); + JSON.parse(content); + console.log(colored('[OK]', 'green')); + this.results.addCheck('Claude Settings', 'success'); + } catch (e) { + console.log(colored('[!]', 'yellow')); + this.results.addCheck( + 'Claude Settings', + 'warning', + `Invalid JSON: ${e.message}`, + 'Run: claude /login' + ); + } + } + + /** + * Check 5: Profile configurations + */ + checkProfiles() { + process.stdout.write('[?] Checking profiles... '); + + const configPath = path.join(this.ccsDir, 'config.json'); + if (!fs.existsSync(configPath)) { + console.log(colored('[SKIP]', 'yellow')); + return; + } + + try { + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + + if (!config.profiles || typeof config.profiles !== 'object') { + console.log(colored('[X]', 'red')); + this.results.addCheck( + 'Profiles', + 'error', + 'config.json missing profiles object', + 'Run: npm install -g @kaitranntt/ccs --force' + ); + return; + } + + const profileCount = Object.keys(config.profiles).length; + console.log(colored('[OK]', 'green'), `(${profileCount} profiles)`); + this.results.addCheck('Profiles', 'success', `${profileCount} profiles configured`); + } catch (e) { + console.log(colored('[X]', 'red')); + this.results.addCheck('Profiles', 'error', e.message); + } + } + + /** + * Check 6: Instance directories (account-based profiles) + */ + checkInstances() { + process.stdout.write('[?] Checking instances... '); + + const instancesDir = path.join(this.ccsDir, 'instances'); + if (!fs.existsSync(instancesDir)) { + console.log(colored('[i]', 'cyan'), '(no account profiles)'); + this.results.addCheck('Instances', 'success', 'No account profiles configured'); + return; + } + + const instances = fs.readdirSync(instancesDir).filter(name => { + return fs.statSync(path.join(instancesDir, name)).isDirectory(); + }); + + if (instances.length === 0) { + console.log(colored('[i]', 'cyan'), '(no account profiles)'); + this.results.addCheck('Instances', 'success', 'No account profiles'); + return; + } + + console.log(colored('[OK]', 'green'), `(${instances.length} instances)`); + this.results.addCheck('Instances', 'success', `${instances.length} account profiles`); + } + + /** + * Check 7: File permissions + */ + checkPermissions() { + process.stdout.write('[?] Checking permissions... '); + + const testFile = path.join(this.ccsDir, '.permission-test'); + + try { + fs.writeFileSync(testFile, 'test', 'utf8'); + fs.unlinkSync(testFile); + console.log(colored('[OK]', 'green')); + this.results.addCheck('Permissions', 'success'); + } catch (e) { + console.log(colored('[X]', 'red')); + this.results.addCheck( + 'Permissions', + 'error', + 'Cannot write to ~/.ccs/', + 'Fix: sudo chown -R $USER ~/.ccs ~/.claude && chmod 755 ~/.ccs ~/.claude' + ); + } + } + + /** + * Show health check report + */ + showReport() { + console.log(''); + console.log(colored('═══════════════════════════════════════════', 'cyan')); + console.log(colored('Health Check Report', 'bold')); + console.log(colored('═══════════════════════════════════════════', 'cyan')); + console.log(''); + + if (this.results.isHealthy() && !this.results.hasWarnings()) { + console.log(colored('✓ All checks passed!', 'green')); + console.log(''); + console.log('Your CCS installation is healthy.'); + console.log(''); + return; + } + + // Show errors + if (this.results.hasErrors()) { + console.log(colored('Errors:', 'red')); + this.results.errors.forEach(err => { + console.log(` [X] ${err.name}: ${err.message}`); + if (err.fix) { + console.log(` Fix: ${err.fix}`); + } + }); + console.log(''); + } + + // Show warnings + if (this.results.hasWarnings()) { + console.log(colored('Warnings:', 'yellow')); + this.results.warnings.forEach(warn => { + console.log(` [!] ${warn.name}: ${warn.message}`); + if (warn.fix) { + console.log(` Fix: ${warn.fix}`); + } + }); + console.log(''); + } + + // Summary + if (this.results.hasErrors()) { + console.log(colored('Status: Installation has errors', 'red')); + console.log('Run suggested fixes above to resolve issues.'); + } else { + console.log(colored('Status: Installation healthy (warnings only)', 'green')); + } + + console.log(''); + } + + /** + * Generate JSON report + */ + generateJsonReport() { + return JSON.stringify({ + timestamp: new Date().toISOString(), + platform: process.platform, + nodeVersion: process.version, + ccsVersion: require('../package.json').version, + checks: this.results.checks, + errors: this.results.errors, + warnings: this.results.warnings, + healthy: this.results.isHealthy() + }, null, 2); + } +} + +module.exports = Doctor; \ No newline at end of file diff --git a/bin/error-manager.js b/bin/error-manager.js new file mode 100644 index 00000000..e3d76fa6 --- /dev/null +++ b/bin/error-manager.js @@ -0,0 +1,159 @@ +'use strict'; + +const { colored } = require('./helpers'); + +/** + * Error types with structured messages + */ +const ErrorTypes = { + NO_CLAUDE_CLI: 'NO_CLAUDE_CLI', + MISSING_SETTINGS: 'MISSING_SETTINGS', + INVALID_CONFIG: 'INVALID_CONFIG', + UNKNOWN_PROFILE: 'UNKNOWN_PROFILE', + PERMISSION_DENIED: 'PERMISSION_DENIED', + GENERIC: 'GENERIC' +}; + +/** + * Enhanced error manager with context-aware messages + */ +class ErrorManager { + /** + * Show Claude CLI not found error + */ + static showClaudeNotFound() { + console.error(''); + console.error(colored('╔══════════════════════════════════════════════════════════╗', 'red')); + console.error(colored('║ ERROR: Claude CLI not found ║', 'red')); + console.error(colored('╚══════════════════════════════════════════════════════════╝', 'red')); + console.error(''); + console.error('CCS requires Claude CLI to be installed.'); + console.error(''); + console.error(colored('Fix:', 'yellow')); + console.error(' 1. Install Claude CLI:'); + console.error(' https://docs.claude.com/en/docs/claude-code/installation'); + console.error(''); + console.error(' 2. Verify installation:'); + console.error(' command -v claude (Unix)'); + console.error(' Get-Command claude (Windows)'); + console.error(''); + console.error(' 3. Custom path (if installed elsewhere):'); + console.error(' export CCS_CLAUDE_PATH="/path/to/claude"'); + console.error(''); + console.error('Restart terminal after installation.'); + console.error(''); + } + + /** + * Show settings file not found error + * @param {string} settingsPath - Path to missing settings file + */ + static showSettingsNotFound(settingsPath) { + const isClaudeSettings = settingsPath.includes('.claude') && settingsPath.endsWith('settings.json'); + + console.error(''); + console.error(colored('╔══════════════════════════════════════════════════════════╗', 'red')); + console.error(colored('║ ERROR: Settings file not found ║', 'red')); + console.error(colored('╚══════════════════════════════════════════════════════════╝', 'red')); + console.error(''); + console.error(`File: ${settingsPath}`); + console.error(''); + + if (isClaudeSettings) { + console.error('This file is auto-created when you login to Claude CLI.'); + console.error(''); + console.error(colored('Fix (copy-paste):', 'yellow')); + console.error(` echo '{}' > ${settingsPath}`); + console.error(' claude /login'); + console.error(''); + console.error('Why: Newer Claude CLI versions require explicit login.'); + } else { + console.error(colored('Fix (copy-paste):', 'yellow')); + console.error(' npm install -g @kaitranntt/ccs --force'); + console.error(''); + console.error('This will recreate missing profile settings.'); + } + + console.error(''); + } + + /** + * Show invalid configuration error + * @param {string} configPath - Path to invalid config + * @param {string} errorDetail - JSON parse error detail + */ + static showInvalidConfig(configPath, errorDetail) { + console.error(''); + console.error(colored('╔══════════════════════════════════════════════════════════╗', 'red')); + console.error(colored('║ ERROR: Configuration invalid ║', 'red')); + console.error(colored('╚══════════════════════════════════════════════════════════╝', 'red')); + console.error(''); + console.error(`File: ${configPath}`); + console.error(`Issue: ${errorDetail}`); + console.error(''); + console.error(colored('Fix (copy-paste):', 'yellow')); + console.error(' # Backup corrupted file'); + console.error(` mv ${configPath} ${configPath}.backup`); + console.error(''); + console.error(' # Reinstall CCS'); + console.error(' npm install -g @kaitranntt/ccs --force'); + console.error(''); + console.error('Your profile settings will be preserved.'); + console.error(''); + } + + /** + * Show profile not found error + * @param {string} profileName - Requested profile name + * @param {string[]} availableProfiles - List of available profiles + * @param {string} suggestion - Suggested profile name (fuzzy match) + */ + static showProfileNotFound(profileName, availableProfiles, suggestion = null) { + console.error(''); + console.error(colored('╔══════════════════════════════════════════════════════════╗', 'red')); + console.error(colored(`║ ERROR: Profile '${profileName}' not found${' '.repeat(Math.max(0, 35 - profileName.length))}║`, 'red')); + console.error(colored('╚══════════════════════════════════════════════════════════╝', 'red')); + console.error(''); + console.error(colored('Available profiles:', 'cyan')); + availableProfiles.forEach(line => console.error(` ${line}`)); + console.error(''); + console.error(colored('Fix:', 'yellow')); + console.error(' # Use existing profile'); + console.error(' ccs "your prompt"'); + console.error(''); + console.error(' # Create new account profile'); + console.error(' ccs auth create '); + console.error(''); + + if (suggestion) { + console.error(colored(`Did you mean: ${suggestion}`, 'yellow')); + console.error(''); + } + } + + /** + * Show permission denied error + * @param {string} path - Path with permission issue + */ + static showPermissionDenied(path) { + console.error(''); + console.error(colored('╔══════════════════════════════════════════════════════════╗', 'red')); + console.error(colored('║ ERROR: Permission denied ║', 'red')); + console.error(colored('╚══════════════════════════════════════════════════════════╝', 'red')); + console.error(''); + console.error(`Cannot write to: ${path}`); + console.error(''); + console.error(colored('Fix (copy-paste):', 'yellow')); + console.error(' # Fix ownership'); + console.error(' sudo chown -R $USER ~/.ccs ~/.claude'); + console.error(''); + console.error(' # Fix permissions'); + console.error(' chmod 755 ~/.ccs ~/.claude'); + console.error(''); + console.error(' # Retry installation'); + console.error(' npm install -g @kaitranntt/ccs --force'); + console.error(''); + } +} + +module.exports = { ErrorManager, ErrorTypes }; \ No newline at end of file diff --git a/bin/recovery-manager.js b/bin/recovery-manager.js new file mode 100644 index 00000000..a5f4cd8f --- /dev/null +++ b/bin/recovery-manager.js @@ -0,0 +1,135 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +/** + * Auto-recovery for missing or corrupted configuration + */ +class RecoveryManager { + constructor() { + this.homedir = os.homedir(); + this.ccsDir = path.join(this.homedir, '.ccs'); + this.claudeDir = path.join(this.homedir, '.claude'); + this.recovered = []; + } + + /** + * Ensure ~/.ccs/ directory exists + */ + ensureCcsDirectory() { + if (!fs.existsSync(this.ccsDir)) { + fs.mkdirSync(this.ccsDir, { recursive: true, mode: 0o755 }); + this.recovered.push('Created ~/.ccs/ directory'); + return true; + } + return false; + } + + /** + * Ensure ~/.ccs/config.json exists with defaults + */ + ensureConfigJson() { + const configPath = path.join(this.ccsDir, 'config.json'); + + // Check if exists and valid + if (fs.existsSync(configPath)) { + try { + const content = fs.readFileSync(configPath, 'utf8'); + JSON.parse(content); // Validate JSON + return false; // No recovery needed + } catch (e) { + // Corrupted - backup and recreate + const backupPath = `${configPath}.backup.${Date.now()}`; + fs.renameSync(configPath, backupPath); + this.recovered.push(`Backed up corrupted config.json to ${path.basename(backupPath)}`); + } + } + + // Create default config + const defaultConfig = { + profiles: { + glm: '~/.ccs/glm.settings.json', + kimi: '~/.ccs/kimi.settings.json', + default: '~/.claude/settings.json' + } + }; + + const tmpPath = `${configPath}.tmp`; + fs.writeFileSync(tmpPath, JSON.stringify(defaultConfig, null, 2) + '\n', 'utf8'); + fs.renameSync(tmpPath, configPath); + + this.recovered.push('Created ~/.ccs/config.json'); + return true; + } + + /** + * Ensure ~/.claude/settings.json exists + */ + ensureClaudeSettings() { + const claudeSettingsPath = path.join(this.claudeDir, 'settings.json'); + + // Create ~/.claude/ if missing + if (!fs.existsSync(this.claudeDir)) { + fs.mkdirSync(this.claudeDir, { recursive: true, mode: 0o755 }); + this.recovered.push('Created ~/.claude/ directory'); + } + + // Create settings.json if missing + if (!fs.existsSync(claudeSettingsPath)) { + const tmpPath = `${claudeSettingsPath}.tmp`; + fs.writeFileSync(tmpPath, '{}\n', 'utf8'); + fs.renameSync(tmpPath, claudeSettingsPath); + + this.recovered.push('Created ~/.claude/settings.json'); + return true; + } + + return false; + } + + /** + * Run all recovery operations + * @returns {boolean} True if any recovery performed + */ + recoverAll() { + this.recovered = []; + + this.ensureCcsDirectory(); + this.ensureConfigJson(); + this.ensureClaudeSettings(); + + return this.recovered.length > 0; + } + + /** + * Get recovery summary + * @returns {string[]} List of recovery actions performed + */ + getRecoverySummary() { + return this.recovered; + } + + /** + * Show recovery hints + */ + showRecoveryHints() { + if (this.recovered.length === 0) return; + + console.log(''); + console.log('[i] Auto-recovery completed:'); + this.recovered.forEach(msg => console.log(` - ${msg}`)); + + // Show login hint if created Claude settings + if (this.recovered.some(msg => msg.includes('settings.json'))) { + console.log(''); + console.log('[i] Next step: Login to Claude CLI'); + console.log(' Run: claude /login'); + } + + console.log(''); + } +} + +module.exports = RecoveryManager; \ No newline at end of file diff --git a/installers/install.ps1 b/installers/install.ps1 index 70aed8c2..991a992c 100644 --- a/installers/install.ps1 +++ b/installers/install.ps1 @@ -31,7 +31,7 @@ $InstallMethod = if ($ScriptDir -and ((Test-Path "$ScriptDir\lib\ccs.ps1") -or ( # IMPORTANT: Update this version when releasing new versions! # This hardcoded version is used for standalone installations (irm | iex) # For git installations, VERSION file is read if available -$CcsVersion = "3.0.0" +$CcsVersion = "3.0.1" # Try to read VERSION file for git installations if ($ScriptDir) { diff --git a/installers/install.sh b/installers/install.sh index eb3c55f1..c75b492f 100755 --- a/installers/install.sh +++ b/installers/install.sh @@ -32,7 +32,7 @@ fi # IMPORTANT: Update this version when releasing new versions! # This hardcoded version is used for standalone installations (curl | bash) # For git installations, VERSION file is read if available -CCS_VERSION="3.0.0" +CCS_VERSION="3.0.1" # Try to read VERSION file for git installations if [[ -f "$SCRIPT_DIR/VERSION" ]]; then diff --git a/lib/ccs b/lib/ccs index dc9c1094..591e1445 100755 --- a/lib/ccs +++ b/lib/ccs @@ -2,7 +2,7 @@ set -euo pipefail # Version (updated by scripts/bump-version.sh) -CCS_VERSION="3.0.0" +CCS_VERSION="3.0.1" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}" readonly PROFILES_JSON="$HOME/.ccs/profiles.json" @@ -44,14 +44,15 @@ show_help() { echo -e " ${YELLOW}ccs${RESET} [flags]" echo "" echo -e "${CYAN}Description:${RESET}" - echo -e " Switch between Claude accounts and models instantly." - echo -e " Supports concurrent multi-account sessions." + echo -e " Switch between multiple Claude accounts (work, personal, team) and" + echo -e " alternative models (GLM, Kimi) instantly. Concurrent sessions with" + echo -e " auto-recovery. Zero downtime." echo "" - echo -e "${CYAN}Profile Switching:${RESET}" - echo -e " ${YELLOW}ccs${RESET} Use default profile" - echo -e " ${YELLOW}ccs glm${RESET} Switch to GLM profile" - echo -e " ${YELLOW}ccs work${RESET} Switch to work account" - echo -e " ${YELLOW}ccs work${RESET} \"debug code\" Run command with work account" + echo -e "${CYAN}Model Switching:${RESET}" + echo -e " ${YELLOW}ccs${RESET} Use default Claude account" + echo -e " ${YELLOW}ccs glm${RESET} Switch to GLM 4.6 model" + echo -e " ${YELLOW}ccs kimi${RESET} Switch to Kimi for Coding" + echo -e " ${YELLOW}ccs glm${RESET} \"debug this code\" Use GLM and run command" echo "" echo -e "${CYAN}Account Management:${RESET}" echo -e " ${YELLOW}ccs auth create ${RESET} Create new account profile" @@ -59,6 +60,12 @@ show_help() { echo -e " ${YELLOW}ccs auth show ${RESET} Show profile details" echo -e " ${YELLOW}ccs auth remove ${RESET} Remove profile (requires --force)" echo -e " ${YELLOW}ccs auth default ${RESET} Set default profile" + echo -e " ${YELLOW}ccs work${RESET} Switch to work account" + echo -e " ${YELLOW}ccs personal${RESET} Switch to personal account" + echo -e " ${YELLOW}ccs work${RESET} \"review code\" Run command with work account" + echo "" + echo -e "${CYAN}Diagnostics:${RESET}" + echo -e " ${YELLOW}ccs doctor${RESET} Run health check and diagnostics" echo "" echo -e "${CYAN}Flags:${RESET}" echo -e " ${YELLOW}-h, --help${RESET} Show this help message" @@ -94,6 +101,210 @@ command -v jq &>/dev/null || { exit 1 } +# --- Auto-Recovery Functions --- + +ensure_ccs_directory() { + [[ -d "$HOME/.ccs" ]] && return 0 + + mkdir -p "$HOME/.ccs" 2>/dev/null || { + msg_error "Cannot create ~/.ccs/ directory. Check permissions." + return 1 + } + + echo "[i] Auto-recovery: Created ~/.ccs/ directory" + return 0 +} + +ensure_config_json() { + local config_file="$HOME/.ccs/config.json" + + # Check if exists and valid + if [[ -f "$config_file" ]]; then + jq empty "$config_file" 2>/dev/null && return 0 + + # Corrupted - backup and recreate + local backup_file="${config_file}.backup.$(date +%s)" + mv "$config_file" "$backup_file" 2>/dev/null + echo "[i] Auto-recovery: Backed up corrupted config.json" + fi + + # Create default config + cat > "$config_file" <<'EOF' +{ + "profiles": { + "glm": "~/.ccs/glm.settings.json", + "kimi": "~/.ccs/kimi.settings.json", + "default": "~/.claude/settings.json" + } +} +EOF + + echo "[i] Auto-recovery: Created ~/.ccs/config.json" + return 0 +} + +ensure_claude_settings() { + local claude_dir="$HOME/.claude" + local settings_file="$claude_dir/settings.json" + + # Create ~/.claude/ if missing + if [[ ! -d "$claude_dir" ]]; then + mkdir -p "$claude_dir" 2>/dev/null || return 1 + echo "[i] Auto-recovery: Created ~/.claude/ directory" + fi + + # Create settings.json if missing + if [[ ! -f "$settings_file" ]]; then + echo '{}' > "$settings_file" 2>/dev/null || return 1 + echo "[i] Auto-recovery: Created ~/.claude/settings.json" + echo "[i] Next step: Run 'claude /login' to authenticate" + return 0 + fi + + return 0 +} + +# Run auto-recovery +auto_recover() { + ensure_ccs_directory || return 1 + ensure_config_json || return 1 + ensure_claude_settings || return 1 + return 0 +} + +# --- Doctor Command --- + +doctor_check() { + local check_name="$1" + local status="$2" # success, warning, error + local message="${3:-}" + + case "$status" in + success) + echo -e "${GREEN}[OK]${RESET} $check_name" + ;; + warning) + echo -e "${YELLOW}[!]${RESET} $check_name${message:+: $message}" + ;; + error) + echo -e "${RED}[X]${RESET} $check_name: $message" + ;; + esac +} + +doctor_run() { + echo -e "${CYAN}Running CCS Health Check...${RESET}" + echo "" + + local has_errors=false + + # Check Claude CLI + if command -v "$(detect_claude_cli)" &>/dev/null; then + doctor_check "Claude CLI" "success" + else + doctor_check "Claude CLI" "error" "Not found in PATH" + has_errors=true + fi + + # Check ~/.ccs/ + if [[ -d "$HOME/.ccs" ]]; then + doctor_check "CCS Directory" "success" + else + doctor_check "CCS Directory" "error" "~/.ccs/ not found" + has_errors=true + fi + + # Check config.json + if [[ -f "$CONFIG_FILE" ]]; then + if jq empty "$CONFIG_FILE" 2>/dev/null; then + doctor_check "config.json" "success" + else + doctor_check "config.json" "error" "Invalid JSON" + has_errors=true + fi + else + doctor_check "config.json" "error" "Not found" + has_errors=true + fi + + # Check glm.settings.json + local glm_file="$HOME/.ccs/glm.settings.json" + if [[ -f "$glm_file" ]]; then + if jq empty "$glm_file" 2>/dev/null; then + doctor_check "glm.settings.json" "success" + else + doctor_check "glm.settings.json" "error" "Invalid JSON" + has_errors=true + fi + else + doctor_check "glm.settings.json" "error" "Not found" + has_errors=true + fi + + # Check kimi.settings.json + local kimi_file="$HOME/.ccs/kimi.settings.json" + if [[ -f "$kimi_file" ]]; then + if jq empty "$kimi_file" 2>/dev/null; then + doctor_check "kimi.settings.json" "success" + else + doctor_check "kimi.settings.json" "error" "Invalid JSON" + has_errors=true + fi + else + doctor_check "kimi.settings.json" "error" "Not found" + has_errors=true + fi + + # Check ~/.claude/settings.json + if [[ -f "$HOME/.claude/settings.json" ]]; then + if jq empty "$HOME/.claude/settings.json" 2>/dev/null; then + doctor_check "Claude Settings" "success" + else + doctor_check "Claude Settings" "warning" "Invalid JSON" + fi + else + doctor_check "Claude Settings" "warning" "Not found - run 'claude /login'" + fi + + # Check profiles + if [[ -f "$CONFIG_FILE" ]]; then + local profile_count=$(jq -r '.profiles | length' "$CONFIG_FILE" 2>/dev/null || echo "0") + doctor_check "Profiles" "success" "($profile_count configured)" + fi + + # Check instances + if [[ -d "$INSTANCES_DIR" ]]; then + local instance_count=$(find "$INSTANCES_DIR" -maxdepth 1 -type d 2>/dev/null | wc -l) + instance_count=$((instance_count - 1)) # Exclude parent dir + doctor_check "Instances" "success" "($instance_count account profiles)" + else + doctor_check "Instances" "success" "(no account profiles)" + fi + + # Check permissions + local test_file="$HOME/.ccs/.permission-test" + if echo "test" > "$test_file" 2>/dev/null; then + rm -f "$test_file" 2>/dev/null + doctor_check "Permissions" "success" + else + doctor_check "Permissions" "error" "Cannot write to ~/.ccs/" + has_errors=true + fi + + # Summary + echo "" + echo -e "${CYAN}═══════════════════════════════════════════${RESET}" + if $has_errors; then + echo -e "${RED}Status: Installation has errors${RESET}" + echo "Run: npm install -g @kaitranntt/ccs --force" + else + echo -e "${GREEN}✓ All checks passed!${RESET}" + fi + echo "" + + $has_errors && exit 1 || exit 0 +} + # --- Claude CLI Detection Logic --- detect_claude_cli() { @@ -727,6 +938,18 @@ if [[ $# -gt 0 ]] && [[ "${1}" == "auth" ]]; then exit $? fi +# Special case: doctor command +if [[ $# -gt 0 ]] && [[ "${1}" == "doctor" || "${1}" == "--doctor" ]]; then + doctor_run + exit $? +fi + +# Run auto-recovery before main logic +auto_recover || { + msg_error "Auto-recovery failed. Check permissions." + exit 1 +} + # Smart profile detection: if first arg starts with '-', it's a flag not a profile if [[ $# -eq 0 ]] || [[ "${1}" =~ ^- ]]; then # No args or first arg is a flag → use default profile diff --git a/lib/ccs.ps1 b/lib/ccs.ps1 index 26c0e857..01144317 100644 --- a/lib/ccs.ps1 +++ b/lib/ccs.ps1 @@ -12,7 +12,7 @@ param( $ErrorActionPreference = "Stop" # Version (updated by scripts/bump-version.sh) -$CcsVersion = "3.0.0" +$CcsVersion = "3.0.1" $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $ConfigFile = if ($env:CCS_CONFIG) { $env:CCS_CONFIG } else { "$env:USERPROFILE\.ccs\config.json" } $ProfilesJson = "$env:USERPROFILE\.ccs\profiles.json" @@ -100,14 +100,15 @@ function Show-Help { Write-ColorLine " ccs [flags]" "Yellow" Write-Host "" Write-ColorLine "Description:" "Cyan" - Write-Host " Switch between Claude accounts and models instantly." - Write-Host " Supports concurrent multi-account sessions." + Write-Host " Switch between multiple Claude accounts (work, personal, team) and" + Write-Host " alternative models (GLM, Kimi) instantly. Concurrent sessions with" + Write-Host " auto-recovery. Zero downtime." Write-Host "" - Write-ColorLine "Profile Switching:" "Cyan" - Write-ColorLine " ccs Use default profile" "Yellow" - Write-ColorLine " ccs glm Switch to GLM profile" "Yellow" - Write-ColorLine " ccs work Switch to work account" "Yellow" - Write-ColorLine " ccs work 'debug code' Run command with work account" "Yellow" + Write-ColorLine "Model Switching:" "Cyan" + Write-ColorLine " ccs Use default Claude account" "Yellow" + Write-ColorLine " ccs glm Switch to GLM 4.6 model" "Yellow" + Write-ColorLine " ccs kimi Switch to Kimi for Coding" "Yellow" + Write-ColorLine " ccs glm 'debug this code' Use GLM and run command" "Yellow" Write-Host "" Write-ColorLine "Account Management:" "Cyan" Write-ColorLine " ccs auth create Create new account profile" "Yellow" @@ -115,6 +116,12 @@ function Show-Help { Write-ColorLine " ccs auth show Show profile details" "Yellow" Write-ColorLine " ccs auth remove Remove profile (requires --force)" "Yellow" Write-ColorLine " ccs auth default Set default profile" "Yellow" + Write-ColorLine " ccs work Switch to work account" "Yellow" + Write-ColorLine " ccs personal Switch to personal account" "Yellow" + Write-ColorLine " ccs work 'review code' Run command with work account" "Yellow" + Write-Host "" + Write-ColorLine "Diagnostics:" "Cyan" + Write-ColorLine " ccs doctor Run health check and diagnostics" "Yellow" Write-Host "" Write-ColorLine "Flags:" "Cyan" Write-ColorLine " -h, --help Show this help message" "Yellow" @@ -202,6 +209,79 @@ function Show-Version { } } +# --- Auto-Recovery Functions --- + +function Ensure-CcsDirectory { + if (Test-Path "$env:USERPROFILE\.ccs") { return $true } + + try { + New-Item -ItemType Directory -Path "$env:USERPROFILE\.ccs" -Force | Out-Null + Write-Host "[i] Auto-recovery: Created ~/.ccs/ directory" + return $true + } catch { + Write-ErrorMsg "Cannot create ~/.ccs/ directory. Check permissions." + return $false + } +} + +function Ensure-ConfigJson { + $ConfigFile = "$env:USERPROFILE\.ccs\config.json" + + # Check if exists and valid + if (Test-Path $ConfigFile) { + try { + Get-Content $ConfigFile -Raw | ConvertFrom-Json | Out-Null + return $true + } catch { + # Corrupted - backup and recreate + $BackupFile = "$ConfigFile.backup.$(Get-Date -Format 'yyyyMMddHHmmss')" + Move-Item $ConfigFile $BackupFile -Force + Write-Host "[i] Auto-recovery: Backed up corrupted config.json" + } + } + + # Create default config + $DefaultConfig = @{ + profiles = @{ + glm = "~/.ccs/glm.settings.json" + kimi = "~/.ccs/kimi.settings.json" + default = "~/.claude/settings.json" + } + } + + $DefaultConfig | ConvertTo-Json -Depth 10 | Set-Content $ConfigFile + Write-Host "[i] Auto-recovery: Created ~/.ccs/config.json" + return $true +} + +function Ensure-ClaudeSettings { + $ClaudeDir = "$env:USERPROFILE\.claude" + $SettingsFile = "$ClaudeDir\settings.json" + + # Create ~/.claude/ if missing + if (-not (Test-Path $ClaudeDir)) { + New-Item -ItemType Directory -Path $ClaudeDir -Force | Out-Null + Write-Host "[i] Auto-recovery: Created ~/.claude/ directory" + } + + # Create settings.json if missing + if (-not (Test-Path $SettingsFile)) { + '{}' | Set-Content $SettingsFile + Write-Host "[i] Auto-recovery: Created ~/.claude/settings.json" + Write-Host "[i] Next step: Run 'claude /login' to authenticate" + return $true + } + + return $false +} + +function Invoke-AutoRecovery { + if (-not (Ensure-CcsDirectory)) { return $false } + if (-not (Ensure-ConfigJson)) { return $false } + Ensure-ClaudeSettings | Out-Null + return $true +} + # --- Profile Registry Functions (Phase 4) --- function Initialize-ProfilesJson { @@ -830,6 +910,12 @@ if ($RemainingArgs.Count -gt 0 -and $RemainingArgs[0] -eq "auth") { exit $LASTEXITCODE } +# Run auto-recovery before main logic +if (-not (Invoke-AutoRecovery)) { + Write-ErrorMsg "Auto-recovery failed. Check permissions." + exit 1 +} + # Smart profile detection: if first arg starts with '-', it's a flag not a profile if ($RemainingArgs.Count -eq 0 -or $RemainingArgs[0] -match '^-') { # No args or first arg is a flag → use default profile diff --git a/package.json b/package.json index a3547e9f..d540f3a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "3.0.0", + "version": "3.0.1", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", diff --git a/scripts/postinstall.js b/scripts/postinstall.js index 9da7cf0f..ff41be1f 100755 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -14,6 +14,52 @@ const os = require('os'); * Cross-platform: Works on Unix, macOS, Windows */ +/** + * Validate created configuration files + * @returns {object} { success: boolean, errors: string[], warnings: string[] } + */ +function validateConfiguration() { + const homedir = os.homedir(); + const errors = []; + const warnings = []; + + // Check ~/.ccs/ directory + const ccsDir = path.join(homedir, '.ccs'); + if (!fs.existsSync(ccsDir)) { + errors.push('~/.ccs/ directory not found'); + } + + // Check required files + const requiredFiles = [ + { path: path.join(ccsDir, 'config.json'), name: 'config.json' }, + { path: path.join(ccsDir, 'glm.settings.json'), name: 'glm.settings.json' }, + { path: path.join(ccsDir, 'kimi.settings.json'), name: 'kimi.settings.json' } + ]; + + for (const file of requiredFiles) { + if (!fs.existsSync(file.path)) { + errors.push(`${file.name} not found`); + continue; + } + + // Validate JSON syntax + try { + const content = fs.readFileSync(file.path, 'utf8'); + JSON.parse(content); + } catch (e) { + errors.push(`${file.name} has invalid JSON: ${e.message}`); + } + } + + // Check ~/.claude/settings.json (warning only, not critical) + const claudeSettings = path.join(homedir, '.claude', 'settings.json'); + if (!fs.existsSync(claudeSettings)) { + warnings.push('~/.claude/settings.json not found - run "claude /login"'); + } + + return { success: errors.length === 0, errors, warnings }; +} + function createConfigFiles() { try { // Get user home directory (cross-platform) @@ -107,23 +153,76 @@ function createConfigFiles() { console.log('[OK] Kimi profile exists: ~/.ccs/kimi.settings.json (preserved)'); } + // Create ~/.claude/settings.json if missing (NEW) + const claudeDir = path.join(homedir, '.claude'); + const claudeSettingsPath = path.join(claudeDir, 'settings.json'); + + if (!fs.existsSync(claudeDir)) { + fs.mkdirSync(claudeDir, { recursive: true, mode: 0o755 }); + console.log('[OK] Created directory: ~/.claude/'); + } + + if (!fs.existsSync(claudeSettingsPath)) { + // Create empty settings (matches Claude CLI behavior) + const tmpPath = `${claudeSettingsPath}.tmp`; + fs.writeFileSync(tmpPath, '{}\n', 'utf8'); + fs.renameSync(tmpPath, claudeSettingsPath); + + console.log('[OK] Created default settings: ~/.claude/settings.json'); + console.log(''); + console.log(' [i] Configure Claude CLI:'); + console.log(' Run: claude /login'); + console.log(''); + } else { + console.log('[OK] Claude settings exist: ~/.claude/settings.json (preserved)'); + } + + // Validate configuration + console.log(''); + console.log('[i] Validating configuration...'); + const validation = validateConfiguration(); + + if (!validation.success) { + console.error(''); + console.error('[X] Configuration validation failed:'); + validation.errors.forEach(err => console.error(` - ${err}`)); + console.error(''); + throw new Error('Configuration incomplete'); + } + + // Show warnings (non-critical) + if (validation.warnings.length > 0) { + console.warn(''); + console.warn('[!] Warnings:'); + validation.warnings.forEach(warn => console.warn(` - ${warn}`)); + } + console.log(''); console.log('[OK] CCS configuration ready!'); console.log(' Run: ccs --version'); } catch (err) { - // Silent failure: don't break npm install - console.warn(''); - console.warn('[!] Could not auto-create CCS configuration'); - console.warn(` Error: ${err.message}`); - console.warn(''); - console.warn(' Manual setup:'); - console.warn(' mkdir -p ~/.ccs'); - console.warn(' # See: https://github.com/kaitranntt/ccs#configuration'); - console.warn(''); + // Show error details + console.error(''); + console.error('[X] CCS configuration failed'); + console.error(` Error: ${err.message}`); + console.error(''); + console.error('Recovery steps:'); + console.error(' 1. Create directory manually:'); + console.error(' mkdir -p ~/.ccs ~/.claude'); + console.error(''); + console.error(' 2. Create empty settings:'); + console.error(' echo "{}" > ~/.claude/settings.json'); + console.error(''); + console.error(' 3. Retry installation:'); + console.error(' npm install -g @kaitranntt/ccs --force'); + console.error(''); + console.error(' 4. If issue persists, report at:'); + console.error(' https://github.com/kaitranntt/ccs/issues'); + console.error(''); - // Don't exit with error code - allow npm install to succeed - process.exit(0); + // Exit with error code (npm will show warning) + process.exit(1); } }