FN-5809: add onboarding skip-flag and env bypass handling
Ensure CLI onboarding auto-launch is bypassed cleanly via explicit flag/env controls. - Parse and strip global `--skip-onboarding` in CLI argument preprocessing while surfacing a dedicated `skipOnboarding` signal. - Pass the surfaced skip signal into onboarding auto-launch decisions and preserve distinct reasons (`skip-flag` vs `skip-env`). - Tighten `FUSION_SKIP_ONBOARDING` parsing to strict truthy values only (`1`, `true`, `yes`, `on`). - Add focused tests covering bypass reasons, truthy env parsing, global flag stripping behavior, and integration with onboarding gating. - Add a patch changeset for @runfusion/fusion documenting the onboarding bypass behavior update. Files changed: .changeset/fn-5809-skip-onboarding-bypass.md | 9 ++ packages/cli/src/bin.ts | 23 +++- .../__tests__/onboard-autolaunch-bypass.test.ts | 140 +++++++++++++++++++++ packages/cli/src/commands/onboard-autolaunch.ts | 11 +- 4 files changed, 173 insertions(+), 10 deletions(-) Fusion-Task-Id: FN-5809 Fusion-Task-Lineage: fc461eab-84df-42dc-924d-9d8cc59502cd
This commit is contained in:
9
.changeset/fn-5809-skip-onboarding-bypass.md
Normal file
9
.changeset/fn-5809-skip-onboarding-bypass.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Refine onboarding auto-launch bypass behavior by treating `--skip-onboarding` and `FUSION_SKIP_ONBOARDING` as first-class skip paths.
|
||||||
|
|
||||||
|
- Parse `FUSION_SKIP_ONBOARDING` with strict truthiness (`1`, `true`, `yes`, `on` only).
|
||||||
|
- Return distinct auto-launch skip reasons for flag (`skip-flag`) and env (`skip-env`).
|
||||||
|
- Strip `--skip-onboarding` as a global CLI flag so it never leaks into downstream command parsers while still informing onboarding gate decisions.
|
||||||
@@ -419,14 +419,19 @@ Columns: triage, todo, in-progress, in-review, done, archived
|
|||||||
Supported file types: png, jpg, gif, webp, txt, log, json, yaml, yml, toml, csv, xml
|
Supported file types: png, jpg, gif, webp, txt, log, json, yaml, yml, toml, csv, xml
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
function extractGlobalProjectFlag(argv: string[]): { cleanedArgs: string[]; projectName?: string } {
|
export function extractGlobalProjectFlag(argv: string[]): {
|
||||||
|
cleanedArgs: string[];
|
||||||
|
projectName?: string;
|
||||||
|
skipOnboarding: boolean;
|
||||||
|
} {
|
||||||
const command = argv[0];
|
const command = argv[0];
|
||||||
if (command === "serve" || command === "daemon") {
|
if (command === "serve" || command === "daemon") {
|
||||||
return { cleanedArgs: [...argv] };
|
return { cleanedArgs: [...argv], skipOnboarding: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanedArgs: string[] = [];
|
const cleanedArgs: string[] = [];
|
||||||
let projectName: string | undefined;
|
let projectName: string | undefined;
|
||||||
|
let skipOnboarding = false;
|
||||||
|
|
||||||
for (let i = 0; i < argv.length; i++) {
|
for (let i = 0; i < argv.length; i++) {
|
||||||
const arg = argv[i];
|
const arg = argv[i];
|
||||||
@@ -442,10 +447,14 @@ function extractGlobalProjectFlag(argv: string[]): { cleanedArgs: string[]; proj
|
|||||||
i++;
|
i++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (arg === "--skip-onboarding") {
|
||||||
|
skipOnboarding = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
cleanedArgs.push(arg);
|
cleanedArgs.push(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { cleanedArgs, projectName };
|
return { cleanedArgs, projectName, skipOnboarding };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFlagValue(args: string[], flag: string): string | undefined {
|
function getFlagValue(args: string[], flag: string): string | undefined {
|
||||||
@@ -533,7 +542,7 @@ function readOwnCliVersion(): string | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const { cleanedArgs: args, projectName } = extractGlobalProjectFlag(process.argv.slice(2));
|
const { cleanedArgs: args, projectName, skipOnboarding } = extractGlobalProjectFlag(process.argv.slice(2));
|
||||||
|
|
||||||
// Print version and exit before any application imports. This is what the
|
// Print version and exit before any application imports. This is what the
|
||||||
// dashboard's CLI Binary panel probes via `<bin> --version`; without an
|
// dashboard's CLI Binary panel probes via `<bin> --version`; without an
|
||||||
@@ -558,7 +567,7 @@ async function main() {
|
|||||||
const command = args[0];
|
const command = args[0];
|
||||||
|
|
||||||
const { maybeAutoLaunchOnboarding } = await import("./commands/onboard-autolaunch.js");
|
const { maybeAutoLaunchOnboarding } = await import("./commands/onboard-autolaunch.js");
|
||||||
await maybeAutoLaunchOnboarding({ command, args });
|
await maybeAutoLaunchOnboarding({ command, args, skipOnboarding });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
runDashboard,
|
runDashboard,
|
||||||
@@ -1849,4 +1858,6 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.FUSION_CLI_SKIP_MAIN !== "1") {
|
||||||
await main();
|
await main();
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
isTruthyEnvFlag,
|
||||||
|
maybeAutoLaunchOnboarding,
|
||||||
|
shouldAutoLaunchOnboarding,
|
||||||
|
} from "../onboard-autolaunch.js";
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.resetModules();
|
||||||
|
delete process.env.FUSION_CLI_SKIP_MAIN;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("onboard bypass reasons", () => {
|
||||||
|
it("uses skip-flag when flag is present", async () => {
|
||||||
|
expect(
|
||||||
|
shouldAutoLaunchOnboarding({
|
||||||
|
command: "task",
|
||||||
|
args: ["task", "list", "--skip-onboarding"],
|
||||||
|
centralDbExists: false,
|
||||||
|
projectInitialized: false,
|
||||||
|
isTTY: true,
|
||||||
|
}),
|
||||||
|
).toEqual({ launch: false, reason: "skip-flag" });
|
||||||
|
|
||||||
|
const runOnboard = vi.fn();
|
||||||
|
await maybeAutoLaunchOnboarding({
|
||||||
|
command: "task",
|
||||||
|
args: ["task", "list", "--skip-onboarding"],
|
||||||
|
isTTY: true,
|
||||||
|
pathExists: () => false,
|
||||||
|
runOnboard,
|
||||||
|
});
|
||||||
|
expect(runOnboard).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses skip-env when env is truthy", async () => {
|
||||||
|
expect(
|
||||||
|
shouldAutoLaunchOnboarding({
|
||||||
|
command: "task",
|
||||||
|
args: ["task", "list"],
|
||||||
|
centralDbExists: false,
|
||||||
|
projectInitialized: false,
|
||||||
|
isTTY: true,
|
||||||
|
env: { FUSION_SKIP_ONBOARDING: "1" },
|
||||||
|
}),
|
||||||
|
).toEqual({ launch: false, reason: "skip-env" });
|
||||||
|
|
||||||
|
const runOnboard = vi.fn();
|
||||||
|
await maybeAutoLaunchOnboarding({
|
||||||
|
command: "task",
|
||||||
|
args: ["task", "list"],
|
||||||
|
env: { FUSION_SKIP_ONBOARDING: "1" },
|
||||||
|
isTTY: true,
|
||||||
|
pathExists: () => false,
|
||||||
|
runOnboard,
|
||||||
|
});
|
||||||
|
expect(runOnboard).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses strict truthy env values", () => {
|
||||||
|
for (const value of ["1", "true", "yes", "on", "TRUE", "On"]) {
|
||||||
|
expect(isTruthyEnvFlag(value)).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const value of [undefined, "", "0", "false", "no", "off", "maybe"]) {
|
||||||
|
expect(isTruthyEnvFlag(value)).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not bypass without flag or env", () => {
|
||||||
|
expect(
|
||||||
|
shouldAutoLaunchOnboarding({
|
||||||
|
command: "task",
|
||||||
|
args: ["task", "list"],
|
||||||
|
centralDbExists: false,
|
||||||
|
projectInitialized: false,
|
||||||
|
isTTY: true,
|
||||||
|
}),
|
||||||
|
).toEqual({ launch: true, reason: "central-db-missing" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("extractGlobalProjectFlag", () => {
|
||||||
|
it("strips --skip-onboarding and surfaces skipOnboarding", async () => {
|
||||||
|
process.env.FUSION_CLI_SKIP_MAIN = "1";
|
||||||
|
const { extractGlobalProjectFlag } = await import("../../bin.js");
|
||||||
|
|
||||||
|
expect(extractGlobalProjectFlag(["task", "list", "--skip-onboarding"])).toEqual({
|
||||||
|
cleanedArgs: ["task", "list"],
|
||||||
|
projectName: undefined,
|
||||||
|
skipOnboarding: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(extractGlobalProjectFlag(["task", "list"])).toEqual({
|
||||||
|
cleanedArgs: ["task", "list"],
|
||||||
|
projectName: undefined,
|
||||||
|
skipOnboarding: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not consume next arg after --skip-onboarding", async () => {
|
||||||
|
process.env.FUSION_CLI_SKIP_MAIN = "1";
|
||||||
|
const { extractGlobalProjectFlag } = await import("../../bin.js");
|
||||||
|
|
||||||
|
expect(extractGlobalProjectFlag(["task", "--skip-onboarding", "list"]).cleanedArgs).toEqual([
|
||||||
|
"task",
|
||||||
|
"list",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drives skip-flag via surfaced skipOnboarding", async () => {
|
||||||
|
process.env.FUSION_CLI_SKIP_MAIN = "1";
|
||||||
|
const { extractGlobalProjectFlag } = await import("../../bin.js");
|
||||||
|
const parsed = extractGlobalProjectFlag(["task", "list", "--skip-onboarding"]);
|
||||||
|
|
||||||
|
const runOnboard = vi.fn();
|
||||||
|
await maybeAutoLaunchOnboarding({
|
||||||
|
command: "task",
|
||||||
|
args: parsed.cleanedArgs,
|
||||||
|
skipOnboarding: parsed.skipOnboarding,
|
||||||
|
isTTY: true,
|
||||||
|
pathExists: () => false,
|
||||||
|
runOnboard,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
shouldAutoLaunchOnboarding({
|
||||||
|
command: "task",
|
||||||
|
args: parsed.cleanedArgs,
|
||||||
|
skipOnboarding: parsed.skipOnboarding,
|
||||||
|
centralDbExists: false,
|
||||||
|
projectInitialized: false,
|
||||||
|
isTTY: true,
|
||||||
|
}),
|
||||||
|
).toEqual({ launch: false, reason: "skip-flag" });
|
||||||
|
expect(runOnboard).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,6 +11,7 @@ export interface AutoLaunchInput {
|
|||||||
projectInitialized: boolean;
|
projectInitialized: boolean;
|
||||||
isTTY: boolean;
|
isTTY: boolean;
|
||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
|
skipOnboarding?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutoLaunchDecision {
|
export interface AutoLaunchDecision {
|
||||||
@@ -42,11 +43,11 @@ export function shouldAutoLaunchOnboarding(input: AutoLaunchInput): AutoLaunchDe
|
|||||||
return { launch: false, reason: "non-tty" };
|
return { launch: false, reason: "non-tty" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.args.includes("--skip-onboarding")) {
|
if (input.skipOnboarding || input.args.includes("--skip-onboarding")) {
|
||||||
return { launch: false, reason: "skip-flag" };
|
return { launch: false, reason: "skip-flag" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTruthy(env.FUSION_SKIP_ONBOARDING)) {
|
if (isTruthyEnvFlag(env.FUSION_SKIP_ONBOARDING)) {
|
||||||
return { launch: false, reason: "skip-env" };
|
return { launch: false, reason: "skip-env" };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,13 +62,13 @@ export function shouldAutoLaunchOnboarding(input: AutoLaunchInput): AutoLaunchDe
|
|||||||
return { launch: true, reason: "central-db-missing" };
|
return { launch: true, reason: "central-db-missing" };
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTruthy(value: string | undefined): boolean {
|
export function isTruthyEnvFlag(value: string | undefined): boolean {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalized = value.trim().toLowerCase();
|
const normalized = value.trim().toLowerCase();
|
||||||
return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "no";
|
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RunOnboardOptions {
|
interface RunOnboardOptions {
|
||||||
@@ -79,6 +80,7 @@ type RunOnboard = (options?: RunOnboardOptions) => Promise<void> | void;
|
|||||||
export interface MaybeAutoLaunchDeps {
|
export interface MaybeAutoLaunchDeps {
|
||||||
command: string;
|
command: string;
|
||||||
args: string[];
|
args: string[];
|
||||||
|
skipOnboarding?: boolean;
|
||||||
centralDbPath?: string;
|
centralDbPath?: string;
|
||||||
projectInitialized?: boolean;
|
projectInitialized?: boolean;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
@@ -114,6 +116,7 @@ export async function maybeAutoLaunchOnboarding(deps: MaybeAutoLaunchDeps): Prom
|
|||||||
projectInitialized,
|
projectInitialized,
|
||||||
isTTY,
|
isTTY,
|
||||||
env,
|
env,
|
||||||
|
skipOnboarding: deps.skipOnboarding,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!decision.launch) {
|
if (!decision.launch) {
|
||||||
|
|||||||
Reference in New Issue
Block a user