swarmkit 0.0.6 → 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.
Files changed (38) hide show
  1. package/dist/cli.js +2 -0
  2. package/dist/commands/configure/configure.d.ts +5 -0
  3. package/dist/commands/configure/configure.js +354 -0
  4. package/dist/commands/configure/configure.test.d.ts +1 -0
  5. package/dist/commands/configure/configure.test.js +539 -0
  6. package/dist/commands/configure/read-config.d.ts +12 -0
  7. package/dist/commands/configure/read-config.js +81 -0
  8. package/dist/commands/configure.d.ts +2 -0
  9. package/dist/commands/configure.js +14 -0
  10. package/dist/commands/init/phases/configure.js +0 -21
  11. package/dist/commands/init/phases/global-setup.d.ts +1 -1
  12. package/dist/commands/init/phases/global-setup.js +22 -44
  13. package/dist/commands/init/phases/integrations.d.ts +16 -0
  14. package/dist/commands/init/phases/integrations.js +172 -0
  15. package/dist/commands/init/phases/package-config.d.ts +10 -0
  16. package/dist/commands/init/phases/package-config.js +117 -0
  17. package/dist/commands/init/phases/phases.test.d.ts +1 -0
  18. package/dist/commands/init/phases/phases.test.js +711 -0
  19. package/dist/commands/init/phases/project.js +17 -0
  20. package/dist/commands/init/phases/review.d.ts +8 -0
  21. package/dist/commands/init/phases/review.js +79 -0
  22. package/dist/commands/init/phases/use-case.js +41 -27
  23. package/dist/commands/init/phases/wizard-flow.test.d.ts +1 -0
  24. package/dist/commands/init/phases/wizard-flow.test.js +657 -0
  25. package/dist/commands/init/phases/wizard-modes.test.d.ts +1 -0
  26. package/dist/commands/init/phases/wizard-modes.test.js +270 -0
  27. package/dist/commands/init/state.d.ts +31 -1
  28. package/dist/commands/init/state.js +4 -0
  29. package/dist/commands/init/state.test.js +7 -0
  30. package/dist/commands/init/wizard.d.ts +1 -0
  31. package/dist/commands/init/wizard.js +31 -23
  32. package/dist/commands/init.js +2 -0
  33. package/dist/packages/registry.d.ts +66 -0
  34. package/dist/packages/registry.js +258 -0
  35. package/dist/packages/setup.d.ts +42 -0
  36. package/dist/packages/setup.js +244 -15
  37. package/dist/packages/setup.test.js +520 -13
  38. 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, or "custom" */
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;
@@ -5,5 +5,9 @@ export function createEmptyState() {
5
5
  embeddingProvider: null,
6
6
  apiKeys: {},
7
7
  usePrefix: true,
8
+ quick: false,
9
+ packageConfigs: {},
10
+ integrationWiring: [],
11
+ configsWritten: [],
8
12
  };
9
13
  }
@@ -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
  });
@@ -1,5 +1,6 @@
1
1
  export interface WizardOptions {
2
2
  noPrefix?: boolean;
3
3
  forceGlobal?: boolean;
4
+ quick?: boolean;
4
5
  }
5
6
  export declare function runWizard(opts?: WizardOptions): Promise<void>;
@@ -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
- // Step 1: Use case / bundle selection
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
- // Step 2: Install packages
51
+ if (opts?.quick) {
52
+ state.quick = true;
53
+ }
54
+ // Phase 2: Install packages
50
55
  await installSelectedPackages(state);
51
- // Step 2.5: Plugin activation (for any Claude Code plugins)
56
+ // Phase 2.5: Plugin activation (for any Claude Code plugins)
52
57
  await activatePlugins(state);
53
- // Step 3: Configure (embedding provider, API keys)
58
+ // Phase 3: Configure (embedding provider, API keys)
54
59
  state = await configureKeys(state);
55
- // Step 4: Global package setup (skill-tree, openhive)
56
- await initGlobal(state);
57
- // Step 5: Project init (if in a project directory)
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
- // Summary
65
- printSummary(state);
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
- }
@@ -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>;