@klhapp/skillmux 0.6.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -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/calibrate.ts +7 -59
- package/src/cli.ts +521 -869
- package/src/commands/config.ts +202 -0
- package/src/commands/core.ts +52 -0
- package/src/commands/project.ts +412 -0
- package/src/commands/shared.ts +45 -0
- package/src/commands/target.ts +110 -0
- package/src/completions.ts +69 -55
- package/src/config-mutation.ts +65 -0
- 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
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
import { Database } from "bun:sqlite";
|
|
3
3
|
import { existsSync, lstatSync, mkdirSync, rmSync } from "node:fs";
|
|
4
4
|
import { hostname } from "node:os";
|
|
5
|
-
import {
|
|
6
|
-
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
7
6
|
import { generateDataset } from "./dataset-generator";
|
|
8
7
|
|
|
9
8
|
import { createClients } from "./clients";
|
|
10
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
expandHome,
|
|
11
|
+
loadConfig,
|
|
12
|
+
migrateLegacyPaths,
|
|
13
|
+
resolveConfigPath,
|
|
14
|
+
} from "./config";
|
|
11
15
|
import { openIndex } from "./db";
|
|
12
16
|
import { diagnose } from "./doctor";
|
|
13
17
|
import { evalVault } from "./eval";
|
|
@@ -43,23 +47,26 @@ import {
|
|
|
43
47
|
} from "./install";
|
|
44
48
|
import {
|
|
45
49
|
parseManifest,
|
|
46
|
-
pinCore,
|
|
47
|
-
pinProject,
|
|
48
50
|
resolveManifestPath,
|
|
49
51
|
serializeManifest,
|
|
50
|
-
unpinCore,
|
|
51
|
-
unpinProject,
|
|
52
|
-
upsertProject,
|
|
53
|
-
updateProjectPaths,
|
|
54
|
-
updateProjectTargets,
|
|
55
52
|
validateManifest,
|
|
56
|
-
writeManifestAtomic,
|
|
57
53
|
} from "./manifest";
|
|
58
54
|
import { downloadLocalModels } from "./models";
|
|
59
|
-
|
|
60
|
-
import {
|
|
55
|
+
|
|
56
|
+
import {
|
|
57
|
+
parseCommaList,
|
|
58
|
+
promptMultiSelect,
|
|
59
|
+
promptText,
|
|
60
|
+
shouldUseWizard,
|
|
61
|
+
} from "./prompts";
|
|
61
62
|
import { backfillEmbeddings, configure, rebuildIndex } from "./router-core";
|
|
62
|
-
import {
|
|
63
|
+
import {
|
|
64
|
+
renderScanJson,
|
|
65
|
+
renderScanText,
|
|
66
|
+
scanExitCode,
|
|
67
|
+
scanPath,
|
|
68
|
+
type ScanSeverity,
|
|
69
|
+
} from "./scan";
|
|
63
70
|
import {
|
|
64
71
|
applyConfigInit,
|
|
65
72
|
inspectVault,
|
|
@@ -87,8 +94,21 @@ import {
|
|
|
87
94
|
type ResolvedTarget,
|
|
88
95
|
} from "./context";
|
|
89
96
|
import { createTargetAdapter, type TargetAdapter } from "./adapters";
|
|
90
|
-
import {
|
|
97
|
+
import {
|
|
98
|
+
emitSuccess,
|
|
99
|
+
formatJsonEnvelope,
|
|
100
|
+
isInteractive,
|
|
101
|
+
mapExitCode,
|
|
102
|
+
renderTable,
|
|
103
|
+
renderTargetBanner,
|
|
104
|
+
suggestCorrection,
|
|
105
|
+
} from "./output";
|
|
91
106
|
import { generateCompletions, type ShellType } from "./completions";
|
|
107
|
+
import { handleConfigCommand } from "./commands/config";
|
|
108
|
+
import { runCore } from "./commands/core";
|
|
109
|
+
import { configuredTargetForSurface, runProject } from "./commands/project";
|
|
110
|
+
import { confirmAction, confirmIfNeeded } from "./commands/shared";
|
|
111
|
+
import { runTarget } from "./commands/target";
|
|
92
112
|
|
|
93
113
|
const KNOWN_COMMANDS = [
|
|
94
114
|
"context",
|
|
@@ -101,14 +121,14 @@ const KNOWN_COMMANDS = [
|
|
|
101
121
|
"init",
|
|
102
122
|
"project",
|
|
103
123
|
"target",
|
|
124
|
+
"core",
|
|
104
125
|
"report",
|
|
105
126
|
"scan",
|
|
106
127
|
"install",
|
|
107
128
|
"eval",
|
|
108
129
|
"doctor",
|
|
109
130
|
"models",
|
|
110
|
-
"
|
|
111
|
-
"manifest",
|
|
131
|
+
"skill",
|
|
112
132
|
"local-vault",
|
|
113
133
|
];
|
|
114
134
|
|
|
@@ -143,9 +163,17 @@ async function main() {
|
|
|
143
163
|
|
|
144
164
|
// Only resolve target if command is target-aware or context/config/calibrate
|
|
145
165
|
const isLocalConfigInit = command === "config" && rawArgv[1] === "init";
|
|
146
|
-
if (
|
|
166
|
+
if (
|
|
167
|
+
(["context", "config", "calibrate"].includes(command) &&
|
|
168
|
+
!isLocalConfigInit) ||
|
|
169
|
+
flagContext ||
|
|
170
|
+
flagServer
|
|
171
|
+
) {
|
|
147
172
|
try {
|
|
148
|
-
resolvedTarget = await resolveTarget({
|
|
173
|
+
resolvedTarget = await resolveTarget({
|
|
174
|
+
context: flagContext,
|
|
175
|
+
server: flagServer,
|
|
176
|
+
});
|
|
149
177
|
} catch (err: any) {
|
|
150
178
|
handleError(err, { target: resolvedTarget, isJson, isVerbose });
|
|
151
179
|
return;
|
|
@@ -159,13 +187,23 @@ async function main() {
|
|
|
159
187
|
try {
|
|
160
188
|
switch (command) {
|
|
161
189
|
case "context":
|
|
162
|
-
await handleContextCommand(subCommand, commandArgs, {
|
|
190
|
+
await handleContextCommand(subCommand, commandArgs, {
|
|
191
|
+
target: resolvedTarget,
|
|
192
|
+
isJson,
|
|
193
|
+
});
|
|
163
194
|
break;
|
|
164
195
|
case "config":
|
|
165
|
-
await handleConfigCommand(adapter, subCommand, commandArgs, {
|
|
196
|
+
await handleConfigCommand(adapter, subCommand, commandArgs, {
|
|
197
|
+
target: resolvedTarget,
|
|
198
|
+
isJson,
|
|
199
|
+
dryRun: isDryRun,
|
|
200
|
+
});
|
|
166
201
|
break;
|
|
167
202
|
case "calibrate":
|
|
168
|
-
await handleCalibrateCommand(adapter, subCommand, rawArgv.slice(1), {
|
|
203
|
+
await handleCalibrateCommand(adapter, subCommand, rawArgv.slice(1), {
|
|
204
|
+
target: resolvedTarget,
|
|
205
|
+
isJson,
|
|
206
|
+
});
|
|
169
207
|
break;
|
|
170
208
|
case "completions":
|
|
171
209
|
await handleCompletionsCommand(subCommand);
|
|
@@ -202,45 +240,59 @@ async function main() {
|
|
|
202
240
|
await runInit(rawArgv.slice(1), { isJson, dryRun: isDryRun });
|
|
203
241
|
break;
|
|
204
242
|
case "project":
|
|
205
|
-
await runProject(subCommand, commandArgs, {
|
|
243
|
+
await runProject(subCommand, commandArgs, {
|
|
244
|
+
isJson,
|
|
245
|
+
dryRun: isDryRun,
|
|
246
|
+
sync: runSync,
|
|
247
|
+
});
|
|
206
248
|
break;
|
|
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,198 +378,24 @@ 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
|
|
|
334
|
-
async function handleConfigCommand(
|
|
335
|
-
adapter: TargetAdapter,
|
|
336
|
-
sub: string,
|
|
337
|
-
args: string[],
|
|
338
|
-
ctx: { target: ResolvedTarget; isJson: boolean; dryRun: boolean }
|
|
339
|
-
) {
|
|
340
|
-
if (sub === "init") {
|
|
341
|
-
let vaultPath: string | undefined;
|
|
342
|
-
let yes = false;
|
|
343
|
-
for (let i = 0; i < args.length; i++) {
|
|
344
|
-
const option = args[i];
|
|
345
|
-
if (option === "--vault") {
|
|
346
|
-
vaultPath = args[++i];
|
|
347
|
-
if (!vaultPath) throw new Error("usage: skillmux config init --vault <path> --yes");
|
|
348
|
-
} else if (option === "--yes") {
|
|
349
|
-
yes = true;
|
|
350
|
-
} else if (option === "--dry-run" || option === "--json") {
|
|
351
|
-
continue;
|
|
352
|
-
} else {
|
|
353
|
-
throw new Error(`unknown config init option: ${option}`);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
if (!vaultPath) {
|
|
357
|
-
if (isInteractive() && !ctx.isJson) {
|
|
358
|
-
vaultPath = "~/skills";
|
|
359
|
-
} else {
|
|
360
|
-
throw new Error("usage: skillmux config init --vault <path> --yes");
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
migrateLegacyPaths();
|
|
365
|
-
const plan = planConfigInit(resolveConfigPath(), expandHome(vaultPath));
|
|
366
|
-
if (plan.action === "preserve") {
|
|
367
|
-
console.log(ctx.isJson
|
|
368
|
-
? JSON.stringify({
|
|
369
|
-
schema_version: 1,
|
|
370
|
-
ok: true,
|
|
371
|
-
command: "config init",
|
|
372
|
-
phase: "result",
|
|
373
|
-
dry_run: ctx.dryRun,
|
|
374
|
-
applied: false,
|
|
375
|
-
plan: { config_path: plan.configPath, vault_path: plan.vaultPath, action: "preserve" },
|
|
376
|
-
})
|
|
377
|
-
: `preserved existing config: ${plan.configPath}`);
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
if (ctx.dryRun) {
|
|
381
|
-
console.log(ctx.isJson
|
|
382
|
-
? JSON.stringify({
|
|
383
|
-
schema_version: 1,
|
|
384
|
-
ok: true,
|
|
385
|
-
command: "config init",
|
|
386
|
-
phase: "plan",
|
|
387
|
-
dry_run: true,
|
|
388
|
-
applied: false,
|
|
389
|
-
plan: { config_path: plan.configPath, vault_path: plan.vaultPath, action: "create" },
|
|
390
|
-
})
|
|
391
|
-
: `config create: ${plan.configPath} (dry-run)`);
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
if (!yes) {
|
|
395
|
-
if (!ctx.isJson && isInteractive()) {
|
|
396
|
-
if (!(await confirmAction(`Create ${plan.configPath} with vault_path ${plan.vaultPath}?`))) {
|
|
397
|
-
console.log("config init cancelled; nothing written");
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
} else {
|
|
401
|
-
throw new Error("config initialization requires --yes in noninteractive mode");
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const result = applyConfigInit(plan);
|
|
406
|
-
console.log(ctx.isJson
|
|
407
|
-
? JSON.stringify({
|
|
408
|
-
schema_version: 1,
|
|
409
|
-
ok: true,
|
|
410
|
-
command: "config init",
|
|
411
|
-
phase: "result",
|
|
412
|
-
dry_run: false,
|
|
413
|
-
applied: result === "created",
|
|
414
|
-
plan: { config_path: plan.configPath, vault_path: plan.vaultPath, action: plan.action },
|
|
415
|
-
})
|
|
416
|
-
: result === "created"
|
|
417
|
-
? `created ${plan.configPath}`
|
|
418
|
-
: `preserved existing config: ${plan.configPath}`);
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
if (sub === "show") {
|
|
423
|
-
const data = await adapter.getConfigShow();
|
|
424
|
-
if (ctx.isJson) {
|
|
425
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data })));
|
|
426
|
-
} else {
|
|
427
|
-
renderTargetBanner(ctx.target);
|
|
428
|
-
console.log(JSON.stringify(data.effective, null, 2));
|
|
429
|
-
}
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (sub === "get") {
|
|
434
|
-
const key = args[0];
|
|
435
|
-
if (!key) throw new Error("usage: skillmux config get <key>");
|
|
436
|
-
const val = await adapter.getConfigGet(key);
|
|
437
|
-
if (ctx.isJson) {
|
|
438
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: { key, value: val } })));
|
|
439
|
-
} else {
|
|
440
|
-
console.log(typeof val === "object" ? JSON.stringify(val) : String(val));
|
|
441
|
-
}
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (sub === "validate") {
|
|
446
|
-
const res = await adapter.configValidate();
|
|
447
|
-
if (ctx.isJson) {
|
|
448
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
449
|
-
} else {
|
|
450
|
-
console.log(res.valid ? "Configuration is valid." : "Configuration is invalid.");
|
|
451
|
-
}
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (sub === "diff") {
|
|
456
|
-
const res = await adapter.configDiff();
|
|
457
|
-
if (ctx.isJson) {
|
|
458
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
459
|
-
} else {
|
|
460
|
-
renderTargetBanner(ctx.target);
|
|
461
|
-
console.log(JSON.stringify(res.diff, null, 2));
|
|
462
|
-
}
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
if (sub === "set") {
|
|
467
|
-
const key = args[0];
|
|
468
|
-
const value = args[1];
|
|
469
|
-
if (!key || value === undefined) {
|
|
470
|
-
throw new Error("usage: skillmux config set <key> <value> [--dry-run]");
|
|
471
|
-
}
|
|
472
|
-
const res = await adapter.configSet(key, value, { dryRun: ctx.dryRun });
|
|
473
|
-
if (ctx.isJson) {
|
|
474
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
475
|
-
} else {
|
|
476
|
-
renderTargetBanner(ctx.target);
|
|
477
|
-
const prefix = ctx.dryRun ? "[dry-run] " : "";
|
|
478
|
-
console.log(`${prefix}${key}: ${JSON.stringify(res.prior_val)} -> ${JSON.stringify(res.resulting_val)}`);
|
|
479
|
-
console.log(`Persistence: ${res.persistence}, Application: ${res.application}`);
|
|
480
|
-
}
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (sub === "status") {
|
|
485
|
-
const res = await adapter.configStatus();
|
|
486
|
-
if (ctx.isJson) {
|
|
487
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
488
|
-
} else {
|
|
489
|
-
renderTargetBanner(ctx.target);
|
|
490
|
-
console.log(`Runtime: ${res.runtime}`);
|
|
491
|
-
console.log(`Active revision: ${res.active_revision}`);
|
|
492
|
-
console.log(`Readiness: ${res.readiness.status}`);
|
|
493
|
-
}
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
throw new Error("usage: skillmux config show");
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
async function confirmAction(prompt: string): Promise<boolean> {
|
|
501
|
-
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
502
|
-
try {
|
|
503
|
-
const answer = (await readline.question(`${prompt} [y/N] `)).trim().toLowerCase();
|
|
504
|
-
return answer === "y" || answer === "yes";
|
|
505
|
-
} finally {
|
|
506
|
-
readline.close();
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
394
|
async function handleCalibrateCommand(
|
|
511
395
|
adapter: TargetAdapter,
|
|
512
396
|
sub: string,
|
|
513
397
|
args: string[],
|
|
514
|
-
ctx: { target: ResolvedTarget; isJson: boolean }
|
|
398
|
+
ctx: { target: ResolvedTarget; isJson: boolean },
|
|
515
399
|
) {
|
|
516
400
|
if (sub === "run") {
|
|
517
401
|
let datasetPath: string | undefined;
|
|
@@ -519,21 +403,17 @@ async function handleCalibrateCommand(
|
|
|
519
403
|
if (args[i] === "--dataset") datasetPath = args[++i];
|
|
520
404
|
}
|
|
521
405
|
const res = await adapter.calibrateRun({ datasetPath });
|
|
522
|
-
|
|
523
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
524
|
-
} else {
|
|
406
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
525
407
|
renderTargetBanner(ctx.target);
|
|
526
408
|
console.log(`Calibration run complete.`);
|
|
527
409
|
if (res.result) console.log(JSON.stringify(res.result, null, 2));
|
|
528
|
-
}
|
|
410
|
+
});
|
|
529
411
|
return;
|
|
530
412
|
}
|
|
531
413
|
|
|
532
414
|
if (sub === "list") {
|
|
533
415
|
const res = await adapter.calibrateList();
|
|
534
|
-
|
|
535
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
536
|
-
} else {
|
|
416
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
537
417
|
renderTargetBanner(ctx.target);
|
|
538
418
|
renderTable(
|
|
539
419
|
[
|
|
@@ -541,9 +421,9 @@ async function handleCalibrateCommand(
|
|
|
541
421
|
{ key: "created_at", header: "CREATED_AT" },
|
|
542
422
|
{ key: "status", header: "STATUS" },
|
|
543
423
|
],
|
|
544
|
-
res
|
|
424
|
+
res,
|
|
545
425
|
);
|
|
546
|
-
}
|
|
426
|
+
});
|
|
547
427
|
return;
|
|
548
428
|
}
|
|
549
429
|
|
|
@@ -551,12 +431,10 @@ async function handleCalibrateCommand(
|
|
|
551
431
|
const runId = args[1];
|
|
552
432
|
if (!runId) throw new Error("usage: skillmux calibrate show <run_id>");
|
|
553
433
|
const res = await adapter.calibrateShow(runId);
|
|
554
|
-
|
|
555
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
556
|
-
} else {
|
|
434
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
557
435
|
renderTargetBanner(ctx.target);
|
|
558
436
|
console.log(JSON.stringify(res, null, 2));
|
|
559
|
-
}
|
|
437
|
+
});
|
|
560
438
|
return;
|
|
561
439
|
}
|
|
562
440
|
|
|
@@ -564,12 +442,10 @@ async function handleCalibrateCommand(
|
|
|
564
442
|
const runId = args[1];
|
|
565
443
|
if (!runId) throw new Error("usage: skillmux calibrate apply <run_id>");
|
|
566
444
|
const res = await adapter.calibrateApply(runId);
|
|
567
|
-
|
|
568
|
-
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, target: ctx.target, data: res })));
|
|
569
|
-
} else {
|
|
445
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
570
446
|
renderTargetBanner(ctx.target);
|
|
571
447
|
console.log(`Applied calibration run "${runId}"`);
|
|
572
|
-
}
|
|
448
|
+
});
|
|
573
449
|
return;
|
|
574
450
|
}
|
|
575
451
|
|
|
@@ -578,7 +454,9 @@ async function handleCalibrateCommand(
|
|
|
578
454
|
return;
|
|
579
455
|
}
|
|
580
456
|
|
|
581
|
-
throw new Error(
|
|
457
|
+
throw new Error(
|
|
458
|
+
"usage: skillmux calibrate generate-dataset [--vault <path>] [--out <file>]",
|
|
459
|
+
);
|
|
582
460
|
}
|
|
583
461
|
|
|
584
462
|
async function handleCompletionsCommand(shell: string) {
|
|
@@ -590,7 +468,7 @@ async function handleCompletionsCommand(shell: string) {
|
|
|
590
468
|
|
|
591
469
|
function handleError(
|
|
592
470
|
err: any,
|
|
593
|
-
opts: { target: ResolvedTarget; isJson: boolean; isVerbose: boolean }
|
|
471
|
+
opts: { target: ResolvedTarget; isJson: boolean; isVerbose: boolean },
|
|
594
472
|
) {
|
|
595
473
|
const code = mapExitCode(err);
|
|
596
474
|
process.exitCode = code;
|
|
@@ -605,7 +483,13 @@ function handleError(
|
|
|
605
483
|
});
|
|
606
484
|
console.log(JSON.stringify(env));
|
|
607
485
|
} else {
|
|
608
|
-
console.error(
|
|
486
|
+
console.error(
|
|
487
|
+
msg.startsWith("usage:") ||
|
|
488
|
+
msg.startsWith("Unknown") ||
|
|
489
|
+
msg.startsWith("error:")
|
|
490
|
+
? msg
|
|
491
|
+
: `error: ${msg}`,
|
|
492
|
+
);
|
|
609
493
|
if (opts.isVerbose && err instanceof Error && err.stack) {
|
|
610
494
|
console.error(err.stack);
|
|
611
495
|
}
|
|
@@ -617,7 +501,7 @@ function printHelp(): void {
|
|
|
617
501
|
|
|
618
502
|
Setup:
|
|
619
503
|
skillmux config init --vault <path> --yes
|
|
620
|
-
skillmux init [--client <name>...] [--target <name>...] [--
|
|
504
|
+
skillmux init [--client <name>...] [--target <name>...] [--dir <dir>]
|
|
621
505
|
[--vault <path>] [--core <skill_id>...]
|
|
622
506
|
[--migrate-full-vault] [--no-instructions] [--no-sync]
|
|
623
507
|
[--interactive|--yes|--dry-run] [--json]
|
|
@@ -626,6 +510,8 @@ Setup:
|
|
|
626
510
|
[--interactive|--yes|--dry-run] [--json]
|
|
627
511
|
skillmux project <list|show|add-path|remove-path|pin|unpin|attach|detach>
|
|
628
512
|
skillmux target <list|show|add|remove>
|
|
513
|
+
skillmux core <pin|unpin> <skill_id>... [--yes] [--dry-run] [--json]
|
|
514
|
+
skillmux skill which <skill_id>
|
|
629
515
|
|
|
630
516
|
Init clients:
|
|
631
517
|
claude-code, codex, gemini-cli, opencode, github-copilot, windsurf,
|
|
@@ -635,8 +521,8 @@ Init targets:
|
|
|
635
521
|
agent-skills, claude-code, codex, custom
|
|
636
522
|
|
|
637
523
|
Commands:
|
|
638
|
-
serve, index, sync, init, project, target, report, scan, install, eval, doctor,
|
|
639
|
-
|
|
524
|
+
serve, index, sync, init, project, target, core, report, scan, install, eval, doctor, skill,
|
|
525
|
+
local-vault, config, models, calibrate, context, completions`);
|
|
640
526
|
}
|
|
641
527
|
|
|
642
528
|
// ---------------------------------------------------------------------------
|
|
@@ -645,7 +531,10 @@ Commands:
|
|
|
645
531
|
|
|
646
532
|
type Transport = "stdio" | "http";
|
|
647
533
|
|
|
648
|
-
function parseServeArgs(args: string[]): {
|
|
534
|
+
function parseServeArgs(args: string[]): {
|
|
535
|
+
transport: Transport;
|
|
536
|
+
port?: number;
|
|
537
|
+
} {
|
|
649
538
|
let transport: Transport = "stdio";
|
|
650
539
|
let port: number | undefined;
|
|
651
540
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -675,7 +564,9 @@ async function runIndex(): Promise<void> {
|
|
|
675
564
|
const config = await loadConfig();
|
|
676
565
|
configure({ config, clients: createClients(config) });
|
|
677
566
|
const report = await rebuildIndex((skillId, error) => {
|
|
678
|
-
console.error(
|
|
567
|
+
console.error(
|
|
568
|
+
`warning: keeping previous index entry for ${skillId}: ${error}`,
|
|
569
|
+
);
|
|
679
570
|
});
|
|
680
571
|
const retainedNote =
|
|
681
572
|
report.retained.length > 0
|
|
@@ -687,43 +578,56 @@ async function runIndex(): Promise<void> {
|
|
|
687
578
|
const backfilled = await backfillEmbeddings();
|
|
688
579
|
console.log(`embeddings: ${backfilled} backfilled`);
|
|
689
580
|
} catch {
|
|
690
|
-
console.log(
|
|
581
|
+
console.log(
|
|
582
|
+
"embeddings: skipped (endpoint unreachable; lexical-only recall until next index)",
|
|
583
|
+
);
|
|
691
584
|
}
|
|
692
585
|
}
|
|
693
586
|
|
|
694
|
-
async function runEval(): Promise<void> {
|
|
587
|
+
async function runEval(options: { isJson: boolean }): Promise<void> {
|
|
695
588
|
const config = await loadConfig();
|
|
696
589
|
configure({ config, clients: createClients(config) });
|
|
697
590
|
|
|
698
591
|
const report = await evalVault().catch((error: unknown) => {
|
|
699
592
|
throw new Error(`eval requires local embeddings: ${String(error)}`);
|
|
700
593
|
});
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
594
|
+
emitSuccess({ isJson: options.isJson }, report, () => {
|
|
595
|
+
console.log(`holdout queries: ${report.queries}`);
|
|
596
|
+
console.log(`lexical recall@3: ${report.lexical.recall_at_3.toFixed(3)}`);
|
|
597
|
+
console.log(`lexical recall@5: ${report.lexical.recall_at_5.toFixed(3)}`);
|
|
598
|
+
console.log(`lexical MRR: ${report.lexical.mrr.toFixed(3)}`);
|
|
599
|
+
console.log(`hybrid recall@3: ${report.hybrid.recall_at_3.toFixed(3)}`);
|
|
600
|
+
console.log(`hybrid recall@5: ${report.hybrid.recall_at_5.toFixed(3)}`);
|
|
601
|
+
console.log(`hybrid MRR: ${report.hybrid.mrr.toFixed(3)}`);
|
|
602
|
+
});
|
|
708
603
|
}
|
|
709
604
|
|
|
710
|
-
async function runDoctor(): Promise<void> {
|
|
605
|
+
async function runDoctor(options: { isJson: boolean }): Promise<void> {
|
|
711
606
|
const report = await diagnose(await loadConfig());
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
607
|
+
emitSuccess({ isJson: options.isJson }, report, () => {
|
|
608
|
+
console.log(`inference mode: ${report.mode}`);
|
|
609
|
+
console.log(`routing capability: ${report.capability}`);
|
|
610
|
+
for (const check of report.checks)
|
|
611
|
+
console.log(
|
|
612
|
+
`${check.ok ? "ok" : "fail"}: ${check.name} - ${check.detail}`,
|
|
613
|
+
);
|
|
614
|
+
});
|
|
716
615
|
if (report.checks.some((check) => !check.ok)) process.exitCode = 1;
|
|
717
616
|
}
|
|
718
617
|
|
|
618
|
+
async function runSkill(subCommand: string, args: string[]): Promise<void> {
|
|
619
|
+
if (subCommand !== "which") throw new Error("usage: skillmux skill <which>");
|
|
620
|
+
await runWhich(args);
|
|
621
|
+
}
|
|
622
|
+
|
|
719
623
|
async function runWhich(args: string[]): Promise<void> {
|
|
720
624
|
const skillId = args[0];
|
|
721
|
-
if (!skillId) throw new Error("usage: skillmux which <skill_id>");
|
|
625
|
+
if (!skillId) throw new Error("usage: skillmux skill which <skill_id>");
|
|
722
626
|
const config = await loadConfig();
|
|
723
627
|
const vaultPath = expandHome(config.vault_path);
|
|
724
628
|
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
725
|
-
const roots = vaultResolutionOrder(vaultPath, localVaultPaths).filter(
|
|
726
|
-
existsSync(join(root, skillId, "SKILL.md")),
|
|
629
|
+
const roots = vaultResolutionOrder(vaultPath, localVaultPaths).filter(
|
|
630
|
+
(root) => existsSync(join(root, skillId, "SKILL.md")),
|
|
727
631
|
);
|
|
728
632
|
if (roots.length === 0) {
|
|
729
633
|
console.log(`${skillId}: not found in vault_path or local_vault_paths`);
|
|
@@ -731,498 +635,69 @@ async function runWhich(args: string[]): Promise<void> {
|
|
|
731
635
|
return;
|
|
732
636
|
}
|
|
733
637
|
console.log(`${skillId}: serving from ${roots[0]}`);
|
|
734
|
-
for (const shadowedRoot of roots.slice(1))
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
const MANIFEST_USAGE = "usage: skillmux manifest <pin|unpin> <skill_id> (--core | --project <group> [--path <path>...])";
|
|
738
|
-
const PROJECT_INIT_USAGE =
|
|
739
|
-
"usage: skillmux project init [path] [--name <group>] [--skill <id>...] [--client <id>...] [--target <name>...] [--yes] [--no-sync]";
|
|
740
|
-
|
|
741
|
-
interface ProjectInitArgs {
|
|
742
|
-
path: string;
|
|
743
|
-
name: string;
|
|
744
|
-
skills: string[];
|
|
745
|
-
clients: string[];
|
|
746
|
-
targets: string[];
|
|
747
|
-
yes: boolean;
|
|
748
|
-
sync: boolean;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
function configuredTargetForSurface(
|
|
752
|
-
manifest: ReturnType<typeof parseManifest>,
|
|
753
|
-
surface: { targetName: string; path: string },
|
|
754
|
-
): string | undefined {
|
|
755
|
-
if (manifest.targets[surface.targetName]) return surface.targetName;
|
|
756
|
-
return Object.entries(manifest.targets).find(
|
|
757
|
-
([, target]) => expandHome(target.dir) === surface.path,
|
|
758
|
-
)?.[0];
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
function configuredTargetsForClients(
|
|
762
|
-
manifest: ReturnType<typeof parseManifest>,
|
|
763
|
-
clients: readonly string[],
|
|
764
|
-
): string[] {
|
|
765
|
-
return planClientSurfaces(clients).surfaces.map((surface) => {
|
|
766
|
-
const target = configuredTargetForSurface(manifest, surface);
|
|
767
|
-
if (target) return target;
|
|
768
|
-
const client = surface.clients[0]!;
|
|
769
|
-
throw new Error(
|
|
770
|
-
`client target for "${client}" is not configured; run "skillmux init --client ${client} --yes" first`,
|
|
771
|
-
);
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
function parseProjectInitArgs(args: string[]): ProjectInitArgs {
|
|
776
|
-
let projectPath: string | undefined;
|
|
777
|
-
let name: string | undefined;
|
|
778
|
-
const skills: string[] = [];
|
|
779
|
-
const clients: string[] = [];
|
|
780
|
-
const targets: string[] = [];
|
|
781
|
-
let yes = false;
|
|
782
|
-
let sync = true;
|
|
783
|
-
|
|
784
|
-
for (let i = 0; i < args.length; i++) {
|
|
785
|
-
const arg = args[i]!;
|
|
786
|
-
if (arg === "--name") {
|
|
787
|
-
name = args[++i];
|
|
788
|
-
if (!name) throw new Error("--name requires a group name");
|
|
789
|
-
} else if (arg === "--skill") {
|
|
790
|
-
const skill = args[++i];
|
|
791
|
-
if (!skill) throw new Error("--skill requires a skill_id");
|
|
792
|
-
skills.push(skill);
|
|
793
|
-
} else if (arg === "--target") {
|
|
794
|
-
const target = args[++i];
|
|
795
|
-
if (!target) throw new Error("--target requires a name");
|
|
796
|
-
targets.push(target);
|
|
797
|
-
} else if (arg === "--client") {
|
|
798
|
-
const client = args[++i];
|
|
799
|
-
if (!client) throw new Error("--client requires a name");
|
|
800
|
-
clients.push(client);
|
|
801
|
-
} else if (arg === "--yes") {
|
|
802
|
-
yes = true;
|
|
803
|
-
} else if (arg === "--no-sync") {
|
|
804
|
-
sync = false;
|
|
805
|
-
} else if (arg === "--dry-run" || arg === "--json" || arg === "--interactive") {
|
|
806
|
-
continue;
|
|
807
|
-
} else if (arg.startsWith("-")) {
|
|
808
|
-
throw new Error(`unknown project init option: ${arg}`);
|
|
809
|
-
} else if (projectPath) {
|
|
810
|
-
throw new Error(PROJECT_INIT_USAGE);
|
|
811
|
-
} else {
|
|
812
|
-
projectPath = arg;
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
const path = resolveProjectDirectory(projectPath ? expandHome(projectPath) : undefined);
|
|
817
|
-
return { path, name: name ?? suggestProjectName(basename(path)), skills, clients, targets, yes, sync };
|
|
638
|
+
for (const shadowedRoot of roots.slice(1))
|
|
639
|
+
console.log(` shadows: ${shadowedRoot}`);
|
|
818
640
|
}
|
|
819
641
|
|
|
820
|
-
async function
|
|
821
|
-
subCommand: string,
|
|
642
|
+
async function runLocalVaultInit(
|
|
822
643
|
args: string[],
|
|
823
644
|
options: { isJson: boolean; dryRun: boolean },
|
|
824
645
|
): Promise<void> {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
const manifestPath = resolveManifestPath(vaultPath);
|
|
829
|
-
if (!manifestPath) throw new Error(`no skillmux.toml found at ${vaultPath}; run skillmux init first`);
|
|
830
|
-
const manifest = parseManifest(await Bun.file(manifestPath).text());
|
|
831
|
-
const names = subCommand === "show"
|
|
832
|
-
? [args[0] ?? ""]
|
|
833
|
-
: Object.keys(manifest.project ?? {});
|
|
834
|
-
if (subCommand === "show" && !manifest.project?.[names[0]!]) {
|
|
835
|
-
throw new Error(`[project.${names[0]}] does not exist`);
|
|
836
|
-
}
|
|
837
|
-
const projects = names.map((name) => ({
|
|
838
|
-
name,
|
|
839
|
-
paths: manifest.project?.[name]!.paths ?? [],
|
|
840
|
-
skills: manifest.project?.[name]!.skills ?? [],
|
|
841
|
-
targets: Object.entries(manifest.targets)
|
|
842
|
-
.filter(([, target]) => target.project_groups.includes(name))
|
|
843
|
-
.map(([target]) => target),
|
|
844
|
-
}));
|
|
845
|
-
if (options.isJson) {
|
|
846
|
-
console.log(JSON.stringify({ schema_version: 1, projects }));
|
|
847
|
-
} else if (projects.length === 0) {
|
|
848
|
-
console.log("no project groups configured");
|
|
849
|
-
} else {
|
|
850
|
-
for (const project of projects) {
|
|
851
|
-
console.log(`${project.name}:`);
|
|
852
|
-
console.log(` paths: ${project.paths.join(", ") || "(none)"}`);
|
|
853
|
-
console.log(` skills: ${project.skills.join(", ") || "(none)"}`);
|
|
854
|
-
console.log(` targets: ${project.targets.join(", ") || "(none)"}`);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
if (subCommand === "add-path" || subCommand === "remove-path") {
|
|
860
|
-
const group = args[0];
|
|
861
|
-
if (!group) throw new Error(`usage: skillmux project ${subCommand} <group> [path] --yes`);
|
|
862
|
-
const rawPath = args[1]?.startsWith("-") ? undefined : args[1];
|
|
863
|
-
const projectPath = resolveProjectDirectory(rawPath ? expandHome(rawPath) : undefined);
|
|
864
|
-
const yes = args.includes("--yes");
|
|
865
|
-
if (!existsSync(projectPath) || !lstatSync(projectPath).isDirectory()) {
|
|
866
|
-
throw new Error(`project path is not a directory: ${projectPath}`);
|
|
867
|
-
}
|
|
868
|
-
const config = await loadConfig();
|
|
869
|
-
const vaultPath = expandHome(config.vault_path);
|
|
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());
|
|
873
|
-
const updated = updateProjectPaths(manifest, group, {
|
|
874
|
-
...(subCommand === "add-path" ? { add: [projectPath] } : { remove: [projectPath] }),
|
|
875
|
-
});
|
|
876
|
-
validateManifest(updated, vaultPath, config.local_vault_paths.map(expandHome));
|
|
877
|
-
if (options.dryRun) {
|
|
878
|
-
console.log(`${subCommand}: [project.${group}] ${projectPath} (dry-run)`);
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
881
|
-
if (!yes) {
|
|
882
|
-
if (!options.isJson && isInteractive()) {
|
|
883
|
-
if (!(await confirmAction(`${subCommand} ${projectPath} in [project.${group}]?`))) return;
|
|
884
|
-
} else {
|
|
885
|
-
throw new Error(`skillmux project ${subCommand} requires --yes when run non-interactively`);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
writeManifestAtomic(manifestPath, updated);
|
|
889
|
-
console.log(`${subCommand}: [project.${group}] ${projectPath}`);
|
|
890
|
-
return;
|
|
891
|
-
}
|
|
892
|
-
if (subCommand === "pin" || subCommand === "unpin") {
|
|
893
|
-
const group = args[0];
|
|
894
|
-
const skills = args.slice(1).filter((arg) => !arg.startsWith("-"));
|
|
895
|
-
if (!group || skills.length === 0) {
|
|
896
|
-
throw new Error(`usage: skillmux project ${subCommand} <group> <skill_id>... --yes`);
|
|
897
|
-
}
|
|
898
|
-
const yes = args.includes("--yes");
|
|
899
|
-
const config = await loadConfig();
|
|
900
|
-
const vaultPath = expandHome(config.vault_path);
|
|
901
|
-
const manifestPath = resolveManifestPath(vaultPath);
|
|
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());
|
|
904
|
-
for (const skill of skills) {
|
|
905
|
-
updated = subCommand === "pin"
|
|
906
|
-
? pinProject(updated, skill, group)
|
|
907
|
-
: unpinProject(updated, skill, group);
|
|
908
|
-
}
|
|
909
|
-
validateManifest(updated, vaultPath, config.local_vault_paths.map(expandHome));
|
|
910
|
-
if (options.dryRun) {
|
|
911
|
-
console.log(`${subCommand}: [project.${group}] ${skills.join(", ")} (dry-run)`);
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
914
|
-
if (!yes) {
|
|
915
|
-
if (!options.isJson && isInteractive()) {
|
|
916
|
-
if (!(await confirmAction(`${subCommand} ${skills.join(", ")} in [project.${group}]?`))) return;
|
|
917
|
-
} else {
|
|
918
|
-
throw new Error(`skillmux project ${subCommand} requires --yes when run non-interactively`);
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
writeManifestAtomic(manifestPath, updated);
|
|
922
|
-
console.log(`${subCommand}: [project.${group}] ${skills.join(", ")}`);
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
if (subCommand === "attach" || subCommand === "detach") {
|
|
926
|
-
const group = args[0];
|
|
927
|
-
if (!group) throw new Error(`usage: skillmux project ${subCommand} <group> (--client <id>... | --target <name>...) --yes`);
|
|
928
|
-
const clients: string[] = [];
|
|
929
|
-
const requestedTargets: string[] = [];
|
|
930
|
-
for (let i = 1; i < args.length; i++) {
|
|
931
|
-
if (args[i] === "--client") {
|
|
932
|
-
const value = args[++i];
|
|
933
|
-
if (!value) throw new Error("--client requires a name");
|
|
934
|
-
clients.push(value);
|
|
935
|
-
} else if (args[i] === "--target") {
|
|
936
|
-
const value = args[++i];
|
|
937
|
-
if (!value) throw new Error("--target requires a name");
|
|
938
|
-
requestedTargets.push(value);
|
|
939
|
-
} else if (args[i] !== "--yes" && args[i] !== "--dry-run" && args[i] !== "--json") {
|
|
940
|
-
throw new Error(`unknown project ${subCommand} option: ${args[i]}`);
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
const config = await loadConfig();
|
|
944
|
-
const vaultPath = expandHome(config.vault_path);
|
|
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());
|
|
948
|
-
const clientTargets = configuredTargetsForClients(manifest, clients);
|
|
949
|
-
const targets = [...new Set([...requestedTargets, ...clientTargets])];
|
|
950
|
-
if (targets.length === 0) {
|
|
951
|
-
throw new Error(`project ${subCommand} requires --client or --target`);
|
|
952
|
-
}
|
|
953
|
-
const updated = updateProjectTargets(manifest, group, {
|
|
954
|
-
...(subCommand === "attach" ? { attach: targets } : { detach: targets }),
|
|
955
|
-
});
|
|
956
|
-
validateManifest(updated, vaultPath, config.local_vault_paths.map(expandHome));
|
|
957
|
-
if (options.dryRun) {
|
|
958
|
-
console.log(`${subCommand}: [project.${group}] ${targets.join(", ")} (dry-run)`);
|
|
959
|
-
return;
|
|
960
|
-
}
|
|
961
|
-
if (!args.includes("--yes")) {
|
|
962
|
-
if (!options.isJson && isInteractive()) {
|
|
963
|
-
if (!(await confirmAction(`${subCommand} [project.${group}] to ${targets.join(", ")}?`))) return;
|
|
964
|
-
} else {
|
|
965
|
-
throw new Error(`skillmux project ${subCommand} requires --yes when run non-interactively`);
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
writeManifestAtomic(manifestPath, updated);
|
|
969
|
-
console.log(`${subCommand}: [project.${group}] ${targets.join(", ")}`);
|
|
970
|
-
return;
|
|
971
|
-
}
|
|
972
|
-
if (subCommand !== "init") throw new Error(PROJECT_INIT_USAGE);
|
|
973
|
-
let request = parseProjectInitArgs(args);
|
|
974
|
-
const guided = shouldUseWizard(args, {
|
|
975
|
-
interactive: isInteractive(),
|
|
976
|
-
json: options.isJson,
|
|
977
|
-
dryRun: options.dryRun,
|
|
978
|
-
});
|
|
979
|
-
if (!existsSync(request.path)) throw new Error(`project path does not exist: ${request.path}`);
|
|
980
|
-
if (!lstatSync(request.path).isDirectory()) {
|
|
981
|
-
throw new Error(`project path is not a directory: ${request.path}`);
|
|
982
|
-
}
|
|
983
|
-
|
|
646
|
+
const path = args[0];
|
|
647
|
+
if (!path) throw new Error("usage: skillmux local-vault init <path> --yes");
|
|
648
|
+
const expanded = expandHome(path);
|
|
984
649
|
const config = await loadConfig();
|
|
985
|
-
const vaultPath = expandHome(config.vault_path);
|
|
986
650
|
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
if (guided) {
|
|
991
|
-
const name = await promptText("Project group", request.name);
|
|
992
|
-
const availableClients = SUPPORTED_CLIENT_IDS.filter((client) => {
|
|
993
|
-
const surface = planClientSurfaces([client]).surfaces[0];
|
|
994
|
-
return surface !== undefined && configuredTargetForSurface(manifest, surface) !== undefined;
|
|
995
|
-
});
|
|
996
|
-
const clients = await promptMultiSelect(
|
|
997
|
-
"Which clients should receive project skills?",
|
|
998
|
-
availableClients.map((client) => ({
|
|
999
|
-
value: client,
|
|
1000
|
-
label: client,
|
|
1001
|
-
selected: request.clients.length === 0 || request.clients.includes(client),
|
|
1002
|
-
})),
|
|
1003
|
-
);
|
|
1004
|
-
const skills = parseCommaList(
|
|
1005
|
-
await promptText("Project skill IDs, comma-separated", request.skills.join(",")),
|
|
651
|
+
if (!localVaultPaths.includes(expanded)) {
|
|
652
|
+
throw new Error(
|
|
653
|
+
`"${path}" is not one of the configured local_vault_paths — add it to config.toml first`,
|
|
1006
654
|
);
|
|
1007
|
-
request = { ...request, name, clients, skills };
|
|
1008
655
|
}
|
|
1009
|
-
|
|
1010
|
-
const
|
|
1011
|
-
const updated = upsertProject(manifest, {
|
|
1012
|
-
name: request.name,
|
|
1013
|
-
paths: [request.path],
|
|
1014
|
-
skills: request.skills,
|
|
1015
|
-
targets,
|
|
1016
|
-
});
|
|
1017
|
-
const { notes } = validateManifest(updated, vaultPath, localVaultPaths);
|
|
1018
|
-
const plan = {
|
|
1019
|
-
mode: "project",
|
|
1020
|
-
project: request.name,
|
|
1021
|
-
path: request.path,
|
|
1022
|
-
skills: request.skills,
|
|
1023
|
-
clients: request.clients,
|
|
1024
|
-
targets,
|
|
1025
|
-
sync: request.sync,
|
|
1026
|
-
notes,
|
|
1027
|
-
};
|
|
1028
|
-
|
|
656
|
+
if (!existsSync(expanded)) throw new Error(`"${path}" does not exist`);
|
|
657
|
+
const markerPath = join(expanded, ".skillmux");
|
|
1029
658
|
if (options.dryRun) {
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
console.log(
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
console.log(` sync: ${request.sync ? "yes" : "no"}`);
|
|
1042
|
-
}
|
|
1043
|
-
if (!(await confirmAction(`Apply project setup for ${request.name} at ${request.path}?`))) {
|
|
1044
|
-
console.log("project setup cancelled");
|
|
1045
|
-
return;
|
|
1046
|
-
}
|
|
1047
|
-
} else {
|
|
1048
|
-
throw new Error("skillmux project init requires --yes when run non-interactively");
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
writeManifestAtomic(manifestPath, updated);
|
|
1053
|
-
if (request.sync) {
|
|
1054
|
-
try {
|
|
1055
|
-
await runSync([]);
|
|
1056
|
-
} catch (error) {
|
|
1057
|
-
throw new Error(
|
|
1058
|
-
`project configuration was saved, but sync failed; fix the reported issue and run "skillmux sync": ${
|
|
1059
|
-
error instanceof Error ? error.message : String(error)
|
|
1060
|
-
}`,
|
|
1061
|
-
);
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
if (options.isJson) {
|
|
1065
|
-
console.log(JSON.stringify({ schema_version: 1, result: plan }));
|
|
1066
|
-
} else {
|
|
1067
|
-
console.log(`project "${request.name}" ready at ${request.path}`);
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
async function runTarget(
|
|
1072
|
-
subCommand: string,
|
|
1073
|
-
args: string[],
|
|
1074
|
-
options: { isJson: boolean; dryRun: boolean },
|
|
1075
|
-
): Promise<void> {
|
|
1076
|
-
const config = await loadConfig();
|
|
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());
|
|
1081
|
-
|
|
1082
|
-
if (subCommand === "list" || subCommand === "show") {
|
|
1083
|
-
const names = subCommand === "show" ? [args[0] ?? ""] : Object.keys(manifest.targets);
|
|
1084
|
-
if (subCommand === "show" && !manifest.targets[names[0]!]) {
|
|
1085
|
-
throw new Error(`target "${names[0]}" does not exist`);
|
|
1086
|
-
}
|
|
1087
|
-
const targets = names.map((name) => {
|
|
1088
|
-
const target = manifest.targets[name]!;
|
|
1089
|
-
const clients = SUPPORTED_CLIENT_IDS.filter((client) => {
|
|
1090
|
-
const surface = planClientSurfaces([client]).surfaces[0];
|
|
1091
|
-
return surface !== undefined && surface.path === expandHome(target.dir);
|
|
1092
|
-
});
|
|
1093
|
-
return { name, ...target, clients };
|
|
1094
|
-
});
|
|
1095
|
-
if (options.isJson) {
|
|
1096
|
-
console.log(JSON.stringify({ schema_version: 1, targets }));
|
|
1097
|
-
} else if (targets.length === 0) {
|
|
1098
|
-
console.log("no targets configured");
|
|
1099
|
-
} else {
|
|
1100
|
-
for (const target of targets) {
|
|
1101
|
-
console.log(`${target.name}:`);
|
|
1102
|
-
console.log(` dir: ${target.dir}`);
|
|
1103
|
-
console.log(` host: ${target.host ?? "(global)"}`);
|
|
1104
|
-
console.log(` clients: ${target.clients.join(", ") || "(custom)"}`);
|
|
1105
|
-
console.log(` projects: ${target.project_groups.join(", ") || "(none)"}`);
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
return;
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
if (subCommand === "add") {
|
|
1112
|
-
const name = args[0];
|
|
1113
|
-
const pathIndex = args.indexOf("--path");
|
|
1114
|
-
const rawPath = pathIndex === -1 ? undefined : args[pathIndex + 1];
|
|
1115
|
-
if (!name || !rawPath) throw new Error("usage: skillmux target add <name> --path <dir> --yes");
|
|
1116
|
-
const path = expandHome(rawPath);
|
|
1117
|
-
if (options.dryRun) {
|
|
1118
|
-
const planned = planInitManifest(vaultPath, [{ name, dir: path }], []);
|
|
1119
|
-
console.log(options.isJson
|
|
1120
|
-
? JSON.stringify({ schema_version: 1, target: planned.targets[name] })
|
|
1121
|
-
: `target add: ${name} -> ${path} (dry-run)`);
|
|
1122
|
-
return;
|
|
1123
|
-
}
|
|
1124
|
-
if (!args.includes("--yes")) {
|
|
1125
|
-
if (!options.isJson && isInteractive()) {
|
|
1126
|
-
if (!(await confirmAction(`Adopt target ${name} at ${path}?`))) return;
|
|
1127
|
-
} else {
|
|
1128
|
-
throw new Error("skillmux target add requires --yes when run non-interactively");
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
applyInit(vaultPath, [{ name, dir: path }]);
|
|
1132
|
-
console.log(`target "${name}" added at ${path}`);
|
|
659
|
+
emitSuccess(
|
|
660
|
+
{ isJson: options.isJson },
|
|
661
|
+
{
|
|
662
|
+
marker_path: markerPath,
|
|
663
|
+
vault_path: expandHome(config.vault_path),
|
|
664
|
+
},
|
|
665
|
+
() =>
|
|
666
|
+
console.log(
|
|
667
|
+
`local-vault init: ${markerPath} (role: local_vault, vault_path: ${expandHome(config.vault_path)}) (dry-run)`,
|
|
668
|
+
),
|
|
669
|
+
);
|
|
1133
670
|
return;
|
|
1134
671
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
}
|
|
1145
|
-
if (!args.includes("--yes")) {
|
|
1146
|
-
if (!options.isJson && isInteractive()) {
|
|
1147
|
-
if (!(await confirmAction(`Remove target ${name} from the manifest and preserve its files?`))) return;
|
|
1148
|
-
} else {
|
|
1149
|
-
throw new Error("skillmux target remove requires --yes when run non-interactively");
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
const targets = { ...manifest.targets };
|
|
1153
|
-
delete targets[name];
|
|
1154
|
-
writeManifestAtomic(manifestPath, { ...manifest, targets });
|
|
1155
|
-
console.log(`target "${name}" removed from the manifest; files preserved at ${manifest.targets[name]!.dir}`);
|
|
672
|
+
if (
|
|
673
|
+
!(await confirmIfNeeded({
|
|
674
|
+
confirmed: args.includes("--yes"),
|
|
675
|
+
isJson: options.isJson,
|
|
676
|
+
prompt: `Mark ${expanded} as a local_vault (role: local_vault, vault_path: ${expandHome(config.vault_path)})?`,
|
|
677
|
+
nonInteractiveError:
|
|
678
|
+
"skillmux local-vault init requires --yes when run non-interactively",
|
|
679
|
+
}))
|
|
680
|
+
)
|
|
1156
681
|
return;
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
throw new Error("usage: skillmux target <list|show|add|remove>");
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
function parseManifestPinArgs(args: string[]): { skillId: string; core: boolean; project?: string; paths: string[] } {
|
|
1163
|
-
const skillId = args[0];
|
|
1164
|
-
if (!skillId) throw new Error(MANIFEST_USAGE);
|
|
1165
|
-
let core = false;
|
|
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> {
|
|
1210
|
-
const path = args[0];
|
|
1211
|
-
if (!path) throw new Error("usage: skillmux local-vault init <path>");
|
|
1212
|
-
const expanded = expandHome(path);
|
|
1213
|
-
const config = await loadConfig();
|
|
1214
|
-
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
1215
|
-
if (!localVaultPaths.includes(expanded)) {
|
|
1216
|
-
throw new Error(`"${path}" is not one of the configured local_vault_paths — add it to config.toml first`);
|
|
1217
|
-
}
|
|
1218
|
-
if (!existsSync(expanded)) throw new Error(`"${path}" does not exist`);
|
|
1219
682
|
writeLocalVaultMarker(expanded, expandHome(config.vault_path));
|
|
1220
|
-
|
|
683
|
+
emitSuccess(
|
|
684
|
+
{ isJson: options.isJson },
|
|
685
|
+
{
|
|
686
|
+
marker_path: markerPath,
|
|
687
|
+
vault_path: expandHome(config.vault_path),
|
|
688
|
+
},
|
|
689
|
+
() =>
|
|
690
|
+
console.log(
|
|
691
|
+
`wrote ${markerPath} (role: local_vault, vault_path: ${expandHome(config.vault_path)})`,
|
|
692
|
+
),
|
|
693
|
+
);
|
|
1221
694
|
}
|
|
1222
695
|
|
|
1223
|
-
async function runModelDownload(): Promise<void> {
|
|
696
|
+
async function runModelDownload(options: { isJson: boolean }): Promise<void> {
|
|
1224
697
|
const cacheDir = await downloadLocalModels(await loadConfig());
|
|
1225
|
-
|
|
698
|
+
emitSuccess({ isJson: options.isJson }, { cache_dir: cacheDir }, () =>
|
|
699
|
+
console.log(`models ready in ${cacheDir}`),
|
|
700
|
+
);
|
|
1226
701
|
}
|
|
1227
702
|
|
|
1228
703
|
function parseSyncArgs(args: string[]): {
|
|
@@ -1289,10 +764,18 @@ async function runSync(args: string[]): Promise<void> {
|
|
|
1289
764
|
|
|
1290
765
|
const suffix = dryRun ? " (dry-run)" : "";
|
|
1291
766
|
const result = syncTarget(
|
|
1292
|
-
{
|
|
767
|
+
{
|
|
768
|
+
vaultPath,
|
|
769
|
+
targetDir,
|
|
770
|
+
targetName,
|
|
771
|
+
coreSkillIds: manifest.core.skills,
|
|
772
|
+
localVaultPaths,
|
|
773
|
+
},
|
|
1293
774
|
{ dryRun },
|
|
1294
775
|
);
|
|
1295
|
-
console.log(
|
|
776
|
+
console.log(
|
|
777
|
+
`${targetName}: +${result.added.length} -${result.removed.length}${suffix}`,
|
|
778
|
+
);
|
|
1296
779
|
|
|
1297
780
|
if (target.project_groups.length > 0) {
|
|
1298
781
|
const allGroups = manifest.project ?? {};
|
|
@@ -1349,9 +832,9 @@ function parseInitArgs(args: string[]): {
|
|
|
1349
832
|
if (!value) throw new Error("--vault requires a path");
|
|
1350
833
|
vaultPath = value;
|
|
1351
834
|
i++;
|
|
1352
|
-
} else if (option === "--
|
|
835
|
+
} else if (option === "--dir") {
|
|
1353
836
|
const value = args[i + 1];
|
|
1354
|
-
if (!value) throw new Error("--
|
|
837
|
+
if (!value) throw new Error("--dir requires a directory");
|
|
1355
838
|
customPath = value;
|
|
1356
839
|
i++;
|
|
1357
840
|
} else if (option === "--core") {
|
|
@@ -1359,7 +842,11 @@ function parseInitArgs(args: string[]): {
|
|
|
1359
842
|
if (!value) throw new Error("--core requires a skill_id");
|
|
1360
843
|
coreSkillIds.push(value);
|
|
1361
844
|
i++;
|
|
1362
|
-
} else if (
|
|
845
|
+
} else if (
|
|
846
|
+
option === "--dry-run" ||
|
|
847
|
+
option === "--json" ||
|
|
848
|
+
option === "--interactive"
|
|
849
|
+
) {
|
|
1363
850
|
continue;
|
|
1364
851
|
} else if (option === "--migrate-full-vault") {
|
|
1365
852
|
migrateFullVault = true;
|
|
@@ -1411,10 +898,13 @@ async function runInit(
|
|
|
1411
898
|
let configPlan: ConfigInitPlan | undefined;
|
|
1412
899
|
let vaultPath: string;
|
|
1413
900
|
if (!existsSync(configPath)) {
|
|
1414
|
-
const bootstrapVaultPath =
|
|
901
|
+
const bootstrapVaultPath =
|
|
902
|
+
requestedVaultPath ??
|
|
1415
903
|
(!options.isJson && isInteractive() ? "~/skills" : undefined);
|
|
1416
904
|
if (!bootstrapVaultPath) {
|
|
1417
|
-
throw new Error(
|
|
905
|
+
throw new Error(
|
|
906
|
+
`machine config does not exist: ${configPath}; re-run with --vault <path>`,
|
|
907
|
+
);
|
|
1418
908
|
}
|
|
1419
909
|
configPlan = planConfigInit(configPath, expandHome(bootstrapVaultPath));
|
|
1420
910
|
vaultPath = configPlan.vaultPath;
|
|
@@ -1439,15 +929,21 @@ async function runInit(
|
|
|
1439
929
|
let selectedClients = requestedClients;
|
|
1440
930
|
if (guided) {
|
|
1441
931
|
const detected = detectInstalledClients({
|
|
1442
|
-
codexHome: process.env.CODEX_HOME
|
|
932
|
+
codexHome: process.env.CODEX_HOME
|
|
933
|
+
? expandHome(process.env.CODEX_HOME)
|
|
934
|
+
: undefined,
|
|
1443
935
|
});
|
|
1444
|
-
const evidence = new Map(
|
|
936
|
+
const evidence = new Map(
|
|
937
|
+
detected.map((item) => [item.client, item.evidence]),
|
|
938
|
+
);
|
|
1445
939
|
selectedClients = await promptMultiSelect(
|
|
1446
940
|
"Which clients do you use?",
|
|
1447
941
|
SUPPORTED_CLIENT_IDS.map((client) => ({
|
|
1448
942
|
value: client,
|
|
1449
943
|
label: client,
|
|
1450
|
-
detail: evidence.has(client)
|
|
944
|
+
detail: evidence.has(client)
|
|
945
|
+
? `detected: ${evidence.get(client)}`
|
|
946
|
+
: undefined,
|
|
1451
947
|
selected: evidence.has(client) || requestedClients.includes(client),
|
|
1452
948
|
})),
|
|
1453
949
|
);
|
|
@@ -1455,16 +951,26 @@ async function runInit(
|
|
|
1455
951
|
let selectedCoreSkillIds = coreSkillIds;
|
|
1456
952
|
if (guided) {
|
|
1457
953
|
selectedCoreSkillIds = parseCommaList(
|
|
1458
|
-
await promptText(
|
|
954
|
+
await promptText(
|
|
955
|
+
"Core skill IDs to add, comma-separated",
|
|
956
|
+
coreSkillIds.join(","),
|
|
957
|
+
),
|
|
1459
958
|
);
|
|
1460
959
|
}
|
|
1461
960
|
|
|
1462
961
|
const clientPlan = planClientSurfaces(selectedClients, {
|
|
1463
|
-
codexHome: process.env.CODEX_HOME
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
codexHome: process.env.CODEX_HOME ? expandHome(process.env.CODEX_HOME) : undefined,
|
|
962
|
+
codexHome: process.env.CODEX_HOME
|
|
963
|
+
? expandHome(process.env.CODEX_HOME)
|
|
964
|
+
: undefined,
|
|
1467
965
|
});
|
|
966
|
+
const instructionPlan = planInstructionSetup(
|
|
967
|
+
skipInstructions ? [] : clientPlan.clients.map((client) => client.id),
|
|
968
|
+
{
|
|
969
|
+
codexHome: process.env.CODEX_HOME
|
|
970
|
+
? expandHome(process.env.CODEX_HOME)
|
|
971
|
+
: undefined,
|
|
972
|
+
},
|
|
973
|
+
);
|
|
1468
974
|
const instructionReadiness: Partial<Record<ClientId, ReadinessAxis>> = {};
|
|
1469
975
|
for (const change of instructionPlan.changes) {
|
|
1470
976
|
for (const client of change.clients) {
|
|
@@ -1475,25 +981,39 @@ async function runInit(
|
|
|
1475
981
|
}
|
|
1476
982
|
}
|
|
1477
983
|
for (const manual of instructionPlan.manual) {
|
|
1478
|
-
instructionReadiness[manual.client] = {
|
|
984
|
+
instructionReadiness[manual.client] = {
|
|
985
|
+
status: "manual",
|
|
986
|
+
detail: manual.reason,
|
|
987
|
+
};
|
|
1479
988
|
}
|
|
1480
|
-
const builtInNames = new Set([
|
|
989
|
+
const builtInNames = new Set([
|
|
990
|
+
"agent-skills",
|
|
991
|
+
"claude-code",
|
|
992
|
+
"codex",
|
|
993
|
+
"custom",
|
|
994
|
+
"agents",
|
|
995
|
+
"claude",
|
|
996
|
+
]);
|
|
1481
997
|
const explicitSurfaceTargets = explicitTargets
|
|
1482
998
|
.filter((name) => builtInNames.has(name))
|
|
1483
999
|
.map((name) =>
|
|
1484
1000
|
resolveBuiltInTarget(name, {
|
|
1485
|
-
codexHome: process.env.CODEX_HOME
|
|
1001
|
+
codexHome: process.env.CODEX_HOME
|
|
1002
|
+
? expandHome(process.env.CODEX_HOME)
|
|
1003
|
+
: undefined,
|
|
1486
1004
|
customPath: customPath ? expandHome(customPath) : undefined,
|
|
1487
1005
|
}),
|
|
1488
1006
|
);
|
|
1489
1007
|
if (customPath && !explicitTargets.includes("custom")) {
|
|
1490
|
-
throw new Error("--
|
|
1008
|
+
throw new Error("--dir may only be used with --target custom");
|
|
1491
1009
|
}
|
|
1492
1010
|
for (const target of explicitSurfaceTargets) {
|
|
1493
1011
|
if (target.warning) console.error(`warning: ${target.warning}`);
|
|
1494
1012
|
}
|
|
1495
1013
|
const targetByPath = new Map(
|
|
1496
|
-
explicitSurfaceTargets.map(
|
|
1014
|
+
explicitSurfaceTargets.map(
|
|
1015
|
+
(target) => [target.path, target.targetName] as const,
|
|
1016
|
+
),
|
|
1497
1017
|
);
|
|
1498
1018
|
const existingManifestPath = resolveManifestPath(vaultPath);
|
|
1499
1019
|
const existingManifest = existingManifestPath
|
|
@@ -1504,7 +1024,8 @@ async function runInit(
|
|
|
1504
1024
|
targetByPath.set(
|
|
1505
1025
|
surface.path,
|
|
1506
1026
|
existingManifest
|
|
1507
|
-
? configuredTargetForSurface(existingManifest, surface) ??
|
|
1027
|
+
? (configuredTargetForSurface(existingManifest, surface) ??
|
|
1028
|
+
surface.targetName)
|
|
1508
1029
|
: surface.targetName,
|
|
1509
1030
|
);
|
|
1510
1031
|
}
|
|
@@ -1518,7 +1039,8 @@ async function runInit(
|
|
|
1518
1039
|
const candidates = detectSurfaces(candidatePaths, vaultPath);
|
|
1519
1040
|
if (!options.isJson) {
|
|
1520
1041
|
for (const candidate of candidates) {
|
|
1521
|
-
const name =
|
|
1042
|
+
const name =
|
|
1043
|
+
targetByPath.get(candidate.path) ?? deriveTargetName(candidate.path);
|
|
1522
1044
|
if (candidate.state === "missing") {
|
|
1523
1045
|
console.log(`${name} (${candidate.path}): not found`);
|
|
1524
1046
|
continue;
|
|
@@ -1528,26 +1050,45 @@ async function runInit(
|
|
|
1528
1050
|
continue;
|
|
1529
1051
|
}
|
|
1530
1052
|
if (candidate.state === "full-vault") {
|
|
1531
|
-
console.log(
|
|
1053
|
+
console.log(
|
|
1054
|
+
`${name} (${candidate.path}): full-vault -> ${candidate.canonicalPath}`,
|
|
1055
|
+
);
|
|
1532
1056
|
continue;
|
|
1533
1057
|
}
|
|
1534
1058
|
if (candidate.state === "external-symlink") {
|
|
1535
|
-
console.log(
|
|
1059
|
+
console.log(
|
|
1060
|
+
`${name} (${candidate.path}): external symlink -> ${candidate.canonicalPath}`,
|
|
1061
|
+
);
|
|
1536
1062
|
continue;
|
|
1537
1063
|
}
|
|
1538
1064
|
if (candidate.state === "unsupported") {
|
|
1539
|
-
console.log(
|
|
1065
|
+
console.log(
|
|
1066
|
+
`${name} (${candidate.path}): unsupported filesystem entry`,
|
|
1067
|
+
);
|
|
1540
1068
|
continue;
|
|
1541
1069
|
}
|
|
1542
1070
|
const kind = "real dir";
|
|
1543
|
-
const marked = candidate.alreadyMarked
|
|
1544
|
-
|
|
1071
|
+
const marked = candidate.alreadyMarked
|
|
1072
|
+
? ", already skillmux-managed"
|
|
1073
|
+
: "";
|
|
1074
|
+
console.log(
|
|
1075
|
+
`${name} (${candidate.path}): ${kind}, ${candidate.skillCount} skills${marked}`,
|
|
1076
|
+
);
|
|
1545
1077
|
}
|
|
1546
|
-
for (const readiness of assessClientReadiness(
|
|
1078
|
+
for (const readiness of assessClientReadiness(
|
|
1079
|
+
clientPlan,
|
|
1080
|
+
instructionReadiness,
|
|
1081
|
+
)) {
|
|
1547
1082
|
console.log(`\n${readiness.client} readiness:`);
|
|
1548
|
-
console.log(
|
|
1549
|
-
|
|
1550
|
-
|
|
1083
|
+
console.log(
|
|
1084
|
+
` skill surface: ${readiness.skillSurface.status} — ${readiness.skillSurface.detail}`,
|
|
1085
|
+
);
|
|
1086
|
+
console.log(
|
|
1087
|
+
` MCP registration: ${readiness.mcpRegistration.status} — ${readiness.mcpRegistration.detail}`,
|
|
1088
|
+
);
|
|
1089
|
+
console.log(
|
|
1090
|
+
` instructions: ${readiness.instructionSetup.status} — ${readiness.instructionSetup.detail}`,
|
|
1091
|
+
);
|
|
1551
1092
|
}
|
|
1552
1093
|
for (const change of instructionPlan.changes) {
|
|
1553
1094
|
console.log(
|
|
@@ -1578,20 +1119,28 @@ async function runInit(
|
|
|
1578
1119
|
|
|
1579
1120
|
const byName = new Map(
|
|
1580
1121
|
candidates
|
|
1581
|
-
.filter(
|
|
1582
|
-
candidate
|
|
1583
|
-
|
|
1122
|
+
.filter(
|
|
1123
|
+
(candidate) =>
|
|
1124
|
+
candidate.deliveryMode === "managed-pins" ||
|
|
1125
|
+
(migrateFullVault && candidate.state === "full-vault"),
|
|
1584
1126
|
)
|
|
1585
|
-
.map(
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1127
|
+
.map(
|
|
1128
|
+
(candidate) =>
|
|
1129
|
+
[
|
|
1130
|
+
targetByPath.get(candidate.path) ??
|
|
1131
|
+
deriveTargetName(candidate.path),
|
|
1132
|
+
candidate,
|
|
1133
|
+
] as const,
|
|
1134
|
+
),
|
|
1589
1135
|
);
|
|
1590
1136
|
const allCandidatesByName = new Map(
|
|
1591
|
-
candidates.map(
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1137
|
+
candidates.map(
|
|
1138
|
+
(candidate) =>
|
|
1139
|
+
[
|
|
1140
|
+
targetByPath.get(candidate.path) ?? deriveTargetName(candidate.path),
|
|
1141
|
+
candidate,
|
|
1142
|
+
] as const,
|
|
1143
|
+
),
|
|
1595
1144
|
);
|
|
1596
1145
|
for (const name of requestedTargets) {
|
|
1597
1146
|
if (!byName.has(name)) {
|
|
@@ -1600,7 +1149,9 @@ async function runInit(
|
|
|
1600
1149
|
`target "${name}" is a full-vault surface; re-run with --migrate-full-vault to convert it to managed pins`,
|
|
1601
1150
|
);
|
|
1602
1151
|
}
|
|
1603
|
-
throw new Error(
|
|
1152
|
+
throw new Error(
|
|
1153
|
+
`unknown --target "${name}": not among detected surfaces`,
|
|
1154
|
+
);
|
|
1604
1155
|
}
|
|
1605
1156
|
}
|
|
1606
1157
|
|
|
@@ -1612,7 +1163,11 @@ async function runInit(
|
|
|
1612
1163
|
...(candidate.state === "full-vault" ? { migrateFullVault: true } : {}),
|
|
1613
1164
|
};
|
|
1614
1165
|
});
|
|
1615
|
-
const plannedManifest = planInitManifest(
|
|
1166
|
+
const plannedManifest = planInitManifest(
|
|
1167
|
+
vaultPath,
|
|
1168
|
+
confirmedTargets,
|
|
1169
|
+
selectedCoreSkillIds,
|
|
1170
|
+
);
|
|
1616
1171
|
const serializedPlan = {
|
|
1617
1172
|
vault_path: vaultPath,
|
|
1618
1173
|
config: configPlan
|
|
@@ -1630,44 +1185,50 @@ async function runInit(
|
|
|
1630
1185
|
};
|
|
1631
1186
|
if (!hasChanges) {
|
|
1632
1187
|
if (options.isJson) {
|
|
1633
|
-
console.log(
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1188
|
+
console.log(
|
|
1189
|
+
JSON.stringify({
|
|
1190
|
+
schema_version: 1,
|
|
1191
|
+
ok: true,
|
|
1192
|
+
command: "init",
|
|
1193
|
+
phase: "plan",
|
|
1194
|
+
dry_run: options.dryRun,
|
|
1195
|
+
applied: false,
|
|
1196
|
+
plan: serializedPlan,
|
|
1197
|
+
}),
|
|
1198
|
+
);
|
|
1642
1199
|
} else {
|
|
1643
1200
|
console.log("\nno managed-pins surface selected — nothing written.");
|
|
1644
1201
|
}
|
|
1645
1202
|
return;
|
|
1646
1203
|
}
|
|
1647
1204
|
if (!options.isJson) {
|
|
1648
|
-
for (const target of confirmedTargets.filter(
|
|
1205
|
+
for (const target of confirmedTargets.filter(
|
|
1206
|
+
(target) => target.migrateFullVault,
|
|
1207
|
+
)) {
|
|
1649
1208
|
console.log(
|
|
1650
1209
|
`full-vault migration ${target.name}: ${vaultHealth.skillCount} visible skills -> ` +
|
|
1651
|
-
|
|
1210
|
+
`${plannedManifest.core.skills.length} core ${plannedManifest.core.skills.length === 1 ? "skill" : "skills"} after sync`,
|
|
1652
1211
|
);
|
|
1653
1212
|
}
|
|
1654
1213
|
}
|
|
1655
1214
|
if (options.dryRun) {
|
|
1656
1215
|
if (options.isJson) {
|
|
1657
|
-
console.log(
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1216
|
+
console.log(
|
|
1217
|
+
JSON.stringify({
|
|
1218
|
+
schema_version: 1,
|
|
1219
|
+
ok: true,
|
|
1220
|
+
command: "init",
|
|
1221
|
+
phase: "plan",
|
|
1222
|
+
dry_run: true,
|
|
1223
|
+
applied: false,
|
|
1224
|
+
plan: serializedPlan,
|
|
1225
|
+
}),
|
|
1226
|
+
);
|
|
1666
1227
|
} else {
|
|
1667
1228
|
console.log(
|
|
1668
1229
|
`\ndry-run: ${confirmedTargets.length} target(s), ` +
|
|
1669
|
-
|
|
1670
|
-
|
|
1230
|
+
`${instructionPlan.changes.filter((change) => change.status !== "unchanged").length} instruction file(s), ` +
|
|
1231
|
+
`core: ${plannedManifest.core.skills.join(", ") || "(unchanged)"}`,
|
|
1671
1232
|
);
|
|
1672
1233
|
}
|
|
1673
1234
|
return;
|
|
@@ -1684,7 +1245,9 @@ async function runInit(
|
|
|
1684
1245
|
console.log(
|
|
1685
1246
|
` instructions: ${instructionPlan.changes.filter((change) => change.status !== "unchanged").length} file(s)`,
|
|
1686
1247
|
);
|
|
1687
|
-
console.log(
|
|
1248
|
+
console.log(
|
|
1249
|
+
` core: ${plannedManifest.core.skills.join(", ") || "(none)"}`,
|
|
1250
|
+
);
|
|
1688
1251
|
console.log(` sync: ${sync ? "yes" : "no"}`);
|
|
1689
1252
|
if (!(await confirmAction("Apply this setup plan?"))) {
|
|
1690
1253
|
console.log("init cancelled");
|
|
@@ -1692,10 +1255,14 @@ async function runInit(
|
|
|
1692
1255
|
}
|
|
1693
1256
|
} else {
|
|
1694
1257
|
const prompts = [
|
|
1695
|
-
...confirmedTargets.map(
|
|
1258
|
+
...confirmedTargets.map(
|
|
1259
|
+
(target) => `Adopt ${target.name} at ${target.dir}?`,
|
|
1260
|
+
),
|
|
1696
1261
|
...instructionPlan.changes
|
|
1697
1262
|
.filter((change) => change.status !== "unchanged")
|
|
1698
|
-
.map(
|
|
1263
|
+
.map(
|
|
1264
|
+
(change) => `${change.status} instruction file ${change.path}?`,
|
|
1265
|
+
),
|
|
1699
1266
|
...(hasConfigWrite ? [`Create machine config ${configPath}?`] : []),
|
|
1700
1267
|
...(selectedCoreSkillIds.length > 0
|
|
1701
1268
|
? [`Pin core skills: ${selectedCoreSkillIds.join(", ")}?`]
|
|
@@ -1754,42 +1321,53 @@ async function runInit(
|
|
|
1754
1321
|
}
|
|
1755
1322
|
|
|
1756
1323
|
if (options.isJson) {
|
|
1757
|
-
console.log(
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1324
|
+
console.log(
|
|
1325
|
+
JSON.stringify({
|
|
1326
|
+
schema_version: 1,
|
|
1327
|
+
ok: true,
|
|
1328
|
+
command: "init",
|
|
1329
|
+
phase: "result",
|
|
1330
|
+
dry_run: false,
|
|
1331
|
+
applied: true,
|
|
1332
|
+
plan: serializedPlan,
|
|
1333
|
+
result: {
|
|
1334
|
+
config_created: configCreated,
|
|
1335
|
+
targets_adopted: confirmedTargets.map((target) => target.name),
|
|
1336
|
+
instructions_changed: instructionPlan.changes
|
|
1337
|
+
.filter((change) => change.status !== "unchanged")
|
|
1338
|
+
.map((change) => change.path),
|
|
1339
|
+
core: plannedManifest.core.skills,
|
|
1340
|
+
},
|
|
1341
|
+
}),
|
|
1342
|
+
);
|
|
1774
1343
|
return;
|
|
1775
1344
|
}
|
|
1776
1345
|
if (configCreated) console.log(`created ${configPath}`);
|
|
1777
1346
|
if (confirmedTargets.length > 0) {
|
|
1778
|
-
console.log(
|
|
1347
|
+
console.log(
|
|
1348
|
+
`\nwrote ${join(vaultPath, "skillmux.toml")}, adopted: ${confirmedTargets.map((t) => t.name).join(", ")}`,
|
|
1349
|
+
);
|
|
1779
1350
|
} else if (selectedCoreSkillIds.length > 0) {
|
|
1780
1351
|
console.log(`\nwrote ${join(vaultPath, "skillmux.toml")}`);
|
|
1781
1352
|
}
|
|
1782
1353
|
if (plannedManifest.core.skills.length === 0 && confirmedTargets.length > 0) {
|
|
1783
|
-
console.log("next: skillmux
|
|
1354
|
+
console.log("next: skillmux core pin <skill_id> --yes");
|
|
1784
1355
|
}
|
|
1785
1356
|
if (confirmedTargets.length > 0) console.log("next: skillmux sync");
|
|
1786
|
-
if (
|
|
1357
|
+
if (
|
|
1358
|
+
selectedClients.length === 0 ||
|
|
1359
|
+
selectedClients.includes("skillmux-mcp")
|
|
1360
|
+
) {
|
|
1787
1361
|
console.log(`\n${printLastMile()}`);
|
|
1788
1362
|
}
|
|
1789
1363
|
if (guided && sync && confirmedTargets.length > 0) await runSync([]);
|
|
1790
1364
|
}
|
|
1791
1365
|
|
|
1792
|
-
function parseReportArgs(args: string[]): {
|
|
1366
|
+
function parseReportArgs(args: string[]): {
|
|
1367
|
+
server?: string;
|
|
1368
|
+
db?: string;
|
|
1369
|
+
since?: string;
|
|
1370
|
+
} {
|
|
1793
1371
|
let server: string | undefined;
|
|
1794
1372
|
let db: string | undefined;
|
|
1795
1373
|
let since: string | undefined;
|
|
@@ -1808,6 +1386,8 @@ function parseReportArgs(args: string[]): { server?: string; db?: string; since?
|
|
|
1808
1386
|
if (!value) throw new Error("--since requires a window");
|
|
1809
1387
|
since = value;
|
|
1810
1388
|
i++;
|
|
1389
|
+
} else if (option === "--json") {
|
|
1390
|
+
// handled globally by main()'s isJson flag; recognized here so it isn't rejected
|
|
1811
1391
|
} else {
|
|
1812
1392
|
throw new Error(`unknown report option: ${option}`);
|
|
1813
1393
|
}
|
|
@@ -1816,24 +1396,45 @@ function parseReportArgs(args: string[]): { server?: string; db?: string; since?
|
|
|
1816
1396
|
return { server, db, since };
|
|
1817
1397
|
}
|
|
1818
1398
|
|
|
1819
|
-
async function runReport(
|
|
1399
|
+
async function runReport(
|
|
1400
|
+
args: string[],
|
|
1401
|
+
options: { isJson: boolean },
|
|
1402
|
+
): Promise<void> {
|
|
1820
1403
|
const { server, db: dbPath, since } = parseReportArgs(args);
|
|
1821
|
-
if (!since)
|
|
1404
|
+
if (!since)
|
|
1405
|
+
throw new Error(
|
|
1406
|
+
"usage: skillmux report [--server <url> | --db <path>] --since <window> [--json]",
|
|
1407
|
+
);
|
|
1822
1408
|
|
|
1823
1409
|
if (server) {
|
|
1824
1410
|
const url = `${server.replace(/\/$/, "")}/stats?since=${encodeURIComponent(since)}`;
|
|
1825
1411
|
const res = await fetch(url);
|
|
1826
|
-
if (!res.ok)
|
|
1827
|
-
|
|
1412
|
+
if (!res.ok)
|
|
1413
|
+
throw new Error(
|
|
1414
|
+
`skillmux report --server failed: ${res.status} ${await res.text()}`,
|
|
1415
|
+
);
|
|
1416
|
+
const stats = (await res.json()) as StatsResponse;
|
|
1417
|
+
emitSuccess({ isJson: options.isJson }, stats, () =>
|
|
1418
|
+
console.log(renderStatsText(stats)),
|
|
1419
|
+
);
|
|
1828
1420
|
return;
|
|
1829
1421
|
}
|
|
1830
1422
|
|
|
1831
|
-
const db = dbPath
|
|
1832
|
-
|
|
1423
|
+
const db = dbPath
|
|
1424
|
+
? new Database(dbPath, { readonly: true })
|
|
1425
|
+
: openIndex(expandHome((await loadConfig()).state_dir));
|
|
1426
|
+
const stats = getStats(db, since);
|
|
1427
|
+
emitSuccess({ isJson: options.isJson }, stats, () =>
|
|
1428
|
+
console.log(renderStatsText(stats)),
|
|
1429
|
+
);
|
|
1833
1430
|
db.close();
|
|
1834
1431
|
}
|
|
1835
1432
|
|
|
1836
|
-
function parseScanArgs(args: string[]): {
|
|
1433
|
+
function parseScanArgs(args: string[]): {
|
|
1434
|
+
path?: string;
|
|
1435
|
+
format: "text" | "json";
|
|
1436
|
+
failOn?: ScanSeverity;
|
|
1437
|
+
} {
|
|
1837
1438
|
let path: string | undefined;
|
|
1838
1439
|
let format: "text" | "json" = "text";
|
|
1839
1440
|
let failOn: ScanSeverity | undefined;
|
|
@@ -1841,7 +1442,8 @@ function parseScanArgs(args: string[]): { path?: string; format: "text" | "json"
|
|
|
1841
1442
|
const option = args[i];
|
|
1842
1443
|
if (option === "--format") {
|
|
1843
1444
|
const value = args[++i];
|
|
1844
|
-
if (value !== "text" && value !== "json")
|
|
1445
|
+
if (value !== "text" && value !== "json")
|
|
1446
|
+
throw new Error("--format must be text or json");
|
|
1845
1447
|
format = value;
|
|
1846
1448
|
} else if (option === "--fail-on") {
|
|
1847
1449
|
const value = args[++i];
|
|
@@ -1849,6 +1451,8 @@ function parseScanArgs(args: string[]): { path?: string; format: "text" | "json"
|
|
|
1849
1451
|
throw new Error("--fail-on must be low, medium, or high");
|
|
1850
1452
|
}
|
|
1851
1453
|
failOn = value;
|
|
1454
|
+
} else if (option === "--json") {
|
|
1455
|
+
// handled globally by main()'s isJson flag; recognized here so it isn't rejected
|
|
1852
1456
|
} else if (option?.startsWith("--")) {
|
|
1853
1457
|
throw new Error(`unknown scan option: ${option}`);
|
|
1854
1458
|
} else if (path !== undefined) {
|
|
@@ -1860,11 +1464,20 @@ function parseScanArgs(args: string[]): { path?: string; format: "text" | "json"
|
|
|
1860
1464
|
return { path, format, failOn };
|
|
1861
1465
|
}
|
|
1862
1466
|
|
|
1863
|
-
async function runScan(
|
|
1467
|
+
async function runScan(
|
|
1468
|
+
args: string[],
|
|
1469
|
+
options: { isJson: boolean },
|
|
1470
|
+
): Promise<void> {
|
|
1864
1471
|
const { path, format, failOn } = parseScanArgs(args);
|
|
1865
|
-
const rootPath = path
|
|
1472
|
+
const rootPath = path
|
|
1473
|
+
? expandHome(path)
|
|
1474
|
+
: expandHome((await loadConfig()).vault_path);
|
|
1866
1475
|
const result = await scanPath(rootPath);
|
|
1867
|
-
|
|
1476
|
+
emitSuccess({ isJson: options.isJson }, result, () => {
|
|
1477
|
+
console.log(
|
|
1478
|
+
format === "json" ? renderScanJson(result) : renderScanText(result),
|
|
1479
|
+
);
|
|
1480
|
+
});
|
|
1868
1481
|
process.exitCode = scanExitCode(result.findings, failOn);
|
|
1869
1482
|
}
|
|
1870
1483
|
|
|
@@ -1888,6 +1501,8 @@ function parseInstallArgs(args: string[]): {
|
|
|
1888
1501
|
throw new Error("--fail-on must be low, medium, or high");
|
|
1889
1502
|
}
|
|
1890
1503
|
failOn = value;
|
|
1504
|
+
} else if (option === "--json") {
|
|
1505
|
+
// handled globally by main()'s isJson flag; recognized here so it isn't rejected
|
|
1891
1506
|
} else if (option?.startsWith("--")) {
|
|
1892
1507
|
throw new Error(`unknown install option: ${option}`);
|
|
1893
1508
|
} else if (repo !== undefined) {
|
|
@@ -1899,39 +1514,73 @@ function parseInstallArgs(args: string[]): {
|
|
|
1899
1514
|
return { repo, force, dryRun, failOn };
|
|
1900
1515
|
}
|
|
1901
1516
|
|
|
1902
|
-
async function runInstall(
|
|
1517
|
+
async function runInstall(
|
|
1518
|
+
args: string[],
|
|
1519
|
+
options: { isJson: boolean },
|
|
1520
|
+
): Promise<void> {
|
|
1903
1521
|
const { repo, force, dryRun, failOn } = parseInstallArgs(args);
|
|
1904
1522
|
if (!repo) {
|
|
1905
|
-
throw new Error(
|
|
1523
|
+
throw new Error(
|
|
1524
|
+
"usage: skillmux install <repo>[/path] [--force] [--fail-on low|medium|high] [--dry-run] [--json]",
|
|
1525
|
+
);
|
|
1906
1526
|
}
|
|
1907
1527
|
|
|
1908
1528
|
const source = resolveRepoSource(repo);
|
|
1909
1529
|
const cloneDir = await cloneToTemp(source.url);
|
|
1910
1530
|
try {
|
|
1911
|
-
const resolved = resolveSkillDir(
|
|
1912
|
-
|
|
1913
|
-
|
|
1531
|
+
const resolved = resolveSkillDir(
|
|
1532
|
+
cloneDir,
|
|
1533
|
+
deriveRepoName(source.url),
|
|
1534
|
+
source.skillPath,
|
|
1535
|
+
);
|
|
1536
|
+
const { findings } = await validateSkillCandidate(
|
|
1537
|
+
resolved.skillId,
|
|
1538
|
+
resolved.dir,
|
|
1539
|
+
);
|
|
1540
|
+
if (!options.isJson) console.log(renderScanText({ scanned: 1, findings }));
|
|
1914
1541
|
|
|
1915
1542
|
if (scanExitCode(findings, failOn) !== 0) {
|
|
1916
1543
|
process.exitCode = 1;
|
|
1917
|
-
console.error(
|
|
1544
|
+
console.error(
|
|
1545
|
+
`aborting install: a finding met the --fail-on ${failOn} threshold`,
|
|
1546
|
+
);
|
|
1918
1547
|
return;
|
|
1919
1548
|
}
|
|
1920
1549
|
|
|
1921
1550
|
const vaultPath = expandHome((await loadConfig()).vault_path);
|
|
1922
1551
|
if (dryRun) {
|
|
1923
|
-
|
|
1552
|
+
const plannedPath = join(vaultPath, resolved.skillId);
|
|
1553
|
+
emitSuccess(
|
|
1554
|
+
{ isJson: options.isJson },
|
|
1555
|
+
{ skill_id: resolved.skillId, would_install_at: plannedPath },
|
|
1556
|
+
() =>
|
|
1557
|
+
console.log(
|
|
1558
|
+
`dry-run: would install "${resolved.skillId}" into ${plannedPath}`,
|
|
1559
|
+
),
|
|
1560
|
+
);
|
|
1924
1561
|
return;
|
|
1925
1562
|
}
|
|
1926
1563
|
|
|
1927
|
-
const targetDir = installIntoVault(
|
|
1928
|
-
|
|
1564
|
+
const targetDir = installIntoVault(
|
|
1565
|
+
vaultPath,
|
|
1566
|
+
resolved.skillId,
|
|
1567
|
+
resolved.dir,
|
|
1568
|
+
force,
|
|
1569
|
+
);
|
|
1570
|
+
emitSuccess(
|
|
1571
|
+
{ isJson: options.isJson },
|
|
1572
|
+
{ skill_id: resolved.skillId, installed_at: targetDir },
|
|
1573
|
+
() => console.log(`installed "${resolved.skillId}" into ${targetDir}`),
|
|
1574
|
+
);
|
|
1929
1575
|
} finally {
|
|
1930
1576
|
rmSync(cloneDir, { recursive: true, force: true });
|
|
1931
1577
|
}
|
|
1932
1578
|
}
|
|
1933
1579
|
|
|
1934
|
-
function parseCalibrateGenerateDatasetArgs(args: string[]): {
|
|
1580
|
+
function parseCalibrateGenerateDatasetArgs(args: string[]): {
|
|
1581
|
+
vault?: string;
|
|
1582
|
+
out?: string;
|
|
1583
|
+
} {
|
|
1935
1584
|
let vault: string | undefined;
|
|
1936
1585
|
let out: string | undefined;
|
|
1937
1586
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -1952,7 +1601,8 @@ function parseCalibrateGenerateDatasetArgs(args: string[]): { vault?: string; ou
|
|
|
1952
1601
|
}
|
|
1953
1602
|
|
|
1954
1603
|
async function runCalibrateGenerateDataset(args: string[]): Promise<void> {
|
|
1955
|
-
const { vault: vaultArg, out: outArg } =
|
|
1604
|
+
const { vault: vaultArg, out: outArg } =
|
|
1605
|
+
parseCalibrateGenerateDatasetArgs(args);
|
|
1956
1606
|
const config = await loadConfig();
|
|
1957
1607
|
const vaultPath = expandHome(vaultArg ?? config.vault_path);
|
|
1958
1608
|
const outPath = expandHome(outArg ?? join(config.state_dir, "queries.json"));
|
|
@@ -1963,7 +1613,9 @@ async function runCalibrateGenerateDataset(args: string[]): Promise<void> {
|
|
|
1963
1613
|
const parentDir = join(outPath, "..");
|
|
1964
1614
|
mkdirSync(parentDir, { recursive: true });
|
|
1965
1615
|
await Bun.write(outPath, JSON.stringify(dataset, null, 2) + "\n");
|
|
1966
|
-
console.log(
|
|
1616
|
+
console.log(
|
|
1617
|
+
`generated synthetic dataset with ${dataset.length} cases at ${outPath}`,
|
|
1618
|
+
);
|
|
1967
1619
|
}
|
|
1968
1620
|
|
|
1969
1621
|
if (import.meta.main) {
|