swarmkit 0.0.4 → 0.0.6
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/doctor/checks.js +28 -3
- package/dist/doctor/checks.test.js +27 -6
- package/dist/doctor/types.d.ts +2 -0
- package/dist/packages/setup.js +144 -64
- package/dist/packages/setup.test.js +94 -41
- package/package.json +1 -1
package/dist/doctor/checks.js
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs";
|
|
1
2
|
import { join } from "node:path";
|
|
2
3
|
import { PACKAGES, getActiveIntegrations } from "../packages/registry.js";
|
|
4
|
+
/** Check if a directory exists (via ctx.exists) and contains at least one file */
|
|
5
|
+
function dirHasContent(dir, exists) {
|
|
6
|
+
if (!exists(dir))
|
|
7
|
+
return false;
|
|
8
|
+
try {
|
|
9
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
if (entry.isFile() || entry.isSymbolicLink())
|
|
12
|
+
return true;
|
|
13
|
+
if (entry.isDirectory() && dirHasContent(join(dir, entry.name), exists))
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
3
22
|
/** Config directories — prefixed layout (.swarm/) */
|
|
4
23
|
const PREFIXED_CONFIG_DIRS = {
|
|
5
24
|
opentasks: ".swarm/opentasks",
|
|
@@ -139,8 +158,11 @@ export function checkProjectConfigs(ctx) {
|
|
|
139
158
|
continue; // Package has no project-level config
|
|
140
159
|
if (PACKAGES[pkg]?.globalOnly)
|
|
141
160
|
continue;
|
|
142
|
-
const
|
|
143
|
-
const
|
|
161
|
+
const prefixedPath = join(ctx.cwd, prefixedDir);
|
|
162
|
+
const flatPath = flatDir ? join(ctx.cwd, flatDir) : null;
|
|
163
|
+
const checkContent = ctx.hasContent ?? ((p) => dirHasContent(p, ctx.exists));
|
|
164
|
+
const hasPrefixed = checkContent(prefixedPath);
|
|
165
|
+
const hasFlat = flatPath ? checkContent(flatPath) : false;
|
|
144
166
|
if (hasPrefixed || hasFlat) {
|
|
145
167
|
const found = hasPrefixed ? prefixedDir : flatDir;
|
|
146
168
|
results.push({
|
|
@@ -150,10 +172,13 @@ export function checkProjectConfigs(ctx) {
|
|
|
150
172
|
});
|
|
151
173
|
}
|
|
152
174
|
else {
|
|
175
|
+
const dirExists = ctx.exists(prefixedPath) || (flatPath ? ctx.exists(flatPath) : false);
|
|
153
176
|
results.push({
|
|
154
177
|
name: `${pkg}-config`,
|
|
155
178
|
status: "warn",
|
|
156
|
-
message:
|
|
179
|
+
message: dirExists
|
|
180
|
+
? `${prefixedDir}/ exists but is empty`
|
|
181
|
+
: `${prefixedDir}/ not found`,
|
|
157
182
|
fix: `swarmkit init (or ${pkg} init)`,
|
|
158
183
|
});
|
|
159
184
|
}
|
|
@@ -151,22 +151,26 @@ describe("checkProjectConfigs", () => {
|
|
|
151
151
|
const results = checkProjectConfigs(ctx);
|
|
152
152
|
expect(results).toEqual([]);
|
|
153
153
|
});
|
|
154
|
-
it("passes when prefixed config directory exists", () => {
|
|
154
|
+
it("passes when prefixed config directory exists with content", () => {
|
|
155
|
+
const hasDir = (path) => path.endsWith(".swarm/opentasks");
|
|
155
156
|
const ctx = createContext({
|
|
156
157
|
isProject: true,
|
|
157
158
|
installedPackages: ["opentasks"],
|
|
158
|
-
exists:
|
|
159
|
+
exists: hasDir,
|
|
160
|
+
hasContent: hasDir,
|
|
159
161
|
});
|
|
160
162
|
const results = checkProjectConfigs(ctx);
|
|
161
163
|
expect(results).toHaveLength(1);
|
|
162
164
|
expect(results[0].status).toBe("pass");
|
|
163
165
|
expect(results[0].message).toContain(".swarm/opentasks");
|
|
164
166
|
});
|
|
165
|
-
it("passes when flat config directory exists", () => {
|
|
167
|
+
it("passes when flat config directory exists with content", () => {
|
|
168
|
+
const hasDir = (path) => path.endsWith(".opentasks");
|
|
166
169
|
const ctx = createContext({
|
|
167
170
|
isProject: true,
|
|
168
171
|
installedPackages: ["opentasks"],
|
|
169
|
-
exists:
|
|
172
|
+
exists: hasDir,
|
|
173
|
+
hasContent: hasDir,
|
|
170
174
|
});
|
|
171
175
|
const results = checkProjectConfigs(ctx);
|
|
172
176
|
expect(results).toHaveLength(1);
|
|
@@ -186,10 +190,12 @@ describe("checkProjectConfigs", () => {
|
|
|
186
190
|
expect(results[0].fix).toContain("opentasks init");
|
|
187
191
|
});
|
|
188
192
|
it("passes for claude-code-swarm with prefixed config", () => {
|
|
193
|
+
const hasDir = (path) => path.endsWith(".swarm/claude-swarm");
|
|
189
194
|
const ctx = createContext({
|
|
190
195
|
isProject: true,
|
|
191
196
|
installedPackages: ["claude-code-swarm"],
|
|
192
|
-
exists:
|
|
197
|
+
exists: hasDir,
|
|
198
|
+
hasContent: hasDir,
|
|
193
199
|
});
|
|
194
200
|
const results = checkProjectConfigs(ctx);
|
|
195
201
|
expect(results).toHaveLength(1);
|
|
@@ -210,11 +216,13 @@ describe("checkProjectConfigs", () => {
|
|
|
210
216
|
"/tmp/test-project/.swarm/opentasks",
|
|
211
217
|
"/tmp/test-project/.minimem",
|
|
212
218
|
]);
|
|
219
|
+
const checkDir = (path) => existingDirs.has(path);
|
|
213
220
|
const ctx = createContext({
|
|
214
221
|
isProject: true,
|
|
215
222
|
cwd: "/tmp/test-project",
|
|
216
223
|
installedPackages: ["opentasks", "minimem", "cognitive-core"],
|
|
217
|
-
exists:
|
|
224
|
+
exists: checkDir,
|
|
225
|
+
hasContent: checkDir,
|
|
218
226
|
});
|
|
219
227
|
const results = checkProjectConfigs(ctx);
|
|
220
228
|
expect(results).toHaveLength(3);
|
|
@@ -222,6 +230,18 @@ describe("checkProjectConfigs", () => {
|
|
|
222
230
|
expect(results[1].status).toBe("pass"); // minimem (flat)
|
|
223
231
|
expect(results[2].status).toBe("warn"); // cognitive-core (missing)
|
|
224
232
|
});
|
|
233
|
+
it("warns with 'empty' message when directory exists but has no content", () => {
|
|
234
|
+
const ctx = createContext({
|
|
235
|
+
isProject: true,
|
|
236
|
+
installedPackages: ["opentasks"],
|
|
237
|
+
exists: (path) => path.endsWith(".swarm/opentasks"),
|
|
238
|
+
hasContent: () => false,
|
|
239
|
+
});
|
|
240
|
+
const results = checkProjectConfigs(ctx);
|
|
241
|
+
expect(results).toHaveLength(1);
|
|
242
|
+
expect(results[0].status).toBe("warn");
|
|
243
|
+
expect(results[0].message).toContain("empty");
|
|
244
|
+
});
|
|
225
245
|
});
|
|
226
246
|
describe("checkIntegrations", () => {
|
|
227
247
|
it("passes when both packages in an integration are installed", async () => {
|
|
@@ -283,6 +303,7 @@ describe("runAllChecks", () => {
|
|
|
283
303
|
env: { HOME: "/home/testuser" },
|
|
284
304
|
getInstalledVersion: async () => "0.1.0",
|
|
285
305
|
exists: () => true,
|
|
306
|
+
hasContent: () => true,
|
|
286
307
|
});
|
|
287
308
|
const report = await runAllChecks(ctx);
|
|
288
309
|
expect(report.packages).toHaveLength(2);
|
package/dist/doctor/types.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface CheckContext {
|
|
|
24
24
|
getInstalledVersion: (pkg: string) => Promise<string | null>;
|
|
25
25
|
/** Check if a file/directory exists — injectable for testing */
|
|
26
26
|
exists: (path: string) => boolean;
|
|
27
|
+
/** Check if a directory has files inside — injectable for testing */
|
|
28
|
+
hasContent?: (path: string) => boolean;
|
|
27
29
|
/** Environment variables */
|
|
28
30
|
env: Record<string, string | undefined>;
|
|
29
31
|
}
|
package/dist/packages/setup.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync,
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
4
4
|
import { basename, join } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
const execFileAsync = promisify(execFile);
|
|
@@ -51,8 +51,8 @@ export const PROJECT_INIT_ORDER = [
|
|
|
51
51
|
export function isProjectInit(cwd, pkg) {
|
|
52
52
|
const prefixed = PROJECT_CONFIG_DIRS[pkg];
|
|
53
53
|
const flat = FLAT_PROJECT_CONFIG_DIRS[pkg];
|
|
54
|
-
return (prefixed ?
|
|
55
|
-
(flat ?
|
|
54
|
+
return (prefixed ? hasContent(join(cwd, prefixed)) : false) ||
|
|
55
|
+
(flat ? hasContent(join(cwd, flat)) : false);
|
|
56
56
|
}
|
|
57
57
|
/** Ensure the .swarm/ project root directory exists */
|
|
58
58
|
function ensureProjectRoot(cwd) {
|
|
@@ -63,9 +63,9 @@ function ensureProjectRoot(cwd) {
|
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
65
|
* After init, relocate a package's config dir into .swarm/ if the CLI
|
|
66
|
-
* created it at the legacy top-level location
|
|
67
|
-
*
|
|
68
|
-
*
|
|
66
|
+
* created it at the legacy top-level location.
|
|
67
|
+
* All supported packages natively check .swarm/<pkg> before .<pkg>,
|
|
68
|
+
* so no symlink is needed.
|
|
69
69
|
*/
|
|
70
70
|
function relocate(cwd, legacyName, targetName) {
|
|
71
71
|
const src = join(cwd, legacyName);
|
|
@@ -76,26 +76,9 @@ function relocate(cwd, legacyName, targetName) {
|
|
|
76
76
|
renameSync(src, dest);
|
|
77
77
|
}
|
|
78
78
|
catch {
|
|
79
|
-
|
|
79
|
+
// Non-fatal — leave in legacy location
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
-
// Create a symlink at the legacy location so packages find their data
|
|
83
|
-
ensureSymlink(cwd, legacyName, targetName);
|
|
84
|
-
}
|
|
85
|
-
/** Create a relative symlink: cwd/<legacyName> → .swarm/<targetName> */
|
|
86
|
-
function ensureSymlink(cwd, legacyName, targetName) {
|
|
87
|
-
const link = join(cwd, legacyName);
|
|
88
|
-
const target = join(PROJECT_ROOT, targetName); // relative path
|
|
89
|
-
if (isSymlink(link))
|
|
90
|
-
return; // Already symlinked
|
|
91
|
-
if (existsSync(link))
|
|
92
|
-
return; // Real directory exists — don't overwrite
|
|
93
|
-
try {
|
|
94
|
-
symlinkSync(target, link);
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
// Non-fatal — package can still be configured via env var
|
|
98
|
-
}
|
|
99
82
|
}
|
|
100
83
|
function isSymlink(path) {
|
|
101
84
|
try {
|
|
@@ -119,6 +102,10 @@ export async function initProjectPackage(pkg, ctx) {
|
|
|
119
102
|
// for packages that create directly in .swarm/, this is a no-op.
|
|
120
103
|
if (ctx.usePrefix)
|
|
121
104
|
relocate(ctx.cwd, ".opentasks", "opentasks");
|
|
105
|
+
// Older opentasks versions write .gitattributes at the project root.
|
|
106
|
+
// Newer versions write it inside the opentasks dir. Clean up the
|
|
107
|
+
// root-level file as a backwards-compat fallback.
|
|
108
|
+
cleanupGitattributes(ctx.cwd);
|
|
122
109
|
return result;
|
|
123
110
|
}
|
|
124
111
|
case "minimem":
|
|
@@ -156,7 +143,7 @@ export function isGlobalInit(pkg) {
|
|
|
156
143
|
const dir = GLOBAL_CONFIG_DIRS[pkg];
|
|
157
144
|
if (!dir)
|
|
158
145
|
return false;
|
|
159
|
-
return
|
|
146
|
+
return hasContent(join(homedir(), dir));
|
|
160
147
|
}
|
|
161
148
|
/** Initialize a single global package */
|
|
162
149
|
export async function initGlobalPackage(pkg, ctx, openhiveOpts) {
|
|
@@ -206,40 +193,52 @@ async function shellInit(command, args, cwd, envOverrides) {
|
|
|
206
193
|
}
|
|
207
194
|
async function initMinimem(ctx) {
|
|
208
195
|
const targetDir = ctx.usePrefix
|
|
209
|
-
? join(
|
|
210
|
-
:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
196
|
+
? join(PROJECT_ROOT, "minimem")
|
|
197
|
+
: ".minimem";
|
|
198
|
+
const absTarget = join(ctx.cwd, targetDir);
|
|
199
|
+
// Try CLI init first (creates contained layout in newer versions)
|
|
200
|
+
const result = await shellInit("minimem", ["init", targetDir], ctx.cwd);
|
|
201
|
+
// Verify the CLI created the contained layout (config.json at root).
|
|
202
|
+
// Older minimem versions create a nested .minimem/ subdir instead.
|
|
203
|
+
if (!existsSync(join(absTarget, "config.json"))) {
|
|
204
|
+
initMinimemInline(absTarget);
|
|
205
|
+
}
|
|
206
|
+
// Patch embedding provider if the wizard chose one
|
|
207
|
+
const provider = ctx.embeddingProvider && ctx.embeddingProvider !== "local"
|
|
208
|
+
? ctx.embeddingProvider
|
|
209
|
+
: null;
|
|
210
|
+
if (provider) {
|
|
211
|
+
const configPath = join(absTarget, "config.json");
|
|
212
|
+
try {
|
|
213
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
214
|
+
config.embedding = { ...config.embedding, provider };
|
|
215
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
223
216
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const configPath = join(targetDir, "config.json");
|
|
227
|
-
if (existsSync(configPath)) {
|
|
228
|
-
try {
|
|
229
|
-
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
230
|
-
config.embedding = config.embedding || {};
|
|
231
|
-
config.embedding.provider = ctx.embeddingProvider;
|
|
232
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
233
|
-
}
|
|
234
|
-
catch {
|
|
235
|
-
// Non-fatal — user can configure manually
|
|
236
|
-
}
|
|
237
|
-
}
|
|
217
|
+
catch {
|
|
218
|
+
// Non-fatal
|
|
238
219
|
}
|
|
239
|
-
return { package: "minimem", success: true };
|
|
240
220
|
}
|
|
241
|
-
|
|
242
|
-
|
|
221
|
+
return { package: "minimem", success: true };
|
|
222
|
+
}
|
|
223
|
+
/** Fallback: create contained layout inline when CLI is unavailable or outdated */
|
|
224
|
+
function initMinimemInline(targetDir) {
|
|
225
|
+
mkdirSync(targetDir, { recursive: true });
|
|
226
|
+
mkdirSync(join(targetDir, "memory"), { recursive: true });
|
|
227
|
+
const configPath = join(targetDir, "config.json");
|
|
228
|
+
if (!existsSync(configPath)) {
|
|
229
|
+
writeFileSync(configPath, JSON.stringify({
|
|
230
|
+
embedding: { provider: "auto" },
|
|
231
|
+
hybrid: { enabled: true, vectorWeight: 0.7, textWeight: 0.3 },
|
|
232
|
+
query: { maxResults: 10, minScore: 0.3 },
|
|
233
|
+
}, null, 2) + "\n");
|
|
234
|
+
}
|
|
235
|
+
const memoryPath = join(targetDir, "MEMORY.md");
|
|
236
|
+
if (!existsSync(memoryPath)) {
|
|
237
|
+
writeFileSync(memoryPath, "# Memory\n\nAdd notes, decisions, and context here.\n");
|
|
238
|
+
}
|
|
239
|
+
const gitignorePath = join(targetDir, ".gitignore");
|
|
240
|
+
if (!existsSync(gitignorePath)) {
|
|
241
|
+
writeFileSync(gitignorePath, "index.db\nindex.db-*\n");
|
|
243
242
|
}
|
|
244
243
|
}
|
|
245
244
|
async function initSdr(ctx) {
|
|
@@ -260,6 +259,12 @@ async function initSkillTreeProject(ctx) {
|
|
|
260
259
|
: join(ctx.cwd, ".skilltree");
|
|
261
260
|
mkdirSync(targetDir, { recursive: true });
|
|
262
261
|
mkdirSync(join(targetDir, "skills"), { recursive: true });
|
|
262
|
+
mkdirSync(join(targetDir, ".cache"), { recursive: true });
|
|
263
|
+
// Gitignore the cache directory
|
|
264
|
+
const gitignorePath = join(targetDir, ".gitignore");
|
|
265
|
+
if (!existsSync(gitignorePath)) {
|
|
266
|
+
writeFileSync(gitignorePath, ".cache/\n");
|
|
267
|
+
}
|
|
263
268
|
return { package: "skill-tree", success: true };
|
|
264
269
|
}
|
|
265
270
|
catch (err) {
|
|
@@ -271,15 +276,24 @@ async function initSkillTreeProject(ctx) {
|
|
|
271
276
|
}
|
|
272
277
|
}
|
|
273
278
|
async function initOpenteamsProject(ctx) {
|
|
279
|
+
const targetDir = ctx.usePrefix
|
|
280
|
+
? join(ctx.cwd, PROJECT_ROOT, "openteams")
|
|
281
|
+
: join(ctx.cwd, ".openteams");
|
|
282
|
+
// Try CLI first — `openteams template init` creates config.json with proper defaults
|
|
283
|
+
const result = await shellInit("openteams", ["template", "init", "-d", ctx.cwd], ctx.cwd, ctx.usePrefix
|
|
284
|
+
? { OPENTEAMS_PROJECT_DIR: join(PROJECT_ROOT, "openteams") }
|
|
285
|
+
: undefined);
|
|
286
|
+
if (result.success) {
|
|
287
|
+
if (ctx.usePrefix)
|
|
288
|
+
relocate(ctx.cwd, ".openteams", "openteams");
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
// Fallback: create config inline if CLI is not available
|
|
274
292
|
try {
|
|
275
|
-
const targetDir = ctx.usePrefix
|
|
276
|
-
? join(ctx.cwd, PROJECT_ROOT, "openteams")
|
|
277
|
-
: join(ctx.cwd, ".openteams");
|
|
278
293
|
mkdirSync(targetDir, { recursive: true });
|
|
279
|
-
// Write default config.json (matches `openteams template init` with no options)
|
|
280
294
|
const configPath = join(targetDir, "config.json");
|
|
281
295
|
if (!existsSync(configPath)) {
|
|
282
|
-
writeFileSync(configPath, JSON.stringify({}, null, 2) + "\n");
|
|
296
|
+
writeFileSync(configPath, JSON.stringify({ defaults: {} }, null, 2) + "\n");
|
|
283
297
|
}
|
|
284
298
|
return { package: "openteams", success: true };
|
|
285
299
|
}
|
|
@@ -300,7 +314,14 @@ async function initSessionlogProject(ctx) {
|
|
|
300
314
|
// Write default settings.json
|
|
301
315
|
const settingsPath = join(targetDir, "settings.json");
|
|
302
316
|
if (!existsSync(settingsPath)) {
|
|
303
|
-
|
|
317
|
+
const defaultSettings = {
|
|
318
|
+
enabled: false,
|
|
319
|
+
strategy: "manual-commit",
|
|
320
|
+
logLevel: "warn",
|
|
321
|
+
telemetryEnabled: false,
|
|
322
|
+
summarizationEnabled: false,
|
|
323
|
+
};
|
|
324
|
+
writeFileSync(settingsPath, JSON.stringify(defaultSettings, null, 2) + "\n");
|
|
304
325
|
}
|
|
305
326
|
return { package: "sessionlog", success: true };
|
|
306
327
|
}
|
|
@@ -371,10 +392,24 @@ async function initClaudeSwarmProject(ctx) {
|
|
|
371
392
|
? join(ctx.cwd, PROJECT_ROOT, "claude-swarm")
|
|
372
393
|
: join(ctx.cwd, ".claude-swarm");
|
|
373
394
|
mkdirSync(targetDir, { recursive: true });
|
|
374
|
-
// Write default config.json
|
|
395
|
+
// Write default config.json with meaningful defaults
|
|
375
396
|
const configPath = join(targetDir, "config.json");
|
|
376
397
|
if (!existsSync(configPath)) {
|
|
377
|
-
|
|
398
|
+
const defaultConfig = {
|
|
399
|
+
template: "",
|
|
400
|
+
map: {
|
|
401
|
+
enabled: false,
|
|
402
|
+
server: "ws://localhost:8080",
|
|
403
|
+
scope: "",
|
|
404
|
+
systemId: "system-claude-swarm",
|
|
405
|
+
sidecar: "session",
|
|
406
|
+
},
|
|
407
|
+
sessionlog: {
|
|
408
|
+
enabled: false,
|
|
409
|
+
sync: "off",
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + "\n");
|
|
378
413
|
}
|
|
379
414
|
// Write .gitignore for tmp/
|
|
380
415
|
const gitignorePath = join(targetDir, ".gitignore");
|
|
@@ -392,6 +427,51 @@ async function initClaudeSwarmProject(ctx) {
|
|
|
392
427
|
}
|
|
393
428
|
}
|
|
394
429
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
430
|
+
/** Check if a directory exists and contains at least one file (recursively) */
|
|
431
|
+
function hasContent(dir) {
|
|
432
|
+
if (!existsSync(dir))
|
|
433
|
+
return false;
|
|
434
|
+
try {
|
|
435
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
436
|
+
for (const entry of entries) {
|
|
437
|
+
if (entry.isFile() || entry.isSymbolicLink())
|
|
438
|
+
return true;
|
|
439
|
+
if (entry.isDirectory() && hasContent(join(dir, entry.name)))
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Remove root-level .gitattributes created by older opentasks versions.
|
|
450
|
+
* Newer versions write .gitattributes inside the opentasks directory.
|
|
451
|
+
*/
|
|
452
|
+
function cleanupGitattributes(cwd) {
|
|
453
|
+
const attrPath = join(cwd, ".gitattributes");
|
|
454
|
+
if (!existsSync(attrPath))
|
|
455
|
+
return;
|
|
456
|
+
try {
|
|
457
|
+
const content = readFileSync(attrPath, "utf-8");
|
|
458
|
+
const cleaned = content
|
|
459
|
+
.split("\n")
|
|
460
|
+
.filter((line) => !line.includes("merge=opentasks") &&
|
|
461
|
+
!line.includes("OpenTasks merge driver"))
|
|
462
|
+
.join("\n")
|
|
463
|
+
.trim();
|
|
464
|
+
if (!cleaned) {
|
|
465
|
+
unlinkSync(attrPath);
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
writeFileSync(attrPath, cleaned + "\n");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
// Non-fatal
|
|
473
|
+
}
|
|
474
|
+
}
|
|
395
475
|
function getProjectName(cwd) {
|
|
396
476
|
const pkgPath = join(cwd, "package.json");
|
|
397
477
|
if (existsSync(pkgPath)) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdirSync, rmSync, existsSync,
|
|
2
|
+
import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync, realpathSync, } from "node:fs";
|
|
3
3
|
import { join, basename } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
@@ -105,12 +105,20 @@ describe("isProjectInit", () => {
|
|
|
105
105
|
expect(isProjectInit(testDir, pkg)).toBe(false);
|
|
106
106
|
}
|
|
107
107
|
});
|
|
108
|
-
it.each(Object.entries(PROJECT_CONFIG_DIRS))("returns true for %s when %s/ exists (prefixed)", (pkg, configDir) => {
|
|
109
|
-
|
|
108
|
+
it.each(Object.entries(PROJECT_CONFIG_DIRS))("returns true for %s when %s/ exists with content (prefixed)", (pkg, configDir) => {
|
|
109
|
+
const dir = join(testDir, configDir);
|
|
110
|
+
mkdirSync(dir, { recursive: true });
|
|
111
|
+
writeFileSync(join(dir, "config.json"), "{}");
|
|
110
112
|
expect(isProjectInit(testDir, pkg)).toBe(true);
|
|
111
113
|
});
|
|
112
|
-
it.each(Object.entries(
|
|
114
|
+
it.each(Object.entries(PROJECT_CONFIG_DIRS))("returns false for %s when %s/ exists but is empty (prefixed)", (pkg, configDir) => {
|
|
113
115
|
mkdirSync(join(testDir, configDir), { recursive: true });
|
|
116
|
+
expect(isProjectInit(testDir, pkg)).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
it.each(Object.entries(FLAT_PROJECT_CONFIG_DIRS))("returns true for %s when %s/ exists with content (flat)", (pkg, configDir) => {
|
|
119
|
+
const dir = join(testDir, configDir);
|
|
120
|
+
mkdirSync(dir, { recursive: true });
|
|
121
|
+
writeFileSync(join(dir, "config.json"), "{}");
|
|
114
122
|
expect(isProjectInit(testDir, pkg)).toBe(true);
|
|
115
123
|
});
|
|
116
124
|
it("returns false for unknown package", () => {
|
|
@@ -127,10 +135,16 @@ describe("isGlobalInit", () => {
|
|
|
127
135
|
expect(isGlobalInit(pkg)).toBe(false);
|
|
128
136
|
}
|
|
129
137
|
});
|
|
130
|
-
it.each(Object.entries(GLOBAL_CONFIG_DIRS))("returns true for %s when ~/%s/ exists", (pkg, configDir) => {
|
|
131
|
-
|
|
138
|
+
it.each(Object.entries(GLOBAL_CONFIG_DIRS))("returns true for %s when ~/%s/ exists with content", (pkg, configDir) => {
|
|
139
|
+
const dir = join(testHome, configDir);
|
|
140
|
+
mkdirSync(dir, { recursive: true });
|
|
141
|
+
writeFileSync(join(dir, "config.json"), "{}");
|
|
132
142
|
expect(isGlobalInit(pkg)).toBe(true);
|
|
133
143
|
});
|
|
144
|
+
it.each(Object.entries(GLOBAL_CONFIG_DIRS))("returns false for %s when ~/%s/ exists but is empty", (pkg, configDir) => {
|
|
145
|
+
mkdirSync(join(testHome, configDir), { recursive: true });
|
|
146
|
+
expect(isGlobalInit(pkg)).toBe(false);
|
|
147
|
+
});
|
|
134
148
|
it("returns false for unknown package", () => {
|
|
135
149
|
expect(isGlobalInit("nonexistent")).toBe(false);
|
|
136
150
|
});
|
|
@@ -138,7 +152,7 @@ describe("isGlobalInit", () => {
|
|
|
138
152
|
// ─── Project: opentasks (real CLI) ───────────────────────────────────────────
|
|
139
153
|
describe("initProjectPackage — opentasks (real CLI)", async () => {
|
|
140
154
|
const installed = await hasCliInstalled("opentasks");
|
|
141
|
-
it.skipIf(!installed)("runs opentasks init
|
|
155
|
+
it.skipIf(!installed)("runs opentasks init and relocates to .swarm/", async () => {
|
|
142
156
|
createProject("test-opentasks");
|
|
143
157
|
const result = await initProjectPackage("opentasks", projectCtx({ cwd: testDir, packages: ["opentasks"] }));
|
|
144
158
|
expect(result.success).toBe(true);
|
|
@@ -148,12 +162,10 @@ describe("initProjectPackage — opentasks (real CLI)", async () => {
|
|
|
148
162
|
expect(existsSync(join(testDir, ".swarm", "opentasks", "config.json"))).toBe(true);
|
|
149
163
|
expect(existsSync(join(testDir, ".swarm", "opentasks", "graph.jsonl"))).toBe(true);
|
|
150
164
|
expect(existsSync(join(testDir, ".swarm", "opentasks", ".gitignore"))).toBe(true);
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
expect(
|
|
155
|
-
// Accessible via symlink
|
|
156
|
-
expect(existsSync(join(testDir, ".opentasks", "config.json"))).toBe(true);
|
|
165
|
+
// No symlink at legacy location (packages natively check .swarm/)
|
|
166
|
+
expect(existsSync(join(testDir, ".opentasks"))).toBe(false);
|
|
167
|
+
// .gitattributes should be cleaned up (merge driver creates it at root)
|
|
168
|
+
expect(existsSync(join(testDir, ".gitattributes"))).toBe(false);
|
|
157
169
|
// Verify config content
|
|
158
170
|
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "opentasks", "config.json"), "utf-8"));
|
|
159
171
|
expect(config.version).toBe("1.0");
|
|
@@ -175,24 +187,30 @@ describe("initProjectPackage — opentasks (real CLI)", async () => {
|
|
|
175
187
|
expect(config.location.name).toBe(basename(testDir));
|
|
176
188
|
});
|
|
177
189
|
});
|
|
178
|
-
// ─── Project: minimem (
|
|
179
|
-
describe("initProjectPackage — minimem
|
|
180
|
-
|
|
181
|
-
it.skipIf(!installed)("runs minimem init and creates .minimem/", async () => {
|
|
190
|
+
// ─── Project: minimem (CLI with inline fallback) ─────────────────────────────
|
|
191
|
+
describe("initProjectPackage — minimem", () => {
|
|
192
|
+
it("creates all artifacts inside .swarm/minimem/", async () => {
|
|
182
193
|
createProject("test-minimem");
|
|
183
194
|
const result = await initProjectPackage("minimem", projectCtx({ cwd: testDir, packages: ["minimem"] }));
|
|
184
195
|
expect(result.success).toBe(true);
|
|
185
|
-
//
|
|
196
|
+
// Core artifacts inside .swarm/minimem/
|
|
186
197
|
expect(existsSync(join(testDir, ".swarm", "minimem"))).toBe(true);
|
|
187
198
|
expect(existsSync(join(testDir, ".swarm", "minimem", "config.json"))).toBe(true);
|
|
188
199
|
expect(existsSync(join(testDir, ".swarm", "minimem", ".gitignore"))).toBe(true);
|
|
189
|
-
expect(existsSync(join(testDir, "MEMORY.md"))).toBe(true);
|
|
200
|
+
expect(existsSync(join(testDir, ".swarm", "minimem", "MEMORY.md"))).toBe(true);
|
|
201
|
+
expect(existsSync(join(testDir, ".swarm", "minimem", "memory"))).toBe(true);
|
|
202
|
+
// Nothing at project root
|
|
203
|
+
expect(existsSync(join(testDir, "MEMORY.md"))).toBe(false);
|
|
204
|
+
expect(existsSync(join(testDir, "memory"))).toBe(false);
|
|
190
205
|
// Verify default config
|
|
191
206
|
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
192
207
|
expect(config.embedding.provider).toBe("auto");
|
|
193
208
|
expect(config.hybrid.enabled).toBe(true);
|
|
209
|
+
// Verify .gitignore content
|
|
210
|
+
const gitignore = readFileSync(join(testDir, ".swarm", "minimem", ".gitignore"), "utf-8");
|
|
211
|
+
expect(gitignore).toContain("index.db");
|
|
194
212
|
});
|
|
195
|
-
it
|
|
213
|
+
it("sets embedding.provider to openai", async () => {
|
|
196
214
|
createProject("test-embed");
|
|
197
215
|
await initProjectPackage("minimem", projectCtx({
|
|
198
216
|
cwd: testDir,
|
|
@@ -201,11 +219,8 @@ describe("initProjectPackage — minimem (real CLI)", async () => {
|
|
|
201
219
|
}));
|
|
202
220
|
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
203
221
|
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
222
|
});
|
|
208
|
-
it
|
|
223
|
+
it("sets embedding.provider to gemini", async () => {
|
|
209
224
|
createProject("test-embed-gemini");
|
|
210
225
|
await initProjectPackage("minimem", projectCtx({
|
|
211
226
|
cwd: testDir,
|
|
@@ -215,7 +230,7 @@ describe("initProjectPackage — minimem (real CLI)", async () => {
|
|
|
215
230
|
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
216
231
|
expect(config.embedding.provider).toBe("gemini");
|
|
217
232
|
});
|
|
218
|
-
it
|
|
233
|
+
it("uses 'auto' when provider is local", async () => {
|
|
219
234
|
createProject("test-local");
|
|
220
235
|
await initProjectPackage("minimem", projectCtx({
|
|
221
236
|
cwd: testDir,
|
|
@@ -225,7 +240,7 @@ describe("initProjectPackage — minimem (real CLI)", async () => {
|
|
|
225
240
|
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
226
241
|
expect(config.embedding.provider).toBe("auto");
|
|
227
242
|
});
|
|
228
|
-
it
|
|
243
|
+
it("uses 'auto' when provider is null", async () => {
|
|
229
244
|
createProject("test-null");
|
|
230
245
|
await initProjectPackage("minimem", projectCtx({
|
|
231
246
|
cwd: testDir,
|
|
@@ -276,12 +291,17 @@ describe("initProjectPackage — skill-tree", () => {
|
|
|
276
291
|
expect(result.package).toBe("skill-tree");
|
|
277
292
|
expect(existsSync(join(testDir, ".swarm", "skilltree"))).toBe(true);
|
|
278
293
|
expect(existsSync(join(testDir, ".swarm", "skilltree", "skills"))).toBe(true);
|
|
294
|
+
expect(existsSync(join(testDir, ".swarm", "skilltree", ".cache"))).toBe(true);
|
|
295
|
+
expect(existsSync(join(testDir, ".swarm", "skilltree", ".gitignore"))).toBe(true);
|
|
296
|
+
expect(readFileSync(join(testDir, ".swarm", "skilltree", ".gitignore"), "utf-8")).toBe(".cache/\n");
|
|
279
297
|
});
|
|
280
298
|
it("creates .skilltree/ with skills/ subdirectory (flat)", async () => {
|
|
281
299
|
const result = await initProjectPackage("skill-tree", projectCtx({ cwd: testDir, packages: ["skill-tree"], usePrefix: false }));
|
|
282
300
|
expect(result.success).toBe(true);
|
|
283
301
|
expect(existsSync(join(testDir, ".skilltree"))).toBe(true);
|
|
284
302
|
expect(existsSync(join(testDir, ".skilltree", "skills"))).toBe(true);
|
|
303
|
+
expect(existsSync(join(testDir, ".skilltree", ".cache"))).toBe(true);
|
|
304
|
+
expect(existsSync(join(testDir, ".skilltree", ".gitignore"))).toBe(true);
|
|
285
305
|
});
|
|
286
306
|
it("is idempotent", async () => {
|
|
287
307
|
await initProjectPackage("skill-tree", projectCtx({ cwd: testDir, packages: ["skill-tree"] }));
|
|
@@ -298,6 +318,7 @@ describe("initProjectPackage — openteams", () => {
|
|
|
298
318
|
expect(existsSync(join(testDir, ".swarm", "openteams"))).toBe(true);
|
|
299
319
|
expect(existsSync(join(testDir, ".swarm", "openteams", "config.json"))).toBe(true);
|
|
300
320
|
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "openteams", "config.json"), "utf-8"));
|
|
321
|
+
// openteams CLI writes {} by default (all built-in templates active)
|
|
301
322
|
expect(config).toEqual({});
|
|
302
323
|
});
|
|
303
324
|
it("creates .openteams/ with config.json (flat)", async () => {
|
|
@@ -329,8 +350,13 @@ describe("initProjectPackage — sessionlog", () => {
|
|
|
329
350
|
expect(existsSync(join(testDir, ".swarm", "sessionlog"))).toBe(true);
|
|
330
351
|
expect(existsSync(join(testDir, ".swarm", "sessionlog", "settings.json"))).toBe(true);
|
|
331
352
|
const settings = JSON.parse(readFileSync(join(testDir, ".swarm", "sessionlog", "settings.json"), "utf-8"));
|
|
332
|
-
expect(settings
|
|
333
|
-
|
|
353
|
+
expect(settings).toEqual({
|
|
354
|
+
enabled: false,
|
|
355
|
+
strategy: "manual-commit",
|
|
356
|
+
logLevel: "warn",
|
|
357
|
+
telemetryEnabled: false,
|
|
358
|
+
summarizationEnabled: false,
|
|
359
|
+
});
|
|
334
360
|
});
|
|
335
361
|
it("creates .sessionlog/ with settings.json (flat)", async () => {
|
|
336
362
|
const result = await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"], usePrefix: false }));
|
|
@@ -363,7 +389,20 @@ describe("initProjectPackage — claude-code-swarm", () => {
|
|
|
363
389
|
expect(existsSync(join(testDir, ".swarm", "claude-swarm", "config.json"))).toBe(true);
|
|
364
390
|
expect(existsSync(join(testDir, ".swarm", "claude-swarm", ".gitignore"))).toBe(true);
|
|
365
391
|
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "claude-swarm", "config.json"), "utf-8"));
|
|
366
|
-
expect(config).toEqual({
|
|
392
|
+
expect(config).toEqual({
|
|
393
|
+
template: "",
|
|
394
|
+
map: {
|
|
395
|
+
enabled: false,
|
|
396
|
+
server: "ws://localhost:8080",
|
|
397
|
+
scope: "",
|
|
398
|
+
systemId: "system-claude-swarm",
|
|
399
|
+
sidecar: "session",
|
|
400
|
+
},
|
|
401
|
+
sessionlog: {
|
|
402
|
+
enabled: false,
|
|
403
|
+
sync: "off",
|
|
404
|
+
},
|
|
405
|
+
});
|
|
367
406
|
const gitignore = readFileSync(join(testDir, ".swarm", "claude-swarm", ".gitignore"), "utf-8");
|
|
368
407
|
expect(gitignore).toBe("tmp/\n");
|
|
369
408
|
});
|
|
@@ -417,7 +456,7 @@ describe("initGlobalPackage — skill-tree (real CLI)", async () => {
|
|
|
417
456
|
expect(existsSync(configPath)).toBe(true);
|
|
418
457
|
const yaml = readFileSync(configPath, "utf-8");
|
|
419
458
|
expect(yaml).toContain("storage:");
|
|
420
|
-
expect(yaml).toContain("sqlite");
|
|
459
|
+
expect(yaml.toLowerCase()).toContain("sqlite");
|
|
421
460
|
expect(yaml).toContain("indexer:");
|
|
422
461
|
});
|
|
423
462
|
it.skipIf(!installed)("skips when config.yaml already exists", async () => {
|
|
@@ -514,8 +553,7 @@ describe("initGlobalPackage — unknown", () => {
|
|
|
514
553
|
// ─── E2E: full project init flow (solo bundle) ──────────────────────────────
|
|
515
554
|
describe("e2e: project init — solo bundle", async () => {
|
|
516
555
|
const opentasksOk = await hasCliInstalled("opentasks");
|
|
517
|
-
const
|
|
518
|
-
const allOk = opentasksOk && minimemOk;
|
|
556
|
+
const allOk = opentasksOk; // minimem init is inline (no CLI needed)
|
|
519
557
|
it.skipIf(!allOk)("initializes opentasks → minimem with cross-wiring", async () => {
|
|
520
558
|
createProject("solo-e2e");
|
|
521
559
|
const ctx = projectCtx({
|
|
@@ -576,9 +614,8 @@ describe("e2e: project init — solo bundle", async () => {
|
|
|
576
614
|
// ─── E2E: full project init flow (team bundle — init order) ─────────────────
|
|
577
615
|
describe("e2e: project init — team bundle (init order)", async () => {
|
|
578
616
|
const opentasksOk = await hasCliInstalled("opentasks");
|
|
579
|
-
const minimemOk = await hasCliInstalled("minimem");
|
|
580
617
|
const ccOk = await hasCliInstalled("cognitive-core");
|
|
581
|
-
const allOk = opentasksOk &&
|
|
618
|
+
const allOk = opentasksOk && ccOk; // minimem init is inline (no CLI needed)
|
|
582
619
|
it.skipIf(!allOk)("cognitive-core runs after minimem (can detect .minimem/)", async () => {
|
|
583
620
|
createProject("team-e2e");
|
|
584
621
|
const ctx = projectCtx({
|
|
@@ -699,7 +736,20 @@ describe("flow: plugin bootstrap — init project dirs via swarmkit", () => {
|
|
|
699
736
|
expect(existsSync(join(cwd, ".swarm", "claude-swarm", "config.json"))).toBe(true);
|
|
700
737
|
expect(existsSync(join(cwd, ".swarm", "claude-swarm", ".gitignore"))).toBe(true);
|
|
701
738
|
const config = JSON.parse(readFileSync(join(cwd, ".swarm", "claude-swarm", "config.json"), "utf-8"));
|
|
702
|
-
expect(config).toEqual({
|
|
739
|
+
expect(config).toEqual({
|
|
740
|
+
template: "",
|
|
741
|
+
map: {
|
|
742
|
+
enabled: false,
|
|
743
|
+
server: "ws://localhost:8080",
|
|
744
|
+
scope: "",
|
|
745
|
+
systemId: "system-claude-swarm",
|
|
746
|
+
sidecar: "session",
|
|
747
|
+
},
|
|
748
|
+
sessionlog: {
|
|
749
|
+
enabled: false,
|
|
750
|
+
sync: "off",
|
|
751
|
+
},
|
|
752
|
+
});
|
|
703
753
|
const gitignore = readFileSync(join(cwd, ".swarm", "claude-swarm", ".gitignore"), "utf-8");
|
|
704
754
|
expect(gitignore).toBe("tmp/\n");
|
|
705
755
|
// isProjectInit now returns true for both
|
|
@@ -734,8 +784,13 @@ describe("flow: plugin bootstrap — init project dirs via swarmkit", () => {
|
|
|
734
784
|
expect(existsSync(join(cwd, ".swarm", "claude-swarm"))).toBe(true);
|
|
735
785
|
// sessionlog has default settings
|
|
736
786
|
const settings = JSON.parse(readFileSync(join(cwd, ".swarm", "sessionlog", "settings.json"), "utf-8"));
|
|
737
|
-
expect(settings
|
|
738
|
-
|
|
787
|
+
expect(settings).toEqual({
|
|
788
|
+
enabled: false,
|
|
789
|
+
strategy: "manual-commit",
|
|
790
|
+
logLevel: "warn",
|
|
791
|
+
telemetryEnabled: false,
|
|
792
|
+
summarizationEnabled: false,
|
|
793
|
+
});
|
|
739
794
|
});
|
|
740
795
|
it("skips already-initialized packages on re-run (idempotent bootstrap)", async () => {
|
|
741
796
|
const cwd = testDir;
|
|
@@ -789,9 +844,8 @@ describe("flow: plugin bootstrap — init project dirs via swarmkit", () => {
|
|
|
789
844
|
});
|
|
790
845
|
});
|
|
791
846
|
// ─── E2E: embedding provider propagation ─────────────────────────────────────
|
|
792
|
-
describe("e2e: embedding provider propagation",
|
|
793
|
-
|
|
794
|
-
it.skipIf(!minimemOk)("openai embedding flows through to real minimem config", async () => {
|
|
847
|
+
describe("e2e: embedding provider propagation", () => {
|
|
848
|
+
it("openai embedding flows through to minimem config", async () => {
|
|
795
849
|
createProject("embed-e2e");
|
|
796
850
|
await initProjectPackage("minimem", projectCtx({
|
|
797
851
|
cwd: testDir,
|
|
@@ -801,7 +855,6 @@ describe("e2e: embedding provider propagation", async () => {
|
|
|
801
855
|
}));
|
|
802
856
|
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
803
857
|
expect(config.embedding.provider).toBe("openai");
|
|
804
|
-
// Real minimem config has these fields too
|
|
805
858
|
expect(config.hybrid).toBeDefined();
|
|
806
859
|
expect(config.query).toBeDefined();
|
|
807
860
|
});
|