@kaitranntt/ccs 2.5.1 → 3.0.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.
@@ -4,6 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
6
  const { error, expandPath } = require('./helpers');
7
+ const { ErrorManager } = require('./error-manager');
7
8
 
8
9
  // Get config file path
9
10
  function getConfigPath() {
@@ -16,7 +17,15 @@ function readConfig() {
16
17
 
17
18
  // Check config exists
18
19
  if (!fs.existsSync(configPath)) {
19
- error(`Config file not found: ${configPath}`);
20
+ // Attempt recovery
21
+ const RecoveryManager = require('./recovery-manager');
22
+ const recovery = new RecoveryManager();
23
+ recovery.ensureConfigJson();
24
+
25
+ if (!fs.existsSync(configPath)) {
26
+ ErrorManager.showInvalidConfig(configPath, 'File not found');
27
+ process.exit(1);
28
+ }
20
29
  }
21
30
 
22
31
  // Read and parse JSON
@@ -25,12 +34,14 @@ function readConfig() {
25
34
  const configContent = fs.readFileSync(configPath, 'utf8');
26
35
  config = JSON.parse(configContent);
27
36
  } catch (e) {
28
- error(`Invalid JSON in ${configPath}: ${e.message}`);
37
+ ErrorManager.showInvalidConfig(configPath, `Invalid JSON: ${e.message}`);
38
+ process.exit(1);
29
39
  }
30
40
 
31
41
  // Validate config has profiles object
32
42
  if (!config.profiles || typeof config.profiles !== 'object') {
33
- error(`Config must have 'profiles' object in ${configPath}`);
43
+ ErrorManager.showInvalidConfig(configPath, "Missing 'profiles' object");
44
+ process.exit(1);
34
45
  }
35
46
 
36
47
  return config;
@@ -44,8 +55,10 @@ function getSettingsPath(profile) {
44
55
  const settingsPath = config.profiles[profile];
45
56
 
46
57
  if (!settingsPath) {
47
- const availableProfiles = Object.keys(config.profiles).join(', ');
48
- error(`Profile '${profile}' not found. Available: ${availableProfiles}`);
58
+ const availableProfiles = Object.keys(config.profiles);
59
+ const profileList = availableProfiles.map(p => ` - ${p}`);
60
+ ErrorManager.showProfileNotFound(profile, profileList);
61
+ process.exit(1);
49
62
  }
50
63
 
51
64
  // Expand path
@@ -53,7 +66,22 @@ function getSettingsPath(profile) {
53
66
 
54
67
  // Validate settings file exists
55
68
  if (!fs.existsSync(expandedPath)) {
56
- error(`Settings file not found: ${expandedPath}`);
69
+ // Auto-create if it's ~/.claude/settings.json
70
+ if (expandedPath.includes('.claude') && expandedPath.endsWith('settings.json')) {
71
+ const RecoveryManager = require('./recovery-manager');
72
+ const recovery = new RecoveryManager();
73
+ recovery.ensureClaudeSettings();
74
+
75
+ if (!fs.existsSync(expandedPath)) {
76
+ ErrorManager.showSettingsNotFound(expandedPath);
77
+ process.exit(1);
78
+ }
79
+
80
+ console.log('[i] Auto-created missing settings file');
81
+ } else {
82
+ ErrorManager.showSettingsNotFound(expandedPath);
83
+ process.exit(1);
84
+ }
57
85
  }
58
86
 
59
87
  // Validate settings file is valid JSON
@@ -61,7 +89,8 @@ function getSettingsPath(profile) {
61
89
  const settingsContent = fs.readFileSync(expandedPath, 'utf8');
62
90
  JSON.parse(settingsContent);
63
91
  } catch (e) {
64
- error(`Invalid JSON in ${expandedPath}: ${e.message}`);
92
+ ErrorManager.showInvalidConfig(expandedPath, `Invalid JSON: ${e.message}`);
93
+ process.exit(1);
65
94
  }
66
95
 
67
96
  return expandedPath;
package/bin/doctor.js ADDED
@@ -0,0 +1,365 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { spawn } = require('child_process');
7
+ const { colored } = require('./helpers');
8
+ const { detectClaudeCli } = require('./claude-detector');
9
+
10
+ /**
11
+ * Health check results
12
+ */
13
+ class HealthCheck {
14
+ constructor() {
15
+ this.checks = [];
16
+ this.warnings = [];
17
+ this.errors = [];
18
+ }
19
+
20
+ addCheck(name, status, message = '', fix = null) {
21
+ this.checks.push({ name, status, message, fix });
22
+
23
+ if (status === 'error') this.errors.push({ name, message, fix });
24
+ if (status === 'warning') this.warnings.push({ name, message, fix });
25
+ }
26
+
27
+ hasErrors() {
28
+ return this.errors.length > 0;
29
+ }
30
+
31
+ hasWarnings() {
32
+ return this.warnings.length > 0;
33
+ }
34
+
35
+ isHealthy() {
36
+ return !this.hasErrors();
37
+ }
38
+ }
39
+
40
+ /**
41
+ * CCS Health Check and Diagnostics
42
+ */
43
+ class Doctor {
44
+ constructor() {
45
+ this.homedir = os.homedir();
46
+ this.ccsDir = path.join(this.homedir, '.ccs');
47
+ this.claudeDir = path.join(this.homedir, '.claude');
48
+ this.results = new HealthCheck();
49
+ }
50
+
51
+ /**
52
+ * Run all health checks
53
+ */
54
+ async runAllChecks() {
55
+ console.log(colored('Running CCS Health Check...', 'cyan'));
56
+ console.log('');
57
+
58
+ await this.checkClaudeCli();
59
+ this.checkCcsDirectory();
60
+ this.checkConfigFiles();
61
+ this.checkClaudeSettings();
62
+ this.checkProfiles();
63
+ this.checkInstances();
64
+ this.checkPermissions();
65
+
66
+ this.showReport();
67
+ return this.results;
68
+ }
69
+
70
+ /**
71
+ * Check 1: Claude CLI availability
72
+ */
73
+ async checkClaudeCli() {
74
+ process.stdout.write('[?] Checking Claude CLI... ');
75
+
76
+ const claudeCli = detectClaudeCli();
77
+
78
+ // Try to execute claude --version
79
+ try {
80
+ const result = await new Promise((resolve, reject) => {
81
+ const child = spawn(claudeCli, ['--version'], {
82
+ stdio: 'pipe',
83
+ timeout: 5000
84
+ });
85
+
86
+ let output = '';
87
+ child.stdout.on('data', data => output += data);
88
+ child.stderr.on('data', data => output += data);
89
+
90
+ child.on('close', code => {
91
+ if (code === 0) resolve(output);
92
+ else reject(new Error('Exit code ' + code));
93
+ });
94
+
95
+ child.on('error', reject);
96
+ });
97
+
98
+ console.log(colored('[OK]', 'green'));
99
+ this.results.addCheck('Claude CLI', 'success', `Found: ${claudeCli}`);
100
+ } catch (err) {
101
+ console.log(colored('[X]', 'red'));
102
+ this.results.addCheck(
103
+ 'Claude CLI',
104
+ 'error',
105
+ 'Claude CLI not found or not working',
106
+ 'Install from: https://docs.claude.com/en/docs/claude-code/installation'
107
+ );
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Check 2: ~/.ccs/ directory
113
+ */
114
+ checkCcsDirectory() {
115
+ process.stdout.write('[?] Checking ~/.ccs/ directory... ');
116
+
117
+ if (fs.existsSync(this.ccsDir)) {
118
+ console.log(colored('[OK]', 'green'));
119
+ this.results.addCheck('CCS Directory', 'success');
120
+ } else {
121
+ console.log(colored('[X]', 'red'));
122
+ this.results.addCheck(
123
+ 'CCS Directory',
124
+ 'error',
125
+ '~/.ccs/ directory not found',
126
+ 'Run: npm install -g @kaitranntt/ccs --force'
127
+ );
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Check 3: Config files
133
+ */
134
+ checkConfigFiles() {
135
+ const files = [
136
+ { path: path.join(this.ccsDir, 'config.json'), name: 'config.json' },
137
+ { path: path.join(this.ccsDir, 'glm.settings.json'), name: 'glm.settings.json' },
138
+ { path: path.join(this.ccsDir, 'kimi.settings.json'), name: 'kimi.settings.json' }
139
+ ];
140
+
141
+ for (const file of files) {
142
+ process.stdout.write(`[?] Checking ${file.name}... `);
143
+
144
+ if (!fs.existsSync(file.path)) {
145
+ console.log(colored('[X]', 'red'));
146
+ this.results.addCheck(
147
+ file.name,
148
+ 'error',
149
+ `${file.name} not found`,
150
+ 'Run: npm install -g @kaitranntt/ccs --force'
151
+ );
152
+ continue;
153
+ }
154
+
155
+ // Validate JSON
156
+ try {
157
+ const content = fs.readFileSync(file.path, 'utf8');
158
+ JSON.parse(content);
159
+ console.log(colored('[OK]', 'green'));
160
+ this.results.addCheck(file.name, 'success');
161
+ } catch (e) {
162
+ console.log(colored('[X]', 'red'));
163
+ this.results.addCheck(
164
+ file.name,
165
+ 'error',
166
+ `Invalid JSON: ${e.message}`,
167
+ `Backup and recreate: mv ${file.path} ${file.path}.backup && npm install -g @kaitranntt/ccs --force`
168
+ );
169
+ }
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Check 4: Claude settings
175
+ */
176
+ checkClaudeSettings() {
177
+ process.stdout.write('[?] Checking ~/.claude/settings.json... ');
178
+
179
+ const settingsPath = path.join(this.claudeDir, 'settings.json');
180
+
181
+ if (!fs.existsSync(settingsPath)) {
182
+ console.log(colored('[!]', 'yellow'));
183
+ this.results.addCheck(
184
+ 'Claude Settings',
185
+ 'warning',
186
+ '~/.claude/settings.json not found',
187
+ 'Run: claude /login'
188
+ );
189
+ return;
190
+ }
191
+
192
+ // Validate JSON
193
+ try {
194
+ const content = fs.readFileSync(settingsPath, 'utf8');
195
+ JSON.parse(content);
196
+ console.log(colored('[OK]', 'green'));
197
+ this.results.addCheck('Claude Settings', 'success');
198
+ } catch (e) {
199
+ console.log(colored('[!]', 'yellow'));
200
+ this.results.addCheck(
201
+ 'Claude Settings',
202
+ 'warning',
203
+ `Invalid JSON: ${e.message}`,
204
+ 'Run: claude /login'
205
+ );
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Check 5: Profile configurations
211
+ */
212
+ checkProfiles() {
213
+ process.stdout.write('[?] Checking profiles... ');
214
+
215
+ const configPath = path.join(this.ccsDir, 'config.json');
216
+ if (!fs.existsSync(configPath)) {
217
+ console.log(colored('[SKIP]', 'yellow'));
218
+ return;
219
+ }
220
+
221
+ try {
222
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
223
+
224
+ if (!config.profiles || typeof config.profiles !== 'object') {
225
+ console.log(colored('[X]', 'red'));
226
+ this.results.addCheck(
227
+ 'Profiles',
228
+ 'error',
229
+ 'config.json missing profiles object',
230
+ 'Run: npm install -g @kaitranntt/ccs --force'
231
+ );
232
+ return;
233
+ }
234
+
235
+ const profileCount = Object.keys(config.profiles).length;
236
+ console.log(colored('[OK]', 'green'), `(${profileCount} profiles)`);
237
+ this.results.addCheck('Profiles', 'success', `${profileCount} profiles configured`);
238
+ } catch (e) {
239
+ console.log(colored('[X]', 'red'));
240
+ this.results.addCheck('Profiles', 'error', e.message);
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Check 6: Instance directories (account-based profiles)
246
+ */
247
+ checkInstances() {
248
+ process.stdout.write('[?] Checking instances... ');
249
+
250
+ const instancesDir = path.join(this.ccsDir, 'instances');
251
+ if (!fs.existsSync(instancesDir)) {
252
+ console.log(colored('[i]', 'cyan'), '(no account profiles)');
253
+ this.results.addCheck('Instances', 'success', 'No account profiles configured');
254
+ return;
255
+ }
256
+
257
+ const instances = fs.readdirSync(instancesDir).filter(name => {
258
+ return fs.statSync(path.join(instancesDir, name)).isDirectory();
259
+ });
260
+
261
+ if (instances.length === 0) {
262
+ console.log(colored('[i]', 'cyan'), '(no account profiles)');
263
+ this.results.addCheck('Instances', 'success', 'No account profiles');
264
+ return;
265
+ }
266
+
267
+ console.log(colored('[OK]', 'green'), `(${instances.length} instances)`);
268
+ this.results.addCheck('Instances', 'success', `${instances.length} account profiles`);
269
+ }
270
+
271
+ /**
272
+ * Check 7: File permissions
273
+ */
274
+ checkPermissions() {
275
+ process.stdout.write('[?] Checking permissions... ');
276
+
277
+ const testFile = path.join(this.ccsDir, '.permission-test');
278
+
279
+ try {
280
+ fs.writeFileSync(testFile, 'test', 'utf8');
281
+ fs.unlinkSync(testFile);
282
+ console.log(colored('[OK]', 'green'));
283
+ this.results.addCheck('Permissions', 'success');
284
+ } catch (e) {
285
+ console.log(colored('[X]', 'red'));
286
+ this.results.addCheck(
287
+ 'Permissions',
288
+ 'error',
289
+ 'Cannot write to ~/.ccs/',
290
+ 'Fix: sudo chown -R $USER ~/.ccs ~/.claude && chmod 755 ~/.ccs ~/.claude'
291
+ );
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Show health check report
297
+ */
298
+ showReport() {
299
+ console.log('');
300
+ console.log(colored('═══════════════════════════════════════════', 'cyan'));
301
+ console.log(colored('Health Check Report', 'bold'));
302
+ console.log(colored('═══════════════════════════════════════════', 'cyan'));
303
+ console.log('');
304
+
305
+ if (this.results.isHealthy() && !this.results.hasWarnings()) {
306
+ console.log(colored('✓ All checks passed!', 'green'));
307
+ console.log('');
308
+ console.log('Your CCS installation is healthy.');
309
+ console.log('');
310
+ return;
311
+ }
312
+
313
+ // Show errors
314
+ if (this.results.hasErrors()) {
315
+ console.log(colored('Errors:', 'red'));
316
+ this.results.errors.forEach(err => {
317
+ console.log(` [X] ${err.name}: ${err.message}`);
318
+ if (err.fix) {
319
+ console.log(` Fix: ${err.fix}`);
320
+ }
321
+ });
322
+ console.log('');
323
+ }
324
+
325
+ // Show warnings
326
+ if (this.results.hasWarnings()) {
327
+ console.log(colored('Warnings:', 'yellow'));
328
+ this.results.warnings.forEach(warn => {
329
+ console.log(` [!] ${warn.name}: ${warn.message}`);
330
+ if (warn.fix) {
331
+ console.log(` Fix: ${warn.fix}`);
332
+ }
333
+ });
334
+ console.log('');
335
+ }
336
+
337
+ // Summary
338
+ if (this.results.hasErrors()) {
339
+ console.log(colored('Status: Installation has errors', 'red'));
340
+ console.log('Run suggested fixes above to resolve issues.');
341
+ } else {
342
+ console.log(colored('Status: Installation healthy (warnings only)', 'green'));
343
+ }
344
+
345
+ console.log('');
346
+ }
347
+
348
+ /**
349
+ * Generate JSON report
350
+ */
351
+ generateJsonReport() {
352
+ return JSON.stringify({
353
+ timestamp: new Date().toISOString(),
354
+ platform: process.platform,
355
+ nodeVersion: process.version,
356
+ ccsVersion: require('../package.json').version,
357
+ checks: this.results.checks,
358
+ errors: this.results.errors,
359
+ warnings: this.results.warnings,
360
+ healthy: this.results.isHealthy()
361
+ }, null, 2);
362
+ }
363
+ }
364
+
365
+ module.exports = Doctor;
@@ -0,0 +1,159 @@
1
+ 'use strict';
2
+
3
+ const { colored } = require('./helpers');
4
+
5
+ /**
6
+ * Error types with structured messages
7
+ */
8
+ const ErrorTypes = {
9
+ NO_CLAUDE_CLI: 'NO_CLAUDE_CLI',
10
+ MISSING_SETTINGS: 'MISSING_SETTINGS',
11
+ INVALID_CONFIG: 'INVALID_CONFIG',
12
+ UNKNOWN_PROFILE: 'UNKNOWN_PROFILE',
13
+ PERMISSION_DENIED: 'PERMISSION_DENIED',
14
+ GENERIC: 'GENERIC'
15
+ };
16
+
17
+ /**
18
+ * Enhanced error manager with context-aware messages
19
+ */
20
+ class ErrorManager {
21
+ /**
22
+ * Show Claude CLI not found error
23
+ */
24
+ static showClaudeNotFound() {
25
+ console.error('');
26
+ console.error(colored('╔══════════════════════════════════════════════════════════╗', 'red'));
27
+ console.error(colored('║ ERROR: Claude CLI not found ║', 'red'));
28
+ console.error(colored('╚══════════════════════════════════════════════════════════╝', 'red'));
29
+ console.error('');
30
+ console.error('CCS requires Claude CLI to be installed.');
31
+ console.error('');
32
+ console.error(colored('Fix:', 'yellow'));
33
+ console.error(' 1. Install Claude CLI:');
34
+ console.error(' https://docs.claude.com/en/docs/claude-code/installation');
35
+ console.error('');
36
+ console.error(' 2. Verify installation:');
37
+ console.error(' command -v claude (Unix)');
38
+ console.error(' Get-Command claude (Windows)');
39
+ console.error('');
40
+ console.error(' 3. Custom path (if installed elsewhere):');
41
+ console.error(' export CCS_CLAUDE_PATH="/path/to/claude"');
42
+ console.error('');
43
+ console.error('Restart terminal after installation.');
44
+ console.error('');
45
+ }
46
+
47
+ /**
48
+ * Show settings file not found error
49
+ * @param {string} settingsPath - Path to missing settings file
50
+ */
51
+ static showSettingsNotFound(settingsPath) {
52
+ const isClaudeSettings = settingsPath.includes('.claude') && settingsPath.endsWith('settings.json');
53
+
54
+ console.error('');
55
+ console.error(colored('╔══════════════════════════════════════════════════════════╗', 'red'));
56
+ console.error(colored('║ ERROR: Settings file not found ║', 'red'));
57
+ console.error(colored('╚══════════════════════════════════════════════════════════╝', 'red'));
58
+ console.error('');
59
+ console.error(`File: ${settingsPath}`);
60
+ console.error('');
61
+
62
+ if (isClaudeSettings) {
63
+ console.error('This file is auto-created when you login to Claude CLI.');
64
+ console.error('');
65
+ console.error(colored('Fix (copy-paste):', 'yellow'));
66
+ console.error(` echo '{}' > ${settingsPath}`);
67
+ console.error(' claude /login');
68
+ console.error('');
69
+ console.error('Why: Newer Claude CLI versions require explicit login.');
70
+ } else {
71
+ console.error(colored('Fix (copy-paste):', 'yellow'));
72
+ console.error(' npm install -g @kaitranntt/ccs --force');
73
+ console.error('');
74
+ console.error('This will recreate missing profile settings.');
75
+ }
76
+
77
+ console.error('');
78
+ }
79
+
80
+ /**
81
+ * Show invalid configuration error
82
+ * @param {string} configPath - Path to invalid config
83
+ * @param {string} errorDetail - JSON parse error detail
84
+ */
85
+ static showInvalidConfig(configPath, errorDetail) {
86
+ console.error('');
87
+ console.error(colored('╔══════════════════════════════════════════════════════════╗', 'red'));
88
+ console.error(colored('║ ERROR: Configuration invalid ║', 'red'));
89
+ console.error(colored('╚══════════════════════════════════════════════════════════╝', 'red'));
90
+ console.error('');
91
+ console.error(`File: ${configPath}`);
92
+ console.error(`Issue: ${errorDetail}`);
93
+ console.error('');
94
+ console.error(colored('Fix (copy-paste):', 'yellow'));
95
+ console.error(' # Backup corrupted file');
96
+ console.error(` mv ${configPath} ${configPath}.backup`);
97
+ console.error('');
98
+ console.error(' # Reinstall CCS');
99
+ console.error(' npm install -g @kaitranntt/ccs --force');
100
+ console.error('');
101
+ console.error('Your profile settings will be preserved.');
102
+ console.error('');
103
+ }
104
+
105
+ /**
106
+ * Show profile not found error
107
+ * @param {string} profileName - Requested profile name
108
+ * @param {string[]} availableProfiles - List of available profiles
109
+ * @param {string} suggestion - Suggested profile name (fuzzy match)
110
+ */
111
+ static showProfileNotFound(profileName, availableProfiles, suggestion = null) {
112
+ console.error('');
113
+ console.error(colored('╔══════════════════════════════════════════════════════════╗', 'red'));
114
+ console.error(colored(`║ ERROR: Profile '${profileName}' not found${' '.repeat(Math.max(0, 35 - profileName.length))}║`, 'red'));
115
+ console.error(colored('╚══════════════════════════════════════════════════════════╝', 'red'));
116
+ console.error('');
117
+ console.error(colored('Available profiles:', 'cyan'));
118
+ availableProfiles.forEach(line => console.error(` ${line}`));
119
+ console.error('');
120
+ console.error(colored('Fix:', 'yellow'));
121
+ console.error(' # Use existing profile');
122
+ console.error(' ccs <profile> "your prompt"');
123
+ console.error('');
124
+ console.error(' # Create new account profile');
125
+ console.error(' ccs auth create <name>');
126
+ console.error('');
127
+
128
+ if (suggestion) {
129
+ console.error(colored(`Did you mean: ${suggestion}`, 'yellow'));
130
+ console.error('');
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Show permission denied error
136
+ * @param {string} path - Path with permission issue
137
+ */
138
+ static showPermissionDenied(path) {
139
+ console.error('');
140
+ console.error(colored('╔══════════════════════════════════════════════════════════╗', 'red'));
141
+ console.error(colored('║ ERROR: Permission denied ║', 'red'));
142
+ console.error(colored('╚══════════════════════════════════════════════════════════╝', 'red'));
143
+ console.error('');
144
+ console.error(`Cannot write to: ${path}`);
145
+ console.error('');
146
+ console.error(colored('Fix (copy-paste):', 'yellow'));
147
+ console.error(' # Fix ownership');
148
+ console.error(' sudo chown -R $USER ~/.ccs ~/.claude');
149
+ console.error('');
150
+ console.error(' # Fix permissions');
151
+ console.error(' chmod 755 ~/.ccs ~/.claude');
152
+ console.error('');
153
+ console.error(' # Retry installation');
154
+ console.error(' npm install -g @kaitranntt/ccs --force');
155
+ console.error('');
156
+ }
157
+ }
158
+
159
+ module.exports = { ErrorManager, ErrorTypes };