@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.
- package/.claude/agents/ccs-delegator.md +117 -0
- package/.claude/commands/ccs/glm/continue.md +22 -0
- package/.claude/commands/ccs/glm.md +22 -0
- package/.claude/commands/ccs/kimi/continue.md +22 -0
- package/.claude/commands/ccs/kimi.md +22 -0
- package/.claude/skills/ccs-delegation/SKILL.md +54 -0
- package/.claude/skills/ccs-delegation/references/README.md +24 -0
- package/.claude/skills/ccs-delegation/references/delegation-guidelines.md +99 -0
- package/.claude/skills/ccs-delegation/references/headless-workflow.md +174 -0
- package/.claude/skills/ccs-delegation/references/troubleshooting.md +268 -0
- package/README.ja.md +470 -146
- package/README.md +532 -145
- package/README.vi.md +484 -157
- package/VERSION +1 -1
- package/bin/auth/auth-commands.js +98 -13
- package/bin/auth/profile-detector.js +11 -6
- package/bin/ccs.js +148 -2
- package/bin/delegation/README.md +189 -0
- package/bin/delegation/delegation-handler.js +212 -0
- package/bin/delegation/headless-executor.js +617 -0
- package/bin/delegation/result-formatter.js +483 -0
- package/bin/delegation/session-manager.js +156 -0
- package/bin/delegation/settings-parser.js +109 -0
- package/bin/management/doctor.js +94 -1
- package/bin/utils/claude-symlink-manager.js +238 -0
- package/bin/utils/delegation-validator.js +154 -0
- package/bin/utils/error-codes.js +59 -0
- package/bin/utils/error-manager.js +38 -32
- package/bin/utils/helpers.js +65 -1
- package/bin/utils/progress-indicator.js +111 -0
- package/bin/utils/prompt.js +134 -0
- package/bin/utils/shell-completion.js +234 -0
- package/lib/ccs +575 -25
- package/lib/ccs.ps1 +381 -20
- package/lib/error-codes.ps1 +55 -0
- package/lib/error-codes.sh +63 -0
- package/lib/progress-indicator.ps1 +120 -0
- package/lib/progress-indicator.sh +117 -0
- package/lib/prompt.ps1 +109 -0
- package/lib/prompt.sh +99 -0
- package/package.json +2 -1
- package/scripts/completion/README.md +308 -0
- package/scripts/completion/ccs.bash +81 -0
- package/scripts/completion/ccs.fish +92 -0
- package/scripts/completion/ccs.ps1 +157 -0
- package/scripts/completion/ccs.zsh +130 -0
- 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('
|
|
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('
|
|
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
|
-
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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}
|
|
115
|
+
* @param {string[]} suggestions - Suggested profile names (fuzzy match)
|
|
110
116
|
*/
|
|
111
|
-
static showProfileNotFound(profileName, availableProfiles,
|
|
117
|
+
static showProfileNotFound(profileName, availableProfiles, suggestions = []) {
|
|
112
118
|
console.error('');
|
|
113
|
-
console.error(colored('
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
|
package/bin/utils/helpers.js
CHANGED
|
@@ -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 };
|