@klhapp/skillmux 1.10.0 → 1.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +26 -19
- package/docs/README.md +3 -3
- package/docs/assets/architecture-dark.svg +39 -32
- package/docs/assets/architecture-light.svg +25 -18
- package/docs/cli.md +80 -33
- package/docs/concepts.md +15 -11
- package/docs/configuration.md +6 -4
- package/docs/deployment.md +1 -1
- package/docs/getting-started.md +21 -13
- package/docs/mcp-routing.md +1 -1
- package/docs/skill-management.md +39 -19
- package/docs/troubleshooting.md +4 -4
- package/package.json +1 -1
- package/src/adapters.ts +11 -11
- package/src/cli.ts +255 -72
- package/src/commands/audit.ts +2 -2
- package/src/commands/config.ts +23 -15
- package/src/commands/context.ts +11 -10
- package/src/commands/core.ts +2 -2
- package/src/commands/doctor.ts +31 -10
- package/src/commands/eval.ts +14 -4
- package/src/commands/init.ts +175 -124
- package/src/commands/install.ts +36 -13
- package/src/commands/project.ts +177 -44
- package/src/commands/report.ts +3 -3
- package/src/commands/scan.ts +18 -8
- package/src/commands/shared.ts +7 -14
- package/src/commands/skill.ts +2 -1
- package/src/commands/target.ts +27 -9
- package/src/commands/update.ts +16 -9
- package/src/completions.ts +41 -15
- package/src/config-service.ts +3 -3
- package/src/init-agents.ts +329 -0
- package/src/init-instructions.ts +47 -28
- package/src/mcp-registration.ts +89 -0
- package/src/output.ts +53 -16
- package/src/prompts.ts +75 -20
- package/src/scan.ts +53 -19
- package/src/server.ts +1 -1
- package/src/init-clients.ts +0 -220
package/src/commands/project.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { existsSync, lstatSync } from "node:fs";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { expandHome } from "../config";
|
|
4
|
-
import {
|
|
4
|
+
import { planAgentSurfaces, SUPPORTED_AGENT_IDS, type AgentId } from "../init-agents";
|
|
5
|
+
import {
|
|
6
|
+
applyInstructionPlan,
|
|
7
|
+
planProjectInstructionSetup,
|
|
8
|
+
} from "../init-instructions";
|
|
5
9
|
import {
|
|
6
10
|
parseManifest,
|
|
7
11
|
pinProject,
|
|
@@ -12,6 +16,11 @@ import {
|
|
|
12
16
|
validateManifest,
|
|
13
17
|
writeManifestAtomic,
|
|
14
18
|
} from "../manifest";
|
|
19
|
+
import {
|
|
20
|
+
MCP_PROJECT_REGISTRABLE_AGENTS,
|
|
21
|
+
registerMcpServer,
|
|
22
|
+
type McpRegistrationResult,
|
|
23
|
+
} from "../mcp-registration";
|
|
15
24
|
import { resolveProjectDirectory, suggestProjectName } from "../project-setup";
|
|
16
25
|
import {
|
|
17
26
|
parseCommaList,
|
|
@@ -19,18 +28,19 @@ import {
|
|
|
19
28
|
promptText,
|
|
20
29
|
shouldUseWizard,
|
|
21
30
|
} from "../prompts";
|
|
22
|
-
import { emitSuccess, isInteractive } from "../output";
|
|
31
|
+
import { emitSuccess, isInteractive, unknownSubcommandError } from "../output";
|
|
23
32
|
import { confirmAction, confirmIfNeeded, loadManifestContext } from "./shared";
|
|
24
33
|
import { isGlobalFlag } from "../global-flags";
|
|
25
34
|
const PROJECT_INIT_USAGE =
|
|
26
|
-
"usage: skillmux project init [path] [--name <group>] [--skill <id>...] [--
|
|
35
|
+
"usage: skillmux project init [path] [--name <group>] [--skill <id>...] [--agent <id>...] [--target <name>...] [--register-mcp] [--yes] [--no-sync]";
|
|
27
36
|
|
|
28
37
|
interface ProjectInitArgs {
|
|
29
38
|
path: string;
|
|
30
39
|
name: string;
|
|
31
40
|
skills: string[];
|
|
32
|
-
|
|
41
|
+
agents: string[];
|
|
33
42
|
targets: string[];
|
|
43
|
+
registerMcp: boolean;
|
|
34
44
|
yes: boolean;
|
|
35
45
|
sync: boolean;
|
|
36
46
|
}
|
|
@@ -45,16 +55,32 @@ export function configuredTargetForSurface(
|
|
|
45
55
|
)?.[0];
|
|
46
56
|
}
|
|
47
57
|
|
|
48
|
-
function
|
|
58
|
+
function configuredTargetsForAgents(
|
|
49
59
|
manifest: ReturnType<typeof parseManifest>,
|
|
50
|
-
|
|
60
|
+
agents: readonly string[],
|
|
51
61
|
): string[] {
|
|
52
|
-
|
|
62
|
+
const plan = planAgentSurfaces(agents);
|
|
63
|
+
|
|
64
|
+
// planAgentSurfaces silently drops any agent with no surfaceId, which is how
|
|
65
|
+
// full-vault agents (goose, hermes) are modelled: they get the whole vault
|
|
66
|
+
// rather than a sync target directory. Dropping one here used to make
|
|
67
|
+
// "project attach --agent goose" a successful no-op, so refuse instead.
|
|
68
|
+
const covered = new Set<string>(plan.surfaces.flatMap((surface) => surface.agents));
|
|
69
|
+
for (const agent of agents) {
|
|
70
|
+
if (!covered.has(agent)) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`agent "${agent}" uses full-vault delivery and maps to no sync target; ` +
|
|
73
|
+
`name a target directly with --target instead`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return plan.surfaces.map((surface) => {
|
|
53
79
|
const target = configuredTargetForSurface(manifest, surface);
|
|
54
80
|
if (target) return target;
|
|
55
|
-
const
|
|
81
|
+
const agent = surface.agents[0]!;
|
|
56
82
|
throw new Error(
|
|
57
|
-
`
|
|
83
|
+
`agent target for "${agent}" is not configured; run "skillmux init --agent ${agent} --yes" first`,
|
|
58
84
|
);
|
|
59
85
|
});
|
|
60
86
|
}
|
|
@@ -63,8 +89,9 @@ function parseProjectInitArgs(args: string[]): ProjectInitArgs {
|
|
|
63
89
|
let projectPath: string | undefined;
|
|
64
90
|
let name: string | undefined;
|
|
65
91
|
const skills: string[] = [];
|
|
66
|
-
const
|
|
92
|
+
const agents: string[] = [];
|
|
67
93
|
const targets: string[] = [];
|
|
94
|
+
let registerMcp = false;
|
|
68
95
|
let yes = false;
|
|
69
96
|
let sync = true;
|
|
70
97
|
|
|
@@ -81,10 +108,12 @@ function parseProjectInitArgs(args: string[]): ProjectInitArgs {
|
|
|
81
108
|
const target = args[++i];
|
|
82
109
|
if (!target) throw new Error("--target requires a name");
|
|
83
110
|
targets.push(target);
|
|
84
|
-
} else if (arg === "--
|
|
85
|
-
const
|
|
86
|
-
if (!
|
|
87
|
-
|
|
111
|
+
} else if (arg === "--agent") {
|
|
112
|
+
const agent = args[++i];
|
|
113
|
+
if (!agent) throw new Error("--agent requires a name");
|
|
114
|
+
agents.push(agent);
|
|
115
|
+
} else if (arg === "--register-mcp") {
|
|
116
|
+
registerMcp = true;
|
|
88
117
|
} else if (arg === "--yes") {
|
|
89
118
|
yes = true;
|
|
90
119
|
} else if (arg === "--no-sync") {
|
|
@@ -110,8 +139,9 @@ function parseProjectInitArgs(args: string[]): ProjectInitArgs {
|
|
|
110
139
|
path,
|
|
111
140
|
name: name ?? suggestProjectName(basename(path)),
|
|
112
141
|
skills,
|
|
113
|
-
|
|
142
|
+
agents,
|
|
114
143
|
targets,
|
|
144
|
+
registerMcp,
|
|
115
145
|
yes,
|
|
116
146
|
sync,
|
|
117
147
|
};
|
|
@@ -263,15 +293,15 @@ export async function runProject(
|
|
|
263
293
|
const group = args[0];
|
|
264
294
|
if (!group)
|
|
265
295
|
throw new Error(
|
|
266
|
-
`usage: skillmux project ${subCommand} <group> (--
|
|
296
|
+
`usage: skillmux project ${subCommand} <group> (--agent <id>... | --target <name>...) --yes`,
|
|
267
297
|
);
|
|
268
|
-
const
|
|
298
|
+
const agents: string[] = [];
|
|
269
299
|
const requestedTargets: string[] = [];
|
|
270
300
|
for (let i = 1; i < args.length; i++) {
|
|
271
|
-
if (args[i] === "--
|
|
301
|
+
if (args[i] === "--agent") {
|
|
272
302
|
const value = args[++i];
|
|
273
|
-
if (!value) throw new Error("--
|
|
274
|
-
|
|
303
|
+
if (!value) throw new Error("--agent requires a name");
|
|
304
|
+
agents.push(value);
|
|
275
305
|
} else if (args[i] === "--target") {
|
|
276
306
|
const value = args[++i];
|
|
277
307
|
if (!value) throw new Error("--target requires a name");
|
|
@@ -286,11 +316,20 @@ export async function runProject(
|
|
|
286
316
|
}
|
|
287
317
|
const { config, vaultPath, manifestPath, manifest } =
|
|
288
318
|
await loadManifestContext();
|
|
289
|
-
const
|
|
290
|
-
const targets = [...new Set([...requestedTargets, ...
|
|
319
|
+
const agentTargets = configuredTargetsForAgents(manifest, agents);
|
|
320
|
+
const targets = [...new Set([...requestedTargets, ...agentTargets])];
|
|
291
321
|
if (targets.length === 0) {
|
|
292
|
-
throw new Error(`project ${subCommand} requires --
|
|
322
|
+
throw new Error(`project ${subCommand} requires --agent or --target`);
|
|
293
323
|
}
|
|
324
|
+
// Several agents can share one target (e.g. opencode/windsurf both use
|
|
325
|
+
// agent-skills) — show the resolved directory, not just the target name,
|
|
326
|
+
// so it's clear at confirmation time which physical folder this affects.
|
|
327
|
+
const targetDirs = Object.fromEntries(
|
|
328
|
+
targets.map((t) => [t, manifest.targets[t]?.dir ?? "(unknown)"]),
|
|
329
|
+
);
|
|
330
|
+
const targetsDisplay = targets
|
|
331
|
+
.map((t) => `${t} (${targetDirs[t]})`)
|
|
332
|
+
.join(", ");
|
|
294
333
|
const updated = updateProjectTargets(manifest, group, {
|
|
295
334
|
...(subCommand === "attach" ? { attach: targets } : { detach: targets }),
|
|
296
335
|
});
|
|
@@ -302,10 +341,10 @@ export async function runProject(
|
|
|
302
341
|
if (options.dryRun) {
|
|
303
342
|
emitSuccess(
|
|
304
343
|
{ isJson: options.isJson },
|
|
305
|
-
{ subcommand: subCommand, group, targets },
|
|
344
|
+
{ subcommand: subCommand, group, targets, target_dirs: targetDirs },
|
|
306
345
|
() =>
|
|
307
346
|
console.log(
|
|
308
|
-
`${subCommand}: [project.${group}] ${
|
|
347
|
+
`${subCommand}: [project.${group}] ${targetsDisplay} (dry-run)`,
|
|
309
348
|
),
|
|
310
349
|
);
|
|
311
350
|
return;
|
|
@@ -314,7 +353,7 @@ export async function runProject(
|
|
|
314
353
|
!(await confirmIfNeeded({
|
|
315
354
|
confirmed: args.includes("--yes"),
|
|
316
355
|
isJson: options.isJson,
|
|
317
|
-
prompt: `${subCommand} [project.${group}] to ${
|
|
356
|
+
prompt: `${subCommand} [project.${group}] to ${targetsDisplay}?`,
|
|
318
357
|
nonInteractiveError: `skillmux project ${subCommand} requires --yes when run non-interactively`,
|
|
319
358
|
}))
|
|
320
359
|
)
|
|
@@ -322,12 +361,23 @@ export async function runProject(
|
|
|
322
361
|
writeManifestAtomic(manifestPath, updated);
|
|
323
362
|
emitSuccess(
|
|
324
363
|
{ isJson: options.isJson },
|
|
325
|
-
{ subcommand: subCommand, group, targets },
|
|
326
|
-
() => console.log(`${subCommand}: [project.${group}] ${
|
|
364
|
+
{ subcommand: subCommand, group, targets, target_dirs: targetDirs },
|
|
365
|
+
() => console.log(`${subCommand}: [project.${group}] ${targetsDisplay}`),
|
|
327
366
|
);
|
|
328
367
|
return;
|
|
329
368
|
}
|
|
330
|
-
if (subCommand !== "init")
|
|
369
|
+
if (subCommand !== "init")
|
|
370
|
+
throw unknownSubcommandError("project", subCommand, [
|
|
371
|
+
"init",
|
|
372
|
+
"list",
|
|
373
|
+
"show",
|
|
374
|
+
"add-path",
|
|
375
|
+
"remove-path",
|
|
376
|
+
"pin",
|
|
377
|
+
"unpin",
|
|
378
|
+
"attach",
|
|
379
|
+
"detach",
|
|
380
|
+
]);
|
|
331
381
|
let request = parseProjectInitArgs(args);
|
|
332
382
|
const guided = shouldUseWizard(args, {
|
|
333
383
|
interactive: isInteractive(),
|
|
@@ -345,20 +395,20 @@ export async function runProject(
|
|
|
345
395
|
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
346
396
|
if (guided) {
|
|
347
397
|
const name = await promptText("Project group", request.name);
|
|
348
|
-
const
|
|
349
|
-
const surface =
|
|
398
|
+
const availableAgents = SUPPORTED_AGENT_IDS.filter((agent) => {
|
|
399
|
+
const surface = planAgentSurfaces([agent]).surfaces[0];
|
|
350
400
|
return (
|
|
351
401
|
surface !== undefined &&
|
|
352
402
|
configuredTargetForSurface(manifest, surface) !== undefined
|
|
353
403
|
);
|
|
354
404
|
});
|
|
355
|
-
const
|
|
356
|
-
"Which
|
|
357
|
-
|
|
358
|
-
value:
|
|
359
|
-
label:
|
|
405
|
+
const agents = await promptMultiSelect(
|
|
406
|
+
"Which agents should receive project skills?",
|
|
407
|
+
availableAgents.map((agent) => ({
|
|
408
|
+
value: agent,
|
|
409
|
+
label: agent,
|
|
360
410
|
selected:
|
|
361
|
-
request.
|
|
411
|
+
request.agents.length === 0 || request.agents.includes(agent),
|
|
362
412
|
})),
|
|
363
413
|
);
|
|
364
414
|
const skills = parseCommaList(
|
|
@@ -367,10 +417,25 @@ export async function runProject(
|
|
|
367
417
|
request.skills.join(","),
|
|
368
418
|
),
|
|
369
419
|
);
|
|
370
|
-
request = { ...request, name,
|
|
420
|
+
request = { ...request, name, agents, skills };
|
|
421
|
+
}
|
|
422
|
+
// Local MCP registration + instruction writing are independent of skill
|
|
423
|
+
// pins — only offered for agents with a verified project-scoped CLI
|
|
424
|
+
// command (currently just claude-code; see MCP_PROJECT_REGISTRABLE_AGENTS).
|
|
425
|
+
const registrableAgents = request.agents.filter((agent) =>
|
|
426
|
+
MCP_PROJECT_REGISTRABLE_AGENTS.includes(agent as AgentId),
|
|
427
|
+
) as AgentId[];
|
|
428
|
+
if (guided && registrableAgents.length > 0) {
|
|
429
|
+
request = {
|
|
430
|
+
...request,
|
|
431
|
+
registerMcp: await confirmAction(
|
|
432
|
+
`Also register skillmux as a project-scoped MCP server for ${registrableAgents.join(", ")}? ` +
|
|
433
|
+
`This writes ${request.path}/.mcp.json, shared via git.`,
|
|
434
|
+
),
|
|
435
|
+
};
|
|
371
436
|
}
|
|
372
|
-
const
|
|
373
|
-
const targets = [...new Set([...request.targets, ...
|
|
437
|
+
const agentTargets = configuredTargetsForAgents(manifest, request.agents);
|
|
438
|
+
const targets = [...new Set([...request.targets, ...agentTargets])];
|
|
374
439
|
const updated = upsertProject(manifest, {
|
|
375
440
|
name: request.name,
|
|
376
441
|
paths: [request.path],
|
|
@@ -378,15 +443,34 @@ export async function runProject(
|
|
|
378
443
|
targets,
|
|
379
444
|
});
|
|
380
445
|
const { notes } = validateManifest(updated, vaultPath, localVaultPaths);
|
|
446
|
+
|
|
447
|
+
// The project-local instruction block only teaches an agent to call
|
|
448
|
+
// resolve_skill/fetch_skill (MCP tools) — write it only for agents that
|
|
449
|
+
// are actually getting a project-scoped MCP registration this run.
|
|
450
|
+
const mcpInstructionAgents = request.registerMcp ? registrableAgents : [];
|
|
451
|
+
const instructionPlan = planProjectInstructionSetup(
|
|
452
|
+
mcpInstructionAgents,
|
|
453
|
+
request.path,
|
|
454
|
+
);
|
|
455
|
+
const hasInstructionWrites = instructionPlan.changes.some(
|
|
456
|
+
(change) => change.status !== "unchanged",
|
|
457
|
+
);
|
|
458
|
+
|
|
381
459
|
const plan = {
|
|
382
460
|
mode: "project",
|
|
383
461
|
project: request.name,
|
|
384
462
|
path: request.path,
|
|
385
463
|
skills: request.skills,
|
|
386
|
-
|
|
464
|
+
agents: request.agents,
|
|
387
465
|
targets,
|
|
388
466
|
sync: request.sync,
|
|
389
467
|
notes,
|
|
468
|
+
instructions: instructionPlan.changes.map(({ path, agents, status }) => ({
|
|
469
|
+
path,
|
|
470
|
+
agents,
|
|
471
|
+
status,
|
|
472
|
+
})),
|
|
473
|
+
register_mcp_for: mcpInstructionAgents,
|
|
390
474
|
};
|
|
391
475
|
|
|
392
476
|
if (options.dryRun) {
|
|
@@ -401,8 +485,14 @@ export async function runProject(
|
|
|
401
485
|
console.log("\nReview");
|
|
402
486
|
console.log(` project: ${request.name}`);
|
|
403
487
|
console.log(` path: ${request.path}`);
|
|
404
|
-
console.log(`
|
|
488
|
+
console.log(` agents: ${request.agents.join(", ") || "(none)"}`);
|
|
405
489
|
console.log(` skills: ${request.skills.join(", ") || "(none)"}`);
|
|
490
|
+
console.log(
|
|
491
|
+
` instructions: ${instructionPlan.changes.filter((change) => change.status !== "unchanged").length} file(s)`,
|
|
492
|
+
);
|
|
493
|
+
console.log(
|
|
494
|
+
` MCP registration: ${mcpInstructionAgents.join(", ") || "(none)"}`,
|
|
495
|
+
);
|
|
406
496
|
console.log(` sync: ${request.sync ? "yes" : "no"}`);
|
|
407
497
|
}
|
|
408
498
|
if (
|
|
@@ -421,6 +511,17 @@ export async function runProject(
|
|
|
421
511
|
}
|
|
422
512
|
|
|
423
513
|
writeManifestAtomic(manifestPath, updated);
|
|
514
|
+
if (hasInstructionWrites) {
|
|
515
|
+
try {
|
|
516
|
+
applyInstructionPlan(instructionPlan);
|
|
517
|
+
} catch (error) {
|
|
518
|
+
throw new Error(
|
|
519
|
+
`project configuration was saved, but writing instruction files failed: ${
|
|
520
|
+
error instanceof Error ? error.message : String(error)
|
|
521
|
+
}`,
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
424
525
|
if (request.sync) {
|
|
425
526
|
try {
|
|
426
527
|
// Reaching here already required approval above (request.yes, or an
|
|
@@ -437,7 +538,39 @@ export async function runProject(
|
|
|
437
538
|
);
|
|
438
539
|
}
|
|
439
540
|
}
|
|
440
|
-
|
|
441
|
-
|
|
541
|
+
|
|
542
|
+
// Best-effort and outside the checks above: this mutates another tool's
|
|
543
|
+
// own config, not skillmux's, so a registration failure is reported, never
|
|
544
|
+
// rolled back — the successful project setup above still stands either way.
|
|
545
|
+
const mcpRegistrations: McpRegistrationResult[] = [];
|
|
546
|
+
if (request.registerMcp) {
|
|
547
|
+
for (const agent of registrableAgents) {
|
|
548
|
+
mcpRegistrations.push(
|
|
549
|
+
await registerMcpServer(agent, { scope: "project", cwd: request.path }),
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
emitSuccess(
|
|
555
|
+
{ isJson: options.isJson },
|
|
556
|
+
{
|
|
557
|
+
result: {
|
|
558
|
+
...plan,
|
|
559
|
+
instructions_changed: instructionPlan.changes
|
|
560
|
+
.filter((change) => change.status !== "unchanged")
|
|
561
|
+
.map((change) => change.path),
|
|
562
|
+
mcp_registrations: mcpRegistrations,
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
() => {
|
|
566
|
+
console.log(`project "${request.name}" ready at ${request.path}`);
|
|
567
|
+
for (const registration of mcpRegistrations) {
|
|
568
|
+
console.log(
|
|
569
|
+
registration.ok
|
|
570
|
+
? `MCP registered: ${registration.agent} (project scope)`
|
|
571
|
+
: `MCP registration failed for ${registration.agent}: ${registration.error}`,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
},
|
|
442
575
|
);
|
|
443
576
|
}
|
package/src/commands/report.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ContextAdapter } from "../adapters";
|
|
3
3
|
import type { ResolvedContext } from "../context";
|
|
4
4
|
import { emitSuccess } from "../output";
|
|
5
5
|
import { getStats, renderStatsText } from "../stats";
|
|
@@ -36,14 +36,14 @@ function parseReportArgs(args: string[]): {
|
|
|
36
36
|
|
|
37
37
|
export async function runReport(
|
|
38
38
|
args: string[],
|
|
39
|
-
options: { isJson: boolean;
|
|
39
|
+
options: { isJson: boolean; context: ResolvedContext; allowInsecure: boolean; adapter: ContextAdapter },
|
|
40
40
|
): Promise<void> {
|
|
41
41
|
const { db: dbPath, since } = parseReportArgs(args);
|
|
42
42
|
if (!since)
|
|
43
43
|
throw new Error(
|
|
44
44
|
"usage: skillmux report [--context <name> | --server <url> | --db <path>] --since <window> [--json]",
|
|
45
45
|
);
|
|
46
|
-
if (dbPath && options.
|
|
46
|
+
if (dbPath && options.context.type === "remote")
|
|
47
47
|
throw new Error("--db and --context/--server are mutually exclusive");
|
|
48
48
|
|
|
49
49
|
if (dbPath) {
|
package/src/commands/scan.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { expandHome, loadConfig } from "../config";
|
|
2
|
-
import { emitSuccess } from "../output";
|
|
2
|
+
import { emitSuccess, warn } from "../output";
|
|
3
3
|
import {
|
|
4
|
+
parseFailOn,
|
|
4
5
|
renderScanJson,
|
|
5
6
|
renderScanText,
|
|
6
7
|
scanExitCode,
|
|
@@ -12,10 +13,12 @@ import { isGlobalFlag } from "../global-flags";
|
|
|
12
13
|
function parseScanArgs(args: string[]): {
|
|
13
14
|
path?: string;
|
|
14
15
|
format: "text" | "json";
|
|
16
|
+
formatExplicit: boolean;
|
|
15
17
|
failOn?: ScanSeverity;
|
|
16
18
|
} {
|
|
17
19
|
let path: string | undefined;
|
|
18
20
|
let format: "text" | "json" = "text";
|
|
21
|
+
let formatExplicit = false;
|
|
19
22
|
let failOn: ScanSeverity | undefined;
|
|
20
23
|
for (let i = 0; i < args.length; i++) {
|
|
21
24
|
const option = args[i];
|
|
@@ -24,12 +27,13 @@ function parseScanArgs(args: string[]): {
|
|
|
24
27
|
if (value !== "text" && value !== "json")
|
|
25
28
|
throw new Error("--format must be text or json");
|
|
26
29
|
format = value;
|
|
30
|
+
formatExplicit = true;
|
|
27
31
|
} else if (option === "--fail-on") {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
failOn =
|
|
32
|
+
// scan accepts "none" for symmetry with install/update, where it is the
|
|
33
|
+
// opt-out. Here it is already the default: scan reports, and the caller
|
|
34
|
+
// opts into a non-zero exit code.
|
|
35
|
+
const parsed = parseFailOn(args[++i]);
|
|
36
|
+
failOn = parsed === "none" ? undefined : parsed;
|
|
33
37
|
} else if (isGlobalFlag(option, "--json")) {
|
|
34
38
|
// handled globally by main()'s isJson flag; recognized here so it isn't rejected
|
|
35
39
|
} else if (option?.startsWith("--")) {
|
|
@@ -40,14 +44,20 @@ function parseScanArgs(args: string[]): {
|
|
|
40
44
|
path = option;
|
|
41
45
|
}
|
|
42
46
|
}
|
|
43
|
-
return { path, format, failOn };
|
|
47
|
+
return { path, format, formatExplicit, failOn };
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
export async function runScan(
|
|
47
51
|
args: string[],
|
|
48
52
|
options: { isJson: boolean },
|
|
49
53
|
): Promise<void> {
|
|
50
|
-
const { path, format, failOn } = parseScanArgs(args);
|
|
54
|
+
const { path, format, formatExplicit, failOn } = parseScanArgs(args);
|
|
55
|
+
if (formatExplicit) {
|
|
56
|
+
// --format predates the shared --json envelope and is the last command
|
|
57
|
+
// flag that emits JSON outside it. Kept working for existing callers;
|
|
58
|
+
// the warning goes to stderr so stdout stays machine-parseable.
|
|
59
|
+
warn("--format is deprecated and will be removed in a future 1.x release; use --json instead");
|
|
60
|
+
}
|
|
51
61
|
const rootPath = path
|
|
52
62
|
? expandHome(path)
|
|
53
63
|
: expandHome((await loadConfig()).vault_path);
|
package/src/commands/shared.ts
CHANGED
|
@@ -1,21 +1,14 @@
|
|
|
1
|
-
import { createInterface } from "node:readline/promises";
|
|
2
1
|
import { expandHome, loadConfig } from "../config";
|
|
3
2
|
import { parseManifest, resolveManifestPath } from "../manifest";
|
|
4
3
|
import { isInteractive } from "../output";
|
|
4
|
+
import { askQuestion, type PromptIO } from "../prompts";
|
|
5
5
|
|
|
6
|
-
export async function confirmAction(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
const answer = (await readline.question(`${prompt} [y/N] `))
|
|
13
|
-
.trim()
|
|
14
|
-
.toLowerCase();
|
|
15
|
-
return answer === "y" || answer === "yes";
|
|
16
|
-
} finally {
|
|
17
|
-
readline.close();
|
|
18
|
-
}
|
|
6
|
+
export async function confirmAction(
|
|
7
|
+
prompt: string,
|
|
8
|
+
io: PromptIO = {},
|
|
9
|
+
): Promise<boolean> {
|
|
10
|
+
const answer = (await askQuestion(`${prompt} [y/N] `, io)).trim().toLowerCase();
|
|
11
|
+
return answer === "y" || answer === "yes";
|
|
19
12
|
}
|
|
20
13
|
|
|
21
14
|
export async function loadManifestContext() {
|
package/src/commands/skill.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { expandHome, loadConfig } from "../config";
|
|
4
|
+
import { unknownSubcommandError } from "../output";
|
|
4
5
|
import { vaultResolutionOrder } from "../vault";
|
|
5
6
|
|
|
6
7
|
export async function runSkill(subCommand: string, args: string[]): Promise<void> {
|
|
7
|
-
if (subCommand !== "which") throw
|
|
8
|
+
if (subCommand !== "which") throw unknownSubcommandError("skill", subCommand, ["which"]);
|
|
8
9
|
await runWhich(args);
|
|
9
10
|
}
|
|
10
11
|
|
package/src/commands/target.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { expandHome } from "../config";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BUILT_IN_TARGET_NAMES,
|
|
4
|
+
planAgentSurfaces,
|
|
5
|
+
resolveBuiltInTarget,
|
|
6
|
+
SUPPORTED_AGENT_IDS,
|
|
7
|
+
} from "../init-agents";
|
|
3
8
|
import { planInitManifest, applyInit } from "../init";
|
|
4
9
|
import { writeManifestAtomic } from "../manifest";
|
|
5
|
-
import { emitSuccess } from "../output";
|
|
10
|
+
import { emitSuccess, unknownSubcommandError } from "../output";
|
|
6
11
|
import { confirmIfNeeded, loadManifestContext } from "./shared";
|
|
12
|
+
|
|
7
13
|
export async function runTarget(
|
|
8
14
|
subCommand: string,
|
|
9
15
|
args: string[],
|
|
@@ -19,11 +25,11 @@ export async function runTarget(
|
|
|
19
25
|
}
|
|
20
26
|
const targets = names.map((name) => {
|
|
21
27
|
const target = manifest.targets[name]!;
|
|
22
|
-
const
|
|
23
|
-
const surface =
|
|
28
|
+
const agents = SUPPORTED_AGENT_IDS.filter((agent) => {
|
|
29
|
+
const surface = planAgentSurfaces([agent]).surfaces[0];
|
|
24
30
|
return surface !== undefined && surface.path === expandHome(target.dir);
|
|
25
31
|
});
|
|
26
|
-
return { name, ...target,
|
|
32
|
+
return { name, ...target, agents };
|
|
27
33
|
});
|
|
28
34
|
emitSuccess({ isJson: options.isJson }, { targets }, () => {
|
|
29
35
|
if (targets.length === 0) {
|
|
@@ -33,7 +39,7 @@ export async function runTarget(
|
|
|
33
39
|
console.log(`${target.name}:`);
|
|
34
40
|
console.log(` dir: ${target.dir}`);
|
|
35
41
|
console.log(` host: ${target.host ?? "(global)"}`);
|
|
36
|
-
console.log(`
|
|
42
|
+
console.log(` agents: ${target.agents.join(", ") || "(custom)"}`);
|
|
37
43
|
console.log(
|
|
38
44
|
` projects: ${target.project_groups.join(", ") || "(none)"}`,
|
|
39
45
|
);
|
|
@@ -47,9 +53,21 @@ export async function runTarget(
|
|
|
47
53
|
const name = args[0];
|
|
48
54
|
const dirIndex = args.indexOf("--dir");
|
|
49
55
|
const rawPath = dirIndex === -1 ? undefined : args[dirIndex + 1];
|
|
50
|
-
if (!name
|
|
56
|
+
if (!name)
|
|
51
57
|
throw new Error("usage: skillmux target add <name> --dir <dir> --yes");
|
|
52
|
-
|
|
58
|
+
|
|
59
|
+
let path: string;
|
|
60
|
+
if (rawPath) {
|
|
61
|
+
path = expandHome(rawPath);
|
|
62
|
+
} else if (BUILT_IN_TARGET_NAMES.has(name)) {
|
|
63
|
+
path = resolveBuiltInTarget(name, {
|
|
64
|
+
codexHome: process.env.CODEX_HOME ? expandHome(process.env.CODEX_HOME) : undefined,
|
|
65
|
+
}).path;
|
|
66
|
+
} else {
|
|
67
|
+
throw new Error(
|
|
68
|
+
"usage: skillmux target add <name> --dir <dir> --yes (--dir may be omitted for built-in target names: agent-skills, claude-code, codex)",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
53
71
|
if (options.dryRun) {
|
|
54
72
|
const planned = planInitManifest(vaultPath, [{ name, dir: path }], []);
|
|
55
73
|
emitSuccess(
|
|
@@ -118,5 +136,5 @@ export async function runTarget(
|
|
|
118
136
|
return;
|
|
119
137
|
}
|
|
120
138
|
|
|
121
|
-
throw
|
|
139
|
+
throw unknownSubcommandError("target", subCommand, ["list", "show", "add", "remove"]);
|
|
122
140
|
}
|
package/src/commands/update.ts
CHANGED
|
@@ -14,7 +14,15 @@ import {
|
|
|
14
14
|
import { emitSuccess } from "../output";
|
|
15
15
|
import { hashSkillContent, readSkillOrigin, writeSkillOrigin } from "../provenance";
|
|
16
16
|
import type { SkillOrigin } from "../provenance";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
FAIL_ON_USAGE,
|
|
19
|
+
parseFailOn,
|
|
20
|
+
resolveMutatingFailOn,
|
|
21
|
+
scanExitCode,
|
|
22
|
+
type FailOnOption,
|
|
23
|
+
type ScanFinding,
|
|
24
|
+
type ScanSeverity,
|
|
25
|
+
} from "../scan";
|
|
18
26
|
import { SKILL_ID_PATTERN } from "../vault";
|
|
19
27
|
import { confirmIfNeeded } from "./shared";
|
|
20
28
|
import { checkOutdated } from "./outdated";
|
|
@@ -175,14 +183,14 @@ function parseUpdateArgs(args: string[]): {
|
|
|
175
183
|
yes: boolean;
|
|
176
184
|
dryRun: boolean;
|
|
177
185
|
force: boolean;
|
|
178
|
-
failOn?:
|
|
186
|
+
failOn?: FailOnOption;
|
|
179
187
|
allowLocalSource: boolean;
|
|
180
188
|
} {
|
|
181
189
|
let skillId: string | undefined;
|
|
182
190
|
let yes = false;
|
|
183
191
|
let dryRun = false;
|
|
184
192
|
let force = false;
|
|
185
|
-
let failOn:
|
|
193
|
+
let failOn: FailOnOption | undefined;
|
|
186
194
|
let allowLocalSource = false;
|
|
187
195
|
for (let i = 0; i < args.length; i++) {
|
|
188
196
|
const arg = args[i];
|
|
@@ -191,11 +199,7 @@ function parseUpdateArgs(args: string[]): {
|
|
|
191
199
|
else if (arg === "--force") force = true;
|
|
192
200
|
else if (arg === "--allow-local-source") allowLocalSource = true;
|
|
193
201
|
else if (arg === "--fail-on") {
|
|
194
|
-
|
|
195
|
-
if (value !== "low" && value !== "medium" && value !== "high") {
|
|
196
|
-
throw new Error("--fail-on must be low, medium, or high");
|
|
197
|
-
}
|
|
198
|
-
failOn = value;
|
|
202
|
+
failOn = parseFailOn(args[++i]);
|
|
199
203
|
} else if (isGlobalFlag(arg, "--json")) {
|
|
200
204
|
// handled globally
|
|
201
205
|
} else if (arg?.startsWith("--")) {
|
|
@@ -215,7 +219,10 @@ export async function runUpdate(args: string[], options: { isJson: boolean }): P
|
|
|
215
219
|
const vaultPath = expandHome(config.vault_path);
|
|
216
220
|
|
|
217
221
|
const candidates = await resolveCandidateOrigins(vaultPath, skillId, allowLocalSource, config.egress?.allowed_hosts);
|
|
218
|
-
|
|
222
|
+
// Same default as install: block on a high-severity finding unless the
|
|
223
|
+
// caller explicitly opts out with --fail-on none.
|
|
224
|
+
const effectiveFailOn = resolveMutatingFailOn(failOn);
|
|
225
|
+
const plan = await buildPlan(vaultPath, candidates, effectiveFailOn, force, config.egress?.allowed_hosts);
|
|
219
226
|
try {
|
|
220
227
|
const toWrite = plan.filter((item) => item.kind === "update");
|
|
221
228
|
|