From 6180603ef44eb053ba74a96595788c65eeea5bf9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 21 Jan 2026 05:23:22 +0000 Subject: [PATCH] feat: improve doctor update flow --- src/cli/plugins-cli.ts | 4 +++ src/cli/update-cli.ts | 3 +- src/commands/doctor-config-flow.ts | 54 +++++++++++++++++++++++------- src/commands/doctor.ts | 3 +- 4 files changed, 49 insertions(+), 15 deletions(-) diff --git a/src/cli/plugins-cli.ts b/src/cli/plugins-cli.ts index 80f61a6f1..0f4468beb 100644 --- a/src/cli/plugins-cli.ts +++ b/src/cli/plugins-cli.ts @@ -454,6 +454,10 @@ export function registerPluginsCli(program: Command) { const targets = opts.all ? Object.keys(installs) : id ? [id] : []; 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."); process.exit(1); } diff --git a/src/cli/update-cli.ts b/src/cli/update-cli.ts index 4e269cee7..0eb297f32 100644 --- a/src/cli/update-cli.ts +++ b/src/cli/update-cli.ts @@ -623,7 +623,8 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise { process.env.CLAWDBOT_UPDATE_IN_PROGRESS = "1"; try { 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) { defaultRuntime.log(theme.warn(`Doctor failed: ${String(err)}`)); } finally { diff --git a/src/commands/doctor-config-flow.ts b/src/commands/doctor-config-flow.ts index d5bfdbca3..a656c6368 100644 --- a/src/commands/doctor-config-flow.ts +++ b/src/commands/doctor-config-flow.ts @@ -121,10 +121,14 @@ export async function loadAndMaybeMigrateDoctorConfig(params: { options: DoctorOptions; confirm: (p: { message: string; initialValue: boolean }) => Promise; }) { - void params.confirm; const shouldRepair = params.options.repair === true || params.options.yes === true; 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) { 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"), "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) { // 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; } else { - note( + fixHints.push( `Run "${formatCliCommand("clawdbot doctor --fix")}" to apply legacy migrations.`, - "Doctor", ); } } - const normalized = normalizeLegacyConfigValues(cfg); + const normalized = normalizeLegacyConfigValues(candidate); if (normalized.changes.length > 0) { note(normalized.changes.join("\n"), "Doctor changes"); + candidate = normalized.config; + pendingChanges = true; if (shouldRepair) { cfg = normalized.config; } 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) { note(autoEnable.changes.join("\n"), "Doctor changes"); + candidate = autoEnable.config; + pendingChanges = true; if (shouldRepair) { cfg = autoEnable.config; } 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) { const lines = unknown.removed.map((path) => `- ${path}`).join("\n"); + candidate = unknown.config; + pendingChanges = true; if (shouldRepair) { cfg = unknown.config; note(lines, "Doctor changes"); } else { 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); - return { cfg, path: snapshot.path ?? CONFIG_PATH_CLAWDBOT }; + return { cfg, path: snapshot.path ?? CONFIG_PATH_CLAWDBOT, shouldWriteConfig }; } diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index ce589eecc..2bc17db11 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -250,7 +250,8 @@ export async function doctorCommand( healthOk, }); - if (prompter.shouldRepair) { + const shouldWriteConfig = prompter.shouldRepair || configResult.shouldWriteConfig; + if (shouldWriteConfig) { cfg = applyWizardMetadata(cfg, { command: "doctor", mode: resolveMode(cfg) }); await writeConfigFile(cfg); runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);