874 lines · 21.3 KB · TypeScript
RawDownload
import { parsePipeline } from "./parser.js";import { createDefaultRegistry } from "./commands/registry.js";import { runPipeline } from "./runtime.js";import { decodeResumeToken, parseResumeArgs, resolveApprovalId } from "./resume.js";import { cleanupApprovalIndexByStateKey, deleteApprovalId } from "./state/store.js";import {	WorkflowResumeArgumentError,	loadWorkflowFile,	resolveWorkflowArgs,	runWorkflowFile,} from "./workflows/file.js";import { renderWorkflowGraph } from "./workflows/graph.js";import type { WorkflowGraphFormat } from "./workflows/graph.js";import { deleteStateJson } from "./state/store.js";import {	finalizePipelineToolRun,	loadPipelineResumeState,	validatePipelineInputResponse,} from "./pipeline_resume_state.js"; export async function runCli(argv) {	const registry = createDefaultRegistry(); 	if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) {		process.stdout.write(helpText());		return;	} 	if (argv[0] === "help") {		const topic = argv[1];		if (!topic) {			process.stdout.write(helpText());			return;		}		const cmd = registry.get(topic);		if (!cmd) {			process.stderr.write(`Unknown command: ${topic}\n`);			process.exitCode = 2;			return;		}		process.stdout.write(cmd.help());		return;	} 	if (argv[0] === "version" || argv[0] === "--version" || argv[0] === "-v") {		process.stdout.write(`${await readVersion()}\n`);		return;	} 	if (argv[0] === "doctor") {		await handleDoctor({ argv: argv.slice(1), registry });		return;	} 	if (argv[0] === "graph") {		await handleGraph({ argv: argv.slice(1) });		return;	} 	if (argv[0] === "run") {		await handleRun({ argv: argv.slice(1), registry });		return;	} 	if (argv[0] === "resume") {		await handleResume({ argv: argv.slice(1), registry });		return;	} 	// Default: treat argv as a pipeline string.	await handleRun({ argv, registry });} async function handleGraph({ argv }) {	const parsed = parseGraphArgs(argv);	if (parsed.help) {		process.stdout.write(graphHelpText());		return;	} 	if (!parsed.filePath) {		process.stderr.write("graph requires a workflow file path (use --file <path>)\n");		process.exitCode = 2;		return;	} 	if (!isWorkflowGraphFormat(parsed.format)) {		process.stderr.write("graph --format must be one of: mermaid, dot, ascii\n");		process.exitCode = 2;		return;	} 	let argsJson: Record<string, unknown> = {};	if (parsed.argsJson) {		try {			const value = JSON.parse(parsed.argsJson);			if (!value || typeof value !== "object" || Array.isArray(value)) {				process.stderr.write("graph --args-json must be a JSON object\n");				process.exitCode = 2;				return;			}			argsJson = value as Record<string, unknown>;		} catch {			process.stderr.write("graph --args-json must be valid JSON\n");			process.exitCode = 2;			return;		}	} 	let filePath: string;	try {		filePath = await resolveWorkflowFile(parsed.filePath);	} catch (err) {		process.stderr.write(`Error: ${err?.message ?? String(err)}\n`);		process.exitCode = 1;		return;	} 	try {		const workflow = await loadWorkflowFile(filePath);		const args = resolveWorkflowArgs(workflow.args, argsJson);		const graph = renderWorkflowGraph({ workflow, format: parsed.format, args });		process.stdout.write(graph);		process.stdout.write("\n");	} catch (err) {		process.stderr.write(`Error: ${err?.message ?? String(err)}\n`);		process.exitCode = 1;	}} function isWorkflowGraphFormat(value: string): value is WorkflowGraphFormat {	return value === "mermaid" || value === "dot" || value === "ascii";} async function handleRun({ argv, registry }) {	const parsed = parseRunArgs(argv);	const { mode, argsJson } = parsed;	const normalizedMode = normalizeMode(mode);	const { rest, filePath, dryRun } = await resolveRunTarget(parsed); 	const workflowFile = filePath		? await resolveWorkflowFile(filePath)		: await detectWorkflowFile(rest);	if (workflowFile) {		let parsedArgs = {};		if (argsJson) {			try {				parsedArgs = JSON.parse(argsJson);			} catch {				if (mode === "tool") {					writeToolEnvelope({						ok: false,						error: { type: "parse_error", message: "run --args-json must be valid JSON" },					});					process.exitCode = 2;					return;				}				process.stderr.write("run --args-json must be valid JSON\n");				process.exitCode = 2;				return;			}		} 		try {			const output = await runWorkflowFile({				filePath: workflowFile,				args: parsedArgs,				ctx: {					stdin: process.stdin,					stdout: process.stdout,					stderr: process.stderr,					env: process.env,					mode: normalizedMode,					registry,					dryRun,				},			}); 			if (normalizedMode === "tool") {				if (output.status === "needs_approval") {					writeToolEnvelope({						ok: true,						status: "needs_approval",						output: [],						requiresApproval: output.requiresApproval ?? null,						requiresInput: null,					});					return;				} 				if (output.status === "needs_input") {					writeToolEnvelope({						ok: true,						status: "needs_input",						output: [],						requiresApproval: null,						requiresInput: output.requiresInput ?? null,					});					return;				} 				writeToolEnvelope({					ok: true,					status: "ok",					output: output.output,					requiresApproval: null,					requiresInput: null,				});				return;			} 			if (output.status === "needs_approval" || output.status === "needs_input") {				process.stdout.write(					JSON.stringify(						{							status: output.status,							output: [],							requiresApproval: output.requiresApproval ?? null,							requiresInput: output.requiresInput ?? null,						},						null,						2,					),				);				process.stdout.write("\n");				return;			} 			if (output.status === "ok" && output.output.length) {				process.stdout.write(JSON.stringify(output.output, null, 2));				process.stdout.write("\n");			}			return;		} catch (err) {			if (normalizedMode === "tool") {				writeToolEnvelope({					ok: false,					error: { type: "runtime_error", message: err?.message ?? String(err) },				});				process.exitCode = 1;				return;			}			process.stderr.write(`Error: ${err?.message ?? String(err)}\n`);			process.exitCode = 1;			return;		}	} 	const pipelineString = rest.join(" "); 	let pipeline;	try {		pipeline = parsePipeline(pipelineString);	} catch (err) {		if (mode === "tool") {			writeToolEnvelope({				ok: false,				error: { type: "parse_error", message: err?.message ?? String(err) },			});			process.exitCode = 2;			return;		}		process.stderr.write(`Parse error: ${err?.message ?? String(err)}\n`);		process.exitCode = 2;		return;	} 	try {		const output = await runPipeline({			pipeline,			registry,			input: [],			stdin: process.stdin,			stdout: process.stdout,			stderr: process.stderr,			env: process.env,			mode: normalizedMode,			dryRun,		}); 		if (normalizedMode === "tool") {			const finalized = await finalizePipelineToolRun({				env: process.env,				pipeline,				output,			});			writeToolEnvelope({				ok: true,				status: finalized.status,				output: finalized.output,				requiresApproval: finalized.requiresApproval,				requiresInput: finalized.requiresInput,			});			return;		} 		if (output.halted && isPipelineInputRequest(output.items)) {			throw new Error("requestInput requires --mode tool when stdin is not interactive");		} 		// Human mode: if the last command didn't render, print JSON.		if (!output.rendered) {			process.stdout.write(JSON.stringify(output.items, null, 2));			process.stdout.write("\n");		}	} catch (err) {		if (normalizedMode === "tool") {			writeToolEnvelope({				ok: false,				error: { type: "runtime_error", message: err?.message ?? String(err) },			});			process.exitCode = 1;			return;		}		process.stderr.write(`Error: ${err?.message ?? String(err)}\n`);		process.exitCode = 1;	}} function isPipelineInputRequest(items) {	return (		items.length === 1 && items[0]?.type === "input_request" && items[0]?.commandInput !== undefined	);} function parseRunArgs(argv) {	const rest = [];	let mode = "human";	let filePath = null;	let argsJson = null;	let dryRun = false; 	for (let i = 0; i < argv.length; i++) {		const tok = argv[i]; 		// Treat --dry-run as a Lobster flag only before positional command/pipeline		// args begin. Once rest has started, the token may belong to the command.		// Trailing workflow-file --dry-run is handled later after we can prove the		// first positional token is actually a workflow file.		if (tok === "--dry-run" && rest.length === 0) {			dryRun = true;			continue;		} 		if (tok === "--mode") {			const value = argv[i + 1];			if (value) {				mode = value;				i++;			}			continue;		} 		if (tok.startsWith("--mode=")) {			mode = tok.slice("--mode=".length) || "human";			continue;		} 		if (tok === "--file") {			const value = argv[i + 1];			if (value) {				filePath = value;				i++;			}			continue;		} 		if (tok.startsWith("--file=")) {			filePath = tok.slice("--file=".length);			continue;		} 		if (tok === "--args-json") {			const value = argv[i + 1];			if (value) {				argsJson = value;				i++;			}			continue;		} 		if (tok.startsWith("--args-json=")) {			argsJson = tok.slice("--args-json=".length);			continue;		} 		rest.push(tok);	} 	return { mode, rest, filePath, argsJson, dryRun };} function parseGraphArgs(argv: string[]) {	const rest: string[] = [];	let filePath: string | null = null;	let format = "mermaid";	let argsJson: string | null = null;	let help = false; 	for (let i = 0; i < argv.length; i++) {		const tok = argv[i]; 		if (tok === "-h" || tok === "--help") {			help = true;			continue;		} 		if (tok === "--file") {			const value = argv[i + 1];			if (value) {				filePath = value;				i++;			}			continue;		} 		if (tok.startsWith("--file=")) {			filePath = tok.slice("--file=".length);			continue;		} 		if (tok === "--format") {			const value = argv[i + 1];			if (value) {				format = value;				i++;			}			continue;		} 		if (tok.startsWith("--format=")) {			format = tok.slice("--format=".length) || "mermaid";			continue;		} 		if (tok === "--args-json") {			const value = argv[i + 1];			if (value) {				argsJson = value;				i++;			}			continue;		} 		if (tok.startsWith("--args-json=")) {			argsJson = tok.slice("--args-json=".length);			continue;		} 		rest.push(tok);	} 	if (!filePath && rest.length > 0) {		filePath = rest[0];	}	return { filePath, format, argsJson, help };} async function resolveRunTarget(parsed: {	rest: string[];	filePath: string | null;	dryRun: boolean;}) {	if (parsed.filePath) return parsed;	const restWithoutDryRun = parsed.rest.filter((token) => token !== "--dry-run");	if (restWithoutDryRun.length === 1 && restWithoutDryRun.length !== parsed.rest.length) {		try {			const workflowFile = await resolveWorkflowFile(restWithoutDryRun[0]);			return { ...parsed, filePath: workflowFile, rest: [], dryRun: true };		} catch {			return parsed;		}	}	return parsed;} function normalizeMode(mode) {	return mode === "tool" ? "tool" : "human";} async function detectWorkflowFile(rest) {	if (rest.length !== 1) return null;	const candidate = rest[0];	if (!candidate || candidate.includes("|")) return null;	try {		return await resolveWorkflowFile(candidate);	} catch {		return null;	}} async function resolveWorkflowFile(candidate) {	const { promises: fsp } = await import("node:fs");	const { resolve, extname, isAbsolute } = await import("node:path");	const resolved = isAbsolute(candidate) ? candidate : resolve(process.cwd(), candidate);	const stat = await fsp.stat(resolved);	if (!stat.isFile()) throw new Error("Workflow path is not a file"); 	const ext = extname(resolved).toLowerCase();	if (![".lobster", ".yaml", ".yml", ".json"].includes(ext)) {

Lines 1–500 of 874.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t