File size: 3,889 Bytes
5448d8b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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.9.0";

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();
  const npmArgs = ["install", ...pkgs, "--no-audit", "--no-fund", "--prefer-online", "--no-save"];
  // 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 === "\\" ? ";" : ":") };
}