import { apiFetch } from "../api.mjs"; import { loadContexts, saveContexts } from "../contexts.mjs"; import { createPrompt, printSuccess, printError, printInfo } from "../io.mjs"; import { t } from "../i18n.mjs"; /** * `omniroute connect ` — 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 ` 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 )."); 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; saveContexts(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 ") .description( t("connect.description") || "Connect to a remote OmniRoute server and enter remote mode" ) .option("--port ", "Server port when the host has none", "20128") .option("--key ", "Use a pre-generated scoped access token (skips the password prompt)") .option("--name ", "Context name to save (default: derived from host)") .option("--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); }); }