Spaces:
Runtime error
Runtime error
File size: 4,261 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | 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 <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;
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 <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);
});
}
|