File size: 4,163 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
128
129
130
131
132
133
134
135
136
137
138
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 === "\\" ? ";" : ":") };
}