diff --git a/CHANGELOG.md b/CHANGELOG.md index 100f1f0ca..41b1f6061 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 2026.1.12-4 ### Fixes +- Onboarding/Configure: refuse to proceed with invalid configs; run `clawdbot doctor` first to avoid wiping custom fields. (#764 — thanks @mukhtharcm) - Anthropic: merge consecutive user turns (preserve newest metadata) before validation to avoid “Incorrect role information” errors. (#804 — thanks @ThomsenDrake) - Discord/Slack: centralize reply-thread planning so auto-thread replies stay in the created thread without parent reply refs. - Update: run `clawdbot doctor --non-interactive` during updates to avoid TTY hangs. (#781 — thanks @ronyrus) diff --git a/docs/start/wizard.md b/docs/start/wizard.md index 26ce724dd..bf57a15f0 100644 --- a/docs/start/wizard.md +++ b/docs/start/wizard.md @@ -64,6 +64,8 @@ Tip: `--json` does **not** imply non-interactive mode. Use `--non-interactive` ( 1) **Existing config detection** - If `~/.clawdbot/clawdbot.json` exists, choose **Keep / Modify / Reset**. + - If the config is invalid or contains legacy keys, the wizard stops and asks + you to run `clawdbot doctor` before continuing. - Reset uses `trash` (never `rm`) and offers scopes: - Config only - Config + credentials + sessions diff --git a/src/commands/configure.ts b/src/commands/configure.ts index 1546b7e28..e9bc2b0f7 100644 --- a/src/commands/configure.ts +++ b/src/commands/configure.ts @@ -586,7 +586,7 @@ export async function runConfigureWizard( const prompter = createClackPrompter(); const snapshot = await readConfigFileSnapshot(); - let baseConfig: ClawdbotConfig = snapshot.valid ? snapshot.config : {}; + const baseConfig: ClawdbotConfig = snapshot.valid ? snapshot.config : {}; if (snapshot.exists) { const title = snapshot.valid @@ -604,14 +604,11 @@ export async function runConfigureWizard( ); } if (!snapshot.valid) { - const reset = guardCancel( - await confirm({ - message: "Config invalid. Start fresh?", - initialValue: true, - }), - runtime, + outro( + "Config invalid. Run `clawdbot doctor` to repair it, then re-run configure.", ); - if (reset) baseConfig = {}; + runtime.exit(1); + return; } } diff --git a/src/commands/onboard-non-interactive.ts b/src/commands/onboard-non-interactive.ts index 0358dbeb7..58ebd9ec2 100644 --- a/src/commands/onboard-non-interactive.ts +++ b/src/commands/onboard-non-interactive.ts @@ -128,6 +128,13 @@ export async function runNonInteractiveOnboarding( runtime: RuntimeEnv = defaultRuntime, ) { const snapshot = await readConfigFileSnapshot(); + if (snapshot.exists && !snapshot.valid) { + runtime.error( + "Config invalid. Run `clawdbot doctor` to repair it, then re-run onboarding.", + ); + runtime.exit(1); + return; + } const baseConfig: ClawdbotConfig = snapshot.valid ? snapshot.config : {}; const mode = opts.mode ?? "local"; if (mode !== "local" && mode !== "remote") { diff --git a/src/config/io.ts b/src/config/io.ts index b6388acc6..288c6a0a6 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -287,6 +287,10 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { } const legacyIssues = findLegacyConfigIssues(resolved); + const resolvedConfig = + typeof resolved === "object" && resolved !== null + ? (resolved as ClawdbotConfig) + : {}; const validated = validateConfigObject(resolved); if (!validated.ok) { @@ -296,7 +300,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { raw, parsed: parsedRes.parsed, valid: false, - config: resolved as ClawdbotConfig, + config: resolvedConfig, issues: validated.issues, legacyIssues, }; diff --git a/src/wizard/onboarding.test.ts b/src/wizard/onboarding.test.ts index ad39b5c06..1aeacfcf2 100644 --- a/src/wizard/onboarding.test.ts +++ b/src/wizard/onboarding.test.ts @@ -80,6 +80,58 @@ vi.mock("../tui/tui.js", () => ({ })); describe("runOnboardingWizard", () => { + it("exits when config is invalid", async () => { + readConfigFileSnapshot.mockResolvedValueOnce({ + path: "/tmp/.clawdbot/clawdbot.json", + exists: true, + raw: "{}", + parsed: {}, + valid: false, + config: {}, + issues: [{ path: "routing.allowFrom", message: "Legacy key" }], + legacyIssues: [{ path: "routing.allowFrom", message: "Legacy key" }], + }); + + const select: WizardPrompter["select"] = vi.fn(async () => "quickstart"); + const prompter: WizardPrompter = { + intro: vi.fn(async () => {}), + outro: vi.fn(async () => {}), + note: vi.fn(async () => {}), + select, + multiselect: vi.fn(async () => []), + text: vi.fn(async () => ""), + confirm: vi.fn(async () => false), + progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })), + }; + + const runtime: RuntimeEnv = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn((code: number) => { + throw new Error(`exit:${code}`); + }), + }; + + await expect( + runOnboardingWizard( + { + flow: "quickstart", + authChoice: "skip", + installDaemon: false, + skipProviders: true, + skipSkills: true, + skipHealth: true, + skipUi: true, + }, + runtime, + prompter, + ), + ).rejects.toThrow("exit:1"); + + expect(select).not.toHaveBeenCalled(); + expect(prompter.outro).toHaveBeenCalled(); + }); + it("skips prompts and setup steps when flags are set", async () => { const select: WizardPrompter["select"] = vi.fn(async () => "quickstart"); const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []); diff --git a/src/wizard/onboarding.ts b/src/wizard/onboarding.ts index daf3cd4e9..b24022d9d 100644 --- a/src/wizard/onboarding.ts +++ b/src/wizard/onboarding.ts @@ -96,6 +96,14 @@ export async function runOnboardingWizard( ); } + if (!snapshot.valid) { + await prompter.outro( + "Config invalid. Run `clawdbot doctor` to repair it, then re-run onboarding.", + ); + runtime.exit(1); + return; + } + const action = (await prompter.select({ message: "Config handling", options: [ @@ -124,8 +132,6 @@ export async function runOnboardingWizard( })) as ResetScope; await handleReset(resetScope, resolveUserPath(workspaceDefault), runtime); baseConfig = {}; - } else if (action === "keep" && !snapshot.valid) { - baseConfig = {}; } }