Skip to content

Commit 9e991aa

Browse files
committed
feat: add --in/--out file options and hierarchical status display to CLI
Add --in and --out options to task/workflow/agent run commands for specifying input/output files when not using stdin/stdout pipes. Add StatusDisplay utility that renders hierarchical progress on stderr with support for multi-line detail (e.g. HFT model file downloads). https://claude.ai/code/session_01RUjsHV3Y7DvGvBnhSi3XLr
1 parent 48d2ac9 commit 9e991aa

5 files changed

Lines changed: 370 additions & 15 deletions

File tree

examples/cli/src/commands/agent.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
import { createGraphFromGraphJSON, type TaskGraphJson } from "@workglow/task-graph";
88
import type { Command } from "commander";
99
import { loadConfig } from "../config";
10+
import { StatusDisplay } from "../status";
1011
import { createAgentRepository } from "../storage";
11-
import { formatTable, readStdin } from "../util";
12+
import { formatTable, readInput, readStdin, writeOutput } from "../util";
1213

1314
export function registerAgentCommand(program: Command): void {
1415
const agent = program.command("agent").description("Manage and run agents");
@@ -88,8 +89,10 @@ export function registerAgentCommand(program: Command): void {
8889
agent
8990
.command("run")
9091
.argument("<id>", "agent identifier to run")
92+
.option("--in <path>", "Read input JSON overrides from a file instead of stdin")
93+
.option("--out <path>", "Write output JSON to a file instead of stdout")
9194
.description("Run a saved agent")
92-
.action(async (id: string) => {
95+
.action(async (id: string, opts: { in?: string; out?: string }) => {
9396
const config = await loadConfig();
9497
const repo = createAgentRepository(config);
9598
await repo.setupDatabase();
@@ -100,7 +103,46 @@ export function registerAgentCommand(program: Command): void {
100103
process.exit(1);
101104
}
102105

103-
const result = await graph.run();
104-
console.log(JSON.stringify(result, null, 2));
106+
// Optional input overrides
107+
const raw = await readInput(opts.in);
108+
if (raw) {
109+
try {
110+
const overrides = JSON.parse(raw) as Record<string, unknown>;
111+
const tasks = graph.getTasks();
112+
for (const task of tasks) {
113+
const sources = graph.getSourceDataflows(task.id as any);
114+
if (sources.length === 0 && overrides) {
115+
Object.assign(task.defaults, overrides);
116+
}
117+
}
118+
} catch {
119+
console.error("Invalid JSON input for overrides.");
120+
process.exit(1);
121+
}
122+
}
123+
124+
// Set up status display
125+
const display = new StatusDisplay();
126+
const tasks = graph.getTasks();
127+
for (const task of tasks) {
128+
display.addTask(task.id, task.title || task.type);
129+
}
130+
131+
graph.subscribeToTaskStatus((taskId, status) => {
132+
display.updateStatus(taskId, status);
133+
});
134+
graph.subscribeToTaskProgress((taskId, progress, message, ...args) => {
135+
display.updateProgress(taskId, progress, message, ...args);
136+
});
137+
138+
try {
139+
const result = await graph.run();
140+
display.finish();
141+
await writeOutput(JSON.stringify(result, null, 2), opts.out);
142+
} catch (err) {
143+
display.finish();
144+
console.error(`Agent failed: ${err instanceof Error ? err.message : String(err)}`);
145+
process.exit(1);
146+
}
105147
});
106148
}

examples/cli/src/commands/task.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
import { TaskRegistry } from "@workglow/task-graph";
88
import type { Command } from "commander";
9-
import { formatTable, readStdin } from "../util";
9+
import { StatusDisplay } from "../status";
10+
import { formatTable, readInput, writeOutput } from "../util";
1011

1112
export function registerTaskCommand(program: Command): void {
1213
const task = program.command("task").description("List and run tasks");
@@ -36,11 +37,15 @@ export function registerTaskCommand(program: Command): void {
3637

3738
task
3839
.command("run")
39-
.description("Run a task from stdin JSON { type, input, config }")
40-
.action(async () => {
41-
const raw = await readStdin();
40+
.description("Run a task from JSON { type, input, config }")
41+
.option("--in <path>", "Read input JSON from a file instead of stdin")
42+
.option("--out <path>", "Write output JSON to a file instead of stdout")
43+
.action(async (opts: { in?: string; out?: string }) => {
44+
const raw = await readInput(opts.in);
4245
if (!raw) {
43-
console.error("No input provided. Pipe JSON to stdin: { type, input, config }");
46+
console.error(
47+
"No input provided. Pipe JSON to stdin or use --in <file>: { type, input, config }"
48+
);
4449
process.exit(1);
4550
}
4651

@@ -64,7 +69,26 @@ export function registerTaskCommand(program: Command): void {
6469
}
6570

6671
const instance = new Ctor(parsed.input ?? {}, parsed.config ?? {});
67-
const result = await instance.run();
68-
console.log(JSON.stringify(result, null, 2));
72+
73+
const display = new StatusDisplay();
74+
const label = instance.title || instance.type;
75+
display.addTask(instance.id, label);
76+
77+
instance.on("status", (status) => {
78+
display.updateStatus(instance.id, status);
79+
});
80+
instance.on("progress", (progress, message, ...args) => {
81+
display.updateProgress(instance.id, progress, message, ...args);
82+
});
83+
84+
try {
85+
const result = await instance.run();
86+
display.finish();
87+
await writeOutput(JSON.stringify(result, null, 2), opts.out);
88+
} catch (err) {
89+
display.finish();
90+
console.error(`Task failed: ${err instanceof Error ? err.message : String(err)}`);
91+
process.exit(1);
92+
}
6993
});
7094
}

examples/cli/src/commands/workflow.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
import { createGraphFromGraphJSON, type TaskGraphJson } from "@workglow/task-graph";
88
import type { Command } from "commander";
99
import { loadConfig } from "../config";
10+
import { StatusDisplay } from "../status";
1011
import { createWorkflowRepository } from "../storage";
11-
import { formatTable, readStdin } from "../util";
12+
import { formatTable, readInput, readStdin, writeOutput } from "../util";
1213

1314
export function registerWorkflowCommand(program: Command): void {
1415
const workflow = program.command("workflow").description("Manage and run workflows");
@@ -89,8 +90,10 @@ export function registerWorkflowCommand(program: Command): void {
8990
workflow
9091
.command("run")
9192
.argument("<id>", "workflow identifier to run")
93+
.option("--in <path>", "Read input JSON overrides from a file instead of stdin")
94+
.option("--out <path>", "Write output JSON to a file instead of stdout")
9295
.description("Run a saved workflow")
93-
.action(async (id: string) => {
96+
.action(async (id: string, opts: { in?: string; out?: string }) => {
9497
const config = await loadConfig();
9598
const repo = createWorkflowRepository(config);
9699
await repo.setupDatabase();
@@ -101,7 +104,47 @@ export function registerWorkflowCommand(program: Command): void {
101104
process.exit(1);
102105
}
103106

104-
const result = await graph.run();
105-
console.log(JSON.stringify(result, null, 2));
107+
// Optional input overrides
108+
const raw = await readInput(opts.in);
109+
if (raw) {
110+
try {
111+
const overrides = JSON.parse(raw) as Record<string, unknown>;
112+
const tasks = graph.getTasks();
113+
for (const task of tasks) {
114+
// Apply overrides to input tasks (tasks with no incoming dataflows)
115+
const sources = graph.getSourceDataflows(task.id as any);
116+
if (sources.length === 0 && overrides) {
117+
Object.assign(task.defaults, overrides);
118+
}
119+
}
120+
} catch {
121+
console.error("Invalid JSON input for overrides.");
122+
process.exit(1);
123+
}
124+
}
125+
126+
// Set up status display
127+
const display = new StatusDisplay();
128+
const tasks = graph.getTasks();
129+
for (const task of tasks) {
130+
display.addTask(task.id, task.title || task.type);
131+
}
132+
133+
graph.subscribeToTaskStatus((taskId, status) => {
134+
display.updateStatus(taskId, status);
135+
});
136+
graph.subscribeToTaskProgress((taskId, progress, message, ...args) => {
137+
display.updateProgress(taskId, progress, message, ...args);
138+
});
139+
140+
try {
141+
const result = await graph.run();
142+
display.finish();
143+
await writeOutput(JSON.stringify(result, null, 2), opts.out);
144+
} catch (err) {
145+
display.finish();
146+
console.error(`Workflow failed: ${err instanceof Error ? err.message : String(err)}`);
147+
process.exit(1);
148+
}
106149
});
107150
}

0 commit comments

Comments
 (0)