@klhapp/skillmux 0.6.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 +42 -0
- package/README.md +9 -3
- package/docs/configuration.md +13 -10
- package/package.json +1 -1
- package/src/adapters.ts +11 -5
- package/src/cli.ts +866 -432
- package/src/completions.ts +69 -55
- package/src/config-watcher.ts +63 -2
- package/src/doctor.ts +19 -1
- package/src/output.ts +21 -23
- package/src/server.ts +228 -63
package/src/cli.ts
CHANGED
|
@@ -7,7 +7,12 @@ 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";
|
|
@@ -57,9 +62,20 @@ import {
|
|
|
57
62
|
} from "./manifest";
|
|
58
63
|
import { downloadLocalModels } from "./models";
|
|
59
64
|
import { resolveProjectDirectory, suggestProjectName } from "./project-setup";
|
|
60
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
parseCommaList,
|
|
67
|
+
promptMultiSelect,
|
|
68
|
+
promptText,
|
|
69
|
+
shouldUseWizard,
|
|
70
|
+
} from "./prompts";
|
|
61
71
|
import { backfillEmbeddings, configure, rebuildIndex } from "./router-core";
|
|
62
|
-
import {
|
|
72
|
+
import {
|
|
73
|
+
renderScanJson,
|
|
74
|
+
renderScanText,
|
|
75
|
+
scanExitCode,
|
|
76
|
+
scanPath,
|
|
77
|
+
type ScanSeverity,
|
|
78
|
+
} from "./scan";
|
|
63
79
|
import {
|
|
64
80
|
applyConfigInit,
|
|
65
81
|
inspectVault,
|
|
@@ -87,7 +103,15 @@ import {
|
|
|
87
103
|
type ResolvedTarget,
|
|
88
104
|
} from "./context";
|
|
89
105
|
import { createTargetAdapter, type TargetAdapter } from "./adapters";
|
|
90
|
-
import {
|
|
106
|
+
import {
|
|
107
|
+
emitSuccess,
|
|
108
|
+
formatJsonEnvelope,
|
|
109
|
+
isInteractive,
|
|
110
|
+
mapExitCode,
|
|
111
|
+
renderTable,
|
|
112
|
+
renderTargetBanner,
|
|
113
|
+
suggestCorrection,
|
|
114
|
+
} from "./output";
|
|
91
115
|
import { generateCompletions, type ShellType } from "./completions";
|
|
92
116
|
|
|
93
117
|
const KNOWN_COMMANDS = [
|
|
@@ -101,14 +125,14 @@ const KNOWN_COMMANDS = [
|
|
|
101
125
|
"init",
|
|
102
126
|
"project",
|
|
103
127
|
"target",
|
|
128
|
+
"core",
|
|
104
129
|
"report",
|
|
105
130
|
"scan",
|
|
106
131
|
"install",
|
|
107
132
|
"eval",
|
|
108
133
|
"doctor",
|
|
109
134
|
"models",
|
|
110
|
-
"
|
|
111
|
-
"manifest",
|
|
135
|
+
"skill",
|
|
112
136
|
"local-vault",
|
|
113
137
|
];
|
|
114
138
|
|
|
@@ -143,9 +167,17 @@ async function main() {
|
|
|
143
167
|
|
|
144
168
|
// Only resolve target if command is target-aware or context/config/calibrate
|
|
145
169
|
const isLocalConfigInit = command === "config" && rawArgv[1] === "init";
|
|
146
|
-
if (
|
|
170
|
+
if (
|
|
171
|
+
(["context", "config", "calibrate"].includes(command) &&
|
|
172
|
+
!isLocalConfigInit) ||
|
|
173
|
+
flagContext ||
|
|
174
|
+
flagServer
|
|
175
|
+
) {
|
|
147
176
|
try {
|
|
148
|
-
resolvedTarget = await resolveTarget({
|
|
177
|
+
resolvedTarget = await resolveTarget({
|
|
178
|
+
context: flagContext,
|
|
179
|
+
server: flagServer,
|
|
180
|
+
});
|
|
149
181
|
} catch (err: any) {
|
|
150
182
|
handleError(err, { target: resolvedTarget, isJson, isVerbose });
|
|
151
183
|
return;
|
|
@@ -159,13 +191,23 @@ async function main() {
|
|
|
159
191
|
try {
|
|
160
192
|
switch (command) {
|
|
161
193
|
case "context":
|
|
162
|
-
await handleContextCommand(subCommand, commandArgs, {
|
|
194
|
+
await handleContextCommand(subCommand, commandArgs, {
|
|
195
|
+
target: resolvedTarget,
|
|
196
|
+
isJson,
|
|
197
|
+
});
|
|
163
198
|
break;
|
|
164
199
|
case "config":
|
|
165
|
-
await handleConfigCommand(adapter, subCommand, commandArgs, {
|
|
200
|
+
await handleConfigCommand(adapter, subCommand, commandArgs, {
|
|
201
|
+
target: resolvedTarget,
|
|
202
|
+
isJson,
|
|
203
|
+
dryRun: isDryRun,
|
|
204
|
+
});
|
|
166
205
|
break;
|
|
167
206
|
case "calibrate":
|
|
168
|
-
await handleCalibrateCommand(adapter, subCommand, rawArgv.slice(1), {
|
|
207
|
+
await handleCalibrateCommand(adapter, subCommand, rawArgv.slice(1), {
|
|
208
|
+
target: resolvedTarget,
|
|
209
|
+
isJson,
|
|
210
|
+
});
|
|
169
211
|
break;
|
|
170
212
|
case "completions":
|
|
171
213
|
await handleCompletionsCommand(subCommand);
|
|
@@ -207,40 +249,50 @@ async function main() {
|
|
|
207
249
|
case "target":
|
|
208
250
|
await runTarget(subCommand, commandArgs, { isJson, dryRun: isDryRun });
|
|
209
251
|
break;
|
|
252
|
+
case "core":
|
|
253
|
+
await runCore(subCommand, commandArgs, { isJson, dryRun: isDryRun });
|
|
254
|
+
break;
|
|
210
255
|
case "report":
|
|
211
|
-
await runReport(rawArgv.slice(1));
|
|
256
|
+
await runReport(rawArgv.slice(1), { isJson });
|
|
212
257
|
break;
|
|
213
258
|
case "scan":
|
|
214
|
-
await runScan(rawArgv.slice(1));
|
|
259
|
+
await runScan(rawArgv.slice(1), { isJson });
|
|
215
260
|
break;
|
|
216
261
|
case "install":
|
|
217
|
-
await runInstall(rawArgv.slice(1));
|
|
262
|
+
await runInstall(rawArgv.slice(1), { isJson });
|
|
218
263
|
break;
|
|
219
264
|
case "eval":
|
|
220
|
-
await runEval();
|
|
265
|
+
await runEval({ isJson });
|
|
221
266
|
break;
|
|
222
267
|
case "doctor":
|
|
223
|
-
await runDoctor();
|
|
268
|
+
await runDoctor({ isJson });
|
|
224
269
|
break;
|
|
225
270
|
case "which":
|
|
226
|
-
|
|
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);
|
|
227
276
|
break;
|
|
228
277
|
case "manifest":
|
|
229
|
-
|
|
230
|
-
|
|
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
|
+
);
|
|
231
281
|
case "local-vault":
|
|
232
|
-
if (subCommand !== "init")
|
|
233
|
-
|
|
282
|
+
if (subCommand !== "init")
|
|
283
|
+
throw new Error("usage: skillmux local-vault init <path>");
|
|
284
|
+
await runLocalVaultInit(commandArgs, { isJson, dryRun: isDryRun });
|
|
234
285
|
break;
|
|
235
286
|
case "models":
|
|
236
|
-
if (subCommand !== "download")
|
|
237
|
-
|
|
287
|
+
if (subCommand !== "download")
|
|
288
|
+
throw new Error("usage: skillmux models download");
|
|
289
|
+
await runModelDownload({ isJson });
|
|
238
290
|
break;
|
|
239
291
|
default: {
|
|
240
292
|
const suggestion = suggestCorrection(command, KNOWN_COMMANDS);
|
|
241
293
|
const msg = suggestion
|
|
242
294
|
? `Unknown command "${command}". Did you mean "${suggestion}"?`
|
|
243
|
-
: `usage: skillmux <serve|index|sync|init|project|target|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>`;
|
|
244
296
|
throw new Error(msg);
|
|
245
297
|
}
|
|
246
298
|
}
|
|
@@ -252,13 +304,11 @@ async function main() {
|
|
|
252
304
|
async function handleContextCommand(
|
|
253
305
|
sub: string,
|
|
254
306
|
args: string[],
|
|
255
|
-
ctx: { target: ResolvedTarget; isJson: boolean }
|
|
307
|
+
ctx: { target: ResolvedTarget; isJson: boolean },
|
|
256
308
|
) {
|
|
257
309
|
if (sub === "list") {
|
|
258
310
|
const contexts = await listContexts();
|
|
259
|
-
|
|
260
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: contexts })));
|
|
261
|
-
} else {
|
|
311
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, contexts, () => {
|
|
262
312
|
renderTargetBanner(ctx.target);
|
|
263
313
|
renderTable(
|
|
264
314
|
[
|
|
@@ -267,20 +317,22 @@ async function handleContextCommand(
|
|
|
267
317
|
{ key: "token_env", header: "TOKEN_ENV" },
|
|
268
318
|
{ key: "isDefault", header: "DEFAULT" },
|
|
269
319
|
],
|
|
270
|
-
contexts.map((c) => ({
|
|
320
|
+
contexts.map((c) => ({
|
|
321
|
+
...c,
|
|
322
|
+
token_env: c.token_env ?? "-",
|
|
323
|
+
isDefault: c.isDefault ? "*" : "",
|
|
324
|
+
})),
|
|
271
325
|
);
|
|
272
|
-
}
|
|
326
|
+
});
|
|
273
327
|
return;
|
|
274
328
|
}
|
|
275
329
|
|
|
276
330
|
if (sub === "current") {
|
|
277
331
|
const current = await getCurrentContext();
|
|
278
|
-
|
|
279
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: current })));
|
|
280
|
-
} else {
|
|
332
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, current, () => {
|
|
281
333
|
renderTargetBanner(ctx.target);
|
|
282
334
|
console.log(`Current context: ${current.name} (${current.server})`);
|
|
283
|
-
}
|
|
335
|
+
});
|
|
284
336
|
return;
|
|
285
337
|
}
|
|
286
338
|
|
|
@@ -293,14 +345,18 @@ async function handleContextCommand(
|
|
|
293
345
|
else if (args[i] === "--token-env") tokenEnv = args[++i];
|
|
294
346
|
}
|
|
295
347
|
if (!name || !server) {
|
|
296
|
-
throw new Error(
|
|
348
|
+
throw new Error(
|
|
349
|
+
"usage: skillmux context add <name> --server <url> [--token-env <env_name>]",
|
|
350
|
+
);
|
|
297
351
|
}
|
|
298
352
|
await addContext(name, { server, token_env: tokenEnv });
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
+
);
|
|
304
360
|
return;
|
|
305
361
|
}
|
|
306
362
|
|
|
@@ -308,11 +364,13 @@ async function handleContextCommand(
|
|
|
308
364
|
const name = args[0];
|
|
309
365
|
if (!name) throw new Error("usage: skillmux context use <name>");
|
|
310
366
|
await useContext(name);
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
367
|
+
emitSuccess(
|
|
368
|
+
{ isJson: ctx.isJson, target: ctx.target },
|
|
369
|
+
{ default_context: name },
|
|
370
|
+
() => {
|
|
371
|
+
console.log(`Switched default context to "${name}"`);
|
|
372
|
+
},
|
|
373
|
+
);
|
|
316
374
|
return;
|
|
317
375
|
}
|
|
318
376
|
|
|
@@ -320,22 +378,56 @@ async function handleContextCommand(
|
|
|
320
378
|
const name = args[0];
|
|
321
379
|
if (!name) throw new Error("usage: skillmux context remove <name>");
|
|
322
380
|
await removeContext(name);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
381
|
+
emitSuccess(
|
|
382
|
+
{ isJson: ctx.isJson, target: ctx.target },
|
|
383
|
+
{ removed: name },
|
|
384
|
+
() => {
|
|
385
|
+
console.log(`Removed context "${name}"`);
|
|
386
|
+
},
|
|
387
|
+
);
|
|
328
388
|
return;
|
|
329
389
|
}
|
|
330
390
|
|
|
331
391
|
throw new Error("usage: skillmux context <add|list|current|use|remove>");
|
|
332
392
|
}
|
|
333
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
|
+
|
|
334
426
|
async function handleConfigCommand(
|
|
335
427
|
adapter: TargetAdapter,
|
|
336
428
|
sub: string,
|
|
337
429
|
args: string[],
|
|
338
|
-
ctx: { target: ResolvedTarget; isJson: boolean; dryRun: boolean }
|
|
430
|
+
ctx: { target: ResolvedTarget; isJson: boolean; dryRun: boolean },
|
|
339
431
|
) {
|
|
340
432
|
if (sub === "init") {
|
|
341
433
|
let vaultPath: string | undefined;
|
|
@@ -344,7 +436,8 @@ async function handleConfigCommand(
|
|
|
344
436
|
const option = args[i];
|
|
345
437
|
if (option === "--vault") {
|
|
346
438
|
vaultPath = args[++i];
|
|
347
|
-
if (!vaultPath)
|
|
439
|
+
if (!vaultPath)
|
|
440
|
+
throw new Error("usage: skillmux config init --vault <path> --yes");
|
|
348
441
|
} else if (option === "--yes") {
|
|
349
442
|
yes = true;
|
|
350
443
|
} else if (option === "--dry-run" || option === "--json") {
|
|
@@ -364,69 +457,65 @@ async function handleConfigCommand(
|
|
|
364
457
|
migrateLegacyPaths();
|
|
365
458
|
const plan = planConfigInit(resolveConfigPath(), expandHome(vaultPath));
|
|
366
459
|
if (plan.action === "preserve") {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
plan: { config_path: plan.configPath, vault_path: plan.vaultPath, action: "preserve" },
|
|
376
|
-
})
|
|
377
|
-
: `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
|
+
});
|
|
378
468
|
return;
|
|
379
469
|
}
|
|
380
470
|
if (ctx.dryRun) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
plan: { config_path: plan.configPath, vault_path: plan.vaultPath, action: "create" },
|
|
390
|
-
})
|
|
391
|
-
: `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
|
+
});
|
|
392
479
|
return;
|
|
393
480
|
}
|
|
394
481
|
if (!yes) {
|
|
395
482
|
if (!ctx.isJson && isInteractive()) {
|
|
396
|
-
if (
|
|
483
|
+
if (
|
|
484
|
+
!(await confirmAction(
|
|
485
|
+
`Create ${plan.configPath} with vault_path ${plan.vaultPath}?`,
|
|
486
|
+
))
|
|
487
|
+
) {
|
|
397
488
|
console.log("config init cancelled; nothing written");
|
|
398
489
|
return;
|
|
399
490
|
}
|
|
400
491
|
} else {
|
|
401
|
-
throw new Error(
|
|
492
|
+
throw new Error(
|
|
493
|
+
"config initialization requires --yes in noninteractive mode",
|
|
494
|
+
);
|
|
402
495
|
}
|
|
403
496
|
}
|
|
404
497
|
|
|
405
498
|
const result = applyConfigInit(plan);
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
? `created ${plan.configPath}`
|
|
418
|
-
: `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
|
+
});
|
|
419
510
|
return;
|
|
420
511
|
}
|
|
421
512
|
|
|
422
513
|
if (sub === "show") {
|
|
423
514
|
const data = await adapter.getConfigShow();
|
|
424
|
-
|
|
425
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data })));
|
|
426
|
-
} else {
|
|
515
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, data, () => {
|
|
427
516
|
renderTargetBanner(ctx.target);
|
|
428
517
|
console.log(JSON.stringify(data.effective, null, 2));
|
|
429
|
-
}
|
|
518
|
+
});
|
|
430
519
|
return;
|
|
431
520
|
}
|
|
432
521
|
|
|
@@ -434,32 +523,34 @@ async function handleConfigCommand(
|
|
|
434
523
|
const key = args[0];
|
|
435
524
|
if (!key) throw new Error("usage: skillmux config get <key>");
|
|
436
525
|
const val = await adapter.getConfigGet(key);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
+
);
|
|
442
535
|
return;
|
|
443
536
|
}
|
|
444
537
|
|
|
445
538
|
if (sub === "validate") {
|
|
446
539
|
const res = await adapter.configValidate();
|
|
447
|
-
|
|
448
|
-
console.log(
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
540
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
541
|
+
console.log(
|
|
542
|
+
res.valid ? "Configuration is valid." : "Configuration is invalid.",
|
|
543
|
+
);
|
|
544
|
+
});
|
|
452
545
|
return;
|
|
453
546
|
}
|
|
454
547
|
|
|
455
548
|
if (sub === "diff") {
|
|
456
549
|
const res = await adapter.configDiff();
|
|
457
|
-
|
|
458
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
459
|
-
} else {
|
|
550
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
460
551
|
renderTargetBanner(ctx.target);
|
|
461
552
|
console.log(JSON.stringify(res.diff, null, 2));
|
|
462
|
-
}
|
|
553
|
+
});
|
|
463
554
|
return;
|
|
464
555
|
}
|
|
465
556
|
|
|
@@ -470,27 +561,27 @@ async function handleConfigCommand(
|
|
|
470
561
|
throw new Error("usage: skillmux config set <key> <value> [--dry-run]");
|
|
471
562
|
}
|
|
472
563
|
const res = await adapter.configSet(key, value, { dryRun: ctx.dryRun });
|
|
473
|
-
|
|
474
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
475
|
-
} else {
|
|
564
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
476
565
|
renderTargetBanner(ctx.target);
|
|
477
566
|
const prefix = ctx.dryRun ? "[dry-run] " : "";
|
|
478
|
-
console.log(
|
|
479
|
-
|
|
480
|
-
|
|
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
|
+
});
|
|
481
574
|
return;
|
|
482
575
|
}
|
|
483
576
|
|
|
484
577
|
if (sub === "status") {
|
|
485
578
|
const res = await adapter.configStatus();
|
|
486
|
-
|
|
487
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
488
|
-
} else {
|
|
579
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
489
580
|
renderTargetBanner(ctx.target);
|
|
490
581
|
console.log(`Runtime: ${res.runtime}`);
|
|
491
582
|
console.log(`Active revision: ${res.active_revision}`);
|
|
492
583
|
console.log(`Readiness: ${res.readiness.status}`);
|
|
493
|
-
}
|
|
584
|
+
});
|
|
494
585
|
return;
|
|
495
586
|
}
|
|
496
587
|
|
|
@@ -498,9 +589,14 @@ async function handleConfigCommand(
|
|
|
498
589
|
}
|
|
499
590
|
|
|
500
591
|
async function confirmAction(prompt: string): Promise<boolean> {
|
|
501
|
-
const readline = createInterface({
|
|
592
|
+
const readline = createInterface({
|
|
593
|
+
input: process.stdin,
|
|
594
|
+
output: process.stdout,
|
|
595
|
+
});
|
|
502
596
|
try {
|
|
503
|
-
const answer = (await readline.question(`${prompt} [y/N] `))
|
|
597
|
+
const answer = (await readline.question(`${prompt} [y/N] `))
|
|
598
|
+
.trim()
|
|
599
|
+
.toLowerCase();
|
|
504
600
|
return answer === "y" || answer === "yes";
|
|
505
601
|
} finally {
|
|
506
602
|
readline.close();
|
|
@@ -511,7 +607,7 @@ async function handleCalibrateCommand(
|
|
|
511
607
|
adapter: TargetAdapter,
|
|
512
608
|
sub: string,
|
|
513
609
|
args: string[],
|
|
514
|
-
ctx: { target: ResolvedTarget; isJson: boolean }
|
|
610
|
+
ctx: { target: ResolvedTarget; isJson: boolean },
|
|
515
611
|
) {
|
|
516
612
|
if (sub === "run") {
|
|
517
613
|
let datasetPath: string | undefined;
|
|
@@ -519,21 +615,17 @@ async function handleCalibrateCommand(
|
|
|
519
615
|
if (args[i] === "--dataset") datasetPath = args[++i];
|
|
520
616
|
}
|
|
521
617
|
const res = await adapter.calibrateRun({ datasetPath });
|
|
522
|
-
|
|
523
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
524
|
-
} else {
|
|
618
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
525
619
|
renderTargetBanner(ctx.target);
|
|
526
620
|
console.log(`Calibration run complete.`);
|
|
527
621
|
if (res.result) console.log(JSON.stringify(res.result, null, 2));
|
|
528
|
-
}
|
|
622
|
+
});
|
|
529
623
|
return;
|
|
530
624
|
}
|
|
531
625
|
|
|
532
626
|
if (sub === "list") {
|
|
533
627
|
const res = await adapter.calibrateList();
|
|
534
|
-
|
|
535
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
536
|
-
} else {
|
|
628
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
537
629
|
renderTargetBanner(ctx.target);
|
|
538
630
|
renderTable(
|
|
539
631
|
[
|
|
@@ -541,9 +633,9 @@ async function handleCalibrateCommand(
|
|
|
541
633
|
{ key: "created_at", header: "CREATED_AT" },
|
|
542
634
|
{ key: "status", header: "STATUS" },
|
|
543
635
|
],
|
|
544
|
-
res
|
|
636
|
+
res,
|
|
545
637
|
);
|
|
546
|
-
}
|
|
638
|
+
});
|
|
547
639
|
return;
|
|
548
640
|
}
|
|
549
641
|
|
|
@@ -551,12 +643,10 @@ async function handleCalibrateCommand(
|
|
|
551
643
|
const runId = args[1];
|
|
552
644
|
if (!runId) throw new Error("usage: skillmux calibrate show <run_id>");
|
|
553
645
|
const res = await adapter.calibrateShow(runId);
|
|
554
|
-
|
|
555
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
556
|
-
} else {
|
|
646
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
557
647
|
renderTargetBanner(ctx.target);
|
|
558
648
|
console.log(JSON.stringify(res, null, 2));
|
|
559
|
-
}
|
|
649
|
+
});
|
|
560
650
|
return;
|
|
561
651
|
}
|
|
562
652
|
|
|
@@ -564,12 +654,10 @@ async function handleCalibrateCommand(
|
|
|
564
654
|
const runId = args[1];
|
|
565
655
|
if (!runId) throw new Error("usage: skillmux calibrate apply <run_id>");
|
|
566
656
|
const res = await adapter.calibrateApply(runId);
|
|
567
|
-
|
|
568
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
569
|
-
} else {
|
|
657
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
570
658
|
renderTargetBanner(ctx.target);
|
|
571
659
|
console.log(`Applied calibration run "${runId}"`);
|
|
572
|
-
}
|
|
660
|
+
});
|
|
573
661
|
return;
|
|
574
662
|
}
|
|
575
663
|
|
|
@@ -578,7 +666,9 @@ async function handleCalibrateCommand(
|
|
|
578
666
|
return;
|
|
579
667
|
}
|
|
580
668
|
|
|
581
|
-
throw new Error(
|
|
669
|
+
throw new Error(
|
|
670
|
+
"usage: skillmux calibrate generate-dataset [--vault <path>] [--out <file>]",
|
|
671
|
+
);
|
|
582
672
|
}
|
|
583
673
|
|
|
584
674
|
async function handleCompletionsCommand(shell: string) {
|
|
@@ -590,7 +680,7 @@ async function handleCompletionsCommand(shell: string) {
|
|
|
590
680
|
|
|
591
681
|
function handleError(
|
|
592
682
|
err: any,
|
|
593
|
-
opts: { target: ResolvedTarget; isJson: boolean; isVerbose: boolean }
|
|
683
|
+
opts: { target: ResolvedTarget; isJson: boolean; isVerbose: boolean },
|
|
594
684
|
) {
|
|
595
685
|
const code = mapExitCode(err);
|
|
596
686
|
process.exitCode = code;
|
|
@@ -605,7 +695,13 @@ function handleError(
|
|
|
605
695
|
});
|
|
606
696
|
console.log(JSON.stringify(env));
|
|
607
697
|
} else {
|
|
608
|
-
console.error(
|
|
698
|
+
console.error(
|
|
699
|
+
msg.startsWith("usage:") ||
|
|
700
|
+
msg.startsWith("Unknown") ||
|
|
701
|
+
msg.startsWith("error:")
|
|
702
|
+
? msg
|
|
703
|
+
: `error: ${msg}`,
|
|
704
|
+
);
|
|
609
705
|
if (opts.isVerbose && err instanceof Error && err.stack) {
|
|
610
706
|
console.error(err.stack);
|
|
611
707
|
}
|
|
@@ -617,7 +713,7 @@ function printHelp(): void {
|
|
|
617
713
|
|
|
618
714
|
Setup:
|
|
619
715
|
skillmux config init --vault <path> --yes
|
|
620
|
-
skillmux init [--client <name>...] [--target <name>...] [--
|
|
716
|
+
skillmux init [--client <name>...] [--target <name>...] [--dir <dir>]
|
|
621
717
|
[--vault <path>] [--core <skill_id>...]
|
|
622
718
|
[--migrate-full-vault] [--no-instructions] [--no-sync]
|
|
623
719
|
[--interactive|--yes|--dry-run] [--json]
|
|
@@ -626,6 +722,8 @@ Setup:
|
|
|
626
722
|
[--interactive|--yes|--dry-run] [--json]
|
|
627
723
|
skillmux project <list|show|add-path|remove-path|pin|unpin|attach|detach>
|
|
628
724
|
skillmux target <list|show|add|remove>
|
|
725
|
+
skillmux core <pin|unpin> <skill_id>... [--yes] [--dry-run] [--json]
|
|
726
|
+
skillmux skill which <skill_id>
|
|
629
727
|
|
|
630
728
|
Init clients:
|
|
631
729
|
claude-code, codex, gemini-cli, opencode, github-copilot, windsurf,
|
|
@@ -635,8 +733,8 @@ Init targets:
|
|
|
635
733
|
agent-skills, claude-code, codex, custom
|
|
636
734
|
|
|
637
735
|
Commands:
|
|
638
|
-
serve, index, sync, init, project, target, report, scan, install, eval, doctor,
|
|
639
|
-
|
|
736
|
+
serve, index, sync, init, project, target, core, report, scan, install, eval, doctor, skill,
|
|
737
|
+
local-vault, config, models, calibrate, context, completions`);
|
|
640
738
|
}
|
|
641
739
|
|
|
642
740
|
// ---------------------------------------------------------------------------
|
|
@@ -645,7 +743,10 @@ Commands:
|
|
|
645
743
|
|
|
646
744
|
type Transport = "stdio" | "http";
|
|
647
745
|
|
|
648
|
-
function parseServeArgs(args: string[]): {
|
|
746
|
+
function parseServeArgs(args: string[]): {
|
|
747
|
+
transport: Transport;
|
|
748
|
+
port?: number;
|
|
749
|
+
} {
|
|
649
750
|
let transport: Transport = "stdio";
|
|
650
751
|
let port: number | undefined;
|
|
651
752
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -675,7 +776,9 @@ async function runIndex(): Promise<void> {
|
|
|
675
776
|
const config = await loadConfig();
|
|
676
777
|
configure({ config, clients: createClients(config) });
|
|
677
778
|
const report = await rebuildIndex((skillId, error) => {
|
|
678
|
-
console.error(
|
|
779
|
+
console.error(
|
|
780
|
+
`warning: keeping previous index entry for ${skillId}: ${error}`,
|
|
781
|
+
);
|
|
679
782
|
});
|
|
680
783
|
const retainedNote =
|
|
681
784
|
report.retained.length > 0
|
|
@@ -687,43 +790,56 @@ async function runIndex(): Promise<void> {
|
|
|
687
790
|
const backfilled = await backfillEmbeddings();
|
|
688
791
|
console.log(`embeddings: ${backfilled} backfilled`);
|
|
689
792
|
} catch {
|
|
690
|
-
console.log(
|
|
793
|
+
console.log(
|
|
794
|
+
"embeddings: skipped (endpoint unreachable; lexical-only recall until next index)",
|
|
795
|
+
);
|
|
691
796
|
}
|
|
692
797
|
}
|
|
693
798
|
|
|
694
|
-
async function runEval(): Promise<void> {
|
|
799
|
+
async function runEval(options: { isJson: boolean }): Promise<void> {
|
|
695
800
|
const config = await loadConfig();
|
|
696
801
|
configure({ config, clients: createClients(config) });
|
|
697
802
|
|
|
698
803
|
const report = await evalVault().catch((error: unknown) => {
|
|
699
804
|
throw new Error(`eval requires local embeddings: ${String(error)}`);
|
|
700
805
|
});
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
+
});
|
|
708
815
|
}
|
|
709
816
|
|
|
710
|
-
async function runDoctor(): Promise<void> {
|
|
817
|
+
async function runDoctor(options: { isJson: boolean }): Promise<void> {
|
|
711
818
|
const report = await diagnose(await loadConfig());
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
+
});
|
|
716
827
|
if (report.checks.some((check) => !check.ok)) process.exitCode = 1;
|
|
717
828
|
}
|
|
718
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
|
+
|
|
719
835
|
async function runWhich(args: string[]): Promise<void> {
|
|
720
836
|
const skillId = args[0];
|
|
721
|
-
if (!skillId) throw new Error("usage: skillmux which <skill_id>");
|
|
837
|
+
if (!skillId) throw new Error("usage: skillmux skill which <skill_id>");
|
|
722
838
|
const config = await loadConfig();
|
|
723
839
|
const vaultPath = expandHome(config.vault_path);
|
|
724
840
|
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
725
|
-
const roots = vaultResolutionOrder(vaultPath, localVaultPaths).filter(
|
|
726
|
-
existsSync(join(root, skillId, "SKILL.md")),
|
|
841
|
+
const roots = vaultResolutionOrder(vaultPath, localVaultPaths).filter(
|
|
842
|
+
(root) => existsSync(join(root, skillId, "SKILL.md")),
|
|
727
843
|
);
|
|
728
844
|
if (roots.length === 0) {
|
|
729
845
|
console.log(`${skillId}: not found in vault_path or local_vault_paths`);
|
|
@@ -731,10 +847,10 @@ async function runWhich(args: string[]): Promise<void> {
|
|
|
731
847
|
return;
|
|
732
848
|
}
|
|
733
849
|
console.log(`${skillId}: serving from ${roots[0]}`);
|
|
734
|
-
for (const shadowedRoot of roots.slice(1))
|
|
850
|
+
for (const shadowedRoot of roots.slice(1))
|
|
851
|
+
console.log(` shadows: ${shadowedRoot}`);
|
|
735
852
|
}
|
|
736
853
|
|
|
737
|
-
const MANIFEST_USAGE = "usage: skillmux manifest <pin|unpin> <skill_id> (--core | --project <group> [--path <path>...])";
|
|
738
854
|
const PROJECT_INIT_USAGE =
|
|
739
855
|
"usage: skillmux project init [path] [--name <group>] [--skill <id>...] [--client <id>...] [--target <name>...] [--yes] [--no-sync]";
|
|
740
856
|
|
|
@@ -802,7 +918,11 @@ function parseProjectInitArgs(args: string[]): ProjectInitArgs {
|
|
|
802
918
|
yes = true;
|
|
803
919
|
} else if (arg === "--no-sync") {
|
|
804
920
|
sync = false;
|
|
805
|
-
} else if (
|
|
921
|
+
} else if (
|
|
922
|
+
arg === "--dry-run" ||
|
|
923
|
+
arg === "--json" ||
|
|
924
|
+
arg === "--interactive"
|
|
925
|
+
) {
|
|
806
926
|
continue;
|
|
807
927
|
} else if (arg.startsWith("-")) {
|
|
808
928
|
throw new Error(`unknown project init option: ${arg}`);
|
|
@@ -813,8 +933,44 @@ function parseProjectInitArgs(args: string[]): ProjectInitArgs {
|
|
|
813
933
|
}
|
|
814
934
|
}
|
|
815
935
|
|
|
816
|
-
const path = resolveProjectDirectory(
|
|
817
|
-
|
|
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
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async function loadManifestContext() {
|
|
951
|
+
const config = await loadConfig();
|
|
952
|
+
const vaultPath = expandHome(config.vault_path);
|
|
953
|
+
const manifestPath = resolveManifestPath(vaultPath);
|
|
954
|
+
if (!manifestPath) {
|
|
955
|
+
throw new Error(
|
|
956
|
+
`no skillmux.toml found at ${vaultPath}; run skillmux init first`,
|
|
957
|
+
);
|
|
958
|
+
}
|
|
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);
|
|
818
974
|
}
|
|
819
975
|
|
|
820
976
|
async function runProject(
|
|
@@ -823,14 +979,11 @@ async function runProject(
|
|
|
823
979
|
options: { isJson: boolean; dryRun: boolean },
|
|
824
980
|
): Promise<void> {
|
|
825
981
|
if (subCommand === "list" || subCommand === "show") {
|
|
826
|
-
const
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const names = subCommand === "show"
|
|
832
|
-
? [args[0] ?? ""]
|
|
833
|
-
: Object.keys(manifest.project ?? {});
|
|
982
|
+
const { manifest } = await loadManifestContext();
|
|
983
|
+
const names =
|
|
984
|
+
subCommand === "show"
|
|
985
|
+
? [args[0] ?? ""]
|
|
986
|
+
: Object.keys(manifest.project ?? {});
|
|
834
987
|
if (subCommand === "show" && !manifest.project?.[names[0]!]) {
|
|
835
988
|
throw new Error(`[project.${names[0]}] does not exist`);
|
|
836
989
|
}
|
|
@@ -858,33 +1011,43 @@ async function runProject(
|
|
|
858
1011
|
}
|
|
859
1012
|
if (subCommand === "add-path" || subCommand === "remove-path") {
|
|
860
1013
|
const group = args[0];
|
|
861
|
-
if (!group)
|
|
1014
|
+
if (!group)
|
|
1015
|
+
throw new Error(
|
|
1016
|
+
`usage: skillmux project ${subCommand} <group> [path] --yes`,
|
|
1017
|
+
);
|
|
862
1018
|
const rawPath = args[1]?.startsWith("-") ? undefined : args[1];
|
|
863
|
-
const projectPath = resolveProjectDirectory(
|
|
1019
|
+
const projectPath = resolveProjectDirectory(
|
|
1020
|
+
rawPath ? expandHome(rawPath) : undefined,
|
|
1021
|
+
);
|
|
864
1022
|
const yes = args.includes("--yes");
|
|
865
1023
|
if (!existsSync(projectPath) || !lstatSync(projectPath).isDirectory()) {
|
|
866
1024
|
throw new Error(`project path is not a directory: ${projectPath}`);
|
|
867
1025
|
}
|
|
868
|
-
const config
|
|
869
|
-
|
|
870
|
-
const manifestPath = resolveManifestPath(vaultPath);
|
|
871
|
-
if (!manifestPath) throw new Error(`no skillmux.toml found at ${vaultPath}; run skillmux init first`);
|
|
872
|
-
const manifest = parseManifest(await Bun.file(manifestPath).text());
|
|
1026
|
+
const { config, vaultPath, manifestPath, manifest } =
|
|
1027
|
+
await loadManifestContext();
|
|
873
1028
|
const updated = updateProjectPaths(manifest, group, {
|
|
874
|
-
...(subCommand === "add-path"
|
|
1029
|
+
...(subCommand === "add-path"
|
|
1030
|
+
? { add: [projectPath] }
|
|
1031
|
+
: { remove: [projectPath] }),
|
|
875
1032
|
});
|
|
876
|
-
validateManifest(
|
|
1033
|
+
validateManifest(
|
|
1034
|
+
updated,
|
|
1035
|
+
vaultPath,
|
|
1036
|
+
config.local_vault_paths.map(expandHome),
|
|
1037
|
+
);
|
|
877
1038
|
if (options.dryRun) {
|
|
878
1039
|
console.log(`${subCommand}: [project.${group}] ${projectPath} (dry-run)`);
|
|
879
1040
|
return;
|
|
880
1041
|
}
|
|
881
|
-
if (
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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;
|
|
888
1051
|
writeManifestAtomic(manifestPath, updated);
|
|
889
1052
|
console.log(`${subCommand}: [project.${group}] ${projectPath}`);
|
|
890
1053
|
return;
|
|
@@ -893,38 +1056,50 @@ async function runProject(
|
|
|
893
1056
|
const group = args[0];
|
|
894
1057
|
const skills = args.slice(1).filter((arg) => !arg.startsWith("-"));
|
|
895
1058
|
if (!group || skills.length === 0) {
|
|
896
|
-
throw new Error(
|
|
1059
|
+
throw new Error(
|
|
1060
|
+
`usage: skillmux project ${subCommand} <group> <skill_id>... --yes`,
|
|
1061
|
+
);
|
|
897
1062
|
}
|
|
898
1063
|
const yes = args.includes("--yes");
|
|
899
|
-
const config
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
if (!manifestPath) throw new Error(`no skillmux.toml found at ${vaultPath}; run skillmux init first`);
|
|
903
|
-
let updated = parseManifest(await Bun.file(manifestPath).text());
|
|
1064
|
+
const { config, vaultPath, manifestPath, manifest } =
|
|
1065
|
+
await loadManifestContext();
|
|
1066
|
+
let updated = manifest;
|
|
904
1067
|
for (const skill of skills) {
|
|
905
|
-
updated =
|
|
906
|
-
|
|
907
|
-
|
|
1068
|
+
updated =
|
|
1069
|
+
subCommand === "pin"
|
|
1070
|
+
? pinProject(updated, skill, group)
|
|
1071
|
+
: unpinProject(updated, skill, group);
|
|
908
1072
|
}
|
|
909
|
-
validateManifest(
|
|
1073
|
+
validateManifest(
|
|
1074
|
+
updated,
|
|
1075
|
+
vaultPath,
|
|
1076
|
+
config.local_vault_paths.map(expandHome),
|
|
1077
|
+
);
|
|
910
1078
|
if (options.dryRun) {
|
|
911
|
-
console.log(
|
|
1079
|
+
console.log(
|
|
1080
|
+
`${subCommand}: [project.${group}] ${skills.join(", ")} (dry-run)`,
|
|
1081
|
+
);
|
|
912
1082
|
return;
|
|
913
1083
|
}
|
|
914
|
-
if (
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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;
|
|
921
1093
|
writeManifestAtomic(manifestPath, updated);
|
|
922
1094
|
console.log(`${subCommand}: [project.${group}] ${skills.join(", ")}`);
|
|
923
1095
|
return;
|
|
924
1096
|
}
|
|
925
1097
|
if (subCommand === "attach" || subCommand === "detach") {
|
|
926
1098
|
const group = args[0];
|
|
927
|
-
if (!group)
|
|
1099
|
+
if (!group)
|
|
1100
|
+
throw new Error(
|
|
1101
|
+
`usage: skillmux project ${subCommand} <group> (--client <id>... | --target <name>...) --yes`,
|
|
1102
|
+
);
|
|
928
1103
|
const clients: string[] = [];
|
|
929
1104
|
const requestedTargets: string[] = [];
|
|
930
1105
|
for (let i = 1; i < args.length; i++) {
|
|
@@ -936,15 +1111,16 @@ async function runProject(
|
|
|
936
1111
|
const value = args[++i];
|
|
937
1112
|
if (!value) throw new Error("--target requires a name");
|
|
938
1113
|
requestedTargets.push(value);
|
|
939
|
-
} else if (
|
|
1114
|
+
} else if (
|
|
1115
|
+
args[i] !== "--yes" &&
|
|
1116
|
+
args[i] !== "--dry-run" &&
|
|
1117
|
+
args[i] !== "--json"
|
|
1118
|
+
) {
|
|
940
1119
|
throw new Error(`unknown project ${subCommand} option: ${args[i]}`);
|
|
941
1120
|
}
|
|
942
1121
|
}
|
|
943
|
-
const config
|
|
944
|
-
|
|
945
|
-
const manifestPath = resolveManifestPath(vaultPath);
|
|
946
|
-
if (!manifestPath) throw new Error(`no skillmux.toml found at ${vaultPath}; run skillmux init first`);
|
|
947
|
-
const manifest = parseManifest(await Bun.file(manifestPath).text());
|
|
1122
|
+
const { config, vaultPath, manifestPath, manifest } =
|
|
1123
|
+
await loadManifestContext();
|
|
948
1124
|
const clientTargets = configuredTargetsForClients(manifest, clients);
|
|
949
1125
|
const targets = [...new Set([...requestedTargets, ...clientTargets])];
|
|
950
1126
|
if (targets.length === 0) {
|
|
@@ -953,18 +1129,26 @@ async function runProject(
|
|
|
953
1129
|
const updated = updateProjectTargets(manifest, group, {
|
|
954
1130
|
...(subCommand === "attach" ? { attach: targets } : { detach: targets }),
|
|
955
1131
|
});
|
|
956
|
-
validateManifest(
|
|
1132
|
+
validateManifest(
|
|
1133
|
+
updated,
|
|
1134
|
+
vaultPath,
|
|
1135
|
+
config.local_vault_paths.map(expandHome),
|
|
1136
|
+
);
|
|
957
1137
|
if (options.dryRun) {
|
|
958
|
-
console.log(
|
|
1138
|
+
console.log(
|
|
1139
|
+
`${subCommand}: [project.${group}] ${targets.join(", ")} (dry-run)`,
|
|
1140
|
+
);
|
|
959
1141
|
return;
|
|
960
1142
|
}
|
|
961
|
-
if (
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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;
|
|
968
1152
|
writeManifestAtomic(manifestPath, updated);
|
|
969
1153
|
console.log(`${subCommand}: [project.${group}] ${targets.join(", ")}`);
|
|
970
1154
|
return;
|
|
@@ -976,33 +1160,38 @@ async function runProject(
|
|
|
976
1160
|
json: options.isJson,
|
|
977
1161
|
dryRun: options.dryRun,
|
|
978
1162
|
});
|
|
979
|
-
if (!existsSync(request.path))
|
|
1163
|
+
if (!existsSync(request.path))
|
|
1164
|
+
throw new Error(`project path does not exist: ${request.path}`);
|
|
980
1165
|
if (!lstatSync(request.path).isDirectory()) {
|
|
981
1166
|
throw new Error(`project path is not a directory: ${request.path}`);
|
|
982
1167
|
}
|
|
983
1168
|
|
|
984
|
-
const config
|
|
985
|
-
|
|
1169
|
+
const { config, vaultPath, manifestPath, manifest } =
|
|
1170
|
+
await loadManifestContext();
|
|
986
1171
|
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
987
|
-
const manifestPath = resolveManifestPath(vaultPath);
|
|
988
|
-
if (!manifestPath) throw new Error(`no skillmux.toml found at ${vaultPath}; run skillmux init first`);
|
|
989
|
-
const manifest = parseManifest(await Bun.file(manifestPath).text());
|
|
990
1172
|
if (guided) {
|
|
991
1173
|
const name = await promptText("Project group", request.name);
|
|
992
1174
|
const availableClients = SUPPORTED_CLIENT_IDS.filter((client) => {
|
|
993
1175
|
const surface = planClientSurfaces([client]).surfaces[0];
|
|
994
|
-
return
|
|
1176
|
+
return (
|
|
1177
|
+
surface !== undefined &&
|
|
1178
|
+
configuredTargetForSurface(manifest, surface) !== undefined
|
|
1179
|
+
);
|
|
995
1180
|
});
|
|
996
1181
|
const clients = await promptMultiSelect(
|
|
997
1182
|
"Which clients should receive project skills?",
|
|
998
1183
|
availableClients.map((client) => ({
|
|
999
1184
|
value: client,
|
|
1000
1185
|
label: client,
|
|
1001
|
-
selected:
|
|
1186
|
+
selected:
|
|
1187
|
+
request.clients.length === 0 || request.clients.includes(client),
|
|
1002
1188
|
})),
|
|
1003
1189
|
);
|
|
1004
1190
|
const skills = parseCommaList(
|
|
1005
|
-
await promptText(
|
|
1191
|
+
await promptText(
|
|
1192
|
+
"Project skill IDs, comma-separated",
|
|
1193
|
+
request.skills.join(","),
|
|
1194
|
+
),
|
|
1006
1195
|
);
|
|
1007
1196
|
request = { ...request, name, clients, skills };
|
|
1008
1197
|
}
|
|
@@ -1027,7 +1216,11 @@ async function runProject(
|
|
|
1027
1216
|
};
|
|
1028
1217
|
|
|
1029
1218
|
if (options.dryRun) {
|
|
1030
|
-
console.log(
|
|
1219
|
+
console.log(
|
|
1220
|
+
options.isJson
|
|
1221
|
+
? JSON.stringify({ schema_version: 1, plan })
|
|
1222
|
+
: `project plan: ${JSON.stringify(plan)}`,
|
|
1223
|
+
);
|
|
1031
1224
|
return;
|
|
1032
1225
|
}
|
|
1033
1226
|
if (!request.yes) {
|
|
@@ -1040,12 +1233,18 @@ async function runProject(
|
|
|
1040
1233
|
console.log(` skills: ${request.skills.join(", ") || "(none)"}`);
|
|
1041
1234
|
console.log(` sync: ${request.sync ? "yes" : "no"}`);
|
|
1042
1235
|
}
|
|
1043
|
-
if (
|
|
1236
|
+
if (
|
|
1237
|
+
!(await confirmAction(
|
|
1238
|
+
`Apply project setup for ${request.name} at ${request.path}?`,
|
|
1239
|
+
))
|
|
1240
|
+
) {
|
|
1044
1241
|
console.log("project setup cancelled");
|
|
1045
1242
|
return;
|
|
1046
1243
|
}
|
|
1047
1244
|
} else {
|
|
1048
|
-
throw new Error(
|
|
1245
|
+
throw new Error(
|
|
1246
|
+
"skillmux project init requires --yes when run non-interactively",
|
|
1247
|
+
);
|
|
1049
1248
|
}
|
|
1050
1249
|
}
|
|
1051
1250
|
|
|
@@ -1068,19 +1267,65 @@ async function runProject(
|
|
|
1068
1267
|
}
|
|
1069
1268
|
}
|
|
1070
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) {
|
|
1287
|
+
updated =
|
|
1288
|
+
subCommand === "pin"
|
|
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
|
+
|
|
1071
1319
|
async function runTarget(
|
|
1072
1320
|
subCommand: string,
|
|
1073
1321
|
args: string[],
|
|
1074
1322
|
options: { isJson: boolean; dryRun: boolean },
|
|
1075
1323
|
): Promise<void> {
|
|
1076
|
-
const
|
|
1077
|
-
const vaultPath = expandHome(config.vault_path);
|
|
1078
|
-
const manifestPath = resolveManifestPath(vaultPath);
|
|
1079
|
-
if (!manifestPath) throw new Error(`no skillmux.toml found at ${vaultPath}; run skillmux init first`);
|
|
1080
|
-
const manifest = parseManifest(await Bun.file(manifestPath).text());
|
|
1324
|
+
const { vaultPath, manifestPath, manifest } = await loadManifestContext();
|
|
1081
1325
|
|
|
1082
1326
|
if (subCommand === "list" || subCommand === "show") {
|
|
1083
|
-
const names =
|
|
1327
|
+
const names =
|
|
1328
|
+
subCommand === "show" ? [args[0] ?? ""] : Object.keys(manifest.targets);
|
|
1084
1329
|
if (subCommand === "show" && !manifest.targets[names[0]!]) {
|
|
1085
1330
|
throw new Error(`target "${names[0]}" does not exist`);
|
|
1086
1331
|
}
|
|
@@ -1102,7 +1347,9 @@ async function runTarget(
|
|
|
1102
1347
|
console.log(` dir: ${target.dir}`);
|
|
1103
1348
|
console.log(` host: ${target.host ?? "(global)"}`);
|
|
1104
1349
|
console.log(` clients: ${target.clients.join(", ") || "(custom)"}`);
|
|
1105
|
-
console.log(
|
|
1350
|
+
console.log(
|
|
1351
|
+
` projects: ${target.project_groups.join(", ") || "(none)"}`,
|
|
1352
|
+
);
|
|
1106
1353
|
}
|
|
1107
1354
|
}
|
|
1108
1355
|
return;
|
|
@@ -1110,24 +1357,30 @@ async function runTarget(
|
|
|
1110
1357
|
|
|
1111
1358
|
if (subCommand === "add") {
|
|
1112
1359
|
const name = args[0];
|
|
1113
|
-
const
|
|
1114
|
-
const rawPath =
|
|
1115
|
-
if (!name || !rawPath)
|
|
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");
|
|
1116
1364
|
const path = expandHome(rawPath);
|
|
1117
1365
|
if (options.dryRun) {
|
|
1118
1366
|
const planned = planInitManifest(vaultPath, [{ name, dir: path }], []);
|
|
1119
|
-
console.log(
|
|
1120
|
-
|
|
1121
|
-
|
|
1367
|
+
console.log(
|
|
1368
|
+
options.isJson
|
|
1369
|
+
? JSON.stringify({ schema_version: 1, target: planned.targets[name] })
|
|
1370
|
+
: `target add: ${name} -> ${path} (dry-run)`,
|
|
1371
|
+
);
|
|
1122
1372
|
return;
|
|
1123
1373
|
}
|
|
1124
|
-
if (
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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;
|
|
1131
1384
|
applyInit(vaultPath, [{ name, dir: path }]);
|
|
1132
1385
|
console.log(`target "${name}" added at ${path}`);
|
|
1133
1386
|
return;
|
|
@@ -1136,93 +1389,97 @@ async function runTarget(
|
|
|
1136
1389
|
if (subCommand === "remove") {
|
|
1137
1390
|
const name = args[0];
|
|
1138
1391
|
if (!name || !manifest.targets[name]) {
|
|
1139
|
-
throw new Error(
|
|
1392
|
+
throw new Error(
|
|
1393
|
+
name
|
|
1394
|
+
? `target "${name}" does not exist`
|
|
1395
|
+
: "usage: skillmux target remove <name> --yes",
|
|
1396
|
+
);
|
|
1140
1397
|
}
|
|
1141
1398
|
if (options.dryRun) {
|
|
1142
1399
|
console.log(`target remove: ${name} (files preserved, dry-run)`);
|
|
1143
1400
|
return;
|
|
1144
1401
|
}
|
|
1145
|
-
if (
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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;
|
|
1152
1412
|
const targets = { ...manifest.targets };
|
|
1153
1413
|
delete targets[name];
|
|
1154
1414
|
writeManifestAtomic(manifestPath, { ...manifest, targets });
|
|
1155
|
-
console.log(
|
|
1415
|
+
console.log(
|
|
1416
|
+
`target "${name}" removed from the manifest; files preserved at ${manifest.targets[name]!.dir}`,
|
|
1417
|
+
);
|
|
1156
1418
|
return;
|
|
1157
1419
|
}
|
|
1158
1420
|
|
|
1159
1421
|
throw new Error("usage: skillmux target <list|show|add|remove>");
|
|
1160
1422
|
}
|
|
1161
1423
|
|
|
1162
|
-
function
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
let project: string | undefined;
|
|
1167
|
-
const paths: string[] = [];
|
|
1168
|
-
for (let i = 1; i < args.length; i++) {
|
|
1169
|
-
const arg = args[i];
|
|
1170
|
-
if (arg === "--core") core = true;
|
|
1171
|
-
else if (arg === "--project") {
|
|
1172
|
-
const value = args[++i];
|
|
1173
|
-
if (!value) throw new Error("--project requires a group name");
|
|
1174
|
-
project = value;
|
|
1175
|
-
} else if (arg === "--path") {
|
|
1176
|
-
const value = args[++i];
|
|
1177
|
-
if (!value) throw new Error("--path requires a path");
|
|
1178
|
-
paths.push(value);
|
|
1179
|
-
} else throw new Error(`unknown manifest option: ${arg}`);
|
|
1180
|
-
}
|
|
1181
|
-
if (core === (project !== undefined)) throw new Error(MANIFEST_USAGE);
|
|
1182
|
-
return { skillId, core, project, paths };
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
async function runManifest(subCommand: string, args: string[]): Promise<void> {
|
|
1186
|
-
if (subCommand !== "pin" && subCommand !== "unpin") throw new Error(MANIFEST_USAGE);
|
|
1187
|
-
const { skillId, core, project, paths } = parseManifestPinArgs(args);
|
|
1188
|
-
const config = await loadConfig();
|
|
1189
|
-
const vaultPath = expandHome(config.vault_path);
|
|
1190
|
-
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
1191
|
-
const manifestPath = resolveManifestPath(vaultPath);
|
|
1192
|
-
if (!manifestPath) throw new Error(`no skillmux.toml found at ${vaultPath}`);
|
|
1193
|
-
const manifest = parseManifest(await Bun.file(manifestPath).text());
|
|
1194
|
-
|
|
1195
|
-
let updated;
|
|
1196
|
-
if (core) {
|
|
1197
|
-
updated = subCommand === "pin" ? pinCore(manifest, skillId) : unpinCore(manifest, skillId);
|
|
1198
|
-
} else {
|
|
1199
|
-
updated =
|
|
1200
|
-
subCommand === "pin"
|
|
1201
|
-
? pinProject(manifest, skillId, project!, paths)
|
|
1202
|
-
: unpinProject(manifest, skillId, project!);
|
|
1203
|
-
}
|
|
1204
|
-
validateManifest(updated, vaultPath, localVaultPaths);
|
|
1205
|
-
await Bun.write(manifestPath, serializeManifest(updated));
|
|
1206
|
-
console.log(`${subCommand === "pin" ? "pinned" : "unpinned"} "${skillId}" ${core ? "[core]" : `[project.${project}]`}`);
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
async function runLocalVaultInit(args: string[]): Promise<void> {
|
|
1424
|
+
async function runLocalVaultInit(
|
|
1425
|
+
args: string[],
|
|
1426
|
+
options: { isJson: boolean; dryRun: boolean },
|
|
1427
|
+
): Promise<void> {
|
|
1210
1428
|
const path = args[0];
|
|
1211
|
-
if (!path) throw new Error("usage: skillmux local-vault init <path>");
|
|
1429
|
+
if (!path) throw new Error("usage: skillmux local-vault init <path> --yes");
|
|
1212
1430
|
const expanded = expandHome(path);
|
|
1213
1431
|
const config = await loadConfig();
|
|
1214
1432
|
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
1215
1433
|
if (!localVaultPaths.includes(expanded)) {
|
|
1216
|
-
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
|
+
);
|
|
1217
1437
|
}
|
|
1218
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;
|
|
1219
1462
|
writeLocalVaultMarker(expanded, expandHome(config.vault_path));
|
|
1220
|
-
|
|
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
|
+
}
|
|
1221
1476
|
}
|
|
1222
1477
|
|
|
1223
|
-
async function runModelDownload(): Promise<void> {
|
|
1478
|
+
async function runModelDownload(options: { isJson: boolean }): Promise<void> {
|
|
1224
1479
|
const cacheDir = await downloadLocalModels(await loadConfig());
|
|
1225
|
-
|
|
1480
|
+
emitSuccess({ isJson: options.isJson }, { cache_dir: cacheDir }, () =>
|
|
1481
|
+
console.log(`models ready in ${cacheDir}`),
|
|
1482
|
+
);
|
|
1226
1483
|
}
|
|
1227
1484
|
|
|
1228
1485
|
function parseSyncArgs(args: string[]): {
|
|
@@ -1289,10 +1546,18 @@ async function runSync(args: string[]): Promise<void> {
|
|
|
1289
1546
|
|
|
1290
1547
|
const suffix = dryRun ? " (dry-run)" : "";
|
|
1291
1548
|
const result = syncTarget(
|
|
1292
|
-
{
|
|
1549
|
+
{
|
|
1550
|
+
vaultPath,
|
|
1551
|
+
targetDir,
|
|
1552
|
+
targetName,
|
|
1553
|
+
coreSkillIds: manifest.core.skills,
|
|
1554
|
+
localVaultPaths,
|
|
1555
|
+
},
|
|
1293
1556
|
{ dryRun },
|
|
1294
1557
|
);
|
|
1295
|
-
console.log(
|
|
1558
|
+
console.log(
|
|
1559
|
+
`${targetName}: +${result.added.length} -${result.removed.length}${suffix}`,
|
|
1560
|
+
);
|
|
1296
1561
|
|
|
1297
1562
|
if (target.project_groups.length > 0) {
|
|
1298
1563
|
const allGroups = manifest.project ?? {};
|
|
@@ -1349,9 +1614,9 @@ function parseInitArgs(args: string[]): {
|
|
|
1349
1614
|
if (!value) throw new Error("--vault requires a path");
|
|
1350
1615
|
vaultPath = value;
|
|
1351
1616
|
i++;
|
|
1352
|
-
} else if (option === "--
|
|
1617
|
+
} else if (option === "--dir") {
|
|
1353
1618
|
const value = args[i + 1];
|
|
1354
|
-
if (!value) throw new Error("--
|
|
1619
|
+
if (!value) throw new Error("--dir requires a directory");
|
|
1355
1620
|
customPath = value;
|
|
1356
1621
|
i++;
|
|
1357
1622
|
} else if (option === "--core") {
|
|
@@ -1359,7 +1624,11 @@ function parseInitArgs(args: string[]): {
|
|
|
1359
1624
|
if (!value) throw new Error("--core requires a skill_id");
|
|
1360
1625
|
coreSkillIds.push(value);
|
|
1361
1626
|
i++;
|
|
1362
|
-
} else if (
|
|
1627
|
+
} else if (
|
|
1628
|
+
option === "--dry-run" ||
|
|
1629
|
+
option === "--json" ||
|
|
1630
|
+
option === "--interactive"
|
|
1631
|
+
) {
|
|
1363
1632
|
continue;
|
|
1364
1633
|
} else if (option === "--migrate-full-vault") {
|
|
1365
1634
|
migrateFullVault = true;
|
|
@@ -1411,10 +1680,13 @@ async function runInit(
|
|
|
1411
1680
|
let configPlan: ConfigInitPlan | undefined;
|
|
1412
1681
|
let vaultPath: string;
|
|
1413
1682
|
if (!existsSync(configPath)) {
|
|
1414
|
-
const bootstrapVaultPath =
|
|
1683
|
+
const bootstrapVaultPath =
|
|
1684
|
+
requestedVaultPath ??
|
|
1415
1685
|
(!options.isJson && isInteractive() ? "~/skills" : undefined);
|
|
1416
1686
|
if (!bootstrapVaultPath) {
|
|
1417
|
-
throw new Error(
|
|
1687
|
+
throw new Error(
|
|
1688
|
+
`machine config does not exist: ${configPath}; re-run with --vault <path>`,
|
|
1689
|
+
);
|
|
1418
1690
|
}
|
|
1419
1691
|
configPlan = planConfigInit(configPath, expandHome(bootstrapVaultPath));
|
|
1420
1692
|
vaultPath = configPlan.vaultPath;
|
|
@@ -1439,15 +1711,21 @@ async function runInit(
|
|
|
1439
1711
|
let selectedClients = requestedClients;
|
|
1440
1712
|
if (guided) {
|
|
1441
1713
|
const detected = detectInstalledClients({
|
|
1442
|
-
codexHome: process.env.CODEX_HOME
|
|
1714
|
+
codexHome: process.env.CODEX_HOME
|
|
1715
|
+
? expandHome(process.env.CODEX_HOME)
|
|
1716
|
+
: undefined,
|
|
1443
1717
|
});
|
|
1444
|
-
const evidence = new Map(
|
|
1718
|
+
const evidence = new Map(
|
|
1719
|
+
detected.map((item) => [item.client, item.evidence]),
|
|
1720
|
+
);
|
|
1445
1721
|
selectedClients = await promptMultiSelect(
|
|
1446
1722
|
"Which clients do you use?",
|
|
1447
1723
|
SUPPORTED_CLIENT_IDS.map((client) => ({
|
|
1448
1724
|
value: client,
|
|
1449
1725
|
label: client,
|
|
1450
|
-
detail: evidence.has(client)
|
|
1726
|
+
detail: evidence.has(client)
|
|
1727
|
+
? `detected: ${evidence.get(client)}`
|
|
1728
|
+
: undefined,
|
|
1451
1729
|
selected: evidence.has(client) || requestedClients.includes(client),
|
|
1452
1730
|
})),
|
|
1453
1731
|
);
|
|
@@ -1455,16 +1733,26 @@ async function runInit(
|
|
|
1455
1733
|
let selectedCoreSkillIds = coreSkillIds;
|
|
1456
1734
|
if (guided) {
|
|
1457
1735
|
selectedCoreSkillIds = parseCommaList(
|
|
1458
|
-
await promptText(
|
|
1736
|
+
await promptText(
|
|
1737
|
+
"Core skill IDs to add, comma-separated",
|
|
1738
|
+
coreSkillIds.join(","),
|
|
1739
|
+
),
|
|
1459
1740
|
);
|
|
1460
1741
|
}
|
|
1461
1742
|
|
|
1462
1743
|
const clientPlan = planClientSurfaces(selectedClients, {
|
|
1463
|
-
codexHome: process.env.CODEX_HOME
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
codexHome: process.env.CODEX_HOME ? expandHome(process.env.CODEX_HOME) : undefined,
|
|
1744
|
+
codexHome: process.env.CODEX_HOME
|
|
1745
|
+
? expandHome(process.env.CODEX_HOME)
|
|
1746
|
+
: undefined,
|
|
1467
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
|
+
);
|
|
1468
1756
|
const instructionReadiness: Partial<Record<ClientId, ReadinessAxis>> = {};
|
|
1469
1757
|
for (const change of instructionPlan.changes) {
|
|
1470
1758
|
for (const client of change.clients) {
|
|
@@ -1475,25 +1763,39 @@ async function runInit(
|
|
|
1475
1763
|
}
|
|
1476
1764
|
}
|
|
1477
1765
|
for (const manual of instructionPlan.manual) {
|
|
1478
|
-
instructionReadiness[manual.client] = {
|
|
1766
|
+
instructionReadiness[manual.client] = {
|
|
1767
|
+
status: "manual",
|
|
1768
|
+
detail: manual.reason,
|
|
1769
|
+
};
|
|
1479
1770
|
}
|
|
1480
|
-
const builtInNames = new Set([
|
|
1771
|
+
const builtInNames = new Set([
|
|
1772
|
+
"agent-skills",
|
|
1773
|
+
"claude-code",
|
|
1774
|
+
"codex",
|
|
1775
|
+
"custom",
|
|
1776
|
+
"agents",
|
|
1777
|
+
"claude",
|
|
1778
|
+
]);
|
|
1481
1779
|
const explicitSurfaceTargets = explicitTargets
|
|
1482
1780
|
.filter((name) => builtInNames.has(name))
|
|
1483
1781
|
.map((name) =>
|
|
1484
1782
|
resolveBuiltInTarget(name, {
|
|
1485
|
-
codexHome: process.env.CODEX_HOME
|
|
1783
|
+
codexHome: process.env.CODEX_HOME
|
|
1784
|
+
? expandHome(process.env.CODEX_HOME)
|
|
1785
|
+
: undefined,
|
|
1486
1786
|
customPath: customPath ? expandHome(customPath) : undefined,
|
|
1487
1787
|
}),
|
|
1488
1788
|
);
|
|
1489
1789
|
if (customPath && !explicitTargets.includes("custom")) {
|
|
1490
|
-
throw new Error("--
|
|
1790
|
+
throw new Error("--dir may only be used with --target custom");
|
|
1491
1791
|
}
|
|
1492
1792
|
for (const target of explicitSurfaceTargets) {
|
|
1493
1793
|
if (target.warning) console.error(`warning: ${target.warning}`);
|
|
1494
1794
|
}
|
|
1495
1795
|
const targetByPath = new Map(
|
|
1496
|
-
explicitSurfaceTargets.map(
|
|
1796
|
+
explicitSurfaceTargets.map(
|
|
1797
|
+
(target) => [target.path, target.targetName] as const,
|
|
1798
|
+
),
|
|
1497
1799
|
);
|
|
1498
1800
|
const existingManifestPath = resolveManifestPath(vaultPath);
|
|
1499
1801
|
const existingManifest = existingManifestPath
|
|
@@ -1504,7 +1806,8 @@ async function runInit(
|
|
|
1504
1806
|
targetByPath.set(
|
|
1505
1807
|
surface.path,
|
|
1506
1808
|
existingManifest
|
|
1507
|
-
? configuredTargetForSurface(existingManifest, surface) ??
|
|
1809
|
+
? (configuredTargetForSurface(existingManifest, surface) ??
|
|
1810
|
+
surface.targetName)
|
|
1508
1811
|
: surface.targetName,
|
|
1509
1812
|
);
|
|
1510
1813
|
}
|
|
@@ -1518,7 +1821,8 @@ async function runInit(
|
|
|
1518
1821
|
const candidates = detectSurfaces(candidatePaths, vaultPath);
|
|
1519
1822
|
if (!options.isJson) {
|
|
1520
1823
|
for (const candidate of candidates) {
|
|
1521
|
-
const name =
|
|
1824
|
+
const name =
|
|
1825
|
+
targetByPath.get(candidate.path) ?? deriveTargetName(candidate.path);
|
|
1522
1826
|
if (candidate.state === "missing") {
|
|
1523
1827
|
console.log(`${name} (${candidate.path}): not found`);
|
|
1524
1828
|
continue;
|
|
@@ -1528,26 +1832,45 @@ async function runInit(
|
|
|
1528
1832
|
continue;
|
|
1529
1833
|
}
|
|
1530
1834
|
if (candidate.state === "full-vault") {
|
|
1531
|
-
console.log(
|
|
1835
|
+
console.log(
|
|
1836
|
+
`${name} (${candidate.path}): full-vault -> ${candidate.canonicalPath}`,
|
|
1837
|
+
);
|
|
1532
1838
|
continue;
|
|
1533
1839
|
}
|
|
1534
1840
|
if (candidate.state === "external-symlink") {
|
|
1535
|
-
console.log(
|
|
1841
|
+
console.log(
|
|
1842
|
+
`${name} (${candidate.path}): external symlink -> ${candidate.canonicalPath}`,
|
|
1843
|
+
);
|
|
1536
1844
|
continue;
|
|
1537
1845
|
}
|
|
1538
1846
|
if (candidate.state === "unsupported") {
|
|
1539
|
-
console.log(
|
|
1847
|
+
console.log(
|
|
1848
|
+
`${name} (${candidate.path}): unsupported filesystem entry`,
|
|
1849
|
+
);
|
|
1540
1850
|
continue;
|
|
1541
1851
|
}
|
|
1542
1852
|
const kind = "real dir";
|
|
1543
|
-
const marked = candidate.alreadyMarked
|
|
1544
|
-
|
|
1853
|
+
const marked = candidate.alreadyMarked
|
|
1854
|
+
? ", already skillmux-managed"
|
|
1855
|
+
: "";
|
|
1856
|
+
console.log(
|
|
1857
|
+
`${name} (${candidate.path}): ${kind}, ${candidate.skillCount} skills${marked}`,
|
|
1858
|
+
);
|
|
1545
1859
|
}
|
|
1546
|
-
for (const readiness of assessClientReadiness(
|
|
1860
|
+
for (const readiness of assessClientReadiness(
|
|
1861
|
+
clientPlan,
|
|
1862
|
+
instructionReadiness,
|
|
1863
|
+
)) {
|
|
1547
1864
|
console.log(`\n${readiness.client} readiness:`);
|
|
1548
|
-
console.log(
|
|
1549
|
-
|
|
1550
|
-
|
|
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
|
+
);
|
|
1551
1874
|
}
|
|
1552
1875
|
for (const change of instructionPlan.changes) {
|
|
1553
1876
|
console.log(
|
|
@@ -1578,20 +1901,28 @@ async function runInit(
|
|
|
1578
1901
|
|
|
1579
1902
|
const byName = new Map(
|
|
1580
1903
|
candidates
|
|
1581
|
-
.filter(
|
|
1582
|
-
candidate
|
|
1583
|
-
|
|
1904
|
+
.filter(
|
|
1905
|
+
(candidate) =>
|
|
1906
|
+
candidate.deliveryMode === "managed-pins" ||
|
|
1907
|
+
(migrateFullVault && candidate.state === "full-vault"),
|
|
1584
1908
|
)
|
|
1585
|
-
.map(
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1909
|
+
.map(
|
|
1910
|
+
(candidate) =>
|
|
1911
|
+
[
|
|
1912
|
+
targetByPath.get(candidate.path) ??
|
|
1913
|
+
deriveTargetName(candidate.path),
|
|
1914
|
+
candidate,
|
|
1915
|
+
] as const,
|
|
1916
|
+
),
|
|
1589
1917
|
);
|
|
1590
1918
|
const allCandidatesByName = new Map(
|
|
1591
|
-
candidates.map(
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1919
|
+
candidates.map(
|
|
1920
|
+
(candidate) =>
|
|
1921
|
+
[
|
|
1922
|
+
targetByPath.get(candidate.path) ?? deriveTargetName(candidate.path),
|
|
1923
|
+
candidate,
|
|
1924
|
+
] as const,
|
|
1925
|
+
),
|
|
1595
1926
|
);
|
|
1596
1927
|
for (const name of requestedTargets) {
|
|
1597
1928
|
if (!byName.has(name)) {
|
|
@@ -1600,7 +1931,9 @@ async function runInit(
|
|
|
1600
1931
|
`target "${name}" is a full-vault surface; re-run with --migrate-full-vault to convert it to managed pins`,
|
|
1601
1932
|
);
|
|
1602
1933
|
}
|
|
1603
|
-
throw new Error(
|
|
1934
|
+
throw new Error(
|
|
1935
|
+
`unknown --target "${name}": not among detected surfaces`,
|
|
1936
|
+
);
|
|
1604
1937
|
}
|
|
1605
1938
|
}
|
|
1606
1939
|
|
|
@@ -1612,7 +1945,11 @@ async function runInit(
|
|
|
1612
1945
|
...(candidate.state === "full-vault" ? { migrateFullVault: true } : {}),
|
|
1613
1946
|
};
|
|
1614
1947
|
});
|
|
1615
|
-
const plannedManifest = planInitManifest(
|
|
1948
|
+
const plannedManifest = planInitManifest(
|
|
1949
|
+
vaultPath,
|
|
1950
|
+
confirmedTargets,
|
|
1951
|
+
selectedCoreSkillIds,
|
|
1952
|
+
);
|
|
1616
1953
|
const serializedPlan = {
|
|
1617
1954
|
vault_path: vaultPath,
|
|
1618
1955
|
config: configPlan
|
|
@@ -1630,44 +1967,50 @@ async function runInit(
|
|
|
1630
1967
|
};
|
|
1631
1968
|
if (!hasChanges) {
|
|
1632
1969
|
if (options.isJson) {
|
|
1633
|
-
console.log(
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
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
|
+
);
|
|
1642
1981
|
} else {
|
|
1643
1982
|
console.log("\nno managed-pins surface selected — nothing written.");
|
|
1644
1983
|
}
|
|
1645
1984
|
return;
|
|
1646
1985
|
}
|
|
1647
1986
|
if (!options.isJson) {
|
|
1648
|
-
for (const target of confirmedTargets.filter(
|
|
1987
|
+
for (const target of confirmedTargets.filter(
|
|
1988
|
+
(target) => target.migrateFullVault,
|
|
1989
|
+
)) {
|
|
1649
1990
|
console.log(
|
|
1650
1991
|
`full-vault migration ${target.name}: ${vaultHealth.skillCount} visible skills -> ` +
|
|
1651
|
-
|
|
1992
|
+
`${plannedManifest.core.skills.length} core ${plannedManifest.core.skills.length === 1 ? "skill" : "skills"} after sync`,
|
|
1652
1993
|
);
|
|
1653
1994
|
}
|
|
1654
1995
|
}
|
|
1655
1996
|
if (options.dryRun) {
|
|
1656
1997
|
if (options.isJson) {
|
|
1657
|
-
console.log(
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
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
|
+
);
|
|
1666
2009
|
} else {
|
|
1667
2010
|
console.log(
|
|
1668
2011
|
`\ndry-run: ${confirmedTargets.length} target(s), ` +
|
|
1669
|
-
|
|
1670
|
-
|
|
2012
|
+
`${instructionPlan.changes.filter((change) => change.status !== "unchanged").length} instruction file(s), ` +
|
|
2013
|
+
`core: ${plannedManifest.core.skills.join(", ") || "(unchanged)"}`,
|
|
1671
2014
|
);
|
|
1672
2015
|
}
|
|
1673
2016
|
return;
|
|
@@ -1684,7 +2027,9 @@ async function runInit(
|
|
|
1684
2027
|
console.log(
|
|
1685
2028
|
` instructions: ${instructionPlan.changes.filter((change) => change.status !== "unchanged").length} file(s)`,
|
|
1686
2029
|
);
|
|
1687
|
-
console.log(
|
|
2030
|
+
console.log(
|
|
2031
|
+
` core: ${plannedManifest.core.skills.join(", ") || "(none)"}`,
|
|
2032
|
+
);
|
|
1688
2033
|
console.log(` sync: ${sync ? "yes" : "no"}`);
|
|
1689
2034
|
if (!(await confirmAction("Apply this setup plan?"))) {
|
|
1690
2035
|
console.log("init cancelled");
|
|
@@ -1692,10 +2037,14 @@ async function runInit(
|
|
|
1692
2037
|
}
|
|
1693
2038
|
} else {
|
|
1694
2039
|
const prompts = [
|
|
1695
|
-
...confirmedTargets.map(
|
|
2040
|
+
...confirmedTargets.map(
|
|
2041
|
+
(target) => `Adopt ${target.name} at ${target.dir}?`,
|
|
2042
|
+
),
|
|
1696
2043
|
...instructionPlan.changes
|
|
1697
2044
|
.filter((change) => change.status !== "unchanged")
|
|
1698
|
-
.map(
|
|
2045
|
+
.map(
|
|
2046
|
+
(change) => `${change.status} instruction file ${change.path}?`,
|
|
2047
|
+
),
|
|
1699
2048
|
...(hasConfigWrite ? [`Create machine config ${configPath}?`] : []),
|
|
1700
2049
|
...(selectedCoreSkillIds.length > 0
|
|
1701
2050
|
? [`Pin core skills: ${selectedCoreSkillIds.join(", ")}?`]
|
|
@@ -1754,42 +2103,53 @@ async function runInit(
|
|
|
1754
2103
|
}
|
|
1755
2104
|
|
|
1756
2105
|
if (options.isJson) {
|
|
1757
|
-
console.log(
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
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
|
+
);
|
|
1774
2125
|
return;
|
|
1775
2126
|
}
|
|
1776
2127
|
if (configCreated) console.log(`created ${configPath}`);
|
|
1777
2128
|
if (confirmedTargets.length > 0) {
|
|
1778
|
-
console.log(
|
|
2129
|
+
console.log(
|
|
2130
|
+
`\nwrote ${join(vaultPath, "skillmux.toml")}, adopted: ${confirmedTargets.map((t) => t.name).join(", ")}`,
|
|
2131
|
+
);
|
|
1779
2132
|
} else if (selectedCoreSkillIds.length > 0) {
|
|
1780
2133
|
console.log(`\nwrote ${join(vaultPath, "skillmux.toml")}`);
|
|
1781
2134
|
}
|
|
1782
2135
|
if (plannedManifest.core.skills.length === 0 && confirmedTargets.length > 0) {
|
|
1783
|
-
console.log("next: skillmux
|
|
2136
|
+
console.log("next: skillmux core pin <skill_id> --yes");
|
|
1784
2137
|
}
|
|
1785
2138
|
if (confirmedTargets.length > 0) console.log("next: skillmux sync");
|
|
1786
|
-
if (
|
|
2139
|
+
if (
|
|
2140
|
+
selectedClients.length === 0 ||
|
|
2141
|
+
selectedClients.includes("skillmux-mcp")
|
|
2142
|
+
) {
|
|
1787
2143
|
console.log(`\n${printLastMile()}`);
|
|
1788
2144
|
}
|
|
1789
2145
|
if (guided && sync && confirmedTargets.length > 0) await runSync([]);
|
|
1790
2146
|
}
|
|
1791
2147
|
|
|
1792
|
-
function parseReportArgs(args: string[]): {
|
|
2148
|
+
function parseReportArgs(args: string[]): {
|
|
2149
|
+
server?: string;
|
|
2150
|
+
db?: string;
|
|
2151
|
+
since?: string;
|
|
2152
|
+
} {
|
|
1793
2153
|
let server: string | undefined;
|
|
1794
2154
|
let db: string | undefined;
|
|
1795
2155
|
let since: string | undefined;
|
|
@@ -1808,6 +2168,8 @@ function parseReportArgs(args: string[]): { server?: string; db?: string; since?
|
|
|
1808
2168
|
if (!value) throw new Error("--since requires a window");
|
|
1809
2169
|
since = value;
|
|
1810
2170
|
i++;
|
|
2171
|
+
} else if (option === "--json") {
|
|
2172
|
+
// handled globally by main()'s isJson flag; recognized here so it isn't rejected
|
|
1811
2173
|
} else {
|
|
1812
2174
|
throw new Error(`unknown report option: ${option}`);
|
|
1813
2175
|
}
|
|
@@ -1816,24 +2178,45 @@ function parseReportArgs(args: string[]): { server?: string; db?: string; since?
|
|
|
1816
2178
|
return { server, db, since };
|
|
1817
2179
|
}
|
|
1818
2180
|
|
|
1819
|
-
async function runReport(
|
|
2181
|
+
async function runReport(
|
|
2182
|
+
args: string[],
|
|
2183
|
+
options: { isJson: boolean },
|
|
2184
|
+
): Promise<void> {
|
|
1820
2185
|
const { server, db: dbPath, since } = parseReportArgs(args);
|
|
1821
|
-
if (!since)
|
|
2186
|
+
if (!since)
|
|
2187
|
+
throw new Error(
|
|
2188
|
+
"usage: skillmux report [--server <url> | --db <path>] --since <window> [--json]",
|
|
2189
|
+
);
|
|
1822
2190
|
|
|
1823
2191
|
if (server) {
|
|
1824
2192
|
const url = `${server.replace(/\/$/, "")}/stats?since=${encodeURIComponent(since)}`;
|
|
1825
2193
|
const res = await fetch(url);
|
|
1826
|
-
if (!res.ok)
|
|
1827
|
-
|
|
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
|
+
);
|
|
1828
2202
|
return;
|
|
1829
2203
|
}
|
|
1830
2204
|
|
|
1831
|
-
const db = dbPath
|
|
1832
|
-
|
|
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
|
+
);
|
|
1833
2212
|
db.close();
|
|
1834
2213
|
}
|
|
1835
2214
|
|
|
1836
|
-
function parseScanArgs(args: string[]): {
|
|
2215
|
+
function parseScanArgs(args: string[]): {
|
|
2216
|
+
path?: string;
|
|
2217
|
+
format: "text" | "json";
|
|
2218
|
+
failOn?: ScanSeverity;
|
|
2219
|
+
} {
|
|
1837
2220
|
let path: string | undefined;
|
|
1838
2221
|
let format: "text" | "json" = "text";
|
|
1839
2222
|
let failOn: ScanSeverity | undefined;
|
|
@@ -1841,7 +2224,8 @@ function parseScanArgs(args: string[]): { path?: string; format: "text" | "json"
|
|
|
1841
2224
|
const option = args[i];
|
|
1842
2225
|
if (option === "--format") {
|
|
1843
2226
|
const value = args[++i];
|
|
1844
|
-
if (value !== "text" && value !== "json")
|
|
2227
|
+
if (value !== "text" && value !== "json")
|
|
2228
|
+
throw new Error("--format must be text or json");
|
|
1845
2229
|
format = value;
|
|
1846
2230
|
} else if (option === "--fail-on") {
|
|
1847
2231
|
const value = args[++i];
|
|
@@ -1849,6 +2233,8 @@ function parseScanArgs(args: string[]): { path?: string; format: "text" | "json"
|
|
|
1849
2233
|
throw new Error("--fail-on must be low, medium, or high");
|
|
1850
2234
|
}
|
|
1851
2235
|
failOn = value;
|
|
2236
|
+
} else if (option === "--json") {
|
|
2237
|
+
// handled globally by main()'s isJson flag; recognized here so it isn't rejected
|
|
1852
2238
|
} else if (option?.startsWith("--")) {
|
|
1853
2239
|
throw new Error(`unknown scan option: ${option}`);
|
|
1854
2240
|
} else if (path !== undefined) {
|
|
@@ -1860,11 +2246,20 @@ function parseScanArgs(args: string[]): { path?: string; format: "text" | "json"
|
|
|
1860
2246
|
return { path, format, failOn };
|
|
1861
2247
|
}
|
|
1862
2248
|
|
|
1863
|
-
async function runScan(
|
|
2249
|
+
async function runScan(
|
|
2250
|
+
args: string[],
|
|
2251
|
+
options: { isJson: boolean },
|
|
2252
|
+
): Promise<void> {
|
|
1864
2253
|
const { path, format, failOn } = parseScanArgs(args);
|
|
1865
|
-
const rootPath = path
|
|
2254
|
+
const rootPath = path
|
|
2255
|
+
? expandHome(path)
|
|
2256
|
+
: expandHome((await loadConfig()).vault_path);
|
|
1866
2257
|
const result = await scanPath(rootPath);
|
|
1867
|
-
|
|
2258
|
+
emitSuccess({ isJson: options.isJson }, result, () => {
|
|
2259
|
+
console.log(
|
|
2260
|
+
format === "json" ? renderScanJson(result) : renderScanText(result),
|
|
2261
|
+
);
|
|
2262
|
+
});
|
|
1868
2263
|
process.exitCode = scanExitCode(result.findings, failOn);
|
|
1869
2264
|
}
|
|
1870
2265
|
|
|
@@ -1888,6 +2283,8 @@ function parseInstallArgs(args: string[]): {
|
|
|
1888
2283
|
throw new Error("--fail-on must be low, medium, or high");
|
|
1889
2284
|
}
|
|
1890
2285
|
failOn = value;
|
|
2286
|
+
} else if (option === "--json") {
|
|
2287
|
+
// handled globally by main()'s isJson flag; recognized here so it isn't rejected
|
|
1891
2288
|
} else if (option?.startsWith("--")) {
|
|
1892
2289
|
throw new Error(`unknown install option: ${option}`);
|
|
1893
2290
|
} else if (repo !== undefined) {
|
|
@@ -1899,39 +2296,73 @@ function parseInstallArgs(args: string[]): {
|
|
|
1899
2296
|
return { repo, force, dryRun, failOn };
|
|
1900
2297
|
}
|
|
1901
2298
|
|
|
1902
|
-
async function runInstall(
|
|
2299
|
+
async function runInstall(
|
|
2300
|
+
args: string[],
|
|
2301
|
+
options: { isJson: boolean },
|
|
2302
|
+
): Promise<void> {
|
|
1903
2303
|
const { repo, force, dryRun, failOn } = parseInstallArgs(args);
|
|
1904
2304
|
if (!repo) {
|
|
1905
|
-
throw new Error(
|
|
2305
|
+
throw new Error(
|
|
2306
|
+
"usage: skillmux install <repo>[/path] [--force] [--fail-on low|medium|high] [--dry-run] [--json]",
|
|
2307
|
+
);
|
|
1906
2308
|
}
|
|
1907
2309
|
|
|
1908
2310
|
const source = resolveRepoSource(repo);
|
|
1909
2311
|
const cloneDir = await cloneToTemp(source.url);
|
|
1910
2312
|
try {
|
|
1911
|
-
const resolved = resolveSkillDir(
|
|
1912
|
-
|
|
1913
|
-
|
|
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 }));
|
|
1914
2323
|
|
|
1915
2324
|
if (scanExitCode(findings, failOn) !== 0) {
|
|
1916
2325
|
process.exitCode = 1;
|
|
1917
|
-
console.error(
|
|
2326
|
+
console.error(
|
|
2327
|
+
`aborting install: a finding met the --fail-on ${failOn} threshold`,
|
|
2328
|
+
);
|
|
1918
2329
|
return;
|
|
1919
2330
|
}
|
|
1920
2331
|
|
|
1921
2332
|
const vaultPath = expandHome((await loadConfig()).vault_path);
|
|
1922
2333
|
if (dryRun) {
|
|
1923
|
-
|
|
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
|
+
);
|
|
1924
2343
|
return;
|
|
1925
2344
|
}
|
|
1926
2345
|
|
|
1927
|
-
const targetDir = installIntoVault(
|
|
1928
|
-
|
|
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
|
+
);
|
|
1929
2357
|
} finally {
|
|
1930
2358
|
rmSync(cloneDir, { recursive: true, force: true });
|
|
1931
2359
|
}
|
|
1932
2360
|
}
|
|
1933
2361
|
|
|
1934
|
-
function parseCalibrateGenerateDatasetArgs(args: string[]): {
|
|
2362
|
+
function parseCalibrateGenerateDatasetArgs(args: string[]): {
|
|
2363
|
+
vault?: string;
|
|
2364
|
+
out?: string;
|
|
2365
|
+
} {
|
|
1935
2366
|
let vault: string | undefined;
|
|
1936
2367
|
let out: string | undefined;
|
|
1937
2368
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -1952,7 +2383,8 @@ function parseCalibrateGenerateDatasetArgs(args: string[]): { vault?: string; ou
|
|
|
1952
2383
|
}
|
|
1953
2384
|
|
|
1954
2385
|
async function runCalibrateGenerateDataset(args: string[]): Promise<void> {
|
|
1955
|
-
const { vault: vaultArg, out: outArg } =
|
|
2386
|
+
const { vault: vaultArg, out: outArg } =
|
|
2387
|
+
parseCalibrateGenerateDatasetArgs(args);
|
|
1956
2388
|
const config = await loadConfig();
|
|
1957
2389
|
const vaultPath = expandHome(vaultArg ?? config.vault_path);
|
|
1958
2390
|
const outPath = expandHome(outArg ?? join(config.state_dir, "queries.json"));
|
|
@@ -1963,7 +2395,9 @@ async function runCalibrateGenerateDataset(args: string[]): Promise<void> {
|
|
|
1963
2395
|
const parentDir = join(outPath, "..");
|
|
1964
2396
|
mkdirSync(parentDir, { recursive: true });
|
|
1965
2397
|
await Bun.write(outPath, JSON.stringify(dataset, null, 2) + "\n");
|
|
1966
|
-
console.log(
|
|
2398
|
+
console.log(
|
|
2399
|
+
`generated synthetic dataset with ${dataset.length} cases at ${outPath}`,
|
|
2400
|
+
);
|
|
1967
2401
|
}
|
|
1968
2402
|
|
|
1969
2403
|
if (import.meta.main) {
|