swarmkit 0.0.6 → 0.0.7

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 (38) hide show
  1. package/dist/cli.js +2 -0
  2. package/dist/commands/configure/configure.d.ts +5 -0
  3. package/dist/commands/configure/configure.js +354 -0
  4. package/dist/commands/configure/configure.test.d.ts +1 -0
  5. package/dist/commands/configure/configure.test.js +539 -0
  6. package/dist/commands/configure/read-config.d.ts +12 -0
  7. package/dist/commands/configure/read-config.js +81 -0
  8. package/dist/commands/configure.d.ts +2 -0
  9. package/dist/commands/configure.js +14 -0
  10. package/dist/commands/init/phases/configure.js +0 -21
  11. package/dist/commands/init/phases/global-setup.d.ts +1 -1
  12. package/dist/commands/init/phases/global-setup.js +22 -44
  13. package/dist/commands/init/phases/integrations.d.ts +16 -0
  14. package/dist/commands/init/phases/integrations.js +172 -0
  15. package/dist/commands/init/phases/package-config.d.ts +10 -0
  16. package/dist/commands/init/phases/package-config.js +117 -0
  17. package/dist/commands/init/phases/phases.test.d.ts +1 -0
  18. package/dist/commands/init/phases/phases.test.js +711 -0
  19. package/dist/commands/init/phases/project.js +17 -0
  20. package/dist/commands/init/phases/review.d.ts +8 -0
  21. package/dist/commands/init/phases/review.js +79 -0
  22. package/dist/commands/init/phases/use-case.js +41 -27
  23. package/dist/commands/init/phases/wizard-flow.test.d.ts +1 -0
  24. package/dist/commands/init/phases/wizard-flow.test.js +657 -0
  25. package/dist/commands/init/phases/wizard-modes.test.d.ts +1 -0
  26. package/dist/commands/init/phases/wizard-modes.test.js +270 -0
  27. package/dist/commands/init/state.d.ts +31 -1
  28. package/dist/commands/init/state.js +4 -0
  29. package/dist/commands/init/state.test.js +7 -0
  30. package/dist/commands/init/wizard.d.ts +1 -0
  31. package/dist/commands/init/wizard.js +31 -23
  32. package/dist/commands/init.js +2 -0
  33. package/dist/packages/registry.d.ts +66 -0
  34. package/dist/packages/registry.js +258 -0
  35. package/dist/packages/setup.d.ts +42 -0
  36. package/dist/packages/setup.js +244 -15
  37. package/dist/packages/setup.test.js +520 -13
  38. package/package.json +1 -1
@@ -0,0 +1,657 @@
1
+ /**
2
+ * Tests for the interactive wizard phases.
3
+ *
4
+ * These tests mock @inquirer/prompts to inject canned answers, then run
5
+ * the actual phase functions end-to-end — verifying the full flow including
6
+ * conditional logic, CLI wizard delegation, and state mutations.
7
+ */
8
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
9
+ import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync, realpathSync, } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { tmpdir } from "node:os";
12
+ import { randomUUID } from "node:crypto";
13
+ import { execFile, execSync } from "node:child_process";
14
+ import { promisify } from "node:util";
15
+ const execFileAsync = promisify(execFile);
16
+ // ─── Mocks ──────────────────────────────────────────────────────────────────
17
+ // Mock homedir AND @inquirer/prompts.
18
+ // The prompt mock queues answers that are consumed in order.
19
+ // ─────────────────────────────────────────────────────────────────────────────
20
+ let testHome;
21
+ vi.mock("node:os", async () => {
22
+ const actual = await import("node:os");
23
+ return { ...actual, homedir: () => testHome };
24
+ });
25
+ // Answer queue: each call to confirm/select/input/password pops the next answer
26
+ let answerQueue = [];
27
+ vi.mock("@inquirer/prompts", () => ({
28
+ confirm: vi.fn(async () => answerQueue.shift()),
29
+ select: vi.fn(async () => answerQueue.shift()),
30
+ input: vi.fn(async () => answerQueue.shift()),
31
+ password: vi.fn(async () => answerQueue.shift()),
32
+ }));
33
+ const { selectUseCase } = await import("./use-case.js");
34
+ const { configurePackages } = await import("./package-config.js");
35
+ const { configureIntegrations } = await import("./integrations.js");
36
+ const { initGlobal } = await import("./global-setup.js");
37
+ const { initProjectPackage, isProjectInit, PROJECT_INIT_ORDER, } = await import("../../../packages/setup.js");
38
+ import { createEmptyState } from "../state.js";
39
+ import { getAllPackageNames } from "../../../packages/registry.js";
40
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
41
+ let testDir;
42
+ beforeEach(() => {
43
+ const realTmp = realpathSync(tmpdir());
44
+ testDir = join(realTmp, `swarmkit-wizard-flow-${randomUUID()}`);
45
+ testHome = join(realTmp, `swarmkit-wizard-home-${randomUUID()}`);
46
+ mkdirSync(testDir, { recursive: true });
47
+ mkdirSync(testHome, { recursive: true });
48
+ answerQueue = [];
49
+ });
50
+ afterEach(() => {
51
+ rmSync(testDir, { recursive: true, force: true });
52
+ rmSync(testHome, { recursive: true, force: true });
53
+ });
54
+ async function hasCliInstalled(command) {
55
+ try {
56
+ await execFileAsync("which", [command]);
57
+ return true;
58
+ }
59
+ catch {
60
+ return false;
61
+ }
62
+ }
63
+ function createProject(name, dir = testDir) {
64
+ execSync("git init -q", { cwd: dir });
65
+ writeFileSync(join(dir, "package.json"), JSON.stringify({ name, version: "1.0.0" }));
66
+ return dir;
67
+ }
68
+ function makeState(overrides = {}) {
69
+ return { ...createEmptyState(), ...overrides };
70
+ }
71
+ // ─── selectUseCase ───────────────────────────────────────────────────────────
72
+ describe("selectUseCase phase", () => {
73
+ it("selects all packages when user chooses 'all'", async () => {
74
+ answerQueue = ["all"];
75
+ const result = await selectUseCase(createEmptyState());
76
+ expect(result.bundle).toBe("all");
77
+ expect(result.selectedPackages).toEqual(getAllPackageNames());
78
+ });
79
+ it("walks through manual selection accepting all", async () => {
80
+ const allNames = getAllPackageNames();
81
+ // "manual" choice, then confirm=true for each package
82
+ answerQueue = ["manual", ...allNames.map(() => true)];
83
+ const result = await selectUseCase(createEmptyState());
84
+ expect(result.bundle).toBe("manual");
85
+ // Order differs from getAllPackageNames because manual walks by CATEGORY_ORDER
86
+ expect(result.selectedPackages.sort()).toEqual([...allNames].sort());
87
+ });
88
+ it("walks through manual selection accepting only some", async () => {
89
+ const allNames = getAllPackageNames();
90
+ // "manual" choice, then alternate true/false
91
+ // The walk order is by CATEGORY_ORDER, not registry order,
92
+ // so we just verify count and that only some were selected
93
+ answerQueue = [
94
+ "manual",
95
+ ...allNames.map((_, i) => i % 2 === 0),
96
+ ];
97
+ const result = await selectUseCase(createEmptyState());
98
+ expect(result.bundle).toBe("manual");
99
+ const expectedCount = allNames.filter((_, i) => i % 2 === 0).length;
100
+ expect(result.selectedPackages.length).toBe(expectedCount);
101
+ // All selected packages should be known
102
+ for (const pkg of result.selectedPackages) {
103
+ expect(allNames).toContain(pkg);
104
+ }
105
+ });
106
+ });
107
+ // ─── configurePackages ───────────────────────────────────────────────────────
108
+ describe("configurePackages phase", () => {
109
+ it("skips entirely in quick mode", async () => {
110
+ const state = makeState({
111
+ selectedPackages: ["minimem", "sessionlog"],
112
+ quick: true,
113
+ });
114
+ const result = await configurePackages(state);
115
+ expect(result.packageConfigs).toEqual({});
116
+ // No prompts should have been consumed
117
+ expect(answerQueue).toEqual([]);
118
+ });
119
+ it("skips packages without setup config", async () => {
120
+ const state = makeState({
121
+ selectedPackages: ["cognitive-core", "multi-agent-protocol"],
122
+ });
123
+ // No prompts needed — these packages have no setup config
124
+ const result = await configurePackages(state);
125
+ expect(Object.keys(result.packageConfigs)).toEqual([]);
126
+ });
127
+ it("declines wizard and declines customize → empty config", async () => {
128
+ const state = makeState({
129
+ selectedPackages: ["minimem"],
130
+ });
131
+ // minimem has cliWizard + inlineOptions
132
+ answerQueue = [
133
+ false, // "Run minimem's interactive setup wizard?" → No
134
+ false, // "Customize minimem settings?" → No
135
+ ];
136
+ const result = await configurePackages(state);
137
+ expect(result.packageConfigs.minimem).toBeDefined();
138
+ expect(result.packageConfigs.minimem.usedCliWizard).toBe(false);
139
+ expect(Object.keys(result.packageConfigs.minimem.values)).toEqual([]);
140
+ });
141
+ it("declines wizard, accepts customize → collects inline options", async () => {
142
+ const state = makeState({
143
+ selectedPackages: ["minimem"],
144
+ });
145
+ answerQueue = [
146
+ false, // "Run minimem's interactive setup wizard?" → No
147
+ true, // "Customize minimem settings?" → Yes
148
+ "0.9", // "Vector search weight (0-1):" → 0.9
149
+ "0.1", // "Text search weight (0-1):" → 0.1
150
+ "25", // "Max search results:" → 25
151
+ "0.5", // "Min relevance score (0-1):" → 0.5
152
+ ];
153
+ const result = await configurePackages(state);
154
+ expect(result.packageConfigs.minimem.usedCliWizard).toBe(false);
155
+ expect(result.packageConfigs.minimem.values).toEqual({
156
+ "hybrid.vectorWeight": "0.9",
157
+ "hybrid.textWeight": "0.1",
158
+ "query.maxResults": "25",
159
+ "query.minScore": "0.5",
160
+ });
161
+ });
162
+ it("configures sessionlog inline (no CLI wizard)", async () => {
163
+ const state = makeState({
164
+ selectedPackages: ["sessionlog"],
165
+ });
166
+ // sessionlog has no cliWizard — goes straight to inline
167
+ answerQueue = [
168
+ true, // "Customize sessionlog settings?" → Yes
169
+ true, // "Enable session logging by default?" → Yes
170
+ "auto-commit", // "Commit strategy:" → auto-commit
171
+ "git@github.com:org/sessions.git", // "Session repo remote URL:" → URL
172
+ "my-project", // "Subdirectory in session repo:" → dir
173
+ true, // "Enable session summarization?" → Yes
174
+ ];
175
+ const result = await configurePackages(state);
176
+ expect(result.packageConfigs.sessionlog.values).toEqual({
177
+ enabled: true,
178
+ strategy: "auto-commit",
179
+ "sessionRepo.remote": "git@github.com:org/sessions.git",
180
+ "sessionRepo.directory": "my-project",
181
+ summarizationEnabled: true,
182
+ });
183
+ });
184
+ it("configures claude-code-swarm with conditional options", async () => {
185
+ // With multi-agent-protocol AND sessionlog installed → all options shown
186
+ // configurePackages iterates selectedPackages in order, filtering to those with setup.
187
+ // multi-agent-protocol has no setup, so the order is: claude-code-swarm, sessionlog
188
+ const state = makeState({
189
+ selectedPackages: [
190
+ "claude-code-swarm",
191
+ "multi-agent-protocol",
192
+ "sessionlog",
193
+ ],
194
+ });
195
+ answerQueue = [
196
+ // claude-code-swarm first (no wizard, has inline)
197
+ true, // "Customize claude-code-swarm settings?" → Yes
198
+ true, // "Enable MAP observability?" → Yes
199
+ "ws://custom:9090", // "MAP server URL:" (shown because MAP pkg installed)
200
+ "my-scope", // "MAP scope:" (shown because MAP pkg installed)
201
+ true, // "Enable sessionlog bridging?" (shown because sessionlog installed)
202
+ // sessionlog next (no wizard, has inline)
203
+ true, // "Customize sessionlog settings?" → Yes
204
+ true, // "Enable session logging?" → Yes
205
+ "auto-commit", // "Commit strategy" → auto-commit
206
+ "", // "Session repo remote URL:" → blank (skip)
207
+ "", // "Subdirectory in session repo:" → blank (auto)
208
+ false, // "Enable summarization?" → No
209
+ ];
210
+ const result = await configurePackages(state);
211
+ expect(result.packageConfigs["claude-code-swarm"].values).toEqual({
212
+ "map.enabled": true,
213
+ "map.server": "ws://custom:9090",
214
+ "map.scope": "my-scope",
215
+ "sessionlog.enabled": true,
216
+ });
217
+ expect(result.packageConfigs.sessionlog.values).toEqual({
218
+ enabled: true,
219
+ strategy: "auto-commit",
220
+ "sessionRepo.remote": "",
221
+ "sessionRepo.directory": "",
222
+ summarizationEnabled: false,
223
+ });
224
+ });
225
+ it("hides conditional options when dependency packages not installed", async () => {
226
+ // Without multi-agent-protocol or sessionlog → conditional options hidden
227
+ const state = makeState({
228
+ selectedPackages: ["claude-code-swarm"],
229
+ });
230
+ answerQueue = [
231
+ true, // "Customize claude-code-swarm settings?" → Yes
232
+ true, // "Enable MAP observability?" → Yes (always shown)
233
+ // "MAP server URL" NOT shown (multi-agent-protocol not installed)
234
+ // "MAP scope" NOT shown
235
+ // "Enable sessionlog bridging" NOT shown (sessionlog not installed)
236
+ ];
237
+ const result = await configurePackages(state);
238
+ // Only the non-conditional option was collected
239
+ expect(result.packageConfigs["claude-code-swarm"].values).toEqual({
240
+ "map.enabled": true,
241
+ });
242
+ });
243
+ it("configures multiple packages in sequence", async () => {
244
+ const state = makeState({
245
+ selectedPackages: ["minimem", "sessionlog", "claude-code-swarm"],
246
+ });
247
+ answerQueue = [
248
+ // minimem
249
+ false, // wizard → No
250
+ false, // customize → No
251
+ // sessionlog
252
+ true, // customize → Yes
253
+ true, // enabled → Yes
254
+ "manual-commit", // strategy
255
+ "", // session repo remote → blank (skip)
256
+ "", // session repo directory → blank (auto)
257
+ false, // summarization → No
258
+ // claude-code-swarm
259
+ true, // customize → Yes
260
+ false, // MAP → No
261
+ ];
262
+ const result = await configurePackages(state);
263
+ expect(Object.keys(result.packageConfigs)).toEqual([
264
+ "minimem",
265
+ "sessionlog",
266
+ "claude-code-swarm",
267
+ ]);
268
+ expect(result.packageConfigs.sessionlog.values.enabled).toBe(true);
269
+ expect(result.packageConfigs["claude-code-swarm"].values["map.enabled"]).toBe(false);
270
+ });
271
+ it("handles self-driving-repo inline fallback (template selection)", async () => {
272
+ const state = makeState({
273
+ selectedPackages: ["self-driving-repo"],
274
+ });
275
+ answerQueue = [
276
+ false, // wizard → No
277
+ true, // customize → Yes
278
+ "pr-review", // template selection
279
+ ];
280
+ const result = await configurePackages(state);
281
+ expect(result.packageConfigs["self-driving-repo"].values).toEqual({
282
+ template: "pr-review",
283
+ });
284
+ });
285
+ });
286
+ // ─── configureIntegrations ───────────────────────────────────────────────────
287
+ describe("configureIntegrations phase", () => {
288
+ it("returns unchanged state when no integrations active", async () => {
289
+ const state = makeState({
290
+ selectedPackages: ["opentasks"], // no integration pairs
291
+ });
292
+ const result = await configureIntegrations(state);
293
+ expect(result.integrationWiring).toEqual([]);
294
+ });
295
+ it("shows auto-detected integrations without prompting", async () => {
296
+ const state = makeState({
297
+ selectedPackages: ["cognitive-core", "minimem"],
298
+ });
299
+ // Auto-detected → no prompts needed
300
+ const result = await configureIntegrations(state);
301
+ // No wiring entries for auto-detected
302
+ expect(result.integrationWiring).toEqual([]);
303
+ });
304
+ it("skips wirable integrations in quick mode", async () => {
305
+ const state = makeState({
306
+ selectedPackages: ["claude-code-swarm", "sessionlog"],
307
+ quick: true,
308
+ });
309
+ const result = await configureIntegrations(state);
310
+ expect(result.integrationWiring).toEqual([]);
311
+ });
312
+ it("prompts for wirable integrations and collects config", async () => {
313
+ const state = makeState({
314
+ selectedPackages: ["claude-code-swarm", "sessionlog"],
315
+ });
316
+ answerQueue = [
317
+ true, // "Configure this integration?" → Yes
318
+ true, // "Enable session data bridging?" → Yes
319
+ "live", // "Session sync mode:" → live
320
+ ];
321
+ const result = await configureIntegrations(state);
322
+ expect(result.integrationWiring).toHaveLength(1);
323
+ expect(result.integrationWiring[0]).toEqual({
324
+ key: "claude-code-swarm:sessionlog",
325
+ enabled: true,
326
+ values: {
327
+ sessionBridging: true,
328
+ sessionSync: "live",
329
+ },
330
+ });
331
+ });
332
+ it("records disabled wiring when user declines integration", async () => {
333
+ const state = makeState({
334
+ selectedPackages: ["claude-code-swarm", "sessionlog"],
335
+ });
336
+ answerQueue = [
337
+ false, // "Configure this integration?" → No
338
+ ];
339
+ const result = await configureIntegrations(state);
340
+ expect(result.integrationWiring).toHaveLength(1);
341
+ expect(result.integrationWiring[0].enabled).toBe(false);
342
+ expect(result.integrationWiring[0].values).toEqual({});
343
+ });
344
+ it("handles multiple wirable integrations", async () => {
345
+ const state = makeState({
346
+ selectedPackages: [
347
+ "claude-code-swarm",
348
+ "openteams",
349
+ "multi-agent-protocol",
350
+ "sessionlog",
351
+ ],
352
+ });
353
+ answerQueue = [
354
+ // claude-code-swarm ↔ openteams
355
+ true, // configure → Yes
356
+ "gsd", // template → gsd
357
+ // claude-code-swarm ↔ multi-agent-protocol
358
+ true, // configure → Yes
359
+ false, // enable MAP → No
360
+ "ws://localhost:8080", // MAP server
361
+ // claude-code-swarm ↔ sessionlog
362
+ false, // configure → No
363
+ ];
364
+ const result = await configureIntegrations(state);
365
+ expect(result.integrationWiring).toHaveLength(3);
366
+ const openteamsWiring = result.integrationWiring.find((w) => w.key === "claude-code-swarm:openteams");
367
+ expect(openteamsWiring?.enabled).toBe(true);
368
+ expect(openteamsWiring?.values.template).toBe("gsd");
369
+ const mapWiring = result.integrationWiring.find((w) => w.key === "claude-code-swarm:multi-agent-protocol");
370
+ expect(mapWiring?.enabled).toBe(true);
371
+ const sessionlogWiring = result.integrationWiring.find((w) => w.key === "claude-code-swarm:sessionlog");
372
+ expect(sessionlogWiring?.enabled).toBe(false);
373
+ });
374
+ it("handles mixed auto-detected + wirable integrations", async () => {
375
+ const state = makeState({
376
+ selectedPackages: [
377
+ "cognitive-core",
378
+ "minimem",
379
+ "claude-code-swarm",
380
+ "sessionlog",
381
+ ],
382
+ });
383
+ answerQueue = [
384
+ // Only claude-code-swarm ↔ sessionlog is wirable
385
+ true, // configure → Yes
386
+ false, // bridging → No
387
+ "off", // sync mode → off
388
+ ];
389
+ const result = await configureIntegrations(state);
390
+ // Only wirable integration has wiring entry
391
+ expect(result.integrationWiring).toHaveLength(1);
392
+ expect(result.integrationWiring[0].key).toBe("claude-code-swarm:sessionlog");
393
+ });
394
+ });
395
+ // ─── configurePackages: CLI wizard delegation path ───────────────────────────
396
+ //
397
+ // These tests answer "true" to "Run wizard?" so configurePackages actually
398
+ // calls runPackageWizard → spawns the real CLI → verifies filesystem results.
399
+ // We chdir into testDir so process.cwd() matches where the wizard should run.
400
+ // ─────────────────────────────────────────────────────────────────────────────
401
+ describe("configurePackages — CLI wizard delegation (real CLIs)", async () => {
402
+ const opentasksOk = await hasCliInstalled("opentasks");
403
+ const skOk = await hasCliInstalled("skill-tree");
404
+ let originalCwd;
405
+ beforeEach(() => {
406
+ originalCwd = process.cwd();
407
+ });
408
+ afterEach(() => {
409
+ process.chdir(originalCwd);
410
+ });
411
+ it.skipIf(!opentasksOk)("accepts wizard → spawns opentasks init → sets usedCliWizard + creates config", async () => {
412
+ createProject("wizard-delegate-ot");
413
+ process.chdir(testDir);
414
+ const state = makeState({
415
+ selectedPackages: ["opentasks"],
416
+ usePrefix: true,
417
+ });
418
+ answerQueue = [
419
+ true, // "Run opentasks's interactive setup wizard?" → Yes
420
+ ];
421
+ const result = await configurePackages(state);
422
+ // usedCliWizard should be set
423
+ expect(result.packageConfigs.opentasks).toBeDefined();
424
+ expect(result.packageConfigs.opentasks.usedCliWizard).toBe(true);
425
+ expect(result.packageConfigs.opentasks.values).toEqual({});
426
+ // opentasks should have created config — either at .swarm/opentasks/ (if
427
+ // the env var was respected) or at .opentasks/ (relocated after)
428
+ const prefixed = existsSync(join(testDir, ".swarm", "opentasks", "config.json"));
429
+ const flat = existsSync(join(testDir, ".opentasks", "config.json"));
430
+ expect(prefixed || flat).toBe(true);
431
+ // Verify the config has valid opentasks structure
432
+ if (prefixed) {
433
+ const config = JSON.parse(readFileSync(join(testDir, ".swarm", "opentasks", "config.json"), "utf-8"));
434
+ expect(config.version).toBe("1.0");
435
+ expect(config.location).toBeDefined();
436
+ expect(config.location.hash).toBeDefined();
437
+ }
438
+ });
439
+ it.skipIf(!opentasksOk)("wizard path with usePrefix=false creates at flat location", async () => {
440
+ createProject("wizard-flat-ot");
441
+ process.chdir(testDir);
442
+ const state = makeState({
443
+ selectedPackages: ["opentasks"],
444
+ usePrefix: false,
445
+ });
446
+ answerQueue = [
447
+ true, // wizard → Yes
448
+ ];
449
+ const result = await configurePackages(state);
450
+ expect(result.packageConfigs.opentasks.usedCliWizard).toBe(true);
451
+ // With usePrefix=false, should be at .opentasks/
452
+ expect(existsSync(join(testDir, ".opentasks", "config.json"))).toBe(true);
453
+ // Should NOT be relocated to .swarm/
454
+ expect(existsSync(join(testDir, ".swarm", "opentasks"))).toBe(false);
455
+ });
456
+ it.skipIf(!opentasksOk)("wizard fallback: wizard fails → falls through to inline options", async () => {
457
+ createProject("wizard-fail-fallback");
458
+ process.chdir(testDir);
459
+ // Use a package that has both cliWizard and inlineOptions: self-driving-repo
460
+ // sdr is not installed, so the wizard will fail → should fall through to inline
461
+ const sdrInstalled = await hasCliInstalled("sdr");
462
+ if (!sdrInstalled) {
463
+ const state = makeState({
464
+ selectedPackages: ["self-driving-repo"],
465
+ usePrefix: true,
466
+ });
467
+ answerQueue = [
468
+ true, // "Run wizard?" → Yes (but sdr not installed → fails)
469
+ true, // "Customize settings?" → Yes (fallback)
470
+ "pr-review", // template selection
471
+ ];
472
+ const result = await configurePackages(state);
473
+ // Should have fallen through to inline
474
+ expect(result.packageConfigs["self-driving-repo"].usedCliWizard).toBe(false);
475
+ expect(result.packageConfigs["self-driving-repo"].values.template).toBe("pr-review");
476
+ }
477
+ });
478
+ it.skipIf(!opentasksOk)("wizard + inline for different packages in same run", async () => {
479
+ createProject("wizard-mixed");
480
+ process.chdir(testDir);
481
+ const state = makeState({
482
+ selectedPackages: ["opentasks", "sessionlog"],
483
+ usePrefix: true,
484
+ });
485
+ answerQueue = [
486
+ // opentasks: accept wizard
487
+ true, // "Run wizard?" → Yes
488
+ // sessionlog: no wizard available, goes to inline
489
+ true, // "Customize?" → Yes
490
+ true, // enabled → Yes
491
+ "auto-commit", // strategy
492
+ false, // summarization → No
493
+ ];
494
+ const result = await configurePackages(state);
495
+ // opentasks used wizard
496
+ expect(result.packageConfigs.opentasks.usedCliWizard).toBe(true);
497
+ // sessionlog used inline
498
+ expect(result.packageConfigs.sessionlog.usedCliWizard).toBe(false);
499
+ expect(result.packageConfigs.sessionlog.values.enabled).toBe(true);
500
+ expect(result.packageConfigs.sessionlog.values.strategy).toBe("auto-commit");
501
+ // opentasks config exists on disk
502
+ const otExists = existsSync(join(testDir, ".swarm", "opentasks", "config.json")) ||
503
+ existsSync(join(testDir, ".opentasks", "config.json"));
504
+ expect(otExists).toBe(true);
505
+ });
506
+ it.skipIf(!opentasksOk)("wizard-configured package is skipped by initGlobal and initProject", async () => {
507
+ createProject("wizard-skip-init");
508
+ process.chdir(testDir);
509
+ const state = makeState({
510
+ selectedPackages: ["opentasks", "sessionlog", "claude-code-swarm"],
511
+ usePrefix: true,
512
+ embeddingProvider: null,
513
+ });
514
+ // Phase 4: opentasks via wizard, sessionlog+claude-code-swarm via inline defaults
515
+ answerQueue = [
516
+ true, // opentasks wizard → Yes
517
+ false, // sessionlog customize → No
518
+ false, // claude-code-swarm customize → No
519
+ ];
520
+ const updated = await configurePackages(state);
521
+ expect(updated.packageConfigs.opentasks.usedCliWizard).toBe(true);
522
+ expect(updated.packageConfigs.sessionlog.usedCliWizard).toBe(false);
523
+ expect(updated.packageConfigs["claude-code-swarm"].usedCliWizard).toBe(false);
524
+ // Phase 5: initGlobal — claude-code-swarm global should still init
525
+ await initGlobal(updated);
526
+ expect(existsSync(join(testHome, ".claude-swarm", "config.json"))).toBe(true);
527
+ // Phase 6: initProject (manually) — opentasks should be skipped
528
+ // Either because isProjectInit sees the wizard's output, or because
529
+ // usedCliWizard is true. Both paths correctly skip re-init.
530
+ const results = [];
531
+ for (const pkg of PROJECT_INIT_ORDER) {
532
+ if (!updated.selectedPackages.includes(pkg))
533
+ continue;
534
+ if (isProjectInit(testDir, pkg)) {
535
+ results.push(`${pkg}:already-init`);
536
+ continue;
537
+ }
538
+ if (updated.packageConfigs[pkg]?.usedCliWizard) {
539
+ results.push(`${pkg}:wizard-skip`);
540
+ continue;
541
+ }
542
+ const ctx = {
543
+ cwd: testDir,
544
+ packages: updated.selectedPackages,
545
+ embeddingProvider: updated.embeddingProvider,
546
+ apiKeys: updated.apiKeys,
547
+ usePrefix: updated.usePrefix,
548
+ packageConfigs: updated.packageConfigs,
549
+ };
550
+ await initProjectPackage(pkg, ctx);
551
+ results.push(`${pkg}:init`);
552
+ }
553
+ // opentasks was skipped (wizard created its config, isProjectInit returns true)
554
+ const otResult = results.find((r) => r.startsWith("opentasks:"));
555
+ expect(otResult).toMatch(/^opentasks:(already-init|wizard-skip)$/);
556
+ // sessionlog + claude-code-swarm were inited normally
557
+ expect(results).toContain("sessionlog:init");
558
+ expect(results).toContain("claude-code-swarm:init");
559
+ // All configs exist on disk
560
+ expect(existsSync(join(testDir, ".swarm", "sessionlog", "settings.json"))).toBe(true);
561
+ expect(existsSync(join(testDir, ".swarm", "claude-swarm", "config.json"))).toBe(true);
562
+ });
563
+ });
564
+ // ─── E2E: full wizard flow simulation ────────────────────────────────────────
565
+ describe("e2e: full wizard flow with mocked prompts", async () => {
566
+ const opentasksOk = await hasCliInstalled("opentasks");
567
+ it.skipIf(!opentasksOk)("selectUseCase → configurePackages → initGlobal → initProject → configureIntegrations", async () => {
568
+ createProject("full-wizard-e2e");
569
+ // Phase 1: select "all"
570
+ answerQueue = ["all"];
571
+ let state = await selectUseCase(createEmptyState());
572
+ expect(state.bundle).toBe("all");
573
+ state.usePrefix = true;
574
+ state.embeddingProvider = "openai";
575
+ // Phase 4: configurePackages — decline all wizards, customize sessionlog + claude-code-swarm
576
+ answerQueue = [
577
+ // self-driving-repo (has wizard + inline)
578
+ false, // wizard → No
579
+ false, // customize → No
580
+ // opentasks (has wizard only)
581
+ false, // wizard → No
582
+ // minimem (has wizard + inline)
583
+ false, // wizard → No
584
+ false, // customize → No
585
+ // skill-tree (has wizard only)
586
+ false, // wizard → No
587
+ // openhive (has wizard + inline)
588
+ false, // wizard → No
589
+ false, // customize → No
590
+ // openteams (has wizard only)
591
+ false, // wizard → No
592
+ // sessionlog (inline only)
593
+ true, // customize → Yes
594
+ true, // enabled → Yes
595
+ "auto-commit", // strategy
596
+ "", // session repo remote → blank (skip)
597
+ "", // session repo directory → blank (auto)
598
+ true, // summarization → Yes
599
+ // claude-code-swarm (inline only)
600
+ true, // customize → Yes
601
+ true, // MAP enabled → Yes
602
+ "ws://test:8080", // MAP server (shown because multi-agent-protocol selected)
603
+ "test-scope", // MAP scope
604
+ true, // sessionlog bridging (shown because sessionlog selected)
605
+ ];
606
+ state = await configurePackages(state);
607
+ expect(state.packageConfigs.sessionlog).toBeDefined();
608
+ expect(state.packageConfigs.sessionlog.values.enabled).toBe(true);
609
+ expect(state.packageConfigs["claude-code-swarm"].values["map.enabled"]).toBe(true);
610
+ // Phase 5: initGlobal — creates skill-tree + claude-code-swarm global
611
+ await initGlobal(state);
612
+ expect(existsSync(join(testHome, ".claude-swarm", "config.json"))).toBe(true);
613
+ // Verify global claude-code-swarm got overrides
614
+ const globalCs = JSON.parse(readFileSync(join(testHome, ".claude-swarm", "config.json"), "utf-8"));
615
+ expect(globalCs.map.enabled).toBe(true);
616
+ expect(globalCs.map.server).toBe("ws://test:8080");
617
+ // Phase 6: initProject (call directly, skipping the confirm prompt)
618
+ const packages = ["opentasks", "minimem", "sessionlog", "claude-code-swarm"];
619
+ for (const pkg of PROJECT_INIT_ORDER) {
620
+ if (!packages.includes(pkg))
621
+ continue;
622
+ if (isProjectInit(testDir, pkg))
623
+ continue;
624
+ const ctx = {
625
+ cwd: testDir,
626
+ packages: state.selectedPackages,
627
+ embeddingProvider: state.embeddingProvider,
628
+ apiKeys: state.apiKeys,
629
+ usePrefix: state.usePrefix,
630
+ packageConfigs: state.packageConfigs,
631
+ };
632
+ await initProjectPackage(pkg, ctx);
633
+ }
634
+ // Verify project configs
635
+ const slSettings = JSON.parse(readFileSync(join(testDir, ".swarm", "sessionlog", "settings.json"), "utf-8"));
636
+ expect(slSettings.enabled).toBe(true);
637
+ expect(slSettings.strategy).toBe("auto-commit");
638
+ expect(slSettings.summarizationEnabled).toBe(true);
639
+ const csConfig = JSON.parse(readFileSync(join(testDir, ".swarm", "claude-swarm", "config.json"), "utf-8"));
640
+ expect(csConfig.map.enabled).toBe(true);
641
+ expect(csConfig.map.server).toBe("ws://test:8080");
642
+ expect(csConfig.sessionlog.enabled).toBe(true);
643
+ // Phase 7: configureIntegrations
644
+ answerQueue = [
645
+ // claude-code-swarm ↔ openteams
646
+ true, "gsd",
647
+ // claude-code-swarm ↔ multi-agent-protocol
648
+ false,
649
+ // claude-code-swarm ↔ sessionlog
650
+ true, true, "live",
651
+ ];
652
+ state = await configureIntegrations(state);
653
+ const sessionWiring = state.integrationWiring.find((w) => w.key === "claude-code-swarm:sessionlog");
654
+ expect(sessionWiring?.enabled).toBe(true);
655
+ expect(sessionWiring?.values.sessionSync).toBe("live");
656
+ });
657
+ });
@@ -0,0 +1 @@
1
+ export {};