swarmkit 0.0.1 → 0.0.3
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/LICENSE +21 -0
- package/README.md +130 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +33 -0
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +98 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +100 -0
- package/dist/commands/hive.d.ts +2 -0
- package/dist/commands/hive.js +248 -0
- package/dist/commands/init/phases/configure.d.ts +2 -0
- package/dist/commands/init/phases/configure.js +85 -0
- package/dist/commands/init/phases/global-setup.d.ts +2 -0
- package/dist/commands/init/phases/global-setup.js +81 -0
- package/dist/commands/init/phases/packages.d.ts +2 -0
- package/dist/commands/init/phases/packages.js +30 -0
- package/dist/commands/init/phases/project.d.ts +2 -0
- package/dist/commands/init/phases/project.js +56 -0
- package/dist/commands/init/phases/use-case.d.ts +2 -0
- package/dist/commands/init/phases/use-case.js +41 -0
- package/dist/commands/init/state.d.ts +13 -0
- package/dist/commands/init/state.js +9 -0
- package/dist/commands/init/state.test.d.ts +1 -0
- package/dist/commands/init/state.test.js +21 -0
- package/dist/commands/init/wizard.d.ts +4 -0
- package/dist/commands/init/wizard.js +108 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +11 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +91 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +19 -0
- package/dist/commands/remove.d.ts +2 -0
- package/dist/commands/remove.js +55 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +87 -0
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +54 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +40 -0
- package/dist/config/global.d.ts +26 -0
- package/dist/config/global.js +71 -0
- package/dist/config/global.test.d.ts +1 -0
- package/dist/config/global.test.js +167 -0
- package/dist/config/keys.d.ts +10 -0
- package/dist/config/keys.js +47 -0
- package/dist/config/keys.test.d.ts +1 -0
- package/dist/config/keys.test.js +87 -0
- package/dist/doctor/checks.d.ts +31 -0
- package/dist/doctor/checks.js +226 -0
- package/dist/doctor/checks.test.d.ts +1 -0
- package/dist/doctor/checks.test.js +301 -0
- package/dist/doctor/types.d.ts +29 -0
- package/dist/doctor/types.js +1 -0
- package/dist/hub/auth-flow.d.ts +16 -0
- package/dist/hub/auth-flow.js +118 -0
- package/dist/hub/auth-flow.test.d.ts +1 -0
- package/dist/hub/auth-flow.test.js +98 -0
- package/dist/hub/client.d.ts +51 -0
- package/dist/hub/client.js +107 -0
- package/dist/hub/client.test.d.ts +1 -0
- package/dist/hub/client.test.js +177 -0
- package/dist/hub/credentials.d.ts +14 -0
- package/dist/hub/credentials.js +41 -0
- package/dist/hub/credentials.test.d.ts +1 -0
- package/dist/hub/credentials.test.js +102 -0
- package/dist/index.d.ts +17 -1
- package/dist/index.js +10 -2
- package/dist/packages/installer.d.ts +42 -0
- package/dist/packages/installer.js +158 -0
- package/dist/packages/installer.test.d.ts +1 -0
- package/dist/packages/installer.test.js +283 -0
- package/dist/packages/plugin.d.ts +13 -0
- package/dist/packages/plugin.js +33 -0
- package/dist/packages/plugin.test.d.ts +1 -0
- package/dist/packages/plugin.test.js +99 -0
- package/dist/packages/registry.d.ts +37 -0
- package/dist/packages/registry.js +154 -0
- package/dist/packages/registry.test.d.ts +1 -0
- package/dist/packages/registry.test.js +188 -0
- package/dist/packages/setup.d.ts +55 -0
- package/dist/packages/setup.js +414 -0
- package/dist/packages/setup.test.d.ts +1 -0
- package/dist/packages/setup.test.js +808 -0
- package/dist/utils/ui.d.ts +10 -0
- package/dist/utils/ui.js +47 -0
- package/dist/utils/ui.test.d.ts +1 -0
- package/dist/utils/ui.test.js +102 -0
- package/package.json +29 -6
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdirSync, rmSync, existsSync, lstatSync, readFileSync, readlinkSync, writeFileSync, realpathSync, } from "node:fs";
|
|
3
|
+
import { join, basename } 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
|
+
//
|
|
11
|
+
// Only ONE thing is mocked: homedir — so global-package tests don't pollute
|
|
12
|
+
// the real home directory. Everything else (filesystem, CLI shell-outs) is real.
|
|
13
|
+
//
|
|
14
|
+
// The shell-out tests call real published CLIs (opentasks, minimem, etc.).
|
|
15
|
+
// If a CLI is not installed, the test is skipped via a check helper.
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
let testHome;
|
|
18
|
+
vi.mock("node:os", async () => {
|
|
19
|
+
const actual = await import("node:os");
|
|
20
|
+
return { ...actual, homedir: () => testHome };
|
|
21
|
+
});
|
|
22
|
+
const { PROJECT_CONFIG_DIRS, FLAT_PROJECT_CONFIG_DIRS, PROJECT_INIT_ORDER, GLOBAL_CONFIG_DIRS, isProjectInit, isGlobalInit, initProjectPackage, initGlobalPackage, } = await import("./setup.js");
|
|
23
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
24
|
+
let testDir;
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
// Use realpathSync to resolve macOS /var → /private/var symlink.
|
|
27
|
+
// Git resolves symlinks in rev-parse --show-toplevel, so paths must match.
|
|
28
|
+
const realTmp = realpathSync(tmpdir());
|
|
29
|
+
testDir = join(realTmp, `swarmkit-setup-test-${randomUUID()}`);
|
|
30
|
+
testHome = join(realTmp, `swarmkit-setup-home-${randomUUID()}`);
|
|
31
|
+
mkdirSync(testDir, { recursive: true });
|
|
32
|
+
mkdirSync(testHome, { recursive: true });
|
|
33
|
+
});
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
36
|
+
rmSync(testHome, { recursive: true, force: true });
|
|
37
|
+
});
|
|
38
|
+
/** Check if a CLI binary is available on PATH */
|
|
39
|
+
async function hasCliInstalled(command) {
|
|
40
|
+
try {
|
|
41
|
+
await execFileAsync("which", [command]);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Create a realistic project directory with real git repo and package.json */
|
|
49
|
+
function createProject(name, dir = testDir) {
|
|
50
|
+
execSync("git init -q", { cwd: dir });
|
|
51
|
+
writeFileSync(join(dir, "package.json"), JSON.stringify({ name, version: "1.0.0" }));
|
|
52
|
+
return dir;
|
|
53
|
+
}
|
|
54
|
+
/** Shared init context builder */
|
|
55
|
+
function projectCtx(overrides = {}) {
|
|
56
|
+
return {
|
|
57
|
+
cwd: overrides.cwd ?? testDir,
|
|
58
|
+
packages: overrides.packages ?? [],
|
|
59
|
+
embeddingProvider: overrides.embeddingProvider ?? null,
|
|
60
|
+
apiKeys: overrides.apiKeys ?? {},
|
|
61
|
+
usePrefix: overrides.usePrefix ?? true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function globalCtx(overrides = {}) {
|
|
65
|
+
return {
|
|
66
|
+
packages: overrides.packages ?? [],
|
|
67
|
+
embeddingProvider: overrides.embeddingProvider ?? null,
|
|
68
|
+
apiKeys: overrides.apiKeys ?? {},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
72
|
+
describe("constants", () => {
|
|
73
|
+
it("PROJECT_INIT_ORDER includes all project-level packages", () => {
|
|
74
|
+
expect(PROJECT_INIT_ORDER).toEqual([
|
|
75
|
+
"opentasks",
|
|
76
|
+
"minimem",
|
|
77
|
+
"cognitive-core",
|
|
78
|
+
"skill-tree",
|
|
79
|
+
"self-driving-repo",
|
|
80
|
+
"openteams",
|
|
81
|
+
"sessionlog",
|
|
82
|
+
"claude-code-swarm",
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
it("every ordered package has a config dir mapping", () => {
|
|
86
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
87
|
+
expect(PROJECT_CONFIG_DIRS[pkg]).toBeDefined();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
it("GLOBAL_CONFIG_DIRS maps all global packages", () => {
|
|
91
|
+
expect(Object.keys(GLOBAL_CONFIG_DIRS).sort()).toEqual(["openhive", "skill-tree"]);
|
|
92
|
+
});
|
|
93
|
+
it("no overlap between project and global config dirs", () => {
|
|
94
|
+
const projectDirs = new Set(Object.values(PROJECT_CONFIG_DIRS));
|
|
95
|
+
const globalDirs = new Set(Object.values(GLOBAL_CONFIG_DIRS));
|
|
96
|
+
for (const d of projectDirs) {
|
|
97
|
+
expect(globalDirs.has(d)).toBe(false);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
// ─── Detection: isProjectInit ────────────────────────────────────────────────
|
|
102
|
+
describe("isProjectInit", () => {
|
|
103
|
+
it("returns false for every package in an empty directory", () => {
|
|
104
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
105
|
+
expect(isProjectInit(testDir, pkg)).toBe(false);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
it.each(Object.entries(PROJECT_CONFIG_DIRS))("returns true for %s when %s/ exists (prefixed)", (pkg, configDir) => {
|
|
109
|
+
mkdirSync(join(testDir, configDir), { recursive: true });
|
|
110
|
+
expect(isProjectInit(testDir, pkg)).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
it.each(Object.entries(FLAT_PROJECT_CONFIG_DIRS))("returns true for %s when %s/ exists (flat)", (pkg, configDir) => {
|
|
113
|
+
mkdirSync(join(testDir, configDir), { recursive: true });
|
|
114
|
+
expect(isProjectInit(testDir, pkg)).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
it("returns false for unknown package", () => {
|
|
117
|
+
expect(isProjectInit(testDir, "nonexistent")).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
it("returns false for nonexistent base dir", () => {
|
|
120
|
+
expect(isProjectInit("/does/not/exist", "opentasks")).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
// ─── Detection: isGlobalInit ─────────────────────────────────────────────────
|
|
124
|
+
describe("isGlobalInit", () => {
|
|
125
|
+
it("returns false for every global package in a fresh home", () => {
|
|
126
|
+
for (const pkg of Object.keys(GLOBAL_CONFIG_DIRS)) {
|
|
127
|
+
expect(isGlobalInit(pkg)).toBe(false);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
it.each(Object.entries(GLOBAL_CONFIG_DIRS))("returns true for %s when ~/%s/ exists", (pkg, configDir) => {
|
|
131
|
+
mkdirSync(join(testHome, configDir), { recursive: true });
|
|
132
|
+
expect(isGlobalInit(pkg)).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
it("returns false for unknown package", () => {
|
|
135
|
+
expect(isGlobalInit("nonexistent")).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
// ─── Project: opentasks (real CLI) ───────────────────────────────────────────
|
|
139
|
+
describe("initProjectPackage — opentasks (real CLI)", async () => {
|
|
140
|
+
const installed = await hasCliInstalled("opentasks");
|
|
141
|
+
it.skipIf(!installed)("runs opentasks init, relocates to .swarm/, and creates symlink", async () => {
|
|
142
|
+
createProject("test-opentasks");
|
|
143
|
+
const result = await initProjectPackage("opentasks", projectCtx({ cwd: testDir, packages: ["opentasks"] }));
|
|
144
|
+
expect(result.success).toBe(true);
|
|
145
|
+
expect(result.package).toBe("opentasks");
|
|
146
|
+
// Verify real filesystem artifacts in .swarm/
|
|
147
|
+
expect(existsSync(join(testDir, ".swarm", "opentasks"))).toBe(true);
|
|
148
|
+
expect(existsSync(join(testDir, ".swarm", "opentasks", "config.json"))).toBe(true);
|
|
149
|
+
expect(existsSync(join(testDir, ".swarm", "opentasks", "graph.jsonl"))).toBe(true);
|
|
150
|
+
expect(existsSync(join(testDir, ".swarm", "opentasks", ".gitignore"))).toBe(true);
|
|
151
|
+
// Verify symlink at legacy location
|
|
152
|
+
const link = join(testDir, ".opentasks");
|
|
153
|
+
expect(lstatSync(link).isSymbolicLink()).toBe(true);
|
|
154
|
+
expect(readlinkSync(link)).toBe(".swarm/opentasks");
|
|
155
|
+
// Accessible via symlink
|
|
156
|
+
expect(existsSync(join(testDir, ".opentasks", "config.json"))).toBe(true);
|
|
157
|
+
// Verify config content
|
|
158
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "opentasks", "config.json"), "utf-8"));
|
|
159
|
+
expect(config.version).toBe("1.0");
|
|
160
|
+
expect(config.location.name).toBe("test-opentasks");
|
|
161
|
+
expect(config.location.hash).toBeDefined();
|
|
162
|
+
expect(config.location.uuid).toBeDefined();
|
|
163
|
+
});
|
|
164
|
+
it.skipIf(!installed)("uses project name from package.json", async () => {
|
|
165
|
+
createProject("my-custom-name");
|
|
166
|
+
await initProjectPackage("opentasks", projectCtx({ cwd: testDir, packages: ["opentasks"] }));
|
|
167
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "opentasks", "config.json"), "utf-8"));
|
|
168
|
+
expect(config.location.name).toBe("my-custom-name");
|
|
169
|
+
});
|
|
170
|
+
it.skipIf(!installed)("falls back to directory name without package.json", async () => {
|
|
171
|
+
// No package.json, just a real git repo
|
|
172
|
+
execSync("git init -q", { cwd: testDir });
|
|
173
|
+
await initProjectPackage("opentasks", projectCtx({ cwd: testDir, packages: ["opentasks"] }));
|
|
174
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "opentasks", "config.json"), "utf-8"));
|
|
175
|
+
expect(config.location.name).toBe(basename(testDir));
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
// ─── Project: minimem (real CLI) + embedding patching ────────────────────────
|
|
179
|
+
describe("initProjectPackage — minimem (real CLI)", async () => {
|
|
180
|
+
const installed = await hasCliInstalled("minimem");
|
|
181
|
+
it.skipIf(!installed)("runs minimem init and creates .minimem/", async () => {
|
|
182
|
+
createProject("test-minimem");
|
|
183
|
+
const result = await initProjectPackage("minimem", projectCtx({ cwd: testDir, packages: ["minimem"] }));
|
|
184
|
+
expect(result.success).toBe(true);
|
|
185
|
+
// Verify real filesystem artifacts
|
|
186
|
+
expect(existsSync(join(testDir, ".swarm", "minimem"))).toBe(true);
|
|
187
|
+
expect(existsSync(join(testDir, ".swarm", "minimem", "config.json"))).toBe(true);
|
|
188
|
+
expect(existsSync(join(testDir, ".swarm", "minimem", ".gitignore"))).toBe(true);
|
|
189
|
+
expect(existsSync(join(testDir, "MEMORY.md"))).toBe(true);
|
|
190
|
+
// Verify default config
|
|
191
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
192
|
+
expect(config.embedding.provider).toBe("auto");
|
|
193
|
+
expect(config.hybrid.enabled).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
it.skipIf(!installed)("patches embedding.provider to openai", async () => {
|
|
196
|
+
createProject("test-embed");
|
|
197
|
+
await initProjectPackage("minimem", projectCtx({
|
|
198
|
+
cwd: testDir,
|
|
199
|
+
packages: ["minimem"],
|
|
200
|
+
embeddingProvider: "openai",
|
|
201
|
+
}));
|
|
202
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
203
|
+
expect(config.embedding.provider).toBe("openai");
|
|
204
|
+
// Other fields preserved
|
|
205
|
+
expect(config.hybrid.enabled).toBe(true);
|
|
206
|
+
expect(config.query.maxResults).toBe(10);
|
|
207
|
+
});
|
|
208
|
+
it.skipIf(!installed)("patches embedding.provider to gemini", async () => {
|
|
209
|
+
createProject("test-embed-gemini");
|
|
210
|
+
await initProjectPackage("minimem", projectCtx({
|
|
211
|
+
cwd: testDir,
|
|
212
|
+
packages: ["minimem"],
|
|
213
|
+
embeddingProvider: "gemini",
|
|
214
|
+
}));
|
|
215
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
216
|
+
expect(config.embedding.provider).toBe("gemini");
|
|
217
|
+
});
|
|
218
|
+
it.skipIf(!installed)("does NOT patch when provider is local", async () => {
|
|
219
|
+
createProject("test-local");
|
|
220
|
+
await initProjectPackage("minimem", projectCtx({
|
|
221
|
+
cwd: testDir,
|
|
222
|
+
packages: ["minimem"],
|
|
223
|
+
embeddingProvider: "local",
|
|
224
|
+
}));
|
|
225
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
226
|
+
expect(config.embedding.provider).toBe("auto");
|
|
227
|
+
});
|
|
228
|
+
it.skipIf(!installed)("does NOT patch when provider is null", async () => {
|
|
229
|
+
createProject("test-null");
|
|
230
|
+
await initProjectPackage("minimem", projectCtx({
|
|
231
|
+
cwd: testDir,
|
|
232
|
+
packages: ["minimem"],
|
|
233
|
+
embeddingProvider: null,
|
|
234
|
+
}));
|
|
235
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
236
|
+
expect(config.embedding.provider).toBe("auto");
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
// ─── Project: cognitive-core (real CLI) ──────────────────────────────────────
|
|
240
|
+
describe("initProjectPackage — cognitive-core (real CLI)", async () => {
|
|
241
|
+
const installed = await hasCliInstalled("cognitive-core");
|
|
242
|
+
it.skipIf(!installed)("runs cognitive-core init and creates .cognitive-core/", async () => {
|
|
243
|
+
createProject("test-cc");
|
|
244
|
+
const result = await initProjectPackage("cognitive-core", projectCtx({ cwd: testDir, packages: ["cognitive-core"] }));
|
|
245
|
+
expect(result.success).toBe(true);
|
|
246
|
+
// Verify directory structure
|
|
247
|
+
expect(existsSync(join(testDir, ".swarm", "cognitive-core"))).toBe(true);
|
|
248
|
+
expect(existsSync(join(testDir, ".swarm", "cognitive-core", "experiences"))).toBe(true);
|
|
249
|
+
expect(existsSync(join(testDir, ".swarm", "cognitive-core", "playbooks"))).toBe(true);
|
|
250
|
+
expect(existsSync(join(testDir, ".swarm", "cognitive-core", "meta-strategies"))).toBe(true);
|
|
251
|
+
expect(existsSync(join(testDir, ".swarm", "cognitive-core", "meta-observations"))).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
// ─── Project: self-driving-repo ──────────────────────────────────────────────
|
|
255
|
+
// sdr is not published with a bin — skip real CLI test
|
|
256
|
+
describe("initProjectPackage — self-driving-repo", async () => {
|
|
257
|
+
const installed = await hasCliInstalled("sdr");
|
|
258
|
+
it.skipIf(!installed)("runs sdr init and creates .self-driving/", async () => {
|
|
259
|
+
createProject("test-sdr");
|
|
260
|
+
const result = await initProjectPackage("self-driving-repo", projectCtx({ cwd: testDir, packages: ["self-driving-repo"] }));
|
|
261
|
+
expect(result.success).toBe(true);
|
|
262
|
+
expect(existsSync(join(testDir, ".swarm", "self-driving"))).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
it.skipIf(installed)("returns failure when sdr CLI is not installed", async () => {
|
|
265
|
+
const result = await initProjectPackage("self-driving-repo", projectCtx({ cwd: testDir, packages: ["self-driving-repo"] }));
|
|
266
|
+
expect(result.success).toBe(false);
|
|
267
|
+
expect(result.package).toBe("self-driving-repo");
|
|
268
|
+
expect(result.message).toBeDefined();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
// ─── Project: skill-tree (direct directory creation — no CLI) ────────────────
|
|
272
|
+
describe("initProjectPackage — skill-tree", () => {
|
|
273
|
+
it("creates .swarm/skilltree/ with skills/ subdirectory (prefixed)", async () => {
|
|
274
|
+
const result = await initProjectPackage("skill-tree", projectCtx({ cwd: testDir, packages: ["skill-tree"] }));
|
|
275
|
+
expect(result.success).toBe(true);
|
|
276
|
+
expect(result.package).toBe("skill-tree");
|
|
277
|
+
expect(existsSync(join(testDir, ".swarm", "skilltree"))).toBe(true);
|
|
278
|
+
expect(existsSync(join(testDir, ".swarm", "skilltree", "skills"))).toBe(true);
|
|
279
|
+
});
|
|
280
|
+
it("creates .skilltree/ with skills/ subdirectory (flat)", async () => {
|
|
281
|
+
const result = await initProjectPackage("skill-tree", projectCtx({ cwd: testDir, packages: ["skill-tree"], usePrefix: false }));
|
|
282
|
+
expect(result.success).toBe(true);
|
|
283
|
+
expect(existsSync(join(testDir, ".skilltree"))).toBe(true);
|
|
284
|
+
expect(existsSync(join(testDir, ".skilltree", "skills"))).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
it("is idempotent", async () => {
|
|
287
|
+
await initProjectPackage("skill-tree", projectCtx({ cwd: testDir, packages: ["skill-tree"] }));
|
|
288
|
+
const result = await initProjectPackage("skill-tree", projectCtx({ cwd: testDir, packages: ["skill-tree"] }));
|
|
289
|
+
expect(result.success).toBe(true);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
// ─── Project: openteams (direct directory creation — no CLI) ─────────────────
|
|
293
|
+
describe("initProjectPackage — openteams", () => {
|
|
294
|
+
it("creates .swarm/openteams/ with config.json (prefixed)", async () => {
|
|
295
|
+
const result = await initProjectPackage("openteams", projectCtx({ cwd: testDir, packages: ["openteams"] }));
|
|
296
|
+
expect(result.success).toBe(true);
|
|
297
|
+
expect(result.package).toBe("openteams");
|
|
298
|
+
expect(existsSync(join(testDir, ".swarm", "openteams"))).toBe(true);
|
|
299
|
+
expect(existsSync(join(testDir, ".swarm", "openteams", "config.json"))).toBe(true);
|
|
300
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "openteams", "config.json"), "utf-8"));
|
|
301
|
+
expect(config).toEqual({});
|
|
302
|
+
});
|
|
303
|
+
it("creates .openteams/ with config.json (flat)", async () => {
|
|
304
|
+
const result = await initProjectPackage("openteams", projectCtx({ cwd: testDir, packages: ["openteams"], usePrefix: false }));
|
|
305
|
+
expect(result.success).toBe(true);
|
|
306
|
+
expect(existsSync(join(testDir, ".openteams"))).toBe(true);
|
|
307
|
+
expect(existsSync(join(testDir, ".openteams", "config.json"))).toBe(true);
|
|
308
|
+
});
|
|
309
|
+
it("does not overwrite existing config.json", async () => {
|
|
310
|
+
mkdirSync(join(testDir, ".swarm", "openteams"), { recursive: true });
|
|
311
|
+
writeFileSync(join(testDir, ".swarm", "openteams", "config.json"), JSON.stringify({ defaults: { include: ["gsd"] } }));
|
|
312
|
+
const result = await initProjectPackage("openteams", projectCtx({ cwd: testDir, packages: ["openteams"] }));
|
|
313
|
+
expect(result.success).toBe(true);
|
|
314
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "openteams", "config.json"), "utf-8"));
|
|
315
|
+
expect(config.defaults.include).toEqual(["gsd"]);
|
|
316
|
+
});
|
|
317
|
+
it("is idempotent", async () => {
|
|
318
|
+
await initProjectPackage("openteams", projectCtx({ cwd: testDir, packages: ["openteams"] }));
|
|
319
|
+
const result = await initProjectPackage("openteams", projectCtx({ cwd: testDir, packages: ["openteams"] }));
|
|
320
|
+
expect(result.success).toBe(true);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
// ─── Project: sessionlog (direct directory creation — no CLI) ────────────────
|
|
324
|
+
describe("initProjectPackage — sessionlog", () => {
|
|
325
|
+
it("creates .swarm/sessionlog/ with settings.json (prefixed)", async () => {
|
|
326
|
+
const result = await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"] }));
|
|
327
|
+
expect(result.success).toBe(true);
|
|
328
|
+
expect(result.package).toBe("sessionlog");
|
|
329
|
+
expect(existsSync(join(testDir, ".swarm", "sessionlog"))).toBe(true);
|
|
330
|
+
expect(existsSync(join(testDir, ".swarm", "sessionlog", "settings.json"))).toBe(true);
|
|
331
|
+
const settings = JSON.parse(readFileSync(join(testDir, ".swarm", "sessionlog", "settings.json"), "utf-8"));
|
|
332
|
+
expect(settings.enabled).toBe(false);
|
|
333
|
+
expect(settings.strategy).toBe("manual-commit");
|
|
334
|
+
});
|
|
335
|
+
it("creates .sessionlog/ with settings.json (flat)", async () => {
|
|
336
|
+
const result = await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"], usePrefix: false }));
|
|
337
|
+
expect(result.success).toBe(true);
|
|
338
|
+
expect(existsSync(join(testDir, ".sessionlog"))).toBe(true);
|
|
339
|
+
expect(existsSync(join(testDir, ".sessionlog", "settings.json"))).toBe(true);
|
|
340
|
+
});
|
|
341
|
+
it("does not overwrite existing settings.json", async () => {
|
|
342
|
+
mkdirSync(join(testDir, ".swarm", "sessionlog"), { recursive: true });
|
|
343
|
+
writeFileSync(join(testDir, ".swarm", "sessionlog", "settings.json"), JSON.stringify({ enabled: true, strategy: "auto" }));
|
|
344
|
+
const result = await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"] }));
|
|
345
|
+
expect(result.success).toBe(true);
|
|
346
|
+
const settings = JSON.parse(readFileSync(join(testDir, ".swarm", "sessionlog", "settings.json"), "utf-8"));
|
|
347
|
+
expect(settings.enabled).toBe(true);
|
|
348
|
+
expect(settings.strategy).toBe("auto");
|
|
349
|
+
});
|
|
350
|
+
it("is idempotent", async () => {
|
|
351
|
+
await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"] }));
|
|
352
|
+
const result = await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"] }));
|
|
353
|
+
expect(result.success).toBe(true);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
// ─── Project: claude-code-swarm (direct directory creation — no CLI) ──────────
|
|
357
|
+
describe("initProjectPackage — claude-code-swarm", () => {
|
|
358
|
+
it("creates .swarm/claude-swarm/ with config.json and .gitignore (prefixed)", async () => {
|
|
359
|
+
const result = await initProjectPackage("claude-code-swarm", projectCtx({ cwd: testDir, packages: ["claude-code-swarm"] }));
|
|
360
|
+
expect(result.success).toBe(true);
|
|
361
|
+
expect(result.package).toBe("claude-code-swarm");
|
|
362
|
+
expect(existsSync(join(testDir, ".swarm", "claude-swarm"))).toBe(true);
|
|
363
|
+
expect(existsSync(join(testDir, ".swarm", "claude-swarm", "config.json"))).toBe(true);
|
|
364
|
+
expect(existsSync(join(testDir, ".swarm", "claude-swarm", ".gitignore"))).toBe(true);
|
|
365
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "claude-swarm", "config.json"), "utf-8"));
|
|
366
|
+
expect(config).toEqual({});
|
|
367
|
+
const gitignore = readFileSync(join(testDir, ".swarm", "claude-swarm", ".gitignore"), "utf-8");
|
|
368
|
+
expect(gitignore).toBe("tmp/\n");
|
|
369
|
+
});
|
|
370
|
+
it("creates .claude-swarm/ with config.json and .gitignore (flat)", async () => {
|
|
371
|
+
const result = await initProjectPackage("claude-code-swarm", projectCtx({ cwd: testDir, packages: ["claude-code-swarm"], usePrefix: false }));
|
|
372
|
+
expect(result.success).toBe(true);
|
|
373
|
+
expect(existsSync(join(testDir, ".claude-swarm"))).toBe(true);
|
|
374
|
+
expect(existsSync(join(testDir, ".claude-swarm", "config.json"))).toBe(true);
|
|
375
|
+
expect(existsSync(join(testDir, ".claude-swarm", ".gitignore"))).toBe(true);
|
|
376
|
+
});
|
|
377
|
+
it("does not overwrite existing config.json", async () => {
|
|
378
|
+
mkdirSync(join(testDir, ".swarm", "claude-swarm"), { recursive: true });
|
|
379
|
+
writeFileSync(join(testDir, ".swarm", "claude-swarm", "config.json"), JSON.stringify({ template: "gsd" }));
|
|
380
|
+
const result = await initProjectPackage("claude-code-swarm", projectCtx({ cwd: testDir, packages: ["claude-code-swarm"] }));
|
|
381
|
+
expect(result.success).toBe(true);
|
|
382
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "claude-swarm", "config.json"), "utf-8"));
|
|
383
|
+
expect(config.template).toBe("gsd");
|
|
384
|
+
});
|
|
385
|
+
it("does not overwrite existing .gitignore", async () => {
|
|
386
|
+
mkdirSync(join(testDir, ".swarm", "claude-swarm"), { recursive: true });
|
|
387
|
+
writeFileSync(join(testDir, ".swarm", "claude-swarm", ".gitignore"), "tmp/\ncustom/\n");
|
|
388
|
+
const result = await initProjectPackage("claude-code-swarm", projectCtx({ cwd: testDir, packages: ["claude-code-swarm"] }));
|
|
389
|
+
expect(result.success).toBe(true);
|
|
390
|
+
const gitignore = readFileSync(join(testDir, ".swarm", "claude-swarm", ".gitignore"), "utf-8");
|
|
391
|
+
expect(gitignore).toBe("tmp/\ncustom/\n");
|
|
392
|
+
});
|
|
393
|
+
it("is idempotent", async () => {
|
|
394
|
+
await initProjectPackage("claude-code-swarm", projectCtx({ cwd: testDir, packages: ["claude-code-swarm"] }));
|
|
395
|
+
const result = await initProjectPackage("claude-code-swarm", projectCtx({ cwd: testDir, packages: ["claude-code-swarm"] }));
|
|
396
|
+
expect(result.success).toBe(true);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
// ─── Project: unknown ────────────────────────────────────────────────────────
|
|
400
|
+
describe("initProjectPackage — unknown", () => {
|
|
401
|
+
it("returns failure with descriptive message", async () => {
|
|
402
|
+
const result = await initProjectPackage("not-a-package", projectCtx({ cwd: testDir }));
|
|
403
|
+
expect(result).toEqual({
|
|
404
|
+
package: "not-a-package",
|
|
405
|
+
success: false,
|
|
406
|
+
message: "Unknown package",
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
// ─── Global: skill-tree (real CLI) ───────────────────────────────────────────
|
|
411
|
+
describe("initGlobalPackage — skill-tree (real CLI)", async () => {
|
|
412
|
+
const installed = await hasCliInstalled("skill-tree");
|
|
413
|
+
it.skipIf(!installed)("runs skill-tree config init and creates config.yaml", async () => {
|
|
414
|
+
const result = await initGlobalPackage("skill-tree", globalCtx({ packages: ["skill-tree"] }));
|
|
415
|
+
expect(result.success).toBe(true);
|
|
416
|
+
const configPath = join(testHome, ".skill-tree", "config.yaml");
|
|
417
|
+
expect(existsSync(configPath)).toBe(true);
|
|
418
|
+
const yaml = readFileSync(configPath, "utf-8");
|
|
419
|
+
expect(yaml).toContain("storage:");
|
|
420
|
+
expect(yaml).toContain("sqlite");
|
|
421
|
+
expect(yaml).toContain("indexer:");
|
|
422
|
+
});
|
|
423
|
+
it.skipIf(!installed)("skips when config.yaml already exists", async () => {
|
|
424
|
+
mkdirSync(join(testHome, ".skill-tree"), { recursive: true });
|
|
425
|
+
writeFileSync(join(testHome, ".skill-tree", "config.yaml"), "custom: true\n");
|
|
426
|
+
const result = await initGlobalPackage("skill-tree", globalCtx({ packages: ["skill-tree"] }));
|
|
427
|
+
expect(result.success).toBe(true);
|
|
428
|
+
expect(result.message).toBe("already configured");
|
|
429
|
+
// Original content preserved
|
|
430
|
+
const yaml = readFileSync(join(testHome, ".skill-tree", "config.yaml"), "utf-8");
|
|
431
|
+
expect(yaml).toBe("custom: true\n");
|
|
432
|
+
});
|
|
433
|
+
it.skipIf(!installed)("does NOT skip when directory exists but config.yaml is missing", async () => {
|
|
434
|
+
mkdirSync(join(testHome, ".skill-tree"), { recursive: true });
|
|
435
|
+
const result = await initGlobalPackage("skill-tree", globalCtx({ packages: ["skill-tree"] }));
|
|
436
|
+
expect(result.success).toBe(true);
|
|
437
|
+
expect(result.message).toBeUndefined();
|
|
438
|
+
expect(existsSync(join(testHome, ".skill-tree", "config.yaml"))).toBe(true);
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
// ─── Global: openhive ────────────────────────────────────────────────────────
|
|
442
|
+
// openhive 0.0.1 is a stub publish without a bin entry
|
|
443
|
+
describe("initGlobalPackage — openhive", async () => {
|
|
444
|
+
const installed = await hasCliInstalled("openhive");
|
|
445
|
+
it.skipIf(!installed)("calls openhive init with non-interactive flags", async () => {
|
|
446
|
+
const result = await initGlobalPackage("openhive", globalCtx({ packages: ["openhive"] }), {
|
|
447
|
+
name: "TestHive",
|
|
448
|
+
port: 4000,
|
|
449
|
+
authMode: "token",
|
|
450
|
+
verification: "invite",
|
|
451
|
+
});
|
|
452
|
+
expect(result.success).toBe(true);
|
|
453
|
+
});
|
|
454
|
+
it("returns failure when openhive options are omitted", async () => {
|
|
455
|
+
const result = await initGlobalPackage("openhive", globalCtx({ packages: ["openhive"] }));
|
|
456
|
+
expect(result.success).toBe(false);
|
|
457
|
+
expect(result.message).toContain("No openhive options");
|
|
458
|
+
});
|
|
459
|
+
it.skipIf(installed)("returns failure when openhive CLI is not installed", async () => {
|
|
460
|
+
const result = await initGlobalPackage("openhive", globalCtx({ packages: ["openhive"] }), {
|
|
461
|
+
name: "H",
|
|
462
|
+
port: 3000,
|
|
463
|
+
authMode: "local",
|
|
464
|
+
verification: "open",
|
|
465
|
+
});
|
|
466
|
+
expect(result.success).toBe(false);
|
|
467
|
+
expect(result.message).toBeDefined();
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
// ─── Global: openteams (no global config — project config handled separately) ─
|
|
471
|
+
describe("initGlobalPackage — openteams", () => {
|
|
472
|
+
it("returns success with no-setup message", async () => {
|
|
473
|
+
const result = await initGlobalPackage("openteams", globalCtx({ packages: ["openteams"] }));
|
|
474
|
+
expect(result).toEqual({
|
|
475
|
+
package: "openteams",
|
|
476
|
+
success: true,
|
|
477
|
+
message: "no setup required",
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
// ─── Global: sessionlog (no global config — project config handled separately) ─
|
|
482
|
+
describe("initGlobalPackage — sessionlog", () => {
|
|
483
|
+
it("returns success with no-setup message", async () => {
|
|
484
|
+
const result = await initGlobalPackage("sessionlog", globalCtx({ packages: ["sessionlog"] }));
|
|
485
|
+
expect(result).toEqual({
|
|
486
|
+
package: "sessionlog",
|
|
487
|
+
success: true,
|
|
488
|
+
message: "no setup required",
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
// ─── Global: claude-code-swarm (no global config) ────────────────────────────
|
|
493
|
+
describe("initGlobalPackage — claude-code-swarm", () => {
|
|
494
|
+
it("returns success with no-setup message", async () => {
|
|
495
|
+
const result = await initGlobalPackage("claude-code-swarm", globalCtx({ packages: ["claude-code-swarm"] }));
|
|
496
|
+
expect(result).toEqual({
|
|
497
|
+
package: "claude-code-swarm",
|
|
498
|
+
success: true,
|
|
499
|
+
message: "no setup required",
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
// ─── Global: unknown ─────────────────────────────────────────────────────────
|
|
504
|
+
describe("initGlobalPackage — unknown", () => {
|
|
505
|
+
it("returns failure", async () => {
|
|
506
|
+
const result = await initGlobalPackage("nonexistent", globalCtx());
|
|
507
|
+
expect(result).toEqual({
|
|
508
|
+
package: "nonexistent",
|
|
509
|
+
success: false,
|
|
510
|
+
message: "Unknown package",
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
// ─── E2E: full project init flow (solo bundle) ──────────────────────────────
|
|
515
|
+
describe("e2e: project init — solo bundle", async () => {
|
|
516
|
+
const opentasksOk = await hasCliInstalled("opentasks");
|
|
517
|
+
const minimemOk = await hasCliInstalled("minimem");
|
|
518
|
+
const allOk = opentasksOk && minimemOk;
|
|
519
|
+
it.skipIf(!allOk)("initializes opentasks → minimem with cross-wiring", async () => {
|
|
520
|
+
createProject("solo-e2e");
|
|
521
|
+
const ctx = projectCtx({
|
|
522
|
+
cwd: testDir,
|
|
523
|
+
packages: ["opentasks", "minimem"],
|
|
524
|
+
embeddingProvider: "openai",
|
|
525
|
+
});
|
|
526
|
+
const results = [];
|
|
527
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
528
|
+
if (!ctx.packages.includes(pkg))
|
|
529
|
+
continue;
|
|
530
|
+
if (isProjectInit(ctx.cwd, pkg))
|
|
531
|
+
continue;
|
|
532
|
+
results.push(await initProjectPackage(pkg, ctx));
|
|
533
|
+
}
|
|
534
|
+
// All succeeded
|
|
535
|
+
expect(results.every((r) => r.success)).toBe(true);
|
|
536
|
+
expect(results.map((r) => r.package)).toEqual([
|
|
537
|
+
"opentasks",
|
|
538
|
+
"minimem",
|
|
539
|
+
]);
|
|
540
|
+
// opentasks: real config with project name
|
|
541
|
+
const otConfig = JSON.parse(readFileSync(join(testDir, ".swarm", "opentasks", "config.json"), "utf-8"));
|
|
542
|
+
expect(otConfig.location.name).toBe("solo-e2e");
|
|
543
|
+
// minimem: real config patched with openai
|
|
544
|
+
const mmConfig = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
545
|
+
expect(mmConfig.embedding.provider).toBe("openai");
|
|
546
|
+
expect(mmConfig.hybrid.enabled).toBe(true);
|
|
547
|
+
});
|
|
548
|
+
it.skipIf(!allOk)("skips already-initialized packages", async () => {
|
|
549
|
+
createProject("skip-e2e");
|
|
550
|
+
// Pre-initialize opentasks
|
|
551
|
+
const preCtx = projectCtx({
|
|
552
|
+
cwd: testDir,
|
|
553
|
+
packages: ["opentasks"],
|
|
554
|
+
});
|
|
555
|
+
await initProjectPackage("opentasks", preCtx);
|
|
556
|
+
expect(isProjectInit(testDir, "opentasks")).toBe(true);
|
|
557
|
+
// Now run full init — opentasks should be skipped
|
|
558
|
+
const ctx = projectCtx({
|
|
559
|
+
cwd: testDir,
|
|
560
|
+
packages: ["opentasks", "minimem"],
|
|
561
|
+
});
|
|
562
|
+
const results = [];
|
|
563
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
564
|
+
if (!ctx.packages.includes(pkg))
|
|
565
|
+
continue;
|
|
566
|
+
if (isProjectInit(ctx.cwd, pkg))
|
|
567
|
+
continue;
|
|
568
|
+
results.push(await initProjectPackage(pkg, ctx));
|
|
569
|
+
}
|
|
570
|
+
// opentasks was skipped
|
|
571
|
+
expect(results.map((r) => r.package)).toEqual([
|
|
572
|
+
"minimem",
|
|
573
|
+
]);
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
// ─── E2E: full project init flow (team bundle — init order) ─────────────────
|
|
577
|
+
describe("e2e: project init — team bundle (init order)", async () => {
|
|
578
|
+
const opentasksOk = await hasCliInstalled("opentasks");
|
|
579
|
+
const minimemOk = await hasCliInstalled("minimem");
|
|
580
|
+
const ccOk = await hasCliInstalled("cognitive-core");
|
|
581
|
+
const allOk = opentasksOk && minimemOk && ccOk;
|
|
582
|
+
it.skipIf(!allOk)("cognitive-core runs after minimem (can detect .minimem/)", async () => {
|
|
583
|
+
createProject("team-e2e");
|
|
584
|
+
const ctx = projectCtx({
|
|
585
|
+
cwd: testDir,
|
|
586
|
+
packages: [
|
|
587
|
+
"opentasks",
|
|
588
|
+
"minimem",
|
|
589
|
+
"cognitive-core",
|
|
590
|
+
],
|
|
591
|
+
embeddingProvider: "gemini",
|
|
592
|
+
});
|
|
593
|
+
const results = [];
|
|
594
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
595
|
+
if (!ctx.packages.includes(pkg))
|
|
596
|
+
continue;
|
|
597
|
+
if (isProjectInit(ctx.cwd, pkg))
|
|
598
|
+
continue;
|
|
599
|
+
results.push(await initProjectPackage(pkg, ctx));
|
|
600
|
+
}
|
|
601
|
+
expect(results.every((r) => r.success)).toBe(true);
|
|
602
|
+
expect(results.map((r) => r.package)).toEqual([
|
|
603
|
+
"opentasks",
|
|
604
|
+
"minimem",
|
|
605
|
+
"cognitive-core",
|
|
606
|
+
]);
|
|
607
|
+
// All directories exist
|
|
608
|
+
expect(existsSync(join(testDir, ".swarm", "opentasks"))).toBe(true);
|
|
609
|
+
expect(existsSync(join(testDir, ".swarm", "minimem"))).toBe(true);
|
|
610
|
+
expect(existsSync(join(testDir, ".swarm", "cognitive-core"))).toBe(true);
|
|
611
|
+
// minimem was patched to gemini
|
|
612
|
+
const mmConfig = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
613
|
+
expect(mmConfig.embedding.provider).toBe("gemini");
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
// ─── E2E: full global init flow ──────────────────────────────────────────────
|
|
617
|
+
describe("e2e: global init — skill-tree", async () => {
|
|
618
|
+
const stOk = await hasCliInstalled("skill-tree");
|
|
619
|
+
it.skipIf(!stOk)("initializes skill-tree", async () => {
|
|
620
|
+
const ctx = globalCtx({
|
|
621
|
+
packages: ["skill-tree"],
|
|
622
|
+
});
|
|
623
|
+
const globalOrder = ["skill-tree"];
|
|
624
|
+
const results = [];
|
|
625
|
+
for (const pkg of globalOrder) {
|
|
626
|
+
if (!ctx.packages.includes(pkg))
|
|
627
|
+
continue;
|
|
628
|
+
if (isGlobalInit(pkg))
|
|
629
|
+
continue;
|
|
630
|
+
results.push(await initGlobalPackage(pkg, ctx));
|
|
631
|
+
}
|
|
632
|
+
expect(results.every((r) => r.success)).toBe(true);
|
|
633
|
+
expect(results.map((r) => r.package)).toEqual(["skill-tree"]);
|
|
634
|
+
// skill-tree: config.yaml exists
|
|
635
|
+
expect(existsSync(join(testHome, ".skill-tree", "config.yaml"))).toBe(true);
|
|
636
|
+
});
|
|
637
|
+
it.skipIf(!stOk)("skips already-configured global packages", async () => {
|
|
638
|
+
// Pre-create skill-tree config
|
|
639
|
+
mkdirSync(join(testHome, ".skill-tree"), { recursive: true });
|
|
640
|
+
writeFileSync(join(testHome, ".skill-tree", "config.yaml"), "existing: true\n");
|
|
641
|
+
const ctx = globalCtx({
|
|
642
|
+
packages: ["skill-tree"],
|
|
643
|
+
});
|
|
644
|
+
const globalOrder = ["skill-tree"];
|
|
645
|
+
const results = [];
|
|
646
|
+
for (const pkg of globalOrder) {
|
|
647
|
+
if (!ctx.packages.includes(pkg))
|
|
648
|
+
continue;
|
|
649
|
+
if (isGlobalInit(pkg))
|
|
650
|
+
continue;
|
|
651
|
+
results.push(await initGlobalPackage(pkg, ctx));
|
|
652
|
+
}
|
|
653
|
+
// Nothing was initialized (already configured)
|
|
654
|
+
expect(results).toEqual([]);
|
|
655
|
+
// Existing config preserved
|
|
656
|
+
expect(readFileSync(join(testHome, ".skill-tree", "config.yaml"), "utf-8")).toBe("existing: true\n");
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
// ─── Flow 2: plugin bootstrap → swarmkit project init ────────────────────────
|
|
660
|
+
//
|
|
661
|
+
// Simulates what claude-code-swarm's bootstrap.mjs does when it calls swarmkit
|
|
662
|
+
// programmatically: check isProjectInit for each dep, then init those missing.
|
|
663
|
+
// This is the "plugin installs first, swarmkit comes bundled" path.
|
|
664
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
665
|
+
describe("flow: plugin bootstrap — init project dirs via swarmkit", () => {
|
|
666
|
+
it("initializes openteams + claude-code-swarm in correct order", async () => {
|
|
667
|
+
// Mirrors bootstrap.mjs's initSwarmProject() + ensureSwarmDir() flow
|
|
668
|
+
const cwd = testDir;
|
|
669
|
+
// Bootstrap determines required packages from config
|
|
670
|
+
const requiredPackages = ["openteams", "claude-code-swarm"];
|
|
671
|
+
const ctx = projectCtx({
|
|
672
|
+
cwd,
|
|
673
|
+
packages: requiredPackages,
|
|
674
|
+
});
|
|
675
|
+
// Bootstrap checks which packages are already initialized
|
|
676
|
+
for (const pkg of requiredPackages) {
|
|
677
|
+
expect(isProjectInit(cwd, pkg)).toBe(false);
|
|
678
|
+
}
|
|
679
|
+
// Bootstrap initializes missing packages using PROJECT_INIT_ORDER
|
|
680
|
+
const results = [];
|
|
681
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
682
|
+
if (!requiredPackages.includes(pkg))
|
|
683
|
+
continue;
|
|
684
|
+
if (isProjectInit(cwd, pkg))
|
|
685
|
+
continue;
|
|
686
|
+
results.push(await initProjectPackage(pkg, ctx));
|
|
687
|
+
}
|
|
688
|
+
// All succeeded
|
|
689
|
+
expect(results.every((r) => r.success)).toBe(true);
|
|
690
|
+
expect(results.map((r) => r.package)).toEqual([
|
|
691
|
+
"openteams",
|
|
692
|
+
"claude-code-swarm",
|
|
693
|
+
]);
|
|
694
|
+
// openteams: .swarm/openteams/config.json exists
|
|
695
|
+
expect(existsSync(join(cwd, ".swarm", "openteams"))).toBe(true);
|
|
696
|
+
expect(existsSync(join(cwd, ".swarm", "openteams", "config.json"))).toBe(true);
|
|
697
|
+
// claude-code-swarm: .swarm/claude-swarm/ exists with config + .gitignore
|
|
698
|
+
expect(existsSync(join(cwd, ".swarm", "claude-swarm"))).toBe(true);
|
|
699
|
+
expect(existsSync(join(cwd, ".swarm", "claude-swarm", "config.json"))).toBe(true);
|
|
700
|
+
expect(existsSync(join(cwd, ".swarm", "claude-swarm", ".gitignore"))).toBe(true);
|
|
701
|
+
const config = JSON.parse(readFileSync(join(cwd, ".swarm", "claude-swarm", "config.json"), "utf-8"));
|
|
702
|
+
expect(config).toEqual({});
|
|
703
|
+
const gitignore = readFileSync(join(cwd, ".swarm", "claude-swarm", ".gitignore"), "utf-8");
|
|
704
|
+
expect(gitignore).toBe("tmp/\n");
|
|
705
|
+
// isProjectInit now returns true for both
|
|
706
|
+
expect(isProjectInit(cwd, "openteams")).toBe(true);
|
|
707
|
+
expect(isProjectInit(cwd, "claude-code-swarm")).toBe(true);
|
|
708
|
+
});
|
|
709
|
+
it("initializes openteams + sessionlog + claude-code-swarm when sessionlog enabled", async () => {
|
|
710
|
+
const cwd = testDir;
|
|
711
|
+
// Config has sessionlog enabled → bootstrap adds it to required
|
|
712
|
+
const requiredPackages = ["openteams", "sessionlog", "claude-code-swarm"];
|
|
713
|
+
const ctx = projectCtx({
|
|
714
|
+
cwd,
|
|
715
|
+
packages: requiredPackages,
|
|
716
|
+
});
|
|
717
|
+
const results = [];
|
|
718
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
719
|
+
if (!requiredPackages.includes(pkg))
|
|
720
|
+
continue;
|
|
721
|
+
if (isProjectInit(cwd, pkg))
|
|
722
|
+
continue;
|
|
723
|
+
results.push(await initProjectPackage(pkg, ctx));
|
|
724
|
+
}
|
|
725
|
+
expect(results.every((r) => r.success)).toBe(true);
|
|
726
|
+
expect(results.map((r) => r.package)).toEqual([
|
|
727
|
+
"openteams",
|
|
728
|
+
"sessionlog",
|
|
729
|
+
"claude-code-swarm",
|
|
730
|
+
]);
|
|
731
|
+
// All three dirs exist
|
|
732
|
+
expect(existsSync(join(cwd, ".swarm", "openteams"))).toBe(true);
|
|
733
|
+
expect(existsSync(join(cwd, ".swarm", "sessionlog"))).toBe(true);
|
|
734
|
+
expect(existsSync(join(cwd, ".swarm", "claude-swarm"))).toBe(true);
|
|
735
|
+
// sessionlog has default settings
|
|
736
|
+
const settings = JSON.parse(readFileSync(join(cwd, ".swarm", "sessionlog", "settings.json"), "utf-8"));
|
|
737
|
+
expect(settings.enabled).toBe(false);
|
|
738
|
+
expect(settings.strategy).toBe("manual-commit");
|
|
739
|
+
});
|
|
740
|
+
it("skips already-initialized packages on re-run (idempotent bootstrap)", async () => {
|
|
741
|
+
const cwd = testDir;
|
|
742
|
+
const requiredPackages = ["openteams", "claude-code-swarm"];
|
|
743
|
+
const ctx = projectCtx({ cwd, packages: requiredPackages });
|
|
744
|
+
// First bootstrap run
|
|
745
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
746
|
+
if (!requiredPackages.includes(pkg))
|
|
747
|
+
continue;
|
|
748
|
+
if (isProjectInit(cwd, pkg))
|
|
749
|
+
continue;
|
|
750
|
+
await initProjectPackage(pkg, ctx);
|
|
751
|
+
}
|
|
752
|
+
// Write custom config to .swarm/claude-swarm/config.json
|
|
753
|
+
writeFileSync(join(cwd, ".swarm", "claude-swarm", "config.json"), JSON.stringify({ template: "gsd" }));
|
|
754
|
+
// Second bootstrap run — should skip both (already initialized)
|
|
755
|
+
const results = [];
|
|
756
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
757
|
+
if (!requiredPackages.includes(pkg))
|
|
758
|
+
continue;
|
|
759
|
+
if (isProjectInit(cwd, pkg))
|
|
760
|
+
continue;
|
|
761
|
+
results.push(await initProjectPackage(pkg, ctx));
|
|
762
|
+
}
|
|
763
|
+
// Nothing was initialized (all already exist)
|
|
764
|
+
expect(results).toEqual([]);
|
|
765
|
+
// Custom config preserved
|
|
766
|
+
const config = JSON.parse(readFileSync(join(cwd, ".swarm", "claude-swarm", "config.json"), "utf-8"));
|
|
767
|
+
expect(config.template).toBe("gsd");
|
|
768
|
+
});
|
|
769
|
+
it("handles pre-existing openteams, only initializes claude-code-swarm", async () => {
|
|
770
|
+
const cwd = testDir;
|
|
771
|
+
// Simulate swarmkit init already ran (openteams exists)
|
|
772
|
+
mkdirSync(join(cwd, ".swarm", "openteams"), { recursive: true });
|
|
773
|
+
writeFileSync(join(cwd, ".swarm", "openteams", "config.json"), JSON.stringify({ existing: true }));
|
|
774
|
+
const requiredPackages = ["openteams", "claude-code-swarm"];
|
|
775
|
+
const ctx = projectCtx({ cwd, packages: requiredPackages });
|
|
776
|
+
const results = [];
|
|
777
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
778
|
+
if (!requiredPackages.includes(pkg))
|
|
779
|
+
continue;
|
|
780
|
+
if (isProjectInit(cwd, pkg))
|
|
781
|
+
continue;
|
|
782
|
+
results.push(await initProjectPackage(pkg, ctx));
|
|
783
|
+
}
|
|
784
|
+
// Only claude-code-swarm was initialized
|
|
785
|
+
expect(results.map((r) => r.package)).toEqual(["claude-code-swarm"]);
|
|
786
|
+
// openteams config preserved
|
|
787
|
+
const otConfig = JSON.parse(readFileSync(join(cwd, ".swarm", "openteams", "config.json"), "utf-8"));
|
|
788
|
+
expect(otConfig.existing).toBe(true);
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
// ─── E2E: embedding provider propagation ─────────────────────────────────────
|
|
792
|
+
describe("e2e: embedding provider propagation", async () => {
|
|
793
|
+
const minimemOk = await hasCliInstalled("minimem");
|
|
794
|
+
it.skipIf(!minimemOk)("openai embedding flows through to real minimem config", async () => {
|
|
795
|
+
createProject("embed-e2e");
|
|
796
|
+
await initProjectPackage("minimem", projectCtx({
|
|
797
|
+
cwd: testDir,
|
|
798
|
+
packages: ["minimem"],
|
|
799
|
+
embeddingProvider: "openai",
|
|
800
|
+
apiKeys: { openai: "sk-embed" },
|
|
801
|
+
}));
|
|
802
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
803
|
+
expect(config.embedding.provider).toBe("openai");
|
|
804
|
+
// Real minimem config has these fields too
|
|
805
|
+
expect(config.hybrid).toBeDefined();
|
|
806
|
+
expect(config.query).toBeDefined();
|
|
807
|
+
});
|
|
808
|
+
});
|