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:
@@ -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
|
||||
`.trim();
|
||||
|
||||
function extractGlobalProjectFlag(argv: string[]): { cleanedArgs: string[]; projectName?: string } {
|
||||
export function extractGlobalProjectFlag(argv: string[]): {
|
||||
cleanedArgs: string[];
|
||||
projectName?: string;
|
||||
skipOnboarding: boolean;
|
||||
} {
|
||||
const command = argv[0];
|
||||
if (command === "serve" || command === "daemon") {
|
||||
return { cleanedArgs: [...argv] };
|
||||
return { cleanedArgs: [...argv], skipOnboarding: false };
|
||||
}
|
||||
|
||||
const cleanedArgs: string[] = [];
|
||||
let projectName: string | undefined;
|
||||
let skipOnboarding = false;
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
@@ -442,10 +447,14 @@ function extractGlobalProjectFlag(argv: string[]): { cleanedArgs: string[]; proj
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--skip-onboarding") {
|
||||
skipOnboarding = true;
|
||||
continue;
|
||||
}
|
||||
cleanedArgs.push(arg);
|
||||
}
|
||||
|
||||
return { cleanedArgs, projectName };
|
||||
return { cleanedArgs, projectName, skipOnboarding };
|
||||
}
|
||||
|
||||
function getFlagValue(args: string[], flag: string): string | undefined {
|
||||
@@ -533,7 +542,7 @@ function readOwnCliVersion(): string | undefined {
|
||||
}
|
||||
|
||||
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
|
||||
// dashboard's CLI Binary panel probes via `<bin> --version`; without an
|
||||
@@ -558,7 +567,7 @@ async function main() {
|
||||
const command = args[0];
|
||||
|
||||
const { maybeAutoLaunchOnboarding } = await import("./commands/onboard-autolaunch.js");
|
||||
await maybeAutoLaunchOnboarding({ command, args });
|
||||
await maybeAutoLaunchOnboarding({ command, args, skipOnboarding });
|
||||
|
||||
const {
|
||||
runDashboard,
|
||||
@@ -1849,4 +1858,6 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
await main();
|
||||
if (process.env.FUSION_CLI_SKIP_MAIN !== "1") {
|
||||
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;
|
||||
isTTY: boolean;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
skipOnboarding?: boolean;
|
||||
}
|
||||
|
||||
export interface AutoLaunchDecision {
|
||||
@@ -42,11 +43,11 @@ export function shouldAutoLaunchOnboarding(input: AutoLaunchInput): AutoLaunchDe
|
||||
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" };
|
||||
}
|
||||
|
||||
if (isTruthy(env.FUSION_SKIP_ONBOARDING)) {
|
||||
if (isTruthyEnvFlag(env.FUSION_SKIP_ONBOARDING)) {
|
||||
return { launch: false, reason: "skip-env" };
|
||||
}
|
||||
|
||||
@@ -61,13 +62,13 @@ export function shouldAutoLaunchOnboarding(input: AutoLaunchInput): AutoLaunchDe
|
||||
return { launch: true, reason: "central-db-missing" };
|
||||
}
|
||||
|
||||
function isTruthy(value: string | undefined): boolean {
|
||||
export function isTruthyEnvFlag(value: string | undefined): boolean {
|
||||
if (value === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "no";
|
||||
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
||||
}
|
||||
|
||||
interface RunOnboardOptions {
|
||||
@@ -79,6 +80,7 @@ type RunOnboard = (options?: RunOnboardOptions) => Promise<void> | void;
|
||||
export interface MaybeAutoLaunchDeps {
|
||||
command: string;
|
||||
args: string[];
|
||||
skipOnboarding?: boolean;
|
||||
centralDbPath?: string;
|
||||
projectInitialized?: boolean;
|
||||
cwd?: string;
|
||||
@@ -114,6 +116,7 @@ export async function maybeAutoLaunchOnboarding(deps: MaybeAutoLaunchDeps): Prom
|
||||
projectInitialized,
|
||||
isTTY,
|
||||
env,
|
||||
skipOnboarding: deps.skipOnboarding,
|
||||
});
|
||||
|
||||
if (!decision.launch) {
|
||||
|
||||
Reference in New Issue
Block a user