@kaitranntt/ccs 4.2.0 → 4.3.1
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 +1 -1
- package/bin/ccs.js +12 -2
- package/bin/management/doctor.js +202 -77
- package/bin/utils/claude-dir-installer.js +67 -9
- package/bin/utils/claude-symlink-manager.js +49 -19
- package/lib/ccs +1 -1
- package/lib/ccs.ps1 +1 -1
- package/package.json +5 -1
- package/scripts/completion/ccs.fish +19 -14
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.
|
|
1
|
+
4.3.1
|
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('
|
|
284
|
-
|
|
292
|
+
console.log('');
|
|
293
|
+
console.log(colored('[OK] Sync complete!', 'green'));
|
|
294
|
+
console.log('');
|
|
285
295
|
|
|
286
296
|
process.exit(0);
|
|
287
297
|
}
|
package/bin/management/doctor.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
+
const spinner = ora('Checking ~/.ccs/ directory').start();
|
|
118
159
|
|
|
119
160
|
if (fs.existsSync(this.ccsDir)) {
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
+
const spinner = ora(`Checking ${file.name}`).start();
|
|
145
192
|
|
|
146
193
|
if (!fs.existsSync(file.path)) {
|
|
147
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
277
|
+
spinner.succeed(` ${'~/.claude/settings.json'.padEnd(26)}${colored('[OK]', 'green')}`);
|
|
199
278
|
this.results.addCheck('Claude Settings', 'success');
|
|
200
279
|
} catch (e) {
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
362
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
426
|
-
if (this.results.
|
|
427
|
-
console.log(colored('
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
100
|
-
|
|
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('
|
|
234
|
-
|
|
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.
|
|
5
|
+
CCS_VERSION="4.3.1"
|
|
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.
|
|
15
|
+
$CcsVersion = "4.3.1"
|
|
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.
|
|
3
|
+
"version": "4.3.1",
|
|
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
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Fish completion for CCS (Claude Code Switch)
|
|
2
2
|
# Compatible with fish 3.0+
|
|
3
3
|
#
|
|
4
|
+
# Features:
|
|
5
|
+
# - Categorized completions with [cmd], [model], and [account] prefixes
|
|
6
|
+
# - Dynamic profile loading from config.json and profiles.json
|
|
7
|
+
# - Context-aware subcommand completion
|
|
8
|
+
#
|
|
4
9
|
# Installation:
|
|
5
10
|
# Copy to ~/.config/fish/completions/:
|
|
6
11
|
# mkdir -p ~/.config/fish/completions
|
|
@@ -72,23 +77,23 @@ complete -c ccs -s h -l help -d 'Show help message'
|
|
|
72
77
|
complete -c ccs -s v -l version -d 'Show version information'
|
|
73
78
|
complete -c ccs -s sc -l shell-completion -d 'Install shell completion'
|
|
74
79
|
|
|
75
|
-
#
|
|
76
|
-
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'auth' -d
|
|
77
|
-
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'doctor' -d
|
|
78
|
-
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'sync' -d
|
|
79
|
-
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'update' -d
|
|
80
|
+
# Commands - grouped with [cmd] prefix for visual distinction
|
|
81
|
+
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'auth' -d '[cmd] Manage multiple Claude accounts'
|
|
82
|
+
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'doctor' -d '[cmd] Run health check and diagnostics'
|
|
83
|
+
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'sync' -d '[cmd] Sync delegation commands and skills'
|
|
84
|
+
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'update' -d '[cmd] Update CCS to latest version'
|
|
80
85
|
|
|
81
|
-
#
|
|
82
|
-
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'default' -d
|
|
83
|
-
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'glm' -d
|
|
84
|
-
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'glmt' -d
|
|
85
|
-
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'kimi' -d
|
|
86
|
+
# Model profiles - grouped with [model] prefix for visual distinction
|
|
87
|
+
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'default' -d '[model] Default Claude Sonnet 4.5'
|
|
88
|
+
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'glm' -d '[model] GLM-4.6 (cost-optimized)'
|
|
89
|
+
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'glmt' -d '[model] GLM-4.6 with thinking mode'
|
|
90
|
+
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'kimi' -d '[model] Kimi for Coding (long-context)'
|
|
86
91
|
|
|
87
|
-
#
|
|
88
|
-
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a '(__fish_ccs_get_custom_settings_profiles)' -d
|
|
92
|
+
# Custom model profiles - dynamic with [model] prefix
|
|
93
|
+
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a '(__fish_ccs_get_custom_settings_profiles)' -d '[model] Settings-based profile'
|
|
89
94
|
|
|
90
|
-
#
|
|
91
|
-
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a '(__fish_ccs_get_account_profiles)' -d
|
|
95
|
+
# Account profiles - dynamic with [account] prefix
|
|
96
|
+
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a '(__fish_ccs_get_account_profiles)' -d '[account] Account-based profile'
|
|
92
97
|
|
|
93
98
|
# shell-completion subflags
|
|
94
99
|
complete -c ccs -n '__fish_seen_argument -l shell-completion; or __fish_seen_argument -s sc' -l bash -d 'Install for bash'
|