@bastani/atomic 0.5.34-0 → 0.6.0-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 +329 -50
- package/dist/commands/cli/session.d.ts +67 -0
- package/dist/commands/cli/session.d.ts.map +1 -0
- package/dist/commands/cli/workflow-status.d.ts +63 -0
- package/dist/commands/cli/workflow-status.d.ts.map +1 -0
- package/dist/sdk/commander.d.ts +74 -0
- package/dist/sdk/commander.d.ts.map +1 -0
- package/dist/sdk/components/workflow-picker-panel.d.ts +14 -17
- package/dist/sdk/components/workflow-picker-panel.d.ts.map +1 -1
- package/dist/sdk/define-workflow.d.ts +18 -9
- package/dist/sdk/define-workflow.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +4 -3
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/management-commands.d.ts +42 -0
- package/dist/sdk/management-commands.d.ts.map +1 -0
- package/dist/sdk/registry.d.ts +27 -0
- package/dist/sdk/registry.d.ts.map +1 -0
- package/dist/sdk/runtime/attached-footer.d.ts +1 -1
- package/dist/sdk/runtime/executor-env.d.ts +20 -0
- package/dist/sdk/runtime/executor-env.d.ts.map +1 -0
- package/dist/sdk/runtime/executor.d.ts +61 -10
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/types.d.ts +147 -4
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/worker-shared.d.ts +42 -0
- package/dist/sdk/worker-shared.d.ts.map +1 -0
- package/dist/sdk/workflow-cli.d.ts +103 -0
- package/dist/sdk/workflow-cli.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin-registry.d.ts +113 -0
- package/dist/sdk/workflows/builtin-registry.d.ts.map +1 -0
- package/dist/sdk/workflows/index.d.ts +5 -5
- package/dist/sdk/workflows/index.d.ts.map +1 -1
- package/package.json +12 -8
- package/src/cli.ts +85 -144
- package/src/commands/cli/chat/index.ts +10 -0
- package/src/commands/cli/workflow-command.test.ts +279 -938
- package/src/commands/cli/workflow-inputs.test.ts +41 -11
- package/src/commands/cli/workflow-inputs.ts +47 -12
- package/src/commands/cli/workflow-list.test.ts +234 -0
- package/src/commands/cli/workflow-list.ts +0 -0
- package/src/commands/cli/workflow.ts +11 -798
- package/src/scripts/constants.ts +2 -1
- package/src/sdk/commander.ts +161 -0
- package/src/sdk/components/workflow-picker-panel.tsx +78 -258
- package/src/sdk/define-workflow.test.ts +104 -11
- package/src/sdk/define-workflow.ts +47 -11
- package/src/sdk/errors.test.ts +16 -0
- package/src/sdk/index.ts +8 -8
- package/src/sdk/management-commands.ts +151 -0
- package/src/sdk/registry.ts +132 -0
- package/src/sdk/runtime/attached-footer.ts +1 -1
- package/src/sdk/runtime/executor-env.ts +45 -0
- package/src/sdk/runtime/executor.test.ts +37 -0
- package/src/sdk/runtime/executor.ts +147 -68
- package/src/sdk/types.ts +169 -4
- package/src/sdk/worker-shared.test.ts +163 -0
- package/src/sdk/worker-shared.ts +155 -0
- package/src/sdk/workflow-cli.ts +409 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +1 -1
- package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +1 -1
- package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +1 -1
- package/src/sdk/workflows/builtin/open-claude-design/claude/index.ts +1 -1
- package/src/sdk/workflows/builtin/open-claude-design/copilot/index.ts +1 -1
- package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +1 -1
- package/src/sdk/workflows/builtin/ralph/claude/index.ts +1 -1
- package/src/sdk/workflows/builtin/ralph/copilot/index.ts +1 -1
- package/src/sdk/workflows/builtin/ralph/opencode/index.ts +1 -1
- package/src/sdk/workflows/builtin-registry.ts +23 -0
- package/src/sdk/workflows/index.ts +10 -20
- package/src/services/system/auth.test.ts +63 -1
- package/.agents/skills/workflow-creator/SKILL.md +0 -334
- package/.agents/skills/workflow-creator/references/agent-sessions.md +0 -888
- package/.agents/skills/workflow-creator/references/computation-and-validation.md +0 -201
- package/.agents/skills/workflow-creator/references/control-flow.md +0 -470
- package/.agents/skills/workflow-creator/references/discovery-and-verification.md +0 -232
- package/.agents/skills/workflow-creator/references/failure-modes.md +0 -903
- package/.agents/skills/workflow-creator/references/getting-started.md +0 -275
- package/.agents/skills/workflow-creator/references/running-workflows.md +0 -235
- package/.agents/skills/workflow-creator/references/session-config.md +0 -384
- package/.agents/skills/workflow-creator/references/state-and-data-flow.md +0 -357
- package/.agents/skills/workflow-creator/references/user-input.md +0 -234
- package/.agents/skills/workflow-creator/references/workflow-inputs.md +0 -272
- package/dist/sdk/runtime/discovery.d.ts +0 -132
- package/dist/sdk/runtime/discovery.d.ts.map +0 -1
- package/dist/sdk/runtime/executor-entry.d.ts +0 -11
- package/dist/sdk/runtime/executor-entry.d.ts.map +0 -1
- package/dist/sdk/runtime/loader.d.ts +0 -70
- package/dist/sdk/runtime/loader.d.ts.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.d.ts.map +0 -1
- package/src/commands/cli/workflow.test.ts +0 -317
- package/src/sdk/runtime/discovery.ts +0 -368
- package/src/sdk/runtime/executor-entry.ts +0 -18
- package/src/sdk/runtime/loader.ts +0 -267
|
@@ -1,803 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Workflow CLI command
|
|
2
|
+
* Workflow CLI command — thin delegation to the SDK WorkflowCli.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* atomic workflow -n <name> -a <agent> --<field>=<value> ...
|
|
8
|
-
* structured-input workflow
|
|
9
|
-
* atomic workflow -n <name> -a <agent> -d <args> run detached (background)
|
|
10
|
-
* atomic workflow list [-a <agent>] list discoverable workflows
|
|
4
|
+
* The Command returned here is mounted directly into the parent program
|
|
5
|
+
* (src/cli.ts), which attaches the `list`, `inputs`, `status`, and
|
|
6
|
+
* `session` subcommands on top of it.
|
|
11
7
|
*/
|
|
12
8
|
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { checkAgentAuth, printAuthError } from "../../services/system/auth.ts";
|
|
17
|
-
import { ensureTmuxInstalled, ensureBunInstalled } from "../../lib/spawn.ts";
|
|
18
|
-
import { ensureProjectSetup } from "./init/index.ts";
|
|
19
|
-
import { ensureAtomicGlobalAgentConfigs } from "../../services/config/atomic-global-config.ts";
|
|
20
|
-
import { getConfigRoot } from "../../services/config/config-path.ts";
|
|
21
|
-
import {
|
|
22
|
-
isTmuxInstalled,
|
|
23
|
-
discoverWorkflows,
|
|
24
|
-
findWorkflow,
|
|
25
|
-
loadWorkflowsMetadata,
|
|
26
|
-
executeWorkflow,
|
|
27
|
-
WorkflowLoader,
|
|
28
|
-
resetMuxBinaryCache,
|
|
29
|
-
} from "../../sdk/workflows/index.ts";
|
|
30
|
-
import type {
|
|
31
|
-
AgentType,
|
|
32
|
-
DiscoveredWorkflow,
|
|
33
|
-
WorkflowInput,
|
|
34
|
-
WorkflowMetadataStatus,
|
|
35
|
-
WorkflowWithMetadata,
|
|
36
|
-
} from "../../sdk/workflows/index.ts";
|
|
37
|
-
import { WorkflowPickerPanel } from "../../sdk/components/workflow-picker-panel.tsx";
|
|
9
|
+
import { createWorkflowCli } from "../../sdk/workflow-cli.ts";
|
|
10
|
+
import { toCommand } from "../../sdk/commander.ts";
|
|
11
|
+
import { createBuiltinRegistry } from "../../sdk/workflows/builtin-registry.ts";
|
|
38
12
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
* positional tokens (the latter get joined to form the free-form prompt).
|
|
44
|
-
*
|
|
45
|
-
* Accepts both `--name=value` and `--name value` forms, mirroring the
|
|
46
|
-
* conventions users already know from native agent CLIs. Flags whose
|
|
47
|
-
* values parse-fail (e.g. a trailing `--foo` with nothing after it) are
|
|
48
|
-
* returned as errors so the caller can print a clear usage hint rather
|
|
49
|
-
* than swallowing the mistake.
|
|
50
|
-
*
|
|
51
|
-
* Short flags (`-x value`) are treated as unknown and left in the
|
|
52
|
-
* positional bucket — we only recognise long-form `--<name>` flags as
|
|
53
|
-
* structured inputs.
|
|
54
|
-
*/
|
|
55
|
-
export function parsePassthroughArgs(args: string[]): {
|
|
56
|
-
flags: Record<string, string>;
|
|
57
|
-
positional: string[];
|
|
58
|
-
errors: string[];
|
|
59
|
-
} {
|
|
60
|
-
const flags: Record<string, string> = {};
|
|
61
|
-
const positional: string[] = [];
|
|
62
|
-
const errors: string[] = [];
|
|
63
|
-
|
|
64
|
-
for (let i = 0; i < args.length; i++) {
|
|
65
|
-
const tok = args[i]!;
|
|
66
|
-
if (tok.startsWith("--")) {
|
|
67
|
-
const body = tok.slice(2);
|
|
68
|
-
const eq = body.indexOf("=");
|
|
69
|
-
if (eq >= 0) {
|
|
70
|
-
const name = body.slice(0, eq);
|
|
71
|
-
const value = body.slice(eq + 1);
|
|
72
|
-
if (name === "") {
|
|
73
|
-
errors.push(`Malformed flag "${tok}" — expected --<name>=<value>.`);
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
flags[name] = value;
|
|
77
|
-
} else {
|
|
78
|
-
const next = args[i + 1];
|
|
79
|
-
if (next === undefined || next.startsWith("-")) {
|
|
80
|
-
errors.push(
|
|
81
|
-
`Missing value for --${body}. Use --${body}=<value> or --${body} <value>.`,
|
|
82
|
-
);
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
flags[body] = next;
|
|
86
|
-
i++;
|
|
87
|
-
}
|
|
88
|
-
} else {
|
|
89
|
-
positional.push(tok);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return { flags, positional, errors };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ─── Validation ─────────────────────────────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Validate a set of CLI-provided input values against a workflow's
|
|
100
|
-
* declared schema. Returns a list of human-readable error strings — the
|
|
101
|
-
* caller should print each on its own line and exit non-zero if any are
|
|
102
|
-
* returned.
|
|
103
|
-
*/
|
|
104
|
-
export function validateInputsAgainstSchema(
|
|
105
|
-
inputs: Record<string, string>,
|
|
106
|
-
schema: readonly WorkflowInput[],
|
|
107
|
-
): string[] {
|
|
108
|
-
const errors: string[] = [];
|
|
109
|
-
const known = new Set(schema.map((i) => i.name));
|
|
110
|
-
|
|
111
|
-
for (const field of schema) {
|
|
112
|
-
const raw = inputs[field.name];
|
|
113
|
-
const defaultStr =
|
|
114
|
-
field.default !== undefined ? String(field.default) : undefined;
|
|
115
|
-
const value =
|
|
116
|
-
raw === undefined || raw === ""
|
|
117
|
-
? defaultStr ?? (field.type === "enum" ? field.values?.[0] ?? "" : "")
|
|
118
|
-
: raw;
|
|
119
|
-
|
|
120
|
-
if (field.required) {
|
|
121
|
-
if (field.type === "enum") {
|
|
122
|
-
if (value === "") {
|
|
123
|
-
errors.push(
|
|
124
|
-
`Missing required input --${field.name} (expected one of: ${(field.values ?? []).join(", ")}).`,
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
} else if (value.trim() === "") {
|
|
128
|
-
errors.push(`Missing required input --${field.name}.`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (field.type === "enum" && value !== "") {
|
|
133
|
-
const allowed = field.values ?? [];
|
|
134
|
-
if (!allowed.includes(value)) {
|
|
135
|
-
errors.push(
|
|
136
|
-
`Invalid value for --${field.name}: "${value}". ` +
|
|
137
|
-
`Expected one of: ${allowed.join(", ")}.`,
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (field.type === "integer" && value !== "") {
|
|
143
|
-
const parsed = Number.parseInt(value, 10);
|
|
144
|
-
if (
|
|
145
|
-
!Number.isFinite(parsed) ||
|
|
146
|
-
!Number.isInteger(parsed) ||
|
|
147
|
-
String(parsed) !== value.trim()
|
|
148
|
-
) {
|
|
149
|
-
errors.push(
|
|
150
|
-
`Invalid value for --${field.name}: "${value}". Expected an integer.`,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
for (const name of Object.keys(inputs)) {
|
|
157
|
-
if (!known.has(name)) {
|
|
158
|
-
errors.push(
|
|
159
|
-
`Unknown input --${name}. ` +
|
|
160
|
-
`Valid inputs: ${schema.length > 0 ? schema.map((i) => `--${i.name}`).join(", ") : "(none — this workflow takes a free-form prompt)"}.`,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return errors;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Merge CLI-provided values with schema defaults so the executor sees a
|
|
170
|
-
* fully-resolved inputs record. Defaults for enum fields fall back to the
|
|
171
|
-
* first declared value when no explicit default is set. Unknown keys are
|
|
172
|
-
* dropped — validation has already flagged them.
|
|
173
|
-
*/
|
|
174
|
-
export function resolveInputs(
|
|
175
|
-
provided: Record<string, string>,
|
|
176
|
-
schema: readonly WorkflowInput[],
|
|
177
|
-
): Record<string, string> {
|
|
178
|
-
const out: Record<string, string> = {};
|
|
179
|
-
for (const field of schema) {
|
|
180
|
-
const raw = provided[field.name];
|
|
181
|
-
if (raw !== undefined && raw !== "") {
|
|
182
|
-
out[field.name] = raw;
|
|
183
|
-
} else if (field.default !== undefined) {
|
|
184
|
-
out[field.name] = String(field.default);
|
|
185
|
-
} else if (field.type === "enum" && field.values && field.values.length > 0) {
|
|
186
|
-
out[field.name] = field.values[0]!;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return out;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// ─── Entry point ────────────────────────────────────────────────────────────
|
|
193
|
-
|
|
194
|
-
export async function workflowCommand(options: {
|
|
195
|
-
name?: string;
|
|
196
|
-
agent?: string;
|
|
197
|
-
list?: boolean;
|
|
198
|
-
/**
|
|
199
|
-
* When true, create the tmux session and return immediately without
|
|
200
|
-
* attaching. Callers can use `atomic workflow session connect <id>`
|
|
201
|
-
* to attach later. Useful for scripting / background automation.
|
|
202
|
-
*/
|
|
203
|
-
detach?: boolean;
|
|
204
|
-
/**
|
|
205
|
-
* Everything commander parked in `cmd.args` — a mix of positional
|
|
206
|
-
* prompt tokens and unknown `--<name>` flags that the
|
|
207
|
-
* {@link parsePassthroughArgs} helper splits apart.
|
|
208
|
-
*/
|
|
209
|
-
passthroughArgs?: string[];
|
|
210
|
-
/**
|
|
211
|
-
* Project root used for workflow discovery. Defaults to
|
|
212
|
-
* `process.cwd()` in production; tests inject a temp dir so they
|
|
213
|
-
* can control which workflows are visible without touching the
|
|
214
|
-
* real filesystem.
|
|
215
|
-
*/
|
|
216
|
-
cwd?: string;
|
|
217
|
-
}): Promise<number> {
|
|
218
|
-
const passthroughArgs = options.passthroughArgs ?? [];
|
|
219
|
-
const cwd = options.cwd;
|
|
220
|
-
|
|
221
|
-
// `ATOMIC_AGENT` is set by `atomic chat` and `atomic workflow` on the tmux
|
|
222
|
-
// session they create, so every pane in that session inherits it. Its
|
|
223
|
-
// presence is a reliable signal that this command is being invoked from
|
|
224
|
-
// inside an atomic-managed session (i.e., the caller is an agent running
|
|
225
|
-
// in a chat or a workflow pane rather than a plain shell). We use that in
|
|
226
|
-
// two places:
|
|
227
|
-
// 1. If `-a` was omitted, fall back to ATOMIC_AGENT so agents don't have
|
|
228
|
-
// to pass their own provider back to themselves.
|
|
229
|
-
// 2. Force `detach = true`, because attaching from inside the atomic
|
|
230
|
-
// socket would `switch-client` the caller's own terminal onto the new
|
|
231
|
-
// session — i.e., the agent would hijack the user's view. Detach is
|
|
232
|
-
// always the safe choice here; the CLI prints attach hints so the
|
|
233
|
-
// user can switch to the workflow whenever they want.
|
|
234
|
-
const atomicAgentEnv = process.env.ATOMIC_AGENT;
|
|
235
|
-
const insideAtomicSession = atomicAgentEnv !== undefined && atomicAgentEnv !== "";
|
|
236
|
-
const detach = insideAtomicSession ? true : (options.detach ?? false);
|
|
237
|
-
|
|
238
|
-
// ── List mode ──
|
|
239
|
-
// `merge: false` keeps local and global entries independent so the
|
|
240
|
-
// list can show both copies of a non-reserved name when they coexist
|
|
241
|
-
// on disk. Reserved builtin names are already filtered out of both
|
|
242
|
-
// merge modes inside `discoverWorkflows`, so shadowed local/global
|
|
243
|
-
// workflows never reach the renderer.
|
|
244
|
-
if (options.list) {
|
|
245
|
-
const discovered = await discoverWorkflows(
|
|
246
|
-
cwd,
|
|
247
|
-
options.agent as AgentType | undefined,
|
|
248
|
-
{ merge: false },
|
|
249
|
-
);
|
|
250
|
-
// Keep workflows that failed to load in the list — their status is
|
|
251
|
-
// surfaced inline as a "needs update" or "broken" tag so the user
|
|
252
|
-
// can see that a workflow still exists on disk even after an SDK
|
|
253
|
-
// bump invalidated it. Silent filtering was the original cause of
|
|
254
|
-
// the "workflow vanished after upgrade" report.
|
|
255
|
-
const workflows = await loadWorkflowsMetadata(discovered);
|
|
256
|
-
process.stdout.write(renderWorkflowList(workflows));
|
|
257
|
-
return 0;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// ── Agent validation (required for every non-list branch) ──
|
|
261
|
-
// Explicit `-a` wins; otherwise fall back to the ATOMIC_AGENT env var so
|
|
262
|
-
// workflows launched from inside an atomic chat/workflow session don't
|
|
263
|
-
// need to re-specify the provider they're already running under.
|
|
264
|
-
const agentInput = options.agent ?? atomicAgentEnv;
|
|
265
|
-
if (!agentInput) {
|
|
266
|
-
console.error(
|
|
267
|
-
`${COLORS.red}Error: Missing agent. Use -a <agent>.${COLORS.reset}`,
|
|
268
|
-
);
|
|
269
|
-
return 1;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const validAgents = Object.keys(AGENT_CONFIG);
|
|
273
|
-
if (!validAgents.includes(agentInput)) {
|
|
274
|
-
console.error(
|
|
275
|
-
`${COLORS.red}Error: Unknown agent '${agentInput}'.${COLORS.reset}`,
|
|
276
|
-
);
|
|
277
|
-
console.error(`Valid agents: ${validAgents.join(", ")}`);
|
|
278
|
-
return 1;
|
|
279
|
-
}
|
|
280
|
-
const agent = agentInput as AgentKey;
|
|
281
|
-
|
|
282
|
-
// ── Preflight checks (shared between picker and named modes) ──
|
|
283
|
-
const preflightCode = await runPrereqChecks(agent);
|
|
284
|
-
if (preflightCode !== 0) return preflightCode;
|
|
285
|
-
|
|
286
|
-
// ── Preflight: global config sync + project onboarding files ──
|
|
287
|
-
// Mirrors `atomic chat` so workflow runs see the same MCP configs,
|
|
288
|
-
// agent settings, and global agent folders the chat command auto-heals.
|
|
289
|
-
const projectRoot = cwd ?? process.cwd();
|
|
290
|
-
await ensureAtomicGlobalAgentConfigs(getConfigRoot());
|
|
291
|
-
await ensureProjectSetup(agent, projectRoot);
|
|
292
|
-
|
|
293
|
-
// ── Picker mode: -a <agent>, no -n ──
|
|
294
|
-
if (!options.name) {
|
|
295
|
-
return runPickerMode(agent, passthroughArgs, cwd, detach);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// ── Named mode: -n <name> -a <agent> [args...] ──
|
|
299
|
-
return runNamedMode(options.name, agent, passthroughArgs, cwd, detach);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// ─── Shared helpers ─────────────────────────────────────────────────────────
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Verify that the agent CLI, tmux (or psmux on Windows), and bun are all
|
|
306
|
-
* installed. Attempts best-effort installs for the latter two and
|
|
307
|
-
* returns a non-zero exit code if any check still fails afterwards.
|
|
308
|
-
*/
|
|
309
|
-
async function runPrereqChecks(agent: AgentKey): Promise<number> {
|
|
310
|
-
if (!isCommandInstalled(AGENT_CONFIG[agent].cmd)) {
|
|
311
|
-
console.error(
|
|
312
|
-
`${COLORS.red}Error: '${AGENT_CONFIG[agent].cmd}' is not installed.${COLORS.reset}`,
|
|
313
|
-
);
|
|
314
|
-
console.error(`Install it from: ${AGENT_CONFIG[agent].install_url}`);
|
|
315
|
-
return 1;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Fail fast when the SDK reports the user isn't authenticated —
|
|
319
|
-
// otherwise the workflow spawns the agent CLI in a detached tmux pane
|
|
320
|
-
// and silently stalls on a login screen the user never sees.
|
|
321
|
-
const auth = await checkAgentAuth(agent);
|
|
322
|
-
if (!auth.loggedIn) {
|
|
323
|
-
printAuthError(agent, auth);
|
|
324
|
-
return 1;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (!isTmuxInstalled()) {
|
|
328
|
-
console.log("Terminal multiplexer not found. Installing...");
|
|
329
|
-
try {
|
|
330
|
-
await ensureTmuxInstalled();
|
|
331
|
-
resetMuxBinaryCache();
|
|
332
|
-
} catch {
|
|
333
|
-
// Fall through to the check below — best effort.
|
|
334
|
-
}
|
|
335
|
-
if (!isTmuxInstalled()) {
|
|
336
|
-
const isWin = process.platform === "win32";
|
|
337
|
-
console.error(
|
|
338
|
-
`${COLORS.red}Error: ${isWin ? "psmux" : "tmux"} is not installed.${COLORS.reset}`,
|
|
339
|
-
);
|
|
340
|
-
console.error(
|
|
341
|
-
isWin
|
|
342
|
-
? "Install psmux: https://github.com/psmux/psmux#installation"
|
|
343
|
-
: "Install tmux: https://github.com/tmux/tmux/wiki/Installing",
|
|
344
|
-
);
|
|
345
|
-
return 1;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (!Bun.which("bun")) {
|
|
350
|
-
console.log("Bun runtime not found. Installing...");
|
|
351
|
-
try {
|
|
352
|
-
await ensureBunInstalled();
|
|
353
|
-
} catch {
|
|
354
|
-
// Best effort — fall through to the check below.
|
|
355
|
-
}
|
|
356
|
-
if (!Bun.which("bun")) {
|
|
357
|
-
console.error(
|
|
358
|
-
`${COLORS.red}Error: bun is not installed.${COLORS.reset}`,
|
|
359
|
-
);
|
|
360
|
-
console.error("Install bun: https://bun.sh");
|
|
361
|
-
return 1;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
return 0;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Run the given workflow definition through the executor, catching any
|
|
370
|
-
* execution errors so the CLI can exit with a non-zero code instead of
|
|
371
|
-
* letting an unhandled promise rejection bubble to `main()`.
|
|
372
|
-
*
|
|
373
|
-
* Free-form workflows ride the same `inputs` pipe — their positional
|
|
374
|
-
* prompt is stored under `inputs.prompt`, so workflow authors read it
|
|
375
|
-
* via `ctx.inputs.prompt ?? ""` whether or not the workflow declares
|
|
376
|
-
* a schema.
|
|
377
|
-
*/
|
|
378
|
-
async function runLoadedWorkflow(args: {
|
|
379
|
-
definition: Parameters<typeof executeWorkflow>[0]["definition"];
|
|
380
|
-
agent: AgentKey;
|
|
381
|
-
inputs: Record<string, string>;
|
|
382
|
-
workflowFile: string;
|
|
383
|
-
detach: boolean;
|
|
384
|
-
}): Promise<number> {
|
|
385
|
-
try {
|
|
386
|
-
await executeWorkflow({
|
|
387
|
-
definition: args.definition,
|
|
388
|
-
agent: args.agent,
|
|
389
|
-
inputs: args.inputs,
|
|
390
|
-
workflowFile: args.workflowFile,
|
|
391
|
-
detach: args.detach,
|
|
392
|
-
});
|
|
393
|
-
return 0;
|
|
394
|
-
} catch (error) {
|
|
395
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
396
|
-
console.error(`${COLORS.red}Workflow failed: ${message}${COLORS.reset}`);
|
|
397
|
-
return 1;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// ─── Picker mode ────────────────────────────────────────────────────────────
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Show the interactive picker, then hand off to the executor if the
|
|
405
|
-
* user confirms a selection. Passthrough args are rejected here — the
|
|
406
|
-
* picker already surfaces the same UI for typing values, so letting CLI
|
|
407
|
-
* flags leak through would create two conflicting sources of truth.
|
|
408
|
-
*/
|
|
409
|
-
async function runPickerMode(
|
|
410
|
-
agent: AgentKey,
|
|
411
|
-
passthroughArgs: string[],
|
|
412
|
-
cwd: string | undefined,
|
|
413
|
-
detach: boolean,
|
|
414
|
-
): Promise<number> {
|
|
415
|
-
if (passthroughArgs.length > 0) {
|
|
416
|
-
console.error(
|
|
417
|
-
`${COLORS.red}Error: unexpected arguments for the interactive picker: ${passthroughArgs.join(" ")}${COLORS.reset}`,
|
|
418
|
-
);
|
|
419
|
-
console.error(
|
|
420
|
-
`Pass workflow-specific flags only alongside -n <name>, or remove them to launch the picker.`,
|
|
421
|
-
);
|
|
422
|
-
return 1;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const discovered = await discoverWorkflows(cwd, agent);
|
|
426
|
-
if (discovered.length === 0) {
|
|
427
|
-
console.error(
|
|
428
|
-
`${COLORS.red}No workflows found for agent '${agent}'.${COLORS.reset}`,
|
|
429
|
-
);
|
|
430
|
-
console.error(
|
|
431
|
-
`Create one at: .atomic/workflows/<name>/${agent}/index.ts`,
|
|
432
|
-
);
|
|
433
|
-
return 1;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const metadata = await loadWorkflowsMetadata(discovered);
|
|
437
|
-
if (metadata.length === 0) {
|
|
438
|
-
console.error(
|
|
439
|
-
`${COLORS.red}All discovered workflows failed to load. Check the files under .atomic/workflows/ and ~/.atomic/workflows/.${COLORS.reset}`,
|
|
440
|
-
);
|
|
441
|
-
return 1;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// Stable sort so the picker list order is deterministic.
|
|
445
|
-
metadata.sort((a, b) => a.name.localeCompare(b.name));
|
|
446
|
-
|
|
447
|
-
const panel = await WorkflowPickerPanel.create({ agent, workflows: metadata });
|
|
448
|
-
let result;
|
|
449
|
-
try {
|
|
450
|
-
result = await panel.waitForSelection();
|
|
451
|
-
} finally {
|
|
452
|
-
panel.destroy();
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (!result) {
|
|
456
|
-
return 0;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
return runResolvedSelection(result.workflow, agent, result.inputs, detach);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* Execute a workflow selected via the picker. The picker already stores
|
|
464
|
-
* free-form prompts under the canonical `prompt` key,
|
|
465
|
-
* so we can hand the inputs record straight through — no split between
|
|
466
|
-
* "prompt" and "structured inputs" is needed.
|
|
467
|
-
*/
|
|
468
|
-
async function runResolvedSelection(
|
|
469
|
-
workflow: WorkflowWithMetadata,
|
|
470
|
-
agent: AgentKey,
|
|
471
|
-
inputs: Record<string, string>,
|
|
472
|
-
detach: boolean,
|
|
473
|
-
): Promise<number> {
|
|
474
|
-
const loaded = await WorkflowLoader.loadWorkflow(workflow, {
|
|
475
|
-
warn(warnings) {
|
|
476
|
-
for (const w of warnings) {
|
|
477
|
-
console.warn(`⚠ [${w.rule}] ${w.message}`);
|
|
478
|
-
}
|
|
479
|
-
},
|
|
480
|
-
error(stage, _error, message) {
|
|
481
|
-
console.error(`${COLORS.red}Error (${stage}): ${message}${COLORS.reset}`);
|
|
482
|
-
},
|
|
483
|
-
});
|
|
484
|
-
if (!loaded.ok) return 1;
|
|
485
|
-
|
|
486
|
-
return runLoadedWorkflow({
|
|
487
|
-
definition: loaded.value.definition,
|
|
488
|
-
agent,
|
|
489
|
-
inputs,
|
|
490
|
-
workflowFile: workflow.path,
|
|
491
|
-
detach,
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// ─── Named mode ─────────────────────────────────────────────────────────────
|
|
496
|
-
|
|
497
|
-
async function runNamedMode(
|
|
498
|
-
name: string,
|
|
499
|
-
agent: AgentKey,
|
|
500
|
-
passthroughArgs: string[],
|
|
501
|
-
cwd: string | undefined,
|
|
502
|
-
detach: boolean,
|
|
503
|
-
): Promise<number> {
|
|
504
|
-
// Find the workflow
|
|
505
|
-
const discovered = await findWorkflow(name, agent, cwd);
|
|
506
|
-
|
|
507
|
-
if (!discovered) {
|
|
508
|
-
console.error(
|
|
509
|
-
`${COLORS.red}Error: Workflow '${name}' not found for agent '${agent}'.${COLORS.reset}`,
|
|
510
|
-
);
|
|
511
|
-
console.error(`\nExpected location:`);
|
|
512
|
-
console.error(
|
|
513
|
-
` .atomic/workflows/${name}/${agent}/index.ts ${COLORS.dim}(local)${COLORS.reset}`,
|
|
514
|
-
);
|
|
515
|
-
console.error(
|
|
516
|
-
` ~/.atomic/workflows/${name}/${agent}/index.ts ${COLORS.dim}(global)${COLORS.reset}`,
|
|
517
|
-
);
|
|
518
|
-
|
|
519
|
-
// Only suggest runnable alternatives — broken/incompatible workflows
|
|
520
|
-
// are visible via `atomic workflow -l` where their status is surfaced;
|
|
521
|
-
// listing them here would mask the real problem (the name the user
|
|
522
|
-
// typed does not exist) behind a dead-end suggestion.
|
|
523
|
-
const available = (
|
|
524
|
-
await loadWorkflowsMetadata(await discoverWorkflows(cwd, agent))
|
|
525
|
-
).filter((w) => w.status.kind === "ok");
|
|
526
|
-
if (available.length > 0) {
|
|
527
|
-
console.error(`\nAvailable ${agent} workflows:`);
|
|
528
|
-
for (const wf of available) {
|
|
529
|
-
console.error(
|
|
530
|
-
` ${COLORS.dim}•${COLORS.reset} ${wf.name} ${COLORS.dim}(${wf.source})${COLORS.reset}`,
|
|
531
|
-
);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
return 1;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Load workflow so we can read the declared input schema before
|
|
539
|
-
// trusting any passthrough values.
|
|
540
|
-
const result = await WorkflowLoader.loadWorkflow(discovered, {
|
|
541
|
-
warn(warnings) {
|
|
542
|
-
for (const w of warnings) {
|
|
543
|
-
console.warn(`⚠ [${w.rule}] ${w.message}`);
|
|
544
|
-
}
|
|
545
|
-
},
|
|
546
|
-
error(stage, _error, message) {
|
|
547
|
-
console.error(`${COLORS.red}Error (${stage}): ${message}${COLORS.reset}`);
|
|
548
|
-
},
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
if (!result.ok) return 1;
|
|
552
|
-
const definition = result.value.definition;
|
|
553
|
-
|
|
554
|
-
// Parse passthrough args into typed flags + positional tokens. The
|
|
555
|
-
// parser intentionally rejects only obviously-broken flags (e.g.
|
|
556
|
-
// `--foo` with nothing after it) — unknown flag names are surfaced
|
|
557
|
-
// later, in validateInputsAgainstSchema, so we can show the valid
|
|
558
|
-
// flag list alongside the error.
|
|
559
|
-
const { flags, positional, errors: parseErrors } =
|
|
560
|
-
parsePassthroughArgs(passthroughArgs);
|
|
561
|
-
if (parseErrors.length > 0) {
|
|
562
|
-
for (const e of parseErrors) {
|
|
563
|
-
console.error(`${COLORS.red}Error: ${e}${COLORS.reset}`);
|
|
564
|
-
}
|
|
565
|
-
return 1;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
const isStructured = definition.inputs.length > 0;
|
|
569
|
-
|
|
570
|
-
if (isStructured) {
|
|
571
|
-
// Positional args are ambiguous for structured workflows — users
|
|
572
|
-
// must go through `--<name>` flags so the executor has a typed
|
|
573
|
-
// record to validate against.
|
|
574
|
-
if (positional.length > 0) {
|
|
575
|
-
console.error(
|
|
576
|
-
`${COLORS.red}Error: workflow '${definition.name}' takes structured inputs — ` +
|
|
577
|
-
`pass them as --<name>=<value> flags instead of a positional prompt.${COLORS.reset}`,
|
|
578
|
-
);
|
|
579
|
-
console.error(
|
|
580
|
-
`Expected flags: ${definition.inputs.map((i) => `--${i.name}`).join(", ")}`,
|
|
581
|
-
);
|
|
582
|
-
return 1;
|
|
583
|
-
}
|
|
584
|
-
const validationErrors = validateInputsAgainstSchema(flags, definition.inputs);
|
|
585
|
-
if (validationErrors.length > 0) {
|
|
586
|
-
for (const e of validationErrors) {
|
|
587
|
-
console.error(`${COLORS.red}Error: ${e}${COLORS.reset}`);
|
|
588
|
-
}
|
|
589
|
-
return 1;
|
|
590
|
-
}
|
|
591
|
-
const resolvedInputs = resolveInputs(flags, definition.inputs);
|
|
592
|
-
return runLoadedWorkflow({
|
|
593
|
-
definition,
|
|
594
|
-
agent,
|
|
595
|
-
inputs: resolvedInputs,
|
|
596
|
-
workflowFile: discovered.path,
|
|
597
|
-
detach,
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Free-form workflows: reject stray --<flag> flags outright, since
|
|
602
|
-
// they have no schema to validate against.
|
|
603
|
-
if (Object.keys(flags).length > 0) {
|
|
604
|
-
console.error(
|
|
605
|
-
`${COLORS.red}Error: workflow '${definition.name}' has no declared inputs — unknown flags: ${Object.keys(flags).map((n) => `--${n}`).join(", ")}.${COLORS.reset}`,
|
|
606
|
-
);
|
|
607
|
-
console.error(
|
|
608
|
-
`Pass your request as a positional prompt: atomic workflow -n ${definition.name} -a ${agent} "your prompt"`,
|
|
609
|
-
);
|
|
610
|
-
return 1;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// Free-form workflows store their single prompt under the `prompt`
|
|
614
|
-
// key so workflow authors can read `ctx.inputs.prompt` uniformly.
|
|
615
|
-
// An empty positional list stays as an empty inputs record and
|
|
616
|
-
// `ctx.inputs.prompt` stays undefined.
|
|
617
|
-
const prompt = positional.join(" ");
|
|
618
|
-
const inputs: Record<string, string> = prompt === "" ? {} : { prompt };
|
|
619
|
-
return runLoadedWorkflow({
|
|
620
|
-
definition,
|
|
621
|
-
agent,
|
|
622
|
-
inputs,
|
|
623
|
-
workflowFile: discovered.path,
|
|
624
|
-
detach,
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/** Stable agent sort order; keeps output deterministic across runs. */
|
|
629
|
-
const AGENT_ORDER: readonly AgentType[] = ["claude", "opencode", "copilot"];
|
|
630
|
-
/** Display names shown as provider sub-headings; honours proper branding. */
|
|
631
|
-
const AGENT_DISPLAY_NAMES: Record<AgentType, string> = {
|
|
632
|
-
claude: "Claude",
|
|
633
|
-
opencode: "OpenCode",
|
|
634
|
-
copilot: "Copilot CLI",
|
|
635
|
-
};
|
|
636
|
-
/** Local first — project-scoped workflows are the most immediately relevant. */
|
|
637
|
-
const SOURCE_ORDER: readonly DiscoveredWorkflow["source"][] = ["local", "global", "builtin"];
|
|
638
|
-
/** Friendly directory labels shown inline with each section heading. */
|
|
639
|
-
const SOURCE_DIRS: Record<DiscoveredWorkflow["source"], string> = {
|
|
640
|
-
local: ".atomic/workflows",
|
|
641
|
-
global: "~/.atomic/workflows",
|
|
642
|
-
builtin: "built-in",
|
|
643
|
-
};
|
|
644
|
-
/** Section heading colour per source — three distinct hues so each
|
|
645
|
-
* source reads at a glance. `accent` (blue) is deliberately reserved
|
|
646
|
-
* for the agent-provider sub-headings nested inside each section, so
|
|
647
|
-
* builtin uses the new `info` (sky) key to avoid a clash. */
|
|
648
|
-
const SOURCE_COLORS: Record<DiscoveredWorkflow["source"], PaletteKey> = {
|
|
649
|
-
local: "success", // green — project-scoped, "yours"
|
|
650
|
-
global: "mauve", // purple — user-scoped, personal
|
|
651
|
-
builtin: "info", // sky — ships with atomic, foundational
|
|
652
|
-
};
|
|
653
|
-
|
|
654
|
-
/**
|
|
655
|
-
* Per-row status badge shown in `atomic workflow -l` output. `ok` rows
|
|
656
|
-
* render with no badge (the list is already dense; flagging only
|
|
657
|
-
* non-ok rows keeps the happy path untouched). Incompatible rows
|
|
658
|
-
* include the required version so the user can compare at a glance;
|
|
659
|
-
* error rows stay terse and defer detail to `atomic workflow -n
|
|
660
|
-
* <name>` which surfaces the structured loader message.
|
|
661
|
-
*/
|
|
662
|
-
function renderStatusBadge(
|
|
663
|
-
paint: ReturnType<typeof createPainter>,
|
|
664
|
-
status: WorkflowMetadataStatus,
|
|
665
|
-
): string {
|
|
666
|
-
if (status.kind === "ok") return "";
|
|
667
|
-
if (status.kind === "incompatible") {
|
|
668
|
-
return (
|
|
669
|
-
" " +
|
|
670
|
-
paint("warning", "⚠ needs v" + status.requiredVersion) +
|
|
671
|
-
paint("dim", ` (installed v${status.currentVersion})`)
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
return " " + paint("error", "✗ broken");
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
/**
|
|
678
|
-
* Render `atomic workflow --list` output as a printable string.
|
|
679
|
-
*
|
|
680
|
-
* Three-level hierarchy: source → provider → workflow name.
|
|
681
|
-
*
|
|
682
|
-
* Layout:
|
|
683
|
-
* N workflows
|
|
684
|
-
*
|
|
685
|
-
* local (.atomic/workflows)
|
|
686
|
-
*
|
|
687
|
-
* Claude
|
|
688
|
-
* <name>
|
|
689
|
-
* <name>
|
|
690
|
-
*
|
|
691
|
-
* OpenCode
|
|
692
|
-
* <name>
|
|
693
|
-
*
|
|
694
|
-
* global (~/.atomic/workflows)
|
|
695
|
-
*
|
|
696
|
-
* Claude
|
|
697
|
-
* <name>
|
|
698
|
-
*
|
|
699
|
-
* run: atomic workflow -n <name> -a <agent>
|
|
700
|
-
*
|
|
701
|
-
* Exported for testing — the pure-function shape makes coverage for the
|
|
702
|
-
* renderer trivial without spinning up a full CLI invocation.
|
|
703
|
-
*/
|
|
704
|
-
export function renderWorkflowList(workflows: WorkflowWithMetadata[]): string {
|
|
705
|
-
const paint = createPainter();
|
|
706
|
-
const lines: string[] = [];
|
|
707
|
-
|
|
708
|
-
// Empty state — teach the user where workflows live.
|
|
709
|
-
if (workflows.length === 0) {
|
|
710
|
-
lines.push("");
|
|
711
|
-
lines.push(" " + paint("text", "no workflows found", { bold: true }));
|
|
712
|
-
lines.push("");
|
|
713
|
-
lines.push(" " + paint("dim", "create one at"));
|
|
714
|
-
lines.push(
|
|
715
|
-
" " +
|
|
716
|
-
paint("accent", ".atomic/workflows/<name>/<agent>/index.ts"),
|
|
717
|
-
);
|
|
718
|
-
lines.push("");
|
|
719
|
-
return lines.join("\n") + "\n";
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// Group by source → agent → sorted entries. Entries carry the full
|
|
723
|
-
// metadata (name + status) so the row renderer can append a status
|
|
724
|
-
// badge to non-ok rows without another lookup.
|
|
725
|
-
type EntrySummary = { name: string; status: WorkflowMetadataStatus };
|
|
726
|
-
type ByAgent = Map<AgentType, EntrySummary[]>;
|
|
727
|
-
const bySource = new Map<DiscoveredWorkflow["source"], ByAgent>();
|
|
728
|
-
for (const wf of workflows) {
|
|
729
|
-
let byAgent = bySource.get(wf.source);
|
|
730
|
-
if (!byAgent) {
|
|
731
|
-
byAgent = new Map();
|
|
732
|
-
bySource.set(wf.source, byAgent);
|
|
733
|
-
}
|
|
734
|
-
const entries = byAgent.get(wf.agent) ?? [];
|
|
735
|
-
entries.push({ name: wf.name, status: wf.status });
|
|
736
|
-
byAgent.set(wf.agent, entries);
|
|
737
|
-
}
|
|
738
|
-
for (const byAgent of bySource.values()) {
|
|
739
|
-
for (const entries of byAgent.values()) {
|
|
740
|
-
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// Top header — data-first: the count is bold (it's the actual info), the
|
|
745
|
-
// noun trails in dim. Handles singular "1 workflow" gracefully.
|
|
746
|
-
const count = workflows.length;
|
|
747
|
-
const noun = count === 1 ? "workflow" : "workflows";
|
|
748
|
-
lines.push("");
|
|
749
|
-
lines.push(
|
|
750
|
-
" " + paint("text", String(count), { bold: true }) + " " + paint("dim", noun),
|
|
751
|
-
);
|
|
752
|
-
|
|
753
|
-
// One stanza per source section, with nested provider sub-groups inside.
|
|
754
|
-
// Rhythm:
|
|
755
|
-
// 1 blank before each source heading (section break)
|
|
756
|
-
// 1 blank before each provider heading (grouped with its entries)
|
|
757
|
-
for (const source of SOURCE_ORDER) {
|
|
758
|
-
const byAgent = bySource.get(source);
|
|
759
|
-
if (!byAgent || byAgent.size === 0) continue;
|
|
760
|
-
|
|
761
|
-
// Section break before the source section.
|
|
762
|
-
lines.push("");
|
|
763
|
-
|
|
764
|
-
// Source heading: bold semantic colour + dim inline directory hint.
|
|
765
|
-
// `local (.atomic/workflows)` — label carries the weight, parens recede.
|
|
766
|
-
lines.push(
|
|
767
|
-
" " +
|
|
768
|
-
paint(SOURCE_COLORS[source], source, { bold: true }) +
|
|
769
|
-
paint("dim", ` (${SOURCE_DIRS[source]})`),
|
|
770
|
-
);
|
|
771
|
-
|
|
772
|
-
for (const agent of AGENT_ORDER) {
|
|
773
|
-
const entries = byAgent.get(agent);
|
|
774
|
-
if (!entries || entries.length === 0) continue;
|
|
775
|
-
|
|
776
|
-
// Provider heading: bold accent blue — a clearly different layer from
|
|
777
|
-
// both the semantic source heading above and the neutral entries below.
|
|
778
|
-
lines.push("");
|
|
779
|
-
lines.push(
|
|
780
|
-
" " + paint("accent", AGENT_DISPLAY_NAMES[agent], { bold: true }),
|
|
781
|
-
);
|
|
782
|
-
|
|
783
|
-
for (const entry of entries) {
|
|
784
|
-
// Dim the name on non-ok rows so the eye lands on the status
|
|
785
|
-
// badge rather than the workflow name — the badge is where the
|
|
786
|
-
// actionable info lives, and the name is already unrunnable.
|
|
787
|
-
const nameCol: PaletteKey = entry.status.kind === "ok" ? "text" : "dim";
|
|
788
|
-
lines.push(
|
|
789
|
-
" " + paint(nameCol, entry.name) + renderStatusBadge(paint, entry.status),
|
|
790
|
-
);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// Footer — dim run hint, separated by a section break.
|
|
796
|
-
lines.push("");
|
|
797
|
-
lines.push(
|
|
798
|
-
" " + paint("dim", "run: atomic workflow -n <name> -a <agent>"),
|
|
799
|
-
);
|
|
800
|
-
lines.push("");
|
|
801
|
-
|
|
802
|
-
return lines.join("\n") + "\n";
|
|
803
|
-
}
|
|
13
|
+
export const workflowCommand = toCommand(
|
|
14
|
+
createWorkflowCli(createBuiltinRegistry()),
|
|
15
|
+
"workflow",
|
|
16
|
+
);
|