feat(doctor): add repair/force flows
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Doctor/Daemon: audit supervisor configs, recommend doctor from daemon status, and document user vs system services. (#?) — thanks @steipete
|
||||
- Doctor/Daemon: audit supervisor configs, add --repair/--force flows, surface service config audits in daemon status, and document user vs system services. — thanks @steipete
|
||||
- Daemon: align generated systemd unit with docs for network-online + restart delay. (#479) — thanks @azade-c
|
||||
- Outbound: default Telegram account selection for config-only tokens; remove heartbeat-specific accountId handling. (follow-up #516) — thanks @YuriNachos
|
||||
- Cron: allow Telegram delivery targets with topic/thread IDs (e.g. `-100…:topic:123`). (#474) — thanks @mitschabaude-bot
|
||||
|
||||
@@ -23,6 +23,18 @@ clawdbot doctor --yes
|
||||
|
||||
Accept defaults without prompting (including restart/service/sandbox repair steps when applicable).
|
||||
|
||||
```bash
|
||||
clawdbot doctor --repair
|
||||
```
|
||||
|
||||
Apply recommended repairs without prompting (repairs + restarts where safe).
|
||||
|
||||
```bash
|
||||
clawdbot doctor --repair --force
|
||||
```
|
||||
|
||||
Apply aggressive repairs too (overwrites custom supervisor configs).
|
||||
|
||||
```bash
|
||||
clawdbot doctor --non-interactive
|
||||
```
|
||||
@@ -153,6 +165,8 @@ rewrite the service file/task to the current defaults.
|
||||
Notes:
|
||||
- `clawdbot doctor` prompts before rewriting supervisor config.
|
||||
- `clawdbot doctor --yes` accepts the default repair prompts.
|
||||
- `clawdbot doctor --repair` applies recommended fixes without prompts.
|
||||
- `clawdbot doctor --repair --force` overwrites custom supervisor configs.
|
||||
- You can always force a full rewrite via `clawdbot daemon install --force`.
|
||||
|
||||
### 12) Gateway runtime + port diagnostics
|
||||
|
||||
@@ -552,7 +552,9 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
|
||||
const detail = issue.detail ? ` (${issue.detail})` : "";
|
||||
defaultRuntime.error(`Service config issue: ${issue.message}${detail}`);
|
||||
}
|
||||
defaultRuntime.error('Recommendation: run "clawdbot doctor".');
|
||||
defaultRuntime.error(
|
||||
'Recommendation: run "clawdbot doctor" (or "clawdbot doctor --repair").',
|
||||
);
|
||||
}
|
||||
if (status.config) {
|
||||
const cliCfg = `${status.config.cli.path}${status.config.cli.exists ? "" : " (missing)"}${status.config.cli.valid ? "" : " (invalid)"}`;
|
||||
|
||||
@@ -328,6 +328,8 @@ export function buildProgram() {
|
||||
false,
|
||||
)
|
||||
.option("--yes", "Accept defaults without prompting", false)
|
||||
.option("--repair", "Apply recommended repairs without prompting", false)
|
||||
.option("--force", "Apply aggressive repairs (overwrites custom service config)", false)
|
||||
.option(
|
||||
"--non-interactive",
|
||||
"Run without prompts (safe migrations only)",
|
||||
@@ -339,6 +341,8 @@ export function buildProgram() {
|
||||
await doctorCommand(defaultRuntime, {
|
||||
workspaceSuggestions: opts.workspaceSuggestions,
|
||||
yes: Boolean(opts.yes),
|
||||
repair: Boolean(opts.repair),
|
||||
force: Boolean(opts.force),
|
||||
nonInteractive: Boolean(opts.nonInteractive),
|
||||
deep: Boolean(opts.deep),
|
||||
});
|
||||
|
||||
@@ -167,10 +167,31 @@ export async function maybeRepairGatewayServiceConfig(
|
||||
"Gateway service config",
|
||||
);
|
||||
|
||||
const repair = await prompter.confirmSkipInNonInteractive({
|
||||
message: "Update gateway service config to the recommended defaults now?",
|
||||
initialValue: true,
|
||||
});
|
||||
const aggressiveIssues = audit.issues.filter(
|
||||
(issue) => issue.level === "aggressive",
|
||||
);
|
||||
const recommendedIssues = audit.issues.filter(
|
||||
(issue) => issue.level !== "aggressive",
|
||||
);
|
||||
const needsAggressive = aggressiveIssues.length > 0;
|
||||
|
||||
if (needsAggressive && !prompter.shouldForce) {
|
||||
note(
|
||||
"Custom or unexpected service edits detected. Rerun with --force to overwrite.",
|
||||
"Gateway service config",
|
||||
);
|
||||
}
|
||||
|
||||
const repair = needsAggressive
|
||||
? await prompter.confirmAggressive({
|
||||
message:
|
||||
"Overwrite gateway service config with current defaults now?",
|
||||
initialValue: Boolean(prompter.shouldForce),
|
||||
})
|
||||
: await prompter.confirmRepair({
|
||||
message: "Update gateway service config to the recommended defaults now?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (!repair) return;
|
||||
|
||||
const devMode =
|
||||
|
||||
@@ -8,14 +8,20 @@ export type DoctorOptions = {
|
||||
yes?: boolean;
|
||||
nonInteractive?: boolean;
|
||||
deep?: boolean;
|
||||
repair?: boolean;
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
export type DoctorPrompter = {
|
||||
confirm: (params: Parameters<typeof confirm>[0]) => Promise<boolean>;
|
||||
confirmRepair: (params: Parameters<typeof confirm>[0]) => Promise<boolean>;
|
||||
confirmAggressive: (params: Parameters<typeof confirm>[0]) => Promise<boolean>;
|
||||
confirmSkipInNonInteractive: (
|
||||
params: Parameters<typeof confirm>[0],
|
||||
) => Promise<boolean>;
|
||||
select: <T>(params: Parameters<typeof select>[0], fallback: T) => Promise<T>;
|
||||
shouldRepair: boolean;
|
||||
shouldForce: boolean;
|
||||
};
|
||||
|
||||
export function createDoctorPrompter(params: {
|
||||
@@ -24,24 +30,42 @@ export function createDoctorPrompter(params: {
|
||||
}): DoctorPrompter {
|
||||
const yes = params.options.yes === true;
|
||||
const requestedNonInteractive = params.options.nonInteractive === true;
|
||||
const shouldRepair = params.options.repair === true || yes;
|
||||
const shouldForce = params.options.force === true;
|
||||
const isTty = Boolean(process.stdin.isTTY);
|
||||
const nonInteractive = requestedNonInteractive || (!isTty && !yes);
|
||||
|
||||
const canPrompt = isTty && !yes && !nonInteractive;
|
||||
const confirmDefault = async (p: Parameters<typeof confirm>[0]) => {
|
||||
if (nonInteractive) return false;
|
||||
if (shouldRepair) return true;
|
||||
if (!canPrompt) return Boolean(p.initialValue ?? false);
|
||||
return guardCancel(await confirm(p), params.runtime) === true;
|
||||
};
|
||||
|
||||
return {
|
||||
confirm: confirmDefault,
|
||||
confirmSkipInNonInteractive: async (p) => {
|
||||
confirmRepair: async (p) => {
|
||||
if (nonInteractive) return false;
|
||||
return confirmDefault(p);
|
||||
},
|
||||
confirmAggressive: async (p) => {
|
||||
if (nonInteractive) return false;
|
||||
if (shouldRepair && shouldForce) return true;
|
||||
if (shouldRepair && !shouldForce) return false;
|
||||
if (!canPrompt) return Boolean(p.initialValue ?? false);
|
||||
return guardCancel(await confirm(p), params.runtime) === true;
|
||||
},
|
||||
confirmSkipInNonInteractive: async (p) => {
|
||||
if (nonInteractive) return false;
|
||||
if (shouldRepair) return true;
|
||||
return confirmDefault(p);
|
||||
},
|
||||
select: async <T>(p: Parameters<typeof select>[0], fallback: T) => {
|
||||
if (!canPrompt) return fallback;
|
||||
if (!canPrompt || shouldRepair) return fallback;
|
||||
return guardCancel(await select(p), params.runtime) as T;
|
||||
},
|
||||
shouldRepair,
|
||||
shouldForce,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export type ServiceConfigIssue = {
|
||||
code: string;
|
||||
message: string;
|
||||
detail?: string;
|
||||
level?: "recommended" | "aggressive";
|
||||
};
|
||||
|
||||
export type ServiceConfigAudit = {
|
||||
@@ -84,6 +85,7 @@ async function auditSystemdUnit(
|
||||
code: "systemd-after-network-online",
|
||||
message: "Missing systemd After=network-online.target",
|
||||
detail: unitPath,
|
||||
level: "recommended",
|
||||
});
|
||||
}
|
||||
if (!parsed.wants.has("network-online.target")) {
|
||||
@@ -91,6 +93,7 @@ async function auditSystemdUnit(
|
||||
code: "systemd-wants-network-online",
|
||||
message: "Missing systemd Wants=network-online.target",
|
||||
detail: unitPath,
|
||||
level: "recommended",
|
||||
});
|
||||
}
|
||||
if (!isRestartSecPreferred(parsed.restartSec)) {
|
||||
@@ -98,6 +101,7 @@ async function auditSystemdUnit(
|
||||
code: "systemd-restart-sec",
|
||||
message: "RestartSec does not match the recommended 5s",
|
||||
detail: unitPath,
|
||||
level: "recommended",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -121,6 +125,7 @@ async function auditLaunchdPlist(
|
||||
code: "launchd-run-at-load",
|
||||
message: "LaunchAgent is missing RunAtLoad=true",
|
||||
detail: plistPath,
|
||||
level: "recommended",
|
||||
});
|
||||
}
|
||||
if (!hasKeepAlive) {
|
||||
@@ -128,6 +133,7 @@ async function auditLaunchdPlist(
|
||||
code: "launchd-keep-alive",
|
||||
message: "LaunchAgent is missing KeepAlive=true",
|
||||
detail: plistPath,
|
||||
level: "recommended",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -141,6 +147,7 @@ function auditGatewayCommand(
|
||||
issues.push({
|
||||
code: "gateway-command-missing",
|
||||
message: "Service command does not include the gateway subcommand",
|
||||
level: "aggressive",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user