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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +130 -1
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +33 -0
  5. package/dist/commands/add.d.ts +2 -0
  6. package/dist/commands/add.js +98 -0
  7. package/dist/commands/doctor.d.ts +2 -0
  8. package/dist/commands/doctor.js +100 -0
  9. package/dist/commands/hive.d.ts +2 -0
  10. package/dist/commands/hive.js +248 -0
  11. package/dist/commands/init/phases/configure.d.ts +2 -0
  12. package/dist/commands/init/phases/configure.js +85 -0
  13. package/dist/commands/init/phases/global-setup.d.ts +2 -0
  14. package/dist/commands/init/phases/global-setup.js +81 -0
  15. package/dist/commands/init/phases/packages.d.ts +2 -0
  16. package/dist/commands/init/phases/packages.js +30 -0
  17. package/dist/commands/init/phases/project.d.ts +2 -0
  18. package/dist/commands/init/phases/project.js +56 -0
  19. package/dist/commands/init/phases/use-case.d.ts +2 -0
  20. package/dist/commands/init/phases/use-case.js +41 -0
  21. package/dist/commands/init/state.d.ts +13 -0
  22. package/dist/commands/init/state.js +9 -0
  23. package/dist/commands/init/state.test.d.ts +1 -0
  24. package/dist/commands/init/state.test.js +21 -0
  25. package/dist/commands/init/wizard.d.ts +4 -0
  26. package/dist/commands/init/wizard.js +108 -0
  27. package/dist/commands/init.d.ts +2 -0
  28. package/dist/commands/init.js +11 -0
  29. package/dist/commands/login.d.ts +2 -0
  30. package/dist/commands/login.js +91 -0
  31. package/dist/commands/logout.d.ts +2 -0
  32. package/dist/commands/logout.js +19 -0
  33. package/dist/commands/remove.d.ts +2 -0
  34. package/dist/commands/remove.js +55 -0
  35. package/dist/commands/status.d.ts +2 -0
  36. package/dist/commands/status.js +87 -0
  37. package/dist/commands/update.d.ts +2 -0
  38. package/dist/commands/update.js +54 -0
  39. package/dist/commands/whoami.d.ts +2 -0
  40. package/dist/commands/whoami.js +40 -0
  41. package/dist/config/global.d.ts +26 -0
  42. package/dist/config/global.js +71 -0
  43. package/dist/config/global.test.d.ts +1 -0
  44. package/dist/config/global.test.js +167 -0
  45. package/dist/config/keys.d.ts +10 -0
  46. package/dist/config/keys.js +47 -0
  47. package/dist/config/keys.test.d.ts +1 -0
  48. package/dist/config/keys.test.js +87 -0
  49. package/dist/doctor/checks.d.ts +31 -0
  50. package/dist/doctor/checks.js +226 -0
  51. package/dist/doctor/checks.test.d.ts +1 -0
  52. package/dist/doctor/checks.test.js +301 -0
  53. package/dist/doctor/types.d.ts +29 -0
  54. package/dist/doctor/types.js +1 -0
  55. package/dist/hub/auth-flow.d.ts +16 -0
  56. package/dist/hub/auth-flow.js +118 -0
  57. package/dist/hub/auth-flow.test.d.ts +1 -0
  58. package/dist/hub/auth-flow.test.js +98 -0
  59. package/dist/hub/client.d.ts +51 -0
  60. package/dist/hub/client.js +107 -0
  61. package/dist/hub/client.test.d.ts +1 -0
  62. package/dist/hub/client.test.js +177 -0
  63. package/dist/hub/credentials.d.ts +14 -0
  64. package/dist/hub/credentials.js +41 -0
  65. package/dist/hub/credentials.test.d.ts +1 -0
  66. package/dist/hub/credentials.test.js +102 -0
  67. package/dist/index.d.ts +17 -1
  68. package/dist/index.js +10 -2
  69. package/dist/packages/installer.d.ts +42 -0
  70. package/dist/packages/installer.js +158 -0
  71. package/dist/packages/installer.test.d.ts +1 -0
  72. package/dist/packages/installer.test.js +283 -0
  73. package/dist/packages/plugin.d.ts +13 -0
  74. package/dist/packages/plugin.js +33 -0
  75. package/dist/packages/plugin.test.d.ts +1 -0
  76. package/dist/packages/plugin.test.js +99 -0
  77. package/dist/packages/registry.d.ts +37 -0
  78. package/dist/packages/registry.js +154 -0
  79. package/dist/packages/registry.test.d.ts +1 -0
  80. package/dist/packages/registry.test.js +188 -0
  81. package/dist/packages/setup.d.ts +55 -0
  82. package/dist/packages/setup.js +414 -0
  83. package/dist/packages/setup.test.d.ts +1 -0
  84. package/dist/packages/setup.test.js +808 -0
  85. package/dist/utils/ui.d.ts +10 -0
  86. package/dist/utils/ui.js +47 -0
  87. package/dist/utils/ui.test.d.ts +1 -0
  88. package/dist/utils/ui.test.js +102 -0
  89. package/package.json +29 -6
@@ -0,0 +1,55 @@
1
+ import chalk from "chalk";
2
+ import { readConfig, removeInstalledPackage } from "../config/global.js";
3
+ import { getLostIntegrations } from "../packages/registry.js";
4
+ import { uninstallPackage, getInstalledVersion } from "../packages/installer.js";
5
+ import { isInstalledPlugin } from "../packages/plugin.js";
6
+ import * as ui from "../utils/ui.js";
7
+ export function registerRemoveCommand(program) {
8
+ program
9
+ .command("remove <package>")
10
+ .description("Uninstall a swarmkit package (preserves project data)")
11
+ .action(async (packageName) => {
12
+ const config = readConfig();
13
+ // Check if tracked by swarmkit
14
+ if (!config.installedPackages.includes(packageName)) {
15
+ ui.fail(`${packageName} is not in the swarmkit package list.`);
16
+ ui.info("Run `swarmkit status` to see installed packages.");
17
+ process.exitCode = 1;
18
+ return;
19
+ }
20
+ // Show what will break
21
+ const lostIntegrations = getLostIntegrations(config.installedPackages, packageName);
22
+ if (lostIntegrations.length > 0) {
23
+ console.log();
24
+ console.log(` ${chalk.bold("Affected integrations:")}`);
25
+ for (const integration of lostIntegrations) {
26
+ const other = integration.packages.find((p) => p !== packageName);
27
+ ui.warn(`${other} — ${integration.description}`);
28
+ }
29
+ console.log();
30
+ }
31
+ // Check if it's a plugin before uninstalling (while we can still resolve the path)
32
+ const wasPlugin = await isInstalledPlugin(packageName);
33
+ // Uninstall
34
+ console.log(` Uninstalling ${chalk.bold(packageName)}...`);
35
+ const version = await getInstalledVersion(packageName);
36
+ try {
37
+ await uninstallPackage(packageName);
38
+ ui.success(`${packageName}${version ? " v" + version : ""} uninstalled`);
39
+ }
40
+ catch (err) {
41
+ const msg = err instanceof Error ? err.message : String(err);
42
+ ui.fail(`Failed to uninstall: ${msg}`);
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+ // Update registry
47
+ removeInstalledPackage(packageName);
48
+ ui.blank();
49
+ ui.info("Project data directories are preserved. Delete them manually if no longer needed.");
50
+ if (wasPlugin) {
51
+ ui.info(`If activated as a Claude Code plugin, also run: claude plugin uninstall ${packageName}`);
52
+ }
53
+ ui.blank();
54
+ });
55
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerStatusCommand(program: Command): void;
@@ -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,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerUpdateCommand(program: Command): void;
@@ -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,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerWhoamiCommand(program: Command): void;
@@ -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,26 @@
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
+ /** Whether project configs are nested under .swarm/ (default true) */
9
+ usePrefix?: boolean;
10
+ }
11
+ /** Get the swarmkit config directory path (~/.swarmkit/) */
12
+ export declare function getConfigDir(): string;
13
+ /** Get the config file path (~/.swarmkit/config.json) */
14
+ export declare function getConfigPath(): string;
15
+ /** Ensure the config directory exists with correct permissions */
16
+ export declare function ensureConfigDir(): void;
17
+ /** Check if this is the first time running swarmkit */
18
+ export declare function isFirstRun(): boolean;
19
+ /** Read the global config, returning defaults if it doesn't exist */
20
+ export declare function readConfig(): GlobalConfig;
21
+ /** Write the global config */
22
+ export declare function writeConfig(config: GlobalConfig): void;
23
+ /** Add packages to the installed list (deduplicates) */
24
+ export declare function addInstalledPackages(packages: string[]): void;
25
+ /** Remove a package from the installed list */
26
+ 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: ["opentasks"],
71
+ embeddingProvider: "openai",
72
+ embeddingModel: "text-embedding-3-small",
73
+ });
74
+ const config = readConfig();
75
+ expect(config.installedPackages).toEqual(["opentasks"]);
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(["opentasks", "minimem"]);
116
+ const config = readConfig();
117
+ expect(config.installedPackages).toEqual(["minimem", "opentasks"]);
118
+ });
119
+ it("deduplicates packages", () => {
120
+ writeConfig({ installedPackages: ["opentasks"] });
121
+ addInstalledPackages(["opentasks", "minimem"]);
122
+ const config = readConfig();
123
+ expect(config.installedPackages).toEqual(["minimem", "opentasks"]);
124
+ });
125
+ it("sorts packages alphabetically", () => {
126
+ addInstalledPackages(["minimem", "cognitive-core", "opentasks"]);
127
+ const config = readConfig();
128
+ expect(config.installedPackages).toEqual([
129
+ "cognitive-core",
130
+ "minimem",
131
+ "opentasks",
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: ["opentasks", "minimem"] });
147
+ removeInstalledPackage("minimem");
148
+ const config = readConfig();
149
+ expect(config.installedPackages).toEqual(["opentasks"]);
150
+ });
151
+ it("does nothing if package is not in list", () => {
152
+ writeConfig({ installedPackages: ["opentasks"] });
153
+ removeInstalledPackage("minimem");
154
+ const config = readConfig();
155
+ expect(config.installedPackages).toEqual(["opentasks"]);
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
+ });