@kaitranntt/ccs 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.2.0
1
+ 4.3.0
package/bin/ccs.js CHANGED
@@ -271,17 +271,27 @@ async function handleDoctorCommand() {
271
271
  }
272
272
 
273
273
  async function handleSyncCommand() {
274
+ const { colored } = require('./utils/helpers');
275
+
276
+ console.log('');
277
+ console.log(colored('Syncing CCS Components...', 'cyan'));
278
+ console.log('');
279
+
274
280
  // First, copy .claude/ directory from package to ~/.ccs/.claude/
275
281
  const ClaudeDirInstaller = require('./utils/claude-dir-installer');
276
282
  const installer = new ClaudeDirInstaller();
277
283
  installer.install();
278
284
 
285
+ console.log('');
286
+
279
287
  // Then, create symlinks from ~/.ccs/.claude/ to ~/.claude/
280
288
  const ClaudeSymlinkManager = require('./utils/claude-symlink-manager');
281
289
  const manager = new ClaudeSymlinkManager();
290
+ manager.install(false);
282
291
 
283
- console.log('[i] Syncing delegation commands and skills to ~/.claude/...');
284
- manager.sync();
292
+ console.log('');
293
+ console.log(colored('[OK] Sync complete!', 'green'));
294
+ console.log('');
285
295
 
286
296
  process.exit(0);
287
297
  }
@@ -6,6 +6,8 @@ const os = require('os');
6
6
  const { spawn } = require('child_process');
7
7
  const { colored } = require('../utils/helpers');
8
8
  const { detectClaudeCli } = require('../utils/claude-detector');
9
+ const ora = require('ora');
10
+ const Table = require('cli-table3');
9
11
 
10
12
  /**
11
13
  * Health check results
@@ -15,13 +17,19 @@ class HealthCheck {
15
17
  this.checks = [];
16
18
  this.warnings = [];
17
19
  this.errors = [];
20
+ this.details = {}; // Store detailed information for summary table
18
21
  }
19
22
 
20
- addCheck(name, status, message = '', fix = null) {
23
+ addCheck(name, status, message = '', fix = null, details = null) {
21
24
  this.checks.push({ name, status, message, fix });
22
25
 
23
26
  if (status === 'error') this.errors.push({ name, message, fix });
24
27
  if (status === 'warning') this.warnings.push({ name, message, fix });
28
+
29
+ // Store details for summary table
30
+ if (details) {
31
+ this.details[name] = details;
32
+ }
25
33
  }
26
34
 
27
35
  hasErrors() {
@@ -46,6 +54,13 @@ class Doctor {
46
54
  this.ccsDir = path.join(this.homedir, '.ccs');
47
55
  this.claudeDir = path.join(this.homedir, '.claude');
48
56
  this.results = new HealthCheck();
57
+
58
+ // Get CCS version
59
+ try {
60
+ this.ccsVersion = require('../../package.json').version;
61
+ } catch (e) {
62
+ this.ccsVersion = 'unknown';
63
+ }
49
64
  }
50
65
 
51
66
  /**
@@ -55,15 +70,33 @@ class Doctor {
55
70
  console.log(colored('Running CCS Health Check...', 'cyan'));
56
71
  console.log('');
57
72
 
73
+ // Store CCS version in details
74
+ this.results.details['CCS Version'] = { status: 'OK', info: `v${this.ccsVersion}` };
75
+
76
+ // Group 1: System
77
+ console.log(colored('System:', 'bold'));
58
78
  await this.checkClaudeCli();
59
79
  this.checkCcsDirectory();
80
+ console.log('');
81
+
82
+ // Group 2: Configuration
83
+ console.log(colored('Configuration:', 'bold'));
60
84
  this.checkConfigFiles();
61
85
  this.checkClaudeSettings();
86
+ console.log('');
87
+
88
+ // Group 3: Profiles & Delegation
89
+ console.log(colored('Profiles & Delegation:', 'bold'));
62
90
  this.checkProfiles();
63
91
  this.checkInstances();
64
92
  this.checkDelegation();
93
+ console.log('');
94
+
95
+ // Group 4: System Health
96
+ console.log(colored('System Health:', 'bold'));
65
97
  this.checkPermissions();
66
98
  this.checkCcsSymlinks();
99
+ console.log('');
67
100
 
68
101
  this.showReport();
69
102
  return this.results;
@@ -73,7 +106,7 @@ class Doctor {
73
106
  * Check 1: Claude CLI availability
74
107
  */
75
108
  async checkClaudeCli() {
76
- process.stdout.write('[?] Checking Claude CLI... ');
109
+ const spinner = ora('Checking Claude CLI').start();
77
110
 
78
111
  const claudeCli = detectClaudeCli();
79
112
 
@@ -97,15 +130,23 @@ class Doctor {
97
130
  child.on('error', reject);
98
131
  });
99
132
 
100
- console.log(colored('[OK]', 'green'));
101
- this.results.addCheck('Claude CLI', 'success', `Found: ${claudeCli}`);
133
+ // Extract version from output
134
+ const versionMatch = result.match(/(\d+\.\d+\.\d+)/);
135
+ const version = versionMatch ? versionMatch[1] : 'unknown';
136
+
137
+ spinner.succeed(` ${'Claude CLI'.padEnd(26)}${colored('[OK]', 'green')} ${claudeCli} (v${version})`);
138
+ this.results.addCheck('Claude CLI', 'success', `Found: ${claudeCli}`, null, {
139
+ status: 'OK',
140
+ info: `v${version} (${claudeCli})`
141
+ });
102
142
  } catch (err) {
103
- console.log(colored('[X]', 'red'));
143
+ spinner.fail(` ${'Claude CLI'.padEnd(26)}${colored('[X]', 'red')} Not found or not working`);
104
144
  this.results.addCheck(
105
145
  'Claude CLI',
106
146
  'error',
107
147
  'Claude CLI not found or not working',
108
- 'Install from: https://docs.claude.com/en/docs/claude-code/installation'
148
+ 'Install from: https://docs.claude.com/en/docs/claude-code/installation',
149
+ { status: 'ERROR', info: 'Not installed' }
109
150
  );
110
151
  }
111
152
  }
@@ -114,18 +155,22 @@ class Doctor {
114
155
  * Check 2: ~/.ccs/ directory
115
156
  */
116
157
  checkCcsDirectory() {
117
- process.stdout.write('[?] Checking ~/.ccs/ directory... ');
158
+ const spinner = ora('Checking ~/.ccs/ directory').start();
118
159
 
119
160
  if (fs.existsSync(this.ccsDir)) {
120
- console.log(colored('[OK]', 'green'));
121
- this.results.addCheck('CCS Directory', 'success');
161
+ spinner.succeed(` ${'CCS Directory'.padEnd(26)}${colored('[OK]', 'green')} ~/.ccs/`);
162
+ this.results.addCheck('CCS Directory', 'success', null, null, {
163
+ status: 'OK',
164
+ info: '~/.ccs/'
165
+ });
122
166
  } else {
123
- console.log(colored('[X]', 'red'));
167
+ spinner.fail(` ${'CCS Directory'.padEnd(26)}${colored('[X]', 'red')} Not found`);
124
168
  this.results.addCheck(
125
169
  'CCS Directory',
126
170
  'error',
127
171
  '~/.ccs/ directory not found',
128
- 'Run: npm install -g @kaitranntt/ccs --force'
172
+ 'Run: npm install -g @kaitranntt/ccs --force',
173
+ { status: 'ERROR', info: 'Not found' }
129
174
  );
130
175
  }
131
176
  }
@@ -135,21 +180,24 @@ class Doctor {
135
180
  */
136
181
  checkConfigFiles() {
137
182
  const files = [
138
- { path: path.join(this.ccsDir, 'config.json'), name: 'config.json' },
139
- { path: path.join(this.ccsDir, 'glm.settings.json'), name: 'glm.settings.json' },
140
- { path: path.join(this.ccsDir, 'kimi.settings.json'), name: 'kimi.settings.json' }
183
+ { path: path.join(this.ccsDir, 'config.json'), name: 'config.json', key: 'config.json' },
184
+ { path: path.join(this.ccsDir, 'glm.settings.json'), name: 'glm.settings.json', key: 'GLM Settings', profile: 'glm' },
185
+ { path: path.join(this.ccsDir, 'kimi.settings.json'), name: 'kimi.settings.json', key: 'Kimi Settings', profile: 'kimi' }
141
186
  ];
142
187
 
188
+ const { DelegationValidator } = require('../utils/delegation-validator');
189
+
143
190
  for (const file of files) {
144
- process.stdout.write(`[?] Checking ${file.name}... `);
191
+ const spinner = ora(`Checking ${file.name}`).start();
145
192
 
146
193
  if (!fs.existsSync(file.path)) {
147
- console.log(colored('[X]', 'red'));
194
+ spinner.fail(` ${file.name.padEnd(26)}${colored('[X]', 'red')} Not found`);
148
195
  this.results.addCheck(
149
196
  file.name,
150
197
  'error',
151
198
  `${file.name} not found`,
152
- 'Run: npm install -g @kaitranntt/ccs --force'
199
+ 'Run: npm install -g @kaitranntt/ccs --force',
200
+ { status: 'ERROR', info: 'Not found' }
153
201
  );
154
202
  continue;
155
203
  }
@@ -157,16 +205,48 @@ class Doctor {
157
205
  // Validate JSON
158
206
  try {
159
207
  const content = fs.readFileSync(file.path, 'utf8');
160
- JSON.parse(content);
161
- console.log(colored('[OK]', 'green'));
162
- this.results.addCheck(file.name, 'success');
208
+ const config = JSON.parse(content);
209
+
210
+ // Extract useful info based on file type
211
+ let info = 'Valid';
212
+ let status = 'OK';
213
+
214
+ if (file.profile) {
215
+ // For settings files, check if API key is configured
216
+ const validation = DelegationValidator.validate(file.profile);
217
+
218
+ if (validation.valid) {
219
+ info = 'Key configured';
220
+ status = 'OK';
221
+ } else if (validation.error && validation.error.includes('placeholder')) {
222
+ info = 'Placeholder key (not configured)';
223
+ status = 'WARN';
224
+ } else {
225
+ info = 'Valid JSON';
226
+ status = 'OK';
227
+ }
228
+ }
229
+
230
+ const statusIcon = status === 'OK' ? colored('[OK]', 'green') : colored('[!]', 'yellow');
231
+
232
+ if (status === 'WARN') {
233
+ spinner.warn(` ${file.name.padEnd(26)}${statusIcon} ${info}`);
234
+ } else {
235
+ spinner.succeed(` ${file.name.padEnd(26)}${statusIcon} ${info}`);
236
+ }
237
+
238
+ this.results.addCheck(file.name, status === 'OK' ? 'success' : 'warning', null, null, {
239
+ status: status,
240
+ info: info
241
+ });
163
242
  } catch (e) {
164
- console.log(colored('[X]', 'red'));
243
+ spinner.fail(` ${file.name.padEnd(26)}${colored('[X]', 'red')} Invalid JSON`);
165
244
  this.results.addCheck(
166
245
  file.name,
167
246
  'error',
168
247
  `Invalid JSON: ${e.message}`,
169
- `Backup and recreate: mv ${file.path} ${file.path}.backup && npm install -g @kaitranntt/ccs --force`
248
+ `Backup and recreate: mv ${file.path} ${file.path}.backup && npm install -g @kaitranntt/ccs --force`,
249
+ { status: 'ERROR', info: 'Invalid JSON' }
170
250
  );
171
251
  }
172
252
  }
@@ -176,12 +256,11 @@ class Doctor {
176
256
  * Check 4: Claude settings
177
257
  */
178
258
  checkClaudeSettings() {
179
- process.stdout.write('[?] Checking ~/.claude/settings.json... ');
180
-
259
+ const spinner = ora('Checking ~/.claude/settings.json').start();
181
260
  const settingsPath = path.join(this.claudeDir, 'settings.json');
182
261
 
183
262
  if (!fs.existsSync(settingsPath)) {
184
- console.log(colored('[!]', 'yellow'));
263
+ spinner.warn(` ${'~/.claude/settings.json'.padEnd(26)}${colored('[!]', 'yellow')} Not found`);
185
264
  this.results.addCheck(
186
265
  'Claude Settings',
187
266
  'warning',
@@ -195,10 +274,10 @@ class Doctor {
195
274
  try {
196
275
  const content = fs.readFileSync(settingsPath, 'utf8');
197
276
  JSON.parse(content);
198
- console.log(colored('[OK]', 'green'));
277
+ spinner.succeed(` ${'~/.claude/settings.json'.padEnd(26)}${colored('[OK]', 'green')}`);
199
278
  this.results.addCheck('Claude Settings', 'success');
200
279
  } catch (e) {
201
- console.log(colored('[!]', 'yellow'));
280
+ spinner.warn(` ${'~/.claude/settings.json'.padEnd(26)}${colored('[!]', 'yellow')} Invalid JSON`);
202
281
  this.results.addCheck(
203
282
  'Claude Settings',
204
283
  'warning',
@@ -212,11 +291,11 @@ class Doctor {
212
291
  * Check 5: Profile configurations
213
292
  */
214
293
  checkProfiles() {
215
- process.stdout.write('[?] Checking profiles... ');
216
-
294
+ const spinner = ora('Checking profiles').start();
217
295
  const configPath = path.join(this.ccsDir, 'config.json');
296
+
218
297
  if (!fs.existsSync(configPath)) {
219
- console.log(colored('[SKIP]', 'yellow'));
298
+ spinner.info(` ${'Profiles'.padEnd(26)}${colored('[SKIP]', 'cyan')} config.json not found`);
220
299
  return;
221
300
  }
222
301
 
@@ -224,22 +303,31 @@ class Doctor {
224
303
  const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
225
304
 
226
305
  if (!config.profiles || typeof config.profiles !== 'object') {
227
- console.log(colored('[X]', 'red'));
306
+ spinner.fail(` ${'Profiles'.padEnd(26)}${colored('[X]', 'red')} Missing profiles object`);
228
307
  this.results.addCheck(
229
308
  'Profiles',
230
309
  'error',
231
310
  'config.json missing profiles object',
232
- 'Run: npm install -g @kaitranntt/ccs --force'
311
+ 'Run: npm install -g @kaitranntt/ccs --force',
312
+ { status: 'ERROR', info: 'Missing profiles object' }
233
313
  );
234
314
  return;
235
315
  }
236
316
 
237
317
  const profileCount = Object.keys(config.profiles).length;
238
- console.log(colored('[OK]', 'green'), `(${profileCount} profiles)`);
239
- this.results.addCheck('Profiles', 'success', `${profileCount} profiles configured`);
318
+ const profileNames = Object.keys(config.profiles).join(', ');
319
+
320
+ spinner.succeed(` ${'Profiles'.padEnd(26)}${colored('[OK]', 'green')} ${profileCount} configured (${profileNames})`);
321
+ this.results.addCheck('Profiles', 'success', `${profileCount} profiles configured`, null, {
322
+ status: 'OK',
323
+ info: `${profileCount} configured (${profileNames.length > 30 ? profileNames.substring(0, 27) + '...' : profileNames})`
324
+ });
240
325
  } catch (e) {
241
- console.log(colored('[X]', 'red'));
242
- this.results.addCheck('Profiles', 'error', e.message);
326
+ spinner.fail(` ${'Profiles'.padEnd(26)}${colored('[X]', 'red')} ${e.message}`);
327
+ this.results.addCheck('Profiles', 'error', e.message, null, {
328
+ status: 'ERROR',
329
+ info: e.message
330
+ });
243
331
  }
244
332
  }
245
333
 
@@ -247,11 +335,11 @@ class Doctor {
247
335
  * Check 6: Instance directories (account-based profiles)
248
336
  */
249
337
  checkInstances() {
250
- process.stdout.write('[?] Checking instances... ');
251
-
338
+ const spinner = ora('Checking instances').start();
252
339
  const instancesDir = path.join(this.ccsDir, 'instances');
340
+
253
341
  if (!fs.existsSync(instancesDir)) {
254
- console.log(colored('[i]', 'cyan'), '(no account profiles)');
342
+ spinner.info(` ${'Instances'.padEnd(26)}${colored('[i]', 'cyan')} No account profiles`);
255
343
  this.results.addCheck('Instances', 'success', 'No account profiles configured');
256
344
  return;
257
345
  }
@@ -261,12 +349,12 @@ class Doctor {
261
349
  });
262
350
 
263
351
  if (instances.length === 0) {
264
- console.log(colored('[i]', 'cyan'), '(no account profiles)');
352
+ spinner.info(` ${'Instances'.padEnd(26)}${colored('[i]', 'cyan')} No account profiles`);
265
353
  this.results.addCheck('Instances', 'success', 'No account profiles');
266
354
  return;
267
355
  }
268
356
 
269
- console.log(colored('[OK]', 'green'), `(${instances.length} instances)`);
357
+ spinner.succeed(` ${'Instances'.padEnd(26)}${colored('[OK]', 'green')} ${instances.length} account profiles`);
270
358
  this.results.addCheck('Instances', 'success', `${instances.length} account profiles`);
271
359
  }
272
360
 
@@ -274,7 +362,7 @@ class Doctor {
274
362
  * Check 7: Delegation system
275
363
  */
276
364
  checkDelegation() {
277
- process.stdout.write('[?] Checking delegation... ');
365
+ const spinner = ora('Checking delegation').start();
278
366
 
279
367
  // Check if delegation commands exist in ~/.ccs/.claude/commands/ccs/
280
368
  const ccsClaudeCommandsDir = path.join(this.ccsDir, '.claude', 'commands', 'ccs');
@@ -282,12 +370,13 @@ class Doctor {
282
370
  const hasKimiCommand = fs.existsSync(path.join(ccsClaudeCommandsDir, 'kimi.md'));
283
371
 
284
372
  if (!hasGlmCommand || !hasKimiCommand) {
285
- console.log(colored('[!]', 'yellow'), '(not installed)');
373
+ spinner.warn(` ${'Delegation'.padEnd(26)}${colored('[!]', 'yellow')} Not installed`);
286
374
  this.results.addCheck(
287
375
  'Delegation',
288
376
  'warning',
289
377
  'Delegation commands not found',
290
- 'Install with: npm install -g @kaitranntt/ccs --force'
378
+ 'Install with: npm install -g @kaitranntt/ccs --force',
379
+ { status: 'WARN', info: 'Not installed' }
291
380
  );
292
381
  return;
293
382
  }
@@ -304,21 +393,24 @@ class Doctor {
304
393
  }
305
394
 
306
395
  if (readyProfiles.length === 0) {
307
- console.log(colored('[!]', 'yellow'), '(no profiles ready)');
396
+ spinner.warn(` ${'Delegation'.padEnd(26)}${colored('[!]', 'yellow')} No profiles ready`);
308
397
  this.results.addCheck(
309
398
  'Delegation',
310
399
  'warning',
311
400
  'Delegation installed but no profiles configured',
312
- 'Configure profiles with valid API keys (not placeholders)'
401
+ 'Configure profiles with valid API keys (not placeholders)',
402
+ { status: 'WARN', info: 'No profiles ready' }
313
403
  );
314
404
  return;
315
405
  }
316
406
 
317
- console.log(colored('[OK]', 'green'), `(${readyProfiles.join(', ')} ready)`);
407
+ spinner.succeed(` ${'Delegation'.padEnd(26)}${colored('[OK]', 'green')} ${readyProfiles.length} profiles ready (${readyProfiles.join(', ')})`);
318
408
  this.results.addCheck(
319
409
  'Delegation',
320
410
  'success',
321
- `${readyProfiles.length} profile(s) ready: ${readyProfiles.join(', ')}`
411
+ `${readyProfiles.length} profile(s) ready: ${readyProfiles.join(', ')}`,
412
+ null,
413
+ { status: 'OK', info: `${readyProfiles.length} profiles ready` }
322
414
  );
323
415
  }
324
416
 
@@ -326,22 +418,25 @@ class Doctor {
326
418
  * Check 8: File permissions
327
419
  */
328
420
  checkPermissions() {
329
- process.stdout.write('[?] Checking permissions... ');
330
-
421
+ const spinner = ora('Checking permissions').start();
331
422
  const testFile = path.join(this.ccsDir, '.permission-test');
332
423
 
333
424
  try {
334
425
  fs.writeFileSync(testFile, 'test', 'utf8');
335
426
  fs.unlinkSync(testFile);
336
- console.log(colored('[OK]', 'green'));
337
- this.results.addCheck('Permissions', 'success');
427
+ spinner.succeed(` ${'Permissions'.padEnd(26)}${colored('[OK]', 'green')} Write access verified`);
428
+ this.results.addCheck('Permissions', 'success', null, null, {
429
+ status: 'OK',
430
+ info: 'Write access verified'
431
+ });
338
432
  } catch (e) {
339
- console.log(colored('[X]', 'red'));
433
+ spinner.fail(` ${'Permissions'.padEnd(26)}${colored('[X]', 'red')} Cannot write to ~/.ccs/`);
340
434
  this.results.addCheck(
341
435
  'Permissions',
342
436
  'error',
343
437
  'Cannot write to ~/.ccs/',
344
- 'Fix: sudo chown -R $USER ~/.ccs ~/.claude && chmod 755 ~/.ccs ~/.claude'
438
+ 'Fix: sudo chown -R $USER ~/.ccs ~/.claude && chmod 755 ~/.ccs ~/.claude',
439
+ { status: 'ERROR', info: 'Cannot write to ~/.ccs/' }
345
440
  );
346
441
  }
347
442
  }
@@ -350,7 +445,7 @@ class Doctor {
350
445
  * Check 9: CCS symlinks to ~/.claude/
351
446
  */
352
447
  checkCcsSymlinks() {
353
- process.stdout.write('[?] Checking CCS symlinks... ');
448
+ const spinner = ora('Checking CCS symlinks').start();
354
449
 
355
450
  try {
356
451
  const ClaudeSymlinkManager = require('../utils/claude-symlink-manager');
@@ -358,24 +453,30 @@ class Doctor {
358
453
  const health = manager.checkHealth();
359
454
 
360
455
  if (health.healthy) {
361
- console.log(colored('[OK]', 'green'));
362
- this.results.addCheck('CCS Symlinks', 'success', 'All CCS items properly symlinked');
456
+ const itemCount = manager.ccsItems.length;
457
+ spinner.succeed(` ${'CCS Symlinks'.padEnd(26)}${colored('[OK]', 'green')} ${itemCount}/${itemCount} items linked`);
458
+ this.results.addCheck('CCS Symlinks', 'success', 'All CCS items properly symlinked', null, {
459
+ status: 'OK',
460
+ info: `${itemCount}/${itemCount} items synced`
461
+ });
363
462
  } else {
364
- console.log(colored('[!]', 'yellow'));
463
+ spinner.warn(` ${'CCS Symlinks'.padEnd(26)}${colored('[!]', 'yellow')} ${health.issues.length} issues found`);
365
464
  this.results.addCheck(
366
465
  'CCS Symlinks',
367
466
  'warning',
368
467
  health.issues.join(', '),
369
- 'Run: ccs sync'
468
+ 'Run: ccs sync',
469
+ { status: 'WARN', info: `${health.issues.length} issues` }
370
470
  );
371
471
  }
372
472
  } catch (e) {
373
- console.log(colored('[!]', 'yellow'));
473
+ spinner.warn(` ${'CCS Symlinks'.padEnd(26)}${colored('[!]', 'yellow')} Could not check`);
374
474
  this.results.addCheck(
375
475
  'CCS Symlinks',
376
476
  'warning',
377
477
  'Could not check CCS symlinks: ' + e.message,
378
- 'Run: ccs sync'
478
+ 'Run: ccs sync',
479
+ { status: 'WARN', info: 'Could not check' }
379
480
  );
380
481
  }
381
482
  }
@@ -385,20 +486,43 @@ class Doctor {
385
486
  */
386
487
  showReport() {
387
488
  console.log('');
388
- console.log(colored('═══════════════════════════════════════════', 'cyan'));
389
- console.log(colored('Health Check Report', 'bold'));
390
- console.log(colored('═══════════════════════════════════════════', 'cyan'));
391
- console.log('');
392
489
 
393
- if (this.results.isHealthy() && !this.results.hasWarnings()) {
394
- console.log(colored('✓ All checks passed!', 'green'));
395
- console.log('');
396
- console.log('Your CCS installation is healthy.');
397
- console.log('');
398
- return;
490
+ // Calculate exact table width to match header bars
491
+ // colWidths: [20, 10, 35] = 65 + borders (4) = 69 total
492
+ const tableWidth = 69;
493
+ const headerBar = '═'.repeat(tableWidth);
494
+
495
+ console.log(colored(headerBar, 'cyan'));
496
+ console.log(colored(' Health Check Summary', 'bold'));
497
+ console.log(colored(headerBar, 'cyan'));
498
+
499
+ // Create summary table with detailed information
500
+ const table = new Table({
501
+ head: [colored('Component', 'cyan'), colored('Status', 'cyan'), colored('Details', 'cyan')],
502
+ colWidths: [20, 10, 35],
503
+ wordWrap: true,
504
+ chars: {
505
+ 'top': '═', 'top-mid': '╤', 'top-left': '╔', 'top-right': '╗',
506
+ 'bottom': '═', 'bottom-mid': '╧', 'bottom-left': '╚', 'bottom-right': '╝',
507
+ 'left': '║', 'left-mid': '╟', 'mid': '─', 'mid-mid': '┼',
508
+ 'right': '║', 'right-mid': '╢', 'middle': '│'
509
+ }
510
+ });
511
+
512
+ // Populate table with collected details
513
+ for (const [component, detail] of Object.entries(this.results.details)) {
514
+ const statusColor = detail.status === 'OK' ? 'green' : detail.status === 'ERROR' ? 'red' : 'yellow';
515
+ table.push([
516
+ component,
517
+ colored(detail.status, statusColor),
518
+ detail.info || ''
519
+ ]);
399
520
  }
400
521
 
401
- // Show errors
522
+ console.log(table.toString());
523
+ console.log('');
524
+
525
+ // Show errors and warnings if present
402
526
  if (this.results.hasErrors()) {
403
527
  console.log(colored('Errors:', 'red'));
404
528
  this.results.errors.forEach(err => {
@@ -410,7 +534,6 @@ class Doctor {
410
534
  console.log('');
411
535
  }
412
536
 
413
- // Show warnings
414
537
  if (this.results.hasWarnings()) {
415
538
  console.log(colored('Warnings:', 'yellow'));
416
539
  this.results.warnings.forEach(warn => {
@@ -422,12 +545,14 @@ class Doctor {
422
545
  console.log('');
423
546
  }
424
547
 
425
- // Summary
426
- if (this.results.hasErrors()) {
427
- console.log(colored('Status: Installation has errors', 'red'));
548
+ // Final status
549
+ if (this.results.isHealthy() && !this.results.hasWarnings()) {
550
+ console.log(colored('[OK] All checks passed! Your CCS installation is healthy.', 'green'));
551
+ } else if (this.results.hasErrors()) {
552
+ console.log(colored('[X] Status: Installation has errors', 'red'));
428
553
  console.log('Run suggested fixes above to resolve issues.');
429
554
  } else {
430
- console.log(colored('Status: Installation healthy (warnings only)', 'green'));
555
+ console.log(colored('[OK] Status: Installation healthy (warnings only)', 'green'));
431
556
  }
432
557
 
433
558
  console.log('');
@@ -4,6 +4,8 @@
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
7
+ const ora = require('ora');
8
+ const { colored } = require('./helpers');
7
9
 
8
10
  /**
9
11
  * ClaudeDirInstaller - Manages copying .claude/ directory from package to ~/.ccs/.claude/
@@ -18,8 +20,11 @@ class ClaudeDirInstaller {
18
20
  /**
19
21
  * Copy .claude/ directory from package to ~/.ccs/.claude/
20
22
  * @param {string} packageDir - Package installation directory (default: auto-detect)
23
+ * @param {boolean} silent - Suppress spinner output
21
24
  */
22
- install(packageDir) {
25
+ install(packageDir, silent = false) {
26
+ const spinner = silent ? null : ora('Copying .claude/ items to ~/.ccs/.claude/').start();
27
+
23
28
  try {
24
29
  // Auto-detect package directory if not provided
25
30
  if (!packageDir) {
@@ -30,21 +35,29 @@ class ClaudeDirInstaller {
30
35
  const packageClaudeDir = path.join(packageDir, '.claude');
31
36
 
32
37
  if (!fs.existsSync(packageClaudeDir)) {
33
- console.log('[!] Package .claude/ directory not found');
34
- console.log(` Searched in: ${packageClaudeDir}`);
35
- console.log(' This may be a development installation');
38
+ const msg = 'Package .claude/ directory not found';
39
+ if (spinner) {
40
+ spinner.warn(`[!] ${msg}`);
41
+ console.log(` Searched in: ${packageClaudeDir}`);
42
+ console.log(' This may be a development installation');
43
+ } else {
44
+ console.log(`[!] ${msg}`);
45
+ console.log(` Searched in: ${packageClaudeDir}`);
46
+ console.log(' This may be a development installation');
47
+ }
36
48
  return false;
37
49
  }
38
50
 
39
- console.log('[i] Installing CCS .claude/ items...');
40
-
41
51
  // Remove old version before copying new one
42
52
  if (fs.existsSync(this.ccsClaudeDir)) {
53
+ if (spinner) spinner.text = 'Removing old .claude/ items...';
43
54
  fs.rmSync(this.ccsClaudeDir, { recursive: true, force: true });
44
55
  }
45
56
 
46
57
  // Use fs.cpSync for recursive copy (Node.js 16.7.0+)
47
58
  // Fallback to manual copy for older Node.js versions
59
+ if (spinner) spinner.text = 'Copying .claude/ items...';
60
+
48
61
  if (fs.cpSync) {
49
62
  fs.cpSync(packageClaudeDir, this.ccsClaudeDir, { recursive: true });
50
63
  } else {
@@ -52,11 +65,25 @@ class ClaudeDirInstaller {
52
65
  this._copyDirRecursive(packageClaudeDir, this.ccsClaudeDir);
53
66
  }
54
67
 
55
- console.log('[OK] Copied .claude/ items to ~/.ccs/.claude/');
68
+ // Count files and directories
69
+ const itemCount = this._countItems(this.ccsClaudeDir);
70
+ const msg = `Copied .claude/ items (${itemCount.files} files, ${itemCount.dirs} directories)`;
71
+
72
+ if (spinner) {
73
+ spinner.succeed(colored('[OK]', 'green') + ` ${msg}`);
74
+ } else {
75
+ console.log(`[OK] ${msg}`);
76
+ }
56
77
  return true;
57
78
  } catch (err) {
58
- console.warn('[!] Failed to copy .claude/ directory:', err.message);
59
- console.warn(' CCS items may not be available');
79
+ const msg = `Failed to copy .claude/ directory: ${err.message}`;
80
+ if (spinner) {
81
+ spinner.fail(colored('[!]', 'yellow') + ` ${msg}`);
82
+ console.warn(' CCS items may not be available');
83
+ } else {
84
+ console.warn(`[!] ${msg}`);
85
+ console.warn(' CCS items may not be available');
86
+ }
60
87
  return false;
61
88
  }
62
89
  }
@@ -90,6 +117,37 @@ class ClaudeDirInstaller {
90
117
  }
91
118
  }
92
119
 
120
+ /**
121
+ * Count files and directories in a path
122
+ * @param {string} dirPath - Directory to count
123
+ * @returns {Object} { files: number, dirs: number }
124
+ * @private
125
+ */
126
+ _countItems(dirPath) {
127
+ let files = 0;
128
+ let dirs = 0;
129
+
130
+ const countRecursive = (p) => {
131
+ const entries = fs.readdirSync(p, { withFileTypes: true });
132
+ for (const entry of entries) {
133
+ if (entry.isDirectory()) {
134
+ dirs++;
135
+ countRecursive(path.join(p, entry.name));
136
+ } else {
137
+ files++;
138
+ }
139
+ }
140
+ };
141
+
142
+ try {
143
+ countRecursive(dirPath);
144
+ } catch (e) {
145
+ // Ignore errors
146
+ }
147
+
148
+ return { files, dirs };
149
+ }
150
+
93
151
  /**
94
152
  * Check if ~/.ccs/.claude/ exists and is valid
95
153
  * @returns {boolean} True if directory exists
@@ -3,6 +3,8 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
+ const ora = require('ora');
7
+ const { colored } = require('./helpers');
6
8
 
7
9
  /**
8
10
  * ClaudeSymlinkManager - Manages selective symlinks from ~/.ccs/.claude/ to ~/.claude/
@@ -35,41 +37,62 @@ class ClaudeSymlinkManager {
35
37
  * Install CCS items to user's ~/.claude/ via selective symlinks
36
38
  * Safe: backs up existing files before creating symlinks
37
39
  */
38
- install() {
40
+ install(silent = false) {
41
+ const spinner = silent ? null : ora('Installing CCS items to ~/.claude/').start();
42
+
39
43
  // Ensure ~/.ccs/.claude/ exists (should be shipped with package)
40
44
  if (!fs.existsSync(this.ccsClaudeDir)) {
41
- console.log('[!] CCS .claude/ directory not found, skipping symlink installation');
45
+ const msg = 'CCS .claude/ directory not found, skipping symlink installation';
46
+ if (spinner) {
47
+ spinner.warn(`[!] ${msg}`);
48
+ } else {
49
+ console.log(`[!] ${msg}`);
50
+ }
42
51
  return;
43
52
  }
44
53
 
45
54
  // Create ~/.claude/ if missing
46
55
  if (!fs.existsSync(this.userClaudeDir)) {
47
- console.log('[i] Creating ~/.claude/ directory');
56
+ if (!silent) {
57
+ if (spinner) spinner.text = 'Creating ~/.claude/ directory';
58
+ }
48
59
  fs.mkdirSync(this.userClaudeDir, { recursive: true, mode: 0o700 });
49
60
  }
50
61
 
51
62
  // Install each CCS item
63
+ let installed = 0;
52
64
  for (const item of this.ccsItems) {
53
- this._installItem(item);
65
+ if (!silent && spinner) {
66
+ spinner.text = `Installing ${item.target}...`;
67
+ }
68
+ const result = this._installItem(item, silent);
69
+ if (result) installed++;
54
70
  }
55
71
 
56
- console.log('[OK] Delegation commands and skills installed to ~/.claude/');
72
+ const msg = `${installed}/${this.ccsItems.length} items installed to ~/.claude/`;
73
+ if (spinner) {
74
+ spinner.succeed(colored('[OK]', 'green') + ` ${msg}`);
75
+ } else {
76
+ console.log(`[OK] ${msg}`);
77
+ }
57
78
  }
58
79
 
59
80
  /**
60
81
  * Install a single CCS item with conflict handling
61
82
  * @param {Object} item - Item descriptor {source, target, type}
83
+ * @param {boolean} silent - Suppress individual item messages
84
+ * @returns {boolean} True if installed successfully
62
85
  * @private
63
86
  */
64
- _installItem(item) {
87
+ _installItem(item, silent = false) {
65
88
  const sourcePath = path.join(this.ccsClaudeDir, item.source);
66
89
  const targetPath = path.join(this.userClaudeDir, item.target);
67
90
  const targetDir = path.dirname(targetPath);
68
91
 
69
92
  // Ensure source exists
70
93
  if (!fs.existsSync(sourcePath)) {
71
- console.log(`[!] Source not found: ${item.source}, skipping`);
72
- return;
94
+ if (!silent) console.log(`[!] Source not found: ${item.source}, skipping`);
95
+ return false;
73
96
  }
74
97
 
75
98
  // Create target parent directory if needed
@@ -81,26 +104,30 @@ class ClaudeSymlinkManager {
81
104
  if (fs.existsSync(targetPath)) {
82
105
  // Check if it's already the correct symlink
83
106
  if (this._isOurSymlink(targetPath, sourcePath)) {
84
- return; // Already correct, skip
107
+ return true; // Already correct, counts as success
85
108
  }
86
109
 
87
110
  // Backup existing file/directory
88
- this._backupItem(targetPath);
111
+ this._backupItem(targetPath, silent);
89
112
  }
90
113
 
91
114
  // Create symlink
92
115
  try {
93
116
  const symlinkType = item.type === 'directory' ? 'dir' : 'file';
94
117
  fs.symlinkSync(sourcePath, targetPath, symlinkType);
95
- console.log(`[OK] Symlinked ${item.target}`);
118
+ if (!silent) console.log(`[OK] Symlinked ${item.target}`);
119
+ return true;
96
120
  } catch (err) {
97
121
  // Windows fallback: stub for now, full implementation in v4.2
98
122
  if (process.platform === 'win32') {
99
- console.log(`[!] Symlink failed for ${item.target} (Windows fallback deferred to v4.2)`);
100
- console.log(`[i] Enable Developer Mode or wait for next update`);
123
+ if (!silent) {
124
+ console.log(`[!] Symlink failed for ${item.target} (Windows fallback deferred to v4.2)`);
125
+ console.log(`[i] Enable Developer Mode or wait for next update`);
126
+ }
101
127
  } else {
102
- console.log(`[!] Failed to symlink ${item.target}: ${err.message}`);
128
+ if (!silent) console.log(`[!] Failed to symlink ${item.target}: ${err.message}`);
103
129
  }
130
+ return false;
104
131
  }
105
132
  }
106
133
 
@@ -131,9 +158,10 @@ class ClaudeSymlinkManager {
131
158
  /**
132
159
  * Backup existing item before replacing with symlink
133
160
  * @param {string} itemPath - Path to item to backup
161
+ * @param {boolean} silent - Suppress backup messages
134
162
  * @private
135
163
  */
136
- _backupItem(itemPath) {
164
+ _backupItem(itemPath, silent = false) {
137
165
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
138
166
  const backupPath = `${itemPath}.backup-${timestamp}`;
139
167
 
@@ -147,9 +175,9 @@ class ClaudeSymlinkManager {
147
175
  }
148
176
 
149
177
  fs.renameSync(itemPath, finalBackupPath);
150
- console.log(`[i] Backed up existing item to ${path.basename(finalBackupPath)}`);
178
+ if (!silent) console.log(`[i] Backed up existing item to ${path.basename(finalBackupPath)}`);
151
179
  } catch (err) {
152
- console.log(`[!] Failed to backup ${itemPath}: ${err.message}`);
180
+ if (!silent) console.log(`[!] Failed to backup ${itemPath}: ${err.message}`);
153
181
  throw err; // Don't proceed if backup fails
154
182
  }
155
183
  }
@@ -230,8 +258,10 @@ class ClaudeSymlinkManager {
230
258
  * Same as install() but with explicit sync message
231
259
  */
232
260
  sync() {
233
- console.log('[i] Syncing delegation commands and skills to ~/.claude/...');
234
- this.install();
261
+ console.log('');
262
+ console.log(colored('Syncing CCS Components...', 'cyan'));
263
+ console.log('');
264
+ this.install(false);
235
265
  }
236
266
  }
237
267
 
package/lib/ccs CHANGED
@@ -2,7 +2,7 @@
2
2
  set -euo pipefail
3
3
 
4
4
  # Version (updated by scripts/bump-version.sh)
5
- CCS_VERSION="4.2.0"
5
+ CCS_VERSION="4.3.0"
6
6
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
7
  readonly CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
8
8
  readonly PROFILES_JSON="$HOME/.ccs/profiles.json"
package/lib/ccs.ps1 CHANGED
@@ -12,7 +12,7 @@ param(
12
12
  $ErrorActionPreference = "Stop"
13
13
 
14
14
  # Version (updated by scripts/bump-version.sh)
15
- $CcsVersion = "4.2.0"
15
+ $CcsVersion = "4.3.0"
16
16
  $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
17
17
  $ConfigFile = if ($env:CCS_CONFIG) { $env:CCS_CONFIG } else { "$env:USERPROFILE\.ccs\config.json" }
18
18
  $ProfilesJson = "$env:USERPROFILE\.ccs\profiles.json"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaitranntt/ccs",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
5
5
  "keywords": [
6
6
  "cli",
@@ -57,6 +57,10 @@
57
57
  "prepare": "node scripts/check-executables.js",
58
58
  "postinstall": "node scripts/postinstall.js"
59
59
  },
60
+ "dependencies": {
61
+ "cli-table3": "^0.6.5",
62
+ "ora": "^5.4.1"
63
+ },
60
64
  "devDependencies": {
61
65
  "mocha": "^11.7.5"
62
66
  }