@marcusrbrown/infra 0.26.0 → 0.26.1
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 +245 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6950,7 +6950,7 @@ import { goke } from "goke";
|
|
|
6950
6950
|
// package.json
|
|
6951
6951
|
var package_default = {
|
|
6952
6952
|
name: "@marcusrbrown/infra",
|
|
6953
|
-
version: "0.26.
|
|
6953
|
+
version: "0.26.1",
|
|
6954
6954
|
description: "Infrastructure management CLI \u2014 deploy automation, health checks, and MCP bridge",
|
|
6955
6955
|
keywords: [
|
|
6956
6956
|
"infra",
|
|
@@ -12129,6 +12129,7 @@ function getGatewayDeployEnv() {
|
|
|
12129
12129
|
GATEWAY_OPERATOR_PUSH_VAPID_PRIVATE_KEY: process.env.GATEWAY_OPERATOR_PUSH_VAPID_PRIVATE_KEY ?? "",
|
|
12130
12130
|
GATEWAY_OPERATOR_PUSH_VAPID_SUBJECT: process.env.GATEWAY_OPERATOR_PUSH_VAPID_SUBJECT ?? "",
|
|
12131
12131
|
GATEWAY_OPERATOR_PUSH_VAPID_KEY_VERSION: process.env.GATEWAY_OPERATOR_PUSH_VAPID_KEY_VERSION ?? "",
|
|
12132
|
+
GATEWAY_BINDER_ALLOWLIST: process.env.GATEWAY_BINDER_ALLOWLIST ?? "",
|
|
12132
12133
|
GATEWAY_VPC_IP: process.env.GATEWAY_VPC_IP ?? "",
|
|
12133
12134
|
DASHBOARD_VPC_IP: process.env.DASHBOARD_VPC_IP ?? "",
|
|
12134
12135
|
GATEWAY_IMAGE_DIGEST: process.env.GATEWAY_IMAGE_DIGEST ?? "",
|
|
@@ -12448,7 +12449,176 @@ function registerGatewayRestore(cli) {
|
|
|
12448
12449
|
|
|
12449
12450
|
// src/commands/gateway/status.ts
|
|
12450
12451
|
import { z as z26 } from "zod";
|
|
12452
|
+
|
|
12453
|
+
// src/commands/gateway/workspace-memory.ts
|
|
12451
12454
|
var COMPOSE_PROJECT_DIR5 = "/opt/gateway/deploy";
|
|
12455
|
+
var MIB = 1024 * 1024;
|
|
12456
|
+
var READYZ_PROBE_HEADROOM_BYTES = 64 * MIB;
|
|
12457
|
+
var OPENCODE_STATES = ["starting", "ready", "down", "degraded"];
|
|
12458
|
+
var SECTION_MARKERS = {
|
|
12459
|
+
id: "@@ws-id@@",
|
|
12460
|
+
inspect: "@@ws-inspect@@",
|
|
12461
|
+
cgroup: "@@ws-cgroup@@",
|
|
12462
|
+
readyz: "@@ws-readyz@@"
|
|
12463
|
+
};
|
|
12464
|
+
var READYZ_SKIPPED_SENTINEL = "@@ws-readyz-skipped@@";
|
|
12465
|
+
var READYZ_URL = "http://127.0.0.1:9100/readyz";
|
|
12466
|
+
function buildReadyzProbeSnippet(url = READYZ_URL) {
|
|
12467
|
+
return `fetch("${url}",{signal:AbortSignal.timeout(3000)}).then(r=>r.text()).then(t=>process.stdout.write(t)).catch(()=>{})`;
|
|
12468
|
+
}
|
|
12469
|
+
function buildWorkspaceProbeScript() {
|
|
12470
|
+
return [
|
|
12471
|
+
`id=$(docker compose --project-directory ${COMPOSE_PROJECT_DIR5} ps -q workspace)`,
|
|
12472
|
+
`echo ${SECTION_MARKERS.id}`,
|
|
12473
|
+
'echo "$id"',
|
|
12474
|
+
`echo ${SECTION_MARKERS.inspect}`,
|
|
12475
|
+
`docker inspect --format '{{.HostConfig.Memory}} {{.State.OOMKilled}} {{.RestartCount}} {{.State.StartedAt}}' "$id"`,
|
|
12476
|
+
`echo ${SECTION_MARKERS.cgroup}`,
|
|
12477
|
+
'docker exec "$id" cat /sys/fs/cgroup/memory.current /sys/fs/cgroup/memory.events',
|
|
12478
|
+
`echo ${SECTION_MARKERS.readyz}`,
|
|
12479
|
+
`limit=$(docker inspect --format '{{.HostConfig.Memory}}' "$id")`,
|
|
12480
|
+
'cur=$(docker exec "$id" cat /sys/fs/cgroup/memory.current)',
|
|
12481
|
+
"run=no",
|
|
12482
|
+
`case "$limit" in ''|*[!0-9]*) ;; *)`,
|
|
12483
|
+
` case "$cur" in ''|*[!0-9]*) ;; *)`,
|
|
12484
|
+
` if [ "$limit" -gt 0 ] && [ $(( limit - cur )) -gt ${READYZ_PROBE_HEADROOM_BYTES} ]; then run=yes; fi ;;`,
|
|
12485
|
+
" esac ;;",
|
|
12486
|
+
"esac",
|
|
12487
|
+
'if [ "$run" = yes ]; then',
|
|
12488
|
+
` docker exec "$id" node -e '${buildReadyzProbeSnippet()}'`,
|
|
12489
|
+
"else",
|
|
12490
|
+
` echo ${READYZ_SKIPPED_SENTINEL}`,
|
|
12491
|
+
"fi",
|
|
12492
|
+
"exit 0"
|
|
12493
|
+
].join(`
|
|
12494
|
+
`);
|
|
12495
|
+
}
|
|
12496
|
+
function splitSections(stdoutText) {
|
|
12497
|
+
const markers = new Set(Object.values(SECTION_MARKERS));
|
|
12498
|
+
const sections = new Map;
|
|
12499
|
+
let current;
|
|
12500
|
+
for (const line of stdoutText.split(`
|
|
12501
|
+
`)) {
|
|
12502
|
+
const trimmed = line.trim();
|
|
12503
|
+
if (markers.has(trimmed)) {
|
|
12504
|
+
if (sections.has(trimmed))
|
|
12505
|
+
return;
|
|
12506
|
+
current = [];
|
|
12507
|
+
sections.set(trimmed, current);
|
|
12508
|
+
} else if (current !== undefined) {
|
|
12509
|
+
current.push(trimmed);
|
|
12510
|
+
}
|
|
12511
|
+
}
|
|
12512
|
+
return sections;
|
|
12513
|
+
}
|
|
12514
|
+
function parseCount(raw) {
|
|
12515
|
+
if (raw === undefined || !/^\d+$/.test(raw))
|
|
12516
|
+
return;
|
|
12517
|
+
const value = Number(raw);
|
|
12518
|
+
return Number.isSafeInteger(value) ? value : undefined;
|
|
12519
|
+
}
|
|
12520
|
+
function parseOpencodeState(body) {
|
|
12521
|
+
if (body.length === 0)
|
|
12522
|
+
return "unknown";
|
|
12523
|
+
let parsed;
|
|
12524
|
+
try {
|
|
12525
|
+
parsed = JSON.parse(body);
|
|
12526
|
+
} catch {
|
|
12527
|
+
return;
|
|
12528
|
+
}
|
|
12529
|
+
if (typeof parsed !== "object" || parsed === null || !("opencode" in parsed))
|
|
12530
|
+
return "unknown";
|
|
12531
|
+
return OPENCODE_STATES.find((state) => state === parsed.opencode) ?? "unknown";
|
|
12532
|
+
}
|
|
12533
|
+
var unknownWorkspace = (reason) => ({ status: "unknown", reason });
|
|
12534
|
+
function parseWorkspaceProbeOutput(stdoutText) {
|
|
12535
|
+
const sections = splitSections(stdoutText);
|
|
12536
|
+
const idLines = sections?.get(SECTION_MARKERS.id);
|
|
12537
|
+
const inspectLines = sections?.get(SECTION_MARKERS.inspect);
|
|
12538
|
+
const cgroupLines = sections?.get(SECTION_MARKERS.cgroup);
|
|
12539
|
+
const readyzLines = sections?.get(SECTION_MARKERS.readyz);
|
|
12540
|
+
if (!idLines || !inspectLines || !cgroupLines || !readyzLines)
|
|
12541
|
+
return unknownWorkspace("incomplete probe output");
|
|
12542
|
+
const ids = idLines.filter((line) => line.length > 0);
|
|
12543
|
+
if (ids.length !== 1 || !/^[\da-f]{12,64}$/i.test(ids[0] ?? "")) {
|
|
12544
|
+
return unknownWorkspace("workspace container not found");
|
|
12545
|
+
}
|
|
12546
|
+
const inspectFields = inspectLines.filter((line) => line.length > 0).join(" ").split(/\s+/);
|
|
12547
|
+
const [memoryRaw, oomKilledRaw, restartRaw, startedAt] = inspectFields;
|
|
12548
|
+
const limitBytes = parseCount(memoryRaw);
|
|
12549
|
+
const restartCount = parseCount(restartRaw);
|
|
12550
|
+
if (inspectFields.length !== 4 || limitBytes === undefined || restartCount === undefined || oomKilledRaw !== "true" && oomKilledRaw !== "false" || startedAt === undefined || !/^\d{4}-\d\d-\d\dT/.test(startedAt)) {
|
|
12551
|
+
return unknownWorkspace("unparsable docker inspect output");
|
|
12552
|
+
}
|
|
12553
|
+
const cgroup = cgroupLines.filter((line) => line.length > 0);
|
|
12554
|
+
const currentBytes = parseCount(cgroup[0]);
|
|
12555
|
+
const events = new Map;
|
|
12556
|
+
for (const line of cgroup.slice(1)) {
|
|
12557
|
+
const [key, value] = line.split(/\s+/);
|
|
12558
|
+
if (key !== undefined && value !== undefined)
|
|
12559
|
+
events.set(key, value);
|
|
12560
|
+
}
|
|
12561
|
+
const maxEvents = parseCount(events.get("max"));
|
|
12562
|
+
const oomKills = parseCount(events.get("oom_kill"));
|
|
12563
|
+
if (currentBytes === undefined || maxEvents === undefined || oomKills === undefined) {
|
|
12564
|
+
return unknownWorkspace("unparsable cgroup memory output");
|
|
12565
|
+
}
|
|
12566
|
+
const probeAllowed = limitBytes > 0 && limitBytes - currentBytes > READYZ_PROBE_HEADROOM_BYTES;
|
|
12567
|
+
let opencode;
|
|
12568
|
+
if (readyzLines.includes(READYZ_SKIPPED_SENTINEL))
|
|
12569
|
+
opencode = "skipped";
|
|
12570
|
+
else if (probeAllowed)
|
|
12571
|
+
opencode = parseOpencodeState(readyzLines.join("").trim());
|
|
12572
|
+
else
|
|
12573
|
+
opencode = "unknown";
|
|
12574
|
+
if (opencode === undefined)
|
|
12575
|
+
return unknownWorkspace("unparsable readyz body");
|
|
12576
|
+
return {
|
|
12577
|
+
status: "ok",
|
|
12578
|
+
limitBytes,
|
|
12579
|
+
currentBytes,
|
|
12580
|
+
maxEvents,
|
|
12581
|
+
oomKills,
|
|
12582
|
+
oomKilled: oomKilledRaw === "true",
|
|
12583
|
+
restartCount,
|
|
12584
|
+
startedAt,
|
|
12585
|
+
opencode
|
|
12586
|
+
};
|
|
12587
|
+
}
|
|
12588
|
+
var formatMiB = (bytes) => `${Math.round(bytes / MIB)}`;
|
|
12589
|
+
function formatOpencode(status) {
|
|
12590
|
+
if (status.opencode !== "skipped")
|
|
12591
|
+
return status.opencode;
|
|
12592
|
+
if (status.limitBytes === 0)
|
|
12593
|
+
return "skipped (no limit)";
|
|
12594
|
+
if (status.limitBytes - status.currentBytes <= READYZ_PROBE_HEADROOM_BYTES) {
|
|
12595
|
+
return `skipped (within ${READYZ_PROBE_HEADROOM_BYTES / MIB} MiB of the limit)`;
|
|
12596
|
+
}
|
|
12597
|
+
return "skipped (memory read unavailable)";
|
|
12598
|
+
}
|
|
12599
|
+
function formatWorkspaceMemory(status) {
|
|
12600
|
+
if (status.status === "unknown")
|
|
12601
|
+
return [`Workspace memory: unknown (${status.reason})`];
|
|
12602
|
+
return [
|
|
12603
|
+
"Workspace memory",
|
|
12604
|
+
` Limit: ${status.limitBytes === 0 ? "none" : `${formatMiB(status.limitBytes)} MiB`}`,
|
|
12605
|
+
` Usage: ${formatMiB(status.currentBytes)} MiB`,
|
|
12606
|
+
` Max events: ${status.maxEvents}`,
|
|
12607
|
+
` OOM kills: ${status.oomKills} (counted since the container last started)`,
|
|
12608
|
+
` Restarts: ${status.restartCount} (started ${status.startedAt})`,
|
|
12609
|
+
` OOMKilled: ${status.oomKilled}`,
|
|
12610
|
+
` OpenCode: ${formatOpencode(status)}`
|
|
12611
|
+
];
|
|
12612
|
+
}
|
|
12613
|
+
function formatWorkspaceSummary(status) {
|
|
12614
|
+
if (status.status === "unknown")
|
|
12615
|
+
return "workspace=unknown";
|
|
12616
|
+
const mem = status.limitBytes === 0 ? `mem=${formatMiB(status.currentBytes)}MiB limit=none` : `mem=${formatMiB(status.currentBytes)}/${formatMiB(status.limitBytes)}MiB`;
|
|
12617
|
+
return `workspace ${mem} oom_kills=${status.oomKills} opencode=${status.opencode}`;
|
|
12618
|
+
}
|
|
12619
|
+
|
|
12620
|
+
// src/commands/gateway/status.ts
|
|
12621
|
+
var WORKSPACE_PROBE_TIMEOUT_MS = 1e4;
|
|
12452
12622
|
function parseComposePsOutput2(stdoutText) {
|
|
12453
12623
|
const trimmed = stdoutText.trim();
|
|
12454
12624
|
if (trimmed.length === 0)
|
|
@@ -12534,9 +12704,55 @@ async function getGatewayComposeStatus(host, spawn = defaultSpawn5) {
|
|
|
12534
12704
|
const ok = isAllRunning2(services);
|
|
12535
12705
|
return { ok, services };
|
|
12536
12706
|
}
|
|
12537
|
-
async function
|
|
12538
|
-
|
|
12539
|
-
|
|
12707
|
+
async function getGatewayWorkspaceStatus(host, spawn = defaultSpawn5, timeoutMs = WORKSPACE_PROBE_TIMEOUT_MS) {
|
|
12708
|
+
validateGatewayHost(host);
|
|
12709
|
+
const knownHostsArgs = buildKnownHostsArgs();
|
|
12710
|
+
const { args: identityArgs, cleanup } = buildIdentityArgs(process.env.GATEWAY_SSH_KEY);
|
|
12711
|
+
const sshCmd = [
|
|
12712
|
+
"ssh",
|
|
12713
|
+
"-o",
|
|
12714
|
+
"BatchMode=yes",
|
|
12715
|
+
"-o",
|
|
12716
|
+
"StrictHostKeyChecking=yes",
|
|
12717
|
+
...knownHostsArgs,
|
|
12718
|
+
...identityArgs,
|
|
12719
|
+
`root@${host}`,
|
|
12720
|
+
buildWorkspaceProbeScript()
|
|
12721
|
+
];
|
|
12722
|
+
const env = {
|
|
12723
|
+
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
|
12724
|
+
HOME: process.env.HOME ?? "/tmp",
|
|
12725
|
+
...process.env.SSH_AUTH_SOCK ? { SSH_AUTH_SOCK: process.env.SSH_AUTH_SOCK } : {}
|
|
12726
|
+
};
|
|
12727
|
+
let timer;
|
|
12728
|
+
const timedOut = new Promise((resolve) => {
|
|
12729
|
+
timer = setTimeout(() => resolve("timeout"), timeoutMs);
|
|
12730
|
+
});
|
|
12731
|
+
try {
|
|
12732
|
+
const child = spawn(sshCmd, { env, stdout: "pipe", stderr: "pipe" });
|
|
12733
|
+
const collected = Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
12734
|
+
collected.catch(() => {
|
|
12735
|
+
return;
|
|
12736
|
+
});
|
|
12737
|
+
const outcome = await Promise.race([collected, timedOut]);
|
|
12738
|
+
if (outcome === "timeout") {
|
|
12739
|
+
child.kill?.();
|
|
12740
|
+
return unknownWorkspace(`timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
12741
|
+
}
|
|
12742
|
+
const [stdoutText, , exitCode] = outcome;
|
|
12743
|
+
if (exitCode !== 0)
|
|
12744
|
+
return unknownWorkspace(`SSH command failed (exit ${exitCode})`);
|
|
12745
|
+
return parseWorkspaceProbeOutput(stdoutText);
|
|
12746
|
+
} catch {
|
|
12747
|
+
return unknownWorkspace("SSH command could not run");
|
|
12748
|
+
} finally {
|
|
12749
|
+
clearTimeout(timer);
|
|
12750
|
+
cleanup();
|
|
12751
|
+
}
|
|
12752
|
+
}
|
|
12753
|
+
async function getGatewayStatusSummary(host, spawn) {
|
|
12754
|
+
const result = await getGatewayComposeStatus(host, spawn);
|
|
12755
|
+
if (result.services.length === 0) {
|
|
12540
12756
|
const errorMsg = result.error ?? "No services reported";
|
|
12541
12757
|
return {
|
|
12542
12758
|
app: "gateway",
|
|
@@ -12548,16 +12764,19 @@ async function getGatewayStatusSummary(host) {
|
|
|
12548
12764
|
};
|
|
12549
12765
|
}
|
|
12550
12766
|
const rows = result.services.map((s) => `${s.service}:${s.state}/${s.health}`).join(", ");
|
|
12767
|
+
const workspace = await getGatewayWorkspaceStatus(host, spawn);
|
|
12768
|
+
const prefix = result.ok ? `OK: ${rows}` : `DEGRADED: ${rows}`;
|
|
12551
12769
|
return {
|
|
12552
12770
|
app: "gateway",
|
|
12553
|
-
http:
|
|
12771
|
+
http: `${prefix}; ${formatWorkspaceSummary(workspace)}`,
|
|
12554
12772
|
lastDeploy: "\u2014",
|
|
12555
12773
|
version: "\u2014",
|
|
12556
12774
|
contentHash: "\u2014",
|
|
12557
|
-
usageStats: "\u2014"
|
|
12775
|
+
usageStats: "\u2014",
|
|
12776
|
+
workspace
|
|
12558
12777
|
};
|
|
12559
12778
|
}
|
|
12560
|
-
async function gatewayStatusAction(options, ctx, spawn) {
|
|
12779
|
+
async function gatewayStatusAction(options, ctx, spawn, workspaceTimeoutMs) {
|
|
12561
12780
|
const hostEnvKey = options.key ?? "GATEWAY_HOST";
|
|
12562
12781
|
const host = process.env[hostEnvKey];
|
|
12563
12782
|
if (!host) {
|
|
@@ -12565,14 +12784,24 @@ async function gatewayStatusAction(options, ctx, spawn) {
|
|
|
12565
12784
|
ctx.process.exit(1);
|
|
12566
12785
|
return;
|
|
12567
12786
|
}
|
|
12568
|
-
|
|
12569
|
-
|
|
12787
|
+
const json = options.json === true;
|
|
12788
|
+
if (!json) {
|
|
12789
|
+
ctx.console.log("Gateway status");
|
|
12790
|
+
ctx.console.log("");
|
|
12791
|
+
}
|
|
12570
12792
|
const result = await getGatewayComposeStatus(host, spawn);
|
|
12571
12793
|
if (!result.ok && result.services.length === 0) {
|
|
12572
12794
|
ctx.console.error(`Error: ${result.error ?? "Unknown error"}`);
|
|
12573
12795
|
ctx.process.exit(1);
|
|
12574
12796
|
return;
|
|
12575
12797
|
}
|
|
12798
|
+
if (json) {
|
|
12799
|
+
const workspace = await getGatewayWorkspaceStatus(host, spawn, workspaceTimeoutMs);
|
|
12800
|
+
ctx.console.log(JSON.stringify({ ok: result.ok, services: result.services, workspace }));
|
|
12801
|
+
if (!result.ok)
|
|
12802
|
+
ctx.process.exit(1);
|
|
12803
|
+
return;
|
|
12804
|
+
}
|
|
12576
12805
|
ctx.console.log("Service State Health");
|
|
12577
12806
|
ctx.console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
12578
12807
|
for (const row of result.services) {
|
|
@@ -12581,6 +12810,11 @@ async function gatewayStatusAction(options, ctx, spawn) {
|
|
|
12581
12810
|
ctx.console.log(`${svc} ${state} ${row.health}`);
|
|
12582
12811
|
}
|
|
12583
12812
|
ctx.console.log("");
|
|
12813
|
+
const workspace = await getGatewayWorkspaceStatus(host, spawn, workspaceTimeoutMs);
|
|
12814
|
+
for (const line of formatWorkspaceMemory(workspace)) {
|
|
12815
|
+
ctx.console.log(line);
|
|
12816
|
+
}
|
|
12817
|
+
ctx.console.log("");
|
|
12584
12818
|
if (result.ok) {
|
|
12585
12819
|
ctx.console.log("Status: OK");
|
|
12586
12820
|
} else {
|
|
@@ -12589,7 +12823,7 @@ async function gatewayStatusAction(options, ctx, spawn) {
|
|
|
12589
12823
|
}
|
|
12590
12824
|
}
|
|
12591
12825
|
function registerGatewayStatus(cli) {
|
|
12592
|
-
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));
|
|
12826
|
+
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.")).option("--json", z26.boolean().describe("Output a single JSON object with ok, services and the structured workspace memory status.")).action((options, ctx) => gatewayStatusAction(options, ctx));
|
|
12593
12827
|
}
|
|
12594
12828
|
|
|
12595
12829
|
// src/commands/gateway/index.ts
|
|
@@ -14123,7 +14357,7 @@ function registerStatus(cli, dependencies = {
|
|
|
14123
14357
|
getVpnStatusSummary,
|
|
14124
14358
|
getBrokerStatusSummary
|
|
14125
14359
|
}) {
|
|
14126
|
-
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));
|
|
14360
|
+
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. The gateway object also carries a structured workspace field.")).option("--verbose", z37.boolean().describe("Include verbose per-app health check details when building the summary rows.")).action((options, ctx) => unifiedStatusAction(options, ctx, dependencies));
|
|
14127
14361
|
}
|
|
14128
14362
|
|
|
14129
14363
|
// src/cli.ts
|