swarmkit 0.0.1 → 0.0.2
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 +194 -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 +55 -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 +54 -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 +11 -0
- package/dist/commands/init/state.js +8 -0
- package/dist/commands/init/state.test.d.ts +1 -0
- package/dist/commands/init/state.test.js +20 -0
- package/dist/commands/init/wizard.d.ts +1 -0
- package/dist/commands/init/wizard.js +56 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +10 -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 +49 -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 +24 -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 +210 -0
- package/dist/doctor/checks.test.d.ts +1 -0
- package/dist/doctor/checks.test.js +276 -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 +16 -1
- package/dist/index.js +9 -2
- package/dist/packages/installer.d.ts +33 -0
- package/dist/packages/installer.js +127 -0
- package/dist/packages/installer.test.d.ts +1 -0
- package/dist/packages/installer.test.js +200 -0
- package/dist/packages/registry.d.ts +37 -0
- package/dist/packages/registry.js +179 -0
- package/dist/packages/registry.test.d.ts +1 -0
- package/dist/packages/registry.test.js +199 -0
- package/dist/packages/setup.d.ts +48 -0
- package/dist/packages/setup.js +309 -0
- package/dist/packages/setup.test.d.ts +1 -0
- package/dist/packages/setup.test.js +717 -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,210 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
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",
|
|
6
|
+
opentasks: ".opentasks",
|
|
7
|
+
minimem: ".minimem",
|
|
8
|
+
"cognitive-core": ".cognitive-core",
|
|
9
|
+
"skill-tree": ".skilltree",
|
|
10
|
+
"self-driving-repo": ".self-driving",
|
|
11
|
+
};
|
|
12
|
+
/** Embedding provider → required key name */
|
|
13
|
+
const EMBEDDING_KEY_MAP = {
|
|
14
|
+
openai: "openai",
|
|
15
|
+
gemini: "gemini",
|
|
16
|
+
};
|
|
17
|
+
/** Packages that need an embedding provider to work fully */
|
|
18
|
+
const EMBEDDING_CONSUMERS = ["minimem", "cognitive-core", "skill-tree"];
|
|
19
|
+
// ── Package checks ──────────────────────────────────────────────
|
|
20
|
+
/**
|
|
21
|
+
* Verify each registered package is actually installed and in PATH.
|
|
22
|
+
*/
|
|
23
|
+
export async function checkPackages(ctx) {
|
|
24
|
+
const results = [];
|
|
25
|
+
for (const pkg of ctx.installedPackages) {
|
|
26
|
+
const version = await ctx.getInstalledVersion(pkg);
|
|
27
|
+
if (version) {
|
|
28
|
+
results.push({
|
|
29
|
+
name: pkg,
|
|
30
|
+
status: "pass",
|
|
31
|
+
message: `${pkg} ${version}`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
results.push({
|
|
36
|
+
name: pkg,
|
|
37
|
+
status: "fail",
|
|
38
|
+
message: `${pkg} not found in PATH`,
|
|
39
|
+
fix: `swarmkit add ${pkg}`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
// ── Credential checks ───────────────────────────────────────────
|
|
46
|
+
/**
|
|
47
|
+
* Check that required API keys are available (stored or in env).
|
|
48
|
+
*/
|
|
49
|
+
export function checkCredentials(ctx) {
|
|
50
|
+
const results = [];
|
|
51
|
+
// Anthropic key — needed for running agents
|
|
52
|
+
const hasAnthropicStored = ctx.storedKeys.includes("anthropic");
|
|
53
|
+
const hasAnthropicEnv = !!ctx.env["ANTHROPIC_API_KEY"];
|
|
54
|
+
if (hasAnthropicStored || hasAnthropicEnv) {
|
|
55
|
+
const source = hasAnthropicEnv ? "environment" : "stored";
|
|
56
|
+
results.push({
|
|
57
|
+
name: "anthropic-key",
|
|
58
|
+
status: "pass",
|
|
59
|
+
message: `Anthropic API key (${source})`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
results.push({
|
|
64
|
+
name: "anthropic-key",
|
|
65
|
+
status: "warn",
|
|
66
|
+
message: "Anthropic API key not found — agents will not be able to run",
|
|
67
|
+
fix: "swarmkit init (or set ANTHROPIC_API_KEY)",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// Embedding key — needed if embedding provider is configured
|
|
71
|
+
const needsEmbeddings = ctx.installedPackages.some((p) => EMBEDDING_CONSUMERS.includes(p));
|
|
72
|
+
if (needsEmbeddings && ctx.embeddingProvider) {
|
|
73
|
+
const requiredKey = EMBEDDING_KEY_MAP[ctx.embeddingProvider];
|
|
74
|
+
if (ctx.embeddingProvider === "local") {
|
|
75
|
+
results.push({
|
|
76
|
+
name: "embedding-key",
|
|
77
|
+
status: "pass",
|
|
78
|
+
message: "Embeddings using local models (no key needed)",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else if (requiredKey) {
|
|
82
|
+
const hasKeyStored = ctx.storedKeys.includes(requiredKey);
|
|
83
|
+
const envVarName = requiredKey === "openai" ? "OPENAI_API_KEY" : "GEMINI_API_KEY";
|
|
84
|
+
const hasKeyEnv = !!ctx.env[envVarName];
|
|
85
|
+
if (hasKeyStored || hasKeyEnv) {
|
|
86
|
+
const source = hasKeyEnv ? "environment" : "stored";
|
|
87
|
+
results.push({
|
|
88
|
+
name: "embedding-key",
|
|
89
|
+
status: "pass",
|
|
90
|
+
message: `${ctx.embeddingProvider} embedding key (${source})`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
results.push({
|
|
95
|
+
name: "embedding-key",
|
|
96
|
+
status: "warn",
|
|
97
|
+
message: `${ctx.embeddingProvider} API key not found — embeddings will fall back to BM25-only`,
|
|
98
|
+
fix: "swarmkit init (or set " + envVarName + ")",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else if (needsEmbeddings && !ctx.embeddingProvider) {
|
|
104
|
+
results.push({
|
|
105
|
+
name: "embedding-provider",
|
|
106
|
+
status: "warn",
|
|
107
|
+
message: "No embedding provider configured — semantic search disabled",
|
|
108
|
+
fix: "swarmkit init",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return results;
|
|
112
|
+
}
|
|
113
|
+
// ── Project config checks ───────────────────────────────────────
|
|
114
|
+
/**
|
|
115
|
+
* Check that project-level config directories exist for installed packages.
|
|
116
|
+
* Only runs when cwd is a project directory.
|
|
117
|
+
*/
|
|
118
|
+
export function checkProjectConfigs(ctx) {
|
|
119
|
+
if (!ctx.isProject)
|
|
120
|
+
return [];
|
|
121
|
+
const results = [];
|
|
122
|
+
for (const pkg of ctx.installedPackages) {
|
|
123
|
+
const configDir = PROJECT_CONFIG_DIRS[pkg];
|
|
124
|
+
if (!configDir)
|
|
125
|
+
continue; // Package has no project-level config
|
|
126
|
+
if (PACKAGES[pkg]?.globalOnly)
|
|
127
|
+
continue;
|
|
128
|
+
const fullPath = join(ctx.cwd, configDir);
|
|
129
|
+
if (ctx.exists(fullPath)) {
|
|
130
|
+
results.push({
|
|
131
|
+
name: `${pkg}-config`,
|
|
132
|
+
status: "pass",
|
|
133
|
+
message: `${configDir}/`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
results.push({
|
|
138
|
+
name: `${pkg}-config`,
|
|
139
|
+
status: "warn",
|
|
140
|
+
message: `${configDir}/ not found`,
|
|
141
|
+
fix: `swarmkit init (or ${pkg} init)`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return results;
|
|
146
|
+
}
|
|
147
|
+
// ── Integration checks ──────────────────────────────────────────
|
|
148
|
+
/**
|
|
149
|
+
* Check that active integrations have both packages installed.
|
|
150
|
+
* Reports on the state of known integration pairs.
|
|
151
|
+
*/
|
|
152
|
+
export async function checkIntegrations(ctx) {
|
|
153
|
+
const integrations = getActiveIntegrations(ctx.installedPackages);
|
|
154
|
+
const results = [];
|
|
155
|
+
for (const integration of integrations) {
|
|
156
|
+
const [a, b] = integration.packages;
|
|
157
|
+
const versionA = await ctx.getInstalledVersion(a);
|
|
158
|
+
const versionB = await ctx.getInstalledVersion(b);
|
|
159
|
+
if (versionA && versionB) {
|
|
160
|
+
results.push({
|
|
161
|
+
name: `${a}+${b}`,
|
|
162
|
+
status: "pass",
|
|
163
|
+
message: `${a} ↔ ${b}`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
const missing = !versionA ? a : b;
|
|
168
|
+
results.push({
|
|
169
|
+
name: `${a}+${b}`,
|
|
170
|
+
status: "fail",
|
|
171
|
+
message: `${a} ↔ ${b} — ${missing} not found`,
|
|
172
|
+
fix: `swarmkit add ${missing}`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return results;
|
|
177
|
+
}
|
|
178
|
+
// ── Global config check ─────────────────────────────────────────
|
|
179
|
+
/**
|
|
180
|
+
* Check that the global swarmkit config exists.
|
|
181
|
+
*/
|
|
182
|
+
export function checkGlobalConfig(ctx) {
|
|
183
|
+
const configPath = join(ctx.env["HOME"] ?? "", ".swarmkit", "config.json");
|
|
184
|
+
if (ctx.exists(configPath)) {
|
|
185
|
+
return {
|
|
186
|
+
name: "global-config",
|
|
187
|
+
status: "pass",
|
|
188
|
+
message: "~/.swarmkit/config.json",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
name: "global-config",
|
|
193
|
+
status: "fail",
|
|
194
|
+
message: "~/.swarmkit/config.json not found",
|
|
195
|
+
fix: "swarmkit init",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
export async function runAllChecks(ctx) {
|
|
199
|
+
const [packages, integrations] = await Promise.all([
|
|
200
|
+
checkPackages(ctx),
|
|
201
|
+
checkIntegrations(ctx),
|
|
202
|
+
]);
|
|
203
|
+
return {
|
|
204
|
+
packages,
|
|
205
|
+
credentials: checkCredentials(ctx),
|
|
206
|
+
projectConfigs: checkProjectConfigs(ctx),
|
|
207
|
+
integrations,
|
|
208
|
+
globalConfig: checkGlobalConfig(ctx),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { checkPackages, checkCredentials, checkProjectConfigs, checkIntegrations, checkGlobalConfig, runAllChecks, } from "./checks.js";
|
|
3
|
+
function createContext(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
installedPackages: [],
|
|
6
|
+
embeddingProvider: undefined,
|
|
7
|
+
storedKeys: [],
|
|
8
|
+
cwd: "/tmp/test-project",
|
|
9
|
+
isProject: false,
|
|
10
|
+
getInstalledVersion: async () => null,
|
|
11
|
+
exists: () => false,
|
|
12
|
+
env: {},
|
|
13
|
+
...overrides,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
describe("checkPackages", () => {
|
|
17
|
+
it("passes when all packages are installed", async () => {
|
|
18
|
+
const ctx = createContext({
|
|
19
|
+
installedPackages: ["macro-agent", "minimem"],
|
|
20
|
+
getInstalledVersion: async (pkg) => {
|
|
21
|
+
const versions = {
|
|
22
|
+
"macro-agent": "0.1.0",
|
|
23
|
+
minimem: "0.2.0",
|
|
24
|
+
};
|
|
25
|
+
return versions[pkg] ?? null;
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const results = await checkPackages(ctx);
|
|
29
|
+
expect(results).toHaveLength(2);
|
|
30
|
+
expect(results[0].status).toBe("pass");
|
|
31
|
+
expect(results[0].message).toContain("macro-agent");
|
|
32
|
+
expect(results[0].message).toContain("0.1.0");
|
|
33
|
+
expect(results[1].status).toBe("pass");
|
|
34
|
+
});
|
|
35
|
+
it("fails when a package is not found", async () => {
|
|
36
|
+
const ctx = createContext({
|
|
37
|
+
installedPackages: ["macro-agent", "minimem"],
|
|
38
|
+
getInstalledVersion: async (pkg) => pkg === "macro-agent" ? "0.1.0" : null,
|
|
39
|
+
});
|
|
40
|
+
const results = await checkPackages(ctx);
|
|
41
|
+
expect(results[0].status).toBe("pass");
|
|
42
|
+
expect(results[1].status).toBe("fail");
|
|
43
|
+
expect(results[1].message).toContain("not found");
|
|
44
|
+
expect(results[1].fix).toBe("swarmkit add minimem");
|
|
45
|
+
});
|
|
46
|
+
it("returns empty for no packages", async () => {
|
|
47
|
+
const ctx = createContext();
|
|
48
|
+
const results = await checkPackages(ctx);
|
|
49
|
+
expect(results).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe("checkCredentials", () => {
|
|
53
|
+
it("passes when anthropic key is stored", () => {
|
|
54
|
+
const ctx = createContext({ storedKeys: ["anthropic"] });
|
|
55
|
+
const results = checkCredentials(ctx);
|
|
56
|
+
const anthropic = results.find((r) => r.name === "anthropic-key");
|
|
57
|
+
expect(anthropic?.status).toBe("pass");
|
|
58
|
+
expect(anthropic?.message).toContain("stored");
|
|
59
|
+
});
|
|
60
|
+
it("passes when anthropic key is in env", () => {
|
|
61
|
+
const ctx = createContext({ env: { ANTHROPIC_API_KEY: "sk-ant-test" } });
|
|
62
|
+
const results = checkCredentials(ctx);
|
|
63
|
+
const anthropic = results.find((r) => r.name === "anthropic-key");
|
|
64
|
+
expect(anthropic?.status).toBe("pass");
|
|
65
|
+
expect(anthropic?.message).toContain("environment");
|
|
66
|
+
});
|
|
67
|
+
it("warns when anthropic key is missing", () => {
|
|
68
|
+
const ctx = createContext();
|
|
69
|
+
const results = checkCredentials(ctx);
|
|
70
|
+
const anthropic = results.find((r) => r.name === "anthropic-key");
|
|
71
|
+
expect(anthropic?.status).toBe("warn");
|
|
72
|
+
expect(anthropic?.fix).toBeTruthy();
|
|
73
|
+
});
|
|
74
|
+
it("checks embedding key when provider is openai and consumers installed", () => {
|
|
75
|
+
const ctx = createContext({
|
|
76
|
+
installedPackages: ["minimem"],
|
|
77
|
+
embeddingProvider: "openai",
|
|
78
|
+
storedKeys: ["openai"],
|
|
79
|
+
});
|
|
80
|
+
const results = checkCredentials(ctx);
|
|
81
|
+
const embedding = results.find((r) => r.name === "embedding-key");
|
|
82
|
+
expect(embedding?.status).toBe("pass");
|
|
83
|
+
});
|
|
84
|
+
it("warns when embedding key is missing for configured provider", () => {
|
|
85
|
+
const ctx = createContext({
|
|
86
|
+
installedPackages: ["minimem"],
|
|
87
|
+
embeddingProvider: "openai",
|
|
88
|
+
storedKeys: [],
|
|
89
|
+
});
|
|
90
|
+
const results = checkCredentials(ctx);
|
|
91
|
+
const embedding = results.find((r) => r.name === "embedding-key");
|
|
92
|
+
expect(embedding?.status).toBe("warn");
|
|
93
|
+
expect(embedding?.message).toContain("BM25-only");
|
|
94
|
+
});
|
|
95
|
+
it("passes embedding key from env", () => {
|
|
96
|
+
const ctx = createContext({
|
|
97
|
+
installedPackages: ["minimem"],
|
|
98
|
+
embeddingProvider: "openai",
|
|
99
|
+
env: { OPENAI_API_KEY: "sk-test" },
|
|
100
|
+
});
|
|
101
|
+
const results = checkCredentials(ctx);
|
|
102
|
+
const embedding = results.find((r) => r.name === "embedding-key");
|
|
103
|
+
expect(embedding?.status).toBe("pass");
|
|
104
|
+
expect(embedding?.message).toContain("environment");
|
|
105
|
+
});
|
|
106
|
+
it("passes for local embedding provider (no key needed)", () => {
|
|
107
|
+
const ctx = createContext({
|
|
108
|
+
installedPackages: ["minimem"],
|
|
109
|
+
embeddingProvider: "local",
|
|
110
|
+
});
|
|
111
|
+
const results = checkCredentials(ctx);
|
|
112
|
+
const embedding = results.find((r) => r.name === "embedding-key");
|
|
113
|
+
expect(embedding?.status).toBe("pass");
|
|
114
|
+
expect(embedding?.message).toContain("local");
|
|
115
|
+
});
|
|
116
|
+
it("warns when embedding consumers installed but no provider configured", () => {
|
|
117
|
+
const ctx = createContext({
|
|
118
|
+
installedPackages: ["cognitive-core"],
|
|
119
|
+
embeddingProvider: undefined,
|
|
120
|
+
});
|
|
121
|
+
const results = checkCredentials(ctx);
|
|
122
|
+
const embedding = results.find((r) => r.name === "embedding-provider");
|
|
123
|
+
expect(embedding?.status).toBe("warn");
|
|
124
|
+
});
|
|
125
|
+
it("skips embedding check when no embedding consumers installed", () => {
|
|
126
|
+
const ctx = createContext({
|
|
127
|
+
installedPackages: ["macro-agent", "opentasks"],
|
|
128
|
+
embeddingProvider: undefined,
|
|
129
|
+
});
|
|
130
|
+
const results = checkCredentials(ctx);
|
|
131
|
+
const embedding = results.find((r) => r.name === "embedding-key" || r.name === "embedding-provider");
|
|
132
|
+
expect(embedding).toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
it("checks gemini key via GEMINI_API_KEY env var", () => {
|
|
135
|
+
const ctx = createContext({
|
|
136
|
+
installedPackages: ["minimem"],
|
|
137
|
+
embeddingProvider: "gemini",
|
|
138
|
+
env: { GEMINI_API_KEY: "test-key" },
|
|
139
|
+
});
|
|
140
|
+
const results = checkCredentials(ctx);
|
|
141
|
+
const embedding = results.find((r) => r.name === "embedding-key");
|
|
142
|
+
expect(embedding?.status).toBe("pass");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
describe("checkProjectConfigs", () => {
|
|
146
|
+
it("returns empty when not in a project", () => {
|
|
147
|
+
const ctx = createContext({
|
|
148
|
+
isProject: false,
|
|
149
|
+
installedPackages: ["macro-agent"],
|
|
150
|
+
});
|
|
151
|
+
const results = checkProjectConfigs(ctx);
|
|
152
|
+
expect(results).toEqual([]);
|
|
153
|
+
});
|
|
154
|
+
it("passes when config directory exists", () => {
|
|
155
|
+
const ctx = createContext({
|
|
156
|
+
isProject: true,
|
|
157
|
+
installedPackages: ["macro-agent"],
|
|
158
|
+
exists: (path) => path.endsWith(".multiagent"),
|
|
159
|
+
});
|
|
160
|
+
const results = checkProjectConfigs(ctx);
|
|
161
|
+
expect(results).toHaveLength(1);
|
|
162
|
+
expect(results[0].status).toBe("pass");
|
|
163
|
+
expect(results[0].message).toContain(".multiagent");
|
|
164
|
+
});
|
|
165
|
+
it("warns when config directory is missing", () => {
|
|
166
|
+
const ctx = createContext({
|
|
167
|
+
isProject: true,
|
|
168
|
+
installedPackages: ["opentasks"],
|
|
169
|
+
exists: () => false,
|
|
170
|
+
});
|
|
171
|
+
const results = checkProjectConfigs(ctx);
|
|
172
|
+
expect(results).toHaveLength(1);
|
|
173
|
+
expect(results[0].status).toBe("warn");
|
|
174
|
+
expect(results[0].message).toContain(".opentasks");
|
|
175
|
+
expect(results[0].fix).toContain("opentasks init");
|
|
176
|
+
});
|
|
177
|
+
it("skips globalOnly packages", () => {
|
|
178
|
+
const ctx = createContext({
|
|
179
|
+
isProject: true,
|
|
180
|
+
installedPackages: ["agent-iam"],
|
|
181
|
+
exists: () => false,
|
|
182
|
+
});
|
|
183
|
+
const results = checkProjectConfigs(ctx);
|
|
184
|
+
expect(results).toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
it("checks multiple packages", () => {
|
|
187
|
+
const existingDirs = new Set(["/tmp/test-project/.multiagent"]);
|
|
188
|
+
const ctx = createContext({
|
|
189
|
+
isProject: true,
|
|
190
|
+
cwd: "/tmp/test-project",
|
|
191
|
+
installedPackages: ["macro-agent", "opentasks", "minimem"],
|
|
192
|
+
exists: (path) => existingDirs.has(path),
|
|
193
|
+
});
|
|
194
|
+
const results = checkProjectConfigs(ctx);
|
|
195
|
+
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
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
describe("checkIntegrations", () => {
|
|
202
|
+
it("passes when both packages in an integration are installed", async () => {
|
|
203
|
+
const ctx = createContext({
|
|
204
|
+
installedPackages: ["macro-agent", "opentasks"],
|
|
205
|
+
getInstalledVersion: async () => "0.1.0",
|
|
206
|
+
});
|
|
207
|
+
const results = await checkIntegrations(ctx);
|
|
208
|
+
expect(results).toHaveLength(1);
|
|
209
|
+
expect(results[0].status).toBe("pass");
|
|
210
|
+
expect(results[0].message).toContain("macro-agent");
|
|
211
|
+
expect(results[0].message).toContain("opentasks");
|
|
212
|
+
});
|
|
213
|
+
it("fails when one package in an integration is missing from PATH", async () => {
|
|
214
|
+
const ctx = createContext({
|
|
215
|
+
installedPackages: ["macro-agent", "opentasks"],
|
|
216
|
+
getInstalledVersion: async (pkg) => pkg === "macro-agent" ? "0.1.0" : null,
|
|
217
|
+
});
|
|
218
|
+
const results = await checkIntegrations(ctx);
|
|
219
|
+
expect(results).toHaveLength(1);
|
|
220
|
+
expect(results[0].status).toBe("fail");
|
|
221
|
+
expect(results[0].fix).toContain("opentasks");
|
|
222
|
+
});
|
|
223
|
+
it("returns empty when no integrations are active", async () => {
|
|
224
|
+
const ctx = createContext({
|
|
225
|
+
installedPackages: ["minimem"],
|
|
226
|
+
getInstalledVersion: async () => "0.1.0",
|
|
227
|
+
});
|
|
228
|
+
const results = await checkIntegrations(ctx);
|
|
229
|
+
expect(results).toEqual([]);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
describe("checkGlobalConfig", () => {
|
|
233
|
+
it("passes when config file exists", () => {
|
|
234
|
+
const ctx = createContext({
|
|
235
|
+
env: { HOME: "/home/testuser" },
|
|
236
|
+
exists: (path) => path === "/home/testuser/.swarmkit/config.json",
|
|
237
|
+
});
|
|
238
|
+
const result = checkGlobalConfig(ctx);
|
|
239
|
+
expect(result.status).toBe("pass");
|
|
240
|
+
});
|
|
241
|
+
it("fails when config file is missing", () => {
|
|
242
|
+
const ctx = createContext({
|
|
243
|
+
env: { HOME: "/home/testuser" },
|
|
244
|
+
exists: () => false,
|
|
245
|
+
});
|
|
246
|
+
const result = checkGlobalConfig(ctx);
|
|
247
|
+
expect(result.status).toBe("fail");
|
|
248
|
+
expect(result.fix).toBe("swarmkit init");
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
describe("runAllChecks", () => {
|
|
252
|
+
it("returns a complete report", async () => {
|
|
253
|
+
const ctx = createContext({
|
|
254
|
+
installedPackages: ["macro-agent", "minimem"],
|
|
255
|
+
embeddingProvider: "openai",
|
|
256
|
+
storedKeys: ["anthropic", "openai"],
|
|
257
|
+
isProject: true,
|
|
258
|
+
env: { HOME: "/home/testuser" },
|
|
259
|
+
getInstalledVersion: async () => "0.1.0",
|
|
260
|
+
exists: () => true,
|
|
261
|
+
});
|
|
262
|
+
const report = await runAllChecks(ctx);
|
|
263
|
+
expect(report.packages).toHaveLength(2);
|
|
264
|
+
expect(report.credentials.length).toBeGreaterThan(0);
|
|
265
|
+
expect(report.projectConfigs.length).toBeGreaterThan(0);
|
|
266
|
+
expect(report.globalConfig).toBeDefined();
|
|
267
|
+
expect(report.globalConfig.status).toBe("pass");
|
|
268
|
+
});
|
|
269
|
+
it("returns empty sections when no packages installed", async () => {
|
|
270
|
+
const ctx = createContext({ env: { HOME: "/tmp" } });
|
|
271
|
+
const report = await runAllChecks(ctx);
|
|
272
|
+
expect(report.packages).toEqual([]);
|
|
273
|
+
expect(report.integrations).toEqual([]);
|
|
274
|
+
expect(report.projectConfigs).toEqual([]);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type CheckStatus = "pass" | "fail" | "warn";
|
|
2
|
+
export interface CheckResult {
|
|
3
|
+
/** What was checked */
|
|
4
|
+
name: string;
|
|
5
|
+
/** Pass, fail, or warning */
|
|
6
|
+
status: CheckStatus;
|
|
7
|
+
/** Human-readable description of the result */
|
|
8
|
+
message: string;
|
|
9
|
+
/** Suggested fix command, if applicable */
|
|
10
|
+
fix?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface CheckContext {
|
|
13
|
+
/** Packages registered in swarmkit config */
|
|
14
|
+
installedPackages: string[];
|
|
15
|
+
/** Embedding provider from config */
|
|
16
|
+
embeddingProvider?: string;
|
|
17
|
+
/** API key providers that have stored keys */
|
|
18
|
+
storedKeys: string[];
|
|
19
|
+
/** Current working directory */
|
|
20
|
+
cwd: string;
|
|
21
|
+
/** Whether cwd looks like a project (has .git or package.json) */
|
|
22
|
+
isProject: boolean;
|
|
23
|
+
/** Resolve installed version — injectable for testing */
|
|
24
|
+
getInstalledVersion: (pkg: string) => Promise<string | null>;
|
|
25
|
+
/** Check if a file/directory exists — injectable for testing */
|
|
26
|
+
exists: (path: string) => boolean;
|
|
27
|
+
/** Environment variables */
|
|
28
|
+
env: Record<string, string | undefined>;
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface LoginResult {
|
|
2
|
+
token: string;
|
|
3
|
+
apiUrl: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Run the login flow on a given port:
|
|
7
|
+
* 1. Start a local HTTP server to receive the OAuth callback
|
|
8
|
+
* 2. Wait for the auth code callback
|
|
9
|
+
* 3. Exchange the code for a JWT
|
|
10
|
+
* 4. Store credentials locally
|
|
11
|
+
*/
|
|
12
|
+
export declare function runLoginFlow(port: number): Promise<LoginResult>;
|
|
13
|
+
/** Build the URL the user should open in their browser */
|
|
14
|
+
export declare function getLoginUrl(callbackPort: number): string;
|
|
15
|
+
/** Find an available port in our range */
|
|
16
|
+
export declare function findAvailablePort(): Promise<number>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { URL } from "node:url";
|
|
3
|
+
import { exchangeAuthCode, getHubUrl } from "./client.js";
|
|
4
|
+
import { writeCredentials } from "./credentials.js";
|
|
5
|
+
const CALLBACK_PORT_START = 9876;
|
|
6
|
+
const CALLBACK_PORT_END = 9886;
|
|
7
|
+
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
8
|
+
/**
|
|
9
|
+
* Run the login flow on a given port:
|
|
10
|
+
* 1. Start a local HTTP server to receive the OAuth callback
|
|
11
|
+
* 2. Wait for the auth code callback
|
|
12
|
+
* 3. Exchange the code for a JWT
|
|
13
|
+
* 4. Store credentials locally
|
|
14
|
+
*/
|
|
15
|
+
export async function runLoginFlow(port) {
|
|
16
|
+
const { code, shutdown } = await waitForCallback(port);
|
|
17
|
+
try {
|
|
18
|
+
const token = await exchangeAuthCode(code);
|
|
19
|
+
const hubUrl = getHubUrl();
|
|
20
|
+
writeCredentials({ token, apiUrl: hubUrl });
|
|
21
|
+
return { token, apiUrl: hubUrl };
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
await shutdown();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Build the URL the user should open in their browser */
|
|
28
|
+
export function getLoginUrl(callbackPort) {
|
|
29
|
+
const callbackUrl = `http://localhost:${callbackPort}/callback`;
|
|
30
|
+
const hubBase = getHubUrl();
|
|
31
|
+
return `${hubBase}/auth/cli?return_to=${encodeURIComponent(callbackUrl)}`;
|
|
32
|
+
}
|
|
33
|
+
/** Find an available port in our range */
|
|
34
|
+
export async function findAvailablePort() {
|
|
35
|
+
for (let port = CALLBACK_PORT_START; port <= CALLBACK_PORT_END; port++) {
|
|
36
|
+
const available = await isPortAvailable(port);
|
|
37
|
+
if (available)
|
|
38
|
+
return port;
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`No available port found in range ${CALLBACK_PORT_START}-${CALLBACK_PORT_END}`);
|
|
41
|
+
}
|
|
42
|
+
function isPortAvailable(port) {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
const server = createServer();
|
|
45
|
+
server.once("error", () => resolve(false));
|
|
46
|
+
server.once("listening", () => {
|
|
47
|
+
server.close(() => resolve(true));
|
|
48
|
+
});
|
|
49
|
+
server.listen(port, "127.0.0.1");
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Start a local server and wait for the OAuth callback with an auth code */
|
|
53
|
+
function waitForCallback(port) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const server = createServer((req, res) => {
|
|
56
|
+
if (!req.url?.startsWith("/callback")) {
|
|
57
|
+
res.writeHead(404);
|
|
58
|
+
res.end("Not found");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
62
|
+
const code = url.searchParams.get("code");
|
|
63
|
+
const error = url.searchParams.get("error");
|
|
64
|
+
if (error) {
|
|
65
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
66
|
+
res.end(errorPage(error));
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
reject(new Error(`Authentication failed: ${error}`));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!code) {
|
|
72
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
73
|
+
res.end(errorPage("No authorization code received"));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
77
|
+
res.end(successPage());
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
resolve({
|
|
80
|
+
code,
|
|
81
|
+
shutdown: () => new Promise((res) => server.close(() => res())),
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
server.listen(port, "127.0.0.1");
|
|
85
|
+
const timer = setTimeout(() => {
|
|
86
|
+
server.close();
|
|
87
|
+
reject(new Error("Login timed out — no callback received within 5 minutes"));
|
|
88
|
+
}, CALLBACK_TIMEOUT_MS);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function successPage() {
|
|
92
|
+
return `<!DOCTYPE html>
|
|
93
|
+
<html>
|
|
94
|
+
<head><title>swarmkit — logged in</title></head>
|
|
95
|
+
<body style="font-family: system-ui, sans-serif; text-align: center; padding: 60px 20px;">
|
|
96
|
+
<h1>Logged in to SwarmHub</h1>
|
|
97
|
+
<p>You can close this window and return to your terminal.</p>
|
|
98
|
+
</body>
|
|
99
|
+
</html>`;
|
|
100
|
+
}
|
|
101
|
+
function errorPage(message) {
|
|
102
|
+
return `<!DOCTYPE html>
|
|
103
|
+
<html>
|
|
104
|
+
<head><title>swarmkit — login failed</title></head>
|
|
105
|
+
<body style="font-family: system-ui, sans-serif; text-align: center; padding: 60px 20px;">
|
|
106
|
+
<h1>Login Failed</h1>
|
|
107
|
+
<p>${escapeHtml(message)}</p>
|
|
108
|
+
<p>Please close this window and try again.</p>
|
|
109
|
+
</body>
|
|
110
|
+
</html>`;
|
|
111
|
+
}
|
|
112
|
+
function escapeHtml(str) {
|
|
113
|
+
return str
|
|
114
|
+
.replace(/&/g, "&")
|
|
115
|
+
.replace(/</g, "<")
|
|
116
|
+
.replace(/>/g, ">")
|
|
117
|
+
.replace(/"/g, """);
|
|
118
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|