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,87 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { readConfig } from "../config/global.js";
|
|
6
|
+
import { listKeys } from "../config/keys.js";
|
|
7
|
+
import { PACKAGES, getActiveIntegrations } from "../packages/registry.js";
|
|
8
|
+
import { getInstalledVersion } from "../packages/installer.js";
|
|
9
|
+
import * as ui from "../utils/ui.js";
|
|
10
|
+
export function registerStatusCommand(program) {
|
|
11
|
+
program
|
|
12
|
+
.command("status")
|
|
13
|
+
.description("Show installed packages and configuration")
|
|
14
|
+
.action(async () => {
|
|
15
|
+
const config = readConfig();
|
|
16
|
+
const { installedPackages } = config;
|
|
17
|
+
console.log();
|
|
18
|
+
console.log(` ${chalk.bold("swarmkit")} ${chalk.dim("v" + getVersion())}`);
|
|
19
|
+
// Packages
|
|
20
|
+
if (installedPackages.length === 0) {
|
|
21
|
+
ui.blank();
|
|
22
|
+
ui.info("No packages installed. Run `swarmkit init` to get started.");
|
|
23
|
+
ui.blank();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
ui.heading(" Packages:");
|
|
27
|
+
const rows = [];
|
|
28
|
+
for (const pkg of installedPackages) {
|
|
29
|
+
const version = await getInstalledVersion(pkg);
|
|
30
|
+
const def = PACKAGES[pkg];
|
|
31
|
+
if (version) {
|
|
32
|
+
rows.push([
|
|
33
|
+
pkg,
|
|
34
|
+
chalk.dim(version),
|
|
35
|
+
chalk.green("installed"),
|
|
36
|
+
chalk.dim(def?.description ?? ""),
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
rows.push([
|
|
41
|
+
pkg,
|
|
42
|
+
chalk.dim("—"),
|
|
43
|
+
chalk.red("not found"),
|
|
44
|
+
chalk.dim(def?.description ?? ""),
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
ui.table(rows);
|
|
49
|
+
// Integrations
|
|
50
|
+
const integrations = getActiveIntegrations(installedPackages);
|
|
51
|
+
if (integrations.length > 0) {
|
|
52
|
+
ui.heading(" Integrations:");
|
|
53
|
+
for (const integration of integrations) {
|
|
54
|
+
const [a, b] = integration.packages;
|
|
55
|
+
ui.bullet(` ${a} ${chalk.dim("↔")} ${b} ${chalk.dim(integration.description)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Embeddings
|
|
59
|
+
if (config.embeddingProvider) {
|
|
60
|
+
ui.heading(" Embeddings:");
|
|
61
|
+
const model = config.embeddingModel ?? "default";
|
|
62
|
+
ui.bullet(` ${config.embeddingProvider} (${model})`);
|
|
63
|
+
}
|
|
64
|
+
// API Keys
|
|
65
|
+
const keys = listKeys();
|
|
66
|
+
if (keys.length > 0) {
|
|
67
|
+
ui.heading(" API Keys:");
|
|
68
|
+
const keyRows = [];
|
|
69
|
+
for (const key of keys) {
|
|
70
|
+
keyRows.push([key, chalk.green("set")]);
|
|
71
|
+
}
|
|
72
|
+
ui.table(keyRows);
|
|
73
|
+
}
|
|
74
|
+
ui.blank();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function getVersion() {
|
|
78
|
+
try {
|
|
79
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
80
|
+
const pkgPath = join(__dirname, "..", "..", "package.json");
|
|
81
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
82
|
+
return pkg.version;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return "0.0.1";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { readConfig } from "../config/global.js";
|
|
3
|
+
import { updatePackages } from "../packages/installer.js";
|
|
4
|
+
import * as ui from "../utils/ui.js";
|
|
5
|
+
export function registerUpdateCommand(program) {
|
|
6
|
+
program
|
|
7
|
+
.command("update")
|
|
8
|
+
.description("Update all installed packages to latest versions")
|
|
9
|
+
.action(async () => {
|
|
10
|
+
const config = readConfig();
|
|
11
|
+
const { installedPackages } = config;
|
|
12
|
+
if (installedPackages.length === 0) {
|
|
13
|
+
ui.info("No packages installed. Run `swarmkit init` to get started.");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
console.log();
|
|
17
|
+
console.log(` Checking for updates...`);
|
|
18
|
+
console.log();
|
|
19
|
+
const results = await updatePackages(installedPackages);
|
|
20
|
+
const rows = [];
|
|
21
|
+
let updatedCount = 0;
|
|
22
|
+
for (const result of results) {
|
|
23
|
+
if (result.error) {
|
|
24
|
+
rows.push([result.package, chalk.red("error"), chalk.dim(result.error)]);
|
|
25
|
+
}
|
|
26
|
+
else if (result.updated) {
|
|
27
|
+
updatedCount++;
|
|
28
|
+
rows.push([
|
|
29
|
+
result.package,
|
|
30
|
+
chalk.dim(result.previousVersion ?? "?") +
|
|
31
|
+
" → " +
|
|
32
|
+
chalk.green(result.newVersion ?? "?"),
|
|
33
|
+
chalk.green("updated"),
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
rows.push([
|
|
38
|
+
result.package,
|
|
39
|
+
chalk.dim(result.previousVersion ?? "?"),
|
|
40
|
+
chalk.dim("up to date"),
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
ui.table(rows);
|
|
45
|
+
ui.blank();
|
|
46
|
+
if (updatedCount > 0) {
|
|
47
|
+
ui.info(`${updatedCount} package${updatedCount === 1 ? "" : "s"} updated.`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
ui.info("All packages are up to date.");
|
|
51
|
+
}
|
|
52
|
+
ui.blank();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { isLoggedIn } from "../hub/credentials.js";
|
|
3
|
+
import { getMe, getHubUrl, HubApiError } from "../hub/client.js";
|
|
4
|
+
import * as ui from "../utils/ui.js";
|
|
5
|
+
export function registerWhoamiCommand(program) {
|
|
6
|
+
program
|
|
7
|
+
.command("whoami")
|
|
8
|
+
.description("Show the currently authenticated SwarmHub user")
|
|
9
|
+
.action(async () => {
|
|
10
|
+
if (!isLoggedIn()) {
|
|
11
|
+
ui.blank();
|
|
12
|
+
ui.info("Not logged in. Run `swarmkit login` to authenticate.");
|
|
13
|
+
ui.blank();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const user = await getMe();
|
|
18
|
+
ui.blank();
|
|
19
|
+
ui.table([
|
|
20
|
+
[chalk.dim("User"), chalk.bold(user.name)],
|
|
21
|
+
[chalk.dim("Email"), user.email],
|
|
22
|
+
[chalk.dim("Hub"), getHubUrl()],
|
|
23
|
+
]);
|
|
24
|
+
ui.blank();
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
ui.blank();
|
|
28
|
+
if (err instanceof HubApiError && err.statusCode === 401) {
|
|
29
|
+
ui.fail("Session expired. Run `swarmkit login` to re-authenticate.");
|
|
30
|
+
}
|
|
31
|
+
else if (err instanceof HubApiError) {
|
|
32
|
+
ui.fail(`SwarmHub error: ${err.message}`);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
ui.fail("Could not reach SwarmHub.");
|
|
36
|
+
}
|
|
37
|
+
ui.blank();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface GlobalConfig {
|
|
2
|
+
/** Packages installed by swarmkit */
|
|
3
|
+
installedPackages: string[];
|
|
4
|
+
/** Embedding provider preference */
|
|
5
|
+
embeddingProvider?: "openai" | "gemini" | "local";
|
|
6
|
+
/** Embedding model (provider-specific) */
|
|
7
|
+
embeddingModel?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Get the swarmkit config directory path (~/.swarmkit/) */
|
|
10
|
+
export declare function getConfigDir(): string;
|
|
11
|
+
/** Get the config file path (~/.swarmkit/config.json) */
|
|
12
|
+
export declare function getConfigPath(): string;
|
|
13
|
+
/** Ensure the config directory exists with correct permissions */
|
|
14
|
+
export declare function ensureConfigDir(): void;
|
|
15
|
+
/** Check if this is the first time running swarmkit */
|
|
16
|
+
export declare function isFirstRun(): boolean;
|
|
17
|
+
/** Read the global config, returning defaults if it doesn't exist */
|
|
18
|
+
export declare function readConfig(): GlobalConfig;
|
|
19
|
+
/** Write the global config */
|
|
20
|
+
export declare function writeConfig(config: GlobalConfig): void;
|
|
21
|
+
/** Add packages to the installed list (deduplicates) */
|
|
22
|
+
export declare function addInstalledPackages(packages: string[]): void;
|
|
23
|
+
/** Remove a package from the installed list */
|
|
24
|
+
export declare function removeInstalledPackage(packageName: string): void;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
installedPackages: [],
|
|
6
|
+
};
|
|
7
|
+
/** Get the swarmkit config directory path (~/.swarmkit/) */
|
|
8
|
+
export function getConfigDir() {
|
|
9
|
+
return join(homedir(), ".swarmkit");
|
|
10
|
+
}
|
|
11
|
+
/** Get the config file path (~/.swarmkit/config.json) */
|
|
12
|
+
export function getConfigPath() {
|
|
13
|
+
return join(getConfigDir(), "config.json");
|
|
14
|
+
}
|
|
15
|
+
/** Ensure the config directory exists with correct permissions */
|
|
16
|
+
export function ensureConfigDir() {
|
|
17
|
+
const dir = getConfigDir();
|
|
18
|
+
if (!existsSync(dir)) {
|
|
19
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
20
|
+
}
|
|
21
|
+
const keysDir = join(dir, "keys");
|
|
22
|
+
if (!existsSync(keysDir)) {
|
|
23
|
+
mkdirSync(keysDir, { recursive: true, mode: 0o700 });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Check if this is the first time running swarmkit */
|
|
27
|
+
export function isFirstRun() {
|
|
28
|
+
return !existsSync(getConfigPath());
|
|
29
|
+
}
|
|
30
|
+
/** Read the global config, returning defaults if it doesn't exist */
|
|
31
|
+
export function readConfig() {
|
|
32
|
+
const path = getConfigPath();
|
|
33
|
+
if (!existsSync(path)) {
|
|
34
|
+
return { ...DEFAULT_CONFIG };
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const raw = readFileSync(path, "utf-8");
|
|
38
|
+
const parsed = JSON.parse(raw);
|
|
39
|
+
return {
|
|
40
|
+
...DEFAULT_CONFIG,
|
|
41
|
+
...parsed,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return { ...DEFAULT_CONFIG };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Write the global config */
|
|
49
|
+
export function writeConfig(config) {
|
|
50
|
+
ensureConfigDir();
|
|
51
|
+
const path = getConfigPath();
|
|
52
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", {
|
|
53
|
+
mode: 0o600,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** Add packages to the installed list (deduplicates) */
|
|
57
|
+
export function addInstalledPackages(packages) {
|
|
58
|
+
const config = readConfig();
|
|
59
|
+
const existing = new Set(config.installedPackages);
|
|
60
|
+
for (const pkg of packages) {
|
|
61
|
+
existing.add(pkg);
|
|
62
|
+
}
|
|
63
|
+
config.installedPackages = [...existing].sort();
|
|
64
|
+
writeConfig(config);
|
|
65
|
+
}
|
|
66
|
+
/** Remove a package from the installed list */
|
|
67
|
+
export function removeInstalledPackage(packageName) {
|
|
68
|
+
const config = readConfig();
|
|
69
|
+
config.installedPackages = config.installedPackages.filter((p) => p !== packageName);
|
|
70
|
+
writeConfig(config);
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
// Mock os.homedir to use a temp directory
|
|
6
|
+
let tempHome;
|
|
7
|
+
vi.mock("node:os", async () => {
|
|
8
|
+
const actual = await vi.importActual("node:os");
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
homedir: () => tempHome,
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
// Import after mock is set up
|
|
15
|
+
const { getConfigDir, getConfigPath, ensureConfigDir, isFirstRun, readConfig, writeConfig, addInstalledPackages, removeInstalledPackage, } = await import("./global.js");
|
|
16
|
+
describe("config/global", () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
tempHome = mkdtempSync(join(tmpdir(), "swarmkit-test-"));
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
rmSync(tempHome, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
describe("getConfigDir", () => {
|
|
24
|
+
it("returns ~/.swarmkit", () => {
|
|
25
|
+
expect(getConfigDir()).toBe(join(tempHome, ".swarmkit"));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe("getConfigPath", () => {
|
|
29
|
+
it("returns ~/.swarmkit/config.json", () => {
|
|
30
|
+
expect(getConfigPath()).toBe(join(tempHome, ".swarmkit", "config.json"));
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe("ensureConfigDir", () => {
|
|
34
|
+
it("creates the config directory if it does not exist", () => {
|
|
35
|
+
ensureConfigDir();
|
|
36
|
+
expect(existsSync(join(tempHome, ".swarmkit"))).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
it("creates the keys subdirectory", () => {
|
|
39
|
+
ensureConfigDir();
|
|
40
|
+
expect(existsSync(join(tempHome, ".swarmkit", "keys"))).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
it("sets directory permissions to 0700", () => {
|
|
43
|
+
ensureConfigDir();
|
|
44
|
+
const stats = statSync(join(tempHome, ".swarmkit"));
|
|
45
|
+
expect(stats.mode & 0o777).toBe(0o700);
|
|
46
|
+
});
|
|
47
|
+
it("is idempotent", () => {
|
|
48
|
+
ensureConfigDir();
|
|
49
|
+
ensureConfigDir();
|
|
50
|
+
expect(existsSync(join(tempHome, ".swarmkit"))).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe("isFirstRun", () => {
|
|
54
|
+
it("returns true when config does not exist", () => {
|
|
55
|
+
expect(isFirstRun()).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
it("returns false after writing config", () => {
|
|
58
|
+
writeConfig({ installedPackages: [] });
|
|
59
|
+
expect(isFirstRun()).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe("readConfig", () => {
|
|
63
|
+
it("returns defaults when no config exists", () => {
|
|
64
|
+
const config = readConfig();
|
|
65
|
+
expect(config.installedPackages).toEqual([]);
|
|
66
|
+
expect(config.embeddingProvider).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
it("reads written config", () => {
|
|
69
|
+
writeConfig({
|
|
70
|
+
installedPackages: ["macro-agent"],
|
|
71
|
+
embeddingProvider: "openai",
|
|
72
|
+
embeddingModel: "text-embedding-3-small",
|
|
73
|
+
});
|
|
74
|
+
const config = readConfig();
|
|
75
|
+
expect(config.installedPackages).toEqual(["macro-agent"]);
|
|
76
|
+
expect(config.embeddingProvider).toBe("openai");
|
|
77
|
+
expect(config.embeddingModel).toBe("text-embedding-3-small");
|
|
78
|
+
});
|
|
79
|
+
it("returns defaults for corrupt config file", async () => {
|
|
80
|
+
ensureConfigDir();
|
|
81
|
+
const { writeFileSync } = await import("node:fs");
|
|
82
|
+
writeFileSync(getConfigPath(), "not json");
|
|
83
|
+
const config = readConfig();
|
|
84
|
+
expect(config.installedPackages).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
it("merges partial config with defaults", async () => {
|
|
87
|
+
ensureConfigDir();
|
|
88
|
+
const { writeFileSync } = await import("node:fs");
|
|
89
|
+
writeFileSync(getConfigPath(), JSON.stringify({ embeddingProvider: "gemini" }));
|
|
90
|
+
const config = readConfig();
|
|
91
|
+
expect(config.installedPackages).toEqual([]);
|
|
92
|
+
expect(config.embeddingProvider).toBe("gemini");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe("writeConfig", () => {
|
|
96
|
+
it("creates the config directory if needed", () => {
|
|
97
|
+
writeConfig({ installedPackages: [] });
|
|
98
|
+
expect(existsSync(join(tempHome, ".swarmkit"))).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
it("writes valid JSON", () => {
|
|
101
|
+
writeConfig({ installedPackages: ["minimem"] });
|
|
102
|
+
const raw = readFileSync(getConfigPath(), "utf-8");
|
|
103
|
+
const parsed = JSON.parse(raw);
|
|
104
|
+
expect(parsed.installedPackages).toEqual(["minimem"]);
|
|
105
|
+
});
|
|
106
|
+
it("sets file permissions to 0600", () => {
|
|
107
|
+
writeConfig({ installedPackages: [] });
|
|
108
|
+
const stats = statSync(getConfigPath());
|
|
109
|
+
expect(stats.mode & 0o777).toBe(0o600);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe("addInstalledPackages", () => {
|
|
113
|
+
it("adds packages to an empty list", () => {
|
|
114
|
+
writeConfig({ installedPackages: [] });
|
|
115
|
+
addInstalledPackages(["macro-agent", "minimem"]);
|
|
116
|
+
const config = readConfig();
|
|
117
|
+
expect(config.installedPackages).toEqual(["macro-agent", "minimem"]);
|
|
118
|
+
});
|
|
119
|
+
it("deduplicates packages", () => {
|
|
120
|
+
writeConfig({ installedPackages: ["macro-agent"] });
|
|
121
|
+
addInstalledPackages(["macro-agent", "minimem"]);
|
|
122
|
+
const config = readConfig();
|
|
123
|
+
expect(config.installedPackages).toEqual(["macro-agent", "minimem"]);
|
|
124
|
+
});
|
|
125
|
+
it("sorts packages alphabetically", () => {
|
|
126
|
+
addInstalledPackages(["minimem", "agent-iam", "macro-agent"]);
|
|
127
|
+
const config = readConfig();
|
|
128
|
+
expect(config.installedPackages).toEqual([
|
|
129
|
+
"agent-iam",
|
|
130
|
+
"macro-agent",
|
|
131
|
+
"minimem",
|
|
132
|
+
]);
|
|
133
|
+
});
|
|
134
|
+
it("preserves other config fields", () => {
|
|
135
|
+
writeConfig({
|
|
136
|
+
installedPackages: [],
|
|
137
|
+
embeddingProvider: "openai",
|
|
138
|
+
});
|
|
139
|
+
addInstalledPackages(["minimem"]);
|
|
140
|
+
const config = readConfig();
|
|
141
|
+
expect(config.embeddingProvider).toBe("openai");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe("removeInstalledPackage", () => {
|
|
145
|
+
it("removes a package from the list", () => {
|
|
146
|
+
writeConfig({ installedPackages: ["macro-agent", "minimem"] });
|
|
147
|
+
removeInstalledPackage("minimem");
|
|
148
|
+
const config = readConfig();
|
|
149
|
+
expect(config.installedPackages).toEqual(["macro-agent"]);
|
|
150
|
+
});
|
|
151
|
+
it("does nothing if package is not in list", () => {
|
|
152
|
+
writeConfig({ installedPackages: ["macro-agent"] });
|
|
153
|
+
removeInstalledPackage("minimem");
|
|
154
|
+
const config = readConfig();
|
|
155
|
+
expect(config.installedPackages).toEqual(["macro-agent"]);
|
|
156
|
+
});
|
|
157
|
+
it("preserves other config fields", () => {
|
|
158
|
+
writeConfig({
|
|
159
|
+
installedPackages: ["minimem"],
|
|
160
|
+
embeddingProvider: "gemini",
|
|
161
|
+
});
|
|
162
|
+
removeInstalledPackage("minimem");
|
|
163
|
+
const config = readConfig();
|
|
164
|
+
expect(config.embeddingProvider).toBe("gemini");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Read an API key by provider name, or null if not stored */
|
|
2
|
+
export declare function readKey(provider: string): string | null;
|
|
3
|
+
/** Write an API key for a provider (stored with 0600 permissions) */
|
|
4
|
+
export declare function writeKey(provider: string, key: string): void;
|
|
5
|
+
/** Delete a stored API key */
|
|
6
|
+
export declare function deleteKey(provider: string): void;
|
|
7
|
+
/** List all stored provider names */
|
|
8
|
+
export declare function listKeys(): string[];
|
|
9
|
+
/** Check if a key is set for a provider */
|
|
10
|
+
export declare function hasKey(provider: string): boolean;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getConfigDir, ensureConfigDir } from "./global.js";
|
|
4
|
+
function getKeysDir() {
|
|
5
|
+
return join(getConfigDir(), "keys");
|
|
6
|
+
}
|
|
7
|
+
/** Read an API key by provider name, or null if not stored */
|
|
8
|
+
export function readKey(provider) {
|
|
9
|
+
const path = join(getKeysDir(), provider);
|
|
10
|
+
if (!existsSync(path))
|
|
11
|
+
return null;
|
|
12
|
+
try {
|
|
13
|
+
return readFileSync(path, "utf-8").trim();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Write an API key for a provider (stored with 0600 permissions) */
|
|
20
|
+
export function writeKey(provider, key) {
|
|
21
|
+
ensureConfigDir();
|
|
22
|
+
const path = join(getKeysDir(), provider);
|
|
23
|
+
writeFileSync(path, key + "\n", { mode: 0o600 });
|
|
24
|
+
}
|
|
25
|
+
/** Delete a stored API key */
|
|
26
|
+
export function deleteKey(provider) {
|
|
27
|
+
const path = join(getKeysDir(), provider);
|
|
28
|
+
if (existsSync(path)) {
|
|
29
|
+
unlinkSync(path);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** List all stored provider names */
|
|
33
|
+
export function listKeys() {
|
|
34
|
+
const dir = getKeysDir();
|
|
35
|
+
if (!existsSync(dir))
|
|
36
|
+
return [];
|
|
37
|
+
try {
|
|
38
|
+
return readdirSync(dir).filter((f) => !f.startsWith(".")).sort();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Check if a key is set for a provider */
|
|
45
|
+
export function hasKey(provider) {
|
|
46
|
+
return readKey(provider) !== null;
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
let tempHome;
|
|
6
|
+
vi.mock("node:os", async () => {
|
|
7
|
+
const actual = await vi.importActual("node:os");
|
|
8
|
+
return {
|
|
9
|
+
...actual,
|
|
10
|
+
homedir: () => tempHome,
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
const { readKey, writeKey, deleteKey, listKeys, hasKey } = await import("./keys.js");
|
|
14
|
+
describe("config/keys", () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tempHome = mkdtempSync(join(tmpdir(), "swarmkit-keys-test-"));
|
|
17
|
+
});
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
rmSync(tempHome, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
describe("writeKey / readKey", () => {
|
|
22
|
+
it("writes and reads a key", () => {
|
|
23
|
+
writeKey("openai", "sk-test-key-123");
|
|
24
|
+
expect(readKey("openai")).toBe("sk-test-key-123");
|
|
25
|
+
});
|
|
26
|
+
it("trims whitespace when reading", () => {
|
|
27
|
+
writeKey("openai", "sk-test-key-123");
|
|
28
|
+
// writeKey appends a newline, readKey should trim it
|
|
29
|
+
expect(readKey("openai")).toBe("sk-test-key-123");
|
|
30
|
+
});
|
|
31
|
+
it("returns null for nonexistent key", () => {
|
|
32
|
+
expect(readKey("nonexistent")).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
it("overwrites existing key", () => {
|
|
35
|
+
writeKey("openai", "old-key");
|
|
36
|
+
writeKey("openai", "new-key");
|
|
37
|
+
expect(readKey("openai")).toBe("new-key");
|
|
38
|
+
});
|
|
39
|
+
it("sets file permissions to 0600", () => {
|
|
40
|
+
writeKey("openai", "sk-test-key");
|
|
41
|
+
const keyPath = join(tempHome, ".swarmkit", "keys", "openai");
|
|
42
|
+
const stats = statSync(keyPath);
|
|
43
|
+
expect(stats.mode & 0o777).toBe(0o600);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
describe("deleteKey", () => {
|
|
47
|
+
it("deletes an existing key", () => {
|
|
48
|
+
writeKey("openai", "sk-test-key");
|
|
49
|
+
expect(readKey("openai")).toBe("sk-test-key");
|
|
50
|
+
deleteKey("openai");
|
|
51
|
+
expect(readKey("openai")).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
it("does nothing for nonexistent key", () => {
|
|
54
|
+
// Should not throw
|
|
55
|
+
deleteKey("nonexistent");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe("listKeys", () => {
|
|
59
|
+
it("returns empty array when no keys exist", () => {
|
|
60
|
+
expect(listKeys()).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
it("lists all stored keys", () => {
|
|
63
|
+
writeKey("openai", "key1");
|
|
64
|
+
writeKey("anthropic", "key2");
|
|
65
|
+
writeKey("gemini", "key3");
|
|
66
|
+
const keys = listKeys();
|
|
67
|
+
expect(keys).toEqual(["anthropic", "gemini", "openai"]);
|
|
68
|
+
});
|
|
69
|
+
it("excludes dotfiles", async () => {
|
|
70
|
+
writeKey("openai", "key1");
|
|
71
|
+
// Manually create a dotfile
|
|
72
|
+
const { writeFileSync } = await import("node:fs");
|
|
73
|
+
writeFileSync(join(tempHome, ".swarmkit", "keys", ".DS_Store"), "");
|
|
74
|
+
const keys = listKeys();
|
|
75
|
+
expect(keys).toEqual(["openai"]);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe("hasKey", () => {
|
|
79
|
+
it("returns false when key does not exist", () => {
|
|
80
|
+
expect(hasKey("openai")).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
it("returns true when key exists", () => {
|
|
83
|
+
writeKey("openai", "sk-test");
|
|
84
|
+
expect(hasKey("openai")).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { CheckResult, CheckContext } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Verify each registered package is actually installed and in PATH.
|
|
4
|
+
*/
|
|
5
|
+
export declare function checkPackages(ctx: CheckContext): Promise<CheckResult[]>;
|
|
6
|
+
/**
|
|
7
|
+
* Check that required API keys are available (stored or in env).
|
|
8
|
+
*/
|
|
9
|
+
export declare function checkCredentials(ctx: CheckContext): CheckResult[];
|
|
10
|
+
/**
|
|
11
|
+
* Check that project-level config directories exist for installed packages.
|
|
12
|
+
* Only runs when cwd is a project directory.
|
|
13
|
+
*/
|
|
14
|
+
export declare function checkProjectConfigs(ctx: CheckContext): CheckResult[];
|
|
15
|
+
/**
|
|
16
|
+
* Check that active integrations have both packages installed.
|
|
17
|
+
* Reports on the state of known integration pairs.
|
|
18
|
+
*/
|
|
19
|
+
export declare function checkIntegrations(ctx: CheckContext): Promise<CheckResult[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Check that the global swarmkit config exists.
|
|
22
|
+
*/
|
|
23
|
+
export declare function checkGlobalConfig(ctx: CheckContext): CheckResult;
|
|
24
|
+
export interface DoctorReport {
|
|
25
|
+
packages: CheckResult[];
|
|
26
|
+
credentials: CheckResult[];
|
|
27
|
+
projectConfigs: CheckResult[];
|
|
28
|
+
integrations: CheckResult[];
|
|
29
|
+
globalConfig: CheckResult;
|
|
30
|
+
}
|
|
31
|
+
export declare function runAllChecks(ctx: CheckContext): Promise<DoctorReport>;
|