agentvibes 5.12.0-alpha.0 → 5.13.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 +15 -1
- package/.agentvibes/install-manifest.json +342 -0
- package/.claude/activation-instructions +54 -54
- package/.claude/commands/agent-vibes/commands.json +0 -20
- package/.claude/commands/agent-vibes/unmute.md +6 -2
- package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
- package/.claude/commands/agent-vibes-rdp.md +24 -24
- package/.claude/config/audio-effects.cfg +6 -6
- package/.claude/config/audio-effects.cfg.bak-kokoro +7 -0
- package/.claude/config/background-music-enabled.txt +1 -1
- package/.claude/config/background-music-position.txt +27 -0
- package/.claude/config/background-music-volume.txt +1 -0
- package/.claude/config/background-music.cfg +1 -0
- package/.claude/config/background-music.txt +1 -0
- package/.claude/config/language.txt +1 -0
- package/.claude/config/personality.txt +1 -0
- package/.claude/config/tts-speech-rate.txt +1 -0
- package/.claude/config/tts-verbosity.txt +1 -0
- package/.claude/docs/TERMUX_SETUP.md +408 -408
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/agentvibes-session-id.sh +69 -0
- package/.claude/hooks/bmad-party-speak.sh +20 -4
- package/.claude/hooks/bmad-speak.sh +60 -2
- package/.claude/hooks/bmad-tts-injector.sh +20 -1
- package/.claude/hooks/bmad-voice-manager.sh +25 -3
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +21 -2
- package/.claude/hooks/clawdbot-receiver.sh +19 -1
- package/.claude/hooks/elevenlabs-voices.sh +62 -0
- package/.claude/hooks/kokoro-installer.sh +20 -10
- package/.claude/hooks/language-manager.sh +10 -3
- package/.claude/hooks/party-set-room.sh +71 -0
- package/.claude/hooks/party-stage-roster.py +328 -0
- package/.claude/hooks/personality-manager.sh +19 -2
- package/.claude/hooks/piper-voice-manager.sh +3 -2
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +24 -5
- package/.claude/hooks/play-tts-elevenlabs.sh +38 -118
- package/.claude/hooks/play-tts-kokoro.sh +33 -6
- package/.claude/hooks/play-tts-soprano.sh +3 -2
- package/.claude/hooks/play-tts-ssh-remote.sh +72 -27
- package/.claude/hooks/play-tts-termux-ssh.sh +5 -4
- package/.claude/hooks/play-tts.sh +86 -69
- package/.claude/hooks/provider-catalog.json +352 -0
- package/.claude/hooks/provider-catalog.sh +161 -0
- package/.claude/hooks/provider-commands.sh +2 -1
- package/.claude/hooks/provider-manager.sh +47 -9
- package/.claude/hooks/python-resolver.sh +117 -0
- package/.claude/hooks/session-id.sh +56 -0
- package/.claude/hooks/session-start-tts.sh +39 -0
- package/.claude/hooks/speed-manager.sh +1 -1
- package/.claude/hooks/translate-manager.sh +3 -2
- package/.claude/hooks/translator.py +1 -1
- package/.claude/hooks/voice-manager.sh +242 -10
- package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +119 -0
- package/.claude/hooks-windows/language-manager.ps1 +7 -1
- package/.claude/hooks-windows/personality-manager.ps1 +16 -1
- package/.claude/hooks-windows/play-tts-kokoro.ps1 +20 -4
- package/.claude/hooks-windows/play-tts.ps1 +32 -3
- package/.claude/hooks-windows/provider-catalog.ps1 +140 -0
- package/.claude/hooks-windows/provider-manager.ps1 +63 -8
- package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +153 -0
- package/.claude/hooks-windows/tts-watcher.ps1 +84 -8
- package/.claude/hooks-windows/voice-manager-windows.ps1 +49 -0
- package/.claude/piper-voices-dir.txt +1 -0
- package/.clawdbot/README.md +105 -105
- package/.mcp.json +15 -0
- package/LICENSE +190 -190
- package/README.md +20 -6
- package/RELEASE_NOTES.md +81 -0
- package/WINDOWS-SETUP.md +208 -208
- package/bin/agent-vibes +39 -39
- package/bin/mcp-server.js +121 -121
- 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/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/pyproject.toml +52 -52
- package/mcp-server/requirements.txt +2 -2
- package/mcp-server/server.py +146 -49
- package/mcp-server/test_mcp_correctness.py +20 -2
- package/mcp-server/test_server.py +395 -395
- package/mcp-server/test_windows_script_parity.py +0 -2
- package/package.json +1 -1
- package/src/cli/list-personalities.js +110 -110
- package/src/cli/list-voices.js +115 -11
- package/src/commands/bmad-voices.js +394 -394
- package/src/console/app.js +1 -9
- package/src/console/bling.js +71 -0
- package/src/console/brand-colors.js +13 -13
- package/src/console/constants/personalities.js +44 -44
- package/src/console/footer-config.js +0 -4
- package/src/console/music-preview.js +79 -0
- package/src/console/navigation.js +0 -1
- package/src/console/tabs/agents-tab.js +14 -2
- package/src/console/tabs/help-tab.js +314 -314
- package/src/console/tabs/music-tab.js +81 -5
- package/src/console/tabs/placeholder-tab.js +0 -2
- package/src/console/tabs/readme-tab.js +272 -272
- package/src/console/tabs/settings-tab.js +207 -14
- package/src/console/tabs/setup-tab.js +39 -36
- package/src/console/tabs/voices-tab.js +102 -23
- package/src/console/widgets/destroy-list.js +25 -25
- package/src/console/widgets/notice.js +55 -55
- package/src/console/widgets/track-picker.js +82 -0
- package/src/installer/language-screen.js +31 -31
- package/src/installer/music-file-input.js +304 -304
- package/src/installer.js +124 -10
- package/src/services/language-service.js +47 -47
- package/src/services/navigation-service.js +1 -1
- package/src/services/provider-catalog.js +412 -0
- package/src/services/provider-voice-catalog.js +52 -73
- package/src/services/tts-engine-service.js +29 -0
- package/src/utils/audio-format-validator.js +277 -277
- package/src/utils/dependency-checker.js +469 -469
- package/src/utils/file-ownership-verifier.js +358 -358
- package/src/utils/music-file-validator.js +285 -285
- package/src/utils/preview-list-prompt.js +144 -144
- package/src/utils/provider-validator.js +62 -12
- package/src/utils/secure-music-storage.js +412 -412
- package/voice-assignments.json +8244 -8244
- package/.claude/commands/agent-vibes/language.md +0 -23
- package/.claude/commands/agent-vibes/learn.md +0 -67
- package/.claude/commands/agent-vibes/replay-target.md +0 -14
- package/.claude/commands/agent-vibes/target-voice.md +0 -26
- package/.claude/commands/agent-vibes/target.md +0 -30
- package/.claude/hooks/learn-manager.sh +0 -492
- package/.claude/hooks/replay-target-audio.sh +0 -95
- package/.claude/hooks-windows/learn-manager.ps1 +0 -241
package/src/installer.js
CHANGED
|
@@ -141,6 +141,33 @@ async function saveManifest(manifestPath, files) {
|
|
|
141
141
|
// • dest hash == manifest hash → copy (action: 'updated')
|
|
142
142
|
// • dest hash != manifest hash → skip (action: 'skipped', .user.bak saved)
|
|
143
143
|
// • no manifest entry yet → copy (action: 'updated')
|
|
144
|
+
// Preserve destPath's CURRENT content in a sidecar backup before anything
|
|
145
|
+
// overwrites it. Never clobbers an existing .user.bak — that backup may hold the
|
|
146
|
+
// only surviving copy of an earlier customization; a second backup gets a
|
|
147
|
+
// content-stamped name instead. Returns true only when the current content is
|
|
148
|
+
// safely stored somewhere, so callers can refuse to overwrite when it isn't.
|
|
149
|
+
async function backupUserFile(destPath) {
|
|
150
|
+
const bak = `${destPath}.user.bak`;
|
|
151
|
+
try {
|
|
152
|
+
const currentHash = await computeFileHash(destPath);
|
|
153
|
+
if (!currentHash) return false;
|
|
154
|
+
|
|
155
|
+
const existingHash = await computeFileHash(bak);
|
|
156
|
+
if (existingHash === currentHash) return true; // already backed up
|
|
157
|
+
|
|
158
|
+
if (existingHash) {
|
|
159
|
+
// A different backup exists — keep it and stamp this one.
|
|
160
|
+
await fs.copyFile(destPath, `${destPath}.user.bak.${currentHash.slice(0, 8)}`);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await fs.copyFile(destPath, bak);
|
|
165
|
+
return true;
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
144
171
|
async function manifestSafeCopy(srcPath, destPath, manifest) {
|
|
145
172
|
const srcHash = await computeFileHash(srcPath);
|
|
146
173
|
if (!srcHash) return { action: 'skipped', hash: null }; // src missing
|
|
@@ -157,15 +184,29 @@ async function manifestSafeCopy(srcPath, destPath, manifest) {
|
|
|
157
184
|
}
|
|
158
185
|
|
|
159
186
|
const manifestHash = manifest[destPath]?.hash;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
187
|
+
|
|
188
|
+
if (manifestHash) {
|
|
189
|
+
if (destHash !== manifestHash) {
|
|
190
|
+
// User modified the file since we last installed it — preserve it
|
|
191
|
+
await backupUserFile(destPath);
|
|
192
|
+
return { action: 'skipped', hash: destHash };
|
|
193
|
+
}
|
|
194
|
+
// Byte-identical to what we last installed — untouched, ours to update
|
|
195
|
+
await fs.copyFile(srcPath, destPath);
|
|
196
|
+
return { action: 'updated', hash: srcHash };
|
|
164
197
|
}
|
|
165
198
|
|
|
166
|
-
//
|
|
199
|
+
// No manifest entry, and dest differs from what we ship. This is either a
|
|
200
|
+
// user's customized file or an older stock copy predating the manifest — the
|
|
201
|
+
// two are indistinguishable, so we must assume it could be a customization.
|
|
202
|
+
// Back it up before overwriting, and if the backup can't be secured, refuse
|
|
203
|
+
// to overwrite at all rather than destroy the only copy.
|
|
204
|
+
// (CLAUDE.md: never overwrite existing user .claude/ config.)
|
|
205
|
+
if (!(await backupUserFile(destPath))) {
|
|
206
|
+
return { action: 'skipped', hash: destHash };
|
|
207
|
+
}
|
|
167
208
|
await fs.copyFile(srcPath, destPath);
|
|
168
|
-
return { action: 'updated', hash: srcHash };
|
|
209
|
+
return { action: 'updated', hash: srcHash, backedUp: true };
|
|
169
210
|
}
|
|
170
211
|
|
|
171
212
|
// Delete only the files listed in manifest that reside under baseDir.
|
|
@@ -3593,14 +3634,17 @@ async function copyCommandFiles(targetDir, spinner) {
|
|
|
3593
3634
|
*/
|
|
3594
3635
|
function shouldIncludeHookFile(file, stat) {
|
|
3595
3636
|
if (isNativeWindows()) {
|
|
3596
|
-
// Include .ps1 scripts, .py helpers,
|
|
3637
|
+
// Include .ps1 scripts, .py helpers, hooks.json, and the generated
|
|
3638
|
+
// provider-catalog.json; exclude dotfiles and prepare-release
|
|
3597
3639
|
return stat.isFile() &&
|
|
3598
|
-
(file.endsWith('.ps1') || file.endsWith('.py') || file === 'hooks.json'
|
|
3640
|
+
(file.endsWith('.ps1') || file.endsWith('.py') || file === 'hooks.json' ||
|
|
3641
|
+
file === 'provider-catalog.json') &&
|
|
3599
3642
|
!file.includes('prepare-release') &&
|
|
3600
3643
|
!file.startsWith('.');
|
|
3601
3644
|
}
|
|
3602
3645
|
return stat.isFile() &&
|
|
3603
|
-
(file.endsWith('.sh') || file.endsWith('.py') || file === 'hooks.json'
|
|
3646
|
+
(file.endsWith('.sh') || file.endsWith('.py') || file === 'hooks.json' ||
|
|
3647
|
+
file === 'provider-catalog.json') &&
|
|
3604
3648
|
!file.includes('prepare-release') &&
|
|
3605
3649
|
!file.startsWith('.');
|
|
3606
3650
|
}
|
|
@@ -5668,6 +5712,16 @@ async function performUpdateOperations(targetDir, spinner) {
|
|
|
5668
5712
|
spinner.text = 'Checking for old configuration...';
|
|
5669
5713
|
await detectAndMigrateOldConfig(targetDir, spinner);
|
|
5670
5714
|
|
|
5715
|
+
// Upgrade safety for the new opt-in injection gate (session-start-tts.sh):
|
|
5716
|
+
// a project that was already installed and talking pre-gate has no marker, so
|
|
5717
|
+
// the gate would newly silence it. Backfill the enable marker for a PROJECT
|
|
5718
|
+
// update so existing users keep their audio. A GLOBAL (home-dir) update stays
|
|
5719
|
+
// opt-in — and we say so, otherwise the user's voice just stops with no clue.
|
|
5720
|
+
if ((await configureInjectionScope(targetDir)) === 'global') {
|
|
5721
|
+
console.log('');
|
|
5722
|
+
warnGlobalInjectionScope();
|
|
5723
|
+
}
|
|
5724
|
+
|
|
5671
5725
|
return {
|
|
5672
5726
|
commandCount,
|
|
5673
5727
|
hookCount: hookResult.count,
|
|
@@ -5721,6 +5775,61 @@ async function updateAgentVibes(targetDir, options) {
|
|
|
5721
5775
|
}
|
|
5722
5776
|
}
|
|
5723
5777
|
|
|
5778
|
+
/**
|
|
5779
|
+
* Configure the session-start injection opt-in marker for this install/update.
|
|
5780
|
+
*
|
|
5781
|
+
* - PROJECT install → drop `.claude/agentvibes-enabled` so session-start-tts.sh
|
|
5782
|
+
* injects the TTS protocol for THIS project (and only this one), preserving the
|
|
5783
|
+
* "install in a project and it just works" experience.
|
|
5784
|
+
* - GLOBAL install (target === home dir) → write nothing; injecting into every
|
|
5785
|
+
* session is exactly the "cacophony of agents" we are preventing.
|
|
5786
|
+
*
|
|
5787
|
+
* Uses `agentvibes-enabled`, NOT `agentvibes-unmuted`, on purpose: the unmuted
|
|
5788
|
+
* marker OVERRIDES a global mute in play-tts.sh, so writing it here would let an
|
|
5789
|
+
* install/update silently defeat a user's `~/.agentvibes-muted` kill-switch.
|
|
5790
|
+
* Non-destructive: never touches an existing mute/unmute/enabled choice.
|
|
5791
|
+
*
|
|
5792
|
+
* @param {string} targetDir - install target
|
|
5793
|
+
* @returns {Promise<'global'|'enabled'|'kept'|'error'>} what happened (for messaging)
|
|
5794
|
+
*/
|
|
5795
|
+
async function configureInjectionScope(targetDir) {
|
|
5796
|
+
try {
|
|
5797
|
+
// path.relative is case-insensitive on win32 and normalizes separators, so a
|
|
5798
|
+
// lowercase drive letter or trailing slash no longer misdetects a home
|
|
5799
|
+
// install; realpath (best-effort) also collapses a symlinked $HOME.
|
|
5800
|
+
let resolvedTarget = path.resolve(targetDir);
|
|
5801
|
+
let resolvedHome = path.resolve(os.homedir());
|
|
5802
|
+
try { resolvedTarget = fsSync.realpathSync.native(resolvedTarget); } catch { /* dir may not exist yet */ }
|
|
5803
|
+
try { resolvedHome = fsSync.realpathSync.native(resolvedHome); } catch { /* ignore */ }
|
|
5804
|
+
if (path.relative(resolvedHome, resolvedTarget) === '') return 'global';
|
|
5805
|
+
|
|
5806
|
+
const claudeDir = path.join(targetDir, '.claude');
|
|
5807
|
+
// Respect any prior explicit choice — enabled OR either mute/unmute marker.
|
|
5808
|
+
for (const m of ['agentvibes-enabled', 'agentvibes-unmuted', 'agentvibes-muted']) {
|
|
5809
|
+
if (await fs.access(path.join(claudeDir, m)).then(() => true).catch(() => false)) {
|
|
5810
|
+
return 'kept';
|
|
5811
|
+
}
|
|
5812
|
+
}
|
|
5813
|
+
await fs.mkdir(claudeDir, { recursive: true });
|
|
5814
|
+
await fs.writeFile(path.join(claudeDir, 'agentvibes-enabled'), '', 'utf8');
|
|
5815
|
+
return 'enabled';
|
|
5816
|
+
} catch (err) {
|
|
5817
|
+
// Never fail install/update over the marker, but don't swallow silently —
|
|
5818
|
+
// a read-only FS here would otherwise produce a mysterious "silent" project.
|
|
5819
|
+
console.log(chalk.gray(` (Could not set TTS injection marker: ${err.code || err.message}. Run /agent-vibes:unmute to enable.)`));
|
|
5820
|
+
return 'error';
|
|
5821
|
+
}
|
|
5822
|
+
}
|
|
5823
|
+
|
|
5824
|
+
// Print the "global install stays silent" guidance (shared by install + update).
|
|
5825
|
+
function warnGlobalInjectionScope() {
|
|
5826
|
+
console.log(chalk.yellow.bold(' ⚠ Global install detected (installed at your home directory)'));
|
|
5827
|
+
console.log(chalk.gray(' TTS stays OFF by default so it will NOT talk in every session at once.'));
|
|
5828
|
+
console.log(chalk.gray(' Enable it in a specific project with: ') + chalk.cyan('/agent-vibes:unmute'));
|
|
5829
|
+
console.log(chalk.gray(' (or re-run the installer inside a project folder, not your home dir).'));
|
|
5830
|
+
console.log('');
|
|
5831
|
+
}
|
|
5832
|
+
|
|
5724
5833
|
// Installation function
|
|
5725
5834
|
async function install(options = {}) {
|
|
5726
5835
|
const currentDir = process.env.INIT_CWD || process.cwd();
|
|
@@ -6243,6 +6352,11 @@ def _strip_markdown(text: str) -> str:
|
|
|
6243
6352
|
console.log(chalk.magenta(' \u2661 Sponsor this Developer github.com/sponsors/paulpreibisch'));
|
|
6244
6353
|
console.log('');
|
|
6245
6354
|
|
|
6355
|
+
// Opt-in injection marker (prevents the global-install cacophony). See
|
|
6356
|
+
// configureInjectionScope: project installs enable THIS project; a home-dir
|
|
6357
|
+
// install stays silent. Uses agentvibes-enabled so it can't override a global mute.
|
|
6358
|
+
if ((await configureInjectionScope(targetDir)) === 'global') warnGlobalInjectionScope();
|
|
6359
|
+
|
|
6246
6360
|
if (!(options.nonInteractive || process.env.AGENT_VIBES_NON_INTERACTIVE === '1')) {
|
|
6247
6361
|
// Clean final summary
|
|
6248
6362
|
console.log('');
|
|
@@ -6997,7 +7111,7 @@ export {
|
|
|
6997
7111
|
CRITICAL_HOOKS, CRITICAL_HOOKS_WINDOWS,
|
|
6998
7112
|
// Manifest utilities (used by tests and external tooling)
|
|
6999
7113
|
getProjectManifestPath, getGlobalManifestPath,
|
|
7000
|
-
loadManifest, saveManifest, computeFileHash, manifestSafeCopy, removeManifestFiles,
|
|
7114
|
+
loadManifest, saveManifest, computeFileHash, manifestSafeCopy, backupUserFile, removeManifestFiles,
|
|
7001
7115
|
// Pure helper functions exported for testing
|
|
7002
7116
|
readJsonConfigSafe, backupConfigFile,
|
|
7003
7117
|
isPiperProvider, supportsEmoji, getPersonalityIcon,
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LanguageService — single source of truth for the selected UI language.
|
|
3
|
-
*
|
|
4
|
-
* Persists the selection to ~/.claude/config/language.txt so it survives
|
|
5
|
-
* process restarts. Notifies registered listeners on every change so the
|
|
6
|
-
* TUI can re-render dynamic labels immediately.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import fs from 'node:fs';
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
import os from 'node:os';
|
|
12
|
-
import { t, SUPPORTED_LANGUAGES } from '../i18n/strings.js';
|
|
13
|
-
|
|
14
|
-
const LANG_FILE = path.join(os.homedir(), '.claude', 'config', 'language.txt');
|
|
15
|
-
const VALID_LANGS = new Set(SUPPORTED_LANGUAGES.map(l => l.value));
|
|
16
|
-
|
|
17
|
-
export class LanguageService {
|
|
18
|
-
constructor() {
|
|
19
|
-
this._lang = this._load();
|
|
20
|
-
this._listeners = [];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
_load() {
|
|
24
|
-
try {
|
|
25
|
-
const val = fs.readFileSync(LANG_FILE, 'utf8').trim();
|
|
26
|
-
return VALID_LANGS.has(val) ? val : 'en';
|
|
27
|
-
} catch {
|
|
28
|
-
return 'en';
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
getLang() { return this._lang; }
|
|
33
|
-
|
|
34
|
-
setLang(lang) {
|
|
35
|
-
if (!VALID_LANGS.has(lang)) return;
|
|
36
|
-
this._lang = lang;
|
|
37
|
-
try {
|
|
38
|
-
fs.mkdirSync(path.dirname(LANG_FILE), { recursive: true });
|
|
39
|
-
fs.writeFileSync(LANG_FILE, lang, { mode: 0o600 });
|
|
40
|
-
} catch { /* non-fatal */ }
|
|
41
|
-
this._listeners.forEach(fn => fn(lang));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
onChange(fn) { this._listeners.push(fn); }
|
|
45
|
-
|
|
46
|
-
t(key) { return t(this._lang, key); }
|
|
47
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* LanguageService — single source of truth for the selected UI language.
|
|
3
|
+
*
|
|
4
|
+
* Persists the selection to ~/.claude/config/language.txt so it survives
|
|
5
|
+
* process restarts. Notifies registered listeners on every change so the
|
|
6
|
+
* TUI can re-render dynamic labels immediately.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import { t, SUPPORTED_LANGUAGES } from '../i18n/strings.js';
|
|
13
|
+
|
|
14
|
+
const LANG_FILE = path.join(os.homedir(), '.claude', 'config', 'language.txt');
|
|
15
|
+
const VALID_LANGS = new Set(SUPPORTED_LANGUAGES.map(l => l.value));
|
|
16
|
+
|
|
17
|
+
export class LanguageService {
|
|
18
|
+
constructor() {
|
|
19
|
+
this._lang = this._load();
|
|
20
|
+
this._listeners = [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_load() {
|
|
24
|
+
try {
|
|
25
|
+
const val = fs.readFileSync(LANG_FILE, 'utf8').trim();
|
|
26
|
+
return VALID_LANGS.has(val) ? val : 'en';
|
|
27
|
+
} catch {
|
|
28
|
+
return 'en';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getLang() { return this._lang; }
|
|
33
|
+
|
|
34
|
+
setLang(lang) {
|
|
35
|
+
if (!VALID_LANGS.has(lang)) return;
|
|
36
|
+
this._lang = lang;
|
|
37
|
+
try {
|
|
38
|
+
fs.mkdirSync(path.dirname(LANG_FILE), { recursive: true });
|
|
39
|
+
fs.writeFileSync(LANG_FILE, lang, { mode: 0o600 });
|
|
40
|
+
} catch { /* non-fatal */ }
|
|
41
|
+
this._listeners.forEach(fn => fn(lang));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
onChange(fn) { this._listeners.push(fn); }
|
|
45
|
+
|
|
46
|
+
t(key) { return t(this._lang, key); }
|
|
47
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
/** Ordered list of all tab IDs — used for cycling and validation */
|
|
10
|
-
export const TAB_ORDER = ['setup', 'settings', '
|
|
10
|
+
export const TAB_ORDER = ['setup', 'settings', 'music', 'agents', 'receiver', 'readme', 'help'];
|
|
11
11
|
|
|
12
12
|
export class NavigationService {
|
|
13
13
|
/**
|