fix: improve gateway diagnostics
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { DEFAULT_AGENTS_FILENAME } from "../agents/workspace.js";
|
||||
@@ -39,3 +40,36 @@ export async function shouldSuggestMemorySystem(
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export type LegacyWorkspaceDetection = {
|
||||
activeWorkspace: string;
|
||||
legacyDirs: string[];
|
||||
};
|
||||
|
||||
export function detectLegacyWorkspaceDirs(params: {
|
||||
workspaceDir: string;
|
||||
homedir?: () => string;
|
||||
exists?: (value: string) => boolean;
|
||||
}): LegacyWorkspaceDetection {
|
||||
const homedir = params.homedir ?? os.homedir;
|
||||
const exists = params.exists ?? fs.existsSync;
|
||||
const home = homedir();
|
||||
const activeWorkspace = path.resolve(params.workspaceDir);
|
||||
const candidates = [path.join(home, "clawdis"), path.join(home, "clawdbot")];
|
||||
const legacyDirs = candidates.filter((candidate) => {
|
||||
if (!exists(candidate)) return false;
|
||||
return path.resolve(candidate) !== activeWorkspace;
|
||||
});
|
||||
return { activeWorkspace, legacyDirs };
|
||||
}
|
||||
|
||||
export function formatLegacyWorkspaceWarning(
|
||||
detection: LegacyWorkspaceDetection,
|
||||
): string {
|
||||
return [
|
||||
"Legacy workspace directories detected (may contain old agent files):",
|
||||
...detection.legacyDirs.map((dir) => `- ${dir}`),
|
||||
`Active workspace: ${detection.activeWorkspace}`,
|
||||
"If unused, archive or move to Trash (e.g. trash ~/clawdis).",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -157,6 +157,7 @@ vi.mock("../daemon/service.js", () => ({
|
||||
restart: serviceRestart,
|
||||
isLoaded: serviceIsLoaded,
|
||||
readCommand: vi.fn(),
|
||||
readRuntime: vi.fn().mockResolvedValue({ status: "running" }),
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -492,6 +493,52 @@ describe("doctor", () => {
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("warns when legacy workspace directories exist", async () => {
|
||||
readConfigFileSnapshot.mockResolvedValue({
|
||||
path: "/tmp/clawdbot.json",
|
||||
exists: true,
|
||||
raw: "{}",
|
||||
parsed: {},
|
||||
valid: true,
|
||||
config: {
|
||||
agent: { workspace: "/Users/steipete/clawd" },
|
||||
},
|
||||
issues: [],
|
||||
legacyIssues: [],
|
||||
});
|
||||
|
||||
note.mockClear();
|
||||
const homedirSpy = vi
|
||||
.spyOn(os, "homedir")
|
||||
.mockReturnValue("/Users/steipete");
|
||||
const realExists = fs.existsSync;
|
||||
const existsSpy = vi.spyOn(fs, "existsSync").mockImplementation((value) => {
|
||||
if (value === "/Users/steipete/clawdis") return true;
|
||||
return realExists(value as never);
|
||||
});
|
||||
|
||||
const { doctorCommand } = await import("./doctor.js");
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
|
||||
await doctorCommand(runtime, { nonInteractive: true });
|
||||
|
||||
expect(
|
||||
note.mock.calls.some(
|
||||
([message, title]) =>
|
||||
title === "Legacy workspace" &&
|
||||
typeof message === "string" &&
|
||||
message.includes("/Users/steipete/clawdis"),
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
homedirSpy.mockRestore();
|
||||
existsSpy.mockRestore();
|
||||
});
|
||||
it("falls back to legacy sandbox image when missing", async () => {
|
||||
readConfigFileSnapshot.mockResolvedValue({
|
||||
path: "/tmp/clawdbot.json",
|
||||
|
||||
@@ -5,10 +5,14 @@ import {
|
||||
CONFIG_PATH_CLAWDBOT,
|
||||
migrateLegacyConfig,
|
||||
readConfigFileSnapshot,
|
||||
resolveGatewayPort,
|
||||
writeConfigFile,
|
||||
} from "../config/config.js";
|
||||
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
|
||||
import { resolveGatewayLogPaths } from "../daemon/launchd.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
import type { GatewayServiceRuntime } from "../daemon/service-runtime.js";
|
||||
import { formatPortDiagnostics, inspectPortUsage } from "../infra/ports.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { resolveUserPath, sleep } from "../utils.js";
|
||||
@@ -36,6 +40,8 @@ import {
|
||||
runLegacyStateMigrations,
|
||||
} from "./doctor-state-migrations.js";
|
||||
import {
|
||||
detectLegacyWorkspaceDirs,
|
||||
formatLegacyWorkspaceWarning,
|
||||
MEMORY_SYSTEM_PROMPT,
|
||||
shouldSuggestMemorySystem,
|
||||
} from "./doctor-workspace.js";
|
||||
@@ -51,6 +57,62 @@ function resolveMode(cfg: ClawdbotConfig): "local" | "remote" {
|
||||
return cfg.gateway?.mode === "remote" ? "remote" : "local";
|
||||
}
|
||||
|
||||
function formatRuntimeSummary(
|
||||
runtime: GatewayServiceRuntime | undefined,
|
||||
): string | null {
|
||||
if (!runtime) return null;
|
||||
const status = runtime.status ?? "unknown";
|
||||
const details: string[] = [];
|
||||
if (runtime.pid) details.push(`pid ${runtime.pid}`);
|
||||
if (runtime.state && runtime.state.toLowerCase() !== status) {
|
||||
details.push(`state ${runtime.state}`);
|
||||
}
|
||||
if (runtime.subState) details.push(`sub ${runtime.subState}`);
|
||||
if (runtime.lastExitStatus !== undefined) {
|
||||
details.push(`last exit ${runtime.lastExitStatus}`);
|
||||
}
|
||||
if (runtime.lastExitReason) {
|
||||
details.push(`reason ${runtime.lastExitReason}`);
|
||||
}
|
||||
if (runtime.lastRunResult) {
|
||||
details.push(`last run ${runtime.lastRunResult}`);
|
||||
}
|
||||
if (runtime.lastRunTime) {
|
||||
details.push(`last run time ${runtime.lastRunTime}`);
|
||||
}
|
||||
if (runtime.detail) details.push(runtime.detail);
|
||||
return details.length > 0 ? `${status} (${details.join(", ")})` : status;
|
||||
}
|
||||
|
||||
function buildGatewayRuntimeHints(
|
||||
runtime: GatewayServiceRuntime | undefined,
|
||||
): string[] {
|
||||
const hints: string[] = [];
|
||||
if (!runtime) return hints;
|
||||
if (runtime.cachedLabel && process.platform === "darwin") {
|
||||
hints.push(
|
||||
`LaunchAgent label cached but plist missing. Clear with: launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}`,
|
||||
);
|
||||
}
|
||||
if (runtime.status === "stopped") {
|
||||
hints.push(
|
||||
"Service is loaded but not running (likely exited immediately).",
|
||||
);
|
||||
if (process.platform === "darwin") {
|
||||
const logs = resolveGatewayLogPaths(process.env);
|
||||
hints.push(`Logs: ${logs.stdoutPath}`);
|
||||
hints.push(`Errors: ${logs.stderrPath}`);
|
||||
} else if (process.platform === "linux") {
|
||||
hints.push(
|
||||
"Logs: journalctl --user -u clawdbot-gateway.service -n 200 --no-pager",
|
||||
);
|
||||
} else if (process.platform === "win32") {
|
||||
hints.push('Logs: schtasks /Query /TN "Clawdbot Gateway" /V /FO LIST');
|
||||
}
|
||||
}
|
||||
return hints;
|
||||
}
|
||||
|
||||
export async function doctorCommand(
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
options: DoctorOptions = {},
|
||||
@@ -168,6 +230,10 @@ export async function doctorCommand(
|
||||
const workspaceDir = resolveUserPath(
|
||||
cfg.agent?.workspace ?? DEFAULT_WORKSPACE,
|
||||
);
|
||||
const legacyWorkspace = detectLegacyWorkspaceDirs({ workspaceDir });
|
||||
if (legacyWorkspace.legacyDirs.length > 0) {
|
||||
note(formatLegacyWorkspaceWarning(legacyWorkspace), "Legacy workspace");
|
||||
}
|
||||
const skillsReport = buildWorkspaceSkillStatus(workspaceDir, { config: cfg });
|
||||
note(
|
||||
[
|
||||
@@ -198,11 +264,29 @@ export async function doctorCommand(
|
||||
}
|
||||
|
||||
if (!healthOk) {
|
||||
if (resolveMode(cfg) === "local") {
|
||||
const port = resolveGatewayPort(cfg, process.env);
|
||||
const diagnostics = await inspectPortUsage(port);
|
||||
if (diagnostics.status === "busy") {
|
||||
note(formatPortDiagnostics(diagnostics).join("\n"), "Gateway port");
|
||||
}
|
||||
}
|
||||
const service = resolveGatewayService();
|
||||
const loaded = await service.isLoaded({ env: process.env });
|
||||
if (!loaded) {
|
||||
note("Gateway daemon not installed.", "Gateway");
|
||||
} else {
|
||||
const serviceRuntime = await service
|
||||
.readRuntime(process.env)
|
||||
.catch(() => undefined);
|
||||
const summary = formatRuntimeSummary(serviceRuntime);
|
||||
const hints = buildGatewayRuntimeHints(serviceRuntime);
|
||||
if (summary || hints.length > 0) {
|
||||
const lines = [];
|
||||
if (summary) lines.push(`Runtime: ${summary}`);
|
||||
lines.push(...hints);
|
||||
note(lines.join("\n"), "Gateway");
|
||||
}
|
||||
if (process.platform === "darwin") {
|
||||
note(
|
||||
`LaunchAgent loaded; stopping requires "clawdbot gateway stop" or launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}.`,
|
||||
|
||||
Reference in New Issue
Block a user