Files
fusion/packages/cli/src/commands/workflow.ts
gsxdsm b31bee03a8 FN-8576: add global quiet flag to CLI
Add a global quiet mode that suppresses informational CLI stdout without hiding requested results.

- Parse --quiet/-q and FUSION_QUIET with command and output exemptions.
- Route result output and interactive prompts around the reversible stdout gate.
- Document the flag and cover quiet output, prompts, and argument parsing.

Files changed:
 .changeset/fn-8576-cli-quiet-flag.md               |   7 ++
 docs/cli-reference.md                              |   8 ++
 packages/cli/src/__tests__/bin.test.ts             |  15 +++
 packages/cli/src/__tests__/cli-quiet-mode.test.ts  |  63 +++++++++++++
 .../__tests__/cli-quiet-prompt-surfaces.test.ts    |  33 +++++++
 packages/cli/src/bin.ts                            |  39 ++++++--
 packages/cli/src/commands/experiment-finalize.ts   |   3 +-
 packages/cli/src/commands/git.ts                   |  17 ++--
 packages/cli/src/commands/goals.ts                 |   4 +-
 packages/cli/src/commands/mission.ts               |   8 +-
 packages/cli/src/commands/node.ts                  |   4 +-
 packages/cli/src/commands/onboard.ts               |   4 +-
 packages/cli/src/commands/org-import.ts            |   5 +-
 packages/cli/src/commands/plugin.ts                |  14 ++-
 packages/cli/src/commands/port-prompt.ts           |   4 +-
 packages/cli/src/commands/project.ts               |   6 +-
 packages/cli/src/commands/research.ts              |   3 +-
 packages/cli/src/commands/task.ts                  | 101 +++++++++++---------
 packages/cli/src/commands/update.ts                |   3 +-
 packages/cli/src/commands/workflow.ts              |  13 +--
 packages/cli/src/output.ts                         | 104 +++++++++++++++++++++
 packages/cli/src/project-resolver.ts               |  20 ++--
 22 files changed, 388 insertions(+), 90 deletions(-)

Fusion-Task-Id: FN-8576

Fusion-Task-Lineage: 2493d6d5-bbd9-4fc2-b903-950457ef30b0

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-25 00:54:46 -07:00

100 lines
4.0 KiB
TypeScript

import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { createTaskStoreForBackend, type TaskStore } from "@fusion/core";
import { validateWorkflowIrDryRun } from "@fusion/engine";
import { cleanupProjectResolution, getStore } from "../project-resolver.js";
import { result } from "../output.js";
export interface RunWorkflowValidateOptions {
workflowId?: string;
file?: string;
projectName?: string;
json?: boolean;
}
interface OwnedWorkflowStore {
store: TaskStore;
shutdown: () => Promise<void>;
}
async function resolveStore(projectName?: string): Promise<OwnedWorkflowStore> {
try {
const store = await getStore({ project: projectName });
return { store, shutdown: cleanupProjectResolution };
} catch (error) {
if (projectName) throw error;
// FNXC:PostgresFinalCutover 2026-07-14-17:20: An unregistered CWD still
// validates workflows against PostgreSQL; it must not revive TaskStore's removed SQLite runtime.
const boot = await createTaskStoreForBackend({ rootDir: process.cwd() });
return { store: boot.taskStore, shutdown: boot.shutdown };
}
}
function printJsonAndExit(payload: unknown, code: number): never {
result(JSON.stringify(payload, null, 2) + "\n");
process.exit(code);
}
/**
* FNXC:WorkflowCli 2026-07-12-00:00:
* Workflow authors need a script-friendly dry-run command that can validate a saved workflow id or an IR JSON file without creating or mutating workflow rows.
*/
export async function runWorkflowValidate(opts: RunWorkflowValidateOptions): Promise<void> {
const workflowId = opts.workflowId?.trim();
if (!workflowId && !opts.file) {
const message = "Usage: fn workflow validate <id> | --file <path> [--json]";
if (opts.json) printJsonAndExit({ valid: false, error: message }, 2);
console.error(message);
process.exit(2);
}
let owned: OwnedWorkflowStore | undefined;
try {
owned = await resolveStore(opts.projectName);
const store = owned.store;
/* FNXC:PostgresCliLifecycle 2026-07-14-19:10: Workflow validation must await the exact startup owner before any process exit; a finally block is insufficient because process.exit skips pending cleanup. */
const exitWithStore = async (payload: unknown | undefined, code: number): Promise<never> => {
if (payload !== undefined) result(JSON.stringify(payload, null, 2) + "\n");
const current = owned;
owned = undefined;
await current!.shutdown();
return process.exit(code);
};
let ir: unknown;
if (opts.file) {
const filePath = resolve(opts.file);
try {
ir = JSON.parse(await readFile(filePath, "utf8"));
} catch (error) {
const message = `Failed to read or parse workflow IR file '${opts.file}': ${error instanceof Error ? error.message : String(error)}`;
if (opts.json) return await exitWithStore({ valid: false, error: message }, 2);
console.error(message);
return await exitWithStore(undefined, 2);
}
} else {
const def = await store.getWorkflowDefinition(workflowId!);
if (!def) {
const message = `Workflow '${workflowId}' not found`;
if (opts.json) return await exitWithStore({ valid: false, error: message }, 2);
console.error(message);
return await exitWithStore(undefined, 2);
}
ir = def.ir;
}
const validation = await validateWorkflowIrDryRun(store, ir, false);
if (opts.json) return await exitWithStore(validation.valid ? { valid: true } : { valid: false, errors: validation.errors }, validation.valid ? 0 : 1);
if (validation.valid) {
console.log("✓ Workflow IR is valid. No workflow was created or mutated.");
return await exitWithStore(undefined, 0);
}
console.error("✗ Workflow IR is invalid:");
for (const error of validation.errors) console.error(` - ${error.message}`);
return await exitWithStore(undefined, 1);
} finally {
const current = owned;
owned = undefined;
await current?.shutdown().catch(() => undefined);
}
}