import { existsSync, readFileSync, writeFileSync, openSync, readSync, closeSync, mkdirSync, } from "node:fs"; import { join, sep } from "node:path"; import { spawnSync } from "node:child_process"; import { platform } from "node:os"; import { resolveDataDir } from "../data-dir.mjs"; const BETTER_SQLITE3_VERSION = "12.10.1"; function runtimeDir() { return join(resolveDataDir(), "runtime"); } function runtimeModules() { return join(runtimeDir(), "node_modules"); } export function ensureRuntimeDir() { const dir = runtimeDir(); mkdirSync(dir, { recursive: true }); const pkgPath = join(dir, "package.json"); if (!existsSync(pkgPath)) { writeFileSync( pkgPath, JSON.stringify( { name: "omniroute-runtime", version: "1.0.0", private: true, description: "User-writable runtime deps for OmniRoute (native binaries)", }, null, 2 ) ); } return dir; } export function getRuntimeNodeModules() { return runtimeModules(); } export function hasModule(name) { return existsSync(join(runtimeModules(), name, "package.json")); } export function isBetterSqliteBinaryValid() { const binary = join( runtimeModules(), "better-sqlite3", "build", "Release", "better_sqlite3.node" ); if (!existsSync(binary)) return false; try { const fd = openSync(binary, "r"); const buf = Buffer.alloc(4); readSync(fd, buf, 0, 4, 0); closeSync(fd); const magic = buf.toString("hex"); const os = platform(); if (os === "linux") return magic.startsWith("7f454c46"); // ELF if (os === "darwin") return magic.startsWith("cffaedfe") || magic.startsWith("cefaedfe"); // Mach-O if (os === "win32") return magic.startsWith("4d5a"); // PE/MZ return true; } catch { return false; } } export function npmInstallRuntime(pkgs, opts = {}) { const cwd = ensureRuntimeDir(); // Persist to the runtime package.json (exact version) instead of --no-save so a later // install of a sibling runtime dep (e.g. systray2 from trayRuntime.ts, which writes to the // same runtime dir) does not prune this package as "extraneous" — that pruning otherwise // reproduces "No SQLite driver available" after a tray install removes better-sqlite3. const npmArgs = [ "install", ...pkgs, "--no-audit", "--no-fund", "--prefer-online", "--save-exact", ]; // On Windows .cmd files cannot be executed without a shell; use cmd.exe /c explicitly // so we never set shell:true (which would propagate env and enable injection). const isWin = platform() === "win32"; const [exe, args] = isWin ? ["cmd.exe", ["/c", "npm", ...npmArgs]] : ["npm", npmArgs]; if (!opts.silent) { process.stdout.write(`[omniroute][runtime] npm ${npmArgs.join(" ")}\n`); } const res = spawnSync(exe, args, { cwd, stdio: opts.silent ? "ignore" : "inherit", timeout: opts.timeout ?? 180_000, shell: false, env: { ...process.env }, }); return res.status === 0; } /** * Ensure better-sqlite3 is installed and valid in the runtime dir. * Returns { betterSqlite: boolean }. */ export function ensureBetterSqliteRuntime({ silent = false, force = false } = {}) { ensureRuntimeDir(); const valid = hasModule("better-sqlite3") && isBetterSqliteBinaryValid(); if (valid && !force) { if (!silent) process.stdout.write("[omniroute][runtime] better-sqlite3 OK\n"); return { betterSqlite: true }; } const ok = npmInstallRuntime([`better-sqlite3@${BETTER_SQLITE3_VERSION}`], { silent }); if (!ok && !silent) { process.stderr.write("[omniroute][runtime] better-sqlite3 install failed\n"); } return { betterSqlite: ok && hasModule("better-sqlite3") && isBetterSqliteBinaryValid() }; } /** * Build an env object with NODE_PATH extended to include the runtime node_modules. */ export function buildEnvWithRuntime(baseEnv = process.env) { const runtimeNm = runtimeModules(); const existing = baseEnv.NODE_PATH || ""; const parts = [runtimeNm, existing].filter(Boolean); return { ...baseEnv, NODE_PATH: parts.join(sep === "\\" ? ";" : ":") }; }