feat: improve UX for ccs doctor and sync commands (#13)

* feat: enhance UX for 'ccs sync' and 'ccs doctor' commands

- Add ora spinners for real-time progress feedback
- Add cli-table3 for structured health check summary table
- Display detailed component information in doctor report
- Show file/directory counts during sync operations
- Improve visual hierarchy and readability
- Fix Unicode checkmark violation (replaced with [OK])

Breaking changes: None
Dependencies added: ora@^5.4.1, cli-table3@^0.6.5

Version bumped to 4.3.0

* fix: match doctor summary header width to table width

* feat: improve settings validation to check API keys, not just endpoints

- Check if API keys are configured or still placeholders
- Show 'Key configured' vs 'Placeholder key (not configured)'
- Previous message 'Valid (API: api.z.ai)' was misleading
- Now users can clearly see which profiles need API key configuration
- Warnings shown for placeholder keys (yellow [!])

* feat: add category grouping and alignment to doctor checks

- Group checks into logical categories: System, Configuration, Profiles & Delegation, System Health
- Add consistent padding (26 chars) for aligned columns
- Add 2-space indent for better visual hierarchy
- Separate categories with blank lines for better readability
- Much cleaner and easier to scan than before

* fix: perfect alignment for all doctor check output

- Use .padEnd(26) consistently for all component names
- Remove extra space before status icons
- Now all status icons [OK]/[X]/[!]/[i] align perfectly
- All component names pad to exactly 26 characters
This commit is contained in:
Kai (Tam Nhu) Tran
2025-11-18 01:19:28 -05:00
committed by kaitranntt
parent d34dbcac1f
commit 67dbd3f259
11 changed files with 691 additions and 144 deletions
+1 -1
View File
@@ -1 +1 @@
4.2.0
4.3.0
+12 -2
View File
@@ -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);
}
+202 -77
View File
@@ -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('');
+67 -9
View File
@@ -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
+49 -19
View File
@@ -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);
}
}
+1 -1
View File
@@ -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) {
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+351 -31
View File
@@ -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",
+5 -1
View File
@@ -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"
}