@klhapp/skillmux 0.5.0 → 1.0.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 +67 -0
- package/README.md +44 -8
- package/docs/configuration.md +13 -10
- package/package.json +1 -1
- package/src/adapters.ts +11 -5
- package/src/cli.ts +1286 -328
- package/src/completions.ts +95 -36
- package/src/config-watcher.ts +63 -2
- package/src/doctor.ts +19 -1
- package/src/init-clients.ts +31 -0
- package/src/manifest.ts +101 -2
- package/src/output.ts +26 -29
- package/src/project-setup.ts +36 -0
- package/src/prompts.ts +69 -0
- package/src/server.ts +228 -63
package/src/cli.ts
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Database } from "bun:sqlite";
|
|
3
|
-
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, rmSync } from "node:fs";
|
|
4
4
|
import { hostname } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
5
|
+
import { basename, join } from "node:path";
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { generateDataset } from "./dataset-generator";
|
|
8
8
|
|
|
9
9
|
import { createClients } from "./clients";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
expandHome,
|
|
12
|
+
loadConfig,
|
|
13
|
+
migrateLegacyPaths,
|
|
14
|
+
resolveConfigPath,
|
|
15
|
+
} from "./config";
|
|
11
16
|
import { openIndex } from "./db";
|
|
12
17
|
import { diagnose } from "./doctor";
|
|
13
18
|
import { evalVault } from "./eval";
|
|
14
19
|
import {
|
|
15
20
|
assessClientReadiness,
|
|
21
|
+
detectInstalledClients,
|
|
16
22
|
planClientSurfaces,
|
|
17
23
|
resolveBuiltInTarget,
|
|
24
|
+
SUPPORTED_CLIENT_IDS,
|
|
18
25
|
type ClientId,
|
|
19
26
|
type ReadinessAxis,
|
|
20
27
|
} from "./init-clients";
|
|
@@ -47,11 +54,28 @@ import {
|
|
|
47
54
|
serializeManifest,
|
|
48
55
|
unpinCore,
|
|
49
56
|
unpinProject,
|
|
57
|
+
upsertProject,
|
|
58
|
+
updateProjectPaths,
|
|
59
|
+
updateProjectTargets,
|
|
50
60
|
validateManifest,
|
|
61
|
+
writeManifestAtomic,
|
|
51
62
|
} from "./manifest";
|
|
52
63
|
import { downloadLocalModels } from "./models";
|
|
64
|
+
import { resolveProjectDirectory, suggestProjectName } from "./project-setup";
|
|
65
|
+
import {
|
|
66
|
+
parseCommaList,
|
|
67
|
+
promptMultiSelect,
|
|
68
|
+
promptText,
|
|
69
|
+
shouldUseWizard,
|
|
70
|
+
} from "./prompts";
|
|
53
71
|
import { backfillEmbeddings, configure, rebuildIndex } from "./router-core";
|
|
54
|
-
import {
|
|
72
|
+
import {
|
|
73
|
+
renderScanJson,
|
|
74
|
+
renderScanText,
|
|
75
|
+
scanExitCode,
|
|
76
|
+
scanPath,
|
|
77
|
+
type ScanSeverity,
|
|
78
|
+
} from "./scan";
|
|
55
79
|
import {
|
|
56
80
|
applyConfigInit,
|
|
57
81
|
inspectVault,
|
|
@@ -79,7 +103,15 @@ import {
|
|
|
79
103
|
type ResolvedTarget,
|
|
80
104
|
} from "./context";
|
|
81
105
|
import { createTargetAdapter, type TargetAdapter } from "./adapters";
|
|
82
|
-
import {
|
|
106
|
+
import {
|
|
107
|
+
emitSuccess,
|
|
108
|
+
formatJsonEnvelope,
|
|
109
|
+
isInteractive,
|
|
110
|
+
mapExitCode,
|
|
111
|
+
renderTable,
|
|
112
|
+
renderTargetBanner,
|
|
113
|
+
suggestCorrection,
|
|
114
|
+
} from "./output";
|
|
83
115
|
import { generateCompletions, type ShellType } from "./completions";
|
|
84
116
|
|
|
85
117
|
const KNOWN_COMMANDS = [
|
|
@@ -91,14 +123,16 @@ const KNOWN_COMMANDS = [
|
|
|
91
123
|
"index",
|
|
92
124
|
"sync",
|
|
93
125
|
"init",
|
|
126
|
+
"project",
|
|
127
|
+
"target",
|
|
128
|
+
"core",
|
|
94
129
|
"report",
|
|
95
130
|
"scan",
|
|
96
131
|
"install",
|
|
97
132
|
"eval",
|
|
98
133
|
"doctor",
|
|
99
134
|
"models",
|
|
100
|
-
"
|
|
101
|
-
"manifest",
|
|
135
|
+
"skill",
|
|
102
136
|
"local-vault",
|
|
103
137
|
];
|
|
104
138
|
|
|
@@ -133,9 +167,17 @@ async function main() {
|
|
|
133
167
|
|
|
134
168
|
// Only resolve target if command is target-aware or context/config/calibrate
|
|
135
169
|
const isLocalConfigInit = command === "config" && rawArgv[1] === "init";
|
|
136
|
-
if (
|
|
170
|
+
if (
|
|
171
|
+
(["context", "config", "calibrate"].includes(command) &&
|
|
172
|
+
!isLocalConfigInit) ||
|
|
173
|
+
flagContext ||
|
|
174
|
+
flagServer
|
|
175
|
+
) {
|
|
137
176
|
try {
|
|
138
|
-
resolvedTarget = await resolveTarget({
|
|
177
|
+
resolvedTarget = await resolveTarget({
|
|
178
|
+
context: flagContext,
|
|
179
|
+
server: flagServer,
|
|
180
|
+
});
|
|
139
181
|
} catch (err: any) {
|
|
140
182
|
handleError(err, { target: resolvedTarget, isJson, isVerbose });
|
|
141
183
|
return;
|
|
@@ -149,13 +191,23 @@ async function main() {
|
|
|
149
191
|
try {
|
|
150
192
|
switch (command) {
|
|
151
193
|
case "context":
|
|
152
|
-
await handleContextCommand(subCommand, commandArgs, {
|
|
194
|
+
await handleContextCommand(subCommand, commandArgs, {
|
|
195
|
+
target: resolvedTarget,
|
|
196
|
+
isJson,
|
|
197
|
+
});
|
|
153
198
|
break;
|
|
154
199
|
case "config":
|
|
155
|
-
await handleConfigCommand(adapter, subCommand, commandArgs, {
|
|
200
|
+
await handleConfigCommand(adapter, subCommand, commandArgs, {
|
|
201
|
+
target: resolvedTarget,
|
|
202
|
+
isJson,
|
|
203
|
+
dryRun: isDryRun,
|
|
204
|
+
});
|
|
156
205
|
break;
|
|
157
206
|
case "calibrate":
|
|
158
|
-
await handleCalibrateCommand(adapter, subCommand, rawArgv.slice(1), {
|
|
207
|
+
await handleCalibrateCommand(adapter, subCommand, rawArgv.slice(1), {
|
|
208
|
+
target: resolvedTarget,
|
|
209
|
+
isJson,
|
|
210
|
+
});
|
|
159
211
|
break;
|
|
160
212
|
case "completions":
|
|
161
213
|
await handleCompletionsCommand(subCommand);
|
|
@@ -191,40 +243,56 @@ async function main() {
|
|
|
191
243
|
case "init":
|
|
192
244
|
await runInit(rawArgv.slice(1), { isJson, dryRun: isDryRun });
|
|
193
245
|
break;
|
|
246
|
+
case "project":
|
|
247
|
+
await runProject(subCommand, commandArgs, { isJson, dryRun: isDryRun });
|
|
248
|
+
break;
|
|
249
|
+
case "target":
|
|
250
|
+
await runTarget(subCommand, commandArgs, { isJson, dryRun: isDryRun });
|
|
251
|
+
break;
|
|
252
|
+
case "core":
|
|
253
|
+
await runCore(subCommand, commandArgs, { isJson, dryRun: isDryRun });
|
|
254
|
+
break;
|
|
194
255
|
case "report":
|
|
195
|
-
await runReport(rawArgv.slice(1));
|
|
256
|
+
await runReport(rawArgv.slice(1), { isJson });
|
|
196
257
|
break;
|
|
197
258
|
case "scan":
|
|
198
|
-
await runScan(rawArgv.slice(1));
|
|
259
|
+
await runScan(rawArgv.slice(1), { isJson });
|
|
199
260
|
break;
|
|
200
261
|
case "install":
|
|
201
|
-
await runInstall(rawArgv.slice(1));
|
|
262
|
+
await runInstall(rawArgv.slice(1), { isJson });
|
|
202
263
|
break;
|
|
203
264
|
case "eval":
|
|
204
|
-
await runEval();
|
|
265
|
+
await runEval({ isJson });
|
|
205
266
|
break;
|
|
206
267
|
case "doctor":
|
|
207
|
-
await runDoctor();
|
|
268
|
+
await runDoctor({ isJson });
|
|
208
269
|
break;
|
|
209
270
|
case "which":
|
|
210
|
-
|
|
271
|
+
throw new Error(
|
|
272
|
+
`skillmux which is removed - use "skillmux skill which ${subCommand || "<skill_id>"}" instead`,
|
|
273
|
+
);
|
|
274
|
+
case "skill":
|
|
275
|
+
await runSkill(subCommand, commandArgs);
|
|
211
276
|
break;
|
|
212
277
|
case "manifest":
|
|
213
|
-
|
|
214
|
-
|
|
278
|
+
throw new Error(
|
|
279
|
+
`skillmux manifest is removed - use "skillmux core ${subCommand || "pin|unpin"}" for [core] skills, or "skillmux project ${subCommand || "pin|unpin"} <group>" for [project.*] skills`,
|
|
280
|
+
);
|
|
215
281
|
case "local-vault":
|
|
216
|
-
if (subCommand !== "init")
|
|
217
|
-
|
|
282
|
+
if (subCommand !== "init")
|
|
283
|
+
throw new Error("usage: skillmux local-vault init <path>");
|
|
284
|
+
await runLocalVaultInit(commandArgs, { isJson, dryRun: isDryRun });
|
|
218
285
|
break;
|
|
219
286
|
case "models":
|
|
220
|
-
if (subCommand !== "download")
|
|
221
|
-
|
|
287
|
+
if (subCommand !== "download")
|
|
288
|
+
throw new Error("usage: skillmux models download");
|
|
289
|
+
await runModelDownload({ isJson });
|
|
222
290
|
break;
|
|
223
291
|
default: {
|
|
224
292
|
const suggestion = suggestCorrection(command, KNOWN_COMMANDS);
|
|
225
293
|
const msg = suggestion
|
|
226
294
|
? `Unknown command "${command}". Did you mean "${suggestion}"?`
|
|
227
|
-
: `usage: skillmux <serve|index|sync|init|report|scan|install|eval|doctor|which|
|
|
295
|
+
: `usage: skillmux <serve|index|sync|init|project|target|core pin/unpin|report|scan|install|eval|doctor|skill which|local-vault init|config show|models download|calibrate generate-dataset>`;
|
|
228
296
|
throw new Error(msg);
|
|
229
297
|
}
|
|
230
298
|
}
|
|
@@ -236,13 +304,11 @@ async function main() {
|
|
|
236
304
|
async function handleContextCommand(
|
|
237
305
|
sub: string,
|
|
238
306
|
args: string[],
|
|
239
|
-
ctx: { target: ResolvedTarget; isJson: boolean }
|
|
307
|
+
ctx: { target: ResolvedTarget; isJson: boolean },
|
|
240
308
|
) {
|
|
241
309
|
if (sub === "list") {
|
|
242
310
|
const contexts = await listContexts();
|
|
243
|
-
|
|
244
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: contexts })));
|
|
245
|
-
} else {
|
|
311
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, contexts, () => {
|
|
246
312
|
renderTargetBanner(ctx.target);
|
|
247
313
|
renderTable(
|
|
248
314
|
[
|
|
@@ -251,20 +317,22 @@ async function handleContextCommand(
|
|
|
251
317
|
{ key: "token_env", header: "TOKEN_ENV" },
|
|
252
318
|
{ key: "isDefault", header: "DEFAULT" },
|
|
253
319
|
],
|
|
254
|
-
contexts.map((c) => ({
|
|
320
|
+
contexts.map((c) => ({
|
|
321
|
+
...c,
|
|
322
|
+
token_env: c.token_env ?? "-",
|
|
323
|
+
isDefault: c.isDefault ? "*" : "",
|
|
324
|
+
})),
|
|
255
325
|
);
|
|
256
|
-
}
|
|
326
|
+
});
|
|
257
327
|
return;
|
|
258
328
|
}
|
|
259
329
|
|
|
260
330
|
if (sub === "current") {
|
|
261
331
|
const current = await getCurrentContext();
|
|
262
|
-
|
|
263
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: current })));
|
|
264
|
-
} else {
|
|
332
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, current, () => {
|
|
265
333
|
renderTargetBanner(ctx.target);
|
|
266
334
|
console.log(`Current context: ${current.name} (${current.server})`);
|
|
267
|
-
}
|
|
335
|
+
});
|
|
268
336
|
return;
|
|
269
337
|
}
|
|
270
338
|
|
|
@@ -277,14 +345,18 @@ async function handleContextCommand(
|
|
|
277
345
|
else if (args[i] === "--token-env") tokenEnv = args[++i];
|
|
278
346
|
}
|
|
279
347
|
if (!name || !server) {
|
|
280
|
-
throw new Error(
|
|
348
|
+
throw new Error(
|
|
349
|
+
"usage: skillmux context add <name> --server <url> [--token-env <env_name>]",
|
|
350
|
+
);
|
|
281
351
|
}
|
|
282
352
|
await addContext(name, { server, token_env: tokenEnv });
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
353
|
+
emitSuccess(
|
|
354
|
+
{ isJson: ctx.isJson, target: ctx.target },
|
|
355
|
+
{ name, server, token_env: tokenEnv },
|
|
356
|
+
() => {
|
|
357
|
+
console.log(`Added context "${name}" -> ${server}`);
|
|
358
|
+
},
|
|
359
|
+
);
|
|
288
360
|
return;
|
|
289
361
|
}
|
|
290
362
|
|
|
@@ -292,11 +364,13 @@ async function handleContextCommand(
|
|
|
292
364
|
const name = args[0];
|
|
293
365
|
if (!name) throw new Error("usage: skillmux context use <name>");
|
|
294
366
|
await useContext(name);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
367
|
+
emitSuccess(
|
|
368
|
+
{ isJson: ctx.isJson, target: ctx.target },
|
|
369
|
+
{ default_context: name },
|
|
370
|
+
() => {
|
|
371
|
+
console.log(`Switched default context to "${name}"`);
|
|
372
|
+
},
|
|
373
|
+
);
|
|
300
374
|
return;
|
|
301
375
|
}
|
|
302
376
|
|
|
@@ -304,22 +378,56 @@ async function handleContextCommand(
|
|
|
304
378
|
const name = args[0];
|
|
305
379
|
if (!name) throw new Error("usage: skillmux context remove <name>");
|
|
306
380
|
await removeContext(name);
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
381
|
+
emitSuccess(
|
|
382
|
+
{ isJson: ctx.isJson, target: ctx.target },
|
|
383
|
+
{ removed: name },
|
|
384
|
+
() => {
|
|
385
|
+
console.log(`Removed context "${name}"`);
|
|
386
|
+
},
|
|
387
|
+
);
|
|
312
388
|
return;
|
|
313
389
|
}
|
|
314
390
|
|
|
315
391
|
throw new Error("usage: skillmux context <add|list|current|use|remove>");
|
|
316
392
|
}
|
|
317
393
|
|
|
394
|
+
function emitConfigInitOutcome(
|
|
395
|
+
ctx: { isJson: boolean },
|
|
396
|
+
opts: {
|
|
397
|
+
phase: "plan" | "result";
|
|
398
|
+
dryRun: boolean;
|
|
399
|
+
applied: boolean;
|
|
400
|
+
plan: ConfigInitPlan;
|
|
401
|
+
action: "create" | "preserve";
|
|
402
|
+
text: string;
|
|
403
|
+
},
|
|
404
|
+
): void {
|
|
405
|
+
if (ctx.isJson) {
|
|
406
|
+
console.log(
|
|
407
|
+
JSON.stringify({
|
|
408
|
+
schema_version: 1,
|
|
409
|
+
ok: true,
|
|
410
|
+
command: "config init",
|
|
411
|
+
phase: opts.phase,
|
|
412
|
+
dry_run: opts.dryRun,
|
|
413
|
+
applied: opts.applied,
|
|
414
|
+
plan: {
|
|
415
|
+
config_path: opts.plan.configPath,
|
|
416
|
+
vault_path: opts.plan.vaultPath,
|
|
417
|
+
action: opts.action,
|
|
418
|
+
},
|
|
419
|
+
}),
|
|
420
|
+
);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
console.log(opts.text);
|
|
424
|
+
}
|
|
425
|
+
|
|
318
426
|
async function handleConfigCommand(
|
|
319
427
|
adapter: TargetAdapter,
|
|
320
428
|
sub: string,
|
|
321
429
|
args: string[],
|
|
322
|
-
ctx: { target: ResolvedTarget; isJson: boolean; dryRun: boolean }
|
|
430
|
+
ctx: { target: ResolvedTarget; isJson: boolean; dryRun: boolean },
|
|
323
431
|
) {
|
|
324
432
|
if (sub === "init") {
|
|
325
433
|
let vaultPath: string | undefined;
|
|
@@ -328,7 +436,8 @@ async function handleConfigCommand(
|
|
|
328
436
|
const option = args[i];
|
|
329
437
|
if (option === "--vault") {
|
|
330
438
|
vaultPath = args[++i];
|
|
331
|
-
if (!vaultPath)
|
|
439
|
+
if (!vaultPath)
|
|
440
|
+
throw new Error("usage: skillmux config init --vault <path> --yes");
|
|
332
441
|
} else if (option === "--yes") {
|
|
333
442
|
yes = true;
|
|
334
443
|
} else if (option === "--dry-run" || option === "--json") {
|
|
@@ -348,69 +457,65 @@ async function handleConfigCommand(
|
|
|
348
457
|
migrateLegacyPaths();
|
|
349
458
|
const plan = planConfigInit(resolveConfigPath(), expandHome(vaultPath));
|
|
350
459
|
if (plan.action === "preserve") {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
plan: { config_path: plan.configPath, vault_path: plan.vaultPath, action: "preserve" },
|
|
360
|
-
})
|
|
361
|
-
: `preserved existing config: ${plan.configPath}`);
|
|
460
|
+
emitConfigInitOutcome(ctx, {
|
|
461
|
+
phase: "result",
|
|
462
|
+
dryRun: ctx.dryRun,
|
|
463
|
+
applied: false,
|
|
464
|
+
plan,
|
|
465
|
+
action: "preserve",
|
|
466
|
+
text: `preserved existing config: ${plan.configPath}`,
|
|
467
|
+
});
|
|
362
468
|
return;
|
|
363
469
|
}
|
|
364
470
|
if (ctx.dryRun) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
plan: { config_path: plan.configPath, vault_path: plan.vaultPath, action: "create" },
|
|
374
|
-
})
|
|
375
|
-
: `config create: ${plan.configPath} (dry-run)`);
|
|
471
|
+
emitConfigInitOutcome(ctx, {
|
|
472
|
+
phase: "plan",
|
|
473
|
+
dryRun: true,
|
|
474
|
+
applied: false,
|
|
475
|
+
plan,
|
|
476
|
+
action: "create",
|
|
477
|
+
text: `config create: ${plan.configPath} (dry-run)`,
|
|
478
|
+
});
|
|
376
479
|
return;
|
|
377
480
|
}
|
|
378
481
|
if (!yes) {
|
|
379
482
|
if (!ctx.isJson && isInteractive()) {
|
|
380
|
-
if (
|
|
483
|
+
if (
|
|
484
|
+
!(await confirmAction(
|
|
485
|
+
`Create ${plan.configPath} with vault_path ${plan.vaultPath}?`,
|
|
486
|
+
))
|
|
487
|
+
) {
|
|
381
488
|
console.log("config init cancelled; nothing written");
|
|
382
489
|
return;
|
|
383
490
|
}
|
|
384
491
|
} else {
|
|
385
|
-
throw new Error(
|
|
492
|
+
throw new Error(
|
|
493
|
+
"config initialization requires --yes in noninteractive mode",
|
|
494
|
+
);
|
|
386
495
|
}
|
|
387
496
|
}
|
|
388
497
|
|
|
389
498
|
const result = applyConfigInit(plan);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
? `created ${plan.configPath}`
|
|
402
|
-
: `preserved existing config: ${plan.configPath}`);
|
|
499
|
+
emitConfigInitOutcome(ctx, {
|
|
500
|
+
phase: "result",
|
|
501
|
+
dryRun: false,
|
|
502
|
+
applied: result === "created",
|
|
503
|
+
plan,
|
|
504
|
+
action: plan.action,
|
|
505
|
+
text:
|
|
506
|
+
result === "created"
|
|
507
|
+
? `created ${plan.configPath}`
|
|
508
|
+
: `preserved existing config: ${plan.configPath}`,
|
|
509
|
+
});
|
|
403
510
|
return;
|
|
404
511
|
}
|
|
405
512
|
|
|
406
513
|
if (sub === "show") {
|
|
407
514
|
const data = await adapter.getConfigShow();
|
|
408
|
-
|
|
409
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data })));
|
|
410
|
-
} else {
|
|
515
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, data, () => {
|
|
411
516
|
renderTargetBanner(ctx.target);
|
|
412
517
|
console.log(JSON.stringify(data.effective, null, 2));
|
|
413
|
-
}
|
|
518
|
+
});
|
|
414
519
|
return;
|
|
415
520
|
}
|
|
416
521
|
|
|
@@ -418,32 +523,34 @@ async function handleConfigCommand(
|
|
|
418
523
|
const key = args[0];
|
|
419
524
|
if (!key) throw new Error("usage: skillmux config get <key>");
|
|
420
525
|
const val = await adapter.getConfigGet(key);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
526
|
+
emitSuccess(
|
|
527
|
+
{ isJson: ctx.isJson, target: ctx.target },
|
|
528
|
+
{ key, value: val },
|
|
529
|
+
() => {
|
|
530
|
+
console.log(
|
|
531
|
+
typeof val === "object" ? JSON.stringify(val) : String(val),
|
|
532
|
+
);
|
|
533
|
+
},
|
|
534
|
+
);
|
|
426
535
|
return;
|
|
427
536
|
}
|
|
428
537
|
|
|
429
538
|
if (sub === "validate") {
|
|
430
539
|
const res = await adapter.configValidate();
|
|
431
|
-
|
|
432
|
-
console.log(
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
540
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
541
|
+
console.log(
|
|
542
|
+
res.valid ? "Configuration is valid." : "Configuration is invalid.",
|
|
543
|
+
);
|
|
544
|
+
});
|
|
436
545
|
return;
|
|
437
546
|
}
|
|
438
547
|
|
|
439
548
|
if (sub === "diff") {
|
|
440
549
|
const res = await adapter.configDiff();
|
|
441
|
-
|
|
442
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
443
|
-
} else {
|
|
550
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
444
551
|
renderTargetBanner(ctx.target);
|
|
445
552
|
console.log(JSON.stringify(res.diff, null, 2));
|
|
446
|
-
}
|
|
553
|
+
});
|
|
447
554
|
return;
|
|
448
555
|
}
|
|
449
556
|
|
|
@@ -454,27 +561,27 @@ async function handleConfigCommand(
|
|
|
454
561
|
throw new Error("usage: skillmux config set <key> <value> [--dry-run]");
|
|
455
562
|
}
|
|
456
563
|
const res = await adapter.configSet(key, value, { dryRun: ctx.dryRun });
|
|
457
|
-
|
|
458
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
459
|
-
} else {
|
|
564
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
460
565
|
renderTargetBanner(ctx.target);
|
|
461
566
|
const prefix = ctx.dryRun ? "[dry-run] " : "";
|
|
462
|
-
console.log(
|
|
463
|
-
|
|
464
|
-
|
|
567
|
+
console.log(
|
|
568
|
+
`${prefix}${key}: ${JSON.stringify(res.prior_val)} -> ${JSON.stringify(res.resulting_val)}`,
|
|
569
|
+
);
|
|
570
|
+
console.log(
|
|
571
|
+
`Persistence: ${res.persistence}, Application: ${res.application}`,
|
|
572
|
+
);
|
|
573
|
+
});
|
|
465
574
|
return;
|
|
466
575
|
}
|
|
467
576
|
|
|
468
577
|
if (sub === "status") {
|
|
469
578
|
const res = await adapter.configStatus();
|
|
470
|
-
|
|
471
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
472
|
-
} else {
|
|
579
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
473
580
|
renderTargetBanner(ctx.target);
|
|
474
581
|
console.log(`Runtime: ${res.runtime}`);
|
|
475
582
|
console.log(`Active revision: ${res.active_revision}`);
|
|
476
583
|
console.log(`Readiness: ${res.readiness.status}`);
|
|
477
|
-
}
|
|
584
|
+
});
|
|
478
585
|
return;
|
|
479
586
|
}
|
|
480
587
|
|
|
@@ -482,9 +589,14 @@ async function handleConfigCommand(
|
|
|
482
589
|
}
|
|
483
590
|
|
|
484
591
|
async function confirmAction(prompt: string): Promise<boolean> {
|
|
485
|
-
const readline = createInterface({
|
|
592
|
+
const readline = createInterface({
|
|
593
|
+
input: process.stdin,
|
|
594
|
+
output: process.stdout,
|
|
595
|
+
});
|
|
486
596
|
try {
|
|
487
|
-
const answer = (await readline.question(`${prompt} [y/N] `))
|
|
597
|
+
const answer = (await readline.question(`${prompt} [y/N] `))
|
|
598
|
+
.trim()
|
|
599
|
+
.toLowerCase();
|
|
488
600
|
return answer === "y" || answer === "yes";
|
|
489
601
|
} finally {
|
|
490
602
|
readline.close();
|
|
@@ -495,7 +607,7 @@ async function handleCalibrateCommand(
|
|
|
495
607
|
adapter: TargetAdapter,
|
|
496
608
|
sub: string,
|
|
497
609
|
args: string[],
|
|
498
|
-
ctx: { target: ResolvedTarget; isJson: boolean }
|
|
610
|
+
ctx: { target: ResolvedTarget; isJson: boolean },
|
|
499
611
|
) {
|
|
500
612
|
if (sub === "run") {
|
|
501
613
|
let datasetPath: string | undefined;
|
|
@@ -503,21 +615,17 @@ async function handleCalibrateCommand(
|
|
|
503
615
|
if (args[i] === "--dataset") datasetPath = args[++i];
|
|
504
616
|
}
|
|
505
617
|
const res = await adapter.calibrateRun({ datasetPath });
|
|
506
|
-
|
|
507
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
508
|
-
} else {
|
|
618
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
509
619
|
renderTargetBanner(ctx.target);
|
|
510
620
|
console.log(`Calibration run complete.`);
|
|
511
621
|
if (res.result) console.log(JSON.stringify(res.result, null, 2));
|
|
512
|
-
}
|
|
622
|
+
});
|
|
513
623
|
return;
|
|
514
624
|
}
|
|
515
625
|
|
|
516
626
|
if (sub === "list") {
|
|
517
627
|
const res = await adapter.calibrateList();
|
|
518
|
-
|
|
519
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
520
|
-
} else {
|
|
628
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
521
629
|
renderTargetBanner(ctx.target);
|
|
522
630
|
renderTable(
|
|
523
631
|
[
|
|
@@ -525,9 +633,9 @@ async function handleCalibrateCommand(
|
|
|
525
633
|
{ key: "created_at", header: "CREATED_AT" },
|
|
526
634
|
{ key: "status", header: "STATUS" },
|
|
527
635
|
],
|
|
528
|
-
res
|
|
636
|
+
res,
|
|
529
637
|
);
|
|
530
|
-
}
|
|
638
|
+
});
|
|
531
639
|
return;
|
|
532
640
|
}
|
|
533
641
|
|
|
@@ -535,12 +643,10 @@ async function handleCalibrateCommand(
|
|
|
535
643
|
const runId = args[1];
|
|
536
644
|
if (!runId) throw new Error("usage: skillmux calibrate show <run_id>");
|
|
537
645
|
const res = await adapter.calibrateShow(runId);
|
|
538
|
-
|
|
539
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
540
|
-
} else {
|
|
646
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
541
647
|
renderTargetBanner(ctx.target);
|
|
542
648
|
console.log(JSON.stringify(res, null, 2));
|
|
543
|
-
}
|
|
649
|
+
});
|
|
544
650
|
return;
|
|
545
651
|
}
|
|
546
652
|
|
|
@@ -548,12 +654,10 @@ async function handleCalibrateCommand(
|
|
|
548
654
|
const runId = args[1];
|
|
549
655
|
if (!runId) throw new Error("usage: skillmux calibrate apply <run_id>");
|
|
550
656
|
const res = await adapter.calibrateApply(runId);
|
|
551
|
-
|
|
552
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
553
|
-
} else {
|
|
657
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
554
658
|
renderTargetBanner(ctx.target);
|
|
555
659
|
console.log(`Applied calibration run "${runId}"`);
|
|
556
|
-
}
|
|
660
|
+
});
|
|
557
661
|
return;
|
|
558
662
|
}
|
|
559
663
|
|
|
@@ -562,7 +666,9 @@ async function handleCalibrateCommand(
|
|
|
562
666
|
return;
|
|
563
667
|
}
|
|
564
668
|
|
|
565
|
-
throw new Error(
|
|
669
|
+
throw new Error(
|
|
670
|
+
"usage: skillmux calibrate generate-dataset [--vault <path>] [--out <file>]",
|
|
671
|
+
);
|
|
566
672
|
}
|
|
567
673
|
|
|
568
674
|
async function handleCompletionsCommand(shell: string) {
|
|
@@ -574,7 +680,7 @@ async function handleCompletionsCommand(shell: string) {
|
|
|
574
680
|
|
|
575
681
|
function handleError(
|
|
576
682
|
err: any,
|
|
577
|
-
opts: { target: ResolvedTarget; isJson: boolean; isVerbose: boolean }
|
|
683
|
+
opts: { target: ResolvedTarget; isJson: boolean; isVerbose: boolean },
|
|
578
684
|
) {
|
|
579
685
|
const code = mapExitCode(err);
|
|
580
686
|
process.exitCode = code;
|
|
@@ -589,7 +695,13 @@ function handleError(
|
|
|
589
695
|
});
|
|
590
696
|
console.log(JSON.stringify(env));
|
|
591
697
|
} else {
|
|
592
|
-
console.error(
|
|
698
|
+
console.error(
|
|
699
|
+
msg.startsWith("usage:") ||
|
|
700
|
+
msg.startsWith("Unknown") ||
|
|
701
|
+
msg.startsWith("error:")
|
|
702
|
+
? msg
|
|
703
|
+
: `error: ${msg}`,
|
|
704
|
+
);
|
|
593
705
|
if (opts.isVerbose && err instanceof Error && err.stack) {
|
|
594
706
|
console.error(err.stack);
|
|
595
707
|
}
|
|
@@ -601,9 +713,17 @@ function printHelp(): void {
|
|
|
601
713
|
|
|
602
714
|
Setup:
|
|
603
715
|
skillmux config init --vault <path> --yes
|
|
604
|
-
skillmux init [--client <name>...] [--target <name>...] [--
|
|
716
|
+
skillmux init [--client <name>...] [--target <name>...] [--dir <dir>]
|
|
605
717
|
[--vault <path>] [--core <skill_id>...]
|
|
606
|
-
[--migrate-full-vault] [--
|
|
718
|
+
[--migrate-full-vault] [--no-instructions] [--no-sync]
|
|
719
|
+
[--interactive|--yes|--dry-run] [--json]
|
|
720
|
+
skillmux project init [path] [--name <group>] [--skill <skill_id>...]
|
|
721
|
+
[--client <name>...] [--target <name>...] [--no-sync]
|
|
722
|
+
[--interactive|--yes|--dry-run] [--json]
|
|
723
|
+
skillmux project <list|show|add-path|remove-path|pin|unpin|attach|detach>
|
|
724
|
+
skillmux target <list|show|add|remove>
|
|
725
|
+
skillmux core <pin|unpin> <skill_id>... [--yes] [--dry-run] [--json]
|
|
726
|
+
skillmux skill which <skill_id>
|
|
607
727
|
|
|
608
728
|
Init clients:
|
|
609
729
|
claude-code, codex, gemini-cli, opencode, github-copilot, windsurf,
|
|
@@ -613,8 +733,8 @@ Init targets:
|
|
|
613
733
|
agent-skills, claude-code, codex, custom
|
|
614
734
|
|
|
615
735
|
Commands:
|
|
616
|
-
serve, index, sync, init, report, scan, install, eval, doctor,
|
|
617
|
-
|
|
736
|
+
serve, index, sync, init, project, target, core, report, scan, install, eval, doctor, skill,
|
|
737
|
+
local-vault, config, models, calibrate, context, completions`);
|
|
618
738
|
}
|
|
619
739
|
|
|
620
740
|
// ---------------------------------------------------------------------------
|
|
@@ -623,7 +743,10 @@ Commands:
|
|
|
623
743
|
|
|
624
744
|
type Transport = "stdio" | "http";
|
|
625
745
|
|
|
626
|
-
function parseServeArgs(args: string[]): {
|
|
746
|
+
function parseServeArgs(args: string[]): {
|
|
747
|
+
transport: Transport;
|
|
748
|
+
port?: number;
|
|
749
|
+
} {
|
|
627
750
|
let transport: Transport = "stdio";
|
|
628
751
|
let port: number | undefined;
|
|
629
752
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -653,7 +776,9 @@ async function runIndex(): Promise<void> {
|
|
|
653
776
|
const config = await loadConfig();
|
|
654
777
|
configure({ config, clients: createClients(config) });
|
|
655
778
|
const report = await rebuildIndex((skillId, error) => {
|
|
656
|
-
console.error(
|
|
779
|
+
console.error(
|
|
780
|
+
`warning: keeping previous index entry for ${skillId}: ${error}`,
|
|
781
|
+
);
|
|
657
782
|
});
|
|
658
783
|
const retainedNote =
|
|
659
784
|
report.retained.length > 0
|
|
@@ -665,43 +790,56 @@ async function runIndex(): Promise<void> {
|
|
|
665
790
|
const backfilled = await backfillEmbeddings();
|
|
666
791
|
console.log(`embeddings: ${backfilled} backfilled`);
|
|
667
792
|
} catch {
|
|
668
|
-
console.log(
|
|
793
|
+
console.log(
|
|
794
|
+
"embeddings: skipped (endpoint unreachable; lexical-only recall until next index)",
|
|
795
|
+
);
|
|
669
796
|
}
|
|
670
797
|
}
|
|
671
798
|
|
|
672
|
-
async function runEval(): Promise<void> {
|
|
799
|
+
async function runEval(options: { isJson: boolean }): Promise<void> {
|
|
673
800
|
const config = await loadConfig();
|
|
674
801
|
configure({ config, clients: createClients(config) });
|
|
675
802
|
|
|
676
803
|
const report = await evalVault().catch((error: unknown) => {
|
|
677
804
|
throw new Error(`eval requires local embeddings: ${String(error)}`);
|
|
678
805
|
});
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
806
|
+
emitSuccess({ isJson: options.isJson }, report, () => {
|
|
807
|
+
console.log(`holdout queries: ${report.queries}`);
|
|
808
|
+
console.log(`lexical recall@3: ${report.lexical.recall_at_3.toFixed(3)}`);
|
|
809
|
+
console.log(`lexical recall@5: ${report.lexical.recall_at_5.toFixed(3)}`);
|
|
810
|
+
console.log(`lexical MRR: ${report.lexical.mrr.toFixed(3)}`);
|
|
811
|
+
console.log(`hybrid recall@3: ${report.hybrid.recall_at_3.toFixed(3)}`);
|
|
812
|
+
console.log(`hybrid recall@5: ${report.hybrid.recall_at_5.toFixed(3)}`);
|
|
813
|
+
console.log(`hybrid MRR: ${report.hybrid.mrr.toFixed(3)}`);
|
|
814
|
+
});
|
|
686
815
|
}
|
|
687
816
|
|
|
688
|
-
async function runDoctor(): Promise<void> {
|
|
817
|
+
async function runDoctor(options: { isJson: boolean }): Promise<void> {
|
|
689
818
|
const report = await diagnose(await loadConfig());
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
819
|
+
emitSuccess({ isJson: options.isJson }, report, () => {
|
|
820
|
+
console.log(`inference mode: ${report.mode}`);
|
|
821
|
+
console.log(`routing capability: ${report.capability}`);
|
|
822
|
+
for (const check of report.checks)
|
|
823
|
+
console.log(
|
|
824
|
+
`${check.ok ? "ok" : "fail"}: ${check.name} - ${check.detail}`,
|
|
825
|
+
);
|
|
826
|
+
});
|
|
694
827
|
if (report.checks.some((check) => !check.ok)) process.exitCode = 1;
|
|
695
828
|
}
|
|
696
829
|
|
|
830
|
+
async function runSkill(subCommand: string, args: string[]): Promise<void> {
|
|
831
|
+
if (subCommand !== "which") throw new Error("usage: skillmux skill <which>");
|
|
832
|
+
await runWhich(args);
|
|
833
|
+
}
|
|
834
|
+
|
|
697
835
|
async function runWhich(args: string[]): Promise<void> {
|
|
698
836
|
const skillId = args[0];
|
|
699
|
-
if (!skillId) throw new Error("usage: skillmux which <skill_id>");
|
|
837
|
+
if (!skillId) throw new Error("usage: skillmux skill which <skill_id>");
|
|
700
838
|
const config = await loadConfig();
|
|
701
839
|
const vaultPath = expandHome(config.vault_path);
|
|
702
840
|
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
703
|
-
const roots = vaultResolutionOrder(vaultPath, localVaultPaths).filter(
|
|
704
|
-
existsSync(join(root, skillId, "SKILL.md")),
|
|
841
|
+
const roots = vaultResolutionOrder(vaultPath, localVaultPaths).filter(
|
|
842
|
+
(root) => existsSync(join(root, skillId, "SKILL.md")),
|
|
705
843
|
);
|
|
706
844
|
if (roots.length === 0) {
|
|
707
845
|
console.log(`${skillId}: not found in vault_path or local_vault_paths`);
|
|
@@ -709,75 +847,639 @@ async function runWhich(args: string[]): Promise<void> {
|
|
|
709
847
|
return;
|
|
710
848
|
}
|
|
711
849
|
console.log(`${skillId}: serving from ${roots[0]}`);
|
|
712
|
-
for (const shadowedRoot of roots.slice(1))
|
|
850
|
+
for (const shadowedRoot of roots.slice(1))
|
|
851
|
+
console.log(` shadows: ${shadowedRoot}`);
|
|
713
852
|
}
|
|
714
853
|
|
|
715
|
-
const
|
|
854
|
+
const PROJECT_INIT_USAGE =
|
|
855
|
+
"usage: skillmux project init [path] [--name <group>] [--skill <id>...] [--client <id>...] [--target <name>...] [--yes] [--no-sync]";
|
|
716
856
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
857
|
+
interface ProjectInitArgs {
|
|
858
|
+
path: string;
|
|
859
|
+
name: string;
|
|
860
|
+
skills: string[];
|
|
861
|
+
clients: string[];
|
|
862
|
+
targets: string[];
|
|
863
|
+
yes: boolean;
|
|
864
|
+
sync: boolean;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function configuredTargetForSurface(
|
|
868
|
+
manifest: ReturnType<typeof parseManifest>,
|
|
869
|
+
surface: { targetName: string; path: string },
|
|
870
|
+
): string | undefined {
|
|
871
|
+
if (manifest.targets[surface.targetName]) return surface.targetName;
|
|
872
|
+
return Object.entries(manifest.targets).find(
|
|
873
|
+
([, target]) => expandHome(target.dir) === surface.path,
|
|
874
|
+
)?.[0];
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function configuredTargetsForClients(
|
|
878
|
+
manifest: ReturnType<typeof parseManifest>,
|
|
879
|
+
clients: readonly string[],
|
|
880
|
+
): string[] {
|
|
881
|
+
return planClientSurfaces(clients).surfaces.map((surface) => {
|
|
882
|
+
const target = configuredTargetForSurface(manifest, surface);
|
|
883
|
+
if (target) return target;
|
|
884
|
+
const client = surface.clients[0]!;
|
|
885
|
+
throw new Error(
|
|
886
|
+
`client target for "${client}" is not configured; run "skillmux init --client ${client} --yes" first`,
|
|
887
|
+
);
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function parseProjectInitArgs(args: string[]): ProjectInitArgs {
|
|
892
|
+
let projectPath: string | undefined;
|
|
893
|
+
let name: string | undefined;
|
|
894
|
+
const skills: string[] = [];
|
|
895
|
+
const clients: string[] = [];
|
|
896
|
+
const targets: string[] = [];
|
|
897
|
+
let yes = false;
|
|
898
|
+
let sync = true;
|
|
899
|
+
|
|
900
|
+
for (let i = 0; i < args.length; i++) {
|
|
901
|
+
const arg = args[i]!;
|
|
902
|
+
if (arg === "--name") {
|
|
903
|
+
name = args[++i];
|
|
904
|
+
if (!name) throw new Error("--name requires a group name");
|
|
905
|
+
} else if (arg === "--skill") {
|
|
906
|
+
const skill = args[++i];
|
|
907
|
+
if (!skill) throw new Error("--skill requires a skill_id");
|
|
908
|
+
skills.push(skill);
|
|
909
|
+
} else if (arg === "--target") {
|
|
910
|
+
const target = args[++i];
|
|
911
|
+
if (!target) throw new Error("--target requires a name");
|
|
912
|
+
targets.push(target);
|
|
913
|
+
} else if (arg === "--client") {
|
|
914
|
+
const client = args[++i];
|
|
915
|
+
if (!client) throw new Error("--client requires a name");
|
|
916
|
+
clients.push(client);
|
|
917
|
+
} else if (arg === "--yes") {
|
|
918
|
+
yes = true;
|
|
919
|
+
} else if (arg === "--no-sync") {
|
|
920
|
+
sync = false;
|
|
921
|
+
} else if (
|
|
922
|
+
arg === "--dry-run" ||
|
|
923
|
+
arg === "--json" ||
|
|
924
|
+
arg === "--interactive"
|
|
925
|
+
) {
|
|
926
|
+
continue;
|
|
927
|
+
} else if (arg.startsWith("-")) {
|
|
928
|
+
throw new Error(`unknown project init option: ${arg}`);
|
|
929
|
+
} else if (projectPath) {
|
|
930
|
+
throw new Error(PROJECT_INIT_USAGE);
|
|
931
|
+
} else {
|
|
932
|
+
projectPath = arg;
|
|
933
|
+
}
|
|
735
934
|
}
|
|
736
|
-
|
|
737
|
-
|
|
935
|
+
|
|
936
|
+
const path = resolveProjectDirectory(
|
|
937
|
+
projectPath ? expandHome(projectPath) : undefined,
|
|
938
|
+
);
|
|
939
|
+
return {
|
|
940
|
+
path,
|
|
941
|
+
name: name ?? suggestProjectName(basename(path)),
|
|
942
|
+
skills,
|
|
943
|
+
clients,
|
|
944
|
+
targets,
|
|
945
|
+
yes,
|
|
946
|
+
sync,
|
|
947
|
+
};
|
|
738
948
|
}
|
|
739
949
|
|
|
740
|
-
async function
|
|
741
|
-
if (subCommand !== "pin" && subCommand !== "unpin") throw new Error(MANIFEST_USAGE);
|
|
742
|
-
const { skillId, core, project, paths } = parseManifestPinArgs(args);
|
|
950
|
+
async function loadManifestContext() {
|
|
743
951
|
const config = await loadConfig();
|
|
744
952
|
const vaultPath = expandHome(config.vault_path);
|
|
745
|
-
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
746
953
|
const manifestPath = resolveManifestPath(vaultPath);
|
|
747
|
-
if (!manifestPath)
|
|
954
|
+
if (!manifestPath) {
|
|
955
|
+
throw new Error(
|
|
956
|
+
`no skillmux.toml found at ${vaultPath}; run skillmux init first`,
|
|
957
|
+
);
|
|
958
|
+
}
|
|
748
959
|
const manifest = parseManifest(await Bun.file(manifestPath).text());
|
|
960
|
+
return { config, vaultPath, manifestPath, manifest };
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
async function confirmIfNeeded(opts: {
|
|
964
|
+
confirmed: boolean;
|
|
965
|
+
isJson: boolean;
|
|
966
|
+
prompt: string;
|
|
967
|
+
nonInteractiveError: string;
|
|
968
|
+
}): Promise<boolean> {
|
|
969
|
+
if (opts.confirmed) return true;
|
|
970
|
+
if (opts.isJson || !isInteractive()) {
|
|
971
|
+
throw new Error(opts.nonInteractiveError);
|
|
972
|
+
}
|
|
973
|
+
return confirmAction(opts.prompt);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
async function runProject(
|
|
977
|
+
subCommand: string,
|
|
978
|
+
args: string[],
|
|
979
|
+
options: { isJson: boolean; dryRun: boolean },
|
|
980
|
+
): Promise<void> {
|
|
981
|
+
if (subCommand === "list" || subCommand === "show") {
|
|
982
|
+
const { manifest } = await loadManifestContext();
|
|
983
|
+
const names =
|
|
984
|
+
subCommand === "show"
|
|
985
|
+
? [args[0] ?? ""]
|
|
986
|
+
: Object.keys(manifest.project ?? {});
|
|
987
|
+
if (subCommand === "show" && !manifest.project?.[names[0]!]) {
|
|
988
|
+
throw new Error(`[project.${names[0]}] does not exist`);
|
|
989
|
+
}
|
|
990
|
+
const projects = names.map((name) => ({
|
|
991
|
+
name,
|
|
992
|
+
paths: manifest.project?.[name]!.paths ?? [],
|
|
993
|
+
skills: manifest.project?.[name]!.skills ?? [],
|
|
994
|
+
targets: Object.entries(manifest.targets)
|
|
995
|
+
.filter(([, target]) => target.project_groups.includes(name))
|
|
996
|
+
.map(([target]) => target),
|
|
997
|
+
}));
|
|
998
|
+
if (options.isJson) {
|
|
999
|
+
console.log(JSON.stringify({ schema_version: 1, projects }));
|
|
1000
|
+
} else if (projects.length === 0) {
|
|
1001
|
+
console.log("no project groups configured");
|
|
1002
|
+
} else {
|
|
1003
|
+
for (const project of projects) {
|
|
1004
|
+
console.log(`${project.name}:`);
|
|
1005
|
+
console.log(` paths: ${project.paths.join(", ") || "(none)"}`);
|
|
1006
|
+
console.log(` skills: ${project.skills.join(", ") || "(none)"}`);
|
|
1007
|
+
console.log(` targets: ${project.targets.join(", ") || "(none)"}`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
if (subCommand === "add-path" || subCommand === "remove-path") {
|
|
1013
|
+
const group = args[0];
|
|
1014
|
+
if (!group)
|
|
1015
|
+
throw new Error(
|
|
1016
|
+
`usage: skillmux project ${subCommand} <group> [path] --yes`,
|
|
1017
|
+
);
|
|
1018
|
+
const rawPath = args[1]?.startsWith("-") ? undefined : args[1];
|
|
1019
|
+
const projectPath = resolveProjectDirectory(
|
|
1020
|
+
rawPath ? expandHome(rawPath) : undefined,
|
|
1021
|
+
);
|
|
1022
|
+
const yes = args.includes("--yes");
|
|
1023
|
+
if (!existsSync(projectPath) || !lstatSync(projectPath).isDirectory()) {
|
|
1024
|
+
throw new Error(`project path is not a directory: ${projectPath}`);
|
|
1025
|
+
}
|
|
1026
|
+
const { config, vaultPath, manifestPath, manifest } =
|
|
1027
|
+
await loadManifestContext();
|
|
1028
|
+
const updated = updateProjectPaths(manifest, group, {
|
|
1029
|
+
...(subCommand === "add-path"
|
|
1030
|
+
? { add: [projectPath] }
|
|
1031
|
+
: { remove: [projectPath] }),
|
|
1032
|
+
});
|
|
1033
|
+
validateManifest(
|
|
1034
|
+
updated,
|
|
1035
|
+
vaultPath,
|
|
1036
|
+
config.local_vault_paths.map(expandHome),
|
|
1037
|
+
);
|
|
1038
|
+
if (options.dryRun) {
|
|
1039
|
+
console.log(`${subCommand}: [project.${group}] ${projectPath} (dry-run)`);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
if (
|
|
1043
|
+
!(await confirmIfNeeded({
|
|
1044
|
+
confirmed: yes,
|
|
1045
|
+
isJson: options.isJson,
|
|
1046
|
+
prompt: `${subCommand} ${projectPath} in [project.${group}]?`,
|
|
1047
|
+
nonInteractiveError: `skillmux project ${subCommand} requires --yes when run non-interactively`,
|
|
1048
|
+
}))
|
|
1049
|
+
)
|
|
1050
|
+
return;
|
|
1051
|
+
writeManifestAtomic(manifestPath, updated);
|
|
1052
|
+
console.log(`${subCommand}: [project.${group}] ${projectPath}`);
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
if (subCommand === "pin" || subCommand === "unpin") {
|
|
1056
|
+
const group = args[0];
|
|
1057
|
+
const skills = args.slice(1).filter((arg) => !arg.startsWith("-"));
|
|
1058
|
+
if (!group || skills.length === 0) {
|
|
1059
|
+
throw new Error(
|
|
1060
|
+
`usage: skillmux project ${subCommand} <group> <skill_id>... --yes`,
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
const yes = args.includes("--yes");
|
|
1064
|
+
const { config, vaultPath, manifestPath, manifest } =
|
|
1065
|
+
await loadManifestContext();
|
|
1066
|
+
let updated = manifest;
|
|
1067
|
+
for (const skill of skills) {
|
|
1068
|
+
updated =
|
|
1069
|
+
subCommand === "pin"
|
|
1070
|
+
? pinProject(updated, skill, group)
|
|
1071
|
+
: unpinProject(updated, skill, group);
|
|
1072
|
+
}
|
|
1073
|
+
validateManifest(
|
|
1074
|
+
updated,
|
|
1075
|
+
vaultPath,
|
|
1076
|
+
config.local_vault_paths.map(expandHome),
|
|
1077
|
+
);
|
|
1078
|
+
if (options.dryRun) {
|
|
1079
|
+
console.log(
|
|
1080
|
+
`${subCommand}: [project.${group}] ${skills.join(", ")} (dry-run)`,
|
|
1081
|
+
);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (
|
|
1085
|
+
!(await confirmIfNeeded({
|
|
1086
|
+
confirmed: yes,
|
|
1087
|
+
isJson: options.isJson,
|
|
1088
|
+
prompt: `${subCommand} ${skills.join(", ")} in [project.${group}]?`,
|
|
1089
|
+
nonInteractiveError: `skillmux project ${subCommand} requires --yes when run non-interactively`,
|
|
1090
|
+
}))
|
|
1091
|
+
)
|
|
1092
|
+
return;
|
|
1093
|
+
writeManifestAtomic(manifestPath, updated);
|
|
1094
|
+
console.log(`${subCommand}: [project.${group}] ${skills.join(", ")}`);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (subCommand === "attach" || subCommand === "detach") {
|
|
1098
|
+
const group = args[0];
|
|
1099
|
+
if (!group)
|
|
1100
|
+
throw new Error(
|
|
1101
|
+
`usage: skillmux project ${subCommand} <group> (--client <id>... | --target <name>...) --yes`,
|
|
1102
|
+
);
|
|
1103
|
+
const clients: string[] = [];
|
|
1104
|
+
const requestedTargets: string[] = [];
|
|
1105
|
+
for (let i = 1; i < args.length; i++) {
|
|
1106
|
+
if (args[i] === "--client") {
|
|
1107
|
+
const value = args[++i];
|
|
1108
|
+
if (!value) throw new Error("--client requires a name");
|
|
1109
|
+
clients.push(value);
|
|
1110
|
+
} else if (args[i] === "--target") {
|
|
1111
|
+
const value = args[++i];
|
|
1112
|
+
if (!value) throw new Error("--target requires a name");
|
|
1113
|
+
requestedTargets.push(value);
|
|
1114
|
+
} else if (
|
|
1115
|
+
args[i] !== "--yes" &&
|
|
1116
|
+
args[i] !== "--dry-run" &&
|
|
1117
|
+
args[i] !== "--json"
|
|
1118
|
+
) {
|
|
1119
|
+
throw new Error(`unknown project ${subCommand} option: ${args[i]}`);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
const { config, vaultPath, manifestPath, manifest } =
|
|
1123
|
+
await loadManifestContext();
|
|
1124
|
+
const clientTargets = configuredTargetsForClients(manifest, clients);
|
|
1125
|
+
const targets = [...new Set([...requestedTargets, ...clientTargets])];
|
|
1126
|
+
if (targets.length === 0) {
|
|
1127
|
+
throw new Error(`project ${subCommand} requires --client or --target`);
|
|
1128
|
+
}
|
|
1129
|
+
const updated = updateProjectTargets(manifest, group, {
|
|
1130
|
+
...(subCommand === "attach" ? { attach: targets } : { detach: targets }),
|
|
1131
|
+
});
|
|
1132
|
+
validateManifest(
|
|
1133
|
+
updated,
|
|
1134
|
+
vaultPath,
|
|
1135
|
+
config.local_vault_paths.map(expandHome),
|
|
1136
|
+
);
|
|
1137
|
+
if (options.dryRun) {
|
|
1138
|
+
console.log(
|
|
1139
|
+
`${subCommand}: [project.${group}] ${targets.join(", ")} (dry-run)`,
|
|
1140
|
+
);
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
if (
|
|
1144
|
+
!(await confirmIfNeeded({
|
|
1145
|
+
confirmed: args.includes("--yes"),
|
|
1146
|
+
isJson: options.isJson,
|
|
1147
|
+
prompt: `${subCommand} [project.${group}] to ${targets.join(", ")}?`,
|
|
1148
|
+
nonInteractiveError: `skillmux project ${subCommand} requires --yes when run non-interactively`,
|
|
1149
|
+
}))
|
|
1150
|
+
)
|
|
1151
|
+
return;
|
|
1152
|
+
writeManifestAtomic(manifestPath, updated);
|
|
1153
|
+
console.log(`${subCommand}: [project.${group}] ${targets.join(", ")}`);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
if (subCommand !== "init") throw new Error(PROJECT_INIT_USAGE);
|
|
1157
|
+
let request = parseProjectInitArgs(args);
|
|
1158
|
+
const guided = shouldUseWizard(args, {
|
|
1159
|
+
interactive: isInteractive(),
|
|
1160
|
+
json: options.isJson,
|
|
1161
|
+
dryRun: options.dryRun,
|
|
1162
|
+
});
|
|
1163
|
+
if (!existsSync(request.path))
|
|
1164
|
+
throw new Error(`project path does not exist: ${request.path}`);
|
|
1165
|
+
if (!lstatSync(request.path).isDirectory()) {
|
|
1166
|
+
throw new Error(`project path is not a directory: ${request.path}`);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
const { config, vaultPath, manifestPath, manifest } =
|
|
1170
|
+
await loadManifestContext();
|
|
1171
|
+
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
1172
|
+
if (guided) {
|
|
1173
|
+
const name = await promptText("Project group", request.name);
|
|
1174
|
+
const availableClients = SUPPORTED_CLIENT_IDS.filter((client) => {
|
|
1175
|
+
const surface = planClientSurfaces([client]).surfaces[0];
|
|
1176
|
+
return (
|
|
1177
|
+
surface !== undefined &&
|
|
1178
|
+
configuredTargetForSurface(manifest, surface) !== undefined
|
|
1179
|
+
);
|
|
1180
|
+
});
|
|
1181
|
+
const clients = await promptMultiSelect(
|
|
1182
|
+
"Which clients should receive project skills?",
|
|
1183
|
+
availableClients.map((client) => ({
|
|
1184
|
+
value: client,
|
|
1185
|
+
label: client,
|
|
1186
|
+
selected:
|
|
1187
|
+
request.clients.length === 0 || request.clients.includes(client),
|
|
1188
|
+
})),
|
|
1189
|
+
);
|
|
1190
|
+
const skills = parseCommaList(
|
|
1191
|
+
await promptText(
|
|
1192
|
+
"Project skill IDs, comma-separated",
|
|
1193
|
+
request.skills.join(","),
|
|
1194
|
+
),
|
|
1195
|
+
);
|
|
1196
|
+
request = { ...request, name, clients, skills };
|
|
1197
|
+
}
|
|
1198
|
+
const clientTargets = configuredTargetsForClients(manifest, request.clients);
|
|
1199
|
+
const targets = [...new Set([...request.targets, ...clientTargets])];
|
|
1200
|
+
const updated = upsertProject(manifest, {
|
|
1201
|
+
name: request.name,
|
|
1202
|
+
paths: [request.path],
|
|
1203
|
+
skills: request.skills,
|
|
1204
|
+
targets,
|
|
1205
|
+
});
|
|
1206
|
+
const { notes } = validateManifest(updated, vaultPath, localVaultPaths);
|
|
1207
|
+
const plan = {
|
|
1208
|
+
mode: "project",
|
|
1209
|
+
project: request.name,
|
|
1210
|
+
path: request.path,
|
|
1211
|
+
skills: request.skills,
|
|
1212
|
+
clients: request.clients,
|
|
1213
|
+
targets,
|
|
1214
|
+
sync: request.sync,
|
|
1215
|
+
notes,
|
|
1216
|
+
};
|
|
749
1217
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1218
|
+
if (options.dryRun) {
|
|
1219
|
+
console.log(
|
|
1220
|
+
options.isJson
|
|
1221
|
+
? JSON.stringify({ schema_version: 1, plan })
|
|
1222
|
+
: `project plan: ${JSON.stringify(plan)}`,
|
|
1223
|
+
);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
if (!request.yes) {
|
|
1227
|
+
if (!options.isJson && isInteractive()) {
|
|
1228
|
+
if (guided) {
|
|
1229
|
+
console.log("\nReview");
|
|
1230
|
+
console.log(` project: ${request.name}`);
|
|
1231
|
+
console.log(` path: ${request.path}`);
|
|
1232
|
+
console.log(` clients: ${request.clients.join(", ") || "(none)"}`);
|
|
1233
|
+
console.log(` skills: ${request.skills.join(", ") || "(none)"}`);
|
|
1234
|
+
console.log(` sync: ${request.sync ? "yes" : "no"}`);
|
|
1235
|
+
}
|
|
1236
|
+
if (
|
|
1237
|
+
!(await confirmAction(
|
|
1238
|
+
`Apply project setup for ${request.name} at ${request.path}?`,
|
|
1239
|
+
))
|
|
1240
|
+
) {
|
|
1241
|
+
console.log("project setup cancelled");
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
} else {
|
|
1245
|
+
throw new Error(
|
|
1246
|
+
"skillmux project init requires --yes when run non-interactively",
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
writeManifestAtomic(manifestPath, updated);
|
|
1252
|
+
if (request.sync) {
|
|
1253
|
+
try {
|
|
1254
|
+
await runSync([]);
|
|
1255
|
+
} catch (error) {
|
|
1256
|
+
throw new Error(
|
|
1257
|
+
`project configuration was saved, but sync failed; fix the reported issue and run "skillmux sync": ${
|
|
1258
|
+
error instanceof Error ? error.message : String(error)
|
|
1259
|
+
}`,
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
if (options.isJson) {
|
|
1264
|
+
console.log(JSON.stringify({ schema_version: 1, result: plan }));
|
|
753
1265
|
} else {
|
|
1266
|
+
console.log(`project "${request.name}" ready at ${request.path}`);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
async function runCore(
|
|
1271
|
+
subCommand: string,
|
|
1272
|
+
args: string[],
|
|
1273
|
+
options: { isJson: boolean; dryRun: boolean },
|
|
1274
|
+
): Promise<void> {
|
|
1275
|
+
if (subCommand !== "pin" && subCommand !== "unpin") {
|
|
1276
|
+
throw new Error("usage: skillmux core <pin|unpin>");
|
|
1277
|
+
}
|
|
1278
|
+
const skillIds = args.filter((arg) => !arg.startsWith("-"));
|
|
1279
|
+
if (skillIds.length === 0) {
|
|
1280
|
+
throw new Error(`usage: skillmux core ${subCommand} <skill_id>... --yes`);
|
|
1281
|
+
}
|
|
1282
|
+
const yes = args.includes("--yes");
|
|
1283
|
+
const { config, vaultPath, manifestPath, manifest } =
|
|
1284
|
+
await loadManifestContext();
|
|
1285
|
+
let updated = manifest;
|
|
1286
|
+
for (const skillId of skillIds) {
|
|
754
1287
|
updated =
|
|
755
1288
|
subCommand === "pin"
|
|
756
|
-
?
|
|
757
|
-
:
|
|
1289
|
+
? pinCore(updated, skillId)
|
|
1290
|
+
: unpinCore(updated, skillId);
|
|
1291
|
+
}
|
|
1292
|
+
validateManifest(
|
|
1293
|
+
updated,
|
|
1294
|
+
vaultPath,
|
|
1295
|
+
config.local_vault_paths.map(expandHome),
|
|
1296
|
+
);
|
|
1297
|
+
if (options.dryRun) {
|
|
1298
|
+
emitSuccess(
|
|
1299
|
+
{ isJson: options.isJson },
|
|
1300
|
+
{ subcommand: subCommand, skill_ids: skillIds },
|
|
1301
|
+
() =>
|
|
1302
|
+
console.log(`${subCommand}: [core] ${skillIds.join(", ")} (dry-run)`),
|
|
1303
|
+
);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
if (
|
|
1307
|
+
!(await confirmIfNeeded({
|
|
1308
|
+
confirmed: yes,
|
|
1309
|
+
isJson: options.isJson,
|
|
1310
|
+
prompt: `${subCommand} ${skillIds.join(", ")} in [core]?`,
|
|
1311
|
+
nonInteractiveError: `skillmux core ${subCommand} requires --yes when run non-interactively`,
|
|
1312
|
+
}))
|
|
1313
|
+
)
|
|
1314
|
+
return;
|
|
1315
|
+
writeManifestAtomic(manifestPath, updated);
|
|
1316
|
+
console.log(`${subCommand}: [core] ${skillIds.join(", ")}`);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
async function runTarget(
|
|
1320
|
+
subCommand: string,
|
|
1321
|
+
args: string[],
|
|
1322
|
+
options: { isJson: boolean; dryRun: boolean },
|
|
1323
|
+
): Promise<void> {
|
|
1324
|
+
const { vaultPath, manifestPath, manifest } = await loadManifestContext();
|
|
1325
|
+
|
|
1326
|
+
if (subCommand === "list" || subCommand === "show") {
|
|
1327
|
+
const names =
|
|
1328
|
+
subCommand === "show" ? [args[0] ?? ""] : Object.keys(manifest.targets);
|
|
1329
|
+
if (subCommand === "show" && !manifest.targets[names[0]!]) {
|
|
1330
|
+
throw new Error(`target "${names[0]}" does not exist`);
|
|
1331
|
+
}
|
|
1332
|
+
const targets = names.map((name) => {
|
|
1333
|
+
const target = manifest.targets[name]!;
|
|
1334
|
+
const clients = SUPPORTED_CLIENT_IDS.filter((client) => {
|
|
1335
|
+
const surface = planClientSurfaces([client]).surfaces[0];
|
|
1336
|
+
return surface !== undefined && surface.path === expandHome(target.dir);
|
|
1337
|
+
});
|
|
1338
|
+
return { name, ...target, clients };
|
|
1339
|
+
});
|
|
1340
|
+
if (options.isJson) {
|
|
1341
|
+
console.log(JSON.stringify({ schema_version: 1, targets }));
|
|
1342
|
+
} else if (targets.length === 0) {
|
|
1343
|
+
console.log("no targets configured");
|
|
1344
|
+
} else {
|
|
1345
|
+
for (const target of targets) {
|
|
1346
|
+
console.log(`${target.name}:`);
|
|
1347
|
+
console.log(` dir: ${target.dir}`);
|
|
1348
|
+
console.log(` host: ${target.host ?? "(global)"}`);
|
|
1349
|
+
console.log(` clients: ${target.clients.join(", ") || "(custom)"}`);
|
|
1350
|
+
console.log(
|
|
1351
|
+
` projects: ${target.project_groups.join(", ") || "(none)"}`,
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
if (subCommand === "add") {
|
|
1359
|
+
const name = args[0];
|
|
1360
|
+
const dirIndex = args.indexOf("--dir");
|
|
1361
|
+
const rawPath = dirIndex === -1 ? undefined : args[dirIndex + 1];
|
|
1362
|
+
if (!name || !rawPath)
|
|
1363
|
+
throw new Error("usage: skillmux target add <name> --dir <dir> --yes");
|
|
1364
|
+
const path = expandHome(rawPath);
|
|
1365
|
+
if (options.dryRun) {
|
|
1366
|
+
const planned = planInitManifest(vaultPath, [{ name, dir: path }], []);
|
|
1367
|
+
console.log(
|
|
1368
|
+
options.isJson
|
|
1369
|
+
? JSON.stringify({ schema_version: 1, target: planned.targets[name] })
|
|
1370
|
+
: `target add: ${name} -> ${path} (dry-run)`,
|
|
1371
|
+
);
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
if (
|
|
1375
|
+
!(await confirmIfNeeded({
|
|
1376
|
+
confirmed: args.includes("--yes"),
|
|
1377
|
+
isJson: options.isJson,
|
|
1378
|
+
prompt: `Adopt target ${name} at ${path}?`,
|
|
1379
|
+
nonInteractiveError:
|
|
1380
|
+
"skillmux target add requires --yes when run non-interactively",
|
|
1381
|
+
}))
|
|
1382
|
+
)
|
|
1383
|
+
return;
|
|
1384
|
+
applyInit(vaultPath, [{ name, dir: path }]);
|
|
1385
|
+
console.log(`target "${name}" added at ${path}`);
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
if (subCommand === "remove") {
|
|
1390
|
+
const name = args[0];
|
|
1391
|
+
if (!name || !manifest.targets[name]) {
|
|
1392
|
+
throw new Error(
|
|
1393
|
+
name
|
|
1394
|
+
? `target "${name}" does not exist`
|
|
1395
|
+
: "usage: skillmux target remove <name> --yes",
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
if (options.dryRun) {
|
|
1399
|
+
console.log(`target remove: ${name} (files preserved, dry-run)`);
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
if (
|
|
1403
|
+
!(await confirmIfNeeded({
|
|
1404
|
+
confirmed: args.includes("--yes"),
|
|
1405
|
+
isJson: options.isJson,
|
|
1406
|
+
prompt: `Remove target ${name} from the manifest and preserve its files?`,
|
|
1407
|
+
nonInteractiveError:
|
|
1408
|
+
"skillmux target remove requires --yes when run non-interactively",
|
|
1409
|
+
}))
|
|
1410
|
+
)
|
|
1411
|
+
return;
|
|
1412
|
+
const targets = { ...manifest.targets };
|
|
1413
|
+
delete targets[name];
|
|
1414
|
+
writeManifestAtomic(manifestPath, { ...manifest, targets });
|
|
1415
|
+
console.log(
|
|
1416
|
+
`target "${name}" removed from the manifest; files preserved at ${manifest.targets[name]!.dir}`,
|
|
1417
|
+
);
|
|
1418
|
+
return;
|
|
758
1419
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
console.log(`${subCommand === "pin" ? "pinned" : "unpinned"} "${skillId}" ${core ? "[core]" : `[project.${project}]`}`);
|
|
1420
|
+
|
|
1421
|
+
throw new Error("usage: skillmux target <list|show|add|remove>");
|
|
762
1422
|
}
|
|
763
1423
|
|
|
764
|
-
async function runLocalVaultInit(
|
|
1424
|
+
async function runLocalVaultInit(
|
|
1425
|
+
args: string[],
|
|
1426
|
+
options: { isJson: boolean; dryRun: boolean },
|
|
1427
|
+
): Promise<void> {
|
|
765
1428
|
const path = args[0];
|
|
766
|
-
if (!path) throw new Error("usage: skillmux local-vault init <path>");
|
|
1429
|
+
if (!path) throw new Error("usage: skillmux local-vault init <path> --yes");
|
|
767
1430
|
const expanded = expandHome(path);
|
|
768
1431
|
const config = await loadConfig();
|
|
769
1432
|
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
770
1433
|
if (!localVaultPaths.includes(expanded)) {
|
|
771
|
-
throw new Error(
|
|
1434
|
+
throw new Error(
|
|
1435
|
+
`"${path}" is not one of the configured local_vault_paths — add it to config.toml first`,
|
|
1436
|
+
);
|
|
772
1437
|
}
|
|
773
1438
|
if (!existsSync(expanded)) throw new Error(`"${path}" does not exist`);
|
|
1439
|
+
const markerPath = join(expanded, ".skillmux");
|
|
1440
|
+
if (options.dryRun) {
|
|
1441
|
+
console.log(
|
|
1442
|
+
options.isJson
|
|
1443
|
+
? JSON.stringify({
|
|
1444
|
+
schema_version: 1,
|
|
1445
|
+
marker_path: markerPath,
|
|
1446
|
+
vault_path: expandHome(config.vault_path),
|
|
1447
|
+
})
|
|
1448
|
+
: `local-vault init: ${markerPath} (role: local_vault, vault_path: ${expandHome(config.vault_path)}) (dry-run)`,
|
|
1449
|
+
);
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
if (
|
|
1453
|
+
!(await confirmIfNeeded({
|
|
1454
|
+
confirmed: args.includes("--yes"),
|
|
1455
|
+
isJson: options.isJson,
|
|
1456
|
+
prompt: `Mark ${expanded} as a local_vault (role: local_vault, vault_path: ${expandHome(config.vault_path)})?`,
|
|
1457
|
+
nonInteractiveError:
|
|
1458
|
+
"skillmux local-vault init requires --yes when run non-interactively",
|
|
1459
|
+
}))
|
|
1460
|
+
)
|
|
1461
|
+
return;
|
|
774
1462
|
writeLocalVaultMarker(expanded, expandHome(config.vault_path));
|
|
775
|
-
|
|
1463
|
+
if (options.isJson) {
|
|
1464
|
+
console.log(
|
|
1465
|
+
JSON.stringify({
|
|
1466
|
+
schema_version: 1,
|
|
1467
|
+
marker_path: markerPath,
|
|
1468
|
+
vault_path: expandHome(config.vault_path),
|
|
1469
|
+
}),
|
|
1470
|
+
);
|
|
1471
|
+
} else {
|
|
1472
|
+
console.log(
|
|
1473
|
+
`wrote ${markerPath} (role: local_vault, vault_path: ${expandHome(config.vault_path)})`,
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
776
1476
|
}
|
|
777
1477
|
|
|
778
|
-
async function runModelDownload(): Promise<void> {
|
|
1478
|
+
async function runModelDownload(options: { isJson: boolean }): Promise<void> {
|
|
779
1479
|
const cacheDir = await downloadLocalModels(await loadConfig());
|
|
780
|
-
|
|
1480
|
+
emitSuccess({ isJson: options.isJson }, { cache_dir: cacheDir }, () =>
|
|
1481
|
+
console.log(`models ready in ${cacheDir}`),
|
|
1482
|
+
);
|
|
781
1483
|
}
|
|
782
1484
|
|
|
783
1485
|
function parseSyncArgs(args: string[]): {
|
|
@@ -844,10 +1546,18 @@ async function runSync(args: string[]): Promise<void> {
|
|
|
844
1546
|
|
|
845
1547
|
const suffix = dryRun ? " (dry-run)" : "";
|
|
846
1548
|
const result = syncTarget(
|
|
847
|
-
{
|
|
1549
|
+
{
|
|
1550
|
+
vaultPath,
|
|
1551
|
+
targetDir,
|
|
1552
|
+
targetName,
|
|
1553
|
+
coreSkillIds: manifest.core.skills,
|
|
1554
|
+
localVaultPaths,
|
|
1555
|
+
},
|
|
848
1556
|
{ dryRun },
|
|
849
1557
|
);
|
|
850
|
-
console.log(
|
|
1558
|
+
console.log(
|
|
1559
|
+
`${targetName}: +${result.added.length} -${result.removed.length}${suffix}`,
|
|
1560
|
+
);
|
|
851
1561
|
|
|
852
1562
|
if (target.project_groups.length > 0) {
|
|
853
1563
|
const allGroups = manifest.project ?? {};
|
|
@@ -873,6 +1583,8 @@ function parseInitArgs(args: string[]): {
|
|
|
873
1583
|
coreSkillIds: string[];
|
|
874
1584
|
customPath?: string;
|
|
875
1585
|
migrateFullVault: boolean;
|
|
1586
|
+
skipInstructions: boolean;
|
|
1587
|
+
sync: boolean;
|
|
876
1588
|
vaultPath?: string;
|
|
877
1589
|
yes: boolean;
|
|
878
1590
|
} {
|
|
@@ -881,6 +1593,8 @@ function parseInitArgs(args: string[]): {
|
|
|
881
1593
|
const coreSkillIds: string[] = [];
|
|
882
1594
|
let customPath: string | undefined;
|
|
883
1595
|
let migrateFullVault = false;
|
|
1596
|
+
let skipInstructions = false;
|
|
1597
|
+
let sync = true;
|
|
884
1598
|
let vaultPath: string | undefined;
|
|
885
1599
|
let yes = false;
|
|
886
1600
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -900,9 +1614,9 @@ function parseInitArgs(args: string[]): {
|
|
|
900
1614
|
if (!value) throw new Error("--vault requires a path");
|
|
901
1615
|
vaultPath = value;
|
|
902
1616
|
i++;
|
|
903
|
-
} else if (option === "--
|
|
1617
|
+
} else if (option === "--dir") {
|
|
904
1618
|
const value = args[i + 1];
|
|
905
|
-
if (!value) throw new Error("--
|
|
1619
|
+
if (!value) throw new Error("--dir requires a directory");
|
|
906
1620
|
customPath = value;
|
|
907
1621
|
i++;
|
|
908
1622
|
} else if (option === "--core") {
|
|
@@ -910,17 +1624,35 @@ function parseInitArgs(args: string[]): {
|
|
|
910
1624
|
if (!value) throw new Error("--core requires a skill_id");
|
|
911
1625
|
coreSkillIds.push(value);
|
|
912
1626
|
i++;
|
|
913
|
-
} else if (
|
|
1627
|
+
} else if (
|
|
1628
|
+
option === "--dry-run" ||
|
|
1629
|
+
option === "--json" ||
|
|
1630
|
+
option === "--interactive"
|
|
1631
|
+
) {
|
|
914
1632
|
continue;
|
|
915
1633
|
} else if (option === "--migrate-full-vault") {
|
|
916
1634
|
migrateFullVault = true;
|
|
1635
|
+
} else if (option === "--no-instructions") {
|
|
1636
|
+
skipInstructions = true;
|
|
1637
|
+
} else if (option === "--no-sync") {
|
|
1638
|
+
sync = false;
|
|
917
1639
|
} else if (option === "--yes") {
|
|
918
1640
|
yes = true;
|
|
919
1641
|
} else {
|
|
920
1642
|
throw new Error(`unknown init option: ${option}`);
|
|
921
1643
|
}
|
|
922
1644
|
}
|
|
923
|
-
return {
|
|
1645
|
+
return {
|
|
1646
|
+
targets,
|
|
1647
|
+
clients,
|
|
1648
|
+
coreSkillIds,
|
|
1649
|
+
customPath,
|
|
1650
|
+
migrateFullVault,
|
|
1651
|
+
skipInstructions,
|
|
1652
|
+
sync,
|
|
1653
|
+
vaultPath,
|
|
1654
|
+
yes,
|
|
1655
|
+
};
|
|
924
1656
|
}
|
|
925
1657
|
|
|
926
1658
|
async function runInit(
|
|
@@ -933,18 +1665,28 @@ async function runInit(
|
|
|
933
1665
|
coreSkillIds,
|
|
934
1666
|
customPath,
|
|
935
1667
|
migrateFullVault,
|
|
1668
|
+
skipInstructions,
|
|
1669
|
+
sync,
|
|
936
1670
|
vaultPath: requestedVaultPath,
|
|
937
1671
|
yes,
|
|
938
1672
|
} = parseInitArgs(args);
|
|
1673
|
+
const guided = shouldUseWizard(args, {
|
|
1674
|
+
interactive: isInteractive(),
|
|
1675
|
+
json: options.isJson,
|
|
1676
|
+
dryRun: options.dryRun,
|
|
1677
|
+
});
|
|
939
1678
|
migrateLegacyPaths();
|
|
940
1679
|
const configPath = resolveConfigPath();
|
|
941
1680
|
let configPlan: ConfigInitPlan | undefined;
|
|
942
1681
|
let vaultPath: string;
|
|
943
1682
|
if (!existsSync(configPath)) {
|
|
944
|
-
const bootstrapVaultPath =
|
|
1683
|
+
const bootstrapVaultPath =
|
|
1684
|
+
requestedVaultPath ??
|
|
945
1685
|
(!options.isJson && isInteractive() ? "~/skills" : undefined);
|
|
946
1686
|
if (!bootstrapVaultPath) {
|
|
947
|
-
throw new Error(
|
|
1687
|
+
throw new Error(
|
|
1688
|
+
`machine config does not exist: ${configPath}; re-run with --vault <path>`,
|
|
1689
|
+
);
|
|
948
1690
|
}
|
|
949
1691
|
configPlan = planConfigInit(configPath, expandHome(bootstrapVaultPath));
|
|
950
1692
|
vaultPath = configPlan.vaultPath;
|
|
@@ -966,12 +1708,51 @@ async function runInit(
|
|
|
966
1708
|
throw new Error(vaultHealth.message);
|
|
967
1709
|
}
|
|
968
1710
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1711
|
+
let selectedClients = requestedClients;
|
|
1712
|
+
if (guided) {
|
|
1713
|
+
const detected = detectInstalledClients({
|
|
1714
|
+
codexHome: process.env.CODEX_HOME
|
|
1715
|
+
? expandHome(process.env.CODEX_HOME)
|
|
1716
|
+
: undefined,
|
|
1717
|
+
});
|
|
1718
|
+
const evidence = new Map(
|
|
1719
|
+
detected.map((item) => [item.client, item.evidence]),
|
|
1720
|
+
);
|
|
1721
|
+
selectedClients = await promptMultiSelect(
|
|
1722
|
+
"Which clients do you use?",
|
|
1723
|
+
SUPPORTED_CLIENT_IDS.map((client) => ({
|
|
1724
|
+
value: client,
|
|
1725
|
+
label: client,
|
|
1726
|
+
detail: evidence.has(client)
|
|
1727
|
+
? `detected: ${evidence.get(client)}`
|
|
1728
|
+
: undefined,
|
|
1729
|
+
selected: evidence.has(client) || requestedClients.includes(client),
|
|
1730
|
+
})),
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
let selectedCoreSkillIds = coreSkillIds;
|
|
1734
|
+
if (guided) {
|
|
1735
|
+
selectedCoreSkillIds = parseCommaList(
|
|
1736
|
+
await promptText(
|
|
1737
|
+
"Core skill IDs to add, comma-separated",
|
|
1738
|
+
coreSkillIds.join(","),
|
|
1739
|
+
),
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
const clientPlan = planClientSurfaces(selectedClients, {
|
|
1744
|
+
codexHome: process.env.CODEX_HOME
|
|
1745
|
+
? expandHome(process.env.CODEX_HOME)
|
|
1746
|
+
: undefined,
|
|
974
1747
|
});
|
|
1748
|
+
const instructionPlan = planInstructionSetup(
|
|
1749
|
+
skipInstructions ? [] : clientPlan.clients.map((client) => client.id),
|
|
1750
|
+
{
|
|
1751
|
+
codexHome: process.env.CODEX_HOME
|
|
1752
|
+
? expandHome(process.env.CODEX_HOME)
|
|
1753
|
+
: undefined,
|
|
1754
|
+
},
|
|
1755
|
+
);
|
|
975
1756
|
const instructionReadiness: Partial<Record<ClientId, ReadinessAxis>> = {};
|
|
976
1757
|
for (const change of instructionPlan.changes) {
|
|
977
1758
|
for (const client of change.clients) {
|
|
@@ -982,28 +1763,54 @@ async function runInit(
|
|
|
982
1763
|
}
|
|
983
1764
|
}
|
|
984
1765
|
for (const manual of instructionPlan.manual) {
|
|
985
|
-
instructionReadiness[manual.client] = {
|
|
1766
|
+
instructionReadiness[manual.client] = {
|
|
1767
|
+
status: "manual",
|
|
1768
|
+
detail: manual.reason,
|
|
1769
|
+
};
|
|
986
1770
|
}
|
|
987
|
-
const builtInNames = new Set([
|
|
1771
|
+
const builtInNames = new Set([
|
|
1772
|
+
"agent-skills",
|
|
1773
|
+
"claude-code",
|
|
1774
|
+
"codex",
|
|
1775
|
+
"custom",
|
|
1776
|
+
"agents",
|
|
1777
|
+
"claude",
|
|
1778
|
+
]);
|
|
988
1779
|
const explicitSurfaceTargets = explicitTargets
|
|
989
1780
|
.filter((name) => builtInNames.has(name))
|
|
990
1781
|
.map((name) =>
|
|
991
1782
|
resolveBuiltInTarget(name, {
|
|
992
|
-
codexHome: process.env.CODEX_HOME
|
|
1783
|
+
codexHome: process.env.CODEX_HOME
|
|
1784
|
+
? expandHome(process.env.CODEX_HOME)
|
|
1785
|
+
: undefined,
|
|
993
1786
|
customPath: customPath ? expandHome(customPath) : undefined,
|
|
994
1787
|
}),
|
|
995
1788
|
);
|
|
996
1789
|
if (customPath && !explicitTargets.includes("custom")) {
|
|
997
|
-
throw new Error("--
|
|
1790
|
+
throw new Error("--dir may only be used with --target custom");
|
|
998
1791
|
}
|
|
999
1792
|
for (const target of explicitSurfaceTargets) {
|
|
1000
1793
|
if (target.warning) console.error(`warning: ${target.warning}`);
|
|
1001
1794
|
}
|
|
1002
1795
|
const targetByPath = new Map(
|
|
1003
|
-
explicitSurfaceTargets.map(
|
|
1796
|
+
explicitSurfaceTargets.map(
|
|
1797
|
+
(target) => [target.path, target.targetName] as const,
|
|
1798
|
+
),
|
|
1004
1799
|
);
|
|
1800
|
+
const existingManifestPath = resolveManifestPath(vaultPath);
|
|
1801
|
+
const existingManifest = existingManifestPath
|
|
1802
|
+
? parseManifest(await Bun.file(existingManifestPath).text())
|
|
1803
|
+
: undefined;
|
|
1005
1804
|
for (const surface of clientPlan.surfaces) {
|
|
1006
|
-
if (!targetByPath.has(surface.path))
|
|
1805
|
+
if (!targetByPath.has(surface.path)) {
|
|
1806
|
+
targetByPath.set(
|
|
1807
|
+
surface.path,
|
|
1808
|
+
existingManifest
|
|
1809
|
+
? (configuredTargetForSurface(existingManifest, surface) ??
|
|
1810
|
+
surface.targetName)
|
|
1811
|
+
: surface.targetName,
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1007
1814
|
}
|
|
1008
1815
|
const candidatePaths = [
|
|
1009
1816
|
...new Set([
|
|
@@ -1014,7 +1821,8 @@ async function runInit(
|
|
|
1014
1821
|
const candidates = detectSurfaces(candidatePaths, vaultPath);
|
|
1015
1822
|
if (!options.isJson) {
|
|
1016
1823
|
for (const candidate of candidates) {
|
|
1017
|
-
const name =
|
|
1824
|
+
const name =
|
|
1825
|
+
targetByPath.get(candidate.path) ?? deriveTargetName(candidate.path);
|
|
1018
1826
|
if (candidate.state === "missing") {
|
|
1019
1827
|
console.log(`${name} (${candidate.path}): not found`);
|
|
1020
1828
|
continue;
|
|
@@ -1024,26 +1832,45 @@ async function runInit(
|
|
|
1024
1832
|
continue;
|
|
1025
1833
|
}
|
|
1026
1834
|
if (candidate.state === "full-vault") {
|
|
1027
|
-
console.log(
|
|
1835
|
+
console.log(
|
|
1836
|
+
`${name} (${candidate.path}): full-vault -> ${candidate.canonicalPath}`,
|
|
1837
|
+
);
|
|
1028
1838
|
continue;
|
|
1029
1839
|
}
|
|
1030
1840
|
if (candidate.state === "external-symlink") {
|
|
1031
|
-
console.log(
|
|
1841
|
+
console.log(
|
|
1842
|
+
`${name} (${candidate.path}): external symlink -> ${candidate.canonicalPath}`,
|
|
1843
|
+
);
|
|
1032
1844
|
continue;
|
|
1033
1845
|
}
|
|
1034
1846
|
if (candidate.state === "unsupported") {
|
|
1035
|
-
console.log(
|
|
1847
|
+
console.log(
|
|
1848
|
+
`${name} (${candidate.path}): unsupported filesystem entry`,
|
|
1849
|
+
);
|
|
1036
1850
|
continue;
|
|
1037
1851
|
}
|
|
1038
1852
|
const kind = "real dir";
|
|
1039
|
-
const marked = candidate.alreadyMarked
|
|
1040
|
-
|
|
1853
|
+
const marked = candidate.alreadyMarked
|
|
1854
|
+
? ", already skillmux-managed"
|
|
1855
|
+
: "";
|
|
1856
|
+
console.log(
|
|
1857
|
+
`${name} (${candidate.path}): ${kind}, ${candidate.skillCount} skills${marked}`,
|
|
1858
|
+
);
|
|
1041
1859
|
}
|
|
1042
|
-
for (const readiness of assessClientReadiness(
|
|
1860
|
+
for (const readiness of assessClientReadiness(
|
|
1861
|
+
clientPlan,
|
|
1862
|
+
instructionReadiness,
|
|
1863
|
+
)) {
|
|
1043
1864
|
console.log(`\n${readiness.client} readiness:`);
|
|
1044
|
-
console.log(
|
|
1045
|
-
|
|
1046
|
-
|
|
1865
|
+
console.log(
|
|
1866
|
+
` skill surface: ${readiness.skillSurface.status} — ${readiness.skillSurface.detail}`,
|
|
1867
|
+
);
|
|
1868
|
+
console.log(
|
|
1869
|
+
` MCP registration: ${readiness.mcpRegistration.status} — ${readiness.mcpRegistration.detail}`,
|
|
1870
|
+
);
|
|
1871
|
+
console.log(
|
|
1872
|
+
` instructions: ${readiness.instructionSetup.status} — ${readiness.instructionSetup.detail}`,
|
|
1873
|
+
);
|
|
1047
1874
|
}
|
|
1048
1875
|
for (const change of instructionPlan.changes) {
|
|
1049
1876
|
console.log(
|
|
@@ -1068,26 +1895,34 @@ async function runInit(
|
|
|
1068
1895
|
const hasChanges = !(
|
|
1069
1896
|
requestedTargets.length === 0 &&
|
|
1070
1897
|
!hasInstructionWrites &&
|
|
1071
|
-
|
|
1898
|
+
selectedCoreSkillIds.length === 0 &&
|
|
1072
1899
|
!hasConfigWrite
|
|
1073
1900
|
);
|
|
1074
1901
|
|
|
1075
1902
|
const byName = new Map(
|
|
1076
1903
|
candidates
|
|
1077
|
-
.filter(
|
|
1078
|
-
candidate
|
|
1079
|
-
|
|
1904
|
+
.filter(
|
|
1905
|
+
(candidate) =>
|
|
1906
|
+
candidate.deliveryMode === "managed-pins" ||
|
|
1907
|
+
(migrateFullVault && candidate.state === "full-vault"),
|
|
1080
1908
|
)
|
|
1081
|
-
.map(
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1909
|
+
.map(
|
|
1910
|
+
(candidate) =>
|
|
1911
|
+
[
|
|
1912
|
+
targetByPath.get(candidate.path) ??
|
|
1913
|
+
deriveTargetName(candidate.path),
|
|
1914
|
+
candidate,
|
|
1915
|
+
] as const,
|
|
1916
|
+
),
|
|
1085
1917
|
);
|
|
1086
1918
|
const allCandidatesByName = new Map(
|
|
1087
|
-
candidates.map(
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1919
|
+
candidates.map(
|
|
1920
|
+
(candidate) =>
|
|
1921
|
+
[
|
|
1922
|
+
targetByPath.get(candidate.path) ?? deriveTargetName(candidate.path),
|
|
1923
|
+
candidate,
|
|
1924
|
+
] as const,
|
|
1925
|
+
),
|
|
1091
1926
|
);
|
|
1092
1927
|
for (const name of requestedTargets) {
|
|
1093
1928
|
if (!byName.has(name)) {
|
|
@@ -1096,7 +1931,9 @@ async function runInit(
|
|
|
1096
1931
|
`target "${name}" is a full-vault surface; re-run with --migrate-full-vault to convert it to managed pins`,
|
|
1097
1932
|
);
|
|
1098
1933
|
}
|
|
1099
|
-
throw new Error(
|
|
1934
|
+
throw new Error(
|
|
1935
|
+
`unknown --target "${name}": not among detected surfaces`,
|
|
1936
|
+
);
|
|
1100
1937
|
}
|
|
1101
1938
|
}
|
|
1102
1939
|
|
|
@@ -1108,7 +1945,11 @@ async function runInit(
|
|
|
1108
1945
|
...(candidate.state === "full-vault" ? { migrateFullVault: true } : {}),
|
|
1109
1946
|
};
|
|
1110
1947
|
});
|
|
1111
|
-
const plannedManifest = planInitManifest(
|
|
1948
|
+
const plannedManifest = planInitManifest(
|
|
1949
|
+
vaultPath,
|
|
1950
|
+
confirmedTargets,
|
|
1951
|
+
selectedCoreSkillIds,
|
|
1952
|
+
);
|
|
1112
1953
|
const serializedPlan = {
|
|
1113
1954
|
vault_path: vaultPath,
|
|
1114
1955
|
config: configPlan
|
|
@@ -1126,44 +1967,50 @@ async function runInit(
|
|
|
1126
1967
|
};
|
|
1127
1968
|
if (!hasChanges) {
|
|
1128
1969
|
if (options.isJson) {
|
|
1129
|
-
console.log(
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1970
|
+
console.log(
|
|
1971
|
+
JSON.stringify({
|
|
1972
|
+
schema_version: 1,
|
|
1973
|
+
ok: true,
|
|
1974
|
+
command: "init",
|
|
1975
|
+
phase: "plan",
|
|
1976
|
+
dry_run: options.dryRun,
|
|
1977
|
+
applied: false,
|
|
1978
|
+
plan: serializedPlan,
|
|
1979
|
+
}),
|
|
1980
|
+
);
|
|
1138
1981
|
} else {
|
|
1139
1982
|
console.log("\nno managed-pins surface selected — nothing written.");
|
|
1140
1983
|
}
|
|
1141
1984
|
return;
|
|
1142
1985
|
}
|
|
1143
1986
|
if (!options.isJson) {
|
|
1144
|
-
for (const target of confirmedTargets.filter(
|
|
1987
|
+
for (const target of confirmedTargets.filter(
|
|
1988
|
+
(target) => target.migrateFullVault,
|
|
1989
|
+
)) {
|
|
1145
1990
|
console.log(
|
|
1146
1991
|
`full-vault migration ${target.name}: ${vaultHealth.skillCount} visible skills -> ` +
|
|
1147
|
-
|
|
1992
|
+
`${plannedManifest.core.skills.length} core ${plannedManifest.core.skills.length === 1 ? "skill" : "skills"} after sync`,
|
|
1148
1993
|
);
|
|
1149
1994
|
}
|
|
1150
1995
|
}
|
|
1151
1996
|
if (options.dryRun) {
|
|
1152
1997
|
if (options.isJson) {
|
|
1153
|
-
console.log(
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1998
|
+
console.log(
|
|
1999
|
+
JSON.stringify({
|
|
2000
|
+
schema_version: 1,
|
|
2001
|
+
ok: true,
|
|
2002
|
+
command: "init",
|
|
2003
|
+
phase: "plan",
|
|
2004
|
+
dry_run: true,
|
|
2005
|
+
applied: false,
|
|
2006
|
+
plan: serializedPlan,
|
|
2007
|
+
}),
|
|
2008
|
+
);
|
|
1162
2009
|
} else {
|
|
1163
2010
|
console.log(
|
|
1164
2011
|
`\ndry-run: ${confirmedTargets.length} target(s), ` +
|
|
1165
|
-
|
|
1166
|
-
|
|
2012
|
+
`${instructionPlan.changes.filter((change) => change.status !== "unchanged").length} instruction file(s), ` +
|
|
2013
|
+
`core: ${plannedManifest.core.skills.join(", ") || "(unchanged)"}`,
|
|
1167
2014
|
);
|
|
1168
2015
|
}
|
|
1169
2016
|
return;
|
|
@@ -1171,19 +2018,44 @@ async function runInit(
|
|
|
1171
2018
|
|
|
1172
2019
|
if (!yes) {
|
|
1173
2020
|
if (!options.isJson && isInteractive()) {
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
.map((
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
2021
|
+
if (guided) {
|
|
2022
|
+
console.log("\nReview");
|
|
2023
|
+
console.log(` clients: ${selectedClients.join(", ") || "(none)"}`);
|
|
2024
|
+
console.log(
|
|
2025
|
+
` targets: ${confirmedTargets.map((target) => `${target.name} -> ${target.dir}`).join(", ") || "(none)"}`,
|
|
2026
|
+
);
|
|
2027
|
+
console.log(
|
|
2028
|
+
` instructions: ${instructionPlan.changes.filter((change) => change.status !== "unchanged").length} file(s)`,
|
|
2029
|
+
);
|
|
2030
|
+
console.log(
|
|
2031
|
+
` core: ${plannedManifest.core.skills.join(", ") || "(none)"}`,
|
|
2032
|
+
);
|
|
2033
|
+
console.log(` sync: ${sync ? "yes" : "no"}`);
|
|
2034
|
+
if (!(await confirmAction("Apply this setup plan?"))) {
|
|
2035
|
+
console.log("init cancelled");
|
|
1185
2036
|
return;
|
|
1186
2037
|
}
|
|
2038
|
+
} else {
|
|
2039
|
+
const prompts = [
|
|
2040
|
+
...confirmedTargets.map(
|
|
2041
|
+
(target) => `Adopt ${target.name} at ${target.dir}?`,
|
|
2042
|
+
),
|
|
2043
|
+
...instructionPlan.changes
|
|
2044
|
+
.filter((change) => change.status !== "unchanged")
|
|
2045
|
+
.map(
|
|
2046
|
+
(change) => `${change.status} instruction file ${change.path}?`,
|
|
2047
|
+
),
|
|
2048
|
+
...(hasConfigWrite ? [`Create machine config ${configPath}?`] : []),
|
|
2049
|
+
...(selectedCoreSkillIds.length > 0
|
|
2050
|
+
? [`Pin core skills: ${selectedCoreSkillIds.join(", ")}?`]
|
|
2051
|
+
: []),
|
|
2052
|
+
];
|
|
2053
|
+
for (const prompt of prompts) {
|
|
2054
|
+
if (!(await confirmAction(prompt))) {
|
|
2055
|
+
console.log("init cancelled; nothing written");
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
1187
2059
|
}
|
|
1188
2060
|
} else {
|
|
1189
2061
|
throw new Error(
|
|
@@ -1214,7 +2086,7 @@ async function runInit(
|
|
|
1214
2086
|
if (configCreated && configPlan) rollbackConfigInit(configPlan);
|
|
1215
2087
|
};
|
|
1216
2088
|
|
|
1217
|
-
if (confirmedTargets.length === 0 &&
|
|
2089
|
+
if (confirmedTargets.length === 0 && selectedCoreSkillIds.length === 0) {
|
|
1218
2090
|
applyAdditional();
|
|
1219
2091
|
} else {
|
|
1220
2092
|
applyInit(
|
|
@@ -1226,46 +2098,58 @@ async function runInit(
|
|
|
1226
2098
|
rollback: rollbackAdditional,
|
|
1227
2099
|
}
|
|
1228
2100
|
: undefined,
|
|
1229
|
-
|
|
2101
|
+
selectedCoreSkillIds,
|
|
1230
2102
|
);
|
|
1231
2103
|
}
|
|
1232
2104
|
|
|
1233
2105
|
if (options.isJson) {
|
|
1234
|
-
console.log(
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
2106
|
+
console.log(
|
|
2107
|
+
JSON.stringify({
|
|
2108
|
+
schema_version: 1,
|
|
2109
|
+
ok: true,
|
|
2110
|
+
command: "init",
|
|
2111
|
+
phase: "result",
|
|
2112
|
+
dry_run: false,
|
|
2113
|
+
applied: true,
|
|
2114
|
+
plan: serializedPlan,
|
|
2115
|
+
result: {
|
|
2116
|
+
config_created: configCreated,
|
|
2117
|
+
targets_adopted: confirmedTargets.map((target) => target.name),
|
|
2118
|
+
instructions_changed: instructionPlan.changes
|
|
2119
|
+
.filter((change) => change.status !== "unchanged")
|
|
2120
|
+
.map((change) => change.path),
|
|
2121
|
+
core: plannedManifest.core.skills,
|
|
2122
|
+
},
|
|
2123
|
+
}),
|
|
2124
|
+
);
|
|
1251
2125
|
return;
|
|
1252
2126
|
}
|
|
1253
2127
|
if (configCreated) console.log(`created ${configPath}`);
|
|
1254
2128
|
if (confirmedTargets.length > 0) {
|
|
1255
|
-
console.log(
|
|
1256
|
-
|
|
2129
|
+
console.log(
|
|
2130
|
+
`\nwrote ${join(vaultPath, "skillmux.toml")}, adopted: ${confirmedTargets.map((t) => t.name).join(", ")}`,
|
|
2131
|
+
);
|
|
2132
|
+
} else if (selectedCoreSkillIds.length > 0) {
|
|
1257
2133
|
console.log(`\nwrote ${join(vaultPath, "skillmux.toml")}`);
|
|
1258
2134
|
}
|
|
1259
2135
|
if (plannedManifest.core.skills.length === 0 && confirmedTargets.length > 0) {
|
|
1260
|
-
console.log("next: skillmux
|
|
2136
|
+
console.log("next: skillmux core pin <skill_id> --yes");
|
|
1261
2137
|
}
|
|
1262
2138
|
if (confirmedTargets.length > 0) console.log("next: skillmux sync");
|
|
1263
|
-
if (
|
|
2139
|
+
if (
|
|
2140
|
+
selectedClients.length === 0 ||
|
|
2141
|
+
selectedClients.includes("skillmux-mcp")
|
|
2142
|
+
) {
|
|
1264
2143
|
console.log(`\n${printLastMile()}`);
|
|
1265
2144
|
}
|
|
2145
|
+
if (guided && sync && confirmedTargets.length > 0) await runSync([]);
|
|
1266
2146
|
}
|
|
1267
2147
|
|
|
1268
|
-
function parseReportArgs(args: string[]): {
|
|
2148
|
+
function parseReportArgs(args: string[]): {
|
|
2149
|
+
server?: string;
|
|
2150
|
+
db?: string;
|
|
2151
|
+
since?: string;
|
|
2152
|
+
} {
|
|
1269
2153
|
let server: string | undefined;
|
|
1270
2154
|
let db: string | undefined;
|
|
1271
2155
|
let since: string | undefined;
|
|
@@ -1284,6 +2168,8 @@ function parseReportArgs(args: string[]): { server?: string; db?: string; since?
|
|
|
1284
2168
|
if (!value) throw new Error("--since requires a window");
|
|
1285
2169
|
since = value;
|
|
1286
2170
|
i++;
|
|
2171
|
+
} else if (option === "--json") {
|
|
2172
|
+
// handled globally by main()'s isJson flag; recognized here so it isn't rejected
|
|
1287
2173
|
} else {
|
|
1288
2174
|
throw new Error(`unknown report option: ${option}`);
|
|
1289
2175
|
}
|
|
@@ -1292,24 +2178,45 @@ function parseReportArgs(args: string[]): { server?: string; db?: string; since?
|
|
|
1292
2178
|
return { server, db, since };
|
|
1293
2179
|
}
|
|
1294
2180
|
|
|
1295
|
-
async function runReport(
|
|
2181
|
+
async function runReport(
|
|
2182
|
+
args: string[],
|
|
2183
|
+
options: { isJson: boolean },
|
|
2184
|
+
): Promise<void> {
|
|
1296
2185
|
const { server, db: dbPath, since } = parseReportArgs(args);
|
|
1297
|
-
if (!since)
|
|
2186
|
+
if (!since)
|
|
2187
|
+
throw new Error(
|
|
2188
|
+
"usage: skillmux report [--server <url> | --db <path>] --since <window> [--json]",
|
|
2189
|
+
);
|
|
1298
2190
|
|
|
1299
2191
|
if (server) {
|
|
1300
2192
|
const url = `${server.replace(/\/$/, "")}/stats?since=${encodeURIComponent(since)}`;
|
|
1301
2193
|
const res = await fetch(url);
|
|
1302
|
-
if (!res.ok)
|
|
1303
|
-
|
|
2194
|
+
if (!res.ok)
|
|
2195
|
+
throw new Error(
|
|
2196
|
+
`skillmux report --server failed: ${res.status} ${await res.text()}`,
|
|
2197
|
+
);
|
|
2198
|
+
const stats = (await res.json()) as StatsResponse;
|
|
2199
|
+
emitSuccess({ isJson: options.isJson }, stats, () =>
|
|
2200
|
+
console.log(renderStatsText(stats)),
|
|
2201
|
+
);
|
|
1304
2202
|
return;
|
|
1305
2203
|
}
|
|
1306
2204
|
|
|
1307
|
-
const db = dbPath
|
|
1308
|
-
|
|
2205
|
+
const db = dbPath
|
|
2206
|
+
? new Database(dbPath, { readonly: true })
|
|
2207
|
+
: openIndex(expandHome((await loadConfig()).state_dir));
|
|
2208
|
+
const stats = getStats(db, since);
|
|
2209
|
+
emitSuccess({ isJson: options.isJson }, stats, () =>
|
|
2210
|
+
console.log(renderStatsText(stats)),
|
|
2211
|
+
);
|
|
1309
2212
|
db.close();
|
|
1310
2213
|
}
|
|
1311
2214
|
|
|
1312
|
-
function parseScanArgs(args: string[]): {
|
|
2215
|
+
function parseScanArgs(args: string[]): {
|
|
2216
|
+
path?: string;
|
|
2217
|
+
format: "text" | "json";
|
|
2218
|
+
failOn?: ScanSeverity;
|
|
2219
|
+
} {
|
|
1313
2220
|
let path: string | undefined;
|
|
1314
2221
|
let format: "text" | "json" = "text";
|
|
1315
2222
|
let failOn: ScanSeverity | undefined;
|
|
@@ -1317,7 +2224,8 @@ function parseScanArgs(args: string[]): { path?: string; format: "text" | "json"
|
|
|
1317
2224
|
const option = args[i];
|
|
1318
2225
|
if (option === "--format") {
|
|
1319
2226
|
const value = args[++i];
|
|
1320
|
-
if (value !== "text" && value !== "json")
|
|
2227
|
+
if (value !== "text" && value !== "json")
|
|
2228
|
+
throw new Error("--format must be text or json");
|
|
1321
2229
|
format = value;
|
|
1322
2230
|
} else if (option === "--fail-on") {
|
|
1323
2231
|
const value = args[++i];
|
|
@@ -1325,6 +2233,8 @@ function parseScanArgs(args: string[]): { path?: string; format: "text" | "json"
|
|
|
1325
2233
|
throw new Error("--fail-on must be low, medium, or high");
|
|
1326
2234
|
}
|
|
1327
2235
|
failOn = value;
|
|
2236
|
+
} else if (option === "--json") {
|
|
2237
|
+
// handled globally by main()'s isJson flag; recognized here so it isn't rejected
|
|
1328
2238
|
} else if (option?.startsWith("--")) {
|
|
1329
2239
|
throw new Error(`unknown scan option: ${option}`);
|
|
1330
2240
|
} else if (path !== undefined) {
|
|
@@ -1336,11 +2246,20 @@ function parseScanArgs(args: string[]): { path?: string; format: "text" | "json"
|
|
|
1336
2246
|
return { path, format, failOn };
|
|
1337
2247
|
}
|
|
1338
2248
|
|
|
1339
|
-
async function runScan(
|
|
2249
|
+
async function runScan(
|
|
2250
|
+
args: string[],
|
|
2251
|
+
options: { isJson: boolean },
|
|
2252
|
+
): Promise<void> {
|
|
1340
2253
|
const { path, format, failOn } = parseScanArgs(args);
|
|
1341
|
-
const rootPath = path
|
|
2254
|
+
const rootPath = path
|
|
2255
|
+
? expandHome(path)
|
|
2256
|
+
: expandHome((await loadConfig()).vault_path);
|
|
1342
2257
|
const result = await scanPath(rootPath);
|
|
1343
|
-
|
|
2258
|
+
emitSuccess({ isJson: options.isJson }, result, () => {
|
|
2259
|
+
console.log(
|
|
2260
|
+
format === "json" ? renderScanJson(result) : renderScanText(result),
|
|
2261
|
+
);
|
|
2262
|
+
});
|
|
1344
2263
|
process.exitCode = scanExitCode(result.findings, failOn);
|
|
1345
2264
|
}
|
|
1346
2265
|
|
|
@@ -1364,6 +2283,8 @@ function parseInstallArgs(args: string[]): {
|
|
|
1364
2283
|
throw new Error("--fail-on must be low, medium, or high");
|
|
1365
2284
|
}
|
|
1366
2285
|
failOn = value;
|
|
2286
|
+
} else if (option === "--json") {
|
|
2287
|
+
// handled globally by main()'s isJson flag; recognized here so it isn't rejected
|
|
1367
2288
|
} else if (option?.startsWith("--")) {
|
|
1368
2289
|
throw new Error(`unknown install option: ${option}`);
|
|
1369
2290
|
} else if (repo !== undefined) {
|
|
@@ -1375,39 +2296,73 @@ function parseInstallArgs(args: string[]): {
|
|
|
1375
2296
|
return { repo, force, dryRun, failOn };
|
|
1376
2297
|
}
|
|
1377
2298
|
|
|
1378
|
-
async function runInstall(
|
|
2299
|
+
async function runInstall(
|
|
2300
|
+
args: string[],
|
|
2301
|
+
options: { isJson: boolean },
|
|
2302
|
+
): Promise<void> {
|
|
1379
2303
|
const { repo, force, dryRun, failOn } = parseInstallArgs(args);
|
|
1380
2304
|
if (!repo) {
|
|
1381
|
-
throw new Error(
|
|
2305
|
+
throw new Error(
|
|
2306
|
+
"usage: skillmux install <repo>[/path] [--force] [--fail-on low|medium|high] [--dry-run] [--json]",
|
|
2307
|
+
);
|
|
1382
2308
|
}
|
|
1383
2309
|
|
|
1384
2310
|
const source = resolveRepoSource(repo);
|
|
1385
2311
|
const cloneDir = await cloneToTemp(source.url);
|
|
1386
2312
|
try {
|
|
1387
|
-
const resolved = resolveSkillDir(
|
|
1388
|
-
|
|
1389
|
-
|
|
2313
|
+
const resolved = resolveSkillDir(
|
|
2314
|
+
cloneDir,
|
|
2315
|
+
deriveRepoName(source.url),
|
|
2316
|
+
source.skillPath,
|
|
2317
|
+
);
|
|
2318
|
+
const { findings } = await validateSkillCandidate(
|
|
2319
|
+
resolved.skillId,
|
|
2320
|
+
resolved.dir,
|
|
2321
|
+
);
|
|
2322
|
+
if (!options.isJson) console.log(renderScanText({ scanned: 1, findings }));
|
|
1390
2323
|
|
|
1391
2324
|
if (scanExitCode(findings, failOn) !== 0) {
|
|
1392
2325
|
process.exitCode = 1;
|
|
1393
|
-
console.error(
|
|
2326
|
+
console.error(
|
|
2327
|
+
`aborting install: a finding met the --fail-on ${failOn} threshold`,
|
|
2328
|
+
);
|
|
1394
2329
|
return;
|
|
1395
2330
|
}
|
|
1396
2331
|
|
|
1397
2332
|
const vaultPath = expandHome((await loadConfig()).vault_path);
|
|
1398
2333
|
if (dryRun) {
|
|
1399
|
-
|
|
2334
|
+
const plannedPath = join(vaultPath, resolved.skillId);
|
|
2335
|
+
emitSuccess(
|
|
2336
|
+
{ isJson: options.isJson },
|
|
2337
|
+
{ skill_id: resolved.skillId, would_install_at: plannedPath },
|
|
2338
|
+
() =>
|
|
2339
|
+
console.log(
|
|
2340
|
+
`dry-run: would install "${resolved.skillId}" into ${plannedPath}`,
|
|
2341
|
+
),
|
|
2342
|
+
);
|
|
1400
2343
|
return;
|
|
1401
2344
|
}
|
|
1402
2345
|
|
|
1403
|
-
const targetDir = installIntoVault(
|
|
1404
|
-
|
|
2346
|
+
const targetDir = installIntoVault(
|
|
2347
|
+
vaultPath,
|
|
2348
|
+
resolved.skillId,
|
|
2349
|
+
resolved.dir,
|
|
2350
|
+
force,
|
|
2351
|
+
);
|
|
2352
|
+
emitSuccess(
|
|
2353
|
+
{ isJson: options.isJson },
|
|
2354
|
+
{ skill_id: resolved.skillId, installed_at: targetDir },
|
|
2355
|
+
() => console.log(`installed "${resolved.skillId}" into ${targetDir}`),
|
|
2356
|
+
);
|
|
1405
2357
|
} finally {
|
|
1406
2358
|
rmSync(cloneDir, { recursive: true, force: true });
|
|
1407
2359
|
}
|
|
1408
2360
|
}
|
|
1409
2361
|
|
|
1410
|
-
function parseCalibrateGenerateDatasetArgs(args: string[]): {
|
|
2362
|
+
function parseCalibrateGenerateDatasetArgs(args: string[]): {
|
|
2363
|
+
vault?: string;
|
|
2364
|
+
out?: string;
|
|
2365
|
+
} {
|
|
1411
2366
|
let vault: string | undefined;
|
|
1412
2367
|
let out: string | undefined;
|
|
1413
2368
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -1428,7 +2383,8 @@ function parseCalibrateGenerateDatasetArgs(args: string[]): { vault?: string; ou
|
|
|
1428
2383
|
}
|
|
1429
2384
|
|
|
1430
2385
|
async function runCalibrateGenerateDataset(args: string[]): Promise<void> {
|
|
1431
|
-
const { vault: vaultArg, out: outArg } =
|
|
2386
|
+
const { vault: vaultArg, out: outArg } =
|
|
2387
|
+
parseCalibrateGenerateDatasetArgs(args);
|
|
1432
2388
|
const config = await loadConfig();
|
|
1433
2389
|
const vaultPath = expandHome(vaultArg ?? config.vault_path);
|
|
1434
2390
|
const outPath = expandHome(outArg ?? join(config.state_dir, "queries.json"));
|
|
@@ -1439,7 +2395,9 @@ async function runCalibrateGenerateDataset(args: string[]): Promise<void> {
|
|
|
1439
2395
|
const parentDir = join(outPath, "..");
|
|
1440
2396
|
mkdirSync(parentDir, { recursive: true });
|
|
1441
2397
|
await Bun.write(outPath, JSON.stringify(dataset, null, 2) + "\n");
|
|
1442
|
-
console.log(
|
|
2398
|
+
console.log(
|
|
2399
|
+
`generated synthetic dataset with ${dataset.length} cases at ${outPath}`,
|
|
2400
|
+
);
|
|
1443
2401
|
}
|
|
1444
2402
|
|
|
1445
2403
|
if (import.meta.main) {
|