@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/README.ja.md +470 -146
- package/README.md +338 -151
- 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 +87 -2
- 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 +541 -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 +1 -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 +24 -0
|
@@ -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 };
|