swarmkit 0.0.6 → 0.0.8

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 (40) 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/index.d.ts +3 -2
  34. package/dist/index.js +2 -1
  35. package/dist/packages/registry.d.ts +66 -0
  36. package/dist/packages/registry.js +258 -0
  37. package/dist/packages/setup.d.ts +42 -0
  38. package/dist/packages/setup.js +244 -15
  39. package/dist/packages/setup.test.js +520 -13
  40. package/package.json +1 -1
@@ -0,0 +1,711 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync, realpathSync, } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { randomUUID } from "node:crypto";
6
+ import { execFile, execSync } from "node:child_process";
7
+ import { promisify } from "node:util";
8
+ const execFileAsync = promisify(execFile);
9
+ // ─── Minimal mocking ────────────────────────────────────────────────────────
10
+ // Only mock homedir — everything else (filesystem, CLI shell-outs) is real.
11
+ // ─────────────────────────────────────────────────────────────────────────────
12
+ let testHome;
13
+ vi.mock("node:os", async () => {
14
+ const actual = await import("node:os");
15
+ return { ...actual, homedir: () => testHome };
16
+ });
17
+ const { initGlobal } = await import("./global-setup.js");
18
+ const { showReview } = await import("./review.js");
19
+ const { applyIntegrationWiring } = await import("./integrations.js");
20
+ const { isProjectInit, isGlobalInit, initProjectPackage, PROJECT_INIT_ORDER, } = await import("../../../packages/setup.js");
21
+ import { createEmptyState } from "../state.js";
22
+ import { readPackageConfig, getNestedValue } from "../../configure/read-config.js";
23
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
24
+ let testDir;
25
+ beforeEach(() => {
26
+ const realTmp = realpathSync(tmpdir());
27
+ testDir = join(realTmp, `swarmkit-phases-test-${randomUUID()}`);
28
+ testHome = join(realTmp, `swarmkit-phases-home-${randomUUID()}`);
29
+ mkdirSync(testDir, { recursive: true });
30
+ mkdirSync(testHome, { recursive: true });
31
+ });
32
+ afterEach(() => {
33
+ rmSync(testDir, { recursive: true, force: true });
34
+ rmSync(testHome, { recursive: true, force: true });
35
+ });
36
+ async function hasCliInstalled(command) {
37
+ try {
38
+ await execFileAsync("which", [command]);
39
+ return true;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ function createProject(name, dir = testDir) {
46
+ execSync("git init -q", { cwd: dir });
47
+ writeFileSync(join(dir, "package.json"), JSON.stringify({ name, version: "1.0.0" }));
48
+ return dir;
49
+ }
50
+ function makeState(overrides = {}) {
51
+ return {
52
+ ...createEmptyState(),
53
+ ...overrides,
54
+ };
55
+ }
56
+ // ─── initGlobal phase ────────────────────────────────────────────────────────
57
+ describe("initGlobal phase", () => {
58
+ it("skips when no global packages selected", async () => {
59
+ const state = makeState({
60
+ selectedPackages: ["opentasks", "minimem"],
61
+ });
62
+ await initGlobal(state);
63
+ // No global dirs created
64
+ expect(existsSync(join(testHome, ".skill-tree"))).toBe(false);
65
+ expect(existsSync(join(testHome, ".claude-swarm"))).toBe(false);
66
+ expect(state.configsWritten).toEqual([]);
67
+ });
68
+ it("initializes claude-code-swarm global config with defaults", async () => {
69
+ const state = makeState({
70
+ selectedPackages: ["claude-code-swarm"],
71
+ });
72
+ await initGlobal(state);
73
+ expect(existsSync(join(testHome, ".claude-swarm", "config.json"))).toBe(true);
74
+ const config = JSON.parse(readFileSync(join(testHome, ".claude-swarm", "config.json"), "utf-8"));
75
+ expect(config.map.server).toBe("");
76
+ expect(config.map.sidecar).toBe("session");
77
+ expect(config.sessionlog.enabled).toBe(false);
78
+ expect(state.configsWritten).toContainEqual(expect.objectContaining({
79
+ package: "claude-code-swarm",
80
+ path: "~/.claude-swarm/",
81
+ }));
82
+ });
83
+ it("initializes claude-code-swarm global with packageConfigs overrides", async () => {
84
+ const state = makeState({
85
+ selectedPackages: ["claude-code-swarm"],
86
+ packageConfigs: {
87
+ "claude-code-swarm": {
88
+ values: {
89
+ "map.server": "ws://custom:9090",
90
+ "sessionlog.enabled": true,
91
+ },
92
+ usedCliWizard: false,
93
+ },
94
+ },
95
+ });
96
+ await initGlobal(state);
97
+ const config = JSON.parse(readFileSync(join(testHome, ".claude-swarm", "config.json"), "utf-8"));
98
+ expect(config.map.server).toBe("ws://custom:9090");
99
+ expect(config.sessionlog.enabled).toBe(true);
100
+ // Defaults preserved
101
+ expect(config.map.sidecar).toBe("session");
102
+ });
103
+ it("skips already-configured global packages", async () => {
104
+ // Pre-create claude-swarm global config
105
+ mkdirSync(join(testHome, ".claude-swarm"), { recursive: true });
106
+ writeFileSync(join(testHome, ".claude-swarm", "config.json"), JSON.stringify({ map: { server: "ws://existing:8080" } }));
107
+ const state = makeState({
108
+ selectedPackages: ["claude-code-swarm"],
109
+ });
110
+ await initGlobal(state);
111
+ // Should not overwrite
112
+ const config = JSON.parse(readFileSync(join(testHome, ".claude-swarm", "config.json"), "utf-8"));
113
+ expect(config.map.server).toBe("ws://existing:8080");
114
+ // No configsWritten since it was skipped
115
+ expect(state.configsWritten).toEqual([]);
116
+ });
117
+ it("re-initializes when force=true even if already configured", async () => {
118
+ // Pre-create claude-swarm global config
119
+ mkdirSync(join(testHome, ".claude-swarm"), { recursive: true });
120
+ writeFileSync(join(testHome, ".claude-swarm", "config.json"), JSON.stringify({ map: { server: "ws://old:8080" } }));
121
+ const state = makeState({
122
+ selectedPackages: ["claude-code-swarm"],
123
+ });
124
+ // force=true, but initGlobalPackage still doesn't overwrite existing config.json
125
+ // (that's initClaudeSwarmGlobal's behavior — the `if (!existsSync(configPath))` check)
126
+ await initGlobal(state, true);
127
+ // The config existed, so it was NOT overwritten (existing file protection)
128
+ const config = JSON.parse(readFileSync(join(testHome, ".claude-swarm", "config.json"), "utf-8"));
129
+ expect(config.map.server).toBe("ws://old:8080");
130
+ // But configsWritten is populated since force ran
131
+ expect(state.configsWritten.length).toBeGreaterThan(0);
132
+ });
133
+ it("skips packages whose CLI wizard already ran", async () => {
134
+ const state = makeState({
135
+ selectedPackages: ["claude-code-swarm"],
136
+ packageConfigs: {
137
+ "claude-code-swarm": {
138
+ values: {},
139
+ usedCliWizard: true,
140
+ },
141
+ },
142
+ });
143
+ await initGlobal(state);
144
+ // Global config should NOT be created (wizard handled it)
145
+ expect(existsSync(join(testHome, ".claude-swarm", "config.json"))).toBe(false);
146
+ });
147
+ });
148
+ describe("initGlobal phase — skill-tree (real CLI)", async () => {
149
+ const installed = await hasCliInstalled("skill-tree");
150
+ it.skipIf(!installed)("initializes skill-tree global config", async () => {
151
+ const state = makeState({
152
+ selectedPackages: ["skill-tree"],
153
+ });
154
+ await initGlobal(state);
155
+ expect(existsSync(join(testHome, ".skill-tree", "config.yaml"))).toBe(true);
156
+ expect(state.configsWritten).toContainEqual(expect.objectContaining({ package: "skill-tree" }));
157
+ });
158
+ it.skipIf(!installed)("initializes skill-tree + claude-code-swarm in correct order", async () => {
159
+ const state = makeState({
160
+ selectedPackages: ["skill-tree", "claude-code-swarm"],
161
+ });
162
+ await initGlobal(state);
163
+ expect(existsSync(join(testHome, ".skill-tree", "config.yaml"))).toBe(true);
164
+ expect(existsSync(join(testHome, ".claude-swarm", "config.json"))).toBe(true);
165
+ // configsWritten order matches GLOBAL_SETUP_ORDER
166
+ const written = state.configsWritten.map((c) => c.package);
167
+ expect(written).toEqual(["skill-tree", "claude-code-swarm"]);
168
+ });
169
+ });
170
+ // ─── E2E: full init flow simulation ──────────────────────────────────────────
171
+ //
172
+ // Simulates the wizard's runFirstTimeSetup flow without interactive prompts:
173
+ // 1. Build state (simulating phases 1-4)
174
+ // 2. Call initGlobal (phase 5)
175
+ // 3. Run project init directly via initProjectPackage (simulating phase 6)
176
+ // 4. Call showReview (phase 8)
177
+ // ─────────────────────────────────────────────────────────────────────────────
178
+ describe("e2e: full init flow — solo bundle", async () => {
179
+ const opentasksOk = await hasCliInstalled("opentasks");
180
+ it.skipIf(!opentasksOk)("initializes global + project configs end-to-end", async () => {
181
+ createProject("e2e-solo");
182
+ const state = makeState({
183
+ bundle: "all",
184
+ selectedPackages: ["opentasks", "minimem"],
185
+ embeddingProvider: "openai",
186
+ usePrefix: true,
187
+ });
188
+ // Phase 5: global setup (no global packages in solo bundle)
189
+ await initGlobal(state);
190
+ expect(state.configsWritten.length).toBe(0);
191
+ // Phase 6: project init (simulate — call initProjectPackage directly)
192
+ for (const pkg of PROJECT_INIT_ORDER) {
193
+ if (!state.selectedPackages.includes(pkg))
194
+ continue;
195
+ if (isProjectInit(testDir, pkg))
196
+ continue;
197
+ const ctx = {
198
+ cwd: testDir,
199
+ packages: state.selectedPackages,
200
+ embeddingProvider: state.embeddingProvider,
201
+ apiKeys: state.apiKeys,
202
+ usePrefix: state.usePrefix,
203
+ packageConfigs: state.packageConfigs,
204
+ };
205
+ const result = await initProjectPackage(pkg, ctx);
206
+ if (result.success) {
207
+ state.configsWritten.push({
208
+ package: pkg,
209
+ path: `.swarm/${pkg}/`,
210
+ description: `Project ${pkg} config`,
211
+ });
212
+ }
213
+ }
214
+ // Verify filesystem
215
+ expect(existsSync(join(testDir, ".swarm", "opentasks", "config.json"))).toBe(true);
216
+ expect(existsSync(join(testDir, ".swarm", "minimem", "config.json"))).toBe(true);
217
+ // Verify configs
218
+ const otConfig = readPackageConfig("opentasks", testDir);
219
+ expect(getNestedValue(otConfig, "location.name")).toBe("e2e-solo");
220
+ const mmConfig = readPackageConfig("minimem", testDir);
221
+ expect(getNestedValue(mmConfig, "embedding.provider")).toBe("openai");
222
+ // Verify state tracking
223
+ expect(state.configsWritten.map((c) => c.package)).toEqual([
224
+ "opentasks",
225
+ "minimem",
226
+ ]);
227
+ // Phase 8: review runs without error
228
+ await showReview(state);
229
+ });
230
+ });
231
+ describe("e2e: full init flow — team bundle with learning", async () => {
232
+ const opentasksOk = await hasCliInstalled("opentasks");
233
+ const ccOk = await hasCliInstalled("cognitive-core");
234
+ const stOk = await hasCliInstalled("skill-tree");
235
+ const allOk = opentasksOk && ccOk && stOk;
236
+ it.skipIf(!allOk)("initializes global + project configs with cross-package wiring", async () => {
237
+ createProject("e2e-team");
238
+ const state = makeState({
239
+ bundle: "all",
240
+ selectedPackages: [
241
+ "opentasks",
242
+ "minimem",
243
+ "cognitive-core",
244
+ "skill-tree",
245
+ ],
246
+ embeddingProvider: "gemini",
247
+ usePrefix: true,
248
+ });
249
+ // Phase 5: global setup (skill-tree is global)
250
+ await initGlobal(state);
251
+ expect(existsSync(join(testHome, ".skill-tree", "config.yaml"))).toBe(true);
252
+ expect(state.configsWritten).toContainEqual(expect.objectContaining({ package: "skill-tree" }));
253
+ // Phase 6: project init
254
+ for (const pkg of PROJECT_INIT_ORDER) {
255
+ if (!state.selectedPackages.includes(pkg))
256
+ continue;
257
+ if (isProjectInit(testDir, pkg))
258
+ continue;
259
+ const ctx = {
260
+ cwd: testDir,
261
+ packages: state.selectedPackages,
262
+ embeddingProvider: state.embeddingProvider,
263
+ apiKeys: state.apiKeys,
264
+ usePrefix: state.usePrefix,
265
+ packageConfigs: state.packageConfigs,
266
+ };
267
+ const result = await initProjectPackage(pkg, ctx);
268
+ if (result.success) {
269
+ state.configsWritten.push({
270
+ package: pkg,
271
+ path: `.swarm/${pkg}/`,
272
+ description: `Project ${pkg} config`,
273
+ });
274
+ }
275
+ }
276
+ // All project dirs exist
277
+ expect(existsSync(join(testDir, ".swarm", "opentasks"))).toBe(true);
278
+ expect(existsSync(join(testDir, ".swarm", "minimem"))).toBe(true);
279
+ expect(existsSync(join(testDir, ".swarm", "cognitive-core"))).toBe(true);
280
+ expect(existsSync(join(testDir, ".swarm", "skilltree"))).toBe(true);
281
+ // minimem has gemini provider
282
+ const mmConfig = readPackageConfig("minimem", testDir);
283
+ expect(getNestedValue(mmConfig, "embedding.provider")).toBe("gemini");
284
+ // Phase 8: review runs without error
285
+ await showReview(state);
286
+ });
287
+ });
288
+ describe("e2e: full init flow — with packageConfigs overrides", async () => {
289
+ const opentasksOk = await hasCliInstalled("opentasks");
290
+ it.skipIf(!opentasksOk)("package config overrides flow through global + project init", async () => {
291
+ createProject("e2e-overrides");
292
+ const state = makeState({
293
+ bundle: "all",
294
+ selectedPackages: [
295
+ "opentasks",
296
+ "minimem",
297
+ "sessionlog",
298
+ "claude-code-swarm",
299
+ ],
300
+ embeddingProvider: "openai",
301
+ usePrefix: true,
302
+ packageConfigs: {
303
+ minimem: {
304
+ values: {
305
+ "query.maxResults": "50",
306
+ "hybrid.vectorWeight": "0.85",
307
+ },
308
+ usedCliWizard: false,
309
+ },
310
+ sessionlog: {
311
+ values: {
312
+ enabled: true,
313
+ strategy: "auto-commit",
314
+ summarizationEnabled: true,
315
+ },
316
+ usedCliWizard: false,
317
+ },
318
+ "claude-code-swarm": {
319
+ values: {
320
+ "map.enabled": true,
321
+ "map.server": "ws://production:8080",
322
+ "sessionlog.enabled": true,
323
+ "sessionlog.sync": "on-finish",
324
+ },
325
+ usedCliWizard: false,
326
+ },
327
+ },
328
+ });
329
+ // Phase 5: global setup (claude-code-swarm global)
330
+ await initGlobal(state);
331
+ expect(existsSync(join(testHome, ".claude-swarm", "config.json"))).toBe(true);
332
+ // Global config has overrides applied
333
+ const globalCsConfig = JSON.parse(readFileSync(join(testHome, ".claude-swarm", "config.json"), "utf-8"));
334
+ expect(globalCsConfig.map.enabled).toBe(true);
335
+ expect(globalCsConfig.map.server).toBe("ws://production:8080");
336
+ expect(globalCsConfig.sessionlog.enabled).toBe(true);
337
+ expect(globalCsConfig.sessionlog.sync).toBe("on-finish");
338
+ // Phase 6: project init
339
+ for (const pkg of PROJECT_INIT_ORDER) {
340
+ if (!state.selectedPackages.includes(pkg))
341
+ continue;
342
+ if (isProjectInit(testDir, pkg))
343
+ continue;
344
+ const ctx = {
345
+ cwd: testDir,
346
+ packages: state.selectedPackages,
347
+ embeddingProvider: state.embeddingProvider,
348
+ apiKeys: state.apiKeys,
349
+ usePrefix: state.usePrefix,
350
+ packageConfigs: state.packageConfigs,
351
+ };
352
+ const result = await initProjectPackage(pkg, ctx);
353
+ if (result.success) {
354
+ state.configsWritten.push({
355
+ package: pkg,
356
+ path: `.swarm/${pkg}/`,
357
+ description: `Project ${pkg} config`,
358
+ });
359
+ }
360
+ }
361
+ // Verify minimem overrides
362
+ const mmConfig = readPackageConfig("minimem", testDir);
363
+ expect(getNestedValue(mmConfig, "query.maxResults")).toBe(50);
364
+ expect(getNestedValue(mmConfig, "hybrid.vectorWeight")).toBe(0.85);
365
+ expect(getNestedValue(mmConfig, "embedding.provider")).toBe("openai"); // from embeddingProvider
366
+ // Verify sessionlog overrides
367
+ const slConfig = readPackageConfig("sessionlog", testDir);
368
+ expect(slConfig.enabled).toBe(true);
369
+ expect(slConfig.strategy).toBe("auto-commit");
370
+ expect(slConfig.summarizationEnabled).toBe(true);
371
+ expect(slConfig.logLevel).toBe("warn"); // default preserved
372
+ // Verify claude-code-swarm project overrides
373
+ const csConfig = readPackageConfig("claude-code-swarm", testDir);
374
+ expect(getNestedValue(csConfig, "map.enabled")).toBe(true);
375
+ expect(getNestedValue(csConfig, "map.server")).toBe("ws://production:8080");
376
+ expect(getNestedValue(csConfig, "sessionlog.enabled")).toBe(true);
377
+ expect(getNestedValue(csConfig, "map.systemId")).toBe("system-claude-swarm"); // default
378
+ // Phase 8: review runs and state is coherent
379
+ await showReview(state);
380
+ // Verify configsWritten captures all packages
381
+ const written = state.configsWritten.map((c) => c.package);
382
+ expect(written).toContain("claude-code-swarm"); // global
383
+ expect(written).toContain("opentasks");
384
+ expect(written).toContain("minimem");
385
+ expect(written).toContain("sessionlog");
386
+ });
387
+ });
388
+ // ─── showReview phase ────────────────────────────────────────────────────────
389
+ describe("showReview phase", () => {
390
+ it("runs without error for empty state", async () => {
391
+ const state = makeState({
392
+ selectedPackages: [],
393
+ });
394
+ // Should not throw
395
+ await showReview(state);
396
+ });
397
+ it("runs without error with packages but no configs written", async () => {
398
+ const state = makeState({
399
+ selectedPackages: ["opentasks", "minimem"],
400
+ });
401
+ await showReview(state);
402
+ });
403
+ it("runs without error with full state", async () => {
404
+ const state = makeState({
405
+ selectedPackages: [
406
+ "opentasks",
407
+ "minimem",
408
+ "cognitive-core",
409
+ "skill-tree",
410
+ "claude-code-swarm",
411
+ "sessionlog",
412
+ ],
413
+ packageConfigs: {
414
+ minimem: { values: { "query.maxResults": "50" }, usedCliWizard: false },
415
+ "skill-tree": { values: {}, usedCliWizard: true },
416
+ sessionlog: { values: { enabled: true }, usedCliWizard: false },
417
+ },
418
+ integrationWiring: [
419
+ { key: "claude-code-swarm:sessionlog", enabled: true, values: {} },
420
+ { key: "claude-code-swarm:openteams", enabled: false, values: {} },
421
+ ],
422
+ configsWritten: [
423
+ { package: "skill-tree", path: "~/.skill-tree/", description: "Global skill-tree config" },
424
+ { package: "opentasks", path: ".swarm/opentasks/", description: "Project opentasks config" },
425
+ { package: "minimem", path: ".swarm/minimem/", description: "Project minimem config" },
426
+ ],
427
+ });
428
+ await showReview(state);
429
+ });
430
+ it("handles state with usedCliWizard, custom config, and defaults labels", async () => {
431
+ const state = makeState({
432
+ selectedPackages: ["minimem", "sessionlog", "opentasks"],
433
+ packageConfigs: {
434
+ minimem: { values: { "query.maxResults": "50" }, usedCliWizard: false },
435
+ sessionlog: { values: {}, usedCliWizard: true },
436
+ // opentasks not in packageConfigs → shows "defaults"
437
+ },
438
+ });
439
+ // Capture console output
440
+ const logs = [];
441
+ const origLog = console.log;
442
+ console.log = (...args) => {
443
+ logs.push(args.map(String).join(" "));
444
+ };
445
+ await showReview(state);
446
+ console.log = origLog;
447
+ const output = logs.join("\n");
448
+ // sessionlog should show "configured via wizard"
449
+ expect(output).toContain("configured via wizard");
450
+ // minimem should show "custom config"
451
+ expect(output).toContain("custom config");
452
+ // opentasks should show "defaults"
453
+ expect(output).toContain("defaults");
454
+ });
455
+ });
456
+ // ─── E2E: idempotent init ────────────────────────────────────────────────────
457
+ describe("e2e: idempotent init — re-running does not overwrite", async () => {
458
+ const opentasksOk = await hasCliInstalled("opentasks");
459
+ it.skipIf(!opentasksOk)("second init run skips already-initialized packages", async () => {
460
+ createProject("idem-e2e");
461
+ const packages = ["opentasks", "minimem", "sessionlog"];
462
+ const ctx = {
463
+ cwd: testDir,
464
+ packages,
465
+ embeddingProvider: "openai",
466
+ apiKeys: {},
467
+ usePrefix: true,
468
+ packageConfigs: undefined,
469
+ };
470
+ // First init
471
+ for (const pkg of PROJECT_INIT_ORDER) {
472
+ if (!packages.includes(pkg))
473
+ continue;
474
+ if (isProjectInit(testDir, pkg))
475
+ continue;
476
+ await initProjectPackage(pkg, ctx);
477
+ }
478
+ // Modify sessionlog config after init
479
+ const slPath = join(testDir, ".swarm", "sessionlog", "settings.json");
480
+ const sl = JSON.parse(readFileSync(slPath, "utf-8"));
481
+ sl.enabled = true;
482
+ sl.strategy = "auto-commit";
483
+ writeFileSync(slPath, JSON.stringify(sl, null, 2) + "\n");
484
+ // Second init — all should be skipped (isProjectInit returns true)
485
+ const secondRunResults = [];
486
+ for (const pkg of PROJECT_INIT_ORDER) {
487
+ if (!packages.includes(pkg))
488
+ continue;
489
+ if (isProjectInit(testDir, pkg))
490
+ continue;
491
+ secondRunResults.push(await initProjectPackage(pkg, ctx));
492
+ }
493
+ // Nothing ran in second pass
494
+ expect(secondRunResults).toEqual([]);
495
+ // Modified config preserved
496
+ const slConfig = JSON.parse(readFileSync(slPath, "utf-8"));
497
+ expect(slConfig.enabled).toBe(true);
498
+ expect(slConfig.strategy).toBe("auto-commit");
499
+ });
500
+ });
501
+ // ─── applyIntegrationWiring ──────────────────────────────────────────────────
502
+ describe("applyIntegrationWiring", () => {
503
+ it("writes wiring values to target package config on disk", () => {
504
+ // Create claude-code-swarm project config
505
+ const csDir = join(testDir, ".swarm", "claude-swarm");
506
+ mkdirSync(csDir, { recursive: true });
507
+ writeFileSync(join(csDir, "config.json"), JSON.stringify({
508
+ template: "",
509
+ map: { enabled: false, server: "ws://localhost:8080" },
510
+ sessionlog: { enabled: false, sync: "off" },
511
+ }, null, 2) + "\n");
512
+ // Simulate cwd being testDir
513
+ const originalCwd = process.cwd();
514
+ process.chdir(testDir);
515
+ try {
516
+ const state = makeState({
517
+ selectedPackages: [
518
+ "claude-code-swarm",
519
+ "openteams",
520
+ "sessionlog",
521
+ ],
522
+ usePrefix: true,
523
+ integrationWiring: [
524
+ {
525
+ key: "claude-code-swarm:openteams",
526
+ enabled: true,
527
+ values: { template: "gsd" },
528
+ },
529
+ {
530
+ key: "claude-code-swarm:sessionlog",
531
+ enabled: true,
532
+ values: { sessionBridging: true, sessionSync: "live" },
533
+ },
534
+ ],
535
+ });
536
+ applyIntegrationWiring(state);
537
+ const config = JSON.parse(readFileSync(join(csDir, "config.json"), "utf-8"));
538
+ expect(config.template).toBe("gsd");
539
+ expect(config.sessionlog.enabled).toBe(true);
540
+ expect(config.sessionlog.sync).toBe("live");
541
+ // Untouched values preserved
542
+ expect(config.map.enabled).toBe(false);
543
+ expect(config.map.server).toBe("ws://localhost:8080");
544
+ }
545
+ finally {
546
+ process.chdir(originalCwd);
547
+ }
548
+ });
549
+ it("skips disabled wiring entries", () => {
550
+ const csDir = join(testDir, ".swarm", "claude-swarm");
551
+ mkdirSync(csDir, { recursive: true });
552
+ writeFileSync(join(csDir, "config.json"), JSON.stringify({
553
+ template: "",
554
+ sessionlog: { enabled: false, sync: "off" },
555
+ }, null, 2) + "\n");
556
+ const originalCwd = process.cwd();
557
+ process.chdir(testDir);
558
+ try {
559
+ const state = makeState({
560
+ selectedPackages: ["claude-code-swarm", "sessionlog"],
561
+ usePrefix: true,
562
+ integrationWiring: [
563
+ {
564
+ key: "claude-code-swarm:sessionlog",
565
+ enabled: false,
566
+ values: {},
567
+ },
568
+ ],
569
+ });
570
+ applyIntegrationWiring(state);
571
+ const config = JSON.parse(readFileSync(join(csDir, "config.json"), "utf-8"));
572
+ // Nothing changed
573
+ expect(config.template).toBe("");
574
+ expect(config.sessionlog.enabled).toBe(false);
575
+ expect(config.sessionlog.sync).toBe("off");
576
+ }
577
+ finally {
578
+ process.chdir(originalCwd);
579
+ }
580
+ });
581
+ it("handles empty integrationWiring gracefully", () => {
582
+ const state = makeState({
583
+ selectedPackages: ["opentasks"],
584
+ integrationWiring: [],
585
+ });
586
+ // Should not throw
587
+ applyIntegrationWiring(state);
588
+ });
589
+ it("writes MAP config options to claude-code-swarm", () => {
590
+ const csDir = join(testDir, ".swarm", "claude-swarm");
591
+ mkdirSync(csDir, { recursive: true });
592
+ writeFileSync(join(csDir, "config.json"), JSON.stringify({
593
+ map: { enabled: false, server: "ws://localhost:8080", scope: "" },
594
+ }, null, 2) + "\n");
595
+ const originalCwd = process.cwd();
596
+ process.chdir(testDir);
597
+ try {
598
+ const state = makeState({
599
+ selectedPackages: ["claude-code-swarm", "multi-agent-protocol"],
600
+ usePrefix: true,
601
+ integrationWiring: [
602
+ {
603
+ key: "claude-code-swarm:multi-agent-protocol",
604
+ enabled: true,
605
+ values: { mapEnabled: true, mapServer: "ws://production:9090" },
606
+ },
607
+ ],
608
+ });
609
+ applyIntegrationWiring(state);
610
+ const config = JSON.parse(readFileSync(join(csDir, "config.json"), "utf-8"));
611
+ expect(config.map.enabled).toBe(true);
612
+ expect(config.map.server).toBe("ws://production:9090");
613
+ }
614
+ finally {
615
+ process.chdir(originalCwd);
616
+ }
617
+ });
618
+ it("is a no-op when target config file doesn't exist", () => {
619
+ const originalCwd = process.cwd();
620
+ process.chdir(testDir);
621
+ try {
622
+ const state = makeState({
623
+ selectedPackages: ["claude-code-swarm", "sessionlog"],
624
+ usePrefix: true,
625
+ integrationWiring: [
626
+ {
627
+ key: "claude-code-swarm:sessionlog",
628
+ enabled: true,
629
+ values: { sessionBridging: true },
630
+ },
631
+ ],
632
+ });
633
+ // No config file exists — should not throw
634
+ applyIntegrationWiring(state);
635
+ // No file created (wiring only patches existing files)
636
+ expect(existsSync(join(testDir, ".swarm", "claude-swarm", "config.json"))).toBe(false);
637
+ }
638
+ finally {
639
+ process.chdir(originalCwd);
640
+ }
641
+ });
642
+ });
643
+ // ─── E2E: init → integration wiring → verify on disk ────────────────────────
644
+ describe("e2e: init → applyIntegrationWiring → verify", async () => {
645
+ const opentasksOk = await hasCliInstalled("opentasks");
646
+ it.skipIf(!opentasksOk)("full flow: init packages then apply wiring to existing configs", async () => {
647
+ createProject("wiring-e2e");
648
+ const state = makeState({
649
+ selectedPackages: [
650
+ "opentasks",
651
+ "claude-code-swarm",
652
+ "sessionlog",
653
+ "openteams",
654
+ ],
655
+ embeddingProvider: null,
656
+ usePrefix: true,
657
+ });
658
+ // Phase 5: global setup
659
+ await initGlobal(state);
660
+ // Phase 6: project init
661
+ for (const pkg of PROJECT_INIT_ORDER) {
662
+ if (!state.selectedPackages.includes(pkg))
663
+ continue;
664
+ if (isProjectInit(testDir, pkg))
665
+ continue;
666
+ const ctx = {
667
+ cwd: testDir,
668
+ packages: state.selectedPackages,
669
+ embeddingProvider: state.embeddingProvider,
670
+ apiKeys: state.apiKeys,
671
+ usePrefix: state.usePrefix,
672
+ packageConfigs: state.packageConfigs,
673
+ };
674
+ await initProjectPackage(pkg, ctx);
675
+ }
676
+ // Verify config exists before wiring
677
+ expect(existsSync(join(testDir, ".swarm", "claude-swarm", "config.json"))).toBe(true);
678
+ const configBefore = JSON.parse(readFileSync(join(testDir, ".swarm", "claude-swarm", "config.json"), "utf-8"));
679
+ expect(configBefore.sessionlog.enabled).toBe(false);
680
+ expect(configBefore.template).toBe("");
681
+ // Phase 7.5: apply wiring
682
+ state.integrationWiring = [
683
+ {
684
+ key: "claude-code-swarm:openteams",
685
+ enabled: true,
686
+ values: { template: "gsd" },
687
+ },
688
+ {
689
+ key: "claude-code-swarm:sessionlog",
690
+ enabled: true,
691
+ values: { sessionBridging: true, sessionSync: "on-finish" },
692
+ },
693
+ ];
694
+ const originalCwd = process.cwd();
695
+ process.chdir(testDir);
696
+ try {
697
+ applyIntegrationWiring(state);
698
+ }
699
+ finally {
700
+ process.chdir(originalCwd);
701
+ }
702
+ // Verify wiring values persisted
703
+ const configAfter = JSON.parse(readFileSync(join(testDir, ".swarm", "claude-swarm", "config.json"), "utf-8"));
704
+ expect(configAfter.template).toBe("gsd");
705
+ expect(configAfter.sessionlog.enabled).toBe(true);
706
+ expect(configAfter.sessionlog.sync).toBe("on-finish");
707
+ // Defaults preserved
708
+ expect(configAfter.map.enabled).toBe(false);
709
+ expect(configAfter.map.server).toBe("ws://localhost:8080");
710
+ });
711
+ });