mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 02:42:24 +03:00
Landed with the design call resolved per the owner's pick — **option 1**: the synced store is now endpoint-agnostic (persistDiscoveredModels and managedModelImport no longer drop non-chat models at write time), and chat selectability moved to read time (auto-pool expansion in autoStrategy applies filterChatSelectableModels; the models-route projection already had its chatOnly filter). Your discovery test now passes end-to-end (3/3): /api/show capabilities persist per connection and image/embedding requests route through the advertising host. Reconciliation notes: conflicted areas merged onto the current tip (adobe discovery import, requestedModel preflight signature, resolvedProvider fast-path coexists with the synced-route override — explicit resolution wins); carried base-red drains (#10055 memoization, #11071 test variants) dropped as already-landed; the managed-model-import exclusion test was propagated to the new contract (image/video models persist; the read filter still hides them from chat pickers — pinned by a new assertion). Full battery: 205/206 focused (the one red is a confirmed periodic-timer timing flake on the loaded devbox — 20/20 isolated), autoCombo vitest 30/30, combo suites 46/46, gates + typecheck clean. Thank you @yourspraveen — the capability probe + routing design was right; it just needed the store contract opened up. Fixes #11087.
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);
|
|
});
|
|
}
|