agentvibes 4.2.0 → 4.4.1
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/bmad/bmad-voices.md +69 -69
- package/.agentvibes/config.json +12 -0
- package/.claude/activation-instructions +54 -54
- package/.claude/audio/tracks/README.md +52 -52
- package/.claude/commands/agent-vibes/add.md +21 -21
- package/.claude/commands/agent-vibes/agent-vibes.md +101 -101
- package/.claude/commands/agent-vibes/agent.md +79 -79
- package/.claude/commands/agent-vibes/background-music.md +111 -111
- package/.claude/commands/agent-vibes/bmad.md +198 -198
- package/.claude/commands/agent-vibes/clean.md +18 -18
- package/.claude/commands/agent-vibes/cleanup.md +18 -18
- package/.claude/commands/agent-vibes/commands.json +145 -145
- package/.claude/commands/agent-vibes/effects.md +97 -97
- package/.claude/commands/agent-vibes/get.md +9 -9
- package/.claude/commands/agent-vibes/hide.md +91 -91
- package/.claude/commands/agent-vibes/language.md +23 -23
- package/.claude/commands/agent-vibes/learn.md +67 -67
- package/.claude/commands/agent-vibes/list.md +13 -13
- package/.claude/commands/agent-vibes/mute.md +37 -37
- package/.claude/commands/agent-vibes/preview.md +17 -17
- package/.claude/commands/agent-vibes/provider.md +68 -68
- package/.claude/commands/agent-vibes/replay-target.md +14 -14
- package/.claude/commands/agent-vibes/sample.md +12 -12
- package/.claude/commands/agent-vibes/set-favorite-voice.md +84 -84
- package/.claude/commands/agent-vibes/set-pretext.md +65 -65
- package/.claude/commands/agent-vibes/set-speed.md +41 -41
- package/.claude/commands/agent-vibes/show.md +84 -84
- package/.claude/commands/agent-vibes/switch.md +87 -87
- package/.claude/commands/agent-vibes/target-voice.md +26 -26
- package/.claude/commands/agent-vibes/target.md +30 -30
- package/.claude/commands/agent-vibes/translate.md +68 -68
- package/.claude/commands/agent-vibes/unmute.md +45 -45
- package/.claude/commands/agent-vibes/verbosity.md +89 -89
- package/.claude/commands/agent-vibes/whoami.md +7 -7
- package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
- package/.claude/commands/agent-vibes-rdp.md +24 -24
- package/.claude/config/agentvibes.json +1 -0
- package/.claude/config/audio-effects.cfg +2 -2
- package/.claude/config/audio-effects.cfg.sample +52 -52
- package/.claude/config/background-music-volume.txt +1 -0
- package/.claude/config/intro-text.txt +1 -0
- package/.claude/config/piper-speech-rate.txt +4 -0
- package/.claude/config/piper-target-speech-rate.txt +1 -0
- package/.claude/config/reverb-level.txt +1 -0
- package/.claude/config/tts-speech-rate.txt +4 -0
- package/.claude/config/tts-target-speech-rate.txt +1 -0
- package/.claude/docs/TERMUX_SETUP.md +408 -408
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/README-TTS-QUEUE.md +135 -135
- package/.claude/hooks/audio-cache-utils.sh +246 -246
- package/.claude/hooks/audio-processor.sh +433 -433
- package/.claude/hooks/background-music-manager.sh +404 -404
- package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
- package/.claude/hooks/bmad-speak.sh +269 -269
- package/.claude/hooks/bmad-tts-injector.sh +568 -568
- package/.claude/hooks/bmad-voice-manager.sh +928 -928
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
- package/.claude/hooks/clawdbot-receiver.sh +107 -107
- package/.claude/hooks/clean-audio-cache.sh +22 -22
- package/.claude/hooks/cleanup-cache.sh +106 -106
- package/.claude/hooks/configure-rdp-mode.sh +137 -137
- package/.claude/hooks/download-extra-voices.sh +244 -244
- package/.claude/hooks/effects-manager.sh +268 -268
- package/.claude/hooks/github-star-reminder.sh +154 -154
- package/.claude/hooks/language-manager.sh +362 -362
- package/.claude/hooks/learn-manager.sh +492 -492
- package/.claude/hooks/macos-voice-manager.sh +205 -205
- package/.claude/hooks/migrate-background-music.sh +125 -125
- package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
- package/.claude/hooks/optimize-background-music.sh +87 -87
- package/.claude/hooks/path-resolver.sh +60 -60
- package/.claude/hooks/personality-manager.sh +448 -448
- package/.claude/hooks/piper-download-voices.sh +225 -225
- package/.claude/hooks/piper-installer.sh +292 -292
- package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
- package/.claude/hooks/piper-voice-manager.sh +24 -3
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -90
- package/.claude/hooks/play-tts-enhanced.sh +105 -105
- package/.claude/hooks/play-tts-macos.sh +368 -368
- package/.claude/hooks/play-tts-piper.sh +679 -679
- package/.claude/hooks/play-tts-soprano.sh +356 -356
- package/.claude/hooks/play-tts-ssh-remote.sh +167 -167
- package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
- package/.claude/hooks/play-tts.sh +301 -301
- package/.claude/hooks/prepare-release.sh +54 -54
- package/.claude/hooks/provider-commands.sh +617 -617
- package/.claude/hooks/provider-manager.sh +399 -399
- package/.claude/hooks/replay-target-audio.sh +95 -95
- package/.claude/hooks/requirements.txt +6 -6
- package/.claude/hooks/sentiment-manager.sh +201 -201
- package/.claude/hooks/session-start-tts.sh +81 -81
- package/.claude/hooks/soprano-gradio-synth.py +139 -139
- package/.claude/hooks/speed-manager.sh +291 -291
- package/.claude/hooks/stop-tts.sh +84 -84
- package/.claude/hooks/termux-installer.sh +261 -261
- package/.claude/hooks/translate-manager.sh +341 -341
- package/.claude/hooks/translator.py +237 -237
- package/.claude/hooks/tts-queue-worker.sh +145 -145
- package/.claude/hooks/tts-queue.sh +165 -165
- package/.claude/hooks/verbosity-manager.sh +178 -178
- package/.claude/hooks/voice-manager.sh +548 -548
- package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
- package/.claude/hooks-windows/background-music-manager.ps1 +348 -0
- package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -0
- package/.claude/hooks-windows/download-extra-voices.ps1 +185 -0
- package/.claude/hooks-windows/effects-manager.ps1 +294 -0
- package/.claude/hooks-windows/language-manager.ps1 +193 -0
- package/.claude/hooks-windows/learn-manager.ps1 +241 -0
- package/.claude/hooks-windows/personality-manager.ps1 +266 -0
- package/.claude/hooks-windows/play-tts-piper.ps1 +209 -0
- package/.claude/hooks-windows/play-tts-sapi.ps1 +108 -0
- package/.claude/hooks-windows/play-tts-soprano.ps1 +159 -158
- package/.claude/hooks-windows/play-tts-windows-piper.ps1 +50 -5
- package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -108
- package/.claude/hooks-windows/play-tts.ps1 +344 -266
- package/.claude/hooks-windows/provider-manager.ps1 +29 -10
- package/.claude/hooks-windows/session-start-tts.ps1 +124 -124
- package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
- package/.claude/hooks-windows/speed-manager.ps1 +166 -0
- package/.claude/hooks-windows/verbosity-manager.ps1 +119 -0
- package/.claude/hooks-windows/voice-manager-windows.ps1 +92 -8
- package/.claude/output-styles/agent-vibes.md +202 -202
- package/.claude/personalities/angry.md +14 -14
- package/.claude/personalities/annoying.md +14 -14
- package/.claude/personalities/crass.md +14 -14
- package/.claude/personalities/dramatic.md +14 -14
- package/.claude/personalities/dry-humor.md +50 -50
- package/.claude/personalities/flirty.md +20 -20
- package/.claude/personalities/funny.md +14 -14
- package/.claude/personalities/grandpa.md +32 -32
- package/.claude/personalities/millennial.md +14 -14
- package/.claude/personalities/moody.md +14 -14
- package/.claude/personalities/normal.md +16 -16
- package/.claude/personalities/pirate.md +14 -14
- package/.claude/personalities/poetic.md +14 -14
- package/.claude/personalities/professional.md +14 -14
- package/.claude/personalities/rapper.md +55 -55
- package/.claude/personalities/robot.md +14 -14
- package/.claude/personalities/sarcastic.md +38 -38
- package/.claude/personalities/sassy.md +14 -14
- package/.claude/personalities/surfer-dude.md +14 -14
- package/.claude/personalities/zen.md +14 -14
- package/.claude/settings.json +15 -15
- package/.claude/verbosity.txt +1 -1
- package/.clawdbot/README.md +105 -105
- package/.clawdbot/skill/SKILL.md +241 -241
- package/.mcp.json +12 -0
- package/CLAUDE.md +170 -170
- package/README.md +2029 -2007
- package/RELEASE_NOTES.md +1310 -1203
- package/WINDOWS-SETUP.md +208 -208
- package/bin/agent-vibes +39 -39
- package/bin/agentvibes-voice-browser.js +1840 -1840
- package/bin/agentvibes.js +48 -2
- package/bin/mcp-server.js +121 -121
- package/bin/mcp-server.sh +206 -206
- package/bin/test-bmad-pr +78 -78
- package/mcp-server/QUICK_START.md +203 -203
- package/mcp-server/README.md +345 -345
- package/mcp-server/WINDOWS_SETUP.md +260 -260
- package/mcp-server/docs/troubleshooting-audio.md +313 -313
- package/mcp-server/examples/claude_desktop_config.json +11 -11
- package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
- package/mcp-server/examples/custom_instructions.md +169 -169
- package/mcp-server/install-deps.js +130 -130
- package/mcp-server/pyproject.toml +52 -52
- package/mcp-server/requirements.txt +2 -2
- package/mcp-server/server.py +1465 -1453
- package/mcp-server/test_server.py +395 -395
- package/mcp-server/test_windows_script_parity.py +336 -0
- package/package.json +110 -110
- package/setup-windows.ps1 +815 -815
- package/src/bmad-detector.js +71 -71
- package/src/cli/list-personalities.js +110 -110
- package/src/cli/list-voices.js +114 -114
- package/src/commands/bmad-voices.js +394 -394
- package/src/commands/install-mcp.js +476 -476
- package/src/console/app.js +824 -824
- package/src/console/audio-env.js +20 -1
- package/src/console/brand-colors.js +13 -13
- package/src/console/constants/personalities.js +44 -44
- package/src/console/footer-config.js +50 -50
- package/src/console/modals/modal-overlay.js +247 -247
- package/src/console/navigation.js +62 -62
- package/src/console/tabs/agents-tab.js +1684 -1516
- package/src/console/tabs/help-tab.js +261 -261
- package/src/console/tabs/install-tab.js +1007 -991
- package/src/console/tabs/music-tab.js +22 -8
- package/src/console/tabs/placeholder-tab.js +53 -53
- package/src/console/tabs/readme-tab.js +267 -267
- package/src/console/tabs/receiver-tab.js +1472 -1212
- package/src/console/tabs/settings-tab.js +208 -84
- package/src/console/tabs/voices-tab.js +100 -21
- package/src/console/widgets/destroy-list.js +25 -25
- package/src/console/widgets/format-utils.js +89 -89
- package/src/console/widgets/notice.js +55 -55
- package/src/console/widgets/personality-picker.js +185 -185
- package/src/console/widgets/reverb-picker.js +94 -94
- package/src/console/widgets/track-picker.js +285 -285
- package/src/installer/music-file-input.js +304 -304
- package/src/installer.js +5895 -5829
- package/src/services/agent-voice-store.js +423 -423
- package/src/services/config-service.js +264 -264
- package/src/services/navigation-service.js +123 -123
- package/src/services/provider-service.js +143 -132
- package/src/services/verbosity-service.js +157 -157
- package/src/utils/audio-duration-validator.js +298 -298
- package/src/utils/audio-format-validator.js +277 -277
- package/src/utils/dependency-checker.js +469 -466
- package/src/utils/file-ownership-verifier.js +358 -358
- package/src/utils/list-formatter.js +194 -194
- package/src/utils/music-file-validator.js +285 -285
- package/src/utils/preview-list-prompt.js +136 -136
- package/src/utils/provider-validator.js +96 -12
- package/src/utils/secure-music-storage.js +412 -412
- package/templates/agentvibes-receiver.sh +482 -482
- package/templates/audio/welcome-music.mp3 +0 -0
- package/voice-assignments.json +8244 -8244
- package/.claude/config/background-music-position.txt +0 -1
|
@@ -1,991 +1,1007 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AgentVibes TUI Console — Install Tab (Installer Wizard)
|
|
3
|
-
* Epic 12: Stories 12.1-12.5
|
|
4
|
-
*
|
|
5
|
-
* Implements the Tab Component Contract:
|
|
6
|
-
* createInstallTab(screen, services) → { box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }
|
|
7
|
-
*
|
|
8
|
-
* 5-screen wizard flow:
|
|
9
|
-
* Screen 1: Welcome & Purpose
|
|
10
|
-
* Screen 2: Auto Dependency Check
|
|
11
|
-
* Screen 3: Provider Selection
|
|
12
|
-
* Screen 4: Voice Config & Intro Text
|
|
13
|
-
* Screen 5: Complete & TTS Greeting
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import path from 'node:path';
|
|
17
|
-
import { execFile } from 'node:child_process';
|
|
18
|
-
import { promisify } from 'node:util';
|
|
19
|
-
import
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
*
|
|
65
|
-
* @
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
*
|
|
75
|
-
* @param {string}
|
|
76
|
-
* @
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* @
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
_commandExistsAsync('
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
await
|
|
375
|
-
spinner.succeed('
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
btn.on('
|
|
487
|
-
|
|
488
|
-
btn.style.
|
|
489
|
-
btn.
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
screen.render();
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
//
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
'',
|
|
596
|
-
|
|
597
|
-
])
|
|
598
|
-
|
|
599
|
-
screen.render();
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
const
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
:
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
if (_screen !==
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
if (
|
|
886
|
-
if (_screen
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
if (
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
if (
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
// [
|
|
936
|
-
screen.key(['
|
|
937
|
-
if (box.hidden
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes TUI Console — Install Tab (Installer Wizard)
|
|
3
|
+
* Epic 12: Stories 12.1-12.5
|
|
4
|
+
*
|
|
5
|
+
* Implements the Tab Component Contract:
|
|
6
|
+
* createInstallTab(screen, services) → { box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }
|
|
7
|
+
*
|
|
8
|
+
* 5-screen wizard flow:
|
|
9
|
+
* Screen 1: Welcome & Purpose
|
|
10
|
+
* Screen 2: Auto Dependency Check
|
|
11
|
+
* Screen 3: Provider Selection
|
|
12
|
+
* Screen 4: Voice Config & Intro Text
|
|
13
|
+
* Screen 5: Complete & TTS Greeting
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { execFile } from 'node:child_process';
|
|
18
|
+
import { promisify } from 'node:util';
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import { promises as _fsP } from 'node:fs';
|
|
21
|
+
import { buildAudioEnv } from '../audio-env.js';
|
|
22
|
+
import {
|
|
23
|
+
copyCommandFiles, copyHookFiles, copyPersonalityFiles,
|
|
24
|
+
copyPluginFiles, copyBmadConfigFiles, copyBackgroundMusicFiles,
|
|
25
|
+
copyConfigFiles, configureSessionStartHook,
|
|
26
|
+
installPluginManifest, checkAndInstallPiper, ensureGitRepo,
|
|
27
|
+
} from '../../installer.js';
|
|
28
|
+
|
|
29
|
+
const _execFileAsync = promisify(execFile);
|
|
30
|
+
|
|
31
|
+
const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
|
|
32
|
+
|
|
33
|
+
let blessed;
|
|
34
|
+
if (!IS_TEST) {
|
|
35
|
+
const { default: b } = await import('blessed');
|
|
36
|
+
blessed = b;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
const COLORS = {
|
|
42
|
+
contentBg: '#0a0e1a',
|
|
43
|
+
sectionHdr: 'bright-cyan', // Matches header "Agent" color
|
|
44
|
+
labelFg: '#e3f2fd',
|
|
45
|
+
valueFg: '#ffff00', // Yellow
|
|
46
|
+
brandPink: '#f06292', // Light magenta — AgentVibes logotype
|
|
47
|
+
successFg: '#69f0ae', // Green — success
|
|
48
|
+
errorFg: '#ef9a9a', // Red — error/missing
|
|
49
|
+
btnDefault: '#283593',
|
|
50
|
+
btnFocus: '#2e7d32', // Green — focused/selected
|
|
51
|
+
btnFocusFg: '#ffffff', // White text on green
|
|
52
|
+
btnPress: '#ff00ff',
|
|
53
|
+
borderFg: 'bright-cyan',
|
|
54
|
+
footerBg: '#5c6bc0', // Lighter indigo — Install tab footer
|
|
55
|
+
noticeFg: '#90a4ae',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const FOOTER_TEXT = '[Enter] Continue/Finish [Esc] Back/Exit [C] Open Console [S/V/M/A/R] Tab [Q] Quit';
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Exported pure helpers (stories 12.1, 12.5)
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Returns the default intro text suggestion (project folder name).
|
|
65
|
+
* @param {string} projectDir
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
export function getIntroDefault(projectDir) {
|
|
69
|
+
if (!projectDir) return '';
|
|
70
|
+
return path.basename(projectDir);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Format the TTS greeting message for Screen 5.
|
|
75
|
+
* @param {string} introText - User's intro text (may be empty)
|
|
76
|
+
* @param {string} projectName - Project folder name
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
export function formatGreeting(introText, projectName) {
|
|
80
|
+
const name = introText || projectName || 'AgentVibes';
|
|
81
|
+
return `${name} is ready! Welcome to AgentVibes. Love AgentVibes? We'd really appreciate it if you could give us a star on GitHub.`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Dependency detection helpers (story 12.2)
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if a command exists on the system (async).
|
|
89
|
+
* Only ENOENT means "not installed" — non-zero exit code still means the binary exists.
|
|
90
|
+
* @param {string} cmd
|
|
91
|
+
* @returns {Promise<boolean>}
|
|
92
|
+
*/
|
|
93
|
+
async function _commandExistsAsync(cmd) {
|
|
94
|
+
try {
|
|
95
|
+
// On Windows, commands like 'npm' are .cmd batch files that require shell: true
|
|
96
|
+
const opts = { stdio: 'pipe', timeout: 5000 };
|
|
97
|
+
if (process.platform === 'win32') opts.shell = true;
|
|
98
|
+
await _execFileAsync(cmd, ['--version'], opts);
|
|
99
|
+
return true;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (err.code === 'ENOENT') return false;
|
|
102
|
+
return true; // binary exists but --version returned non-zero
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Run dependency checks asynchronously. Returns results map.
|
|
108
|
+
* @returns {Promise<{ node: boolean, npm: boolean, piper: boolean, soprano: boolean }>}
|
|
109
|
+
*/
|
|
110
|
+
async function _checkDependenciesAsync() {
|
|
111
|
+
const [node, npm, piperCmd, sopranoTts, sopranoWebui, ffmpeg] = await Promise.all([
|
|
112
|
+
_commandExistsAsync('node'),
|
|
113
|
+
_commandExistsAsync('npm'),
|
|
114
|
+
_commandExistsAsync('piper'),
|
|
115
|
+
_commandExistsAsync('soprano-tts'),
|
|
116
|
+
_commandExistsAsync('soprano-webui'),
|
|
117
|
+
_commandExistsAsync('ffmpeg'),
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
// On Windows, Piper is a standalone exe at %LOCALAPPDATA%\Programs\Piper\piper.exe
|
|
121
|
+
let piper = piperCmd;
|
|
122
|
+
if (!piper && process.platform === 'win32') {
|
|
123
|
+
const localAppData = process.env.LOCALAPPDATA ||
|
|
124
|
+
(process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
|
|
125
|
+
if (localAppData) {
|
|
126
|
+
piper = fs.existsSync(path.join(localAppData, 'Programs', 'Piper', 'piper.exe'));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { node, npm, piper, soprano: sopranoTts || sopranoWebui, ffmpeg };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Test stub
|
|
135
|
+
|
|
136
|
+
function createTestStub() {
|
|
137
|
+
return {
|
|
138
|
+
box: {},
|
|
139
|
+
show: () => {},
|
|
140
|
+
hide: () => {},
|
|
141
|
+
onFocus: () => {},
|
|
142
|
+
onBlur: () => {},
|
|
143
|
+
getFooterText: () => FOOTER_TEXT,
|
|
144
|
+
getFooterColor: () => COLORS.footerBg,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Create the Install tab component.
|
|
152
|
+
*
|
|
153
|
+
* @param {object} screen - Blessed screen instance (or test stub)
|
|
154
|
+
* @param {object} services
|
|
155
|
+
* @param {import('../../services/config-service.js').ConfigService} services.configService
|
|
156
|
+
* @returns {{ box, show, hide, onFocus, onBlur, getFooterText, getFooterColor }}
|
|
157
|
+
*/
|
|
158
|
+
export function createInstallTab(screen, services) {
|
|
159
|
+
if (IS_TEST) return createTestStub();
|
|
160
|
+
|
|
161
|
+
const { configService, providerService, navigationService, focusMainTabBar } = services;
|
|
162
|
+
|
|
163
|
+
// -------------------------------------------------------------------------
|
|
164
|
+
// Container
|
|
165
|
+
|
|
166
|
+
const box = blessed.box({
|
|
167
|
+
parent: screen,
|
|
168
|
+
top: 4,
|
|
169
|
+
left: 0,
|
|
170
|
+
width: '100%',
|
|
171
|
+
bottom: 2,
|
|
172
|
+
hidden: true,
|
|
173
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
174
|
+
border: { type: 'line' },
|
|
175
|
+
borderStyle: { fg: COLORS.borderFg },
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// -------------------------------------------------------------------------
|
|
179
|
+
// Wizard state
|
|
180
|
+
|
|
181
|
+
let _screen = 1;
|
|
182
|
+
let _lastScreen = 0;
|
|
183
|
+
let _deps = null;
|
|
184
|
+
let _checking = false;
|
|
185
|
+
let _selectedProvider = null;
|
|
186
|
+
let _introText = getIntroDefault(process.cwd());
|
|
187
|
+
let _screen5Announced = false; // TTS greeting fires once per wizard run
|
|
188
|
+
let _completionModalOpen = false;
|
|
189
|
+
let _completionModalBox = null;
|
|
190
|
+
|
|
191
|
+
// Install state (populated during screen 5)
|
|
192
|
+
let _installLog = []; // array of blessed-tagged strings
|
|
193
|
+
let _installRunning = false;
|
|
194
|
+
let _installComplete = false;
|
|
195
|
+
let _installError = null;
|
|
196
|
+
let _lastSpinnerIdx = -1; // index of last ⟳ entry, replaced by ✓ on succeed
|
|
197
|
+
|
|
198
|
+
// -------------------------------------------------------------------------
|
|
199
|
+
// Content area — single persistent box, never detached.
|
|
200
|
+
//
|
|
201
|
+
// KEY INSIGHT: detach+recreate fails because the new widget has no previous
|
|
202
|
+
// cell state, so blessed's diff renderer doesn't know which cells to clear.
|
|
203
|
+
// Keeping the SAME element and calling setContent('') lets blessed diff
|
|
204
|
+
// old-content → empty and write spaces over every character that was there.
|
|
205
|
+
|
|
206
|
+
const contentBox = blessed.box({
|
|
207
|
+
parent: box,
|
|
208
|
+
top: 1,
|
|
209
|
+
left: 2,
|
|
210
|
+
width: '96%',
|
|
211
|
+
bottom: 5,
|
|
212
|
+
tags: true,
|
|
213
|
+
wrap: false,
|
|
214
|
+
scrollable: false,
|
|
215
|
+
style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Footer hint
|
|
219
|
+
const hintLine = blessed.text({
|
|
220
|
+
parent: box,
|
|
221
|
+
bottom: 2,
|
|
222
|
+
left: 2,
|
|
223
|
+
right: 2, // explicit right bound — prevents blessed auto-shrink which leaves stale chars
|
|
224
|
+
tags: true,
|
|
225
|
+
content: '',
|
|
226
|
+
style: { fg: COLORS.noticeFg, bg: COLORS.contentBg },
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
function _c(lines) { return lines.join('\n'); }
|
|
230
|
+
|
|
231
|
+
// -------------------------------------------------------------------------
|
|
232
|
+
// Screen 4 action button callbacks
|
|
233
|
+
|
|
234
|
+
function _doEdit() {
|
|
235
|
+
if (box.hidden || _screen !== 4) return;
|
|
236
|
+
const prompt = blessed.prompt({
|
|
237
|
+
parent: screen,
|
|
238
|
+
top: 'center',
|
|
239
|
+
left: 'center',
|
|
240
|
+
height: 'shrink',
|
|
241
|
+
width: '60%',
|
|
242
|
+
border: 'line',
|
|
243
|
+
tags: true,
|
|
244
|
+
style: {
|
|
245
|
+
fg: COLORS.labelFg,
|
|
246
|
+
bg: COLORS.contentBg,
|
|
247
|
+
border: { fg: COLORS.sectionHdr },
|
|
248
|
+
label: { fg: COLORS.sectionHdr },
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
prompt.input('Intro text (prefix spoken before every TTS message):', _introText, (err, val) => {
|
|
252
|
+
prompt.destroy();
|
|
253
|
+
if (!err && val !== null) {
|
|
254
|
+
_introText = val.trim();
|
|
255
|
+
_renderScreen4();
|
|
256
|
+
}
|
|
257
|
+
screen.render();
|
|
258
|
+
});
|
|
259
|
+
screen.render();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// -------------------------------------------------------------------------
|
|
263
|
+
// TUI spinner adapter — captures copy-function progress into _installLog
|
|
264
|
+
|
|
265
|
+
function _makeSpinner() {
|
|
266
|
+
return {
|
|
267
|
+
start(msg) {
|
|
268
|
+
_installLog.push(`{${COLORS.noticeFg}-fg} ⟳ ${msg}{/${COLORS.noticeFg}-fg}`);
|
|
269
|
+
_lastSpinnerIdx = _installLog.length - 1;
|
|
270
|
+
_renderScreen5();
|
|
271
|
+
},
|
|
272
|
+
succeed(msg) {
|
|
273
|
+
const line = `{${COLORS.successFg}-fg} ✓ ${msg || ''}{/${COLORS.successFg}-fg}`;
|
|
274
|
+
if (_lastSpinnerIdx >= 0) {
|
|
275
|
+
_installLog[_lastSpinnerIdx] = line;
|
|
276
|
+
} else {
|
|
277
|
+
_installLog.push(line);
|
|
278
|
+
}
|
|
279
|
+
_lastSpinnerIdx = -1;
|
|
280
|
+
_renderScreen5();
|
|
281
|
+
},
|
|
282
|
+
info(msg) {
|
|
283
|
+
_installLog.push(`{${COLORS.noticeFg}-fg} ℹ ${msg}{/${COLORS.noticeFg}-fg}`);
|
|
284
|
+
_renderScreen5();
|
|
285
|
+
},
|
|
286
|
+
warn(msg) {
|
|
287
|
+
_installLog.push(`{#ffcc00-fg} ⚠ ${msg}{/#ffcc00-fg}`);
|
|
288
|
+
_renderScreen5();
|
|
289
|
+
},
|
|
290
|
+
stop() {},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// -------------------------------------------------------------------------
|
|
295
|
+
// Write AgentVibes config files into targetDir/.claude/
|
|
296
|
+
|
|
297
|
+
async function _writeInstallConfig(targetDir, provider) {
|
|
298
|
+
const claudeDir = path.join(targetDir, '.claude');
|
|
299
|
+
const configDir = path.join(claudeDir, 'config');
|
|
300
|
+
await _fsP.mkdir(configDir, { recursive: true });
|
|
301
|
+
|
|
302
|
+
const defaultVoices = {
|
|
303
|
+
piper: 'en_US-ryan-high',
|
|
304
|
+
macos: 'Samantha',
|
|
305
|
+
soprano: 'soprano-default',
|
|
306
|
+
sapi: 'Microsoft David Desktop',
|
|
307
|
+
};
|
|
308
|
+
// Use voice from Settings if configured, otherwise fall back to provider default
|
|
309
|
+
const configuredVoice = configService?.getConfig?.()?.voice;
|
|
310
|
+
const voice = configuredVoice ?? (defaultVoices[provider] ?? 'en_US-ryan-high');
|
|
311
|
+
|
|
312
|
+
await _fsP.writeFile(path.join(claudeDir, 'tts-provider.txt'), provider);
|
|
313
|
+
await _fsP.writeFile(path.join(claudeDir, 'tts-voice.txt'), voice);
|
|
314
|
+
await _fsP.writeFile(path.join(claudeDir, 'tts-verbosity.txt'), 'medium');
|
|
315
|
+
|
|
316
|
+
const pretext = _introText?.trim() ?? '';
|
|
317
|
+
if (pretext) {
|
|
318
|
+
await _fsP.writeFile(path.join(configDir, 'tts-pretext.txt'), pretext, { mode: 0o600 });
|
|
319
|
+
} else {
|
|
320
|
+
try { await _fsP.unlink(path.join(configDir, 'tts-pretext.txt')); } catch { /* ok */ }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Apply background music settings from Settings tab.
|
|
324
|
+
// play-tts-piper.sh reads background-music-enabled.txt (not background-music.txt),
|
|
325
|
+
// so we must write that file explicitly when music is enabled.
|
|
326
|
+
const bgMusic = configService?.getConfig?.()?.backgroundMusic;
|
|
327
|
+
if (bgMusic?.enabled) {
|
|
328
|
+
await _fsP.writeFile(path.join(configDir, 'background-music-enabled.txt'), 'true');
|
|
329
|
+
// Update the track in audio-effects.cfg (copied from package defaults a moment ago).
|
|
330
|
+
// Only apply if the track name is a safe filename (no pipe characters or path separators).
|
|
331
|
+
const track = bgMusic.track;
|
|
332
|
+
if (track && !/[|/\\]/.test(track)) {
|
|
333
|
+
try {
|
|
334
|
+
const audioEffectsPath = path.join(configDir, 'audio-effects.cfg');
|
|
335
|
+
let content = await _fsP.readFile(audioEffectsPath, 'utf-8');
|
|
336
|
+
content = content.replace(
|
|
337
|
+
/^default\|([^|]*)\|([^|]*)\|(.*)$/m,
|
|
338
|
+
`default|$1|${track}|$3`,
|
|
339
|
+
);
|
|
340
|
+
await _fsP.writeFile(audioEffectsPath, content);
|
|
341
|
+
} catch { /* audio-effects.cfg not yet present — non-fatal */ }
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// -------------------------------------------------------------------------
|
|
347
|
+
// Full installation sequence (runs on screen 5)
|
|
348
|
+
|
|
349
|
+
async function _runInstall() {
|
|
350
|
+
_installLog = [];
|
|
351
|
+
_installRunning = true;
|
|
352
|
+
_installComplete = false;
|
|
353
|
+
_installError = null;
|
|
354
|
+
_lastSpinnerIdx = -1;
|
|
355
|
+
|
|
356
|
+
const targetDir = process.cwd();
|
|
357
|
+
const provider = _selectedProvider ?? 'piper';
|
|
358
|
+
const spinner = _makeSpinner();
|
|
359
|
+
|
|
360
|
+
// Suppress console output from installer.js copy functions — they use
|
|
361
|
+
// chalk+console.log which would corrupt the blessed display.
|
|
362
|
+
const _origLog = console.log;
|
|
363
|
+
const _origWarn = console.warn;
|
|
364
|
+
const _origErr = console.error;
|
|
365
|
+
console.log = () => {};
|
|
366
|
+
console.warn = () => {};
|
|
367
|
+
console.error = () => {};
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
// Create directory structure
|
|
371
|
+
spinner.start('Preparing .claude directory...');
|
|
372
|
+
await _fsP.mkdir(path.join(targetDir, '.claude', 'commands'), { recursive: true });
|
|
373
|
+
await _fsP.mkdir(path.join(targetDir, '.claude', 'hooks'), { recursive: true });
|
|
374
|
+
await _fsP.mkdir(path.join(targetDir, '.claude', 'audio', 'tracks'), { recursive: true });
|
|
375
|
+
spinner.succeed('Directory structure ready');
|
|
376
|
+
|
|
377
|
+
await copyCommandFiles(targetDir, spinner);
|
|
378
|
+
await copyHookFiles(targetDir, spinner);
|
|
379
|
+
await copyPersonalityFiles(targetDir, spinner);
|
|
380
|
+
await copyPluginFiles(targetDir, spinner);
|
|
381
|
+
await copyBmadConfigFiles(targetDir, spinner);
|
|
382
|
+
await copyBackgroundMusicFiles(targetDir, spinner);
|
|
383
|
+
await copyConfigFiles(targetDir, spinner);
|
|
384
|
+
await configureSessionStartHook(targetDir, spinner);
|
|
385
|
+
await installPluginManifest(targetDir, spinner);
|
|
386
|
+
await ensureGitRepo(targetDir, spinner);
|
|
387
|
+
|
|
388
|
+
spinner.start('Writing configuration...');
|
|
389
|
+
await _writeInstallConfig(targetDir, provider);
|
|
390
|
+
spinner.succeed('Configuration saved');
|
|
391
|
+
|
|
392
|
+
// Create .mcp.json if it doesn't already exist
|
|
393
|
+
const mcpConfigPath = path.join(targetDir, '.mcp.json');
|
|
394
|
+
let _mcpCreated = false;
|
|
395
|
+
try {
|
|
396
|
+
await _fsP.access(mcpConfigPath);
|
|
397
|
+
// Already exists — skip to avoid overwriting user's config
|
|
398
|
+
} catch {
|
|
399
|
+
const mcpConfig = {
|
|
400
|
+
mcpServers: {
|
|
401
|
+
agentvibes: {
|
|
402
|
+
command: 'npx',
|
|
403
|
+
args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server'],
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
spinner.start('Creating .mcp.json...');
|
|
408
|
+
await _fsP.writeFile(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
409
|
+
spinner.succeed('.mcp.json created');
|
|
410
|
+
_mcpCreated = true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (provider === 'piper') {
|
|
414
|
+
spinner.start('Checking Piper TTS voices...');
|
|
415
|
+
await checkAndInstallPiper(targetDir, { yes: true, silent: true });
|
|
416
|
+
spinner.succeed('Piper TTS ready');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
_installComplete = true;
|
|
420
|
+
_installRunning = false;
|
|
421
|
+
_installLog.push('');
|
|
422
|
+
_installLog.push(`{${COLORS.successFg}-fg} ✅ AgentVibes installed successfully!{/${COLORS.successFg}-fg}`);
|
|
423
|
+
if (_mcpCreated) {
|
|
424
|
+
_installLog.push(`{${COLORS.successFg}-fg} 📡 .mcp.json created — run: claude --mcp-config .mcp.json{/${COLORS.successFg}-fg}`);
|
|
425
|
+
}
|
|
426
|
+
_installLog.push(`{${COLORS.noticeFg}-fg} ⭐ Star us on GitHub: github.com/preibisch/agentvibes{/${COLORS.noticeFg}-fg}`);
|
|
427
|
+
|
|
428
|
+
} catch (err) {
|
|
429
|
+
_installRunning = false;
|
|
430
|
+
_installError = err.message;
|
|
431
|
+
_installLog.push(`{${COLORS.errorFg}-fg} ✗ Installation failed: ${err.message}{/${COLORS.errorFg}-fg}`);
|
|
432
|
+
} finally {
|
|
433
|
+
console.log = _origLog;
|
|
434
|
+
console.warn = _origWarn;
|
|
435
|
+
console.error = _origErr;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
_renderScreen5();
|
|
439
|
+
|
|
440
|
+
// Show OK button now that install is done (success or error)
|
|
441
|
+
_s5OkBtn.show();
|
|
442
|
+
_s5OkBtn.focus();
|
|
443
|
+
screen.render();
|
|
444
|
+
|
|
445
|
+
// Play TTS greeting on success
|
|
446
|
+
if (_installComplete && !_screen5Announced) {
|
|
447
|
+
_screen5Announced = true;
|
|
448
|
+
const greeting = formatGreeting(_introText, getIntroDefault(process.cwd()));
|
|
449
|
+
const ttsScript = path.resolve(targetDir, '.claude/hooks/play-tts.sh');
|
|
450
|
+
execFile('bash', [ttsScript, greeting], {
|
|
451
|
+
env: buildAudioEnv(),
|
|
452
|
+
timeout: 30000,
|
|
453
|
+
}, () => {});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function _doAccept() {
|
|
458
|
+
if (_screen !== 4 || _installRunning) return;
|
|
459
|
+
_screen++;
|
|
460
|
+
_showCurrentScreen();
|
|
461
|
+
// Start install after screen transition renders (50ms delay in _showCurrentScreen)
|
|
462
|
+
setTimeout(() => _runInstall().catch(() => {}), 100);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// -------------------------------------------------------------------------
|
|
466
|
+
// Screen 4 action buttons — real blessed widgets for keyboard focus + ←/→ nav
|
|
467
|
+
|
|
468
|
+
function _createInstallBtn(label, bg, onClick, textColor = '#ffffff') {
|
|
469
|
+
const btn = blessed.button({
|
|
470
|
+
parent: box,
|
|
471
|
+
content: label,
|
|
472
|
+
mouse: true,
|
|
473
|
+
keys: true,
|
|
474
|
+
shrink: true,
|
|
475
|
+
hidden: true,
|
|
476
|
+
padding: { left: 1, right: 1 },
|
|
477
|
+
style: {
|
|
478
|
+
bg,
|
|
479
|
+
fg: textColor,
|
|
480
|
+
focus: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Focus indicator: ►label◄ with blinking █ — matches settings-tab standard
|
|
485
|
+
let _blinkInterval = null;
|
|
486
|
+
btn.on('focus', () => {
|
|
487
|
+
btn.style.bg = COLORS.btnFocus;
|
|
488
|
+
btn.style.fg = COLORS.btnFocusFg;
|
|
489
|
+
const raw = btn.content.replace(/[►◄█]/g, '').trim();
|
|
490
|
+
btn.setContent(`►${raw}◄ █`);
|
|
491
|
+
let _on = true;
|
|
492
|
+
screen.render();
|
|
493
|
+
_blinkInterval = setInterval(() => {
|
|
494
|
+
_on = !_on;
|
|
495
|
+
if (!btn.content.includes('►')) return;
|
|
496
|
+
const r = btn.content.replace(/[►◄█]/g, '').trim();
|
|
497
|
+
btn.setContent(_on ? `►${r}◄ █` : `►${r}◄`);
|
|
498
|
+
screen.render();
|
|
499
|
+
}, 500);
|
|
500
|
+
});
|
|
501
|
+
btn.on('blur', () => {
|
|
502
|
+
if (_blinkInterval) { clearInterval(_blinkInterval); _blinkInterval = null; }
|
|
503
|
+
btn.style.bg = bg;
|
|
504
|
+
btn.style.fg = textColor;
|
|
505
|
+
const raw = btn.content.replace(/[►◄█]/g, '').trim();
|
|
506
|
+
btn.setContent(raw);
|
|
507
|
+
screen.render();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Press: magenta flash then invoke onClick
|
|
511
|
+
// Guard: don't fire onClick when the completion modal is open — Enter should dismiss it.
|
|
512
|
+
btn.key(['enter', 'space'], () => {
|
|
513
|
+
if (_completionModalOpen) return;
|
|
514
|
+
btn.style.bg = COLORS.btnPress;
|
|
515
|
+
btn.style.fg = 'white';
|
|
516
|
+
screen.render();
|
|
517
|
+
setTimeout(() => {
|
|
518
|
+
btn.style.bg = bg;
|
|
519
|
+
btn.style.fg = textColor;
|
|
520
|
+
screen.render();
|
|
521
|
+
onClick();
|
|
522
|
+
}, 150);
|
|
523
|
+
});
|
|
524
|
+
btn.on('click', () => btn.press());
|
|
525
|
+
return btn;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const _editBtn = _createInstallBtn('Edit', '#1565c0', _doEdit);
|
|
529
|
+
const _acceptBtn = _createInstallBtn('✓ Accept & Install', COLORS.btnDefault, _doAccept);
|
|
530
|
+
|
|
531
|
+
// Edit sits inline with the intro text row; Accept & Install is below
|
|
532
|
+
_editBtn.top = 8; _editBtn.left = 36;
|
|
533
|
+
_acceptBtn.top = 13; _acceptBtn.left = 4;
|
|
534
|
+
|
|
535
|
+
// ↓/↑ navigate between Edit and Accept & Install
|
|
536
|
+
// Note: Tab is NOT used here — 'tab' is registered globally by navigation.js (cycles tabs)
|
|
537
|
+
_editBtn.key(['down', 'right'], () => { _acceptBtn.focus(); screen.render(); });
|
|
538
|
+
_acceptBtn.key(['up', 'left'], () => { _editBtn.focus(); screen.render(); });
|
|
539
|
+
|
|
540
|
+
// -------------------------------------------------------------------------
|
|
541
|
+
// Screen 1 buttons — Begin (cyan) and Exit (grey)
|
|
542
|
+
|
|
543
|
+
const _s1BeginBtn = _createInstallBtn('▶ Begin', '#00838f', () => {
|
|
544
|
+
_screen++;
|
|
545
|
+
_showCurrentScreen();
|
|
546
|
+
});
|
|
547
|
+
const _s1ExitBtn = _createInstallBtn('✗ Exit', '#546e7a', () => {
|
|
548
|
+
box.hide();
|
|
549
|
+
screen.render();
|
|
550
|
+
if (typeof focusMainTabBar === 'function') focusMainTabBar();
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
_s1BeginBtn.top = 5; _s1BeginBtn.left = 4;
|
|
554
|
+
_s1ExitBtn.top = 5; _s1ExitBtn.left = 20;
|
|
555
|
+
|
|
556
|
+
// ←/→ horizontal and ↑/↓ vertical — both navigate between the two buttons
|
|
557
|
+
_s1BeginBtn.key(['right', 'down'], () => { _s1ExitBtn.focus(); screen.render(); });
|
|
558
|
+
_s1ExitBtn.key(['right', 'down'], () => { _s1BeginBtn.focus(); screen.render(); });
|
|
559
|
+
_s1ExitBtn.key(['left', 'up', 'S-tab'], () => { _s1BeginBtn.focus(); screen.render(); });
|
|
560
|
+
_s1BeginBtn.key(['left', 'up', 'S-tab'], () => { _s1ExitBtn.focus(); screen.render(); });
|
|
561
|
+
|
|
562
|
+
// -------------------------------------------------------------------------
|
|
563
|
+
// Screen 2 button — Continue (shown after deps check passes)
|
|
564
|
+
|
|
565
|
+
const _s2ContinueBtn = _createInstallBtn('Continue →', '#1565c0', () => {
|
|
566
|
+
_screen++;
|
|
567
|
+
_showCurrentScreen();
|
|
568
|
+
});
|
|
569
|
+
_s2ContinueBtn.top = 12; _s2ContinueBtn.left = 4;
|
|
570
|
+
// → also advances without the flash delay
|
|
571
|
+
_s2ContinueBtn.key(['right'], () => { _screen++; _showCurrentScreen(); });
|
|
572
|
+
|
|
573
|
+
// Screen 3: no Continue button — Enter/→ on the list confirms selection and advances
|
|
574
|
+
|
|
575
|
+
// -------------------------------------------------------------------------
|
|
576
|
+
// Screen 5 button — OK (summary page only, config already saved on screen 4)
|
|
577
|
+
|
|
578
|
+
const _s5OkBtn = _createInstallBtn('✓ OK — Done', '#1565c0', () => {
|
|
579
|
+
_dismissCompletionModal();
|
|
580
|
+
});
|
|
581
|
+
_s5OkBtn.bottom = 3; _s5OkBtn.left = 4; // bottom-anchored: sits above hintLine (bottom:2)
|
|
582
|
+
|
|
583
|
+
// -------------------------------------------------------------------------
|
|
584
|
+
// Screen renderers
|
|
585
|
+
|
|
586
|
+
const _HDR = (emoji, label) =>
|
|
587
|
+
`{${COLORS.sectionHdr}-fg}${emoji} ${label} ${'─'.repeat(100)}{/${COLORS.sectionHdr}-fg}`;
|
|
588
|
+
|
|
589
|
+
function _renderScreen1() {
|
|
590
|
+
contentBox.setContent(_c([
|
|
591
|
+
_HDR('🔧', 'Setup Wizard'),
|
|
592
|
+
'',
|
|
593
|
+
` {${COLORS.noticeFg}-fg}TTS for AI assistants with personality.{/${COLORS.noticeFg}-fg}`,
|
|
594
|
+
'',
|
|
595
|
+
'', // ← [▶ Begin] [✗ Exit] buttons here (box row 5)
|
|
596
|
+
]));
|
|
597
|
+
hintLine.setContent(' Screen 1/5: Welcome | [←/→] Navigate | [Enter] Begin | [Esc] Exit');
|
|
598
|
+
_s1BeginBtn.focus();
|
|
599
|
+
screen.render();
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async function _renderScreen2() {
|
|
603
|
+
const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
604
|
+
let frameIdx = 0;
|
|
605
|
+
_checking = true;
|
|
606
|
+
_s2ContinueBtn.hide(); // hidden during spinner
|
|
607
|
+
|
|
608
|
+
contentBox.setContent(_c([
|
|
609
|
+
_HDR('🔍', 'Dependency Check'),
|
|
610
|
+
'',
|
|
611
|
+
` {${COLORS.noticeFg}-fg}${frames[0]} Checking dependencies...{/${COLORS.noticeFg}-fg}`,
|
|
612
|
+
]));
|
|
613
|
+
hintLine.setContent(' Screen 2/5: Dependencies | [←] Back | [Enter] Next');
|
|
614
|
+
screen.render();
|
|
615
|
+
|
|
616
|
+
const spinInterval = setInterval(() => {
|
|
617
|
+
frameIdx = (frameIdx + 1) % frames.length;
|
|
618
|
+
contentBox.setContent(_c([
|
|
619
|
+
_HDR('🔍', 'Dependency Check'),
|
|
620
|
+
'',
|
|
621
|
+
` {${COLORS.noticeFg}-fg}${frames[frameIdx]} Checking dependencies...{/${COLORS.noticeFg}-fg}`,
|
|
622
|
+
]));
|
|
623
|
+
screen.render();
|
|
624
|
+
}, 100);
|
|
625
|
+
|
|
626
|
+
try {
|
|
627
|
+
_deps = await _checkDependenciesAsync();
|
|
628
|
+
} finally {
|
|
629
|
+
clearInterval(spinInterval);
|
|
630
|
+
_checking = false;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const ok = () => `{${COLORS.successFg}-fg}✅ Installed{/${COLORS.successFg}-fg}`;
|
|
634
|
+
const bad = () => `{${COLORS.errorFg}-fg}❌ Not found{/${COLORS.errorFg}-fg}`;
|
|
635
|
+
|
|
636
|
+
const ttsOk = _deps.piper || _deps.soprano;
|
|
637
|
+
contentBox.setContent(_c([
|
|
638
|
+
_HDR('🔍', 'Dependency Check'),
|
|
639
|
+
'',
|
|
640
|
+
` {${COLORS.noticeFg}-fg}${'Dependency'.padEnd(14)}Status{/${COLORS.noticeFg}-fg}`,
|
|
641
|
+
` {${COLORS.noticeFg}-fg}${'─'.repeat(78)}{/${COLORS.noticeFg}-fg}`,
|
|
642
|
+
` {${COLORS.labelFg}-fg}${'Node.js'.padEnd(14)}{/${COLORS.labelFg}-fg}${_deps.node ? ok() : bad()}`,
|
|
643
|
+
` {${COLORS.labelFg}-fg}${'npm'.padEnd(14)}{/${COLORS.labelFg}-fg}${_deps.npm ? ok() : bad()}`,
|
|
644
|
+
` {${COLORS.labelFg}-fg}${'Piper TTS'.padEnd(14)}{/${COLORS.labelFg}-fg}${_deps.piper ? ok() : bad()}`,
|
|
645
|
+
` {${COLORS.labelFg}-fg}${'Soprano TTS'.padEnd(14)}{/${COLORS.labelFg}-fg}${_deps.soprano ? ok() : bad()}`,
|
|
646
|
+
` {${COLORS.labelFg}-fg}${'ffmpeg'.padEnd(14)}{/${COLORS.labelFg}-fg}${_deps.ffmpeg ? ok() : `{${COLORS.errorFg}-fg}⚠ Not found (needed for background music){/${COLORS.errorFg}-fg}`}`,
|
|
647
|
+
'',
|
|
648
|
+
ttsOk
|
|
649
|
+
? ` {${COLORS.successFg}-fg}✅ TTS Providers Detected{/${COLORS.successFg}-fg}`
|
|
650
|
+
: ` {${COLORS.errorFg}-fg}⚠ No TTS provider found. Install Piper or Soprano first.{/${COLORS.errorFg}-fg}`,
|
|
651
|
+
'', // blank separator
|
|
652
|
+
'', // ← [Continue →] button here (box row 12) when TTS detected
|
|
653
|
+
]));
|
|
654
|
+
if (ttsOk) {
|
|
655
|
+
_s2ContinueBtn.show();
|
|
656
|
+
_s2ContinueBtn.focus();
|
|
657
|
+
}
|
|
658
|
+
screen.render();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function _renderScreen3() {
|
|
662
|
+
const providers = [];
|
|
663
|
+
if (_deps?.piper) providers.push('piper');
|
|
664
|
+
if (_deps?.soprano) providers.push('soprano');
|
|
665
|
+
|
|
666
|
+
if (providers.length === 0) providers.push('piper'); // fallback
|
|
667
|
+
if (!_selectedProvider) _selectedProvider = providers[0];
|
|
668
|
+
|
|
669
|
+
// Pad items to 96 visible chars so they fully overwrite any stale cells from Screen 2.
|
|
670
|
+
// Selected row uses cyan bg + black text (matches button focus standard).
|
|
671
|
+
const items = providers.map(p =>
|
|
672
|
+
p === _selectedProvider
|
|
673
|
+
? `{#00e5ff-bg}{#000000-fg}{bold} ● ${p.padEnd(92)}{/bold}{/#000000-fg}{/#00e5ff-bg}`
|
|
674
|
+
: `{${COLORS.labelFg}-fg} ${p.padEnd(93)}{/${COLORS.labelFg}-fg}`
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
// Pad item list to 3 entries so the Continue button sits at a fixed row
|
|
678
|
+
// and all stale lines from Screen 2 (which has ~10 lines) are overwritten.
|
|
679
|
+
const paddedItems = [...items];
|
|
680
|
+
while (paddedItems.length < 3) paddedItems.push(` ${''.padEnd(93)}`);
|
|
681
|
+
|
|
682
|
+
// Append trailing blank rows (space-padded) so blessed rewrites every cell that
|
|
683
|
+
// screen 2 used. Two screen.render() calls in the same tick are batched, so the
|
|
684
|
+
// intermediate "clear" render never fires — trailing spaces here fix that in one pass.
|
|
685
|
+
const _blank = ' '.repeat(120);
|
|
686
|
+
const _trail = Array(12).fill(_blank);
|
|
687
|
+
contentBox.setContent(_c([
|
|
688
|
+
_HDR('🎤', 'Provider Selection'),
|
|
689
|
+
'',
|
|
690
|
+
` {${COLORS.noticeFg}-fg}${'Available TTS providers:'.padEnd(94)}{/${COLORS.noticeFg}-fg}`,
|
|
691
|
+
'',
|
|
692
|
+
...paddedItems.map(i => ` ${i}`),
|
|
693
|
+
..._trail,
|
|
694
|
+
]));
|
|
695
|
+
hintLine.setContent(' Screen 3/5: Provider | [←] Back | [↑↓] Choose | [Enter/→] Confirm & Continue');
|
|
696
|
+
box.focus();
|
|
697
|
+
screen.render();
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function _renderScreen4() {
|
|
701
|
+
const provider = _selectedProvider ?? 'piper';
|
|
702
|
+
const intro = _introText || '';
|
|
703
|
+
const folderName = getIntroDefault(process.cwd()) || 'AgentVibes';
|
|
704
|
+
const example = `${folderName}: Here`;
|
|
705
|
+
const voiceId = providerService?.getActiveVoiceId?.() ?? 'en_US-amy-medium';
|
|
706
|
+
|
|
707
|
+
contentBox.setContent(_c([
|
|
708
|
+
_HDR('🎤', 'Provider & Voice'),
|
|
709
|
+
'',
|
|
710
|
+
` {${COLORS.labelFg}-fg}${'Provider:'.padEnd(14)}{/${COLORS.labelFg}-fg}{${COLORS.valueFg}-fg}${provider}{/${COLORS.valueFg}-fg}`,
|
|
711
|
+
` {${COLORS.labelFg}-fg}${'Voice:'.padEnd(14)}{/${COLORS.labelFg}-fg}{${COLORS.valueFg}-fg}${voiceId}{/${COLORS.valueFg}-fg} {${COLORS.noticeFg}-fg}(after installation, you can change in Settings){/${COLORS.noticeFg}-fg}`,
|
|
712
|
+
'',
|
|
713
|
+
_HDR('✍️', 'Intro Text'),
|
|
714
|
+
'',
|
|
715
|
+
` {${COLORS.labelFg}-fg}${'Intro text:'.padEnd(14)}{/${COLORS.labelFg}-fg}{${COLORS.valueFg}-fg}${intro || '(none)'}{/${COLORS.valueFg}-fg}`,
|
|
716
|
+
// ↑ [Edit] button rendered inline at box row 8, left=36
|
|
717
|
+
'',
|
|
718
|
+
` {${COLORS.noticeFg}-fg}Example:{/${COLORS.noticeFg}-fg} {${COLORS.valueFg}-fg}"${example}"{/${COLORS.valueFg}-fg}`,
|
|
719
|
+
'',
|
|
720
|
+
'',
|
|
721
|
+
'', // ← [✓ Accept & Install] button rendered as real widget here (box row 13)
|
|
722
|
+
]));
|
|
723
|
+
hintLine.setContent(' Screen 4/5: Config | [Esc] Back | [E] Edit | [↓] Accept & Install');
|
|
724
|
+
_acceptBtn.focus();
|
|
725
|
+
screen.render();
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function _renderScreen5() {
|
|
729
|
+
const header = _installError
|
|
730
|
+
? _HDR('❌', 'Installation Failed')
|
|
731
|
+
: _installComplete
|
|
732
|
+
? _HDR('✅', 'Installation Complete')
|
|
733
|
+
: _HDR('⚙️', 'Installing AgentVibes...');
|
|
734
|
+
|
|
735
|
+
const hint = (_installComplete || _installError)
|
|
736
|
+
? ' Screen 5/5: Complete | [Enter] OK — Done'
|
|
737
|
+
: ' Screen 5/5: Installing... | Please wait';
|
|
738
|
+
|
|
739
|
+
// Show last 18 log lines so content fits in the box
|
|
740
|
+
const MAX_LINES = 18;
|
|
741
|
+
const visibleLog = _installLog.length > MAX_LINES
|
|
742
|
+
? _installLog.slice(-MAX_LINES)
|
|
743
|
+
: _installLog;
|
|
744
|
+
|
|
745
|
+
contentBox.setContent(_c([
|
|
746
|
+
header,
|
|
747
|
+
'',
|
|
748
|
+
...visibleLog,
|
|
749
|
+
]));
|
|
750
|
+
hintLine.setContent(hint);
|
|
751
|
+
screen.render();
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function _showInstallNotice(message) {
|
|
755
|
+
const width = Math.max(28, message.length + 6);
|
|
756
|
+
const notice = blessed.box({
|
|
757
|
+
parent: screen,
|
|
758
|
+
top: 'center',
|
|
759
|
+
left: 'center',
|
|
760
|
+
width,
|
|
761
|
+
height: 3,
|
|
762
|
+
border: { type: 'line' },
|
|
763
|
+
tags: true,
|
|
764
|
+
content: `{center}${message}{/center}`,
|
|
765
|
+
style: {
|
|
766
|
+
fg: '#e3f2fd',
|
|
767
|
+
bg: COLORS.contentBg,
|
|
768
|
+
border: { fg: 'bright-cyan' },
|
|
769
|
+
},
|
|
770
|
+
});
|
|
771
|
+
screen.render();
|
|
772
|
+
setTimeout(() => { try { notice.destroy(); screen.render(); } catch {} }, 2500);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function _dismissCompletionModal() {
|
|
776
|
+
if (_completionModalBox) {
|
|
777
|
+
_completionModalBox.destroy();
|
|
778
|
+
_completionModalBox = null;
|
|
779
|
+
}
|
|
780
|
+
_completionModalOpen = false;
|
|
781
|
+
_screen = 1;
|
|
782
|
+
box.hide();
|
|
783
|
+
_showInstallNotice('Installation Complete — Settings Saved');
|
|
784
|
+
screen.render();
|
|
785
|
+
navigationService?.switchTab('settings');
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function _showCurrentScreen() {
|
|
789
|
+
// Show Screen 1 buttons only on screen 1
|
|
790
|
+
if (_screen === 1) {
|
|
791
|
+
_s1BeginBtn.show(); _s1ExitBtn.show();
|
|
792
|
+
} else {
|
|
793
|
+
_s1BeginBtn.hide(); _s1ExitBtn.hide();
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Screen 2 continue button: hidden on other screens; _renderScreen2 manages show/focus
|
|
797
|
+
if (_screen !== 2) _s2ContinueBtn.hide();
|
|
798
|
+
|
|
799
|
+
// Screen 5 OK button: hidden during active install, shown by _runInstall() on completion
|
|
800
|
+
if (_screen === 5 && (_installComplete || _installError)) {
|
|
801
|
+
_s5OkBtn.show();
|
|
802
|
+
} else {
|
|
803
|
+
_s5OkBtn.hide();
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Show Screen 4 action buttons only on screen 4
|
|
807
|
+
if (_screen === 4) {
|
|
808
|
+
_editBtn.show(); _acceptBtn.show();
|
|
809
|
+
} else {
|
|
810
|
+
_editBtn.hide(); _acceptBtn.hide();
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (_screen !== _lastScreen) {
|
|
814
|
+
// Nuclear clear: force-invalidate every olines cell so blessed's diff renderer
|
|
815
|
+
// actually writes blanks to the terminal (blessed skips cells it thinks are
|
|
816
|
+
// unchanged — setting attr=-1 is impossible for any real cell so draw() is
|
|
817
|
+
// forced to physically rewrite every character).
|
|
818
|
+
try {
|
|
819
|
+
for (let r = 0; r < screen.height; r++) {
|
|
820
|
+
const orow = screen.olines?.[r];
|
|
821
|
+
if (!orow) continue;
|
|
822
|
+
for (let c = 0; c < screen.width; c++) {
|
|
823
|
+
if (orow[c]) orow[c][0] = -1;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
// Row 2 (header bottom) never becomes dirty on its own — force it so
|
|
827
|
+
// draw() writes headerBg+spaces and overwrites any ghost terminal content.
|
|
828
|
+
if (screen.lines?.[2]) screen.lines[2].dirty = true;
|
|
829
|
+
} catch {}
|
|
830
|
+
|
|
831
|
+
const _clearLine = ' '.repeat(150);
|
|
832
|
+
const _clearPage = Array(25).fill(_clearLine).join('\n');
|
|
833
|
+
contentBox.setContent(_clearPage);
|
|
834
|
+
hintLine.setContent(_clearLine);
|
|
835
|
+
screen.render();
|
|
836
|
+
|
|
837
|
+
const targetScreen = _screen;
|
|
838
|
+
_lastScreen = _screen;
|
|
839
|
+
// 50 ms delay: enough for the terminal to display the blank frame before
|
|
840
|
+
// the new screen content overwrites it. setTimeout(0) is too fast —
|
|
841
|
+
// both renders land in the same display frame.
|
|
842
|
+
setTimeout(() => {
|
|
843
|
+
if (_screen !== targetScreen) return;
|
|
844
|
+
switch (_screen) {
|
|
845
|
+
case 1: _renderScreen1(); break;
|
|
846
|
+
case 2: _renderScreen2(); break;
|
|
847
|
+
case 3: _renderScreen3(); break;
|
|
848
|
+
case 4: _renderScreen4(); break;
|
|
849
|
+
case 5: _renderScreen5(); break;
|
|
850
|
+
}
|
|
851
|
+
}, 50);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
switch (_screen) {
|
|
855
|
+
case 1: _renderScreen1(); break;
|
|
856
|
+
case 2: _renderScreen2(); break;
|
|
857
|
+
case 3: _renderScreen3(); break;
|
|
858
|
+
case 4: _renderScreen4(); break;
|
|
859
|
+
case 5: _renderScreen5(); break;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// -------------------------------------------------------------------------
|
|
864
|
+
// Navigation
|
|
865
|
+
|
|
866
|
+
// Use screen.key() instead of box.key() so handlers fire regardless of which
|
|
867
|
+
// blessed element currently holds focus. Guard with `box.hidden` so they are
|
|
868
|
+
// no-ops when another tab is active.
|
|
869
|
+
|
|
870
|
+
screen.key(['enter'], () => {
|
|
871
|
+
if (box.hidden || _checking) return;
|
|
872
|
+
if (_completionModalOpen) { _dismissCompletionModal(); return; } // always first
|
|
873
|
+
if (_screen === 1) return; // Screen 1: Enter handled by Begin/Exit buttons
|
|
874
|
+
if (_screen === 2) return; // Screen 2: Enter handled by Continue button
|
|
875
|
+
if (_screen === 4) return; // Screen 4: Enter handled by the focused button
|
|
876
|
+
if (_screen === 5) return; // Screen 5: Enter handled by OK button
|
|
877
|
+
if (_screen < 5) {
|
|
878
|
+
_screen++;
|
|
879
|
+
_showCurrentScreen();
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
screen.key(['escape'], () => {
|
|
884
|
+
if (box.hidden || _checking) return;
|
|
885
|
+
if (_completionModalOpen) { _dismissCompletionModal(); return; }
|
|
886
|
+
if (_screen > 1) {
|
|
887
|
+
_screen--;
|
|
888
|
+
_showCurrentScreen();
|
|
889
|
+
} else {
|
|
890
|
+
box.hide();
|
|
891
|
+
screen.render();
|
|
892
|
+
// Defer so the escape keypress event finishes propagating before focus changes.
|
|
893
|
+
// Calling focusMainTabBar() synchronously here would set focus to the tab bar
|
|
894
|
+
// item mid-event, causing its own key(['escape']) handler to fire in the same
|
|
895
|
+
// emission and call onFocus() → re-focus a button inside the now-hidden box.
|
|
896
|
+
if (typeof focusMainTabBar === 'function') setTimeout(() => focusMainTabBar(), 0);
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
screen.key(['up'], () => {
|
|
901
|
+
if (box.hidden) return;
|
|
902
|
+
if (_screen === 3 && _deps) {
|
|
903
|
+
const providers = [];
|
|
904
|
+
if (_deps.piper) providers.push('piper');
|
|
905
|
+
if (_deps.soprano) providers.push('soprano');
|
|
906
|
+
const idx = providers.indexOf(_selectedProvider ?? providers[0]);
|
|
907
|
+
_selectedProvider = providers[Math.max(0, idx - 1)];
|
|
908
|
+
_renderScreen3();
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
// Left arrow = go back (same logic as Escape)
|
|
913
|
+
// Screen 4: left arrow is handled by button ←/→ navigation; use Escape to go back
|
|
914
|
+
screen.key(['left'], () => {
|
|
915
|
+
if (box.hidden || _checking) return;
|
|
916
|
+
if (_screen === 4) return;
|
|
917
|
+
if (_screen > 1) {
|
|
918
|
+
_screen--;
|
|
919
|
+
_showCurrentScreen();
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
// Right arrow = go forward (same logic as Enter, without save/finish side-effects)
|
|
924
|
+
// Screen 1: right arrow handled by button ←/→ navigation
|
|
925
|
+
screen.key(['right'], () => {
|
|
926
|
+
if (box.hidden || _checking) return;
|
|
927
|
+
if (_screen === 1) return;
|
|
928
|
+
if (_screen === 2) return; // Screen 2: → handled by Continue button
|
|
929
|
+
if (_screen === 3) { _screen++; _showCurrentScreen(); return; } // → confirms provider and advances
|
|
930
|
+
if (_screen === 4) return; // Screen 4: → handled by button nav
|
|
931
|
+
if (_screen === 5) return; // Screen 5: → handled by button nav
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
// Down arrow: Screen 3 provider nav; Screen 1 ↓ is handled by button key handlers
|
|
935
|
+
// (tab bar's el.key(['down']) → onFocus() focuses Begin, then button ↓ → Exit)
|
|
936
|
+
screen.key(['down'], () => {
|
|
937
|
+
if (box.hidden) return;
|
|
938
|
+
if (_screen === 3 && _deps) {
|
|
939
|
+
const providers = [];
|
|
940
|
+
if (_deps.piper) providers.push('piper');
|
|
941
|
+
if (_deps.soprano) providers.push('soprano');
|
|
942
|
+
const idx = providers.indexOf(_selectedProvider ?? providers[0]);
|
|
943
|
+
_selectedProvider = providers[Math.min(providers.length - 1, idx + 1)];
|
|
944
|
+
_renderScreen3();
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
// [E] on Screen 4: edit intro text inline
|
|
949
|
+
screen.key(['e', 'E'], () => { _doEdit(); });
|
|
950
|
+
|
|
951
|
+
// [O] anywhere: dismiss the completion modal (OK button)
|
|
952
|
+
screen.key(['o', 'O'], () => {
|
|
953
|
+
if (box.hidden || !_completionModalOpen) return;
|
|
954
|
+
_dismissCompletionModal();
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
// -------------------------------------------------------------------------
|
|
958
|
+
// Tab Component Contract
|
|
959
|
+
|
|
960
|
+
return {
|
|
961
|
+
box,
|
|
962
|
+
|
|
963
|
+
show() {
|
|
964
|
+
_screen = 1;
|
|
965
|
+
_screen5Announced = false;
|
|
966
|
+
_installLog = [];
|
|
967
|
+
_installRunning = false;
|
|
968
|
+
_installComplete = false;
|
|
969
|
+
_installError = null;
|
|
970
|
+
_lastSpinnerIdx = -1;
|
|
971
|
+
if (_completionModalBox) { _completionModalBox.destroy(); _completionModalBox = null; }
|
|
972
|
+
_completionModalOpen = false;
|
|
973
|
+
box.show();
|
|
974
|
+
_showCurrentScreen();
|
|
975
|
+
screen.render();
|
|
976
|
+
},
|
|
977
|
+
|
|
978
|
+
hide() {
|
|
979
|
+
box.hide();
|
|
980
|
+
screen.render();
|
|
981
|
+
},
|
|
982
|
+
|
|
983
|
+
onFocus() {
|
|
984
|
+
// Focus the active interactive element, not just the box container
|
|
985
|
+
if (_screen === 1) {
|
|
986
|
+
_s1BeginBtn.focus();
|
|
987
|
+
} else if (_screen === 4) {
|
|
988
|
+
_editBtn.focus();
|
|
989
|
+
} else if (_screen === 5 && (_installComplete || _installError)) {
|
|
990
|
+
_s5OkBtn.focus();
|
|
991
|
+
} else {
|
|
992
|
+
box.focus();
|
|
993
|
+
}
|
|
994
|
+
screen.render();
|
|
995
|
+
},
|
|
996
|
+
|
|
997
|
+
onBlur() {},
|
|
998
|
+
|
|
999
|
+
getFooterText() {
|
|
1000
|
+
return FOOTER_TEXT;
|
|
1001
|
+
},
|
|
1002
|
+
|
|
1003
|
+
getFooterColor() {
|
|
1004
|
+
return COLORS.footerBg;
|
|
1005
|
+
},
|
|
1006
|
+
};
|
|
1007
|
+
}
|