@marcusrbrown/infra 0.20.0 → 0.22.0

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.
Files changed (2) hide show
  1. package/dist/cli.js +152 -53
  2. package/package.json +1 -1
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.20.0",
6952
+ version: "0.22.0",
6953
6953
  description: "Infrastructure management CLI \u2014 deploy automation, health checks, and MCP bridge",
6954
6954
  keywords: [
6955
6955
  "infra",
@@ -7142,7 +7142,7 @@ async function promptValue(promise, cancelMessage) {
7142
7142
  }
7143
7143
  function buildApiKeyValue(keyName) {
7144
7144
  const slug = (keyName.trim().toLowerCase().match(/[a-z0-9]+/g) ?? []).join("-").slice(0, 24);
7145
- const random = crypto.randomUUID().split("-").join("");
7145
+ const random = crypto.randomUUID().replaceAll("-", "");
7146
7146
  return `sk-${slug || "cliproxy"}-${random}`;
7147
7147
  }
7148
7148
  function extractErrorMessage(error) {
@@ -7842,7 +7842,7 @@ async function verifyModelsAvailable(baseUrl, key, providers, model) {
7842
7842
  }
7843
7843
  const slashIndex = model.indexOf("/");
7844
7844
  const bareId = slashIndex === -1 ? model : model.slice(slashIndex + 1);
7845
- const providerPrefix = slashIndex >= 0 ? model.slice(0, slashIndex) : undefined;
7845
+ const providerPrefix = slashIndex === -1 ? undefined : model.slice(0, slashIndex);
7846
7846
  const modelPresent = entries.some((e) => e.id === bareId || e.id === model);
7847
7847
  if (!modelPresent) {
7848
7848
  const matchingIds = providerPrefix ? entries.filter((e) => entryMatchesProvider(e, providerPrefix)).map((e) => e.id) : entries.map((e) => e.id);
@@ -11404,6 +11404,104 @@ function registerCliproxyOpen(cli) {
11404
11404
  });
11405
11405
  }
11406
11406
 
11407
+ // src/commands/cliproxy/reset-quota.ts
11408
+ import { z as z18 } from "zod";
11409
+ var DEFAULT_CLIPROXY_URL8 = "https://cliproxy.fro.bot";
11410
+ var ResetQuotaResponseSchema = z18.object({
11411
+ status: z18.literal("ok"),
11412
+ auth_index: z18.string(),
11413
+ models: z18.array(z18.unknown())
11414
+ });
11415
+ var AuthFileSchema = z18.object({
11416
+ id: z18.string(),
11417
+ auth_index: z18.string(),
11418
+ name: z18.string(),
11419
+ type: z18.string(),
11420
+ provider: z18.string(),
11421
+ email: z18.string()
11422
+ });
11423
+ var AuthFilesResponseSchema = z18.object({
11424
+ files: z18.array(AuthFileSchema)
11425
+ });
11426
+ var ManagementErrorSchema = z18.object({ error: z18.string() });
11427
+ function stripTrailingSlash6(value) {
11428
+ return value.endsWith("/") ? value.slice(0, -1) : value;
11429
+ }
11430
+ function resolveBaseUrl4(input) {
11431
+ return stripTrailingSlash6(input ?? process.env.CLIPROXY_URL ?? DEFAULT_CLIPROXY_URL8);
11432
+ }
11433
+ function resolveManagementKey4(input) {
11434
+ const key = input ?? process.env.CLIPROXY_MANAGEMENT_KEY;
11435
+ if (!key) {
11436
+ throw new Error("Management API key is required. Pass --key or set CLIPROXY_MANAGEMENT_KEY.");
11437
+ }
11438
+ return key;
11439
+ }
11440
+ async function requestParsedJson(endpoint, init, schema) {
11441
+ const response = await fetch(endpoint, {
11442
+ ...init,
11443
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS)
11444
+ });
11445
+ const body = await response.text();
11446
+ let payload;
11447
+ try {
11448
+ payload = body.length === 0 ? null : JSON.parse(body);
11449
+ } catch {
11450
+ payload = undefined;
11451
+ }
11452
+ if (!response.ok) {
11453
+ const error = ManagementErrorSchema.safeParse(payload);
11454
+ if (error.success) {
11455
+ throw new Error(error.data.error);
11456
+ }
11457
+ throw new Error(`${init.method ?? "GET"} ${endpoint} failed with HTTP ${response.status}: ${body}`);
11458
+ }
11459
+ return schema.parse(payload);
11460
+ }
11461
+ function formatAuthFile(file, index) {
11462
+ return `${index + 1}. ${file.auth_index} \u2014 ${file.name} (${file.provider}, ${file.email})`;
11463
+ }
11464
+ async function cliproxyResetQuotaAction(authIndex, options, ctx) {
11465
+ try {
11466
+ const baseUrl = resolveBaseUrl4(options.url);
11467
+ const managementKey = resolveManagementKey4(options.key);
11468
+ if (options.list) {
11469
+ const files = (await requestParsedJson(`${baseUrl}/v0/management/auth-files`, {
11470
+ method: "GET",
11471
+ headers: managementHeaders(managementKey)
11472
+ }, AuthFilesResponseSchema)).files;
11473
+ if (options.json) {
11474
+ ctx.console.log(JSON.stringify(files, null, 2));
11475
+ } else if (files.length === 0) {
11476
+ ctx.console.log("No auth files configured");
11477
+ } else {
11478
+ for (const [index, file] of files.entries()) {
11479
+ ctx.console.log(formatAuthFile(file, index));
11480
+ }
11481
+ }
11482
+ return files;
11483
+ }
11484
+ if (!authIndex) {
11485
+ throw new Error("auth_index is required. Pass an auth index or use --list to enumerate auth files.");
11486
+ }
11487
+ const payload = await requestParsedJson(`${baseUrl}/v0/management/reset-quota`, {
11488
+ method: "POST",
11489
+ headers: managementHeaders(managementKey),
11490
+ body: JSON.stringify({ auth_index: authIndex })
11491
+ }, ResetQuotaResponseSchema);
11492
+ ctx.console.log(JSON.stringify(payload, null, 2));
11493
+ return payload;
11494
+ } catch (error) {
11495
+ const message = error instanceof Error ? error.message : String(error);
11496
+ ctx.console.error(message);
11497
+ ctx.process.exit(1);
11498
+ return [];
11499
+ }
11500
+ }
11501
+ function registerCliproxyResetQuota(cli) {
11502
+ cli.command("cliproxy reset-quota [auth-index]", "Reset the quota for one CLIProxyAPI auth record, or list valid auth indexes.").option("--url [url]", z18.string().describe("Base URL for CLIProxyAPI management requests. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.")).option("--key [key]", z18.string().describe("Management API key. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.")).option("--list", "List auth records and their auth indexes without resetting quota.").option("--json", "Output auth records as JSON when used with --list.").example("# List valid auth indexes before resetting one").example("infra cliproxy reset-quota --list").example("# Reset quota for one auth record").example("infra cliproxy reset-quota auth-index-123").action(cliproxyResetQuotaAction);
11503
+ }
11504
+
11407
11505
  // src/commands/cliproxy/setup.ts
11408
11506
  function registerCliproxySetup(cli) {
11409
11507
  registerSetupCommand(cli, "cliproxy setup");
@@ -11419,11 +11517,12 @@ function registerCliproxyCommands(cli) {
11419
11517
  registerCliproxyModels(cli);
11420
11518
  registerCliproxyMonitor(cli);
11421
11519
  registerCliproxyOpen(cli);
11520
+ registerCliproxyResetQuota(cli);
11422
11521
  registerCliproxySetup(cli);
11423
11522
  }
11424
11523
 
11425
11524
  // src/commands/dashboard/deploy.ts
11426
- import { z as z18 } from "zod";
11525
+ import { z as z19 } from "zod";
11427
11526
  var REPO3 = "marcusrbrown/infra";
11428
11527
  var WORKFLOW_NAME3 = "Deploy Dashboard";
11429
11528
  var WORKFLOW_URL3 = "https://github.com/marcusrbrown/infra/actions/workflows/deploy-dashboard.yaml";
@@ -11485,7 +11584,7 @@ function validateDashboardRemotePreconditions() {
11485
11584
  }
11486
11585
  }
11487
11586
  function registerDashboardDeploy(cli) {
11488
- 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", z18.boolean().default(false).describe("Run local deployment with Bun using apps/dashboard instead of triggering GitHub Actions.")).option("--dry-run", z18.boolean().default(false).describe("Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.")).option("--image-version <version>", z18.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>", z18.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) => {
11587
+ 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", z19.boolean().default(false).describe("Run local deployment with Bun using apps/dashboard instead of triggering GitHub Actions.")).option("--dry-run", z19.boolean().default(false).describe("Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.")).option("--image-version <version>", z19.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>", z19.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) => {
11489
11588
  validateReleaseFlags({ imageVersion: options.imageVersion, digest: options.digest, local: options.local });
11490
11589
  if (options.local) {
11491
11590
  const command = ["bun", "run", "--cwd", "apps/dashboard", "deploy"];
@@ -11528,7 +11627,7 @@ function registerDashboardDeploy(cli) {
11528
11627
  }
11529
11628
 
11530
11629
  // src/commands/dashboard/logs.ts
11531
- import { z as z19 } from "zod";
11630
+ import { z as z20 } from "zod";
11532
11631
 
11533
11632
  // src/commands/dashboard/host.ts
11534
11633
  var VALID_HOST_RE3 = /^[a-z\d][a-z\d.\-]*$/i;
@@ -11601,7 +11700,7 @@ async function streamDashboardLogs(opts, spawn = defaultLogsSpawn2, printOut, pr
11601
11700
  return { refused: false, exitCode };
11602
11701
  }
11603
11702
  function registerDashboardLogs(cli) {
11604
- cli.command("dashboard logs [service]", "Stream logs from a dashboard service via SSH and docker compose.").option("--tail [n]", z19.number().default(100).describe("Number of log lines to tail from each service.")).option("--allow-ci", z19.boolean().default(false).describe("Allow log streaming in CI environments. Logs may contain sensitive credentials.")).option("--key [key]", z19.string().describe("Environment variable name holding the SSH host. Falls back to DASHBOARD_DOMAIN when omitted.")).action(async (service, options) => {
11703
+ cli.command("dashboard logs [service]", "Stream logs from a dashboard service via SSH and docker compose.").option("--tail [n]", z20.number().default(100).describe("Number of log lines to tail from each service.")).option("--allow-ci", z20.boolean().default(false).describe("Allow log streaming in CI environments. Logs may contain sensitive credentials.")).option("--key [key]", z20.string().describe("Environment variable name holding the SSH host. Falls back to DASHBOARD_DOMAIN when omitted.")).action(async (service, options) => {
11605
11704
  const targetService = service ?? "dashboard";
11606
11705
  try {
11607
11706
  validateService(targetService);
@@ -11631,7 +11730,7 @@ function registerDashboardLogs(cli) {
11631
11730
  }
11632
11731
 
11633
11732
  // src/commands/dashboard/status.ts
11634
- import { z as z20 } from "zod";
11733
+ import { z as z21 } from "zod";
11635
11734
  var COMPOSE_PROJECT_DIR2 = "/opt/dashboard";
11636
11735
  function parseComposePsOutput(stdoutText) {
11637
11736
  const trimmed = stdoutText.trim();
@@ -11805,7 +11904,7 @@ async function dashboardStatusAction(options, ctx, spawn) {
11805
11904
  }
11806
11905
  }
11807
11906
  function registerDashboardStatus(cli) {
11808
- cli.command("dashboard status", "Show operational health of the dashboard deployment via docker compose ps.").option("--key [key]", z20.string().optional().describe("Environment variable name holding the SSH host. Falls back to DASHBOARD_DOMAIN when omitted.")).action((options, ctx) => dashboardStatusAction(options, ctx));
11907
+ cli.command("dashboard status", "Show operational health of the dashboard deployment via docker compose ps.").option("--key [key]", z21.string().optional().describe("Environment variable name holding the SSH host. Falls back to DASHBOARD_DOMAIN when omitted.")).action((options, ctx) => dashboardStatusAction(options, ctx));
11809
11908
  }
11810
11909
 
11811
11910
  // src/commands/dashboard/index.ts
@@ -11817,7 +11916,7 @@ function registerDashboardCommands(cli) {
11817
11916
 
11818
11917
  // src/commands/gateway/backup.ts
11819
11918
  import { closeSync, constants as fsConstants, openSync, renameSync, unlinkSync, writeSync } from "fs";
11820
- import { z as z21 } from "zod";
11919
+ import { z as z22 } from "zod";
11821
11920
 
11822
11921
  // src/commands/gateway/host.ts
11823
11922
  var VALID_HOST_RE4 = /^[a-z\d][a-z\d.\-]*$/i;
@@ -11927,11 +12026,11 @@ async function gatewayBackupAction(options, ctx, spawn) {
11927
12026
  ctx.console.log(`CA backup written to: ${output}`);
11928
12027
  }
11929
12028
  function registerGatewayBackup(cli) {
11930
- 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]", z21.string().default(DEFAULT_OUTPUT).describe("Local path to write the CA tarball. File permissions are set to 0600 after writing.")).option("--include-ca", z21.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);
12029
+ 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]", z22.string().default(DEFAULT_OUTPUT).describe("Local path to write the CA tarball. File permissions are set to 0600 after writing.")).option("--include-ca", z22.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);
11931
12030
  }
11932
12031
 
11933
12032
  // src/commands/gateway/deploy.ts
11934
- import { z as z22 } from "zod";
12033
+ import { z as z23 } from "zod";
11935
12034
  var REPO4 = "marcusrbrown/infra";
11936
12035
  var WORKFLOW_NAME4 = "Deploy Gateway";
11937
12036
  var WORKFLOW_URL4 = "https://github.com/marcusrbrown/infra/actions/workflows/deploy-gateway.yaml";
@@ -12000,7 +12099,7 @@ function validateGatewayRemotePreconditions() {
12000
12099
  }
12001
12100
  }
12002
12101
  function registerGatewayDeploy(cli) {
12003
- 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", z22.boolean().default(false).describe("Run local deployment with Bun using apps/gateway instead of triggering GitHub Actions.")).option("--dry-run", z22.boolean().default(false).describe("Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.")).option("--force-recreate", z22.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) => {
12102
+ 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", z23.boolean().default(false).describe("Run local deployment with Bun using apps/gateway instead of triggering GitHub Actions.")).option("--dry-run", z23.boolean().default(false).describe("Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.")).option("--force-recreate", z23.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) => {
12004
12103
  if (options.local) {
12005
12104
  const command = [
12006
12105
  "bun",
@@ -12048,7 +12147,7 @@ function registerGatewayDeploy(cli) {
12048
12147
  }
12049
12148
 
12050
12149
  // src/commands/gateway/logs.ts
12051
- import { z as z23 } from "zod";
12150
+ import { z as z24 } from "zod";
12052
12151
  var COMPOSE_PROJECT_DIR3 = "/opt/gateway/deploy";
12053
12152
  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.";
12054
12153
  var VALID_SERVICES2 = ["gateway", "workspace", "mitmproxy"];
@@ -12097,7 +12196,7 @@ async function streamGatewayLogs(opts, spawn = defaultLogsSpawn3, printOut, prin
12097
12196
  return { refused: false, exitCode };
12098
12197
  }
12099
12198
  function registerGatewayLogs(cli) {
12100
- cli.command("gateway logs [service]", "Stream logs from a gateway service via SSH and docker compose.").option("--tail [n]", z23.number().default(100).describe("Number of log lines to tail from each service.")).option("--allow-ci", z23.boolean().default(false).describe("Allow log streaming in CI environments. Logs may contain sensitive credentials.")).option("--key [key]", z23.string().describe("Environment variable name holding the SSH host. Falls back to GATEWAY_HOST when omitted.")).action(async (service, options) => {
12199
+ cli.command("gateway logs [service]", "Stream logs from a gateway service via SSH and docker compose.").option("--tail [n]", z24.number().default(100).describe("Number of log lines to tail from each service.")).option("--allow-ci", z24.boolean().default(false).describe("Allow log streaming in CI environments. Logs may contain sensitive credentials.")).option("--key [key]", z24.string().describe("Environment variable name holding the SSH host. Falls back to GATEWAY_HOST when omitted.")).action(async (service, options) => {
12101
12200
  const targetService = service ?? "gateway";
12102
12201
  try {
12103
12202
  validateService2(targetService);
@@ -12127,7 +12226,7 @@ function registerGatewayLogs(cli) {
12127
12226
  }
12128
12227
 
12129
12228
  // src/commands/gateway/restore.ts
12130
- import { z as z24 } from "zod";
12229
+ import { z as z25 } from "zod";
12131
12230
  var MITMPROXY_CERTS_VOLUME2 = "mitmproxy-certs";
12132
12231
  var CA_CERT_FILE2 = "mitmproxy-ca-cert.pem";
12133
12232
  var CA_KEY_FILE2 = "mitmproxy-ca.pem";
@@ -12281,7 +12380,7 @@ async function restoreGatewayCa(opts, spawn = defaultSpawn4) {
12281
12380
  }
12282
12381
  }
12283
12382
  function registerGatewayRestore(cli) {
12284
- 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>", z24.string().describe("Path to the tarball produced by `gateway backup --include-ca`. Required.")).option("--include-ca", z24.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) => {
12383
+ 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>", z25.string().describe("Path to the tarball produced by `gateway backup --include-ca`. Required.")).option("--include-ca", z25.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) => {
12285
12384
  const hostEnvKey = "GATEWAY_HOST";
12286
12385
  const host = process.env[hostEnvKey];
12287
12386
  if (!host) {
@@ -12302,7 +12401,7 @@ function registerGatewayRestore(cli) {
12302
12401
  }
12303
12402
 
12304
12403
  // src/commands/gateway/status.ts
12305
- import { z as z25 } from "zod";
12404
+ import { z as z26 } from "zod";
12306
12405
  var COMPOSE_PROJECT_DIR5 = "/opt/gateway/deploy";
12307
12406
  function parseComposePsOutput2(stdoutText) {
12308
12407
  const trimmed = stdoutText.trim();
@@ -12444,7 +12543,7 @@ async function gatewayStatusAction(options, ctx, spawn) {
12444
12543
  }
12445
12544
  }
12446
12545
  function registerGatewayStatus(cli) {
12447
- cli.command("gateway status", "Show operational health of the gateway deployment via docker compose ps.").option("--key [key]", z25.string().describe("Environment variable name holding the SSH host. Falls back to GATEWAY_HOST when omitted.")).action((options, ctx) => gatewayStatusAction(options, ctx));
12546
+ cli.command("gateway status", "Show operational health of the gateway deployment via docker compose ps.").option("--key [key]", z26.string().describe("Environment variable name holding the SSH host. Falls back to GATEWAY_HOST when omitted.")).action((options, ctx) => gatewayStatusAction(options, ctx));
12448
12547
  }
12449
12548
 
12450
12549
  // src/commands/gateway/index.ts
@@ -12459,7 +12558,7 @@ function registerGatewayCommands(cli) {
12459
12558
  // src/commands/keeweb/deploy.ts
12460
12559
  import { existsSync as existsSync5 } from "fs";
12461
12560
  import { join as join6 } from "path";
12462
- import { z as z26 } from "zod";
12561
+ import { z as z27 } from "zod";
12463
12562
  var REPO5 = "marcusrbrown/infra";
12464
12563
  var WORKFLOW_NAME5 = "Deploy KeeWeb";
12465
12564
  var WORKFLOW_URL5 = "https://github.com/marcusrbrown/infra/actions/workflows/deploy-keeweb.yaml";
@@ -12513,7 +12612,7 @@ function validateRemotePreconditions3() {
12513
12612
  }
12514
12613
  }
12515
12614
  function registerKeewebDeploy(cli) {
12516
- 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", z26.boolean().default(false).describe("Run local deployment using apps/keeweb/deploy.sh instead of triggering GitHub Actions.")).option("--nginx", z26.boolean().default(false).describe("Include nginx config deployment. Valid only with --local and passed through to deploy.sh as --nginx.")).option("--dry-run", z26.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) => {
12615
+ 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", z27.boolean().default(false).describe("Run local deployment using apps/keeweb/deploy.sh instead of triggering GitHub Actions.")).option("--nginx", z27.boolean().default(false).describe("Include nginx config deployment. Valid only with --local and passed through to deploy.sh as --nginx.")).option("--dry-run", z27.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) => {
12517
12616
  if (options.nginx && !options.local) {
12518
12617
  throw new Error("--nginx is only valid with --local");
12519
12618
  }
@@ -12592,13 +12691,13 @@ function registerKeewebOpen(cli) {
12592
12691
 
12593
12692
  // src/commands/keeweb/status.ts
12594
12693
  import { join as join7 } from "path";
12595
- import { z as z27 } from "zod";
12694
+ import { z as z28 } from "zod";
12596
12695
  var SITE_URL = "https://kw.igg.ms/";
12597
12696
  var GH_REPO = "marcusrbrown/infra";
12598
12697
  var HTTP_TIMEOUT_MS4 = 1e4;
12599
- var ghRunSchema = z27.array(z27.object({
12600
- createdAt: z27.string().min(1),
12601
- url: z27.url()
12698
+ var ghRunSchema = z28.array(z28.object({
12699
+ createdAt: z28.string().min(1),
12700
+ url: z28.url()
12602
12701
  }));
12603
12702
  function levelLabel2(level) {
12604
12703
  if (level === "ok") {
@@ -12896,10 +12995,10 @@ function registerMcp(cli) {
12896
12995
  }
12897
12996
 
12898
12997
  // src/commands/status.ts
12899
- import { z as z36 } from "zod";
12998
+ import { z as z37 } from "zod";
12900
12999
 
12901
13000
  // src/commands/umami/deploy.ts
12902
- import { z as z28 } from "zod";
13001
+ import { z as z29 } from "zod";
12903
13002
  var REPO6 = "marcusrbrown/infra";
12904
13003
  var WORKFLOW_NAME6 = "Deploy Umami";
12905
13004
  var WORKFLOW_URL6 = "https://github.com/marcusrbrown/infra/actions/workflows/deploy-umami.yaml";
@@ -12934,7 +13033,7 @@ function validateUmamiRemotePreconditions() {
12934
13033
  }
12935
13034
  }
12936
13035
  function registerUmamiDeploy(cli) {
12937
- 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", z28.boolean().default(false).describe("Run local deployment with Bun using apps/umami instead of triggering GitHub Actions.")).option("--dry-run", z28.boolean().default(false).describe("Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.")).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) => {
13036
+ 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", z29.boolean().default(false).describe("Run local deployment with Bun using apps/umami instead of triggering GitHub Actions.")).option("--dry-run", z29.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) => {
12938
13037
  if (options.local) {
12939
13038
  const command = ["bun", "run", "--cwd", "apps/umami", "deploy"];
12940
13039
  if (options.dryRun) {
@@ -12975,7 +13074,7 @@ function registerUmamiDeploy(cli) {
12975
13074
  }
12976
13075
 
12977
13076
  // src/commands/umami/logs.ts
12978
- import { z as z29 } from "zod";
13077
+ import { z as z30 } from "zod";
12979
13078
 
12980
13079
  // src/commands/umami/host.ts
12981
13080
  var VALID_HOST_RE5 = /^[a-z\d][a-z\d.\-]*$/i;
@@ -13039,7 +13138,7 @@ async function streamUmamiLogs(opts, spawn = defaultLogsSpawn4, printOut, printE
13039
13138
  return { refused: false, exitCode };
13040
13139
  }
13041
13140
  function registerUmamiLogs(cli) {
13042
- cli.command("umami logs [service]", "Stream logs from an Umami service via SSH and docker compose.").option("--tail [n]", z29.number().default(100).describe("Number of log lines to tail from each service.")).option("--allow-ci", z29.boolean().default(false).describe("Allow log streaming in CI environments. Logs may contain sensitive credentials.")).option("--key [key]", z29.string().describe("Environment variable name holding the SSH host. Falls back to UMAMI_DOMAIN when omitted.")).action(async (service, options) => {
13141
+ cli.command("umami logs [service]", "Stream logs from an Umami service via SSH and docker compose.").option("--tail [n]", z30.number().default(100).describe("Number of log lines to tail from each service.")).option("--allow-ci", z30.boolean().default(false).describe("Allow log streaming in CI environments. Logs may contain sensitive credentials.")).option("--key [key]", z30.string().describe("Environment variable name holding the SSH host. Falls back to UMAMI_DOMAIN when omitted.")).action(async (service, options) => {
13043
13142
  const targetService = service ?? "umami";
13044
13143
  try {
13045
13144
  validateService3(targetService);
@@ -13069,7 +13168,7 @@ function registerUmamiLogs(cli) {
13069
13168
  }
13070
13169
 
13071
13170
  // src/commands/umami/status.ts
13072
- import { z as z30 } from "zod";
13171
+ import { z as z31 } from "zod";
13073
13172
  var COMPOSE_PROJECT_DIR7 = "/opt/umami";
13074
13173
  function parseComposePsOutput3(stdoutText) {
13075
13174
  const trimmed = stdoutText.trim();
@@ -13240,7 +13339,7 @@ async function umamiStatusAction(options, ctx, spawn) {
13240
13339
  }
13241
13340
  }
13242
13341
  function registerUmamiStatus(cli) {
13243
- cli.command("umami status", "Show operational health of the Umami analytics deployment via docker compose ps.").option("--key [key]", z30.string().optional().describe("Environment variable name holding the SSH host. Falls back to UMAMI_DOMAIN when omitted.")).action((options, ctx) => umamiStatusAction(options, ctx));
13342
+ cli.command("umami status", "Show operational health of the Umami analytics deployment via docker compose ps.").option("--key [key]", z31.string().optional().describe("Environment variable name holding the SSH host. Falls back to UMAMI_DOMAIN when omitted.")).action((options, ctx) => umamiStatusAction(options, ctx));
13244
13343
  }
13245
13344
 
13246
13345
  // src/commands/umami/index.ts
@@ -13253,24 +13352,24 @@ function registerUmamiCommands(cli) {
13253
13352
  // src/commands/vpn/client.ts
13254
13353
  import { mkdir, writeFile as writeFile2 } from "fs/promises";
13255
13354
  import { join as join8, resolve as resolve2 } from "path";
13256
- import { z as z32 } from "zod";
13355
+ import { z as z33 } from "zod";
13257
13356
 
13258
13357
  // src/commands/vpn/peers.ts
13259
13358
  import { readFile, writeFile } from "fs/promises";
13260
- import { z as z31 } from "zod";
13261
- var peerSchema = z31.object({
13262
- name: z31.string(),
13263
- publicKey: z31.string(),
13264
- tunnelIp: z31.string()
13359
+ import { z as z32 } from "zod";
13360
+ var peerSchema = z32.object({
13361
+ name: z32.string(),
13362
+ publicKey: z32.string(),
13363
+ tunnelIp: z32.string()
13265
13364
  });
13266
- var peersFileSchema = z31.object({
13267
- peers: z31.array(peerSchema)
13365
+ var peersFileSchema = z32.object({
13366
+ peers: z32.array(peerSchema)
13268
13367
  }).superRefine((data, ctx) => {
13269
13368
  const seenIps = new Set;
13270
13369
  for (const peer of data.peers) {
13271
13370
  if (seenIps.has(peer.tunnelIp)) {
13272
13371
  ctx.addIssue({
13273
- code: z31.ZodIssueCode.custom,
13372
+ code: z32.ZodIssueCode.custom,
13274
13373
  message: `Duplicate tunnel IP "${peer.tunnelIp}" \u2014 each peer must have a unique tunnel IP.`,
13275
13374
  path: ["peers"]
13276
13375
  });
@@ -13282,7 +13381,7 @@ var peersFileSchema = z31.object({
13282
13381
  for (const peer of data.peers) {
13283
13382
  if (seenNames.has(peer.name)) {
13284
13383
  ctx.addIssue({
13285
- code: z31.ZodIssueCode.custom,
13384
+ code: z32.ZodIssueCode.custom,
13286
13385
  message: `Duplicate name "${peer.name}" \u2014 each peer must have a unique name.`,
13287
13386
  path: ["peers"]
13288
13387
  });
@@ -13524,7 +13623,7 @@ function resolveDefaultClientsDir() {
13524
13623
  return join8(findRepoRoot(), "apps", "vpn", "clients");
13525
13624
  }
13526
13625
  function registerVpnClient(cli) {
13527
- 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]", z32.string().describe("Comma-separated CIDRs for split-tunnel routing. Defaults to 0.0.0.0/0 (full tunnel).")).option("--dns [dns]", z32.string().describe("DNS server for the client config.")).option("--server-pubkey [key]", z32.string().describe("Server public key. Falls back to VPN_SERVER_PUBKEY env var.")).option("--endpoint [host]", z32.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) => {
13626
+ 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]", z33.string().describe("Comma-separated CIDRs for split-tunnel routing. Defaults to 0.0.0.0/0 (full tunnel).")).option("--dns [dns]", z33.string().describe("DNS server for the client config.")).option("--server-pubkey [key]", z33.string().describe("Server public key. Falls back to VPN_SERVER_PUBKEY env var.")).option("--endpoint [host]", z33.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) => {
13528
13627
  const serverPublicKey = options.serverPubkey ?? process.env.VPN_SERVER_PUBKEY;
13529
13628
  const endpoint = options.endpoint ?? process.env.VPN_HOST;
13530
13629
  if (!serverPublicKey) {
@@ -13588,7 +13687,7 @@ function registerVpnClient(cli) {
13588
13687
  }
13589
13688
 
13590
13689
  // src/commands/vpn/deploy.ts
13591
- import { z as z33 } from "zod";
13690
+ import { z as z34 } from "zod";
13592
13691
  var REPO7 = "marcusrbrown/infra";
13593
13692
  var WORKFLOW_NAME7 = "Deploy VPN";
13594
13693
  var WORKFLOW_URL7 = "https://github.com/marcusrbrown/infra/actions/workflows/deploy-vpn.yaml";
@@ -13618,7 +13717,7 @@ function validateVpnRemotePreconditions() {
13618
13717
  }
13619
13718
  }
13620
13719
  function registerVpnDeploy(cli) {
13621
- 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", z33.boolean().default(false).describe("Run local deployment with Bun using apps/vpn instead of triggering GitHub Actions.")).option("--dry-run", z33.boolean().default(false).describe("Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.")).option("--force-server-key", z33.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) => {
13720
+ 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", z34.boolean().default(false).describe("Run local deployment with Bun using apps/vpn instead of triggering GitHub Actions.")).option("--dry-run", z34.boolean().default(false).describe("Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.")).option("--force-server-key", z34.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) => {
13622
13721
  if (options.local) {
13623
13722
  const command = [
13624
13723
  "bun",
@@ -13666,7 +13765,7 @@ function registerVpnDeploy(cli) {
13666
13765
  }
13667
13766
 
13668
13767
  // src/commands/vpn/logs.ts
13669
- import { z as z34 } from "zod";
13768
+ import { z as z35 } from "zod";
13670
13769
 
13671
13770
  // src/commands/vpn/host.ts
13672
13771
  var VALID_HOST_RE6 = /^[a-z\d][a-z\d.\-]*$/i;
@@ -13722,7 +13821,7 @@ async function streamVpnLogs(opts, spawn = defaultLogsSpawn5, printOut, printErr
13722
13821
  return { refused: false, exitCode };
13723
13822
  }
13724
13823
  function registerVpnLogs(cli) {
13725
- cli.command("vpn logs", "Stream WireGuard VPN logs via SSH and journalctl.").option("--tail [n]", z34.number().default(100).describe("Number of log lines to tail.")).option("--allow-ci", z34.boolean().default(false).describe("Allow log streaming in CI environments. Logs may contain sensitive VPN data.")).option("--key [key]", z34.string().describe("Environment variable name holding the SSH host. Falls back to VPN_HOST when omitted.")).action(async (options) => {
13824
+ cli.command("vpn logs", "Stream WireGuard VPN logs via SSH and journalctl.").option("--tail [n]", z35.number().default(100).describe("Number of log lines to tail.")).option("--allow-ci", z35.boolean().default(false).describe("Allow log streaming in CI environments. Logs may contain sensitive VPN data.")).option("--key [key]", z35.string().describe("Environment variable name holding the SSH host. Falls back to VPN_HOST when omitted.")).action(async (options) => {
13726
13825
  const hostEnvKey = options.key ?? "VPN_HOST";
13727
13826
  const host = process.env[hostEnvKey];
13728
13827
  if (!host) {
@@ -13744,7 +13843,7 @@ function registerVpnLogs(cli) {
13744
13843
  }
13745
13844
 
13746
13845
  // src/commands/vpn/status.ts
13747
- import { z as z35 } from "zod";
13846
+ import { z as z36 } from "zod";
13748
13847
 
13749
13848
  // src/commands/vpn/shared.ts
13750
13849
  function parseWgShowOutput(output) {
@@ -13871,7 +13970,7 @@ async function vpnStatusAction(options, ctx, spawn) {
13871
13970
  }
13872
13971
  }
13873
13972
  function registerVpnStatus(cli) {
13874
- cli.command("vpn status", "Show operational health of the VPN box via SSH and wg show.").option("--key [key]", z35.string().describe("Environment variable name holding the SSH host. Falls back to VPN_HOST when omitted.")).action((options, ctx) => vpnStatusAction(options, ctx));
13973
+ cli.command("vpn status", "Show operational health of the VPN box via SSH and wg show.").option("--key [key]", z36.string().describe("Environment variable name holding the SSH host. Falls back to VPN_HOST when omitted.")).action((options, ctx) => vpnStatusAction(options, ctx));
13875
13974
  }
13876
13975
 
13877
13976
  // src/commands/vpn/index.ts
@@ -13883,7 +13982,7 @@ function registerVpnCommands(cli) {
13883
13982
  }
13884
13983
 
13885
13984
  // src/commands/status.ts
13886
- var DEFAULT_CLIPROXY_URL8 = "https://cliproxy.fro.bot";
13985
+ var DEFAULT_CLIPROXY_URL9 = "https://cliproxy.fro.bot";
13887
13986
  var ERROR_PREFIX = "\u274C";
13888
13987
  var TABLE_COLUMNS = [
13889
13988
  { key: "app", label: "App" },
@@ -13893,7 +13992,7 @@ var TABLE_COLUMNS = [
13893
13992
  { key: "contentHash", label: "Content Hash" },
13894
13993
  { key: "usageStats", label: "Usage Stats" }
13895
13994
  ];
13896
- function stripTrailingSlash6(value) {
13995
+ function stripTrailingSlash7(value) {
13897
13996
  return value.endsWith("/") ? value.slice(0, -1) : value;
13898
13997
  }
13899
13998
  function errorSummary(app, reason) {
@@ -13933,7 +14032,7 @@ async function unifiedStatusAction(options, ctx, dependencies = {
13933
14032
  getBrokerStatusSummary
13934
14033
  }) {
13935
14034
  const verbose = options.verbose === true;
13936
- const cliproxyBaseUrl = stripTrailingSlash6(process.env.CLIPROXY_URL ?? DEFAULT_CLIPROXY_URL8);
14035
+ const cliproxyBaseUrl = stripTrailingSlash7(process.env.CLIPROXY_URL ?? DEFAULT_CLIPROXY_URL9);
13937
14036
  const cliproxyKey = process.env.CLIPROXY_MANAGEMENT_KEY ?? "";
13938
14037
  const gatewayHost = process.env.GATEWAY_HOST ?? "";
13939
14038
  const umamiHost = process.env.UMAMI_DOMAIN ?? "";
@@ -13978,7 +14077,7 @@ function registerStatus(cli, dependencies = {
13978
14077
  getVpnStatusSummary,
13979
14078
  getBrokerStatusSummary
13980
14079
  }) {
13981
- cli.command("status", "Show status of all deployments").option("--json", z36.boolean().describe("Output machine-readable JSON with keeweb, cliproxy, gateway, umami, dashboard, vpn, and broker summary objects.")).option("--verbose", z36.boolean().describe("Include verbose per-app health check details when building the summary rows.")).action((options, ctx) => unifiedStatusAction(options, ctx, dependencies));
14080
+ cli.command("status", "Show status of all deployments").option("--json", z37.boolean().describe("Output machine-readable JSON with keeweb, cliproxy, gateway, umami, dashboard, vpn, and broker summary objects.")).option("--verbose", z37.boolean().describe("Include verbose per-app health check details when building the summary rows.")).action((options, ctx) => unifiedStatusAction(options, ctx, dependencies));
13982
14081
  }
13983
14082
 
13984
14083
  // src/cli.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marcusrbrown/infra",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "Infrastructure management CLI — deploy automation, health checks, and MCP bridge",
5
5
  "keywords": [
6
6
  "infra",