@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.
@@ -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 };
@@ -0,0 +1,234 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { execSync } = require('child_process');
7
+
8
+ /**
9
+ * Shell Completion Installer
10
+ * Auto-configures shell completion for bash, zsh, fish, PowerShell
11
+ */
12
+
13
+ class ShellCompletionInstaller {
14
+ constructor() {
15
+ this.homeDir = os.homedir();
16
+ this.ccsDir = path.join(this.homeDir, '.ccs');
17
+ this.completionDir = path.join(this.ccsDir, 'completions');
18
+ this.scriptsDir = path.join(__dirname, '../../scripts/completion');
19
+ }
20
+
21
+ /**
22
+ * Detect current shell
23
+ */
24
+ detectShell() {
25
+ const shell = process.env.SHELL || '';
26
+
27
+ if (shell.includes('bash')) return 'bash';
28
+ if (shell.includes('zsh')) return 'zsh';
29
+ if (shell.includes('fish')) return 'fish';
30
+ if (process.platform === 'win32') return 'powershell';
31
+
32
+ return null;
33
+ }
34
+
35
+ /**
36
+ * Ensure completion files are in ~/.ccs/completions/
37
+ */
38
+ ensureCompletionFiles() {
39
+ if (!fs.existsSync(this.completionDir)) {
40
+ fs.mkdirSync(this.completionDir, { recursive: true });
41
+ }
42
+
43
+ // Copy completion scripts
44
+ const files = ['ccs.bash', 'ccs.zsh', 'ccs.fish', 'ccs.ps1'];
45
+ files.forEach(file => {
46
+ const src = path.join(this.scriptsDir, file);
47
+ const dest = path.join(this.completionDir, file);
48
+
49
+ if (fs.existsSync(src)) {
50
+ fs.copyFileSync(src, dest);
51
+ }
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Install bash completion
57
+ */
58
+ installBash() {
59
+ const rcFile = path.join(this.homeDir, '.bashrc');
60
+ const completionPath = path.join(this.completionDir, 'ccs.bash');
61
+
62
+ if (!fs.existsSync(completionPath)) {
63
+ throw new Error('Completion file not found. Please reinstall CCS.');
64
+ }
65
+
66
+ const marker = '# CCS shell completion';
67
+ const sourceCmd = `source "${completionPath}"`;
68
+ const block = `\n${marker}\n${sourceCmd}\n`;
69
+
70
+ // Check if already installed
71
+ if (fs.existsSync(rcFile)) {
72
+ const content = fs.readFileSync(rcFile, 'utf8');
73
+ if (content.includes(marker)) {
74
+ return { success: true, alreadyInstalled: true };
75
+ }
76
+ }
77
+
78
+ // Append to .bashrc
79
+ fs.appendFileSync(rcFile, block);
80
+
81
+ return {
82
+ success: true,
83
+ message: `Added to ${rcFile}`,
84
+ reload: 'source ~/.bashrc'
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Install zsh completion
90
+ */
91
+ installZsh() {
92
+ const rcFile = path.join(this.homeDir, '.zshrc');
93
+ const completionPath = path.join(this.completionDir, 'ccs.zsh');
94
+ const zshCompDir = path.join(this.homeDir, '.zsh', 'completion');
95
+
96
+ if (!fs.existsSync(completionPath)) {
97
+ throw new Error('Completion file not found. Please reinstall CCS.');
98
+ }
99
+
100
+ // Create zsh completion directory
101
+ if (!fs.existsSync(zshCompDir)) {
102
+ fs.mkdirSync(zshCompDir, { recursive: true });
103
+ }
104
+
105
+ // Copy to zsh completion directory
106
+ const destFile = path.join(zshCompDir, '_ccs');
107
+ fs.copyFileSync(completionPath, destFile);
108
+
109
+ const marker = '# CCS shell completion';
110
+ const setupCmds = [
111
+ 'fpath=(~/.zsh/completion $fpath)',
112
+ 'autoload -Uz compinit && compinit'
113
+ ];
114
+ const block = `\n${marker}\n${setupCmds.join('\n')}\n`;
115
+
116
+ // Check if already installed
117
+ if (fs.existsSync(rcFile)) {
118
+ const content = fs.readFileSync(rcFile, 'utf8');
119
+ if (content.includes(marker)) {
120
+ return { success: true, alreadyInstalled: true };
121
+ }
122
+ }
123
+
124
+ // Append to .zshrc
125
+ fs.appendFileSync(rcFile, block);
126
+
127
+ return {
128
+ success: true,
129
+ message: `Added to ${rcFile}`,
130
+ reload: 'source ~/.zshrc'
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Install fish completion
136
+ */
137
+ installFish() {
138
+ const completionPath = path.join(this.completionDir, 'ccs.fish');
139
+ const fishCompDir = path.join(this.homeDir, '.config', 'fish', 'completions');
140
+
141
+ if (!fs.existsSync(completionPath)) {
142
+ throw new Error('Completion file not found. Please reinstall CCS.');
143
+ }
144
+
145
+ // Create fish completion directory
146
+ if (!fs.existsSync(fishCompDir)) {
147
+ fs.mkdirSync(fishCompDir, { recursive: true });
148
+ }
149
+
150
+ // Copy to fish completion directory (fish auto-loads from here)
151
+ const destFile = path.join(fishCompDir, 'ccs.fish');
152
+ fs.copyFileSync(completionPath, destFile);
153
+
154
+ return {
155
+ success: true,
156
+ message: `Installed to ${destFile}`,
157
+ reload: 'Fish auto-loads completions (no reload needed)'
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Install PowerShell completion
163
+ */
164
+ installPowerShell() {
165
+ const profilePath = process.env.PROFILE || path.join(
166
+ this.homeDir,
167
+ 'Documents',
168
+ 'PowerShell',
169
+ 'Microsoft.PowerShell_profile.ps1'
170
+ );
171
+ const completionPath = path.join(this.completionDir, 'ccs.ps1');
172
+
173
+ if (!fs.existsSync(completionPath)) {
174
+ throw new Error('Completion file not found. Please reinstall CCS.');
175
+ }
176
+
177
+ const marker = '# CCS shell completion';
178
+ const sourceCmd = `. "${completionPath.replace(/\\/g, '\\\\')}"`;
179
+ const block = `\n${marker}\n${sourceCmd}\n`;
180
+
181
+ // Create profile directory if needed
182
+ const profileDir = path.dirname(profilePath);
183
+ if (!fs.existsSync(profileDir)) {
184
+ fs.mkdirSync(profileDir, { recursive: true });
185
+ }
186
+
187
+ // Check if already installed
188
+ if (fs.existsSync(profilePath)) {
189
+ const content = fs.readFileSync(profilePath, 'utf8');
190
+ if (content.includes(marker)) {
191
+ return { success: true, alreadyInstalled: true };
192
+ }
193
+ }
194
+
195
+ // Append to PowerShell profile
196
+ fs.appendFileSync(profilePath, block);
197
+
198
+ return {
199
+ success: true,
200
+ message: `Added to ${profilePath}`,
201
+ reload: '. $PROFILE'
202
+ };
203
+ }
204
+
205
+ /**
206
+ * Install for detected or specified shell
207
+ */
208
+ install(shell = null) {
209
+ const targetShell = shell || this.detectShell();
210
+
211
+ if (!targetShell) {
212
+ throw new Error('Could not detect shell. Please specify: --bash, --zsh, --fish, or --powershell');
213
+ }
214
+
215
+ // Ensure completion files exist
216
+ this.ensureCompletionFiles();
217
+
218
+ // Install for target shell
219
+ switch (targetShell) {
220
+ case 'bash':
221
+ return this.installBash();
222
+ case 'zsh':
223
+ return this.installZsh();
224
+ case 'fish':
225
+ return this.installFish();
226
+ case 'powershell':
227
+ return this.installPowerShell();
228
+ default:
229
+ throw new Error(`Unsupported shell: ${targetShell}`);
230
+ }
231
+ }
232
+ }
233
+
234
+ module.exports = { ShellCompletionInstaller };