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);
    });
}