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:
diegosouzapw
2026-05-15 10:50:54 -03:00
229 changed files with 30264 additions and 4992 deletions

View File

@@ -2,6 +2,7 @@ import { isAuthRequired, isDashboardSessionAuthenticated } from "@/shared/utils/
import { createErrorResponse } from "@/lib/api/errorResponse";
import { extractApiKey, isValidApiKey } from "@/sse/services/auth";
import { getApiKeyMetadata } from "@/lib/db/apiKeys";
import { isCliTokenAuthValid } from "@/lib/middleware/cliTokenAuth";
export const MANAGE_SCOPE = "manage";
@@ -18,6 +19,11 @@ export async function requireManagementAuth(request: Request): Promise<Response
return null;
}
// CLI machine-id token allows localhost CLI access without an explicit API key.
if (await isCliTokenAuthValid(request)) {
return null;
}
const apiKey = extractApiKey(request);
if (apiKey) {
let meta: Awaited<ReturnType<typeof getApiKeyMetadata>>;

View File

@@ -260,3 +260,15 @@ export async function deleteCombo(id: string) {
backupDbFile("pre-write");
return true;
}
export async function deleteComboByName(name: string) {
const combo = await getComboByName(name);
if (!combo || typeof combo.id !== "string") return false;
return deleteCombo(combo.id);
}
export function setActiveCombo(name: string, db = getDbInstance()) {
db.prepare(
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('settings', 'activeCombo', ?)"
).run(JSON.stringify(name));
}

33
src/lib/db/recovery.ts Normal file
View File

@@ -0,0 +1,33 @@
import { getDbInstance } from "./core";
type DbInstance = ReturnType<typeof getDbInstance>;
const ENCRYPTED_COLUMNS = ["api_key", "access_token", "refresh_token", "id_token"] as const;
const ENCRYPTED_PATTERN = "enc:v1:%";
function buildWhereClause(): string {
return ENCRYPTED_COLUMNS.map((col) => `${col} LIKE '${ENCRYPTED_PATTERN}'`).join(" OR ");
}
export function countEncryptedCredentials(db: DbInstance = getDbInstance()): number {
const where = buildWhereClause();
const row = db
.prepare(`SELECT COUNT(*) AS cnt FROM provider_connections WHERE ${where}`)
.get() as { cnt: number } | undefined;
return row?.cnt ?? 0;
}
export function resetEncryptedColumns(
{ dryRun }: { dryRun: boolean },
db: DbInstance = getDbInstance()
): { affected: number } {
const affected = countEncryptedCredentials(db);
if (dryRun || affected === 0) return { affected };
const nullCols = ENCRYPTED_COLUMNS.map((col) => `${col} = NULL`).join(", ");
const where = buildWhereClause();
db.prepare(`UPDATE provider_connections SET ${nullCols} WHERE ${where}`).run();
return { affected };
}

View File

@@ -0,0 +1,46 @@
import crypto from "node:crypto";
import { headers } from "next/headers";
const SALT = "omniroute-cli-auth-v1";
const HEADER_NAME = "x-omniroute-cli-token";
export function isLoopback(ip: string): boolean {
const normalized = ip.replace(/^::ffff:/, "");
return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
}
/**
* Validates the CLI machine-id token sent by the local omniroute CLI.
* Only accepted from loopback IPs. Disabled via OMNIROUTE_DISABLE_CLI_TOKEN=true.
*/
export async function isCliTokenAuthValid(request: Request): Promise<boolean> {
if (process.env.OMNIROUTE_DISABLE_CLI_TOKEN === "true") return false;
const hdrs = await headers();
const token = hdrs.get(HEADER_NAME);
if (!token || token.length !== 32) return false;
// Only allow loopback origin — check forwarded-for, real-ip, then host header.
const ip =
(hdrs.get("x-forwarded-for") ?? "").split(",")[0].trim() || hdrs.get("x-real-ip") || "";
if (ip && !isLoopback(ip)) return false;
let expected: string;
try {
const { machineIdSync } = await import("node-machine-id");
const mid = machineIdSync();
expected = crypto
.createHash("sha256")
.update(mid + SALT)
.digest("hex")
.substring(0, 32);
} catch {
return false;
}
try {
return crypto.timingSafeEqual(Buffer.from(token, "utf8"), Buffer.from(expected, "utf8"));
} catch {
return false;
}
}

View File

@@ -526,6 +526,22 @@ amp --model "{{model}}"
},
};
// ─── Registry helpers ────────────────────────────────────────────────────────
export type CliToolEntry = (typeof CLI_TOOLS)[keyof typeof CLI_TOOLS];
/** Returns an ordered list of all registered CLI tools. */
export function listCliTools(): CliToolEntry[] {
return Object.values(CLI_TOOLS) as CliToolEntry[];
}
/** Returns a single tool by id, or undefined if not found. */
export function getCliTool(id: string): CliToolEntry | undefined {
return (CLI_TOOLS as Record<string, CliToolEntry>)[id];
}
// ─── Provider model mapping helper ───────────────────────────────────────────
// Get all provider models for mapping dropdown
export const getProviderModelsForMapping = (providers) => {
const result = [];