mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +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:
103
scripts/check/check-cli-i18n.mjs
Normal file
103
scripts/check/check-cli-i18n.mjs
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Validates that:
|
||||
* 1. All t("key") calls in bin/cli/commands/ resolve to existing keys in en.json.
|
||||
* 2. pt-BR.json has the same top-level shape as en.json (no missing top-level sections).
|
||||
* 3. No raw string literals are passed to .description() in commands without going
|
||||
* through t() — only warns, does not fail hard (many descriptions use || fallback).
|
||||
*/
|
||||
import { readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(__dirname, "..", "..");
|
||||
const COMMANDS_DIR = join(ROOT, "bin", "cli", "commands");
|
||||
const LOCALES_DIR = join(ROOT, "bin", "cli", "locales");
|
||||
|
||||
// Paths that look like t() keys but are actually import paths — skip them.
|
||||
const IGNORE_AS_KEY = new Set([".", ".."]);
|
||||
const IMPORT_PATH_RE = /^(\.\.?\/|node:|\/)/;
|
||||
|
||||
function walk(dir) {
|
||||
const results = [];
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const full = join(dir, entry);
|
||||
if (statSync(full).isDirectory()) {
|
||||
results.push(...walk(full));
|
||||
} else if (entry.endsWith(".mjs") || entry.endsWith(".js")) {
|
||||
results.push(full);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function flattenKeys(obj, prefix = "") {
|
||||
const keys = new Set();
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
const full = prefix ? `${prefix}.${k}` : k;
|
||||
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
||||
for (const sub of flattenKeys(v, full)) keys.add(sub);
|
||||
} else {
|
||||
keys.add(full);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function collectTKeys(files) {
|
||||
const used = new Set();
|
||||
const re = /\bt\(\s*["']([^"']+)["']/g;
|
||||
for (const file of files) {
|
||||
const src = readFileSync(file, "utf8");
|
||||
let m;
|
||||
re.lastIndex = 0;
|
||||
while ((m = re.exec(src)) !== null) {
|
||||
const key = m[1];
|
||||
if (IGNORE_AS_KEY.has(key) || IMPORT_PATH_RE.test(key)) continue;
|
||||
used.add(key);
|
||||
}
|
||||
}
|
||||
return used;
|
||||
}
|
||||
|
||||
function loadJson(file) {
|
||||
return JSON.parse(readFileSync(file, "utf8"));
|
||||
}
|
||||
|
||||
const files = walk(COMMANDS_DIR);
|
||||
const usedKeys = collectTKeys(files);
|
||||
const en = loadJson(join(LOCALES_DIR, "en.json"));
|
||||
const ptBR = loadJson(join(LOCALES_DIR, "pt-BR.json"));
|
||||
const enKeys = flattenKeys(en);
|
||||
|
||||
let errors = 0;
|
||||
|
||||
// Check 1: all used keys exist in en.json
|
||||
const missingInEn = [...usedKeys].filter((k) => !enKeys.has(k));
|
||||
if (missingInEn.length > 0) {
|
||||
console.error("[cli-i18n] Keys used in commands but missing in en.json:");
|
||||
for (const k of missingInEn) console.error(` ✗ ${k}`);
|
||||
errors += missingInEn.length;
|
||||
} else {
|
||||
console.log(`[cli-i18n] ✓ All ${usedKeys.size} t() keys found in en.json`);
|
||||
}
|
||||
|
||||
// Check 2: pt-BR.json has the same top-level sections as en.json
|
||||
const enTopLevel = Object.keys(en);
|
||||
const ptTopLevel = new Set(Object.keys(ptBR));
|
||||
const missingTopLevel = enTopLevel.filter((k) => !ptTopLevel.has(k));
|
||||
if (missingTopLevel.length > 0) {
|
||||
console.error("[cli-i18n] Top-level sections in en.json missing from pt-BR.json:");
|
||||
for (const k of missingTopLevel) console.error(` ✗ ${k}`);
|
||||
errors += missingTopLevel.length;
|
||||
} else {
|
||||
console.log(`[cli-i18n] ✓ pt-BR.json has all ${enTopLevel.length} top-level sections`);
|
||||
}
|
||||
|
||||
if (errors > 0) {
|
||||
console.error(`[cli-i18n] FAIL — ${errors} error(s) found`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("[cli-i18n] PASS — CLI i18n is consistent");
|
||||
}
|
||||
@@ -244,6 +244,18 @@ try {
|
||||
checkI18nMirrorFile("llm.txt", llmPath);
|
||||
// CHANGELOG.md mirrors are translations — check version sections and size, not exact content
|
||||
checkI18nChangelogFile(changelogPath);
|
||||
|
||||
// Anti-regression: legacy duplicate docs that have been superseded must not return.
|
||||
// Use docs/reference/* as the source of truth.
|
||||
const supersededDocs = [{ legacy: "docs/CLI-TOOLS.md", current: "docs/reference/CLI-TOOLS.md" }];
|
||||
for (const { legacy, current } of supersededDocs) {
|
||||
const legacyAbs = path.resolve(cwd, legacy);
|
||||
if (fs.existsSync(legacyAbs)) {
|
||||
fail(
|
||||
`legacy duplicate ${legacy} reappeared — use ${current} instead (single source of truth)`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
fail(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ const IGNORE_FROM_CODE = new Set([
|
||||
"TZ",
|
||||
"LANG",
|
||||
"LC_ALL",
|
||||
"LC_MESSAGES",
|
||||
"CI",
|
||||
"GITHUB_ACTIONS",
|
||||
"RUNNER_OS",
|
||||
@@ -64,6 +65,25 @@ const IGNORE_FROM_CODE = new Set([
|
||||
// CI providers (set by the runner).
|
||||
"GITHUB_BASE_REF",
|
||||
"GITHUB_BASE_SHA",
|
||||
// CLI machine-id token opt-out (server-side flag; not user-configurable via .env).
|
||||
"OMNIROUTE_DISABLE_CLI_TOKEN",
|
||||
// update-notifier opt-out for the CLI binary.
|
||||
"OMNIROUTE_NO_UPDATE_NOTIFIER",
|
||||
// Platform / OS detection vars read by CLI environment helper (bin/cli/utils/environment.mjs).
|
||||
// These are external signals set by the host OS or cloud provider — not OmniRoute config.
|
||||
"CODESPACES",
|
||||
"GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN",
|
||||
"GITPOD_WORKSPACE_ID",
|
||||
"NO_COLOR",
|
||||
"REPL_ID",
|
||||
"REPL_SLUG",
|
||||
"WSL_DISTRO_NAME",
|
||||
"WSL_INTEROP",
|
||||
// X11/Wayland display server vars used by tray heuristic (isTraySupported).
|
||||
"DISPLAY",
|
||||
"WAYLAND_DISPLAY",
|
||||
// Build-time override for OpenAPI spec path used by generate-api-commands.mjs.
|
||||
"OPENAPI_SPEC",
|
||||
// Aliases for documented vars handled via fallback ordering.
|
||||
"API_KEY",
|
||||
"APP_URL",
|
||||
|
||||
Reference in New Issue
Block a user