mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 07:12:12 +03:00
Merge PR #2280: feat(cli): CLI v4 — Commander.js, 50+ commands, TUI, i18n, plugins (Phases 0-9)
Complete rewrite of the OmniRoute CLI: - Commander.js-based modular architecture (50+ command files) - Full i18n support (en + pt-BR, 1222 keys each) - TUI interactive interface (OAuthFlow, EvalWatch, ProvidersTestAll) - Plugin system (omniroute-cmd-*) - OpenAPI codegen (omniroute api <tag> <op>) - Commands: serve, combo, compression, keys, tunnel, backup, test-provider, health, memory, MCP, A2A, oauth, skills, webhooks, usage, cost, eval, context-eng, dashboard, doctor, env, files, logs, models, nodes, oneproxy, open, openapi, plugin, policy, pricing, providers, quota, registry, repl, reset-encrypted-columns, resilience, restart, runtime, sessions, setup, simulate, status, stop, stream, sync, tags, telemetry, translator, tray, update - Code review fixes: C1-C3, I1-I5, M1-M4 applied # Conflicts: # bin/cli/commands/config.mjs # bin/omniroute.mjs # package-lock.json # package.json
This commit is contained in:
126
bin/cli/runtime/nativeDeps.mjs
Normal file
126
bin/cli/runtime/nativeDeps.mjs
Normal file
@@ -0,0 +1,126 @@
|
||||
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 === "\\" ? ";" : ":") };
|
||||
}
|
||||
113
bin/cli/runtime/processSupervisor.mjs
Normal file
113
bin/cli/runtime/processSupervisor.mjs
Normal file
@@ -0,0 +1,113 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { dirname } from "node:path";
|
||||
import { writePidFile, cleanupPidFile, killAllSubprocesses } from "../utils/pid.mjs";
|
||||
|
||||
const CRASH_LOG_LINES = 50;
|
||||
const RESTART_RESET_MS = 30_000;
|
||||
|
||||
export class ServerSupervisor {
|
||||
constructor({ serverPath, env, maxRestarts = 2, memoryLimit = 512, onCrashCallback }) {
|
||||
this.serverPath = serverPath;
|
||||
this.env = env;
|
||||
this.maxRestarts = maxRestarts;
|
||||
this.memoryLimit = memoryLimit;
|
||||
this.onCrashCallback = onCrashCallback;
|
||||
this.restartCount = 0;
|
||||
this.startedAt = 0;
|
||||
this.crashLog = [];
|
||||
this.child = null;
|
||||
this.isShuttingDown = false;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.startedAt = Date.now();
|
||||
this.crashLog = [];
|
||||
|
||||
const showLog = process.env.OMNIROUTE_SHOW_LOG === "1";
|
||||
this.child = spawn("node", [`--max-old-space-size=${this.memoryLimit}`, this.serverPath], {
|
||||
cwd: dirname(this.serverPath),
|
||||
env: this.env,
|
||||
stdio: showLog ? "inherit" : ["ignore", "ignore", "pipe"],
|
||||
});
|
||||
|
||||
writePidFile("server", this.child.pid);
|
||||
|
||||
if (this.child.stderr) {
|
||||
this.child.stderr.on("data", (data) => {
|
||||
const lines = data.toString().split("\n").filter(Boolean);
|
||||
this.crashLog.push(...lines);
|
||||
if (this.crashLog.length > CRASH_LOG_LINES) {
|
||||
this.crashLog = this.crashLog.slice(-CRASH_LOG_LINES);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.child.on("error", (err) => this.handleExit(err.code ?? -1, err));
|
||||
this.child.on("exit", (code) => this.handleExit(code));
|
||||
|
||||
return this.child;
|
||||
}
|
||||
|
||||
handleExit(code) {
|
||||
cleanupPidFile("server");
|
||||
|
||||
if (this.isShuttingDown || code === 0) {
|
||||
process.exit(code || 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const aliveMs = Date.now() - this.startedAt;
|
||||
if (aliveMs >= RESTART_RESET_MS) this.restartCount = 0;
|
||||
|
||||
if (this.restartCount >= this.maxRestarts) {
|
||||
console.error(`\n⚠ Server crashed ${this.maxRestarts} times in <30s.`);
|
||||
if (this.onCrashCallback) {
|
||||
const action = this.onCrashCallback(this.crashLog);
|
||||
if (action === "disable-mitm-and-retry") {
|
||||
console.error("⚠ Disabling MITM and retrying...\n");
|
||||
this.restartCount = 0;
|
||||
this.start();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.dumpCrashLog();
|
||||
process.exit(code ?? 1);
|
||||
return;
|
||||
}
|
||||
|
||||
this.restartCount++;
|
||||
const delay = Math.min(1000 * 2 ** (this.restartCount - 1), 10_000);
|
||||
console.error(
|
||||
`\n⚠ Server exited (code=${code ?? "?"}). Restarting in ${delay / 1000}s... (${this.restartCount}/${this.maxRestarts})`
|
||||
);
|
||||
if (this.crashLog.length) this.dumpCrashLog();
|
||||
setTimeout(() => this.start(), delay);
|
||||
}
|
||||
|
||||
dumpCrashLog() {
|
||||
console.error("\n--- Server crash log ---");
|
||||
this.crashLog.forEach((l) => console.error(l));
|
||||
console.error("--- End crash log ---\n");
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.isShuttingDown = true;
|
||||
if (this.child?.pid) {
|
||||
try {
|
||||
process.kill(this.child.pid, "SIGTERM");
|
||||
} catch {}
|
||||
setTimeout(() => {
|
||||
try {
|
||||
process.kill(this.child.pid, "SIGKILL");
|
||||
} catch {}
|
||||
}, 5000);
|
||||
}
|
||||
killAllSubprocesses();
|
||||
}
|
||||
}
|
||||
|
||||
export function detectMitmCrash(crashLog) {
|
||||
const text = crashLog.join("\n").toLowerCase();
|
||||
const signals = ["mitm", "tls socket", "certificate", "hosts", "eaccess"];
|
||||
return signals.filter((s) => text.includes(s)).length >= 2;
|
||||
}
|
||||
Reference in New Issue
Block a user