Rudder is the first plugin that isn't a map: it shows that one plugin-designs/ folder, developed live in the Plugin Studio, can ship as a desktop app with its own local backend. - plugin-designs/rudder-voice: <rudder-voice> element. Words stream into the end of a text area while you type/paste anywhere (typing holds the stream; nothing duplicated or lost). Mic picker with hot-plug, 3-second mic test, plain-language errors, info panel, privacy-safe debug report. meta.json declares "targets": ["desktop"]. - backend/rudder-engine (Go): supervises NVIDIA NeMo-Speech.cpp (ggml, CPU) with the Nemotron 3.5 ASR Streaming GGUF over a loopback WebSocket; records with parec/pw-record only while recording; recovers dead Bluetooth mic links (reopen -> CVSD voice profile -> optional RUDDER_BT_RESET) and starts headsets on CVSD. No telemetry: traced with strace, zero sockets/DNS while transcribing. - Privacy: log lines redact MAC addresses and the home folder, transcripts are never logged, Host check against DNS rebinding, CORS only for the desktop webview, random API key for the speech server. - internal/backend-dev-plugin.mjs + vite.config.js: `bun run dev` starts built plugin backends and proxies /api/<name> same-origin, so a plugin calls the same API in the studio and on desktop. - backend/rudder-engine/setup-runtime.sh: fetches the runtime and model pinned by SHA-256. Not yet: the Tauri desktop build (sidecar) and the PocketBase word-correction table.
61 lines
2.7 KiB
JavaScript
61 lines
2.7 KiB
JavaScript
/**
|
|
* internal/backend-dev-plugin.mjs — `bun run dev` starts the local plugin backends.
|
|
*
|
|
* WHY: some plugins ship to DESKTOP with a local backend (rudder-voice → backend/bin/rudder-engine,
|
|
* run as a Tauri sidecar). In the Plugin Studio the same backend must be running, or the plugin can't
|
|
* be tested in the browser. This plugin starts each built backend with the dev server, forwards its
|
|
* /api/<name>/ routes, and stops it when the dev server stops. A backend that isn't built is skipped
|
|
* with a hint: plugins without a backend never notice this file.
|
|
*
|
|
* DEV ONLY (`apply: 'serve'`). A production build never starts or bundles a backend.
|
|
*/
|
|
import { spawn } from 'node:child_process'
|
|
import { existsSync } from 'node:fs'
|
|
import { resolve } from 'node:path'
|
|
|
|
// name → how to run it. `route` is proxied to `port` on 127.0.0.1.
|
|
export const DEV_BACKENDS = [
|
|
{
|
|
name: 'rudder-engine',
|
|
bin: 'backend/bin/rudder-engine',
|
|
build: 'cd backend/rudder-engine && go build -o ../bin/rudder-engine .',
|
|
route: '/api/rudder',
|
|
port: Number(process.env.RUDDER_PORT || 5193),
|
|
env: { RUDDER_PORT: String(process.env.RUDDER_PORT || 5193), RUDDER_NEMO_PORT: String(process.env.RUDDER_NEMO_PORT || 5194) },
|
|
},
|
|
]
|
|
|
|
// The proxy table for vite's `server.proxy`. SSE (`/events`) streams through unbuffered.
|
|
export function devBackendProxy() {
|
|
return Object.fromEntries(DEV_BACKENDS.map((b) => [b.route, { target: `http://127.0.0.1:${b.port}`, changeOrigin: false }]))
|
|
}
|
|
|
|
export function backendDevPlugin(root) {
|
|
const children = []
|
|
return {
|
|
name: 'dx-backend-dev',
|
|
apply: 'serve',
|
|
configureServer(server) {
|
|
for (const b of DEV_BACKENDS) {
|
|
const bin = resolve(root, b.bin)
|
|
if (!existsSync(bin)) {
|
|
server.config.logger.warn(`[backend] ${b.name} not built — its plugin will show "engine offline". Build: ${b.build}`)
|
|
continue
|
|
}
|
|
const child = spawn(bin, [], { cwd: root, env: { ...process.env, ...b.env }, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
const tag = `[${b.name}]`
|
|
const out = (d) => String(d).trim().split('\n').forEach((l) => l && server.config.logger.info(`${tag} ${l}`))
|
|
child.stdout.on('data', out)
|
|
child.stderr.on('data', out)
|
|
child.on('exit', (code) => server.config.logger.warn(`${tag} exited (${code})`))
|
|
children.push(child)
|
|
server.config.logger.info(`${tag} started → ${b.route} on 127.0.0.1:${b.port}`)
|
|
}
|
|
const stop = () => children.forEach((c) => { try { c.kill('SIGTERM') } catch {} })
|
|
server.httpServer?.on('close', stop)
|
|
process.once('exit', stop)
|
|
process.once('SIGINT', () => { stop(); process.exit(0) })
|
|
process.once('SIGTERM', () => { stop(); process.exit(0) })
|
|
},
|
|
}
|
|
}
|