swarmkit 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +130 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +33 -0
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +98 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +100 -0
- package/dist/commands/hive.d.ts +2 -0
- package/dist/commands/hive.js +248 -0
- package/dist/commands/init/phases/configure.d.ts +2 -0
- package/dist/commands/init/phases/configure.js +85 -0
- package/dist/commands/init/phases/global-setup.d.ts +2 -0
- package/dist/commands/init/phases/global-setup.js +81 -0
- package/dist/commands/init/phases/packages.d.ts +2 -0
- package/dist/commands/init/phases/packages.js +30 -0
- package/dist/commands/init/phases/project.d.ts +2 -0
- package/dist/commands/init/phases/project.js +56 -0
- package/dist/commands/init/phases/use-case.d.ts +2 -0
- package/dist/commands/init/phases/use-case.js +41 -0
- package/dist/commands/init/state.d.ts +13 -0
- package/dist/commands/init/state.js +9 -0
- package/dist/commands/init/state.test.d.ts +1 -0
- package/dist/commands/init/state.test.js +21 -0
- package/dist/commands/init/wizard.d.ts +4 -0
- package/dist/commands/init/wizard.js +108 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +11 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +91 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +19 -0
- package/dist/commands/remove.d.ts +2 -0
- package/dist/commands/remove.js +55 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +87 -0
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +54 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +40 -0
- package/dist/config/global.d.ts +26 -0
- package/dist/config/global.js +71 -0
- package/dist/config/global.test.d.ts +1 -0
- package/dist/config/global.test.js +167 -0
- package/dist/config/keys.d.ts +10 -0
- package/dist/config/keys.js +47 -0
- package/dist/config/keys.test.d.ts +1 -0
- package/dist/config/keys.test.js +87 -0
- package/dist/doctor/checks.d.ts +31 -0
- package/dist/doctor/checks.js +226 -0
- package/dist/doctor/checks.test.d.ts +1 -0
- package/dist/doctor/checks.test.js +301 -0
- package/dist/doctor/types.d.ts +29 -0
- package/dist/doctor/types.js +1 -0
- package/dist/hub/auth-flow.d.ts +16 -0
- package/dist/hub/auth-flow.js +118 -0
- package/dist/hub/auth-flow.test.d.ts +1 -0
- package/dist/hub/auth-flow.test.js +98 -0
- package/dist/hub/client.d.ts +51 -0
- package/dist/hub/client.js +107 -0
- package/dist/hub/client.test.d.ts +1 -0
- package/dist/hub/client.test.js +177 -0
- package/dist/hub/credentials.d.ts +14 -0
- package/dist/hub/credentials.js +41 -0
- package/dist/hub/credentials.test.d.ts +1 -0
- package/dist/hub/credentials.test.js +102 -0
- package/dist/index.d.ts +17 -1
- package/dist/index.js +10 -2
- package/dist/packages/installer.d.ts +42 -0
- package/dist/packages/installer.js +158 -0
- package/dist/packages/installer.test.d.ts +1 -0
- package/dist/packages/installer.test.js +283 -0
- package/dist/packages/plugin.d.ts +13 -0
- package/dist/packages/plugin.js +33 -0
- package/dist/packages/plugin.test.d.ts +1 -0
- package/dist/packages/plugin.test.js +99 -0
- package/dist/packages/registry.d.ts +37 -0
- package/dist/packages/registry.js +154 -0
- package/dist/packages/registry.test.d.ts +1 -0
- package/dist/packages/registry.test.js +188 -0
- package/dist/packages/setup.d.ts +55 -0
- package/dist/packages/setup.js +414 -0
- package/dist/packages/setup.test.d.ts +1 -0
- package/dist/packages/setup.test.js +808 -0
- package/dist/utils/ui.d.ts +10 -0
- package/dist/utils/ui.js +47 -0
- package/dist/utils/ui.test.d.ts +1 -0
- package/dist/utils/ui.test.js +102 -0
- package/package.json +29 -6
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
// Mock homedir so config writes go to a temp dir
|
|
7
|
+
let testDir;
|
|
8
|
+
vi.mock("node:os", async () => {
|
|
9
|
+
const actual = await import("node:os");
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
homedir: () => testDir,
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
const { readCredentials, writeCredentials, deleteCredentials, isLoggedIn, } = await import("./credentials.js");
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
testDir = join(tmpdir(), `swarmkit-cred-test-${randomUUID()}`);
|
|
18
|
+
mkdirSync(testDir, { recursive: true });
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
describe("readCredentials", () => {
|
|
24
|
+
it("returns null when no credentials file exists", () => {
|
|
25
|
+
expect(readCredentials()).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
it("returns credentials when file exists", () => {
|
|
28
|
+
writeCredentials({
|
|
29
|
+
token: "jwt-token-123",
|
|
30
|
+
apiUrl: "https://api.swarmkit.ai",
|
|
31
|
+
});
|
|
32
|
+
const creds = readCredentials();
|
|
33
|
+
expect(creds).toEqual({
|
|
34
|
+
token: "jwt-token-123",
|
|
35
|
+
apiUrl: "https://api.swarmkit.ai",
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
it("returns null for malformed JSON", () => {
|
|
39
|
+
const dir = join(testDir, ".swarmkit");
|
|
40
|
+
mkdirSync(dir, { recursive: true });
|
|
41
|
+
writeFileSync(join(dir, "credentials.json"), "not json");
|
|
42
|
+
expect(readCredentials()).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
it("returns null when token is missing", () => {
|
|
45
|
+
const dir = join(testDir, ".swarmkit");
|
|
46
|
+
mkdirSync(dir, { recursive: true });
|
|
47
|
+
writeFileSync(join(dir, "credentials.json"), JSON.stringify({ apiUrl: "https://api.swarmkit.ai" }));
|
|
48
|
+
expect(readCredentials()).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe("writeCredentials", () => {
|
|
52
|
+
it("creates the credentials file", () => {
|
|
53
|
+
writeCredentials({
|
|
54
|
+
token: "test-token",
|
|
55
|
+
apiUrl: "https://api.example.com",
|
|
56
|
+
});
|
|
57
|
+
const path = join(testDir, ".swarmkit", "credentials.json");
|
|
58
|
+
expect(existsSync(path)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
it("writes valid JSON", () => {
|
|
61
|
+
writeCredentials({
|
|
62
|
+
token: "test-token",
|
|
63
|
+
apiUrl: "https://api.example.com",
|
|
64
|
+
});
|
|
65
|
+
const path = join(testDir, ".swarmkit", "credentials.json");
|
|
66
|
+
const raw = readFileSync(path, "utf-8");
|
|
67
|
+
const parsed = JSON.parse(raw);
|
|
68
|
+
expect(parsed.token).toBe("test-token");
|
|
69
|
+
expect(parsed.apiUrl).toBe("https://api.example.com");
|
|
70
|
+
});
|
|
71
|
+
it("overwrites existing credentials", () => {
|
|
72
|
+
writeCredentials({ token: "old", apiUrl: "https://old.com" });
|
|
73
|
+
writeCredentials({ token: "new", apiUrl: "https://new.com" });
|
|
74
|
+
const creds = readCredentials();
|
|
75
|
+
expect(creds?.token).toBe("new");
|
|
76
|
+
expect(creds?.apiUrl).toBe("https://new.com");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe("deleteCredentials", () => {
|
|
80
|
+
it("removes the credentials file", () => {
|
|
81
|
+
writeCredentials({ token: "test", apiUrl: "https://api.test.com" });
|
|
82
|
+
deleteCredentials();
|
|
83
|
+
expect(readCredentials()).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
it("does nothing when no credentials exist", () => {
|
|
86
|
+
expect(() => deleteCredentials()).not.toThrow();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe("isLoggedIn", () => {
|
|
90
|
+
it("returns false when not logged in", () => {
|
|
91
|
+
expect(isLoggedIn()).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
it("returns true when credentials exist", () => {
|
|
94
|
+
writeCredentials({ token: "test", apiUrl: "https://api.test.com" });
|
|
95
|
+
expect(isLoggedIn()).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
it("returns false after logout", () => {
|
|
98
|
+
writeCredentials({ token: "test", apiUrl: "https://api.test.com" });
|
|
99
|
+
deleteCredentials();
|
|
100
|
+
expect(isLoggedIn()).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1,17 @@
|
|
|
1
|
-
export {};
|
|
1
|
+
export { PACKAGES, BUNDLES, INTEGRATIONS, getBundlePackages, getActiveIntegrations, getNewIntegrations, getLostIntegrations, getNpmName, isKnownPackage, getAllPackageNames, } from "./packages/registry.js";
|
|
2
|
+
export type { PackageDefinition, BundleDefinition, Integration, } from "./packages/registry.js";
|
|
3
|
+
export { installPackages, uninstallPackage, getInstalledVersion, getLatestVersion, updatePackages, isClaudeCliAvailable, getGlobalPackagePath, } from "./packages/installer.js";
|
|
4
|
+
export type { InstallResult, UpdateResult } from "./packages/installer.js";
|
|
5
|
+
export { isInstalledPlugin, registerPlugin, } from "./packages/plugin.js";
|
|
6
|
+
export { PROJECT_CONFIG_DIRS, PROJECT_INIT_ORDER, GLOBAL_CONFIG_DIRS, isProjectInit, isGlobalInit, initProjectPackage, initGlobalPackage, } from "./packages/setup.js";
|
|
7
|
+
export type { InitContext, GlobalContext, OpenhiveOptions, SetupResult, } from "./packages/setup.js";
|
|
8
|
+
export { readConfig, writeConfig, isFirstRun, getConfigDir, addInstalledPackages, removeInstalledPackage, } from "./config/global.js";
|
|
9
|
+
export type { GlobalConfig } from "./config/global.js";
|
|
10
|
+
export { readKey, writeKey, deleteKey, listKeys, hasKey, } from "./config/keys.js";
|
|
11
|
+
export { runAllChecks } from "./doctor/checks.js";
|
|
12
|
+
export type { CheckResult, CheckContext, CheckStatus, } from "./doctor/types.js";
|
|
13
|
+
export type { DoctorReport } from "./doctor/checks.js";
|
|
14
|
+
export { readCredentials, writeCredentials, deleteCredentials, isLoggedIn, } from "./hub/credentials.js";
|
|
15
|
+
export type { HubCredentials } from "./hub/credentials.js";
|
|
16
|
+
export { exchangeAuthCode, refreshToken, getMe, createHive, listHives, getHive, startHive, stopHive, destroyHive, getHubUrl, HubApiError, } from "./hub/client.js";
|
|
17
|
+
export type { HubUser, Hive, HiveStatus, HiveTier, CreateHiveOptions, } from "./hub/client.js";
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Public API — re-export modules for programmatic use
|
|
2
|
+
export { PACKAGES, BUNDLES, INTEGRATIONS, getBundlePackages, getActiveIntegrations, getNewIntegrations, getLostIntegrations, getNpmName, isKnownPackage, getAllPackageNames, } from "./packages/registry.js";
|
|
3
|
+
export { installPackages, uninstallPackage, getInstalledVersion, getLatestVersion, updatePackages, isClaudeCliAvailable, getGlobalPackagePath, } from "./packages/installer.js";
|
|
4
|
+
export { isInstalledPlugin, registerPlugin, } from "./packages/plugin.js";
|
|
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";
|
|
7
|
+
export { readKey, writeKey, deleteKey, listKeys, hasKey, } from "./config/keys.js";
|
|
8
|
+
export { runAllChecks } from "./doctor/checks.js";
|
|
9
|
+
export { readCredentials, writeCredentials, deleteCredentials, isLoggedIn, } from "./hub/credentials.js";
|
|
10
|
+
export { exchangeAuthCode, refreshToken, getMe, createHive, listHives, getHive, startHive, stopHive, destroyHive, getHubUrl, HubApiError, } from "./hub/client.js";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface InstallResult {
|
|
2
|
+
package: string;
|
|
3
|
+
success: boolean;
|
|
4
|
+
version?: string;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface UpdateResult {
|
|
8
|
+
package: string;
|
|
9
|
+
previousVersion: string | null;
|
|
10
|
+
newVersion: string | null;
|
|
11
|
+
updated: boolean;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Install packages globally via npm.
|
|
16
|
+
*/
|
|
17
|
+
export declare function installPackages(packages: string[]): Promise<InstallResult[]>;
|
|
18
|
+
/**
|
|
19
|
+
* Uninstall a package globally via npm.
|
|
20
|
+
*/
|
|
21
|
+
export declare function uninstallPackage(packageName: string): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Get the currently installed global version of a package, or null if not installed.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getInstalledVersion(packageName: string): Promise<string | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Get the latest published version of a package from the registry.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getLatestVersion(packageName: string): Promise<string | null>;
|
|
30
|
+
/**
|
|
31
|
+
* Update installed packages to their latest versions.
|
|
32
|
+
*/
|
|
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>;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { getNpmName } from "./registry.js";
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
/**
|
|
6
|
+
* Install packages globally via npm.
|
|
7
|
+
*/
|
|
8
|
+
export async function installPackages(packages) {
|
|
9
|
+
const results = [];
|
|
10
|
+
for (const pkg of packages) {
|
|
11
|
+
results.push(await installNpmPackage(pkg));
|
|
12
|
+
}
|
|
13
|
+
return results;
|
|
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
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Uninstall a package globally via npm.
|
|
31
|
+
*/
|
|
32
|
+
export async function uninstallPackage(packageName) {
|
|
33
|
+
const npmName = getNpmName(packageName);
|
|
34
|
+
await execFileAsync("npm", ["uninstall", "-g", npmName], {
|
|
35
|
+
timeout: 60_000,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get the currently installed global version of a package, or null if not installed.
|
|
40
|
+
*/
|
|
41
|
+
export async function getInstalledVersion(packageName) {
|
|
42
|
+
const npmName = getNpmName(packageName);
|
|
43
|
+
try {
|
|
44
|
+
const { stdout } = await execFileAsync("npm", ["list", "-g", npmName, "--json", "--depth=0"], { timeout: 15_000 });
|
|
45
|
+
const data = JSON.parse(stdout);
|
|
46
|
+
return data.dependencies?.[npmName]?.version ?? null;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get the latest published version of a package from the registry.
|
|
54
|
+
*/
|
|
55
|
+
export async function getLatestVersion(packageName) {
|
|
56
|
+
const npmName = getNpmName(packageName);
|
|
57
|
+
try {
|
|
58
|
+
const { stdout } = await execFileAsync("npm", ["view", npmName, "version"], { timeout: 15_000 });
|
|
59
|
+
return stdout.trim() || null;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Update installed packages to their latest versions.
|
|
67
|
+
*/
|
|
68
|
+
export async function updatePackages(packages) {
|
|
69
|
+
const results = [];
|
|
70
|
+
for (const pkg of packages) {
|
|
71
|
+
const previousVersion = await getInstalledVersion(pkg);
|
|
72
|
+
const latestVersion = await getLatestVersion(pkg);
|
|
73
|
+
if (!latestVersion) {
|
|
74
|
+
results.push({
|
|
75
|
+
package: pkg,
|
|
76
|
+
previousVersion,
|
|
77
|
+
newVersion: null,
|
|
78
|
+
updated: false,
|
|
79
|
+
error: "Could not fetch latest version",
|
|
80
|
+
});
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (previousVersion === latestVersion) {
|
|
84
|
+
results.push({
|
|
85
|
+
package: pkg,
|
|
86
|
+
previousVersion,
|
|
87
|
+
newVersion: latestVersion,
|
|
88
|
+
updated: false,
|
|
89
|
+
});
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const npmName = getNpmName(pkg);
|
|
93
|
+
try {
|
|
94
|
+
await execFileAsync("npm", ["install", "-g", `${npmName}@latest`], {
|
|
95
|
+
timeout: 120_000,
|
|
96
|
+
});
|
|
97
|
+
const newVersion = await getInstalledVersion(pkg);
|
|
98
|
+
results.push({
|
|
99
|
+
package: pkg,
|
|
100
|
+
previousVersion,
|
|
101
|
+
newVersion,
|
|
102
|
+
updated: true,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
107
|
+
results.push({
|
|
108
|
+
package: pkg,
|
|
109
|
+
previousVersion,
|
|
110
|
+
newVersion: null,
|
|
111
|
+
updated: false,
|
|
112
|
+
error: formatNpmError(message),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return results;
|
|
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
|
+
}
|
|
146
|
+
function formatNpmError(message) {
|
|
147
|
+
if (message.includes("EACCES") || message.includes("permission")) {
|
|
148
|
+
return "Permission denied — try running with sudo or configure npm prefix";
|
|
149
|
+
}
|
|
150
|
+
if (message.includes("ENOTFOUND") || message.includes("network")) {
|
|
151
|
+
return "Network error — check your internet connection";
|
|
152
|
+
}
|
|
153
|
+
if (message.includes("404") || message.includes("Not Found")) {
|
|
154
|
+
return "Package not found in npm registry";
|
|
155
|
+
}
|
|
156
|
+
// Return first line of error to keep it concise
|
|
157
|
+
return message.split("\n")[0];
|
|
158
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
// Mock child_process.execFile
|
|
3
|
+
const mockExecFile = vi.fn();
|
|
4
|
+
vi.mock("node:child_process", () => ({
|
|
5
|
+
execFile: mockExecFile,
|
|
6
|
+
}));
|
|
7
|
+
// Mock promisify to return our mock
|
|
8
|
+
vi.mock("node:util", async () => {
|
|
9
|
+
const actual = await vi.importActual("node:util");
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
promisify: () => mockExecFile,
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
const { installPackages, uninstallPackage, getInstalledVersion, getLatestVersion, updatePackages, isClaudeCliAvailable, getGlobalPackagePath, } = await import("./installer.js");
|
|
16
|
+
describe("installer", () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
mockExecFile.mockReset();
|
|
19
|
+
});
|
|
20
|
+
describe("getInstalledVersion", () => {
|
|
21
|
+
it("returns version when package is installed", async () => {
|
|
22
|
+
mockExecFile.mockResolvedValueOnce({
|
|
23
|
+
stdout: JSON.stringify({
|
|
24
|
+
dependencies: { minimem: { version: "0.1.0" } },
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
const version = await getInstalledVersion("minimem");
|
|
28
|
+
expect(version).toBe("0.1.0");
|
|
29
|
+
expect(mockExecFile).toHaveBeenCalledWith("npm", ["list", "-g", "minimem", "--json", "--depth=0"], { timeout: 15_000 });
|
|
30
|
+
});
|
|
31
|
+
it("returns null when package is not installed", async () => {
|
|
32
|
+
mockExecFile.mockRejectedValueOnce(new Error("not found"));
|
|
33
|
+
const version = await getInstalledVersion("not-installed");
|
|
34
|
+
expect(version).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
it("returns null when dependencies field is missing", async () => {
|
|
37
|
+
mockExecFile.mockResolvedValueOnce({ stdout: JSON.stringify({}) });
|
|
38
|
+
const version = await getInstalledVersion("minimem");
|
|
39
|
+
expect(version).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe("getLatestVersion", () => {
|
|
43
|
+
it("returns version from npm registry", async () => {
|
|
44
|
+
mockExecFile.mockResolvedValueOnce({ stdout: "0.2.0\n" });
|
|
45
|
+
const version = await getLatestVersion("minimem");
|
|
46
|
+
expect(version).toBe("0.2.0");
|
|
47
|
+
expect(mockExecFile).toHaveBeenCalledWith("npm", ["view", "minimem", "version"], { timeout: 15_000 });
|
|
48
|
+
});
|
|
49
|
+
it("returns null on network error", async () => {
|
|
50
|
+
mockExecFile.mockRejectedValueOnce(new Error("ENOTFOUND"));
|
|
51
|
+
const version = await getLatestVersion("minimem");
|
|
52
|
+
expect(version).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
it("returns null for empty stdout", async () => {
|
|
55
|
+
mockExecFile.mockResolvedValueOnce({ stdout: "" });
|
|
56
|
+
const version = await getLatestVersion("minimem");
|
|
57
|
+
expect(version).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe("installPackages", () => {
|
|
61
|
+
it("installs a single package successfully", async () => {
|
|
62
|
+
// First call: npm install -g
|
|
63
|
+
mockExecFile.mockResolvedValueOnce({ stdout: "", stderr: "" });
|
|
64
|
+
// Second call: npm list -g (getInstalledVersion)
|
|
65
|
+
mockExecFile.mockResolvedValueOnce({
|
|
66
|
+
stdout: JSON.stringify({
|
|
67
|
+
dependencies: { minimem: { version: "0.1.0" } },
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
const results = await installPackages(["minimem"]);
|
|
71
|
+
expect(results).toHaveLength(1);
|
|
72
|
+
expect(results[0]).toEqual({
|
|
73
|
+
package: "minimem",
|
|
74
|
+
success: true,
|
|
75
|
+
version: "0.1.0",
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
it("reports failure on install error", async () => {
|
|
79
|
+
mockExecFile.mockRejectedValueOnce(new Error("EACCES permission denied"));
|
|
80
|
+
const results = await installPackages(["minimem"]);
|
|
81
|
+
expect(results).toHaveLength(1);
|
|
82
|
+
expect(results[0].success).toBe(false);
|
|
83
|
+
expect(results[0].error).toContain("Permission denied");
|
|
84
|
+
});
|
|
85
|
+
it("installs multiple packages sequentially", async () => {
|
|
86
|
+
// Package 1: success
|
|
87
|
+
mockExecFile.mockResolvedValueOnce({ stdout: "" });
|
|
88
|
+
mockExecFile.mockResolvedValueOnce({
|
|
89
|
+
stdout: JSON.stringify({
|
|
90
|
+
dependencies: { minimem: { version: "0.1.0" } },
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
// Package 2: success
|
|
94
|
+
mockExecFile.mockResolvedValueOnce({ stdout: "" });
|
|
95
|
+
mockExecFile.mockResolvedValueOnce({
|
|
96
|
+
stdout: JSON.stringify({
|
|
97
|
+
dependencies: { opentasks: { version: "0.0.3" } },
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
const results = await installPackages(["minimem", "opentasks"]);
|
|
101
|
+
expect(results).toHaveLength(2);
|
|
102
|
+
expect(results[0].success).toBe(true);
|
|
103
|
+
expect(results[1].success).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
it("handles mixed success and failure", async () => {
|
|
106
|
+
// Package 1: success
|
|
107
|
+
mockExecFile.mockResolvedValueOnce({ stdout: "" });
|
|
108
|
+
mockExecFile.mockResolvedValueOnce({
|
|
109
|
+
stdout: JSON.stringify({
|
|
110
|
+
dependencies: { minimem: { version: "0.1.0" } },
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
113
|
+
// Package 2: failure
|
|
114
|
+
mockExecFile.mockRejectedValueOnce(new Error("404 Not Found"));
|
|
115
|
+
const results = await installPackages(["minimem", "bad-package"]);
|
|
116
|
+
expect(results[0].success).toBe(true);
|
|
117
|
+
expect(results[1].success).toBe(false);
|
|
118
|
+
expect(results[1].error).toContain("not found");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe("uninstallPackage", () => {
|
|
122
|
+
it("calls npm uninstall -g", async () => {
|
|
123
|
+
mockExecFile.mockResolvedValueOnce({ stdout: "" });
|
|
124
|
+
await uninstallPackage("minimem");
|
|
125
|
+
expect(mockExecFile).toHaveBeenCalledWith("npm", ["uninstall", "-g", "minimem"], { timeout: 60_000 });
|
|
126
|
+
});
|
|
127
|
+
it("throws on failure", async () => {
|
|
128
|
+
mockExecFile.mockRejectedValueOnce(new Error("failed"));
|
|
129
|
+
await expect(uninstallPackage("minimem")).rejects.toThrow("failed");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe("updatePackages", () => {
|
|
133
|
+
it("reports package as up to date when versions match", async () => {
|
|
134
|
+
// getInstalledVersion
|
|
135
|
+
mockExecFile.mockResolvedValueOnce({
|
|
136
|
+
stdout: JSON.stringify({
|
|
137
|
+
dependencies: { minimem: { version: "0.1.0" } },
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
// getLatestVersion
|
|
141
|
+
mockExecFile.mockResolvedValueOnce({ stdout: "0.1.0\n" });
|
|
142
|
+
const results = await updatePackages(["minimem"]);
|
|
143
|
+
expect(results).toHaveLength(1);
|
|
144
|
+
expect(results[0].updated).toBe(false);
|
|
145
|
+
expect(results[0].previousVersion).toBe("0.1.0");
|
|
146
|
+
expect(results[0].newVersion).toBe("0.1.0");
|
|
147
|
+
});
|
|
148
|
+
it("updates package when newer version is available", async () => {
|
|
149
|
+
// getInstalledVersion (before)
|
|
150
|
+
mockExecFile.mockResolvedValueOnce({
|
|
151
|
+
stdout: JSON.stringify({
|
|
152
|
+
dependencies: { minimem: { version: "0.1.0" } },
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
155
|
+
// getLatestVersion
|
|
156
|
+
mockExecFile.mockResolvedValueOnce({ stdout: "0.2.0\n" });
|
|
157
|
+
// npm install -g minimem@latest
|
|
158
|
+
mockExecFile.mockResolvedValueOnce({ stdout: "" });
|
|
159
|
+
// getInstalledVersion (after)
|
|
160
|
+
mockExecFile.mockResolvedValueOnce({
|
|
161
|
+
stdout: JSON.stringify({
|
|
162
|
+
dependencies: { minimem: { version: "0.2.0" } },
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
const results = await updatePackages(["minimem"]);
|
|
166
|
+
expect(results).toHaveLength(1);
|
|
167
|
+
expect(results[0].updated).toBe(true);
|
|
168
|
+
expect(results[0].previousVersion).toBe("0.1.0");
|
|
169
|
+
expect(results[0].newVersion).toBe("0.2.0");
|
|
170
|
+
});
|
|
171
|
+
it("reports error when latest version cannot be fetched", async () => {
|
|
172
|
+
// getInstalledVersion
|
|
173
|
+
mockExecFile.mockResolvedValueOnce({
|
|
174
|
+
stdout: JSON.stringify({
|
|
175
|
+
dependencies: { minimem: { version: "0.1.0" } },
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
// getLatestVersion fails
|
|
179
|
+
mockExecFile.mockRejectedValueOnce(new Error("network error"));
|
|
180
|
+
const results = await updatePackages(["minimem"]);
|
|
181
|
+
expect(results[0].updated).toBe(false);
|
|
182
|
+
expect(results[0].error).toBe("Could not fetch latest version");
|
|
183
|
+
});
|
|
184
|
+
it("reports error when install fails during update", async () => {
|
|
185
|
+
// getInstalledVersion
|
|
186
|
+
mockExecFile.mockResolvedValueOnce({
|
|
187
|
+
stdout: JSON.stringify({
|
|
188
|
+
dependencies: { minimem: { version: "0.1.0" } },
|
|
189
|
+
}),
|
|
190
|
+
});
|
|
191
|
+
// getLatestVersion
|
|
192
|
+
mockExecFile.mockResolvedValueOnce({ stdout: "0.2.0\n" });
|
|
193
|
+
// npm install fails
|
|
194
|
+
mockExecFile.mockRejectedValueOnce(new Error("EACCES permission denied"));
|
|
195
|
+
const results = await updatePackages(["minimem"]);
|
|
196
|
+
expect(results[0].updated).toBe(false);
|
|
197
|
+
expect(results[0].error).toContain("Permission denied");
|
|
198
|
+
});
|
|
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
|
+
});
|
|
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 {};
|