@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.
Files changed (48) hide show
  1. package/README.md +110 -11
  2. package/dist/{chunk-mn870nrv.js → chunk-xkxndz5g.js} +213 -154
  3. package/dist/sdk/components/workflow-picker-panel.d.ts +120 -0
  4. package/dist/sdk/define-workflow.d.ts +1 -1
  5. package/dist/sdk/index.js +1 -1
  6. package/dist/sdk/runtime/discovery.d.ts +57 -3
  7. package/dist/sdk/runtime/executor.d.ts +15 -2
  8. package/dist/sdk/runtime/tmux.d.ts +9 -0
  9. package/dist/sdk/types.d.ts +63 -4
  10. package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts +61 -0
  11. package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts +48 -0
  12. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.d.ts +25 -0
  13. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts +91 -0
  14. package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts +56 -0
  15. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts +48 -0
  16. package/dist/sdk/workflows/builtin/ralph/claude/index.js +6 -5
  17. package/dist/sdk/workflows/builtin/ralph/copilot/index.js +6 -5
  18. package/dist/sdk/workflows/builtin/ralph/opencode/index.js +6 -5
  19. package/dist/sdk/workflows/index.d.ts +4 -4
  20. package/dist/sdk/workflows/index.js +7 -1
  21. package/package.json +1 -1
  22. package/src/cli.ts +25 -3
  23. package/src/commands/cli/chat/index.ts +5 -5
  24. package/src/commands/cli/init/index.ts +79 -77
  25. package/src/commands/cli/workflow-command.test.ts +757 -0
  26. package/src/commands/cli/workflow.test.ts +310 -0
  27. package/src/commands/cli/workflow.ts +445 -105
  28. package/src/sdk/components/workflow-picker-panel.tsx +1462 -0
  29. package/src/sdk/define-workflow.test.ts +101 -0
  30. package/src/sdk/define-workflow.ts +62 -2
  31. package/src/sdk/runtime/discovery.ts +111 -8
  32. package/src/sdk/runtime/executor.ts +89 -32
  33. package/src/sdk/runtime/tmux.conf +55 -0
  34. package/src/sdk/runtime/tmux.ts +34 -10
  35. package/src/sdk/types.ts +67 -4
  36. package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +294 -0
  37. package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +276 -0
  38. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.ts +38 -0
  39. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.ts +816 -0
  40. package/src/sdk/workflows/builtin/deep-research-codebase/helpers/scout.ts +334 -0
  41. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +284 -0
  42. package/src/sdk/workflows/builtin/ralph/claude/index.ts +8 -4
  43. package/src/sdk/workflows/builtin/ralph/copilot/index.ts +10 -4
  44. package/src/sdk/workflows/builtin/ralph/opencode/index.ts +8 -4
  45. package/src/sdk/workflows/index.ts +9 -1
  46. package/src/services/system/auto-sync.ts +1 -1
  47. package/src/services/system/install-ui.ts +109 -39
  48. package/src/theme/colors.ts +65 -1
@@ -0,0 +1,310 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from "bun:test";
2
+ import {
3
+ parsePassthroughArgs,
4
+ validateInputsAgainstSchema,
5
+ resolveInputs,
6
+ renderWorkflowList,
7
+ } from "./workflow.ts";
8
+ import type { WorkflowInput } from "@/sdk/workflows/index.ts";
9
+ import type { DiscoveredWorkflow } from "@/sdk/workflows/index.ts";
10
+
11
+ // ─── Colour handling ────────────────────────────────────────────────────────
12
+ // The renderer emits ANSI sequences when the host terminal claims truecolor
13
+ // support. Force `NO_COLOR=1` for this file so renderWorkflowList assertions
14
+ // can match on plain-text output rather than brittle SGR escapes. Restore the
15
+ // prior value on teardown so other suites in the same run are unaffected.
16
+ let originalNoColor: string | undefined;
17
+ beforeAll(() => {
18
+ originalNoColor = process.env.NO_COLOR;
19
+ process.env.NO_COLOR = "1";
20
+ });
21
+ afterAll(() => {
22
+ if (originalNoColor === undefined) delete process.env.NO_COLOR;
23
+ else process.env.NO_COLOR = originalNoColor;
24
+ });
25
+
26
+ // ─── parsePassthroughArgs ──────────────────────────────────────────────────
27
+
28
+ describe("parsePassthroughArgs", () => {
29
+ test("parses --name=value flag pairs", () => {
30
+ const out = parsePassthroughArgs(["--foo=bar", "--baz=qux"]);
31
+ expect(out.flags).toEqual({ foo: "bar", baz: "qux" });
32
+ expect(out.positional).toEqual([]);
33
+ expect(out.errors).toEqual([]);
34
+ });
35
+
36
+ test("parses --name value flag pairs", () => {
37
+ const out = parsePassthroughArgs(["--foo", "bar", "--baz", "qux"]);
38
+ expect(out.flags).toEqual({ foo: "bar", baz: "qux" });
39
+ expect(out.errors).toEqual([]);
40
+ });
41
+
42
+ test("preserves values containing equals signs", () => {
43
+ const out = parsePassthroughArgs(["--query=key=value"]);
44
+ expect(out.flags).toEqual({ query: "key=value" });
45
+ });
46
+
47
+ test("collects positional tokens separately", () => {
48
+ const out = parsePassthroughArgs(["fix", "the", "bug"]);
49
+ expect(out.positional).toEqual(["fix", "the", "bug"]);
50
+ expect(out.flags).toEqual({});
51
+ });
52
+
53
+ test("returns an error when a flag has no value", () => {
54
+ const out = parsePassthroughArgs(["--foo"]);
55
+ expect(out.errors.length).toBe(1);
56
+ expect(out.errors[0]).toContain("--foo");
57
+ });
58
+
59
+ test("flags an empty flag name when `--=value` slips through", () => {
60
+ // `--=foo` tokenises to body="=foo", splits at the first `=`,
61
+ // and yields an empty name. The parser rejects this so users see
62
+ // "did you mean --name=foo?" rather than silently accepting a
63
+ // nameless entry in the flags record.
64
+ const out = parsePassthroughArgs(["--=foo"]);
65
+ expect(out.errors.length).toBe(1);
66
+ expect(out.errors[0]).toContain("Malformed flag");
67
+ expect(out.flags).toEqual({});
68
+ });
69
+
70
+ test("treats a trailing --flag --other as a missing value", () => {
71
+ // Second `--other` looks like a flag so the parser should not
72
+ // silently consume it as the first flag's value.
73
+ const out = parsePassthroughArgs(["--foo", "--other=x"]);
74
+ expect(out.errors.length).toBe(1);
75
+ expect(out.flags).toEqual({ other: "x" });
76
+ });
77
+
78
+ test("handles mixed positional and flag tokens", () => {
79
+ const out = parsePassthroughArgs([
80
+ "run",
81
+ "--mode=fast",
82
+ "now",
83
+ "--retries",
84
+ "3",
85
+ ]);
86
+ expect(out.positional).toEqual(["run", "now"]);
87
+ expect(out.flags).toEqual({ mode: "fast", retries: "3" });
88
+ expect(out.errors).toEqual([]);
89
+ });
90
+ });
91
+
92
+ // ─── validateInputsAgainstSchema ───────────────────────────────────────────
93
+
94
+ const schema: WorkflowInput[] = [
95
+ {
96
+ name: "research_doc",
97
+ type: "string",
98
+ required: true,
99
+ description: "path",
100
+ },
101
+ {
102
+ name: "focus",
103
+ type: "enum",
104
+ required: true,
105
+ values: ["minimal", "standard", "exhaustive"],
106
+ default: "standard",
107
+ },
108
+ {
109
+ name: "notes",
110
+ type: "text",
111
+ },
112
+ ];
113
+
114
+ describe("validateInputsAgainstSchema", () => {
115
+ test("passes when all required fields are present", () => {
116
+ const errors = validateInputsAgainstSchema(
117
+ { research_doc: "notes.md", focus: "standard" },
118
+ schema,
119
+ );
120
+ expect(errors).toEqual([]);
121
+ });
122
+
123
+ test("accepts an omitted enum with a declared default", () => {
124
+ // `focus` has a default of "standard", so omission should be OK.
125
+ const errors = validateInputsAgainstSchema(
126
+ { research_doc: "notes.md" },
127
+ schema,
128
+ );
129
+ expect(errors).toEqual([]);
130
+ });
131
+
132
+ test("flags a missing required string input", () => {
133
+ const errors = validateInputsAgainstSchema(
134
+ { focus: "standard" },
135
+ schema,
136
+ );
137
+ expect(errors.length).toBe(1);
138
+ expect(errors[0]).toContain("--research_doc");
139
+ });
140
+
141
+ test("rejects required strings that are whitespace only", () => {
142
+ const errors = validateInputsAgainstSchema(
143
+ { research_doc: " ", focus: "standard" },
144
+ schema,
145
+ );
146
+ expect(errors[0]).toContain("--research_doc");
147
+ });
148
+
149
+ test("rejects enum values that are not in the allowed list", () => {
150
+ const errors = validateInputsAgainstSchema(
151
+ { research_doc: "notes.md", focus: "bogus" },
152
+ schema,
153
+ );
154
+ expect(errors.length).toBe(1);
155
+ expect(errors[0]).toContain("bogus");
156
+ expect(errors[0]).toContain("minimal");
157
+ });
158
+
159
+ test("flags unknown inputs", () => {
160
+ const errors = validateInputsAgainstSchema(
161
+ {
162
+ research_doc: "notes.md",
163
+ focus: "standard",
164
+ bogus: "hello",
165
+ },
166
+ schema,
167
+ );
168
+ expect(errors.length).toBe(1);
169
+ expect(errors[0]).toContain("--bogus");
170
+ });
171
+
172
+ test("flags a required enum that resolves to empty when no values are declared", () => {
173
+ // Pathological but reachable: a declared enum with an empty
174
+ // `values` array and no default falls through to value==="" in the
175
+ // resolver, which should trip the enum-specific required-input
176
+ // error branch rather than the generic string-required branch.
177
+ const brokenSchema: WorkflowInput[] = [
178
+ { name: "mode", type: "enum", required: true, values: [] },
179
+ ];
180
+ const errors = validateInputsAgainstSchema({}, brokenSchema);
181
+ expect(errors.length).toBe(1);
182
+ expect(errors[0]).toContain("--mode");
183
+ expect(errors[0]).toContain("expected one of:");
184
+ });
185
+ });
186
+
187
+ // ─── renderWorkflowList ────────────────────────────────────────────────────
188
+
189
+ function wf(
190
+ name: string,
191
+ agent: DiscoveredWorkflow["agent"],
192
+ source: DiscoveredWorkflow["source"],
193
+ ): DiscoveredWorkflow {
194
+ return {
195
+ name,
196
+ agent,
197
+ source,
198
+ path: `/tmp/fake/${source}/${name}/${agent}/index.ts`,
199
+ };
200
+ }
201
+
202
+ describe("renderWorkflowList", () => {
203
+ test("renders an empty-state stanza when no workflows are available", () => {
204
+ const out = renderWorkflowList([]);
205
+ expect(out).toContain("no workflows found");
206
+ // Teaches the user where to drop a new workflow.
207
+ expect(out).toContain(".atomic/workflows/<name>/<agent>/index.ts");
208
+ });
209
+
210
+ test("uses singular 'workflow' for a count of exactly one", () => {
211
+ const out = renderWorkflowList([wf("only", "claude", "local")]);
212
+ // Must say "1 workflow", never "1 workflows".
213
+ expect(out).toMatch(/\b1 workflow\b/);
214
+ expect(out).not.toMatch(/\b1 workflows\b/);
215
+ expect(out).toContain("only");
216
+ });
217
+
218
+ test("groups entries by source → provider and sorts names", () => {
219
+ const workflows: DiscoveredWorkflow[] = [
220
+ wf("zebra", "claude", "local"),
221
+ wf("apple", "claude", "local"),
222
+ wf("middle", "opencode", "local"),
223
+ wf("personal", "claude", "global"),
224
+ wf("shipped", "copilot", "builtin"),
225
+ ];
226
+ const out = renderWorkflowList(workflows);
227
+
228
+ // Count uses plural noun.
229
+ expect(out).toMatch(/\b5 workflows\b/);
230
+
231
+ // Section headings appear with friendly directory hints.
232
+ expect(out).toContain("local");
233
+ expect(out).toContain(".atomic/workflows");
234
+ expect(out).toContain("global");
235
+ expect(out).toContain("~/.atomic/workflows");
236
+ expect(out).toContain("builtin");
237
+ expect(out).toContain("built-in");
238
+
239
+ // Provider sub-headings are present with branded names.
240
+ expect(out).toContain("Claude");
241
+ expect(out).toContain("OpenCode");
242
+ expect(out).toContain("Copilot CLI");
243
+
244
+ // Run-hint footer is appended.
245
+ expect(out).toContain("run: atomic workflow -n <name> -a <agent>");
246
+
247
+ // Local/Claude names are sorted alphabetically: apple before zebra.
248
+ const appleIdx = out.indexOf("apple");
249
+ const zebraIdx = out.indexOf("zebra");
250
+ expect(appleIdx).toBeGreaterThanOrEqual(0);
251
+ expect(zebraIdx).toBeGreaterThan(appleIdx);
252
+
253
+ // Source ordering: local before global before builtin.
254
+ const localIdx = out.indexOf("local");
255
+ const globalIdx = out.indexOf("global");
256
+ const builtinIdx = out.indexOf("builtin");
257
+ expect(localIdx).toBeLessThan(globalIdx);
258
+ expect(globalIdx).toBeLessThan(builtinIdx);
259
+ });
260
+
261
+ test("omits provider sub-groups that have no entries for a given source", () => {
262
+ // Only a copilot workflow exists, so the local stanza should render a
263
+ // single "Copilot CLI" sub-heading — never "Claude" or "OpenCode".
264
+ const out = renderWorkflowList([wf("alone", "copilot", "local")]);
265
+ expect(out).toContain("Copilot CLI");
266
+ expect(out).not.toContain("Claude");
267
+ expect(out).not.toContain("OpenCode");
268
+ expect(out).toContain("alone");
269
+ });
270
+ });
271
+
272
+ // ─── resolveInputs ─────────────────────────────────────────────────────────
273
+
274
+ describe("resolveInputs", () => {
275
+ test("fills in declared defaults", () => {
276
+ const out = resolveInputs({ research_doc: "notes.md" }, schema);
277
+ expect(out.research_doc).toBe("notes.md");
278
+ expect(out.focus).toBe("standard");
279
+ // `notes` has no default — should be absent, not empty-string.
280
+ expect(out.notes).toBeUndefined();
281
+ });
282
+
283
+ test("prefers provided values over defaults", () => {
284
+ const out = resolveInputs(
285
+ { research_doc: "notes.md", focus: "exhaustive" },
286
+ schema,
287
+ );
288
+ expect(out.focus).toBe("exhaustive");
289
+ });
290
+
291
+ test("falls back to the first enum value when no default and no input", () => {
292
+ const enumOnly: WorkflowInput[] = [
293
+ {
294
+ name: "mode",
295
+ type: "enum",
296
+ required: true,
297
+ values: ["a", "b", "c"],
298
+ },
299
+ ];
300
+ expect(resolveInputs({}, enumOnly)).toEqual({ mode: "a" });
301
+ });
302
+
303
+ test("ignores unknown provided keys", () => {
304
+ const out = resolveInputs(
305
+ { research_doc: "notes.md", bogus: "hello" },
306
+ schema,
307
+ );
308
+ expect(out.bogus).toBeUndefined();
309
+ });
310
+ });