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