@klhapp/skillmux 1.9.3 → 1.11.0

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 (53) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +19 -19
  3. package/docs/README.md +4 -4
  4. package/docs/assets/architecture-dark.svg +39 -32
  5. package/docs/assets/architecture-light.svg +25 -18
  6. package/docs/cli.md +147 -36
  7. package/docs/concepts.md +11 -11
  8. package/docs/configuration.md +7 -5
  9. package/docs/deployment.md +10 -6
  10. package/docs/getting-started.md +18 -14
  11. package/docs/mcp-routing.md +1 -1
  12. package/docs/skill-management.md +17 -11
  13. package/docs/troubleshooting.md +4 -4
  14. package/package.json +1 -1
  15. package/src/adapters.ts +157 -11
  16. package/src/cli.ts +396 -1319
  17. package/src/commands/audit.ts +53 -56
  18. package/src/commands/config.ts +33 -26
  19. package/src/commands/context.ts +104 -0
  20. package/src/commands/core.ts +7 -3
  21. package/src/commands/doctor.ts +97 -0
  22. package/src/commands/eval.ts +22 -15
  23. package/src/commands/init.ts +672 -0
  24. package/src/commands/install.ts +132 -0
  25. package/src/commands/local-vault.ts +60 -0
  26. package/src/commands/models.ts +10 -0
  27. package/src/commands/outdated.ts +2 -1
  28. package/src/commands/project.ts +194 -51
  29. package/src/commands/report.ts +66 -0
  30. package/src/commands/scan.ts +61 -0
  31. package/src/commands/shared.ts +7 -14
  32. package/src/commands/skill.ts +33 -0
  33. package/src/commands/sync.ts +232 -0
  34. package/src/commands/target.ts +45 -15
  35. package/src/commands/update.ts +2 -1
  36. package/src/completions.ts +41 -15
  37. package/src/config-service.ts +4 -54
  38. package/src/context.ts +8 -3
  39. package/src/db-audit.ts +286 -0
  40. package/src/db-index.ts +238 -0
  41. package/src/db.ts +3 -521
  42. package/src/global-flags.ts +46 -0
  43. package/src/init-agents.ts +329 -0
  44. package/src/init-instructions.ts +47 -28
  45. package/src/logger.ts +26 -0
  46. package/src/mcp-registration.ts +89 -0
  47. package/src/output.ts +80 -18
  48. package/src/prompts.ts +75 -20
  49. package/src/router-core.ts +8 -27
  50. package/src/scan.ts +19 -19
  51. package/src/server.ts +161 -14
  52. package/src/toml-writer.ts +51 -0
  53. package/src/init-clients.ts +0 -220
@@ -0,0 +1,672 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import {
4
+ expandHome,
5
+ loadConfig,
6
+ migrateLegacyPaths,
7
+ resolveConfigPath,
8
+ } from "../config";
9
+ import {
10
+ applyInit,
11
+ deriveTargetName,
12
+ detectSurfaces,
13
+ planInitManifest,
14
+ printLastMile,
15
+ surfaceCandidates,
16
+ } from "../init";
17
+ import {
18
+ assessAgentReadiness,
19
+ detectInstalledAgents,
20
+ planAgentSurfaces,
21
+ SUPPORTED_AGENT_IDS,
22
+ type AgentId,
23
+ type ReadinessAxis,
24
+ } from "../init-agents";
25
+ import {
26
+ applyInstructionPlan,
27
+ planInstructionSetup,
28
+ rollbackInstructionPlan,
29
+ } from "../init-instructions";
30
+ import { parseManifest, resolveManifestPath } from "../manifest";
31
+ import {
32
+ MCP_REGISTRABLE_AGENTS,
33
+ registerMcpServer,
34
+ type McpRegistrationResult,
35
+ } from "../mcp-registration";
36
+ import { isInteractive } from "../output";
37
+ import { isGlobalFlag } from "../global-flags";
38
+ import {
39
+ parseCommaList,
40
+ promptMultiSelect,
41
+ promptText,
42
+ shouldUseWizard,
43
+ } from "../prompts";
44
+ import {
45
+ applyConfigInit,
46
+ inspectVault,
47
+ planConfigInit,
48
+ rollbackConfigInit,
49
+ type ConfigInitPlan,
50
+ } from "../setup";
51
+ import { configuredTargetForSurface } from "./project";
52
+ import { confirmAction } from "./shared";
53
+ import { runSync } from "./sync";
54
+
55
+ function parseInitArgs(args: string[]): {
56
+ agents: string[];
57
+ coreSkillIds: string[];
58
+ migrateFullVault: boolean;
59
+ showMcpSetup: boolean;
60
+ registerMcp: boolean;
61
+ skipInstructions: boolean;
62
+ sync: boolean;
63
+ vaultPath?: string;
64
+ yes: boolean;
65
+ } {
66
+ const agents: string[] = [];
67
+ const coreSkillIds: string[] = [];
68
+ let migrateFullVault = false;
69
+ let showMcpSetup = false;
70
+ let registerMcp = false;
71
+ let skipInstructions = false;
72
+ let sync = true;
73
+ let vaultPath: string | undefined;
74
+ let yes = false;
75
+ for (let i = 0; i < args.length; i++) {
76
+ const option = args[i];
77
+ if (option === "--target" || option === "--dir" || option === "--client") {
78
+ throw new Error(
79
+ `${option} was removed; select a specific supported agent with --agent instead (see "skillmux init --help")`,
80
+ );
81
+ } else if (option === "--agent") {
82
+ const value = args[i + 1];
83
+ if (!value) throw new Error("--agent requires a name");
84
+ agents.push(value);
85
+ i++;
86
+ } else if (option === "--vault") {
87
+ const value = args[i + 1];
88
+ if (!value) throw new Error("--vault requires a path");
89
+ vaultPath = value;
90
+ i++;
91
+ } else if (option === "--core") {
92
+ const value = args[i + 1];
93
+ if (!value) throw new Error("--core requires a skill_id");
94
+ coreSkillIds.push(value);
95
+ i++;
96
+ } else if (
97
+ isGlobalFlag(option, "--dry-run", "--json") ||
98
+ option === "--interactive"
99
+ ) {
100
+ continue;
101
+ } else if (option === "--migrate-full-vault") {
102
+ migrateFullVault = true;
103
+ } else if (option === "--show-mcp-setup") {
104
+ showMcpSetup = true;
105
+ } else if (option === "--register-mcp") {
106
+ registerMcp = true;
107
+ } else if (option === "--no-instructions") {
108
+ skipInstructions = true;
109
+ } else if (option === "--no-sync") {
110
+ sync = false;
111
+ } else if (option === "--yes") {
112
+ yes = true;
113
+ } else {
114
+ throw new Error(`unknown init option: ${option}`);
115
+ }
116
+ }
117
+ return {
118
+ agents,
119
+ coreSkillIds,
120
+ migrateFullVault,
121
+ showMcpSetup,
122
+ registerMcp,
123
+ skipInstructions,
124
+ sync,
125
+ vaultPath,
126
+ yes,
127
+ };
128
+ }
129
+
130
+ interface InitJsonPayload {
131
+ command: "init";
132
+ phase: "plan" | "result";
133
+ dry_run: boolean;
134
+ applied: boolean;
135
+ plan: unknown;
136
+ result?: unknown;
137
+ }
138
+
139
+ /**
140
+ * init predates the shared emitSuccess envelope and shipped its own top-level
141
+ * keys, which existing automation reads. Emit the documented envelope
142
+ * (`target`/`data`/`error`, see docs/cli.md) so generic consumers work, while
143
+ * keeping the original keys so nothing breaks. init's error path already goes
144
+ * through the shared handler and needs no bridging.
145
+ *
146
+ * DEPRECATED: the duplicated top-level `command`/`phase`/`dry_run`/`applied`/
147
+ * `plan`/`result` keys should be dropped in the next major version, leaving
148
+ * only the standard envelope. init is local-only, so `target` is always
149
+ * "local".
150
+ */
151
+ function initJsonEnvelope(payload: InitJsonPayload): string {
152
+ return JSON.stringify({
153
+ schema_version: 1,
154
+ ok: true,
155
+ context: "local",
156
+ target: "local",
157
+ data: payload,
158
+ error: null,
159
+ ...payload,
160
+ });
161
+ }
162
+
163
+ export async function runInit(
164
+ args: string[],
165
+ options: { isJson: boolean; dryRun: boolean },
166
+ ): Promise<void> {
167
+ const {
168
+ agents: requestedAgents,
169
+ coreSkillIds,
170
+ migrateFullVault,
171
+ showMcpSetup,
172
+ registerMcp: requestedRegisterMcp,
173
+ skipInstructions,
174
+ sync,
175
+ vaultPath: requestedVaultPath,
176
+ yes,
177
+ } = parseInitArgs(args);
178
+ const guided = shouldUseWizard(args, {
179
+ interactive: isInteractive(),
180
+ json: options.isJson,
181
+ dryRun: options.dryRun,
182
+ });
183
+ migrateLegacyPaths();
184
+ const configPath = resolveConfigPath();
185
+ let configPlan: ConfigInitPlan | undefined;
186
+ let vaultPath: string;
187
+ if (!existsSync(configPath)) {
188
+ let bootstrapVaultPath = requestedVaultPath;
189
+ if (!bootstrapVaultPath) {
190
+ if (guided) {
191
+ bootstrapVaultPath = await promptText("Skill vault path", "~/skills");
192
+ } else if (!options.isJson && isInteractive()) {
193
+ bootstrapVaultPath = "~/skills";
194
+ }
195
+ }
196
+ if (!bootstrapVaultPath) {
197
+ throw new Error(
198
+ `machine config does not exist: ${configPath}; re-run with --vault <path>`,
199
+ );
200
+ }
201
+ configPlan = planConfigInit(configPath, expandHome(bootstrapVaultPath));
202
+ vaultPath = configPlan.vaultPath;
203
+ if (!options.isJson) {
204
+ console.log(`config create: ${configPath} (vault: ${vaultPath})`);
205
+ }
206
+ } else {
207
+ const config = await loadConfig();
208
+ vaultPath = expandHome(config.vault_path);
209
+ if (requestedVaultPath && expandHome(requestedVaultPath) !== vaultPath) {
210
+ throw new Error(
211
+ `machine config already uses vault_path ${vaultPath}; --vault does not overwrite existing config`,
212
+ );
213
+ }
214
+ }
215
+
216
+ const vaultHealth = inspectVault(vaultPath);
217
+ if (!vaultHealth.ok) {
218
+ throw new Error(vaultHealth.message);
219
+ }
220
+
221
+ let selectedAgents = requestedAgents;
222
+ if (guided) {
223
+ const detected = detectInstalledAgents({
224
+ codexHome: process.env.CODEX_HOME
225
+ ? expandHome(process.env.CODEX_HOME)
226
+ : undefined,
227
+ });
228
+ const evidence = new Map(
229
+ detected.map((item) => [item.agent, item.evidence]),
230
+ );
231
+ selectedAgents = await promptMultiSelect(
232
+ "Which agents do you use?",
233
+ SUPPORTED_AGENT_IDS.map((agent) => ({
234
+ value: agent,
235
+ label: agent,
236
+ detail: evidence.has(agent)
237
+ ? `detected: ${evidence.get(agent)}`
238
+ : undefined,
239
+ selected: evidence.has(agent) || requestedAgents.includes(agent),
240
+ })),
241
+ );
242
+ }
243
+ let selectedCoreSkillIds = coreSkillIds;
244
+ if (guided) {
245
+ selectedCoreSkillIds = parseCommaList(
246
+ await promptText(
247
+ "Core skill IDs to add, comma-separated",
248
+ coreSkillIds.join(","),
249
+ ),
250
+ );
251
+ }
252
+
253
+ // Native pins and MCP registration are independent choices — this stays
254
+ // opt-in and only offered for agents whose own CLI we've verified, so a
255
+ // user who only wants native skill management sees nothing new here.
256
+ const registrableAgents = selectedAgents.filter((agent) =>
257
+ MCP_REGISTRABLE_AGENTS.includes(agent as AgentId),
258
+ ) as AgentId[];
259
+ let registerMcp = requestedRegisterMcp;
260
+ if (guided && registrableAgents.length > 0) {
261
+ registerMcp = await confirmAction(
262
+ `Also register skillmux as an MCP server for ${registrableAgents.join(", ")}?`,
263
+ );
264
+ }
265
+
266
+ const agentPlan = planAgentSurfaces(selectedAgents, {
267
+ codexHome: process.env.CODEX_HOME
268
+ ? expandHome(process.env.CODEX_HOME)
269
+ : undefined,
270
+ });
271
+ // The managed instruction block only teaches an agent to call resolve_skill
272
+ // /fetch_skill (MCP tools) — writing it for an agent with no MCP registered
273
+ // or requested would tell that agent to call tools that don't exist. Only
274
+ // agents actually getting MCP this run (or whose snippet the user asked to
275
+ // see) get the block; native-only setups get no instruction writes at all.
276
+ const mcpInstructionAgentIds = agentPlan.agents
277
+ .map((agent) => agent.id)
278
+ .filter(
279
+ (agentId) =>
280
+ showMcpSetup || (registerMcp && registrableAgents.includes(agentId)),
281
+ );
282
+ const instructionPlan = planInstructionSetup(
283
+ skipInstructions ? [] : mcpInstructionAgentIds,
284
+ {
285
+ codexHome: process.env.CODEX_HOME
286
+ ? expandHome(process.env.CODEX_HOME)
287
+ : undefined,
288
+ },
289
+ );
290
+ const instructionReadiness: Partial<Record<AgentId, ReadinessAxis>> = {};
291
+ for (const change of instructionPlan.changes) {
292
+ for (const agent of change.agents) {
293
+ instructionReadiness[agent] = {
294
+ status: change.status === "unchanged" ? "ready" : "planned",
295
+ detail: change.path,
296
+ };
297
+ }
298
+ }
299
+ for (const manual of instructionPlan.manual) {
300
+ instructionReadiness[manual.agent] = {
301
+ status: "manual",
302
+ detail: manual.reason,
303
+ };
304
+ }
305
+ for (const agent of agentPlan.agents) {
306
+ if (mcpInstructionAgentIds.includes(agent.id)) continue;
307
+ if (instructionReadiness[agent.id]) continue;
308
+ instructionReadiness[agent.id] = {
309
+ status: "not-applicable",
310
+ detail: "no MCP requested — see --show-mcp-setup / --register-mcp",
311
+ };
312
+ }
313
+ const existingManifestPath = resolveManifestPath(vaultPath);
314
+ const existingManifest = existingManifestPath
315
+ ? parseManifest(await Bun.file(existingManifestPath).text())
316
+ : undefined;
317
+ const targetByPath = new Map<string, string>();
318
+ for (const surface of agentPlan.surfaces) {
319
+ targetByPath.set(
320
+ surface.path,
321
+ existingManifest
322
+ ? (configuredTargetForSurface(existingManifest, surface) ??
323
+ surface.targetName)
324
+ : surface.targetName,
325
+ );
326
+ }
327
+ const candidatePaths = [
328
+ ...new Set([
329
+ ...surfaceCandidates().map(expandHome),
330
+ ...targetByPath.keys(),
331
+ ]),
332
+ ];
333
+ const candidates = detectSurfaces(candidatePaths, vaultPath);
334
+ if (!options.isJson) {
335
+ for (const candidate of candidates) {
336
+ const name =
337
+ targetByPath.get(candidate.path) ?? deriveTargetName(candidate.path);
338
+ if (candidate.state === "missing") {
339
+ console.log(`${name} (${candidate.path}): not found`);
340
+ continue;
341
+ }
342
+ if (candidate.state === "broken-symlink") {
343
+ console.log(`${name} (${candidate.path}): broken symlink`);
344
+ continue;
345
+ }
346
+ if (candidate.state === "full-vault") {
347
+ console.log(
348
+ `${name} (${candidate.path}): full-vault -> ${candidate.canonicalPath}`,
349
+ );
350
+ continue;
351
+ }
352
+ if (candidate.state === "external-symlink") {
353
+ console.log(
354
+ `${name} (${candidate.path}): external symlink -> ${candidate.canonicalPath}`,
355
+ );
356
+ continue;
357
+ }
358
+ if (candidate.state === "unsupported") {
359
+ console.log(
360
+ `${name} (${candidate.path}): unsupported filesystem entry`,
361
+ );
362
+ continue;
363
+ }
364
+ const kind = "real dir";
365
+ const marked = candidate.alreadyMarked
366
+ ? ", already skillmux-managed"
367
+ : "";
368
+ console.log(
369
+ `${name} (${candidate.path}): ${kind}, ${candidate.skillCount} skills${marked}`,
370
+ );
371
+ }
372
+ for (const readiness of assessAgentReadiness(
373
+ agentPlan,
374
+ instructionReadiness,
375
+ )) {
376
+ console.log(`\n${readiness.agent} readiness:`);
377
+ console.log(
378
+ ` skill surface: ${readiness.skillSurface.status} — ${readiness.skillSurface.detail}`,
379
+ );
380
+ console.log(
381
+ ` MCP registration: ${readiness.mcpRegistration.status} — ${readiness.mcpRegistration.detail}`,
382
+ );
383
+ console.log(
384
+ ` instructions: ${readiness.instructionSetup.status} — ${readiness.instructionSetup.detail}`,
385
+ );
386
+ }
387
+ for (const change of instructionPlan.changes) {
388
+ console.log(
389
+ `instructions ${change.status}: ${change.path} (${change.agents.join(", ")})`,
390
+ );
391
+ }
392
+ for (const manual of instructionPlan.manual) {
393
+ console.log(`instructions manual: ${manual.agent} — ${manual.reason}`);
394
+ }
395
+ }
396
+
397
+ const requestedTargets = [...new Set(targetByPath.values())];
398
+ const hasInstructionWrites = instructionPlan.changes.some(
399
+ (change) => change.status !== "unchanged",
400
+ );
401
+ const hasConfigWrite = configPlan?.action === "create";
402
+ const hasChanges = !(
403
+ requestedTargets.length === 0 &&
404
+ !hasInstructionWrites &&
405
+ selectedCoreSkillIds.length === 0 &&
406
+ !hasConfigWrite
407
+ );
408
+
409
+ const byName = new Map(
410
+ candidates
411
+ .filter(
412
+ (candidate) =>
413
+ candidate.deliveryMode === "managed-pins" ||
414
+ (migrateFullVault && candidate.state === "full-vault"),
415
+ )
416
+ .map(
417
+ (candidate) =>
418
+ [
419
+ targetByPath.get(candidate.path) ??
420
+ deriveTargetName(candidate.path),
421
+ candidate,
422
+ ] as const,
423
+ ),
424
+ );
425
+ const allCandidatesByName = new Map(
426
+ candidates.map(
427
+ (candidate) =>
428
+ [
429
+ targetByPath.get(candidate.path) ?? deriveTargetName(candidate.path),
430
+ candidate,
431
+ ] as const,
432
+ ),
433
+ );
434
+ for (const name of requestedTargets) {
435
+ if (!byName.has(name)) {
436
+ if (allCandidatesByName.get(name)?.state === "full-vault") {
437
+ throw new Error(
438
+ `target "${name}" is a full-vault surface; re-run with --migrate-full-vault to convert it to managed pins`,
439
+ );
440
+ }
441
+ throw new Error(
442
+ `target "${name}" not among detected surfaces`,
443
+ );
444
+ }
445
+ }
446
+
447
+ const confirmedTargets = requestedTargets.map((name) => {
448
+ const candidate = byName.get(name)!;
449
+ return {
450
+ name,
451
+ dir: candidate.path,
452
+ ...(candidate.state === "full-vault" ? { migrateFullVault: true } : {}),
453
+ };
454
+ });
455
+ const plannedManifest = planInitManifest(
456
+ vaultPath,
457
+ confirmedTargets,
458
+ selectedCoreSkillIds,
459
+ );
460
+ const serializedPlan = {
461
+ vault_path: vaultPath,
462
+ config: configPlan
463
+ ? { path: configPlan.configPath, action: configPlan.action }
464
+ : { path: configPath, action: "preserve" },
465
+ agents: agentPlan.agents.map((agent) => agent.id),
466
+ targets: confirmedTargets,
467
+ core: plannedManifest.core.skills,
468
+ instructions: instructionPlan.changes.map(({ path, agents, status }) => ({
469
+ path,
470
+ agents,
471
+ status,
472
+ })),
473
+ manual: instructionPlan.manual,
474
+ register_mcp_for: registerMcp ? registrableAgents : [],
475
+ };
476
+ if (!hasChanges) {
477
+ if (options.isJson) {
478
+ console.log(
479
+ initJsonEnvelope({
480
+ command: "init",
481
+ phase: "plan",
482
+ dry_run: options.dryRun,
483
+ applied: false,
484
+ plan: serializedPlan,
485
+ }),
486
+ );
487
+ } else {
488
+ console.log("\nno managed-pins surface selected — nothing written.");
489
+ }
490
+ return;
491
+ }
492
+ if (!options.isJson) {
493
+ for (const target of confirmedTargets.filter(
494
+ (target) => target.migrateFullVault,
495
+ )) {
496
+ console.log(
497
+ `full-vault migration ${target.name}: ${vaultHealth.skillCount} visible skills -> ` +
498
+ `${plannedManifest.core.skills.length} core ${plannedManifest.core.skills.length === 1 ? "skill" : "skills"} after sync`,
499
+ );
500
+ }
501
+ }
502
+ if (options.dryRun) {
503
+ if (options.isJson) {
504
+ console.log(
505
+ initJsonEnvelope({
506
+ command: "init",
507
+ phase: "plan",
508
+ dry_run: true,
509
+ applied: false,
510
+ plan: serializedPlan,
511
+ }),
512
+ );
513
+ } else {
514
+ console.log(
515
+ `\ndry-run: ${confirmedTargets.length} target(s), ` +
516
+ `${instructionPlan.changes.filter((change) => change.status !== "unchanged").length} instruction file(s), ` +
517
+ `core: ${plannedManifest.core.skills.join(", ") || "(unchanged)"}, ` +
518
+ `MCP registration: ${registerMcp ? registrableAgents.join(", ") || "(none)" : "(none)"}`,
519
+ );
520
+ }
521
+ return;
522
+ }
523
+
524
+ if (!yes) {
525
+ if (!options.isJson && isInteractive()) {
526
+ if (guided) {
527
+ console.log("\nReview");
528
+ console.log(` agents: ${selectedAgents.join(", ") || "(none)"}`);
529
+ console.log(
530
+ ` targets: ${confirmedTargets.map((target) => `${target.name} -> ${target.dir}`).join(", ") || "(none)"}`,
531
+ );
532
+ console.log(
533
+ ` instructions: ${instructionPlan.changes.filter((change) => change.status !== "unchanged").length} file(s)`,
534
+ );
535
+ console.log(
536
+ ` core: ${plannedManifest.core.skills.join(", ") || "(none)"}`,
537
+ );
538
+ console.log(
539
+ ` MCP registration: ${registerMcp ? registrableAgents.join(", ") || "(none)" : "(none)"}`,
540
+ );
541
+ console.log(` sync: ${sync ? "yes" : "no"}`);
542
+ if (!(await confirmAction("apply this setup plan?"))) {
543
+ console.log("init cancelled");
544
+ return;
545
+ }
546
+ } else {
547
+ const prompts = [
548
+ ...confirmedTargets.map(
549
+ (target) => `adopt ${target.name} at ${target.dir}?`,
550
+ ),
551
+ ...instructionPlan.changes
552
+ .filter((change) => change.status !== "unchanged")
553
+ .map(
554
+ (change) => `${change.status} instruction file ${change.path}?`,
555
+ ),
556
+ ...(hasConfigWrite ? [`create machine config ${configPath}?`] : []),
557
+ ...(selectedCoreSkillIds.length > 0
558
+ ? [`pin core skills: ${selectedCoreSkillIds.join(", ")}?`]
559
+ : []),
560
+ ];
561
+ for (const prompt of prompts) {
562
+ if (!(await confirmAction(prompt))) {
563
+ console.log("init cancelled; nothing written");
564
+ return;
565
+ }
566
+ }
567
+ }
568
+ } else {
569
+ throw new Error(
570
+ "skillmux init requires --yes before applying target, instruction, or core changes non-interactively",
571
+ );
572
+ }
573
+ }
574
+
575
+ let configCreated = false;
576
+ let instructionsApplied = false;
577
+ const applyAdditional = () => {
578
+ try {
579
+ if (configPlan?.action === "create") {
580
+ configCreated = applyConfigInit(configPlan) === "created";
581
+ }
582
+ if (hasInstructionWrites) {
583
+ applyInstructionPlan(instructionPlan);
584
+ instructionsApplied = true;
585
+ }
586
+ } catch (error) {
587
+ if (configCreated && configPlan) rollbackConfigInit(configPlan);
588
+ configCreated = false;
589
+ throw error;
590
+ }
591
+ };
592
+ const rollbackAdditional = () => {
593
+ if (instructionsApplied) rollbackInstructionPlan(instructionPlan);
594
+ if (configCreated && configPlan) rollbackConfigInit(configPlan);
595
+ };
596
+
597
+ if (confirmedTargets.length === 0 && selectedCoreSkillIds.length === 0) {
598
+ applyAdditional();
599
+ } else {
600
+ applyInit(
601
+ vaultPath,
602
+ confirmedTargets,
603
+ hasInstructionWrites || hasConfigWrite
604
+ ? {
605
+ apply: applyAdditional,
606
+ rollback: rollbackAdditional,
607
+ }
608
+ : undefined,
609
+ selectedCoreSkillIds,
610
+ );
611
+ }
612
+
613
+ // Best-effort and outside the rollback above: this mutates another tool's
614
+ // own config, not skillmux's, so a registration failure is reported, never
615
+ // rolled back — the successful native setup above still stands either way.
616
+ const mcpRegistrations: McpRegistrationResult[] = [];
617
+ if (registerMcp) {
618
+ for (const agent of registrableAgents) {
619
+ mcpRegistrations.push(await registerMcpServer(agent));
620
+ }
621
+ }
622
+
623
+ if (options.isJson) {
624
+ console.log(
625
+ initJsonEnvelope({
626
+ command: "init",
627
+ phase: "result",
628
+ dry_run: false,
629
+ applied: true,
630
+ plan: serializedPlan,
631
+ result: {
632
+ config_created: configCreated,
633
+ targets_adopted: confirmedTargets.map((target) => target.name),
634
+ instructions_changed: instructionPlan.changes
635
+ .filter((change) => change.status !== "unchanged")
636
+ .map((change) => change.path),
637
+ core: plannedManifest.core.skills,
638
+ mcp_registrations: mcpRegistrations,
639
+ },
640
+ }),
641
+ );
642
+ return;
643
+ }
644
+ if (configCreated) console.log(`created ${configPath}`);
645
+ if (confirmedTargets.length > 0) {
646
+ console.log(
647
+ `\nwrote ${join(vaultPath, "skillmux.toml")}, adopted: ${confirmedTargets.map((t) => t.name).join(", ")}`,
648
+ );
649
+ } else if (selectedCoreSkillIds.length > 0) {
650
+ console.log(`\nwrote ${join(vaultPath, "skillmux.toml")}`);
651
+ }
652
+ if (plannedManifest.core.skills.length === 0 && confirmedTargets.length > 0) {
653
+ console.log("next: skillmux core pin <skill_id> --yes");
654
+ }
655
+ if (!sync && confirmedTargets.length > 0) console.log("next: skillmux sync");
656
+ for (const registration of mcpRegistrations) {
657
+ console.log(
658
+ registration.ok
659
+ ? `registered skillmux as an MCP server for ${registration.agent}`
660
+ : `failed to register skillmux as an MCP server for ${registration.agent}: ${registration.error}`,
661
+ );
662
+ }
663
+ if (selectedAgents.length === 0 || showMcpSetup) {
664
+ console.log(`\n${printLastMile()}`);
665
+ }
666
+ // Reaching this point already required approval above (--yes, or an accepted
667
+ // confirmAction naming these exact targets/dirs) — that approval covers whatever
668
+ // new target directories this init just adopted, so runSync's own new-target
669
+ // confirmation gate would just be a redundant (and non-interactively,
670
+ // silently-skipping) re-ask.
671
+ if (sync && confirmedTargets.length > 0) await runSync(["--yes"]);
672
+ }