feat(cli): relay-like CLI closure — target manifest, Codex TOML, Gemini launcher, guards

- canonical executable manifest (bin/cli/cli-manifest.mjs): run/configure/completion
  derive targets, aliases and --model wiring from one table; drift test cross-checks
  manifest x cliRuntime x UI catalog (tests/unit/cli/cli-manifest-drift.test.ts)
- dashboard Codex generator converged to ~/.codex/config.toml (modern Codex v0.137+,
  verified against codex-cli 0.147.0): conservative merge, env_key auth (key never
  written), refuses invalid TOML, reports legacy config.yaml as migration note
- omniroute run gemini: launcher over OmniRoute's /v1beta surface via
  GOOGLE_GEMINI_BASE_URL + isolated GEMINI_CLI_HOME forcing gemini-api-key auth
  (contract proven against @google/gemini-cli 0.50.0); ACP registration kept distinct
- opt-in real smoke harness for upstream CLIs (RUN_CLI_SMOKE=1, credential by env
  NAME, redacted output): tests/integration/upstream-cli-smoke.int.test.ts
- container-guard homologation for POST /api/cli-tools/apply (422 in container,
  dry-run preview allowed, host write passes) + docs; guard untouched
- typecheck: omniglyphAdapter union narrowing, usageTracking typed signatures
  (UsageLike, no any), models.ts isValidModel params — typecheck:core and
  typecheck:noimplicit:core now clean
- relay core (prior session of this effort): omniroute run for 6 CLIs, configure
  picker with per-context favorites/recents, contexts with optional keychain +
  0600 fallback, provider CRUD with recursive redaction, completion updates, docs
This commit is contained in:
Xiangzhe
2026-08-18 08:24:05 -03:00
parent 34bb018d21
commit 0a74bfbdea
50 changed files with 3499 additions and 284 deletions

View File

@@ -198,6 +198,34 @@ const CLI_TOOLS: Record<string, any> = {
env: ".qwen/.env",
},
},
aider: {
defaultCommand: "aider",
envBinKey: "CLI_AIDER_BIN",
requiresBinary: true,
healthcheckTimeoutMs: 12000,
paths: {
config: ".aider.conf.yml",
},
},
goose: {
defaultCommand: "goose",
envBinKey: "CLI_GOOSE_BIN",
requiresBinary: true,
healthcheckTimeoutMs: 12000,
paths: {
config: ".config/goose/config.yaml",
},
},
gemini: {
defaultCommand: "gemini",
envBinKey: "CLI_GEMINI_BIN",
requiresBinary: true,
// gemini-cli cold start (bundle + extension discovery) can exceed 4s.
healthcheckTimeoutMs: 15000,
paths: {
settings: ".gemini/settings.json",
},
},
// ── Plan 14 — new "custom" configType tools ───────────────────────────────
forge: {
defaultCommand: "forge",
@@ -286,6 +314,33 @@ const CLI_TOOLS: Record<string, any> = {
},
};
/**
* Compatibility aliases accepted by CLI/API callers.
*
* The runtime catalog keeps one canonical id per executable. Older surfaces
* exposed a binary name (notably `kilocode`) or launcher aliases instead of
* that id, so normalize them at the boundary rather than duplicating entries.
*/
export const CLI_TOOL_ALIASES: Readonly<Record<string, string>> = {
kilocode: "kilo",
"kilo-code": "kilo",
kilo_cli: "kilo",
cc: "claude",
"claude-code": "claude",
"openai-codex": "codex",
openai: "codex",
cn: "continue",
qodercli: "qoder",
};
/** Resolve a user-facing or legacy id to the canonical runtime id. */
export const normalizeCliToolId = (toolId: string): string => {
const normalized = String(toolId || "")
.trim()
.toLowerCase();
return CLI_TOOL_ALIASES[normalized] || normalized;
};
const isWindows = () => process.platform === "win32";
/**
@@ -568,6 +623,7 @@ const getExtraPaths = () =>
* Works on all platforms — Windows checks .cmd wrappers, Linux/macOS checks bare names.
*/
export const getKnownToolPaths = (toolId: string): string[] => {
toolId = normalizeCliToolId(toolId);
const home = os.homedir();
const paths: string[] = [];
@@ -730,7 +786,7 @@ export const getLookupEnv = () => {
};
const resolveToolCommands = (toolId: string): string[] => {
const tool = CLI_TOOLS[toolId];
const tool = CLI_TOOLS[normalizeCliToolId(toolId)];
if (!tool) return [];
const envCommand = String(process.env[tool.envBinKey] || "").trim();
if (envCommand) return [envCommand];
@@ -740,6 +796,16 @@ const resolveToolCommands = (toolId: string): string[] => {
return tool.defaultCommand ? [tool.defaultCommand] : [];
};
/**
* Return command candidates without probing the filesystem.
*
* Lightweight consumers (config status and CLI inventory) use this to build
* a version probe while getCliRuntimeStatus() remains the authoritative
* health/runnability check.
*/
export const getCliToolCommandCandidates = (toolId: string): string[] =>
resolveToolCommands(toolId);
const checkExplicitPath = async (commandPath: string) => {
// Reject paths that look like injection attempts
if (!isSafePath(commandPath)) {
@@ -781,13 +847,13 @@ export const locateCommand = async (command: string, env: Record<string, string
// and a .cmd wrapper. We must prefer the Windows executable extension.
const lines = located.stdout
.split(/\r?\n/)
.map((l) => l.trim())
.map((l: string) => l.trim())
.filter(Boolean);
if (lines.length === 0) {
return { installed: false, commandPath: null, reason: "not_found" };
}
const winExt = /\.(cmd|exe|bat|com)$/i;
const preferred = lines.find((l) => winExt.test(l)) || lines[0];
const preferred = lines.find((l: string) => winExt.test(l)) || lines[0];
return { installed: true, commandPath: normalizeMsys2Path(preferred), reason: null };
}
return { installed: false, commandPath: null, reason: "not_found" };
@@ -1025,6 +1091,7 @@ export const resolveOpencodeConfigPath = (
export const getOpenCodeConfigPath = () => resolveOpencodeConfigPath();
export const getCliConfigPaths = (toolId: string) => {
toolId = normalizeCliToolId(toolId);
const tool = CLI_TOOLS[toolId];
if (!tool) return null;
@@ -1071,6 +1138,7 @@ export const getCliPrimaryConfigPath = (toolId: string) => {
};
export const getCliRuntimeStatus = async (toolId: string) => {
toolId = normalizeCliToolId(toolId);
const tool = CLI_TOOLS[toolId];
const runtimeMode = getRuntimeMode();
if (!tool) {