#!/usr/bin/env node /** * Launch the desktop app with a mock inference provider — no real API * keys needed. Starts a local OpenAI-compatible server that returns a * canned reply, writes an isolated config.yaml + .env, and launches the * built Electron app against them. * * This reuses the same mock-server core as the E2E fixtures * (scripts/mock-inference-server.mjs, apps/desktop/e2e/mock-server.ts + * fixtures.ts), so local dev, desktop E2E, and the installer/updater E2E * all exercise the same mock inference chain. * * Prerequisite: `npm run build` must have been run so dist/ exists. * * Usage: * node scripts/dev-mock.mjs * npm run dev:mock * * The mock server listens on an ephemeral port and replies to every * chat completion with: * "Hello from the mock inference server! The full boot chain is working." */ import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { spawn, spawnSync } from 'node:child_process' import { startMockServer } from '../../../scripts/mock-inference-server.mjs' const DESKTOP_ROOT = path.resolve(import.meta.dirname, '..') const REPO_ROOT = path.resolve(DESKTOP_ROOT, '..', '..') // ── Config + env writing (mirrors e2e/fixtures.ts) ───────────────────── function createSandbox() { const root = fs.mkdtempSync(path.join(os.tmpdir(), `hermes-dev-mock-${Date.now()}`)) const hermesHome = path.join(root, 'hermes-home') const userDataDir = path.join(root, 'electron-user-data') fs.mkdirSync(hermesHome, { recursive: true }) fs.mkdirSync(userDataDir, { recursive: true }) return { root, hermesHome, userDataDir, cleanup: () => fs.rmSync(root, { recursive: true, force: true }) } } function writeMockConfig(hermesHome, mockUrl) { fs.writeFileSync( path.join(hermesHome, 'config.yaml'), `# Auto-generated by dev-mock.mjs model: default: mock-model provider: mock providers: mock: api: ${mockUrl}/v1 name: Mock api_mode: chat_completions key_env: MOCK_API_KEY models: mock-model: {} context_length: 4096 `, 'utf8', ) fs.writeFileSync(path.join(hermesHome, '.env'), 'MOCK_API_KEY=e2e-mock-key\n', 'utf8') } // ── Electron launch ──────────────────────────────────────────────────── function findElectron() { const local = path.join(REPO_ROOT, 'node_modules', 'electron', 'dist', 'electron') if (fs.existsSync(local)) return local const r = spawnSync('which', ['electron'], { encoding: 'utf8' }) if (r.status === 0 && r.stdout.trim()) return r.stdout.trim() throw new Error('Electron binary not found. Run "npm install" from the repo root.') } function assertDistBuilt() { const electronMain = path.join(DESKTOP_ROOT, 'dist', 'electron-main.mjs') const indexHtml = path.join(DESKTOP_ROOT, 'dist', 'index.html') if (!fs.existsSync(electronMain) || !fs.existsSync(indexHtml)) { throw new Error( `Desktop dist not built. Run 'cd apps/desktop && npm run build' first.\n` + `Missing: ${electronMain}`, ) } } // ── Main ─────────────────────────────────────────────────────────────── async function main() { assertDistBuilt() console.log('Starting mock inference server...') const mock = await startMockServer() console.log(` Mock server: ${mock.url}`) const sandbox = createSandbox() writeMockConfig(sandbox.hermesHome, mock.url) console.log(` HERMES_HOME: ${sandbox.hermesHome}`) const electronBin = findElectron() const env = { ...process.env, HERMES_HOME: sandbox.hermesHome, HERMES_DESKTOP_USER_DATA_DIR: sandbox.userDataDir, HERMES_DESKTOP_IGNORE_EXISTING: '1', HERMES_DESKTOP_HERMES_ROOT: REPO_ROOT, HERMES_DESKTOP_APP_NAME: `HermesDevMock-${Date.now()}`, } console.log('Launching Electron...') const child = spawn(electronBin, [DESKTOP_ROOT, '--disable-gpu', '--no-sandbox'], { env, cwd: DESKTOP_ROOT, stdio: 'inherit', }) child.on('exit', (code) => { mock.close() sandbox.cleanup() process.exit(code ?? 0) }) } main().catch((err) => { console.error(err) process.exit(1) })