refactor(commands): split CLI commands

This commit is contained in:
Peter Steinberger
2026-01-14 05:39:47 +00:00
parent 2b60ee96f2
commit a58ff1ac63
74 changed files with 7995 additions and 7806 deletions

View File

@@ -6,7 +6,6 @@ import {
resolveGatewayPort,
} from "../config/config.js";
import { readLastGatewayErrorLine } from "../daemon/diagnostics.js";
import { resolveGatewayLogPaths } from "../daemon/launchd.js";
import { resolveGatewayService } from "../daemon/service.js";
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
import { normalizeControlUiBasePath } from "../gateway/control-ui.js";
@@ -14,11 +13,8 @@ import { probeGateway } from "../gateway/probe.js";
import { collectChannelStatusIssues } from "../infra/channels-status-issues.js";
import { resolveClawdbotPackageRoot } from "../infra/clawdbot-root.js";
import { resolveOsSummary } from "../infra/os-summary.js";
import { formatPortDiagnostics, inspectPortUsage } from "../infra/ports.js";
import {
readRestartSentinel,
summarizeRestartSentinel,
} from "../infra/restart-sentinel.js";
import { inspectPortUsage } from "../infra/ports.js";
import { readRestartSentinel } from "../infra/restart-sentinel.js";
import { readTailscaleStatusJson } from "../infra/tailscale.js";
import {
checkUpdateStatus,
@@ -26,23 +22,13 @@ import {
} from "../infra/update-check.js";
import { runExec } from "../process/exec.js";
import type { RuntimeEnv } from "../runtime.js";
import { renderTable } from "../terminal/table.js";
import { isRich, theme } from "../terminal/theme.js";
import { VERSION } from "../version.js";
import { resolveControlUiLinks } from "./onboard-helpers.js";
import { getAgentLocalStatuses } from "./status-all/agents.js";
import { buildChannelsTable } from "./status-all/channels.js";
import {
formatAge,
formatDuration,
formatGatewayAuthUsed,
redactSecrets,
} from "./status-all/format.js";
import {
pickGatewaySelfPresence,
readFileTailLines,
summarizeLogTail,
} from "./status-all/gateway.js";
import { formatDuration, formatGatewayAuthUsed } from "./status-all/format.js";
import { pickGatewaySelfPresence } from "./status-all/gateway.js";
import { buildStatusAllReportLines } from "./status-all/report-lines.js";
export async function statusAllCommand(
runtime: RuntimeEnv,
@@ -410,331 +396,34 @@ export async function statusAllCommand(
},
];
const rich = isRich();
const heading = (text: string) => (rich ? theme.heading(text) : text);
const ok = (text: string) => (rich ? theme.success(text) : text);
const warn = (text: string) => (rich ? theme.warn(text) : text);
const fail = (text: string) => (rich ? theme.error(text) : text);
const muted = (text: string) => (rich ? theme.muted(text) : text);
const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1);
const overview = renderTable({
width: tableWidth,
columns: [
{ key: "Item", header: "Item", minWidth: 10 },
{ key: "Value", header: "Value", flex: true, minWidth: 24 },
],
rows: overviewRows,
const lines = await buildStatusAllReportLines({
progress,
overviewRows,
channels,
channelIssues: channelIssues.map((issue) => ({
channel: issue.channel,
message: issue.message,
})),
agentStatus,
connectionDetailsForReport,
diagnosis: {
snap,
remoteUrlMissing,
sentinel,
lastErr,
port,
portUsage,
tailscaleMode,
tailscale,
tailscaleHttpsUrl,
skillStatus,
channelsStatus,
channelIssues,
gatewayReachable,
health,
},
});
const channelRows = channels.rows.map((row) => ({
channelId: row.id,
Channel: row.label,
Enabled: row.enabled ? ok("ON") : muted("OFF"),
State:
row.state === "ok"
? ok("OK")
: row.state === "warn"
? warn("WARN")
: row.state === "off"
? muted("OFF")
: theme.accentDim("SETUP"),
Detail: row.detail,
}));
const channelIssuesByChannel = (() => {
const map = new Map<string, typeof channelIssues>();
for (const issue of channelIssues) {
const key = issue.channel;
const list = map.get(key);
if (list) list.push(issue);
else map.set(key, [issue]);
}
return map;
})();
const channelRowsWithIssues = channelRows.map((row) => {
const issues = channelIssuesByChannel.get(row.channelId) ?? [];
if (issues.length === 0) return row;
const issue = issues[0];
const suffix = ` · ${warn(`gateway: ${String(issue.message).slice(0, 90)}`)}`;
return {
...row,
State: warn("WARN"),
Detail: `${row.Detail}${suffix}`,
};
});
const channelsTable = renderTable({
width: tableWidth,
columns: [
{ key: "Channel", header: "Channel", minWidth: 10 },
{ key: "Enabled", header: "Enabled", minWidth: 7 },
{ key: "State", header: "State", minWidth: 8 },
{ key: "Detail", header: "Detail", flex: true, minWidth: 28 },
],
rows: channelRowsWithIssues,
});
const agentRows = agentStatus.agents.map((a) => ({
Agent: a.name?.trim() ? `${a.id} (${a.name.trim()})` : a.id,
Bootstrap:
a.bootstrapPending === true
? warn("PENDING")
: a.bootstrapPending === false
? ok("OK")
: "unknown",
Sessions: String(a.sessionsCount),
Active:
a.lastActiveAgeMs != null ? formatAge(a.lastActiveAgeMs) : "unknown",
Store: a.sessionsPath,
}));
const agentsTable = renderTable({
width: tableWidth,
columns: [
{ key: "Agent", header: "Agent", minWidth: 12 },
{ key: "Bootstrap", header: "Bootstrap", minWidth: 10 },
{ key: "Sessions", header: "Sessions", align: "right", minWidth: 8 },
{ key: "Active", header: "Active", minWidth: 10 },
{ key: "Store", header: "Store", flex: true, minWidth: 34 },
],
rows: agentRows,
});
const lines: string[] = [];
lines.push(heading("Clawdbot status --all"));
lines.push("");
lines.push(heading("Overview"));
lines.push(overview.trimEnd());
lines.push("");
lines.push(heading("Channels"));
lines.push(channelsTable.trimEnd());
for (const detail of channels.details) {
lines.push("");
lines.push(heading(detail.title));
lines.push(
renderTable({
width: tableWidth,
columns: detail.columns.map((c) => ({
key: c,
header: c,
flex: c === "Notes",
minWidth: c === "Notes" ? 28 : 10,
})),
rows: detail.rows.map((r) => ({
...r,
...(r.Status === "OK"
? { Status: ok("OK") }
: r.Status === "WARN"
? { Status: warn("WARN") }
: {}),
})),
}).trimEnd(),
);
}
lines.push("");
lines.push(heading("Agents"));
lines.push(agentsTable.trimEnd());
lines.push("");
lines.push(heading("Diagnosis (read-only)"));
const emitCheck = (label: string, status: "ok" | "warn" | "fail") => {
const icon =
status === "ok" ? ok("✓") : status === "warn" ? warn("!") : fail("✗");
const colored =
status === "ok"
? ok(label)
: status === "warn"
? warn(label)
: fail(label);
lines.push(`${icon} ${colored}`);
};
lines.push("");
lines.push(`${muted("Gateway connection details:")}`);
for (const line of redactSecrets(connectionDetailsForReport)
.split("\n")
.map((l) => l.trimEnd())) {
lines.push(` ${muted(line)}`);
}
lines.push("");
if (snap) {
const status = !snap.exists ? "fail" : snap.valid ? "ok" : "warn";
emitCheck(`Config: ${snap.path ?? "(unknown)"}`, status);
const issues = [...(snap.legacyIssues ?? []), ...(snap.issues ?? [])];
const uniqueIssues = issues.filter(
(issue, index) =>
issues.findIndex(
(x) => x.path === issue.path && x.message === issue.message,
) === index,
);
for (const issue of uniqueIssues.slice(0, 12)) {
lines.push(` - ${issue.path}: ${issue.message}`);
}
if (uniqueIssues.length > 12) {
lines.push(` ${muted(`… +${uniqueIssues.length - 12} more`)}`);
}
} else {
emitCheck("Config: read failed", "warn");
}
if (remoteUrlMissing) {
lines.push("");
emitCheck(
"Gateway remote mode misconfigured (gateway.remote.url missing)",
"warn",
);
lines.push(
` ${muted("Fix: set gateway.remote.url, or set gateway.mode=local.")}`,
);
}
if (sentinel?.payload) {
emitCheck("Restart sentinel present", "warn");
lines.push(
` ${muted(`${summarizeRestartSentinel(sentinel.payload)} · ${formatAge(Date.now() - sentinel.payload.ts)}`)}`,
);
} else {
emitCheck("Restart sentinel: none", "ok");
}
const lastErrClean = lastErr?.trim() ?? "";
const isTrivialLastErr =
lastErrClean.length < 8 || lastErrClean === "}" || lastErrClean === "{";
if (lastErrClean && !isTrivialLastErr) {
lines.push("");
lines.push(`${muted("Gateway last log line:")}`);
lines.push(` ${muted(redactSecrets(lastErrClean))}`);
}
if (portUsage) {
const portOk = portUsage.listeners.length === 0;
emitCheck(`Port ${port}`, portOk ? "ok" : "warn");
if (!portOk) {
for (const line of formatPortDiagnostics(portUsage)) {
lines.push(` ${muted(line)}`);
}
}
}
{
const backend = tailscale.backendState ?? "unknown";
const okBackend = backend === "Running";
const hasDns = Boolean(tailscale.dnsName);
const label =
tailscaleMode === "off"
? `Tailscale: off · ${backend}${tailscale.dnsName ? ` · ${tailscale.dnsName}` : ""}`
: `Tailscale: ${tailscaleMode} · ${backend}${tailscale.dnsName ? ` · ${tailscale.dnsName}` : ""}`;
emitCheck(
label,
okBackend && (tailscaleMode === "off" || hasDns) ? "ok" : "warn",
);
if (tailscale.error) {
lines.push(` ${muted(`error: ${tailscale.error}`)}`);
}
if (tailscale.ips.length > 0) {
lines.push(
` ${muted(`ips: ${tailscale.ips.slice(0, 3).join(", ")}${tailscale.ips.length > 3 ? "…" : ""}`)}`,
);
}
if (tailscaleHttpsUrl) {
lines.push(` ${muted(`https: ${tailscaleHttpsUrl}`)}`);
}
}
if (skillStatus) {
const eligible = skillStatus.skills.filter((s) => s.eligible).length;
const missing = skillStatus.skills.filter(
(s) =>
s.eligible && Object.values(s.missing).some((arr) => arr.length),
).length;
emitCheck(
`Skills: ${eligible} eligible · ${missing} missing · ${skillStatus.workspaceDir}`,
missing === 0 ? "ok" : "warn",
);
}
progress.setLabel("Reading logs…");
const logPaths = (() => {
try {
return resolveGatewayLogPaths(process.env);
} catch {
return null;
}
})();
if (logPaths) {
progress.setLabel("Reading logs…");
const [stderrTail, stdoutTail] = await Promise.all([
readFileTailLines(logPaths.stderrPath, 40).catch(() => []),
readFileTailLines(logPaths.stdoutPath, 40).catch(() => []),
]);
if (stderrTail.length > 0 || stdoutTail.length > 0) {
lines.push("");
lines.push(
`${muted(`Gateway logs (tail, summarized): ${logPaths.logDir}`)}`,
);
lines.push(` ${muted(`# stderr: ${logPaths.stderrPath}`)}`);
for (const line of summarizeLogTail(stderrTail, { maxLines: 22 }).map(
redactSecrets,
)) {
lines.push(` ${muted(line)}`);
}
lines.push(` ${muted(`# stdout: ${logPaths.stdoutPath}`)}`);
for (const line of summarizeLogTail(stdoutTail, { maxLines: 22 }).map(
redactSecrets,
)) {
lines.push(` ${muted(line)}`);
}
}
}
progress.tick();
if (channelsStatus) {
emitCheck(
`Channel issues (${channelIssues.length || "none"})`,
channelIssues.length === 0 ? "ok" : "warn",
);
for (const issue of channelIssues.slice(0, 12)) {
const fixText = issue.fix ? ` · fix: ${issue.fix}` : "";
lines.push(
` - ${issue.channel}[${issue.accountId}] ${issue.kind}: ${issue.message}${fixText}`,
);
}
if (channelIssues.length > 12) {
lines.push(` ${muted(`… +${channelIssues.length - 12} more`)}`);
}
} else {
emitCheck(
`Channel issues skipped (gateway ${gatewayReachable ? "query failed" : "unreachable"})`,
"warn",
);
}
const healthErr = (() => {
if (!health || typeof health !== "object") return "";
const record = health as Record<string, unknown>;
if (!("error" in record)) return "";
const value = record.error;
if (!value) return "";
if (typeof value === "string") return value;
try {
return JSON.stringify(value, null, 2);
} catch {
return "[unserializable error]";
}
})();
if (healthErr) {
lines.push("");
lines.push(`${muted("Gateway health:")}`);
lines.push(` ${muted(redactSecrets(healthErr))}`);
}
lines.push("");
lines.push(muted("Pasteable debug report. Auth tokens redacted."));
lines.push("Troubleshooting: https://docs.clawd.bot/troubleshooting");
lines.push("");
progress.setLabel("Rendering…");
runtime.log(lines.join("\n"));
progress.tick();