@klhapp/skillmux 1.0.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/src/cli.ts CHANGED
@@ -2,8 +2,7 @@
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 { basename, join } from "node:path";
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";
@@ -48,20 +47,12 @@ import {
48
47
  } from "./install";
49
48
  import {
50
49
  parseManifest,
51
- pinCore,
52
- pinProject,
53
50
  resolveManifestPath,
54
51
  serializeManifest,
55
- unpinCore,
56
- unpinProject,
57
- upsertProject,
58
- updateProjectPaths,
59
- updateProjectTargets,
60
52
  validateManifest,
61
- writeManifestAtomic,
62
53
  } from "./manifest";
63
54
  import { downloadLocalModels } from "./models";
64
- import { resolveProjectDirectory, suggestProjectName } from "./project-setup";
55
+
65
56
  import {
66
57
  parseCommaList,
67
58
  promptMultiSelect,
@@ -113,6 +104,11 @@ import {
113
104
  suggestCorrection,
114
105
  } from "./output";
115
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";
116
112
 
117
113
  const KNOWN_COMMANDS = [
118
114
  "context",
@@ -244,7 +240,11 @@ async function main() {
244
240
  await runInit(rawArgv.slice(1), { isJson, dryRun: isDryRun });
245
241
  break;
246
242
  case "project":
247
- await runProject(subCommand, commandArgs, { isJson, dryRun: isDryRun });
243
+ await runProject(subCommand, commandArgs, {
244
+ isJson,
245
+ dryRun: isDryRun,
246
+ sync: runSync,
247
+ });
248
248
  break;
249
249
  case "target":
250
250
  await runTarget(subCommand, commandArgs, { isJson, dryRun: isDryRun });
@@ -391,218 +391,6 @@ async function handleContextCommand(
391
391
  throw new Error("usage: skillmux context <add|list|current|use|remove>");
392
392
  }
393
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
-
426
- async function handleConfigCommand(
427
- adapter: TargetAdapter,
428
- sub: string,
429
- args: string[],
430
- ctx: { target: ResolvedTarget; isJson: boolean; dryRun: boolean },
431
- ) {
432
- if (sub === "init") {
433
- let vaultPath: string | undefined;
434
- let yes = false;
435
- for (let i = 0; i < args.length; i++) {
436
- const option = args[i];
437
- if (option === "--vault") {
438
- vaultPath = args[++i];
439
- if (!vaultPath)
440
- throw new Error("usage: skillmux config init --vault <path> --yes");
441
- } else if (option === "--yes") {
442
- yes = true;
443
- } else if (option === "--dry-run" || option === "--json") {
444
- continue;
445
- } else {
446
- throw new Error(`unknown config init option: ${option}`);
447
- }
448
- }
449
- if (!vaultPath) {
450
- if (isInteractive() && !ctx.isJson) {
451
- vaultPath = "~/skills";
452
- } else {
453
- throw new Error("usage: skillmux config init --vault <path> --yes");
454
- }
455
- }
456
-
457
- migrateLegacyPaths();
458
- const plan = planConfigInit(resolveConfigPath(), expandHome(vaultPath));
459
- if (plan.action === "preserve") {
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
- });
468
- return;
469
- }
470
- if (ctx.dryRun) {
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
- });
479
- return;
480
- }
481
- if (!yes) {
482
- if (!ctx.isJson && isInteractive()) {
483
- if (
484
- !(await confirmAction(
485
- `Create ${plan.configPath} with vault_path ${plan.vaultPath}?`,
486
- ))
487
- ) {
488
- console.log("config init cancelled; nothing written");
489
- return;
490
- }
491
- } else {
492
- throw new Error(
493
- "config initialization requires --yes in noninteractive mode",
494
- );
495
- }
496
- }
497
-
498
- const result = applyConfigInit(plan);
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
- });
510
- return;
511
- }
512
-
513
- if (sub === "show") {
514
- const data = await adapter.getConfigShow();
515
- emitSuccess({ isJson: ctx.isJson, target: ctx.target }, data, () => {
516
- renderTargetBanner(ctx.target);
517
- console.log(JSON.stringify(data.effective, null, 2));
518
- });
519
- return;
520
- }
521
-
522
- if (sub === "get") {
523
- const key = args[0];
524
- if (!key) throw new Error("usage: skillmux config get <key>");
525
- const val = await adapter.getConfigGet(key);
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
- );
535
- return;
536
- }
537
-
538
- if (sub === "validate") {
539
- const res = await adapter.configValidate();
540
- emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
541
- console.log(
542
- res.valid ? "Configuration is valid." : "Configuration is invalid.",
543
- );
544
- });
545
- return;
546
- }
547
-
548
- if (sub === "diff") {
549
- const res = await adapter.configDiff();
550
- emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
551
- renderTargetBanner(ctx.target);
552
- console.log(JSON.stringify(res.diff, null, 2));
553
- });
554
- return;
555
- }
556
-
557
- if (sub === "set") {
558
- const key = args[0];
559
- const value = args[1];
560
- if (!key || value === undefined) {
561
- throw new Error("usage: skillmux config set <key> <value> [--dry-run]");
562
- }
563
- const res = await adapter.configSet(key, value, { dryRun: ctx.dryRun });
564
- emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
565
- renderTargetBanner(ctx.target);
566
- const prefix = ctx.dryRun ? "[dry-run] " : "";
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
- });
574
- return;
575
- }
576
-
577
- if (sub === "status") {
578
- const res = await adapter.configStatus();
579
- emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
580
- renderTargetBanner(ctx.target);
581
- console.log(`Runtime: ${res.runtime}`);
582
- console.log(`Active revision: ${res.active_revision}`);
583
- console.log(`Readiness: ${res.readiness.status}`);
584
- });
585
- return;
586
- }
587
-
588
- throw new Error("usage: skillmux config show");
589
- }
590
-
591
- async function confirmAction(prompt: string): Promise<boolean> {
592
- const readline = createInterface({
593
- input: process.stdin,
594
- output: process.stdout,
595
- });
596
- try {
597
- const answer = (await readline.question(`${prompt} [y/N] `))
598
- .trim()
599
- .toLowerCase();
600
- return answer === "y" || answer === "yes";
601
- } finally {
602
- readline.close();
603
- }
604
- }
605
-
606
394
  async function handleCalibrateCommand(
607
395
  adapter: TargetAdapter,
608
396
  sub: string,
@@ -851,576 +639,6 @@ async function runWhich(args: string[]): Promise<void> {
851
639
  console.log(` shadows: ${shadowedRoot}`);
852
640
  }
853
641
 
854
- const PROJECT_INIT_USAGE =
855
- "usage: skillmux project init [path] [--name <group>] [--skill <id>...] [--client <id>...] [--target <name>...] [--yes] [--no-sync]";
856
-
857
- interface ProjectInitArgs {
858
- path: string;
859
- name: string;
860
- skills: string[];
861
- clients: string[];
862
- targets: string[];
863
- yes: boolean;
864
- sync: boolean;
865
- }
866
-
867
- function configuredTargetForSurface(
868
- manifest: ReturnType<typeof parseManifest>,
869
- surface: { targetName: string; path: string },
870
- ): string | undefined {
871
- if (manifest.targets[surface.targetName]) return surface.targetName;
872
- return Object.entries(manifest.targets).find(
873
- ([, target]) => expandHome(target.dir) === surface.path,
874
- )?.[0];
875
- }
876
-
877
- function configuredTargetsForClients(
878
- manifest: ReturnType<typeof parseManifest>,
879
- clients: readonly string[],
880
- ): string[] {
881
- return planClientSurfaces(clients).surfaces.map((surface) => {
882
- const target = configuredTargetForSurface(manifest, surface);
883
- if (target) return target;
884
- const client = surface.clients[0]!;
885
- throw new Error(
886
- `client target for "${client}" is not configured; run "skillmux init --client ${client} --yes" first`,
887
- );
888
- });
889
- }
890
-
891
- function parseProjectInitArgs(args: string[]): ProjectInitArgs {
892
- let projectPath: string | undefined;
893
- let name: string | undefined;
894
- const skills: string[] = [];
895
- const clients: string[] = [];
896
- const targets: string[] = [];
897
- let yes = false;
898
- let sync = true;
899
-
900
- for (let i = 0; i < args.length; i++) {
901
- const arg = args[i]!;
902
- if (arg === "--name") {
903
- name = args[++i];
904
- if (!name) throw new Error("--name requires a group name");
905
- } else if (arg === "--skill") {
906
- const skill = args[++i];
907
- if (!skill) throw new Error("--skill requires a skill_id");
908
- skills.push(skill);
909
- } else if (arg === "--target") {
910
- const target = args[++i];
911
- if (!target) throw new Error("--target requires a name");
912
- targets.push(target);
913
- } else if (arg === "--client") {
914
- const client = args[++i];
915
- if (!client) throw new Error("--client requires a name");
916
- clients.push(client);
917
- } else if (arg === "--yes") {
918
- yes = true;
919
- } else if (arg === "--no-sync") {
920
- sync = false;
921
- } else if (
922
- arg === "--dry-run" ||
923
- arg === "--json" ||
924
- arg === "--interactive"
925
- ) {
926
- continue;
927
- } else if (arg.startsWith("-")) {
928
- throw new Error(`unknown project init option: ${arg}`);
929
- } else if (projectPath) {
930
- throw new Error(PROJECT_INIT_USAGE);
931
- } else {
932
- projectPath = arg;
933
- }
934
- }
935
-
936
- const path = resolveProjectDirectory(
937
- projectPath ? expandHome(projectPath) : undefined,
938
- );
939
- return {
940
- path,
941
- name: name ?? suggestProjectName(basename(path)),
942
- skills,
943
- clients,
944
- targets,
945
- yes,
946
- sync,
947
- };
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);
974
- }
975
-
976
- async function runProject(
977
- subCommand: string,
978
- args: string[],
979
- options: { isJson: boolean; dryRun: boolean },
980
- ): Promise<void> {
981
- if (subCommand === "list" || subCommand === "show") {
982
- const { manifest } = await loadManifestContext();
983
- const names =
984
- subCommand === "show"
985
- ? [args[0] ?? ""]
986
- : Object.keys(manifest.project ?? {});
987
- if (subCommand === "show" && !manifest.project?.[names[0]!]) {
988
- throw new Error(`[project.${names[0]}] does not exist`);
989
- }
990
- const projects = names.map((name) => ({
991
- name,
992
- paths: manifest.project?.[name]!.paths ?? [],
993
- skills: manifest.project?.[name]!.skills ?? [],
994
- targets: Object.entries(manifest.targets)
995
- .filter(([, target]) => target.project_groups.includes(name))
996
- .map(([target]) => target),
997
- }));
998
- if (options.isJson) {
999
- console.log(JSON.stringify({ schema_version: 1, projects }));
1000
- } else if (projects.length === 0) {
1001
- console.log("no project groups configured");
1002
- } else {
1003
- for (const project of projects) {
1004
- console.log(`${project.name}:`);
1005
- console.log(` paths: ${project.paths.join(", ") || "(none)"}`);
1006
- console.log(` skills: ${project.skills.join(", ") || "(none)"}`);
1007
- console.log(` targets: ${project.targets.join(", ") || "(none)"}`);
1008
- }
1009
- }
1010
- return;
1011
- }
1012
- if (subCommand === "add-path" || subCommand === "remove-path") {
1013
- const group = args[0];
1014
- if (!group)
1015
- throw new Error(
1016
- `usage: skillmux project ${subCommand} <group> [path] --yes`,
1017
- );
1018
- const rawPath = args[1]?.startsWith("-") ? undefined : args[1];
1019
- const projectPath = resolveProjectDirectory(
1020
- rawPath ? expandHome(rawPath) : undefined,
1021
- );
1022
- const yes = args.includes("--yes");
1023
- if (!existsSync(projectPath) || !lstatSync(projectPath).isDirectory()) {
1024
- throw new Error(`project path is not a directory: ${projectPath}`);
1025
- }
1026
- const { config, vaultPath, manifestPath, manifest } =
1027
- await loadManifestContext();
1028
- const updated = updateProjectPaths(manifest, group, {
1029
- ...(subCommand === "add-path"
1030
- ? { add: [projectPath] }
1031
- : { remove: [projectPath] }),
1032
- });
1033
- validateManifest(
1034
- updated,
1035
- vaultPath,
1036
- config.local_vault_paths.map(expandHome),
1037
- );
1038
- if (options.dryRun) {
1039
- console.log(`${subCommand}: [project.${group}] ${projectPath} (dry-run)`);
1040
- return;
1041
- }
1042
- if (
1043
- !(await confirmIfNeeded({
1044
- confirmed: yes,
1045
- isJson: options.isJson,
1046
- prompt: `${subCommand} ${projectPath} in [project.${group}]?`,
1047
- nonInteractiveError: `skillmux project ${subCommand} requires --yes when run non-interactively`,
1048
- }))
1049
- )
1050
- return;
1051
- writeManifestAtomic(manifestPath, updated);
1052
- console.log(`${subCommand}: [project.${group}] ${projectPath}`);
1053
- return;
1054
- }
1055
- if (subCommand === "pin" || subCommand === "unpin") {
1056
- const group = args[0];
1057
- const skills = args.slice(1).filter((arg) => !arg.startsWith("-"));
1058
- if (!group || skills.length === 0) {
1059
- throw new Error(
1060
- `usage: skillmux project ${subCommand} <group> <skill_id>... --yes`,
1061
- );
1062
- }
1063
- const yes = args.includes("--yes");
1064
- const { config, vaultPath, manifestPath, manifest } =
1065
- await loadManifestContext();
1066
- let updated = manifest;
1067
- for (const skill of skills) {
1068
- updated =
1069
- subCommand === "pin"
1070
- ? pinProject(updated, skill, group)
1071
- : unpinProject(updated, skill, group);
1072
- }
1073
- validateManifest(
1074
- updated,
1075
- vaultPath,
1076
- config.local_vault_paths.map(expandHome),
1077
- );
1078
- if (options.dryRun) {
1079
- console.log(
1080
- `${subCommand}: [project.${group}] ${skills.join(", ")} (dry-run)`,
1081
- );
1082
- return;
1083
- }
1084
- if (
1085
- !(await confirmIfNeeded({
1086
- confirmed: yes,
1087
- isJson: options.isJson,
1088
- prompt: `${subCommand} ${skills.join(", ")} in [project.${group}]?`,
1089
- nonInteractiveError: `skillmux project ${subCommand} requires --yes when run non-interactively`,
1090
- }))
1091
- )
1092
- return;
1093
- writeManifestAtomic(manifestPath, updated);
1094
- console.log(`${subCommand}: [project.${group}] ${skills.join(", ")}`);
1095
- return;
1096
- }
1097
- if (subCommand === "attach" || subCommand === "detach") {
1098
- const group = args[0];
1099
- if (!group)
1100
- throw new Error(
1101
- `usage: skillmux project ${subCommand} <group> (--client <id>... | --target <name>...) --yes`,
1102
- );
1103
- const clients: string[] = [];
1104
- const requestedTargets: string[] = [];
1105
- for (let i = 1; i < args.length; i++) {
1106
- if (args[i] === "--client") {
1107
- const value = args[++i];
1108
- if (!value) throw new Error("--client requires a name");
1109
- clients.push(value);
1110
- } else if (args[i] === "--target") {
1111
- const value = args[++i];
1112
- if (!value) throw new Error("--target requires a name");
1113
- requestedTargets.push(value);
1114
- } else if (
1115
- args[i] !== "--yes" &&
1116
- args[i] !== "--dry-run" &&
1117
- args[i] !== "--json"
1118
- ) {
1119
- throw new Error(`unknown project ${subCommand} option: ${args[i]}`);
1120
- }
1121
- }
1122
- const { config, vaultPath, manifestPath, manifest } =
1123
- await loadManifestContext();
1124
- const clientTargets = configuredTargetsForClients(manifest, clients);
1125
- const targets = [...new Set([...requestedTargets, ...clientTargets])];
1126
- if (targets.length === 0) {
1127
- throw new Error(`project ${subCommand} requires --client or --target`);
1128
- }
1129
- const updated = updateProjectTargets(manifest, group, {
1130
- ...(subCommand === "attach" ? { attach: targets } : { detach: targets }),
1131
- });
1132
- validateManifest(
1133
- updated,
1134
- vaultPath,
1135
- config.local_vault_paths.map(expandHome),
1136
- );
1137
- if (options.dryRun) {
1138
- console.log(
1139
- `${subCommand}: [project.${group}] ${targets.join(", ")} (dry-run)`,
1140
- );
1141
- return;
1142
- }
1143
- if (
1144
- !(await confirmIfNeeded({
1145
- confirmed: args.includes("--yes"),
1146
- isJson: options.isJson,
1147
- prompt: `${subCommand} [project.${group}] to ${targets.join(", ")}?`,
1148
- nonInteractiveError: `skillmux project ${subCommand} requires --yes when run non-interactively`,
1149
- }))
1150
- )
1151
- return;
1152
- writeManifestAtomic(manifestPath, updated);
1153
- console.log(`${subCommand}: [project.${group}] ${targets.join(", ")}`);
1154
- return;
1155
- }
1156
- if (subCommand !== "init") throw new Error(PROJECT_INIT_USAGE);
1157
- let request = parseProjectInitArgs(args);
1158
- const guided = shouldUseWizard(args, {
1159
- interactive: isInteractive(),
1160
- json: options.isJson,
1161
- dryRun: options.dryRun,
1162
- });
1163
- if (!existsSync(request.path))
1164
- throw new Error(`project path does not exist: ${request.path}`);
1165
- if (!lstatSync(request.path).isDirectory()) {
1166
- throw new Error(`project path is not a directory: ${request.path}`);
1167
- }
1168
-
1169
- const { config, vaultPath, manifestPath, manifest } =
1170
- await loadManifestContext();
1171
- const localVaultPaths = config.local_vault_paths.map(expandHome);
1172
- if (guided) {
1173
- const name = await promptText("Project group", request.name);
1174
- const availableClients = SUPPORTED_CLIENT_IDS.filter((client) => {
1175
- const surface = planClientSurfaces([client]).surfaces[0];
1176
- return (
1177
- surface !== undefined &&
1178
- configuredTargetForSurface(manifest, surface) !== undefined
1179
- );
1180
- });
1181
- const clients = await promptMultiSelect(
1182
- "Which clients should receive project skills?",
1183
- availableClients.map((client) => ({
1184
- value: client,
1185
- label: client,
1186
- selected:
1187
- request.clients.length === 0 || request.clients.includes(client),
1188
- })),
1189
- );
1190
- const skills = parseCommaList(
1191
- await promptText(
1192
- "Project skill IDs, comma-separated",
1193
- request.skills.join(","),
1194
- ),
1195
- );
1196
- request = { ...request, name, clients, skills };
1197
- }
1198
- const clientTargets = configuredTargetsForClients(manifest, request.clients);
1199
- const targets = [...new Set([...request.targets, ...clientTargets])];
1200
- const updated = upsertProject(manifest, {
1201
- name: request.name,
1202
- paths: [request.path],
1203
- skills: request.skills,
1204
- targets,
1205
- });
1206
- const { notes } = validateManifest(updated, vaultPath, localVaultPaths);
1207
- const plan = {
1208
- mode: "project",
1209
- project: request.name,
1210
- path: request.path,
1211
- skills: request.skills,
1212
- clients: request.clients,
1213
- targets,
1214
- sync: request.sync,
1215
- notes,
1216
- };
1217
-
1218
- if (options.dryRun) {
1219
- console.log(
1220
- options.isJson
1221
- ? JSON.stringify({ schema_version: 1, plan })
1222
- : `project plan: ${JSON.stringify(plan)}`,
1223
- );
1224
- return;
1225
- }
1226
- if (!request.yes) {
1227
- if (!options.isJson && isInteractive()) {
1228
- if (guided) {
1229
- console.log("\nReview");
1230
- console.log(` project: ${request.name}`);
1231
- console.log(` path: ${request.path}`);
1232
- console.log(` clients: ${request.clients.join(", ") || "(none)"}`);
1233
- console.log(` skills: ${request.skills.join(", ") || "(none)"}`);
1234
- console.log(` sync: ${request.sync ? "yes" : "no"}`);
1235
- }
1236
- if (
1237
- !(await confirmAction(
1238
- `Apply project setup for ${request.name} at ${request.path}?`,
1239
- ))
1240
- ) {
1241
- console.log("project setup cancelled");
1242
- return;
1243
- }
1244
- } else {
1245
- throw new Error(
1246
- "skillmux project init requires --yes when run non-interactively",
1247
- );
1248
- }
1249
- }
1250
-
1251
- writeManifestAtomic(manifestPath, updated);
1252
- if (request.sync) {
1253
- try {
1254
- await runSync([]);
1255
- } catch (error) {
1256
- throw new Error(
1257
- `project configuration was saved, but sync failed; fix the reported issue and run "skillmux sync": ${
1258
- error instanceof Error ? error.message : String(error)
1259
- }`,
1260
- );
1261
- }
1262
- }
1263
- if (options.isJson) {
1264
- console.log(JSON.stringify({ schema_version: 1, result: plan }));
1265
- } else {
1266
- console.log(`project "${request.name}" ready at ${request.path}`);
1267
- }
1268
- }
1269
-
1270
- async function runCore(
1271
- subCommand: string,
1272
- args: string[],
1273
- options: { isJson: boolean; dryRun: boolean },
1274
- ): Promise<void> {
1275
- if (subCommand !== "pin" && subCommand !== "unpin") {
1276
- throw new Error("usage: skillmux core <pin|unpin>");
1277
- }
1278
- const skillIds = args.filter((arg) => !arg.startsWith("-"));
1279
- if (skillIds.length === 0) {
1280
- throw new Error(`usage: skillmux core ${subCommand} <skill_id>... --yes`);
1281
- }
1282
- const yes = args.includes("--yes");
1283
- const { config, vaultPath, manifestPath, manifest } =
1284
- await loadManifestContext();
1285
- let updated = manifest;
1286
- for (const skillId of skillIds) {
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
-
1319
- async function runTarget(
1320
- subCommand: string,
1321
- args: string[],
1322
- options: { isJson: boolean; dryRun: boolean },
1323
- ): Promise<void> {
1324
- const { vaultPath, manifestPath, manifest } = await loadManifestContext();
1325
-
1326
- if (subCommand === "list" || subCommand === "show") {
1327
- const names =
1328
- subCommand === "show" ? [args[0] ?? ""] : Object.keys(manifest.targets);
1329
- if (subCommand === "show" && !manifest.targets[names[0]!]) {
1330
- throw new Error(`target "${names[0]}" does not exist`);
1331
- }
1332
- const targets = names.map((name) => {
1333
- const target = manifest.targets[name]!;
1334
- const clients = SUPPORTED_CLIENT_IDS.filter((client) => {
1335
- const surface = planClientSurfaces([client]).surfaces[0];
1336
- return surface !== undefined && surface.path === expandHome(target.dir);
1337
- });
1338
- return { name, ...target, clients };
1339
- });
1340
- if (options.isJson) {
1341
- console.log(JSON.stringify({ schema_version: 1, targets }));
1342
- } else if (targets.length === 0) {
1343
- console.log("no targets configured");
1344
- } else {
1345
- for (const target of targets) {
1346
- console.log(`${target.name}:`);
1347
- console.log(` dir: ${target.dir}`);
1348
- console.log(` host: ${target.host ?? "(global)"}`);
1349
- console.log(` clients: ${target.clients.join(", ") || "(custom)"}`);
1350
- console.log(
1351
- ` projects: ${target.project_groups.join(", ") || "(none)"}`,
1352
- );
1353
- }
1354
- }
1355
- return;
1356
- }
1357
-
1358
- if (subCommand === "add") {
1359
- const name = args[0];
1360
- const dirIndex = args.indexOf("--dir");
1361
- const rawPath = dirIndex === -1 ? undefined : args[dirIndex + 1];
1362
- if (!name || !rawPath)
1363
- throw new Error("usage: skillmux target add <name> --dir <dir> --yes");
1364
- const path = expandHome(rawPath);
1365
- if (options.dryRun) {
1366
- const planned = planInitManifest(vaultPath, [{ name, dir: path }], []);
1367
- console.log(
1368
- options.isJson
1369
- ? JSON.stringify({ schema_version: 1, target: planned.targets[name] })
1370
- : `target add: ${name} -> ${path} (dry-run)`,
1371
- );
1372
- return;
1373
- }
1374
- if (
1375
- !(await confirmIfNeeded({
1376
- confirmed: args.includes("--yes"),
1377
- isJson: options.isJson,
1378
- prompt: `Adopt target ${name} at ${path}?`,
1379
- nonInteractiveError:
1380
- "skillmux target add requires --yes when run non-interactively",
1381
- }))
1382
- )
1383
- return;
1384
- applyInit(vaultPath, [{ name, dir: path }]);
1385
- console.log(`target "${name}" added at ${path}`);
1386
- return;
1387
- }
1388
-
1389
- if (subCommand === "remove") {
1390
- const name = args[0];
1391
- if (!name || !manifest.targets[name]) {
1392
- throw new Error(
1393
- name
1394
- ? `target "${name}" does not exist`
1395
- : "usage: skillmux target remove <name> --yes",
1396
- );
1397
- }
1398
- if (options.dryRun) {
1399
- console.log(`target remove: ${name} (files preserved, dry-run)`);
1400
- return;
1401
- }
1402
- if (
1403
- !(await confirmIfNeeded({
1404
- confirmed: args.includes("--yes"),
1405
- isJson: options.isJson,
1406
- prompt: `Remove target ${name} from the manifest and preserve its files?`,
1407
- nonInteractiveError:
1408
- "skillmux target remove requires --yes when run non-interactively",
1409
- }))
1410
- )
1411
- return;
1412
- const targets = { ...manifest.targets };
1413
- delete targets[name];
1414
- writeManifestAtomic(manifestPath, { ...manifest, targets });
1415
- console.log(
1416
- `target "${name}" removed from the manifest; files preserved at ${manifest.targets[name]!.dir}`,
1417
- );
1418
- return;
1419
- }
1420
-
1421
- throw new Error("usage: skillmux target <list|show|add|remove>");
1422
- }
1423
-
1424
642
  async function runLocalVaultInit(
1425
643
  args: string[],
1426
644
  options: { isJson: boolean; dryRun: boolean },
@@ -1438,14 +656,16 @@ async function runLocalVaultInit(
1438
656
  if (!existsSync(expanded)) throw new Error(`"${path}" does not exist`);
1439
657
  const markerPath = join(expanded, ".skillmux");
1440
658
  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)`,
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
+ ),
1449
669
  );
1450
670
  return;
1451
671
  }
@@ -1460,19 +680,17 @@ async function runLocalVaultInit(
1460
680
  )
1461
681
  return;
1462
682
  writeLocalVaultMarker(expanded, expandHome(config.vault_path));
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
- }
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
+ );
1476
694
  }
1477
695
 
1478
696
  async function runModelDownload(options: { isJson: boolean }): Promise<void> {