TUI Source
This commit is contained in:
22
cli/hooks/postinstall.js
Normal file
22
cli/hooks/postinstall.js
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Postinstall: warm-up SQLite deps into ~/.9router/runtime so the first
|
||||
// `9router` start doesn't need network. Failure here is non-fatal —
|
||||
// cli.js will retry at runtime if anything is missing.
|
||||
const { ensureSqliteRuntime } = require("./sqliteRuntime");
|
||||
const { ensureTrayRuntime } = require("./trayRuntime");
|
||||
|
||||
try {
|
||||
ensureSqliteRuntime({ silent: false });
|
||||
console.log("[9router] runtime SQLite deps ready");
|
||||
} catch (e) {
|
||||
console.warn(`[9router] runtime warm-up skipped: ${e.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
ensureTrayRuntime({ silent: false });
|
||||
} catch (e) {
|
||||
console.warn(`[9router] tray runtime skipped: ${e.message}`);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
114
cli/hooks/sqliteRuntime.js
Normal file
114
cli/hooks/sqliteRuntime.js
Normal file
@@ -0,0 +1,114 @@
|
||||
// Ensure better-sqlite3 is installed in USER_DATA_DIR/runtime/node_modules
|
||||
// (user-writable, avoids Windows EBUSY locks during npm i -g updates).
|
||||
// sql.js is bundled in bin/app already; node:sqlite / bun:sqlite are built-in.
|
||||
const { execSync, spawnSync } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
|
||||
const BETTER_SQLITE3_VERSION = "12.6.2";
|
||||
|
||||
function getDataDir() {
|
||||
if (process.env.DATA_DIR) return process.env.DATA_DIR;
|
||||
return process.platform === "win32"
|
||||
? path.join(process.env.APPDATA || os.homedir(), "9router")
|
||||
: path.join(os.homedir(), ".9router");
|
||||
}
|
||||
|
||||
function getRuntimeDir() {
|
||||
return path.join(getDataDir(), "runtime");
|
||||
}
|
||||
|
||||
function getRuntimeNodeModules() {
|
||||
return path.join(getRuntimeDir(), "node_modules");
|
||||
}
|
||||
|
||||
function ensureRuntimeDir() {
|
||||
const dir = getRuntimeDir();
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
// Minimal package.json so npm treats it as a project root
|
||||
const pkgPath = path.join(dir, "package.json");
|
||||
if (!fs.existsSync(pkgPath)) {
|
||||
fs.writeFileSync(pkgPath, JSON.stringify({
|
||||
name: "9router-runtime",
|
||||
version: "1.0.0",
|
||||
private: true,
|
||||
description: "User-writable runtime deps for 9router (better-sqlite3 native binary)",
|
||||
}, null, 2));
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
function hasModule(name) {
|
||||
return fs.existsSync(path.join(getRuntimeNodeModules(), name, "package.json"));
|
||||
}
|
||||
|
||||
function isBetterSqliteBinaryValid() {
|
||||
const binary = path.join(getRuntimeNodeModules(), "better-sqlite3", "build", "Release", "better_sqlite3.node");
|
||||
if (!fs.existsSync(binary)) return false;
|
||||
try {
|
||||
const fd = fs.openSync(binary, "r");
|
||||
const buf = Buffer.alloc(4);
|
||||
fs.readSync(fd, buf, 0, 4, 0);
|
||||
fs.closeSync(fd);
|
||||
const magic = buf.toString("hex");
|
||||
if (process.platform === "linux") return magic.startsWith("7f454c46");
|
||||
if (process.platform === "darwin") return magic.startsWith("cffaedfe") || magic.startsWith("cefaedfe");
|
||||
if (process.platform === "win32") return magic.startsWith("4d5a");
|
||||
return true;
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
function npmInstall(pkgs, opts = {}) {
|
||||
const cwd = ensureRuntimeDir();
|
||||
const args = ["install", ...pkgs, "--no-audit", "--no-fund", "--prefer-online"];
|
||||
if (opts.optional) args.push("--no-save");
|
||||
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
console.log(`[9router][runtime] ${npmCmd} ${args.join(" ")} (cwd: ${cwd})`);
|
||||
const res = spawnSync(npmCmd, args, {
|
||||
cwd,
|
||||
stdio: opts.silent ? "ignore" : "inherit",
|
||||
timeout: opts.timeout || 180000,
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
return res.status === 0;
|
||||
}
|
||||
|
||||
// Public: ensure better-sqlite3 native module is installed in user-writable
|
||||
// runtime dir. sql.js is bundled in bin/app already; node:sqlite is built-in.
|
||||
// This is purely a *speed optimization* — app works without it via fallbacks.
|
||||
function ensureSqliteRuntime({ silent = false } = {}) {
|
||||
ensureRuntimeDir();
|
||||
|
||||
const needBetterSqlite = !hasModule("better-sqlite3") || !isBetterSqliteBinaryValid();
|
||||
if (!needBetterSqlite) {
|
||||
if (!silent) console.log("[9router][runtime] better-sqlite3 OK");
|
||||
return { betterSqlite: true };
|
||||
}
|
||||
|
||||
const ok = npmInstall([`better-sqlite3@${BETTER_SQLITE3_VERSION}`], { optional: true, silent });
|
||||
if (!ok && !silent) {
|
||||
console.warn("[9router][runtime] better-sqlite3 install failed (will use node:sqlite or sql.js fallback)");
|
||||
}
|
||||
return {
|
||||
betterSqlite: ok && hasModule("better-sqlite3") && isBetterSqliteBinaryValid(),
|
||||
};
|
||||
}
|
||||
|
||||
// Inject runtime + bundled node_modules into NODE_PATH so child Node processes
|
||||
// resolve sql.js (bundled in bin/app/node_modules) and better-sqlite3 (runtime).
|
||||
function buildEnvWithRuntime(baseEnv = process.env) {
|
||||
const runtimeNm = getRuntimeNodeModules();
|
||||
const bundledNm = path.join(__dirname, "..", "app", "node_modules");
|
||||
const existing = baseEnv.NODE_PATH || "";
|
||||
const NODE_PATH = [runtimeNm, bundledNm, existing].filter(Boolean).join(path.delimiter);
|
||||
return { ...baseEnv, NODE_PATH };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ensureSqliteRuntime,
|
||||
buildEnvWithRuntime,
|
||||
getRuntimeDir,
|
||||
getRuntimeNodeModules,
|
||||
};
|
||||
84
cli/hooks/trayRuntime.js
Normal file
84
cli/hooks/trayRuntime.js
Normal file
@@ -0,0 +1,84 @@
|
||||
// Lazy install systray for macOS/Linux into USER_DATA_DIR/runtime/node_modules.
|
||||
// Windows uses PowerShell NotifyIcon (no binary) → no systray needed.
|
||||
// This keeps the published npm tarball free of unsigned Go binaries that
|
||||
// trigger antivirus false positives (e.g. Kaspersky flagging tray_windows.exe).
|
||||
const { spawnSync } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { getRuntimeDir, getRuntimeNodeModules } = require("./sqliteRuntime");
|
||||
|
||||
const SYSTRAY_VERSION = "1.0.5";
|
||||
|
||||
function hasSystray() {
|
||||
return fs.existsSync(path.join(getRuntimeNodeModules(), "systray", "package.json"));
|
||||
}
|
||||
|
||||
// Remove legacy systray from all known locations on Windows (AV false positive cleanup)
|
||||
function cleanupWindowsSystray({ silent = false } = {}) {
|
||||
if (process.platform !== "win32") return;
|
||||
// 1) Runtime dir: %APPDATA%\9router\runtime\node_modules\systray
|
||||
// 2) npm global nested: <npm_prefix>\node_modules\9router\node_modules\systray
|
||||
// __dirname here = <npm_prefix>\node_modules\9router\hooks → up 1 = pkg root
|
||||
const targets = [
|
||||
path.join(getRuntimeNodeModules(), "systray"),
|
||||
path.join(__dirname, "..", "node_modules", "systray")
|
||||
];
|
||||
for (const dir of targets) {
|
||||
if (fs.existsSync(dir)) {
|
||||
try {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
if (!silent) console.log(`[9router][runtime] removed legacy systray: ${dir}`);
|
||||
} catch (e) {
|
||||
if (!silent) console.warn(`[9router][runtime] failed to remove ${dir}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensureRuntimeDir() {
|
||||
const dir = getRuntimeDir();
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
const pkgPath = path.join(dir, "package.json");
|
||||
if (!fs.existsSync(pkgPath)) {
|
||||
fs.writeFileSync(pkgPath, JSON.stringify({
|
||||
name: "9router-runtime",
|
||||
version: "1.0.0",
|
||||
private: true
|
||||
}, null, 2));
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
function npmInstall(pkgs, { silent = false } = {}) {
|
||||
const cwd = ensureRuntimeDir();
|
||||
const args = ["install", ...pkgs, "--no-audit", "--no-fund", "--no-save", "--prefer-online"];
|
||||
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
if (!silent) console.log(`[9router][runtime] ${npmCmd} ${args.join(" ")} (cwd: ${cwd})`);
|
||||
const res = spawnSync(npmCmd, args, {
|
||||
cwd,
|
||||
stdio: silent ? "ignore" : "inherit",
|
||||
timeout: 120000,
|
||||
shell: process.platform === "win32"
|
||||
});
|
||||
return res.status === 0;
|
||||
}
|
||||
|
||||
// Public: ensure systray is installed on macOS/Linux only.
|
||||
// Windows skips entirely (uses PowerShell tray).
|
||||
function ensureTrayRuntime({ silent = false } = {}) {
|
||||
if (process.platform === "win32") {
|
||||
cleanupWindowsSystray({ silent });
|
||||
return { systray: false, skipped: true };
|
||||
}
|
||||
if (hasSystray()) {
|
||||
if (!silent) console.log("[9router][runtime] systray OK");
|
||||
return { systray: true };
|
||||
}
|
||||
const ok = npmInstall([`systray@${SYSTRAY_VERSION}`], { silent });
|
||||
if (!ok && !silent) {
|
||||
console.warn("[9router][runtime] systray install failed (tray will be disabled)");
|
||||
}
|
||||
return { systray: ok && hasSystray() };
|
||||
}
|
||||
|
||||
module.exports = { ensureTrayRuntime };
|
||||
Reference in New Issue
Block a user