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. Load more
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)) {