From b67e3aa8bce4f23c3eab5b9a4f0eb982120c91af Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 18 Aug 2026 20:55:18 -0700 Subject: [PATCH] fix: stop onboarding ambushing a working install with questions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two defects, both visible as "why is it asking me about AI provider setup when I just started a dev server?". The auto-launch gate probed ~/.fusion/fusion-central.db to decide whether the install was initialized. SQLite central was removed, so a Postgres install never creates that file and the probe was permanently false: onboarding auto-launched on every interactive start of a completely working Fusion, until something happened to stamp the completion marker. The probe now also accepts the embedded Postgres data directory. And auto-launched onboarding ran the full interactive flow. It fires while the operator is starting something else, so its questions interrupt work nobody asked to interrupt — and a dev server stopped on a prompt never listens, which is why `pnpm dev --tunnel` produced no dev server and so no tunnel link. Auto-launch is now non-interactive: create the central database, stamp the marker, point at the dashboard, ask nothing. `fn onboard` still runs every step. Co-Authored-By: Claude Opus 5 --- .../onboarding-autolaunch-non-interactive.md | 7 +++++ .../__tests__/onboard-autolaunch.test.ts | 26 +++++++++++++++++++ .../src/commands/__tests__/onboard.test.ts | 25 ++++++++++++++++++ .../cli/src/commands/onboard-autolaunch.ts | 15 ++++++++--- packages/cli/src/commands/onboard.ts | 18 +++++++++++++ 5 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 .changeset/onboarding-autolaunch-non-interactive.md diff --git a/.changeset/onboarding-autolaunch-non-interactive.md b/.changeset/onboarding-autolaunch-non-interactive.md new file mode 100644 index 0000000000..f542799a8a --- /dev/null +++ b/.changeset/onboarding-autolaunch-non-interactive.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Starting Fusion no longer interrupts you with onboarding questions on a working install. +category: fix +dev: Two defects. (1) `maybeAutoLaunchOnboarding` probed `~/.fusion/fusion-central.db` to decide whether the install was initialized, but SQLite central was removed — a Postgres install never creates that file, so `centralDbExists` was permanently false and onboarding auto-launched on every interactive start until something stamped the completion marker. The probe now also accepts the embedded Postgres data directory. (2) Auto-launched onboarding ran the full interactive flow, so a dashboard or `pnpm dev --tunnel` start could stop dead on "Run ai provider setup now?" and never reach listening. `runOnboard` takes `interactive` (default true); auto-launch passes `false`, which creates the central database, stamps the marker, and points at the dashboard without asking anything. Explicit `fn onboard` keeps every step. diff --git a/packages/cli/src/commands/__tests__/onboard-autolaunch.test.ts b/packages/cli/src/commands/__tests__/onboard-autolaunch.test.ts index 51f887f196..9e418bd3a1 100644 --- a/packages/cli/src/commands/__tests__/onboard-autolaunch.test.ts +++ b/packages/cli/src/commands/__tests__/onboard-autolaunch.test.ts @@ -185,6 +185,32 @@ describe("maybeAutoLaunchOnboarding", () => { }); expect(runOnboard).toHaveBeenCalledTimes(1); + // FNXC:Onboarding 2026-08-19-03:38: auto-launch never interrogates the operator — it fires while + // they are starting something else, and a dev server stopped on a prompt never listens. + expect(runOnboard).toHaveBeenCalledWith({ interactive: false }); + }); + + /* + FNXC:Onboarding 2026-08-19-03:38: + A Postgres install has no `fusion-central.db`; SQLite central was removed. Probing only that path + left centralDbExists permanently false, so onboarding auto-launched on every interactive start of + a fully working Fusion. The embedded Postgres data directory counts as an initialized install. + */ + it("treats an embedded-postgres install as initialized", async () => { + const runOnboard = vi.fn(); + + await maybeAutoLaunchOnboarding({ + command: "task", + args: ["task", "list"], + centralDbPath: "/virtual/fusion-central.db", + isTTY: true, + // No SQLite file, but the Postgres data directory is present. + pathExists: (candidate: string) => candidate === "/virtual/embedded-postgres", + cliOnboardingCompleted: false, + runOnboard, + }); + + expect(runOnboard).not.toHaveBeenCalled(); }); it("does not invoke runOnboard when gate fails", async () => { diff --git a/packages/cli/src/commands/__tests__/onboard.test.ts b/packages/cli/src/commands/__tests__/onboard.test.ts index bd6f8e27cd..bf5e9d2ec4 100644 --- a/packages/cli/src/commands/__tests__/onboard.test.ts +++ b/packages/cli/src/commands/__tests__/onboard.test.ts @@ -279,6 +279,31 @@ describe("onboard", () => { acceptSession.close(); }); + /* + FNXC:Onboarding 2026-08-19-03:38: + Auto-launch fires while the operator is starting something ELSE — a dashboard, a `pnpm dev + --tunnel`. Its questions interrupt work nobody asked to interrupt, and a dev server stopped on + "Run ai provider setup now?" never listens, so nothing is served and no tunnel can point at it. + The non-interactive path prepares the install and asks nothing. + */ + it("asks nothing when not interactive, but still prepares the install", async () => { + const providerAuth = makeProviderAuth(); + mockProviderAuthFactory.mockReturnValue(providerAuth); + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + // No input at all: any prompt would hang or cancel rather than pass. + await runOnboard({ interactive: false }); + + expect(centralInitMock).toHaveBeenCalled(); + expect(typeof globalSettingsState.cliOnboardingCompletedAt).toBe("string"); + // The interactive-only steps must not have run. + expect(mockRunInit).not.toHaveBeenCalled(); + expect(providerAuth.setApiKey).not.toHaveBeenCalled(); + const printed = logSpy.mock.calls.map((call) => String(call[0])).join("\n"); + expect(printed).not.toMatch(/Run ai provider setup now/i); + expect(printed).toMatch(/dashboard|fn onboard/i); + }); + it("allows fully skipping onboarding steps while still persisting completion marker", async () => { const providerAuth = makeProviderAuth(); mockProviderAuthFactory.mockReturnValue(providerAuth); diff --git a/packages/cli/src/commands/onboard-autolaunch.ts b/packages/cli/src/commands/onboard-autolaunch.ts index 9f28edbcc4..135092911d 100644 --- a/packages/cli/src/commands/onboard-autolaunch.ts +++ b/packages/cli/src/commands/onboard-autolaunch.ts @@ -1,5 +1,5 @@ import { existsSync } from "node:fs"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; import { getDefaultCentralDbPath, GlobalSettingsStore } from "@fusion/core"; import { isTTYAvailable } from "./dashboard-tui/index.js"; @@ -132,7 +132,14 @@ export async function maybeAutoLaunchOnboarding(deps: MaybeAutoLaunchDeps): Prom const cwd = deps.cwd ?? process.cwd(); const projectMarkerPath = join(cwd, ".fusion", "project.json"); const legacyProjectDbPath = join(cwd, ".fusion", "fusion.db"); - centralDbExists = pathExists(centralDbPath); + /* + FNXC:Onboarding 2026-08-19-03:38: + The install is initialized if EITHER the legacy SQLite central file exists or the embedded + Postgres data directory does. Probing only the SQLite path made this permanently false on every + Postgres install — SQLite central was removed — so onboarding auto-launched on each interactive + start of a fully working Fusion until something happened to stamp the completion marker. + */ + centralDbExists = pathExists(centralDbPath) || pathExists(join(dirname(centralDbPath), "embedded-postgres")); // FNXC:ProjectIdentityMarker 2026-07-14-17:20: Onboarding probes the new // marker first and recognizes fusion.db only as a pre-cutover project signal. projectInitialized = deps.projectInitialized @@ -171,7 +178,9 @@ export async function maybeAutoLaunchOnboarding(deps: MaybeAutoLaunchDeps): Prom try { const runOnboard = deps.runOnboard ?? (await import("./onboard.js")).runOnboard; - await runOnboard(); + // FNXC:Onboarding 2026-08-19-03:38: auto-launch never interrogates the operator; see + // OnboardOptions.interactive. + await runOnboard({ interactive: false }); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(`[onboard-autolaunch] non-fatal onboard launch failure: ${message}`); diff --git a/packages/cli/src/commands/onboard.ts b/packages/cli/src/commands/onboard.ts index 7919b45c87..7a85130b0f 100644 --- a/packages/cli/src/commands/onboard.ts +++ b/packages/cli/src/commands/onboard.ts @@ -12,6 +12,15 @@ import { getModelRegistryModelsPath } from "./auth-paths.js"; export interface OnboardOptions { force?: boolean; input?: NodeJS.ReadableStream; + /* + FNXC:Onboarding 2026-08-19-03:38: + False for the AUTO-LAUNCHED path (see onboard-autolaunch): prepare the install and stamp the + marker, but ask nothing. Auto-launch fires while the operator is starting something else — a + dashboard, a `pnpm dev --tunnel` — so its questions (AI provider setup, project setup, core + settings) interrupt work nobody asked to interrupt, and a dev server stopped on a prompt never + listens at all. `fn onboard` is the command for the interactive flow and keeps every step. + */ + interactive?: boolean; } const PROMPT_CANCELLED_ERROR = "Interactive prompt cancelled"; @@ -274,6 +283,7 @@ export async function runOnboard(options: OnboardOptions = {}): Promise { return; } + const interactive = options.interactive !== false; const prompts = createPromptSession(options.input); try { @@ -297,6 +307,14 @@ export async function runOnboard(options: OnboardOptions = {}): Promise { console.log("✓ Central DB initialized"); } + if (!interactive) { + await globalSettingsStore.updateSettings({ + cliOnboardingCompletedAt: new Date().toISOString(), + }); + console.log("\nConnect a provider and finish setup in the dashboard, or run `fn onboard` anytime."); + return; + } + const authStorage = createFusionAuthStorage(); const modelRegistry = await createFusionModelRegistry(authStorage); const providerAuth = wrapAuthStorageWithApiKeyProviders(authStorage, modelRegistry);