@kaitranntt/ccs 3.4.6 → 3.5.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
- 3.4.6
1
+ 3.5.0
@@ -1,10 +1,14 @@
1
1
  'use strict';
2
2
 
3
3
  const { spawn } = require('child_process');
4
+ const fs = require('fs');
5
+ const path = require('path');
4
6
  const ProfileRegistry = require('./profile-registry');
5
7
  const InstanceManager = require('../management/instance-manager');
6
8
  const { colored } = require('../utils/helpers');
7
9
  const { detectClaudeCli } = require('../utils/claude-detector');
10
+ const { InteractivePrompt } = require('../utils/prompt');
11
+ const CCS_VERSION = require('../../package.json').version;
8
12
 
9
13
  /**
10
14
  * Auth Commands (Simplified)
@@ -45,7 +49,10 @@ class AuthCommands {
45
49
  console.log(` ${colored('ccs "review code"', 'yellow')} # Use default profile`);
46
50
  console.log('');
47
51
  console.log(colored('Options:', 'cyan'));
48
- console.log(` ${colored('--force', 'yellow')} Allow overwriting existing profile`);
52
+ console.log(` ${colored('--force', 'yellow')} Allow overwriting existing profile (create)`);
53
+ console.log(` ${colored('--yes, -y', 'yellow')} Skip confirmation prompts (remove)`);
54
+ console.log(` ${colored('--json', 'yellow')} Output in JSON format (list, show)`);
55
+ console.log(` ${colored('--verbose', 'yellow')} Show additional details (list)`);
49
56
  console.log('');
50
57
  console.log(colored('Note:', 'cyan'));
51
58
  console.log(` By default, ${colored('ccs', 'yellow')} uses Claude CLI defaults from ~/.claude/`);
@@ -159,12 +166,37 @@ class AuthCommands {
159
166
  */
160
167
  async handleList(args) {
161
168
  const verbose = args.includes('--verbose');
169
+ const json = args.includes('--json');
162
170
 
163
171
  try {
164
172
  const profiles = this.registry.getAllProfiles();
165
173
  const defaultProfile = this.registry.getDefaultProfile();
166
174
  const profileNames = Object.keys(profiles);
167
175
 
176
+ // JSON output mode
177
+ if (json) {
178
+ const output = {
179
+ version: CCS_VERSION,
180
+ profiles: profileNames.map(name => {
181
+ const profile = profiles[name];
182
+ const isDefault = name === defaultProfile;
183
+ const instancePath = this.instanceMgr.getInstancePath(name);
184
+
185
+ return {
186
+ name: name,
187
+ type: profile.type || 'account',
188
+ is_default: isDefault,
189
+ created: profile.created,
190
+ last_used: profile.last_used || null,
191
+ instance_path: instancePath
192
+ };
193
+ })
194
+ };
195
+ console.log(JSON.stringify(output, null, 2));
196
+ return;
197
+ }
198
+
199
+ // Human-readable output
168
200
  if (profileNames.length === 0) {
169
201
  console.log(colored('No account profiles found', 'yellow'));
170
202
  console.log('');
@@ -234,11 +266,12 @@ class AuthCommands {
234
266
  */
235
267
  async handleShow(args) {
236
268
  const profileName = args.find(arg => !arg.startsWith('--'));
269
+ const json = args.includes('--json');
237
270
 
238
271
  if (!profileName) {
239
272
  console.error('[X] Profile name is required');
240
273
  console.log('');
241
- console.log(`Usage: ${colored('ccs auth show <profile>', 'yellow')}`);
274
+ console.log(`Usage: ${colored('ccs auth show <profile> [--json]', 'yellow')}`);
242
275
  process.exit(1);
243
276
  }
244
277
 
@@ -246,12 +279,41 @@ class AuthCommands {
246
279
  const profile = this.registry.getProfile(profileName);
247
280
  const defaultProfile = this.registry.getDefaultProfile();
248
281
  const isDefault = profileName === defaultProfile;
282
+ const instancePath = this.instanceMgr.getInstancePath(profileName);
283
+
284
+ // Count sessions
285
+ let sessionCount = 0;
286
+ try {
287
+ const sessionsDir = path.join(instancePath, 'session-env');
288
+ if (fs.existsSync(sessionsDir)) {
289
+ const files = fs.readdirSync(sessionsDir);
290
+ sessionCount = files.filter(f => f.endsWith('.json')).length;
291
+ }
292
+ } catch (e) {
293
+ // Ignore errors counting sessions
294
+ }
249
295
 
296
+ // JSON output mode
297
+ if (json) {
298
+ const output = {
299
+ name: profileName,
300
+ type: profile.type || 'account',
301
+ is_default: isDefault,
302
+ created: profile.created,
303
+ last_used: profile.last_used || null,
304
+ instance_path: instancePath,
305
+ session_count: sessionCount
306
+ };
307
+ console.log(JSON.stringify(output, null, 2));
308
+ return;
309
+ }
310
+
311
+ // Human-readable output
250
312
  console.log(colored(`Profile: ${profileName}`, 'bold'));
251
313
  console.log('');
252
314
  console.log(` Type: ${profile.type || 'account'}`);
253
315
  console.log(` Default: ${isDefault ? 'Yes' : 'No'}`);
254
- console.log(` Instance: ${this.instanceMgr.getInstancePath(profileName)}`);
316
+ console.log(` Instance: ${instancePath}`);
255
317
  console.log(` Created: ${new Date(profile.created).toLocaleString()}`);
256
318
 
257
319
  if (profile.last_used) {
@@ -273,13 +335,12 @@ class AuthCommands {
273
335
  * @param {Array} args - Command arguments
274
336
  */
275
337
  async handleRemove(args) {
276
- const profileName = args.find(arg => !arg.startsWith('--'));
277
- const force = args.includes('--force');
338
+ const profileName = args.find(arg => !arg.startsWith('--') && !arg.startsWith('-'));
278
339
 
279
340
  if (!profileName) {
280
341
  console.error('[X] Profile name is required');
281
342
  console.log('');
282
- console.log(`Usage: ${colored('ccs auth remove <profile> [--force]', 'yellow')}`);
343
+ console.log(`Usage: ${colored('ccs auth remove <profile> [--yes]', 'yellow')}`);
283
344
  process.exit(1);
284
345
  }
285
346
 
@@ -288,15 +349,39 @@ class AuthCommands {
288
349
  process.exit(1);
289
350
  }
290
351
 
291
- // Require --force for safety
292
- if (!force) {
293
- console.error('[X] Removal requires --force flag for safety');
352
+ try {
353
+ // Get instance path and session count for impact display
354
+ const instancePath = this.instanceMgr.getInstancePath(profileName);
355
+ let sessionCount = 0;
356
+
357
+ try {
358
+ const sessionsDir = path.join(instancePath, 'session-env');
359
+ if (fs.existsSync(sessionsDir)) {
360
+ const files = fs.readdirSync(sessionsDir);
361
+ sessionCount = files.filter(f => f.endsWith('.json')).length;
362
+ }
363
+ } catch (e) {
364
+ // Ignore errors counting sessions
365
+ }
366
+
367
+ // Display impact
368
+ console.log('');
369
+ console.log(`Profile '${colored(profileName, 'cyan')}' will be permanently deleted.`);
370
+ console.log(` Instance path: ${instancePath}`);
371
+ console.log(` Sessions: ${sessionCount} conversation${sessionCount !== 1 ? 's' : ''}`);
294
372
  console.log('');
295
- console.log(`Run: ${colored(`ccs auth remove ${profileName} --force`, 'yellow')}`);
296
- process.exit(1);
297
- }
298
373
 
299
- try {
374
+ // Interactive confirmation (or --yes flag)
375
+ const confirmed = await InteractivePrompt.confirm(
376
+ 'Delete this profile?',
377
+ { default: false } // Default to NO (safe)
378
+ );
379
+
380
+ if (!confirmed) {
381
+ console.log('[i] Cancelled');
382
+ process.exit(0);
383
+ }
384
+
300
385
  // Delete instance
301
386
  this.instanceMgr.deleteInstance(profileName);
302
387
 
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
+ const { findSimilarStrings } = require('../utils/helpers');
6
7
 
7
8
  /**
8
9
  * Profile Detector
@@ -85,12 +86,16 @@ class ProfileDetector {
85
86
  };
86
87
  }
87
88
 
88
- // Not found
89
- throw new Error(
90
- `Profile not found: ${profileName}\n` +
91
- `Available profiles:\n` +
92
- this._listAvailableProfiles()
93
- );
89
+ // Not found - generate suggestions
90
+ const allProfiles = this.getAllProfiles();
91
+ const allProfileNames = [...allProfiles.settings, ...allProfiles.accounts];
92
+ const suggestions = findSimilarStrings(profileName, allProfileNames);
93
+
94
+ const error = new Error(`Profile not found: ${profileName}`);
95
+ error.profileName = profileName;
96
+ error.suggestions = suggestions;
97
+ error.availableProfiles = this._listAvailableProfiles();
98
+ throw error;
94
99
  }
95
100
 
96
101
  /**
package/bin/ccs.js CHANGED
@@ -130,6 +130,7 @@ function handleHelpCommand() {
130
130
  console.log(colored('Flags:', 'cyan'));
131
131
  console.log(` ${colored('-h, --help', 'yellow')} Show this help message`);
132
132
  console.log(` ${colored('-v, --version', 'yellow')} Show version and installation info`);
133
+ console.log(` ${colored('--shell-completion', 'yellow')} Install shell auto-completion`);
133
134
  console.log('');
134
135
 
135
136
  // Configuration
@@ -149,6 +150,19 @@ function handleHelpCommand() {
149
150
  console.log(' Note: Commands, skills, and agents are symlinked across all profiles');
150
151
  console.log('');
151
152
 
153
+ // Examples
154
+ console.log(colored('Examples:', 'cyan'));
155
+ console.log(' Quick start:');
156
+ console.log(` ${colored('$ ccs', 'yellow')} # Use default account`);
157
+ console.log(` ${colored('$ ccs glm "implement API"', 'yellow')} # Cost-optimized model`);
158
+ console.log('');
159
+ console.log(' Multi-account workflow:');
160
+ console.log(` ${colored('$ ccs auth create work', 'yellow')} # Create work profile`);
161
+ console.log(` ${colored('$ ccs work "review PR"', 'yellow')} # Use work account`);
162
+ console.log('');
163
+ console.log(` For more: ${colored('https://github.com/kaitranntt/ccs#usage', 'cyan')}`);
164
+ console.log('');
165
+
152
166
  // Uninstall
153
167
  console.log(colored('Uninstall:', 'yellow'));
154
168
  console.log(' npm: npm uninstall -g @kaitranntt/ccs');
@@ -241,6 +255,10 @@ async function execClaudeWithProxy(claudeCli, profileName, args) {
241
255
  });
242
256
 
243
257
  // 3. Wait for proxy ready signal (with timeout)
258
+ const { ProgressIndicator } = require('./utils/progress-indicator');
259
+ const spinner = new ProgressIndicator('Starting GLMT proxy');
260
+ spinner.start();
261
+
244
262
  let port;
245
263
  try {
246
264
  port = await new Promise((resolve, reject) => {
@@ -268,8 +286,11 @@ async function execClaudeWithProxy(claudeCli, profileName, args) {
268
286
  }
269
287
  });
270
288
  });
289
+
290
+ spinner.succeed(`GLMT proxy ready on port ${port}`);
271
291
  } catch (error) {
272
- console.error('[X] Failed to start GLMT proxy:', error.message);
292
+ spinner.fail('Failed to start GLMT proxy');
293
+ console.error('[X] Error:', error.message);
273
294
  console.error('');
274
295
  console.error('Possible causes:');
275
296
  console.error(' 1. Port conflict (unlikely with random port)');
@@ -341,6 +362,58 @@ async function execClaudeWithProxy(claudeCli, profileName, args) {
341
362
  });
342
363
  }
343
364
 
365
+ /**
366
+ * Handle shell completion installation
367
+ */
368
+ async function handleShellCompletionCommand(args) {
369
+ const { ShellCompletionInstaller } = require('./utils/shell-completion');
370
+ const { colored } = require('./utils/helpers');
371
+
372
+ console.log(colored('Shell Completion Installer', 'bold'));
373
+ console.log('');
374
+
375
+ // Parse flags
376
+ let targetShell = null;
377
+ if (args.includes('--bash')) targetShell = 'bash';
378
+ else if (args.includes('--zsh')) targetShell = 'zsh';
379
+ else if (args.includes('--fish')) targetShell = 'fish';
380
+ else if (args.includes('--powershell')) targetShell = 'powershell';
381
+
382
+ try {
383
+ const installer = new ShellCompletionInstaller();
384
+ const result = installer.install(targetShell);
385
+
386
+ if (result.alreadyInstalled) {
387
+ console.log(colored('[OK] Shell completion already installed', 'green'));
388
+ console.log('');
389
+ return;
390
+ }
391
+
392
+ console.log(colored('[OK] Shell completion installed successfully!', 'green'));
393
+ console.log('');
394
+ console.log(result.message);
395
+ console.log('');
396
+ console.log(colored('To activate:', 'cyan'));
397
+ console.log(` ${result.reload}`);
398
+ console.log('');
399
+ console.log(colored('Then test:', 'cyan'));
400
+ console.log(' ccs <TAB> # See available profiles');
401
+ console.log(' ccs auth <TAB> # See auth subcommands');
402
+ console.log('');
403
+ } catch (error) {
404
+ console.error(colored('[X] Error:', 'red'), error.message);
405
+ console.error('');
406
+ console.error(colored('Usage:', 'yellow'));
407
+ console.error(' ccs --shell-completion # Auto-detect shell');
408
+ console.error(' ccs --shell-completion --bash # Install for bash');
409
+ console.error(' ccs --shell-completion --zsh # Install for zsh');
410
+ console.error(' ccs --shell-completion --fish # Install for fish');
411
+ console.error(' ccs --shell-completion --powershell # Install for PowerShell');
412
+ console.error('');
413
+ process.exit(1);
414
+ }
415
+ }
416
+
344
417
  // Main execution
345
418
  async function main() {
346
419
  const args = process.argv.slice(2);
@@ -369,6 +442,12 @@ async function main() {
369
442
  return;
370
443
  }
371
444
 
445
+ // Special case: shell completion installer
446
+ if (firstArg === '--shell-completion') {
447
+ await handleShellCompletionCommand(args.slice(1));
448
+ return;
449
+ }
450
+
372
451
  // Special case: doctor command
373
452
  if (firstArg === 'doctor' || firstArg === '--doctor') {
374
453
  await handleDoctorCommand();
@@ -443,7 +522,13 @@ async function main() {
443
522
  execClaude(claudeCli, remainingArgs);
444
523
  }
445
524
  } catch (error) {
446
- console.error(`[X] ${error.message}`);
525
+ // Check if this is a profile not found error with suggestions
526
+ if (error.profileName && error.availableProfiles !== undefined) {
527
+ const allProfiles = error.availableProfiles.split('\n');
528
+ ErrorManager.showProfileNotFound(error.profileName, allProfiles, error.suggestions);
529
+ } else {
530
+ console.error(`[X] ${error.message}`);
531
+ }
447
532
  process.exit(1);
448
533
  }
449
534
  }
@@ -0,0 +1,59 @@
1
+ // CCS Error Codes
2
+ // Documentation: ../../docs/errors/README.md
3
+
4
+ const ERROR_CODES = {
5
+ // Configuration Errors (E100-E199)
6
+ CONFIG_MISSING: 'E101',
7
+ CONFIG_INVALID_JSON: 'E102',
8
+ CONFIG_INVALID_PROFILE: 'E103',
9
+
10
+ // Profile Management Errors (E200-E299)
11
+ PROFILE_NOT_FOUND: 'E104',
12
+ PROFILE_ALREADY_EXISTS: 'E105',
13
+ PROFILE_CANNOT_DELETE_DEFAULT: 'E106',
14
+ PROFILE_INVALID_NAME: 'E107',
15
+
16
+ // Claude CLI Detection Errors (E300-E399)
17
+ CLAUDE_NOT_FOUND: 'E301',
18
+ CLAUDE_VERSION_INCOMPATIBLE: 'E302',
19
+ CLAUDE_EXECUTION_FAILED: 'E303',
20
+
21
+ // Network/API Errors (E400-E499)
22
+ GLMT_PROXY_TIMEOUT: 'E401',
23
+ API_KEY_MISSING: 'E402',
24
+ API_AUTH_FAILED: 'E403',
25
+ API_RATE_LIMIT: 'E404',
26
+
27
+ // File System Errors (E500-E599)
28
+ FS_CANNOT_CREATE_DIR: 'E501',
29
+ FS_CANNOT_WRITE_FILE: 'E502',
30
+ FS_CANNOT_READ_FILE: 'E503',
31
+ FS_INSTANCE_NOT_FOUND: 'E504',
32
+
33
+ // Internal Errors (E900-E999)
34
+ INTERNAL_ERROR: 'E900',
35
+ INVALID_STATE: 'E901'
36
+ };
37
+
38
+ // Error code documentation URL generator
39
+ function getErrorDocUrl(errorCode) {
40
+ return `https://github.com/kaitranntt/ccs/blob/main/docs/errors/README.md#${errorCode.toLowerCase()}`;
41
+ }
42
+
43
+ // Get error category from code
44
+ function getErrorCategory(errorCode) {
45
+ const code = parseInt(errorCode.substring(1));
46
+ if (code >= 100 && code < 200) return 'Configuration';
47
+ if (code >= 200 && code < 300) return 'Profile Management';
48
+ if (code >= 300 && code < 400) return 'Claude CLI Detection';
49
+ if (code >= 400 && code < 500) return 'Network/API';
50
+ if (code >= 500 && code < 600) return 'File System';
51
+ if (code >= 900 && code < 1000) return 'Internal';
52
+ return 'Unknown';
53
+ }
54
+
55
+ module.exports = {
56
+ ERROR_CODES,
57
+ getErrorDocUrl,
58
+ getErrorCategory
59
+ };
@@ -1,9 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  const { colored } = require('./helpers');
4
+ const { ERROR_CODES, getErrorDocUrl } = require('./error-codes');
4
5
 
5
6
  /**
6
- * Error types with structured messages
7
+ * Error types with structured messages (Legacy - kept for compatibility)
7
8
  */
8
9
  const ErrorTypes = {
9
10
  NO_CLAUDE_CLI: 'NO_CLAUDE_CLI',
@@ -18,18 +19,26 @@ const ErrorTypes = {
18
19
  * Enhanced error manager with context-aware messages
19
20
  */
20
21
  class ErrorManager {
22
+ /**
23
+ * Show error code and documentation URL
24
+ * @param {string} errorCode - Error code (e.g., E301)
25
+ */
26
+ static showErrorCode(errorCode) {
27
+ console.error(colored(`Error: ${errorCode}`, 'yellow'));
28
+ console.error(colored(getErrorDocUrl(errorCode), 'yellow'));
29
+ console.error('');
30
+ }
31
+
21
32
  /**
22
33
  * Show Claude CLI not found error
23
34
  */
24
35
  static showClaudeNotFound() {
25
36
  console.error('');
26
- console.error(colored('╔══════════════════════════════════════════════════════════╗', 'red'));
27
- console.error(colored('║ ERROR: Claude CLI not found ║', 'red'));
28
- console.error(colored('╚══════════════════════════════════════════════════════════╝', 'red'));
37
+ console.error(colored('[X] Claude CLI not found', 'red'));
29
38
  console.error('');
30
- console.error('CCS requires Claude CLI to be installed.');
39
+ console.error('CCS requires Claude CLI to be installed and available in PATH.');
31
40
  console.error('');
32
- console.error(colored('Fix:', 'yellow'));
41
+ console.error(colored('Solutions:', 'yellow'));
33
42
  console.error(' 1. Install Claude CLI:');
34
43
  console.error(' https://docs.claude.com/en/docs/claude-code/installation');
35
44
  console.error('');
@@ -40,8 +49,7 @@ class ErrorManager {
40
49
  console.error(' 3. Custom path (if installed elsewhere):');
41
50
  console.error(' export CCS_CLAUDE_PATH="/path/to/claude"');
42
51
  console.error('');
43
- console.error('Restart terminal after installation.');
44
- console.error('');
52
+ this.showErrorCode(ERROR_CODES.CLAUDE_NOT_FOUND);
45
53
  }
46
54
 
47
55
  /**
@@ -52,9 +60,7 @@ class ErrorManager {
52
60
  const isClaudeSettings = settingsPath.includes('.claude') && settingsPath.endsWith('settings.json');
53
61
 
54
62
  console.error('');
55
- console.error(colored('╔══════════════════════════════════════════════════════════╗', 'red'));
56
- console.error(colored('║ ERROR: Settings file not found ║', 'red'));
57
- console.error(colored('╚══════════════════════════════════════════════════════════╝', 'red'));
63
+ console.error(colored('[X] Settings file not found', 'red'));
58
64
  console.error('');
59
65
  console.error(`File: ${settingsPath}`);
60
66
  console.error('');
@@ -62,19 +68,20 @@ class ErrorManager {
62
68
  if (isClaudeSettings) {
63
69
  console.error('This file is auto-created when you login to Claude CLI.');
64
70
  console.error('');
65
- console.error(colored('Fix (copy-paste):', 'yellow'));
71
+ console.error(colored('Solutions:', 'yellow'));
66
72
  console.error(` echo '{}' > ${settingsPath}`);
67
73
  console.error(' claude /login');
68
74
  console.error('');
69
75
  console.error('Why: Newer Claude CLI versions require explicit login.');
70
76
  } else {
71
- console.error(colored('Fix (copy-paste):', 'yellow'));
77
+ console.error(colored('Solutions:', 'yellow'));
72
78
  console.error(' npm install -g @kaitranntt/ccs --force');
73
79
  console.error('');
74
80
  console.error('This will recreate missing profile settings.');
75
81
  }
76
82
 
77
83
  console.error('');
84
+ this.showErrorCode(ERROR_CODES.CONFIG_INVALID_PROFILE);
78
85
  }
79
86
 
80
87
  /**
@@ -84,14 +91,12 @@ class ErrorManager {
84
91
  */
85
92
  static showInvalidConfig(configPath, errorDetail) {
86
93
  console.error('');
87
- console.error(colored('╔══════════════════════════════════════════════════════════╗', 'red'));
88
- console.error(colored('║ ERROR: Configuration invalid ║', 'red'));
89
- console.error(colored('╚══════════════════════════════════════════════════════════╝', 'red'));
94
+ console.error(colored('[X] Configuration invalid', 'red'));
90
95
  console.error('');
91
96
  console.error(`File: ${configPath}`);
92
97
  console.error(`Issue: ${errorDetail}`);
93
98
  console.error('');
94
- console.error(colored('Fix (copy-paste):', 'yellow'));
99
+ console.error(colored('Solutions:', 'yellow'));
95
100
  console.error(' # Backup corrupted file');
96
101
  console.error(` mv ${configPath} ${configPath}.backup`);
97
102
  console.error('');
@@ -100,35 +105,37 @@ class ErrorManager {
100
105
  console.error('');
101
106
  console.error('Your profile settings will be preserved.');
102
107
  console.error('');
108
+ this.showErrorCode(ERROR_CODES.CONFIG_INVALID_JSON);
103
109
  }
104
110
 
105
111
  /**
106
112
  * Show profile not found error
107
113
  * @param {string} profileName - Requested profile name
108
114
  * @param {string[]} availableProfiles - List of available profiles
109
- * @param {string} suggestion - Suggested profile name (fuzzy match)
115
+ * @param {string[]} suggestions - Suggested profile names (fuzzy match)
110
116
  */
111
- static showProfileNotFound(profileName, availableProfiles, suggestion = null) {
117
+ static showProfileNotFound(profileName, availableProfiles, suggestions = []) {
112
118
  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'));
119
+ console.error(colored(`[X] Profile '${profileName}' not found`, 'red'));
116
120
  console.error('');
121
+
122
+ if (suggestions && suggestions.length > 0) {
123
+ console.error(colored('Did you mean:', 'yellow'));
124
+ suggestions.forEach(s => console.error(` ${s}`));
125
+ console.error('');
126
+ }
127
+
117
128
  console.error(colored('Available profiles:', 'cyan'));
118
129
  availableProfiles.forEach(line => console.error(` ${line}`));
119
130
  console.error('');
120
- console.error(colored('Fix:', 'yellow'));
131
+ console.error(colored('Solutions:', 'yellow'));
121
132
  console.error(' # Use existing profile');
122
133
  console.error(' ccs <profile> "your prompt"');
123
134
  console.error('');
124
135
  console.error(' # Create new account profile');
125
136
  console.error(' ccs auth create <name>');
126
137
  console.error('');
127
-
128
- if (suggestion) {
129
- console.error(colored(`Did you mean: ${suggestion}`, 'yellow'));
130
- console.error('');
131
- }
138
+ this.showErrorCode(ERROR_CODES.PROFILE_NOT_FOUND);
132
139
  }
133
140
 
134
141
  /**
@@ -137,13 +144,11 @@ class ErrorManager {
137
144
  */
138
145
  static showPermissionDenied(path) {
139
146
  console.error('');
140
- console.error(colored('╔══════════════════════════════════════════════════════════╗', 'red'));
141
- console.error(colored('║ ERROR: Permission denied ║', 'red'));
142
- console.error(colored('╚══════════════════════════════════════════════════════════╝', 'red'));
147
+ console.error(colored('[X] Permission denied', 'red'));
143
148
  console.error('');
144
149
  console.error(`Cannot write to: ${path}`);
145
150
  console.error('');
146
- console.error(colored('Fix (copy-paste):', 'yellow'));
151
+ console.error(colored('Solutions:', 'yellow'));
147
152
  console.error(' # Fix ownership');
148
153
  console.error(' sudo chown -R $USER ~/.ccs ~/.claude');
149
154
  console.error('');
@@ -153,6 +158,7 @@ class ErrorManager {
153
158
  console.error(' # Retry installation');
154
159
  console.error(' npm install -g @kaitranntt/ccs --force');
155
160
  console.error('');
161
+ this.showErrorCode(ERROR_CODES.FS_CANNOT_WRITE_FILE);
156
162
  }
157
163
  }
158
164
 
@@ -63,10 +63,74 @@ function expandPath(pathStr) {
63
63
  return path.normalize(pathStr);
64
64
  }
65
65
 
66
+ /**
67
+ * Calculate Levenshtein distance between two strings
68
+ * @param {string} a - First string
69
+ * @param {string} b - Second string
70
+ * @returns {number} Edit distance
71
+ */
72
+ function levenshteinDistance(a, b) {
73
+ if (a.length === 0) return b.length;
74
+ if (b.length === 0) return a.length;
75
+
76
+ const matrix = [];
77
+
78
+ // Initialize first row and column
79
+ for (let i = 0; i <= b.length; i++) {
80
+ matrix[i] = [i];
81
+ }
82
+
83
+ for (let j = 0; j <= a.length; j++) {
84
+ matrix[0][j] = j;
85
+ }
86
+
87
+ // Fill in the rest of the matrix
88
+ for (let i = 1; i <= b.length; i++) {
89
+ for (let j = 1; j <= a.length; j++) {
90
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
91
+ matrix[i][j] = matrix[i - 1][j - 1];
92
+ } else {
93
+ matrix[i][j] = Math.min(
94
+ matrix[i - 1][j - 1] + 1, // substitution
95
+ matrix[i][j - 1] + 1, // insertion
96
+ matrix[i - 1][j] + 1 // deletion
97
+ );
98
+ }
99
+ }
100
+ }
101
+
102
+ return matrix[b.length][a.length];
103
+ }
104
+
105
+ /**
106
+ * Find similar strings using fuzzy matching
107
+ * @param {string} target - Target string
108
+ * @param {string[]} candidates - List of candidate strings
109
+ * @param {number} maxDistance - Maximum edit distance (default: 2)
110
+ * @returns {string[]} Similar strings sorted by distance
111
+ */
112
+ function findSimilarStrings(target, candidates, maxDistance = 2) {
113
+ const targetLower = target.toLowerCase();
114
+
115
+ const matches = candidates
116
+ .map(candidate => ({
117
+ name: candidate,
118
+ distance: levenshteinDistance(targetLower, candidate.toLowerCase())
119
+ }))
120
+ .filter(item => item.distance <= maxDistance && item.distance > 0)
121
+ .sort((a, b) => a.distance - b.distance)
122
+ .slice(0, 3) // Show at most 3 suggestions
123
+ .map(item => item.name);
124
+
125
+ return matches;
126
+ }
127
+
66
128
 
67
129
  module.exports = {
68
130
  colors,
69
131
  colored,
70
132
  error,
71
- expandPath
133
+ expandPath,
134
+ levenshteinDistance,
135
+ findSimilarStrings
72
136
  };