swarmkit 0.0.5 → 0.0.7
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/cli.js +2 -0
- package/dist/commands/configure/configure.d.ts +5 -0
- package/dist/commands/configure/configure.js +354 -0
- package/dist/commands/configure/configure.test.d.ts +1 -0
- package/dist/commands/configure/configure.test.js +539 -0
- package/dist/commands/configure/read-config.d.ts +12 -0
- package/dist/commands/configure/read-config.js +81 -0
- package/dist/commands/configure.d.ts +2 -0
- package/dist/commands/configure.js +14 -0
- package/dist/commands/init/phases/configure.js +0 -21
- package/dist/commands/init/phases/global-setup.d.ts +1 -1
- package/dist/commands/init/phases/global-setup.js +22 -44
- package/dist/commands/init/phases/integrations.d.ts +16 -0
- package/dist/commands/init/phases/integrations.js +172 -0
- package/dist/commands/init/phases/package-config.d.ts +10 -0
- package/dist/commands/init/phases/package-config.js +117 -0
- package/dist/commands/init/phases/phases.test.d.ts +1 -0
- package/dist/commands/init/phases/phases.test.js +711 -0
- package/dist/commands/init/phases/project.js +17 -0
- package/dist/commands/init/phases/review.d.ts +8 -0
- package/dist/commands/init/phases/review.js +79 -0
- package/dist/commands/init/phases/use-case.js +41 -27
- package/dist/commands/init/phases/wizard-flow.test.d.ts +1 -0
- package/dist/commands/init/phases/wizard-flow.test.js +657 -0
- package/dist/commands/init/phases/wizard-modes.test.d.ts +1 -0
- package/dist/commands/init/phases/wizard-modes.test.js +270 -0
- package/dist/commands/init/state.d.ts +31 -1
- package/dist/commands/init/state.js +4 -0
- package/dist/commands/init/state.test.js +7 -0
- package/dist/commands/init/wizard.d.ts +1 -0
- package/dist/commands/init/wizard.js +31 -23
- package/dist/commands/init.js +2 -0
- package/dist/packages/registry.d.ts +66 -0
- package/dist/packages/registry.js +258 -0
- package/dist/packages/setup.d.ts +42 -0
- package/dist/packages/setup.js +311 -56
- package/dist/packages/setup.test.js +546 -42
- package/package.json +1 -1
package/dist/packages/setup.js
CHANGED
|
@@ -1,9 +1,99 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync,
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, 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);
|
|
7
|
+
/**
|
|
8
|
+
* Run a package's own setup wizard.
|
|
9
|
+
*
|
|
10
|
+
* In interactive mode (default): uses `stdio: 'inherit'` so the package's
|
|
11
|
+
* prompts take over the terminal.
|
|
12
|
+
*
|
|
13
|
+
* In non-interactive mode: uses `stdio: 'pipe'` with `nonInteractiveArgs`
|
|
14
|
+
* (falling back to `args`), suitable for --quick mode, CI, and testing.
|
|
15
|
+
*/
|
|
16
|
+
export async function runPackageWizard(pkg, wizardConfig, state, opts) {
|
|
17
|
+
// Backwards compat: opts used to be just cwd string
|
|
18
|
+
const options = typeof opts === "string"
|
|
19
|
+
? { cwd: opts }
|
|
20
|
+
: opts ?? {};
|
|
21
|
+
const interactive = options.interactive ?? true;
|
|
22
|
+
const env = {
|
|
23
|
+
...cleanEnv(),
|
|
24
|
+
HOME: homedir(),
|
|
25
|
+
};
|
|
26
|
+
// Pass swarmkit context via env vars
|
|
27
|
+
if (state.embeddingProvider) {
|
|
28
|
+
env.SWARMKIT_EMBEDDING_PROVIDER = state.embeddingProvider;
|
|
29
|
+
}
|
|
30
|
+
if (state.usePrefix !== undefined) {
|
|
31
|
+
env.SWARMKIT_USE_PREFIX = String(state.usePrefix);
|
|
32
|
+
}
|
|
33
|
+
if (state.apiKeys.openai) {
|
|
34
|
+
env.OPENAI_API_KEY = state.apiKeys.openai;
|
|
35
|
+
}
|
|
36
|
+
if (state.apiKeys.gemini) {
|
|
37
|
+
env.GEMINI_API_KEY = state.apiKeys.gemini;
|
|
38
|
+
}
|
|
39
|
+
// Merge package-specific env overrides
|
|
40
|
+
if (wizardConfig.env) {
|
|
41
|
+
Object.assign(env, wizardConfig.env);
|
|
42
|
+
}
|
|
43
|
+
const timeout = wizardConfig.timeout ?? 120_000;
|
|
44
|
+
const args = (!interactive && wizardConfig.nonInteractiveArgs)
|
|
45
|
+
? wizardConfig.nonInteractiveArgs
|
|
46
|
+
: wizardConfig.args;
|
|
47
|
+
const stdio = interactive ? "inherit" : "pipe";
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
const child = spawn(wizardConfig.command, args, {
|
|
50
|
+
stdio,
|
|
51
|
+
env,
|
|
52
|
+
cwd: options.cwd,
|
|
53
|
+
timeout,
|
|
54
|
+
});
|
|
55
|
+
child.on("close", (code) => {
|
|
56
|
+
if (code === 0) {
|
|
57
|
+
resolve({ package: pkg, success: true });
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
resolve({
|
|
61
|
+
package: pkg,
|
|
62
|
+
success: false,
|
|
63
|
+
message: `exited with code ${code}`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
child.on("error", (err) => {
|
|
68
|
+
resolve({
|
|
69
|
+
package: pkg,
|
|
70
|
+
success: false,
|
|
71
|
+
message: formatError(err),
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Env var overrides that tell each package's CLI to create config in the
|
|
78
|
+
* .swarm/<pkg>/ prefix layout instead of the default flat location.
|
|
79
|
+
* Used by both initProjectPackage (via shellInit) and runPackageWizard.
|
|
80
|
+
*/
|
|
81
|
+
export const PREFIX_ENV_VARS = {
|
|
82
|
+
opentasks: "OPENTASKS_PROJECT_DIR",
|
|
83
|
+
"cognitive-core": "COGNITIVE_CORE_HOME",
|
|
84
|
+
"self-driving-repo": "SDR_CONFIG_DIR",
|
|
85
|
+
openteams: "OPENTEAMS_PROJECT_DIR",
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Flat dir name → prefixed dir name for relocation after CLI wizard runs.
|
|
89
|
+
* Packages that create at .<name>/ need relocation to .swarm/<name>/.
|
|
90
|
+
*/
|
|
91
|
+
export const RELOCATE_MAP = {
|
|
92
|
+
opentasks: [".opentasks", "opentasks"],
|
|
93
|
+
"cognitive-core": [".cognitive-core", "cognitive-core"],
|
|
94
|
+
"self-driving-repo": [".self-driving", "self-driving"],
|
|
95
|
+
openteams: [".openteams", "openteams"],
|
|
96
|
+
};
|
|
7
97
|
// ─── Project-level setup ─────────────────────────────────────────────────────
|
|
8
98
|
/** Root directory for all swarmkit project-level config */
|
|
9
99
|
export const PROJECT_ROOT = ".swarm";
|
|
@@ -33,6 +123,23 @@ export const FLAT_PROJECT_CONFIG_DIRS = {
|
|
|
33
123
|
export function projectConfigDirs(usePrefix) {
|
|
34
124
|
return usePrefix ? PROJECT_CONFIG_DIRS : FLAT_PROJECT_CONFIG_DIRS;
|
|
35
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* After a CLI wizard runs for a project-level package, relocate its output
|
|
128
|
+
* from the flat location to the .swarm/ prefix location if needed.
|
|
129
|
+
*/
|
|
130
|
+
export function relocateAfterWizard(cwd, pkg, usePrefix) {
|
|
131
|
+
if (!usePrefix)
|
|
132
|
+
return;
|
|
133
|
+
const entry = RELOCATE_MAP[pkg];
|
|
134
|
+
if (!entry)
|
|
135
|
+
return;
|
|
136
|
+
const root = join(cwd, PROJECT_ROOT);
|
|
137
|
+
if (!existsSync(root)) {
|
|
138
|
+
mkdirSync(root, { recursive: true });
|
|
139
|
+
}
|
|
140
|
+
const [legacyName, targetName] = entry;
|
|
141
|
+
relocate(cwd, legacyName, targetName);
|
|
142
|
+
}
|
|
36
143
|
/**
|
|
37
144
|
* Project-level packages in correct init order.
|
|
38
145
|
* Order matters: minimem before cognitive-core (runtime detection).
|
|
@@ -63,9 +170,9 @@ function ensureProjectRoot(cwd) {
|
|
|
63
170
|
}
|
|
64
171
|
/**
|
|
65
172
|
* After init, relocate a package's config dir into .swarm/ if the CLI
|
|
66
|
-
* created it at the legacy top-level location
|
|
67
|
-
*
|
|
68
|
-
*
|
|
173
|
+
* created it at the legacy top-level location.
|
|
174
|
+
* All supported packages natively check .swarm/<pkg> before .<pkg>,
|
|
175
|
+
* so no symlink is needed.
|
|
69
176
|
*/
|
|
70
177
|
function relocate(cwd, legacyName, targetName) {
|
|
71
178
|
const src = join(cwd, legacyName);
|
|
@@ -76,26 +183,9 @@ function relocate(cwd, legacyName, targetName) {
|
|
|
76
183
|
renameSync(src, dest);
|
|
77
184
|
}
|
|
78
185
|
catch {
|
|
79
|
-
|
|
186
|
+
// Non-fatal — leave in legacy location
|
|
80
187
|
}
|
|
81
188
|
}
|
|
82
|
-
// Create a symlink at the legacy location so packages find their data
|
|
83
|
-
ensureSymlink(cwd, legacyName, targetName);
|
|
84
|
-
}
|
|
85
|
-
/** Create a relative symlink: cwd/<legacyName> → .swarm/<targetName> */
|
|
86
|
-
function ensureSymlink(cwd, legacyName, targetName) {
|
|
87
|
-
const link = join(cwd, legacyName);
|
|
88
|
-
const target = join(PROJECT_ROOT, targetName); // relative path
|
|
89
|
-
if (isSymlink(link))
|
|
90
|
-
return; // Already symlinked
|
|
91
|
-
if (existsSync(link))
|
|
92
|
-
return; // Real directory exists — don't overwrite
|
|
93
|
-
try {
|
|
94
|
-
symlinkSync(target, link);
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
// Non-fatal — package can still be configured via env var
|
|
98
|
-
}
|
|
99
189
|
}
|
|
100
190
|
function isSymlink(path) {
|
|
101
191
|
try {
|
|
@@ -119,6 +209,10 @@ export async function initProjectPackage(pkg, ctx) {
|
|
|
119
209
|
// for packages that create directly in .swarm/, this is a no-op.
|
|
120
210
|
if (ctx.usePrefix)
|
|
121
211
|
relocate(ctx.cwd, ".opentasks", "opentasks");
|
|
212
|
+
// Older opentasks versions write .gitattributes at the project root.
|
|
213
|
+
// Newer versions write it inside the opentasks dir. Clean up the
|
|
214
|
+
// root-level file as a backwards-compat fallback.
|
|
215
|
+
cleanupGitattributes(ctx.cwd);
|
|
122
216
|
return result;
|
|
123
217
|
}
|
|
124
218
|
case "minimem":
|
|
@@ -150,6 +244,7 @@ export async function initProjectPackage(pkg, ctx) {
|
|
|
150
244
|
export const GLOBAL_CONFIG_DIRS = {
|
|
151
245
|
"skill-tree": ".skill-tree",
|
|
152
246
|
openhive: ".openhive",
|
|
247
|
+
"claude-code-swarm": ".claude-swarm",
|
|
153
248
|
};
|
|
154
249
|
/** Check whether a global package is already configured */
|
|
155
250
|
export function isGlobalInit(pkg) {
|
|
@@ -176,7 +271,7 @@ export async function initGlobalPackage(pkg, ctx, openhiveOpts) {
|
|
|
176
271
|
case "sessionlog":
|
|
177
272
|
return { package: "sessionlog", success: true, message: "no setup required" };
|
|
178
273
|
case "claude-code-swarm":
|
|
179
|
-
return
|
|
274
|
+
return initClaudeSwarmGlobal(ctx);
|
|
180
275
|
default:
|
|
181
276
|
return { package: pkg, success: false, message: "Unknown package" };
|
|
182
277
|
}
|
|
@@ -206,44 +301,65 @@ async function shellInit(command, args, cwd, envOverrides) {
|
|
|
206
301
|
}
|
|
207
302
|
async function initMinimem(ctx) {
|
|
208
303
|
const targetDir = ctx.usePrefix
|
|
209
|
-
? join(
|
|
210
|
-
:
|
|
304
|
+
? join(PROJECT_ROOT, "minimem")
|
|
305
|
+
: ".minimem";
|
|
306
|
+
const absTarget = join(ctx.cwd, targetDir);
|
|
307
|
+
// Try CLI init first (creates contained layout in newer versions)
|
|
308
|
+
const result = await shellInit("minimem", ["init", targetDir], ctx.cwd);
|
|
309
|
+
// Verify the CLI created the contained layout (config.json at root).
|
|
310
|
+
// Older minimem versions create a nested .minimem/ subdir instead.
|
|
311
|
+
if (!existsSync(join(absTarget, "config.json"))) {
|
|
312
|
+
initMinimemInline(absTarget);
|
|
313
|
+
}
|
|
314
|
+
// Patch config with wizard choices
|
|
315
|
+
const configPath = join(absTarget, "config.json");
|
|
211
316
|
try {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// relocate handles packages that don't yet respect the env var;
|
|
220
|
-
// for packages that create directly in .swarm/, this is a no-op.
|
|
221
|
-
if (ctx.usePrefix) {
|
|
222
|
-
relocate(ctx.cwd, ".minimem", "minimem");
|
|
317
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
318
|
+
// Patch embedding provider if the wizard chose one
|
|
319
|
+
const provider = ctx.embeddingProvider && ctx.embeddingProvider !== "local"
|
|
320
|
+
? ctx.embeddingProvider
|
|
321
|
+
: null;
|
|
322
|
+
if (provider) {
|
|
323
|
+
config.embedding = { ...config.embedding, provider };
|
|
223
324
|
}
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
try {
|
|
229
|
-
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
230
|
-
config.embedding = config.embedding || {};
|
|
231
|
-
config.embedding.provider = ctx.embeddingProvider;
|
|
232
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
233
|
-
}
|
|
234
|
-
catch {
|
|
235
|
-
// Non-fatal — user can configure manually
|
|
236
|
-
}
|
|
237
|
-
}
|
|
325
|
+
// Apply per-package config overrides from the interactive config phase
|
|
326
|
+
const overrides = ctx.packageConfigs?.minimem?.values;
|
|
327
|
+
if (overrides) {
|
|
328
|
+
applyNestedOverrides(config, overrides);
|
|
238
329
|
}
|
|
239
|
-
|
|
330
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
240
331
|
}
|
|
241
|
-
catch
|
|
242
|
-
|
|
332
|
+
catch {
|
|
333
|
+
// Non-fatal
|
|
334
|
+
}
|
|
335
|
+
return { package: "minimem", success: true };
|
|
336
|
+
}
|
|
337
|
+
/** Fallback: create contained layout inline when CLI is unavailable or outdated */
|
|
338
|
+
function initMinimemInline(targetDir) {
|
|
339
|
+
mkdirSync(targetDir, { recursive: true });
|
|
340
|
+
mkdirSync(join(targetDir, "memory"), { recursive: true });
|
|
341
|
+
const configPath = join(targetDir, "config.json");
|
|
342
|
+
if (!existsSync(configPath)) {
|
|
343
|
+
writeFileSync(configPath, JSON.stringify({
|
|
344
|
+
embedding: { provider: "auto" },
|
|
345
|
+
hybrid: { enabled: true, vectorWeight: 0.7, textWeight: 0.3 },
|
|
346
|
+
query: { maxResults: 10, minScore: 0.3 },
|
|
347
|
+
}, null, 2) + "\n");
|
|
348
|
+
}
|
|
349
|
+
const memoryPath = join(targetDir, "MEMORY.md");
|
|
350
|
+
if (!existsSync(memoryPath)) {
|
|
351
|
+
writeFileSync(memoryPath, "# Memory\n\nAdd notes, decisions, and context here.\n");
|
|
352
|
+
}
|
|
353
|
+
const gitignorePath = join(targetDir, ".gitignore");
|
|
354
|
+
if (!existsSync(gitignorePath)) {
|
|
355
|
+
writeFileSync(gitignorePath, "index.db\nindex.db-*\n");
|
|
243
356
|
}
|
|
244
357
|
}
|
|
245
358
|
async function initSdr(ctx) {
|
|
246
|
-
|
|
359
|
+
// Use template from interactive config if available, otherwise default
|
|
360
|
+
const template = ctx.packageConfigs?.["self-driving-repo"]?.values?.template ??
|
|
361
|
+
"triage-only";
|
|
362
|
+
const result = await shellInit("sdr", ["init", "-t", template], ctx.cwd, ctx.usePrefix
|
|
247
363
|
? { SDR_CONFIG_DIR: join(PROJECT_ROOT, "self-driving") }
|
|
248
364
|
: undefined);
|
|
249
365
|
// relocate handles packages that don't yet respect the env var;
|
|
@@ -312,7 +428,15 @@ async function initSessionlogProject(ctx) {
|
|
|
312
428
|
? join(ctx.cwd, PROJECT_ROOT, "sessionlog")
|
|
313
429
|
: join(ctx.cwd, ".sessionlog");
|
|
314
430
|
mkdirSync(targetDir, { recursive: true });
|
|
315
|
-
//
|
|
431
|
+
// Apply per-package config overrides
|
|
432
|
+
const overrides = ctx.packageConfigs?.sessionlog?.values;
|
|
433
|
+
// Extract session repo config from dot-notation overrides.
|
|
434
|
+
// sessionRepo.remote and sessionRepo.directory are committable (go to settings.json).
|
|
435
|
+
// sessionRepo.localPath is machine-specific (goes to settings.local.json).
|
|
436
|
+
const repoRemote = overrides?.["sessionRepo.remote"];
|
|
437
|
+
const repoDirectory = overrides?.["sessionRepo.directory"];
|
|
438
|
+
const repoLocalPath = overrides?.["sessionRepo.localPath"];
|
|
439
|
+
// Write settings.json with defaults (committed, shared across clones)
|
|
316
440
|
const settingsPath = join(targetDir, "settings.json");
|
|
317
441
|
if (!existsSync(settingsPath)) {
|
|
318
442
|
const defaultSettings = {
|
|
@@ -322,8 +446,48 @@ async function initSessionlogProject(ctx) {
|
|
|
322
446
|
telemetryEnabled: false,
|
|
323
447
|
summarizationEnabled: false,
|
|
324
448
|
};
|
|
449
|
+
// Apply overrides, stripping session repo dot-notation keys
|
|
450
|
+
if (overrides) {
|
|
451
|
+
const sessionRepoKeys = ["sessionRepo.remote", "sessionRepo.directory", "sessionRepo.localPath", "sessionRepo.autoPush"];
|
|
452
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
453
|
+
if (!sessionRepoKeys.includes(key)) {
|
|
454
|
+
defaultSettings[key] = value;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// Build sessionRepo object for committable fields (remote + directory)
|
|
459
|
+
if (repoRemote) {
|
|
460
|
+
const sessionRepo = { remote: repoRemote };
|
|
461
|
+
if (repoDirectory) {
|
|
462
|
+
sessionRepo.directory = repoDirectory;
|
|
463
|
+
}
|
|
464
|
+
defaultSettings.sessionRepo = sessionRepo;
|
|
465
|
+
}
|
|
325
466
|
writeFileSync(settingsPath, JSON.stringify(defaultSettings, null, 2) + "\n");
|
|
326
467
|
}
|
|
468
|
+
// Write machine-specific session repo fields to settings.local.json
|
|
469
|
+
if (repoLocalPath) {
|
|
470
|
+
const localSettingsPath = join(targetDir, "settings.local.json");
|
|
471
|
+
const localSettings = {};
|
|
472
|
+
if (existsSync(localSettingsPath)) {
|
|
473
|
+
try {
|
|
474
|
+
Object.assign(localSettings, JSON.parse(readFileSync(localSettingsPath, "utf-8")));
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
// Start fresh
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
localSettings.sessionRepo = {
|
|
481
|
+
...(localSettings.sessionRepo ?? {}),
|
|
482
|
+
localPath: repoLocalPath,
|
|
483
|
+
};
|
|
484
|
+
writeFileSync(localSettingsPath, JSON.stringify(localSettings, null, 2) + "\n");
|
|
485
|
+
}
|
|
486
|
+
// Ensure settings.local.json is gitignored
|
|
487
|
+
const gitignorePath = join(targetDir, ".gitignore");
|
|
488
|
+
if (!existsSync(gitignorePath)) {
|
|
489
|
+
writeFileSync(gitignorePath, "# Sessionlog local files (not committed)\nsettings.local.json\ntmp/\nlogs/\n");
|
|
490
|
+
}
|
|
327
491
|
return { package: "sessionlog", success: true };
|
|
328
492
|
}
|
|
329
493
|
catch (err) {
|
|
@@ -387,6 +551,40 @@ async function initOpenhive(opts) {
|
|
|
387
551
|
};
|
|
388
552
|
}
|
|
389
553
|
}
|
|
554
|
+
async function initClaudeSwarmGlobal(ctx) {
|
|
555
|
+
try {
|
|
556
|
+
const targetDir = join(homedir(), ".claude-swarm");
|
|
557
|
+
mkdirSync(targetDir, { recursive: true });
|
|
558
|
+
// Write default global config.json (user preferences that apply across projects)
|
|
559
|
+
const configPath = join(targetDir, "config.json");
|
|
560
|
+
if (!existsSync(configPath)) {
|
|
561
|
+
const defaultConfig = {
|
|
562
|
+
map: {
|
|
563
|
+
server: "",
|
|
564
|
+
sidecar: "session",
|
|
565
|
+
},
|
|
566
|
+
sessionlog: {
|
|
567
|
+
enabled: false,
|
|
568
|
+
sync: "off",
|
|
569
|
+
},
|
|
570
|
+
};
|
|
571
|
+
// Apply per-package config overrides
|
|
572
|
+
const overrides = ctx.packageConfigs?.["claude-code-swarm"]?.values;
|
|
573
|
+
if (overrides) {
|
|
574
|
+
applyNestedOverrides(defaultConfig, overrides);
|
|
575
|
+
}
|
|
576
|
+
writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + "\n", { mode: 0o600 });
|
|
577
|
+
}
|
|
578
|
+
return { package: "claude-code-swarm", success: true };
|
|
579
|
+
}
|
|
580
|
+
catch (err) {
|
|
581
|
+
return {
|
|
582
|
+
package: "claude-code-swarm",
|
|
583
|
+
success: false,
|
|
584
|
+
message: formatError(err),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
}
|
|
390
588
|
async function initClaudeSwarmProject(ctx) {
|
|
391
589
|
try {
|
|
392
590
|
const targetDir = ctx.usePrefix
|
|
@@ -410,6 +608,11 @@ async function initClaudeSwarmProject(ctx) {
|
|
|
410
608
|
sync: "off",
|
|
411
609
|
},
|
|
412
610
|
};
|
|
611
|
+
// Apply per-package config overrides
|
|
612
|
+
const overrides = ctx.packageConfigs?.["claude-code-swarm"]?.values;
|
|
613
|
+
if (overrides) {
|
|
614
|
+
applyNestedOverrides(defaultConfig, overrides);
|
|
615
|
+
}
|
|
413
616
|
writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + "\n");
|
|
414
617
|
}
|
|
415
618
|
// Write .gitignore for tmp/
|
|
@@ -428,6 +631,31 @@ async function initClaudeSwarmProject(ctx) {
|
|
|
428
631
|
}
|
|
429
632
|
}
|
|
430
633
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
634
|
+
/**
|
|
635
|
+
* Apply dotted-key overrides to a nested config object.
|
|
636
|
+
* e.g., { "hybrid.vectorWeight": 0.8 } sets config.hybrid.vectorWeight = 0.8
|
|
637
|
+
*/
|
|
638
|
+
function applyNestedOverrides(config, overrides) {
|
|
639
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
640
|
+
const parts = key.split(".");
|
|
641
|
+
let target = config;
|
|
642
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
643
|
+
if (typeof target[parts[i]] !== "object" ||
|
|
644
|
+
target[parts[i]] === null) {
|
|
645
|
+
target[parts[i]] = {};
|
|
646
|
+
}
|
|
647
|
+
target = target[parts[i]];
|
|
648
|
+
}
|
|
649
|
+
const finalKey = parts[parts.length - 1];
|
|
650
|
+
// Coerce numeric strings to numbers for config values
|
|
651
|
+
if (typeof value === "string" && /^\d+(\.\d+)?$/.test(value)) {
|
|
652
|
+
target[finalKey] = parseFloat(value);
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
target[finalKey] = value;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
431
659
|
/** Check if a directory exists and contains at least one file (recursively) */
|
|
432
660
|
function hasContent(dir) {
|
|
433
661
|
if (!existsSync(dir))
|
|
@@ -446,6 +674,33 @@ function hasContent(dir) {
|
|
|
446
674
|
return false;
|
|
447
675
|
}
|
|
448
676
|
}
|
|
677
|
+
/**
|
|
678
|
+
* Remove root-level .gitattributes created by older opentasks versions.
|
|
679
|
+
* Newer versions write .gitattributes inside the opentasks directory.
|
|
680
|
+
*/
|
|
681
|
+
function cleanupGitattributes(cwd) {
|
|
682
|
+
const attrPath = join(cwd, ".gitattributes");
|
|
683
|
+
if (!existsSync(attrPath))
|
|
684
|
+
return;
|
|
685
|
+
try {
|
|
686
|
+
const content = readFileSync(attrPath, "utf-8");
|
|
687
|
+
const cleaned = content
|
|
688
|
+
.split("\n")
|
|
689
|
+
.filter((line) => !line.includes("merge=opentasks") &&
|
|
690
|
+
!line.includes("OpenTasks merge driver"))
|
|
691
|
+
.join("\n")
|
|
692
|
+
.trim();
|
|
693
|
+
if (!cleaned) {
|
|
694
|
+
unlinkSync(attrPath);
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
writeFileSync(attrPath, cleaned + "\n");
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
catch {
|
|
701
|
+
// Non-fatal
|
|
702
|
+
}
|
|
703
|
+
}
|
|
449
704
|
function getProjectName(cwd) {
|
|
450
705
|
const pkgPath = join(cwd, "package.json");
|
|
451
706
|
if (existsSync(pkgPath)) {
|