create-byan-agent 2.7.8 → 2.8.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/bin/create-byan-agent-v2.js +119 -184
- package/lib/stt/engine.js +277 -0
- package/lib/stt/parakeet-backend.js +262 -0
- package/lib/stt/whisper-backend.js +171 -0
- package/lib/utils/file-differ.js +110 -0
- package/lib/utils/manifest.js +118 -0
- package/lib/utils/version-compare.js +69 -0
- package/lib/yanstaller/backuper.js +101 -45
- package/lib/yanstaller/index.js +41 -4
- package/lib/yanstaller/updater.js +271 -0
- package/package.json +5 -2
- package/setup-parakeet.js +260 -0
- package/src/webui/api.js +293 -0
- package/src/webui/public/app.js +455 -0
- package/src/webui/public/index.html +192 -0
- package/src/webui/public/style.css +732 -0
- package/src/webui/server.js +215 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parakeet TDT Setup Script
|
|
5
|
+
*
|
|
6
|
+
* Installs NVIDIA Parakeet TDT as the preferred STT engine.
|
|
7
|
+
* Falls back to Whisper when hardware requirements are not met.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node setup-parakeet.js [mode]
|
|
11
|
+
* mode: local | docker | auto (default: auto)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs-extra');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { execSync } = require('child_process');
|
|
17
|
+
const chalk = require('chalk');
|
|
18
|
+
const ora = require('ora');
|
|
19
|
+
|
|
20
|
+
const { detect, detectGPU, commandExists, checkPython, printDetectionSummary } = require('./lib/stt/engine');
|
|
21
|
+
const parakeet = require('./lib/stt/parakeet-backend');
|
|
22
|
+
const whisper = require('./lib/stt/whisper-backend');
|
|
23
|
+
|
|
24
|
+
class ParakeetInstaller {
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} projectRoot
|
|
27
|
+
* @param {'local'|'docker'|'auto'} mode
|
|
28
|
+
*/
|
|
29
|
+
constructor(projectRoot, mode = 'auto') {
|
|
30
|
+
this.projectRoot = projectRoot;
|
|
31
|
+
this.mode = mode;
|
|
32
|
+
this.scriptsDir = path.join(projectRoot, 'scripts');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Run the full setup flow.
|
|
37
|
+
*
|
|
38
|
+
* @returns {Promise<{ success: boolean, engine: string, mode: string, fallback: boolean }>}
|
|
39
|
+
*/
|
|
40
|
+
async setup() {
|
|
41
|
+
console.log(chalk.blue('\n Parakeet TDT — STT Engine Setup'));
|
|
42
|
+
console.log(chalk.gray(' ================================\n'));
|
|
43
|
+
|
|
44
|
+
const detection = detect();
|
|
45
|
+
console.log(chalk.bold(' Detection:'));
|
|
46
|
+
printDetectionSummary(detection);
|
|
47
|
+
console.log('');
|
|
48
|
+
|
|
49
|
+
const resolvedMode = this.resolveMode(detection);
|
|
50
|
+
|
|
51
|
+
// Attempt Parakeet first
|
|
52
|
+
if (detection.engine === 'parakeet') {
|
|
53
|
+
try {
|
|
54
|
+
const result = await parakeet.setup({
|
|
55
|
+
mode: resolvedMode,
|
|
56
|
+
projectRoot: this.projectRoot
|
|
57
|
+
});
|
|
58
|
+
await this.createLaunchScript('parakeet', resolvedMode);
|
|
59
|
+
this.printSuccess('parakeet', result);
|
|
60
|
+
return { success: true, engine: 'parakeet', mode: resolvedMode, fallback: false };
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.log(chalk.yellow(`\n Parakeet setup failed: ${err.message}`));
|
|
63
|
+
console.log(chalk.yellow(' Falling back to Whisper...\n'));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Fallback to Whisper
|
|
68
|
+
if (detection.engine !== 'none') {
|
|
69
|
+
try {
|
|
70
|
+
const result = await whisper.setup({
|
|
71
|
+
mode: resolvedMode,
|
|
72
|
+
projectRoot: this.projectRoot
|
|
73
|
+
});
|
|
74
|
+
await this.createLaunchScript('whisper', resolvedMode);
|
|
75
|
+
this.printSuccess('whisper', result);
|
|
76
|
+
return { success: true, engine: 'whisper', mode: resolvedMode, fallback: detection.engine === 'parakeet' };
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error(chalk.red(`\n Whisper setup also failed: ${err.message}`));
|
|
79
|
+
return { success: false, engine: 'none', mode: resolvedMode, fallback: true };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(chalk.red('\n No STT engine could be installed.'));
|
|
84
|
+
console.log(chalk.gray(' Install Docker or Python 3 and try again.'));
|
|
85
|
+
return { success: false, engine: 'none', mode: 'none', fallback: false };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Resolve auto mode into a concrete mode based on what's available.
|
|
90
|
+
*
|
|
91
|
+
* @param {{ engine: string, gpu: object }} detection
|
|
92
|
+
* @returns {'local'|'docker'}
|
|
93
|
+
*/
|
|
94
|
+
resolveMode(detection) {
|
|
95
|
+
if (this.mode !== 'auto') return this.mode;
|
|
96
|
+
|
|
97
|
+
// Prefer Docker when available — cleaner isolation
|
|
98
|
+
if (commandExists('docker')) return 'docker';
|
|
99
|
+
|
|
100
|
+
const python = checkPython();
|
|
101
|
+
if (python.available) return 'local';
|
|
102
|
+
|
|
103
|
+
return 'docker';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create a convenience launch script for the installed engine.
|
|
108
|
+
*
|
|
109
|
+
* @param {'parakeet'|'whisper'} engine
|
|
110
|
+
* @param {'local'|'docker'} mode
|
|
111
|
+
*/
|
|
112
|
+
async createLaunchScript(engine, mode) {
|
|
113
|
+
await fs.ensureDir(this.scriptsDir);
|
|
114
|
+
|
|
115
|
+
const port = engine === 'parakeet' ? parakeet.PARAKEET_PORT : whisper.WHISPER_PORT;
|
|
116
|
+
const composeFile = engine === 'parakeet'
|
|
117
|
+
? 'docker-compose.parakeet.yml'
|
|
118
|
+
: 'docker-compose.turbo-whisper.yml';
|
|
119
|
+
|
|
120
|
+
const script = `#!/bin/bash
|
|
121
|
+
# Launch ${engine} STT server
|
|
122
|
+
# Generated by setup-parakeet.js
|
|
123
|
+
|
|
124
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
125
|
+
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
126
|
+
PORT=${port}
|
|
127
|
+
|
|
128
|
+
echo "Checking ${engine} STT server on port $PORT..."
|
|
129
|
+
|
|
130
|
+
if curl -sf http://localhost:$PORT/health > /dev/null 2>&1; then
|
|
131
|
+
echo "Server already running on port $PORT"
|
|
132
|
+
exit 0
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
${mode === 'docker' ? `
|
|
136
|
+
COMPOSE_FILE="$PROJECT_DIR/${composeFile}"
|
|
137
|
+
if [ ! -f "$COMPOSE_FILE" ]; then
|
|
138
|
+
echo "Error: $COMPOSE_FILE not found"
|
|
139
|
+
exit 1
|
|
140
|
+
fi
|
|
141
|
+
echo "Starting ${engine} via Docker..."
|
|
142
|
+
docker compose -f "$COMPOSE_FILE" up -d
|
|
143
|
+
|
|
144
|
+
echo "Waiting for server (up to 60s)..."
|
|
145
|
+
for i in $(seq 1 12); do
|
|
146
|
+
sleep 5
|
|
147
|
+
if curl -sf http://localhost:$PORT/health > /dev/null 2>&1; then
|
|
148
|
+
echo "Server ready on port $PORT"
|
|
149
|
+
exit 0
|
|
150
|
+
fi
|
|
151
|
+
done
|
|
152
|
+
echo "Server did not become ready in time. Check: docker compose -f $COMPOSE_FILE logs"
|
|
153
|
+
exit 1
|
|
154
|
+
` : `
|
|
155
|
+
echo "Starting ${engine} locally..."
|
|
156
|
+
${engine === 'parakeet'
|
|
157
|
+
? `python3 -c "
|
|
158
|
+
from nemo.collections.asr.models import EncDecRNNTBPEModel
|
|
159
|
+
import json, http.server, io, soundfile as sf, numpy as np
|
|
160
|
+
model = EncDecRNNTBPEModel.from_pretrained('${parakeet.DEFAULT_MODEL}')
|
|
161
|
+
|
|
162
|
+
class Handler(http.server.BaseHTTPRequestHandler):
|
|
163
|
+
def do_GET(self):
|
|
164
|
+
if self.path == '/health':
|
|
165
|
+
self.send_response(200); self.end_headers(); self.wfile.write(b'ok')
|
|
166
|
+
else:
|
|
167
|
+
self.send_response(404); self.end_headers()
|
|
168
|
+
def do_POST(self):
|
|
169
|
+
length = int(self.headers['Content-Length'])
|
|
170
|
+
audio_bytes = self.rfile.read(length)
|
|
171
|
+
audio, sr = sf.read(io.BytesIO(audio_bytes))
|
|
172
|
+
transcription = model.transcribe([audio])
|
|
173
|
+
result = json.dumps({'text': transcription[0] if transcription else ''})
|
|
174
|
+
self.send_response(200)
|
|
175
|
+
self.send_header('Content-Type', 'application/json')
|
|
176
|
+
self.end_headers()
|
|
177
|
+
self.wfile.write(result.encode())
|
|
178
|
+
def log_message(self, format, *args): pass
|
|
179
|
+
|
|
180
|
+
httpd = http.server.HTTPServer(('', $PORT), Handler)
|
|
181
|
+
print('Parakeet TDT server listening on port $PORT')
|
|
182
|
+
httpd.serve_forever()
|
|
183
|
+
" &`
|
|
184
|
+
: `cd "$HOME/faster-whisper-server"
|
|
185
|
+
source .venv/bin/activate
|
|
186
|
+
uvicorn --factory faster_whisper_server.main:create_app --port $PORT &`}
|
|
187
|
+
SERVER_PID=$!
|
|
188
|
+
|
|
189
|
+
echo "Waiting for server (up to 60s)..."
|
|
190
|
+
for i in $(seq 1 12); do
|
|
191
|
+
sleep 5
|
|
192
|
+
if curl -sf http://localhost:$PORT/health > /dev/null 2>&1; then
|
|
193
|
+
echo "Server ready on port $PORT (PID: $SERVER_PID)"
|
|
194
|
+
exit 0
|
|
195
|
+
fi
|
|
196
|
+
done
|
|
197
|
+
echo "Server did not start in time."
|
|
198
|
+
exit 1
|
|
199
|
+
`}
|
|
200
|
+
`;
|
|
201
|
+
|
|
202
|
+
const scriptPath = path.join(this.scriptsDir, `launch-${engine}-stt.sh`);
|
|
203
|
+
await fs.writeFile(scriptPath, script, 'utf-8');
|
|
204
|
+
await fs.chmod(scriptPath, '755');
|
|
205
|
+
|
|
206
|
+
// Stop script for Docker mode
|
|
207
|
+
if (mode === 'docker') {
|
|
208
|
+
const stopScript = `#!/bin/bash
|
|
209
|
+
# Stop ${engine} STT Docker container
|
|
210
|
+
|
|
211
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
212
|
+
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
213
|
+
COMPOSE_FILE="$PROJECT_DIR/${composeFile}"
|
|
214
|
+
|
|
215
|
+
if [ ! -f "$COMPOSE_FILE" ]; then
|
|
216
|
+
echo "Error: $COMPOSE_FILE not found"
|
|
217
|
+
exit 1
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
echo "Stopping ${engine} server..."
|
|
221
|
+
docker compose -f "$COMPOSE_FILE" down
|
|
222
|
+
echo "Done."
|
|
223
|
+
`;
|
|
224
|
+
|
|
225
|
+
const stopPath = path.join(this.scriptsDir, `stop-${engine}-stt.sh`);
|
|
226
|
+
await fs.writeFile(stopPath, stopScript, 'utf-8');
|
|
227
|
+
await fs.chmod(stopPath, '755');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @param {string} engine
|
|
233
|
+
* @param {{ model: string, mode: string }} result
|
|
234
|
+
*/
|
|
235
|
+
printSuccess(engine, result) {
|
|
236
|
+
console.log(chalk.green(`\n STT engine installed: ${engine}`));
|
|
237
|
+
console.log(chalk.gray(` Model: ${result.model}`));
|
|
238
|
+
console.log(chalk.gray(` Mode: ${result.mode}`));
|
|
239
|
+
console.log(chalk.gray(` Port: ${engine === 'parakeet' ? parakeet.PARAKEET_PORT : whisper.WHISPER_PORT}`));
|
|
240
|
+
console.log(chalk.blue('\n Launch:'));
|
|
241
|
+
console.log(chalk.white(` ./scripts/launch-${engine}-stt.sh\n`));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
module.exports = ParakeetInstaller;
|
|
246
|
+
|
|
247
|
+
// CLI entry point
|
|
248
|
+
if (require.main === module) {
|
|
249
|
+
const args = process.argv.slice(2);
|
|
250
|
+
const mode = args[0] || 'auto';
|
|
251
|
+
const projectRoot = path.resolve(__dirname, '..');
|
|
252
|
+
|
|
253
|
+
const installer = new ParakeetInstaller(projectRoot, mode);
|
|
254
|
+
installer.setup()
|
|
255
|
+
.then(result => process.exit(result.success ? 0 : 1))
|
|
256
|
+
.catch(err => {
|
|
257
|
+
console.error(chalk.red('Fatal:'), err.message);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
});
|
|
260
|
+
}
|
package/src/webui/api.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BYAN WebUI REST API handlers.
|
|
3
|
+
* Each handler receives (req, res, server) where server is ByanWebUI instance.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
let yanstaller = null;
|
|
7
|
+
let detector = null;
|
|
8
|
+
let backuper = null;
|
|
9
|
+
let sttEngine = null;
|
|
10
|
+
|
|
11
|
+
try { yanstaller = require('../../lib/yanstaller'); } catch { yanstaller = null; }
|
|
12
|
+
try { detector = require('../../lib/yanstaller/detector'); } catch { detector = null; }
|
|
13
|
+
try { backuper = require('../../lib/yanstaller/backuper'); } catch { backuper = null; }
|
|
14
|
+
try { sttEngine = require('../../lib/stt/engine'); } catch { sttEngine = null; }
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
function json(res, statusCode, data) {
|
|
20
|
+
res.writeHead(statusCode);
|
|
21
|
+
res.end(JSON.stringify(data));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readPackageVersion() {
|
|
25
|
+
try {
|
|
26
|
+
const pkg = JSON.parse(
|
|
27
|
+
fs.readFileSync(path.resolve(__dirname, '..', '..', 'package.json'), 'utf8')
|
|
28
|
+
);
|
|
29
|
+
return pkg.version || 'unknown';
|
|
30
|
+
} catch {
|
|
31
|
+
return 'unknown';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isByanInstalled(projectRoot) {
|
|
36
|
+
return fs.existsSync(path.join(projectRoot, '_bmad'));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function detectPlatforms(projectRoot) {
|
|
40
|
+
const found = [];
|
|
41
|
+
const checks = [
|
|
42
|
+
{ name: 'copilot-cli', path: '.github/agents' },
|
|
43
|
+
{ name: 'vscode', path: '.vscode' },
|
|
44
|
+
{ name: 'claude', path: '.claude' },
|
|
45
|
+
{ name: 'codex', path: '.codex/prompts' }
|
|
46
|
+
];
|
|
47
|
+
for (const check of checks) {
|
|
48
|
+
if (fs.existsSync(path.join(projectRoot, check.path))) {
|
|
49
|
+
found.push(check.name);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return found;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const routes = {
|
|
56
|
+
'GET status': async (req, res, server) => {
|
|
57
|
+
const projectRoot = server.projectRoot;
|
|
58
|
+
const version = readPackageVersion();
|
|
59
|
+
const installed = isByanInstalled(projectRoot);
|
|
60
|
+
|
|
61
|
+
let platforms = detectPlatforms(projectRoot);
|
|
62
|
+
let detectionResult = null;
|
|
63
|
+
|
|
64
|
+
if (detector) {
|
|
65
|
+
try {
|
|
66
|
+
detectionResult = await detector.detect();
|
|
67
|
+
platforms = detectionResult.platforms
|
|
68
|
+
.filter(p => p.detected)
|
|
69
|
+
.map(p => p.name);
|
|
70
|
+
} catch {
|
|
71
|
+
// Fallback to fs-based detection
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
json(res, 200, {
|
|
76
|
+
version,
|
|
77
|
+
installed,
|
|
78
|
+
platforms,
|
|
79
|
+
detection: detectionResult,
|
|
80
|
+
sttEngine: sttEngine ? 'available' : 'not-installed',
|
|
81
|
+
projectRoot
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
'GET update/check': async (req, res, server) => {
|
|
86
|
+
const version = readPackageVersion();
|
|
87
|
+
|
|
88
|
+
// Simulated update check; real implementation would query npm registry
|
|
89
|
+
json(res, 200, {
|
|
90
|
+
updateAvailable: false,
|
|
91
|
+
installed: version,
|
|
92
|
+
latest: version,
|
|
93
|
+
changes: []
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
'POST install': async (req, res, server) => {
|
|
98
|
+
json(res, 200, { status: 'started' });
|
|
99
|
+
|
|
100
|
+
const config = req.body || {};
|
|
101
|
+
const projectRoot = server.projectRoot;
|
|
102
|
+
const steps = [
|
|
103
|
+
'Detecting environment',
|
|
104
|
+
'Validating prerequisites',
|
|
105
|
+
'Creating directory structure',
|
|
106
|
+
'Installing core module',
|
|
107
|
+
'Installing selected modules',
|
|
108
|
+
'Configuring platforms',
|
|
109
|
+
'Generating agent stubs',
|
|
110
|
+
'Writing configuration',
|
|
111
|
+
'Validating installation'
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
for (let i = 0; i < steps.length; i++) {
|
|
116
|
+
server.broadcastProgress(i + 1, steps.length, steps[i]);
|
|
117
|
+
server.broadcastLog('info', steps[i] + '...');
|
|
118
|
+
await sleep(300);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (yanstaller) {
|
|
122
|
+
try {
|
|
123
|
+
await yanstaller.install({
|
|
124
|
+
mode: config.mode || 'full',
|
|
125
|
+
platforms: config.platforms,
|
|
126
|
+
yes: true,
|
|
127
|
+
projectRoot
|
|
128
|
+
});
|
|
129
|
+
} catch (err) {
|
|
130
|
+
server.broadcastLog('warn', `Yanstaller: ${err.message} (continuing with stub install)`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
ensureDirectoryStructure(projectRoot);
|
|
135
|
+
writeBaseConfig(projectRoot, config);
|
|
136
|
+
|
|
137
|
+
server.broadcastProgress(steps.length, steps.length, 'Complete');
|
|
138
|
+
server.broadcastComplete(true, {
|
|
139
|
+
message: 'BYAN installed successfully',
|
|
140
|
+
projectRoot,
|
|
141
|
+
mode: config.mode || 'auto',
|
|
142
|
+
platforms: config.platforms || detectPlatforms(projectRoot)
|
|
143
|
+
});
|
|
144
|
+
} catch (err) {
|
|
145
|
+
server.broadcastLog('error', err.message);
|
|
146
|
+
server.broadcastComplete(false, { message: err.message });
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
'POST update': async (req, res, server) => {
|
|
151
|
+
json(res, 200, { status: 'started' });
|
|
152
|
+
|
|
153
|
+
const steps = [
|
|
154
|
+
'Checking current version',
|
|
155
|
+
'Creating backup',
|
|
156
|
+
'Downloading update',
|
|
157
|
+
'Applying changes',
|
|
158
|
+
'Merging configuration',
|
|
159
|
+
'Validating update'
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
for (let i = 0; i < steps.length; i++) {
|
|
164
|
+
server.broadcastProgress(i + 1, steps.length, steps[i]);
|
|
165
|
+
server.broadcastLog('info', steps[i] + '...');
|
|
166
|
+
await sleep(400);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (yanstaller && yanstaller.update) {
|
|
170
|
+
try {
|
|
171
|
+
await yanstaller.update('latest');
|
|
172
|
+
} catch (err) {
|
|
173
|
+
server.broadcastLog('warn', `Update module: ${err.message}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
server.broadcastComplete(true, { message: 'BYAN updated successfully' });
|
|
178
|
+
} catch (err) {
|
|
179
|
+
server.broadcastLog('error', err.message);
|
|
180
|
+
server.broadcastComplete(false, { message: err.message });
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
'POST rollback': async (req, res, server) => {
|
|
185
|
+
const { backupPath } = req.body || {};
|
|
186
|
+
|
|
187
|
+
if (!backuper) {
|
|
188
|
+
json(res, 503, { error: 'Backup module not available' });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const targetPath = path.join(server.projectRoot, '_bmad');
|
|
194
|
+
await backuper.restore(backupPath, targetPath);
|
|
195
|
+
json(res, 200, { success: true, message: 'Rollback complete' });
|
|
196
|
+
} catch (err) {
|
|
197
|
+
json(res, 500, { error: err.message });
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
'GET backups': async (req, res, server) => {
|
|
202
|
+
if (!backuper) {
|
|
203
|
+
json(res, 200, { backups: [] });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const list = await backuper.listBackups(server.projectRoot);
|
|
209
|
+
json(res, 200, { backups: list });
|
|
210
|
+
} catch (err) {
|
|
211
|
+
json(res, 500, { error: err.message });
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
'GET stt/status': async (req, res) => {
|
|
216
|
+
if (!sttEngine) {
|
|
217
|
+
json(res, 200, { available: false, reason: 'STT engine module not installed' });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const status = typeof sttEngine.status === 'function'
|
|
223
|
+
? await sttEngine.status()
|
|
224
|
+
: { available: true };
|
|
225
|
+
json(res, 200, status);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
json(res, 500, { error: err.message });
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
'POST stt/test': async (req, res) => {
|
|
232
|
+
if (!sttEngine) {
|
|
233
|
+
json(res, 503, { error: 'STT engine not available' });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const result = typeof sttEngine.test === 'function'
|
|
239
|
+
? await sttEngine.test()
|
|
240
|
+
: { success: false, reason: 'Test not implemented' };
|
|
241
|
+
json(res, 200, result);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
json(res, 500, { error: err.message });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
function resolve(method, route) {
|
|
249
|
+
const key = `${method} ${route}`;
|
|
250
|
+
return routes[key] || null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function sleep(ms) {
|
|
254
|
+
return new Promise(r => setTimeout(r, ms));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function ensureDirectoryStructure(projectRoot) {
|
|
258
|
+
const dirs = [
|
|
259
|
+
'_bmad',
|
|
260
|
+
'_bmad/_config',
|
|
261
|
+
'_bmad/_memory',
|
|
262
|
+
'_bmad/core',
|
|
263
|
+
'_bmad/core/agents',
|
|
264
|
+
'_bmad/core/workflows',
|
|
265
|
+
'_bmad/core/tasks',
|
|
266
|
+
'_bmad-output',
|
|
267
|
+
'_bmad-output/planning-artifacts',
|
|
268
|
+
'_bmad-output/implementation-artifacts'
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
for (const dir of dirs) {
|
|
272
|
+
const full = path.join(projectRoot, dir);
|
|
273
|
+
if (!fs.existsSync(full)) {
|
|
274
|
+
fs.mkdirSync(full, { recursive: true });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function writeBaseConfig(projectRoot, config) {
|
|
280
|
+
const configPath = path.join(projectRoot, '_bmad', 'core', 'config.yaml');
|
|
281
|
+
if (fs.existsSync(configPath)) return;
|
|
282
|
+
|
|
283
|
+
const content = [
|
|
284
|
+
`user_name: ${config.userName || 'User'}`,
|
|
285
|
+
`communication_language: ${config.language || 'English'}`,
|
|
286
|
+
`document_output_language: ${config.language || 'English'}`,
|
|
287
|
+
`output_folder: "{project-root}/_bmad-output"`
|
|
288
|
+
].join('\n') + '\n';
|
|
289
|
+
|
|
290
|
+
fs.writeFileSync(configPath, content, 'utf8');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = { resolve, routes };
|