@klhapp/skillmux 1.11.2 → 1.13.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 (104) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +20 -10
  3. package/bin/skillmux.js +89 -0
  4. package/docs/README.md +4 -4
  5. package/docs/cli.md +15 -5
  6. package/docs/concepts.md +4 -3
  7. package/docs/configuration.md +22 -5
  8. package/docs/deployment.md +3 -3
  9. package/docs/getting-started.md +32 -7
  10. package/docs/releasing.md +17 -2
  11. package/docs/sdd/cli-remote-target-parity/schema-bucket-b.yaml +297 -0
  12. package/docs/sdd/cli-remote-target-parity/spec-bucket-b.md +95 -0
  13. package/docs/sdd/cli-remote-target-parity/spec.md +111 -0
  14. package/docs/sdd/cli-remote-target-parity/think.md +183 -0
  15. package/docs/sdd/cli-surface-consistency/think.md +149 -0
  16. package/docs/sdd/config-authority-rerank-resilience/plan.md +338 -0
  17. package/docs/sdd/config-authority-rerank-resilience/review.md +124 -0
  18. package/docs/sdd/config-authority-rerank-resilience/spec.md +78 -0
  19. package/docs/sdd/feat-packaging-deployment/review.md +95 -0
  20. package/docs/sdd/fetch-outcome-flywheel/review.md +103 -0
  21. package/docs/sdd/fetch-outcome-flywheel/schema.json +347 -0
  22. package/docs/sdd/fetch-outcome-flywheel/spec.md +185 -0
  23. package/docs/sdd/npm-native-binaries/review.md +103 -0
  24. package/docs/sdd/npm-native-binaries/spec.md +148 -0
  25. package/docs/sdd/ranked-shortlist-2/pr3-calibration-removal.md +56 -0
  26. package/docs/sdd/refactor-builtin-target-paths/spec.md +41 -0
  27. package/docs/sdd/runtime-resource-hardening/review.md +86 -0
  28. package/docs/sdd/runtime-resource-hardening/spec.md +43 -0
  29. package/docs/sdd/security-hardening-phase/think.md +129 -0
  30. package/docs/sdd/security-observability/review.md +96 -0
  31. package/docs/sdd/security-observability/spec.md +55 -0
  32. package/docs/sdd/skill-provenance-update/review.md +152 -0
  33. package/docs/sdd/skill-provenance-update/schema.json +193 -0
  34. package/docs/sdd/skill-provenance-update/spec.md +134 -0
  35. package/docs/sdd/supply-chain-hardening/review.md +50 -0
  36. package/docs/sdd/supply-chain-hardening/spec.md +42 -0
  37. package/docs/sdd/target-marker-rehome/spec.md +33 -0
  38. package/docs/skill-management.md +1 -1
  39. package/docs/troubleshooting.md +18 -2
  40. package/package.json +15 -8
  41. package/src/adapters.ts +0 -438
  42. package/src/audit.ts +0 -21
  43. package/src/cli.ts +0 -928
  44. package/src/clients.ts +0 -368
  45. package/src/commands/audit.ts +0 -85
  46. package/src/commands/config.ts +0 -224
  47. package/src/commands/context.ts +0 -104
  48. package/src/commands/core.ts +0 -56
  49. package/src/commands/doctor.ts +0 -97
  50. package/src/commands/eval.ts +0 -88
  51. package/src/commands/init.ts +0 -672
  52. package/src/commands/install.ts +0 -155
  53. package/src/commands/local-vault.ts +0 -60
  54. package/src/commands/models.ts +0 -10
  55. package/src/commands/outdated.ts +0 -115
  56. package/src/commands/project.ts +0 -580
  57. package/src/commands/report.ts +0 -66
  58. package/src/commands/scan.ts +0 -71
  59. package/src/commands/shared.ts +0 -38
  60. package/src/commands/skill.ts +0 -33
  61. package/src/commands/sync.ts +0 -233
  62. package/src/commands/target.ts +0 -225
  63. package/src/commands/update.ts +0 -277
  64. package/src/completions.ts +0 -196
  65. package/src/concurrency-limiter.ts +0 -61
  66. package/src/config-service.ts +0 -398
  67. package/src/config-watcher.ts +0 -253
  68. package/src/config.ts +0 -583
  69. package/src/context.ts +0 -188
  70. package/src/db-audit.ts +0 -286
  71. package/src/db-index.ts +0 -238
  72. package/src/db.ts +0 -3
  73. package/src/deployment.ts +0 -39
  74. package/src/doctor.ts +0 -205
  75. package/src/eval.ts +0 -302
  76. package/src/global-flags.ts +0 -46
  77. package/src/init-agents.ts +0 -329
  78. package/src/init-instructions.ts +0 -192
  79. package/src/init.ts +0 -389
  80. package/src/install.ts +0 -232
  81. package/src/lifecycle.ts +0 -51
  82. package/src/logger.ts +0 -26
  83. package/src/manifest.ts +0 -352
  84. package/src/mcp-registration.ts +0 -89
  85. package/src/metrics.ts +0 -121
  86. package/src/models.ts +0 -20
  87. package/src/output.ts +0 -214
  88. package/src/project-setup.ts +0 -36
  89. package/src/prompts.ts +0 -124
  90. package/src/provenance.ts +0 -99
  91. package/src/rate-limiter.ts +0 -137
  92. package/src/readiness.ts +0 -30
  93. package/src/redact.ts +0 -52
  94. package/src/router-core.ts +0 -666
  95. package/src/rrf.ts +0 -31
  96. package/src/scan.ts +0 -300
  97. package/src/server.ts +0 -930
  98. package/src/setup.ts +0 -145
  99. package/src/snapshot.ts +0 -135
  100. package/src/stats.ts +0 -340
  101. package/src/sync.ts +0 -482
  102. package/src/toml-writer.ts +0 -51
  103. package/src/types.ts +0 -212
  104. package/src/vault.ts +0 -207
@@ -1,580 +0,0 @@
1
- import { existsSync, lstatSync } from "node:fs";
2
- import { basename } from "node:path";
3
- import { expandHome } from "../config";
4
- import { planAgentSurfaces, SUPPORTED_AGENT_IDS, type AgentId } from "../init-agents";
5
- import {
6
- applyInstructionPlan,
7
- planProjectInstructionSetup,
8
- } from "../init-instructions";
9
- import {
10
- parseManifest,
11
- pinProject,
12
- unpinProject,
13
- updateProjectPaths,
14
- updateProjectTargets,
15
- upsertProject,
16
- validateManifest,
17
- resolveTargetDir,
18
- writeManifestAtomic,
19
- } from "../manifest";
20
- import {
21
- MCP_PROJECT_REGISTRABLE_AGENTS,
22
- registerMcpServer,
23
- type McpRegistrationResult,
24
- } from "../mcp-registration";
25
- import { resolveProjectDirectory, suggestProjectName } from "../project-setup";
26
- import {
27
- parseCommaList,
28
- promptMultiSelect,
29
- promptText,
30
- shouldUseWizard,
31
- } from "../prompts";
32
- import { emitSuccess, isInteractive, unknownSubcommandError } from "../output";
33
- import { confirmAction, confirmIfNeeded, loadManifestContext } from "./shared";
34
- import { isGlobalFlag } from "../global-flags";
35
- const PROJECT_INIT_USAGE =
36
- "usage: skillmux project init [path] [--name <group>] [--skill <id>...] [--agent <id>...] [--target <name>...] [--register-mcp] [--yes] [--no-sync]";
37
-
38
- interface ProjectInitArgs {
39
- path: string;
40
- name: string;
41
- skills: string[];
42
- agents: string[];
43
- targets: string[];
44
- registerMcp: boolean;
45
- yes: boolean;
46
- sync: boolean;
47
- }
48
-
49
- export function configuredTargetForSurface(
50
- manifest: ReturnType<typeof parseManifest>,
51
- surface: { targetName: string; path: string },
52
- ): string | undefined {
53
- if (manifest.targets[surface.targetName]) return surface.targetName;
54
- return Object.entries(manifest.targets).find(
55
- ([name, target]) => resolveTargetDir(name, target) === surface.path,
56
- )?.[0];
57
- }
58
-
59
- function configuredTargetsForAgents(
60
- manifest: ReturnType<typeof parseManifest>,
61
- agents: readonly string[],
62
- ): string[] {
63
- const plan = planAgentSurfaces(agents);
64
-
65
- // planAgentSurfaces silently drops any agent with no surfaceId, which is how
66
- // full-vault agents (goose, hermes) are modelled: they get the whole vault
67
- // rather than a sync target directory. Dropping one here used to make
68
- // "project attach --agent goose" a successful no-op, so refuse instead.
69
- const covered = new Set<string>(plan.surfaces.flatMap((surface) => surface.agents));
70
- for (const agent of agents) {
71
- if (!covered.has(agent)) {
72
- throw new Error(
73
- `agent "${agent}" uses full-vault delivery and maps to no sync target; ` +
74
- `name a target directly with --target instead`,
75
- );
76
- }
77
- }
78
-
79
- return plan.surfaces.map((surface) => {
80
- const target = configuredTargetForSurface(manifest, surface);
81
- if (target) return target;
82
- const agent = surface.agents[0]!;
83
- throw new Error(
84
- `agent target for "${agent}" is not configured; run "skillmux init --agent ${agent} --yes" first`,
85
- );
86
- });
87
- }
88
-
89
- function parseProjectInitArgs(args: string[]): ProjectInitArgs {
90
- let projectPath: string | undefined;
91
- let name: string | undefined;
92
- const skills: string[] = [];
93
- const agents: string[] = [];
94
- const targets: string[] = [];
95
- let registerMcp = false;
96
- let yes = false;
97
- let sync = true;
98
-
99
- for (let i = 0; i < args.length; i++) {
100
- const arg = args[i]!;
101
- if (arg === "--name") {
102
- name = args[++i];
103
- if (!name) throw new Error("--name requires a group name");
104
- } else if (arg === "--skill") {
105
- const skill = args[++i];
106
- if (!skill) throw new Error("--skill requires a skill_id");
107
- skills.push(skill);
108
- } else if (arg === "--target") {
109
- const target = args[++i];
110
- if (!target) throw new Error("--target requires a name");
111
- targets.push(target);
112
- } else if (arg === "--agent") {
113
- const agent = args[++i];
114
- if (!agent) throw new Error("--agent requires a name");
115
- agents.push(agent);
116
- } else if (arg === "--register-mcp") {
117
- registerMcp = true;
118
- } else if (arg === "--yes") {
119
- yes = true;
120
- } else if (arg === "--no-sync") {
121
- sync = false;
122
- } else if (
123
- isGlobalFlag(arg, "--dry-run", "--json") ||
124
- arg === "--interactive"
125
- ) {
126
- continue;
127
- } else if (arg.startsWith("-")) {
128
- throw new Error(`unknown project init option: ${arg}`);
129
- } else if (projectPath) {
130
- throw new Error(PROJECT_INIT_USAGE);
131
- } else {
132
- projectPath = arg;
133
- }
134
- }
135
-
136
- const path = resolveProjectDirectory(
137
- projectPath ? expandHome(projectPath) : undefined,
138
- );
139
- return {
140
- path,
141
- name: name ?? suggestProjectName(basename(path)),
142
- skills,
143
- agents,
144
- targets,
145
- registerMcp,
146
- yes,
147
- sync,
148
- };
149
- }
150
-
151
- export async function runProject(
152
- subCommand: string,
153
- args: string[],
154
- options: {
155
- isJson: boolean;
156
- dryRun: boolean;
157
- sync: (args: string[]) => Promise<void>;
158
- },
159
- ): Promise<void> {
160
- if (subCommand === "list" || subCommand === "show") {
161
- const { manifest } = await loadManifestContext();
162
- const names =
163
- subCommand === "show"
164
- ? [args[0] ?? ""]
165
- : Object.keys(manifest.project ?? {});
166
- if (subCommand === "show" && !manifest.project?.[names[0]!]) {
167
- throw new Error(`[project.${names[0]}] does not exist`);
168
- }
169
- const projects = names.map((name) => ({
170
- name,
171
- paths: manifest.project?.[name]!.paths ?? [],
172
- skills: manifest.project?.[name]!.skills ?? [],
173
- targets: Object.entries(manifest.targets)
174
- .filter(([, target]) => target.project_groups.includes(name))
175
- .map(([target]) => target),
176
- }));
177
- emitSuccess({ isJson: options.isJson }, { projects }, () => {
178
- if (projects.length === 0) {
179
- console.log("no project groups configured");
180
- } else {
181
- for (const project of projects) {
182
- console.log(`${project.name}:`);
183
- console.log(` paths: ${project.paths.join(", ") || "(none)"}`);
184
- console.log(` skills: ${project.skills.join(", ") || "(none)"}`);
185
- console.log(` targets: ${project.targets.join(", ") || "(none)"}`);
186
- }
187
- }
188
- });
189
- return;
190
- }
191
- if (subCommand === "add-path" || subCommand === "remove-path") {
192
- const group = args[0];
193
- if (!group)
194
- throw new Error(
195
- `usage: skillmux project ${subCommand} <group> [path] --yes`,
196
- );
197
- const rawPath = args[1]?.startsWith("-") ? undefined : args[1];
198
- const projectPath = resolveProjectDirectory(
199
- rawPath ? expandHome(rawPath) : undefined,
200
- );
201
- const yes = args.includes("--yes");
202
- if (!existsSync(projectPath) || !lstatSync(projectPath).isDirectory()) {
203
- throw new Error(`project path is not a directory: ${projectPath}`);
204
- }
205
- const { config, vaultPath, manifestPath, manifest } =
206
- await loadManifestContext();
207
- const updated = updateProjectPaths(manifest, group, {
208
- ...(subCommand === "add-path"
209
- ? { add: [projectPath] }
210
- : { remove: [projectPath] }),
211
- });
212
- validateManifest(
213
- updated,
214
- vaultPath,
215
- config.local_vault_paths.map(expandHome),
216
- );
217
- if (options.dryRun) {
218
- emitSuccess(
219
- { isJson: options.isJson },
220
- { subcommand: subCommand, group, path: projectPath },
221
- () => console.log(`${subCommand}: [project.${group}] ${projectPath} (dry-run)`),
222
- );
223
- return;
224
- }
225
- if (
226
- !(await confirmIfNeeded({
227
- confirmed: yes,
228
- isJson: options.isJson,
229
- prompt: `${subCommand} ${projectPath} in [project.${group}]?`,
230
- nonInteractiveError: `skillmux project ${subCommand} requires --yes when run non-interactively`,
231
- }))
232
- )
233
- return;
234
- writeManifestAtomic(manifestPath, updated);
235
- emitSuccess(
236
- { isJson: options.isJson },
237
- { subcommand: subCommand, group, path: projectPath },
238
- () => console.log(`${subCommand}: [project.${group}] ${projectPath}`),
239
- );
240
- return;
241
- }
242
- if (subCommand === "pin" || subCommand === "unpin") {
243
- const group = args[0];
244
- const skills = args.slice(1).filter((arg) => !arg.startsWith("-"));
245
- if (!group || skills.length === 0) {
246
- throw new Error(
247
- `usage: skillmux project ${subCommand} <group> <skill_id>... --yes`,
248
- );
249
- }
250
- const yes = args.includes("--yes");
251
- const { config, vaultPath, manifestPath, manifest } =
252
- await loadManifestContext();
253
- let updated = manifest;
254
- for (const skill of skills) {
255
- updated =
256
- subCommand === "pin"
257
- ? pinProject(updated, skill, group)
258
- : unpinProject(updated, skill, group);
259
- }
260
- validateManifest(
261
- updated,
262
- vaultPath,
263
- config.local_vault_paths.map(expandHome),
264
- );
265
- if (options.dryRun) {
266
- emitSuccess(
267
- { isJson: options.isJson },
268
- { subcommand: subCommand, group, skill_ids: skills },
269
- () =>
270
- console.log(
271
- `${subCommand}: [project.${group}] ${skills.join(", ")} (dry-run)`,
272
- ),
273
- );
274
- return;
275
- }
276
- if (
277
- !(await confirmIfNeeded({
278
- confirmed: yes,
279
- isJson: options.isJson,
280
- prompt: `${subCommand} ${skills.join(", ")} in [project.${group}]?`,
281
- nonInteractiveError: `skillmux project ${subCommand} requires --yes when run non-interactively`,
282
- }))
283
- )
284
- return;
285
- writeManifestAtomic(manifestPath, updated);
286
- emitSuccess(
287
- { isJson: options.isJson },
288
- { subcommand: subCommand, group, skill_ids: skills },
289
- () => console.log(`${subCommand}: [project.${group}] ${skills.join(", ")}`),
290
- );
291
- return;
292
- }
293
- if (subCommand === "attach" || subCommand === "detach") {
294
- const group = args[0];
295
- if (!group)
296
- throw new Error(
297
- `usage: skillmux project ${subCommand} <group> (--agent <id>... | --target <name>...) --yes`,
298
- );
299
- const agents: string[] = [];
300
- const requestedTargets: string[] = [];
301
- for (let i = 1; i < args.length; i++) {
302
- if (args[i] === "--agent") {
303
- const value = args[++i];
304
- if (!value) throw new Error("--agent requires a name");
305
- agents.push(value);
306
- } else if (args[i] === "--target") {
307
- const value = args[++i];
308
- if (!value) throw new Error("--target requires a name");
309
- requestedTargets.push(value);
310
- } else if (
311
- args[i] !== "--yes" &&
312
- args[i] !== "--dry-run" &&
313
- args[i] !== "--json"
314
- ) {
315
- throw new Error(`unknown project ${subCommand} option: ${args[i]}`);
316
- }
317
- }
318
- const { config, vaultPath, manifestPath, manifest } =
319
- await loadManifestContext();
320
- const agentTargets = configuredTargetsForAgents(manifest, agents);
321
- const targets = [...new Set([...requestedTargets, ...agentTargets])];
322
- if (targets.length === 0) {
323
- throw new Error(`project ${subCommand} requires --agent or --target`);
324
- }
325
- // Several agents can share one target (e.g. opencode/windsurf both use
326
- // agent-skills) — show the resolved directory, not just the target name,
327
- // so it's clear at confirmation time which physical folder this affects.
328
- const targetDirs = Object.fromEntries(
329
- targets.map((t) => {
330
- const target = manifest.targets[t];
331
- return [t, target ? resolveTargetDir(t, target) : "(unknown)"];
332
- }),
333
- );
334
- const targetsDisplay = targets
335
- .map((t) => `${t} (${targetDirs[t]})`)
336
- .join(", ");
337
- const updated = updateProjectTargets(manifest, group, {
338
- ...(subCommand === "attach" ? { attach: targets } : { detach: targets }),
339
- });
340
- validateManifest(
341
- updated,
342
- vaultPath,
343
- config.local_vault_paths.map(expandHome),
344
- );
345
- if (options.dryRun) {
346
- emitSuccess(
347
- { isJson: options.isJson },
348
- { subcommand: subCommand, group, targets, target_dirs: targetDirs },
349
- () =>
350
- console.log(
351
- `${subCommand}: [project.${group}] ${targetsDisplay} (dry-run)`,
352
- ),
353
- );
354
- return;
355
- }
356
- if (
357
- !(await confirmIfNeeded({
358
- confirmed: args.includes("--yes"),
359
- isJson: options.isJson,
360
- prompt: `${subCommand} [project.${group}] to ${targetsDisplay}?`,
361
- nonInteractiveError: `skillmux project ${subCommand} requires --yes when run non-interactively`,
362
- }))
363
- )
364
- return;
365
- writeManifestAtomic(manifestPath, updated);
366
- emitSuccess(
367
- { isJson: options.isJson },
368
- { subcommand: subCommand, group, targets, target_dirs: targetDirs },
369
- () => console.log(`${subCommand}: [project.${group}] ${targetsDisplay}`),
370
- );
371
- return;
372
- }
373
- if (subCommand !== "init")
374
- throw unknownSubcommandError("project", subCommand, [
375
- "init",
376
- "list",
377
- "show",
378
- "add-path",
379
- "remove-path",
380
- "pin",
381
- "unpin",
382
- "attach",
383
- "detach",
384
- ]);
385
- let request = parseProjectInitArgs(args);
386
- const guided = shouldUseWizard(args, {
387
- interactive: isInteractive(),
388
- json: options.isJson,
389
- dryRun: options.dryRun,
390
- });
391
- if (!existsSync(request.path))
392
- throw new Error(`project path does not exist: ${request.path}`);
393
- if (!lstatSync(request.path).isDirectory()) {
394
- throw new Error(`project path is not a directory: ${request.path}`);
395
- }
396
-
397
- const { config, vaultPath, manifestPath, manifest } =
398
- await loadManifestContext();
399
- const localVaultPaths = config.local_vault_paths.map(expandHome);
400
- if (guided) {
401
- const name = await promptText("Project group", request.name);
402
- const availableAgents = SUPPORTED_AGENT_IDS.filter((agent) => {
403
- const surface = planAgentSurfaces([agent]).surfaces[0];
404
- return (
405
- surface !== undefined &&
406
- configuredTargetForSurface(manifest, surface) !== undefined
407
- );
408
- });
409
- const agents = await promptMultiSelect(
410
- "Which agents should receive project skills?",
411
- availableAgents.map((agent) => ({
412
- value: agent,
413
- label: agent,
414
- selected:
415
- request.agents.length === 0 || request.agents.includes(agent),
416
- })),
417
- );
418
- const skills = parseCommaList(
419
- await promptText(
420
- "Project skill IDs, comma-separated",
421
- request.skills.join(","),
422
- ),
423
- );
424
- request = { ...request, name, agents, skills };
425
- }
426
- // Local MCP registration + instruction writing are independent of skill
427
- // pins — only offered for agents with a verified project-scoped CLI
428
- // command (currently just claude-code; see MCP_PROJECT_REGISTRABLE_AGENTS).
429
- const registrableAgents = request.agents.filter((agent) =>
430
- MCP_PROJECT_REGISTRABLE_AGENTS.includes(agent as AgentId),
431
- ) as AgentId[];
432
- if (guided && registrableAgents.length > 0) {
433
- request = {
434
- ...request,
435
- registerMcp: await confirmAction(
436
- `Also register skillmux as a project-scoped MCP server for ${registrableAgents.join(", ")}? ` +
437
- `This writes ${request.path}/.mcp.json, shared via git.`,
438
- ),
439
- };
440
- }
441
- const agentTargets = configuredTargetsForAgents(manifest, request.agents);
442
- const targets = [...new Set([...request.targets, ...agentTargets])];
443
- const updated = upsertProject(manifest, {
444
- name: request.name,
445
- paths: [request.path],
446
- skills: request.skills,
447
- targets,
448
- });
449
- const { notes } = validateManifest(updated, vaultPath, localVaultPaths);
450
-
451
- // The project-local instruction block only teaches an agent to call
452
- // resolve_skill/fetch_skill (MCP tools) — write it only for agents that
453
- // are actually getting a project-scoped MCP registration this run.
454
- const mcpInstructionAgents = request.registerMcp ? registrableAgents : [];
455
- const instructionPlan = planProjectInstructionSetup(
456
- mcpInstructionAgents,
457
- request.path,
458
- );
459
- const hasInstructionWrites = instructionPlan.changes.some(
460
- (change) => change.status !== "unchanged",
461
- );
462
-
463
- const plan = {
464
- mode: "project",
465
- project: request.name,
466
- path: request.path,
467
- skills: request.skills,
468
- agents: request.agents,
469
- targets,
470
- sync: request.sync,
471
- notes,
472
- instructions: instructionPlan.changes.map(({ path, agents, status }) => ({
473
- path,
474
- agents,
475
- status,
476
- })),
477
- register_mcp_for: mcpInstructionAgents,
478
- };
479
-
480
- if (options.dryRun) {
481
- emitSuccess({ isJson: options.isJson }, { plan }, () =>
482
- console.log(`project plan: ${JSON.stringify(plan)}`),
483
- );
484
- return;
485
- }
486
- if (!request.yes) {
487
- if (!options.isJson && isInteractive()) {
488
- if (guided) {
489
- console.log("\nReview");
490
- console.log(` project: ${request.name}`);
491
- console.log(` path: ${request.path}`);
492
- console.log(` agents: ${request.agents.join(", ") || "(none)"}`);
493
- console.log(` skills: ${request.skills.join(", ") || "(none)"}`);
494
- console.log(
495
- ` instructions: ${instructionPlan.changes.filter((change) => change.status !== "unchanged").length} file(s)`,
496
- );
497
- console.log(
498
- ` MCP registration: ${mcpInstructionAgents.join(", ") || "(none)"}`,
499
- );
500
- console.log(` sync: ${request.sync ? "yes" : "no"}`);
501
- }
502
- if (
503
- !(await confirmAction(
504
- `apply project setup for ${request.name} at ${request.path}?`,
505
- ))
506
- ) {
507
- console.log("project setup cancelled");
508
- return;
509
- }
510
- } else {
511
- throw new Error(
512
- "skillmux project init requires --yes when run non-interactively",
513
- );
514
- }
515
- }
516
-
517
- writeManifestAtomic(manifestPath, updated);
518
- if (hasInstructionWrites) {
519
- try {
520
- applyInstructionPlan(instructionPlan);
521
- } catch (error) {
522
- throw new Error(
523
- `project configuration was saved, but writing instruction files failed: ${
524
- error instanceof Error ? error.message : String(error)
525
- }`,
526
- );
527
- }
528
- }
529
- if (request.sync) {
530
- try {
531
- // Reaching here already required approval above (request.yes, or an
532
- // accepted interactive confirmAction) — that approval covers whatever
533
- // new target/pin directories this project setup implies, so the
534
- // downstream sync's own new-target confirmation gate would just be a
535
- // redundant (and, non-interactively, silently-skipping) re-ask.
536
- await options.sync(["--yes"]);
537
- } catch (error) {
538
- throw new Error(
539
- `project configuration was saved, but sync failed; fix the reported issue and run "skillmux sync": ${
540
- error instanceof Error ? error.message : String(error)
541
- }`,
542
- );
543
- }
544
- }
545
-
546
- // Best-effort and outside the checks above: this mutates another tool's
547
- // own config, not skillmux's, so a registration failure is reported, never
548
- // rolled back — the successful project setup above still stands either way.
549
- const mcpRegistrations: McpRegistrationResult[] = [];
550
- if (request.registerMcp) {
551
- for (const agent of registrableAgents) {
552
- mcpRegistrations.push(
553
- await registerMcpServer(agent, { scope: "project", cwd: request.path }),
554
- );
555
- }
556
- }
557
-
558
- emitSuccess(
559
- { isJson: options.isJson },
560
- {
561
- result: {
562
- ...plan,
563
- instructions_changed: instructionPlan.changes
564
- .filter((change) => change.status !== "unchanged")
565
- .map((change) => change.path),
566
- mcp_registrations: mcpRegistrations,
567
- },
568
- },
569
- () => {
570
- console.log(`project "${request.name}" ready at ${request.path}`);
571
- for (const registration of mcpRegistrations) {
572
- console.log(
573
- registration.ok
574
- ? `MCP registered: ${registration.agent} (project scope)`
575
- : `MCP registration failed for ${registration.agent}: ${registration.error}`,
576
- );
577
- }
578
- },
579
- );
580
- }
@@ -1,66 +0,0 @@
1
- import { Database } from "bun:sqlite";
2
- import type { ContextAdapter } from "../adapters";
3
- import type { ResolvedContext } from "../context";
4
- import { emitSuccess } from "../output";
5
- import { getStats, renderStatsText } from "../stats";
6
- import { isGlobalFlag, isGlobalFlagWithValue } from "../global-flags";
7
-
8
- function parseReportArgs(args: string[]): {
9
- db?: string;
10
- since?: string;
11
- } {
12
- let db: string | undefined;
13
- let since: string | undefined;
14
- for (let i = 0; i < args.length; i++) {
15
- const option = args[i];
16
- const value = args[i + 1];
17
- if (option === "--db") {
18
- if (!value) throw new Error("--db requires a path");
19
- db = value;
20
- i++;
21
- } else if (option === "--since") {
22
- if (!value) throw new Error("--since requires a window");
23
- since = value;
24
- i++;
25
- } else if (isGlobalFlag(option, "--json", "--allow-insecure")) {
26
- // handled globally by main()'s isJson/allowInsecure flags; recognized here so it isn't rejected
27
- } else if (isGlobalFlagWithValue(option)) {
28
- // handled globally by main()'s resolveContext(); recognized here so it isn't rejected
29
- i++;
30
- } else {
31
- throw new Error(`unknown report option: ${option}`);
32
- }
33
- }
34
- return { db, since };
35
- }
36
-
37
- export async function runReport(
38
- args: string[],
39
- options: { isJson: boolean; context: ResolvedContext; allowInsecure: boolean; adapter: ContextAdapter },
40
- ): Promise<void> {
41
- const { db: dbPath, since } = parseReportArgs(args);
42
- if (!since)
43
- throw new Error(
44
- "usage: skillmux report [--context <name> | --server <url> | --db <path>] --since <window> [--json]",
45
- );
46
- if (dbPath && options.context.type === "remote")
47
- throw new Error("--db and --context/--server are mutually exclusive");
48
-
49
- if (dbPath) {
50
- const db = new Database(dbPath, { readonly: true });
51
- try {
52
- const stats = getStats(db, since);
53
- emitSuccess({ isJson: options.isJson }, stats, () =>
54
- console.log(renderStatsText(stats)),
55
- );
56
- } finally {
57
- db.close();
58
- }
59
- return;
60
- }
61
-
62
- const stats = await options.adapter.getStats(since);
63
- emitSuccess({ isJson: options.isJson }, stats, () =>
64
- console.log(renderStatsText(stats)),
65
- );
66
- }