@bastani/atomic 0.5.20-0 → 0.5.21-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/workflow-creator/SKILL.md +56 -8
- package/dist/sdk/components/orchestrator-panel.d.ts +23 -1
- package/dist/sdk/components/orchestrator-panel.d.ts.map +1 -1
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/runtime/status-writer.d.ts +101 -0
- package/dist/sdk/runtime/status-writer.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +57 -3
- package/src/commands/cli/session.test.ts +43 -0
- package/src/commands/cli/session.ts +18 -8
- package/src/commands/cli/workflow-inputs.test.ts +321 -0
- package/src/commands/cli/workflow-inputs.ts +219 -0
- package/src/commands/cli/workflow-status.test.ts +451 -0
- package/src/commands/cli/workflow-status.ts +330 -0
- package/src/sdk/components/orchestrator-panel.tsx +36 -1
- package/src/sdk/runtime/executor.ts +37 -0
- package/src/sdk/runtime/status-writer.test.ts +245 -0
- package/src/sdk/runtime/status-writer.ts +201 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the workflow-inputs CLI command.
|
|
3
|
+
*
|
|
4
|
+
* Focused on the pure helpers (`buildInputsPayload` + `renderInputsText`)
|
|
5
|
+
* since they carry the schema-shaping logic. The thin command wrapper
|
|
6
|
+
* is exercised end-to-end by the existing workflow-command harness.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, test, expect, beforeAll, afterAll, mock } from "bun:test";
|
|
10
|
+
import {
|
|
11
|
+
buildInputsPayload,
|
|
12
|
+
renderInputsText,
|
|
13
|
+
workflowInputsCommand,
|
|
14
|
+
type WorkflowInputsDeps,
|
|
15
|
+
} from "./workflow-inputs.ts";
|
|
16
|
+
import type {
|
|
17
|
+
WorkflowInput,
|
|
18
|
+
DiscoveredWorkflow,
|
|
19
|
+
WorkflowDefinition,
|
|
20
|
+
} from "../../sdk/workflows/index.ts";
|
|
21
|
+
|
|
22
|
+
let originalNoColor: string | undefined;
|
|
23
|
+
beforeAll(() => {
|
|
24
|
+
originalNoColor = process.env.NO_COLOR;
|
|
25
|
+
process.env.NO_COLOR = "1";
|
|
26
|
+
});
|
|
27
|
+
afterAll(() => {
|
|
28
|
+
if (originalNoColor === undefined) delete process.env.NO_COLOR;
|
|
29
|
+
else process.env.NO_COLOR = originalNoColor;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("buildInputsPayload", () => {
|
|
33
|
+
test("synthesises a 'prompt' field for free-form workflows", () => {
|
|
34
|
+
const out = buildInputsPayload("ralph", "claude", "loop", []);
|
|
35
|
+
expect(out.freeform).toBe(true);
|
|
36
|
+
expect(out.inputs).toHaveLength(1);
|
|
37
|
+
expect(out.inputs[0]!.name).toBe("prompt");
|
|
38
|
+
expect(out.inputs[0]!.type).toBe("text");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("clones structured inputs without mutating callers' arrays", () => {
|
|
42
|
+
const schema: WorkflowInput[] = [
|
|
43
|
+
{ name: "research_doc", type: "string", required: true },
|
|
44
|
+
{
|
|
45
|
+
name: "focus",
|
|
46
|
+
type: "enum",
|
|
47
|
+
values: ["minimal", "standard"],
|
|
48
|
+
default: "standard",
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
const out = buildInputsPayload("gen-spec", "claude", "spec", schema);
|
|
52
|
+
expect(out.freeform).toBe(false);
|
|
53
|
+
expect(out.inputs).toHaveLength(2);
|
|
54
|
+
expect(out.inputs[0]!.name).toBe("research_doc");
|
|
55
|
+
expect(out.inputs[1]!.values).toEqual(["minimal", "standard"]);
|
|
56
|
+
// mutating the output must not leak into the input
|
|
57
|
+
out.inputs[0]!.required = false;
|
|
58
|
+
expect(schema[0]!.required).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("propagates description and agent into the payload", () => {
|
|
62
|
+
const out = buildInputsPayload("foo", "copilot", "describe me", []);
|
|
63
|
+
expect(out.workflow).toBe("foo");
|
|
64
|
+
expect(out.agent).toBe("copilot");
|
|
65
|
+
expect(out.description).toBe("describe me");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("renderInputsText", () => {
|
|
70
|
+
test("free-form workflows show the positional-prompt run hint", () => {
|
|
71
|
+
const payload = buildInputsPayload("ralph", "claude", "loop", []);
|
|
72
|
+
const out = renderInputsText(payload);
|
|
73
|
+
expect(out).toContain("ralph");
|
|
74
|
+
expect(out).toContain("claude");
|
|
75
|
+
expect(out).toContain("free-form");
|
|
76
|
+
expect(out).toContain('atomic workflow -n ralph -a claude "<prompt>"');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("renders placeholder hint when a field declares one", () => {
|
|
80
|
+
const schema: WorkflowInput[] = [
|
|
81
|
+
{
|
|
82
|
+
name: "note",
|
|
83
|
+
type: "text",
|
|
84
|
+
placeholder: "short summary goes here",
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
const payload = buildInputsPayload("foo", "claude", "", schema);
|
|
88
|
+
const out = renderInputsText(payload);
|
|
89
|
+
expect(out).toContain("placeholder:");
|
|
90
|
+
expect(out).toContain("short summary goes here");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("structured workflows render flag names, types, required, defaults, and enum values", () => {
|
|
94
|
+
const schema: WorkflowInput[] = [
|
|
95
|
+
{
|
|
96
|
+
name: "research_doc",
|
|
97
|
+
type: "string",
|
|
98
|
+
required: true,
|
|
99
|
+
description: "path to research notes",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "focus",
|
|
103
|
+
type: "enum",
|
|
104
|
+
values: ["minimal", "standard", "exhaustive"],
|
|
105
|
+
default: "standard",
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
const payload = buildInputsPayload("gen-spec", "claude", "spec", schema);
|
|
109
|
+
const out = renderInputsText(payload);
|
|
110
|
+
|
|
111
|
+
expect(out).toContain("--research_doc");
|
|
112
|
+
expect(out).toContain("(required)");
|
|
113
|
+
expect(out).toContain("[string]");
|
|
114
|
+
expect(out).toContain("path to research notes");
|
|
115
|
+
|
|
116
|
+
expect(out).toContain("--focus");
|
|
117
|
+
expect(out).toContain("[enum]");
|
|
118
|
+
expect(out).toContain("minimal, standard, exhaustive");
|
|
119
|
+
expect(out).toContain("default: standard");
|
|
120
|
+
|
|
121
|
+
// run hint references both flags
|
|
122
|
+
expect(out).toContain("--research_doc=<string>");
|
|
123
|
+
expect(out).toContain("--focus=<enum>");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ─── workflowInputsCommand ─────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
function captureOutput(): {
|
|
130
|
+
stdout: () => string;
|
|
131
|
+
stderr: () => string;
|
|
132
|
+
restore: () => void;
|
|
133
|
+
} {
|
|
134
|
+
const outChunks: string[] = [];
|
|
135
|
+
const errChunks: string[] = [];
|
|
136
|
+
const origOut = process.stdout.write;
|
|
137
|
+
const origErr = process.stderr.write;
|
|
138
|
+
process.stdout.write = ((c: string | Uint8Array) => {
|
|
139
|
+
outChunks.push(typeof c === "string" ? c : new TextDecoder().decode(c));
|
|
140
|
+
return true;
|
|
141
|
+
}) as typeof process.stdout.write;
|
|
142
|
+
process.stderr.write = ((c: string | Uint8Array) => {
|
|
143
|
+
errChunks.push(typeof c === "string" ? c : new TextDecoder().decode(c));
|
|
144
|
+
return true;
|
|
145
|
+
}) as typeof process.stderr.write;
|
|
146
|
+
return {
|
|
147
|
+
stdout: () => outChunks.join(""),
|
|
148
|
+
stderr: () => errChunks.join(""),
|
|
149
|
+
restore: () => {
|
|
150
|
+
process.stdout.write = origOut;
|
|
151
|
+
process.stderr.write = origErr;
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function fakeDiscovered(name: string): DiscoveredWorkflow {
|
|
157
|
+
return {
|
|
158
|
+
name,
|
|
159
|
+
agent: "claude",
|
|
160
|
+
path: `/fake/path/${name}.ts`,
|
|
161
|
+
source: "builtin",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function fakeDefinition(
|
|
166
|
+
name: string,
|
|
167
|
+
description: string,
|
|
168
|
+
inputs: WorkflowInput[],
|
|
169
|
+
): WorkflowDefinition {
|
|
170
|
+
return {
|
|
171
|
+
__brand: "WorkflowDefinition",
|
|
172
|
+
name,
|
|
173
|
+
description,
|
|
174
|
+
inputs,
|
|
175
|
+
run: async () => {},
|
|
176
|
+
} as WorkflowDefinition;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function makeDeps(overrides: Partial<WorkflowInputsDeps> = {}): WorkflowInputsDeps {
|
|
180
|
+
return {
|
|
181
|
+
findWorkflow: mock(async () => fakeDiscovered("gen-spec")) as unknown as
|
|
182
|
+
WorkflowInputsDeps["findWorkflow"],
|
|
183
|
+
loadWorkflow: mock(async (plan) => ({
|
|
184
|
+
ok: true,
|
|
185
|
+
value: {
|
|
186
|
+
...plan,
|
|
187
|
+
warnings: [],
|
|
188
|
+
definition: fakeDefinition("gen-spec", "spec generator", [
|
|
189
|
+
{ name: "research_doc", type: "string", required: true },
|
|
190
|
+
]),
|
|
191
|
+
},
|
|
192
|
+
})) as unknown as WorkflowInputsDeps["loadWorkflow"],
|
|
193
|
+
...overrides,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
describe("workflowInputsCommand", () => {
|
|
198
|
+
test("returns 1 with a JSON error envelope on unknown agent", async () => {
|
|
199
|
+
const cap = captureOutput();
|
|
200
|
+
try {
|
|
201
|
+
const code = await workflowInputsCommand(
|
|
202
|
+
{ name: "gen-spec", agent: "bogus", format: "json" },
|
|
203
|
+
makeDeps(),
|
|
204
|
+
);
|
|
205
|
+
expect(code).toBe(1);
|
|
206
|
+
const parsed = JSON.parse(cap.stdout());
|
|
207
|
+
expect(parsed.error).toContain("Unknown agent");
|
|
208
|
+
} finally {
|
|
209
|
+
cap.restore();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("returns 1 with a JSON error envelope when the workflow is missing", async () => {
|
|
214
|
+
const deps = makeDeps({
|
|
215
|
+
findWorkflow: mock(async () => null) as unknown as
|
|
216
|
+
WorkflowInputsDeps["findWorkflow"],
|
|
217
|
+
});
|
|
218
|
+
const cap = captureOutput();
|
|
219
|
+
try {
|
|
220
|
+
const code = await workflowInputsCommand(
|
|
221
|
+
{ name: "missing", agent: "claude", format: "json" },
|
|
222
|
+
deps,
|
|
223
|
+
);
|
|
224
|
+
expect(code).toBe(1);
|
|
225
|
+
const parsed = JSON.parse(cap.stdout());
|
|
226
|
+
expect(parsed.error).toContain("not found");
|
|
227
|
+
} finally {
|
|
228
|
+
cap.restore();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("returns 1 when the loader fails to load the workflow", async () => {
|
|
233
|
+
const deps = makeDeps({
|
|
234
|
+
loadWorkflow: mock(async () => ({
|
|
235
|
+
ok: false,
|
|
236
|
+
stage: "load" as const,
|
|
237
|
+
error: new Error("boom"),
|
|
238
|
+
message: "boom",
|
|
239
|
+
})) as unknown as WorkflowInputsDeps["loadWorkflow"],
|
|
240
|
+
});
|
|
241
|
+
const cap = captureOutput();
|
|
242
|
+
try {
|
|
243
|
+
const code = await workflowInputsCommand(
|
|
244
|
+
{ name: "gen-spec", agent: "claude", format: "json" },
|
|
245
|
+
deps,
|
|
246
|
+
);
|
|
247
|
+
expect(code).toBe(1);
|
|
248
|
+
const parsed = JSON.parse(cap.stdout());
|
|
249
|
+
expect(parsed.error).toBe("boom");
|
|
250
|
+
} finally {
|
|
251
|
+
cap.restore();
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("prints the JSON payload on success", async () => {
|
|
256
|
+
const cap = captureOutput();
|
|
257
|
+
try {
|
|
258
|
+
const code = await workflowInputsCommand(
|
|
259
|
+
{ name: "gen-spec", agent: "claude", format: "json" },
|
|
260
|
+
makeDeps(),
|
|
261
|
+
);
|
|
262
|
+
expect(code).toBe(0);
|
|
263
|
+
const parsed = JSON.parse(cap.stdout());
|
|
264
|
+
expect(parsed.workflow).toBe("gen-spec");
|
|
265
|
+
expect(parsed.agent).toBe("claude");
|
|
266
|
+
expect(parsed.inputs).toHaveLength(1);
|
|
267
|
+
expect(parsed.inputs[0].name).toBe("research_doc");
|
|
268
|
+
} finally {
|
|
269
|
+
cap.restore();
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("prints the text render on success when format is 'text'", async () => {
|
|
274
|
+
const cap = captureOutput();
|
|
275
|
+
try {
|
|
276
|
+
const code = await workflowInputsCommand(
|
|
277
|
+
{ name: "gen-spec", agent: "claude", format: "text" },
|
|
278
|
+
makeDeps(),
|
|
279
|
+
);
|
|
280
|
+
expect(code).toBe(0);
|
|
281
|
+
const out = cap.stdout();
|
|
282
|
+
expect(out).toContain("gen-spec");
|
|
283
|
+
expect(out).toContain("--research_doc");
|
|
284
|
+
} finally {
|
|
285
|
+
cap.restore();
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("writes errors to stderr when format is 'text'", async () => {
|
|
290
|
+
const deps = makeDeps({
|
|
291
|
+
findWorkflow: mock(async () => null) as unknown as
|
|
292
|
+
WorkflowInputsDeps["findWorkflow"],
|
|
293
|
+
});
|
|
294
|
+
const cap = captureOutput();
|
|
295
|
+
try {
|
|
296
|
+
const code = await workflowInputsCommand(
|
|
297
|
+
{ name: "missing", agent: "claude", format: "text" },
|
|
298
|
+
deps,
|
|
299
|
+
);
|
|
300
|
+
expect(code).toBe(1);
|
|
301
|
+
expect(cap.stderr()).toContain("not found");
|
|
302
|
+
} finally {
|
|
303
|
+
cap.restore();
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("defaults format to 'json' when omitted", async () => {
|
|
308
|
+
const cap = captureOutput();
|
|
309
|
+
try {
|
|
310
|
+
const code = await workflowInputsCommand(
|
|
311
|
+
{ name: "gen-spec", agent: "claude" },
|
|
312
|
+
makeDeps(),
|
|
313
|
+
);
|
|
314
|
+
expect(code).toBe(0);
|
|
315
|
+
// JSON parses cleanly
|
|
316
|
+
JSON.parse(cap.stdout());
|
|
317
|
+
} finally {
|
|
318
|
+
cap.restore();
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
});
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `atomic workflow inputs <name> -a <agent>` — print a workflow's
|
|
3
|
+
* declared input schema so an orchestrating agent can build a valid
|
|
4
|
+
* `atomic workflow -n <name> -a <agent> --<field>=<value>` invocation
|
|
5
|
+
* without having to read the workflow source.
|
|
6
|
+
*
|
|
7
|
+
* Output formats:
|
|
8
|
+
* --format json (default) — machine-parseable JSON
|
|
9
|
+
* --format text — human-friendly text table
|
|
10
|
+
*
|
|
11
|
+
* Free-form workflows (no declared inputs) report a single synthetic
|
|
12
|
+
* `prompt` field so callers can treat both shapes uniformly.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { COLORS, createPainter } from "../../theme/colors.ts";
|
|
16
|
+
import { AGENT_CONFIG, type AgentKey } from "../../services/config/index.ts";
|
|
17
|
+
import {
|
|
18
|
+
findWorkflow as _findWorkflow,
|
|
19
|
+
WorkflowLoader,
|
|
20
|
+
} from "../../sdk/workflows/index.ts";
|
|
21
|
+
import type { WorkflowInput } from "../../sdk/workflows/index.ts";
|
|
22
|
+
|
|
23
|
+
export type WorkflowInputsFormat = "json" | "text";
|
|
24
|
+
|
|
25
|
+
export interface WorkflowInputsResult {
|
|
26
|
+
workflow: string;
|
|
27
|
+
agent: string;
|
|
28
|
+
description: string;
|
|
29
|
+
freeform: boolean;
|
|
30
|
+
inputs: WorkflowInput[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build the JSON payload returned to the agent. Free-form workflows
|
|
35
|
+
* synthesise a single optional `prompt` field so consumers don't have
|
|
36
|
+
* to special-case them — the same call shape works for both kinds.
|
|
37
|
+
*/
|
|
38
|
+
export function buildInputsPayload(
|
|
39
|
+
workflowName: string,
|
|
40
|
+
agent: string,
|
|
41
|
+
description: string,
|
|
42
|
+
inputs: readonly WorkflowInput[],
|
|
43
|
+
): WorkflowInputsResult {
|
|
44
|
+
const freeform = inputs.length === 0;
|
|
45
|
+
const declared: WorkflowInput[] = freeform
|
|
46
|
+
? [
|
|
47
|
+
{
|
|
48
|
+
name: "prompt",
|
|
49
|
+
type: "text",
|
|
50
|
+
required: false,
|
|
51
|
+
description:
|
|
52
|
+
"Free-form prompt — pass as a positional arg to `atomic workflow -n <name> -a <agent> \"<prompt>\"`.",
|
|
53
|
+
},
|
|
54
|
+
]
|
|
55
|
+
: inputs.map((i) => ({ ...i }));
|
|
56
|
+
return {
|
|
57
|
+
workflow: workflowName,
|
|
58
|
+
agent,
|
|
59
|
+
description,
|
|
60
|
+
freeform,
|
|
61
|
+
inputs: declared,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Render the payload as a human-friendly text block. */
|
|
66
|
+
export function renderInputsText(payload: WorkflowInputsResult): string {
|
|
67
|
+
const paint = createPainter();
|
|
68
|
+
const lines: string[] = [];
|
|
69
|
+
lines.push("");
|
|
70
|
+
lines.push(
|
|
71
|
+
" " +
|
|
72
|
+
paint("text", payload.workflow, { bold: true }) +
|
|
73
|
+
paint("dim", " (") +
|
|
74
|
+
paint("accent", payload.agent) +
|
|
75
|
+
paint("dim", ")"),
|
|
76
|
+
);
|
|
77
|
+
if (payload.description) {
|
|
78
|
+
lines.push(" " + paint("dim", payload.description));
|
|
79
|
+
}
|
|
80
|
+
lines.push("");
|
|
81
|
+
if (payload.freeform) {
|
|
82
|
+
lines.push(" " + paint("dim", "free-form workflow — single positional prompt"));
|
|
83
|
+
lines.push("");
|
|
84
|
+
lines.push(
|
|
85
|
+
" " +
|
|
86
|
+
paint("dim", "run: ") +
|
|
87
|
+
paint(
|
|
88
|
+
"accent",
|
|
89
|
+
`atomic workflow -n ${payload.workflow} -a ${payload.agent} "<prompt>"`,
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
lines.push("");
|
|
93
|
+
return lines.join("\n") + "\n";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const field of payload.inputs) {
|
|
97
|
+
const requiredLabel = field.required ? paint("warning", " (required)") : "";
|
|
98
|
+
const typeLabel = paint("dim", ` [${field.type}]`);
|
|
99
|
+
lines.push(
|
|
100
|
+
" " +
|
|
101
|
+
paint("accent", `--${field.name}`) +
|
|
102
|
+
typeLabel +
|
|
103
|
+
requiredLabel,
|
|
104
|
+
);
|
|
105
|
+
if (field.description) {
|
|
106
|
+
lines.push(" " + paint("text", field.description));
|
|
107
|
+
}
|
|
108
|
+
if (field.type === "enum" && field.values && field.values.length > 0) {
|
|
109
|
+
lines.push(
|
|
110
|
+
" " +
|
|
111
|
+
paint("dim", "values: ") +
|
|
112
|
+
paint("text", field.values.join(", ")),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (field.default !== undefined) {
|
|
116
|
+
lines.push(
|
|
117
|
+
" " + paint("dim", "default: ") + paint("text", field.default),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (field.placeholder) {
|
|
121
|
+
lines.push(
|
|
122
|
+
" " +
|
|
123
|
+
paint("dim", "placeholder: ") +
|
|
124
|
+
paint("text", field.placeholder),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
lines.push("");
|
|
130
|
+
const flagExample = payload.inputs
|
|
131
|
+
.map((i) => `--${i.name}=<${i.type}>`)
|
|
132
|
+
.join(" ");
|
|
133
|
+
lines.push(
|
|
134
|
+
" " +
|
|
135
|
+
paint("dim", "run: ") +
|
|
136
|
+
paint(
|
|
137
|
+
"accent",
|
|
138
|
+
`atomic workflow -n ${payload.workflow} -a ${payload.agent} ${flagExample}`,
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
lines.push("");
|
|
142
|
+
return lines.join("\n") + "\n";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface WorkflowInputsOptions {
|
|
146
|
+
name: string;
|
|
147
|
+
agent: string;
|
|
148
|
+
format?: WorkflowInputsFormat;
|
|
149
|
+
cwd?: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Deps for `workflowInputsCommand`. Injected so tests can drive every
|
|
154
|
+
* branch (unknown agent / missing workflow / load failure / success)
|
|
155
|
+
* without the SDK's real filesystem-dependent discovery.
|
|
156
|
+
*/
|
|
157
|
+
export interface WorkflowInputsDeps {
|
|
158
|
+
findWorkflow: typeof _findWorkflow;
|
|
159
|
+
loadWorkflow: typeof WorkflowLoader.loadWorkflow;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const defaultDeps: WorkflowInputsDeps = {
|
|
163
|
+
findWorkflow: _findWorkflow,
|
|
164
|
+
loadWorkflow: WorkflowLoader.loadWorkflow,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Resolve the workflow, then either print its input schema (success)
|
|
169
|
+
* or print an error and return a non-zero exit code. The json branch
|
|
170
|
+
* also writes errors as JSON so an agent can parse a single envelope
|
|
171
|
+
* regardless of outcome.
|
|
172
|
+
*/
|
|
173
|
+
export async function workflowInputsCommand(
|
|
174
|
+
options: WorkflowInputsOptions,
|
|
175
|
+
deps: WorkflowInputsDeps = defaultDeps,
|
|
176
|
+
): Promise<number> {
|
|
177
|
+
const format: WorkflowInputsFormat = options.format ?? "json";
|
|
178
|
+
|
|
179
|
+
const validAgents = Object.keys(AGENT_CONFIG);
|
|
180
|
+
if (!validAgents.includes(options.agent)) {
|
|
181
|
+
return reportError(
|
|
182
|
+
format,
|
|
183
|
+
`Unknown agent '${options.agent}'. Valid agents: ${validAgents.join(", ")}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const agent = options.agent as AgentKey;
|
|
187
|
+
|
|
188
|
+
const discovered = await deps.findWorkflow(options.name, agent, options.cwd);
|
|
189
|
+
if (!discovered) {
|
|
190
|
+
return reportError(
|
|
191
|
+
format,
|
|
192
|
+
`Workflow '${options.name}' not found for agent '${agent}'.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const loaded = await deps.loadWorkflow(discovered);
|
|
197
|
+
if (!loaded.ok) {
|
|
198
|
+
return reportError(format, loaded.message);
|
|
199
|
+
}
|
|
200
|
+
const def = loaded.value.definition;
|
|
201
|
+
|
|
202
|
+
const payload = buildInputsPayload(def.name, agent, def.description, def.inputs);
|
|
203
|
+
|
|
204
|
+
if (format === "json") {
|
|
205
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
206
|
+
} else {
|
|
207
|
+
process.stdout.write(renderInputsText(payload));
|
|
208
|
+
}
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function reportError(format: WorkflowInputsFormat, message: string): number {
|
|
213
|
+
if (format === "json") {
|
|
214
|
+
process.stdout.write(JSON.stringify({ error: message }, null, 2) + "\n");
|
|
215
|
+
} else {
|
|
216
|
+
process.stderr.write(`${COLORS.red}Error: ${message}${COLORS.reset}\n`);
|
|
217
|
+
}
|
|
218
|
+
return 1;
|
|
219
|
+
}
|