@marcusrbrown/infra 0.13.12 → 0.13.14
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 +564 -285
- package/dist/{known_hosts-tk724cp0. → known_hosts-c3ns9jne.} +18 -0
- package/package.json +2 -2
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.
|
|
6952
|
+
version: "0.13.14",
|
|
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.
|
|
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
|
|
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(
|
|
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]",
|
|
7246
|
-
cli.command("cliproxy config set <field> <value>", "Update a mutable CLIProxyAPI config field via its dedicated management endpoint.").option("--url [url]",
|
|
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
|
|
7251
|
-
import { join as
|
|
7252
|
-
import { z as
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
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
|
|
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
|
|
7308
|
-
const deployScriptPath =
|
|
7309
|
-
if (!
|
|
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
|
-
|
|
7608
|
+
getLocalDeployEnv2();
|
|
7313
7609
|
return { deployScriptPath };
|
|
7314
7610
|
}
|
|
7315
|
-
function
|
|
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",
|
|
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 } =
|
|
7324
|
-
const env =
|
|
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
|
-
|
|
7643
|
+
validateRemotePreconditions2();
|
|
7348
7644
|
if (options.dryRun) {
|
|
7349
7645
|
console.log("Dry run: remote CLIProxyAPI deploy");
|
|
7350
|
-
console.log(`- command: gh workflow run ${
|
|
7351
|
-
console.log(`- 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",
|
|
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 ${
|
|
7656
|
+
throw new Error(`Failed to trigger ${WORKFLOW_NAME2} workflow (exit code ${exitCode})`);
|
|
7361
7657
|
}
|
|
7362
|
-
console.log(`Workflow triggered: ${
|
|
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
|
|
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]",
|
|
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]",
|
|
7458
|
-
cli.command("cliproxy keys remove <key>", "Remove an API key via management API endpoint query parameter.").option("--url [url]",
|
|
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
|
|
7758
|
+
import { z as z7 } from "zod";
|
|
7463
7759
|
|
|
7464
7760
|
// src/commands/cliproxy/host.ts
|
|
7465
|
-
var
|
|
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 (!
|
|
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]",
|
|
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
|
|
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 =
|
|
7557
|
-
id:
|
|
7558
|
-
owned_by:
|
|
7559
|
-
created:
|
|
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 =
|
|
7562
|
-
data:
|
|
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]",
|
|
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
|
|
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]",
|
|
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
|
|
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:
|
|
7879
|
-
const ghRepoViewSchema =
|
|
7880
|
-
nameWithOwner:
|
|
7881
|
-
viewerPermission:
|
|
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:
|
|
7895
|
-
const ghNameListSchema =
|
|
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
|
|
7982
|
-
var providerIdSchema =
|
|
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
|
|
8072
|
-
var baselineRunSchema =
|
|
8073
|
-
var ghRunEntrySchema =
|
|
8074
|
-
databaseId:
|
|
8075
|
-
status:
|
|
8076
|
-
conclusion:
|
|
8077
|
-
url:
|
|
8078
|
-
createdAt:
|
|
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((
|
|
8381
|
+
await new Promise((resolve2) => setTimeout(resolve2, internals._testDelayMs));
|
|
8086
8382
|
}
|
|
8087
8383
|
return;
|
|
8088
8384
|
}
|
|
8089
|
-
await new Promise((
|
|
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
|
|
8209
|
-
var harnessSchema =
|
|
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
|
|
8301
|
-
var modelEntrySchema2 =
|
|
8302
|
-
id:
|
|
8303
|
-
owned_by:
|
|
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 =
|
|
8306
|
-
data:
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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]",
|
|
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
|
|
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(
|
|
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: ${
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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]",
|
|
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
|
|
9356
|
-
var
|
|
9357
|
-
var
|
|
9358
|
-
var
|
|
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",
|
|
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",
|
|
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: ${
|
|
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 "${
|
|
9748
|
+
throw new Error(`Failed to trigger "${WORKFLOW_NAME3}" workflow (exit code ${exitCode})`);
|
|
9453
9749
|
}
|
|
9454
|
-
console.log(`Workflow triggered: ${
|
|
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
|
|
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
|
|
9761
|
+
import { join as join5 } from "path";
|
|
9490
9762
|
function materializeIdentityFile(privateKey) {
|
|
9491
|
-
const dir = mkdtempSync(
|
|
9492
|
-
const path =
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
|
9818
|
+
function defaultLogsSpawn2(cmd, opts) {
|
|
9547
9819
|
return Bun.spawn(cmd, opts);
|
|
9548
9820
|
}
|
|
9549
|
-
async function streamDashboardLogs(opts, spawn =
|
|
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(
|
|
9834
|
+
printErr(SENSITIVE_WARNING2);
|
|
9563
9835
|
} else {
|
|
9564
|
-
console.error(
|
|
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]",
|
|
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
|
|
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]",
|
|
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
|
|
10094
|
+
import { z as z19 } from "zod";
|
|
9823
10095
|
|
|
9824
10096
|
// src/commands/gateway/host.ts
|
|
9825
|
-
var
|
|
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 (!
|
|
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
|
|
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(
|
|
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]",
|
|
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
|
|
9937
|
-
var
|
|
9938
|
-
var
|
|
9939
|
-
var
|
|
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",
|
|
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 "${
|
|
10032
|
-
console.log(`- workflow URL: ${
|
|
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",
|
|
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 "${
|
|
10313
|
+
throw new Error(`Failed to trigger "${WORKFLOW_NAME4}" workflow (exit code ${exitCode})`);
|
|
10042
10314
|
}
|
|
10043
|
-
console.log(`Workflow triggered: ${
|
|
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
|
|
10321
|
+
import { z as z21 } from "zod";
|
|
10050
10322
|
var COMPOSE_PROJECT_DIR3 = "/opt/gateway/deploy";
|
|
10051
|
-
var
|
|
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
|
|
10330
|
+
function defaultLogsSpawn3(cmd, opts) {
|
|
10059
10331
|
return Bun.spawn(cmd, opts);
|
|
10060
10332
|
}
|
|
10061
|
-
async function streamGatewayLogs(opts, spawn =
|
|
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(
|
|
10345
|
+
printErr(SENSITIVE_WARNING4);
|
|
10074
10346
|
} else {
|
|
10075
|
-
console.error(
|
|
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]",
|
|
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
|
|
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>",
|
|
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
|
|
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]",
|
|
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
|
|
10459
|
-
import { join as
|
|
10460
|
-
import { z as
|
|
10461
|
-
var
|
|
10462
|
-
var
|
|
10463
|
-
var
|
|
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
|
|
10740
|
+
return join6(findRepoRoot(), "apps", "keeweb", "deploy.sh");
|
|
10469
10741
|
}
|
|
10470
10742
|
function resolveDistIndexPath() {
|
|
10471
|
-
return
|
|
10743
|
+
return join6(findRepoRoot(), "apps", "keeweb", "dist", "index.html");
|
|
10472
10744
|
}
|
|
10473
|
-
function
|
|
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
|
|
10767
|
+
function validateLocalPreconditions3(options) {
|
|
10496
10768
|
const deployScriptPath = resolveDeployScriptPath();
|
|
10497
|
-
if (!
|
|
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 (!
|
|
10773
|
+
if (!existsSync5(distIndexPath)) {
|
|
10502
10774
|
throw new Error(`Local deploy requires built assets. Missing: ${distIndexPath}`);
|
|
10503
10775
|
}
|
|
10504
|
-
|
|
10776
|
+
getLocalDeployEnv3();
|
|
10505
10777
|
if (options.nginx) {}
|
|
10506
10778
|
return { deployScriptPath, distIndexPath };
|
|
10507
10779
|
}
|
|
10508
|
-
function
|
|
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",
|
|
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 } =
|
|
10532
|
-
const env =
|
|
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 ${
|
|
10551
|
-
console.log(`- workflow URL: ${
|
|
10822
|
+
console.log(`- command: gh workflow run ${WORKFLOW_NAME5} --repo ${REPO5}`);
|
|
10823
|
+
console.log(`- workflow URL: ${WORKFLOW_URL5}`);
|
|
10552
10824
|
return;
|
|
10553
10825
|
}
|
|
10554
|
-
|
|
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",
|
|
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: ${
|
|
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
|
|
10593
|
-
import { z as
|
|
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
|
|
10597
|
-
var ghRunSchema =
|
|
10598
|
-
createdAt:
|
|
10599
|
-
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(
|
|
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: ${
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
11169
|
+
import { z as z34 } from "zod";
|
|
10897
11170
|
|
|
10898
11171
|
// src/commands/umami/deploy.ts
|
|
10899
|
-
import { z as
|
|
10900
|
-
var
|
|
10901
|
-
var
|
|
10902
|
-
var
|
|
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",
|
|
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 "${
|
|
10958
|
-
console.log(`- workflow URL: ${
|
|
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",
|
|
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 "${
|
|
11240
|
+
throw new Error(`Failed to trigger "${WORKFLOW_NAME6}" workflow (exit code ${exitCode})`);
|
|
10968
11241
|
}
|
|
10969
|
-
console.log(`Workflow triggered: ${
|
|
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
|
|
11248
|
+
import { z as z27 } from "zod";
|
|
10976
11249
|
|
|
10977
11250
|
// src/commands/umami/host.ts
|
|
10978
|
-
var
|
|
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 (!
|
|
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
|
|
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
|
|
11271
|
+
function defaultLogsSpawn4(cmd, opts) {
|
|
10999
11272
|
return Bun.spawn(cmd, opts);
|
|
11000
11273
|
}
|
|
11001
|
-
async function streamUmamiLogs(opts, spawn =
|
|
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(
|
|
11287
|
+
printErr(SENSITIVE_WARNING5);
|
|
11015
11288
|
} else {
|
|
11016
|
-
console.error(
|
|
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]",
|
|
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
|
|
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]",
|
|
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
|
|
11253
|
-
import { z as
|
|
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
|
|
11258
|
-
var peerSchema =
|
|
11259
|
-
name:
|
|
11260
|
-
publicKey:
|
|
11261
|
-
tunnelIp:
|
|
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 =
|
|
11264
|
-
peers:
|
|
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:
|
|
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:
|
|
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
|
|
11791
|
+
return join8(findRepoRoot(), "apps", "vpn", "config", "peers.json");
|
|
11519
11792
|
}
|
|
11520
11793
|
function resolveDefaultClientsDir() {
|
|
11521
|
-
return
|
|
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]",
|
|
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
|
|
11589
|
-
var
|
|
11590
|
-
var
|
|
11591
|
-
var
|
|
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",
|
|
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 "${
|
|
11649
|
-
console.log(`- workflow URL: ${
|
|
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",
|
|
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 "${
|
|
11931
|
+
throw new Error(`Failed to trigger "${WORKFLOW_NAME7}" workflow (exit code ${exitCode})`);
|
|
11659
11932
|
}
|
|
11660
|
-
console.log(`Workflow triggered: ${
|
|
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
|
|
11939
|
+
import { z as z32 } from "zod";
|
|
11667
11940
|
|
|
11668
11941
|
// src/commands/vpn/host.ts
|
|
11669
|
-
var
|
|
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 (!
|
|
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
|
|
11682
|
-
function
|
|
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 =
|
|
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(
|
|
11970
|
+
printErr(SENSITIVE_WARNING6);
|
|
11698
11971
|
} else {
|
|
11699
|
-
console.error(
|
|
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]",
|
|
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
|
|
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]",
|
|
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",
|
|
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);
|