@bastani/atomic 0.5.3-1 → 0.5.4-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/README.md +110 -11
- package/dist/{chunk-mn870nrv.js → chunk-xkxndz5g.js} +213 -154
- package/dist/sdk/components/workflow-picker-panel.d.ts +120 -0
- package/dist/sdk/define-workflow.d.ts +1 -1
- package/dist/sdk/index.js +1 -1
- package/dist/sdk/runtime/discovery.d.ts +57 -3
- package/dist/sdk/runtime/executor.d.ts +15 -2
- package/dist/sdk/runtime/tmux.d.ts +9 -0
- package/dist/sdk/types.d.ts +63 -4
- package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts +61 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts +48 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.d.ts +25 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts +91 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts +56 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts +48 -0
- package/dist/sdk/workflows/builtin/ralph/claude/index.js +6 -5
- package/dist/sdk/workflows/builtin/ralph/copilot/index.js +6 -5
- package/dist/sdk/workflows/builtin/ralph/opencode/index.js +6 -5
- package/dist/sdk/workflows/index.d.ts +4 -4
- package/dist/sdk/workflows/index.js +7 -1
- package/package.json +1 -1
- package/src/cli.ts +25 -3
- package/src/commands/cli/chat/index.ts +5 -5
- package/src/commands/cli/init/index.ts +79 -77
- package/src/commands/cli/workflow-command.test.ts +757 -0
- package/src/commands/cli/workflow.test.ts +310 -0
- package/src/commands/cli/workflow.ts +445 -105
- package/src/sdk/components/workflow-picker-panel.tsx +1462 -0
- package/src/sdk/define-workflow.test.ts +101 -0
- package/src/sdk/define-workflow.ts +62 -2
- package/src/sdk/runtime/discovery.ts +111 -8
- package/src/sdk/runtime/executor.ts +89 -32
- package/src/sdk/runtime/tmux.conf +55 -0
- package/src/sdk/runtime/tmux.ts +34 -10
- package/src/sdk/types.ts +67 -4
- package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +294 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +276 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.ts +38 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.ts +816 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/scout.ts +334 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +284 -0
- package/src/sdk/workflows/builtin/ralph/claude/index.ts +8 -4
- package/src/sdk/workflows/builtin/ralph/copilot/index.ts +10 -4
- package/src/sdk/workflows/builtin/ralph/opencode/index.ts +8 -4
- package/src/sdk/workflows/index.ts +9 -1
- package/src/services/system/auto-sync.ts +1 -1
- package/src/services/system/install-ui.ts +109 -39
- package/src/theme/colors.ts +65 -1
|
@@ -2,76 +2,271 @@
|
|
|
2
2
|
* Workflow CLI command
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
|
-
* atomic workflow -
|
|
6
|
-
* atomic workflow
|
|
5
|
+
* atomic workflow -a <agent> interactive picker
|
|
6
|
+
* atomic workflow -n <name> -a <agent> <prompt> free-form workflow
|
|
7
|
+
* atomic workflow -n <name> -a <agent> --<field>=<value> ...
|
|
8
|
+
* structured-input workflow
|
|
9
|
+
* atomic workflow --list list discoverable workflows
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
import { AGENT_CONFIG, type AgentKey } from "@/services/config/index.ts";
|
|
10
|
-
import { COLORS } from "@/theme/colors.ts";
|
|
11
|
-
import { isCommandInstalled
|
|
13
|
+
import { COLORS, createPainter, type PaletteKey } from "@/theme/colors.ts";
|
|
14
|
+
import { isCommandInstalled } from "@/services/system/detect.ts";
|
|
12
15
|
import { ensureTmuxInstalled, ensureBunInstalled } from "../../lib/spawn.ts";
|
|
13
16
|
import {
|
|
14
17
|
isTmuxInstalled,
|
|
15
18
|
discoverWorkflows,
|
|
16
19
|
findWorkflow,
|
|
20
|
+
loadWorkflowsMetadata,
|
|
17
21
|
executeWorkflow,
|
|
18
22
|
WorkflowLoader,
|
|
19
23
|
resetMuxBinaryCache,
|
|
20
24
|
} from "@/sdk/workflows/index.ts";
|
|
21
|
-
import type {
|
|
25
|
+
import type {
|
|
26
|
+
AgentType,
|
|
27
|
+
DiscoveredWorkflow,
|
|
28
|
+
WorkflowInput,
|
|
29
|
+
WorkflowWithMetadata,
|
|
30
|
+
} from "@/sdk/workflows/index.ts";
|
|
31
|
+
import { WorkflowPickerPanel } from "@/sdk/components/workflow-picker-panel.tsx";
|
|
32
|
+
|
|
33
|
+
// ─── Flag parser ────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Split commander's passthrough arg list into structured input flags and
|
|
37
|
+
* positional tokens (the latter get joined to form the free-form prompt).
|
|
38
|
+
*
|
|
39
|
+
* Accepts both `--name=value` and `--name value` forms, mirroring the
|
|
40
|
+
* conventions users already know from native agent CLIs. Flags whose
|
|
41
|
+
* values parse-fail (e.g. a trailing `--foo` with nothing after it) are
|
|
42
|
+
* returned as errors so the caller can print a clear usage hint rather
|
|
43
|
+
* than swallowing the mistake.
|
|
44
|
+
*
|
|
45
|
+
* Short flags (`-x value`) are treated as unknown and left in the
|
|
46
|
+
* positional bucket — we only recognise long-form `--<name>` flags as
|
|
47
|
+
* structured inputs.
|
|
48
|
+
*/
|
|
49
|
+
export function parsePassthroughArgs(args: string[]): {
|
|
50
|
+
flags: Record<string, string>;
|
|
51
|
+
positional: string[];
|
|
52
|
+
errors: string[];
|
|
53
|
+
} {
|
|
54
|
+
const flags: Record<string, string> = {};
|
|
55
|
+
const positional: string[] = [];
|
|
56
|
+
const errors: string[] = [];
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < args.length; i++) {
|
|
59
|
+
const tok = args[i]!;
|
|
60
|
+
if (tok.startsWith("--")) {
|
|
61
|
+
const body = tok.slice(2);
|
|
62
|
+
const eq = body.indexOf("=");
|
|
63
|
+
if (eq >= 0) {
|
|
64
|
+
const name = body.slice(0, eq);
|
|
65
|
+
const value = body.slice(eq + 1);
|
|
66
|
+
if (name === "") {
|
|
67
|
+
errors.push(`Malformed flag "${tok}" — expected --<name>=<value>.`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
flags[name] = value;
|
|
71
|
+
} else {
|
|
72
|
+
const next = args[i + 1];
|
|
73
|
+
if (next === undefined || next.startsWith("-")) {
|
|
74
|
+
errors.push(
|
|
75
|
+
`Missing value for --${body}. Use --${body}=<value> or --${body} <value>.`,
|
|
76
|
+
);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
flags[body] = next;
|
|
80
|
+
i++;
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
positional.push(tok);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { flags, positional, errors };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Validation ─────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Validate a set of CLI-provided input values against a workflow's
|
|
94
|
+
* declared schema. Returns a list of human-readable error strings — the
|
|
95
|
+
* caller should print each on its own line and exit non-zero if any are
|
|
96
|
+
* returned.
|
|
97
|
+
*/
|
|
98
|
+
export function validateInputsAgainstSchema(
|
|
99
|
+
inputs: Record<string, string>,
|
|
100
|
+
schema: readonly WorkflowInput[],
|
|
101
|
+
): string[] {
|
|
102
|
+
const errors: string[] = [];
|
|
103
|
+
const known = new Set(schema.map((i) => i.name));
|
|
104
|
+
|
|
105
|
+
for (const field of schema) {
|
|
106
|
+
const raw = inputs[field.name];
|
|
107
|
+
const value =
|
|
108
|
+
raw === undefined || raw === ""
|
|
109
|
+
? field.default ?? (field.type === "enum" ? field.values?.[0] ?? "" : "")
|
|
110
|
+
: raw;
|
|
111
|
+
|
|
112
|
+
if (field.required) {
|
|
113
|
+
if (field.type === "enum") {
|
|
114
|
+
if (value === "") {
|
|
115
|
+
errors.push(
|
|
116
|
+
`Missing required input --${field.name} (expected one of: ${(field.values ?? []).join(", ")}).`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
} else if (value.trim() === "") {
|
|
120
|
+
errors.push(`Missing required input --${field.name}.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (field.type === "enum" && value !== "") {
|
|
125
|
+
const allowed = field.values ?? [];
|
|
126
|
+
if (!allowed.includes(value)) {
|
|
127
|
+
errors.push(
|
|
128
|
+
`Invalid value for --${field.name}: "${value}". ` +
|
|
129
|
+
`Expected one of: ${allowed.join(", ")}.`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const name of Object.keys(inputs)) {
|
|
136
|
+
if (!known.has(name)) {
|
|
137
|
+
errors.push(
|
|
138
|
+
`Unknown input --${name}. ` +
|
|
139
|
+
`Valid inputs: ${schema.length > 0 ? schema.map((i) => `--${i.name}`).join(", ") : "(none — this workflow takes a free-form prompt)"}.`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return errors;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Merge CLI-provided values with schema defaults so the executor sees a
|
|
149
|
+
* fully-resolved inputs record. Defaults for enum fields fall back to the
|
|
150
|
+
* first declared value when no explicit default is set. Unknown keys are
|
|
151
|
+
* dropped — validation has already flagged them.
|
|
152
|
+
*/
|
|
153
|
+
export function resolveInputs(
|
|
154
|
+
provided: Record<string, string>,
|
|
155
|
+
schema: readonly WorkflowInput[],
|
|
156
|
+
): Record<string, string> {
|
|
157
|
+
const out: Record<string, string> = {};
|
|
158
|
+
for (const field of schema) {
|
|
159
|
+
const raw = provided[field.name];
|
|
160
|
+
if (raw !== undefined && raw !== "") {
|
|
161
|
+
out[field.name] = raw;
|
|
162
|
+
} else if (field.default !== undefined) {
|
|
163
|
+
out[field.name] = field.default;
|
|
164
|
+
} else if (field.type === "enum" && field.values && field.values.length > 0) {
|
|
165
|
+
out[field.name] = field.values[0]!;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─── Entry point ────────────────────────────────────────────────────────────
|
|
22
172
|
|
|
23
173
|
export async function workflowCommand(options: {
|
|
24
174
|
name?: string;
|
|
25
175
|
agent?: string;
|
|
26
|
-
prompt?: string;
|
|
27
176
|
list?: boolean;
|
|
177
|
+
/**
|
|
178
|
+
* Everything commander parked in `cmd.args` — a mix of positional
|
|
179
|
+
* prompt tokens and unknown `--<name>` flags that the
|
|
180
|
+
* {@link parsePassthroughArgs} helper splits apart.
|
|
181
|
+
*/
|
|
182
|
+
passthroughArgs?: string[];
|
|
183
|
+
/**
|
|
184
|
+
* Project root used for workflow discovery. Defaults to
|
|
185
|
+
* `process.cwd()` in production; tests inject a temp dir so they
|
|
186
|
+
* can control which workflows are visible without touching the
|
|
187
|
+
* real filesystem.
|
|
188
|
+
*/
|
|
189
|
+
cwd?: string;
|
|
28
190
|
}): Promise<number> {
|
|
29
|
-
|
|
191
|
+
const passthroughArgs = options.passthroughArgs ?? [];
|
|
192
|
+
const cwd = options.cwd;
|
|
193
|
+
|
|
194
|
+
// ── List mode ──
|
|
195
|
+
// `merge: false` keeps local and global entries independent so the
|
|
196
|
+
// list can show both copies of a non-reserved name when they coexist
|
|
197
|
+
// on disk. Reserved builtin names are already filtered out of both
|
|
198
|
+
// merge modes inside `discoverWorkflows`, so shadowed local/global
|
|
199
|
+
// workflows never reach the renderer.
|
|
30
200
|
if (options.list) {
|
|
31
|
-
const workflows = await discoverWorkflows(
|
|
201
|
+
const workflows = await discoverWorkflows(
|
|
202
|
+
cwd,
|
|
203
|
+
options.agent as AgentType | undefined,
|
|
204
|
+
{ merge: false },
|
|
205
|
+
);
|
|
32
206
|
process.stdout.write(renderWorkflowList(workflows));
|
|
33
207
|
return 0;
|
|
34
208
|
}
|
|
35
209
|
|
|
36
|
-
//
|
|
37
|
-
if (!options.name) {
|
|
38
|
-
console.error(`${COLORS.red}Error: Missing workflow name. Use -n <name>.${COLORS.reset}`);
|
|
39
|
-
return 1;
|
|
40
|
-
}
|
|
41
|
-
|
|
210
|
+
// ── Agent validation (required for every non-list branch) ──
|
|
42
211
|
if (!options.agent) {
|
|
43
|
-
console.error(
|
|
212
|
+
console.error(
|
|
213
|
+
`${COLORS.red}Error: Missing agent. Use -a <agent>.${COLORS.reset}`,
|
|
214
|
+
);
|
|
44
215
|
return 1;
|
|
45
216
|
}
|
|
46
217
|
|
|
47
218
|
const validAgents = Object.keys(AGENT_CONFIG);
|
|
48
219
|
if (!validAgents.includes(options.agent)) {
|
|
49
|
-
console.error(
|
|
220
|
+
console.error(
|
|
221
|
+
`${COLORS.red}Error: Unknown agent '${options.agent}'.${COLORS.reset}`,
|
|
222
|
+
);
|
|
50
223
|
console.error(`Valid agents: ${validAgents.join(", ")}`);
|
|
51
224
|
return 1;
|
|
52
225
|
}
|
|
53
|
-
|
|
54
226
|
const agent = options.agent as AgentKey;
|
|
55
227
|
|
|
56
|
-
//
|
|
228
|
+
// ── Preflight checks (shared between picker and named modes) ──
|
|
229
|
+
const preflightCode = await runPrereqChecks(agent);
|
|
230
|
+
if (preflightCode !== 0) return preflightCode;
|
|
231
|
+
|
|
232
|
+
// ── Picker mode: -a <agent>, no -n ──
|
|
233
|
+
if (!options.name) {
|
|
234
|
+
return runPickerMode(agent, passthroughArgs, cwd);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Named mode: -n <name> -a <agent> [args...] ──
|
|
238
|
+
return runNamedMode(options.name, agent, passthroughArgs, cwd);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ─── Shared helpers ─────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Verify that the agent CLI, tmux (or psmux on Windows), and bun are all
|
|
245
|
+
* installed. Attempts best-effort installs for the latter two and
|
|
246
|
+
* returns a non-zero exit code if any check still fails afterwards.
|
|
247
|
+
*/
|
|
248
|
+
async function runPrereqChecks(agent: AgentKey): Promise<number> {
|
|
57
249
|
if (!isCommandInstalled(AGENT_CONFIG[agent].cmd)) {
|
|
58
|
-
console.error(
|
|
250
|
+
console.error(
|
|
251
|
+
`${COLORS.red}Error: '${AGENT_CONFIG[agent].cmd}' is not installed.${COLORS.reset}`,
|
|
252
|
+
);
|
|
59
253
|
console.error(`Install it from: ${AGENT_CONFIG[agent].install_url}`);
|
|
60
254
|
return 1;
|
|
61
255
|
}
|
|
62
256
|
|
|
63
|
-
// Ensure tmux/psmux is installed
|
|
64
257
|
if (!isTmuxInstalled()) {
|
|
65
258
|
console.log("Terminal multiplexer not found. Installing...");
|
|
66
259
|
try {
|
|
67
260
|
await ensureTmuxInstalled();
|
|
68
261
|
resetMuxBinaryCache();
|
|
69
262
|
} catch {
|
|
70
|
-
//
|
|
263
|
+
// Fall through to the check below — best effort.
|
|
71
264
|
}
|
|
72
265
|
if (!isTmuxInstalled()) {
|
|
73
266
|
const isWin = process.platform === "win32";
|
|
74
|
-
console.error(
|
|
267
|
+
console.error(
|
|
268
|
+
`${COLORS.red}Error: ${isWin ? "psmux" : "tmux"} is not installed.${COLORS.reset}`,
|
|
269
|
+
);
|
|
75
270
|
console.error(
|
|
76
271
|
isWin
|
|
77
272
|
? "Install psmux: https://github.com/psmux/psmux#installation"
|
|
@@ -81,43 +276,185 @@ export async function workflowCommand(options: {
|
|
|
81
276
|
}
|
|
82
277
|
}
|
|
83
278
|
|
|
84
|
-
// Ensure bun is installed (required for workflow execution)
|
|
85
279
|
if (!Bun.which("bun")) {
|
|
86
280
|
console.log("Bun runtime not found. Installing...");
|
|
87
281
|
try {
|
|
88
282
|
await ensureBunInstalled();
|
|
89
283
|
} catch {
|
|
90
|
-
//
|
|
284
|
+
// Best effort — fall through to the check below.
|
|
91
285
|
}
|
|
92
286
|
if (!Bun.which("bun")) {
|
|
93
|
-
console.error(
|
|
287
|
+
console.error(
|
|
288
|
+
`${COLORS.red}Error: bun is not installed.${COLORS.reset}`,
|
|
289
|
+
);
|
|
94
290
|
console.error("Install bun: https://bun.sh");
|
|
95
291
|
return 1;
|
|
96
292
|
}
|
|
97
293
|
}
|
|
98
294
|
|
|
295
|
+
return 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Run the given workflow definition through the executor, catching any
|
|
300
|
+
* execution errors so the CLI can exit with a non-zero code instead of
|
|
301
|
+
* letting an unhandled promise rejection bubble to `main()`.
|
|
302
|
+
*
|
|
303
|
+
* Free-form workflows ride the same `inputs` pipe — their positional
|
|
304
|
+
* prompt is stored under `inputs.prompt`, so workflow authors read it
|
|
305
|
+
* via `ctx.inputs.prompt ?? ""` whether or not the workflow declares
|
|
306
|
+
* a schema.
|
|
307
|
+
*/
|
|
308
|
+
async function runLoadedWorkflow(args: {
|
|
309
|
+
definition: Parameters<typeof executeWorkflow>[0]["definition"];
|
|
310
|
+
agent: AgentKey;
|
|
311
|
+
inputs: Record<string, string>;
|
|
312
|
+
workflowFile: string;
|
|
313
|
+
}): Promise<number> {
|
|
314
|
+
try {
|
|
315
|
+
await executeWorkflow({
|
|
316
|
+
definition: args.definition,
|
|
317
|
+
agent: args.agent,
|
|
318
|
+
inputs: args.inputs,
|
|
319
|
+
workflowFile: args.workflowFile,
|
|
320
|
+
});
|
|
321
|
+
return 0;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
324
|
+
console.error(`${COLORS.red}Workflow failed: ${message}${COLORS.reset}`);
|
|
325
|
+
return 1;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ─── Picker mode ────────────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Show the interactive picker, then hand off to the executor if the
|
|
333
|
+
* user confirms a selection. Passthrough args are rejected here — the
|
|
334
|
+
* picker already surfaces the same UI for typing values, so letting CLI
|
|
335
|
+
* flags leak through would create two conflicting sources of truth.
|
|
336
|
+
*/
|
|
337
|
+
async function runPickerMode(
|
|
338
|
+
agent: AgentKey,
|
|
339
|
+
passthroughArgs: string[],
|
|
340
|
+
cwd: string | undefined,
|
|
341
|
+
): Promise<number> {
|
|
342
|
+
if (passthroughArgs.length > 0) {
|
|
343
|
+
console.error(
|
|
344
|
+
`${COLORS.red}Error: unexpected arguments for the interactive picker: ${passthroughArgs.join(" ")}${COLORS.reset}`,
|
|
345
|
+
);
|
|
346
|
+
console.error(
|
|
347
|
+
`Pass workflow-specific flags only alongside -n <name>, or remove them to launch the picker.`,
|
|
348
|
+
);
|
|
349
|
+
return 1;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const discovered = await discoverWorkflows(cwd, agent);
|
|
353
|
+
if (discovered.length === 0) {
|
|
354
|
+
console.error(
|
|
355
|
+
`${COLORS.red}No workflows found for agent '${agent}'.${COLORS.reset}`,
|
|
356
|
+
);
|
|
357
|
+
console.error(
|
|
358
|
+
`Create one at: .atomic/workflows/<name>/${agent}/index.ts`,
|
|
359
|
+
);
|
|
360
|
+
return 1;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const metadata = await loadWorkflowsMetadata(discovered);
|
|
364
|
+
if (metadata.length === 0) {
|
|
365
|
+
console.error(
|
|
366
|
+
`${COLORS.red}All discovered workflows failed to load. Check the files under .atomic/workflows/ and ~/.atomic/workflows/.${COLORS.reset}`,
|
|
367
|
+
);
|
|
368
|
+
return 1;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Stable sort so the picker list order is deterministic.
|
|
372
|
+
metadata.sort((a, b) => a.name.localeCompare(b.name));
|
|
373
|
+
|
|
374
|
+
const panel = await WorkflowPickerPanel.create({ agent, workflows: metadata });
|
|
375
|
+
let result;
|
|
376
|
+
try {
|
|
377
|
+
result = await panel.waitForSelection();
|
|
378
|
+
} finally {
|
|
379
|
+
panel.destroy();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!result) {
|
|
383
|
+
return 0;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return runResolvedSelection(result.workflow, agent, result.inputs);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Execute a workflow selected via the picker. The picker already stores
|
|
391
|
+
* free-form prompts under the `prompt` key (via `DEFAULT_PROMPT_INPUT`),
|
|
392
|
+
* so we can hand the inputs record straight through — no split between
|
|
393
|
+
* "prompt" and "structured inputs" is needed.
|
|
394
|
+
*/
|
|
395
|
+
async function runResolvedSelection(
|
|
396
|
+
workflow: WorkflowWithMetadata,
|
|
397
|
+
agent: AgentKey,
|
|
398
|
+
inputs: Record<string, string>,
|
|
399
|
+
): Promise<number> {
|
|
400
|
+
const loaded = await WorkflowLoader.loadWorkflow(workflow, {
|
|
401
|
+
warn(warnings) {
|
|
402
|
+
for (const w of warnings) {
|
|
403
|
+
console.warn(`⚠ [${w.rule}] ${w.message}`);
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
error(stage, _error, message) {
|
|
407
|
+
console.error(`${COLORS.red}Error (${stage}): ${message}${COLORS.reset}`);
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
if (!loaded.ok) return 1;
|
|
411
|
+
|
|
412
|
+
return runLoadedWorkflow({
|
|
413
|
+
definition: loaded.value.definition,
|
|
414
|
+
agent,
|
|
415
|
+
inputs,
|
|
416
|
+
workflowFile: workflow.path,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ─── Named mode ─────────────────────────────────────────────────────────────
|
|
421
|
+
|
|
422
|
+
async function runNamedMode(
|
|
423
|
+
name: string,
|
|
424
|
+
agent: AgentKey,
|
|
425
|
+
passthroughArgs: string[],
|
|
426
|
+
cwd: string | undefined,
|
|
427
|
+
): Promise<number> {
|
|
99
428
|
// Find the workflow
|
|
100
|
-
const discovered = await findWorkflow(
|
|
429
|
+
const discovered = await findWorkflow(name, agent, cwd);
|
|
101
430
|
|
|
102
431
|
if (!discovered) {
|
|
103
|
-
console.error(
|
|
432
|
+
console.error(
|
|
433
|
+
`${COLORS.red}Error: Workflow '${name}' not found for agent '${agent}'.${COLORS.reset}`,
|
|
434
|
+
);
|
|
104
435
|
console.error(`\nExpected location:`);
|
|
105
|
-
console.error(
|
|
106
|
-
|
|
436
|
+
console.error(
|
|
437
|
+
` .atomic/workflows/${name}/${agent}/index.ts ${COLORS.dim}(local)${COLORS.reset}`,
|
|
438
|
+
);
|
|
439
|
+
console.error(
|
|
440
|
+
` ~/.atomic/workflows/${name}/${agent}/index.ts ${COLORS.dim}(global)${COLORS.reset}`,
|
|
441
|
+
);
|
|
107
442
|
|
|
108
|
-
const available = await discoverWorkflows(
|
|
443
|
+
const available = await discoverWorkflows(cwd, agent);
|
|
109
444
|
if (available.length > 0) {
|
|
110
445
|
console.error(`\nAvailable ${agent} workflows:`);
|
|
111
446
|
for (const wf of available) {
|
|
112
|
-
console.error(
|
|
447
|
+
console.error(
|
|
448
|
+
` ${COLORS.dim}•${COLORS.reset} ${wf.name} ${COLORS.dim}(${wf.source})${COLORS.reset}`,
|
|
449
|
+
);
|
|
113
450
|
}
|
|
114
451
|
}
|
|
115
452
|
|
|
116
453
|
return 1;
|
|
117
454
|
}
|
|
118
455
|
|
|
119
|
-
// Load workflow
|
|
120
|
-
//
|
|
456
|
+
// Load workflow so we can read the declared input schema before
|
|
457
|
+
// trusting any passthrough values.
|
|
121
458
|
const result = await WorkflowLoader.loadWorkflow(discovered, {
|
|
122
459
|
warn(warnings) {
|
|
123
460
|
for (const w of warnings) {
|
|
@@ -129,51 +466,81 @@ export async function workflowCommand(options: {
|
|
|
129
466
|
},
|
|
130
467
|
});
|
|
131
468
|
|
|
132
|
-
if (!result.ok)
|
|
469
|
+
if (!result.ok) return 1;
|
|
470
|
+
const definition = result.value.definition;
|
|
471
|
+
|
|
472
|
+
// Parse passthrough args into typed flags + positional tokens. The
|
|
473
|
+
// parser intentionally rejects only obviously-broken flags (e.g.
|
|
474
|
+
// `--foo` with nothing after it) — unknown flag names are surfaced
|
|
475
|
+
// later, in validateInputsAgainstSchema, so we can show the valid
|
|
476
|
+
// flag list alongside the error.
|
|
477
|
+
const { flags, positional, errors: parseErrors } =
|
|
478
|
+
parsePassthroughArgs(passthroughArgs);
|
|
479
|
+
if (parseErrors.length > 0) {
|
|
480
|
+
for (const e of parseErrors) {
|
|
481
|
+
console.error(`${COLORS.red}Error: ${e}${COLORS.reset}`);
|
|
482
|
+
}
|
|
133
483
|
return 1;
|
|
134
484
|
}
|
|
135
485
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
486
|
+
const isStructured = definition.inputs.length > 0;
|
|
487
|
+
|
|
488
|
+
if (isStructured) {
|
|
489
|
+
// Positional args are ambiguous for structured workflows — users
|
|
490
|
+
// must go through `--<name>` flags so the executor has a typed
|
|
491
|
+
// record to validate against.
|
|
492
|
+
if (positional.length > 0) {
|
|
493
|
+
console.error(
|
|
494
|
+
`${COLORS.red}Error: workflow '${definition.name}' takes structured inputs — ` +
|
|
495
|
+
`pass them as --<name>=<value> flags instead of a positional prompt.${COLORS.reset}`,
|
|
496
|
+
);
|
|
497
|
+
console.error(
|
|
498
|
+
`Expected flags: ${definition.inputs.map((i) => `--${i.name}`).join(", ")}`,
|
|
499
|
+
);
|
|
500
|
+
return 1;
|
|
501
|
+
}
|
|
502
|
+
const validationErrors = validateInputsAgainstSchema(flags, definition.inputs);
|
|
503
|
+
if (validationErrors.length > 0) {
|
|
504
|
+
for (const e of validationErrors) {
|
|
505
|
+
console.error(`${COLORS.red}Error: ${e}${COLORS.reset}`);
|
|
506
|
+
}
|
|
507
|
+
return 1;
|
|
508
|
+
}
|
|
509
|
+
const resolvedInputs = resolveInputs(flags, definition.inputs);
|
|
510
|
+
return runLoadedWorkflow({
|
|
511
|
+
definition,
|
|
140
512
|
agent,
|
|
141
|
-
|
|
513
|
+
inputs: resolvedInputs,
|
|
142
514
|
workflowFile: discovered.path,
|
|
143
515
|
});
|
|
144
|
-
return 0;
|
|
145
|
-
} catch (error) {
|
|
146
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
147
|
-
console.error(`${COLORS.red}Workflow failed: ${message}${COLORS.reset}`);
|
|
148
|
-
return 1;
|
|
149
516
|
}
|
|
150
|
-
}
|
|
151
517
|
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const PALETTE: Record<PaletteKey, readonly [number, number, number]> = {
|
|
164
|
-
text: [205, 214, 244], // #cdd6f4 — primary text
|
|
165
|
-
dim: [127, 132, 156], // #7f849c — secondary text
|
|
166
|
-
accent: [137, 180, 250], // #89b4fa — blue accent
|
|
167
|
-
success: [166, 227, 161], // #a6e3a1 — green (local source)
|
|
168
|
-
mauve: [203, 166, 247], // #cba6f7 — mauve (global source)
|
|
169
|
-
};
|
|
518
|
+
// Free-form workflows: reject stray --<flag> flags outright, since
|
|
519
|
+
// they have no schema to validate against.
|
|
520
|
+
if (Object.keys(flags).length > 0) {
|
|
521
|
+
console.error(
|
|
522
|
+
`${COLORS.red}Error: workflow '${definition.name}' has no declared inputs — unknown flags: ${Object.keys(flags).map((n) => `--${n}`).join(", ")}.${COLORS.reset}`,
|
|
523
|
+
);
|
|
524
|
+
console.error(
|
|
525
|
+
`Pass your request as a positional prompt: atomic workflow -n ${definition.name} -a ${agent} "your prompt"`,
|
|
526
|
+
);
|
|
527
|
+
return 1;
|
|
528
|
+
}
|
|
170
529
|
|
|
171
|
-
|
|
172
|
-
|
|
530
|
+
// Free-form workflows store their single prompt under the `prompt`
|
|
531
|
+
// key so workflow authors can read `ctx.inputs.prompt` uniformly.
|
|
532
|
+
// An empty positional list stays as an empty inputs record and
|
|
533
|
+
// `ctx.inputs.prompt` stays undefined.
|
|
534
|
+
const prompt = positional.join(" ");
|
|
535
|
+
const inputs: Record<string, string> = prompt === "" ? {} : { prompt };
|
|
536
|
+
return runLoadedWorkflow({
|
|
537
|
+
definition,
|
|
538
|
+
agent,
|
|
539
|
+
inputs,
|
|
540
|
+
workflowFile: discovered.path,
|
|
541
|
+
});
|
|
173
542
|
}
|
|
174
543
|
|
|
175
|
-
type Paint = (key: PaletteKey, text: string, opts?: PaintOptions) => string;
|
|
176
|
-
|
|
177
544
|
/** Stable agent sort order; keeps output deterministic across runs. */
|
|
178
545
|
const AGENT_ORDER: readonly AgentType[] = ["claude", "opencode", "copilot"];
|
|
179
546
|
/** Display names shown as provider sub-headings; honours proper branding. */
|
|
@@ -190,46 +557,16 @@ const SOURCE_DIRS: Record<DiscoveredWorkflow["source"], string> = {
|
|
|
190
557
|
global: "~/.atomic/workflows",
|
|
191
558
|
builtin: "built-in",
|
|
192
559
|
};
|
|
193
|
-
/** Section heading colour per source —
|
|
560
|
+
/** Section heading colour per source — three distinct hues so each
|
|
561
|
+
* source reads at a glance. `accent` (blue) is deliberately reserved
|
|
562
|
+
* for the agent-provider sub-headings nested inside each section, so
|
|
563
|
+
* builtin uses the new `info` (sky) key to avoid a clash. */
|
|
194
564
|
const SOURCE_COLORS: Record<DiscoveredWorkflow["source"], PaletteKey> = {
|
|
195
|
-
local: "success",
|
|
196
|
-
global: "mauve",
|
|
197
|
-
builtin: "
|
|
565
|
+
local: "success", // green — project-scoped, "yours"
|
|
566
|
+
global: "mauve", // purple — user-scoped, personal
|
|
567
|
+
builtin: "info", // sky — ships with atomic, foundational
|
|
198
568
|
};
|
|
199
569
|
|
|
200
|
-
/**
|
|
201
|
-
* Build a colour-aware painter for the current terminal.
|
|
202
|
-
* Truecolor terminals get the full Catppuccin palette; legacy terminals
|
|
203
|
-
* degrade to basic ANSI; NO_COLOR emits plain text. The optional `bold`
|
|
204
|
-
* flag adds weight contrast — essential for typographic hierarchy in a
|
|
205
|
-
* monospace medium where size and family are fixed.
|
|
206
|
-
*/
|
|
207
|
-
function createPainter(): Paint {
|
|
208
|
-
if (supportsTrueColor()) {
|
|
209
|
-
return (key, text, opts) => {
|
|
210
|
-
const [r, g, b] = PALETTE[key];
|
|
211
|
-
const sgr = opts?.bold
|
|
212
|
-
? `\x1b[1;38;2;${r};${g};${b}m`
|
|
213
|
-
: `\x1b[38;2;${r};${g};${b}m`;
|
|
214
|
-
return `${sgr}${text}\x1b[0m`;
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
if (supportsColor()) {
|
|
218
|
-
const ANSI: Record<PaletteKey, string> = {
|
|
219
|
-
text: "",
|
|
220
|
-
dim: "\x1b[2m",
|
|
221
|
-
accent: "\x1b[34m",
|
|
222
|
-
success: "\x1b[32m",
|
|
223
|
-
mauve: "\x1b[35m",
|
|
224
|
-
};
|
|
225
|
-
return (key, text, opts) => {
|
|
226
|
-
const weight = opts?.bold ? "\x1b[1m" : "";
|
|
227
|
-
return `${weight}${ANSI[key]}${text}\x1b[0m`;
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
return (_key, text) => text;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
570
|
/**
|
|
234
571
|
* Render `atomic workflow --list` output as a printable string.
|
|
235
572
|
*
|
|
@@ -253,8 +590,11 @@ function createPainter(): Paint {
|
|
|
253
590
|
* <name>
|
|
254
591
|
*
|
|
255
592
|
* run: atomic workflow -n <name> -a <agent>
|
|
593
|
+
*
|
|
594
|
+
* Exported for testing — the pure-function shape makes coverage for the
|
|
595
|
+
* renderer trivial without spinning up a full CLI invocation.
|
|
256
596
|
*/
|
|
257
|
-
function renderWorkflowList(workflows: DiscoveredWorkflow[]): string {
|
|
597
|
+
export function renderWorkflowList(workflows: DiscoveredWorkflow[]): string {
|
|
258
598
|
const paint = createPainter();
|
|
259
599
|
const lines: string[] = [];
|
|
260
600
|
|