Files
omarchy/dot_local/bin/executable_free-router
T

79 lines
3.4 KiB
TypeScript
Executable File

#!/usr/bin/env bun
// free-router: run the freellmapi router under Bun on 127.0.0.1:$FREE_ROUTER_PORT
// with its unified API key pinned to $FREE_ROUTER_API_KEY. Both come from
// ~/.config/free-router/env (free-router.service loads it; the shells export it).
//
// Source: ~/.local/share/freellmapi (chezmoi external). Whenever its HEAD
// moves, deps are reinstalled and the dashboard rebuilt before boot.
// State: ~/.local/state/free-router (SQLite DB, generated ENCRYPTION_KEY).
import { randomBytes } from 'node:crypto';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
const home = homedir();
const src = join(process.env.XDG_DATA_HOME || join(home, '.local/share'), 'freellmapi');
const state = join(process.env.XDG_STATE_HOME || join(home, '.local/state'), 'free-router');
function die(msg) {
console.error(`free-router: ${msg}`);
process.exit(1);
}
const { FREE_ROUTER_PORT: port, FREE_ROUTER_API_KEY: apiKey } = process.env;
if (!port || !apiKey) die('FREE_ROUTER_PORT and FREE_ROUTER_API_KEY must be set (see ~/.config/free-router/env)');
if (!existsSync(join(src, 'server/src/index.ts'))) die(`no freellmapi checkout at ${src} (run: chezmoi apply ${src})`);
function run(cmd, cwd) {
const { exitCode } = Bun.spawnSync(cmd, { cwd, stdio: ['ignore', 'inherit', 'inherit'] });
if (exitCode !== 0) die(`\`${cmd.join(' ')}\` failed in ${cwd} (exit ${exitCode})`);
}
mkdirSync(state, { recursive: true, mode: 0o700 });
// Deps + dashboard, once per upstream commit. --no-save keeps bun.lock out of
// the checkout so chezmoi's --ff-only pulls never conflict.
const head = Bun.spawnSync(['git', '-C', src, 'rev-parse', 'HEAD']).stdout.toString().trim();
const stampFile = join(state, 'built-head');
if (!existsSync(stampFile) || readFileSync(stampFile, 'utf8').trim() !== head) {
run([process.execPath, 'install', '--no-save'], src);
run([process.execPath, '--bun', 'run', 'build'], join(src, 'client'));
writeFileSync(stampFile, `${head}\n`);
}
// Provider keys are AES-256-GCM encrypted with this; losing it orphans them.
const keyFile = join(state, 'encryption-key');
if (!existsSync(keyFile)) writeFileSync(keyFile, `${randomBytes(32).toString('hex')}\n`, { mode: 0o600 });
Object.assign(process.env, {
NODE_ENV: 'production',
HOST: '127.0.0.1',
PORT: port,
FREEAPI_DB_PATH: join(state, 'freeapi.db'),
FREEAPI_DB_DIR_HARDENING: '1', // state dir belongs to this router alone
ENCRYPTION_KEY: readFileSync(keyFile, 'utf8').trim(),
});
// Bun refuses to load better-sqlite3 (oven-sh/bun#4290). Serve it from the
// server's own node:sqlite adapter (its Android/Termux driver) instead; the
// "module.exports" key is what require('better-sqlite3') returns.
const { nodeSqliteFactory } = await import(join(src, 'server/src/db/node-sqlite.ts'));
function Database(path) {
return nodeSqliteFactory(path);
}
Bun.plugin({
name: 'better-sqlite3 -> node:sqlite',
setup(build) {
build.module('better-sqlite3', () => ({ loader: 'object', exports: { 'module.exports': Database } }));
},
});
// Migrate, then pin the unified key before the server opens the DB itself and
// starts listening, so no request ever sees the randomly generated one.
const db = await import(join(src, 'server/src/db/index.ts'));
db.initDb();
db.setSetting('unified_api_key', apiKey);
db.getDb().close?.();
await import(join(src, 'server/src/index.ts'));