FN-5847: honor onboarding completion marker in auto-launch
CLI onboarding auto-launch now skips once the persisted completion marker is present. - Load `cliOnboardingCompletedAt` from global settings before launching onboarding when the central DB is missing. - Preserve existing skip precedence for non-TTY, command, flag, environment, and central DB guards. - Add unit and backcompat coverage for completion-marker and skipped-central-DB scenarios. - Document the updated auto-launch behavior and add a patch changeset. Files changed: .changeset/fn-5847-onboard-autolaunch-marker.md | 5 ++ docs/cli-reference.md | 12 +-- .../onboard-autolaunch-backcompat-e2e.test.ts | 7 ++ .../onboard-autolaunch-backcompat.test.ts | 8 ++ .../__tests__/onboard-autolaunch-bypass.test.ts | 7 ++ .../commands/__tests__/onboard-autolaunch.test.ts | 93 ++++++++++++++++++++++ packages/cli/src/commands/onboard-autolaunch.ts | 46 ++++++++++- 7 files changed, 171 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-5847 Fusion-Task-Lineage: db3e474e-e4b8-4299-8087-07b6cd1f4518
This commit is contained in:
5
.changeset/fn-5847-onboard-autolaunch-marker.md
Normal file
5
.changeset/fn-5847-onboard-autolaunch-marker.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
CLI auto-launch now honors the persisted `cliOnboardingCompletedAt` marker so onboarding fires only once, even when the Central DB step was skipped during `fn onboard`.
|
||||
@@ -72,13 +72,15 @@ On successful completion, Fusion records `cliOnboardingCompletedAt` in global
|
||||
settings.
|
||||
|
||||
Auto-launch behavior: before interactive commands, Fusion auto-launches onboarding
|
||||
only when the central DB at `getDefaultCentralDbPath()` is missing. Auto-launch
|
||||
is skipped for `serve`, `daemon`, non-TTY runs, `--skip-onboarding`, and
|
||||
`FUSION_SKIP_ONBOARDING`.
|
||||
only when the central DB at `getDefaultCentralDbPath()` is missing and CLI
|
||||
onboarding has not already completed. Auto-launch is skipped for `serve`,
|
||||
`daemon`, non-TTY runs, `--skip-onboarding`, `FUSION_SKIP_ONBOARDING`, and once
|
||||
`cliOnboardingCompletedAt` is set.
|
||||
|
||||
Backward-compatibility guard: existing setups are never blocked — when central DB
|
||||
already exists (including the central-DB + registered-project case), onboarding
|
||||
does not auto-launch.
|
||||
already exists (including the central-DB + registered-project case), or when the
|
||||
CLI onboarding completion marker exists even if the central DB step was skipped,
|
||||
onboarding does not auto-launch.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ describe("maybeAutoLaunchOnboarding backward-compat e2e guard", () => {
|
||||
cwd: "/repo/demo",
|
||||
isTTY: true,
|
||||
pathExists,
|
||||
cliOnboardingCompleted: false,
|
||||
runOnboard,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
@@ -38,6 +39,7 @@ describe("maybeAutoLaunchOnboarding backward-compat e2e guard", () => {
|
||||
centralDbPath: "/virtual/central.db",
|
||||
isTTY: false,
|
||||
pathExists: () => false,
|
||||
cliOnboardingCompleted: false,
|
||||
runOnboard,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
@@ -74,6 +76,7 @@ describe("maybeAutoLaunchOnboarding backward-compat e2e guard", () => {
|
||||
centralDbPath: "/virtual/central.db",
|
||||
isTTY: true,
|
||||
pathExists: () => false,
|
||||
cliOnboardingCompleted: false,
|
||||
runOnboard,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
@@ -92,6 +95,7 @@ describe("maybeAutoLaunchOnboarding backward-compat e2e guard", () => {
|
||||
isTTY: true,
|
||||
env: { FUSION_SKIP_ONBOARDING: "true" },
|
||||
pathExists: () => false,
|
||||
cliOnboardingCompleted: false,
|
||||
runOnboard,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
@@ -110,6 +114,7 @@ describe("maybeAutoLaunchOnboarding backward-compat e2e guard", () => {
|
||||
centralDbPath: "/virtual/central.db",
|
||||
isTTY: true,
|
||||
pathExists: () => false,
|
||||
cliOnboardingCompleted: false,
|
||||
runOnboard,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
@@ -130,6 +135,7 @@ describe("maybeAutoLaunchOnboarding backward-compat e2e guard", () => {
|
||||
centralDbPath: "/virtual/central.db",
|
||||
isTTY: true,
|
||||
pathExists: () => false,
|
||||
cliOnboardingCompleted: false,
|
||||
runOnboard,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
@@ -151,6 +157,7 @@ describe("maybeAutoLaunchOnboarding backward-compat e2e guard", () => {
|
||||
cwd: "/workspace/demo",
|
||||
isTTY: true,
|
||||
pathExists,
|
||||
cliOnboardingCompleted: false,
|
||||
runOnboard,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
@@ -17,6 +17,7 @@ describe("onboard autolaunch backward-compat guard", () => {
|
||||
args: ["task", "list"],
|
||||
centralDbExists: true,
|
||||
projectInitialized: true,
|
||||
cliOnboardingCompleted: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "central-db-and-project-exist" });
|
||||
@@ -29,6 +30,7 @@ describe("onboard autolaunch backward-compat guard", () => {
|
||||
args: ["task", "list"],
|
||||
centralDbExists: true,
|
||||
projectInitialized: false,
|
||||
cliOnboardingCompleted: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "central-db-exists" });
|
||||
@@ -41,6 +43,7 @@ describe("onboard autolaunch backward-compat guard", () => {
|
||||
args: ["task", "list"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: true,
|
||||
cliOnboardingCompleted: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: true, reason: "central-db-missing" });
|
||||
@@ -53,6 +56,7 @@ describe("onboard autolaunch backward-compat guard", () => {
|
||||
args: ["task", "list"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
cliOnboardingCompleted: false,
|
||||
isTTY: false,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "non-tty" });
|
||||
@@ -66,6 +70,7 @@ describe("onboard autolaunch backward-compat guard", () => {
|
||||
args: [command],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
cliOnboardingCompleted: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "command-skip" });
|
||||
@@ -85,6 +90,7 @@ describe("onboard autolaunch backward-compat guard", () => {
|
||||
args: ["task", "list"],
|
||||
centralDbPath: "/virtual/central.db",
|
||||
pathExists: () => false,
|
||||
cliOnboardingCompleted: false,
|
||||
runOnboard,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
@@ -104,6 +110,7 @@ describe("onboard autolaunch backward-compat guard", () => {
|
||||
cwd: "/workspace/demo",
|
||||
isTTY: true,
|
||||
pathExists,
|
||||
cliOnboardingCompleted: false,
|
||||
runOnboard,
|
||||
});
|
||||
|
||||
@@ -123,6 +130,7 @@ describe("onboard autolaunch backward-compat guard", () => {
|
||||
centralDbPath: "/virtual/central.db",
|
||||
isTTY: true,
|
||||
pathExists: () => false,
|
||||
cliOnboardingCompleted: false,
|
||||
runOnboard,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
@@ -20,6 +20,7 @@ describe("onboard bypass reasons", () => {
|
||||
args: ["task", "list", "--skip-onboarding"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
cliOnboardingCompleted: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "skip-flag" });
|
||||
@@ -30,6 +31,7 @@ describe("onboard bypass reasons", () => {
|
||||
args: ["task", "list", "--skip-onboarding"],
|
||||
isTTY: true,
|
||||
pathExists: () => false,
|
||||
cliOnboardingCompleted: false,
|
||||
runOnboard,
|
||||
});
|
||||
expect(runOnboard).not.toHaveBeenCalled();
|
||||
@@ -42,6 +44,7 @@ describe("onboard bypass reasons", () => {
|
||||
args: ["task", "list"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
cliOnboardingCompleted: false,
|
||||
isTTY: true,
|
||||
env: { FUSION_SKIP_ONBOARDING: "1" },
|
||||
}),
|
||||
@@ -54,6 +57,7 @@ describe("onboard bypass reasons", () => {
|
||||
env: { FUSION_SKIP_ONBOARDING: "1" },
|
||||
isTTY: true,
|
||||
pathExists: () => false,
|
||||
cliOnboardingCompleted: false,
|
||||
runOnboard,
|
||||
});
|
||||
expect(runOnboard).not.toHaveBeenCalled();
|
||||
@@ -76,6 +80,7 @@ describe("onboard bypass reasons", () => {
|
||||
args: ["task", "list"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
cliOnboardingCompleted: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: true, reason: "central-db-missing" });
|
||||
@@ -122,6 +127,7 @@ describe("extractGlobalProjectFlag", () => {
|
||||
skipOnboarding: parsed.skipOnboarding,
|
||||
isTTY: true,
|
||||
pathExists: () => false,
|
||||
cliOnboardingCompleted: false,
|
||||
runOnboard,
|
||||
});
|
||||
|
||||
@@ -132,6 +138,7 @@ describe("extractGlobalProjectFlag", () => {
|
||||
skipOnboarding: parsed.skipOnboarding,
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
cliOnboardingCompleted: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "skip-flag" });
|
||||
|
||||
@@ -13,6 +13,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
||||
args: ["task", "list"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
cliOnboardingCompleted: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: true, reason: "central-db-missing" });
|
||||
@@ -25,11 +26,53 @@ describe("shouldAutoLaunchOnboarding", () => {
|
||||
args: ["dashboard"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
cliOnboardingCompleted: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: true, reason: "central-db-missing" });
|
||||
});
|
||||
|
||||
it("skips when onboarding completion marker is present", () => {
|
||||
expect(
|
||||
shouldAutoLaunchOnboarding({
|
||||
command: "task",
|
||||
args: ["task", "list"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
cliOnboardingCompleted: true,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "onboarding-complete-marker" });
|
||||
});
|
||||
|
||||
it("keeps non-TTY precedence over onboarding completion marker", () => {
|
||||
expect(
|
||||
shouldAutoLaunchOnboarding({
|
||||
command: "task",
|
||||
args: ["task", "list"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
cliOnboardingCompleted: true,
|
||||
isTTY: false,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "non-tty" });
|
||||
});
|
||||
|
||||
it("keeps command skip precedence over onboarding completion marker", () => {
|
||||
for (const command of ["serve", "daemon"]) {
|
||||
expect(
|
||||
shouldAutoLaunchOnboarding({
|
||||
command,
|
||||
args: [command],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
cliOnboardingCompleted: true,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "command-skip" });
|
||||
}
|
||||
});
|
||||
|
||||
it("skips when skip flag is present in args", () => {
|
||||
const args = ["task", "list", "--skip-onboarding"];
|
||||
expect(args).toContain("--skip-onboarding");
|
||||
@@ -39,6 +82,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
||||
args,
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
cliOnboardingCompleted: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "skip-flag" });
|
||||
@@ -51,6 +95,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
||||
args: ["task", "list"],
|
||||
centralDbExists: true,
|
||||
projectInitialized: false,
|
||||
cliOnboardingCompleted: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "central-db-exists" });
|
||||
@@ -63,6 +108,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
||||
args: ["task", "list"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
cliOnboardingCompleted: false,
|
||||
isTTY: false,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "non-tty" });
|
||||
@@ -75,6 +121,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
||||
args: ["serve"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
cliOnboardingCompleted: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "command-skip" });
|
||||
@@ -85,6 +132,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
||||
args: ["daemon"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
cliOnboardingCompleted: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "command-skip" });
|
||||
@@ -97,6 +145,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
||||
args: ["onboard"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
cliOnboardingCompleted: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "onboard-command" });
|
||||
@@ -109,6 +158,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
||||
args: ["task", "list"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
cliOnboardingCompleted: false,
|
||||
isTTY: true,
|
||||
env: { FUSION_SKIP_ONBOARDING: "1" },
|
||||
}),
|
||||
@@ -130,6 +180,7 @@ describe("maybeAutoLaunchOnboarding", () => {
|
||||
centralDbPath: "/virtual/fusion-central.db",
|
||||
isTTY: true,
|
||||
pathExists: () => false,
|
||||
cliOnboardingCompleted: false,
|
||||
runOnboard,
|
||||
});
|
||||
|
||||
@@ -145,12 +196,53 @@ describe("maybeAutoLaunchOnboarding", () => {
|
||||
centralDbPath: "/virtual/fusion-central.db",
|
||||
isTTY: true,
|
||||
pathExists: () => true,
|
||||
cliOnboardingCompleted: false,
|
||||
runOnboard,
|
||||
});
|
||||
|
||||
expect(runOnboard).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not invoke runOnboard when injected marker is complete", async () => {
|
||||
const runOnboard = vi.fn();
|
||||
|
||||
await maybeAutoLaunchOnboarding({
|
||||
command: "task",
|
||||
args: ["task", "list"],
|
||||
centralDbPath: "/virtual/fusion-central.db",
|
||||
isTTY: true,
|
||||
pathExists: () => false,
|
||||
loadOnboardingComplete: () => true,
|
||||
runOnboard,
|
||||
});
|
||||
|
||||
expect(runOnboard).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("treats marker resolver failure as incomplete and emits diagnostic", async () => {
|
||||
const runOnboard = vi.fn();
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
|
||||
await maybeAutoLaunchOnboarding({
|
||||
command: "task",
|
||||
args: ["task", "list"],
|
||||
centralDbPath: "/virtual/fusion-central.db",
|
||||
isTTY: true,
|
||||
pathExists: () => false,
|
||||
loadOnboardingComplete: () => {
|
||||
throw new Error("settings unavailable");
|
||||
},
|
||||
runOnboard,
|
||||
});
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"[onboard-autolaunch] onboarding marker probe failed; treating as incomplete: settings unavailable",
|
||||
),
|
||||
);
|
||||
expect(runOnboard).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("swallows runOnboard errors and emits diagnostic", async () => {
|
||||
const runOnboard = vi.fn().mockRejectedValue(new Error("boom"));
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
@@ -162,6 +254,7 @@ describe("maybeAutoLaunchOnboarding", () => {
|
||||
centralDbPath: "/virtual/fusion-central.db",
|
||||
isTTY: true,
|
||||
pathExists: () => false,
|
||||
cliOnboardingCompleted: false,
|
||||
runOnboard,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { getDefaultCentralDbPath } from "@fusion/core";
|
||||
import { getDefaultCentralDbPath, GlobalSettingsStore } from "@fusion/core";
|
||||
|
||||
import { isTTYAvailable } from "./dashboard-tui/index.js";
|
||||
import { isCliOnboardingComplete } from "./onboard.js";
|
||||
|
||||
export interface AutoLaunchInput {
|
||||
command: string;
|
||||
args: string[];
|
||||
centralDbExists: boolean;
|
||||
projectInitialized: boolean;
|
||||
cliOnboardingCompleted: boolean;
|
||||
isTTY: boolean;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
skipOnboarding?: boolean;
|
||||
@@ -51,6 +53,10 @@ export function shouldAutoLaunchOnboarding(input: AutoLaunchInput): AutoLaunchDe
|
||||
return { launch: false, reason: "skip-env" };
|
||||
}
|
||||
|
||||
if (input.cliOnboardingCompleted) {
|
||||
return { launch: false, reason: "onboarding-complete-marker" };
|
||||
}
|
||||
|
||||
if (input.centralDbExists && input.projectInitialized) {
|
||||
return { launch: false, reason: "central-db-and-project-exist" };
|
||||
}
|
||||
@@ -88,6 +94,30 @@ export interface MaybeAutoLaunchDeps {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
runOnboard?: RunOnboard;
|
||||
pathExists?: (path: string) => boolean;
|
||||
cliOnboardingCompleted?: boolean;
|
||||
loadOnboardingComplete?: () => Promise<boolean> | boolean;
|
||||
}
|
||||
|
||||
async function loadCliOnboardingComplete(): Promise<boolean> {
|
||||
const globalSettingsStore = new GlobalSettingsStore();
|
||||
await globalSettingsStore.init();
|
||||
const settings = await globalSettingsStore.getSettings();
|
||||
return isCliOnboardingComplete(settings);
|
||||
}
|
||||
|
||||
async function resolveCliOnboardingCompleted(deps: MaybeAutoLaunchDeps): Promise<boolean> {
|
||||
if (deps.cliOnboardingCompleted !== undefined) {
|
||||
return deps.cliOnboardingCompleted;
|
||||
}
|
||||
|
||||
const loadOnboardingComplete = deps.loadOnboardingComplete ?? loadCliOnboardingComplete;
|
||||
try {
|
||||
return await loadOnboardingComplete();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[onboard-autolaunch] onboarding marker probe failed; treating as incomplete: ${message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function maybeAutoLaunchOnboarding(deps: MaybeAutoLaunchDeps): Promise<void> {
|
||||
@@ -109,14 +139,26 @@ export async function maybeAutoLaunchOnboarding(deps: MaybeAutoLaunchDeps): Prom
|
||||
return;
|
||||
}
|
||||
|
||||
const decision = shouldAutoLaunchOnboarding({
|
||||
const baseDecisionInput = {
|
||||
command: deps.command,
|
||||
args: deps.args,
|
||||
centralDbExists,
|
||||
projectInitialized,
|
||||
cliOnboardingCompleted: false,
|
||||
isTTY,
|
||||
env,
|
||||
skipOnboarding: deps.skipOnboarding,
|
||||
};
|
||||
|
||||
const baseDecision = shouldAutoLaunchOnboarding(baseDecisionInput);
|
||||
if (!baseDecision.launch) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cliOnboardingCompleted = await resolveCliOnboardingCompleted(deps);
|
||||
const decision = shouldAutoLaunchOnboarding({
|
||||
...baseDecisionInput,
|
||||
cliOnboardingCompleted,
|
||||
});
|
||||
|
||||
if (!decision.launch) {
|
||||
|
||||
Reference in New Issue
Block a user