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>
100 lines
4.0 KiB
TypeScript
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);
|
|
}
|
|
}
|