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.
- package/dist/cli.js +2 -0
- package/dist/commands/configure/configure.d.ts +5 -0
- package/dist/commands/configure/configure.js +354 -0
- package/dist/commands/configure/configure.test.d.ts +1 -0
- package/dist/commands/configure/configure.test.js +539 -0
- package/dist/commands/configure/read-config.d.ts +12 -0
- package/dist/commands/configure/read-config.js +81 -0
- package/dist/commands/configure.d.ts +2 -0
- package/dist/commands/configure.js +14 -0
- package/dist/commands/init/phases/configure.js +0 -21
- package/dist/commands/init/phases/global-setup.d.ts +1 -1
- package/dist/commands/init/phases/global-setup.js +22 -44
- package/dist/commands/init/phases/integrations.d.ts +16 -0
- package/dist/commands/init/phases/integrations.js +172 -0
- package/dist/commands/init/phases/package-config.d.ts +10 -0
- package/dist/commands/init/phases/package-config.js +117 -0
- package/dist/commands/init/phases/phases.test.d.ts +1 -0
- package/dist/commands/init/phases/phases.test.js +711 -0
- package/dist/commands/init/phases/project.js +17 -0
- package/dist/commands/init/phases/review.d.ts +8 -0
- package/dist/commands/init/phases/review.js +79 -0
- package/dist/commands/init/phases/use-case.js +41 -27
- package/dist/commands/init/phases/wizard-flow.test.d.ts +1 -0
- package/dist/commands/init/phases/wizard-flow.test.js +657 -0
- package/dist/commands/init/phases/wizard-modes.test.d.ts +1 -0
- package/dist/commands/init/phases/wizard-modes.test.js +270 -0
- package/dist/commands/init/state.d.ts +31 -1
- package/dist/commands/init/state.js +4 -0
- package/dist/commands/init/state.test.js +7 -0
- package/dist/commands/init/wizard.d.ts +1 -0
- package/dist/commands/init/wizard.js +31 -23
- package/dist/commands/init.js +2 -0
- package/dist/packages/registry.d.ts +66 -0
- package/dist/packages/registry.js +258 -0
- package/dist/packages/setup.d.ts +42 -0
- package/dist/packages/setup.js +244 -15
- package/dist/packages/setup.test.js +520 -13
- 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,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
|
+
}
|