agentvibes 5.4.0 → 5.6.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/.agentvibes/config.json +9 -1
- package/.claude/config/audio-effects.cfg +12 -0
- package/.claude/config/background-music-enabled.txt +1 -0
- package/.claude/config/background-music-position.txt +1 -1
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/bmad-party-speak.sh +0 -0
- package/.claude/hooks/bmad-speak.sh +6 -2
- package/.claude/hooks/play-tts-piper.sh +2 -2
- package/.claude/hooks/play-tts.sh +22 -1
- package/.claude/hooks-windows/play-tts-windows-piper.ps1 +178 -164
- package/.claude/hooks-windows/play-tts.ps1 +208 -41
- package/README.md +72 -85
- package/RELEASE_NOTES.md +63 -0
- package/bin/agentvibes.js +28 -0
- package/mcp-server/server.py +17 -1
- package/package.json +1 -1
- package/src/console/tabs/music-tab.js +5 -2
- package/src/console/tabs/voices-tab.js +71 -37
- package/src/installer.js +10 -0
- package/src/services/llm-provider-service.js +1 -1
package/bin/agentvibes.js
CHANGED
|
@@ -75,6 +75,16 @@ export function resolveStartTab(args, configService) {
|
|
|
75
75
|
return { startTab: 'install' };
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
if (cmd === 'update') {
|
|
79
|
+
// Always route update to CLI installer (src/installer.js)
|
|
80
|
+
return { cliUpdate: true, args: args.slice(1) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (cmd === 'uninstall') {
|
|
84
|
+
// Always route uninstall to CLI installer (src/installer.js)
|
|
85
|
+
return { cliUninstall: true, args: args.slice(1) };
|
|
86
|
+
}
|
|
87
|
+
|
|
78
88
|
if (cmd === 'config' || cmd === 'configure') {
|
|
79
89
|
return { startTab: 'settings' };
|
|
80
90
|
}
|
|
@@ -154,6 +164,24 @@ if (_argv1 === _thisFile) {
|
|
|
154
164
|
process.exit(0);
|
|
155
165
|
}
|
|
156
166
|
|
|
167
|
+
if (result.cliUpdate) {
|
|
168
|
+
const installerPath = path.resolve(__dirname, '..', 'src', 'installer.js');
|
|
169
|
+
execFileSync(process.execPath, [installerPath, 'update', ...result.args], {
|
|
170
|
+
stdio: 'inherit',
|
|
171
|
+
shell: false,
|
|
172
|
+
});
|
|
173
|
+
process.exit(0);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (result.cliUninstall) {
|
|
177
|
+
const installerPath = path.resolve(__dirname, '..', 'src', 'installer.js');
|
|
178
|
+
execFileSync(process.execPath, [installerPath, 'uninstall', ...result.args], {
|
|
179
|
+
stdio: 'inherit',
|
|
180
|
+
shell: false,
|
|
181
|
+
});
|
|
182
|
+
process.exit(0);
|
|
183
|
+
}
|
|
184
|
+
|
|
157
185
|
launchConsole({ startTab: result.startTab }).catch(err => {
|
|
158
186
|
process.stderr.write(`Failed to launch AgentVibes console: ${err.message}\n`);
|
|
159
187
|
process.exit(1);
|
package/mcp-server/server.py
CHANGED
|
@@ -192,6 +192,17 @@ class AgentVibesServer:
|
|
|
192
192
|
original_language = await self._get_language()
|
|
193
193
|
await self._run_script(self.LANGUAGE_MANAGER_SCRIPT, ["set", language])
|
|
194
194
|
|
|
195
|
+
# Resolve LLM key: AGENTVIBES_LLM > CLAUDECODE=1 > AGENTVIBES_MCP_FALLBACK > "default"
|
|
196
|
+
llm_key = os.environ.get("AGENTVIBES_LLM", "").strip()
|
|
197
|
+
if llm_key and not _re.match(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$", llm_key):
|
|
198
|
+
llm_key = ""
|
|
199
|
+
if not llm_key and os.environ.get("CLAUDECODE", "").strip() == "1":
|
|
200
|
+
llm_key = "claude-code"
|
|
201
|
+
if not llm_key:
|
|
202
|
+
fallback = os.environ.get("AGENTVIBES_MCP_FALLBACK", "").strip()
|
|
203
|
+
if fallback and _re.match(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$", fallback):
|
|
204
|
+
llm_key = fallback
|
|
205
|
+
|
|
195
206
|
# Call the TTS script via appropriate shell
|
|
196
207
|
tts_script = "play-tts.ps1" if self.is_windows else "play-tts.sh"
|
|
197
208
|
play_tts = self.hooks_dir / tts_script
|
|
@@ -199,8 +210,13 @@ class AgentVibesServer:
|
|
|
199
210
|
args = ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(play_tts), text]
|
|
200
211
|
if voice:
|
|
201
212
|
args.extend(["-VoiceOverride", voice])
|
|
213
|
+
if llm_key:
|
|
214
|
+
args.extend(["-llm", llm_key])
|
|
202
215
|
else:
|
|
203
|
-
args = ["bash", str(play_tts)
|
|
216
|
+
args = ["bash", str(play_tts)]
|
|
217
|
+
if llm_key:
|
|
218
|
+
args.extend(["--llm", llm_key])
|
|
219
|
+
args.append(text)
|
|
204
220
|
if voice:
|
|
205
221
|
args.append(voice)
|
|
206
222
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "agentvibes",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.6.0",
|
|
5
5
|
"description": "Now your AI Agents can finally talk back! Professional TTS voice for Claude Code, Claude Desktop (via MCP), and Clawdbot with multi-provider support.",
|
|
6
6
|
"homepage": "https://agentvibes.org",
|
|
7
7
|
"keywords": [
|
|
@@ -185,12 +185,15 @@ export function scanTracks() {
|
|
|
185
185
|
const tracksDir = _getTracksDir();
|
|
186
186
|
try {
|
|
187
187
|
const files = fs.readdirSync(tracksDir);
|
|
188
|
+
const mp3s = files.filter(f => /\.mp3$/i.test(f));
|
|
189
|
+
// If the directory exists but has no mp3s (e.g. empty npm package dir),
|
|
190
|
+
// fall back to the static catalog so bundled tracks always show.
|
|
191
|
+
if (mp3s.length === 0) return BUILT_IN_TRACK_CATALOG.map(t => ({ ...t, isBuiltIn: true }));
|
|
188
192
|
const builtInIds = new Set(BUILT_IN_TRACK_CATALOG.map(t => t.id));
|
|
189
193
|
// Sort by the alphabetic part of the label (skip leading emoji/symbols)
|
|
190
194
|
// so the order reflects the track NAME, not the emoji codepoint.
|
|
191
195
|
const _sortKey = (s) => s.replace(/^[^a-zA-Z]+/, '');
|
|
192
|
-
return
|
|
193
|
-
.filter(f => /\.mp3$/i.test(f))
|
|
196
|
+
return mp3s
|
|
194
197
|
.map(f => ({ id: f, label: formatTrackLabel(f), isBuiltIn: builtInIds.has(f) }))
|
|
195
198
|
.sort((a, b) => _sortKey(a.label).localeCompare(_sortKey(b.label), undefined, { sensitivity: 'base' }));
|
|
196
199
|
} catch {
|
|
@@ -133,6 +133,10 @@ function loadCatalog() {
|
|
|
133
133
|
// Build lookup map for O(1) access by voiceId
|
|
134
134
|
_catalogMap = new Map();
|
|
135
135
|
for (const c of _catalogEntries) _catalogMap.set(c.voiceId, c);
|
|
136
|
+
|
|
137
|
+
// Patch libritts_r onnx.json files so their speaker IDs become friendly names.
|
|
138
|
+
// Must run after catalog loads so the name mapping is available.
|
|
139
|
+
patchLibriTTSSpeakerNames();
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
/**
|
|
@@ -142,45 +146,48 @@ function loadCatalog() {
|
|
|
142
146
|
* Safe to call multiple times — skips if already patched.
|
|
143
147
|
*/
|
|
144
148
|
function patchLibriTTSSpeakerNames() {
|
|
149
|
+
// Load catalog once for all models
|
|
150
|
+
const catalogPath = path.resolve(__dirname, '..', '..', '..', 'voice-assignments.json');
|
|
151
|
+
if (!fs.existsSync(catalogPath)) return;
|
|
152
|
+
let speakers;
|
|
145
153
|
try {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
154
|
+
speakers = JSON.parse(fs.readFileSync(catalogPath, 'utf8')).libritts_speakers ?? {};
|
|
155
|
+
} catch { return; }
|
|
156
|
+
|
|
157
|
+
// Models to patch and how to detect unpatched keys:
|
|
158
|
+
// libritts-high → raw keys are p-prefixed corpus IDs (p3922, p8699, …)
|
|
159
|
+
// libritts_r-* → raw keys are plain numeric corpus IDs (3922, 8699, …)
|
|
160
|
+
const MODELS = [
|
|
161
|
+
{ file: 'en_US-libritts-high.onnx.json', notPatched: (k) => /^p\d+$/.test(k) },
|
|
162
|
+
{ file: 'en_US-libritts_r-medium.onnx.json', notPatched: (k) => /^\d+$/.test(k) },
|
|
163
|
+
{ file: 'en_US-libritts_r-high.onnx.json', notPatched: (k) => /^\d+$/.test(k) },
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
for (const { file, notPatched } of MODELS) {
|
|
167
|
+
try {
|
|
168
|
+
const jsonPath = path.join(PIPER_VOICES_DIR, file);
|
|
169
|
+
if (!fs.existsSync(jsonPath)) continue;
|
|
170
|
+
const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
171
|
+
if (!data.speaker_id_map || data.num_speakers <= 1) continue;
|
|
160
172
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const friendly = speakers[idx]?.voice_name;
|
|
171
|
-
if (friendly) {
|
|
172
|
-
newMap[friendly] = parseInt(idx, 10);
|
|
173
|
-
} else {
|
|
174
|
-
newMap[pname] = parseInt(idx, 10);
|
|
173
|
+
const names = Object.keys(data.speaker_id_map);
|
|
174
|
+
// Skip if already patched (first key is a friendly name, not a raw corpus ID)
|
|
175
|
+
if (names.length === 0 || !notPatched(names[0])) continue;
|
|
176
|
+
|
|
177
|
+
// Values are 0-based sequential indices into the model — use as catalog key
|
|
178
|
+
const newMap = {};
|
|
179
|
+
for (const [rawKey, idx] of Object.entries(data.speaker_id_map)) {
|
|
180
|
+
const friendly = speakers[String(idx)]?.voice_name;
|
|
181
|
+
newMap[friendly ?? rawKey] = idx;
|
|
175
182
|
}
|
|
176
|
-
}
|
|
177
183
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
+
data.speaker_id_map = newMap;
|
|
185
|
+
// Verify file ownership before writing (security: CLAUDE.md)
|
|
186
|
+
const stat = fs.statSync(jsonPath);
|
|
187
|
+
if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) continue;
|
|
188
|
+
fs.writeFileSync(jsonPath, JSON.stringify(data, null, 2), 'utf8');
|
|
189
|
+
} catch { /* non-fatal — skip this model */ }
|
|
190
|
+
}
|
|
184
191
|
}
|
|
185
192
|
|
|
186
193
|
// Column widths for the multi-column voice list
|
|
@@ -417,9 +424,9 @@ export function parseMultiSpeaker(voiceId) {
|
|
|
417
424
|
try {
|
|
418
425
|
const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
419
426
|
let speakerId = data.speaker_id_map?.[speakerName] ?? null;
|
|
420
|
-
// Fallback: if the .onnx.json still has raw
|
|
427
|
+
// Fallback: if the .onnx.json still has raw corpus IDs (not yet patched),
|
|
421
428
|
// look up the numeric speaker ID from voice-assignments.json catalog.
|
|
422
|
-
if (speakerId == null && model === 'en_US-libritts-high') {
|
|
429
|
+
if (speakerId == null && (model === 'en_US-libritts-high' || /^en_US-libritts_r-/.test(model))) {
|
|
423
430
|
try {
|
|
424
431
|
const catalogPath = path.resolve(__dirname, '..', '..', '..', 'voice-assignments.json');
|
|
425
432
|
const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
|
|
@@ -443,6 +450,9 @@ export function parseMultiSpeaker(voiceId) {
|
|
|
443
450
|
* @returns {string[]}
|
|
444
451
|
*/
|
|
445
452
|
export function scanInstalledVoices() {
|
|
453
|
+
// Ensure catalog is loaded and libritts_r onnx.json files are patched
|
|
454
|
+
// before we read their speaker_id_map keys (otherwise we get raw corpus IDs).
|
|
455
|
+
loadCatalog();
|
|
446
456
|
try {
|
|
447
457
|
const files = fs.readdirSync(PIPER_VOICES_DIR);
|
|
448
458
|
const onnxFiles = files
|
|
@@ -588,6 +598,30 @@ export function getVoiceMeta(voiceId) {
|
|
|
588
598
|
_metaCache.set(voiceId, result);
|
|
589
599
|
return result;
|
|
590
600
|
}
|
|
601
|
+
// libritts_r variants share speaker names with libritts-high after patching
|
|
602
|
+
if (/^en_US-libritts_r-/.test(ms.model)) {
|
|
603
|
+
// After patching: speakerName is a friendly name — look up in libritts-high catalog
|
|
604
|
+
const highCat = _catalogMap.get(`en_US-libritts-high${MS_SEP}${ms.speakerName}`);
|
|
605
|
+
if (highCat) {
|
|
606
|
+
const result = { displayName: highCat.displayName, gender: highCat.gender, provider: `Piper (${ms.model})` };
|
|
607
|
+
_metaCache.set(voiceId, result);
|
|
608
|
+
return result;
|
|
609
|
+
}
|
|
610
|
+
// Before patching: speakerName is a raw numeric corpus ID — resolve via onnx.json value
|
|
611
|
+
if (/^\d+$/.test(ms.speakerName)) {
|
|
612
|
+
try {
|
|
613
|
+
const jsonPath = path.join(PIPER_VOICES_DIR, ms.model + '.onnx.json');
|
|
614
|
+
const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
615
|
+
const seqIdx = data.speaker_id_map?.[ms.speakerName];
|
|
616
|
+
if (seqIdx != null && _catalogEntries[seqIdx]) {
|
|
617
|
+
const cat = _catalogEntries[seqIdx];
|
|
618
|
+
const result = { displayName: cat.displayName, gender: cat.gender, provider: `Piper (${ms.model})` };
|
|
619
|
+
_metaCache.set(voiceId, result);
|
|
620
|
+
return result;
|
|
621
|
+
}
|
|
622
|
+
} catch { /* fall through */ }
|
|
623
|
+
}
|
|
624
|
+
}
|
|
591
625
|
// Fallback for speakers not in the catalog (e.g. 16Speakers model)
|
|
592
626
|
const displayName = uniquifyVoiceName(ms.speakerName.replace(/_/g, ' '));
|
|
593
627
|
const result = {
|
package/src/installer.js
CHANGED
|
@@ -3249,6 +3249,7 @@ async function handleTermuxSshConfiguration() {
|
|
|
3249
3249
|
* @returns {Promise<{count: number, boxen: string}>} Number of files copied and boxen content
|
|
3250
3250
|
*/
|
|
3251
3251
|
async function copyCommandFiles(targetDir, spinner) {
|
|
3252
|
+
spinner = createRobustSpinner(spinner);
|
|
3252
3253
|
spinner.start('Installing /agent-vibes slash commands...');
|
|
3253
3254
|
const srcCommandsDir = path.join(__dirname, '..', '.claude', 'commands', 'agent-vibes');
|
|
3254
3255
|
const commandsDir = path.join(targetDir, '.claude', 'commands');
|
|
@@ -3462,6 +3463,7 @@ function buildHookInstallationBoxen(installedFiles, failedFiles) {
|
|
|
3462
3463
|
* @returns {Promise<{count: number, boxen: string|null}>} Number of files copied and boxen content
|
|
3463
3464
|
*/
|
|
3464
3465
|
async function copyHookFiles(targetDir, spinner) {
|
|
3466
|
+
spinner = createRobustSpinner(spinner);
|
|
3465
3467
|
spinner.start('Installing TTS helper scripts...');
|
|
3466
3468
|
const hooksSubdir = isNativeWindows() ? 'hooks-windows' : 'hooks';
|
|
3467
3469
|
const srcHooksDir = path.join(__dirname, '..', '.claude', hooksSubdir);
|
|
@@ -3516,6 +3518,7 @@ async function copyHookFiles(targetDir, spinner) {
|
|
|
3516
3518
|
* @returns {Promise<{count: number, boxen: string|null}>} Number of files copied and boxen content
|
|
3517
3519
|
*/
|
|
3518
3520
|
async function copyPersonalityFiles(targetDir, spinner) {
|
|
3521
|
+
spinner = createRobustSpinner(spinner);
|
|
3519
3522
|
spinner.start('Installing personality templates...');
|
|
3520
3523
|
const srcPersonalitiesDir = path.join(__dirname, '..', '.claude', 'personalities');
|
|
3521
3524
|
const destPersonalitiesDir = path.join(targetDir, '.claude', 'personalities');
|
|
@@ -3598,6 +3601,7 @@ async function copyPersonalityFiles(targetDir, spinner) {
|
|
|
3598
3601
|
* @returns {Promise<number>} Number of files copied
|
|
3599
3602
|
*/
|
|
3600
3603
|
async function copyPluginFiles(targetDir, spinner) {
|
|
3604
|
+
spinner = createRobustSpinner(spinner);
|
|
3601
3605
|
spinner.start('Installing BMAD plugin files...');
|
|
3602
3606
|
const srcPluginsDir = path.join(__dirname, '..', '.claude', 'plugins');
|
|
3603
3607
|
const destPluginsDir = path.join(targetDir, '.claude', 'plugins');
|
|
@@ -3632,6 +3636,7 @@ async function copyPluginFiles(targetDir, spinner) {
|
|
|
3632
3636
|
* @returns {Promise<number>} Number of files copied
|
|
3633
3637
|
*/
|
|
3634
3638
|
async function copyBmadConfigFiles(targetDir, spinner) {
|
|
3639
|
+
spinner = createRobustSpinner(spinner);
|
|
3635
3640
|
spinner.start('Installing BMAD config files...');
|
|
3636
3641
|
const srcBmadDir = path.join(__dirname, '..', '.agentvibes', 'bmad');
|
|
3637
3642
|
const destBmadDir = path.join(targetDir, '.agentvibes', 'bmad');
|
|
@@ -3664,6 +3669,7 @@ async function copyBmadConfigFiles(targetDir, spinner) {
|
|
|
3664
3669
|
* @returns {Promise<{count: number, boxen: string}>} Number of files copied and boxen content
|
|
3665
3670
|
*/
|
|
3666
3671
|
async function copyBackgroundMusicFiles(targetDir, spinner) {
|
|
3672
|
+
spinner = createRobustSpinner(spinner);
|
|
3667
3673
|
spinner.start('Installing background music tracks...');
|
|
3668
3674
|
const srcBackgroundsDir = path.join(__dirname, '..', '.claude', 'audio', 'tracks');
|
|
3669
3675
|
const destBackgroundsDir = path.join(targetDir, '.claude', 'audio', 'tracks');
|
|
@@ -3786,6 +3792,7 @@ async function copyBackgroundMusicFiles(targetDir, spinner) {
|
|
|
3786
3792
|
* @returns {Promise<number>} Number of files copied
|
|
3787
3793
|
*/
|
|
3788
3794
|
async function copyConfigFiles(targetDir, spinner) {
|
|
3795
|
+
spinner = createRobustSpinner(spinner);
|
|
3789
3796
|
spinner.start('Installing configuration files...');
|
|
3790
3797
|
const srcConfigDir = path.join(__dirname, '..', '.claude', 'config');
|
|
3791
3798
|
const destConfigDir = path.join(targetDir, '.claude', 'config');
|
|
@@ -3848,6 +3855,7 @@ async function copyConfigFiles(targetDir, spinner) {
|
|
|
3848
3855
|
* @param {Object} spinner - Ora spinner instance
|
|
3849
3856
|
*/
|
|
3850
3857
|
async function copyCodexFiles(targetDir, spinner) {
|
|
3858
|
+
spinner = createRobustSpinner(spinner);
|
|
3851
3859
|
spinner.start('Installing Codex integration files...');
|
|
3852
3860
|
const srcCodexDir = path.join(__dirname, '..', '.codex');
|
|
3853
3861
|
const destCodexDir = path.join(targetDir, '.codex');
|
|
@@ -3901,6 +3909,7 @@ async function copyCodexFiles(targetDir, spinner) {
|
|
|
3901
3909
|
* @param {Object} spinner - Ora spinner instance
|
|
3902
3910
|
*/
|
|
3903
3911
|
async function configureSessionStartHook(targetDir, spinner) {
|
|
3912
|
+
spinner = createRobustSpinner(spinner);
|
|
3904
3913
|
spinner.start('Configuring AgentVibes hook for automatic TTS...');
|
|
3905
3914
|
const claudeDir = path.join(targetDir, '.claude');
|
|
3906
3915
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
@@ -3956,6 +3965,7 @@ async function configureSessionStartHook(targetDir, spinner) {
|
|
|
3956
3965
|
* @param {Object} spinner - Ora spinner instance
|
|
3957
3966
|
*/
|
|
3958
3967
|
async function configurePartyModeHook(targetDir, spinner, homeDirOverride) {
|
|
3968
|
+
spinner = createRobustSpinner(spinner);
|
|
3959
3969
|
spinner.start('Configuring BMAD party mode TTS hook...');
|
|
3960
3970
|
const homeDir = homeDirOverride || os.homedir();
|
|
3961
3971
|
const globalClaudeDir = path.join(homeDir, '.claude');
|
|
@@ -198,7 +198,7 @@ export async function installClaudeMcp(targetDir) {
|
|
|
198
198
|
|
|
199
199
|
try {
|
|
200
200
|
// Copy hooks, commands, config, personality, plugin, bmad config files
|
|
201
|
-
const silentSpinner = { start: () => {}, succeed: () => {}, fail: () => {} };
|
|
201
|
+
const silentSpinner = { start: () => {}, stop: () => {}, succeed: () => {}, fail: () => {}, warn: () => {}, info: () => {}, stopAndPersist: () => {}, get text() { return ''; }, set text(_) {}, get isSpinning() { return false; } };
|
|
202
202
|
const installer = await import('../installer.js');
|
|
203
203
|
await installer.copyHookFiles(targetDir, silentSpinner);
|
|
204
204
|
await installer.copyCommandFiles(targetDir, silentSpinner);
|