@bastani/atomic 0.5.34 → 0.6.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
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkflowCli — the single entry-point factory for workflow CLIs.
|
|
3
|
+
*
|
|
4
|
+
* Parses `-n/--name` + `-a/--agent` from argv, exposes a union of flags
|
|
5
|
+
* across every workflow in the registry, opens an interactive picker when
|
|
6
|
+
* agent is given without a name in a TTY, and handles orchestrator
|
|
7
|
+
* re-entry from detached runs.
|
|
8
|
+
*
|
|
9
|
+
* Framework-agnostic: the returned `WorkflowCli` type has no direct
|
|
10
|
+
* Commander dependency. To embed under a parent Commander CLI, use
|
|
11
|
+
* `toCommand(cli)` from `@bastani/atomic/workflows/commander`.
|
|
12
|
+
*
|
|
13
|
+
* Used by the internal `atomic workflow` command. Per-workflow CLI
|
|
14
|
+
* files call `createWorkflowCli(workflow)` — the same factory supports
|
|
15
|
+
* a lone workflow, an array, or a full `Registry`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { Command } from "@commander-js/extra-typings";
|
|
19
|
+
import type {
|
|
20
|
+
AgentType,
|
|
21
|
+
Registry,
|
|
22
|
+
RegistrableWorkflow,
|
|
23
|
+
WorkflowCli,
|
|
24
|
+
WorkflowDefinition,
|
|
25
|
+
WorkflowInput,
|
|
26
|
+
CreateWorkflowCliOptions,
|
|
27
|
+
} from "./types.ts";
|
|
28
|
+
import {
|
|
29
|
+
executeWorkflow,
|
|
30
|
+
handleOrchestratorReEntry,
|
|
31
|
+
} from "./runtime/executor.ts";
|
|
32
|
+
import { WorkflowPickerPanel } from "./components/workflow-picker-panel.tsx";
|
|
33
|
+
import { createRegistry } from "./registry.ts";
|
|
34
|
+
import {
|
|
35
|
+
toCamelCase,
|
|
36
|
+
validateAndResolve,
|
|
37
|
+
buildInputUnion,
|
|
38
|
+
} from "./worker-shared.ts";
|
|
39
|
+
|
|
40
|
+
// ─── Input normalization ─────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Normalize the three accepted `createWorkflowCli` input shapes into a
|
|
44
|
+
* `Registry`. Detection is structural:
|
|
45
|
+
*
|
|
46
|
+
* - `Registry` has `.register`, `.list`, `.resolve` methods.
|
|
47
|
+
* - Arrays are iterable; loop-register into a fresh registry.
|
|
48
|
+
* - Anything else is treated as a single compiled workflow.
|
|
49
|
+
*/
|
|
50
|
+
function normalizeToRegistry<T extends Record<string, WorkflowDefinition>>(
|
|
51
|
+
target: unknown,
|
|
52
|
+
): Registry<T> {
|
|
53
|
+
// Registry detection — check for the `register` method (distinct from
|
|
54
|
+
// a workflow's `run`, which is the only thing a plain definition has).
|
|
55
|
+
if (target && typeof target === "object" && "register" in target &&
|
|
56
|
+
typeof (target as { register?: unknown }).register === "function") {
|
|
57
|
+
return target as Registry<T>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Array of workflows — loop-register.
|
|
61
|
+
if (Array.isArray(target)) {
|
|
62
|
+
let reg = createRegistry() as Registry;
|
|
63
|
+
for (const wf of target) {
|
|
64
|
+
reg = reg.register(wf as Parameters<typeof reg.register>[0]);
|
|
65
|
+
}
|
|
66
|
+
return reg as Registry<T>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Single workflow.
|
|
70
|
+
return createRegistry().register(
|
|
71
|
+
target as Parameters<ReturnType<typeof createRegistry>["register"]>[0],
|
|
72
|
+
) as Registry<T>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
const VALID_AGENTS: readonly AgentType[] = ["claude", "opencode", "copilot"];
|
|
78
|
+
|
|
79
|
+
// ─── Core dispatch (internal — shared with the Commander adapter) ───────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolve the workflow definition, merge inputs (with precedence), validate,
|
|
83
|
+
* and hand off to the executor.
|
|
84
|
+
*
|
|
85
|
+
* Input precedence (highest → lowest):
|
|
86
|
+
* cliInputs > runInputs > dispatcherInputs > defineWorkflow defaults
|
|
87
|
+
*
|
|
88
|
+
* Exported for `./commander.ts` — not part of the public API.
|
|
89
|
+
*/
|
|
90
|
+
export async function resolveAndStart(
|
|
91
|
+
registry: Registry,
|
|
92
|
+
name: string,
|
|
93
|
+
agent: AgentType,
|
|
94
|
+
opts: {
|
|
95
|
+
cliInputs?: Record<string, string>;
|
|
96
|
+
runInputs?: Record<string, string>;
|
|
97
|
+
dispatcherInputs?: Record<string, string>;
|
|
98
|
+
detach?: boolean;
|
|
99
|
+
entry: string;
|
|
100
|
+
},
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
const def = registry.resolve(name, agent);
|
|
103
|
+
if (!def) {
|
|
104
|
+
const available = registry
|
|
105
|
+
.list()
|
|
106
|
+
.filter((w) => w.name === name)
|
|
107
|
+
.map((w) => w.agent);
|
|
108
|
+
const availableMsg =
|
|
109
|
+
available.length > 0
|
|
110
|
+
? `available agents for "${name}": ${available.join(", ")}`
|
|
111
|
+
: `no workflow named "${name}" in registry`;
|
|
112
|
+
throw new Error(
|
|
113
|
+
`no workflow named "${name}" for agent "${agent}"; ${availableMsg}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const merged: Record<string, string> = {
|
|
118
|
+
...opts.dispatcherInputs,
|
|
119
|
+
...opts.runInputs,
|
|
120
|
+
...opts.cliInputs,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const resolvedInputs =
|
|
124
|
+
def.inputs.length > 0
|
|
125
|
+
? validateAndResolve(merged, def.inputs)
|
|
126
|
+
: { ...merged };
|
|
127
|
+
|
|
128
|
+
await executeWorkflow({
|
|
129
|
+
definition: def,
|
|
130
|
+
agent,
|
|
131
|
+
inputs: resolvedInputs,
|
|
132
|
+
entrypointFile: opts.entry,
|
|
133
|
+
workflowKey: `${agent}/${name}`,
|
|
134
|
+
detach: opts.detach ?? false,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Commander command builder (internal — shared with the adapter) ─────────
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Build the Commander Command that drives the workflow CLI. Used by both
|
|
142
|
+
* the standalone `run()` path and the `toCommand` adapter.
|
|
143
|
+
*
|
|
144
|
+
* Exported for `./commander.ts` — not part of the public API.
|
|
145
|
+
*/
|
|
146
|
+
export function buildCliCommand(
|
|
147
|
+
registry: Registry,
|
|
148
|
+
unionInputs: Map<string, WorkflowInput>,
|
|
149
|
+
onAction: (params: {
|
|
150
|
+
name: string | undefined;
|
|
151
|
+
agent: AgentType | undefined;
|
|
152
|
+
cliInputs: Record<string, string>;
|
|
153
|
+
detach: boolean;
|
|
154
|
+
}) => Promise<void>,
|
|
155
|
+
mountName?: string,
|
|
156
|
+
): Command {
|
|
157
|
+
const allWorkflows = registry.list();
|
|
158
|
+
const allNames = [...new Set(allWorkflows.map((w) => w.name))];
|
|
159
|
+
|
|
160
|
+
const cmd = new Command(mountName);
|
|
161
|
+
|
|
162
|
+
// Required so auto-registered subcommands (session/status) can declare
|
|
163
|
+
// their own `-a <agent>` without the parent greedily binding the flag
|
|
164
|
+
// first. Matches what `atomic workflow` does at the top-level.
|
|
165
|
+
cmd.enablePositionalOptions();
|
|
166
|
+
|
|
167
|
+
cmd.option("-n, --name <name>", "Workflow name", (v) => {
|
|
168
|
+
if (allNames.length > 0 && !allNames.includes(v)) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`[atomic/worker] Unknown workflow name "${v}". Available: ${allNames.join(", ")}.`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return v;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
cmd.option("-a, --agent <agent>", "Agent (claude | opencode | copilot)", (v) => {
|
|
177
|
+
if (!(VALID_AGENTS as string[]).includes(v)) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`[atomic/worker] Unknown agent "${v}". Valid agents: ${VALID_AGENTS.join(", ")}.`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return v as AgentType;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
for (const [, input] of unionInputs) {
|
|
186
|
+
const desc =
|
|
187
|
+
input.description ??
|
|
188
|
+
(input.type === "enum" ? `one of: ${(input.values ?? []).join(", ")}` : input.type);
|
|
189
|
+
cmd.option(`--${input.name} <value>`, desc);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
cmd.option("-d, --detach", "Run workflow in background (detach from tmux)");
|
|
193
|
+
|
|
194
|
+
cmd.argument("[prompt...]", "Free-form prompt (joined, stored as inputs.prompt)");
|
|
195
|
+
|
|
196
|
+
cmd.allowUnknownOption(false);
|
|
197
|
+
cmd.allowExcessArguments(true);
|
|
198
|
+
|
|
199
|
+
cmd.action(async function (this: Command) {
|
|
200
|
+
const options = this.opts() as Record<string, string | boolean | undefined>;
|
|
201
|
+
const promptTokens: string[] = this.args;
|
|
202
|
+
|
|
203
|
+
const name = options["name"] as string | undefined;
|
|
204
|
+
const agent = options["agent"] as AgentType | undefined;
|
|
205
|
+
const detach = options["detach"] === true;
|
|
206
|
+
|
|
207
|
+
const cliInputs: Record<string, string> = {};
|
|
208
|
+
for (const [inputName] of unionInputs) {
|
|
209
|
+
const camelKey = toCamelCase(inputName);
|
|
210
|
+
const v = options[camelKey];
|
|
211
|
+
if (typeof v === "string" && v !== "") {
|
|
212
|
+
cliInputs[inputName] = v;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const promptStr = promptTokens.join(" ");
|
|
217
|
+
if (promptStr !== "" && name) {
|
|
218
|
+
const def = registry.resolve(name, agent as AgentType);
|
|
219
|
+
if (def && def.inputs.length === 0) {
|
|
220
|
+
cliInputs["prompt"] = promptStr;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await onAction({ name, agent, cliInputs, detach });
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return cmd;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Interactive-picker path used by both `run()` and the Commander adapter.
|
|
232
|
+
* Depends on `process.stdout.isTTY`; returns without side effects when
|
|
233
|
+
* the user cancels or no terminal is attached.
|
|
234
|
+
*
|
|
235
|
+
* Exported for `./commander.ts` — not part of the public API.
|
|
236
|
+
*/
|
|
237
|
+
export async function runPicker(
|
|
238
|
+
registry: Registry,
|
|
239
|
+
agent: AgentType,
|
|
240
|
+
detach: boolean,
|
|
241
|
+
entry: string,
|
|
242
|
+
dispatcherInputs: Record<string, string> | undefined,
|
|
243
|
+
): Promise<void> {
|
|
244
|
+
const panel = await WorkflowPickerPanel.create({ agent, registry });
|
|
245
|
+
const result = await panel.waitForSelection();
|
|
246
|
+
panel.destroy();
|
|
247
|
+
if (!result) {
|
|
248
|
+
process.stdout.write("No workflow selected.\n");
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const { workflow: selectedWf, inputs: pickerInputs } = result;
|
|
252
|
+
await resolveAndStart(registry, selectedWf.name, agent, {
|
|
253
|
+
cliInputs: pickerInputs,
|
|
254
|
+
dispatcherInputs,
|
|
255
|
+
detach,
|
|
256
|
+
entry,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ─── Public factory ──────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Create a workflow CLI that resolves `--name` + `--agent` from argv and
|
|
264
|
+
* runs the matching workflow from the registry.
|
|
265
|
+
*
|
|
266
|
+
* Accepts three input shapes — pick whichever is cleanest:
|
|
267
|
+
*
|
|
268
|
+
* - **A single workflow.** `createWorkflowCli(workflow).run()`.
|
|
269
|
+
* - **An array of workflows.** `createWorkflowCli([claude, copilot]).run()`.
|
|
270
|
+
* - **A `Registry`.** For programmatic/dynamic composition, or sharing a
|
|
271
|
+
* registry across multiple CLIs. Build with `createRegistry().register(...)`.
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* ```ts
|
|
275
|
+
* // Single workflow — ~70% of use cases
|
|
276
|
+
* const cli = createWorkflowCli(workflow);
|
|
277
|
+
* await cli.run();
|
|
278
|
+
*
|
|
279
|
+
* // Multi-workflow, multi-agent — by far the most common multi-case
|
|
280
|
+
* await createWorkflowCli([claude, copilot, opencode]).run();
|
|
281
|
+
*
|
|
282
|
+
* // Dynamic composition
|
|
283
|
+
* const registry = workflowFiles.reduce(
|
|
284
|
+
* (r, wf) => r.register(wf),
|
|
285
|
+
* createRegistry(),
|
|
286
|
+
* );
|
|
287
|
+
* await createWorkflowCli(registry).run();
|
|
288
|
+
* ```
|
|
289
|
+
*
|
|
290
|
+
* To embed under a parent Commander CLI:
|
|
291
|
+
*
|
|
292
|
+
* ```ts
|
|
293
|
+
* import { toCommand, runCli } from "@bastani/atomic/workflows/commander";
|
|
294
|
+
* parent.addCommand(toCommand(cli));
|
|
295
|
+
* await runCli(cli, () => parent.parseAsync());
|
|
296
|
+
* ```
|
|
297
|
+
*
|
|
298
|
+
* The single/array overloads use generic constraints (`W extends
|
|
299
|
+
* RegistrableWorkflow`) rather than a plain parameter type. This matters
|
|
300
|
+
* under `--strictFunctionTypes`: a `WorkflowDefinition<"claude", ...>`
|
|
301
|
+
* will not assign to a property-typed parameter because its narrow
|
|
302
|
+
* `run(ctx: WorkflowContext<"claude">)` is contravariant against the
|
|
303
|
+
* broader target. Routing through a generic `W` lets TS check bivariantly
|
|
304
|
+
* via `extends`, which matches how `Registry.register` already accepts
|
|
305
|
+
* the same inputs.
|
|
306
|
+
*/
|
|
307
|
+
export function createWorkflowCli<W extends RegistrableWorkflow>(
|
|
308
|
+
target: W,
|
|
309
|
+
options?: CreateWorkflowCliOptions,
|
|
310
|
+
): WorkflowCli;
|
|
311
|
+
export function createWorkflowCli<W extends RegistrableWorkflow>(
|
|
312
|
+
target: readonly W[],
|
|
313
|
+
options?: CreateWorkflowCliOptions,
|
|
314
|
+
): WorkflowCli;
|
|
315
|
+
export function createWorkflowCli<T extends Record<string, WorkflowDefinition>>(
|
|
316
|
+
target: Registry<T>,
|
|
317
|
+
options?: CreateWorkflowCliOptions,
|
|
318
|
+
): WorkflowCli<T>;
|
|
319
|
+
export function createWorkflowCli<T extends Record<string, WorkflowDefinition>>(
|
|
320
|
+
target: RegistrableWorkflow | readonly RegistrableWorkflow[] | Registry<T>,
|
|
321
|
+
options: CreateWorkflowCliOptions = {},
|
|
322
|
+
): WorkflowCli<T> {
|
|
323
|
+
const registry = normalizeToRegistry<T>(target);
|
|
324
|
+
const defaultInputs = options.inputs;
|
|
325
|
+
const extend = options.extend;
|
|
326
|
+
const entry = options.entry ?? process.argv[1]!;
|
|
327
|
+
const includeManagementCommands = options.includeManagementCommands !== false;
|
|
328
|
+
|
|
329
|
+
// Build input union at construction time — throws on type conflicts.
|
|
330
|
+
const unionInputs = buildInputUnion(registry.list());
|
|
331
|
+
|
|
332
|
+
const cli: WorkflowCli<T> = {
|
|
333
|
+
registry,
|
|
334
|
+
entry,
|
|
335
|
+
defaults: defaultInputs,
|
|
336
|
+
|
|
337
|
+
async run(runOpts = {}): Promise<void> {
|
|
338
|
+
if (await handleOrchestratorReEntry((n, a) => registry.resolve(n, a))) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const { argv } = runOpts;
|
|
343
|
+
|
|
344
|
+
if (argv === false) {
|
|
345
|
+
// Programmatic: skip Commander entirely.
|
|
346
|
+
if (!runOpts.name || !runOpts.agent) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
"cli.run({ argv: false }) requires both `name` and `agent`",
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
await resolveAndStart(registry, runOpts.name, runOpts.agent, {
|
|
352
|
+
runInputs: runOpts.inputs,
|
|
353
|
+
dispatcherInputs: defaultInputs,
|
|
354
|
+
detach: runOpts.detach,
|
|
355
|
+
entry,
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// CLI mode — build a fresh command, fold runOpts in as defaults.
|
|
361
|
+
let cmd!: Command;
|
|
362
|
+
cmd = buildCliCommand(
|
|
363
|
+
registry,
|
|
364
|
+
unionInputs,
|
|
365
|
+
async (params) => {
|
|
366
|
+
// Programmatic `name`/`agent`/`detach` layer beneath parsed values.
|
|
367
|
+
const effectiveName = params.name ?? runOpts.name;
|
|
368
|
+
const effectiveAgent = params.agent ?? runOpts.agent;
|
|
369
|
+
const effectiveDetach = params.detach || (runOpts.detach ?? false);
|
|
370
|
+
|
|
371
|
+
// Interactive picker: agent given, name omitted, running in a real terminal.
|
|
372
|
+
if (!effectiveName && effectiveAgent && process.stdout.isTTY) {
|
|
373
|
+
await runPicker(registry, effectiveAgent, effectiveDetach, entry, defaultInputs);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!effectiveName || !effectiveAgent) {
|
|
378
|
+
// Commander's `help()` calls `process.exit()` and is typed `never`.
|
|
379
|
+
cmd.help();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
await resolveAndStart(registry, effectiveName, effectiveAgent, {
|
|
383
|
+
cliInputs: params.cliInputs,
|
|
384
|
+
runInputs: runOpts.inputs,
|
|
385
|
+
dispatcherInputs: defaultInputs,
|
|
386
|
+
detach: effectiveDetach,
|
|
387
|
+
entry,
|
|
388
|
+
});
|
|
389
|
+
},
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
// Auto-register `session` + `status` subcommands so SDK users get the
|
|
393
|
+
// same monitoring surface as the global `atomic` CLI without needing
|
|
394
|
+
// the global binary. Sessions live on the shared atomic tmux socket,
|
|
395
|
+
// so these are pure pass-throughs. Opt out with
|
|
396
|
+
// `createWorkflowCli(wf, { includeManagementCommands: false })`.
|
|
397
|
+
if (includeManagementCommands) {
|
|
398
|
+
const { addManagementCommands } = await import("./management-commands.ts");
|
|
399
|
+
addManagementCommands(cmd, "workflow");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (extend) extend(cmd);
|
|
403
|
+
|
|
404
|
+
await cmd.parseAsync(argv ?? process.argv);
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
return cli;
|
|
409
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createRegistry } from "../registry";
|
|
2
|
+
|
|
3
|
+
// ralph
|
|
4
|
+
import ralphClaude from "./builtin/ralph/claude";
|
|
5
|
+
import ralphCopilot from "./builtin/ralph/copilot";
|
|
6
|
+
import ralphOpencode from "./builtin/ralph/opencode";
|
|
7
|
+
|
|
8
|
+
// deep-research-codebase
|
|
9
|
+
import drcClaude from "./builtin/deep-research-codebase/claude";
|
|
10
|
+
import drcCopilot from "./builtin/deep-research-codebase/copilot";
|
|
11
|
+
import drcOpencode from "./builtin/deep-research-codebase/opencode";
|
|
12
|
+
|
|
13
|
+
// open-claude-design
|
|
14
|
+
import ocdClaude from "./builtin/open-claude-design/claude";
|
|
15
|
+
import ocdCopilot from "./builtin/open-claude-design/copilot";
|
|
16
|
+
import ocdOpencode from "./builtin/open-claude-design/opencode";
|
|
17
|
+
|
|
18
|
+
export function createBuiltinRegistry() {
|
|
19
|
+
return createRegistry()
|
|
20
|
+
.register(ralphClaude).register(ralphCopilot).register(ralphOpencode)
|
|
21
|
+
.register(drcClaude).register(drcCopilot).register(drcOpencode)
|
|
22
|
+
.register(ocdClaude).register(ocdCopilot).register(ocdOpencode);
|
|
23
|
+
}
|
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export { defineWorkflow, WorkflowBuilder } from "../define-workflow.ts";
|
|
10
|
+
export { createRegistry } from "../registry.ts";
|
|
11
|
+
export type { Registry } from "../registry.ts";
|
|
12
|
+
|
|
13
|
+
// WorkflowCli — the single factory that drives workflow CLIs. Accepts a
|
|
14
|
+
// lone workflow, an array of workflows, or a Registry for programmatic
|
|
15
|
+
// composition. Ships with the interactive picker out of the box.
|
|
16
|
+
export { createWorkflowCli } from "../workflow-cli.ts";
|
|
17
|
+
export type { WorkflowCli, CreateWorkflowCliOptions } from "../types.ts";
|
|
18
|
+
|
|
19
|
+
export type { ArgvMode } from "../types.ts";
|
|
10
20
|
|
|
11
21
|
export type {
|
|
12
22
|
AgentType,
|
|
@@ -87,23 +97,3 @@ export {
|
|
|
87
97
|
normalizeTmuxLines,
|
|
88
98
|
} from "../runtime/tmux.ts";
|
|
89
99
|
|
|
90
|
-
// Runtime — workflow discovery
|
|
91
|
-
export {
|
|
92
|
-
AGENTS,
|
|
93
|
-
discoverWorkflows,
|
|
94
|
-
findWorkflow,
|
|
95
|
-
loadWorkflowsMetadata,
|
|
96
|
-
WORKFLOWS_GITIGNORE,
|
|
97
|
-
} from "../runtime/discovery.ts";
|
|
98
|
-
export type {
|
|
99
|
-
DiscoveredWorkflow,
|
|
100
|
-
WorkflowWithMetadata,
|
|
101
|
-
WorkflowMetadataStatus,
|
|
102
|
-
} from "../runtime/discovery.ts";
|
|
103
|
-
|
|
104
|
-
// Runtime — workflow loader pipeline
|
|
105
|
-
export { WorkflowLoader } from "../runtime/loader.ts";
|
|
106
|
-
|
|
107
|
-
// Runtime — workflow executor
|
|
108
|
-
export { executeWorkflow } from "../runtime/executor.ts";
|
|
109
|
-
export type { WorkflowRunOptions } from "../runtime/executor.ts";
|
|
@@ -111,7 +111,7 @@ afterAll(() => {
|
|
|
111
111
|
mock.module("@anthropic-ai/claude-agent-sdk", () => ({ ...actualClaudeSdk }));
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
const { checkAgentAuth } = await import("./auth.ts");
|
|
114
|
+
const { checkAgentAuth, printAuthError } = await import("./auth.ts");
|
|
115
115
|
|
|
116
116
|
beforeEach(() => {
|
|
117
117
|
copilotStart.mockClear();
|
|
@@ -226,3 +226,65 @@ describe("checkAgentAuth(opencode)", () => {
|
|
|
226
226
|
expect(claudeInit).not.toHaveBeenCalled();
|
|
227
227
|
});
|
|
228
228
|
});
|
|
229
|
+
|
|
230
|
+
describe("printAuthError", () => {
|
|
231
|
+
function captureStderr(): {
|
|
232
|
+
output: () => string;
|
|
233
|
+
restore: () => void;
|
|
234
|
+
} {
|
|
235
|
+
const chunks: string[] = [];
|
|
236
|
+
const orig = process.stderr.write;
|
|
237
|
+
process.stderr.write = ((c: string | Uint8Array) => {
|
|
238
|
+
chunks.push(typeof c === "string" ? c : new TextDecoder().decode(c));
|
|
239
|
+
return true;
|
|
240
|
+
}) as typeof process.stderr.write;
|
|
241
|
+
const origErr = console.error;
|
|
242
|
+
console.error = (...args: unknown[]) => {
|
|
243
|
+
chunks.push(args.map((a) => String(a)).join(" ") + "\n");
|
|
244
|
+
};
|
|
245
|
+
return {
|
|
246
|
+
output: () => chunks.join(""),
|
|
247
|
+
restore: () => {
|
|
248
|
+
process.stderr.write = orig;
|
|
249
|
+
console.error = origErr;
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
test("prints the Claude login hint with optional detail line", () => {
|
|
255
|
+
const cap = captureStderr();
|
|
256
|
+
try {
|
|
257
|
+
printAuthError("claude", { loggedIn: false, detail: "token expired" });
|
|
258
|
+
const out = cap.output();
|
|
259
|
+
expect(out).toContain("Not logged in to Claude Code");
|
|
260
|
+
expect(out).toContain("token expired");
|
|
261
|
+
expect(out).toContain("/login");
|
|
262
|
+
} finally {
|
|
263
|
+
cap.restore();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("omits the detail line when no detail is given", () => {
|
|
268
|
+
const cap = captureStderr();
|
|
269
|
+
try {
|
|
270
|
+
printAuthError("copilot", { loggedIn: false });
|
|
271
|
+
const out = cap.output();
|
|
272
|
+
expect(out).toContain("Not logged in to GitHub Copilot CLI");
|
|
273
|
+
expect(out).toContain("`/login`");
|
|
274
|
+
} finally {
|
|
275
|
+
cap.restore();
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("prints the OpenCode login hint", () => {
|
|
280
|
+
const cap = captureStderr();
|
|
281
|
+
try {
|
|
282
|
+
printAuthError("opencode", { loggedIn: false });
|
|
283
|
+
const out = cap.output();
|
|
284
|
+
expect(out).toContain("Not logged in to OpenCode");
|
|
285
|
+
expect(out).toContain("opencode auth login");
|
|
286
|
+
} finally {
|
|
287
|
+
cap.restore();
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
});
|