swarmkit 0.0.3 → 0.0.5
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/dist/commands/init/wizard.d.ts +1 -0
- package/dist/commands/init/wizard.js +23 -5
- package/dist/commands/init.js +5 -1
- package/dist/config/global.d.ts +10 -0
- package/dist/config/global.js +34 -1
- package/dist/config/global.test.js +34 -1
- package/dist/doctor/checks.js +28 -3
- package/dist/doctor/checks.test.js +27 -6
- package/dist/doctor/types.d.ts +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/packages/setup.js +66 -12
- package/dist/packages/setup.test.js +68 -12
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import { select } from "@inquirer/prompts";
|
|
3
|
-
import { isFirstRun, readConfig, writeConfig, ensureConfigDir } from "../../config/global.js";
|
|
2
|
+
import { select, confirm } from "@inquirer/prompts";
|
|
3
|
+
import { isFirstRun, readConfig, writeConfig, ensureConfigDir, isConfigOutdated, getSwarmkitVersion } from "../../config/global.js";
|
|
4
4
|
import * as ui from "../../utils/ui.js";
|
|
5
5
|
import { getActiveIntegrations } from "../../packages/registry.js";
|
|
6
6
|
import { isClaudeCliAvailable } from "../../packages/installer.js";
|
|
@@ -15,11 +15,28 @@ export async function runWizard(opts) {
|
|
|
15
15
|
console.log();
|
|
16
16
|
console.log(` ${chalk.bold("swarmkit")} ${chalk.dim("— multi-agent infrastructure toolkit")}`);
|
|
17
17
|
console.log();
|
|
18
|
-
if (isFirstRun()) {
|
|
18
|
+
if (isFirstRun() || opts?.forceGlobal) {
|
|
19
19
|
await runFirstTimeSetup(opts);
|
|
20
20
|
}
|
|
21
21
|
else {
|
|
22
|
-
|
|
22
|
+
const config = readConfig();
|
|
23
|
+
if (isConfigOutdated(config)) {
|
|
24
|
+
ui.warn(`Global config was created with swarmkit ${config.configVersion ?? "< 0.1.0"}, ` +
|
|
25
|
+
`but you are now running ${getSwarmkitVersion()}.`);
|
|
26
|
+
const shouldRerun = await confirm({
|
|
27
|
+
message: "Re-run global setup to update your configuration?",
|
|
28
|
+
default: true,
|
|
29
|
+
});
|
|
30
|
+
if (shouldRerun) {
|
|
31
|
+
await runFirstTimeSetup(opts);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
await runProjectSetup(opts);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
await runProjectSetup(opts);
|
|
39
|
+
}
|
|
23
40
|
}
|
|
24
41
|
}
|
|
25
42
|
async function runFirstTimeSetup(opts) {
|
|
@@ -39,9 +56,10 @@ async function runFirstTimeSetup(opts) {
|
|
|
39
56
|
await initGlobal(state);
|
|
40
57
|
// Step 5: Project init (if in a project directory)
|
|
41
58
|
await initProject(state);
|
|
42
|
-
// Persist prefix preference
|
|
59
|
+
// Persist prefix preference and config version
|
|
43
60
|
const config = readConfig();
|
|
44
61
|
config.usePrefix = state.usePrefix;
|
|
62
|
+
config.configVersion = getSwarmkitVersion();
|
|
45
63
|
writeConfig(config);
|
|
46
64
|
// Summary
|
|
47
65
|
printSummary(state);
|
package/dist/commands/init.js
CHANGED
|
@@ -3,9 +3,13 @@ export function registerInitCommand(program) {
|
|
|
3
3
|
.command("init")
|
|
4
4
|
.description("Interactive setup wizard")
|
|
5
5
|
.option("--no-prefix", "Use flat layout (e.g. .opentasks/) instead of nesting under .swarm/")
|
|
6
|
+
.option("-g, --global", "Re-run full global setup (even if already configured)")
|
|
6
7
|
.action(async (opts) => {
|
|
7
8
|
// Dynamic import to avoid loading @inquirer/prompts for other commands
|
|
8
9
|
const { runWizard } = await import("./init/wizard.js");
|
|
9
|
-
await runWizard({
|
|
10
|
+
await runWizard({
|
|
11
|
+
noPrefix: opts.prefix === false,
|
|
12
|
+
forceGlobal: opts.global === true,
|
|
13
|
+
});
|
|
10
14
|
});
|
|
11
15
|
}
|
package/dist/config/global.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface GlobalConfig {
|
|
|
7
7
|
embeddingModel?: string;
|
|
8
8
|
/** Whether project configs are nested under .swarm/ (default true) */
|
|
9
9
|
usePrefix?: boolean;
|
|
10
|
+
/** The swarmkit version that last ran full global setup */
|
|
11
|
+
configVersion?: string;
|
|
10
12
|
}
|
|
11
13
|
/** Get the swarmkit config directory path (~/.swarmkit/) */
|
|
12
14
|
export declare function getConfigDir(): string;
|
|
@@ -24,3 +26,11 @@ export declare function writeConfig(config: GlobalConfig): void;
|
|
|
24
26
|
export declare function addInstalledPackages(packages: string[]): void;
|
|
25
27
|
/** Remove a package from the installed list */
|
|
26
28
|
export declare function removeInstalledPackage(packageName: string): void;
|
|
29
|
+
/** Get the current swarmkit version from package.json */
|
|
30
|
+
export declare function getSwarmkitVersion(): string;
|
|
31
|
+
/**
|
|
32
|
+
* Check if the config version is outdated compared to the current swarmkit version.
|
|
33
|
+
* Outdated if configVersion is missing or major/minor version differs.
|
|
34
|
+
* Patch-only bumps are not considered outdated.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isConfigOutdated(config: GlobalConfig, currentVersion?: string): boolean;
|
package/dist/config/global.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
const DEFAULT_CONFIG = {
|
|
5
6
|
installedPackages: [],
|
|
6
7
|
};
|
|
@@ -69,3 +70,35 @@ export function removeInstalledPackage(packageName) {
|
|
|
69
70
|
config.installedPackages = config.installedPackages.filter((p) => p !== packageName);
|
|
70
71
|
writeConfig(config);
|
|
71
72
|
}
|
|
73
|
+
/** Get the current swarmkit version from package.json */
|
|
74
|
+
export function getSwarmkitVersion() {
|
|
75
|
+
try {
|
|
76
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
77
|
+
const pkgPath = join(__dirname, "..", "package.json");
|
|
78
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
79
|
+
return pkg.version;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return "0.0.0";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function parseVersion(version) {
|
|
86
|
+
const parts = version.split(".").map(Number);
|
|
87
|
+
return {
|
|
88
|
+
major: parts[0] ?? 0,
|
|
89
|
+
minor: parts[1] ?? 0,
|
|
90
|
+
patch: parts[2] ?? 0,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check if the config version is outdated compared to the current swarmkit version.
|
|
95
|
+
* Outdated if configVersion is missing or major/minor version differs.
|
|
96
|
+
* Patch-only bumps are not considered outdated.
|
|
97
|
+
*/
|
|
98
|
+
export function isConfigOutdated(config, currentVersion) {
|
|
99
|
+
if (!config.configVersion)
|
|
100
|
+
return true;
|
|
101
|
+
const current = parseVersion(currentVersion ?? getSwarmkitVersion());
|
|
102
|
+
const saved = parseVersion(config.configVersion);
|
|
103
|
+
return current.major !== saved.major || current.minor !== saved.minor;
|
|
104
|
+
}
|
|
@@ -12,7 +12,7 @@ vi.mock("node:os", async () => {
|
|
|
12
12
|
};
|
|
13
13
|
});
|
|
14
14
|
// Import after mock is set up
|
|
15
|
-
const { getConfigDir, getConfigPath, ensureConfigDir, isFirstRun, readConfig, writeConfig, addInstalledPackages, removeInstalledPackage, } = await import("./global.js");
|
|
15
|
+
const { getConfigDir, getConfigPath, ensureConfigDir, isFirstRun, readConfig, writeConfig, addInstalledPackages, removeInstalledPackage, getSwarmkitVersion, isConfigOutdated, } = await import("./global.js");
|
|
16
16
|
describe("config/global", () => {
|
|
17
17
|
beforeEach(() => {
|
|
18
18
|
tempHome = mkdtempSync(join(tmpdir(), "swarmkit-test-"));
|
|
@@ -164,4 +164,37 @@ describe("config/global", () => {
|
|
|
164
164
|
expect(config.embeddingProvider).toBe("gemini");
|
|
165
165
|
});
|
|
166
166
|
});
|
|
167
|
+
describe("getSwarmkitVersion", () => {
|
|
168
|
+
it("returns a semver string", () => {
|
|
169
|
+
const version = getSwarmkitVersion();
|
|
170
|
+
expect(version).toMatch(/^\d+\.\d+\.\d+/);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
describe("isConfigOutdated", () => {
|
|
174
|
+
it("returns true when configVersion is missing", () => {
|
|
175
|
+
const config = { installedPackages: [] };
|
|
176
|
+
expect(isConfigOutdated(config, "1.0.0")).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
it("returns false when versions match exactly", () => {
|
|
179
|
+
const config = { installedPackages: [], configVersion: "1.2.3" };
|
|
180
|
+
expect(isConfigOutdated(config, "1.2.3")).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
it("returns false for patch-only difference", () => {
|
|
183
|
+
const config = { installedPackages: [], configVersion: "1.2.0" };
|
|
184
|
+
expect(isConfigOutdated(config, "1.2.5")).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
it("returns true when minor version differs", () => {
|
|
187
|
+
const config = { installedPackages: [], configVersion: "1.0.0" };
|
|
188
|
+
expect(isConfigOutdated(config, "1.1.0")).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
it("returns true when major version differs", () => {
|
|
191
|
+
const config = { installedPackages: [], configVersion: "0.1.0" };
|
|
192
|
+
expect(isConfigOutdated(config, "1.0.0")).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
it("uses getSwarmkitVersion when currentVersion is not provided", () => {
|
|
195
|
+
const version = getSwarmkitVersion();
|
|
196
|
+
const config = { installedPackages: [], configVersion: version };
|
|
197
|
+
expect(isConfigOutdated(config)).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
167
200
|
});
|
package/dist/doctor/checks.js
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs";
|
|
1
2
|
import { join } from "node:path";
|
|
2
3
|
import { PACKAGES, getActiveIntegrations } from "../packages/registry.js";
|
|
4
|
+
/** Check if a directory exists (via ctx.exists) and contains at least one file */
|
|
5
|
+
function dirHasContent(dir, exists) {
|
|
6
|
+
if (!exists(dir))
|
|
7
|
+
return false;
|
|
8
|
+
try {
|
|
9
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
if (entry.isFile() || entry.isSymbolicLink())
|
|
12
|
+
return true;
|
|
13
|
+
if (entry.isDirectory() && dirHasContent(join(dir, entry.name), exists))
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
3
22
|
/** Config directories — prefixed layout (.swarm/) */
|
|
4
23
|
const PREFIXED_CONFIG_DIRS = {
|
|
5
24
|
opentasks: ".swarm/opentasks",
|
|
@@ -139,8 +158,11 @@ export function checkProjectConfigs(ctx) {
|
|
|
139
158
|
continue; // Package has no project-level config
|
|
140
159
|
if (PACKAGES[pkg]?.globalOnly)
|
|
141
160
|
continue;
|
|
142
|
-
const
|
|
143
|
-
const
|
|
161
|
+
const prefixedPath = join(ctx.cwd, prefixedDir);
|
|
162
|
+
const flatPath = flatDir ? join(ctx.cwd, flatDir) : null;
|
|
163
|
+
const checkContent = ctx.hasContent ?? ((p) => dirHasContent(p, ctx.exists));
|
|
164
|
+
const hasPrefixed = checkContent(prefixedPath);
|
|
165
|
+
const hasFlat = flatPath ? checkContent(flatPath) : false;
|
|
144
166
|
if (hasPrefixed || hasFlat) {
|
|
145
167
|
const found = hasPrefixed ? prefixedDir : flatDir;
|
|
146
168
|
results.push({
|
|
@@ -150,10 +172,13 @@ export function checkProjectConfigs(ctx) {
|
|
|
150
172
|
});
|
|
151
173
|
}
|
|
152
174
|
else {
|
|
175
|
+
const dirExists = ctx.exists(prefixedPath) || (flatPath ? ctx.exists(flatPath) : false);
|
|
153
176
|
results.push({
|
|
154
177
|
name: `${pkg}-config`,
|
|
155
178
|
status: "warn",
|
|
156
|
-
message:
|
|
179
|
+
message: dirExists
|
|
180
|
+
? `${prefixedDir}/ exists but is empty`
|
|
181
|
+
: `${prefixedDir}/ not found`,
|
|
157
182
|
fix: `swarmkit init (or ${pkg} init)`,
|
|
158
183
|
});
|
|
159
184
|
}
|
|
@@ -151,22 +151,26 @@ describe("checkProjectConfigs", () => {
|
|
|
151
151
|
const results = checkProjectConfigs(ctx);
|
|
152
152
|
expect(results).toEqual([]);
|
|
153
153
|
});
|
|
154
|
-
it("passes when prefixed config directory exists", () => {
|
|
154
|
+
it("passes when prefixed config directory exists with content", () => {
|
|
155
|
+
const hasDir = (path) => path.endsWith(".swarm/opentasks");
|
|
155
156
|
const ctx = createContext({
|
|
156
157
|
isProject: true,
|
|
157
158
|
installedPackages: ["opentasks"],
|
|
158
|
-
exists:
|
|
159
|
+
exists: hasDir,
|
|
160
|
+
hasContent: hasDir,
|
|
159
161
|
});
|
|
160
162
|
const results = checkProjectConfigs(ctx);
|
|
161
163
|
expect(results).toHaveLength(1);
|
|
162
164
|
expect(results[0].status).toBe("pass");
|
|
163
165
|
expect(results[0].message).toContain(".swarm/opentasks");
|
|
164
166
|
});
|
|
165
|
-
it("passes when flat config directory exists", () => {
|
|
167
|
+
it("passes when flat config directory exists with content", () => {
|
|
168
|
+
const hasDir = (path) => path.endsWith(".opentasks");
|
|
166
169
|
const ctx = createContext({
|
|
167
170
|
isProject: true,
|
|
168
171
|
installedPackages: ["opentasks"],
|
|
169
|
-
exists:
|
|
172
|
+
exists: hasDir,
|
|
173
|
+
hasContent: hasDir,
|
|
170
174
|
});
|
|
171
175
|
const results = checkProjectConfigs(ctx);
|
|
172
176
|
expect(results).toHaveLength(1);
|
|
@@ -186,10 +190,12 @@ describe("checkProjectConfigs", () => {
|
|
|
186
190
|
expect(results[0].fix).toContain("opentasks init");
|
|
187
191
|
});
|
|
188
192
|
it("passes for claude-code-swarm with prefixed config", () => {
|
|
193
|
+
const hasDir = (path) => path.endsWith(".swarm/claude-swarm");
|
|
189
194
|
const ctx = createContext({
|
|
190
195
|
isProject: true,
|
|
191
196
|
installedPackages: ["claude-code-swarm"],
|
|
192
|
-
exists:
|
|
197
|
+
exists: hasDir,
|
|
198
|
+
hasContent: hasDir,
|
|
193
199
|
});
|
|
194
200
|
const results = checkProjectConfigs(ctx);
|
|
195
201
|
expect(results).toHaveLength(1);
|
|
@@ -210,11 +216,13 @@ describe("checkProjectConfigs", () => {
|
|
|
210
216
|
"/tmp/test-project/.swarm/opentasks",
|
|
211
217
|
"/tmp/test-project/.minimem",
|
|
212
218
|
]);
|
|
219
|
+
const checkDir = (path) => existingDirs.has(path);
|
|
213
220
|
const ctx = createContext({
|
|
214
221
|
isProject: true,
|
|
215
222
|
cwd: "/tmp/test-project",
|
|
216
223
|
installedPackages: ["opentasks", "minimem", "cognitive-core"],
|
|
217
|
-
exists:
|
|
224
|
+
exists: checkDir,
|
|
225
|
+
hasContent: checkDir,
|
|
218
226
|
});
|
|
219
227
|
const results = checkProjectConfigs(ctx);
|
|
220
228
|
expect(results).toHaveLength(3);
|
|
@@ -222,6 +230,18 @@ describe("checkProjectConfigs", () => {
|
|
|
222
230
|
expect(results[1].status).toBe("pass"); // minimem (flat)
|
|
223
231
|
expect(results[2].status).toBe("warn"); // cognitive-core (missing)
|
|
224
232
|
});
|
|
233
|
+
it("warns with 'empty' message when directory exists but has no content", () => {
|
|
234
|
+
const ctx = createContext({
|
|
235
|
+
isProject: true,
|
|
236
|
+
installedPackages: ["opentasks"],
|
|
237
|
+
exists: (path) => path.endsWith(".swarm/opentasks"),
|
|
238
|
+
hasContent: () => false,
|
|
239
|
+
});
|
|
240
|
+
const results = checkProjectConfigs(ctx);
|
|
241
|
+
expect(results).toHaveLength(1);
|
|
242
|
+
expect(results[0].status).toBe("warn");
|
|
243
|
+
expect(results[0].message).toContain("empty");
|
|
244
|
+
});
|
|
225
245
|
});
|
|
226
246
|
describe("checkIntegrations", () => {
|
|
227
247
|
it("passes when both packages in an integration are installed", async () => {
|
|
@@ -283,6 +303,7 @@ describe("runAllChecks", () => {
|
|
|
283
303
|
env: { HOME: "/home/testuser" },
|
|
284
304
|
getInstalledVersion: async () => "0.1.0",
|
|
285
305
|
exists: () => true,
|
|
306
|
+
hasContent: () => true,
|
|
286
307
|
});
|
|
287
308
|
const report = await runAllChecks(ctx);
|
|
288
309
|
expect(report.packages).toHaveLength(2);
|
package/dist/doctor/types.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface CheckContext {
|
|
|
24
24
|
getInstalledVersion: (pkg: string) => Promise<string | null>;
|
|
25
25
|
/** Check if a file/directory exists — injectable for testing */
|
|
26
26
|
exists: (path: string) => boolean;
|
|
27
|
+
/** Check if a directory has files inside — injectable for testing */
|
|
28
|
+
hasContent?: (path: string) => boolean;
|
|
27
29
|
/** Environment variables */
|
|
28
30
|
env: Record<string, string | undefined>;
|
|
29
31
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type { InstallResult, UpdateResult } from "./packages/installer.js";
|
|
|
5
5
|
export { isInstalledPlugin, registerPlugin, } from "./packages/plugin.js";
|
|
6
6
|
export { PROJECT_CONFIG_DIRS, PROJECT_INIT_ORDER, GLOBAL_CONFIG_DIRS, isProjectInit, isGlobalInit, initProjectPackage, initGlobalPackage, } from "./packages/setup.js";
|
|
7
7
|
export type { InitContext, GlobalContext, OpenhiveOptions, SetupResult, } from "./packages/setup.js";
|
|
8
|
-
export { readConfig, writeConfig, isFirstRun, getConfigDir, addInstalledPackages, removeInstalledPackage, } from "./config/global.js";
|
|
8
|
+
export { readConfig, writeConfig, isFirstRun, getConfigDir, addInstalledPackages, removeInstalledPackage, getSwarmkitVersion, isConfigOutdated, } from "./config/global.js";
|
|
9
9
|
export type { GlobalConfig } from "./config/global.js";
|
|
10
10
|
export { readKey, writeKey, deleteKey, listKeys, hasKey, } from "./config/keys.js";
|
|
11
11
|
export { runAllChecks } from "./doctor/checks.js";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export { PACKAGES, BUNDLES, INTEGRATIONS, getBundlePackages, getActiveIntegratio
|
|
|
3
3
|
export { installPackages, uninstallPackage, getInstalledVersion, getLatestVersion, updatePackages, isClaudeCliAvailable, getGlobalPackagePath, } from "./packages/installer.js";
|
|
4
4
|
export { isInstalledPlugin, registerPlugin, } from "./packages/plugin.js";
|
|
5
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";
|
|
6
|
+
export { readConfig, writeConfig, isFirstRun, getConfigDir, addInstalledPackages, removeInstalledPackage, getSwarmkitVersion, isConfigOutdated, } from "./config/global.js";
|
|
7
7
|
export { readKey, writeKey, deleteKey, listKeys, hasKey, } from "./config/keys.js";
|
|
8
8
|
export { runAllChecks } from "./doctor/checks.js";
|
|
9
9
|
export { readCredentials, writeCredentials, deleteCredentials, isLoggedIn, } from "./hub/credentials.js";
|
package/dist/packages/setup.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
4
4
|
import { basename, join } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
const execFileAsync = promisify(execFile);
|
|
@@ -51,8 +51,8 @@ export const PROJECT_INIT_ORDER = [
|
|
|
51
51
|
export function isProjectInit(cwd, pkg) {
|
|
52
52
|
const prefixed = PROJECT_CONFIG_DIRS[pkg];
|
|
53
53
|
const flat = FLAT_PROJECT_CONFIG_DIRS[pkg];
|
|
54
|
-
return (prefixed ?
|
|
55
|
-
(flat ?
|
|
54
|
+
return (prefixed ? hasContent(join(cwd, prefixed)) : false) ||
|
|
55
|
+
(flat ? hasContent(join(cwd, flat)) : false);
|
|
56
56
|
}
|
|
57
57
|
/** Ensure the .swarm/ project root directory exists */
|
|
58
58
|
function ensureProjectRoot(cwd) {
|
|
@@ -156,7 +156,7 @@ export function isGlobalInit(pkg) {
|
|
|
156
156
|
const dir = GLOBAL_CONFIG_DIRS[pkg];
|
|
157
157
|
if (!dir)
|
|
158
158
|
return false;
|
|
159
|
-
return
|
|
159
|
+
return hasContent(join(homedir(), dir));
|
|
160
160
|
}
|
|
161
161
|
/** Initialize a single global package */
|
|
162
162
|
export async function initGlobalPackage(pkg, ctx, openhiveOpts) {
|
|
@@ -260,6 +260,12 @@ async function initSkillTreeProject(ctx) {
|
|
|
260
260
|
: join(ctx.cwd, ".skilltree");
|
|
261
261
|
mkdirSync(targetDir, { recursive: true });
|
|
262
262
|
mkdirSync(join(targetDir, "skills"), { recursive: true });
|
|
263
|
+
mkdirSync(join(targetDir, ".cache"), { recursive: true });
|
|
264
|
+
// Gitignore the cache directory
|
|
265
|
+
const gitignorePath = join(targetDir, ".gitignore");
|
|
266
|
+
if (!existsSync(gitignorePath)) {
|
|
267
|
+
writeFileSync(gitignorePath, ".cache/\n");
|
|
268
|
+
}
|
|
263
269
|
return { package: "skill-tree", success: true };
|
|
264
270
|
}
|
|
265
271
|
catch (err) {
|
|
@@ -271,15 +277,24 @@ async function initSkillTreeProject(ctx) {
|
|
|
271
277
|
}
|
|
272
278
|
}
|
|
273
279
|
async function initOpenteamsProject(ctx) {
|
|
280
|
+
const targetDir = ctx.usePrefix
|
|
281
|
+
? join(ctx.cwd, PROJECT_ROOT, "openteams")
|
|
282
|
+
: join(ctx.cwd, ".openteams");
|
|
283
|
+
// Try CLI first — `openteams template init` creates config.json with proper defaults
|
|
284
|
+
const result = await shellInit("openteams", ["template", "init", "-d", ctx.cwd], ctx.cwd, ctx.usePrefix
|
|
285
|
+
? { OPENTEAMS_PROJECT_DIR: join(PROJECT_ROOT, "openteams") }
|
|
286
|
+
: undefined);
|
|
287
|
+
if (result.success) {
|
|
288
|
+
if (ctx.usePrefix)
|
|
289
|
+
relocate(ctx.cwd, ".openteams", "openteams");
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
// Fallback: create config inline if CLI is not available
|
|
274
293
|
try {
|
|
275
|
-
const targetDir = ctx.usePrefix
|
|
276
|
-
? join(ctx.cwd, PROJECT_ROOT, "openteams")
|
|
277
|
-
: join(ctx.cwd, ".openteams");
|
|
278
294
|
mkdirSync(targetDir, { recursive: true });
|
|
279
|
-
// Write default config.json (matches `openteams template init` with no options)
|
|
280
295
|
const configPath = join(targetDir, "config.json");
|
|
281
296
|
if (!existsSync(configPath)) {
|
|
282
|
-
writeFileSync(configPath, JSON.stringify({}, null, 2) + "\n");
|
|
297
|
+
writeFileSync(configPath, JSON.stringify({ defaults: {} }, null, 2) + "\n");
|
|
283
298
|
}
|
|
284
299
|
return { package: "openteams", success: true };
|
|
285
300
|
}
|
|
@@ -300,7 +315,14 @@ async function initSessionlogProject(ctx) {
|
|
|
300
315
|
// Write default settings.json
|
|
301
316
|
const settingsPath = join(targetDir, "settings.json");
|
|
302
317
|
if (!existsSync(settingsPath)) {
|
|
303
|
-
|
|
318
|
+
const defaultSettings = {
|
|
319
|
+
enabled: false,
|
|
320
|
+
strategy: "manual-commit",
|
|
321
|
+
logLevel: "warn",
|
|
322
|
+
telemetryEnabled: false,
|
|
323
|
+
summarizationEnabled: false,
|
|
324
|
+
};
|
|
325
|
+
writeFileSync(settingsPath, JSON.stringify(defaultSettings, null, 2) + "\n");
|
|
304
326
|
}
|
|
305
327
|
return { package: "sessionlog", success: true };
|
|
306
328
|
}
|
|
@@ -371,10 +393,24 @@ async function initClaudeSwarmProject(ctx) {
|
|
|
371
393
|
? join(ctx.cwd, PROJECT_ROOT, "claude-swarm")
|
|
372
394
|
: join(ctx.cwd, ".claude-swarm");
|
|
373
395
|
mkdirSync(targetDir, { recursive: true });
|
|
374
|
-
// Write default config.json
|
|
396
|
+
// Write default config.json with meaningful defaults
|
|
375
397
|
const configPath = join(targetDir, "config.json");
|
|
376
398
|
if (!existsSync(configPath)) {
|
|
377
|
-
|
|
399
|
+
const defaultConfig = {
|
|
400
|
+
template: "",
|
|
401
|
+
map: {
|
|
402
|
+
enabled: false,
|
|
403
|
+
server: "ws://localhost:8080",
|
|
404
|
+
scope: "",
|
|
405
|
+
systemId: "system-claude-swarm",
|
|
406
|
+
sidecar: "session",
|
|
407
|
+
},
|
|
408
|
+
sessionlog: {
|
|
409
|
+
enabled: false,
|
|
410
|
+
sync: "off",
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + "\n");
|
|
378
414
|
}
|
|
379
415
|
// Write .gitignore for tmp/
|
|
380
416
|
const gitignorePath = join(targetDir, ".gitignore");
|
|
@@ -392,6 +428,24 @@ async function initClaudeSwarmProject(ctx) {
|
|
|
392
428
|
}
|
|
393
429
|
}
|
|
394
430
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
431
|
+
/** Check if a directory exists and contains at least one file (recursively) */
|
|
432
|
+
function hasContent(dir) {
|
|
433
|
+
if (!existsSync(dir))
|
|
434
|
+
return false;
|
|
435
|
+
try {
|
|
436
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
437
|
+
for (const entry of entries) {
|
|
438
|
+
if (entry.isFile() || entry.isSymbolicLink())
|
|
439
|
+
return true;
|
|
440
|
+
if (entry.isDirectory() && hasContent(join(dir, entry.name)))
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
395
449
|
function getProjectName(cwd) {
|
|
396
450
|
const pkgPath = join(cwd, "package.json");
|
|
397
451
|
if (existsSync(pkgPath)) {
|
|
@@ -105,12 +105,20 @@ describe("isProjectInit", () => {
|
|
|
105
105
|
expect(isProjectInit(testDir, pkg)).toBe(false);
|
|
106
106
|
}
|
|
107
107
|
});
|
|
108
|
-
it.each(Object.entries(PROJECT_CONFIG_DIRS))("returns true for %s when %s/ exists (prefixed)", (pkg, configDir) => {
|
|
109
|
-
|
|
108
|
+
it.each(Object.entries(PROJECT_CONFIG_DIRS))("returns true for %s when %s/ exists with content (prefixed)", (pkg, configDir) => {
|
|
109
|
+
const dir = join(testDir, configDir);
|
|
110
|
+
mkdirSync(dir, { recursive: true });
|
|
111
|
+
writeFileSync(join(dir, "config.json"), "{}");
|
|
110
112
|
expect(isProjectInit(testDir, pkg)).toBe(true);
|
|
111
113
|
});
|
|
112
|
-
it.each(Object.entries(
|
|
114
|
+
it.each(Object.entries(PROJECT_CONFIG_DIRS))("returns false for %s when %s/ exists but is empty (prefixed)", (pkg, configDir) => {
|
|
113
115
|
mkdirSync(join(testDir, configDir), { recursive: true });
|
|
116
|
+
expect(isProjectInit(testDir, pkg)).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
it.each(Object.entries(FLAT_PROJECT_CONFIG_DIRS))("returns true for %s when %s/ exists with content (flat)", (pkg, configDir) => {
|
|
119
|
+
const dir = join(testDir, configDir);
|
|
120
|
+
mkdirSync(dir, { recursive: true });
|
|
121
|
+
writeFileSync(join(dir, "config.json"), "{}");
|
|
114
122
|
expect(isProjectInit(testDir, pkg)).toBe(true);
|
|
115
123
|
});
|
|
116
124
|
it("returns false for unknown package", () => {
|
|
@@ -127,10 +135,16 @@ describe("isGlobalInit", () => {
|
|
|
127
135
|
expect(isGlobalInit(pkg)).toBe(false);
|
|
128
136
|
}
|
|
129
137
|
});
|
|
130
|
-
it.each(Object.entries(GLOBAL_CONFIG_DIRS))("returns true for %s when ~/%s/ exists", (pkg, configDir) => {
|
|
131
|
-
|
|
138
|
+
it.each(Object.entries(GLOBAL_CONFIG_DIRS))("returns true for %s when ~/%s/ exists with content", (pkg, configDir) => {
|
|
139
|
+
const dir = join(testHome, configDir);
|
|
140
|
+
mkdirSync(dir, { recursive: true });
|
|
141
|
+
writeFileSync(join(dir, "config.json"), "{}");
|
|
132
142
|
expect(isGlobalInit(pkg)).toBe(true);
|
|
133
143
|
});
|
|
144
|
+
it.each(Object.entries(GLOBAL_CONFIG_DIRS))("returns false for %s when ~/%s/ exists but is empty", (pkg, configDir) => {
|
|
145
|
+
mkdirSync(join(testHome, configDir), { recursive: true });
|
|
146
|
+
expect(isGlobalInit(pkg)).toBe(false);
|
|
147
|
+
});
|
|
134
148
|
it("returns false for unknown package", () => {
|
|
135
149
|
expect(isGlobalInit("nonexistent")).toBe(false);
|
|
136
150
|
});
|
|
@@ -276,12 +290,17 @@ describe("initProjectPackage — skill-tree", () => {
|
|
|
276
290
|
expect(result.package).toBe("skill-tree");
|
|
277
291
|
expect(existsSync(join(testDir, ".swarm", "skilltree"))).toBe(true);
|
|
278
292
|
expect(existsSync(join(testDir, ".swarm", "skilltree", "skills"))).toBe(true);
|
|
293
|
+
expect(existsSync(join(testDir, ".swarm", "skilltree", ".cache"))).toBe(true);
|
|
294
|
+
expect(existsSync(join(testDir, ".swarm", "skilltree", ".gitignore"))).toBe(true);
|
|
295
|
+
expect(readFileSync(join(testDir, ".swarm", "skilltree", ".gitignore"), "utf-8")).toBe(".cache/\n");
|
|
279
296
|
});
|
|
280
297
|
it("creates .skilltree/ with skills/ subdirectory (flat)", async () => {
|
|
281
298
|
const result = await initProjectPackage("skill-tree", projectCtx({ cwd: testDir, packages: ["skill-tree"], usePrefix: false }));
|
|
282
299
|
expect(result.success).toBe(true);
|
|
283
300
|
expect(existsSync(join(testDir, ".skilltree"))).toBe(true);
|
|
284
301
|
expect(existsSync(join(testDir, ".skilltree", "skills"))).toBe(true);
|
|
302
|
+
expect(existsSync(join(testDir, ".skilltree", ".cache"))).toBe(true);
|
|
303
|
+
expect(existsSync(join(testDir, ".skilltree", ".gitignore"))).toBe(true);
|
|
285
304
|
});
|
|
286
305
|
it("is idempotent", async () => {
|
|
287
306
|
await initProjectPackage("skill-tree", projectCtx({ cwd: testDir, packages: ["skill-tree"] }));
|
|
@@ -298,6 +317,7 @@ describe("initProjectPackage — openteams", () => {
|
|
|
298
317
|
expect(existsSync(join(testDir, ".swarm", "openteams"))).toBe(true);
|
|
299
318
|
expect(existsSync(join(testDir, ".swarm", "openteams", "config.json"))).toBe(true);
|
|
300
319
|
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "openteams", "config.json"), "utf-8"));
|
|
320
|
+
// openteams CLI writes {} by default (all built-in templates active)
|
|
301
321
|
expect(config).toEqual({});
|
|
302
322
|
});
|
|
303
323
|
it("creates .openteams/ with config.json (flat)", async () => {
|
|
@@ -329,8 +349,13 @@ describe("initProjectPackage — sessionlog", () => {
|
|
|
329
349
|
expect(existsSync(join(testDir, ".swarm", "sessionlog"))).toBe(true);
|
|
330
350
|
expect(existsSync(join(testDir, ".swarm", "sessionlog", "settings.json"))).toBe(true);
|
|
331
351
|
const settings = JSON.parse(readFileSync(join(testDir, ".swarm", "sessionlog", "settings.json"), "utf-8"));
|
|
332
|
-
expect(settings
|
|
333
|
-
|
|
352
|
+
expect(settings).toEqual({
|
|
353
|
+
enabled: false,
|
|
354
|
+
strategy: "manual-commit",
|
|
355
|
+
logLevel: "warn",
|
|
356
|
+
telemetryEnabled: false,
|
|
357
|
+
summarizationEnabled: false,
|
|
358
|
+
});
|
|
334
359
|
});
|
|
335
360
|
it("creates .sessionlog/ with settings.json (flat)", async () => {
|
|
336
361
|
const result = await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"], usePrefix: false }));
|
|
@@ -363,7 +388,20 @@ describe("initProjectPackage — claude-code-swarm", () => {
|
|
|
363
388
|
expect(existsSync(join(testDir, ".swarm", "claude-swarm", "config.json"))).toBe(true);
|
|
364
389
|
expect(existsSync(join(testDir, ".swarm", "claude-swarm", ".gitignore"))).toBe(true);
|
|
365
390
|
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "claude-swarm", "config.json"), "utf-8"));
|
|
366
|
-
expect(config).toEqual({
|
|
391
|
+
expect(config).toEqual({
|
|
392
|
+
template: "",
|
|
393
|
+
map: {
|
|
394
|
+
enabled: false,
|
|
395
|
+
server: "ws://localhost:8080",
|
|
396
|
+
scope: "",
|
|
397
|
+
systemId: "system-claude-swarm",
|
|
398
|
+
sidecar: "session",
|
|
399
|
+
},
|
|
400
|
+
sessionlog: {
|
|
401
|
+
enabled: false,
|
|
402
|
+
sync: "off",
|
|
403
|
+
},
|
|
404
|
+
});
|
|
367
405
|
const gitignore = readFileSync(join(testDir, ".swarm", "claude-swarm", ".gitignore"), "utf-8");
|
|
368
406
|
expect(gitignore).toBe("tmp/\n");
|
|
369
407
|
});
|
|
@@ -417,7 +455,7 @@ describe("initGlobalPackage — skill-tree (real CLI)", async () => {
|
|
|
417
455
|
expect(existsSync(configPath)).toBe(true);
|
|
418
456
|
const yaml = readFileSync(configPath, "utf-8");
|
|
419
457
|
expect(yaml).toContain("storage:");
|
|
420
|
-
expect(yaml).toContain("sqlite");
|
|
458
|
+
expect(yaml.toLowerCase()).toContain("sqlite");
|
|
421
459
|
expect(yaml).toContain("indexer:");
|
|
422
460
|
});
|
|
423
461
|
it.skipIf(!installed)("skips when config.yaml already exists", async () => {
|
|
@@ -699,7 +737,20 @@ describe("flow: plugin bootstrap — init project dirs via swarmkit", () => {
|
|
|
699
737
|
expect(existsSync(join(cwd, ".swarm", "claude-swarm", "config.json"))).toBe(true);
|
|
700
738
|
expect(existsSync(join(cwd, ".swarm", "claude-swarm", ".gitignore"))).toBe(true);
|
|
701
739
|
const config = JSON.parse(readFileSync(join(cwd, ".swarm", "claude-swarm", "config.json"), "utf-8"));
|
|
702
|
-
expect(config).toEqual({
|
|
740
|
+
expect(config).toEqual({
|
|
741
|
+
template: "",
|
|
742
|
+
map: {
|
|
743
|
+
enabled: false,
|
|
744
|
+
server: "ws://localhost:8080",
|
|
745
|
+
scope: "",
|
|
746
|
+
systemId: "system-claude-swarm",
|
|
747
|
+
sidecar: "session",
|
|
748
|
+
},
|
|
749
|
+
sessionlog: {
|
|
750
|
+
enabled: false,
|
|
751
|
+
sync: "off",
|
|
752
|
+
},
|
|
753
|
+
});
|
|
703
754
|
const gitignore = readFileSync(join(cwd, ".swarm", "claude-swarm", ".gitignore"), "utf-8");
|
|
704
755
|
expect(gitignore).toBe("tmp/\n");
|
|
705
756
|
// isProjectInit now returns true for both
|
|
@@ -734,8 +785,13 @@ describe("flow: plugin bootstrap — init project dirs via swarmkit", () => {
|
|
|
734
785
|
expect(existsSync(join(cwd, ".swarm", "claude-swarm"))).toBe(true);
|
|
735
786
|
// sessionlog has default settings
|
|
736
787
|
const settings = JSON.parse(readFileSync(join(cwd, ".swarm", "sessionlog", "settings.json"), "utf-8"));
|
|
737
|
-
expect(settings
|
|
738
|
-
|
|
788
|
+
expect(settings).toEqual({
|
|
789
|
+
enabled: false,
|
|
790
|
+
strategy: "manual-commit",
|
|
791
|
+
logLevel: "warn",
|
|
792
|
+
telemetryEnabled: false,
|
|
793
|
+
summarizationEnabled: false,
|
|
794
|
+
});
|
|
739
795
|
});
|
|
740
796
|
it("skips already-initialized packages on re-run (idempotent bootstrap)", async () => {
|
|
741
797
|
const cwd = testDir;
|