swarmkit 0.0.2 → 0.0.4

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.
@@ -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-"));
@@ -67,12 +67,12 @@ describe("config/global", () => {
67
67
  });
68
68
  it("reads written config", () => {
69
69
  writeConfig({
70
- installedPackages: ["macro-agent"],
70
+ installedPackages: ["opentasks"],
71
71
  embeddingProvider: "openai",
72
72
  embeddingModel: "text-embedding-3-small",
73
73
  });
74
74
  const config = readConfig();
75
- expect(config.installedPackages).toEqual(["macro-agent"]);
75
+ expect(config.installedPackages).toEqual(["opentasks"]);
76
76
  expect(config.embeddingProvider).toBe("openai");
77
77
  expect(config.embeddingModel).toBe("text-embedding-3-small");
78
78
  });
@@ -112,23 +112,23 @@ describe("config/global", () => {
112
112
  describe("addInstalledPackages", () => {
113
113
  it("adds packages to an empty list", () => {
114
114
  writeConfig({ installedPackages: [] });
115
- addInstalledPackages(["macro-agent", "minimem"]);
115
+ addInstalledPackages(["opentasks", "minimem"]);
116
116
  const config = readConfig();
117
- expect(config.installedPackages).toEqual(["macro-agent", "minimem"]);
117
+ expect(config.installedPackages).toEqual(["minimem", "opentasks"]);
118
118
  });
119
119
  it("deduplicates packages", () => {
120
- writeConfig({ installedPackages: ["macro-agent"] });
121
- addInstalledPackages(["macro-agent", "minimem"]);
120
+ writeConfig({ installedPackages: ["opentasks"] });
121
+ addInstalledPackages(["opentasks", "minimem"]);
122
122
  const config = readConfig();
123
- expect(config.installedPackages).toEqual(["macro-agent", "minimem"]);
123
+ expect(config.installedPackages).toEqual(["minimem", "opentasks"]);
124
124
  });
125
125
  it("sorts packages alphabetically", () => {
126
- addInstalledPackages(["minimem", "agent-iam", "macro-agent"]);
126
+ addInstalledPackages(["minimem", "cognitive-core", "opentasks"]);
127
127
  const config = readConfig();
128
128
  expect(config.installedPackages).toEqual([
129
- "agent-iam",
130
- "macro-agent",
129
+ "cognitive-core",
131
130
  "minimem",
131
+ "opentasks",
132
132
  ]);
133
133
  });
134
134
  it("preserves other config fields", () => {
@@ -143,16 +143,16 @@ describe("config/global", () => {
143
143
  });
144
144
  describe("removeInstalledPackage", () => {
145
145
  it("removes a package from the list", () => {
146
- writeConfig({ installedPackages: ["macro-agent", "minimem"] });
146
+ writeConfig({ installedPackages: ["opentasks", "minimem"] });
147
147
  removeInstalledPackage("minimem");
148
148
  const config = readConfig();
149
- expect(config.installedPackages).toEqual(["macro-agent"]);
149
+ expect(config.installedPackages).toEqual(["opentasks"]);
150
150
  });
151
151
  it("does nothing if package is not in list", () => {
152
- writeConfig({ installedPackages: ["macro-agent"] });
152
+ writeConfig({ installedPackages: ["opentasks"] });
153
153
  removeInstalledPackage("minimem");
154
154
  const config = readConfig();
155
- expect(config.installedPackages).toEqual(["macro-agent"]);
155
+ expect(config.installedPackages).toEqual(["opentasks"]);
156
156
  });
157
157
  it("preserves other config fields", () => {
158
158
  writeConfig({
@@ -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
  });
@@ -9,7 +9,7 @@ export declare function checkPackages(ctx: CheckContext): Promise<CheckResult[]>
9
9
  export declare function checkCredentials(ctx: CheckContext): CheckResult[];
10
10
  /**
11
11
  * Check that project-level config directories exist for installed packages.
12
- * Only runs when cwd is a project directory.
12
+ * Only runs when cwd is a project directory. Checks both .swarm/ and flat layouts.
13
13
  */
14
14
  export declare function checkProjectConfigs(ctx: CheckContext): CheckResult[];
15
15
  /**
@@ -1,13 +1,26 @@
1
1
  import { join } from "node:path";
2
2
  import { PACKAGES, getActiveIntegrations } from "../packages/registry.js";
3
- /** Config directories each package creates at the project level */
4
- const PROJECT_CONFIG_DIRS = {
5
- "macro-agent": ".multiagent",
3
+ /** Config directories prefixed layout (.swarm/) */
4
+ const PREFIXED_CONFIG_DIRS = {
5
+ opentasks: ".swarm/opentasks",
6
+ minimem: ".swarm/minimem",
7
+ "cognitive-core": ".swarm/cognitive-core",
8
+ "skill-tree": ".swarm/skilltree",
9
+ "self-driving-repo": ".swarm/self-driving",
10
+ openteams: ".swarm/openteams",
11
+ sessionlog: ".swarm/sessionlog",
12
+ "claude-code-swarm": ".swarm/claude-swarm",
13
+ };
14
+ /** Config directories — flat layout (legacy / --no-prefix) */
15
+ const FLAT_CONFIG_DIRS = {
6
16
  opentasks: ".opentasks",
7
17
  minimem: ".minimem",
8
18
  "cognitive-core": ".cognitive-core",
9
19
  "skill-tree": ".skilltree",
10
20
  "self-driving-repo": ".self-driving",
21
+ openteams: ".openteams",
22
+ sessionlog: ".sessionlog",
23
+ "claude-code-swarm": ".claude-swarm",
11
24
  };
12
25
  /** Embedding provider → required key name */
13
26
  const EMBEDDING_KEY_MAP = {
@@ -113,31 +126,34 @@ export function checkCredentials(ctx) {
113
126
  // ── Project config checks ───────────────────────────────────────
114
127
  /**
115
128
  * Check that project-level config directories exist for installed packages.
116
- * Only runs when cwd is a project directory.
129
+ * Only runs when cwd is a project directory. Checks both .swarm/ and flat layouts.
117
130
  */
118
131
  export function checkProjectConfigs(ctx) {
119
132
  if (!ctx.isProject)
120
133
  return [];
121
134
  const results = [];
122
135
  for (const pkg of ctx.installedPackages) {
123
- const configDir = PROJECT_CONFIG_DIRS[pkg];
124
- if (!configDir)
136
+ const prefixedDir = PREFIXED_CONFIG_DIRS[pkg];
137
+ const flatDir = FLAT_CONFIG_DIRS[pkg];
138
+ if (!prefixedDir)
125
139
  continue; // Package has no project-level config
126
140
  if (PACKAGES[pkg]?.globalOnly)
127
141
  continue;
128
- const fullPath = join(ctx.cwd, configDir);
129
- if (ctx.exists(fullPath)) {
142
+ const hasPrefixed = ctx.exists(join(ctx.cwd, prefixedDir));
143
+ const hasFlat = flatDir ? ctx.exists(join(ctx.cwd, flatDir)) : false;
144
+ if (hasPrefixed || hasFlat) {
145
+ const found = hasPrefixed ? prefixedDir : flatDir;
130
146
  results.push({
131
147
  name: `${pkg}-config`,
132
148
  status: "pass",
133
- message: `${configDir}/`,
149
+ message: `${found}/`,
134
150
  });
135
151
  }
136
152
  else {
137
153
  results.push({
138
154
  name: `${pkg}-config`,
139
155
  status: "warn",
140
- message: `${configDir}/ not found`,
156
+ message: `${prefixedDir}/ not found`,
141
157
  fix: `swarmkit init (or ${pkg} init)`,
142
158
  });
143
159
  }
@@ -16,10 +16,10 @@ function createContext(overrides = {}) {
16
16
  describe("checkPackages", () => {
17
17
  it("passes when all packages are installed", async () => {
18
18
  const ctx = createContext({
19
- installedPackages: ["macro-agent", "minimem"],
19
+ installedPackages: ["opentasks", "minimem"],
20
20
  getInstalledVersion: async (pkg) => {
21
21
  const versions = {
22
- "macro-agent": "0.1.0",
22
+ opentasks: "0.1.0",
23
23
  minimem: "0.2.0",
24
24
  };
25
25
  return versions[pkg] ?? null;
@@ -28,14 +28,14 @@ describe("checkPackages", () => {
28
28
  const results = await checkPackages(ctx);
29
29
  expect(results).toHaveLength(2);
30
30
  expect(results[0].status).toBe("pass");
31
- expect(results[0].message).toContain("macro-agent");
31
+ expect(results[0].message).toContain("opentasks");
32
32
  expect(results[0].message).toContain("0.1.0");
33
33
  expect(results[1].status).toBe("pass");
34
34
  });
35
35
  it("fails when a package is not found", async () => {
36
36
  const ctx = createContext({
37
- installedPackages: ["macro-agent", "minimem"],
38
- getInstalledVersion: async (pkg) => pkg === "macro-agent" ? "0.1.0" : null,
37
+ installedPackages: ["opentasks", "minimem"],
38
+ getInstalledVersion: async (pkg) => pkg === "opentasks" ? "0.1.0" : null,
39
39
  });
40
40
  const results = await checkPackages(ctx);
41
41
  expect(results[0].status).toBe("pass");
@@ -124,7 +124,7 @@ describe("checkCredentials", () => {
124
124
  });
125
125
  it("skips embedding check when no embedding consumers installed", () => {
126
126
  const ctx = createContext({
127
- installedPackages: ["macro-agent", "opentasks"],
127
+ installedPackages: ["opentasks", "self-driving-repo"],
128
128
  embeddingProvider: undefined,
129
129
  });
130
130
  const results = checkCredentials(ctx);
@@ -146,21 +146,32 @@ describe("checkProjectConfigs", () => {
146
146
  it("returns empty when not in a project", () => {
147
147
  const ctx = createContext({
148
148
  isProject: false,
149
- installedPackages: ["macro-agent"],
149
+ installedPackages: ["opentasks"],
150
150
  });
151
151
  const results = checkProjectConfigs(ctx);
152
152
  expect(results).toEqual([]);
153
153
  });
154
- it("passes when config directory exists", () => {
154
+ it("passes when prefixed config directory exists", () => {
155
155
  const ctx = createContext({
156
156
  isProject: true,
157
- installedPackages: ["macro-agent"],
158
- exists: (path) => path.endsWith(".multiagent"),
157
+ installedPackages: ["opentasks"],
158
+ exists: (path) => path.endsWith(".swarm/opentasks"),
159
159
  });
160
160
  const results = checkProjectConfigs(ctx);
161
161
  expect(results).toHaveLength(1);
162
162
  expect(results[0].status).toBe("pass");
163
- expect(results[0].message).toContain(".multiagent");
163
+ expect(results[0].message).toContain(".swarm/opentasks");
164
+ });
165
+ it("passes when flat config directory exists", () => {
166
+ const ctx = createContext({
167
+ isProject: true,
168
+ installedPackages: ["opentasks"],
169
+ exists: (path) => path.endsWith(".opentasks"),
170
+ });
171
+ const results = checkProjectConfigs(ctx);
172
+ expect(results).toHaveLength(1);
173
+ expect(results[0].status).toBe("pass");
174
+ expect(results[0].message).toContain(".opentasks");
164
175
  });
165
176
  it("warns when config directory is missing", () => {
166
177
  const ctx = createContext({
@@ -171,54 +182,68 @@ describe("checkProjectConfigs", () => {
171
182
  const results = checkProjectConfigs(ctx);
172
183
  expect(results).toHaveLength(1);
173
184
  expect(results[0].status).toBe("warn");
174
- expect(results[0].message).toContain(".opentasks");
185
+ expect(results[0].message).toContain(".swarm/opentasks");
175
186
  expect(results[0].fix).toContain("opentasks init");
176
187
  });
177
- it("skips globalOnly packages", () => {
188
+ it("passes for claude-code-swarm with prefixed config", () => {
189
+ const ctx = createContext({
190
+ isProject: true,
191
+ installedPackages: ["claude-code-swarm"],
192
+ exists: (path) => path.endsWith(".swarm/claude-swarm"),
193
+ });
194
+ const results = checkProjectConfigs(ctx);
195
+ expect(results).toHaveLength(1);
196
+ expect(results[0].status).toBe("pass");
197
+ expect(results[0].message).toContain(".swarm/claude-swarm");
198
+ });
199
+ it("skips packages without project config dirs", () => {
178
200
  const ctx = createContext({
179
201
  isProject: true,
180
- installedPackages: ["agent-iam"],
202
+ installedPackages: ["multi-agent-protocol"],
181
203
  exists: () => false,
182
204
  });
183
205
  const results = checkProjectConfigs(ctx);
184
206
  expect(results).toEqual([]);
185
207
  });
186
- it("checks multiple packages", () => {
187
- const existingDirs = new Set(["/tmp/test-project/.multiagent"]);
208
+ it("checks multiple packages (mixed layouts)", () => {
209
+ const existingDirs = new Set([
210
+ "/tmp/test-project/.swarm/opentasks",
211
+ "/tmp/test-project/.minimem",
212
+ ]);
188
213
  const ctx = createContext({
189
214
  isProject: true,
190
215
  cwd: "/tmp/test-project",
191
- installedPackages: ["macro-agent", "opentasks", "minimem"],
216
+ installedPackages: ["opentasks", "minimem", "cognitive-core"],
192
217
  exists: (path) => existingDirs.has(path),
193
218
  });
194
219
  const results = checkProjectConfigs(ctx);
195
220
  expect(results).toHaveLength(3);
196
- expect(results[0].status).toBe("pass"); // macro-agent
197
- expect(results[1].status).toBe("warn"); // opentasks
198
- expect(results[2].status).toBe("warn"); // minimem
221
+ expect(results[0].status).toBe("pass"); // opentasks (prefixed)
222
+ expect(results[1].status).toBe("pass"); // minimem (flat)
223
+ expect(results[2].status).toBe("warn"); // cognitive-core (missing)
199
224
  });
200
225
  });
201
226
  describe("checkIntegrations", () => {
202
227
  it("passes when both packages in an integration are installed", async () => {
203
228
  const ctx = createContext({
204
- installedPackages: ["macro-agent", "opentasks"],
229
+ installedPackages: ["cognitive-core", "minimem"],
205
230
  getInstalledVersion: async () => "0.1.0",
206
231
  });
207
232
  const results = await checkIntegrations(ctx);
208
233
  expect(results).toHaveLength(1);
209
234
  expect(results[0].status).toBe("pass");
210
- expect(results[0].message).toContain("macro-agent");
211
- expect(results[0].message).toContain("opentasks");
235
+ expect(results[0].message).toContain("cognitive-core");
236
+ expect(results[0].message).toContain("minimem");
212
237
  });
213
238
  it("fails when one package in an integration is missing from PATH", async () => {
214
239
  const ctx = createContext({
215
- installedPackages: ["macro-agent", "opentasks"],
216
- getInstalledVersion: async (pkg) => pkg === "macro-agent" ? "0.1.0" : null,
240
+ installedPackages: ["cognitive-core", "minimem"],
241
+ getInstalledVersion: async (pkg) => pkg === "cognitive-core" ? "0.1.0" : null,
217
242
  });
218
243
  const results = await checkIntegrations(ctx);
219
244
  expect(results).toHaveLength(1);
220
245
  expect(results[0].status).toBe("fail");
221
- expect(results[0].fix).toContain("opentasks");
246
+ expect(results[0].fix).toContain("minimem");
222
247
  });
223
248
  it("returns empty when no integrations are active", async () => {
224
249
  const ctx = createContext({
@@ -251,7 +276,7 @@ describe("checkGlobalConfig", () => {
251
276
  describe("runAllChecks", () => {
252
277
  it("returns a complete report", async () => {
253
278
  const ctx = createContext({
254
- installedPackages: ["macro-agent", "minimem"],
279
+ installedPackages: ["opentasks", "minimem"],
255
280
  embeddingProvider: "openai",
256
281
  storedKeys: ["anthropic", "openai"],
257
282
  isProject: true,
package/dist/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  export { PACKAGES, BUNDLES, INTEGRATIONS, getBundlePackages, getActiveIntegrations, getNewIntegrations, getLostIntegrations, getNpmName, isKnownPackage, getAllPackageNames, } from "./packages/registry.js";
2
2
  export type { PackageDefinition, BundleDefinition, Integration, } from "./packages/registry.js";
3
- export { installPackages, uninstallPackage, getInstalledVersion, getLatestVersion, updatePackages, } from "./packages/installer.js";
3
+ export { installPackages, uninstallPackage, getInstalledVersion, getLatestVersion, updatePackages, isClaudeCliAvailable, getGlobalPackagePath, } from "./packages/installer.js";
4
4
  export type { InstallResult, UpdateResult } from "./packages/installer.js";
5
+ export { isInstalledPlugin, registerPlugin, } from "./packages/plugin.js";
5
6
  export { PROJECT_CONFIG_DIRS, PROJECT_INIT_ORDER, GLOBAL_CONFIG_DIRS, isProjectInit, isGlobalInit, initProjectPackage, initGlobalPackage, } from "./packages/setup.js";
6
7
  export type { InitContext, GlobalContext, OpenhiveOptions, SetupResult, } from "./packages/setup.js";
7
- 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";
8
9
  export type { GlobalConfig } from "./config/global.js";
9
10
  export { readKey, writeKey, deleteKey, listKeys, hasKey, } from "./config/keys.js";
10
11
  export { runAllChecks } from "./doctor/checks.js";
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  // Public API — re-export modules for programmatic use
2
2
  export { PACKAGES, BUNDLES, INTEGRATIONS, getBundlePackages, getActiveIntegrations, getNewIntegrations, getLostIntegrations, getNpmName, isKnownPackage, getAllPackageNames, } from "./packages/registry.js";
3
- export { installPackages, uninstallPackage, getInstalledVersion, getLatestVersion, updatePackages, } from "./packages/installer.js";
3
+ export { installPackages, uninstallPackage, getInstalledVersion, getLatestVersion, updatePackages, isClaudeCliAvailable, getGlobalPackagePath, } from "./packages/installer.js";
4
+ export { isInstalledPlugin, registerPlugin, } from "./packages/plugin.js";
4
5
  export { PROJECT_CONFIG_DIRS, PROJECT_INIT_ORDER, GLOBAL_CONFIG_DIRS, isProjectInit, isGlobalInit, initProjectPackage, initGlobalPackage, } from "./packages/setup.js";
5
- 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";
6
7
  export { readKey, writeKey, deleteKey, listKeys, hasKey, } from "./config/keys.js";
7
8
  export { runAllChecks } from "./doctor/checks.js";
8
9
  export { readCredentials, writeCredentials, deleteCredentials, isLoggedIn, } from "./hub/credentials.js";
@@ -31,3 +31,12 @@ export declare function getLatestVersion(packageName: string): Promise<string |
31
31
  * Update installed packages to their latest versions.
32
32
  */
33
33
  export declare function updatePackages(packages: string[]): Promise<UpdateResult[]>;
34
+ /**
35
+ * Check if the Claude Code CLI is available.
36
+ */
37
+ export declare function isClaudeCliAvailable(): Promise<boolean>;
38
+ /**
39
+ * Resolve the global installation path of an npm package.
40
+ * Uses `npm list -g --parseable` to get the filesystem path.
41
+ */
42
+ export declare function getGlobalPackagePath(npmName: string): Promise<string | null>;
@@ -8,21 +8,24 @@ const execFileAsync = promisify(execFile);
8
8
  export async function installPackages(packages) {
9
9
  const results = [];
10
10
  for (const pkg of packages) {
11
- const npmName = getNpmName(pkg);
12
- try {
13
- await execFileAsync("npm", ["install", "-g", npmName], {
14
- timeout: 120_000,
15
- });
16
- const version = await getInstalledVersion(pkg);
17
- results.push({ package: pkg, success: true, version: version ?? undefined });
18
- }
19
- catch (err) {
20
- const message = err instanceof Error ? err.message : String(err);
21
- results.push({ package: pkg, success: false, error: formatNpmError(message) });
22
- }
11
+ results.push(await installNpmPackage(pkg));
23
12
  }
24
13
  return results;
25
14
  }
15
+ async function installNpmPackage(pkg) {
16
+ const npmName = getNpmName(pkg);
17
+ try {
18
+ await execFileAsync("npm", ["install", "-g", npmName], {
19
+ timeout: 120_000,
20
+ });
21
+ const version = await getInstalledVersion(pkg);
22
+ return { package: pkg, success: true, version: version ?? undefined };
23
+ }
24
+ catch (err) {
25
+ const message = err instanceof Error ? err.message : String(err);
26
+ return { package: pkg, success: false, error: formatNpmError(message) };
27
+ }
28
+ }
26
29
  /**
27
30
  * Uninstall a package globally via npm.
28
31
  */
@@ -112,6 +115,34 @@ export async function updatePackages(packages) {
112
115
  }
113
116
  return results;
114
117
  }
118
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
119
+ /**
120
+ * Check if the Claude Code CLI is available.
121
+ */
122
+ export async function isClaudeCliAvailable() {
123
+ try {
124
+ await execFileAsync("claude", ["--version"], { timeout: 10_000 });
125
+ return true;
126
+ }
127
+ catch {
128
+ return false;
129
+ }
130
+ }
131
+ /**
132
+ * Resolve the global installation path of an npm package.
133
+ * Uses `npm list -g --parseable` to get the filesystem path.
134
+ */
135
+ export async function getGlobalPackagePath(npmName) {
136
+ try {
137
+ const { stdout } = await execFileAsync("npm", ["list", "-g", npmName, "--parseable", "--depth=0"], { timeout: 15_000 });
138
+ // --parseable returns: prefix-root\npackage-path
139
+ const lines = stdout.trim().split("\n").filter(Boolean);
140
+ return lines.length > 1 ? lines[lines.length - 1] : null;
141
+ }
142
+ catch {
143
+ return null;
144
+ }
145
+ }
115
146
  function formatNpmError(message) {
116
147
  if (message.includes("EACCES") || message.includes("permission")) {
117
148
  return "Permission denied — try running with sudo or configure npm prefix";
@@ -12,7 +12,7 @@ vi.mock("node:util", async () => {
12
12
  promisify: () => mockExecFile,
13
13
  };
14
14
  });
15
- const { installPackages, uninstallPackage, getInstalledVersion, getLatestVersion, updatePackages, } = await import("./installer.js");
15
+ const { installPackages, uninstallPackage, getInstalledVersion, getLatestVersion, updatePackages, isClaudeCliAvailable, getGlobalPackagePath, } = await import("./installer.js");
16
16
  describe("installer", () => {
17
17
  beforeEach(() => {
18
18
  mockExecFile.mockReset();
@@ -197,4 +197,87 @@ describe("installer", () => {
197
197
  expect(results[0].error).toContain("Permission denied");
198
198
  });
199
199
  });
200
+ describe("isClaudeCliAvailable", () => {
201
+ it("returns true when claude --version succeeds", async () => {
202
+ mockExecFile.mockResolvedValueOnce({ stdout: "1.0.0\n" });
203
+ const available = await isClaudeCliAvailable();
204
+ expect(available).toBe(true);
205
+ expect(mockExecFile).toHaveBeenCalledWith("claude", ["--version"], { timeout: 10_000 });
206
+ });
207
+ it("returns false when claude is not found", async () => {
208
+ mockExecFile.mockRejectedValueOnce(new Error("ENOENT"));
209
+ const available = await isClaudeCliAvailable();
210
+ expect(available).toBe(false);
211
+ });
212
+ });
213
+ describe("getGlobalPackagePath", () => {
214
+ it("returns path from npm list --parseable", async () => {
215
+ mockExecFile.mockResolvedValueOnce({
216
+ stdout: "/usr/local/lib/node_modules\n/usr/local/lib/node_modules/claude-code-swarm\n",
217
+ });
218
+ const result = await getGlobalPackagePath("claude-code-swarm");
219
+ expect(result).toBe("/usr/local/lib/node_modules/claude-code-swarm");
220
+ });
221
+ it("returns null when package is not installed", async () => {
222
+ mockExecFile.mockRejectedValueOnce(new Error("not found"));
223
+ const result = await getGlobalPackagePath("not-installed");
224
+ expect(result).toBeNull();
225
+ });
226
+ });
227
+ describe("installPackages — claude-code-swarm as normal npm", () => {
228
+ it("installs claude-code-swarm via npm without any claude CLI calls", async () => {
229
+ // npm install -g
230
+ mockExecFile.mockResolvedValueOnce({ stdout: "" });
231
+ // getInstalledVersion
232
+ mockExecFile.mockResolvedValueOnce({
233
+ stdout: JSON.stringify({
234
+ dependencies: { "claude-code-swarm": { version: "0.3.1" } },
235
+ }),
236
+ });
237
+ const results = await installPackages(["claude-code-swarm"]);
238
+ expect(results).toHaveLength(1);
239
+ expect(results[0].success).toBe(true);
240
+ expect(results[0].version).toBe("0.3.1");
241
+ // Verify no calls to claude CLI
242
+ for (const call of mockExecFile.mock.calls) {
243
+ expect(call[0]).not.toBe("claude");
244
+ }
245
+ });
246
+ });
247
+ describe("uninstallPackage — claude-code-swarm as normal npm", () => {
248
+ it("just calls npm uninstall (no plugin deregistration)", async () => {
249
+ mockExecFile.mockResolvedValueOnce({ stdout: "" });
250
+ await uninstallPackage("claude-code-swarm");
251
+ expect(mockExecFile).toHaveBeenCalledTimes(1);
252
+ expect(mockExecFile).toHaveBeenCalledWith("npm", ["uninstall", "-g", "claude-code-swarm"], { timeout: 60_000 });
253
+ });
254
+ });
255
+ describe("updatePackages — claude-code-swarm as normal npm", () => {
256
+ it("updates via npm without plugin re-registration", async () => {
257
+ // getInstalledVersion (before)
258
+ mockExecFile.mockResolvedValueOnce({
259
+ stdout: JSON.stringify({
260
+ dependencies: { "claude-code-swarm": { version: "0.3.0" } },
261
+ }),
262
+ });
263
+ // getLatestVersion
264
+ mockExecFile.mockResolvedValueOnce({ stdout: "0.4.0\n" });
265
+ // npm install -g claude-code-swarm@latest
266
+ mockExecFile.mockResolvedValueOnce({ stdout: "" });
267
+ // getInstalledVersion (after)
268
+ mockExecFile.mockResolvedValueOnce({
269
+ stdout: JSON.stringify({
270
+ dependencies: { "claude-code-swarm": { version: "0.4.0" } },
271
+ }),
272
+ });
273
+ const results = await updatePackages(["claude-code-swarm"]);
274
+ expect(results).toHaveLength(1);
275
+ expect(results[0].updated).toBe(true);
276
+ expect(results[0].newVersion).toBe("0.4.0");
277
+ // Verify no calls to claude CLI
278
+ for (const call of mockExecFile.mock.calls) {
279
+ expect(call[0]).not.toBe("claude");
280
+ }
281
+ });
282
+ });
200
283
  });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Check if a globally installed npm package is a Claude Code plugin.
3
+ * A package is a Claude plugin if it contains `.claude-plugin/plugin.json`.
4
+ */
5
+ export declare function isInstalledPlugin(packageName: string): Promise<boolean>;
6
+ /**
7
+ * Register a package as a Claude Code plugin using `claude plugin add`.
8
+ * Scope controls where the plugin is activated:
9
+ * - "user": all projects (default)
10
+ * - "project": this repository only (shared with collaborators)
11
+ * - "local": this repository only (not shared)
12
+ */
13
+ export declare function registerPlugin(packageName: string, scope?: "user" | "project" | "local"): Promise<void>;
@@ -0,0 +1,33 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { existsSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { getNpmName } from "./registry.js";
6
+ import { getGlobalPackagePath } from "./installer.js";
7
+ const execFileAsync = promisify(execFile);
8
+ /**
9
+ * Check if a globally installed npm package is a Claude Code plugin.
10
+ * A package is a Claude plugin if it contains `.claude-plugin/plugin.json`.
11
+ */
12
+ export async function isInstalledPlugin(packageName) {
13
+ const npmName = getNpmName(packageName);
14
+ const pkgPath = await getGlobalPackagePath(npmName);
15
+ if (!pkgPath)
16
+ return false;
17
+ return existsSync(join(pkgPath, ".claude-plugin", "plugin.json"));
18
+ }
19
+ /**
20
+ * Register a package as a Claude Code plugin using `claude plugin add`.
21
+ * Scope controls where the plugin is activated:
22
+ * - "user": all projects (default)
23
+ * - "project": this repository only (shared with collaborators)
24
+ * - "local": this repository only (not shared)
25
+ */
26
+ export async function registerPlugin(packageName, scope = "user") {
27
+ const npmName = getNpmName(packageName);
28
+ const pkgPath = await getGlobalPackagePath(npmName);
29
+ if (!pkgPath) {
30
+ throw new Error(`Could not resolve global install path for ${packageName}`);
31
+ }
32
+ await execFileAsync("claude", ["plugin", "add", pkgPath, "--scope", scope], { timeout: 30_000 });
33
+ }
@@ -0,0 +1 @@
1
+ export {};