@clawops/cli 1.7.9 → 2.0.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/README.md +504 -76
- package/dist/apply-KYFPWI5U.js +27 -0
- package/dist/automation-RMAIBNVC.js +44 -0
- package/dist/bedrock-XVASJX2K.js +61 -0
- package/dist/{bootstrap-HL4DNUNG.js → bootstrap-HNGKY6WW.js} +31 -19
- package/dist/bootstrap.sh.tmpl +51 -31
- package/dist/channels-KBUR42IU.js +53 -0
- package/dist/chunk-2I7KCMXZ.js +142 -0
- package/dist/{chunk-DQDAWNGB.js → chunk-2Z7J3ERQ.js} +1 -1
- package/dist/chunk-3OLUJPCB.js +36 -0
- package/dist/chunk-5XT5IAJK.js +69 -0
- package/dist/chunk-AEEMRFZV.js +24 -0
- package/dist/chunk-BSLIYB5Q.js +106 -0
- package/dist/{chunk-3XK66DKK.js → chunk-E4NJSJ54.js} +57 -93
- package/dist/chunk-FEK7VJAY.js +16 -0
- package/dist/{chunk-UKTP332Z.js → chunk-GAZPPSKU.js} +6 -7
- package/dist/chunk-GIEVAGEK.js +126 -0
- package/dist/chunk-HSPHXG6W.js +67 -0
- package/dist/chunk-IVWP4QUJ.js +901 -0
- package/dist/{chunk-PVUVI35Y.js → chunk-JW5A7E3X.js} +5 -1
- package/dist/{chunk-GZN7PKP4.js → chunk-KC3GC3PE.js} +328 -121
- package/dist/chunk-LBE6COC4.js +244 -0
- package/dist/chunk-LEVIW2PV.js +890 -0
- package/dist/chunk-LT4XMO5D.js +50 -0
- package/dist/chunk-MC5ATCXS.js +108 -0
- package/dist/chunk-N4WZLWJ4.js +16 -0
- package/dist/chunk-NUDIHZEH.js +55 -0
- package/dist/chunk-NUL5F6YV.js +56 -0
- package/dist/chunk-RHOWYIWT.js +52 -0
- package/dist/chunk-STLRHLLV.js +193 -0
- package/dist/chunk-V25QRZRB.js +45 -0
- package/dist/chunk-VOEJYRIK.js +33 -0
- package/dist/{providers-2OABPW2E.js → chunk-VUFMZEIM.js} +3 -2
- package/dist/chunk-W3OLNNJW.js +51 -0
- package/dist/cli-WLD3EBEM.js +11 -0
- package/dist/cli-auth-GD5SK7JU.js +13 -0
- package/dist/cli.js +1296 -695
- package/dist/config-validate-K2N6XLDX.js +76 -0
- package/dist/context-7EVC6W45.js +20 -0
- package/dist/docker-3PDO4TS7.js +12 -0
- package/dist/{firewall-XQGO7HWK.js → firewall-5VKZ4WBH.js} +32 -1
- package/dist/generate-I6YQ5V2T.js +29 -0
- package/dist/{harden-URPXLSVF.js → harden-72JMRT4E.js} +51 -11
- package/dist/health-5CCOXYGG.js +11 -0
- package/dist/index.d.ts +56 -0
- package/dist/known-hosts-file-I3Q3NBYQ.js +22 -0
- package/dist/logs-5A7YYA53.js +15 -0
- package/dist/{mcp-wire-7E5JQY47.js → mcp-wire-3SWBM7D6.js} +32 -16
- package/dist/models-AGTQWYDL.js +32 -0
- package/dist/network-args-PYJRJBYO.js +64 -0
- package/dist/{package-5AR4ZPK4.js → package-5BCMEBTE.js} +11 -4
- package/dist/passphrase-MJBE45N7.js +15 -0
- package/dist/plugins-DTST3OER.js +49 -0
- package/dist/{pool-QGFQRMGU.js → pool-PIIKJ4L2.js} +3 -2
- package/dist/preflight-EF52S3WW.js +162 -0
- package/dist/preflight-MWWM5JT3.js +181 -0
- package/dist/preflight-TFD4N5ZU.js +141 -0
- package/dist/privileged-JFA6LLTV.js +17 -0
- package/dist/providers-FW6H6OPF.js +11 -0
- package/dist/ready-KDNRBNSU.js +95 -0
- package/dist/{remote-config-QDLN4UMG.js → remote-config-3HZLUWEA.js} +4 -3
- package/dist/runtime-X3KW5TCP.js +44 -0
- package/dist/server-GBA7BJGK.js +46 -0
- package/dist/spec-path-KQOOIQCA.js +9 -0
- package/dist/ssh-VAI5DU5M.js +11 -0
- package/dist/ssh-key-DUO6OKOI.js +7 -0
- package/dist/stack-config-R4SPSUQZ.js +12 -0
- package/dist/upgrade-ULD4KFYC.js +88 -0
- package/dist/validate-YVI3BJ55.js +14 -0
- package/dist/{version-guard-VUCNZXQS.js → version-guard-XVYW5WFJ.js} +2 -2
- package/dist/{versions-2UVAT6PQ.js → versions-W5LRQCAJ.js} +2 -2
- package/dist/wait-WO72TRRQ.js +79 -0
- package/package.json +11 -4
- package/spec/deploy-plan.schema.json +114 -25
- package/spec/integrations.yaml +135 -28
- package/spec/mcp-tools.yaml +89 -37
- package/spec/models.yaml +23 -3
- package/spec/openclaw-2.0.config.schema.json +1 -0
- package/spec/openclaw-versions.yaml +116 -27
- package/spec/providers.schema.json +163 -28
- package/dist/apply-F3KY2HTB.js +0 -17
- package/dist/automation-RULUSJBW.js +0 -30
- package/dist/aws-L4RR2LSE.js +0 -254
- package/dist/azure-7OXQENQL.js +0 -262
- package/dist/chunk-35WVQY3X.js +0 -122
- package/dist/chunk-6X45DDGH.js +0 -42
- package/dist/chunk-A4QR4TXW.js +0 -120
- package/dist/chunk-C5X375RO.js +0 -85
- package/dist/chunk-FNS56UQY.js +0 -741
- package/dist/chunk-P5LKWRPL.js +0 -114
- package/dist/chunk-SEZMVVAS.js +0 -156
- package/dist/context-5LCUG2QR.js +0 -10
- package/dist/gcp-UG6AWPIZ.js +0 -193
- package/dist/generate-OAGYT3VH.js +0 -15
- package/dist/local-DXBEVZ5C.js +0 -47
- package/dist/server-FIZCOLHL.js +0 -32
- package/dist/ssh-CNJNLM3E.js +0 -8
- package/dist/validate-CFQ4FH2T.js +0 -10
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
inspectContainer
|
|
4
|
+
} from "./chunk-3OLUJPCB.js";
|
|
5
|
+
import {
|
|
6
|
+
execPrivileged
|
|
7
|
+
} from "./chunk-NUDIHZEH.js";
|
|
8
|
+
import {
|
|
9
|
+
interpretProbe,
|
|
10
|
+
probeCommand
|
|
11
|
+
} from "./chunk-LT4XMO5D.js";
|
|
12
|
+
import {
|
|
13
|
+
PUBLISH_INSPECT_CMD,
|
|
14
|
+
STATE_DIR_HOST_LINUX,
|
|
15
|
+
configPathForOS,
|
|
16
|
+
publishForRestart
|
|
17
|
+
} from "./chunk-GIEVAGEK.js";
|
|
18
|
+
import {
|
|
19
|
+
GATEWAY_PORT
|
|
20
|
+
} from "./chunk-VOEJYRIK.js";
|
|
21
|
+
import {
|
|
22
|
+
chalk,
|
|
23
|
+
failure
|
|
24
|
+
} from "./chunk-Q7NQY5HV.js";
|
|
25
|
+
|
|
26
|
+
// src/cli/commands/monitor.ts
|
|
27
|
+
import { defineCommand } from "citty";
|
|
28
|
+
import process from "process";
|
|
29
|
+
function formatUptime(startedAt) {
|
|
30
|
+
if (!startedAt) return "\u2014";
|
|
31
|
+
const ms = Date.now() - new Date(startedAt).getTime();
|
|
32
|
+
if (ms < 0 || isNaN(ms)) return "\u2014";
|
|
33
|
+
const totalSecs = Math.floor(ms / 1e3);
|
|
34
|
+
const mins = Math.floor(totalSecs / 60);
|
|
35
|
+
const hours = Math.floor(mins / 60);
|
|
36
|
+
const days = Math.floor(hours / 24);
|
|
37
|
+
if (days > 0) return `${days}d ${hours % 24}h ${mins % 60}m`;
|
|
38
|
+
if (hours > 0) return `${hours}h ${mins % 60}m`;
|
|
39
|
+
return `${mins}m ${totalSecs % 60}s`;
|
|
40
|
+
}
|
|
41
|
+
async function gatherSnapshot(session, signal, tailLines = 10) {
|
|
42
|
+
const DOCKER = "PATH=/usr/local/bin:/opt/homebrew/bin:$PATH docker";
|
|
43
|
+
const [inspectRaw, statsRaw, healthRaw, configRaw, diskRaw, logsRaw] = await Promise.all([
|
|
44
|
+
inspectContainer(
|
|
45
|
+
session,
|
|
46
|
+
"openclaw",
|
|
47
|
+
"{{.State.Status}}|{{.Config.Image}}|{{.State.StartedAt}}|{{.RestartCount}}",
|
|
48
|
+
signal,
|
|
49
|
+
DOCKER
|
|
50
|
+
).then((r) => ({
|
|
51
|
+
// The dashboard shows a word per field; a refusal says so rather than borrowing
|
|
52
|
+
// "not found", which would claim the container is gone.
|
|
53
|
+
stdout: r.kind === "ok" ? r.value : r.kind === "missing" ? "not found|||0" : "unreachable|||0",
|
|
54
|
+
stderr: r.kind === "error" ? r.detail : "",
|
|
55
|
+
code: r.kind === "ok" ? 0 : 1
|
|
56
|
+
})),
|
|
57
|
+
execPrivileged(
|
|
58
|
+
session,
|
|
59
|
+
`${DOCKER} stats openclaw --no-stream --format '{{.MemUsage}}|{{.CPUPerc}}' 2>/dev/null || echo '\u2014|\u2014'`,
|
|
60
|
+
signal
|
|
61
|
+
),
|
|
62
|
+
session.exec(probeCommand("live", GATEWAY_PORT), signal),
|
|
63
|
+
session.exec(
|
|
64
|
+
`cat ${configPathForOS("Linux")} 2>/dev/null || echo '{}'`,
|
|
65
|
+
signal
|
|
66
|
+
),
|
|
67
|
+
session.exec(
|
|
68
|
+
// The state directory, not the service user's home: 2.0 keeps the SQLite database
|
|
69
|
+
// there, so that is the filesystem that fills up.
|
|
70
|
+
`df -h ${STATE_DIR_HOST_LINUX} 2>/dev/null | awk 'NR==2{print $5" used ("$3" of "$2")"}'`,
|
|
71
|
+
signal
|
|
72
|
+
),
|
|
73
|
+
execPrivileged(
|
|
74
|
+
session,
|
|
75
|
+
`${DOCKER} logs openclaw -n ${tailLines} 2>&1 || echo '(no logs)'`,
|
|
76
|
+
signal
|
|
77
|
+
)
|
|
78
|
+
]);
|
|
79
|
+
const inspectParts = inspectRaw.stdout.trim().split("|");
|
|
80
|
+
const status = inspectParts[0] ?? "not found";
|
|
81
|
+
const image = inspectParts[1] ?? "";
|
|
82
|
+
const startedAt = inspectParts[2] ?? "";
|
|
83
|
+
const restartCount = parseInt(inspectParts[3] ?? "0", 10) || 0;
|
|
84
|
+
const statsParts = statsRaw.stdout.trim().split("|");
|
|
85
|
+
const memUsage = statsParts[0] ?? "\u2014";
|
|
86
|
+
const cpuPct = statsParts[1] ?? "\u2014";
|
|
87
|
+
let version = "unknown";
|
|
88
|
+
let authMode = "unknown";
|
|
89
|
+
try {
|
|
90
|
+
const cfg = JSON.parse(configRaw.stdout.trim());
|
|
91
|
+
version = cfg.meta?.lastTouchedVersion ?? "unknown";
|
|
92
|
+
authMode = cfg.gateway?.auth?.mode ?? "unknown";
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
container: { status, image, startedAt, restartCount, memUsage, cpuPct },
|
|
97
|
+
gateway: {
|
|
98
|
+
// Judged on the payload: a status code cannot distinguish a healthy gateway from
|
|
99
|
+
// a path that fell through to the Control UI. See src/openclaw/health.ts.
|
|
100
|
+
reachable: interpretProbe("live", healthRaw.stdout).ok,
|
|
101
|
+
version,
|
|
102
|
+
authMode
|
|
103
|
+
},
|
|
104
|
+
disk: diskRaw.stdout.trim() || "\u2014",
|
|
105
|
+
logLines: logsRaw.stdout.split("\n").filter((l) => l.trim().length > 0),
|
|
106
|
+
capturedAt: /* @__PURE__ */ new Date()
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function renderSnapshot(snap, opts) {
|
|
110
|
+
const noColor = opts.noColor ?? false;
|
|
111
|
+
const g = noColor ? { green: (s) => s, red: (s) => s, yellow: (s) => s, dim: (s) => s, bold: (s) => s } : chalk;
|
|
112
|
+
const LINE = "\u2500".repeat(68);
|
|
113
|
+
const timeStr = snap.capturedAt.toLocaleTimeString();
|
|
114
|
+
const header = `clawops monitor \u2014 ${opts.stackName}`;
|
|
115
|
+
const padding = Math.max(1, 68 - header.length - `updated ${timeStr}`.length);
|
|
116
|
+
const gatewayStr = snap.gateway.reachable ? g.green("\u2713 healthy") : g.red("\u2717 unreachable");
|
|
117
|
+
const containerStr = snap.container.status === "running" ? g.green("\u2713 running") : snap.container.status === "not found" ? g.red("\u2717 not found") : g.yellow(`\u26A0 ${snap.container.status}`);
|
|
118
|
+
const uptime = formatUptime(snap.container.startedAt);
|
|
119
|
+
const shortImage = snap.container.image.replace("ghcr.io/openclaw/openclaw:", "") || "\u2014";
|
|
120
|
+
const lines = [
|
|
121
|
+
g.bold(header) + " ".repeat(padding) + g.dim(`updated ${timeStr}`),
|
|
122
|
+
LINE,
|
|
123
|
+
"",
|
|
124
|
+
` Stack ${g.bold(opts.stackName)}`,
|
|
125
|
+
` Gateway ${gatewayStr} version ${g.bold(snap.gateway.version)} auth ${snap.gateway.authMode}`,
|
|
126
|
+
` Container ${containerStr} ${g.dim(snap.container.image)}`,
|
|
127
|
+
` Image tag ${shortImage} uptime ${uptime}`,
|
|
128
|
+
` Resources CPU ${snap.container.cpuPct} Memory ${snap.container.memUsage}`,
|
|
129
|
+
` Restarts ${snap.container.restartCount} Disk ${snap.disk}`,
|
|
130
|
+
"",
|
|
131
|
+
LINE,
|
|
132
|
+
""
|
|
133
|
+
];
|
|
134
|
+
if (opts.showLogs) {
|
|
135
|
+
lines.push(` Logs (last ${snap.logLines.length} lines)`);
|
|
136
|
+
for (const l of snap.logLines) {
|
|
137
|
+
lines.push(" " + g.dim(l));
|
|
138
|
+
}
|
|
139
|
+
lines.push("");
|
|
140
|
+
lines.push(LINE);
|
|
141
|
+
lines.push("");
|
|
142
|
+
}
|
|
143
|
+
const logToggle = opts.showLogs ? "hide logs" : "show logs";
|
|
144
|
+
const backHint = opts.menuMode ? " [s] back" : "";
|
|
145
|
+
lines.push(g.dim(` [r] refresh [l] ${logToggle} [q] quit${backHint} interval: ${opts.intervalSec}s`));
|
|
146
|
+
return lines.join("\n");
|
|
147
|
+
}
|
|
148
|
+
async function probeEntries() {
|
|
149
|
+
const { buildContext } = await import("./context-7EVC6W45.js");
|
|
150
|
+
const { getConfig } = await import("./store-SDUR52Z5.js");
|
|
151
|
+
const config = getConfig();
|
|
152
|
+
if (!config) return [];
|
|
153
|
+
return Promise.all(
|
|
154
|
+
Object.entries(config.stacks).map(async ([name, stackCfg]) => {
|
|
155
|
+
const base = {
|
|
156
|
+
name,
|
|
157
|
+
provider: stackCfg.provider,
|
|
158
|
+
region: stackCfg.region ?? "\u2014",
|
|
159
|
+
deployed: false
|
|
160
|
+
};
|
|
161
|
+
try {
|
|
162
|
+
const ctx = buildContext({ stack: name });
|
|
163
|
+
if (ctx.adapter.name === "local") {
|
|
164
|
+
return { ...base, deployed: !!ctx.localState };
|
|
165
|
+
}
|
|
166
|
+
const stack = await ctx.getStack();
|
|
167
|
+
const outputMap = await stack.outputs();
|
|
168
|
+
const outputs = Object.fromEntries(
|
|
169
|
+
Object.entries(outputMap).map(([k, v]) => [k, v.value])
|
|
170
|
+
);
|
|
171
|
+
return { ...base, deployed: !!outputs["publicIp"] };
|
|
172
|
+
} catch {
|
|
173
|
+
return base;
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
function renderMenu(entries, selectedIdx, showAll, noColor, confirmDelete) {
|
|
179
|
+
const g = noColor ? { green: (s) => s, red: (s) => s, dim: (s) => s, bold: (s) => s, yellow: (s) => s } : chalk;
|
|
180
|
+
const visible = showAll ? entries : entries.filter((e) => e.deployed);
|
|
181
|
+
const totalDeployed = entries.filter((e) => e.deployed).length;
|
|
182
|
+
const heading = showAll ? "clawops monitor \u2014 all stacks" : "clawops monitor \u2014 select a stack";
|
|
183
|
+
const countStr = showAll ? `(${totalDeployed} of ${entries.length} running)` : `(${totalDeployed} running \xB7 ${entries.length - totalDeployed} not deployed)`;
|
|
184
|
+
const lines = ["", ` ${g.bold(heading)} ${g.dim(countStr)}`, ""];
|
|
185
|
+
if (visible.length === 0) {
|
|
186
|
+
if (showAll) {
|
|
187
|
+
lines.push(" No stacks configured. Run `clawops init` to set up.");
|
|
188
|
+
} else {
|
|
189
|
+
lines.push(" No deployed stacks found.");
|
|
190
|
+
lines.push("");
|
|
191
|
+
lines.push(g.dim(" [a] show all [q] quit"));
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
for (let i = 0; i < visible.length; i++) {
|
|
195
|
+
const e = visible[i];
|
|
196
|
+
const cursor = i === selectedIdx ? "\u25B6" : " ";
|
|
197
|
+
const nameStr = i === selectedIdx ? g.bold(e.name.padEnd(24)) : e.name.padEnd(24);
|
|
198
|
+
const provStr = e.provider.padEnd(10);
|
|
199
|
+
const regStr = e.region.padEnd(14);
|
|
200
|
+
const statusStr = showAll ? e.deployed ? g.green("\u2713 running") : g.dim("\u2717 not deployed") : "";
|
|
201
|
+
lines.push(` ${cursor} ${nameStr}${provStr}${regStr}${statusStr}`);
|
|
202
|
+
}
|
|
203
|
+
lines.push("");
|
|
204
|
+
if (confirmDelete !== null) {
|
|
205
|
+
lines.push(` ${g.yellow(`Delete "${confirmDelete}" from registry?`)} [y/n]`);
|
|
206
|
+
} else {
|
|
207
|
+
const sel = visible[selectedIdx];
|
|
208
|
+
const canDelete = showAll && sel !== void 0 && !sel.deployed;
|
|
209
|
+
const toggleLabel = showAll ? "[a] running only" : "[a] show all";
|
|
210
|
+
const deleteHint = canDelete ? " [d] delete" : "";
|
|
211
|
+
lines.push(g.dim(` [\u2191\u2193] navigate [enter] select${deleteHint} ${toggleLabel} [q] quit`));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
lines.push("");
|
|
215
|
+
return lines.join("\n");
|
|
216
|
+
}
|
|
217
|
+
async function deleteFromRegistry(name) {
|
|
218
|
+
const { requireConfig, setConfig } = await import("./store-SDUR52Z5.js");
|
|
219
|
+
const config = requireConfig();
|
|
220
|
+
const newStacks = { ...config.stacks };
|
|
221
|
+
delete newStacks[name];
|
|
222
|
+
const updated = { ...config, stacks: newStacks };
|
|
223
|
+
if (name === config.defaults.stack) {
|
|
224
|
+
updated.defaults = { ...config.defaults, stack: Object.keys(newStacks)[0] };
|
|
225
|
+
}
|
|
226
|
+
setConfig(updated);
|
|
227
|
+
}
|
|
228
|
+
async function runStackMenu(ac, setKeyHandler, noColor) {
|
|
229
|
+
process.stdout.write("\x1B[2J\x1B[H\n Checking stacks...\n");
|
|
230
|
+
const entries = await probeEntries();
|
|
231
|
+
if (ac.signal.aborted) return null;
|
|
232
|
+
let selectedIdx = 0;
|
|
233
|
+
let showAll = false;
|
|
234
|
+
let confirmDelete = null;
|
|
235
|
+
function visible() {
|
|
236
|
+
return showAll ? entries : entries.filter((e) => e.deployed);
|
|
237
|
+
}
|
|
238
|
+
function redraw() {
|
|
239
|
+
process.stdout.write("\x1B[2J\x1B[H" + renderMenu(entries, selectedIdx, showAll, noColor, confirmDelete));
|
|
240
|
+
}
|
|
241
|
+
redraw();
|
|
242
|
+
return new Promise((resolve) => {
|
|
243
|
+
if (ac.signal.aborted) {
|
|
244
|
+
resolve(null);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
ac.signal.addEventListener("abort", () => resolve(null), { once: true });
|
|
248
|
+
setKeyHandler((key) => {
|
|
249
|
+
if (confirmDelete !== null) {
|
|
250
|
+
if (key === "y" || key === "Y") {
|
|
251
|
+
const nameToDelete = confirmDelete;
|
|
252
|
+
confirmDelete = null;
|
|
253
|
+
void deleteFromRegistry(nameToDelete).then(() => {
|
|
254
|
+
const idx = entries.findIndex((e) => e.name === nameToDelete);
|
|
255
|
+
if (idx !== -1) entries.splice(idx, 1);
|
|
256
|
+
const vis2 = visible();
|
|
257
|
+
selectedIdx = Math.min(selectedIdx, Math.max(0, vis2.length - 1));
|
|
258
|
+
redraw();
|
|
259
|
+
}).catch(() => {
|
|
260
|
+
redraw();
|
|
261
|
+
});
|
|
262
|
+
} else {
|
|
263
|
+
confirmDelete = null;
|
|
264
|
+
redraw();
|
|
265
|
+
}
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (key === "q" || key === "") {
|
|
269
|
+
resolve(null);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const vis = visible();
|
|
273
|
+
if (key === "\x1B[A") {
|
|
274
|
+
if (vis.length > 0) selectedIdx = Math.max(0, selectedIdx - 1);
|
|
275
|
+
redraw();
|
|
276
|
+
} else if (key === "\x1B[B") {
|
|
277
|
+
if (vis.length > 0) selectedIdx = Math.min(vis.length - 1, selectedIdx + 1);
|
|
278
|
+
redraw();
|
|
279
|
+
} else if (key === "\r") {
|
|
280
|
+
const sel = vis[selectedIdx];
|
|
281
|
+
if (sel?.deployed) resolve(sel.name);
|
|
282
|
+
} else if (key === "a") {
|
|
283
|
+
showAll = !showAll;
|
|
284
|
+
selectedIdx = 0;
|
|
285
|
+
redraw();
|
|
286
|
+
} else if (key === "d" && showAll) {
|
|
287
|
+
const sel = vis[selectedIdx];
|
|
288
|
+
if (sel !== void 0 && !sel.deployed) {
|
|
289
|
+
confirmDelete = sel.name;
|
|
290
|
+
redraw();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
async function runDashboard(session, stackName, opts, ac, setKeyHandler, menuMode) {
|
|
297
|
+
let showLogs = true;
|
|
298
|
+
let lastSnapshot = null;
|
|
299
|
+
let refreshTimer = null;
|
|
300
|
+
function scheduleRefresh() {
|
|
301
|
+
refreshTimer = setTimeout(() => void doRefresh(), opts.intervalSec * 1e3);
|
|
302
|
+
}
|
|
303
|
+
function cancelRefresh() {
|
|
304
|
+
if (refreshTimer !== null) {
|
|
305
|
+
clearTimeout(refreshTimer);
|
|
306
|
+
refreshTimer = null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function redraw() {
|
|
310
|
+
if (!lastSnapshot) return;
|
|
311
|
+
const out = renderSnapshot(lastSnapshot, {
|
|
312
|
+
stackName,
|
|
313
|
+
intervalSec: opts.intervalSec,
|
|
314
|
+
showLogs,
|
|
315
|
+
noColor: opts.noColor,
|
|
316
|
+
menuMode
|
|
317
|
+
});
|
|
318
|
+
process.stdout.write("\x1B[2J\x1B[H" + out + "\n");
|
|
319
|
+
}
|
|
320
|
+
async function doRefresh() {
|
|
321
|
+
cancelRefresh();
|
|
322
|
+
if (ac.signal.aborted) return;
|
|
323
|
+
try {
|
|
324
|
+
lastSnapshot = await gatherSnapshot(session, ac.signal, opts.tailLines);
|
|
325
|
+
redraw();
|
|
326
|
+
} catch (err) {
|
|
327
|
+
if (!ac.signal.aborted) {
|
|
328
|
+
process.stdout.write("\x1B[2J\x1B[H Error gathering snapshot: " + (err instanceof Error ? err.message : String(err)) + "\n");
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (!ac.signal.aborted) scheduleRefresh();
|
|
332
|
+
}
|
|
333
|
+
return new Promise((resolve) => {
|
|
334
|
+
if (ac.signal.aborted) {
|
|
335
|
+
resolve("quit");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
ac.signal.addEventListener("abort", () => {
|
|
339
|
+
cancelRefresh();
|
|
340
|
+
resolve("quit");
|
|
341
|
+
}, { once: true });
|
|
342
|
+
setKeyHandler((key) => {
|
|
343
|
+
if (key === "q" || key === "") {
|
|
344
|
+
ac.abort();
|
|
345
|
+
cancelRefresh();
|
|
346
|
+
resolve("quit");
|
|
347
|
+
} else if (key === "s" && menuMode) {
|
|
348
|
+
cancelRefresh();
|
|
349
|
+
resolve("back");
|
|
350
|
+
} else if (key === "r") {
|
|
351
|
+
void doRefresh();
|
|
352
|
+
} else if (key === "l") {
|
|
353
|
+
showLogs = !showLogs;
|
|
354
|
+
redraw();
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
void doRefresh();
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
var monitor_default = defineCommand({
|
|
361
|
+
meta: {
|
|
362
|
+
name: "monitor",
|
|
363
|
+
description: "Live dashboard: gateway health, container status, resource usage, log tail"
|
|
364
|
+
},
|
|
365
|
+
args: {
|
|
366
|
+
stack: { type: "string", description: "Target stack name (omit to pick from a list)" },
|
|
367
|
+
interval: { type: "string", description: "Refresh interval in seconds (default: 10)" },
|
|
368
|
+
tail: { type: "string", description: "Log lines to show (default: 10)" },
|
|
369
|
+
"no-color": { type: "boolean", description: "Disable ANSI colors (auto-detected when not a TTY)" }
|
|
370
|
+
},
|
|
371
|
+
async run({ args }) {
|
|
372
|
+
const intervalSec = Math.max(2, parseInt(String(args.interval ?? "10"), 10) || 10);
|
|
373
|
+
const tailLines = Math.max(1, parseInt(String(args.tail ?? "10"), 10) || 10);
|
|
374
|
+
const isTTY = Boolean(process.stdout.isTTY);
|
|
375
|
+
const noColor = Boolean(args["no-color"]) || !isTTY;
|
|
376
|
+
const { buildContext } = await import("./context-7EVC6W45.js");
|
|
377
|
+
const { acquireSession, drainPool } = await import("./pool-PIIKJ4L2.js");
|
|
378
|
+
const ac = new AbortController();
|
|
379
|
+
process.on("SIGINT", () => ac.abort());
|
|
380
|
+
process.on("SIGTERM", () => ac.abort());
|
|
381
|
+
if (args.stack) {
|
|
382
|
+
const ctx = buildContext(args);
|
|
383
|
+
let conn;
|
|
384
|
+
if (ctx.adapter.name === "local") {
|
|
385
|
+
if (!ctx.localState) {
|
|
386
|
+
failure("Stack is not bootstrapped. Run `clawops up` first.");
|
|
387
|
+
process.exit(4);
|
|
388
|
+
}
|
|
389
|
+
const ls = ctx.localState;
|
|
390
|
+
conn = { host: ls.sshHost, port: ls.sshPort, user: ls.sshUser, privateKeyPath: ls.privateKeyPath, knownHostsPath: ls.knownHostsPath };
|
|
391
|
+
} else {
|
|
392
|
+
const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
|
|
393
|
+
const stack = await ctx.getStack();
|
|
394
|
+
const outputMap = await stack.outputs();
|
|
395
|
+
const outputs = Object.fromEntries(
|
|
396
|
+
Object.entries(outputMap).map(([k, v]) => [k, v.value])
|
|
397
|
+
);
|
|
398
|
+
if (!outputs["publicIp"]) {
|
|
399
|
+
failure("Stack has no outputs. Run `clawops up` first.");
|
|
400
|
+
process.exit(4);
|
|
401
|
+
}
|
|
402
|
+
const base = extractBaseOutputs(outputs);
|
|
403
|
+
conn = ctx.adapter.getConnectionInfo({
|
|
404
|
+
...base,
|
|
405
|
+
privateKeyPath: ctx.config.ssh.keyPath,
|
|
406
|
+
knownHostsPath: ctx.config.ssh.knownHostsPath
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
if (!isTTY) {
|
|
410
|
+
const { session: session2, release: release2 } = await acquireSession({ ...conn, signal: ac.signal });
|
|
411
|
+
try {
|
|
412
|
+
const snap = await gatherSnapshot(session2, ac.signal, tailLines);
|
|
413
|
+
process.stdout.write(
|
|
414
|
+
renderSnapshot(snap, { stackName: ctx.stackName, intervalSec, showLogs: true, noColor }) + "\n"
|
|
415
|
+
);
|
|
416
|
+
} finally {
|
|
417
|
+
release2();
|
|
418
|
+
drainPool();
|
|
419
|
+
}
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const { session, release } = await acquireSession({ ...conn, signal: ac.signal });
|
|
423
|
+
process.stdin.setRawMode(true);
|
|
424
|
+
process.stdin.resume();
|
|
425
|
+
process.stdin.setEncoding("utf8");
|
|
426
|
+
process.stdout.write("\x1B[?25l");
|
|
427
|
+
let keyHandler2 = () => {
|
|
428
|
+
};
|
|
429
|
+
process.stdin.on("data", (key) => keyHandler2(key));
|
|
430
|
+
try {
|
|
431
|
+
await runDashboard(session, ctx.stackName, { intervalSec, tailLines, noColor }, ac, (fn) => {
|
|
432
|
+
keyHandler2 = fn;
|
|
433
|
+
}, false);
|
|
434
|
+
} finally {
|
|
435
|
+
process.stdin.setRawMode(false);
|
|
436
|
+
process.stdin.pause();
|
|
437
|
+
process.stdout.write("\x1B[?25h\n");
|
|
438
|
+
release();
|
|
439
|
+
drainPool();
|
|
440
|
+
}
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (!isTTY) {
|
|
444
|
+
failure("Pass --stack <name> or run in a TTY for interactive stack selection.");
|
|
445
|
+
process.exit(2);
|
|
446
|
+
}
|
|
447
|
+
process.stdin.setRawMode(true);
|
|
448
|
+
process.stdin.resume();
|
|
449
|
+
process.stdin.setEncoding("utf8");
|
|
450
|
+
process.stdout.write("\x1B[?25l");
|
|
451
|
+
let keyHandler = () => {
|
|
452
|
+
};
|
|
453
|
+
process.stdin.on("data", (key) => keyHandler(key));
|
|
454
|
+
try {
|
|
455
|
+
while (!ac.signal.aborted) {
|
|
456
|
+
const stackName = await runStackMenu(ac, (fn) => {
|
|
457
|
+
keyHandler = fn;
|
|
458
|
+
}, noColor);
|
|
459
|
+
if (!stackName || ac.signal.aborted) break;
|
|
460
|
+
const ctx = buildContext({ stack: stackName });
|
|
461
|
+
let conn = null;
|
|
462
|
+
if (ctx.adapter.name === "local") {
|
|
463
|
+
if (!ctx.localState) continue;
|
|
464
|
+
const ls = ctx.localState;
|
|
465
|
+
conn = { host: ls.sshHost, port: ls.sshPort, user: ls.sshUser, privateKeyPath: ls.privateKeyPath, knownHostsPath: ls.knownHostsPath };
|
|
466
|
+
} else {
|
|
467
|
+
try {
|
|
468
|
+
const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
|
|
469
|
+
const stack = await ctx.getStack();
|
|
470
|
+
const outputMap = await stack.outputs();
|
|
471
|
+
const outputs = Object.fromEntries(
|
|
472
|
+
Object.entries(outputMap).map(([k, v]) => [k, v.value])
|
|
473
|
+
);
|
|
474
|
+
if (!outputs["publicIp"]) continue;
|
|
475
|
+
const base = extractBaseOutputs(outputs);
|
|
476
|
+
conn = ctx.adapter.getConnectionInfo({
|
|
477
|
+
...base,
|
|
478
|
+
privateKeyPath: ctx.config.ssh.keyPath,
|
|
479
|
+
knownHostsPath: ctx.config.ssh.knownHostsPath
|
|
480
|
+
});
|
|
481
|
+
} catch {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (!conn) continue;
|
|
486
|
+
const { session, release } = await acquireSession({ ...conn, signal: ac.signal });
|
|
487
|
+
try {
|
|
488
|
+
const result = await runDashboard(session, stackName, { intervalSec, tailLines, noColor }, ac, (fn) => {
|
|
489
|
+
keyHandler = fn;
|
|
490
|
+
}, true);
|
|
491
|
+
if (result === "quit") break;
|
|
492
|
+
} finally {
|
|
493
|
+
release();
|
|
494
|
+
drainPool();
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
} finally {
|
|
498
|
+
process.stdin.setRawMode(false);
|
|
499
|
+
process.stdin.pause();
|
|
500
|
+
process.stdout.write("\x1B[?25h\n");
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// src/diagnostics/index.ts
|
|
506
|
+
import process2 from "process";
|
|
507
|
+
import { accessSync, mkdirSync, readFileSync, constants } from "fs";
|
|
508
|
+
import ssh2 from "ssh2";
|
|
509
|
+
import path from "path";
|
|
510
|
+
async function runDiagnostics(opts = {}, deps = {}) {
|
|
511
|
+
const { getConfig, getConfigDir } = await import("./store-SDUR52Z5.js");
|
|
512
|
+
const config = getConfig();
|
|
513
|
+
const sections = [];
|
|
514
|
+
sections.push({ title: "Runtime", checks: await runtimeChecks(getConfigDir()) });
|
|
515
|
+
sections.push({
|
|
516
|
+
title: "Config",
|
|
517
|
+
checks: [
|
|
518
|
+
config ? { name: "Config file", status: "pass", detail: path.join(getConfigDir(), "config.json") } : { name: "Config file", status: "warn", detail: "not found", remedy: "run `clawops init`" }
|
|
519
|
+
]
|
|
520
|
+
});
|
|
521
|
+
sections.push({ title: "SSH", checks: sshChecks(config) });
|
|
522
|
+
sections.push({ title: "Credentials", checks: await credentialChecks(config, opts.provider) });
|
|
523
|
+
sections.push({ title: "OpenClaw", checks: await versionChecks() });
|
|
524
|
+
const account = await accountChecks(config, opts.stack, opts.provider, opts.instanceType);
|
|
525
|
+
if (account.length > 0) sections.push({ title: "Cloud account", checks: account });
|
|
526
|
+
if (opts.stack) {
|
|
527
|
+
const open = deps.openSession ?? defaultOpenSession;
|
|
528
|
+
let handle;
|
|
529
|
+
try {
|
|
530
|
+
handle = await open(opts.stack, opts.signal);
|
|
531
|
+
} catch (err) {
|
|
532
|
+
sections.push({
|
|
533
|
+
title: "Remote health",
|
|
534
|
+
checks: [{ name: "Connection", status: "fail", detail: messageOf(err) }]
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
if (handle) {
|
|
538
|
+
try {
|
|
539
|
+
sections.push({
|
|
540
|
+
title: "Remote health",
|
|
541
|
+
checks: await remoteChecks(handle.session, opts.signal)
|
|
542
|
+
});
|
|
543
|
+
sections.push({
|
|
544
|
+
title: "Hardening",
|
|
545
|
+
checks: await hardeningChecks(handle.conn, opts.stack, opts.signal)
|
|
546
|
+
});
|
|
547
|
+
} finally {
|
|
548
|
+
handle.release();
|
|
549
|
+
const { drainPool } = await import("./pool-PIIKJ4L2.js");
|
|
550
|
+
drainPool();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return summarise(sections);
|
|
555
|
+
}
|
|
556
|
+
function summarise(sections) {
|
|
557
|
+
const counts = { pass: 0, fail: 0, warn: 0, info: 0 };
|
|
558
|
+
for (const s of sections) for (const c of s.checks) counts[c.status]++;
|
|
559
|
+
return { sections, ok: counts.fail === 0, counts };
|
|
560
|
+
}
|
|
561
|
+
async function runtimeChecks(configDir) {
|
|
562
|
+
const checks = [];
|
|
563
|
+
const nodeMajor = parseInt(process2.version.slice(1).split(".")[0] ?? "0", 10);
|
|
564
|
+
checks.push(
|
|
565
|
+
nodeMajor >= 22 ? { name: "Node.js", status: "pass", detail: process2.version } : {
|
|
566
|
+
name: "Node.js",
|
|
567
|
+
status: "fail",
|
|
568
|
+
detail: `${process2.version} (requires >=22)`,
|
|
569
|
+
remedy: "install Node.js 22 or later"
|
|
570
|
+
}
|
|
571
|
+
);
|
|
572
|
+
const pulumiHome = path.join(configDir, ".pulumi");
|
|
573
|
+
try {
|
|
574
|
+
mkdirSync(pulumiHome, { recursive: true });
|
|
575
|
+
checks.push({ name: "Pulumi home", status: "pass", detail: pulumiHome });
|
|
576
|
+
} catch {
|
|
577
|
+
checks.push({ name: "Pulumi home", status: "fail", detail: `${pulumiHome} (not writable)` });
|
|
578
|
+
}
|
|
579
|
+
const { pulumiCliStatus } = await import("./cli-WLD3EBEM.js");
|
|
580
|
+
const cli = await pulumiCliStatus(configDir);
|
|
581
|
+
checks.push(
|
|
582
|
+
cli.kind === "managed" ? { name: "Pulumi CLI", status: "pass", detail: `${cli.version} (${cli.root})` } : cli.kind === "path" ? { name: "Pulumi CLI", status: "pass", detail: `${cli.version} (on PATH)` } : {
|
|
583
|
+
name: "Pulumi CLI",
|
|
584
|
+
status: "warn",
|
|
585
|
+
detail: "not installed",
|
|
586
|
+
remedy: `clawops installs it into ${path.join(configDir, ".pulumi-cli")} on first apply`
|
|
587
|
+
}
|
|
588
|
+
);
|
|
589
|
+
const { passphraseStatus, passphrasePath } = await import("./passphrase-MJBE45N7.js");
|
|
590
|
+
const passphrase = passphraseStatus(configDir);
|
|
591
|
+
checks.push(
|
|
592
|
+
passphrase === "environment" ? {
|
|
593
|
+
name: "State passphrase",
|
|
594
|
+
status: "pass",
|
|
595
|
+
detail: "PULUMI_CONFIG_PASSPHRASE set in the environment"
|
|
596
|
+
} : passphrase === "stored" ? {
|
|
597
|
+
name: "State passphrase",
|
|
598
|
+
status: "pass",
|
|
599
|
+
detail: passphrasePath(configDir),
|
|
600
|
+
remedy: "back this up \u2014 without it, an existing stack's secrets cannot be read"
|
|
601
|
+
} : {
|
|
602
|
+
name: "State passphrase",
|
|
603
|
+
status: "warn",
|
|
604
|
+
detail: "not created yet",
|
|
605
|
+
remedy: `clawops generates one at ${passphrasePath(configDir)} on first apply`
|
|
606
|
+
}
|
|
607
|
+
);
|
|
608
|
+
return checks;
|
|
609
|
+
}
|
|
610
|
+
function expandHome(p) {
|
|
611
|
+
return p.replace(/^~/, process2.env["HOME"] ?? "~");
|
|
612
|
+
}
|
|
613
|
+
function sshChecks(config) {
|
|
614
|
+
if (!config) return [{ name: "SSH", status: "info", detail: "skipped \u2014 no config" }];
|
|
615
|
+
const checks = [];
|
|
616
|
+
const keyPath = expandHome(config.ssh.keyPath);
|
|
617
|
+
try {
|
|
618
|
+
accessSync(keyPath, constants.R_OK);
|
|
619
|
+
const parsed = ssh2.utils.parseKey(readFileSync(keyPath));
|
|
620
|
+
checks.push(
|
|
621
|
+
parsed instanceof Error ? {
|
|
622
|
+
name: "SSH key",
|
|
623
|
+
status: "fail",
|
|
624
|
+
detail: `${keyPath} (${parsed.message.toLowerCase()})`,
|
|
625
|
+
remedy: `clawops connects with ssh2. Regenerate in OpenSSH format: ssh-keygen -t ed25519 -f ${keyPath} -N '' -C clawops`
|
|
626
|
+
} : { name: "SSH key", status: "pass", detail: `${keyPath} (${parsed.type})` }
|
|
627
|
+
);
|
|
628
|
+
} catch {
|
|
629
|
+
checks.push({
|
|
630
|
+
name: "SSH key",
|
|
631
|
+
status: "fail",
|
|
632
|
+
detail: `${keyPath} (not found or not readable)`
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
const knownHostsPath = expandHome(config.ssh.knownHostsPath);
|
|
636
|
+
try {
|
|
637
|
+
accessSync(knownHostsPath, constants.F_OK);
|
|
638
|
+
checks.push({ name: "known_hosts", status: "pass", detail: knownHostsPath });
|
|
639
|
+
} catch {
|
|
640
|
+
checks.push({
|
|
641
|
+
name: "known_hosts",
|
|
642
|
+
status: "warn",
|
|
643
|
+
detail: `${knownHostsPath} (does not exist \u2014 will be created on first connect)`
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
return checks;
|
|
647
|
+
}
|
|
648
|
+
async function providerCredentialCheck(provider, why) {
|
|
649
|
+
if (provider === "local") {
|
|
650
|
+
return { name: "local", status: "pass", detail: "SSH-only, no cloud credentials required" };
|
|
651
|
+
}
|
|
652
|
+
const { getProvider } = await import("./providers-FW6H6OPF.js");
|
|
653
|
+
try {
|
|
654
|
+
const adapter = getProvider(provider);
|
|
655
|
+
const result = await adapter.validateConfig();
|
|
656
|
+
return result.ok ? { name: provider, status: "pass", detail: why } : { name: provider, status: "fail", detail: result.errors.join("; ") };
|
|
657
|
+
} catch (err) {
|
|
658
|
+
return { name: provider, status: "fail", detail: messageOf(err) };
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
async function credentialChecks(config, provider) {
|
|
662
|
+
if (provider) return [await providerCredentialCheck(provider, "requested with --provider")];
|
|
663
|
+
if (!config) return [{ name: "Credentials", status: "info", detail: "skipped \u2014 no config" }];
|
|
664
|
+
const { getProvider } = await import("./providers-FW6H6OPF.js");
|
|
665
|
+
const checks = [];
|
|
666
|
+
const seen = /* @__PURE__ */ new Set();
|
|
667
|
+
for (const [stackName, stackCfg] of Object.entries(config.stacks)) {
|
|
668
|
+
const providerName = stackCfg.provider;
|
|
669
|
+
if (seen.has(providerName)) continue;
|
|
670
|
+
seen.add(providerName);
|
|
671
|
+
if (providerName === "local") {
|
|
672
|
+
checks.push({
|
|
673
|
+
name: providerName,
|
|
674
|
+
status: "pass",
|
|
675
|
+
detail: `stack "${stackName}" (SSH-only, no cloud credentials required)`
|
|
676
|
+
});
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
const adapter = getProvider(providerName);
|
|
681
|
+
const result = await adapter.validateConfig();
|
|
682
|
+
if (result.ok) {
|
|
683
|
+
checks.push({ name: providerName, status: "pass", detail: `stack "${stackName}"` });
|
|
684
|
+
} else {
|
|
685
|
+
for (const err of result.errors) {
|
|
686
|
+
checks.push({ name: providerName, status: "fail", detail: `stack "${stackName}" \u2014 ${err}` });
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
} catch (err) {
|
|
690
|
+
checks.push({
|
|
691
|
+
name: providerName,
|
|
692
|
+
status: "fail",
|
|
693
|
+
detail: `stack "${stackName}" \u2014 ${messageOf(err)}`
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (seen.size === 0) {
|
|
698
|
+
checks.push({ name: "Stacks", status: "warn", detail: "none configured", remedy: "run `clawops init`" });
|
|
699
|
+
}
|
|
700
|
+
return checks;
|
|
701
|
+
}
|
|
702
|
+
async function versionChecks() {
|
|
703
|
+
try {
|
|
704
|
+
const yaml = await import("./js-yaml-PTEEG4FO.js");
|
|
705
|
+
const { loadVersionSpec, describeRange } = await import("./versions-W5LRQCAJ.js");
|
|
706
|
+
const support = loadVersionSpec(yaml).support;
|
|
707
|
+
const checks = [
|
|
708
|
+
{
|
|
709
|
+
name: "Supported range",
|
|
710
|
+
status: "pass",
|
|
711
|
+
detail: `${describeRange(support)} (recommended ${support.recommended})`
|
|
712
|
+
}
|
|
713
|
+
];
|
|
714
|
+
if (!support.max) {
|
|
715
|
+
checks.push({
|
|
716
|
+
name: "Upper bound",
|
|
717
|
+
status: "warn",
|
|
718
|
+
detail: "none declared \u2014 this line would accept any OpenClaw release"
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
return checks;
|
|
722
|
+
} catch (e) {
|
|
723
|
+
return [{ name: "Version matrix", status: "fail", detail: messageOf(e) }];
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async function remoteChecks(session, signal) {
|
|
727
|
+
const checks = [];
|
|
728
|
+
const { containerStatus } = await import("./docker-3PDO4TS7.js");
|
|
729
|
+
const container = await containerStatus(session, "openclaw", signal);
|
|
730
|
+
checks.push(
|
|
731
|
+
container.error ? {
|
|
732
|
+
// "not found" would be a claim about the deployment. This is a claim about clawops.
|
|
733
|
+
name: "Container",
|
|
734
|
+
status: "fail",
|
|
735
|
+
detail: `could not ask docker \u2014 ${container.error}`,
|
|
736
|
+
remedy: "the SSH user may have been added to the docker group after this session opened"
|
|
737
|
+
} : container.status === "running" ? { name: "Container", status: "pass", detail: "running" } : { name: "Container", status: "fail", detail: container.status }
|
|
738
|
+
);
|
|
739
|
+
const imageResult = await execPrivileged(
|
|
740
|
+
session,
|
|
741
|
+
`docker inspect openclaw --format '{{.Config.Image}}' 2>/dev/null || echo ''`,
|
|
742
|
+
signal
|
|
743
|
+
);
|
|
744
|
+
const image = imageResult.stdout.trim();
|
|
745
|
+
const deployedVersion = image.includes(":") ? image.slice(image.lastIndexOf(":") + 1) : "";
|
|
746
|
+
if (deployedVersion) {
|
|
747
|
+
checks.push(await deployedVersionCheck(image, deployedVersion));
|
|
748
|
+
}
|
|
749
|
+
const probe = await session.exec(probeCommand("started", GATEWAY_PORT), signal);
|
|
750
|
+
const verdict = interpretProbe("started", probe.stdout);
|
|
751
|
+
checks.push(
|
|
752
|
+
verdict.ok ? { name: "Gateway", status: "pass", detail: `answering on 127.0.0.1:${GATEWAY_PORT}` } : { name: "Gateway", status: "fail", detail: verdict.reason ?? "not answering" }
|
|
753
|
+
);
|
|
754
|
+
const pubResult = await execPrivileged(session, PUBLISH_INSPECT_CMD, signal);
|
|
755
|
+
checks.push(
|
|
756
|
+
publishForRestart(pubResult.stdout) === "all" ? {
|
|
757
|
+
name: "Published",
|
|
758
|
+
status: "warn",
|
|
759
|
+
detail: "0.0.0.0 \u2014 the gateway port is open on every interface, serving plaintext HTTP",
|
|
760
|
+
remedy: 'front it with TLS, or set network.publishGateway to "loopback" and use `clawops tunnel`'
|
|
761
|
+
} : { name: "Published", status: "pass", detail: "127.0.0.1 only (reach it with `clawops tunnel`)" }
|
|
762
|
+
);
|
|
763
|
+
const diskResult = await session.exec(
|
|
764
|
+
// The state directory: 2.0's SQLite lives there, not in the user's home.
|
|
765
|
+
`df -h ${STATE_DIR_HOST_LINUX} 2>/dev/null | awk 'NR==2{print $5" used ("$3" of "$2")"}'`,
|
|
766
|
+
signal
|
|
767
|
+
);
|
|
768
|
+
const diskUsage = diskResult.stdout.trim();
|
|
769
|
+
if (diskUsage) {
|
|
770
|
+
const pct = parseInt(diskUsage.match(/^(\d+)%/)?.[1] ?? "0", 10);
|
|
771
|
+
checks.push({
|
|
772
|
+
name: "Disk",
|
|
773
|
+
status: pct >= 90 ? "fail" : pct >= 75 ? "warn" : "pass",
|
|
774
|
+
detail: diskUsage
|
|
775
|
+
});
|
|
776
|
+
} else {
|
|
777
|
+
checks.push({ name: "Disk", status: "warn", detail: "unable to determine disk usage" });
|
|
778
|
+
}
|
|
779
|
+
const logrotateResult = await session.exec(
|
|
780
|
+
`test -f /etc/logrotate.d/openclaw && echo 'configured' || echo 'not configured'`,
|
|
781
|
+
signal
|
|
782
|
+
);
|
|
783
|
+
checks.push(
|
|
784
|
+
logrotateResult.stdout.trim() === "configured" ? { name: "Log rotation", status: "pass", detail: "configured" } : { name: "Log rotation", status: "warn", detail: "not configured \u2014 logs may grow unbounded" }
|
|
785
|
+
);
|
|
786
|
+
return checks;
|
|
787
|
+
}
|
|
788
|
+
async function deployedVersionCheck(image, deployedVersion) {
|
|
789
|
+
const yaml = await import("./js-yaml-PTEEG4FO.js");
|
|
790
|
+
const { loadVersionSpec, checkVersion, isMovingTag } = await import("./versions-W5LRQCAJ.js");
|
|
791
|
+
const support = loadVersionSpec(yaml).support;
|
|
792
|
+
if (isMovingTag(deployedVersion)) {
|
|
793
|
+
return {
|
|
794
|
+
name: "Deployed",
|
|
795
|
+
status: "warn",
|
|
796
|
+
detail: `${image} (moving tag)`,
|
|
797
|
+
remedy: "pin an explicit version \u2014 a moving tag resolves to whatever is newest"
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
const check = checkVersion(deployedVersion, support);
|
|
801
|
+
if (check.ok) return { name: "Deployed", status: "pass", detail: `OpenClaw ${deployedVersion}` };
|
|
802
|
+
return {
|
|
803
|
+
name: "Deployed",
|
|
804
|
+
status: "fail",
|
|
805
|
+
detail: `OpenClaw ${deployedVersion} is not supported by this clawops line`,
|
|
806
|
+
remedy: check.error.reason === "too-old" ? "this clawops line requires OpenClaw 2.0 or later \u2014 run `clawops migrate` to move an existing 1.x deployment across, which preserves its state and device identity" : "upgrade clawops, or pin an OpenClaw version this line supports"
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
async function hardeningChecks(conn, stack, signal) {
|
|
810
|
+
try {
|
|
811
|
+
const { MODULE_CATALOG, resolveModules, withRemoteExec } = await import("./harden-72JMRT4E.js");
|
|
812
|
+
const { buildContext } = await import("./context-7EVC6W45.js");
|
|
813
|
+
const provider = buildContext({ stack }).adapter.name;
|
|
814
|
+
const modules = resolveModules(MODULE_CATALOG, void 0, provider);
|
|
815
|
+
const checks = [];
|
|
816
|
+
await withRemoteExec(conn, signal, async (exec) => {
|
|
817
|
+
for (const mod of modules) {
|
|
818
|
+
const result = await mod.check(exec);
|
|
819
|
+
if (result.status === "applied") {
|
|
820
|
+
checks.push({ name: mod.label, status: "pass", detail: "applied" });
|
|
821
|
+
} else if (result.status === "drifted") {
|
|
822
|
+
checks.push({ name: mod.label, status: "warn", detail: `drifted \u2014 ${result.detail}` });
|
|
823
|
+
} else if (result.status === "missing") {
|
|
824
|
+
checks.push({ name: mod.label, status: "info", detail: "not applied", remedy: "run `clawops harden`" });
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
return checks;
|
|
829
|
+
} catch (err) {
|
|
830
|
+
return [{ name: "Hardening checks", status: "fail", detail: messageOf(err) }];
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
async function defaultOpenSession(stack, signal) {
|
|
834
|
+
const { getConfig } = await import("./store-SDUR52Z5.js");
|
|
835
|
+
const { buildContext } = await import("./context-7EVC6W45.js");
|
|
836
|
+
const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
|
|
837
|
+
const { acquireSession } = await import("./pool-PIIKJ4L2.js");
|
|
838
|
+
const config = getConfig();
|
|
839
|
+
if (!config) throw new Error("no config \u2014 run `clawops init`");
|
|
840
|
+
const ctx = buildContext({ stack });
|
|
841
|
+
const stackObj = await ctx.getStack();
|
|
842
|
+
const outputMap = await stackObj.outputs();
|
|
843
|
+
const outputs = Object.fromEntries(
|
|
844
|
+
Object.entries(outputMap).map(([k, v]) => [k, v.value])
|
|
845
|
+
);
|
|
846
|
+
const base = extractBaseOutputs(outputs);
|
|
847
|
+
const conn = ctx.adapter.getConnectionInfo({
|
|
848
|
+
...base,
|
|
849
|
+
privateKeyPath: config.ssh.keyPath,
|
|
850
|
+
knownHostsPath: config.ssh.knownHostsPath
|
|
851
|
+
});
|
|
852
|
+
const { session, release } = await acquireSession({
|
|
853
|
+
host: conn.host,
|
|
854
|
+
port: conn.port,
|
|
855
|
+
user: conn.user,
|
|
856
|
+
privateKeyPath: conn.privateKeyPath,
|
|
857
|
+
knownHostsPath: conn.knownHostsPath,
|
|
858
|
+
signal
|
|
859
|
+
});
|
|
860
|
+
return { session, release, conn };
|
|
861
|
+
}
|
|
862
|
+
function messageOf(err) {
|
|
863
|
+
return err instanceof Error ? err.message : String(err);
|
|
864
|
+
}
|
|
865
|
+
async function accountChecks(config, stack, provider, instanceType) {
|
|
866
|
+
if (!config && !provider) return [];
|
|
867
|
+
const stackName = stack ?? config?.defaults?.stack;
|
|
868
|
+
const fromStack = stackName ? config?.stacks[stackName] : void 0;
|
|
869
|
+
const stackCfg = provider && fromStack?.provider !== provider ? Object.values(config?.stacks ?? {}).find((s) => s.provider === provider) : fromStack;
|
|
870
|
+
const providerName = provider ?? stackCfg?.provider;
|
|
871
|
+
if (!providerName || providerName === "local") return [];
|
|
872
|
+
try {
|
|
873
|
+
const { getProvider } = await import("./providers-FW6H6OPF.js");
|
|
874
|
+
const adapter = getProvider(providerName);
|
|
875
|
+
if (!adapter.preflight) return [];
|
|
876
|
+
const bucket = stackCfg ? bucketFromStateUrl(stackCfg.stateUrl) : void 0;
|
|
877
|
+
const results = await adapter.preflight({ region: stackCfg?.region, bucket, instanceType });
|
|
878
|
+
return results.map((r) => ({
|
|
879
|
+
name: r.label,
|
|
880
|
+
// A question clawops could not ask is not an answer about the account. Failing the
|
|
881
|
+
// report over a denied read permission would be as wrong as passing it.
|
|
882
|
+
status: r.unknown ? "warn" : r.ok ? "pass" : "fail",
|
|
883
|
+
detail: r.detail,
|
|
884
|
+
remedy: r.mutates ? `clawops setup can fix this: ${r.mutates}` : void 0
|
|
885
|
+
}));
|
|
886
|
+
} catch (err) {
|
|
887
|
+
return [{ name: "Account preflight", status: "warn", detail: messageOf(err) }];
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
function bucketFromStateUrl(stateUrl) {
|
|
891
|
+
if (!stateUrl) return void 0;
|
|
892
|
+
const match = /^[a-z0-9]+:\/\/([^/]+)/.exec(stateUrl);
|
|
893
|
+
return match?.[1];
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
export {
|
|
897
|
+
runDiagnostics,
|
|
898
|
+
formatUptime,
|
|
899
|
+
gatherSnapshot,
|
|
900
|
+
monitor_default
|
|
901
|
+
};
|