@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.
- package/CHANGELOG.md +15 -0
- package/README.md +20 -10
- package/bin/skillmux.js +89 -0
- package/docs/README.md +4 -4
- package/docs/cli.md +15 -5
- package/docs/concepts.md +4 -3
- package/docs/configuration.md +22 -5
- package/docs/deployment.md +3 -3
- package/docs/getting-started.md +32 -7
- package/docs/releasing.md +17 -2
- package/docs/sdd/cli-remote-target-parity/schema-bucket-b.yaml +297 -0
- package/docs/sdd/cli-remote-target-parity/spec-bucket-b.md +95 -0
- package/docs/sdd/cli-remote-target-parity/spec.md +111 -0
- package/docs/sdd/cli-remote-target-parity/think.md +183 -0
- package/docs/sdd/cli-surface-consistency/think.md +149 -0
- package/docs/sdd/config-authority-rerank-resilience/plan.md +338 -0
- package/docs/sdd/config-authority-rerank-resilience/review.md +124 -0
- package/docs/sdd/config-authority-rerank-resilience/spec.md +78 -0
- package/docs/sdd/feat-packaging-deployment/review.md +95 -0
- package/docs/sdd/fetch-outcome-flywheel/review.md +103 -0
- package/docs/sdd/fetch-outcome-flywheel/schema.json +347 -0
- package/docs/sdd/fetch-outcome-flywheel/spec.md +185 -0
- package/docs/sdd/npm-native-binaries/review.md +103 -0
- package/docs/sdd/npm-native-binaries/spec.md +148 -0
- package/docs/sdd/ranked-shortlist-2/pr3-calibration-removal.md +56 -0
- package/docs/sdd/refactor-builtin-target-paths/spec.md +41 -0
- package/docs/sdd/runtime-resource-hardening/review.md +86 -0
- package/docs/sdd/runtime-resource-hardening/spec.md +43 -0
- package/docs/sdd/security-hardening-phase/think.md +129 -0
- package/docs/sdd/security-observability/review.md +96 -0
- package/docs/sdd/security-observability/spec.md +55 -0
- package/docs/sdd/skill-provenance-update/review.md +152 -0
- package/docs/sdd/skill-provenance-update/schema.json +193 -0
- package/docs/sdd/skill-provenance-update/spec.md +134 -0
- package/docs/sdd/supply-chain-hardening/review.md +50 -0
- package/docs/sdd/supply-chain-hardening/spec.md +42 -0
- package/docs/sdd/target-marker-rehome/spec.md +33 -0
- package/docs/skill-management.md +1 -1
- package/docs/troubleshooting.md +18 -2
- package/package.json +15 -8
- package/src/adapters.ts +0 -438
- package/src/audit.ts +0 -21
- package/src/cli.ts +0 -928
- package/src/clients.ts +0 -368
- package/src/commands/audit.ts +0 -85
- package/src/commands/config.ts +0 -224
- package/src/commands/context.ts +0 -104
- package/src/commands/core.ts +0 -56
- package/src/commands/doctor.ts +0 -97
- package/src/commands/eval.ts +0 -88
- package/src/commands/init.ts +0 -672
- package/src/commands/install.ts +0 -155
- package/src/commands/local-vault.ts +0 -60
- package/src/commands/models.ts +0 -10
- package/src/commands/outdated.ts +0 -115
- package/src/commands/project.ts +0 -580
- package/src/commands/report.ts +0 -66
- package/src/commands/scan.ts +0 -71
- package/src/commands/shared.ts +0 -38
- package/src/commands/skill.ts +0 -33
- package/src/commands/sync.ts +0 -233
- package/src/commands/target.ts +0 -225
- package/src/commands/update.ts +0 -277
- package/src/completions.ts +0 -196
- package/src/concurrency-limiter.ts +0 -61
- package/src/config-service.ts +0 -398
- package/src/config-watcher.ts +0 -253
- package/src/config.ts +0 -583
- package/src/context.ts +0 -188
- package/src/db-audit.ts +0 -286
- package/src/db-index.ts +0 -238
- package/src/db.ts +0 -3
- package/src/deployment.ts +0 -39
- package/src/doctor.ts +0 -205
- package/src/eval.ts +0 -302
- package/src/global-flags.ts +0 -46
- package/src/init-agents.ts +0 -329
- package/src/init-instructions.ts +0 -192
- package/src/init.ts +0 -389
- package/src/install.ts +0 -232
- package/src/lifecycle.ts +0 -51
- package/src/logger.ts +0 -26
- package/src/manifest.ts +0 -352
- package/src/mcp-registration.ts +0 -89
- package/src/metrics.ts +0 -121
- package/src/models.ts +0 -20
- package/src/output.ts +0 -214
- package/src/project-setup.ts +0 -36
- package/src/prompts.ts +0 -124
- package/src/provenance.ts +0 -99
- package/src/rate-limiter.ts +0 -137
- package/src/readiness.ts +0 -30
- package/src/redact.ts +0 -52
- package/src/router-core.ts +0 -666
- package/src/rrf.ts +0 -31
- package/src/scan.ts +0 -300
- package/src/server.ts +0 -930
- package/src/setup.ts +0 -145
- package/src/snapshot.ts +0 -135
- package/src/stats.ts +0 -340
- package/src/sync.ts +0 -482
- package/src/toml-writer.ts +0 -51
- package/src/types.ts +0 -212
- package/src/vault.ts +0 -207
package/src/commands/project.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/report.ts
DELETED
|
@@ -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
|
-
}
|