@marcusrbrown/infra 0.21.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.
- package/dist/cli.js +150 -51
- 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.
|
|
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",
|
|
@@ -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
|
|
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",
|
|
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
|
|
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]",
|
|
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
|
|
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]",
|
|
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
|
|
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]",
|
|
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
|
|
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",
|
|
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
|
|
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]",
|
|
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
|
|
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>",
|
|
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
|
|
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]",
|
|
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
|
|
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",
|
|
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
|
|
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 =
|
|
12600
|
-
createdAt:
|
|
12601
|
-
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
|
|
12998
|
+
import { z as z37 } from "zod";
|
|
12900
12999
|
|
|
12901
13000
|
// src/commands/umami/deploy.ts
|
|
12902
|
-
import { z as
|
|
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",
|
|
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
|
|
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]",
|
|
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
|
|
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]",
|
|
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
|
|
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
|
|
13261
|
-
var peerSchema =
|
|
13262
|
-
name:
|
|
13263
|
-
publicKey:
|
|
13264
|
-
tunnelIp:
|
|
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 =
|
|
13267
|
-
peers:
|
|
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:
|
|
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:
|
|
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]",
|
|
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
|
|
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",
|
|
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
|
|
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]",
|
|
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
|
|
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]",
|
|
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
|
|
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
|
|
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 =
|
|
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",
|
|
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
|