fix: stop onboarding ambushing a working install with questions
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 <noreply@anthropic.com>
This commit is contained in:
7
.changeset/onboarding-autolaunch-non-interactive.md
Normal file
7
.changeset/onboarding-autolaunch-non-interactive.md
Normal file
@@ -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.
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const interactive = options.interactive !== false;
|
||||
const prompts = createPromptSession(options.input);
|
||||
|
||||
try {
|
||||
@@ -297,6 +307,14 @@ export async function runOnboard(options: OnboardOptions = {}): Promise<void> {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user