swarmkit 0.0.3 → 0.0.5

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.
@@ -1,4 +1,5 @@
1
1
  export interface WizardOptions {
2
2
  noPrefix?: boolean;
3
+ forceGlobal?: boolean;
3
4
  }
4
5
  export declare function runWizard(opts?: WizardOptions): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  import chalk from "chalk";
2
- import { select } from "@inquirer/prompts";
3
- import { isFirstRun, readConfig, writeConfig, ensureConfigDir } from "../../config/global.js";
2
+ import { select, confirm } from "@inquirer/prompts";
3
+ import { isFirstRun, readConfig, writeConfig, ensureConfigDir, isConfigOutdated, getSwarmkitVersion } from "../../config/global.js";
4
4
  import * as ui from "../../utils/ui.js";
5
5
  import { getActiveIntegrations } from "../../packages/registry.js";
6
6
  import { isClaudeCliAvailable } from "../../packages/installer.js";
@@ -15,11 +15,28 @@ export async function runWizard(opts) {
15
15
  console.log();
16
16
  console.log(` ${chalk.bold("swarmkit")} ${chalk.dim("— multi-agent infrastructure toolkit")}`);
17
17
  console.log();
18
- if (isFirstRun()) {
18
+ if (isFirstRun() || opts?.forceGlobal) {
19
19
  await runFirstTimeSetup(opts);
20
20
  }
21
21
  else {
22
- await runProjectSetup(opts);
22
+ const config = readConfig();
23
+ if (isConfigOutdated(config)) {
24
+ ui.warn(`Global config was created with swarmkit ${config.configVersion ?? "< 0.1.0"}, ` +
25
+ `but you are now running ${getSwarmkitVersion()}.`);
26
+ const shouldRerun = await confirm({
27
+ message: "Re-run global setup to update your configuration?",
28
+ default: true,
29
+ });
30
+ if (shouldRerun) {
31
+ await runFirstTimeSetup(opts);
32
+ }
33
+ else {
34
+ await runProjectSetup(opts);
35
+ }
36
+ }
37
+ else {
38
+ await runProjectSetup(opts);
39
+ }
23
40
  }
24
41
  }
25
42
  async function runFirstTimeSetup(opts) {
@@ -39,9 +56,10 @@ async function runFirstTimeSetup(opts) {
39
56
  await initGlobal(state);
40
57
  // Step 5: Project init (if in a project directory)
41
58
  await initProject(state);
42
- // Persist prefix preference
59
+ // Persist prefix preference and config version
43
60
  const config = readConfig();
44
61
  config.usePrefix = state.usePrefix;
62
+ config.configVersion = getSwarmkitVersion();
45
63
  writeConfig(config);
46
64
  // Summary
47
65
  printSummary(state);
@@ -3,9 +3,13 @@ export function registerInitCommand(program) {
3
3
  .command("init")
4
4
  .description("Interactive setup wizard")
5
5
  .option("--no-prefix", "Use flat layout (e.g. .opentasks/) instead of nesting under .swarm/")
6
+ .option("-g, --global", "Re-run full global setup (even if already configured)")
6
7
  .action(async (opts) => {
7
8
  // Dynamic import to avoid loading @inquirer/prompts for other commands
8
9
  const { runWizard } = await import("./init/wizard.js");
9
- await runWizard({ noPrefix: opts.prefix === false });
10
+ await runWizard({
11
+ noPrefix: opts.prefix === false,
12
+ forceGlobal: opts.global === true,
13
+ });
10
14
  });
11
15
  }
@@ -7,6 +7,8 @@ export interface GlobalConfig {
7
7
  embeddingModel?: string;
8
8
  /** Whether project configs are nested under .swarm/ (default true) */
9
9
  usePrefix?: boolean;
10
+ /** The swarmkit version that last ran full global setup */
11
+ configVersion?: string;
10
12
  }
11
13
  /** Get the swarmkit config directory path (~/.swarmkit/) */
12
14
  export declare function getConfigDir(): string;
@@ -24,3 +26,11 @@ export declare function writeConfig(config: GlobalConfig): void;
24
26
  export declare function addInstalledPackages(packages: string[]): void;
25
27
  /** Remove a package from the installed list */
26
28
  export declare function removeInstalledPackage(packageName: string): void;
29
+ /** Get the current swarmkit version from package.json */
30
+ export declare function getSwarmkitVersion(): string;
31
+ /**
32
+ * Check if the config version is outdated compared to the current swarmkit version.
33
+ * Outdated if configVersion is missing or major/minor version differs.
34
+ * Patch-only bumps are not considered outdated.
35
+ */
36
+ export declare function isConfigOutdated(config: GlobalConfig, currentVersion?: string): boolean;
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { join } from "node:path";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  const DEFAULT_CONFIG = {
5
6
  installedPackages: [],
6
7
  };
@@ -69,3 +70,35 @@ export function removeInstalledPackage(packageName) {
69
70
  config.installedPackages = config.installedPackages.filter((p) => p !== packageName);
70
71
  writeConfig(config);
71
72
  }
73
+ /** Get the current swarmkit version from package.json */
74
+ export function getSwarmkitVersion() {
75
+ try {
76
+ const __dirname = dirname(fileURLToPath(import.meta.url));
77
+ const pkgPath = join(__dirname, "..", "package.json");
78
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
79
+ return pkg.version;
80
+ }
81
+ catch {
82
+ return "0.0.0";
83
+ }
84
+ }
85
+ function parseVersion(version) {
86
+ const parts = version.split(".").map(Number);
87
+ return {
88
+ major: parts[0] ?? 0,
89
+ minor: parts[1] ?? 0,
90
+ patch: parts[2] ?? 0,
91
+ };
92
+ }
93
+ /**
94
+ * Check if the config version is outdated compared to the current swarmkit version.
95
+ * Outdated if configVersion is missing or major/minor version differs.
96
+ * Patch-only bumps are not considered outdated.
97
+ */
98
+ export function isConfigOutdated(config, currentVersion) {
99
+ if (!config.configVersion)
100
+ return true;
101
+ const current = parseVersion(currentVersion ?? getSwarmkitVersion());
102
+ const saved = parseVersion(config.configVersion);
103
+ return current.major !== saved.major || current.minor !== saved.minor;
104
+ }
@@ -12,7 +12,7 @@ vi.mock("node:os", async () => {
12
12
  };
13
13
  });
14
14
  // Import after mock is set up
15
- const { getConfigDir, getConfigPath, ensureConfigDir, isFirstRun, readConfig, writeConfig, addInstalledPackages, removeInstalledPackage, } = await import("./global.js");
15
+ const { getConfigDir, getConfigPath, ensureConfigDir, isFirstRun, readConfig, writeConfig, addInstalledPackages, removeInstalledPackage, getSwarmkitVersion, isConfigOutdated, } = await import("./global.js");
16
16
  describe("config/global", () => {
17
17
  beforeEach(() => {
18
18
  tempHome = mkdtempSync(join(tmpdir(), "swarmkit-test-"));
@@ -164,4 +164,37 @@ describe("config/global", () => {
164
164
  expect(config.embeddingProvider).toBe("gemini");
165
165
  });
166
166
  });
167
+ describe("getSwarmkitVersion", () => {
168
+ it("returns a semver string", () => {
169
+ const version = getSwarmkitVersion();
170
+ expect(version).toMatch(/^\d+\.\d+\.\d+/);
171
+ });
172
+ });
173
+ describe("isConfigOutdated", () => {
174
+ it("returns true when configVersion is missing", () => {
175
+ const config = { installedPackages: [] };
176
+ expect(isConfigOutdated(config, "1.0.0")).toBe(true);
177
+ });
178
+ it("returns false when versions match exactly", () => {
179
+ const config = { installedPackages: [], configVersion: "1.2.3" };
180
+ expect(isConfigOutdated(config, "1.2.3")).toBe(false);
181
+ });
182
+ it("returns false for patch-only difference", () => {
183
+ const config = { installedPackages: [], configVersion: "1.2.0" };
184
+ expect(isConfigOutdated(config, "1.2.5")).toBe(false);
185
+ });
186
+ it("returns true when minor version differs", () => {
187
+ const config = { installedPackages: [], configVersion: "1.0.0" };
188
+ expect(isConfigOutdated(config, "1.1.0")).toBe(true);
189
+ });
190
+ it("returns true when major version differs", () => {
191
+ const config = { installedPackages: [], configVersion: "0.1.0" };
192
+ expect(isConfigOutdated(config, "1.0.0")).toBe(true);
193
+ });
194
+ it("uses getSwarmkitVersion when currentVersion is not provided", () => {
195
+ const version = getSwarmkitVersion();
196
+ const config = { installedPackages: [], configVersion: version };
197
+ expect(isConfigOutdated(config)).toBe(false);
198
+ });
199
+ });
167
200
  });
@@ -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 hasPrefixed = ctx.exists(join(ctx.cwd, prefixedDir));
143
- const hasFlat = flatDir ? ctx.exists(join(ctx.cwd, flatDir)) : false;
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: `${prefixedDir}/ not found`,
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: (path) => path.endsWith(".swarm/opentasks"),
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: (path) => path.endsWith(".opentasks"),
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: (path) => path.endsWith(".swarm/claude-swarm"),
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: (path) => existingDirs.has(path),
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);
@@ -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/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export type { InstallResult, UpdateResult } from "./packages/installer.js";
5
5
  export { isInstalledPlugin, registerPlugin, } from "./packages/plugin.js";
6
6
  export { PROJECT_CONFIG_DIRS, PROJECT_INIT_ORDER, GLOBAL_CONFIG_DIRS, isProjectInit, isGlobalInit, initProjectPackage, initGlobalPackage, } from "./packages/setup.js";
7
7
  export type { InitContext, GlobalContext, OpenhiveOptions, SetupResult, } from "./packages/setup.js";
8
- export { readConfig, writeConfig, isFirstRun, getConfigDir, addInstalledPackages, removeInstalledPackage, } from "./config/global.js";
8
+ export { readConfig, writeConfig, isFirstRun, getConfigDir, addInstalledPackages, removeInstalledPackage, getSwarmkitVersion, isConfigOutdated, } from "./config/global.js";
9
9
  export type { GlobalConfig } from "./config/global.js";
10
10
  export { readKey, writeKey, deleteKey, listKeys, hasKey, } from "./config/keys.js";
11
11
  export { runAllChecks } from "./doctor/checks.js";
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ export { PACKAGES, BUNDLES, INTEGRATIONS, getBundlePackages, getActiveIntegratio
3
3
  export { installPackages, uninstallPackage, getInstalledVersion, getLatestVersion, updatePackages, isClaudeCliAvailable, getGlobalPackagePath, } from "./packages/installer.js";
4
4
  export { isInstalledPlugin, registerPlugin, } from "./packages/plugin.js";
5
5
  export { PROJECT_CONFIG_DIRS, PROJECT_INIT_ORDER, GLOBAL_CONFIG_DIRS, isProjectInit, isGlobalInit, initProjectPackage, initGlobalPackage, } from "./packages/setup.js";
6
- export { readConfig, writeConfig, isFirstRun, getConfigDir, addInstalledPackages, removeInstalledPackage, } from "./config/global.js";
6
+ export { readConfig, writeConfig, isFirstRun, getConfigDir, addInstalledPackages, removeInstalledPackage, getSwarmkitVersion, isConfigOutdated, } from "./config/global.js";
7
7
  export { readKey, writeKey, deleteKey, listKeys, hasKey, } from "./config/keys.js";
8
8
  export { runAllChecks } from "./doctor/checks.js";
9
9
  export { readCredentials, writeCredentials, deleteCredentials, isLoggedIn, } from "./hub/credentials.js";
@@ -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, symlinkSync, writeFileSync, } from "node:fs";
3
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, symlinkSync, 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 ? existsSync(join(cwd, prefixed)) : false) ||
55
- (flat ? existsSync(join(cwd, flat)) : false);
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) {
@@ -156,7 +156,7 @@ export function isGlobalInit(pkg) {
156
156
  const dir = GLOBAL_CONFIG_DIRS[pkg];
157
157
  if (!dir)
158
158
  return false;
159
- return existsSync(join(homedir(), dir));
159
+ return hasContent(join(homedir(), dir));
160
160
  }
161
161
  /** Initialize a single global package */
162
162
  export async function initGlobalPackage(pkg, ctx, openhiveOpts) {
@@ -260,6 +260,12 @@ async function initSkillTreeProject(ctx) {
260
260
  : join(ctx.cwd, ".skilltree");
261
261
  mkdirSync(targetDir, { recursive: true });
262
262
  mkdirSync(join(targetDir, "skills"), { recursive: true });
263
+ mkdirSync(join(targetDir, ".cache"), { recursive: true });
264
+ // Gitignore the cache directory
265
+ const gitignorePath = join(targetDir, ".gitignore");
266
+ if (!existsSync(gitignorePath)) {
267
+ writeFileSync(gitignorePath, ".cache/\n");
268
+ }
263
269
  return { package: "skill-tree", success: true };
264
270
  }
265
271
  catch (err) {
@@ -271,15 +277,24 @@ async function initSkillTreeProject(ctx) {
271
277
  }
272
278
  }
273
279
  async function initOpenteamsProject(ctx) {
280
+ const targetDir = ctx.usePrefix
281
+ ? join(ctx.cwd, PROJECT_ROOT, "openteams")
282
+ : join(ctx.cwd, ".openteams");
283
+ // Try CLI first — `openteams template init` creates config.json with proper defaults
284
+ const result = await shellInit("openteams", ["template", "init", "-d", ctx.cwd], ctx.cwd, ctx.usePrefix
285
+ ? { OPENTEAMS_PROJECT_DIR: join(PROJECT_ROOT, "openteams") }
286
+ : undefined);
287
+ if (result.success) {
288
+ if (ctx.usePrefix)
289
+ relocate(ctx.cwd, ".openteams", "openteams");
290
+ return result;
291
+ }
292
+ // Fallback: create config inline if CLI is not available
274
293
  try {
275
- const targetDir = ctx.usePrefix
276
- ? join(ctx.cwd, PROJECT_ROOT, "openteams")
277
- : join(ctx.cwd, ".openteams");
278
294
  mkdirSync(targetDir, { recursive: true });
279
- // Write default config.json (matches `openteams template init` with no options)
280
295
  const configPath = join(targetDir, "config.json");
281
296
  if (!existsSync(configPath)) {
282
- writeFileSync(configPath, JSON.stringify({}, null, 2) + "\n");
297
+ writeFileSync(configPath, JSON.stringify({ defaults: {} }, null, 2) + "\n");
283
298
  }
284
299
  return { package: "openteams", success: true };
285
300
  }
@@ -300,7 +315,14 @@ async function initSessionlogProject(ctx) {
300
315
  // Write default settings.json
301
316
  const settingsPath = join(targetDir, "settings.json");
302
317
  if (!existsSync(settingsPath)) {
303
- writeFileSync(settingsPath, JSON.stringify({ enabled: false, strategy: "manual-commit" }, null, 2) + "\n");
318
+ const defaultSettings = {
319
+ enabled: false,
320
+ strategy: "manual-commit",
321
+ logLevel: "warn",
322
+ telemetryEnabled: false,
323
+ summarizationEnabled: false,
324
+ };
325
+ writeFileSync(settingsPath, JSON.stringify(defaultSettings, null, 2) + "\n");
304
326
  }
305
327
  return { package: "sessionlog", success: true };
306
328
  }
@@ -371,10 +393,24 @@ async function initClaudeSwarmProject(ctx) {
371
393
  ? join(ctx.cwd, PROJECT_ROOT, "claude-swarm")
372
394
  : join(ctx.cwd, ".claude-swarm");
373
395
  mkdirSync(targetDir, { recursive: true });
374
- // Write default config.json
396
+ // Write default config.json with meaningful defaults
375
397
  const configPath = join(targetDir, "config.json");
376
398
  if (!existsSync(configPath)) {
377
- writeFileSync(configPath, JSON.stringify({}, null, 2) + "\n");
399
+ const defaultConfig = {
400
+ template: "",
401
+ map: {
402
+ enabled: false,
403
+ server: "ws://localhost:8080",
404
+ scope: "",
405
+ systemId: "system-claude-swarm",
406
+ sidecar: "session",
407
+ },
408
+ sessionlog: {
409
+ enabled: false,
410
+ sync: "off",
411
+ },
412
+ };
413
+ writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + "\n");
378
414
  }
379
415
  // Write .gitignore for tmp/
380
416
  const gitignorePath = join(targetDir, ".gitignore");
@@ -392,6 +428,24 @@ async function initClaudeSwarmProject(ctx) {
392
428
  }
393
429
  }
394
430
  // ─── Helpers ─────────────────────────────────────────────────────────────────
431
+ /** Check if a directory exists and contains at least one file (recursively) */
432
+ function hasContent(dir) {
433
+ if (!existsSync(dir))
434
+ return false;
435
+ try {
436
+ const entries = readdirSync(dir, { withFileTypes: true });
437
+ for (const entry of entries) {
438
+ if (entry.isFile() || entry.isSymbolicLink())
439
+ return true;
440
+ if (entry.isDirectory() && hasContent(join(dir, entry.name)))
441
+ return true;
442
+ }
443
+ return false;
444
+ }
445
+ catch {
446
+ return false;
447
+ }
448
+ }
395
449
  function getProjectName(cwd) {
396
450
  const pkgPath = join(cwd, "package.json");
397
451
  if (existsSync(pkgPath)) {
@@ -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
- mkdirSync(join(testDir, configDir), { recursive: true });
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(FLAT_PROJECT_CONFIG_DIRS))("returns true for %s when %s/ exists (flat)", (pkg, configDir) => {
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
- mkdirSync(join(testHome, configDir), { recursive: true });
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
  });
@@ -276,12 +290,17 @@ describe("initProjectPackage — skill-tree", () => {
276
290
  expect(result.package).toBe("skill-tree");
277
291
  expect(existsSync(join(testDir, ".swarm", "skilltree"))).toBe(true);
278
292
  expect(existsSync(join(testDir, ".swarm", "skilltree", "skills"))).toBe(true);
293
+ expect(existsSync(join(testDir, ".swarm", "skilltree", ".cache"))).toBe(true);
294
+ expect(existsSync(join(testDir, ".swarm", "skilltree", ".gitignore"))).toBe(true);
295
+ expect(readFileSync(join(testDir, ".swarm", "skilltree", ".gitignore"), "utf-8")).toBe(".cache/\n");
279
296
  });
280
297
  it("creates .skilltree/ with skills/ subdirectory (flat)", async () => {
281
298
  const result = await initProjectPackage("skill-tree", projectCtx({ cwd: testDir, packages: ["skill-tree"], usePrefix: false }));
282
299
  expect(result.success).toBe(true);
283
300
  expect(existsSync(join(testDir, ".skilltree"))).toBe(true);
284
301
  expect(existsSync(join(testDir, ".skilltree", "skills"))).toBe(true);
302
+ expect(existsSync(join(testDir, ".skilltree", ".cache"))).toBe(true);
303
+ expect(existsSync(join(testDir, ".skilltree", ".gitignore"))).toBe(true);
285
304
  });
286
305
  it("is idempotent", async () => {
287
306
  await initProjectPackage("skill-tree", projectCtx({ cwd: testDir, packages: ["skill-tree"] }));
@@ -298,6 +317,7 @@ describe("initProjectPackage — openteams", () => {
298
317
  expect(existsSync(join(testDir, ".swarm", "openteams"))).toBe(true);
299
318
  expect(existsSync(join(testDir, ".swarm", "openteams", "config.json"))).toBe(true);
300
319
  const config = JSON.parse(readFileSync(join(testDir, ".swarm", "openteams", "config.json"), "utf-8"));
320
+ // openteams CLI writes {} by default (all built-in templates active)
301
321
  expect(config).toEqual({});
302
322
  });
303
323
  it("creates .openteams/ with config.json (flat)", async () => {
@@ -329,8 +349,13 @@ describe("initProjectPackage — sessionlog", () => {
329
349
  expect(existsSync(join(testDir, ".swarm", "sessionlog"))).toBe(true);
330
350
  expect(existsSync(join(testDir, ".swarm", "sessionlog", "settings.json"))).toBe(true);
331
351
  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");
352
+ expect(settings).toEqual({
353
+ enabled: false,
354
+ strategy: "manual-commit",
355
+ logLevel: "warn",
356
+ telemetryEnabled: false,
357
+ summarizationEnabled: false,
358
+ });
334
359
  });
335
360
  it("creates .sessionlog/ with settings.json (flat)", async () => {
336
361
  const result = await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"], usePrefix: false }));
@@ -363,7 +388,20 @@ describe("initProjectPackage — claude-code-swarm", () => {
363
388
  expect(existsSync(join(testDir, ".swarm", "claude-swarm", "config.json"))).toBe(true);
364
389
  expect(existsSync(join(testDir, ".swarm", "claude-swarm", ".gitignore"))).toBe(true);
365
390
  const config = JSON.parse(readFileSync(join(testDir, ".swarm", "claude-swarm", "config.json"), "utf-8"));
366
- expect(config).toEqual({});
391
+ expect(config).toEqual({
392
+ template: "",
393
+ map: {
394
+ enabled: false,
395
+ server: "ws://localhost:8080",
396
+ scope: "",
397
+ systemId: "system-claude-swarm",
398
+ sidecar: "session",
399
+ },
400
+ sessionlog: {
401
+ enabled: false,
402
+ sync: "off",
403
+ },
404
+ });
367
405
  const gitignore = readFileSync(join(testDir, ".swarm", "claude-swarm", ".gitignore"), "utf-8");
368
406
  expect(gitignore).toBe("tmp/\n");
369
407
  });
@@ -417,7 +455,7 @@ describe("initGlobalPackage — skill-tree (real CLI)", async () => {
417
455
  expect(existsSync(configPath)).toBe(true);
418
456
  const yaml = readFileSync(configPath, "utf-8");
419
457
  expect(yaml).toContain("storage:");
420
- expect(yaml).toContain("sqlite");
458
+ expect(yaml.toLowerCase()).toContain("sqlite");
421
459
  expect(yaml).toContain("indexer:");
422
460
  });
423
461
  it.skipIf(!installed)("skips when config.yaml already exists", async () => {
@@ -699,7 +737,20 @@ describe("flow: plugin bootstrap — init project dirs via swarmkit", () => {
699
737
  expect(existsSync(join(cwd, ".swarm", "claude-swarm", "config.json"))).toBe(true);
700
738
  expect(existsSync(join(cwd, ".swarm", "claude-swarm", ".gitignore"))).toBe(true);
701
739
  const config = JSON.parse(readFileSync(join(cwd, ".swarm", "claude-swarm", "config.json"), "utf-8"));
702
- expect(config).toEqual({});
740
+ expect(config).toEqual({
741
+ template: "",
742
+ map: {
743
+ enabled: false,
744
+ server: "ws://localhost:8080",
745
+ scope: "",
746
+ systemId: "system-claude-swarm",
747
+ sidecar: "session",
748
+ },
749
+ sessionlog: {
750
+ enabled: false,
751
+ sync: "off",
752
+ },
753
+ });
703
754
  const gitignore = readFileSync(join(cwd, ".swarm", "claude-swarm", ".gitignore"), "utf-8");
704
755
  expect(gitignore).toBe("tmp/\n");
705
756
  // isProjectInit now returns true for both
@@ -734,8 +785,13 @@ describe("flow: plugin bootstrap — init project dirs via swarmkit", () => {
734
785
  expect(existsSync(join(cwd, ".swarm", "claude-swarm"))).toBe(true);
735
786
  // sessionlog has default settings
736
787
  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");
788
+ expect(settings).toEqual({
789
+ enabled: false,
790
+ strategy: "manual-commit",
791
+ logLevel: "warn",
792
+ telemetryEnabled: false,
793
+ summarizationEnabled: false,
794
+ });
739
795
  });
740
796
  it("skips already-initialized packages on re-run (idempotent bootstrap)", async () => {
741
797
  const cwd = testDir;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swarmkit",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Multi-agent infa toolkit",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",