diff --git a/src/wizard/onboarding.finalize.ts b/src/wizard/onboarding.finalize.ts index 2ef87f73f..ed9ce580d 100644 --- a/src/wizard/onboarding.finalize.ts +++ b/src/wizard/onboarding.finalize.ts @@ -299,99 +299,83 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption ].join("\n"), "Start TUI (best option!)", ); - await prompter.note( - [ - "Gateway token: shared auth for the Gateway + Control UI.", - "Stored in: ~/.clawdbot/clawdbot.json (gateway.auth.token) or CLAWDBOT_GATEWAY_TOKEN.", - "Web UI stores a copy in this browser's localStorage (clawdbot.control.settings.v1).", - `Get the tokenized link anytime: ${formatCliCommand("clawdbot dashboard --no-open")}`, - ].join("\n"), - "Token", - ); + } - hatchChoice = (await prompter.select({ - message: "How do you want to hatch your bot?", - options: [ - { value: "tui", label: "Hatch in TUI (recommended)" }, - { value: "web", label: "Open the Web UI" }, - { value: "later", label: "Do this later" }, - ], - initialValue: "tui", - })) as "tui" | "web" | "later"; + await prompter.note( + [ + "Gateway token: shared auth for the Gateway + Control UI.", + "Stored in: ~/.clawdbot/clawdbot.json (gateway.auth.token) or CLAWDBOT_GATEWAY_TOKEN.", + "Web UI stores a copy in this browser's localStorage (clawdbot.control.settings.v1).", + `Get the tokenized link anytime: ${formatCliCommand("clawdbot dashboard --no-open")}`, + ].join("\n"), + "Token", + ); - if (hatchChoice === "tui") { - await runTui({ - url: links.wsUrl, - token: settings.authMode === "token" ? settings.gatewayToken : undefined, - password: settings.authMode === "password" ? nextConfig.gateway?.auth?.password : "", - // Safety: onboarding TUI should not auto-deliver to lastProvider/lastTo. - deliver: false, - message: "Wake up, my friend!", - }); - if (settings.authMode === "token" && settings.gatewayToken) { - seededInBackground = await openUrlInBackground(authedUrl); - } - if (seededInBackground) { - await prompter.note( - `Web UI seeded in the background. Open later with: ${formatCliCommand( - "clawdbot dashboard --no-open", - )}`, - "Web UI", - ); - } - } else if (hatchChoice === "web") { - const browserSupport = await detectBrowserOpenSupport(); - if (browserSupport.ok) { - controlUiOpened = await openUrl(authedUrl); - if (!controlUiOpened) { - controlUiOpenHint = formatControlUiSshHint({ - port: settings.port, - basePath: controlUiBasePath, - token: settings.gatewayToken, - }); - } - } else { + hatchChoice = (await prompter.select({ + message: "How do you want to hatch your bot?", + options: [ + { value: "tui", label: "Hatch in TUI (recommended)" }, + { value: "web", label: "Open the Web UI" }, + { value: "later", label: "Do this later" }, + ], + initialValue: "tui", + })) as "tui" | "web" | "later"; + + if (hatchChoice === "tui") { + await runTui({ + url: links.wsUrl, + token: settings.authMode === "token" ? settings.gatewayToken : undefined, + password: settings.authMode === "password" ? nextConfig.gateway?.auth?.password : "", + // Safety: onboarding TUI should not auto-deliver to lastProvider/lastTo. + deliver: false, + message: hasBootstrap ? "Wake up, my friend!" : undefined, + }); + if (settings.authMode === "token" && settings.gatewayToken) { + seededInBackground = await openUrlInBackground(authedUrl); + } + if (seededInBackground) { + await prompter.note( + `Web UI seeded in the background. Open later with: ${formatCliCommand( + "clawdbot dashboard --no-open", + )}`, + "Web UI", + ); + } + } else if (hatchChoice === "web") { + const browserSupport = await detectBrowserOpenSupport(); + if (browserSupport.ok) { + controlUiOpened = await openUrl(authedUrl); + if (!controlUiOpened) { controlUiOpenHint = formatControlUiSshHint({ port: settings.port, basePath: controlUiBasePath, token: settings.gatewayToken, }); } - await prompter.note( - [ - `Dashboard link (with token): ${authedUrl}`, - controlUiOpened - ? "Opened in your browser. Keep that tab to control Clawdbot." - : "Copy/paste this URL in a browser on this machine to control Clawdbot.", - controlUiOpenHint, - ] - .filter(Boolean) - .join("\n"), - "Dashboard ready", - ); } else { - await prompter.note( - `When you're ready: ${formatCliCommand("clawdbot dashboard --no-open")}`, - "Later", - ); + controlUiOpenHint = formatControlUiSshHint({ + port: settings.port, + basePath: controlUiBasePath, + token: settings.gatewayToken, + }); } + await prompter.note( + [ + `Dashboard link (with token): ${authedUrl}`, + controlUiOpened + ? "Opened in your browser. Keep that tab to control Clawdbot." + : "Copy/paste this URL in a browser on this machine to control Clawdbot.", + controlUiOpenHint, + ] + .filter(Boolean) + .join("\n"), + "Dashboard ready", + ); } else { - const browserSupport = await detectBrowserOpenSupport(); - if (!browserSupport.ok) { - await prompter.note( - formatControlUiSshHint({ - port: settings.port, - basePath: controlUiBasePath, - token: settings.authMode === "token" ? settings.gatewayToken : undefined, - }), - "Open Control UI", - ); - } else { - await prompter.note( - "Opening Control UI automatically after onboarding (no extra prompts).", - "Open Control UI", - ); - } + await prompter.note( + `When you're ready: ${formatCliCommand("clawdbot dashboard --no-open")}`, + "Later", + ); } } else if (opts.skipUi) { await prompter.note("Skipping Control UI/TUI prompts.", "Control UI"); diff --git a/src/wizard/onboarding.test.ts b/src/wizard/onboarding.test.ts index 4cbae643f..23e4a6018 100644 --- a/src/wizard/onboarding.test.ts +++ b/src/wizard/onboarding.test.ts @@ -227,6 +227,61 @@ describe("runOnboardingWizard", () => { await fs.rm(workspaceDir, { recursive: true, force: true }); }); + it("offers TUI hatch even without BOOTSTRAP.md", async () => { + runTui.mockClear(); + + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-onboard-")); + + const select: WizardPrompter["select"] = vi.fn(async (opts) => { + if (opts.message === "How do you want to hatch your bot?") return "tui"; + return "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 runOnboardingWizard( + { + acceptRisk: true, + flow: "quickstart", + mode: "local", + workspace: workspaceDir, + authChoice: "skip", + skipProviders: true, + skipSkills: true, + skipHealth: true, + installDaemon: false, + }, + runtime, + prompter, + ); + + expect(runTui).toHaveBeenCalledWith( + expect.objectContaining({ + deliver: false, + message: undefined, + }), + ); + + await fs.rm(workspaceDir, { recursive: true, force: true }); + }); + it("shows the web search hint at the end of onboarding", async () => { const prevBraveKey = process.env.BRAVE_API_KEY; delete process.env.BRAVE_API_KEY;