create-byan-agent 2.7.9 → 2.8.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.
@@ -1695,230 +1695,181 @@ program
1695
1695
  .version(BYAN_VERSION)
1696
1696
  .action(install);
1697
1697
 
1698
- // Update Command
1698
+ // Update Command (Yanstaller v3)
1699
1699
  program
1700
1700
  .command('update')
1701
- .description('Mettre a jour BYAN vers la derniere version npm')
1702
- .option('--dry-run', 'Analyser sans appliquer les changements')
1703
- .option('--force', 'Forcer la mise a jour meme si deja a jour')
1701
+ .description('Update BYAN to the latest npm version')
1702
+ .option('--preview', 'Show what would change without applying')
1703
+ .option('--force', 'Force update even if already up to date')
1704
1704
  .action(async (options) => {
1705
- const installPath = process.cwd();
1706
-
1707
- // Import update modules
1708
- const Analyzer = require('../../update-byan-agent/lib/analyzer');
1709
- const Backup = require('../../update-byan-agent/lib/backup');
1710
- const CustomizationDetector = require('../../update-byan-agent/lib/customization-detector');
1711
-
1705
+ const yanstaller = require('../lib/yanstaller');
1706
+ const projectRoot = process.cwd();
1707
+
1712
1708
  try {
1713
- // Step 1: Check version
1714
- const spinner = ora('Verification version...').start();
1715
- const analyzer = new Analyzer(installPath);
1716
- const versionInfo = await analyzer.checkVersion();
1717
- spinner.succeed(`Version actuelle: ${versionInfo.current}, npm: ${versionInfo.latest}`);
1718
-
1719
- if (versionInfo.upToDate && !options.force) {
1720
- console.log(chalk.green('\nBYAN est deja a jour!'));
1721
- console.log(chalk.gray(` Version actuelle: ${versionInfo.current}`));
1722
- return;
1723
- }
1724
-
1725
- if (options.dryRun) {
1726
- console.log(chalk.cyan('\nMode dry-run: Aucune modification appliquee'));
1727
- console.log(chalk.gray(` Mise a jour disponible: ${versionInfo.current} -> ${versionInfo.latest}`));
1728
- return;
1729
- }
1730
-
1731
- // Step 2: Confirm update
1732
- const { confirmUpdate } = await inquirer.prompt([{
1733
- type: 'confirm',
1734
- name: 'confirmUpdate',
1735
- message: `Mettre a jour BYAN ${versionInfo.current} -> ${versionInfo.latest}?`,
1736
- default: true
1737
- }]);
1738
-
1739
- if (!confirmUpdate) {
1740
- console.log(chalk.yellow('Mise a jour annulee'));
1741
- return;
1742
- }
1743
-
1744
- // Step 3: Detect customizations
1745
- const detectorSpinner = ora('Detection des personnalisations...').start();
1746
- const detector = new CustomizationDetector(installPath);
1747
- const customizations = await detector.detectCustomizations();
1748
- detectorSpinner.succeed(`${customizations.length} fichiers a preserver detectes`);
1749
-
1750
- // Step 4: Create backup
1751
- const backupSpinner = ora('Creation backup...').start();
1752
- const backup = new Backup(installPath);
1753
- const backupPath = await backup.create();
1754
- backupSpinner.succeed(`Backup cree: ${path.basename(backupPath)}`);
1755
-
1756
- // Step 5: Preserve customizations
1757
- const preserveSpinner = ora('Sauvegarde des personnalisations...').start();
1758
- const tempDir = path.join(installPath, '.byan-update-temp');
1759
- if (fs.existsSync(tempDir)) {
1760
- await fs.remove(tempDir);
1761
- }
1762
- await fs.ensureDir(tempDir);
1763
-
1764
- for (const custom of customizations) {
1765
- if (await fs.pathExists(custom.path)) {
1766
- const relativePath = path.relative(installPath, custom.path);
1767
- const tempPath = path.join(tempDir, relativePath);
1768
- const tempParent = path.dirname(tempPath);
1769
-
1770
- await fs.ensureDir(tempParent);
1771
- await fs.copy(custom.path, tempPath);
1709
+ if (options.preview) {
1710
+ const spinner = ora('Analyzing update...').start();
1711
+ const { diff, userModified, installed, latest } = await yanstaller.updater.preview(projectRoot);
1712
+ spinner.succeed('Analysis complete');
1713
+
1714
+ console.log('');
1715
+ console.log(chalk.bold(`Version: ${installed} -> ${latest}`));
1716
+ console.log('');
1717
+
1718
+ if (diff.toUpdate.length) {
1719
+ console.log(chalk.yellow(` Updated files (${diff.toUpdate.length}):`));
1720
+ diff.toUpdate.forEach(f => {
1721
+ const tag = userModified.includes(f) ? chalk.red(' [user-modified, will skip]') : '';
1722
+ console.log(chalk.gray(` ~ ${f}${tag}`));
1723
+ });
1772
1724
  }
1773
- }
1774
- preserveSpinner.succeed('Personnalisations sauvegardees');
1775
-
1776
- // Step 6: Download and install latest version
1777
- const updateSpinner = ora('Telechargement derniere version...').start();
1778
- try {
1779
- // Remove current _byan directory
1780
- const byanDir = path.join(installPath, '_byan');
1781
- if (await fs.pathExists(byanDir)) {
1782
- await fs.remove(byanDir);
1725
+ if (diff.toAdd.length) {
1726
+ console.log(chalk.green(` New files (${diff.toAdd.length}):`));
1727
+ diff.toAdd.forEach(f => console.log(chalk.gray(` + ${f}`)));
1783
1728
  }
1784
-
1785
- // Run npm install to get latest create-byan-agent
1786
- execSync('npm install --no-save create-byan-agent@latest', {
1787
- cwd: installPath,
1788
- stdio: 'pipe'
1789
- });
1790
-
1791
- // Copy _byan from node_modules to project root
1792
- const nodeModulesByan = path.join(installPath, 'node_modules', 'create-byan-agent', '_byan');
1793
- if (await fs.pathExists(nodeModulesByan)) {
1794
- await fs.copy(nodeModulesByan, byanDir);
1795
- } else {
1796
- throw new Error('_byan directory not found in npm package');
1729
+ if (diff.toKeep.length) {
1730
+ console.log(chalk.blue(` User files kept (${diff.toKeep.length}):`));
1731
+ diff.toKeep.forEach(f => console.log(chalk.gray(` = ${f}`)));
1797
1732
  }
1798
-
1799
- updateSpinner.succeed('Derniere version installee');
1800
- } catch (error) {
1801
- updateSpinner.fail('Erreur installation');
1802
-
1803
- // Rollback
1804
- const rollbackSpinner = ora('Restauration backup...').start();
1805
- await backup.restore(backupPath);
1806
- rollbackSpinner.succeed('Backup restaure');
1807
-
1808
- throw error;
1809
- }
1810
-
1811
- // Step 7: Restore customizations
1812
- const restoreSpinner = ora('Restauration personnalisations...').start();
1813
- for (const custom of customizations) {
1814
- const relativePath = path.relative(installPath, custom.path);
1815
- const tempPath = path.join(tempDir, relativePath);
1816
-
1817
- if (await fs.pathExists(tempPath)) {
1818
- const targetParent = path.dirname(custom.path);
1819
- await fs.ensureDir(targetParent);
1820
- await fs.copy(tempPath, custom.path);
1733
+ if (diff.toSkip.length) {
1734
+ console.log(chalk.gray(` Unchanged: ${diff.toSkip.length} files`));
1821
1735
  }
1736
+ console.log('');
1737
+ return;
1822
1738
  }
1823
- restoreSpinner.succeed('Personnalisations restaurees');
1824
-
1825
- // Cleanup temp directory
1826
- if (await fs.pathExists(tempDir)) {
1827
- await fs.remove(tempDir);
1828
- }
1829
-
1830
- // Update version in config.yaml
1831
- const configPath = path.join(installPath, '_byan', 'bmb', 'config.yaml');
1832
- if (await fs.pathExists(configPath)) {
1833
- const configContent = await fs.readFile(configPath, 'utf8');
1834
- const config = yaml.load(configContent);
1835
- config.byan_version = versionInfo.latest;
1836
- await fs.writeFile(configPath, yaml.dump(config), 'utf8');
1739
+
1740
+ const checkSpinner = ora('Checking for updates...').start();
1741
+ const check = await yanstaller.updater.checkForUpdate(projectRoot);
1742
+
1743
+ if (!check.updateAvailable && !options.force) {
1744
+ checkSpinner.succeed(`Already up to date (${check.installed})`);
1745
+ return;
1837
1746
  }
1838
-
1839
- console.log('');
1840
- console.log(chalk.green.bold('Mise a jour terminee avec succes!'));
1841
- console.log(chalk.gray(` ${versionInfo.current} -> ${versionInfo.latest}`));
1747
+ checkSpinner.succeed(`Update available: ${check.installed} -> ${check.latest}`);
1748
+
1749
+ const updateSpinner = ora('Updating BYAN...').start();
1750
+ const result = await yanstaller.update(projectRoot, { force: options.force });
1751
+ updateSpinner.succeed('Update complete');
1752
+
1842
1753
  console.log('');
1843
- console.log(chalk.cyan('Fichiers preserves:'));
1844
- customizations.forEach(c => {
1845
- console.log(chalk.gray(` ${path.relative(installPath, c.path)}`));
1846
- });
1754
+ console.log(chalk.green.bold(`Updated: ${result.previousVersion} -> ${result.newVersion}`));
1755
+ console.log(chalk.gray(` Files updated: ${result.filesUpdated}`));
1756
+ console.log(chalk.gray(` Files added: ${result.filesAdded}`));
1757
+ if (result.filesSkipped > 0) {
1758
+ console.log(chalk.yellow(` Files skipped: ${result.filesSkipped} (user-modified)`));
1759
+ }
1760
+ if (result.backupPath) {
1761
+ console.log(chalk.gray(` Backup: ${path.basename(result.backupPath)}`));
1762
+ }
1847
1763
  console.log('');
1848
-
1849
1764
  } catch (error) {
1850
1765
  console.error('');
1851
- console.error(chalk.red.bold('Erreur lors de la mise a jour:'));
1766
+ console.error(chalk.red.bold('Update failed:'));
1852
1767
  console.error(chalk.red(` ${error.message}`));
1853
1768
  console.error('');
1854
- console.error(chalk.yellow('Le backup est disponible dans _byan.backup/'));
1855
- console.error(chalk.gray('Restaurer avec: npx create-byan-agent restore'));
1769
+ console.error(chalk.yellow('Restore with: npx create-byan-agent rollback'));
1856
1770
  process.exit(1);
1857
1771
  }
1858
1772
  });
1859
1773
 
1860
- // Restore Command
1774
+ // Rollback Command (Yanstaller v3)
1861
1775
  program
1862
- .command('restore')
1863
- .description('Restaurer BYAN depuis backup')
1864
- .option('-p, --path <path>', 'Chemin du backup (dernier par defaut)')
1865
- .action(async (options) => {
1866
- const Backup = require('../../update-byan-agent/lib/backup');
1867
- const spinner = ora('Restauration backup...').start();
1868
-
1776
+ .command('rollback')
1777
+ .description('Restore BYAN from the most recent backup')
1778
+ .action(async () => {
1779
+ const yanstaller = require('../lib/yanstaller');
1780
+ const projectRoot = process.cwd();
1781
+ const spinner = ora('Restoring from backup...').start();
1782
+
1869
1783
  try {
1870
- const installPath = process.cwd();
1871
- const backup = new Backup(installPath);
1872
-
1873
- await backup.restore(options.path);
1874
-
1875
- spinner.succeed('Backup restaure avec succes');
1876
- console.log(chalk.green('\nBYAN restaure depuis backup'));
1877
-
1784
+ await yanstaller.rollback(projectRoot);
1785
+ spinner.succeed('Rollback complete');
1786
+ console.log(chalk.green('\nBYAN restored from latest backup'));
1878
1787
  } catch (error) {
1879
- spinner.fail('Erreur restauration backup');
1788
+ spinner.fail('Rollback failed');
1880
1789
  console.error(chalk.red(` ${error.message}`));
1881
1790
  process.exit(1);
1882
1791
  }
1883
1792
  });
1884
1793
 
1885
- // Check Version Command
1794
+ // Backups Command (Yanstaller v3)
1795
+ program
1796
+ .command('backups')
1797
+ .description('List available BYAN backups')
1798
+ .action(async () => {
1799
+ const yanstaller = require('../lib/yanstaller');
1800
+ const projectRoot = process.cwd();
1801
+
1802
+ try {
1803
+ const backups = await yanstaller.listBackups(projectRoot);
1804
+
1805
+ if (backups.length === 0) {
1806
+ console.log(chalk.yellow('No backups found.'));
1807
+ return;
1808
+ }
1809
+
1810
+ console.log(chalk.bold(`\nAvailable backups (${backups.length}):\n`));
1811
+ for (const backupPath of backups) {
1812
+ const name = path.basename(backupPath);
1813
+ const timestamp = parseInt(name.replace('_byan.backup-', ''), 10);
1814
+ const date = new Date(timestamp).toLocaleString();
1815
+ const size = await yanstaller.backuper.getBackupSize(backupPath);
1816
+ const sizeKB = (size / 1024).toFixed(1);
1817
+ console.log(chalk.gray(` ${name} ${date} (${sizeKB} KB)`));
1818
+ }
1819
+ console.log('');
1820
+ } catch (error) {
1821
+ console.error(chalk.red(` ${error.message}`));
1822
+ process.exit(1);
1823
+ }
1824
+ });
1825
+
1826
+ // Check Version Command (Yanstaller v3)
1886
1827
  program
1887
1828
  .command('check')
1888
- .description('Verifier version actuelle vs derniere version npm')
1829
+ .description('Check installed version vs latest npm version')
1889
1830
  .action(async () => {
1890
- const Analyzer = require('../../update-byan-agent/lib/analyzer');
1891
- const spinner = ora('Verification version BYAN...').start();
1892
-
1831
+ const yanstaller = require('../lib/yanstaller');
1832
+ const projectRoot = process.cwd();
1833
+ const spinner = ora('Checking BYAN version...').start();
1834
+
1893
1835
  try {
1894
- const installPath = process.cwd();
1895
- const analyzer = new Analyzer(installPath);
1896
-
1897
- const versionInfo = await analyzer.checkVersion();
1898
-
1899
- spinner.succeed('Verification terminee');
1900
-
1836
+ const check = await yanstaller.updater.checkForUpdate(projectRoot);
1837
+ spinner.succeed('Check complete');
1838
+
1901
1839
  console.log('');
1902
- console.log(chalk.bold('Informations de version:'));
1903
- console.log(chalk.gray(' Version actuelle: ') + chalk.cyan(versionInfo.current));
1904
- console.log(chalk.gray(' Version npm: ') + chalk.cyan(versionInfo.latest));
1840
+ console.log(chalk.bold('Version info:'));
1841
+ console.log(chalk.gray(' Installed: ') + chalk.cyan(check.installed));
1842
+ console.log(chalk.gray(' Latest: ') + chalk.cyan(check.latest));
1905
1843
  console.log('');
1906
-
1907
- if (versionInfo.upToDate) {
1908
- console.log(chalk.green(' BYAN est a jour!'));
1909
- } else if (versionInfo.needsUpdate) {
1910
- console.log(chalk.yellow(' → Une mise a jour est disponible'));
1911
- console.log(chalk.gray(' Executer: npx create-byan-agent update'));
1912
- } else if (versionInfo.ahead) {
1913
- console.log(chalk.blue(' → Version dev en avance sur npm'));
1844
+
1845
+ if (check.updateAvailable) {
1846
+ console.log(chalk.yellow(` Update available (${check.changes.length} files changed)`));
1847
+ console.log(chalk.gray(' Run: npx create-byan-agent update'));
1848
+ } else {
1849
+ console.log(chalk.green(' Up to date'));
1914
1850
  }
1915
1851
  console.log('');
1916
-
1917
1852
  } catch (error) {
1918
- spinner.fail('Erreur verification version');
1853
+ spinner.fail('Version check failed');
1919
1854
  console.error(chalk.red(` ${error.message}`));
1920
1855
  process.exit(1);
1921
1856
  }
1922
1857
  });
1923
1858
 
1859
+ program
1860
+ .command('web')
1861
+ .description('Launch BYAN WebUI installer in the browser')
1862
+ .option('-p, --port <port>', 'Port number', '3000')
1863
+ .action(async (options) => {
1864
+ const ByanWebUI = require('../src/webui/server');
1865
+ const port = parseInt(options.port, 10);
1866
+ const projectRoot = process.cwd();
1867
+
1868
+ console.log(chalk.cyan.bold('\n BYAN WebUI\n'));
1869
+ const server = new ByanWebUI({ port, projectRoot });
1870
+ server.start();
1871
+ console.log(chalk.green(` Server running at http://localhost:${port}`));
1872
+ console.log(chalk.gray(' Press Ctrl+C to stop\n'));
1873
+ });
1874
+
1924
1875
  program.parse(process.argv);
@@ -0,0 +1,277 @@
1
+ /**
2
+ * STT Engine Abstraction Layer
3
+ *
4
+ * Auto-detects the best available speech-to-text engine.
5
+ * Priority: Parakeet TDT > Whisper > none.
6
+ *
7
+ * @module stt/engine
8
+ */
9
+
10
+ const { execSync } = require('child_process');
11
+ const chalk = require('chalk');
12
+
13
+ const PARAKEET_MIN_VRAM = 4000; // 4 GB
14
+ const WHISPER_MIN_VRAM = 1000; // 1 GB for GPU mode
15
+
16
+ /**
17
+ * Detect GPU presence and capabilities via nvidia-smi.
18
+ * Shared across all STT backends to avoid duplication.
19
+ *
20
+ * @returns {{ hasGPU: boolean, vram: number, gpuName: string }}
21
+ */
22
+ function detectGPU() {
23
+ try {
24
+ const raw = execSync(
25
+ 'nvidia-smi --query-gpu=name,memory.total --format=csv,noheader',
26
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }
27
+ ).trim();
28
+
29
+ if (!raw || raw === 'NO_GPU') {
30
+ return { hasGPU: false, vram: 0, gpuName: '' };
31
+ }
32
+
33
+ const [gpuName, vramStr] = raw.split(',').map(s => s.trim());
34
+ const vram = parseInt(vramStr, 10) || 0;
35
+
36
+ return { hasGPU: true, vram, gpuName };
37
+ } catch {
38
+ return { hasGPU: false, vram: 0, gpuName: '' };
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Check whether a shell command is available on PATH.
44
+ *
45
+ * @param {string} cmd
46
+ * @returns {boolean}
47
+ */
48
+ function commandExists(cmd) {
49
+ try {
50
+ execSync(`which ${cmd}`, { stdio: 'pipe' });
51
+ return true;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Check Python availability and optional package presence.
59
+ *
60
+ * @returns {{ available: boolean, version: string, hasNemo: boolean }}
61
+ */
62
+ function checkPython() {
63
+ const available = commandExists('python3');
64
+ if (!available) {
65
+ return { available: false, version: '', hasNemo: false };
66
+ }
67
+
68
+ let version = '';
69
+ try {
70
+ version = execSync('python3 --version', {
71
+ encoding: 'utf-8',
72
+ stdio: ['pipe', 'pipe', 'ignore']
73
+ }).trim().replace('Python ', '');
74
+ } catch { /* ignore */ }
75
+
76
+ let hasNemo = false;
77
+ try {
78
+ execSync('python3 -c "import nemo.collections.asr"', {
79
+ stdio: ['pipe', 'pipe', 'ignore']
80
+ });
81
+ hasNemo = true;
82
+ } catch { /* ignore */ }
83
+
84
+ return { available, version, hasNemo };
85
+ }
86
+
87
+ /**
88
+ * Detect the best available STT engine based on hardware and software.
89
+ *
90
+ * Decision logic:
91
+ * 1. GPU with >= 4 GB VRAM + (Python+NeMo or Docker) -> parakeet
92
+ * 2. GPU with >= 1 GB VRAM or Docker available -> whisper
93
+ * 3. Otherwise -> none
94
+ *
95
+ * @returns {{ engine: 'parakeet'|'whisper'|'none', gpu: object, reason: string }}
96
+ */
97
+ function detect() {
98
+ const gpu = detectGPU();
99
+ const python = checkPython();
100
+ const hasDocker = commandExists('docker');
101
+
102
+ // Parakeet needs a capable GPU
103
+ if (gpu.hasGPU && gpu.vram >= PARAKEET_MIN_VRAM) {
104
+ const canRunLocal = python.available && python.hasNemo;
105
+ if (canRunLocal || hasDocker) {
106
+ return {
107
+ engine: 'parakeet',
108
+ gpu,
109
+ reason: `GPU ${gpu.gpuName} with ${gpu.vram} MB VRAM meets Parakeet requirements`
110
+ + (canRunLocal ? ' (local NeMo available)' : ' (Docker available)')
111
+ };
112
+ }
113
+ }
114
+
115
+ // Whisper is less demanding
116
+ if (gpu.hasGPU && gpu.vram >= WHISPER_MIN_VRAM) {
117
+ return {
118
+ engine: 'whisper',
119
+ gpu,
120
+ reason: `GPU ${gpu.gpuName} with ${gpu.vram} MB VRAM — Whisper GPU mode`
121
+ };
122
+ }
123
+
124
+ if (hasDocker) {
125
+ return {
126
+ engine: 'whisper',
127
+ gpu,
128
+ reason: 'No sufficient GPU — Whisper CPU mode via Docker'
129
+ };
130
+ }
131
+
132
+ if (python.available) {
133
+ return {
134
+ engine: 'whisper',
135
+ gpu,
136
+ reason: 'No GPU/Docker — Whisper CPU mode via local Python'
137
+ };
138
+ }
139
+
140
+ return {
141
+ engine: 'none',
142
+ gpu,
143
+ reason: 'No GPU, Docker, or Python found — cannot run STT'
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Return configuration defaults for a given engine.
149
+ *
150
+ * @param {'parakeet'|'whisper'} engine
151
+ * @returns {object}
152
+ */
153
+ function getConfig(engine) {
154
+ if (engine === 'parakeet') {
155
+ return {
156
+ engine: 'parakeet',
157
+ port: 8001,
158
+ model: 'nvidia/parakeet-tdt-0.6b-v2',
159
+ languages: ['fr', 'en'],
160
+ dockerImage: 'nvcr.io/nvidia/nemo:24.07',
161
+ minVram: PARAKEET_MIN_VRAM,
162
+ healthEndpoint: 'http://localhost:8001/health'
163
+ };
164
+ }
165
+
166
+ // Default: whisper
167
+ const gpu = detectGPU();
168
+ return {
169
+ engine: 'whisper',
170
+ port: 8000,
171
+ model: getWhisperModel(gpu.vram),
172
+ dockerImage: `fedirz/faster-whisper-server:latest-${gpu.hasGPU ? 'cuda' : 'cpu'}`,
173
+ minVram: WHISPER_MIN_VRAM,
174
+ healthEndpoint: 'http://localhost:8000/health'
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Map available VRAM to the best Whisper model.
180
+ *
181
+ * @param {number} vram - VRAM in MB
182
+ * @returns {string}
183
+ */
184
+ function getWhisperModel(vram) {
185
+ if (vram >= 10000) return 'large-v3';
186
+ if (vram >= 6000) return 'large-v2';
187
+ if (vram >= 4000) return 'medium';
188
+ if (vram >= 2000) return 'small';
189
+ if (vram >= 1000) return 'tiny';
190
+ return 'base';
191
+ }
192
+
193
+ /**
194
+ * Check runtime status of either STT engine.
195
+ *
196
+ * @returns {{ engine: string, mode: 'local'|'docker'|'unknown', running: boolean, model: string }}
197
+ */
198
+ function getStatus() {
199
+ const parakeetRunning = isPortOpen(8001);
200
+ const whisperRunning = isPortOpen(8000);
201
+
202
+ if (parakeetRunning) {
203
+ return { engine: 'parakeet', mode: detectMode(8001), running: true, model: 'parakeet-tdt-0.6b-v2' };
204
+ }
205
+ if (whisperRunning) {
206
+ return { engine: 'whisper', mode: detectMode(8000), running: true, model: 'whisper' };
207
+ }
208
+ return { engine: 'none', mode: 'unknown', running: false, model: '' };
209
+ }
210
+
211
+ /**
212
+ * @param {number} port
213
+ * @returns {boolean}
214
+ */
215
+ function isPortOpen(port) {
216
+ try {
217
+ execSync(`curl -sf http://localhost:${port}/health`, {
218
+ stdio: ['pipe', 'pipe', 'ignore'],
219
+ timeout: 3000
220
+ });
221
+ return true;
222
+ } catch {
223
+ return false;
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Heuristic: if a Docker container is listening on the port, it's Docker mode.
229
+ *
230
+ * @param {number} port
231
+ * @returns {'local'|'docker'|'unknown'}
232
+ */
233
+ function detectMode(port) {
234
+ try {
235
+ const out = execSync(`docker ps --format '{{.Ports}}'`, {
236
+ encoding: 'utf-8',
237
+ stdio: ['pipe', 'pipe', 'ignore']
238
+ });
239
+ if (out.includes(`:${port}->`)) return 'docker';
240
+ return 'local';
241
+ } catch {
242
+ return 'unknown';
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Print a summary of the detection result.
248
+ *
249
+ * @param {{ engine: string, gpu: object, reason: string }} result
250
+ */
251
+ function printDetectionSummary(result) {
252
+ const { engine, gpu, reason } = result;
253
+
254
+ if (gpu.hasGPU) {
255
+ console.log(chalk.green(` GPU: ${gpu.gpuName} (${gpu.vram} MB VRAM)`));
256
+ } else {
257
+ console.log(chalk.yellow(' GPU: not detected'));
258
+ }
259
+
260
+ const color = engine === 'parakeet' ? chalk.cyan
261
+ : engine === 'whisper' ? chalk.blue
262
+ : chalk.red;
263
+
264
+ console.log(color(` Engine: ${engine}`));
265
+ console.log(chalk.gray(` Reason: ${reason}`));
266
+ }
267
+
268
+ module.exports = {
269
+ detect,
270
+ detectGPU,
271
+ commandExists,
272
+ checkPython,
273
+ getConfig,
274
+ getStatus,
275
+ getWhisperModel,
276
+ printDetectionSummary
277
+ };