@marcusrbrown/infra 0.13.13 → 0.13.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -6949,7 +6949,7 @@ import { goke } from "goke";
6949
6949
  // package.json
6950
6950
  var package_default = {
6951
6951
  name: "@marcusrbrown/infra",
6952
- version: "0.13.13",
6952
+ version: "0.13.15",
6953
6953
  description: "Infrastructure management CLI \u2014 deploy automation, health checks, and MCP bridge",
6954
6954
  keywords: [
6955
6955
  "infra",
@@ -6985,7 +6985,7 @@ var package_default = {
6985
6985
  },
6986
6986
  dependencies: {
6987
6987
  "@clack/prompts": "^1.2.0",
6988
- "@goke/mcp": "^0.0.10",
6988
+ "@goke/mcp": "^0.0.11",
6989
6989
  goke: "^6.3.2",
6990
6990
  "string-dedent": "^3.0.2",
6991
6991
  zod: "^4.3.6"
@@ -7004,6 +7004,329 @@ var package_default = {
7004
7004
  }
7005
7005
  };
7006
7006
 
7007
+ // src/commands/broker/deploy.ts
7008
+ import { existsSync as existsSync2 } from "fs";
7009
+ import { join as join2 } from "path";
7010
+ import { z } from "zod";
7011
+
7012
+ // src/lib/repo-root.ts
7013
+ import { existsSync, readFileSync } from "fs";
7014
+ import { dirname, join } from "path";
7015
+ var WORKSPACE_MARKER_NAME = "@marcusrbrown/infra-workspace";
7016
+ function findRepoRoot(start) {
7017
+ let current = start ?? import.meta.dir;
7018
+ while (true) {
7019
+ const candidate = join(current, "package.json");
7020
+ if (existsSync(candidate)) {
7021
+ try {
7022
+ const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
7023
+ if (pkg.name === WORKSPACE_MARKER_NAME) {
7024
+ return current;
7025
+ }
7026
+ } catch {}
7027
+ }
7028
+ const parent = dirname(current);
7029
+ if (parent === current) {
7030
+ break;
7031
+ }
7032
+ current = parent;
7033
+ }
7034
+ throw new Error(`Workspace root not found: could not locate a package.json with name "${WORKSPACE_MARKER_NAME}" ` + `in any ancestor of "${start ?? import.meta.dir}". ` + `These commands require a source checkout of the marcusrbrown/infra monorepo.`);
7035
+ }
7036
+
7037
+ // src/commands/broker/deploy.ts
7038
+ var REPO = "marcusrbrown/infra";
7039
+ var WORKFLOW_NAME = "Deploy Broker";
7040
+ var WORKFLOW_URL = "https://github.com/marcusrbrown/infra/actions/workflows/deploy-broker.yaml";
7041
+ function resolveLocalDeployScriptPath() {
7042
+ return join2(findRepoRoot(), "apps", "broker", "src", "deploy.ts");
7043
+ }
7044
+ function getLocalDeployEnv() {
7045
+ const path = process.env.PATH;
7046
+ const home = process.env.HOME;
7047
+ const sshAuthSock = process.env.SSH_AUTH_SOCK;
7048
+ if (!path) {
7049
+ throw new Error("PATH is required for local deploy");
7050
+ }
7051
+ if (!home) {
7052
+ throw new Error("HOME is required for local deploy");
7053
+ }
7054
+ if (!sshAuthSock) {
7055
+ throw new Error("SSH_AUTH_SOCK is required for local deploy. Start ssh-agent and load your deploy key first.");
7056
+ }
7057
+ return {
7058
+ PATH: path,
7059
+ HOME: home,
7060
+ SSH_AUTH_SOCK: sshAuthSock,
7061
+ BROKER_DOMAIN: process.env.BROKER_DOMAIN ?? "",
7062
+ BROKER_HOST: process.env.BROKER_HOST ?? "",
7063
+ CLIPROXY_MANAGEMENT_KEY: process.env.CLIPROXY_MANAGEMENT_KEY ?? "",
7064
+ BROKER_AUD: process.env.BROKER_AUD ?? ""
7065
+ };
7066
+ }
7067
+ function validateLocalPreconditions() {
7068
+ const deployScriptPath = resolveLocalDeployScriptPath();
7069
+ if (!existsSync2(deployScriptPath)) {
7070
+ throw new Error(`Local deploy script not found at expected path: ${deployScriptPath}`);
7071
+ }
7072
+ getLocalDeployEnv();
7073
+ return { deployScriptPath };
7074
+ }
7075
+ function validateRemotePreconditions() {
7076
+ if (!Bun.which("gh")) {
7077
+ throw new Error("gh CLI is required for remote deploy. Install gh and run `gh auth login`.");
7078
+ }
7079
+ }
7080
+ function registerBrokerDeploy(cli) {
7081
+ cli.command("broker deploy", "Deploy the credential broker. Default mode triggers the GitHub Deploy Broker workflow, while --local runs apps/broker/src/deploy.ts directly with Bun.").option("--local", z.boolean().default(false).describe("Run local deployment with Bun using apps/broker/src/deploy.ts instead of triggering GitHub Actions.")).option("--dry-run", z.boolean().default(false).describe("Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.")).example("# Trigger remote GitHub Actions deploy (default mode)").example("infra broker deploy").example("# Validate local deploy preconditions and planned command").example("infra broker deploy --local --dry-run").example("# Run local deploy with explicit SSH agent context").example("infra broker deploy --local").action(async (options) => {
7082
+ if (options.local) {
7083
+ const { deployScriptPath } = validateLocalPreconditions();
7084
+ const env = getLocalDeployEnv();
7085
+ const command = ["bun", deployScriptPath];
7086
+ if (options.dryRun) {
7087
+ console.log("Dry run: local broker deploy");
7088
+ console.log(`- deploy script: ${deployScriptPath}`);
7089
+ console.log(`- command: ${command.join(" ")}`);
7090
+ console.log(`- BROKER_DOMAIN=${env.BROKER_DOMAIN || "(unset)"}`);
7091
+ return;
7092
+ }
7093
+ const child2 = Bun.spawn(command, {
7094
+ env,
7095
+ stdout: "inherit",
7096
+ stderr: "inherit"
7097
+ });
7098
+ const exitCode2 = await child2.exited;
7099
+ if (exitCode2 !== 0) {
7100
+ throw new Error(`Local deploy failed with exit code ${exitCode2}`);
7101
+ }
7102
+ return;
7103
+ }
7104
+ validateRemotePreconditions();
7105
+ if (options.dryRun) {
7106
+ console.log("Dry run: remote broker deploy");
7107
+ console.log(`- command: gh workflow run "${WORKFLOW_NAME}" --repo ${REPO}`);
7108
+ console.log(`- workflow URL: ${WORKFLOW_URL}`);
7109
+ return;
7110
+ }
7111
+ const child = Bun.spawn(["gh", "workflow", "run", WORKFLOW_NAME, "--repo", REPO], {
7112
+ stdout: "inherit",
7113
+ stderr: "inherit"
7114
+ });
7115
+ const exitCode = await child.exited;
7116
+ if (exitCode !== 0) {
7117
+ throw new Error(`Failed to trigger "${WORKFLOW_NAME}" workflow (exit code ${exitCode})`);
7118
+ }
7119
+ console.log(`Workflow triggered: ${WORKFLOW_URL}`);
7120
+ console.log("Approve the broker environment deployment in GitHub Actions to continue.");
7121
+ });
7122
+ }
7123
+
7124
+ // src/commands/broker/logs.ts
7125
+ import { z as z2 } from "zod";
7126
+
7127
+ // src/lib/known-hosts.ts
7128
+ import { existsSync as existsSync3 } from "fs";
7129
+ import { join as join3, resolve } from "path";
7130
+
7131
+ // src/resources/known_hosts
7132
+ var known_hosts_default = "./known_hosts-c3ns9jne.";
7133
+
7134
+ // src/lib/known-hosts.ts
7135
+ var FAIL_CLOSED_ERROR = "Pinned SSH known_hosts file not found; reinstall @marcusrbrown/infra or run from the repo checkout";
7136
+ function resolveKnownHostsPath(libDir) {
7137
+ const dir = libDir ?? import.meta.dir;
7138
+ const repoRoot = resolve(dir, "..", "..", "..", "..");
7139
+ const repoCandidate = join3(repoRoot, ".github", "known_hosts");
7140
+ if (existsSync3(repoCandidate))
7141
+ return repoCandidate;
7142
+ if (existsSync3(known_hosts_default))
7143
+ return known_hosts_default;
7144
+ throw new Error(FAIL_CLOSED_ERROR);
7145
+ }
7146
+ function buildKnownHostsArgs(libDir) {
7147
+ const path = resolveKnownHostsPath(libDir);
7148
+ return ["-o", `UserKnownHostsFile=${path}`];
7149
+ }
7150
+
7151
+ // src/commands/broker/host.ts
7152
+ var VALID_HOST_RE = /^[a-z\d][a-z\d.\-]*$/i;
7153
+ function validateBrokerHost(host) {
7154
+ if (!host) {
7155
+ throw new Error("Invalid BROKER_HOST: value is empty");
7156
+ }
7157
+ if (!VALID_HOST_RE.test(host)) {
7158
+ const excerpt = host.slice(0, 30).replaceAll(/[^\u0020-\u007E]/g, "?");
7159
+ throw new Error(`Invalid BROKER_HOST: "${excerpt}" \u2014 must match ${String.raw`[A-Za-z0-9][A-Za-z0-9.\-]*`}`);
7160
+ }
7161
+ return host;
7162
+ }
7163
+
7164
+ // src/commands/broker/logs.ts
7165
+ var SENSITIVE_WARNING = "Warning: Logs may contain run identities, minted key prefixes, or other sensitive broker information. Treat output as sensitive; do not capture in shared logs or chat.";
7166
+ function defaultLogsSpawn(cmd, opts) {
7167
+ return Bun.spawn(cmd, opts);
7168
+ }
7169
+ async function streamBrokerLogs(opts, spawn = defaultLogsSpawn, printOut, printErr) {
7170
+ const isCI = process.env.CI === "true";
7171
+ if (isCI && !opts.allowCi) {
7172
+ const msg = "Refusing to stream logs in CI without --allow-ci. Logs may contain sensitive broker data.";
7173
+ if (printOut) {
7174
+ printOut(msg);
7175
+ } else {
7176
+ console.log(msg);
7177
+ }
7178
+ return { refused: true };
7179
+ }
7180
+ if (printErr) {
7181
+ printErr(SENSITIVE_WARNING);
7182
+ } else {
7183
+ console.error(SENSITIVE_WARNING);
7184
+ }
7185
+ validateBrokerHost(opts.host);
7186
+ const sshCmd = [
7187
+ "ssh",
7188
+ "-o",
7189
+ "BatchMode=yes",
7190
+ "-o",
7191
+ "StrictHostKeyChecking=yes",
7192
+ ...buildKnownHostsArgs(),
7193
+ `root@${opts.host}`,
7194
+ `docker compose -f /opt/broker/docker-compose.yaml logs ${opts.service} --no-log-prefix --tail ${opts.tail}`
7195
+ ];
7196
+ const env = {
7197
+ PATH: process.env.PATH ?? "/usr/bin:/bin",
7198
+ HOME: process.env.HOME ?? "/tmp",
7199
+ ...process.env.SSH_AUTH_SOCK ? { SSH_AUTH_SOCK: process.env.SSH_AUTH_SOCK } : {}
7200
+ };
7201
+ const child = spawn(sshCmd, { env, stdout: "inherit", stderr: "inherit" });
7202
+ const exitCode = await child.exited;
7203
+ return { refused: false, exitCode };
7204
+ }
7205
+ function registerBrokerLogs(cli) {
7206
+ cli.command("broker logs", "Stream credential broker service logs via SSH.").option("--tail [n]", z2.number().default(100).describe("Number of log lines to tail.")).option("--service [service]", z2.string().default("broker").describe("Docker Compose service name to stream logs from.")).option("--allow-ci", z2.boolean().default(false).describe("Allow log streaming in CI environments. Logs may contain sensitive broker data.")).option("--key [key]", z2.string().describe("Environment variable name holding the SSH host. Falls back to BROKER_HOST when omitted.")).action(async (options) => {
7207
+ const hostEnvKey = options.key ?? "BROKER_HOST";
7208
+ const host = process.env[hostEnvKey] ?? process.env.BROKER_DOMAIN;
7209
+ if (!host) {
7210
+ console.error(`Broker host not set. Export ${hostEnvKey} or pass --key <env-name> pointing to a set variable.`);
7211
+ process.exitCode = 1;
7212
+ return;
7213
+ }
7214
+ const tail = typeof options.tail === "number" ? options.tail : 100;
7215
+ const service = typeof options.service === "string" ? options.service : "broker";
7216
+ const allowCi = options.allowCi === true;
7217
+ const result = await streamBrokerLogs({ host, tail, service, allowCi });
7218
+ if (result.refused) {
7219
+ process.exitCode = 1;
7220
+ return;
7221
+ }
7222
+ if (result.exitCode !== 0) {
7223
+ process.exitCode = result.exitCode ?? 1;
7224
+ }
7225
+ });
7226
+ }
7227
+
7228
+ // src/commands/broker/status.ts
7229
+ import { z as z3 } from "zod";
7230
+ var DEFAULT_BROKER_URL = "https://broker.fro.bot";
7231
+ var HTTP_TIMEOUT_MS = 1e4;
7232
+ async function checkBrokerHealth(baseUrl) {
7233
+ const url = `${baseUrl}/healthz`;
7234
+ const startedAt = performance.now();
7235
+ try {
7236
+ const response = await fetch(url, {
7237
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
7238
+ });
7239
+ const elapsedMs = Math.max(0, Math.round(performance.now() - startedAt));
7240
+ return {
7241
+ ok: response.ok,
7242
+ summary: `GET ${url} \u2192 ${response.status} (${elapsedMs}ms)`
7243
+ };
7244
+ } catch (error) {
7245
+ const elapsedMs = Math.max(0, Math.round(performance.now() - startedAt));
7246
+ const message = error instanceof Error ? error.message : String(error);
7247
+ return {
7248
+ ok: false,
7249
+ summary: `Request failed after ${elapsedMs}ms: ${message}`
7250
+ };
7251
+ }
7252
+ }
7253
+ async function getBrokerStatusSummary(host) {
7254
+ if (!host) {
7255
+ return {
7256
+ app: "broker",
7257
+ http: "\u2014 (host not set)",
7258
+ lastDeploy: "\u2014",
7259
+ version: "\u2014",
7260
+ contentHash: "\u2014",
7261
+ usageStats: "\u2014"
7262
+ };
7263
+ }
7264
+ const baseUrl = `https://${host}`;
7265
+ const health = await checkBrokerHealth(baseUrl);
7266
+ return {
7267
+ app: "broker",
7268
+ http: health.ok ? `OK: ${health.summary}` : `ERROR: ${health.summary}`,
7269
+ lastDeploy: "\u2014",
7270
+ version: "\u2014",
7271
+ contentHash: "\u2014",
7272
+ usageStats: "\u2014"
7273
+ };
7274
+ }
7275
+ async function brokerStatusAction(options, ctx) {
7276
+ let errorCount = 0;
7277
+ try {
7278
+ const hostEnvKey = options.key ?? "BROKER_HOST";
7279
+ const hostFromEnv = process.env[hostEnvKey] ?? process.env.BROKER_DOMAIN ?? process.env.BROKER_HOST;
7280
+ const baseUrl = options.url ?? hostFromEnv ?? DEFAULT_BROKER_URL;
7281
+ if (!options.url && !hostFromEnv) {
7282
+ ctx.console.log("Broker status");
7283
+ ctx.console.log("");
7284
+ ctx.console.error("[WARN] Broker host not set. Export BROKER_DOMAIN or BROKER_HOST, or pass --url. Skipping health check.");
7285
+ ctx.process.exit(1);
7286
+ return;
7287
+ }
7288
+ try {
7289
+ const urlObj = new URL(baseUrl.startsWith("http") ? baseUrl : `https://${baseUrl}`);
7290
+ validateBrokerHost(urlObj.hostname);
7291
+ } catch (error) {
7292
+ ctx.console.error(error instanceof Error ? error.message : String(error));
7293
+ ctx.process.exit(1);
7294
+ return;
7295
+ }
7296
+ ctx.console.log("Broker status");
7297
+ ctx.console.log("");
7298
+ const health = await checkBrokerHealth(baseUrl.startsWith("http") ? baseUrl : `https://${baseUrl}`);
7299
+ if (health.ok) {
7300
+ ctx.console.log(`[OK] HTTP reachability`);
7301
+ ctx.console.log(` ${health.summary}`);
7302
+ } else {
7303
+ ctx.console.log(`[ERROR] HTTP reachability`);
7304
+ ctx.console.log(` ${health.summary}`);
7305
+ errorCount++;
7306
+ }
7307
+ ctx.console.log("");
7308
+ ctx.console.log(`Summary: 1 check, ${errorCount} errors, 0 warnings`);
7309
+ } catch (error) {
7310
+ const message = error instanceof Error ? error.message : String(error);
7311
+ ctx.console.error(message);
7312
+ ctx.process.exit(1);
7313
+ return;
7314
+ }
7315
+ if (errorCount > 0) {
7316
+ ctx.process.exit(1);
7317
+ }
7318
+ }
7319
+ function registerBrokerStatus(cli) {
7320
+ cli.command("broker status", "Show operational health of the credential broker.").option("--url [url]", z3.string().describe("Base URL for broker health checks. Falls back to BROKER_DOMAIN, BROKER_HOST, or https://broker.fro.bot.")).option("--key [key]", z3.string().describe("Environment variable name holding the broker host. Falls back to BROKER_DOMAIN when omitted.")).option("--verbose", "Enable verbose output for all commands").action(brokerStatusAction);
7321
+ }
7322
+
7323
+ // src/commands/broker/index.ts
7324
+ function registerBrokerCommands(cli) {
7325
+ registerBrokerStatus(cli);
7326
+ registerBrokerDeploy(cli);
7327
+ registerBrokerLogs(cli);
7328
+ }
7329
+
7007
7330
  // src/commands/cliproxy/config.ts
7008
7331
  import { chmodSync } from "fs";
7009
7332
 
@@ -7054,7 +7377,7 @@ var $visit = visit.visit;
7054
7377
  var $visitAsync = visit.visitAsync;
7055
7378
 
7056
7379
  // ../shared/cliproxy/management.ts
7057
- var HTTP_TIMEOUT_MS = 1e4;
7380
+ var HTTP_TIMEOUT_MS2 = 1e4;
7058
7381
  function toStringArray(payload) {
7059
7382
  if (Array.isArray(payload)) {
7060
7383
  return payload.filter((item) => typeof item === "string");
@@ -7093,7 +7416,7 @@ function managementHeaders(key) {
7093
7416
  async function requestJson(endpoint, init) {
7094
7417
  const response = await fetch(endpoint, {
7095
7418
  ...init,
7096
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
7419
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS2)
7097
7420
  });
7098
7421
  if (!response.ok) {
7099
7422
  const body = await response.text();
@@ -7110,7 +7433,7 @@ async function requestJson(endpoint, init) {
7110
7433
  }
7111
7434
 
7112
7435
  // src/commands/cliproxy/config.ts
7113
- import { z } from "zod";
7436
+ import { z as z4 } from "zod";
7114
7437
  var DEFAULT_CLIPROXY_URL = "https://cliproxy.fro.bot";
7115
7438
  function stripTrailingSlash(value) {
7116
7439
  return value.endsWith("/") ? value.slice(0, -1) : value;
@@ -7242,48 +7565,21 @@ async function cliproxyConfigSetAction(field, value, options, ctx) {
7242
7565
  }
7243
7566
  }
7244
7567
  function registerCliproxyConfig(cli) {
7245
- cli.command("cliproxy config get", "Fetch current CLIProxyAPI config as JSON.").option("--url [url]", z.string().describe("Base URL for CLIProxyAPI management requests. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.")).option("--key [key]", z.string().describe("Management API key. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.")).option("--output [file]", z.string().describe("Write config JSON to a file instead of stdout. File permissions set to 0600 (owner-read-only).")).option("--json", "Output raw JSON instead of aligned key: value columns.").action(cliproxyConfigGetAction);
7246
- cli.command("cliproxy config set <field> <value>", "Update a mutable CLIProxyAPI config field via its dedicated management endpoint.").option("--url [url]", z.string().describe("Base URL for CLIProxyAPI management requests. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.")).option("--key [key]", z.string().describe("Management API key. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.")).example("infra cliproxy config set debug true").example("infra cliproxy config set request-retry 5").example("infra cliproxy config set proxy-url https://proxy.example.com").action(cliproxyConfigSetAction);
7568
+ cli.command("cliproxy config get", "Fetch current CLIProxyAPI config as JSON.").option("--url [url]", z4.string().describe("Base URL for CLIProxyAPI management requests. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.")).option("--key [key]", z4.string().describe("Management API key. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.")).option("--output [file]", z4.string().describe("Write config JSON to a file instead of stdout. File permissions set to 0600 (owner-read-only).")).option("--json", "Output raw JSON instead of aligned key: value columns.").action(cliproxyConfigGetAction);
7569
+ cli.command("cliproxy config set <field> <value>", "Update a mutable CLIProxyAPI config field via its dedicated management endpoint.").option("--url [url]", z4.string().describe("Base URL for CLIProxyAPI management requests. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.")).option("--key [key]", z4.string().describe("Management API key. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.")).example("infra cliproxy config set debug true").example("infra cliproxy config set request-retry 5").example("infra cliproxy config set proxy-url https://proxy.example.com").action(cliproxyConfigSetAction);
7247
7570
  }
7248
7571
 
7249
7572
  // src/commands/cliproxy/deploy.ts
7250
- import { existsSync as existsSync2 } from "fs";
7251
- import { join as join2 } from "path";
7252
- import { z as z2 } from "zod";
7253
-
7254
- // src/lib/repo-root.ts
7255
- import { existsSync, readFileSync } from "fs";
7256
- import { dirname, join } from "path";
7257
- var WORKSPACE_MARKER_NAME = "@marcusrbrown/infra-workspace";
7258
- function findRepoRoot(start) {
7259
- let current = start ?? import.meta.dir;
7260
- while (true) {
7261
- const candidate = join(current, "package.json");
7262
- if (existsSync(candidate)) {
7263
- try {
7264
- const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
7265
- if (pkg.name === WORKSPACE_MARKER_NAME) {
7266
- return current;
7267
- }
7268
- } catch {}
7269
- }
7270
- const parent = dirname(current);
7271
- if (parent === current) {
7272
- break;
7273
- }
7274
- current = parent;
7275
- }
7276
- throw new Error(`Workspace root not found: could not locate a package.json with name "${WORKSPACE_MARKER_NAME}" ` + `in any ancestor of "${start ?? import.meta.dir}". ` + `These commands require a source checkout of the marcusrbrown/infra monorepo.`);
7277
- }
7278
-
7279
- // src/commands/cliproxy/deploy.ts
7280
- var REPO = "marcusrbrown/infra";
7281
- var WORKFLOW_NAME = "Deploy CLIProxy";
7282
- var WORKFLOW_URL = "https://github.com/marcusrbrown/infra/actions/workflows/deploy-cliproxy.yaml";
7283
- function resolveLocalDeployScriptPath() {
7284
- return join2(findRepoRoot(), "apps", "cliproxy", "src", "deploy.ts");
7573
+ import { existsSync as existsSync4 } from "fs";
7574
+ import { join as join4 } from "path";
7575
+ import { z as z5 } from "zod";
7576
+ var REPO2 = "marcusrbrown/infra";
7577
+ var WORKFLOW_NAME2 = "Deploy CLIProxy";
7578
+ var WORKFLOW_URL2 = "https://github.com/marcusrbrown/infra/actions/workflows/deploy-cliproxy.yaml";
7579
+ function resolveLocalDeployScriptPath2() {
7580
+ return join4(findRepoRoot(), "apps", "cliproxy", "src", "deploy.ts");
7285
7581
  }
7286
- function getLocalDeployEnv() {
7582
+ function getLocalDeployEnv2() {
7287
7583
  const path = process.env.PATH;
7288
7584
  const home = process.env.HOME;
7289
7585
  const sshAuthSock = process.env.SSH_AUTH_SOCK;
@@ -7304,24 +7600,24 @@ function getLocalDeployEnv() {
7304
7600
  CLIPROXY_MANAGEMENT_KEY: process.env.CLIPROXY_MANAGEMENT_KEY ?? ""
7305
7601
  };
7306
7602
  }
7307
- function validateLocalPreconditions() {
7308
- const deployScriptPath = resolveLocalDeployScriptPath();
7309
- if (!existsSync2(deployScriptPath)) {
7603
+ function validateLocalPreconditions2() {
7604
+ const deployScriptPath = resolveLocalDeployScriptPath2();
7605
+ if (!existsSync4(deployScriptPath)) {
7310
7606
  throw new Error(`Local deploy script not found at expected path: ${deployScriptPath}`);
7311
7607
  }
7312
- getLocalDeployEnv();
7608
+ getLocalDeployEnv2();
7313
7609
  return { deployScriptPath };
7314
7610
  }
7315
- function validateRemotePreconditions() {
7611
+ function validateRemotePreconditions2() {
7316
7612
  if (!Bun.which("gh")) {
7317
7613
  throw new Error("gh CLI is required for remote deploy. Install gh and run `gh auth login`.");
7318
7614
  }
7319
7615
  }
7320
7616
  function registerCliproxyDeploy(cli) {
7321
- cli.command("cliproxy deploy", "Deploy CLIProxyAPI. Default mode triggers the GitHub Deploy CLIProxy workflow, while --local runs apps/cliproxy/src/deploy.ts directly with Bun.").option("--local", z2.boolean().default(false).describe("Run local deployment with Bun using apps/cliproxy/src/deploy.ts instead of triggering GitHub Actions.")).option("--force-config", z2.boolean().default(false).describe("Force upload config.yaml even if it exists on the server. WARNING: overwrites runtime API keys and settings.")).option("--dry-run", z2.boolean().default(false).describe("Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.")).example("# Trigger remote GitHub Actions deploy (default mode)").example("infra cliproxy deploy").example("# Validate local deploy preconditions and planned command").example("infra cliproxy deploy --local --dry-run").example("# Run local deploy with explicit SSH agent context").example("infra cliproxy deploy --local").action(async (options) => {
7617
+ cli.command("cliproxy deploy", "Deploy CLIProxyAPI. Default mode triggers the GitHub Deploy CLIProxy workflow, while --local runs apps/cliproxy/src/deploy.ts directly with Bun.").option("--local", z5.boolean().default(false).describe("Run local deployment with Bun using apps/cliproxy/src/deploy.ts instead of triggering GitHub Actions.")).option("--force-config", z5.boolean().default(false).describe("Force upload config.yaml even if it exists on the server. WARNING: overwrites runtime API keys and settings.")).option("--dry-run", z5.boolean().default(false).describe("Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.")).example("# Trigger remote GitHub Actions deploy (default mode)").example("infra cliproxy deploy").example("# Validate local deploy preconditions and planned command").example("infra cliproxy deploy --local --dry-run").example("# Run local deploy with explicit SSH agent context").example("infra cliproxy deploy --local").action(async (options) => {
7322
7618
  if (options.local) {
7323
- const { deployScriptPath } = validateLocalPreconditions();
7324
- const env = getLocalDeployEnv();
7619
+ const { deployScriptPath } = validateLocalPreconditions2();
7620
+ const env = getLocalDeployEnv2();
7325
7621
  const command = ["bun", deployScriptPath, ...options.forceConfig ? ["--force-config"] : []];
7326
7622
  if (options.dryRun) {
7327
7623
  console.log("Dry run: local CLIProxyAPI deploy");
@@ -7344,28 +7640,28 @@ function registerCliproxyDeploy(cli) {
7344
7640
  }
7345
7641
  return;
7346
7642
  }
7347
- validateRemotePreconditions();
7643
+ validateRemotePreconditions2();
7348
7644
  if (options.dryRun) {
7349
7645
  console.log("Dry run: remote CLIProxyAPI deploy");
7350
- console.log(`- command: gh workflow run ${WORKFLOW_NAME} --repo ${REPO}`);
7351
- console.log(`- workflow URL: ${WORKFLOW_URL}`);
7646
+ console.log(`- command: gh workflow run ${WORKFLOW_NAME2} --repo ${REPO2}`);
7647
+ console.log(`- workflow URL: ${WORKFLOW_URL2}`);
7352
7648
  return;
7353
7649
  }
7354
- const child = Bun.spawn(["gh", "workflow", "run", WORKFLOW_NAME, "--repo", REPO], {
7650
+ const child = Bun.spawn(["gh", "workflow", "run", WORKFLOW_NAME2, "--repo", REPO2], {
7355
7651
  stdout: "inherit",
7356
7652
  stderr: "inherit"
7357
7653
  });
7358
7654
  const exitCode = await child.exited;
7359
7655
  if (exitCode !== 0) {
7360
- throw new Error(`Failed to trigger ${WORKFLOW_NAME} workflow (exit code ${exitCode})`);
7656
+ throw new Error(`Failed to trigger ${WORKFLOW_NAME2} workflow (exit code ${exitCode})`);
7361
7657
  }
7362
- console.log(`Workflow triggered: ${WORKFLOW_URL}`);
7658
+ console.log(`Workflow triggered: ${WORKFLOW_URL2}`);
7363
7659
  console.log("Approve the cliproxy environment deployment in GitHub Actions to continue.");
7364
7660
  });
7365
7661
  }
7366
7662
 
7367
7663
  // src/commands/cliproxy/keys.ts
7368
- import { z as z3 } from "zod";
7664
+ import { z as z6 } from "zod";
7369
7665
  var DEFAULT_CLIPROXY_URL2 = "https://cliproxy.fro.bot";
7370
7666
  function stripTrailingSlash2(value) {
7371
7667
  return value.endsWith("/") ? value.slice(0, -1) : value;
@@ -7453,21 +7749,21 @@ async function cliproxyKeysRemoveAction(apiKeyToRemove, options, ctx) {
7453
7749
  }
7454
7750
  }
7455
7751
  function registerCliproxyKeys(cli) {
7456
- cli.command("cliproxy keys list", "List CLIProxyAPI API keys from the management API.").option("--url [url]", z3.string().describe("Base URL for CLIProxyAPI management requests. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.")).option("--key [key]", z3.string().describe("Management API key. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.")).option("--json", "Output raw JSON array instead of a numbered list.").action(cliproxyKeysListAction);
7457
- cli.command("cliproxy keys add <key>", "Add an API key by fetching current keys, appending the value, and replacing full key set.").option("--url [url]", z3.string().describe("Base URL for CLIProxyAPI management requests. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.")).option("--key [key]", z3.string().describe("Management API key. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.")).example("# Add a new API key to the current key set").example("infra cliproxy keys add sk-live-123").action(cliproxyKeysAddAction);
7458
- cli.command("cliproxy keys remove <key>", "Remove an API key via management API endpoint query parameter.").option("--url [url]", z3.string().describe("Base URL for CLIProxyAPI management requests. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.")).option("--key [key]", z3.string().describe("Management API key. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.")).example("# Remove an API key from CLIProxyAPI").example("infra cliproxy keys remove sk-live-123").action(cliproxyKeysRemoveAction);
7752
+ cli.command("cliproxy keys list", "List CLIProxyAPI API keys from the management API.").option("--url [url]", z6.string().describe("Base URL for CLIProxyAPI management requests. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.")).option("--key [key]", z6.string().describe("Management API key. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.")).option("--json", "Output raw JSON array instead of a numbered list.").action(cliproxyKeysListAction);
7753
+ cli.command("cliproxy keys add <key>", "Add an API key by fetching current keys, appending the value, and replacing full key set.").option("--url [url]", z6.string().describe("Base URL for CLIProxyAPI management requests. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.")).option("--key [key]", z6.string().describe("Management API key. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.")).example("# Add a new API key to the current key set").example("infra cliproxy keys add sk-live-123").action(cliproxyKeysAddAction);
7754
+ cli.command("cliproxy keys remove <key>", "Remove an API key via management API endpoint query parameter.").option("--url [url]", z6.string().describe("Base URL for CLIProxyAPI management requests. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.")).option("--key [key]", z6.string().describe("Management API key. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.")).example("# Remove an API key from CLIProxyAPI").example("infra cliproxy keys remove sk-live-123").action(cliproxyKeysRemoveAction);
7459
7755
  }
7460
7756
 
7461
7757
  // src/commands/cliproxy/login.ts
7462
- import { z as z4 } from "zod";
7758
+ import { z as z7 } from "zod";
7463
7759
 
7464
7760
  // src/commands/cliproxy/host.ts
7465
- var VALID_HOST_RE = /^[a-z\d][a-z\d.\-]*$/i;
7761
+ var VALID_HOST_RE2 = /^[a-z\d][a-z\d.\-]*$/i;
7466
7762
  function validateCliproxyHost(host) {
7467
7763
  if (!host) {
7468
7764
  throw new Error("Invalid CLIPROXY_DOMAIN: value is empty");
7469
7765
  }
7470
- if (!VALID_HOST_RE.test(host)) {
7766
+ if (!VALID_HOST_RE2.test(host)) {
7471
7767
  const excerpt = host.slice(0, 30).replaceAll(/[^\u0020-\u007E]/g, "?");
7472
7768
  throw new Error(`Invalid CLIPROXY_DOMAIN: "${excerpt}" \u2014 must match ${String.raw`[A-Za-z0-9][A-Za-z0-9.\-]*`}`);
7473
7769
  }
@@ -7544,22 +7840,22 @@ async function cliproxyLoginAction(provider, options, spawnFn = Bun.spawn) {
7544
7840
  console.log("If an OAuth URL was printed above, open it in your browser to complete login.");
7545
7841
  }
7546
7842
  function registerCliproxyLogin(cli) {
7547
- cli.command("cliproxy login <provider>", `Run provider login on the remote CLIProxyAPI host. Supported providers: ${SUPPORTED_PROVIDERS_DISPLAY}.`).option("--host [host]", z4.string().describe("CLIProxyAPI droplet host for SSH execution. Falls back to CLIPROXY_DOMAIN or cliproxy.fro.bot.")).example("# Start Claude login flow on remote CLIProxyAPI instance").example("infra cliproxy login claude").example("# Start ChatGPT Pro login flow via device-code").example("infra cliproxy login codex").action((provider, options) => cliproxyLoginAction(provider, options));
7843
+ cli.command("cliproxy login <provider>", `Run provider login on the remote CLIProxyAPI host. Supported providers: ${SUPPORTED_PROVIDERS_DISPLAY}.`).option("--host [host]", z7.string().describe("CLIProxyAPI droplet host for SSH execution. Falls back to CLIPROXY_DOMAIN or cliproxy.fro.bot.")).example("# Start Claude login flow on remote CLIProxyAPI instance").example("infra cliproxy login claude").example("# Start ChatGPT Pro login flow via device-code").example("infra cliproxy login codex").action((provider, options) => cliproxyLoginAction(provider, options));
7548
7844
  }
7549
7845
 
7550
7846
  // src/commands/cliproxy/models.ts
7551
- import { z as z5 } from "zod";
7847
+ import { z as z8 } from "zod";
7552
7848
  var DEFAULT_CLIPROXY_URL3 = "https://cliproxy.fro.bot";
7553
7849
  function stripTrailingSlash3(value) {
7554
7850
  return value.endsWith("/") ? value.slice(0, -1) : value;
7555
7851
  }
7556
- var modelEntrySchema = z5.object({
7557
- id: z5.string(),
7558
- owned_by: z5.string().optional(),
7559
- created: z5.number().optional()
7852
+ var modelEntrySchema = z8.object({
7853
+ id: z8.string(),
7854
+ owned_by: z8.string().optional(),
7855
+ created: z8.number().optional()
7560
7856
  }).passthrough();
7561
- var modelsResponseSchema = z5.object({
7562
- data: z5.array(modelEntrySchema)
7857
+ var modelsResponseSchema = z8.object({
7858
+ data: z8.array(modelEntrySchema)
7563
7859
  }).passthrough();
7564
7860
  var PROVIDER_ID_PATTERNS = {
7565
7861
  openai: /^(?:gpt-|o1-|o3-|codex)/i,
@@ -7665,11 +7961,11 @@ async function cliproxyModelsAction(options, ctx) {
7665
7961
  }
7666
7962
  }
7667
7963
  function registerCliproxyModels(cli) {
7668
- cli.command("cliproxy models [provider]", "List the models CLIProxyAPI serves.").option("--url [url]", z5.string().describe("Base URL for CLIProxyAPI. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.")).option("--key [key]", z5.string().describe("API key (bearer). Falls back to CLIPROXY_API_KEY when omitted.")).option("--verbose", "Show owned_by and created date for each model.").example("# List all models served by CLIProxyAPI").example("infra cliproxy models").example("# Filter to Anthropic models only").example("infra cliproxy models anthropic").example("# Show verbose output with owned_by and created date").example("infra cliproxy models --verbose").example("# Filter to OpenAI models with verbose output").example("infra cliproxy models openai --verbose").action((provider, options, ctx) => cliproxyModelsAction({ ...options, provider }, ctx));
7964
+ cli.command("cliproxy models [provider]", "List the models CLIProxyAPI serves.").option("--url [url]", z8.string().describe("Base URL for CLIProxyAPI. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.")).option("--key [key]", z8.string().describe("API key (bearer). Falls back to CLIPROXY_API_KEY when omitted.")).option("--verbose", "Show owned_by and created date for each model.").example("# List all models served by CLIProxyAPI").example("infra cliproxy models").example("# Filter to Anthropic models only").example("infra cliproxy models anthropic").example("# Show verbose output with owned_by and created date").example("infra cliproxy models --verbose").example("# Filter to OpenAI models with verbose output").example("infra cliproxy models openai --verbose").action((provider, options, ctx) => cliproxyModelsAction({ ...options, provider }, ctx));
7669
7965
  }
7670
7966
 
7671
7967
  // src/commands/cliproxy/open.ts
7672
- import { z as z6 } from "zod";
7968
+ import { z as z9 } from "zod";
7673
7969
  var DEFAULT_HOST2 = "cliproxy.fro.bot";
7674
7970
  var DEFAULT_REMOTE_USER2 = "root";
7675
7971
  function resolveHost2(input) {
@@ -7680,7 +7976,7 @@ function resolveHost2(input) {
7680
7976
  return host;
7681
7977
  }
7682
7978
  function registerCliproxyOpen(cli) {
7683
- cli.command("cliproxy open", "Launch CLIProxyAPI TUI dashboard via SSH.").option("--host [host]", z6.string().describe("SSH host for CLIProxyAPI server. Falls back to CLIPROXY_DOMAIN or cliproxy.fro.bot.")).example("# Open the TUI dashboard on the remote CLIProxyAPI instance").example("infra cliproxy open").example("# Open on a custom host").example("infra cliproxy open --host custom.example.com").action(async (options) => {
7979
+ cli.command("cliproxy open", "Launch CLIProxyAPI TUI dashboard via SSH.").option("--host [host]", z9.string().describe("SSH host for CLIProxyAPI server. Falls back to CLIPROXY_DOMAIN or cliproxy.fro.bot.")).example("# Open the TUI dashboard on the remote CLIProxyAPI instance").example("infra cliproxy open").example("# Open on a custom host").example("infra cliproxy open --host custom.example.com").action(async (options) => {
7684
7980
  const host = validateCliproxyHost(resolveHost2(options.host));
7685
7981
  if (!process.stdin.isTTY) {
7686
7982
  throw new Error("cliproxy open requires an interactive terminal. Run from a shell with TTY attached.");
@@ -7721,7 +8017,7 @@ function registerCliproxyOpen(cli) {
7721
8017
 
7722
8018
  // src/commands/cliproxy/setup.ts
7723
8019
  import { cancel as cancel2, confirm as confirm2, intro, log as log2, note, outro, select as select2, text as text3 } from "@clack/prompts";
7724
- import { z as z11 } from "zod";
8020
+ import { z as z14 } from "zod";
7725
8021
 
7726
8022
  // src/commands/cliproxy/setup/gh.ts
7727
8023
  import { confirm, log, spinner } from "@clack/prompts";
@@ -7875,10 +8171,10 @@ async function assertGhAuthenticated() {
7875
8171
  }
7876
8172
  }
7877
8173
  async function assertRepoAccess(repo) {
7878
- const { z: z7 } = await import("zod");
7879
- const ghRepoViewSchema = z7.object({
7880
- nameWithOwner: z7.string(),
7881
- viewerPermission: z7.string()
8174
+ const { z: z10 } = await import("zod");
8175
+ const ghRepoViewSchema = z10.object({
8176
+ nameWithOwner: z10.string(),
8177
+ viewerPermission: z10.string()
7882
8178
  });
7883
8179
  const result = await runGh(["repo", "view", repo, "--json", "nameWithOwner,viewerPermission"]);
7884
8180
  if (result.exitCode !== 0) {
@@ -7891,8 +8187,8 @@ async function assertRepoAccess(repo) {
7891
8187
  }
7892
8188
  }
7893
8189
  async function listExistingGhNames(repo, kind) {
7894
- const { z: z7 } = await import("zod");
7895
- const ghNameListSchema = z7.array(z7.object({ name: z7.string() }));
8190
+ const { z: z10 } = await import("zod");
8191
+ const ghNameListSchema = z10.array(z10.object({ name: z10.string() }));
7896
8192
  const result = await runGh([kind, "list", "--repo", repo, "--json", "name"]);
7897
8193
  if (result.exitCode !== 0) {
7898
8194
  throw new Error(`Unable to list existing GitHub ${kind}s for ${repo}. ${result.stderr.trim()}`.trim());
@@ -7978,8 +8274,8 @@ function formatDryRunPreview(opts) {
7978
8274
 
7979
8275
  // src/commands/cliproxy/setup/providers.ts
7980
8276
  import { isCancel as isCancel2, multiselect, select, text as text2 } from "@clack/prompts";
7981
- import { z as z7 } from "zod";
7982
- var providerIdSchema = z7.enum(["anthropic", "openai"]);
8277
+ import { z as z10 } from "zod";
8278
+ var providerIdSchema = z10.enum(["anthropic", "openai"]);
7983
8279
  var MODEL_ID_RE = /^(?:anthropic|openai)\/[a-z\d](?:[a-z\d.\-]*[a-z\d])?$/;
7984
8280
  function parseProviders(input) {
7985
8281
  const parts = input.split(",").map((p) => p.trim()).filter(Boolean);
@@ -8068,25 +8364,25 @@ async function promptForCustomModel() {
8068
8364
  }
8069
8365
 
8070
8366
  // src/commands/cliproxy/setup/smoke-test.ts
8071
- import { z as z8 } from "zod";
8072
- var baselineRunSchema = z8.array(z8.object({ databaseId: z8.number() }));
8073
- var ghRunEntrySchema = z8.object({
8074
- databaseId: z8.number(),
8075
- status: z8.string(),
8076
- conclusion: z8.string().nullable(),
8077
- url: z8.string(),
8078
- createdAt: z8.string()
8367
+ import { z as z11 } from "zod";
8368
+ var baselineRunSchema = z11.array(z11.object({ databaseId: z11.number() }));
8369
+ var ghRunEntrySchema = z11.object({
8370
+ databaseId: z11.number(),
8371
+ status: z11.string(),
8372
+ conclusion: z11.string().nullable(),
8373
+ url: z11.string(),
8374
+ createdAt: z11.string()
8079
8375
  });
8080
8376
  async function runSmokeTest(repo, _model, internals = {}) {
8081
8377
  const BACKOFF_MS = [5000, 15000, 30000, 60000, 60000];
8082
8378
  const delayFn = async (ms) => {
8083
8379
  if (internals._testDelayMs !== undefined) {
8084
8380
  if (internals._testDelayMs > 0) {
8085
- await new Promise((resolve) => setTimeout(resolve, internals._testDelayMs));
8381
+ await new Promise((resolve2) => setTimeout(resolve2, internals._testDelayMs));
8086
8382
  }
8087
8383
  return;
8088
8384
  }
8089
- await new Promise((resolve) => setTimeout(resolve, ms));
8385
+ await new Promise((resolve2) => setTimeout(resolve2, ms));
8090
8386
  };
8091
8387
  const repoUrl = `https://github.com/${repo}`;
8092
8388
  let baselineId = null;
@@ -8205,8 +8501,8 @@ async function runSmokeTest(repo, _model, internals = {}) {
8205
8501
  }
8206
8502
 
8207
8503
  // src/commands/cliproxy/setup/templates.ts
8208
- import { z as z9 } from "zod";
8209
- var harnessSchema = z9.enum(["opencode", "claude-code", "generic"]);
8504
+ import { z as z12 } from "zod";
8505
+ var harnessSchema = z12.enum(["opencode", "claude-code", "generic"]);
8210
8506
  var DEFAULT_CLIPROXY_URL4 = "https://cliproxy.fro.bot";
8211
8507
  function stripTrailingSlash4(value) {
8212
8508
  return value.endsWith("/") ? value.slice(0, -1) : value;
@@ -8297,16 +8593,16 @@ function collectCollisions(template, existingSecrets, existingVariables) {
8297
8593
  }
8298
8594
 
8299
8595
  // src/commands/cliproxy/setup/validation.ts
8300
- import { z as z10 } from "zod";
8301
- var modelEntrySchema2 = z10.object({
8302
- id: z10.string(),
8303
- owned_by: z10.string().optional()
8596
+ import { z as z13 } from "zod";
8597
+ var modelEntrySchema2 = z13.object({
8598
+ id: z13.string(),
8599
+ owned_by: z13.string().optional()
8304
8600
  }).passthrough();
8305
- var modelsResponseSchema2 = z10.object({
8306
- data: z10.array(modelEntrySchema2)
8601
+ var modelsResponseSchema2 = z13.object({
8602
+ data: z13.array(modelEntrySchema2)
8307
8603
  }).passthrough();
8308
8604
  var MODEL_ID_RE2 = /^(?:anthropic|openai)\/[a-z\d](?:[a-z\d.\-]*[a-z\d])?$/;
8309
- var HTTP_TIMEOUT_MS2 = 1e4;
8605
+ var HTTP_TIMEOUT_MS3 = 1e4;
8310
8606
  var PROVIDER_ID_PATTERNS2 = {
8311
8607
  openai: /^(?:gpt-|o1-|o3-|codex)/i,
8312
8608
  anthropic: /^claude-/i
@@ -8378,7 +8674,7 @@ async function verifyModelsAvailable(baseUrl, key, providers, model) {
8378
8674
  try {
8379
8675
  parsed = modelsResponseSchema2.parse(json);
8380
8676
  } catch (error) {
8381
- const message = error instanceof z10.ZodError ? error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ") : extractErrorMessage3(error);
8677
+ const message = error instanceof z13.ZodError ? error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ") : extractErrorMessage3(error);
8382
8678
  throw new Error(`Malformed /v1/models response: ${message}`);
8383
8679
  }
8384
8680
  const entries = parsed.data;
@@ -8401,7 +8697,7 @@ async function verifyModelsAvailable(baseUrl, key, providers, model) {
8401
8697
  async function assertProxyReachable(baseUrl) {
8402
8698
  try {
8403
8699
  const response = await fetch(`${baseUrl}/healthz`, {
8404
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS2)
8700
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS3)
8405
8701
  });
8406
8702
  if (!response.ok) {
8407
8703
  throw new Error(`Proxy check failed for ${baseUrl}: HTTP ${response.status}. Is the proxy running and reachable?`);
@@ -8419,7 +8715,7 @@ async function assertProxyKeyWorks(baseUrl, keyValue) {
8419
8715
  headers: {
8420
8716
  authorization: `Bearer ${keyValue}`
8421
8717
  },
8422
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS2)
8718
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS3)
8423
8719
  });
8424
8720
  if (!response.ok) {
8425
8721
  throw new Error(`Proxy key verification failed with HTTP ${response.status}`);
@@ -8924,13 +9220,13 @@ async function runSetupCommand(options, deps = {}) {
8924
9220
  }
8925
9221
  }
8926
9222
  function registerCliproxySetup(cli) {
8927
- cli.command("cliproxy setup", "Interactively onboard a GitHub repository to CLIProxyAPI by creating or reusing a key and wiring the required GitHub secrets and variables.").option("--key [key]", z11.string().describe("Existing CLIProxyAPI API key value. When provided, setup skips key creation and reuses this key for GitHub secrets.")).option("--repo [repo]", z11.string().describe("Target GitHub repository in owner/repo format. Skips the repository prompt when provided.")).option("--harness [harness]", harnessSchema.describe("Harness template to configure. Choose opencode, claude-code, or generic. Generic remains interactive-only.")).option("--providers [providers]", z11.string().describe("Comma-separated list of providers to enable. Default: anthropic. Supported values: anthropic, openai. Example: --providers anthropic,openai")).option("--model [model]", z11.string().regex(MODEL_ID_RE2).describe("Override the default model. Must be provider-prefixed and lowercase. Required when multiple providers selected. Examples: anthropic/claude-sonnet-4-6, openai/gpt-4o")).option("--force", z11.boolean().optional().describe("Overwrite existing GitHub secrets and variables without prompting.")).option("--dry-run", z11.boolean().optional().describe("Print the plan without applying any changes.")).option("--verify-smoke", z11.boolean().optional().describe("Run a smoke test against the proxy after setup completes.")).option("--ack-key-reuse", z11.boolean().default(false).describe("Acknowledge that --key matches the bearer token inside the existing OPENCODE_AUTH_JSON. Required in non-interactive mode when --key is supplied for a repo with existing OPENCODE_AUTH_JSON.")).example("# Preview planned actions without applying any changes (no flags required)").example("infra cliproxy setup --dry-run").example("# Run the interactive onboarding wizard").example("infra cliproxy setup").example("# Run non-interactively with an existing key (anthropic-only)").example("infra cliproxy setup --repo owner/repo --harness opencode --key sk-existing --force").example("# Enable both providers non-interactively (requires --force and --model)").example("infra cliproxy setup --repo owner/repo --harness opencode --providers anthropic,openai --model openai/gpt-5.4-mini --key sk-existing --ack-key-reuse --force").action(async (options, ctx) => {
9223
+ cli.command("cliproxy setup", "Interactively onboard a GitHub repository to CLIProxyAPI by creating or reusing a key and wiring the required GitHub secrets and variables.").option("--key [key]", z14.string().describe("Existing CLIProxyAPI API key value. When provided, setup skips key creation and reuses this key for GitHub secrets.")).option("--repo [repo]", z14.string().describe("Target GitHub repository in owner/repo format. Skips the repository prompt when provided.")).option("--harness [harness]", harnessSchema.describe("Harness template to configure. Choose opencode, claude-code, or generic. Generic remains interactive-only.")).option("--providers [providers]", z14.string().describe("Comma-separated list of providers to enable. Default: anthropic. Supported values: anthropic, openai. Example: --providers anthropic,openai")).option("--model [model]", z14.string().regex(MODEL_ID_RE2).describe("Override the default model. Must be provider-prefixed and lowercase. Required when multiple providers selected. Examples: anthropic/claude-sonnet-4-6, openai/gpt-4o")).option("--force", z14.boolean().optional().describe("Overwrite existing GitHub secrets and variables without prompting.")).option("--dry-run", z14.boolean().optional().describe("Print the plan without applying any changes.")).option("--verify-smoke", z14.boolean().optional().describe("Run a smoke test against the proxy after setup completes.")).option("--ack-key-reuse", z14.boolean().default(false).describe("Acknowledge that --key matches the bearer token inside the existing OPENCODE_AUTH_JSON. Required in non-interactive mode when --key is supplied for a repo with existing OPENCODE_AUTH_JSON.")).example("# Preview planned actions without applying any changes (no flags required)").example("infra cliproxy setup --dry-run").example("# Run the interactive onboarding wizard").example("infra cliproxy setup").example("# Run non-interactively with an existing key (anthropic-only)").example("infra cliproxy setup --repo owner/repo --harness opencode --key sk-existing --force").example("# Enable both providers non-interactively (requires --force and --model)").example("infra cliproxy setup --repo owner/repo --harness opencode --providers anthropic,openai --model openai/gpt-5.4-mini --key sk-existing --ack-key-reuse --force").action(async (options, ctx) => {
8928
9224
  await runSetupCommand(options, { ctx });
8929
9225
  });
8930
9226
  }
8931
9227
 
8932
9228
  // src/commands/cliproxy/status.ts
8933
- import { z as z12 } from "zod";
9229
+ import { z as z15 } from "zod";
8934
9230
  var DEFAULT_CLIPROXY_URL6 = "https://cliproxy.fro.bot";
8935
9231
  function levelLabel(level) {
8936
9232
  if (level === "ok") {
@@ -8961,7 +9257,7 @@ async function checkHttpReachability(url, verbose) {
8961
9257
  const startedAt = performance.now();
8962
9258
  try {
8963
9259
  const response = await fetch(url, {
8964
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
9260
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS2)
8965
9261
  });
8966
9262
  const elapsedMs = performance.now() - startedAt;
8967
9263
  const details = [];
@@ -8985,7 +9281,7 @@ async function checkHttpReachability(url, verbose) {
8985
9281
  title: "HTTP reachability",
8986
9282
  level: "error",
8987
9283
  summary: `Request failed after ${formatDurationMs(elapsedMs)}: ${message}`,
8988
- details: verbose ? [`URL: ${url}`, `Timeout: ${HTTP_TIMEOUT_MS}ms`] : undefined
9284
+ details: verbose ? [`URL: ${url}`, `Timeout: ${HTTP_TIMEOUT_MS2}ms`] : undefined
8989
9285
  };
8990
9286
  }
8991
9287
  }
@@ -9012,7 +9308,7 @@ async function checkUsageStats(baseUrl, key) {
9012
9308
  try {
9013
9309
  const response = await fetch(endpoint, {
9014
9310
  headers: managementHeaders(key),
9015
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
9311
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS2)
9016
9312
  });
9017
9313
  if (response.status === 429) {
9018
9314
  return {
@@ -9064,7 +9360,7 @@ async function checkVersion(baseUrl, key) {
9064
9360
  try {
9065
9361
  const response = await fetch(endpoint, {
9066
9362
  headers: managementHeaders(key),
9067
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
9363
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS2)
9068
9364
  });
9069
9365
  if (response.status === 429) {
9070
9366
  return {
@@ -9114,7 +9410,7 @@ async function checkProviderAuth(baseUrl, apiKey, options) {
9114
9410
  messages: [{ role: "user", content: "ping" }],
9115
9411
  max_tokens: 1
9116
9412
  }),
9117
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
9413
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS2)
9118
9414
  });
9119
9415
  const details = [];
9120
9416
  if (verbose) {
@@ -9174,7 +9470,7 @@ async function probeManagementAuth(baseUrl, key) {
9174
9470
  try {
9175
9471
  const response = await fetch(endpoint, {
9176
9472
  headers: managementHeaders(key),
9177
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
9473
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS2)
9178
9474
  });
9179
9475
  if (response.ok) {
9180
9476
  return null;
@@ -9336,7 +9632,7 @@ async function cliproxyStatusAction(options, ctx) {
9336
9632
  }
9337
9633
  }
9338
9634
  function registerCliproxyStatus(cli) {
9339
- cli.command("cliproxy status", "Show operational health of CLIProxyAPI and its management endpoints.").option("--url [url]", z12.string().describe("Base URL for CLIProxyAPI health checks. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.")).option("--key [key]", z12.string().describe("Management API key. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.")).option("--api-key [key]", z12.string().describe("Downstream API key (bearer) for the upstream provider-auth probe. Falls back to CLIPROXY_API_KEY. Skipped when absent.")).option("--verbose", "Enable verbose output for all commands").action(cliproxyStatusAction);
9635
+ cli.command("cliproxy status", "Show operational health of CLIProxyAPI and its management endpoints.").option("--url [url]", z15.string().describe("Base URL for CLIProxyAPI health checks. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.")).option("--key [key]", z15.string().describe("Management API key. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.")).option("--api-key [key]", z15.string().describe("Downstream API key (bearer) for the upstream provider-auth probe. Falls back to CLIPROXY_API_KEY. Skipped when absent.")).option("--verbose", "Enable verbose output for all commands").action(cliproxyStatusAction);
9340
9636
  }
9341
9637
 
9342
9638
  // src/commands/cliproxy/index.ts
@@ -9352,10 +9648,10 @@ function registerCliproxyCommands(cli) {
9352
9648
  }
9353
9649
 
9354
9650
  // src/commands/dashboard/deploy.ts
9355
- import { z as z13 } from "zod";
9356
- var REPO2 = "marcusrbrown/infra";
9357
- var WORKFLOW_NAME2 = "Deploy Dashboard";
9358
- var WORKFLOW_URL2 = "https://github.com/marcusrbrown/infra/actions/workflows/deploy-dashboard.yaml";
9651
+ import { z as z16 } from "zod";
9652
+ var REPO3 = "marcusrbrown/infra";
9653
+ var WORKFLOW_NAME3 = "Deploy Dashboard";
9654
+ var WORKFLOW_URL3 = "https://github.com/marcusrbrown/infra/actions/workflows/deploy-dashboard.yaml";
9359
9655
  var CALVER_RE = /^\d{4}\.\d{2}\.\d+$/;
9360
9656
  var DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
9361
9657
  function validateReleaseFlags(options) {
@@ -9374,7 +9670,7 @@ function validateReleaseFlags(options) {
9374
9670
  }
9375
9671
  }
9376
9672
  function buildRemoteArgv(options) {
9377
- const argv = ["gh", "workflow", "run", WORKFLOW_NAME2, "--repo", REPO2];
9673
+ const argv = ["gh", "workflow", "run", WORKFLOW_NAME3, "--repo", REPO3];
9378
9674
  if (options.imageVersion !== undefined) {
9379
9675
  argv.push("-f", `version=${options.imageVersion}`);
9380
9676
  }
@@ -9414,7 +9710,7 @@ function validateDashboardRemotePreconditions() {
9414
9710
  }
9415
9711
  }
9416
9712
  function registerDashboardDeploy(cli) {
9417
- cli.command("dashboard deploy", "Deploy the dashboard. Default mode triggers the GitHub Deploy Dashboard workflow, while --local runs apps/dashboard deploy directly with Bun.").option("--local", z13.boolean().default(false).describe("Run local deployment with Bun using apps/dashboard instead of triggering GitHub Actions.")).option("--dry-run", z13.boolean().default(false).describe("Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.")).option("--image-version <version>", z13.string().optional().describe("CalVer version to deploy (e.g. 2026.06.47). Remote-only. Triggers the versioned release path and requires dashboard environment approval. Must match YYYY.MM.N. Forwarded as -f version=<value> to gh workflow run.")).option("--digest <digest>", z13.string().optional().describe("Expected sha256 digest (e.g. sha256:abc...def). Remote-only. Requires --image-version. When set, the workflow fails if GHCR resolves the tag to a different digest. Must match sha256:<64 hex chars>.")).example("# Trigger remote GitHub Actions deploy (default mode)").example("infra dashboard deploy").example("# Deploy a specific CalVer release via GitHub Actions").example("infra dashboard deploy --image-version 2026.06.47").example("# Deploy a specific release with digest cross-check").example("infra dashboard deploy --image-version 2026.06.47 --digest sha256:<64 hex>").example("# Validate local deploy preconditions and planned command").example("infra dashboard deploy --local --dry-run").example("# Run local deploy with explicit SSH agent context").example("infra dashboard deploy --local").action(async (options) => {
9713
+ cli.command("dashboard deploy", "Deploy the dashboard. Default mode triggers the GitHub Deploy Dashboard workflow, while --local runs apps/dashboard deploy directly with Bun.").option("--local", z16.boolean().default(false).describe("Run local deployment with Bun using apps/dashboard instead of triggering GitHub Actions.")).option("--dry-run", z16.boolean().default(false).describe("Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.")).option("--image-version <version>", z16.string().optional().describe("CalVer version to deploy (e.g. 2026.06.47). Remote-only. Triggers the versioned release path and requires dashboard environment approval. Must match YYYY.MM.N. Forwarded as -f version=<value> to gh workflow run.")).option("--digest <digest>", z16.string().optional().describe("Expected sha256 digest (e.g. sha256:abc...def). Remote-only. Requires --image-version. When set, the workflow fails if GHCR resolves the tag to a different digest. Must match sha256:<64 hex chars>.")).example("# Trigger remote GitHub Actions deploy (default mode)").example("infra dashboard deploy").example("# Deploy a specific CalVer release via GitHub Actions").example("infra dashboard deploy --image-version 2026.06.47").example("# Deploy a specific release with digest cross-check").example("infra dashboard deploy --image-version 2026.06.47 --digest sha256:<64 hex>").example("# Validate local deploy preconditions and planned command").example("infra dashboard deploy --local --dry-run").example("# Run local deploy with explicit SSH agent context").example("infra dashboard deploy --local").action(async (options) => {
9418
9714
  validateReleaseFlags({ imageVersion: options.imageVersion, digest: options.digest, local: options.local });
9419
9715
  if (options.local) {
9420
9716
  const command = ["bun", "run", "--cwd", "apps/dashboard", "deploy"];
@@ -9440,7 +9736,7 @@ function registerDashboardDeploy(cli) {
9440
9736
  if (options.dryRun) {
9441
9737
  console.log("Dry run: remote dashboard deploy");
9442
9738
  console.log(`- command: ${formatArgv(remoteArgv)}`);
9443
- console.log(`- workflow URL: ${WORKFLOW_URL2}`);
9739
+ console.log(`- workflow URL: ${WORKFLOW_URL3}`);
9444
9740
  return;
9445
9741
  }
9446
9742
  const child = Bun.spawn(remoteArgv, {
@@ -9449,47 +9745,23 @@ function registerDashboardDeploy(cli) {
9449
9745
  });
9450
9746
  const exitCode = await child.exited;
9451
9747
  if (exitCode !== 0) {
9452
- throw new Error(`Failed to trigger "${WORKFLOW_NAME2}" workflow (exit code ${exitCode})`);
9748
+ throw new Error(`Failed to trigger "${WORKFLOW_NAME3}" workflow (exit code ${exitCode})`);
9453
9749
  }
9454
- console.log(`Workflow triggered: ${WORKFLOW_URL2}`);
9750
+ console.log(`Workflow triggered: ${WORKFLOW_URL3}`);
9455
9751
  console.log("Approve the dashboard environment deployment in GitHub Actions to continue.");
9456
9752
  });
9457
9753
  }
9458
9754
 
9459
9755
  // src/commands/dashboard/logs.ts
9460
- import { z as z14 } from "zod";
9461
-
9462
- // src/lib/known-hosts.ts
9463
- import { existsSync as existsSync3 } from "fs";
9464
- import { join as join3, resolve } from "path";
9465
-
9466
- // src/resources/known_hosts
9467
- var known_hosts_default = "./known_hosts-tk724cp0.";
9468
-
9469
- // src/lib/known-hosts.ts
9470
- var FAIL_CLOSED_ERROR = "Pinned SSH known_hosts file not found; reinstall @marcusrbrown/infra or run from the repo checkout";
9471
- function resolveKnownHostsPath(libDir) {
9472
- const dir = libDir ?? import.meta.dir;
9473
- const repoRoot = resolve(dir, "..", "..", "..", "..");
9474
- const repoCandidate = join3(repoRoot, ".github", "known_hosts");
9475
- if (existsSync3(repoCandidate))
9476
- return repoCandidate;
9477
- if (existsSync3(known_hosts_default))
9478
- return known_hosts_default;
9479
- throw new Error(FAIL_CLOSED_ERROR);
9480
- }
9481
- function buildKnownHostsArgs(libDir) {
9482
- const path = resolveKnownHostsPath(libDir);
9483
- return ["-o", `UserKnownHostsFile=${path}`];
9484
- }
9756
+ import { z as z17 } from "zod";
9485
9757
 
9486
9758
  // ../shared/server/droplet-helpers.ts
9487
9759
  import { appendFileSync, chmodSync as chmodSync2, mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
9488
9760
  import { tmpdir } from "os";
9489
- import { join as join4 } from "path";
9761
+ import { join as join5 } from "path";
9490
9762
  function materializeIdentityFile(privateKey) {
9491
- const dir = mkdtempSync(join4(tmpdir(), "infra-ssh-key-"));
9492
- const path = join4(dir, "id");
9763
+ const dir = mkdtempSync(join5(tmpdir(), "infra-ssh-key-"));
9764
+ const path = join5(dir, "id");
9493
9765
  const cleanup = () => {
9494
9766
  try {
9495
9767
  rmSync(dir, { recursive: true, force: true });
@@ -9523,12 +9795,12 @@ function buildIdentityArgs(privateKey) {
9523
9795
  }
9524
9796
 
9525
9797
  // src/commands/dashboard/host.ts
9526
- var VALID_HOST_RE2 = /^[a-z\d][a-z\d.\-]*$/i;
9798
+ var VALID_HOST_RE3 = /^[a-z\d][a-z\d.\-]*$/i;
9527
9799
  function validateDashboardHost(host) {
9528
9800
  if (!host) {
9529
9801
  throw new Error("Invalid DASHBOARD_DOMAIN: value is empty");
9530
9802
  }
9531
- if (!VALID_HOST_RE2.test(host)) {
9803
+ if (!VALID_HOST_RE3.test(host)) {
9532
9804
  throw new Error(`Invalid DASHBOARD_DOMAIN: value is not a valid hostname (must match ${String.raw`[A-Za-z0-9][A-Za-z0-9.\-]*`})`);
9533
9805
  }
9534
9806
  return host;
@@ -9536,17 +9808,17 @@ function validateDashboardHost(host) {
9536
9808
 
9537
9809
  // src/commands/dashboard/logs.ts
9538
9810
  var COMPOSE_PROJECT_DIR = "/opt/dashboard";
9539
- var SENSITIVE_WARNING = "Warning: Logs may contain database passwords, app secrets, or user data. Treat output as sensitive; do not capture in shared logs or chat.";
9811
+ var SENSITIVE_WARNING2 = "Warning: Logs may contain database passwords, app secrets, or user data. Treat output as sensitive; do not capture in shared logs or chat.";
9540
9812
  var VALID_SERVICES = ["dashboard", "caddy"];
9541
9813
  function validateService(service) {
9542
9814
  if (!VALID_SERVICES.includes(service)) {
9543
9815
  throw new Error(`Invalid service "${service}". Valid services: ${VALID_SERVICES.join(", ")}`);
9544
9816
  }
9545
9817
  }
9546
- function defaultLogsSpawn(cmd, opts) {
9818
+ function defaultLogsSpawn2(cmd, opts) {
9547
9819
  return Bun.spawn(cmd, opts);
9548
9820
  }
9549
- async function streamDashboardLogs(opts, spawn = defaultLogsSpawn, printOut, printErr) {
9821
+ async function streamDashboardLogs(opts, spawn = defaultLogsSpawn2, printOut, printErr) {
9550
9822
  validateService(opts.service);
9551
9823
  const isCI = process.env.CI === "true";
9552
9824
  if (isCI && !opts.allowCi) {
@@ -9559,9 +9831,9 @@ async function streamDashboardLogs(opts, spawn = defaultLogsSpawn, printOut, pri
9559
9831
  return { refused: true };
9560
9832
  }
9561
9833
  if (printErr) {
9562
- printErr(SENSITIVE_WARNING);
9834
+ printErr(SENSITIVE_WARNING2);
9563
9835
  } else {
9564
- console.error(SENSITIVE_WARNING);
9836
+ console.error(SENSITIVE_WARNING2);
9565
9837
  }
9566
9838
  validateDashboardHost(opts.host);
9567
9839
  const { args: identityArgs, cleanup } = buildIdentityArgs(process.env.DASHBOARD_SSH_KEY);
@@ -9593,7 +9865,7 @@ async function streamDashboardLogs(opts, spawn = defaultLogsSpawn, printOut, pri
9593
9865
  return { refused: false, exitCode };
9594
9866
  }
9595
9867
  function registerDashboardLogs(cli) {
9596
- cli.command("dashboard logs [service]", "Stream logs from a dashboard service via SSH and docker compose.").option("--tail [n]", z14.number().default(100).describe("Number of log lines to tail from each service.")).option("--allow-ci", z14.boolean().default(false).describe("Allow log streaming in CI environments. Logs may contain sensitive credentials.")).option("--key [key]", z14.string().describe("Environment variable name holding the SSH host. Falls back to DASHBOARD_DOMAIN when omitted.")).action(async (service, options) => {
9868
+ cli.command("dashboard logs [service]", "Stream logs from a dashboard service via SSH and docker compose.").option("--tail [n]", z17.number().default(100).describe("Number of log lines to tail from each service.")).option("--allow-ci", z17.boolean().default(false).describe("Allow log streaming in CI environments. Logs may contain sensitive credentials.")).option("--key [key]", z17.string().describe("Environment variable name holding the SSH host. Falls back to DASHBOARD_DOMAIN when omitted.")).action(async (service, options) => {
9597
9869
  const targetService = service ?? "dashboard";
9598
9870
  try {
9599
9871
  validateService(targetService);
@@ -9623,7 +9895,7 @@ function registerDashboardLogs(cli) {
9623
9895
  }
9624
9896
 
9625
9897
  // src/commands/dashboard/status.ts
9626
- import { z as z15 } from "zod";
9898
+ import { z as z18 } from "zod";
9627
9899
 
9628
9900
  // src/lib/redact.ts
9629
9901
  function redactHost(text4, host) {
@@ -9807,7 +10079,7 @@ async function dashboardStatusAction(options, ctx, spawn) {
9807
10079
  }
9808
10080
  }
9809
10081
  function registerDashboardStatus(cli) {
9810
- cli.command("dashboard status", "Show operational health of the dashboard deployment via docker compose ps.").option("--key [key]", z15.string().optional().describe("Environment variable name holding the SSH host. Falls back to DASHBOARD_DOMAIN when omitted.")).action((options, ctx) => dashboardStatusAction(options, ctx));
10082
+ cli.command("dashboard status", "Show operational health of the dashboard deployment via docker compose ps.").option("--key [key]", z18.string().optional().describe("Environment variable name holding the SSH host. Falls back to DASHBOARD_DOMAIN when omitted.")).action((options, ctx) => dashboardStatusAction(options, ctx));
9811
10083
  }
9812
10084
 
9813
10085
  // src/commands/dashboard/index.ts
@@ -9819,15 +10091,15 @@ function registerDashboardCommands(cli) {
9819
10091
 
9820
10092
  // src/commands/gateway/backup.ts
9821
10093
  import { closeSync, constants as fsConstants, openSync, renameSync, unlinkSync, writeSync } from "fs";
9822
- import { z as z16 } from "zod";
10094
+ import { z as z19 } from "zod";
9823
10095
 
9824
10096
  // src/commands/gateway/host.ts
9825
- var VALID_HOST_RE3 = /^[a-z\d][a-z\d.\-]*$/i;
10097
+ var VALID_HOST_RE4 = /^[a-z\d][a-z\d.\-]*$/i;
9826
10098
  function validateGatewayHost(host) {
9827
10099
  if (!host) {
9828
10100
  throw new Error("Invalid GATEWAY_HOST: value is empty");
9829
10101
  }
9830
- if (!VALID_HOST_RE3.test(host)) {
10102
+ if (!VALID_HOST_RE4.test(host)) {
9831
10103
  throw new Error(`Invalid GATEWAY_HOST: value is not a valid hostname (must match ${String.raw`[A-Za-z0-9][A-Za-z0-9.\-]*`})`);
9832
10104
  }
9833
10105
  return host;
@@ -9838,7 +10110,7 @@ var MITMPROXY_CERTS_VOLUME = "mitmproxy-certs";
9838
10110
  var CA_CERT_FILE = "mitmproxy-ca-cert.pem";
9839
10111
  var CA_KEY_FILE = "mitmproxy-ca.pem";
9840
10112
  var DEFAULT_OUTPUT = "apps/gateway/.local/mitmproxy-ca.tar";
9841
- var SENSITIVE_WARNING2 = "Warning: Output contains the mitmproxy CA private trust anchor; treat as sensitive.";
10113
+ var SENSITIVE_WARNING3 = "Warning: Output contains the mitmproxy CA private trust anchor; treat as sensitive.";
9842
10114
  function defaultSpawn2(cmd, opts) {
9843
10115
  return Bun.spawn(cmd, opts);
9844
10116
  }
@@ -9906,7 +10178,7 @@ async function backupGatewayCa(opts, spawn = defaultSpawn2, printErr) {
9906
10178
  }
9907
10179
  const warn = printErr ?? ((msg) => process.stderr.write(`${msg}
9908
10180
  `));
9909
- warn(SENSITIVE_WARNING2);
10181
+ warn(SENSITIVE_WARNING3);
9910
10182
  return { ok: true, output: opts.output, bytesWritten: tarBytes.byteLength };
9911
10183
  }
9912
10184
  async function gatewayBackupAction(options, ctx, spawn) {
@@ -9929,14 +10201,14 @@ async function gatewayBackupAction(options, ctx, spawn) {
9929
10201
  ctx.console.log(`CA backup written to: ${output}`);
9930
10202
  }
9931
10203
  function registerGatewayBackup(cli) {
9932
- cli.command("gateway backup", "Back up the mitmproxy CA certificate and private key from the gateway Docker volume to a local tarball. The output contains the CA private trust anchor \u2014 treat it as sensitive material.").option("--output [file]", z16.string().default(DEFAULT_OUTPUT).describe("Local path to write the CA tarball. File permissions are set to 0600 after writing.")).option("--include-ca", z16.boolean().default(true).describe("Include the mitmproxy CA certificate and private key in the backup. Currently the only supported backup target.")).example("# Back up the CA to the default path").example("infra gateway backup --include-ca").example("# Back up the CA to a custom path").example("infra gateway backup --output /secure/backup/mitmproxy-ca.tar").action(gatewayBackupAction);
10204
+ cli.command("gateway backup", "Back up the mitmproxy CA certificate and private key from the gateway Docker volume to a local tarball. The output contains the CA private trust anchor \u2014 treat it as sensitive material.").option("--output [file]", z19.string().default(DEFAULT_OUTPUT).describe("Local path to write the CA tarball. File permissions are set to 0600 after writing.")).option("--include-ca", z19.boolean().default(true).describe("Include the mitmproxy CA certificate and private key in the backup. Currently the only supported backup target.")).example("# Back up the CA to the default path").example("infra gateway backup --include-ca").example("# Back up the CA to a custom path").example("infra gateway backup --output /secure/backup/mitmproxy-ca.tar").action(gatewayBackupAction);
9933
10205
  }
9934
10206
 
9935
10207
  // src/commands/gateway/deploy.ts
9936
- import { z as z17 } from "zod";
9937
- var REPO3 = "marcusrbrown/infra";
9938
- var WORKFLOW_NAME3 = "Deploy Gateway";
9939
- var WORKFLOW_URL3 = "https://github.com/marcusrbrown/infra/actions/workflows/deploy-gateway.yaml";
10208
+ import { z as z20 } from "zod";
10209
+ var REPO4 = "marcusrbrown/infra";
10210
+ var WORKFLOW_NAME4 = "Deploy Gateway";
10211
+ var WORKFLOW_URL4 = "https://github.com/marcusrbrown/infra/actions/workflows/deploy-gateway.yaml";
9940
10212
  function getGatewayDeployEnv() {
9941
10213
  const path = process.env.PATH;
9942
10214
  const home = process.env.HOME;
@@ -9998,7 +10270,7 @@ function validateGatewayRemotePreconditions() {
9998
10270
  }
9999
10271
  }
10000
10272
  function registerGatewayDeploy(cli) {
10001
- cli.command("gateway deploy", "Deploy the gateway. Default mode triggers the GitHub Deploy Gateway workflow, while --local runs apps/gateway deploy directly with Bun.").option("--local", z17.boolean().default(false).describe("Run local deployment with Bun using apps/gateway instead of triggering GitHub Actions.")).option("--dry-run", z17.boolean().default(false).describe("Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.")).option("--force-recreate", z17.boolean().default(false).describe("Force recreate containers. Forwarded only in local mode.")).example("# Trigger remote GitHub Actions deploy (default mode)").example("infra gateway deploy").example("# Validate local deploy preconditions and planned command").example("infra gateway deploy --local --dry-run").example("# Run local deploy with explicit SSH agent context").example("infra gateway deploy --local").action(async (options) => {
10273
+ cli.command("gateway deploy", "Deploy the gateway. Default mode triggers the GitHub Deploy Gateway workflow, while --local runs apps/gateway deploy directly with Bun.").option("--local", z20.boolean().default(false).describe("Run local deployment with Bun using apps/gateway instead of triggering GitHub Actions.")).option("--dry-run", z20.boolean().default(false).describe("Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.")).option("--force-recreate", z20.boolean().default(false).describe("Force recreate containers. Forwarded only in local mode.")).example("# Trigger remote GitHub Actions deploy (default mode)").example("infra gateway deploy").example("# Validate local deploy preconditions and planned command").example("infra gateway deploy --local --dry-run").example("# Run local deploy with explicit SSH agent context").example("infra gateway deploy --local").action(async (options) => {
10002
10274
  if (options.local) {
10003
10275
  const command = [
10004
10276
  "bun",
@@ -10028,37 +10300,37 @@ function registerGatewayDeploy(cli) {
10028
10300
  validateGatewayRemotePreconditions();
10029
10301
  if (options.dryRun) {
10030
10302
  console.log("Dry run: remote gateway deploy");
10031
- console.log(`- command: gh workflow run "${WORKFLOW_NAME3}" --repo ${REPO3}`);
10032
- console.log(`- workflow URL: ${WORKFLOW_URL3}`);
10303
+ console.log(`- command: gh workflow run "${WORKFLOW_NAME4}" --repo ${REPO4}`);
10304
+ console.log(`- workflow URL: ${WORKFLOW_URL4}`);
10033
10305
  return;
10034
10306
  }
10035
- const child = Bun.spawn(["gh", "workflow", "run", WORKFLOW_NAME3, "--repo", REPO3], {
10307
+ const child = Bun.spawn(["gh", "workflow", "run", WORKFLOW_NAME4, "--repo", REPO4], {
10036
10308
  stdout: "inherit",
10037
10309
  stderr: "inherit"
10038
10310
  });
10039
10311
  const exitCode = await child.exited;
10040
10312
  if (exitCode !== 0) {
10041
- throw new Error(`Failed to trigger "${WORKFLOW_NAME3}" workflow (exit code ${exitCode})`);
10313
+ throw new Error(`Failed to trigger "${WORKFLOW_NAME4}" workflow (exit code ${exitCode})`);
10042
10314
  }
10043
- console.log(`Workflow triggered: ${WORKFLOW_URL3}`);
10315
+ console.log(`Workflow triggered: ${WORKFLOW_URL4}`);
10044
10316
  console.log("Approve the gateway environment deployment in GitHub Actions to continue.");
10045
10317
  });
10046
10318
  }
10047
10319
 
10048
10320
  // src/commands/gateway/logs.ts
10049
- import { z as z18 } from "zod";
10321
+ import { z as z21 } from "zod";
10050
10322
  var COMPOSE_PROJECT_DIR3 = "/opt/gateway/deploy";
10051
- var SENSITIVE_WARNING3 = "Warning: Logs may contain Discord tokens, S3 credentials, or user data. Treat output as sensitive; do not capture in shared logs or chat.";
10323
+ var SENSITIVE_WARNING4 = "Warning: Logs may contain Discord tokens, S3 credentials, or user data. Treat output as sensitive; do not capture in shared logs or chat.";
10052
10324
  var VALID_SERVICES2 = ["gateway", "workspace", "mitmproxy"];
10053
10325
  function validateService2(service) {
10054
10326
  if (!VALID_SERVICES2.includes(service)) {
10055
10327
  throw new Error(`Invalid service "${service}". Valid services: ${VALID_SERVICES2.join(", ")}`);
10056
10328
  }
10057
10329
  }
10058
- function defaultLogsSpawn2(cmd, opts) {
10330
+ function defaultLogsSpawn3(cmd, opts) {
10059
10331
  return Bun.spawn(cmd, opts);
10060
10332
  }
10061
- async function streamGatewayLogs(opts, spawn = defaultLogsSpawn2, printOut, printErr) {
10333
+ async function streamGatewayLogs(opts, spawn = defaultLogsSpawn3, printOut, printErr) {
10062
10334
  const isCI = process.env.CI === "true";
10063
10335
  if (isCI && !opts.allowCi) {
10064
10336
  const msg = "Refusing to stream logs in CI without --allow-ci. Logs may contain sensitive tokens or user data.";
@@ -10070,9 +10342,9 @@ async function streamGatewayLogs(opts, spawn = defaultLogsSpawn2, printOut, prin
10070
10342
  return { refused: true };
10071
10343
  }
10072
10344
  if (printErr) {
10073
- printErr(SENSITIVE_WARNING3);
10345
+ printErr(SENSITIVE_WARNING4);
10074
10346
  } else {
10075
- console.error(SENSITIVE_WARNING3);
10347
+ console.error(SENSITIVE_WARNING4);
10076
10348
  }
10077
10349
  validateGatewayHost(opts.host);
10078
10350
  const sshCmd = [
@@ -10095,7 +10367,7 @@ async function streamGatewayLogs(opts, spawn = defaultLogsSpawn2, printOut, prin
10095
10367
  return { refused: false, exitCode };
10096
10368
  }
10097
10369
  function registerGatewayLogs(cli) {
10098
- cli.command("gateway logs [service]", "Stream logs from a gateway service via SSH and docker compose.").option("--tail [n]", z18.number().default(100).describe("Number of log lines to tail from each service.")).option("--allow-ci", z18.boolean().default(false).describe("Allow log streaming in CI environments. Logs may contain sensitive credentials.")).option("--key [key]", z18.string().describe("Environment variable name holding the SSH host. Falls back to GATEWAY_HOST when omitted.")).action(async (service, options) => {
10370
+ cli.command("gateway logs [service]", "Stream logs from a gateway service via SSH and docker compose.").option("--tail [n]", z21.number().default(100).describe("Number of log lines to tail from each service.")).option("--allow-ci", z21.boolean().default(false).describe("Allow log streaming in CI environments. Logs may contain sensitive credentials.")).option("--key [key]", z21.string().describe("Environment variable name holding the SSH host. Falls back to GATEWAY_HOST when omitted.")).action(async (service, options) => {
10099
10371
  const targetService = service ?? "gateway";
10100
10372
  try {
10101
10373
  validateService2(targetService);
@@ -10125,7 +10397,7 @@ function registerGatewayLogs(cli) {
10125
10397
  }
10126
10398
 
10127
10399
  // src/commands/gateway/restore.ts
10128
- import { z as z19 } from "zod";
10400
+ import { z as z22 } from "zod";
10129
10401
  var MITMPROXY_CERTS_VOLUME2 = "mitmproxy-certs";
10130
10402
  var CA_CERT_FILE2 = "mitmproxy-ca-cert.pem";
10131
10403
  var CA_KEY_FILE2 = "mitmproxy-ca.pem";
@@ -10279,7 +10551,7 @@ async function restoreGatewayCa(opts, spawn = defaultSpawn3) {
10279
10551
  }
10280
10552
  }
10281
10553
  function registerGatewayRestore(cli) {
10282
- cli.command("gateway restore", "Restore the mitmproxy CA certificate and private key from a local tarball into the gateway Docker volume. Restarts mitmproxy and gateway services after restore and confirms byte-equal content before reporting success.").option("--input <file>", z19.string().describe("Path to the tarball produced by `gateway backup --include-ca`. Required.")).option("--include-ca", z19.boolean().default(true).describe("Restore the mitmproxy CA certificate and private key. Currently the only supported restore target.")).example("# Restore CA from a backup tarball").example("infra gateway restore --input apps/gateway/.local/mitmproxy-ca.tar").example("# Restore CA from a custom path").example("infra gateway restore --input /secure/backup/mitmproxy-ca.tar --include-ca").action(async (options) => {
10554
+ cli.command("gateway restore", "Restore the mitmproxy CA certificate and private key from a local tarball into the gateway Docker volume. Restarts mitmproxy and gateway services after restore and confirms byte-equal content before reporting success.").option("--input <file>", z22.string().describe("Path to the tarball produced by `gateway backup --include-ca`. Required.")).option("--include-ca", z22.boolean().default(true).describe("Restore the mitmproxy CA certificate and private key. Currently the only supported restore target.")).example("# Restore CA from a backup tarball").example("infra gateway restore --input apps/gateway/.local/mitmproxy-ca.tar").example("# Restore CA from a custom path").example("infra gateway restore --input /secure/backup/mitmproxy-ca.tar --include-ca").action(async (options) => {
10283
10555
  const hostEnvKey = "GATEWAY_HOST";
10284
10556
  const host = process.env[hostEnvKey];
10285
10557
  if (!host) {
@@ -10300,7 +10572,7 @@ function registerGatewayRestore(cli) {
10300
10572
  }
10301
10573
 
10302
10574
  // src/commands/gateway/status.ts
10303
- import { z as z20 } from "zod";
10575
+ import { z as z23 } from "zod";
10304
10576
  var COMPOSE_PROJECT_DIR5 = "/opt/gateway/deploy";
10305
10577
  function parseComposePsOutput2(stdoutText) {
10306
10578
  const trimmed = stdoutText.trim();
@@ -10442,7 +10714,7 @@ async function gatewayStatusAction(options, ctx, spawn) {
10442
10714
  }
10443
10715
  }
10444
10716
  function registerGatewayStatus(cli) {
10445
- cli.command("gateway status", "Show operational health of the gateway deployment via docker compose ps.").option("--key [key]", z20.string().describe("Environment variable name holding the SSH host. Falls back to GATEWAY_HOST when omitted.")).action((options, ctx) => gatewayStatusAction(options, ctx));
10717
+ cli.command("gateway status", "Show operational health of the gateway deployment via docker compose ps.").option("--key [key]", z23.string().describe("Environment variable name holding the SSH host. Falls back to GATEWAY_HOST when omitted.")).action((options, ctx) => gatewayStatusAction(options, ctx));
10446
10718
  }
10447
10719
 
10448
10720
  // src/commands/gateway/index.ts
@@ -10455,22 +10727,22 @@ function registerGatewayCommands(cli) {
10455
10727
  }
10456
10728
 
10457
10729
  // src/commands/keeweb/deploy.ts
10458
- import { existsSync as existsSync4 } from "fs";
10459
- import { join as join5 } from "path";
10460
- import { z as z21 } from "zod";
10461
- var REPO4 = "marcusrbrown/infra";
10462
- var WORKFLOW_NAME4 = "Deploy KeeWeb";
10463
- var WORKFLOW_URL4 = "https://github.com/marcusrbrown/infra/actions/workflows/deploy-keeweb.yaml";
10730
+ import { existsSync as existsSync5 } from "fs";
10731
+ import { join as join6 } from "path";
10732
+ import { z as z24 } from "zod";
10733
+ var REPO5 = "marcusrbrown/infra";
10734
+ var WORKFLOW_NAME5 = "Deploy KeeWeb";
10735
+ var WORKFLOW_URL5 = "https://github.com/marcusrbrown/infra/actions/workflows/deploy-keeweb.yaml";
10464
10736
  var DEFAULT_HOST3 = "box.heatvision.co";
10465
10737
  var DEFAULT_REMOTE_USER3 = "deploy-kw";
10466
10738
  var DEFAULT_SITE_DIR = "/home/user-data/www/kw.igg.ms";
10467
10739
  function resolveDeployScriptPath() {
10468
- return join5(findRepoRoot(), "apps", "keeweb", "deploy.sh");
10740
+ return join6(findRepoRoot(), "apps", "keeweb", "deploy.sh");
10469
10741
  }
10470
10742
  function resolveDistIndexPath() {
10471
- return join5(findRepoRoot(), "apps", "keeweb", "dist", "index.html");
10743
+ return join6(findRepoRoot(), "apps", "keeweb", "dist", "index.html");
10472
10744
  }
10473
- function getLocalDeployEnv2() {
10745
+ function getLocalDeployEnv3() {
10474
10746
  const path = process.env.PATH;
10475
10747
  const home = process.env.HOME;
10476
10748
  const sshAuthSock = process.env.SSH_AUTH_SOCK;
@@ -10492,26 +10764,26 @@ function getLocalDeployEnv2() {
10492
10764
  HOME: home
10493
10765
  };
10494
10766
  }
10495
- function validateLocalPreconditions2(options) {
10767
+ function validateLocalPreconditions3(options) {
10496
10768
  const deployScriptPath = resolveDeployScriptPath();
10497
- if (!existsSync4(deployScriptPath)) {
10769
+ if (!existsSync5(deployScriptPath)) {
10498
10770
  throw new Error(`deploy.sh not found at expected path: ${deployScriptPath}`);
10499
10771
  }
10500
10772
  const distIndexPath = resolveDistIndexPath();
10501
- if (!existsSync4(distIndexPath)) {
10773
+ if (!existsSync5(distIndexPath)) {
10502
10774
  throw new Error(`Local deploy requires built assets. Missing: ${distIndexPath}`);
10503
10775
  }
10504
- getLocalDeployEnv2();
10776
+ getLocalDeployEnv3();
10505
10777
  if (options.nginx) {}
10506
10778
  return { deployScriptPath, distIndexPath };
10507
10779
  }
10508
- function validateRemotePreconditions2() {
10780
+ function validateRemotePreconditions3() {
10509
10781
  if (!Bun.which("gh")) {
10510
10782
  throw new Error("gh CLI is required for remote deploy. Install gh and run `gh auth login`.");
10511
10783
  }
10512
10784
  }
10513
10785
  function registerKeewebDeploy(cli) {
10514
- cli.command("keeweb deploy", "Trigger KeeWeb deployment. Default mode dispatches the GitHub Deploy KeeWeb workflow. Use --local to run apps/keeweb/deploy.sh directly from this repo.").option("--local", z21.boolean().default(false).describe("Run local deployment using apps/keeweb/deploy.sh instead of triggering GitHub Actions.")).option("--nginx", z21.boolean().default(false).describe("Include nginx config deployment. Valid only with --local and passed through to deploy.sh as --nginx.")).option("--dry-run", z21.boolean().default(false).describe("Print planned actions without validating preconditions, executing deploy.sh, or triggering GitHub Actions.")).example("# Trigger GitHub Deploy KeeWeb workflow (default mode)").example("infra keeweb deploy").example("# Preview local deploy plan without side effects").example("infra keeweb deploy --local --dry-run").example("# Run local deploy including nginx config update").example("infra keeweb deploy --local --nginx").action(async (options) => {
10786
+ cli.command("keeweb deploy", "Trigger KeeWeb deployment. Default mode dispatches the GitHub Deploy KeeWeb workflow. Use --local to run apps/keeweb/deploy.sh directly from this repo.").option("--local", z24.boolean().default(false).describe("Run local deployment using apps/keeweb/deploy.sh instead of triggering GitHub Actions.")).option("--nginx", z24.boolean().default(false).describe("Include nginx config deployment. Valid only with --local and passed through to deploy.sh as --nginx.")).option("--dry-run", z24.boolean().default(false).describe("Print planned actions without validating preconditions, executing deploy.sh, or triggering GitHub Actions.")).example("# Trigger GitHub Deploy KeeWeb workflow (default mode)").example("infra keeweb deploy").example("# Preview local deploy plan without side effects").example("infra keeweb deploy --local --dry-run").example("# Run local deploy including nginx config update").example("infra keeweb deploy --local --nginx").action(async (options) => {
10515
10787
  if (options.nginx && !options.local) {
10516
10788
  throw new Error("--nginx is only valid with --local");
10517
10789
  }
@@ -10528,8 +10800,8 @@ function registerKeewebDeploy(cli) {
10528
10800
  console.log(`- command: ${args2.join(" ")}`);
10529
10801
  return;
10530
10802
  }
10531
- const { deployScriptPath } = validateLocalPreconditions2({ nginx: options.nginx });
10532
- const env = getLocalDeployEnv2();
10803
+ const { deployScriptPath } = validateLocalPreconditions3({ nginx: options.nginx });
10804
+ const env = getLocalDeployEnv3();
10533
10805
  const args = ["bash", deployScriptPath];
10534
10806
  if (options.nginx) {
10535
10807
  args.push("--nginx");
@@ -10547,14 +10819,14 @@ function registerKeewebDeploy(cli) {
10547
10819
  }
10548
10820
  if (options.dryRun) {
10549
10821
  console.log("Dry run: remote KeeWeb deploy");
10550
- console.log(`- command: gh workflow run ${WORKFLOW_NAME4} --repo ${REPO4}`);
10551
- console.log(`- workflow URL: ${WORKFLOW_URL4}`);
10822
+ console.log(`- command: gh workflow run ${WORKFLOW_NAME5} --repo ${REPO5}`);
10823
+ console.log(`- workflow URL: ${WORKFLOW_URL5}`);
10552
10824
  return;
10553
10825
  }
10554
- validateRemotePreconditions2();
10826
+ validateRemotePreconditions3();
10555
10827
  console.warn("Warning: the Deploy KeeWeb workflow includes nginx config deployment as part of workflow_dispatch logic.");
10556
10828
  console.warn("Warning: the workflow requires keeweb environment approval before jobs execute.");
10557
- const child = Bun.spawn(["gh", "workflow", "run", WORKFLOW_NAME4, "--repo", REPO4], {
10829
+ const child = Bun.spawn(["gh", "workflow", "run", WORKFLOW_NAME5, "--repo", REPO5], {
10558
10830
  stdout: "inherit",
10559
10831
  stderr: "inherit"
10560
10832
  });
@@ -10562,7 +10834,7 @@ function registerKeewebDeploy(cli) {
10562
10834
  if (exitCode !== 0) {
10563
10835
  throw new Error(`Failed to trigger Deploy KeeWeb workflow (exit code ${exitCode})`);
10564
10836
  }
10565
- console.log(`Workflow triggered: ${WORKFLOW_URL4}`);
10837
+ console.log(`Workflow triggered: ${WORKFLOW_URL5}`);
10566
10838
  console.log("Approve the keeweb environment deployment in GitHub Actions to continue.");
10567
10839
  });
10568
10840
  }
@@ -10589,14 +10861,14 @@ function registerKeewebOpen(cli) {
10589
10861
  }
10590
10862
 
10591
10863
  // src/commands/keeweb/status.ts
10592
- import { join as join6 } from "path";
10593
- import { z as z22 } from "zod";
10864
+ import { join as join7 } from "path";
10865
+ import { z as z25 } from "zod";
10594
10866
  var SITE_URL = "https://kw.igg.ms/";
10595
10867
  var GH_REPO = "marcusrbrown/infra";
10596
- var HTTP_TIMEOUT_MS3 = 1e4;
10597
- var ghRunSchema = z22.array(z22.object({
10598
- createdAt: z22.string().min(1),
10599
- url: z22.url()
10868
+ var HTTP_TIMEOUT_MS4 = 1e4;
10869
+ var ghRunSchema = z25.array(z25.object({
10870
+ createdAt: z25.string().min(1),
10871
+ url: z25.url()
10600
10872
  }));
10601
10873
  function levelLabel2(level) {
10602
10874
  if (level === "ok") {
@@ -10635,7 +10907,7 @@ async function checkHttpReachability2(verbose) {
10635
10907
  const startedAt = performance.now();
10636
10908
  try {
10637
10909
  const response = await fetch(SITE_URL, {
10638
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS3)
10910
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS4)
10639
10911
  });
10640
10912
  const elapsedMs = performance.now() - startedAt;
10641
10913
  const details = [];
@@ -10662,7 +10934,7 @@ async function checkHttpReachability2(verbose) {
10662
10934
  title: "HTTP reachability",
10663
10935
  level: "error",
10664
10936
  summary: `Request failed after ${formatDurationMs2(elapsedMs)}: ${message}`,
10665
- details: verbose ? [`URL: ${SITE_URL}`, `Timeout: ${HTTP_TIMEOUT_MS3}ms`] : undefined
10937
+ details: verbose ? [`URL: ${SITE_URL}`, `Timeout: ${HTTP_TIMEOUT_MS4}ms`] : undefined
10666
10938
  };
10667
10939
  }
10668
10940
  }
@@ -10736,7 +11008,7 @@ async function checkLastDeploy(verbose) {
10736
11008
  }
10737
11009
  }
10738
11010
  async function isSourceCheckout(root) {
10739
- const sourceMarker = join6(root, "apps", "keeweb", "src", "build.ts");
11011
+ const sourceMarker = join7(root, "apps", "keeweb", "src", "build.ts");
10740
11012
  return Bun.file(sourceMarker).exists();
10741
11013
  }
10742
11014
  async function checkContentHash(verbose, repoRoot) {
@@ -10755,7 +11027,7 @@ async function checkContentHash(verbose, repoRoot) {
10755
11027
  } else {
10756
11028
  root = repoRoot;
10757
11029
  }
10758
- const distIndexPath = join6(root, "apps", "keeweb", "dist", "index.html");
11030
+ const distIndexPath = join7(root, "apps", "keeweb", "dist", "index.html");
10759
11031
  const sourceCheckout = await isSourceCheckout(root);
10760
11032
  if (!sourceCheckout) {
10761
11033
  return {
@@ -10777,7 +11049,7 @@ async function checkContentHash(verbose, repoRoot) {
10777
11049
  }
10778
11050
  try {
10779
11051
  const response = await fetch(SITE_URL, {
10780
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS3)
11052
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS4)
10781
11053
  });
10782
11054
  if (!response.ok) {
10783
11055
  return {
@@ -10883,6 +11155,7 @@ var MCP_ALLOWLIST = new Set([
10883
11155
  "umami status",
10884
11156
  "dashboard status",
10885
11157
  "vpn status",
11158
+ "broker status",
10886
11159
  "status"
10887
11160
  ]);
10888
11161
  function registerMcp(cli) {
@@ -10893,13 +11166,13 @@ function registerMcp(cli) {
10893
11166
  }
10894
11167
 
10895
11168
  // src/commands/status.ts
10896
- import { z as z31 } from "zod";
11169
+ import { z as z34 } from "zod";
10897
11170
 
10898
11171
  // src/commands/umami/deploy.ts
10899
- import { z as z23 } from "zod";
10900
- var REPO5 = "marcusrbrown/infra";
10901
- var WORKFLOW_NAME5 = "Deploy Umami";
10902
- var WORKFLOW_URL5 = "https://github.com/marcusrbrown/infra/actions/workflows/deploy-umami.yaml";
11172
+ import { z as z26 } from "zod";
11173
+ var REPO6 = "marcusrbrown/infra";
11174
+ var WORKFLOW_NAME6 = "Deploy Umami";
11175
+ var WORKFLOW_URL6 = "https://github.com/marcusrbrown/infra/actions/workflows/deploy-umami.yaml";
10903
11176
  function getUmamiDeployEnv() {
10904
11177
  const path = process.env.PATH;
10905
11178
  const home = process.env.HOME;
@@ -10931,7 +11204,7 @@ function validateUmamiRemotePreconditions() {
10931
11204
  }
10932
11205
  }
10933
11206
  function registerUmamiDeploy(cli) {
10934
- cli.command("umami deploy", "Deploy Umami analytics. Default mode triggers the GitHub Deploy Umami workflow, while --local runs apps/umami deploy directly with Bun.").option("--local", z23.boolean().default(false).describe("Run local deployment with Bun using apps/umami instead of triggering GitHub Actions.")).option("--dry-run", z23.boolean().default(false).describe("Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.")).example("# Trigger remote GitHub Actions deploy (default mode)").example("infra umami deploy").example("# Validate local deploy preconditions and planned command").example("infra umami deploy --local --dry-run").example("# Run local deploy with explicit SSH agent context").example("infra umami deploy --local").action(async (options) => {
11207
+ cli.command("umami deploy", "Deploy Umami analytics. Default mode triggers the GitHub Deploy Umami workflow, while --local runs apps/umami deploy directly with Bun.").option("--local", z26.boolean().default(false).describe("Run local deployment with Bun using apps/umami instead of triggering GitHub Actions.")).option("--dry-run", z26.boolean().default(false).describe("Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.")).example("# Trigger remote GitHub Actions deploy (default mode)").example("infra umami deploy").example("# Validate local deploy preconditions and planned command").example("infra umami deploy --local --dry-run").example("# Run local deploy with explicit SSH agent context").example("infra umami deploy --local").action(async (options) => {
10935
11208
  if (options.local) {
10936
11209
  const command = ["bun", "run", "--cwd", "apps/umami", "deploy"];
10937
11210
  if (options.dryRun) {
@@ -10954,33 +11227,33 @@ function registerUmamiDeploy(cli) {
10954
11227
  validateUmamiRemotePreconditions();
10955
11228
  if (options.dryRun) {
10956
11229
  console.log("Dry run: remote umami deploy");
10957
- console.log(`- command: gh workflow run "${WORKFLOW_NAME5}" --repo ${REPO5}`);
10958
- console.log(`- workflow URL: ${WORKFLOW_URL5}`);
11230
+ console.log(`- command: gh workflow run "${WORKFLOW_NAME6}" --repo ${REPO6}`);
11231
+ console.log(`- workflow URL: ${WORKFLOW_URL6}`);
10959
11232
  return;
10960
11233
  }
10961
- const child = Bun.spawn(["gh", "workflow", "run", WORKFLOW_NAME5, "--repo", REPO5], {
11234
+ const child = Bun.spawn(["gh", "workflow", "run", WORKFLOW_NAME6, "--repo", REPO6], {
10962
11235
  stdout: "inherit",
10963
11236
  stderr: "inherit"
10964
11237
  });
10965
11238
  const exitCode = await child.exited;
10966
11239
  if (exitCode !== 0) {
10967
- throw new Error(`Failed to trigger "${WORKFLOW_NAME5}" workflow (exit code ${exitCode})`);
11240
+ throw new Error(`Failed to trigger "${WORKFLOW_NAME6}" workflow (exit code ${exitCode})`);
10968
11241
  }
10969
- console.log(`Workflow triggered: ${WORKFLOW_URL5}`);
11242
+ console.log(`Workflow triggered: ${WORKFLOW_URL6}`);
10970
11243
  console.log("Approve the umami environment deployment in GitHub Actions to continue.");
10971
11244
  });
10972
11245
  }
10973
11246
 
10974
11247
  // src/commands/umami/logs.ts
10975
- import { z as z24 } from "zod";
11248
+ import { z as z27 } from "zod";
10976
11249
 
10977
11250
  // src/commands/umami/host.ts
10978
- var VALID_HOST_RE4 = /^[a-z\d][a-z\d.\-]*$/i;
11251
+ var VALID_HOST_RE5 = /^[a-z\d][a-z\d.\-]*$/i;
10979
11252
  function validateUmamiHost(host) {
10980
11253
  if (!host) {
10981
11254
  throw new Error("Invalid UMAMI_DOMAIN: value is empty");
10982
11255
  }
10983
- if (!VALID_HOST_RE4.test(host)) {
11256
+ if (!VALID_HOST_RE5.test(host)) {
10984
11257
  throw new Error(`Invalid UMAMI_DOMAIN: value is not a valid hostname (must match ${String.raw`[A-Za-z0-9][A-Za-z0-9.\-]*`})`);
10985
11258
  }
10986
11259
  return host;
@@ -10988,17 +11261,17 @@ function validateUmamiHost(host) {
10988
11261
 
10989
11262
  // src/commands/umami/logs.ts
10990
11263
  var COMPOSE_PROJECT_DIR6 = "/opt/umami";
10991
- var SENSITIVE_WARNING4 = "Warning: Logs may contain database passwords, app secrets, or user data. Treat output as sensitive; do not capture in shared logs or chat.";
11264
+ var SENSITIVE_WARNING5 = "Warning: Logs may contain database passwords, app secrets, or user data. Treat output as sensitive; do not capture in shared logs or chat.";
10992
11265
  var VALID_SERVICES3 = ["umami", "db", "caddy"];
10993
11266
  function validateService3(service) {
10994
11267
  if (!VALID_SERVICES3.includes(service)) {
10995
11268
  throw new Error(`Invalid service "${service}". Valid services: ${VALID_SERVICES3.join(", ")}`);
10996
11269
  }
10997
11270
  }
10998
- function defaultLogsSpawn3(cmd, opts) {
11271
+ function defaultLogsSpawn4(cmd, opts) {
10999
11272
  return Bun.spawn(cmd, opts);
11000
11273
  }
11001
- async function streamUmamiLogs(opts, spawn = defaultLogsSpawn3, printOut, printErr) {
11274
+ async function streamUmamiLogs(opts, spawn = defaultLogsSpawn4, printOut, printErr) {
11002
11275
  validateService3(opts.service);
11003
11276
  const isCI = process.env.CI === "true";
11004
11277
  if (isCI && !opts.allowCi) {
@@ -11011,9 +11284,9 @@ async function streamUmamiLogs(opts, spawn = defaultLogsSpawn3, printOut, printE
11011
11284
  return { refused: true };
11012
11285
  }
11013
11286
  if (printErr) {
11014
- printErr(SENSITIVE_WARNING4);
11287
+ printErr(SENSITIVE_WARNING5);
11015
11288
  } else {
11016
- console.error(SENSITIVE_WARNING4);
11289
+ console.error(SENSITIVE_WARNING5);
11017
11290
  }
11018
11291
  validateUmamiHost(opts.host);
11019
11292
  const sshCmd = [
@@ -11036,7 +11309,7 @@ async function streamUmamiLogs(opts, spawn = defaultLogsSpawn3, printOut, printE
11036
11309
  return { refused: false, exitCode };
11037
11310
  }
11038
11311
  function registerUmamiLogs(cli) {
11039
- cli.command("umami logs [service]", "Stream logs from an Umami service via SSH and docker compose.").option("--tail [n]", z24.number().default(100).describe("Number of log lines to tail from each service.")).option("--allow-ci", z24.boolean().default(false).describe("Allow log streaming in CI environments. Logs may contain sensitive credentials.")).option("--key [key]", z24.string().describe("Environment variable name holding the SSH host. Falls back to UMAMI_DOMAIN when omitted.")).action(async (service, options) => {
11312
+ cli.command("umami logs [service]", "Stream logs from an Umami service via SSH and docker compose.").option("--tail [n]", z27.number().default(100).describe("Number of log lines to tail from each service.")).option("--allow-ci", z27.boolean().default(false).describe("Allow log streaming in CI environments. Logs may contain sensitive credentials.")).option("--key [key]", z27.string().describe("Environment variable name holding the SSH host. Falls back to UMAMI_DOMAIN when omitted.")).action(async (service, options) => {
11040
11313
  const targetService = service ?? "umami";
11041
11314
  try {
11042
11315
  validateService3(targetService);
@@ -11066,7 +11339,7 @@ function registerUmamiLogs(cli) {
11066
11339
  }
11067
11340
 
11068
11341
  // src/commands/umami/status.ts
11069
- import { z as z25 } from "zod";
11342
+ import { z as z28 } from "zod";
11070
11343
  var COMPOSE_PROJECT_DIR7 = "/opt/umami";
11071
11344
  function parseComposePsOutput3(stdoutText) {
11072
11345
  const trimmed = stdoutText.trim();
@@ -11237,7 +11510,7 @@ async function umamiStatusAction(options, ctx, spawn) {
11237
11510
  }
11238
11511
  }
11239
11512
  function registerUmamiStatus(cli) {
11240
- cli.command("umami status", "Show operational health of the Umami analytics deployment via docker compose ps.").option("--key [key]", z25.string().optional().describe("Environment variable name holding the SSH host. Falls back to UMAMI_DOMAIN when omitted.")).action((options, ctx) => umamiStatusAction(options, ctx));
11513
+ cli.command("umami status", "Show operational health of the Umami analytics deployment via docker compose ps.").option("--key [key]", z28.string().optional().describe("Environment variable name holding the SSH host. Falls back to UMAMI_DOMAIN when omitted.")).action((options, ctx) => umamiStatusAction(options, ctx));
11241
11514
  }
11242
11515
 
11243
11516
  // src/commands/umami/index.ts
@@ -11249,25 +11522,25 @@ function registerUmamiCommands(cli) {
11249
11522
 
11250
11523
  // src/commands/vpn/client.ts
11251
11524
  import { mkdir, writeFile as writeFile2 } from "fs/promises";
11252
- import { join as join7, resolve as resolve2 } from "path";
11253
- import { z as z27 } from "zod";
11525
+ import { join as join8, resolve as resolve2 } from "path";
11526
+ import { z as z30 } from "zod";
11254
11527
 
11255
11528
  // src/commands/vpn/peers.ts
11256
11529
  import { readFile, writeFile } from "fs/promises";
11257
- import { z as z26 } from "zod";
11258
- var peerSchema = z26.object({
11259
- name: z26.string(),
11260
- publicKey: z26.string(),
11261
- tunnelIp: z26.string()
11530
+ import { z as z29 } from "zod";
11531
+ var peerSchema = z29.object({
11532
+ name: z29.string(),
11533
+ publicKey: z29.string(),
11534
+ tunnelIp: z29.string()
11262
11535
  });
11263
- var peersFileSchema = z26.object({
11264
- peers: z26.array(peerSchema)
11536
+ var peersFileSchema = z29.object({
11537
+ peers: z29.array(peerSchema)
11265
11538
  }).superRefine((data, ctx) => {
11266
11539
  const seenIps = new Set;
11267
11540
  for (const peer of data.peers) {
11268
11541
  if (seenIps.has(peer.tunnelIp)) {
11269
11542
  ctx.addIssue({
11270
- code: z26.ZodIssueCode.custom,
11543
+ code: z29.ZodIssueCode.custom,
11271
11544
  message: `Duplicate tunnel IP "${peer.tunnelIp}" \u2014 each peer must have a unique tunnel IP.`,
11272
11545
  path: ["peers"]
11273
11546
  });
@@ -11279,7 +11552,7 @@ var peersFileSchema = z26.object({
11279
11552
  for (const peer of data.peers) {
11280
11553
  if (seenNames.has(peer.name)) {
11281
11554
  ctx.addIssue({
11282
- code: z26.ZodIssueCode.custom,
11555
+ code: z29.ZodIssueCode.custom,
11283
11556
  message: `Duplicate name "${peer.name}" \u2014 each peer must have a unique name.`,
11284
11557
  path: ["peers"]
11285
11558
  });
@@ -11515,13 +11788,13 @@ async function vpnClientRemove(name, peersJsonPath, opts = {}) {
11515
11788
  await syncPeersSecret(peersJson, spawnFn, syncTimeoutMs);
11516
11789
  }
11517
11790
  function resolveDefaultPeersJsonPath() {
11518
- return join7(findRepoRoot(), "apps", "vpn", "config", "peers.json");
11791
+ return join8(findRepoRoot(), "apps", "vpn", "config", "peers.json");
11519
11792
  }
11520
11793
  function resolveDefaultClientsDir() {
11521
- return join7(findRepoRoot(), "apps", "vpn", "clients");
11794
+ return join8(findRepoRoot(), "apps", "vpn", "clients");
11522
11795
  }
11523
11796
  function registerVpnClient(cli) {
11524
- cli.command("vpn client add <name>", "Add a new VPN client peer. Generates a keypair, assigns a tunnel IP, writes the client .conf to apps/vpn/clients/, and syncs the roster to the VPN_PEERS GitHub secret (best-effort).").option("--split-tunnel [cidrs]", z27.string().describe("Comma-separated CIDRs for split-tunnel routing. Defaults to 0.0.0.0/0 (full tunnel).")).option("--dns [dns]", z27.string().describe("DNS server for the client config.")).option("--server-pubkey [key]", z27.string().describe("Server public key. Falls back to VPN_SERVER_PUBKEY env var.")).option("--endpoint [host]", z27.string().describe("VPN server endpoint (IP or hostname). Falls back to VPN_HOST env var.")).example('# Add a new client named "laptop"').example("infra vpn client add laptop").example("# Add a split-tunnel client").example("infra vpn client add laptop --split-tunnel 10.0.0.0/8,192.168.0.0/16").action(async (name, options) => {
11797
+ cli.command("vpn client add <name>", "Add a new VPN client peer. Generates a keypair, assigns a tunnel IP, writes the client .conf to apps/vpn/clients/, and syncs the roster to the VPN_PEERS GitHub secret (best-effort).").option("--split-tunnel [cidrs]", z30.string().describe("Comma-separated CIDRs for split-tunnel routing. Defaults to 0.0.0.0/0 (full tunnel).")).option("--dns [dns]", z30.string().describe("DNS server for the client config.")).option("--server-pubkey [key]", z30.string().describe("Server public key. Falls back to VPN_SERVER_PUBKEY env var.")).option("--endpoint [host]", z30.string().describe("VPN server endpoint (IP or hostname). Falls back to VPN_HOST env var.")).example('# Add a new client named "laptop"').example("infra vpn client add laptop").example("# Add a split-tunnel client").example("infra vpn client add laptop --split-tunnel 10.0.0.0/8,192.168.0.0/16").action(async (name, options) => {
11525
11798
  const serverPublicKey = options.serverPubkey ?? process.env.VPN_SERVER_PUBKEY;
11526
11799
  const endpoint = options.endpoint ?? process.env.VPN_HOST;
11527
11800
  if (!serverPublicKey) {
@@ -11585,10 +11858,10 @@ function registerVpnClient(cli) {
11585
11858
  }
11586
11859
 
11587
11860
  // src/commands/vpn/deploy.ts
11588
- import { z as z28 } from "zod";
11589
- var REPO6 = "marcusrbrown/infra";
11590
- var WORKFLOW_NAME6 = "Deploy VPN";
11591
- var WORKFLOW_URL6 = "https://github.com/marcusrbrown/infra/actions/workflows/deploy-vpn.yaml";
11861
+ import { z as z31 } from "zod";
11862
+ var REPO7 = "marcusrbrown/infra";
11863
+ var WORKFLOW_NAME7 = "Deploy VPN";
11864
+ var WORKFLOW_URL7 = "https://github.com/marcusrbrown/infra/actions/workflows/deploy-vpn.yaml";
11592
11865
  function getVpnDeployEnv() {
11593
11866
  const path = process.env.PATH;
11594
11867
  const home = process.env.HOME;
@@ -11615,7 +11888,7 @@ function validateVpnRemotePreconditions() {
11615
11888
  }
11616
11889
  }
11617
11890
  function registerVpnDeploy(cli) {
11618
- cli.command("vpn deploy", "Deploy the VPN. Default mode triggers the GitHub Deploy VPN workflow, while --local runs apps/vpn deploy directly with Bun.").option("--local", z28.boolean().default(false).describe("Run local deployment with Bun using apps/vpn instead of triggering GitHub Actions.")).option("--dry-run", z28.boolean().default(false).describe("Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.")).option("--force-server-key", z28.boolean().default(false).describe("Force regeneration of the server WireGuard keypair. WARNING: invalidates all existing client configs.")).example("# Trigger remote GitHub Actions deploy (default mode)").example("infra vpn deploy").example("# Validate local deploy preconditions and planned command").example("infra vpn deploy --local --dry-run").example("# Run local deploy with explicit SSH agent context").example("infra vpn deploy --local").action(async (options) => {
11891
+ cli.command("vpn deploy", "Deploy the VPN. Default mode triggers the GitHub Deploy VPN workflow, while --local runs apps/vpn deploy directly with Bun.").option("--local", z31.boolean().default(false).describe("Run local deployment with Bun using apps/vpn instead of triggering GitHub Actions.")).option("--dry-run", z31.boolean().default(false).describe("Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.")).option("--force-server-key", z31.boolean().default(false).describe("Force regeneration of the server WireGuard keypair. WARNING: invalidates all existing client configs.")).example("# Trigger remote GitHub Actions deploy (default mode)").example("infra vpn deploy").example("# Validate local deploy preconditions and planned command").example("infra vpn deploy --local --dry-run").example("# Run local deploy with explicit SSH agent context").example("infra vpn deploy --local").action(async (options) => {
11619
11892
  if (options.local) {
11620
11893
  const command = [
11621
11894
  "bun",
@@ -11645,44 +11918,44 @@ function registerVpnDeploy(cli) {
11645
11918
  validateVpnRemotePreconditions();
11646
11919
  if (options.dryRun) {
11647
11920
  console.log("Dry run: remote vpn deploy");
11648
- console.log(`- command: gh workflow run "${WORKFLOW_NAME6}" --repo ${REPO6}`);
11649
- console.log(`- workflow URL: ${WORKFLOW_URL6}`);
11921
+ console.log(`- command: gh workflow run "${WORKFLOW_NAME7}" --repo ${REPO7}`);
11922
+ console.log(`- workflow URL: ${WORKFLOW_URL7}`);
11650
11923
  return;
11651
11924
  }
11652
- const child = Bun.spawn(["gh", "workflow", "run", WORKFLOW_NAME6, "--repo", REPO6], {
11925
+ const child = Bun.spawn(["gh", "workflow", "run", WORKFLOW_NAME7, "--repo", REPO7], {
11653
11926
  stdout: "inherit",
11654
11927
  stderr: "inherit"
11655
11928
  });
11656
11929
  const exitCode = await child.exited;
11657
11930
  if (exitCode !== 0) {
11658
- throw new Error(`Failed to trigger "${WORKFLOW_NAME6}" workflow (exit code ${exitCode})`);
11931
+ throw new Error(`Failed to trigger "${WORKFLOW_NAME7}" workflow (exit code ${exitCode})`);
11659
11932
  }
11660
- console.log(`Workflow triggered: ${WORKFLOW_URL6}`);
11933
+ console.log(`Workflow triggered: ${WORKFLOW_URL7}`);
11661
11934
  console.log("Approve the vpn environment deployment in GitHub Actions to continue.");
11662
11935
  });
11663
11936
  }
11664
11937
 
11665
11938
  // src/commands/vpn/logs.ts
11666
- import { z as z29 } from "zod";
11939
+ import { z as z32 } from "zod";
11667
11940
 
11668
11941
  // src/commands/vpn/host.ts
11669
- var VALID_HOST_RE5 = /^[a-z\d][a-z\d.\-]*$/i;
11942
+ var VALID_HOST_RE6 = /^[a-z\d][a-z\d.\-]*$/i;
11670
11943
  function validateVpnHost(host) {
11671
11944
  if (!host) {
11672
11945
  throw new Error("Invalid VPN_HOST: value is empty");
11673
11946
  }
11674
- if (!VALID_HOST_RE5.test(host)) {
11947
+ if (!VALID_HOST_RE6.test(host)) {
11675
11948
  throw new Error(`Invalid VPN_HOST: value is not a valid hostname (must match ${String.raw`[A-Za-z0-9][A-Za-z0-9.\-]*`})`);
11676
11949
  }
11677
11950
  return host;
11678
11951
  }
11679
11952
 
11680
11953
  // src/commands/vpn/logs.ts
11681
- var SENSITIVE_WARNING5 = "Warning: Logs may contain peer IP addresses, handshake data, or other sensitive VPN information. Treat output as sensitive; do not capture in shared logs or chat.";
11682
- function defaultLogsSpawn4(cmd, opts) {
11954
+ var SENSITIVE_WARNING6 = "Warning: Logs may contain peer IP addresses, handshake data, or other sensitive VPN information. Treat output as sensitive; do not capture in shared logs or chat.";
11955
+ function defaultLogsSpawn5(cmd, opts) {
11683
11956
  return Bun.spawn(cmd, opts);
11684
11957
  }
11685
- async function streamVpnLogs(opts, spawn = defaultLogsSpawn4, printOut, printErr) {
11958
+ async function streamVpnLogs(opts, spawn = defaultLogsSpawn5, printOut, printErr) {
11686
11959
  const isCI = process.env.CI === "true";
11687
11960
  if (isCI && !opts.allowCi) {
11688
11961
  const msg = "Refusing to stream logs in CI without --allow-ci. Logs may contain sensitive VPN data.";
@@ -11694,9 +11967,9 @@ async function streamVpnLogs(opts, spawn = defaultLogsSpawn4, printOut, printErr
11694
11967
  return { refused: true };
11695
11968
  }
11696
11969
  if (printErr) {
11697
- printErr(SENSITIVE_WARNING5);
11970
+ printErr(SENSITIVE_WARNING6);
11698
11971
  } else {
11699
- console.error(SENSITIVE_WARNING5);
11972
+ console.error(SENSITIVE_WARNING6);
11700
11973
  }
11701
11974
  validateVpnHost(opts.host);
11702
11975
  const sshCmd = [
@@ -11719,7 +11992,7 @@ async function streamVpnLogs(opts, spawn = defaultLogsSpawn4, printOut, printErr
11719
11992
  return { refused: false, exitCode };
11720
11993
  }
11721
11994
  function registerVpnLogs(cli) {
11722
- cli.command("vpn logs", "Stream WireGuard VPN logs via SSH and journalctl.").option("--tail [n]", z29.number().default(100).describe("Number of log lines to tail.")).option("--allow-ci", z29.boolean().default(false).describe("Allow log streaming in CI environments. Logs may contain sensitive VPN data.")).option("--key [key]", z29.string().describe("Environment variable name holding the SSH host. Falls back to VPN_HOST when omitted.")).action(async (options) => {
11995
+ cli.command("vpn logs", "Stream WireGuard VPN logs via SSH and journalctl.").option("--tail [n]", z32.number().default(100).describe("Number of log lines to tail.")).option("--allow-ci", z32.boolean().default(false).describe("Allow log streaming in CI environments. Logs may contain sensitive VPN data.")).option("--key [key]", z32.string().describe("Environment variable name holding the SSH host. Falls back to VPN_HOST when omitted.")).action(async (options) => {
11723
11996
  const hostEnvKey = options.key ?? "VPN_HOST";
11724
11997
  const host = process.env[hostEnvKey];
11725
11998
  if (!host) {
@@ -11741,7 +12014,7 @@ function registerVpnLogs(cli) {
11741
12014
  }
11742
12015
 
11743
12016
  // src/commands/vpn/status.ts
11744
- import { z as z30 } from "zod";
12017
+ import { z as z33 } from "zod";
11745
12018
 
11746
12019
  // src/commands/vpn/shared.ts
11747
12020
  function parseWgShowOutput(output) {
@@ -11868,7 +12141,7 @@ async function vpnStatusAction(options, ctx, spawn) {
11868
12141
  }
11869
12142
  }
11870
12143
  function registerVpnStatus(cli) {
11871
- cli.command("vpn status", "Show operational health of the VPN box via SSH and wg show.").option("--key [key]", z30.string().describe("Environment variable name holding the SSH host. Falls back to VPN_HOST when omitted.")).action((options, ctx) => vpnStatusAction(options, ctx));
12144
+ cli.command("vpn status", "Show operational health of the VPN box via SSH and wg show.").option("--key [key]", z33.string().describe("Environment variable name holding the SSH host. Falls back to VPN_HOST when omitted.")).action((options, ctx) => vpnStatusAction(options, ctx));
11872
12145
  }
11873
12146
 
11874
12147
  // src/commands/vpn/index.ts
@@ -11912,7 +12185,8 @@ function toJsonPayload(rows) {
11912
12185
  gateway: rows.find((row) => row.app === "gateway") ?? errorSummary("gateway", "missing result"),
11913
12186
  umami: rows.find((row) => row.app === "umami") ?? errorSummary("umami", "missing result"),
11914
12187
  dashboard: rows.find((row) => row.app === "dashboard") ?? errorSummary("dashboard", "missing result"),
11915
- vpn: rows.find((row) => row.app === "vpn") ?? errorSummary("vpn", "missing result")
12188
+ vpn: rows.find((row) => row.app === "vpn") ?? errorSummary("vpn", "missing result"),
12189
+ broker: rows.find((row) => row.app === "broker") ?? errorSummary("broker", "missing result")
11916
12190
  };
11917
12191
  }
11918
12192
  function formatRow(row) {
@@ -11925,7 +12199,8 @@ async function unifiedStatusAction(options, ctx, dependencies = {
11925
12199
  getGatewayStatusSummary,
11926
12200
  getUmamiStatusSummary,
11927
12201
  getDashboardStatusSummary,
11928
- getVpnStatusSummary
12202
+ getVpnStatusSummary,
12203
+ getBrokerStatusSummary
11929
12204
  }) {
11930
12205
  const verbose = options.verbose === true;
11931
12206
  const cliproxyBaseUrl = stripTrailingSlash6(process.env.CLIPROXY_URL ?? DEFAULT_CLIPROXY_URL7);
@@ -11934,15 +12209,17 @@ async function unifiedStatusAction(options, ctx, dependencies = {
11934
12209
  const umamiHost = process.env.UMAMI_DOMAIN ?? "";
11935
12210
  const dashboardHost = process.env.DASHBOARD_DOMAIN ?? "";
11936
12211
  const vpnHost = process.env.VPN_HOST ?? "";
12212
+ const brokerHost = process.env.BROKER_DOMAIN ?? process.env.BROKER_HOST ?? "";
11937
12213
  const results = await Promise.allSettled([
11938
12214
  dependencies.getKeewebStatusSummary(verbose),
11939
12215
  dependencies.getCliproxyStatusSummary(cliproxyBaseUrl, cliproxyKey, verbose),
11940
12216
  dependencies.getGatewayStatusSummary(gatewayHost),
11941
12217
  dependencies.getUmamiStatusSummary(umamiHost),
11942
12218
  dependencies.getDashboardStatusSummary(dashboardHost),
11943
- dependencies.getVpnStatusSummary(vpnHost)
12219
+ dependencies.getVpnStatusSummary(vpnHost),
12220
+ dependencies.getBrokerStatusSummary(brokerHost)
11944
12221
  ]);
11945
- const appNames = ["keeweb", "cliproxy", "gateway", "umami", "dashboard", "vpn"];
12222
+ const appNames = ["keeweb", "cliproxy", "gateway", "umami", "dashboard", "vpn", "broker"];
11946
12223
  const rows = results.map((result, index) => {
11947
12224
  const app = appNames[index] ?? "keeweb";
11948
12225
  if (result.status === "fulfilled") {
@@ -11968,9 +12245,10 @@ function registerStatus(cli, dependencies = {
11968
12245
  getGatewayStatusSummary,
11969
12246
  getUmamiStatusSummary,
11970
12247
  getDashboardStatusSummary,
11971
- getVpnStatusSummary
12248
+ getVpnStatusSummary,
12249
+ getBrokerStatusSummary
11972
12250
  }) {
11973
- cli.command("status", "Show status of all deployments").option("--json", z31.boolean().describe("Output machine-readable JSON with keeweb, cliproxy, gateway, umami, dashboard, and vpn summary objects.")).option("--verbose", z31.boolean().describe("Include verbose per-app health check details when building the summary rows.")).action((options, ctx) => unifiedStatusAction(options, ctx, dependencies));
12251
+ cli.command("status", "Show status of all deployments").option("--json", z34.boolean().describe("Output machine-readable JSON with keeweb, cliproxy, gateway, umami, dashboard, vpn, and broker summary objects.")).option("--verbose", z34.boolean().describe("Include verbose per-app health check details when building the summary rows.")).action((options, ctx) => unifiedStatusAction(options, ctx, dependencies));
11974
12252
  }
11975
12253
 
11976
12254
  // src/cli.ts
@@ -11978,6 +12256,7 @@ var cli = goke("infra");
11978
12256
  cli.option("--verbose", "Enable verbose output for all commands");
11979
12257
  registerKeewebCommands(cli);
11980
12258
  registerCliproxyCommands(cli);
12259
+ registerBrokerCommands(cli);
11981
12260
  registerGatewayCommands(cli);
11982
12261
  registerUmamiCommands(cli);
11983
12262
  registerDashboardCommands(cli);