mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 21:22:28 +03:00
- 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
135 lines
4.2 KiB
JavaScript
135 lines
4.2 KiB
JavaScript
import { apiFetch } from "../api.mjs";
|
|
import { loadContexts, saveContextsSecure } from "../contexts.mjs";
|
|
import { createPrompt, printSuccess, printError, printInfo } from "../io.mjs";
|
|
import { t } from "../i18n.mjs";
|
|
|
|
/**
|
|
* `omniroute connect <host>` — remote mode.
|
|
*
|
|
* Logs into a remote OmniRoute server and saves the result as the active context
|
|
* so every subsequent command targets that server. Two flows:
|
|
* - password: prompts for the management password → POST /api/cli/connect →
|
|
* server mints a scoped access token (default scope: admin).
|
|
* - token: `--key <oma_...>` validates via GET /api/cli/whoami and saves it.
|
|
*/
|
|
|
|
/** Normalize a host/URL into a server root baseUrl (no trailing path). */
|
|
export function normalizeBaseUrl(host, port) {
|
|
let value = String(host || "").trim();
|
|
if (!value) return "";
|
|
const hadScheme = /^https?:\/\//i.test(value);
|
|
if (!hadScheme) value = `http://${value}`;
|
|
try {
|
|
const u = new URL(value);
|
|
// Only apply the default port to a bare host; a full URL is taken as-is.
|
|
if (!hadScheme && !u.port && port) u.port = String(port);
|
|
return u.origin;
|
|
} catch {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
/** Derive a clean context name from a host (strip scheme/port). */
|
|
export function hostLabel(host) {
|
|
let value = String(host || "")
|
|
.trim()
|
|
.replace(/^https?:\/\//i, "");
|
|
value = value.split("/")[0].split(":")[0];
|
|
return value || "remote";
|
|
}
|
|
|
|
async function readErrorMessage(res) {
|
|
try {
|
|
const body = await res.json();
|
|
return body?.error?.message || body?.error || `HTTP ${res.status}`;
|
|
} catch {
|
|
return `HTTP ${res.status}`;
|
|
}
|
|
}
|
|
|
|
export async function runConnectCommand(host, opts = {}) {
|
|
const baseUrl = normalizeBaseUrl(host, opts.port || "20128");
|
|
if (!baseUrl) {
|
|
printError("A host is required, e.g. omniroute connect 192.168.0.15");
|
|
return 2;
|
|
}
|
|
const name = opts.name || hostLabel(host);
|
|
|
|
let accessToken;
|
|
let scope;
|
|
|
|
if (opts.key) {
|
|
// Validate the pasted token against the remote.
|
|
const res = await apiFetch("/api/cli/whoami", {
|
|
baseUrl,
|
|
apiKey: opts.key,
|
|
acceptNotOk: true,
|
|
});
|
|
if (!res.ok) {
|
|
printError(`Token rejected by ${baseUrl}: ${await readErrorMessage(res)}`);
|
|
return res.exitCode || 1;
|
|
}
|
|
const body = await res.json();
|
|
accessToken = opts.key;
|
|
scope = body.scope || "unknown";
|
|
} else {
|
|
const prompt = createPrompt();
|
|
let password;
|
|
try {
|
|
password = await prompt.askSecret(`Management password for ${baseUrl}`);
|
|
} finally {
|
|
prompt.close();
|
|
}
|
|
if (!password) {
|
|
printError("Password is required (or use --key <token>).");
|
|
return 2;
|
|
}
|
|
const res = await apiFetch("/api/cli/connect", {
|
|
baseUrl,
|
|
method: "POST",
|
|
body: { password, name, scope: opts.scope },
|
|
acceptNotOk: true,
|
|
retry: false,
|
|
});
|
|
if (!res.ok) {
|
|
printError(`Connect failed (${res.status}): ${await readErrorMessage(res)}`);
|
|
return res.exitCode || 1;
|
|
}
|
|
const body = await res.json();
|
|
accessToken = body.token;
|
|
scope = body.scope;
|
|
}
|
|
|
|
const cfg = loadContexts();
|
|
cfg.contexts = cfg.contexts || {};
|
|
cfg.contexts[name] = {
|
|
baseUrl,
|
|
accessToken,
|
|
scope,
|
|
description: `Remote OmniRoute (${host})`,
|
|
};
|
|
cfg.currentContext = name;
|
|
await saveContextsSecure(cfg);
|
|
|
|
printSuccess(`Connected to ${baseUrl} — context '${name}' (scope: ${scope})`);
|
|
printInfo("All commands now target this server.");
|
|
printInfo("Switch back to local with: omniroute contexts use default");
|
|
return 0;
|
|
}
|
|
|
|
export function registerConnect(program) {
|
|
program
|
|
.command("connect <host>")
|
|
.description(
|
|
t("connect.description") || "Connect to a remote OmniRoute server and enter remote mode"
|
|
)
|
|
.option("--port <port>", "Server port when the host has none", "20128")
|
|
.option("--key <token>", "Use a pre-generated scoped access token (skips the password prompt)")
|
|
.option("--name <name>", "Context name to save (default: derived from host)")
|
|
.option("--scope <scope>", "Requested scope for the password flow (read|write|admin)")
|
|
.action(async (host, opts) => {
|
|
const code = await runConnectCommand(host, opts);
|
|
if (code !== 0) process.exit(code);
|
|
});
|
|
}
|