feat: scan extra gateways in doctor
This commit is contained in:
@@ -176,10 +176,13 @@ Interactive configuration wizard (models, providers, skills, gateway).
|
|||||||
Audit and modernize the local configuration.
|
Audit and modernize the local configuration.
|
||||||
|
|
||||||
### `doctor`
|
### `doctor`
|
||||||
Health checks + quick fixes.
|
Health checks + quick fixes (config + gateway + legacy services).
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
- `--no-workspace-suggestions`: disable workspace memory hints.
|
- `--no-workspace-suggestions`: disable workspace memory hints.
|
||||||
|
- `--yes`: accept defaults without prompting (headless).
|
||||||
|
- `--non-interactive`: skip prompts; apply safe migrations only.
|
||||||
|
- `--deep`: scan system services for extra gateway installs.
|
||||||
|
|
||||||
## Auth + provider helpers
|
## Auth + provider helpers
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ read_when:
|
|||||||
- Migrates legacy `~/.clawdis/clawdis.json` when no Clawdbot config exists.
|
- Migrates legacy `~/.clawdis/clawdis.json` when no Clawdbot config exists.
|
||||||
- Checks sandbox Docker images when sandboxing is enabled (offers to build or switch to legacy names).
|
- Checks sandbox Docker images when sandboxing is enabled (offers to build or switch to legacy names).
|
||||||
- Detects legacy Clawdis services (launchd/systemd; legacy schtasks for native Windows) and offers to migrate them.
|
- Detects legacy Clawdis services (launchd/systemd; legacy schtasks for native Windows) and offers to migrate them.
|
||||||
|
- Detects other gateway-like services and prints cleanup hints (optional deep scan for system services).
|
||||||
- On Linux, checks if systemd user lingering is enabled and can enable it (required to keep the Gateway alive after logout).
|
- On Linux, checks if systemd user lingering is enabled and can enable it (required to keep the Gateway alive after logout).
|
||||||
- Migrates legacy on-disk state layouts (sessions, agentDir, provider auth dirs) into the current per-agent/per-account structure.
|
- Migrates legacy on-disk state layouts (sessions, agentDir, provider auth dirs) into the current per-agent/per-account structure.
|
||||||
|
|
||||||
@@ -70,6 +71,12 @@ clawdbot doctor --non-interactive
|
|||||||
|
|
||||||
Run without prompts and only apply safe migrations (config normalization + on-disk state moves). Skips restart/service/sandbox actions that require human confirmation.
|
Run without prompts and only apply safe migrations (config normalization + on-disk state moves). Skips restart/service/sandbox actions that require human confirmation.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot doctor --deep
|
||||||
|
```
|
||||||
|
|
||||||
|
Scan system services for extra gateway installs (launchd/systemd/schtasks).
|
||||||
|
|
||||||
If you want to review changes before writing, open the config file first:
|
If you want to review changes before writing, open the config file first:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
type FindExtraGatewayServicesOptions,
|
type FindExtraGatewayServicesOptions,
|
||||||
findExtraGatewayServices,
|
findExtraGatewayServices,
|
||||||
|
renderGatewayServiceCleanupHints,
|
||||||
} from "../daemon/inspect.js";
|
} from "../daemon/inspect.js";
|
||||||
import { findLegacyGatewayServices } from "../daemon/legacy.js";
|
import { findLegacyGatewayServices } from "../daemon/legacy.js";
|
||||||
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
||||||
@@ -110,25 +111,6 @@ function renderGatewayServiceStartHints(): string[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGatewayServiceCleanupHints(): string[] {
|
|
||||||
switch (process.platform) {
|
|
||||||
case "darwin":
|
|
||||||
return [
|
|
||||||
`launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}`,
|
|
||||||
`rm ~/Library/LaunchAgents/${GATEWAY_LAUNCH_AGENT_LABEL}.plist`,
|
|
||||||
];
|
|
||||||
case "linux":
|
|
||||||
return [
|
|
||||||
`systemctl --user disable --now ${GATEWAY_SYSTEMD_SERVICE_NAME}.service`,
|
|
||||||
`rm ~/.config/systemd/user/${GATEWAY_SYSTEMD_SERVICE_NAME}.service`,
|
|
||||||
];
|
|
||||||
case "win32":
|
|
||||||
return [`schtasks /Delete /TN "${GATEWAY_WINDOWS_TASK_NAME}" /F`];
|
|
||||||
default:
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function gatherDaemonStatus(opts: {
|
async function gatherDaemonStatus(opts: {
|
||||||
rpc: GatewayRpcOpts;
|
rpc: GatewayRpcOpts;
|
||||||
probe: boolean;
|
probe: boolean;
|
||||||
|
|||||||
@@ -324,12 +324,14 @@ export function buildProgram() {
|
|||||||
"Run without prompts (safe migrations only)",
|
"Run without prompts (safe migrations only)",
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
.option("--deep", "Scan system services for extra gateway installs", false)
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
try {
|
try {
|
||||||
await doctorCommand(defaultRuntime, {
|
await doctorCommand(defaultRuntime, {
|
||||||
workspaceSuggestions: opts.workspaceSuggestions,
|
workspaceSuggestions: opts.workspaceSuggestions,
|
||||||
yes: Boolean(opts.yes),
|
yes: Boolean(opts.yes),
|
||||||
nonInteractive: Boolean(opts.nonInteractive),
|
nonInteractive: Boolean(opts.nonInteractive),
|
||||||
|
deep: Boolean(opts.deep),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
defaultRuntime.error(String(err));
|
defaultRuntime.error(String(err));
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ const createConfigIO = vi.fn(() => ({
|
|||||||
|
|
||||||
const findLegacyGatewayServices = vi.fn().mockResolvedValue([]);
|
const findLegacyGatewayServices = vi.fn().mockResolvedValue([]);
|
||||||
const uninstallLegacyGatewayServices = vi.fn().mockResolvedValue([]);
|
const uninstallLegacyGatewayServices = vi.fn().mockResolvedValue([]);
|
||||||
|
const findExtraGatewayServices = vi.fn().mockResolvedValue([]);
|
||||||
|
const renderGatewayServiceCleanupHints = vi.fn().mockReturnValue(["cleanup"]);
|
||||||
const resolveGatewayProgramArguments = vi.fn().mockResolvedValue({
|
const resolveGatewayProgramArguments = vi.fn().mockResolvedValue({
|
||||||
programArguments: ["node", "cli", "gateway-daemon", "--port", "18789"],
|
programArguments: ["node", "cli", "gateway-daemon", "--port", "18789"],
|
||||||
});
|
});
|
||||||
@@ -98,6 +100,11 @@ vi.mock("../daemon/legacy.js", () => ({
|
|||||||
uninstallLegacyGatewayServices,
|
uninstallLegacyGatewayServices,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("../daemon/inspect.js", () => ({
|
||||||
|
findExtraGatewayServices,
|
||||||
|
renderGatewayServiceCleanupHints,
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("../daemon/program-args.js", () => ({
|
vi.mock("../daemon/program-args.js", () => ({
|
||||||
resolveGatewayProgramArguments,
|
resolveGatewayProgramArguments,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ import {
|
|||||||
} from "../config/config.js";
|
} from "../config/config.js";
|
||||||
import { resolveGatewayPort, resolveIsNixMode } from "../config/paths.js";
|
import { resolveGatewayPort, resolveIsNixMode } from "../config/paths.js";
|
||||||
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
|
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
|
||||||
|
import {
|
||||||
|
findExtraGatewayServices,
|
||||||
|
renderGatewayServiceCleanupHints,
|
||||||
|
} from "../daemon/inspect.js";
|
||||||
import {
|
import {
|
||||||
findLegacyGatewayServices,
|
findLegacyGatewayServices,
|
||||||
uninstallLegacyGatewayServices,
|
uninstallLegacyGatewayServices,
|
||||||
@@ -351,6 +355,7 @@ type DoctorOptions = {
|
|||||||
workspaceSuggestions?: boolean;
|
workspaceSuggestions?: boolean;
|
||||||
yes?: boolean;
|
yes?: boolean;
|
||||||
nonInteractive?: boolean;
|
nonInteractive?: boolean;
|
||||||
|
deep?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DoctorPrompter = {
|
type DoctorPrompter = {
|
||||||
@@ -863,6 +868,34 @@ async function maybeMigrateLegacyGatewayService(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function maybeScanExtraGatewayServices(options: DoctorOptions) {
|
||||||
|
const extraServices = await findExtraGatewayServices(process.env, {
|
||||||
|
deep: options.deep,
|
||||||
|
});
|
||||||
|
if (extraServices.length === 0) return;
|
||||||
|
|
||||||
|
note(
|
||||||
|
extraServices
|
||||||
|
.map((svc) => `- ${svc.label} (${svc.scope}, ${svc.detail})`)
|
||||||
|
.join("\n"),
|
||||||
|
"Other gateway-like services detected",
|
||||||
|
);
|
||||||
|
|
||||||
|
const cleanupHints = renderGatewayServiceCleanupHints();
|
||||||
|
if (cleanupHints.length > 0) {
|
||||||
|
note(cleanupHints.map((hint) => `- ${hint}`).join("\n"), "Cleanup hints");
|
||||||
|
}
|
||||||
|
|
||||||
|
note(
|
||||||
|
[
|
||||||
|
"Recommendation: run a single gateway per machine.",
|
||||||
|
"One gateway supports multiple agents.",
|
||||||
|
"If you need multiple gateways, isolate ports + config/state (see docs: /gateway#multiple-gateways-same-host).",
|
||||||
|
].join("\n"),
|
||||||
|
"Gateway recommendation",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function doctorCommand(
|
export async function doctorCommand(
|
||||||
runtime: RuntimeEnv = defaultRuntime,
|
runtime: RuntimeEnv = defaultRuntime,
|
||||||
options: DoctorOptions = {},
|
options: DoctorOptions = {},
|
||||||
@@ -939,6 +972,7 @@ export async function doctorCommand(
|
|||||||
cfg = await maybeRepairSandboxImages(cfg, runtime, prompter);
|
cfg = await maybeRepairSandboxImages(cfg, runtime, prompter);
|
||||||
|
|
||||||
await maybeMigrateLegacyGatewayService(cfg, runtime, prompter);
|
await maybeMigrateLegacyGatewayService(cfg, runtime, prompter);
|
||||||
|
await maybeScanExtraGatewayServices(options);
|
||||||
|
|
||||||
await noteSecurityWarnings(cfg);
|
await noteSecurityWarnings(cfg);
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,25 @@ export type FindExtraGatewayServicesOptions = {
|
|||||||
const EXTRA_MARKERS = ["clawdbot", "clawdis", "gateway-daemon"];
|
const EXTRA_MARKERS = ["clawdbot", "clawdis", "gateway-daemon"];
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
export function renderGatewayServiceCleanupHints(): string[] {
|
||||||
|
switch (process.platform) {
|
||||||
|
case "darwin":
|
||||||
|
return [
|
||||||
|
`launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}`,
|
||||||
|
`rm ~/Library/LaunchAgents/${GATEWAY_LAUNCH_AGENT_LABEL}.plist`,
|
||||||
|
];
|
||||||
|
case "linux":
|
||||||
|
return [
|
||||||
|
`systemctl --user disable --now ${GATEWAY_SYSTEMD_SERVICE_NAME}.service`,
|
||||||
|
`rm ~/.config/systemd/user/${GATEWAY_SYSTEMD_SERVICE_NAME}.service`,
|
||||||
|
];
|
||||||
|
case "win32":
|
||||||
|
return [`schtasks /Delete /TN "${GATEWAY_WINDOWS_TASK_NAME}" /F`];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function resolveHomeDir(env: Record<string, string | undefined>): string {
|
function resolveHomeDir(env: Record<string, string | undefined>): string {
|
||||||
const home = env.HOME?.trim() || env.USERPROFILE?.trim();
|
const home = env.HOME?.trim() || env.USERPROFILE?.trim();
|
||||||
if (!home) throw new Error("Missing HOME");
|
if (!home) throw new Error("Missing HOME");
|
||||||
|
|||||||
Reference in New Issue
Block a user