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
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for interactive vs non-interactive wizard modes.
|
|
3
|
+
*
|
|
4
|
+
* These tests verify that runPackageWizard correctly handles both modes:
|
|
5
|
+
* - Interactive: stdio: "inherit", uses `args` (wizard takes over terminal)
|
|
6
|
+
* - Non-interactive: stdio: "pipe", uses `nonInteractiveArgs` (silent, for CI/testing)
|
|
7
|
+
*
|
|
8
|
+
* All tests use real CLIs against real filesystems in temp directories.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
11
|
+
import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync, realpathSync, } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
import { execFile, execSync } from "node:child_process";
|
|
16
|
+
import { promisify } from "node:util";
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
18
|
+
// ─── Mock homedir only ──────────────────────────────────────────────────────
|
|
19
|
+
let testHome;
|
|
20
|
+
vi.mock("node:os", async () => {
|
|
21
|
+
const actual = await import("node:os");
|
|
22
|
+
return { ...actual, homedir: () => testHome };
|
|
23
|
+
});
|
|
24
|
+
const { runPackageWizard, relocateAfterWizard, isProjectInit, initProjectPackage, PROJECT_INIT_ORDER, } = await import("../../../packages/setup.js");
|
|
25
|
+
import { PACKAGES } from "../../../packages/registry.js";
|
|
26
|
+
import { createEmptyState } from "../state.js";
|
|
27
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
28
|
+
let testDir;
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
const realTmp = realpathSync(tmpdir());
|
|
31
|
+
testDir = join(realTmp, `swarmkit-wizard-modes-${randomUUID()}`);
|
|
32
|
+
testHome = join(realTmp, `swarmkit-wizard-modes-home-${randomUUID()}`);
|
|
33
|
+
mkdirSync(testDir, { recursive: true });
|
|
34
|
+
mkdirSync(testHome, { recursive: true });
|
|
35
|
+
});
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
38
|
+
rmSync(testHome, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
async function hasCliInstalled(command) {
|
|
41
|
+
try {
|
|
42
|
+
await execFileAsync("which", [command]);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function createProject(name, dir = testDir) {
|
|
50
|
+
execSync("git init -q", { cwd: dir });
|
|
51
|
+
writeFileSync(join(dir, "package.json"), JSON.stringify({ name, version: "1.0.0" }));
|
|
52
|
+
return dir;
|
|
53
|
+
}
|
|
54
|
+
// ─── runPackageWizard: interactive vs non-interactive ────────────────────────
|
|
55
|
+
describe("runPackageWizard — interactive mode (stdio: inherit)", async () => {
|
|
56
|
+
const opentasksOk = await hasCliInstalled("opentasks");
|
|
57
|
+
const skOk = await hasCliInstalled("skill-tree");
|
|
58
|
+
it.skipIf(!opentasksOk)("opentasks: interactive mode uses args, creates config", async () => {
|
|
59
|
+
createProject("interactive-ot");
|
|
60
|
+
const state = createEmptyState();
|
|
61
|
+
state.selectedPackages = ["opentasks"];
|
|
62
|
+
const wizardConfig = PACKAGES.opentasks.setup.cliWizard;
|
|
63
|
+
const result = await runPackageWizard("opentasks", { ...wizardConfig, args: ["init", "--name", "interactive-ot"] }, state, { cwd: testDir, interactive: true });
|
|
64
|
+
expect(result.success).toBe(true);
|
|
65
|
+
expect(existsSync(join(testDir, ".opentasks", "config.json"))).toBe(true);
|
|
66
|
+
const config = JSON.parse(readFileSync(join(testDir, ".opentasks", "config.json"), "utf-8"));
|
|
67
|
+
expect(config.location.name).toBe("interactive-ot");
|
|
68
|
+
});
|
|
69
|
+
it.skipIf(!skOk)("skill-tree: interactive mode runs config init", async () => {
|
|
70
|
+
const state = createEmptyState();
|
|
71
|
+
state.selectedPackages = ["skill-tree"];
|
|
72
|
+
const result = await runPackageWizard("skill-tree", PACKAGES["skill-tree"].setup.cliWizard, state, { cwd: testDir, interactive: true });
|
|
73
|
+
expect(result.success).toBe(true);
|
|
74
|
+
expect(existsSync(join(testHome, ".skill-tree", "config.yaml"))).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe("runPackageWizard — non-interactive mode (stdio: pipe)", async () => {
|
|
78
|
+
const opentasksOk = await hasCliInstalled("opentasks");
|
|
79
|
+
const skOk = await hasCliInstalled("skill-tree");
|
|
80
|
+
it.skipIf(!opentasksOk)("opentasks: non-interactive mode completes silently", async () => {
|
|
81
|
+
createProject("noninteractive-ot");
|
|
82
|
+
const state = createEmptyState();
|
|
83
|
+
state.selectedPackages = ["opentasks"];
|
|
84
|
+
const result = await runPackageWizard("opentasks", { ...PACKAGES.opentasks.setup.cliWizard, args: ["init", "--name", "noninteractive-ot"] }, state, { cwd: testDir, interactive: false });
|
|
85
|
+
expect(result.success).toBe(true);
|
|
86
|
+
expect(existsSync(join(testDir, ".opentasks", "config.json"))).toBe(true);
|
|
87
|
+
const config = JSON.parse(readFileSync(join(testDir, ".opentasks", "config.json"), "utf-8"));
|
|
88
|
+
expect(config.location.name).toBe("noninteractive-ot");
|
|
89
|
+
});
|
|
90
|
+
it.skipIf(!skOk)("skill-tree: non-interactive mode runs silently", async () => {
|
|
91
|
+
const state = createEmptyState();
|
|
92
|
+
state.selectedPackages = ["skill-tree"];
|
|
93
|
+
const result = await runPackageWizard("skill-tree", PACKAGES["skill-tree"].setup.cliWizard, state, { cwd: testDir, interactive: false });
|
|
94
|
+
expect(result.success).toBe(true);
|
|
95
|
+
expect(existsSync(join(testHome, ".skill-tree", "config.yaml"))).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
it("non-interactive uses nonInteractiveArgs when available", async () => {
|
|
98
|
+
const state = createEmptyState();
|
|
99
|
+
// Use a node script to verify which args were received
|
|
100
|
+
const result = await runPackageWizard("test-pkg", {
|
|
101
|
+
command: "node",
|
|
102
|
+
args: ["-e", "process.exit(1)"], // interactive would fail
|
|
103
|
+
nonInteractiveArgs: ["-e", "process.exit(0)"], // non-interactive succeeds
|
|
104
|
+
}, state, { interactive: false });
|
|
105
|
+
// Should have used nonInteractiveArgs (exit 0), not args (exit 1)
|
|
106
|
+
expect(result.success).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
it("non-interactive falls back to args when nonInteractiveArgs not set", async () => {
|
|
109
|
+
const state = createEmptyState();
|
|
110
|
+
const result = await runPackageWizard("test-pkg", {
|
|
111
|
+
command: "node",
|
|
112
|
+
args: ["-e", "process.exit(0)"],
|
|
113
|
+
// no nonInteractiveArgs
|
|
114
|
+
}, state, { interactive: false });
|
|
115
|
+
expect(result.success).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
it("interactive mode ignores nonInteractiveArgs", async () => {
|
|
118
|
+
const state = createEmptyState();
|
|
119
|
+
const result = await runPackageWizard("test-pkg", {
|
|
120
|
+
command: "node",
|
|
121
|
+
args: ["-e", "process.exit(0)"], // interactive uses these
|
|
122
|
+
nonInteractiveArgs: ["-e", "process.exit(1)"], // would fail
|
|
123
|
+
}, state, { interactive: true });
|
|
124
|
+
// Should have used args (exit 0), not nonInteractiveArgs (exit 1)
|
|
125
|
+
expect(result.success).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
// ─── Non-interactive mode with sdr ───────────────────────────────────────────
|
|
129
|
+
describe("runPackageWizard — sdr non-interactive (template flag)", async () => {
|
|
130
|
+
const sdrOk = await hasCliInstalled("sdr");
|
|
131
|
+
it.skipIf(!sdrOk)("sdr: non-interactive uses -t template instead of interactive wizard", async () => {
|
|
132
|
+
createProject("sdr-noninteractive");
|
|
133
|
+
const state = createEmptyState();
|
|
134
|
+
state.selectedPackages = ["self-driving-repo"];
|
|
135
|
+
const wizardConfig = PACKAGES["self-driving-repo"].setup.cliWizard;
|
|
136
|
+
// Verify nonInteractiveArgs is defined
|
|
137
|
+
expect(wizardConfig.nonInteractiveArgs).toBeDefined();
|
|
138
|
+
expect(wizardConfig.nonInteractiveArgs).toContain("-t");
|
|
139
|
+
const result = await runPackageWizard("self-driving-repo", wizardConfig, state, { cwd: testDir, interactive: false });
|
|
140
|
+
expect(result.success).toBe(true);
|
|
141
|
+
expect(existsSync(join(testDir, ".self-driving"))).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
// ─── E2E: non-interactive full init flow ─────────────────────────────────────
|
|
145
|
+
describe("e2e: non-interactive wizard → init → verify", async () => {
|
|
146
|
+
const opentasksOk = await hasCliInstalled("opentasks");
|
|
147
|
+
it.skipIf(!opentasksOk)("non-interactive opentasks wizard + prefix relocation", async () => {
|
|
148
|
+
createProject("e2e-noninteractive");
|
|
149
|
+
const state = createEmptyState();
|
|
150
|
+
state.selectedPackages = ["opentasks"];
|
|
151
|
+
state.usePrefix = true;
|
|
152
|
+
// Run wizard non-interactively
|
|
153
|
+
const wizardConfig = {
|
|
154
|
+
...PACKAGES.opentasks.setup.cliWizard,
|
|
155
|
+
args: ["init", "--name", "e2e-noninteractive"],
|
|
156
|
+
};
|
|
157
|
+
const result = await runPackageWizard("opentasks", wizardConfig, state, { cwd: testDir, interactive: false });
|
|
158
|
+
expect(result.success).toBe(true);
|
|
159
|
+
// Relocate to prefixed layout
|
|
160
|
+
relocateAfterWizard(testDir, "opentasks", true);
|
|
161
|
+
// Should be at .swarm/opentasks/
|
|
162
|
+
expect(existsSync(join(testDir, ".swarm", "opentasks", "config.json"))).toBe(true);
|
|
163
|
+
expect(existsSync(join(testDir, ".opentasks"))).toBe(false);
|
|
164
|
+
// isProjectInit should detect it
|
|
165
|
+
expect(isProjectInit(testDir, "opentasks")).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
it.skipIf(!opentasksOk)("non-interactive wizard then normal init skips wizard-configured package", async () => {
|
|
168
|
+
createProject("e2e-skip");
|
|
169
|
+
const state = createEmptyState();
|
|
170
|
+
state.selectedPackages = ["opentasks", "minimem", "sessionlog"];
|
|
171
|
+
state.usePrefix = true;
|
|
172
|
+
// Run opentasks wizard non-interactively
|
|
173
|
+
const result = await runPackageWizard("opentasks", {
|
|
174
|
+
...PACKAGES.opentasks.setup.cliWizard,
|
|
175
|
+
args: ["init", "--name", "e2e-skip"],
|
|
176
|
+
}, state, { cwd: testDir, interactive: false });
|
|
177
|
+
expect(result.success).toBe(true);
|
|
178
|
+
relocateAfterWizard(testDir, "opentasks", true);
|
|
179
|
+
// Now run normal project init for remaining packages
|
|
180
|
+
const initResults = [];
|
|
181
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
182
|
+
if (!state.selectedPackages.includes(pkg))
|
|
183
|
+
continue;
|
|
184
|
+
if (isProjectInit(testDir, pkg)) {
|
|
185
|
+
initResults.push(`${pkg}:skipped`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const ctx = {
|
|
189
|
+
cwd: testDir,
|
|
190
|
+
packages: state.selectedPackages,
|
|
191
|
+
embeddingProvider: state.embeddingProvider,
|
|
192
|
+
apiKeys: state.apiKeys,
|
|
193
|
+
usePrefix: state.usePrefix,
|
|
194
|
+
};
|
|
195
|
+
await initProjectPackage(pkg, ctx);
|
|
196
|
+
initResults.push(`${pkg}:init`);
|
|
197
|
+
}
|
|
198
|
+
// opentasks skipped (already done by wizard), others initialized
|
|
199
|
+
expect(initResults).toContain("opentasks:skipped");
|
|
200
|
+
expect(initResults).toContain("minimem:init");
|
|
201
|
+
expect(initResults).toContain("sessionlog:init");
|
|
202
|
+
// All three have configs on disk
|
|
203
|
+
expect(existsSync(join(testDir, ".swarm", "opentasks", "config.json"))).toBe(true);
|
|
204
|
+
expect(existsSync(join(testDir, ".swarm", "minimem", "config.json"))).toBe(true);
|
|
205
|
+
expect(existsSync(join(testDir, ".swarm", "sessionlog", "settings.json"))).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
// ─── Interactive vs non-interactive produce same filesystem result ───────────
|
|
209
|
+
describe("interactive vs non-interactive produce same result", async () => {
|
|
210
|
+
const opentasksOk = await hasCliInstalled("opentasks");
|
|
211
|
+
it.skipIf(!opentasksOk)("opentasks: both modes create identical config structure", async () => {
|
|
212
|
+
// Run interactive in one dir
|
|
213
|
+
const interactiveDir = join(testDir, "interactive");
|
|
214
|
+
mkdirSync(interactiveDir, { recursive: true });
|
|
215
|
+
createProject("mode-compare", interactiveDir);
|
|
216
|
+
const state1 = createEmptyState();
|
|
217
|
+
const r1 = await runPackageWizard("opentasks", { ...PACKAGES.opentasks.setup.cliWizard, args: ["init", "--name", "mode-compare"] }, state1, { cwd: interactiveDir, interactive: true });
|
|
218
|
+
expect(r1.success).toBe(true);
|
|
219
|
+
// Run non-interactive in another dir
|
|
220
|
+
const noninteractiveDir = join(testDir, "noninteractive");
|
|
221
|
+
mkdirSync(noninteractiveDir, { recursive: true });
|
|
222
|
+
createProject("mode-compare", noninteractiveDir);
|
|
223
|
+
const state2 = createEmptyState();
|
|
224
|
+
const r2 = await runPackageWizard("opentasks", { ...PACKAGES.opentasks.setup.cliWizard, args: ["init", "--name", "mode-compare"] }, state2, { cwd: noninteractiveDir, interactive: false });
|
|
225
|
+
expect(r2.success).toBe(true);
|
|
226
|
+
// Both should have the same files
|
|
227
|
+
const iConfig = JSON.parse(readFileSync(join(interactiveDir, ".opentasks", "config.json"), "utf-8"));
|
|
228
|
+
const nConfig = JSON.parse(readFileSync(join(noninteractiveDir, ".opentasks", "config.json"), "utf-8"));
|
|
229
|
+
// Same structure (different UUIDs/hashes, but same keys)
|
|
230
|
+
expect(Object.keys(iConfig).sort()).toEqual(Object.keys(nConfig).sort());
|
|
231
|
+
expect(iConfig.version).toBe(nConfig.version);
|
|
232
|
+
expect(iConfig.location.name).toBe(nConfig.location.name);
|
|
233
|
+
expect(typeof iConfig.location.hash).toBe("string");
|
|
234
|
+
expect(typeof nConfig.location.hash).toBe("string");
|
|
235
|
+
// Same files created
|
|
236
|
+
const iFiles = ["config.json", "graph.jsonl", ".gitignore"];
|
|
237
|
+
const nFiles = ["config.json", "graph.jsonl", ".gitignore"];
|
|
238
|
+
for (const f of iFiles) {
|
|
239
|
+
expect(existsSync(join(interactiveDir, ".opentasks", f))).toBe(true);
|
|
240
|
+
}
|
|
241
|
+
for (const f of nFiles) {
|
|
242
|
+
expect(existsSync(join(noninteractiveDir, ".opentasks", f))).toBe(true);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
// ─── Registry: nonInteractiveArgs declarations ──────────────────────────────
|
|
247
|
+
describe("registry: nonInteractiveArgs declarations", () => {
|
|
248
|
+
it("sdr has nonInteractiveArgs with template flag", () => {
|
|
249
|
+
const config = PACKAGES["self-driving-repo"].setup.cliWizard;
|
|
250
|
+
expect(config.nonInteractiveArgs).toBeDefined();
|
|
251
|
+
expect(config.nonInteractiveArgs).toContain("-t");
|
|
252
|
+
expect(config.nonInteractiveArgs).toContain("triage-only");
|
|
253
|
+
});
|
|
254
|
+
it("openhive has nonInteractiveArgs with all flags", () => {
|
|
255
|
+
const config = PACKAGES.openhive.setup.cliWizard;
|
|
256
|
+
expect(config.nonInteractiveArgs).toBeDefined();
|
|
257
|
+
expect(config.nonInteractiveArgs).toContain("--name");
|
|
258
|
+
expect(config.nonInteractiveArgs).toContain("--port");
|
|
259
|
+
expect(config.nonInteractiveArgs).toContain("--auth-mode");
|
|
260
|
+
expect(config.nonInteractiveArgs).toContain("--verification");
|
|
261
|
+
});
|
|
262
|
+
it("opentasks has no nonInteractiveArgs (already non-interactive)", () => {
|
|
263
|
+
const config = PACKAGES.opentasks.setup.cliWizard;
|
|
264
|
+
expect(config.nonInteractiveArgs).toBeUndefined();
|
|
265
|
+
});
|
|
266
|
+
it("skill-tree has no nonInteractiveArgs (already non-interactive)", () => {
|
|
267
|
+
const config = PACKAGES["skill-tree"].setup.cliWizard;
|
|
268
|
+
expect(config.nonInteractiveArgs).toBeUndefined();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
@@ -1,5 +1,27 @@
|
|
|
1
|
+
export interface PackageConfig {
|
|
2
|
+
/** Config values collected (key-value pairs matching InlineOption keys) */
|
|
3
|
+
values: Record<string, unknown>;
|
|
4
|
+
/** Whether a CLI wizard was used (vs inline prompts or defaults) */
|
|
5
|
+
usedCliWizard: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface IntegrationWiring {
|
|
8
|
+
/** Integration key, e.g. "claude-code-swarm:sessionlog" */
|
|
9
|
+
key: string;
|
|
10
|
+
/** Whether user enabled this integration */
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
/** Config values chosen */
|
|
13
|
+
values: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
export interface ConfigWritten {
|
|
16
|
+
/** Package name */
|
|
17
|
+
package: string;
|
|
18
|
+
/** Relative path to the config file/directory */
|
|
19
|
+
path: string;
|
|
20
|
+
/** Brief description of what was written */
|
|
21
|
+
description: string;
|
|
22
|
+
}
|
|
1
23
|
export interface WizardState {
|
|
2
|
-
/** Selected bundle name,
|
|
24
|
+
/** Selected bundle name: "all", "manual", or legacy bundle name */
|
|
3
25
|
bundle: string;
|
|
4
26
|
/** Packages to install */
|
|
5
27
|
selectedPackages: string[];
|
|
@@ -9,5 +31,13 @@ export interface WizardState {
|
|
|
9
31
|
apiKeys: Record<string, string>;
|
|
10
32
|
/** Whether to nest project configs under .swarm/ (default true) */
|
|
11
33
|
usePrefix: boolean;
|
|
34
|
+
/** Skip interactive per-package config and use defaults */
|
|
35
|
+
quick: boolean;
|
|
36
|
+
/** Per-package config choices from the interactive config phase */
|
|
37
|
+
packageConfigs: Record<string, PackageConfig>;
|
|
38
|
+
/** Integration wiring decisions */
|
|
39
|
+
integrationWiring: IntegrationWiring[];
|
|
40
|
+
/** Tracks which config files/dirs were written (for review phase) */
|
|
41
|
+
configsWritten: ConfigWritten[];
|
|
12
42
|
}
|
|
13
43
|
export declare function createEmptyState(): WizardState;
|
|
@@ -9,6 +9,10 @@ describe("WizardState", () => {
|
|
|
9
9
|
expect(state.embeddingProvider).toBeNull();
|
|
10
10
|
expect(state.apiKeys).toEqual({});
|
|
11
11
|
expect(state.usePrefix).toBe(true);
|
|
12
|
+
expect(state.quick).toBe(false);
|
|
13
|
+
expect(state.packageConfigs).toEqual({});
|
|
14
|
+
expect(state.integrationWiring).toEqual([]);
|
|
15
|
+
expect(state.configsWritten).toEqual([]);
|
|
12
16
|
});
|
|
13
17
|
it("returns a new object each time", () => {
|
|
14
18
|
const a = createEmptyState();
|
|
@@ -16,6 +20,9 @@ describe("WizardState", () => {
|
|
|
16
20
|
expect(a).not.toBe(b);
|
|
17
21
|
expect(a.selectedPackages).not.toBe(b.selectedPackages);
|
|
18
22
|
expect(a.apiKeys).not.toBe(b.apiKeys);
|
|
23
|
+
expect(a.packageConfigs).not.toBe(b.packageConfigs);
|
|
24
|
+
expect(a.integrationWiring).not.toBe(b.integrationWiring);
|
|
25
|
+
expect(a.configsWritten).not.toBe(b.configsWritten);
|
|
19
26
|
});
|
|
20
27
|
});
|
|
21
28
|
});
|
|
@@ -2,15 +2,17 @@ import chalk from "chalk";
|
|
|
2
2
|
import { select, confirm } from "@inquirer/prompts";
|
|
3
3
|
import { isFirstRun, readConfig, writeConfig, ensureConfigDir, isConfigOutdated, getSwarmkitVersion } from "../../config/global.js";
|
|
4
4
|
import * as ui from "../../utils/ui.js";
|
|
5
|
-
import { getActiveIntegrations } from "../../packages/registry.js";
|
|
6
5
|
import { isClaudeCliAvailable } from "../../packages/installer.js";
|
|
7
6
|
import { isInstalledPlugin, registerPlugin } from "../../packages/plugin.js";
|
|
8
7
|
import { createEmptyState } from "./state.js";
|
|
9
8
|
import { selectUseCase } from "./phases/use-case.js";
|
|
10
9
|
import { installSelectedPackages } from "./phases/packages.js";
|
|
11
10
|
import { configureKeys } from "./phases/configure.js";
|
|
11
|
+
import { configurePackages } from "./phases/package-config.js";
|
|
12
12
|
import { initProject } from "./phases/project.js";
|
|
13
13
|
import { initGlobal } from "./phases/global-setup.js";
|
|
14
|
+
import { configureIntegrations, applyIntegrationWiring } from "./phases/integrations.js";
|
|
15
|
+
import { showReview } from "./phases/review.js";
|
|
14
16
|
export async function runWizard(opts) {
|
|
15
17
|
console.log();
|
|
16
18
|
console.log(` ${chalk.bold("swarmkit")} ${chalk.dim("— multi-agent infrastructure toolkit")}`);
|
|
@@ -41,28 +43,37 @@ export async function runWizard(opts) {
|
|
|
41
43
|
}
|
|
42
44
|
async function runFirstTimeSetup(opts) {
|
|
43
45
|
ensureConfigDir();
|
|
44
|
-
//
|
|
46
|
+
// Phase 1: Package selection (all vs manual)
|
|
45
47
|
let state = await selectUseCase(createEmptyState());
|
|
46
48
|
if (opts?.noPrefix) {
|
|
47
49
|
state.usePrefix = false;
|
|
48
50
|
}
|
|
49
|
-
|
|
51
|
+
if (opts?.quick) {
|
|
52
|
+
state.quick = true;
|
|
53
|
+
}
|
|
54
|
+
// Phase 2: Install packages
|
|
50
55
|
await installSelectedPackages(state);
|
|
51
|
-
//
|
|
56
|
+
// Phase 2.5: Plugin activation (for any Claude Code plugins)
|
|
52
57
|
await activatePlugins(state);
|
|
53
|
-
//
|
|
58
|
+
// Phase 3: Configure (embedding provider, API keys)
|
|
54
59
|
state = await configureKeys(state);
|
|
55
|
-
//
|
|
56
|
-
await
|
|
57
|
-
//
|
|
60
|
+
// Phase 4: Per-package interactive configuration
|
|
61
|
+
state = await configurePackages(state);
|
|
62
|
+
// Phase 5: Global package setup (reads from state.packageConfigs)
|
|
63
|
+
await initGlobal(state, opts?.forceGlobal);
|
|
64
|
+
// Phase 6: Project init (reads from state.packageConfigs)
|
|
58
65
|
await initProject(state);
|
|
66
|
+
// Phase 7: Integration wiring (collect user choices)
|
|
67
|
+
state = await configureIntegrations(state);
|
|
68
|
+
// Phase 7.5: Apply wiring values to config files on disk
|
|
69
|
+
applyIntegrationWiring(state);
|
|
59
70
|
// Persist prefix preference and config version
|
|
60
71
|
const config = readConfig();
|
|
61
72
|
config.usePrefix = state.usePrefix;
|
|
62
73
|
config.configVersion = getSwarmkitVersion();
|
|
63
74
|
writeConfig(config);
|
|
64
|
-
//
|
|
65
|
-
|
|
75
|
+
// Phase 8: Review & next steps
|
|
76
|
+
await showReview(state);
|
|
66
77
|
}
|
|
67
78
|
async function runProjectSetup(opts) {
|
|
68
79
|
const config = readConfig();
|
|
@@ -70,7 +81,17 @@ async function runProjectSetup(opts) {
|
|
|
70
81
|
const state = createEmptyState();
|
|
71
82
|
state.selectedPackages = [...config.installedPackages];
|
|
72
83
|
state.usePrefix = opts?.noPrefix === true ? false : (config.usePrefix ?? true);
|
|
84
|
+
state.quick = opts?.quick === true;
|
|
85
|
+
// Per-package config for project setup (if not quick mode)
|
|
86
|
+
if (!state.quick) {
|
|
87
|
+
const updated = await configurePackages(state);
|
|
88
|
+
Object.assign(state, updated);
|
|
89
|
+
}
|
|
73
90
|
await initProject(state);
|
|
91
|
+
// Show a brief summary for project-only setup
|
|
92
|
+
if (state.configsWritten.length > 0) {
|
|
93
|
+
await showReview(state);
|
|
94
|
+
}
|
|
74
95
|
}
|
|
75
96
|
async function activatePlugins(state) {
|
|
76
97
|
if (!(await isClaudeCliAvailable()))
|
|
@@ -111,16 +132,3 @@ async function activatePlugins(state) {
|
|
|
111
132
|
}
|
|
112
133
|
}
|
|
113
134
|
}
|
|
114
|
-
function printSummary(state) {
|
|
115
|
-
const integrations = getActiveIntegrations(state.selectedPackages);
|
|
116
|
-
if (integrations.length > 0) {
|
|
117
|
-
ui.heading(" Active integrations:");
|
|
118
|
-
for (const integration of integrations) {
|
|
119
|
-
const [a, b] = integration.packages;
|
|
120
|
-
ui.bullet(` ${a} ${chalk.dim("↔")} ${b} ${chalk.dim(integration.description)}`);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
ui.blank();
|
|
124
|
-
console.log(` ${chalk.green("Done.")} Run ${chalk.bold("`swarmkit status`")} to see your setup.`);
|
|
125
|
-
ui.blank();
|
|
126
|
-
}
|
package/dist/commands/init.js
CHANGED
|
@@ -4,12 +4,14 @@ export function registerInitCommand(program) {
|
|
|
4
4
|
.description("Interactive setup wizard")
|
|
5
5
|
.option("--no-prefix", "Use flat layout (e.g. .opentasks/) instead of nesting under .swarm/")
|
|
6
6
|
.option("-g, --global", "Re-run full global setup (even if already configured)")
|
|
7
|
+
.option("-q, --quick", "Skip interactive per-package config and use defaults")
|
|
7
8
|
.action(async (opts) => {
|
|
8
9
|
// Dynamic import to avoid loading @inquirer/prompts for other commands
|
|
9
10
|
const { runWizard } = await import("./init/wizard.js");
|
|
10
11
|
await runWizard({
|
|
11
12
|
noPrefix: opts.prefix === false,
|
|
12
13
|
forceGlobal: opts.global === true,
|
|
14
|
+
quick: opts.quick === true,
|
|
13
15
|
});
|
|
14
16
|
});
|
|
15
17
|
}
|
|
@@ -9,6 +9,45 @@ export interface PackageDefinition {
|
|
|
9
9
|
category: "orchestration" | "protocol" | "tasks" | "interface" | "learning" | "observability";
|
|
10
10
|
/** No per-project config — global only */
|
|
11
11
|
globalOnly?: boolean;
|
|
12
|
+
/** Interactive setup configuration */
|
|
13
|
+
setup?: PackageSetupConfig;
|
|
14
|
+
}
|
|
15
|
+
export interface PackageSetupConfig {
|
|
16
|
+
/** CLI command for the package's own interactive setup wizard */
|
|
17
|
+
cliWizard?: {
|
|
18
|
+
command: string;
|
|
19
|
+
/** Args for interactive mode (wizard takes over terminal) */
|
|
20
|
+
args: string[];
|
|
21
|
+
/**
|
|
22
|
+
* Args for non-interactive mode (no terminal prompts).
|
|
23
|
+
* Used by `--quick` flag and in test environments.
|
|
24
|
+
* Falls back to `args` if not provided.
|
|
25
|
+
*/
|
|
26
|
+
nonInteractiveArgs?: string[];
|
|
27
|
+
/** Extra env vars swarmkit passes to the wizard */
|
|
28
|
+
env?: Record<string, string>;
|
|
29
|
+
/** Timeout in ms (default 120_000) */
|
|
30
|
+
timeout?: number;
|
|
31
|
+
};
|
|
32
|
+
/** Inline config options swarmkit prompts when no CLI wizard exists */
|
|
33
|
+
inlineOptions?: InlineOption[];
|
|
34
|
+
}
|
|
35
|
+
export interface InlineOption {
|
|
36
|
+
/** Config key path, e.g. "enabled" */
|
|
37
|
+
key: string;
|
|
38
|
+
/** Prompt message */
|
|
39
|
+
label: string;
|
|
40
|
+
/** Prompt type */
|
|
41
|
+
type: "input" | "select" | "confirm" | "password";
|
|
42
|
+
/** Default value */
|
|
43
|
+
default?: string | boolean | number;
|
|
44
|
+
/** Choices for select type */
|
|
45
|
+
choices?: Array<{
|
|
46
|
+
name: string;
|
|
47
|
+
value: string;
|
|
48
|
+
}>;
|
|
49
|
+
/** Only show if condition is met (receives selected package list) */
|
|
50
|
+
when?: (packages: string[]) => boolean;
|
|
12
51
|
}
|
|
13
52
|
export interface BundleDefinition {
|
|
14
53
|
name: string;
|
|
@@ -16,9 +55,32 @@ export interface BundleDefinition {
|
|
|
16
55
|
description: string;
|
|
17
56
|
packages: string[];
|
|
18
57
|
}
|
|
58
|
+
export interface IntegrationConfigOption {
|
|
59
|
+
/** Config key */
|
|
60
|
+
key: string;
|
|
61
|
+
/** Prompt message */
|
|
62
|
+
label: string;
|
|
63
|
+
/** Prompt type */
|
|
64
|
+
type: "confirm" | "input" | "select";
|
|
65
|
+
/** Default value */
|
|
66
|
+
default?: string | boolean;
|
|
67
|
+
/** Choices for select type */
|
|
68
|
+
choices?: Array<{
|
|
69
|
+
name: string;
|
|
70
|
+
value: string;
|
|
71
|
+
}>;
|
|
72
|
+
/** Which package's config file to write into */
|
|
73
|
+
targetPackage: string;
|
|
74
|
+
/** JSON path in that package's config */
|
|
75
|
+
configPath: string;
|
|
76
|
+
}
|
|
19
77
|
export interface Integration {
|
|
20
78
|
packages: [string, string];
|
|
21
79
|
description: string;
|
|
80
|
+
/** Whether this integration requires explicit user opt-in (vs auto-detect) */
|
|
81
|
+
requiresWiring?: boolean;
|
|
82
|
+
/** Config options for wiring this integration */
|
|
83
|
+
configOptions?: IntegrationConfigOption[];
|
|
22
84
|
}
|
|
23
85
|
export declare const PACKAGES: Record<string, PackageDefinition>;
|
|
24
86
|
export declare const BUNDLES: Record<string, BundleDefinition>;
|
|
@@ -35,3 +97,7 @@ export declare function getLostIntegrations(currentPackages: string[], removedPa
|
|
|
35
97
|
export declare function getNpmName(registryKey: string): string;
|
|
36
98
|
export declare function isKnownPackage(name: string): boolean;
|
|
37
99
|
export declare function getAllPackageNames(): string[];
|
|
100
|
+
/** Category display order for manual package selection */
|
|
101
|
+
export declare const CATEGORY_ORDER: PackageDefinition["category"][];
|
|
102
|
+
/** Human-readable category labels */
|
|
103
|
+
export declare const CATEGORY_LABELS: Record<PackageDefinition["category"], string>;
|