@kaitranntt/ccs 3.4.6 → 4.1.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.
Files changed (47) hide show
  1. package/.claude/agents/ccs-delegator.md +117 -0
  2. package/.claude/commands/ccs/glm/continue.md +22 -0
  3. package/.claude/commands/ccs/glm.md +22 -0
  4. package/.claude/commands/ccs/kimi/continue.md +22 -0
  5. package/.claude/commands/ccs/kimi.md +22 -0
  6. package/.claude/skills/ccs-delegation/SKILL.md +54 -0
  7. package/.claude/skills/ccs-delegation/references/README.md +24 -0
  8. package/.claude/skills/ccs-delegation/references/delegation-guidelines.md +99 -0
  9. package/.claude/skills/ccs-delegation/references/headless-workflow.md +174 -0
  10. package/.claude/skills/ccs-delegation/references/troubleshooting.md +268 -0
  11. package/README.ja.md +470 -146
  12. package/README.md +532 -145
  13. package/README.vi.md +484 -157
  14. package/VERSION +1 -1
  15. package/bin/auth/auth-commands.js +98 -13
  16. package/bin/auth/profile-detector.js +11 -6
  17. package/bin/ccs.js +148 -2
  18. package/bin/delegation/README.md +189 -0
  19. package/bin/delegation/delegation-handler.js +212 -0
  20. package/bin/delegation/headless-executor.js +617 -0
  21. package/bin/delegation/result-formatter.js +483 -0
  22. package/bin/delegation/session-manager.js +156 -0
  23. package/bin/delegation/settings-parser.js +109 -0
  24. package/bin/management/doctor.js +94 -1
  25. package/bin/utils/claude-symlink-manager.js +238 -0
  26. package/bin/utils/delegation-validator.js +154 -0
  27. package/bin/utils/error-codes.js +59 -0
  28. package/bin/utils/error-manager.js +38 -32
  29. package/bin/utils/helpers.js +65 -1
  30. package/bin/utils/progress-indicator.js +111 -0
  31. package/bin/utils/prompt.js +134 -0
  32. package/bin/utils/shell-completion.js +234 -0
  33. package/lib/ccs +575 -25
  34. package/lib/ccs.ps1 +381 -20
  35. package/lib/error-codes.ps1 +55 -0
  36. package/lib/error-codes.sh +63 -0
  37. package/lib/progress-indicator.ps1 +120 -0
  38. package/lib/progress-indicator.sh +117 -0
  39. package/lib/prompt.ps1 +109 -0
  40. package/lib/prompt.sh +99 -0
  41. package/package.json +2 -1
  42. package/scripts/completion/README.md +308 -0
  43. package/scripts/completion/ccs.bash +81 -0
  44. package/scripts/completion/ccs.fish +92 -0
  45. package/scripts/completion/ccs.ps1 +157 -0
  46. package/scripts/completion/ccs.zsh +130 -0
  47. package/scripts/postinstall.js +35 -0
@@ -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
  };
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Simple Progress Indicator (no external dependencies)
5
+ *
6
+ * Features:
7
+ * - ASCII-only spinner frames (cross-platform compatible)
8
+ * - TTY detection (no spinners in pipes/logs)
9
+ * - Elapsed time display
10
+ * - CI environment detection
11
+ */
12
+
13
+ class ProgressIndicator {
14
+ /**
15
+ * Create a progress indicator
16
+ * @param {string} message - Message to display
17
+ * @param {Object} options - Options
18
+ * @param {string[]} options.frames - Spinner frames (default: ASCII)
19
+ * @param {number} options.interval - Frame interval in ms (default: 80)
20
+ */
21
+ constructor(message, options = {}) {
22
+ this.message = message;
23
+ // ASCII-only frames for cross-platform compatibility
24
+ this.frames = options.frames || ['|', '/', '-', '\\'];
25
+ this.frameIndex = 0;
26
+ this.interval = null;
27
+ this.startTime = Date.now();
28
+
29
+ // TTY detection: only animate if stderr is TTY and not in CI
30
+ this.isTTY = process.stderr.isTTY === true && !process.env.CI && !process.env.NO_COLOR;
31
+ }
32
+
33
+ /**
34
+ * Start the spinner
35
+ */
36
+ start() {
37
+ if (!this.isTTY) {
38
+ // Non-TTY: just print message once
39
+ process.stderr.write(`[i] ${this.message}...\n`);
40
+ return;
41
+ }
42
+
43
+ // TTY: animate spinner
44
+ this.interval = setInterval(() => {
45
+ const frame = this.frames[this.frameIndex];
46
+ const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
47
+ process.stderr.write(`\r[${frame}] ${this.message}... (${elapsed}s)`);
48
+ this.frameIndex = (this.frameIndex + 1) % this.frames.length;
49
+ }, 80); // 12.5fps for smooth animation
50
+ }
51
+
52
+ /**
53
+ * Stop spinner with success message
54
+ * @param {string} message - Optional success message (defaults to original message)
55
+ */
56
+ succeed(message) {
57
+ this.stop();
58
+ const finalMessage = message || this.message;
59
+ const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
60
+
61
+ if (this.isTTY) {
62
+ // Clear spinner line and show success
63
+ process.stderr.write(`\r[OK] ${finalMessage} (${elapsed}s)\n`);
64
+ } else {
65
+ // Non-TTY: just show completion
66
+ process.stderr.write(`[OK] ${finalMessage}\n`);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Stop spinner with failure message
72
+ * @param {string} message - Optional failure message (defaults to original message)
73
+ */
74
+ fail(message) {
75
+ this.stop();
76
+ const finalMessage = message || this.message;
77
+
78
+ if (this.isTTY) {
79
+ // Clear spinner line and show failure
80
+ process.stderr.write(`\r[X] ${finalMessage}\n`);
81
+ } else {
82
+ // Non-TTY: just show failure
83
+ process.stderr.write(`[X] ${finalMessage}\n`);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Update spinner message (while running)
89
+ * @param {string} newMessage - New message to display
90
+ */
91
+ update(newMessage) {
92
+ this.message = newMessage;
93
+ }
94
+
95
+ /**
96
+ * Stop the spinner without showing success/failure
97
+ */
98
+ stop() {
99
+ if (this.interval) {
100
+ clearInterval(this.interval);
101
+ this.interval = null;
102
+
103
+ if (this.isTTY) {
104
+ // Clear the spinner line
105
+ process.stderr.write('\r\x1b[K');
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ module.exports = { ProgressIndicator };
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+
3
+ const readline = require('readline');
4
+
5
+ /**
6
+ * Interactive Prompt Utilities (NO external dependencies)
7
+ *
8
+ * Features:
9
+ * - TTY detection (auto-confirm in non-TTY)
10
+ * - --yes flag support for automation
11
+ * - --no-input flag support for CI
12
+ * - Safe defaults (N for destructive actions)
13
+ * - Input validation with retry
14
+ */
15
+
16
+ class InteractivePrompt {
17
+ /**
18
+ * Ask for confirmation
19
+ * @param {string} message - Confirmation message
20
+ * @param {Object} options - Options
21
+ * @param {boolean} options.default - Default value (true=Yes, false=No)
22
+ * @returns {Promise<boolean>} User confirmation
23
+ */
24
+ static async confirm(message, options = {}) {
25
+ const { default: defaultValue = false } = options;
26
+
27
+ // Check for --yes flag (automation) - always returns true
28
+ if (process.env.CCS_YES === '1' || process.argv.includes('--yes') || process.argv.includes('-y')) {
29
+ return true;
30
+ }
31
+
32
+ // Check for --no-input flag (CI)
33
+ if (process.env.CCS_NO_INPUT === '1' || process.argv.includes('--no-input')) {
34
+ throw new Error('Interactive input required but --no-input specified');
35
+ }
36
+
37
+ // Non-TTY: use default
38
+ if (!process.stdin.isTTY) {
39
+ return defaultValue;
40
+ }
41
+
42
+ // Interactive prompt
43
+ const rl = readline.createInterface({
44
+ input: process.stdin,
45
+ output: process.stderr,
46
+ terminal: true
47
+ });
48
+
49
+ const promptText = defaultValue
50
+ ? `${message} [Y/n]: `
51
+ : `${message} [y/N]: `;
52
+
53
+ return new Promise((resolve) => {
54
+ rl.question(promptText, (answer) => {
55
+ rl.close();
56
+
57
+ const normalized = answer.trim().toLowerCase();
58
+
59
+ // Empty answer: use default
60
+ if (normalized === '') {
61
+ resolve(defaultValue);
62
+ return;
63
+ }
64
+
65
+ // Valid answers
66
+ if (normalized === 'y' || normalized === 'yes') {
67
+ resolve(true);
68
+ return;
69
+ }
70
+
71
+ if (normalized === 'n' || normalized === 'no') {
72
+ resolve(false);
73
+ return;
74
+ }
75
+
76
+ // Invalid input: retry
77
+ console.error('[!] Please answer y or n');
78
+ resolve(InteractivePrompt.confirm(message, options));
79
+ });
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Get text input from user
85
+ * @param {string} message - Prompt message
86
+ * @param {Object} options - Options
87
+ * @param {string} options.default - Default value
88
+ * @param {Function} options.validate - Validation function
89
+ * @returns {Promise<string>} User input
90
+ */
91
+ static async input(message, options = {}) {
92
+ const { default: defaultValue = '', validate = null } = options;
93
+
94
+ // Non-TTY: use default or error
95
+ if (!process.stdin.isTTY) {
96
+ if (defaultValue) {
97
+ return defaultValue;
98
+ }
99
+ throw new Error('Interactive input required but stdin is not a TTY');
100
+ }
101
+
102
+ const rl = readline.createInterface({
103
+ input: process.stdin,
104
+ output: process.stderr,
105
+ terminal: true
106
+ });
107
+
108
+ const promptText = defaultValue
109
+ ? `${message} [${defaultValue}]: `
110
+ : `${message}: `;
111
+
112
+ return new Promise((resolve) => {
113
+ rl.question(promptText, (answer) => {
114
+ rl.close();
115
+
116
+ const value = answer.trim() || defaultValue;
117
+
118
+ // Validate input if validator provided
119
+ if (validate) {
120
+ const error = validate(value);
121
+ if (error) {
122
+ console.error(`[!] ${error}`);
123
+ resolve(InteractivePrompt.input(message, options));
124
+ return;
125
+ }
126
+ }
127
+
128
+ resolve(value);
129
+ });
130
+ });
131
+ }
132
+ }
133
+
134
+ module.exports = { InteractivePrompt };