diff --git a/VERSION b/VERSION index 6aba2b24..80895903 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.2.0 +4.3.0 diff --git a/bin/ccs.js b/bin/ccs.js index d477c09b..05673384 100755 --- a/bin/ccs.js +++ b/bin/ccs.js @@ -271,17 +271,27 @@ async function handleDoctorCommand() { } 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(''); + // Then, create symlinks from ~/.ccs/.claude/ to ~/.claude/ const ClaudeSymlinkManager = require('./utils/claude-symlink-manager'); const manager = new ClaudeSymlinkManager(); + manager.install(false); - console.log('[i] Syncing delegation commands and skills to ~/.claude/...'); - manager.sync(); + console.log(''); + console.log(colored('[OK] Sync complete!', 'green')); + console.log(''); process.exit(0); } diff --git a/bin/management/doctor.js b/bin/management/doctor.js index 735dd924..e0f7f846 100644 --- a/bin/management/doctor.js +++ b/bin/management/doctor.js @@ -6,6 +6,8 @@ const os = require('os'); const { spawn } = require('child_process'); const { colored } = require('../utils/helpers'); const { detectClaudeCli } = require('../utils/claude-detector'); +const ora = require('ora'); +const Table = require('cli-table3'); /** * Health check results @@ -15,13 +17,19 @@ class HealthCheck { this.checks = []; this.warnings = []; this.errors = []; + this.details = {}; // Store detailed information for summary table } - addCheck(name, status, message = '', fix = null) { + addCheck(name, status, message = '', fix = null, details = 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 }); + + // Store details for summary table + if (details) { + this.details[name] = details; + } } hasErrors() { @@ -46,6 +54,13 @@ class Doctor { this.ccsDir = path.join(this.homedir, '.ccs'); this.claudeDir = path.join(this.homedir, '.claude'); this.results = new HealthCheck(); + + // Get CCS version + try { + this.ccsVersion = require('../../package.json').version; + } catch (e) { + this.ccsVersion = 'unknown'; + } } /** @@ -55,15 +70,33 @@ class Doctor { console.log(colored('Running CCS Health Check...', 'cyan')); console.log(''); + // Store CCS version in details + this.results.details['CCS Version'] = { status: 'OK', info: `v${this.ccsVersion}` }; + + // Group 1: System + console.log(colored('System:', 'bold')); await this.checkClaudeCli(); this.checkCcsDirectory(); + console.log(''); + + // Group 2: Configuration + console.log(colored('Configuration:', 'bold')); this.checkConfigFiles(); this.checkClaudeSettings(); + console.log(''); + + // Group 3: Profiles & Delegation + console.log(colored('Profiles & Delegation:', 'bold')); this.checkProfiles(); this.checkInstances(); this.checkDelegation(); + console.log(''); + + // Group 4: System Health + console.log(colored('System Health:', 'bold')); this.checkPermissions(); this.checkCcsSymlinks(); + console.log(''); this.showReport(); return this.results; @@ -73,7 +106,7 @@ class Doctor { * Check 1: Claude CLI availability */ async checkClaudeCli() { - process.stdout.write('[?] Checking Claude CLI... '); + const spinner = ora('Checking Claude CLI').start(); const claudeCli = detectClaudeCli(); @@ -97,15 +130,23 @@ class Doctor { child.on('error', reject); }); - console.log(colored('[OK]', 'green')); - this.results.addCheck('Claude CLI', 'success', `Found: ${claudeCli}`); + // Extract version from output + const versionMatch = result.match(/(\d+\.\d+\.\d+)/); + const version = versionMatch ? versionMatch[1] : 'unknown'; + + spinner.succeed(` ${'Claude CLI'.padEnd(26)}${colored('[OK]', 'green')} ${claudeCli} (v${version})`); + this.results.addCheck('Claude CLI', 'success', `Found: ${claudeCli}`, null, { + status: 'OK', + info: `v${version} (${claudeCli})` + }); } catch (err) { - console.log(colored('[X]', 'red')); + spinner.fail(` ${'Claude CLI'.padEnd(26)}${colored('[X]', 'red')} Not found or not working`); this.results.addCheck( 'Claude CLI', 'error', 'Claude CLI not found or not working', - 'Install from: https://docs.claude.com/en/docs/claude-code/installation' + 'Install from: https://docs.claude.com/en/docs/claude-code/installation', + { status: 'ERROR', info: 'Not installed' } ); } } @@ -114,18 +155,22 @@ class Doctor { * Check 2: ~/.ccs/ directory */ checkCcsDirectory() { - process.stdout.write('[?] Checking ~/.ccs/ directory... '); + const spinner = ora('Checking ~/.ccs/ directory').start(); if (fs.existsSync(this.ccsDir)) { - console.log(colored('[OK]', 'green')); - this.results.addCheck('CCS Directory', 'success'); + spinner.succeed(` ${'CCS Directory'.padEnd(26)}${colored('[OK]', 'green')} ~/.ccs/`); + this.results.addCheck('CCS Directory', 'success', null, null, { + status: 'OK', + info: '~/.ccs/' + }); } else { - console.log(colored('[X]', 'red')); + spinner.fail(` ${'CCS Directory'.padEnd(26)}${colored('[X]', 'red')} Not found`); this.results.addCheck( 'CCS Directory', 'error', '~/.ccs/ directory not found', - 'Run: npm install -g @kaitranntt/ccs --force' + 'Run: npm install -g @kaitranntt/ccs --force', + { status: 'ERROR', info: 'Not found' } ); } } @@ -135,21 +180,24 @@ class Doctor { */ 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' } + { path: path.join(this.ccsDir, 'config.json'), name: 'config.json', key: 'config.json' }, + { path: path.join(this.ccsDir, 'glm.settings.json'), name: 'glm.settings.json', key: 'GLM Settings', profile: 'glm' }, + { path: path.join(this.ccsDir, 'kimi.settings.json'), name: 'kimi.settings.json', key: 'Kimi Settings', profile: 'kimi' } ]; + const { DelegationValidator } = require('../utils/delegation-validator'); + for (const file of files) { - process.stdout.write(`[?] Checking ${file.name}... `); + const spinner = ora(`Checking ${file.name}`).start(); if (!fs.existsSync(file.path)) { - console.log(colored('[X]', 'red')); + spinner.fail(` ${file.name.padEnd(26)}${colored('[X]', 'red')} Not found`); this.results.addCheck( file.name, 'error', `${file.name} not found`, - 'Run: npm install -g @kaitranntt/ccs --force' + 'Run: npm install -g @kaitranntt/ccs --force', + { status: 'ERROR', info: 'Not found' } ); continue; } @@ -157,16 +205,48 @@ class Doctor { // Validate JSON try { const content = fs.readFileSync(file.path, 'utf8'); - JSON.parse(content); - console.log(colored('[OK]', 'green')); - this.results.addCheck(file.name, 'success'); + const config = JSON.parse(content); + + // Extract useful info based on file type + let info = 'Valid'; + let status = 'OK'; + + if (file.profile) { + // For settings files, check if API key is configured + const validation = DelegationValidator.validate(file.profile); + + if (validation.valid) { + info = 'Key configured'; + status = 'OK'; + } else if (validation.error && validation.error.includes('placeholder')) { + info = 'Placeholder key (not configured)'; + status = 'WARN'; + } else { + info = 'Valid JSON'; + status = 'OK'; + } + } + + const statusIcon = status === 'OK' ? colored('[OK]', 'green') : colored('[!]', 'yellow'); + + if (status === 'WARN') { + spinner.warn(` ${file.name.padEnd(26)}${statusIcon} ${info}`); + } else { + spinner.succeed(` ${file.name.padEnd(26)}${statusIcon} ${info}`); + } + + this.results.addCheck(file.name, status === 'OK' ? 'success' : 'warning', null, null, { + status: status, + info: info + }); } catch (e) { - console.log(colored('[X]', 'red')); + spinner.fail(` ${file.name.padEnd(26)}${colored('[X]', 'red')} Invalid JSON`); 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` + `Backup and recreate: mv ${file.path} ${file.path}.backup && npm install -g @kaitranntt/ccs --force`, + { status: 'ERROR', info: 'Invalid JSON' } ); } } @@ -176,12 +256,11 @@ class Doctor { * Check 4: Claude settings */ checkClaudeSettings() { - process.stdout.write('[?] Checking ~/.claude/settings.json... '); - + const spinner = ora('Checking ~/.claude/settings.json').start(); const settingsPath = path.join(this.claudeDir, 'settings.json'); if (!fs.existsSync(settingsPath)) { - console.log(colored('[!]', 'yellow')); + spinner.warn(` ${'~/.claude/settings.json'.padEnd(26)}${colored('[!]', 'yellow')} Not found`); this.results.addCheck( 'Claude Settings', 'warning', @@ -195,10 +274,10 @@ class Doctor { try { const content = fs.readFileSync(settingsPath, 'utf8'); JSON.parse(content); - console.log(colored('[OK]', 'green')); + spinner.succeed(` ${'~/.claude/settings.json'.padEnd(26)}${colored('[OK]', 'green')}`); this.results.addCheck('Claude Settings', 'success'); } catch (e) { - console.log(colored('[!]', 'yellow')); + spinner.warn(` ${'~/.claude/settings.json'.padEnd(26)}${colored('[!]', 'yellow')} Invalid JSON`); this.results.addCheck( 'Claude Settings', 'warning', @@ -212,11 +291,11 @@ class Doctor { * Check 5: Profile configurations */ checkProfiles() { - process.stdout.write('[?] Checking profiles... '); - + const spinner = ora('Checking profiles').start(); const configPath = path.join(this.ccsDir, 'config.json'); + if (!fs.existsSync(configPath)) { - console.log(colored('[SKIP]', 'yellow')); + spinner.info(` ${'Profiles'.padEnd(26)}${colored('[SKIP]', 'cyan')} config.json not found`); return; } @@ -224,22 +303,31 @@ class Doctor { const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); if (!config.profiles || typeof config.profiles !== 'object') { - console.log(colored('[X]', 'red')); + spinner.fail(` ${'Profiles'.padEnd(26)}${colored('[X]', 'red')} Missing profiles object`); this.results.addCheck( 'Profiles', 'error', 'config.json missing profiles object', - 'Run: npm install -g @kaitranntt/ccs --force' + 'Run: npm install -g @kaitranntt/ccs --force', + { status: 'ERROR', info: 'Missing profiles object' } ); return; } const profileCount = Object.keys(config.profiles).length; - console.log(colored('[OK]', 'green'), `(${profileCount} profiles)`); - this.results.addCheck('Profiles', 'success', `${profileCount} profiles configured`); + const profileNames = Object.keys(config.profiles).join(', '); + + spinner.succeed(` ${'Profiles'.padEnd(26)}${colored('[OK]', 'green')} ${profileCount} configured (${profileNames})`); + this.results.addCheck('Profiles', 'success', `${profileCount} profiles configured`, null, { + status: 'OK', + info: `${profileCount} configured (${profileNames.length > 30 ? profileNames.substring(0, 27) + '...' : profileNames})` + }); } catch (e) { - console.log(colored('[X]', 'red')); - this.results.addCheck('Profiles', 'error', e.message); + spinner.fail(` ${'Profiles'.padEnd(26)}${colored('[X]', 'red')} ${e.message}`); + this.results.addCheck('Profiles', 'error', e.message, null, { + status: 'ERROR', + info: e.message + }); } } @@ -247,11 +335,11 @@ class Doctor { * Check 6: Instance directories (account-based profiles) */ checkInstances() { - process.stdout.write('[?] Checking instances... '); - + const spinner = ora('Checking instances').start(); const instancesDir = path.join(this.ccsDir, 'instances'); + if (!fs.existsSync(instancesDir)) { - console.log(colored('[i]', 'cyan'), '(no account profiles)'); + spinner.info(` ${'Instances'.padEnd(26)}${colored('[i]', 'cyan')} No account profiles`); this.results.addCheck('Instances', 'success', 'No account profiles configured'); return; } @@ -261,12 +349,12 @@ class Doctor { }); if (instances.length === 0) { - console.log(colored('[i]', 'cyan'), '(no account profiles)'); + spinner.info(` ${'Instances'.padEnd(26)}${colored('[i]', 'cyan')} No account profiles`); this.results.addCheck('Instances', 'success', 'No account profiles'); return; } - console.log(colored('[OK]', 'green'), `(${instances.length} instances)`); + spinner.succeed(` ${'Instances'.padEnd(26)}${colored('[OK]', 'green')} ${instances.length} account profiles`); this.results.addCheck('Instances', 'success', `${instances.length} account profiles`); } @@ -274,7 +362,7 @@ class Doctor { * Check 7: Delegation system */ checkDelegation() { - process.stdout.write('[?] Checking delegation... '); + const spinner = ora('Checking delegation').start(); // Check if delegation commands exist in ~/.ccs/.claude/commands/ccs/ const ccsClaudeCommandsDir = path.join(this.ccsDir, '.claude', 'commands', 'ccs'); @@ -282,12 +370,13 @@ class Doctor { const hasKimiCommand = fs.existsSync(path.join(ccsClaudeCommandsDir, 'kimi.md')); if (!hasGlmCommand || !hasKimiCommand) { - console.log(colored('[!]', 'yellow'), '(not installed)'); + spinner.warn(` ${'Delegation'.padEnd(26)}${colored('[!]', 'yellow')} Not installed`); this.results.addCheck( 'Delegation', 'warning', 'Delegation commands not found', - 'Install with: npm install -g @kaitranntt/ccs --force' + 'Install with: npm install -g @kaitranntt/ccs --force', + { status: 'WARN', info: 'Not installed' } ); return; } @@ -304,21 +393,24 @@ class Doctor { } if (readyProfiles.length === 0) { - console.log(colored('[!]', 'yellow'), '(no profiles ready)'); + spinner.warn(` ${'Delegation'.padEnd(26)}${colored('[!]', 'yellow')} No profiles ready`); this.results.addCheck( 'Delegation', 'warning', 'Delegation installed but no profiles configured', - 'Configure profiles with valid API keys (not placeholders)' + 'Configure profiles with valid API keys (not placeholders)', + { status: 'WARN', info: 'No profiles ready' } ); return; } - console.log(colored('[OK]', 'green'), `(${readyProfiles.join(', ')} ready)`); + spinner.succeed(` ${'Delegation'.padEnd(26)}${colored('[OK]', 'green')} ${readyProfiles.length} profiles ready (${readyProfiles.join(', ')})`); this.results.addCheck( 'Delegation', 'success', - `${readyProfiles.length} profile(s) ready: ${readyProfiles.join(', ')}` + `${readyProfiles.length} profile(s) ready: ${readyProfiles.join(', ')}`, + null, + { status: 'OK', info: `${readyProfiles.length} profiles ready` } ); } @@ -326,22 +418,25 @@ class Doctor { * Check 8: File permissions */ checkPermissions() { - process.stdout.write('[?] Checking permissions... '); - + const spinner = ora('Checking permissions').start(); 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'); + spinner.succeed(` ${'Permissions'.padEnd(26)}${colored('[OK]', 'green')} Write access verified`); + this.results.addCheck('Permissions', 'success', null, null, { + status: 'OK', + info: 'Write access verified' + }); } catch (e) { - console.log(colored('[X]', 'red')); + spinner.fail(` ${'Permissions'.padEnd(26)}${colored('[X]', 'red')} Cannot write to ~/.ccs/`); this.results.addCheck( 'Permissions', 'error', 'Cannot write to ~/.ccs/', - 'Fix: sudo chown -R $USER ~/.ccs ~/.claude && chmod 755 ~/.ccs ~/.claude' + 'Fix: sudo chown -R $USER ~/.ccs ~/.claude && chmod 755 ~/.ccs ~/.claude', + { status: 'ERROR', info: 'Cannot write to ~/.ccs/' } ); } } @@ -350,7 +445,7 @@ class Doctor { * Check 9: CCS symlinks to ~/.claude/ */ checkCcsSymlinks() { - process.stdout.write('[?] Checking CCS symlinks... '); + const spinner = ora('Checking CCS symlinks').start(); try { const ClaudeSymlinkManager = require('../utils/claude-symlink-manager'); @@ -358,24 +453,30 @@ class Doctor { const health = manager.checkHealth(); if (health.healthy) { - console.log(colored('[OK]', 'green')); - this.results.addCheck('CCS Symlinks', 'success', 'All CCS items properly symlinked'); + const itemCount = manager.ccsItems.length; + spinner.succeed(` ${'CCS Symlinks'.padEnd(26)}${colored('[OK]', 'green')} ${itemCount}/${itemCount} items linked`); + this.results.addCheck('CCS Symlinks', 'success', 'All CCS items properly symlinked', null, { + status: 'OK', + info: `${itemCount}/${itemCount} items synced` + }); } else { - console.log(colored('[!]', 'yellow')); + spinner.warn(` ${'CCS Symlinks'.padEnd(26)}${colored('[!]', 'yellow')} ${health.issues.length} issues found`); this.results.addCheck( 'CCS Symlinks', 'warning', health.issues.join(', '), - 'Run: ccs sync' + 'Run: ccs sync', + { status: 'WARN', info: `${health.issues.length} issues` } ); } } catch (e) { - console.log(colored('[!]', 'yellow')); + spinner.warn(` ${'CCS Symlinks'.padEnd(26)}${colored('[!]', 'yellow')} Could not check`); this.results.addCheck( 'CCS Symlinks', 'warning', 'Could not check CCS symlinks: ' + e.message, - 'Run: ccs sync' + 'Run: ccs sync', + { status: 'WARN', info: 'Could not check' } ); } } @@ -385,20 +486,43 @@ class Doctor { */ 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; + // Calculate exact table width to match header bars + // colWidths: [20, 10, 35] = 65 + borders (4) = 69 total + const tableWidth = 69; + const headerBar = '═'.repeat(tableWidth); + + console.log(colored(headerBar, 'cyan')); + console.log(colored(' Health Check Summary', 'bold')); + console.log(colored(headerBar, 'cyan')); + + // Create summary table with detailed information + const table = new Table({ + head: [colored('Component', 'cyan'), colored('Status', 'cyan'), colored('Details', 'cyan')], + colWidths: [20, 10, 35], + wordWrap: true, + chars: { + 'top': '═', 'top-mid': '╤', 'top-left': '╔', 'top-right': '╗', + 'bottom': '═', 'bottom-mid': '╧', 'bottom-left': '╚', 'bottom-right': '╝', + 'left': '║', 'left-mid': '╟', 'mid': '─', 'mid-mid': '┼', + 'right': '║', 'right-mid': '╢', 'middle': '│' + } + }); + + // Populate table with collected details + for (const [component, detail] of Object.entries(this.results.details)) { + const statusColor = detail.status === 'OK' ? 'green' : detail.status === 'ERROR' ? 'red' : 'yellow'; + table.push([ + component, + colored(detail.status, statusColor), + detail.info || '' + ]); } - // Show errors + console.log(table.toString()); + console.log(''); + + // Show errors and warnings if present if (this.results.hasErrors()) { console.log(colored('Errors:', 'red')); this.results.errors.forEach(err => { @@ -410,7 +534,6 @@ class Doctor { console.log(''); } - // Show warnings if (this.results.hasWarnings()) { console.log(colored('Warnings:', 'yellow')); this.results.warnings.forEach(warn => { @@ -422,12 +545,14 @@ class Doctor { console.log(''); } - // Summary - if (this.results.hasErrors()) { - console.log(colored('Status: Installation has errors', 'red')); + // Final status + if (this.results.isHealthy() && !this.results.hasWarnings()) { + console.log(colored('[OK] All checks passed! Your CCS installation is healthy.', 'green')); + } else if (this.results.hasErrors()) { + console.log(colored('[X] 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(colored('[OK] Status: Installation healthy (warnings only)', 'green')); } console.log(''); diff --git a/bin/utils/claude-dir-installer.js b/bin/utils/claude-dir-installer.js index 7d57a1ef..5f1b2cf7 100644 --- a/bin/utils/claude-dir-installer.js +++ b/bin/utils/claude-dir-installer.js @@ -4,6 +4,8 @@ const fs = require('fs'); const path = require('path'); const os = require('os'); +const ora = require('ora'); +const { colored } = require('./helpers'); /** * ClaudeDirInstaller - Manages copying .claude/ directory from package to ~/.ccs/.claude/ @@ -18,8 +20,11 @@ class ClaudeDirInstaller { /** * Copy .claude/ directory from package to ~/.ccs/.claude/ * @param {string} packageDir - Package installation directory (default: auto-detect) + * @param {boolean} silent - Suppress spinner output */ - install(packageDir) { + install(packageDir, silent = false) { + const spinner = silent ? null : ora('Copying .claude/ items to ~/.ccs/.claude/').start(); + try { // Auto-detect package directory if not provided if (!packageDir) { @@ -30,21 +35,29 @@ class ClaudeDirInstaller { const packageClaudeDir = path.join(packageDir, '.claude'); if (!fs.existsSync(packageClaudeDir)) { - console.log('[!] Package .claude/ directory not found'); - console.log(` Searched in: ${packageClaudeDir}`); - console.log(' This may be a development installation'); + const msg = 'Package .claude/ directory not found'; + if (spinner) { + spinner.warn(`[!] ${msg}`); + console.log(` Searched in: ${packageClaudeDir}`); + console.log(' This may be a development installation'); + } else { + console.log(`[!] ${msg}`); + console.log(` Searched in: ${packageClaudeDir}`); + console.log(' This may be a development installation'); + } return false; } - console.log('[i] Installing CCS .claude/ items...'); - // Remove old version before copying new one if (fs.existsSync(this.ccsClaudeDir)) { + if (spinner) spinner.text = 'Removing old .claude/ items...'; fs.rmSync(this.ccsClaudeDir, { recursive: true, force: true }); } // Use fs.cpSync for recursive copy (Node.js 16.7.0+) // Fallback to manual copy for older Node.js versions + if (spinner) spinner.text = 'Copying .claude/ items...'; + if (fs.cpSync) { fs.cpSync(packageClaudeDir, this.ccsClaudeDir, { recursive: true }); } else { @@ -52,11 +65,25 @@ class ClaudeDirInstaller { this._copyDirRecursive(packageClaudeDir, this.ccsClaudeDir); } - console.log('[OK] Copied .claude/ items to ~/.ccs/.claude/'); + // Count files and directories + const itemCount = this._countItems(this.ccsClaudeDir); + const msg = `Copied .claude/ items (${itemCount.files} files, ${itemCount.dirs} directories)`; + + if (spinner) { + spinner.succeed(colored('[OK]', 'green') + ` ${msg}`); + } else { + console.log(`[OK] ${msg}`); + } return true; } catch (err) { - console.warn('[!] Failed to copy .claude/ directory:', err.message); - console.warn(' CCS items may not be available'); + const msg = `Failed to copy .claude/ directory: ${err.message}`; + if (spinner) { + spinner.fail(colored('[!]', 'yellow') + ` ${msg}`); + console.warn(' CCS items may not be available'); + } else { + console.warn(`[!] ${msg}`); + console.warn(' CCS items may not be available'); + } return false; } } @@ -90,6 +117,37 @@ class ClaudeDirInstaller { } } + /** + * Count files and directories in a path + * @param {string} dirPath - Directory to count + * @returns {Object} { files: number, dirs: number } + * @private + */ + _countItems(dirPath) { + let files = 0; + let dirs = 0; + + const countRecursive = (p) => { + const entries = fs.readdirSync(p, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) { + dirs++; + countRecursive(path.join(p, entry.name)); + } else { + files++; + } + } + }; + + try { + countRecursive(dirPath); + } catch (e) { + // Ignore errors + } + + return { files, dirs }; + } + /** * Check if ~/.ccs/.claude/ exists and is valid * @returns {boolean} True if directory exists diff --git a/bin/utils/claude-symlink-manager.js b/bin/utils/claude-symlink-manager.js index 103dbb49..39ad2d3f 100644 --- a/bin/utils/claude-symlink-manager.js +++ b/bin/utils/claude-symlink-manager.js @@ -3,6 +3,8 @@ const fs = require('fs'); const path = require('path'); const os = require('os'); +const ora = require('ora'); +const { colored } = require('./helpers'); /** * ClaudeSymlinkManager - Manages selective symlinks from ~/.ccs/.claude/ to ~/.claude/ @@ -35,41 +37,62 @@ class ClaudeSymlinkManager { * Install CCS items to user's ~/.claude/ via selective symlinks * Safe: backs up existing files before creating symlinks */ - install() { + install(silent = false) { + const spinner = silent ? null : ora('Installing CCS items to ~/.claude/').start(); + // Ensure ~/.ccs/.claude/ exists (should be shipped with package) if (!fs.existsSync(this.ccsClaudeDir)) { - console.log('[!] CCS .claude/ directory not found, skipping symlink installation'); + const msg = 'CCS .claude/ directory not found, skipping symlink installation'; + if (spinner) { + spinner.warn(`[!] ${msg}`); + } else { + console.log(`[!] ${msg}`); + } return; } // Create ~/.claude/ if missing if (!fs.existsSync(this.userClaudeDir)) { - console.log('[i] Creating ~/.claude/ directory'); + if (!silent) { + if (spinner) spinner.text = 'Creating ~/.claude/ directory'; + } fs.mkdirSync(this.userClaudeDir, { recursive: true, mode: 0o700 }); } // Install each CCS item + let installed = 0; for (const item of this.ccsItems) { - this._installItem(item); + if (!silent && spinner) { + spinner.text = `Installing ${item.target}...`; + } + const result = this._installItem(item, silent); + if (result) installed++; } - console.log('[OK] Delegation commands and skills installed to ~/.claude/'); + const msg = `${installed}/${this.ccsItems.length} items installed to ~/.claude/`; + if (spinner) { + spinner.succeed(colored('[OK]', 'green') + ` ${msg}`); + } else { + console.log(`[OK] ${msg}`); + } } /** * Install a single CCS item with conflict handling * @param {Object} item - Item descriptor {source, target, type} + * @param {boolean} silent - Suppress individual item messages + * @returns {boolean} True if installed successfully * @private */ - _installItem(item) { + _installItem(item, silent = false) { const sourcePath = path.join(this.ccsClaudeDir, item.source); const targetPath = path.join(this.userClaudeDir, item.target); const targetDir = path.dirname(targetPath); // Ensure source exists if (!fs.existsSync(sourcePath)) { - console.log(`[!] Source not found: ${item.source}, skipping`); - return; + if (!silent) console.log(`[!] Source not found: ${item.source}, skipping`); + return false; } // Create target parent directory if needed @@ -81,26 +104,30 @@ class ClaudeSymlinkManager { if (fs.existsSync(targetPath)) { // Check if it's already the correct symlink if (this._isOurSymlink(targetPath, sourcePath)) { - return; // Already correct, skip + return true; // Already correct, counts as success } // Backup existing file/directory - this._backupItem(targetPath); + this._backupItem(targetPath, silent); } // Create symlink try { const symlinkType = item.type === 'directory' ? 'dir' : 'file'; fs.symlinkSync(sourcePath, targetPath, symlinkType); - console.log(`[OK] Symlinked ${item.target}`); + if (!silent) console.log(`[OK] Symlinked ${item.target}`); + return true; } catch (err) { // Windows fallback: stub for now, full implementation in v4.2 if (process.platform === 'win32') { - console.log(`[!] Symlink failed for ${item.target} (Windows fallback deferred to v4.2)`); - console.log(`[i] Enable Developer Mode or wait for next update`); + if (!silent) { + console.log(`[!] Symlink failed for ${item.target} (Windows fallback deferred to v4.2)`); + console.log(`[i] Enable Developer Mode or wait for next update`); + } } else { - console.log(`[!] Failed to symlink ${item.target}: ${err.message}`); + if (!silent) console.log(`[!] Failed to symlink ${item.target}: ${err.message}`); } + return false; } } @@ -131,9 +158,10 @@ class ClaudeSymlinkManager { /** * Backup existing item before replacing with symlink * @param {string} itemPath - Path to item to backup + * @param {boolean} silent - Suppress backup messages * @private */ - _backupItem(itemPath) { + _backupItem(itemPath, silent = false) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0]; const backupPath = `${itemPath}.backup-${timestamp}`; @@ -147,9 +175,9 @@ class ClaudeSymlinkManager { } fs.renameSync(itemPath, finalBackupPath); - console.log(`[i] Backed up existing item to ${path.basename(finalBackupPath)}`); + if (!silent) console.log(`[i] Backed up existing item to ${path.basename(finalBackupPath)}`); } catch (err) { - console.log(`[!] Failed to backup ${itemPath}: ${err.message}`); + if (!silent) console.log(`[!] Failed to backup ${itemPath}: ${err.message}`); throw err; // Don't proceed if backup fails } } @@ -230,8 +258,10 @@ class ClaudeSymlinkManager { * Same as install() but with explicit sync message */ sync() { - console.log('[i] Syncing delegation commands and skills to ~/.claude/...'); - this.install(); + console.log(''); + console.log(colored('Syncing CCS Components...', 'cyan')); + console.log(''); + this.install(false); } } diff --git a/installers/install.ps1 b/installers/install.ps1 index bf904244..60881504 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 = "4.2.0" +$CcsVersion = "4.3.0" # Try to read VERSION file for git installations if ($ScriptDir) { diff --git a/installers/install.sh b/installers/install.sh index 632b3e0d..f34171cd 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="4.2.0" +CCS_VERSION="4.3.0" # Try to read VERSION file for git installations if [[ -f "$SCRIPT_DIR/VERSION" ]]; then diff --git a/lib/ccs b/lib/ccs index b0d1a22b..fa5ca959 100755 --- a/lib/ccs +++ b/lib/ccs @@ -2,7 +2,7 @@ set -euo pipefail # Version (updated by scripts/bump-version.sh) -CCS_VERSION="4.2.0" +CCS_VERSION="4.3.0" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}" readonly PROFILES_JSON="$HOME/.ccs/profiles.json" diff --git a/lib/ccs.ps1 b/lib/ccs.ps1 index 349097a7..80c8edb2 100644 --- a/lib/ccs.ps1 +++ b/lib/ccs.ps1 @@ -12,7 +12,7 @@ param( $ErrorActionPreference = "Stop" # Version (updated by scripts/bump-version.sh) -$CcsVersion = "4.2.0" +$CcsVersion = "4.3.0" $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" diff --git a/package-lock.json b/package-lock.json index 643d54d1..bace879d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@kaitranntt/ccs", - "version": "3.4.2", + "version": "4.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@kaitranntt/ccs", - "version": "3.4.2", + "version": "4.2.0", "hasInstallScript": true, "license": "MIT", "os": [ @@ -14,6 +14,10 @@ "linux", "win32" ], + "dependencies": { + "cli-table3": "^0.6.5", + "ora": "^5.4.1" + }, "bin": { "ccs": "bin/ccs.js" }, @@ -24,6 +28,16 @@ "node": ">=14.0.0" } }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -70,7 +84,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -96,6 +109,37 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -113,6 +157,30 @@ "dev": true, "license": "ISC" }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -130,7 +198,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -147,7 +214,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -172,6 +238,86 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-table3/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cli-table3/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -250,11 +396,19 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -267,7 +421,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/cross-spawn": { @@ -316,6 +469,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/diff": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", @@ -442,7 +607,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -458,11 +622,45 @@ "he": "bin/he" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", "license": "MIT", "engines": { "node": ">=8" @@ -492,7 +690,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -557,7 +754,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.0", @@ -577,6 +773,15 @@ "dev": true, "license": "ISC" }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -647,6 +852,65 @@ "dev": true, "license": "MIT" }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -740,26 +1004,19 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/randombytes/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } }, "node_modules/readdirp": { "version": "4.1.2", @@ -785,6 +1042,45 @@ "node": ">=0.10.0" } }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -831,6 +1127,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -964,6 +1269,21 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 3767b98b..e2f68afd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "4.2.0", + "version": "4.3.0", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", @@ -57,6 +57,10 @@ "prepare": "node scripts/check-executables.js", "postinstall": "node scripts/postinstall.js" }, + "dependencies": { + "cli-table3": "^0.6.5", + "ora": "^5.4.1" + }, "devDependencies": { "mocha": "^11.7.5" }