feat: improve doctor update flow

This commit is contained in:
Peter Steinberger
2026-01-21 05:23:22 +00:00
parent 810374d648
commit 6180603ef4
4 changed files with 49 additions and 15 deletions

View File

@@ -454,6 +454,10 @@ export function registerPluginsCli(program: Command) {
const targets = opts.all ? Object.keys(installs) : id ? [id] : []; const targets = opts.all ? Object.keys(installs) : id ? [id] : [];
if (targets.length === 0) { if (targets.length === 0) {
if (opts.all) {
defaultRuntime.log("No npm-installed plugins to update.");
return;
}
defaultRuntime.error("Provide a plugin id or use --all."); defaultRuntime.error("Provide a plugin id or use --all.");
process.exit(1); process.exit(1);
} }

View File

@@ -623,7 +623,8 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
process.env.CLAWDBOT_UPDATE_IN_PROGRESS = "1"; process.env.CLAWDBOT_UPDATE_IN_PROGRESS = "1";
try { try {
const { doctorCommand } = await import("../commands/doctor.js"); const { doctorCommand } = await import("../commands/doctor.js");
await doctorCommand(defaultRuntime, { nonInteractive: true }); const interactiveDoctor = Boolean(process.stdin.isTTY) && !opts.json && opts.yes !== true;
await doctorCommand(defaultRuntime, { nonInteractive: !interactiveDoctor });
} catch (err) { } catch (err) {
defaultRuntime.log(theme.warn(`Doctor failed: ${String(err)}`)); defaultRuntime.log(theme.warn(`Doctor failed: ${String(err)}`));
} finally { } finally {

View File

@@ -121,10 +121,14 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
options: DoctorOptions; options: DoctorOptions;
confirm: (p: { message: string; initialValue: boolean }) => Promise<boolean>; confirm: (p: { message: string; initialValue: boolean }) => Promise<boolean>;
}) { }) {
void params.confirm;
const shouldRepair = params.options.repair === true || params.options.yes === true; const shouldRepair = params.options.repair === true || params.options.yes === true;
const snapshot = await readConfigFileSnapshot(); const snapshot = await readConfigFileSnapshot();
let cfg: ClawdbotConfig = snapshot.config ?? {}; const baseCfg = snapshot.config ?? {};
let cfg: ClawdbotConfig = baseCfg;
let candidate = structuredClone(baseCfg) as ClawdbotConfig;
let pendingChanges = false;
let shouldWriteConfig = false;
const fixHints: string[] = [];
if (snapshot.exists && !snapshot.valid && snapshot.legacyIssues.length === 0) { if (snapshot.exists && !snapshot.valid && snapshot.legacyIssues.length === 0) {
note("Config invalid; doctor will run with best-effort config.", "Config"); note("Config invalid; doctor will run with best-effort config.", "Config");
} }
@@ -139,52 +143,76 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
snapshot.legacyIssues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n"), snapshot.legacyIssues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n"),
"Legacy config keys detected", "Legacy config keys detected",
); );
const { config: migrated, changes } = migrateLegacyConfig(snapshot.parsed);
if (changes.length > 0) {
note(changes.join("\n"), "Doctor changes");
}
if (migrated) {
candidate = migrated;
pendingChanges = pendingChanges || changes.length > 0;
}
if (shouldRepair) { if (shouldRepair) {
// Legacy migration (2026-01-02, commit: 16420e5b) — normalize per-provider allowlists; move WhatsApp gating into channels.whatsapp.allowFrom. // Legacy migration (2026-01-02, commit: 16420e5b) — normalize per-provider allowlists; move WhatsApp gating into channels.whatsapp.allowFrom.
const { config: migrated, changes } = migrateLegacyConfig(snapshot.parsed);
if (changes.length > 0) note(changes.join("\n"), "Doctor changes");
if (migrated) cfg = migrated; if (migrated) cfg = migrated;
} else { } else {
note( fixHints.push(
`Run "${formatCliCommand("clawdbot doctor --fix")}" to apply legacy migrations.`, `Run "${formatCliCommand("clawdbot doctor --fix")}" to apply legacy migrations.`,
"Doctor",
); );
} }
} }
const normalized = normalizeLegacyConfigValues(cfg); const normalized = normalizeLegacyConfigValues(candidate);
if (normalized.changes.length > 0) { if (normalized.changes.length > 0) {
note(normalized.changes.join("\n"), "Doctor changes"); note(normalized.changes.join("\n"), "Doctor changes");
candidate = normalized.config;
pendingChanges = true;
if (shouldRepair) { if (shouldRepair) {
cfg = normalized.config; cfg = normalized.config;
} else { } else {
note(`Run "${formatCliCommand("clawdbot doctor --fix")}" to apply these changes.`, "Doctor"); fixHints.push(`Run "${formatCliCommand("clawdbot doctor --fix")}" to apply these changes.`);
} }
} }
const autoEnable = applyPluginAutoEnable({ config: cfg, env: process.env }); const autoEnable = applyPluginAutoEnable({ config: candidate, env: process.env });
if (autoEnable.changes.length > 0) { if (autoEnable.changes.length > 0) {
note(autoEnable.changes.join("\n"), "Doctor changes"); note(autoEnable.changes.join("\n"), "Doctor changes");
candidate = autoEnable.config;
pendingChanges = true;
if (shouldRepair) { if (shouldRepair) {
cfg = autoEnable.config; cfg = autoEnable.config;
} else { } else {
note(`Run "${formatCliCommand("clawdbot doctor --fix")}" to apply these changes.`, "Doctor"); fixHints.push(`Run "${formatCliCommand("clawdbot doctor --fix")}" to apply these changes.`);
} }
} }
const unknown = stripUnknownConfigKeys(cfg); const unknown = stripUnknownConfigKeys(candidate);
if (unknown.removed.length > 0) { if (unknown.removed.length > 0) {
const lines = unknown.removed.map((path) => `- ${path}`).join("\n"); const lines = unknown.removed.map((path) => `- ${path}`).join("\n");
candidate = unknown.config;
pendingChanges = true;
if (shouldRepair) { if (shouldRepair) {
cfg = unknown.config; cfg = unknown.config;
note(lines, "Doctor changes"); note(lines, "Doctor changes");
} else { } else {
note(lines, "Unknown config keys"); note(lines, "Unknown config keys");
note('Run "clawdbot doctor --fix" to remove these keys.', "Doctor"); fixHints.push('Run "clawdbot doctor --fix" to remove these keys.');
}
}
if (!shouldRepair && pendingChanges) {
const shouldApply = await params.confirm({
message: "Apply recommended config repairs now?",
initialValue: true,
});
if (shouldApply) {
cfg = candidate;
shouldWriteConfig = true;
} else if (fixHints.length > 0) {
note(fixHints.join("\n"), "Doctor");
} }
} }
noteOpencodeProviderOverrides(cfg); noteOpencodeProviderOverrides(cfg);
return { cfg, path: snapshot.path ?? CONFIG_PATH_CLAWDBOT }; return { cfg, path: snapshot.path ?? CONFIG_PATH_CLAWDBOT, shouldWriteConfig };
} }

View File

@@ -250,7 +250,8 @@ export async function doctorCommand(
healthOk, healthOk,
}); });
if (prompter.shouldRepair) { const shouldWriteConfig = prompter.shouldRepair || configResult.shouldWriteConfig;
if (shouldWriteConfig) {
cfg = applyWizardMetadata(cfg, { command: "doctor", mode: resolveMode(cfg) }); cfg = applyWizardMetadata(cfg, { command: "doctor", mode: resolveMode(cfg) });
await writeConfigFile(cfg); await writeConfigFile(cfg);
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`); runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);