swarmkit 0.0.5 → 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 +311 -56
  37. package/dist/packages/setup.test.js +546 -42
  38. package/package.json +1 -1
@@ -0,0 +1,539 @@
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 { readPackageConfig, getNestedValue } = await import("./read-config.js");
18
+ const { initProjectPackage, initGlobalPackage, isProjectInit, PROJECT_CONFIG_DIRS, GLOBAL_CONFIG_DIRS, runPackageWizard, } = await import("../../packages/setup.js");
19
+ import { createEmptyState } from "../init/state.js";
20
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
21
+ let testDir;
22
+ beforeEach(() => {
23
+ const realTmp = realpathSync(tmpdir());
24
+ testDir = join(realTmp, `swarmkit-configure-test-${randomUUID()}`);
25
+ testHome = join(realTmp, `swarmkit-configure-home-${randomUUID()}`);
26
+ mkdirSync(testDir, { recursive: true });
27
+ mkdirSync(testHome, { recursive: true });
28
+ });
29
+ afterEach(() => {
30
+ rmSync(testDir, { recursive: true, force: true });
31
+ rmSync(testHome, { recursive: true, force: true });
32
+ });
33
+ async function hasCliInstalled(command) {
34
+ try {
35
+ await execFileAsync("which", [command]);
36
+ return true;
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ function createProject(name, dir = testDir) {
43
+ execSync("git init -q", { cwd: dir });
44
+ writeFileSync(join(dir, "package.json"), JSON.stringify({ name, version: "1.0.0" }));
45
+ return dir;
46
+ }
47
+ function projectCtx(overrides = {}) {
48
+ return {
49
+ cwd: overrides.cwd ?? testDir,
50
+ packages: overrides.packages ?? [],
51
+ embeddingProvider: overrides.embeddingProvider ?? null,
52
+ apiKeys: overrides.apiKeys ?? {},
53
+ usePrefix: overrides.usePrefix ?? true,
54
+ packageConfigs: overrides.packageConfigs,
55
+ };
56
+ }
57
+ function globalCtx(overrides = {}) {
58
+ return {
59
+ packages: overrides.packages ?? [],
60
+ embeddingProvider: overrides.embeddingProvider ?? null,
61
+ apiKeys: overrides.apiKeys ?? {},
62
+ packageConfigs: overrides.packageConfigs,
63
+ };
64
+ }
65
+ // ─── readPackageConfig ───────────────────────────────────────────────────────
66
+ describe("readPackageConfig", () => {
67
+ it("returns null when no config exists", () => {
68
+ const result = readPackageConfig("minimem", testDir);
69
+ expect(result).toBeNull();
70
+ });
71
+ it("reads prefixed project config", () => {
72
+ const configDir = join(testDir, ".swarm", "minimem");
73
+ mkdirSync(configDir, { recursive: true });
74
+ writeFileSync(join(configDir, "config.json"), JSON.stringify({
75
+ embedding: { provider: "openai" },
76
+ hybrid: { vectorWeight: 0.8 },
77
+ }));
78
+ const result = readPackageConfig("minimem", testDir);
79
+ expect(result).not.toBeNull();
80
+ expect(result.embedding.provider).toBe("openai");
81
+ expect(result.hybrid.vectorWeight).toBe(0.8);
82
+ });
83
+ it("reads flat project config when prefixed doesn't exist", () => {
84
+ const configDir = join(testDir, ".minimem");
85
+ mkdirSync(configDir, { recursive: true });
86
+ writeFileSync(join(configDir, "config.json"), JSON.stringify({ embedding: { provider: "gemini" } }));
87
+ const result = readPackageConfig("minimem", testDir);
88
+ expect(result).not.toBeNull();
89
+ expect(result.embedding.provider).toBe("gemini");
90
+ });
91
+ it("prefers prefixed over flat when both exist", () => {
92
+ // Create both
93
+ mkdirSync(join(testDir, ".swarm", "minimem"), { recursive: true });
94
+ writeFileSync(join(testDir, ".swarm", "minimem", "config.json"), JSON.stringify({ embedding: { provider: "openai" } }));
95
+ mkdirSync(join(testDir, ".minimem"), { recursive: true });
96
+ writeFileSync(join(testDir, ".minimem", "config.json"), JSON.stringify({ embedding: { provider: "gemini" } }));
97
+ const result = readPackageConfig("minimem", testDir);
98
+ expect(result.embedding.provider).toBe("openai");
99
+ });
100
+ it("reads sessionlog settings.json instead of config.json", () => {
101
+ const configDir = join(testDir, ".swarm", "sessionlog");
102
+ mkdirSync(configDir, { recursive: true });
103
+ writeFileSync(join(configDir, "settings.json"), JSON.stringify({ enabled: true, strategy: "auto-commit" }));
104
+ const result = readPackageConfig("sessionlog", testDir);
105
+ expect(result).not.toBeNull();
106
+ expect(result.enabled).toBe(true);
107
+ expect(result.strategy).toBe("auto-commit");
108
+ });
109
+ it("reads global config for claude-code-swarm", () => {
110
+ const configDir = join(testHome, ".claude-swarm");
111
+ mkdirSync(configDir, { recursive: true });
112
+ writeFileSync(join(configDir, "config.json"), JSON.stringify({ map: { server: "ws://global:8080" } }));
113
+ const result = readPackageConfig("claude-code-swarm", testDir);
114
+ expect(result).not.toBeNull();
115
+ expect(result.map.server).toBe("ws://global:8080");
116
+ });
117
+ it("merges global + project config (project wins)", () => {
118
+ // Global config
119
+ const globalDir = join(testHome, ".claude-swarm");
120
+ mkdirSync(globalDir, { recursive: true });
121
+ writeFileSync(join(globalDir, "config.json"), JSON.stringify({
122
+ map: { server: "ws://global:8080", sidecar: "session" },
123
+ sessionlog: { enabled: false },
124
+ }));
125
+ // Project config (overrides map.server)
126
+ const projectDir = join(testDir, ".swarm", "claude-swarm");
127
+ mkdirSync(projectDir, { recursive: true });
128
+ writeFileSync(join(projectDir, "config.json"), JSON.stringify({
129
+ template: "gsd",
130
+ map: { server: "ws://project:9090", enabled: true },
131
+ }));
132
+ const result = readPackageConfig("claude-code-swarm", testDir);
133
+ expect(result).not.toBeNull();
134
+ // Project overrides global
135
+ expect(result.map.server).toBe("ws://project:9090");
136
+ expect(result.map.enabled).toBe(true);
137
+ // Global value preserved when not overridden
138
+ expect(result.map.sidecar).toBe("session");
139
+ // Global-only field preserved
140
+ expect(result.sessionlog.enabled).toBe(false);
141
+ // Project-only field preserved
142
+ expect(result.template).toBe("gsd");
143
+ });
144
+ });
145
+ // ─── getNestedValue ──────────────────────────────────────────────────────────
146
+ describe("getNestedValue", () => {
147
+ const config = {
148
+ embedding: { provider: "openai" },
149
+ hybrid: { enabled: true, vectorWeight: 0.7, textWeight: 0.3 },
150
+ query: { maxResults: 10 },
151
+ simple: "value",
152
+ };
153
+ it("resolves top-level keys", () => {
154
+ expect(getNestedValue(config, "simple")).toBe("value");
155
+ });
156
+ it("resolves nested keys", () => {
157
+ expect(getNestedValue(config, "embedding.provider")).toBe("openai");
158
+ expect(getNestedValue(config, "hybrid.vectorWeight")).toBe(0.7);
159
+ expect(getNestedValue(config, "query.maxResults")).toBe(10);
160
+ });
161
+ it("returns undefined for missing keys", () => {
162
+ expect(getNestedValue(config, "missing")).toBeUndefined();
163
+ expect(getNestedValue(config, "embedding.missing")).toBeUndefined();
164
+ expect(getNestedValue(config, "deep.nested.missing")).toBeUndefined();
165
+ });
166
+ });
167
+ // ─── E2E: init → readPackageConfig cycle ─────────────────────────────────────
168
+ describe("e2e: init then readPackageConfig — minimem", () => {
169
+ it("reads back minimem config after init with defaults", async () => {
170
+ createProject("read-mm");
171
+ await initProjectPackage("minimem", projectCtx({ cwd: testDir, packages: ["minimem"] }));
172
+ const config = readPackageConfig("minimem", testDir);
173
+ expect(config).not.toBeNull();
174
+ expect(getNestedValue(config, "embedding.provider")).toBe("auto");
175
+ expect(getNestedValue(config, "hybrid.vectorWeight")).toBe(0.7);
176
+ expect(getNestedValue(config, "hybrid.textWeight")).toBe(0.3);
177
+ expect(getNestedValue(config, "query.maxResults")).toBe(10);
178
+ expect(getNestedValue(config, "query.minScore")).toBe(0.3);
179
+ });
180
+ it("reads back minimem config after init with openai embedding", async () => {
181
+ createProject("read-mm-openai");
182
+ await initProjectPackage("minimem", projectCtx({
183
+ cwd: testDir,
184
+ packages: ["minimem"],
185
+ embeddingProvider: "openai",
186
+ }));
187
+ const config = readPackageConfig("minimem", testDir);
188
+ expect(getNestedValue(config, "embedding.provider")).toBe("openai");
189
+ });
190
+ it("reads back minimem config after init with packageConfigs overrides", async () => {
191
+ createProject("read-mm-overrides");
192
+ await initProjectPackage("minimem", projectCtx({
193
+ cwd: testDir,
194
+ packages: ["minimem"],
195
+ packageConfigs: {
196
+ minimem: {
197
+ values: { "query.maxResults": "42", "hybrid.vectorWeight": "0.9" },
198
+ usedCliWizard: false,
199
+ },
200
+ },
201
+ }));
202
+ const config = readPackageConfig("minimem", testDir);
203
+ expect(getNestedValue(config, "query.maxResults")).toBe(42);
204
+ expect(getNestedValue(config, "hybrid.vectorWeight")).toBe(0.9);
205
+ // Defaults preserved for non-overridden fields
206
+ expect(getNestedValue(config, "hybrid.textWeight")).toBe(0.3);
207
+ });
208
+ });
209
+ describe("e2e: init then readPackageConfig — sessionlog", () => {
210
+ it("reads back sessionlog settings after init with defaults", async () => {
211
+ await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"] }));
212
+ const config = readPackageConfig("sessionlog", testDir);
213
+ expect(config).not.toBeNull();
214
+ expect(config.enabled).toBe(false);
215
+ expect(config.strategy).toBe("manual-commit");
216
+ expect(config.logLevel).toBe("warn");
217
+ expect(config.summarizationEnabled).toBe(false);
218
+ });
219
+ it("reads back sessionlog settings after init with overrides", async () => {
220
+ await initProjectPackage("sessionlog", projectCtx({
221
+ cwd: testDir,
222
+ packages: ["sessionlog"],
223
+ packageConfigs: {
224
+ sessionlog: {
225
+ values: { enabled: true, strategy: "auto-commit" },
226
+ usedCliWizard: false,
227
+ },
228
+ },
229
+ }));
230
+ const config = readPackageConfig("sessionlog", testDir);
231
+ expect(config.enabled).toBe(true);
232
+ expect(config.strategy).toBe("auto-commit");
233
+ // Defaults for untouched fields
234
+ expect(config.logLevel).toBe("warn");
235
+ });
236
+ });
237
+ describe("e2e: init then readPackageConfig — claude-code-swarm", () => {
238
+ it("reads back project config after init", async () => {
239
+ await initProjectPackage("claude-code-swarm", projectCtx({ cwd: testDir, packages: ["claude-code-swarm"] }));
240
+ const config = readPackageConfig("claude-code-swarm", testDir);
241
+ expect(config).not.toBeNull();
242
+ expect(getNestedValue(config, "map.enabled")).toBe(false);
243
+ expect(getNestedValue(config, "map.server")).toBe("ws://localhost:8080");
244
+ expect(getNestedValue(config, "sessionlog.enabled")).toBe(false);
245
+ });
246
+ it("reads back project config after init with overrides", async () => {
247
+ await initProjectPackage("claude-code-swarm", projectCtx({
248
+ cwd: testDir,
249
+ packages: ["claude-code-swarm"],
250
+ packageConfigs: {
251
+ "claude-code-swarm": {
252
+ values: {
253
+ "map.enabled": true,
254
+ "map.server": "ws://custom:9090",
255
+ "sessionlog.enabled": true,
256
+ },
257
+ usedCliWizard: false,
258
+ },
259
+ },
260
+ }));
261
+ const config = readPackageConfig("claude-code-swarm", testDir);
262
+ expect(getNestedValue(config, "map.enabled")).toBe(true);
263
+ expect(getNestedValue(config, "map.server")).toBe("ws://custom:9090");
264
+ expect(getNestedValue(config, "sessionlog.enabled")).toBe(true);
265
+ // Defaults preserved
266
+ expect(getNestedValue(config, "map.systemId")).toBe("system-claude-swarm");
267
+ });
268
+ it("reads back global config after global init", async () => {
269
+ await initGlobalPackage("claude-code-swarm", globalCtx({ packages: ["claude-code-swarm"] }));
270
+ const config = readPackageConfig("claude-code-swarm", testDir);
271
+ expect(config).not.toBeNull();
272
+ expect(getNestedValue(config, "map.server")).toBe("");
273
+ expect(getNestedValue(config, "map.sidecar")).toBe("session");
274
+ expect(getNestedValue(config, "sessionlog.enabled")).toBe(false);
275
+ });
276
+ it("merges global + project config correctly", async () => {
277
+ // Init global first
278
+ await initGlobalPackage("claude-code-swarm", globalCtx({
279
+ packages: ["claude-code-swarm"],
280
+ packageConfigs: {
281
+ "claude-code-swarm": {
282
+ values: { "map.server": "ws://global:8080" },
283
+ usedCliWizard: false,
284
+ },
285
+ },
286
+ }));
287
+ // Init project with different overrides
288
+ await initProjectPackage("claude-code-swarm", projectCtx({
289
+ cwd: testDir,
290
+ packages: ["claude-code-swarm"],
291
+ packageConfigs: {
292
+ "claude-code-swarm": {
293
+ values: { "map.enabled": true, "sessionlog.enabled": true },
294
+ usedCliWizard: false,
295
+ },
296
+ },
297
+ }));
298
+ const config = readPackageConfig("claude-code-swarm", testDir);
299
+ // Project values
300
+ expect(getNestedValue(config, "map.enabled")).toBe(true);
301
+ expect(getNestedValue(config, "sessionlog.enabled")).toBe(true);
302
+ // Project default overrides global (project has ws://localhost:8080 default)
303
+ expect(getNestedValue(config, "map.server")).toBe("ws://localhost:8080");
304
+ });
305
+ });
306
+ // ─── E2E: init → patch → verify config cycle ────────────────────────────────
307
+ describe("e2e: init → patch config → verify", () => {
308
+ it("patches minimem config in-place", async () => {
309
+ createProject("patch-mm");
310
+ // Initialize with defaults
311
+ await initProjectPackage("minimem", projectCtx({ cwd: testDir, packages: ["minimem"] }));
312
+ // Read the config file directly and patch it (simulating what configure does)
313
+ const configPath = join(testDir, ".swarm", "minimem", "config.json");
314
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
315
+ // Simulate the setNestedValue patching
316
+ config.hybrid.vectorWeight = 0.9;
317
+ config.query.maxResults = 25;
318
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
319
+ // Read it back through readPackageConfig
320
+ const readBack = readPackageConfig("minimem", testDir);
321
+ expect(getNestedValue(readBack, "hybrid.vectorWeight")).toBe(0.9);
322
+ expect(getNestedValue(readBack, "query.maxResults")).toBe(25);
323
+ // Other values preserved
324
+ expect(getNestedValue(readBack, "hybrid.textWeight")).toBe(0.3);
325
+ expect(getNestedValue(readBack, "embedding.provider")).toBe("auto");
326
+ });
327
+ it("patches sessionlog settings in-place", async () => {
328
+ await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"] }));
329
+ const settingsPath = join(testDir, ".swarm", "sessionlog", "settings.json");
330
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
331
+ settings.enabled = true;
332
+ settings.strategy = "auto-commit";
333
+ settings.summarizationEnabled = true;
334
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
335
+ const readBack = readPackageConfig("sessionlog", testDir);
336
+ expect(readBack.enabled).toBe(true);
337
+ expect(readBack.strategy).toBe("auto-commit");
338
+ expect(readBack.summarizationEnabled).toBe(true);
339
+ // Preserved
340
+ expect(readBack.logLevel).toBe("warn");
341
+ });
342
+ it("patches claude-code-swarm nested config in-place", async () => {
343
+ await initProjectPackage("claude-code-swarm", projectCtx({ cwd: testDir, packages: ["claude-code-swarm"] }));
344
+ const configPath = join(testDir, ".swarm", "claude-swarm", "config.json");
345
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
346
+ config.map.enabled = true;
347
+ config.map.server = "ws://reconfigured:9090";
348
+ config.map.scope = "my-scope";
349
+ config.sessionlog.enabled = true;
350
+ config.sessionlog.sync = "live";
351
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
352
+ const readBack = readPackageConfig("claude-code-swarm", testDir);
353
+ expect(getNestedValue(readBack, "map.enabled")).toBe(true);
354
+ expect(getNestedValue(readBack, "map.server")).toBe("ws://reconfigured:9090");
355
+ expect(getNestedValue(readBack, "map.scope")).toBe("my-scope");
356
+ expect(getNestedValue(readBack, "sessionlog.enabled")).toBe(true);
357
+ expect(getNestedValue(readBack, "sessionlog.sync")).toBe("live");
358
+ // Preserved defaults
359
+ expect(getNestedValue(readBack, "map.systemId")).toBe("system-claude-swarm");
360
+ expect(getNestedValue(readBack, "template")).toBe("");
361
+ });
362
+ });
363
+ // ─── E2E: full init → reconfigure via packageConfigs → verify ───────────────
364
+ describe("e2e: init → reconfigure with new packageConfigs", async () => {
365
+ const opentasksOk = await hasCliInstalled("opentasks");
366
+ it.skipIf(!opentasksOk)("init with defaults, then re-init with overrides updates config", async () => {
367
+ createProject("reconfig-e2e");
368
+ const packages = ["opentasks", "minimem", "sessionlog", "claude-code-swarm"];
369
+ // Phase 1: Init with all defaults
370
+ const ctx1 = projectCtx({
371
+ cwd: testDir,
372
+ packages,
373
+ });
374
+ const { PROJECT_INIT_ORDER } = await import("../../packages/setup.js");
375
+ for (const pkg of PROJECT_INIT_ORDER) {
376
+ if (!packages.includes(pkg))
377
+ continue;
378
+ if (isProjectInit(testDir, pkg))
379
+ continue;
380
+ await initProjectPackage(pkg, ctx1);
381
+ }
382
+ // Verify defaults
383
+ let mmConfig = readPackageConfig("minimem", testDir);
384
+ expect(getNestedValue(mmConfig, "query.maxResults")).toBe(10);
385
+ let slConfig = readPackageConfig("sessionlog", testDir);
386
+ expect(slConfig.enabled).toBe(false);
387
+ let csConfig = readPackageConfig("claude-code-swarm", testDir);
388
+ expect(getNestedValue(csConfig, "map.enabled")).toBe(false);
389
+ // Phase 2: Simulate reconfigure by patching config files directly
390
+ // (This is what configure.ts's applyConfigUpdates does)
391
+ const mmPath = join(testDir, ".swarm", "minimem", "config.json");
392
+ const mm = JSON.parse(readFileSync(mmPath, "utf-8"));
393
+ mm.query.maxResults = 50;
394
+ mm.hybrid.vectorWeight = 0.85;
395
+ writeFileSync(mmPath, JSON.stringify(mm, null, 2) + "\n");
396
+ const slPath = join(testDir, ".swarm", "sessionlog", "settings.json");
397
+ const sl = JSON.parse(readFileSync(slPath, "utf-8"));
398
+ sl.enabled = true;
399
+ sl.strategy = "auto-commit";
400
+ writeFileSync(slPath, JSON.stringify(sl, null, 2) + "\n");
401
+ const csPath = join(testDir, ".swarm", "claude-swarm", "config.json");
402
+ const cs = JSON.parse(readFileSync(csPath, "utf-8"));
403
+ cs.map.enabled = true;
404
+ cs.map.server = "ws://production:8080";
405
+ cs.sessionlog.enabled = true;
406
+ cs.sessionlog.sync = "on-finish";
407
+ writeFileSync(csPath, JSON.stringify(cs, null, 2) + "\n");
408
+ // Phase 3: Verify reconfigured values via readPackageConfig
409
+ mmConfig = readPackageConfig("minimem", testDir);
410
+ expect(getNestedValue(mmConfig, "query.maxResults")).toBe(50);
411
+ expect(getNestedValue(mmConfig, "hybrid.vectorWeight")).toBe(0.85);
412
+ expect(getNestedValue(mmConfig, "hybrid.textWeight")).toBe(0.3); // preserved
413
+ slConfig = readPackageConfig("sessionlog", testDir);
414
+ expect(slConfig.enabled).toBe(true);
415
+ expect(slConfig.strategy).toBe("auto-commit");
416
+ expect(slConfig.logLevel).toBe("warn"); // preserved
417
+ csConfig = readPackageConfig("claude-code-swarm", testDir);
418
+ expect(getNestedValue(csConfig, "map.enabled")).toBe(true);
419
+ expect(getNestedValue(csConfig, "map.server")).toBe("ws://production:8080");
420
+ expect(getNestedValue(csConfig, "sessionlog.enabled")).toBe(true);
421
+ expect(getNestedValue(csConfig, "sessionlog.sync")).toBe("on-finish");
422
+ expect(getNestedValue(csConfig, "map.systemId")).toBe("system-claude-swarm"); // preserved
423
+ // opentasks should be untouched
424
+ expect(isProjectInit(testDir, "opentasks")).toBe(true);
425
+ const otConfig = JSON.parse(readFileSync(join(testDir, ".swarm", "opentasks", "config.json"), "utf-8"));
426
+ expect(otConfig.location.name).toBe("reconfig-e2e");
427
+ });
428
+ });
429
+ // ─── E2E: CLI wizard reconfigure with live packages ──────────────────────────
430
+ describe("e2e: CLI wizard reconfigure — opentasks", async () => {
431
+ const installed = await hasCliInstalled("opentasks");
432
+ it.skipIf(!installed)("re-runs opentasks wizard on already-initialized project", async () => {
433
+ createProject("wizard-reconfig");
434
+ // First init
435
+ await initProjectPackage("opentasks", projectCtx({ cwd: testDir, packages: ["opentasks"] }));
436
+ expect(isProjectInit(testDir, "opentasks")).toBe(true);
437
+ const configBefore = JSON.parse(readFileSync(join(testDir, ".swarm", "opentasks", "config.json"), "utf-8"));
438
+ expect(configBefore.location.name).toBe("wizard-reconfig");
439
+ // Re-run wizard (simulates configure path)
440
+ const state = createEmptyState();
441
+ state.selectedPackages = ["opentasks"];
442
+ state.usePrefix = true;
443
+ const result = await runPackageWizard("opentasks", {
444
+ command: "opentasks",
445
+ args: ["init", "--name", "wizard-reconfig-v2"],
446
+ env: { OPENTASKS_PROJECT_DIR: ".swarm/opentasks" },
447
+ }, state, testDir);
448
+ expect(result.success).toBe(true);
449
+ // Config should be updated by the re-run
450
+ // (opentasks init is idempotent on already-initialized dirs,
451
+ // but verifies the wizard path completes successfully)
452
+ expect(isProjectInit(testDir, "opentasks")).toBe(true);
453
+ });
454
+ });
455
+ describe("e2e: CLI wizard reconfigure — skill-tree global", async () => {
456
+ const installed = await hasCliInstalled("skill-tree");
457
+ it.skipIf(!installed)("re-runs skill-tree config init on existing config", async () => {
458
+ // First init via global setup
459
+ await initGlobalPackage("skill-tree", globalCtx({ packages: ["skill-tree"] }));
460
+ expect(existsSync(join(testHome, ".skill-tree", "config.yaml"))).toBe(true);
461
+ // Re-run wizard with --force (simulates configure path)
462
+ // skill-tree config init requires --force to overwrite existing config
463
+ const state = createEmptyState();
464
+ state.selectedPackages = ["skill-tree"];
465
+ const result = await runPackageWizard("skill-tree", {
466
+ command: "skill-tree",
467
+ args: ["config", "init", "--force"],
468
+ }, state, testDir);
469
+ expect(result.success).toBe(true);
470
+ expect(existsSync(join(testHome, ".skill-tree", "config.yaml"))).toBe(true);
471
+ });
472
+ });
473
+ // ─── E2E: full multi-package init → reconfigure → verify ────────────────────
474
+ describe("e2e: full multi-package init → reconfigure cycle", async () => {
475
+ const opentasksOk = await hasCliInstalled("opentasks");
476
+ it.skipIf(!opentasksOk)("initializes solo bundle, reconfigures minimem, verifies all configs intact", async () => {
477
+ createProject("full-reconfig");
478
+ const packages = ["opentasks", "minimem"];
479
+ const { PROJECT_INIT_ORDER } = await import("../../packages/setup.js");
480
+ // Step 1: Init with openai embedding
481
+ const ctx = projectCtx({
482
+ cwd: testDir,
483
+ packages,
484
+ embeddingProvider: "openai",
485
+ });
486
+ for (const pkg of PROJECT_INIT_ORDER) {
487
+ if (!packages.includes(pkg))
488
+ continue;
489
+ if (isProjectInit(testDir, pkg))
490
+ continue;
491
+ await initProjectPackage(pkg, ctx);
492
+ }
493
+ // Verify initial state
494
+ let mmConfig = readPackageConfig("minimem", testDir);
495
+ expect(getNestedValue(mmConfig, "embedding.provider")).toBe("openai");
496
+ expect(getNestedValue(mmConfig, "hybrid.vectorWeight")).toBe(0.7);
497
+ expect(getNestedValue(mmConfig, "query.maxResults")).toBe(10);
498
+ // Step 2: Reconfigure minimem (simulate what configure does)
499
+ const mmPath = join(testDir, ".swarm", "minimem", "config.json");
500
+ const mm = JSON.parse(readFileSync(mmPath, "utf-8"));
501
+ mm.hybrid.vectorWeight = 0.95;
502
+ mm.hybrid.textWeight = 0.05;
503
+ mm.query.maxResults = 100;
504
+ mm.query.minScore = 0.5;
505
+ writeFileSync(mmPath, JSON.stringify(mm, null, 2) + "\n");
506
+ // Step 3: Verify reconfigured values
507
+ mmConfig = readPackageConfig("minimem", testDir);
508
+ expect(getNestedValue(mmConfig, "embedding.provider")).toBe("openai"); // unchanged
509
+ expect(getNestedValue(mmConfig, "hybrid.vectorWeight")).toBe(0.95);
510
+ expect(getNestedValue(mmConfig, "hybrid.textWeight")).toBe(0.05);
511
+ expect(getNestedValue(mmConfig, "query.maxResults")).toBe(100);
512
+ expect(getNestedValue(mmConfig, "query.minScore")).toBe(0.5);
513
+ // Step 4: opentasks should be completely untouched
514
+ const otConfig = JSON.parse(readFileSync(join(testDir, ".swarm", "opentasks", "config.json"), "utf-8"));
515
+ expect(otConfig.location.name).toBe("full-reconfig");
516
+ expect(otConfig.version).toBe("1.0");
517
+ });
518
+ });
519
+ // ─── E2E: flat layout reconfigure ────────────────────────────────────────────
520
+ describe("e2e: reconfigure with flat layout", () => {
521
+ it("reads and patches flat layout configs", async () => {
522
+ // Init with flat layout
523
+ await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"], usePrefix: false }));
524
+ expect(existsSync(join(testDir, ".sessionlog", "settings.json"))).toBe(true);
525
+ // Read back via readPackageConfig (checks flat)
526
+ let config = readPackageConfig("sessionlog", testDir);
527
+ expect(config).not.toBeNull();
528
+ expect(config.enabled).toBe(false);
529
+ // Patch
530
+ const settingsPath = join(testDir, ".sessionlog", "settings.json");
531
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
532
+ settings.enabled = true;
533
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
534
+ // Verify
535
+ config = readPackageConfig("sessionlog", testDir);
536
+ expect(config.enabled).toBe(true);
537
+ expect(config.strategy).toBe("manual-commit"); // preserved
538
+ });
539
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Read an existing package config file and return its parsed contents.
3
+ * Checks both prefixed and flat project layouts, plus global config.
4
+ *
5
+ * Returns the merged result of global + project config (project wins).
6
+ */
7
+ export declare function readPackageConfig(pkg: string, cwd: string): Record<string, unknown> | null;
8
+ /**
9
+ * Resolve a dotted key path from a nested config object.
10
+ * e.g., getNestedValue(config, "hybrid.vectorWeight") → 0.7
11
+ */
12
+ export declare function getNestedValue(config: Record<string, unknown>, keyPath: string): unknown;
@@ -0,0 +1,81 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { PROJECT_CONFIG_DIRS, FLAT_PROJECT_CONFIG_DIRS, GLOBAL_CONFIG_DIRS, } from "../../packages/setup.js";
5
+ /**
6
+ * Read an existing package config file and return its parsed contents.
7
+ * Checks both prefixed and flat project layouts, plus global config.
8
+ *
9
+ * Returns the merged result of global + project config (project wins).
10
+ */
11
+ export function readPackageConfig(pkg, cwd) {
12
+ const configs = [];
13
+ // Read global config if exists
14
+ const globalDir = GLOBAL_CONFIG_DIRS[pkg];
15
+ if (globalDir) {
16
+ const globalConfig = readJsonConfig(join(homedir(), globalDir, "config.json"));
17
+ if (globalConfig)
18
+ configs.push(globalConfig);
19
+ }
20
+ // Read project config (try prefixed first, then flat)
21
+ const prefixedDir = PROJECT_CONFIG_DIRS[pkg];
22
+ const flatDir = FLAT_PROJECT_CONFIG_DIRS[pkg];
23
+ const projectConfigFile = pkg === "sessionlog" ? "settings.json" : "config.json";
24
+ if (prefixedDir) {
25
+ const prefixedConfig = readJsonConfig(join(cwd, prefixedDir, projectConfigFile));
26
+ if (prefixedConfig) {
27
+ configs.push(prefixedConfig);
28
+ }
29
+ else if (flatDir) {
30
+ const flatConfig = readJsonConfig(join(cwd, flatDir, projectConfigFile));
31
+ if (flatConfig)
32
+ configs.push(flatConfig);
33
+ }
34
+ }
35
+ if (configs.length === 0)
36
+ return null;
37
+ // Merge: later entries override earlier
38
+ const merged = {};
39
+ for (const config of configs) {
40
+ deepMerge(merged, config);
41
+ }
42
+ return merged;
43
+ }
44
+ /**
45
+ * Resolve a dotted key path from a nested config object.
46
+ * e.g., getNestedValue(config, "hybrid.vectorWeight") → 0.7
47
+ */
48
+ export function getNestedValue(config, keyPath) {
49
+ const parts = keyPath.split(".");
50
+ let current = config;
51
+ for (const part of parts) {
52
+ if (typeof current !== "object" || current === null)
53
+ return undefined;
54
+ current = current[part];
55
+ }
56
+ return current;
57
+ }
58
+ function readJsonConfig(path) {
59
+ if (!existsSync(path))
60
+ return null;
61
+ try {
62
+ return JSON.parse(readFileSync(path, "utf-8"));
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ function deepMerge(target, source) {
69
+ for (const [key, value] of Object.entries(source)) {
70
+ if (typeof value === "object" &&
71
+ value !== null &&
72
+ !Array.isArray(value) &&
73
+ typeof target[key] === "object" &&
74
+ target[key] !== null) {
75
+ deepMerge(target[key], value);
76
+ }
77
+ else {
78
+ target[key] = value;
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerConfigureCommand(program: Command): void;
@@ -0,0 +1,14 @@
1
+ export function registerConfigureCommand(program) {
2
+ program
3
+ .command("configure [package]")
4
+ .description("Reconfigure package settings (or all packages if none specified)")
5
+ .option("-q, --quick", "Reset to defaults without prompting")
6
+ .action(async (packageName, opts) => {
7
+ // Dynamic import to avoid loading @inquirer/prompts for other commands
8
+ const { runConfigure } = await import("./configure/configure.js");
9
+ await runConfigure({
10
+ packageName,
11
+ quick: opts.quick === true,
12
+ });
13
+ });
14
+ }