diff --git a/CHANGELOG.md b/CHANGELOG.md index c169107d3..8a6244a9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ - Heartbeat: tighten prompt guidance + suppress duplicate alerts for 24h. (#980) — thanks @voidserf. - Plugins: add provider auth registry + `clawdbot models auth login` for plugin-driven OAuth/API key flows. +- Onboarding: prompt to modify/disable/delete when reconfiguring existing channel accounts and keep channel selection looping until Finished. - Fix: list model picker entries as provider/model pairs for explicit selection. (#970) — thanks @mcinteerj. +- Fix: persist `gateway.mode=local` after selecting Local run mode in `clawdbot configure`, even if no other sections are chosen. - Daemon: fix profile-aware service label resolution (env-driven) and add coverage for launchd/systemd/schtasks. (#969) — thanks @bjesuiter. - Daemon: share profile/state-dir resolution across service helpers and honor `CLAWDBOT_STATE_DIR` for Windows task scripts. - Docs: clarify multi-gateway rescue bot guidance. (#969) — thanks @bjesuiter. diff --git a/docs/cli/configure.md b/docs/cli/configure.md index 3b882f2c7..9679a5e4b 100644 --- a/docs/cli/configure.md +++ b/docs/cli/configure.md @@ -11,10 +11,12 @@ Interactive prompt to set up credentials, devices, and agent defaults. Related: - Gateway configuration reference: [Configuration](/gateway/configuration) +Notes: +- Choosing where the Gateway runs always updates `gateway.mode`. You can select "Continue" without other sections if that is all you need. + ## Examples ```bash clawdbot configure clawdbot configure --section models --section channels ``` - diff --git a/src/commands/configure.wizard.test.ts b/src/commands/configure.wizard.test.ts new file mode 100644 index 000000000..e416c0644 --- /dev/null +++ b/src/commands/configure.wizard.test.ts @@ -0,0 +1,134 @@ +import { describe, expect, it, vi } from "vitest"; + +import type { ClawdbotConfig } from "../config/config.js"; + +const mocks = vi.hoisted(() => ({ + clackIntro: vi.fn(), + clackOutro: vi.fn(), + clackSelect: vi.fn(), + clackText: vi.fn(), + clackConfirm: vi.fn(), + readConfigFileSnapshot: vi.fn(), + writeConfigFile: vi.fn(), + resolveGatewayPort: vi.fn(), + ensureControlUiAssetsBuilt: vi.fn(), + createClackPrompter: vi.fn(), + note: vi.fn(), + printWizardHeader: vi.fn(), + probeGatewayReachable: vi.fn(), + waitForGatewayReachable: vi.fn(), + resolveControlUiLinks: vi.fn(), + summarizeExistingConfig: vi.fn(), +})); + +vi.mock("@clack/prompts", () => ({ + intro: mocks.clackIntro, + outro: mocks.clackOutro, + select: mocks.clackSelect, + text: mocks.clackText, + confirm: mocks.clackConfirm, +})); + +vi.mock("../config/config.js", () => ({ + CONFIG_PATH_CLAWDBOT: "~/.clawdbot/clawdbot.json", + readConfigFileSnapshot: mocks.readConfigFileSnapshot, + writeConfigFile: mocks.writeConfigFile, + resolveGatewayPort: mocks.resolveGatewayPort, +})); + +vi.mock("../infra/control-ui-assets.js", () => ({ + ensureControlUiAssetsBuilt: mocks.ensureControlUiAssetsBuilt, +})); + +vi.mock("../wizard/clack-prompter.js", () => ({ + createClackPrompter: mocks.createClackPrompter, +})); + +vi.mock("../terminal/note.js", () => ({ + note: mocks.note, +})); + +vi.mock("./onboard-helpers.js", () => ({ + DEFAULT_WORKSPACE: "~/.clawdbot/workspace", + applyWizardMetadata: (cfg: ClawdbotConfig) => cfg, + ensureWorkspaceAndSessions: vi.fn(), + guardCancel: (value: T) => value, + printWizardHeader: mocks.printWizardHeader, + probeGatewayReachable: mocks.probeGatewayReachable, + resolveControlUiLinks: mocks.resolveControlUiLinks, + summarizeExistingConfig: mocks.summarizeExistingConfig, + waitForGatewayReachable: mocks.waitForGatewayReachable, +})); + +vi.mock("./health.js", () => ({ + healthCommand: vi.fn(), +})); + +vi.mock("./health-format.js", () => ({ + formatHealthCheckFailure: vi.fn(), +})); + +vi.mock("./configure.gateway.js", () => ({ + promptGatewayConfig: vi.fn(), +})); + +vi.mock("./configure.gateway-auth.js", () => ({ + promptAuthConfig: vi.fn(), +})); + +vi.mock("./configure.channels.js", () => ({ + removeChannelConfigWizard: vi.fn(), +})); + +vi.mock("./configure.daemon.js", () => ({ + maybeInstallDaemon: vi.fn(), +})); + +vi.mock("./onboard-remote.js", () => ({ + promptRemoteGatewayConfig: vi.fn(), +})); + +vi.mock("./onboard-skills.js", () => ({ + setupSkills: vi.fn(), +})); + +vi.mock("./onboard-channels.js", () => ({ + setupChannels: vi.fn(), +})); + +import { runConfigureWizard } from "./configure.wizard.js"; + +describe("runConfigureWizard", () => { + it("persists gateway.mode=local when only the run mode is selected", async () => { + mocks.readConfigFileSnapshot.mockResolvedValue({ + exists: false, + valid: true, + config: {}, + issues: [], + }); + mocks.resolveGatewayPort.mockReturnValue(18789); + mocks.probeGatewayReachable.mockResolvedValue({ ok: false }); + mocks.resolveControlUiLinks.mockReturnValue({ wsUrl: "ws://127.0.0.1:18789" }); + mocks.summarizeExistingConfig.mockReturnValue(""); + mocks.createClackPrompter.mockReturnValue({}); + + const selectQueue = ["local", "__continue"]; + mocks.clackSelect.mockImplementation(async () => selectQueue.shift()); + mocks.clackIntro.mockResolvedValue(undefined); + mocks.clackOutro.mockResolvedValue(undefined); + mocks.clackText.mockResolvedValue(""); + mocks.clackConfirm.mockResolvedValue(false); + + await runConfigureWizard({ command: "configure" }, { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }); + + expect(mocks.writeConfigFile).toHaveBeenCalledWith( + expect.objectContaining({ + gateway: expect.objectContaining({ mode: "local" }), + }), + ); + }); +}); diff --git a/src/commands/configure.wizard.ts b/src/commands/configure.wizard.ts index 204e2e5f6..77c9fadf2 100644 --- a/src/commands/configure.wizard.ts +++ b/src/commands/configure.wizard.ts @@ -256,6 +256,17 @@ export async function runConfigureWizard( } let nextConfig = { ...baseConfig }; + let didSetGatewayMode = false; + if (nextConfig.gateway?.mode !== "local") { + nextConfig = { + ...nextConfig, + gateway: { + ...nextConfig.gateway, + mode: "local", + }, + }; + didSetGatewayMode = true; + } let workspaceDir = nextConfig.agents?.defaults?.workspace ?? baseConfig.agents?.defaults?.workspace ?? @@ -512,6 +523,11 @@ export async function runConfigureWizard( } if (!ranSection) { + if (didSetGatewayMode) { + await persistConfig(); + outro("Gateway mode set to local."); + return; + } outro("No changes selected."); return; }