@klhapp/skillmux 0.4.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +57 -11
- package/docs/configuration.md +50 -9
- package/package.json +1 -1
- package/src/cli.ts +524 -44
- package/src/completions.ts +33 -2
- package/src/config.ts +7 -3
- package/src/init-clients.ts +189 -0
- package/src/init-instructions.ts +173 -0
- package/src/init.ts +280 -17
- package/src/manifest.ts +24 -13
- package/src/setup.ts +145 -0
- package/src/sync.ts +136 -19
package/src/cli.ts
CHANGED
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Database } from "bun:sqlite";
|
|
3
3
|
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
4
|
+
import { hostname } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
5
7
|
import { generateDataset } from "./dataset-generator";
|
|
6
8
|
|
|
7
9
|
import { createClients } from "./clients";
|
|
8
|
-
import { loadConfig } from "./config";
|
|
9
|
-
import { expandHome } from "./config";
|
|
10
|
+
import { expandHome, loadConfig, migrateLegacyPaths, resolveConfigPath } from "./config";
|
|
10
11
|
import { openIndex } from "./db";
|
|
11
12
|
import { diagnose } from "./doctor";
|
|
12
13
|
import { evalVault } from "./eval";
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
assessClientReadiness,
|
|
16
|
+
planClientSurfaces,
|
|
17
|
+
resolveBuiltInTarget,
|
|
18
|
+
type ClientId,
|
|
19
|
+
type ReadinessAxis,
|
|
20
|
+
} from "./init-clients";
|
|
21
|
+
import {
|
|
22
|
+
applyInstructionPlan,
|
|
23
|
+
planInstructionSetup,
|
|
24
|
+
rollbackInstructionPlan,
|
|
25
|
+
} from "./init-instructions";
|
|
26
|
+
import {
|
|
27
|
+
applyInit,
|
|
28
|
+
deriveTargetName,
|
|
29
|
+
detectSurfaces,
|
|
30
|
+
planInitManifest,
|
|
31
|
+
printLastMile,
|
|
32
|
+
surfaceCandidates,
|
|
33
|
+
} from "./init";
|
|
14
34
|
import {
|
|
15
35
|
cloneToTemp,
|
|
16
36
|
deriveRepoName,
|
|
@@ -32,6 +52,13 @@ import {
|
|
|
32
52
|
import { downloadLocalModels } from "./models";
|
|
33
53
|
import { backfillEmbeddings, configure, rebuildIndex } from "./router-core";
|
|
34
54
|
import { renderScanJson, renderScanText, scanExitCode, scanPath, type ScanSeverity } from "./scan";
|
|
55
|
+
import {
|
|
56
|
+
applyConfigInit,
|
|
57
|
+
inspectVault,
|
|
58
|
+
planConfigInit,
|
|
59
|
+
rollbackConfigInit,
|
|
60
|
+
type ConfigInitPlan,
|
|
61
|
+
} from "./setup";
|
|
35
62
|
import { getStats, renderStatsText, type StatsResponse } from "./stats";
|
|
36
63
|
import {
|
|
37
64
|
installPostMergeHook,
|
|
@@ -105,7 +132,8 @@ async function main() {
|
|
|
105
132
|
let resolvedTarget: ResolvedTarget = { type: "local", name: "local" };
|
|
106
133
|
|
|
107
134
|
// Only resolve target if command is target-aware or context/config/calibrate
|
|
108
|
-
|
|
135
|
+
const isLocalConfigInit = command === "config" && rawArgv[1] === "init";
|
|
136
|
+
if ((["context", "config", "calibrate"].includes(command) && !isLocalConfigInit) || flagContext || flagServer) {
|
|
109
137
|
try {
|
|
110
138
|
resolvedTarget = await resolveTarget({ context: flagContext, server: flagServer });
|
|
111
139
|
} catch (err: any) {
|
|
@@ -161,7 +189,7 @@ async function main() {
|
|
|
161
189
|
await runSync(rawArgv.slice(1));
|
|
162
190
|
break;
|
|
163
191
|
case "init":
|
|
164
|
-
await runInit(rawArgv.slice(1));
|
|
192
|
+
await runInit(rawArgv.slice(1), { isJson, dryRun: isDryRun });
|
|
165
193
|
break;
|
|
166
194
|
case "report":
|
|
167
195
|
await runReport(rawArgv.slice(1));
|
|
@@ -293,6 +321,88 @@ async function handleConfigCommand(
|
|
|
293
321
|
args: string[],
|
|
294
322
|
ctx: { target: ResolvedTarget; isJson: boolean; dryRun: boolean }
|
|
295
323
|
) {
|
|
324
|
+
if (sub === "init") {
|
|
325
|
+
let vaultPath: string | undefined;
|
|
326
|
+
let yes = false;
|
|
327
|
+
for (let i = 0; i < args.length; i++) {
|
|
328
|
+
const option = args[i];
|
|
329
|
+
if (option === "--vault") {
|
|
330
|
+
vaultPath = args[++i];
|
|
331
|
+
if (!vaultPath) throw new Error("usage: skillmux config init --vault <path> --yes");
|
|
332
|
+
} else if (option === "--yes") {
|
|
333
|
+
yes = true;
|
|
334
|
+
} else if (option === "--dry-run" || option === "--json") {
|
|
335
|
+
continue;
|
|
336
|
+
} else {
|
|
337
|
+
throw new Error(`unknown config init option: ${option}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (!vaultPath) {
|
|
341
|
+
if (isInteractive() && !ctx.isJson) {
|
|
342
|
+
vaultPath = "~/skills";
|
|
343
|
+
} else {
|
|
344
|
+
throw new Error("usage: skillmux config init --vault <path> --yes");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
migrateLegacyPaths();
|
|
349
|
+
const plan = planConfigInit(resolveConfigPath(), expandHome(vaultPath));
|
|
350
|
+
if (plan.action === "preserve") {
|
|
351
|
+
console.log(ctx.isJson
|
|
352
|
+
? JSON.stringify({
|
|
353
|
+
schema_version: 1,
|
|
354
|
+
ok: true,
|
|
355
|
+
command: "config init",
|
|
356
|
+
phase: "result",
|
|
357
|
+
dry_run: ctx.dryRun,
|
|
358
|
+
applied: false,
|
|
359
|
+
plan: { config_path: plan.configPath, vault_path: plan.vaultPath, action: "preserve" },
|
|
360
|
+
})
|
|
361
|
+
: `preserved existing config: ${plan.configPath}`);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (ctx.dryRun) {
|
|
365
|
+
console.log(ctx.isJson
|
|
366
|
+
? JSON.stringify({
|
|
367
|
+
schema_version: 1,
|
|
368
|
+
ok: true,
|
|
369
|
+
command: "config init",
|
|
370
|
+
phase: "plan",
|
|
371
|
+
dry_run: true,
|
|
372
|
+
applied: false,
|
|
373
|
+
plan: { config_path: plan.configPath, vault_path: plan.vaultPath, action: "create" },
|
|
374
|
+
})
|
|
375
|
+
: `config create: ${plan.configPath} (dry-run)`);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (!yes) {
|
|
379
|
+
if (!ctx.isJson && isInteractive()) {
|
|
380
|
+
if (!(await confirmAction(`Create ${plan.configPath} with vault_path ${plan.vaultPath}?`))) {
|
|
381
|
+
console.log("config init cancelled; nothing written");
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
throw new Error("config initialization requires --yes in noninteractive mode");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const result = applyConfigInit(plan);
|
|
390
|
+
console.log(ctx.isJson
|
|
391
|
+
? JSON.stringify({
|
|
392
|
+
schema_version: 1,
|
|
393
|
+
ok: true,
|
|
394
|
+
command: "config init",
|
|
395
|
+
phase: "result",
|
|
396
|
+
dry_run: false,
|
|
397
|
+
applied: result === "created",
|
|
398
|
+
plan: { config_path: plan.configPath, vault_path: plan.vaultPath, action: plan.action },
|
|
399
|
+
})
|
|
400
|
+
: result === "created"
|
|
401
|
+
? `created ${plan.configPath}`
|
|
402
|
+
: `preserved existing config: ${plan.configPath}`);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
296
406
|
if (sub === "show") {
|
|
297
407
|
const data = await adapter.getConfigShow();
|
|
298
408
|
if (ctx.isJson) {
|
|
@@ -371,6 +481,16 @@ async function handleConfigCommand(
|
|
|
371
481
|
throw new Error("usage: skillmux config show");
|
|
372
482
|
}
|
|
373
483
|
|
|
484
|
+
async function confirmAction(prompt: string): Promise<boolean> {
|
|
485
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
486
|
+
try {
|
|
487
|
+
const answer = (await readline.question(`${prompt} [y/N] `)).trim().toLowerCase();
|
|
488
|
+
return answer === "y" || answer === "yes";
|
|
489
|
+
} finally {
|
|
490
|
+
readline.close();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
374
494
|
async function handleCalibrateCommand(
|
|
375
495
|
adapter: TargetAdapter,
|
|
376
496
|
sub: string,
|
|
@@ -477,7 +597,24 @@ function handleError(
|
|
|
477
597
|
}
|
|
478
598
|
|
|
479
599
|
function printHelp(): void {
|
|
480
|
-
console.log(`usage: skillmux <
|
|
600
|
+
console.log(`usage: skillmux <command> [options]
|
|
601
|
+
|
|
602
|
+
Setup:
|
|
603
|
+
skillmux config init --vault <path> --yes
|
|
604
|
+
skillmux init [--client <name>...] [--target <name>...] [--path <dir>]
|
|
605
|
+
[--vault <path>] [--core <skill_id>...]
|
|
606
|
+
[--migrate-full-vault] [--yes|--dry-run] [--json]
|
|
607
|
+
|
|
608
|
+
Init clients:
|
|
609
|
+
claude-code, codex, gemini-cli, opencode, github-copilot, windsurf,
|
|
610
|
+
antigravity, goose, hermes, skillmux-mcp
|
|
611
|
+
|
|
612
|
+
Init targets:
|
|
613
|
+
agent-skills, claude-code, codex, custom
|
|
614
|
+
|
|
615
|
+
Commands:
|
|
616
|
+
serve, index, sync, init, report, scan, install, eval, doctor, which,
|
|
617
|
+
manifest, local-vault, config, models, calibrate, context, completions`);
|
|
481
618
|
}
|
|
482
619
|
|
|
483
620
|
// ---------------------------------------------------------------------------
|
|
@@ -575,14 +712,14 @@ async function runWhich(args: string[]): Promise<void> {
|
|
|
575
712
|
for (const shadowedRoot of roots.slice(1)) console.log(` shadows: ${shadowedRoot}`);
|
|
576
713
|
}
|
|
577
714
|
|
|
578
|
-
const MANIFEST_USAGE = "usage: skillmux manifest <pin|unpin> <skill_id> (--core | --project <group> [--
|
|
715
|
+
const MANIFEST_USAGE = "usage: skillmux manifest <pin|unpin> <skill_id> (--core | --project <group> [--path <path>...])";
|
|
579
716
|
|
|
580
|
-
function parseManifestPinArgs(args: string[]): { skillId: string; core: boolean; project?: string;
|
|
717
|
+
function parseManifestPinArgs(args: string[]): { skillId: string; core: boolean; project?: string; paths: string[] } {
|
|
581
718
|
const skillId = args[0];
|
|
582
719
|
if (!skillId) throw new Error(MANIFEST_USAGE);
|
|
583
720
|
let core = false;
|
|
584
721
|
let project: string | undefined;
|
|
585
|
-
const
|
|
722
|
+
const paths: string[] = [];
|
|
586
723
|
for (let i = 1; i < args.length; i++) {
|
|
587
724
|
const arg = args[i];
|
|
588
725
|
if (arg === "--core") core = true;
|
|
@@ -590,19 +727,19 @@ function parseManifestPinArgs(args: string[]): { skillId: string; core: boolean;
|
|
|
590
727
|
const value = args[++i];
|
|
591
728
|
if (!value) throw new Error("--project requires a group name");
|
|
592
729
|
project = value;
|
|
593
|
-
} else if (arg === "--
|
|
730
|
+
} else if (arg === "--path") {
|
|
594
731
|
const value = args[++i];
|
|
595
|
-
if (!value) throw new Error("--
|
|
596
|
-
|
|
732
|
+
if (!value) throw new Error("--path requires a path");
|
|
733
|
+
paths.push(value);
|
|
597
734
|
} else throw new Error(`unknown manifest option: ${arg}`);
|
|
598
735
|
}
|
|
599
736
|
if (core === (project !== undefined)) throw new Error(MANIFEST_USAGE);
|
|
600
|
-
return { skillId, core, project,
|
|
737
|
+
return { skillId, core, project, paths };
|
|
601
738
|
}
|
|
602
739
|
|
|
603
740
|
async function runManifest(subCommand: string, args: string[]): Promise<void> {
|
|
604
741
|
if (subCommand !== "pin" && subCommand !== "unpin") throw new Error(MANIFEST_USAGE);
|
|
605
|
-
const { skillId, core, project,
|
|
742
|
+
const { skillId, core, project, paths } = parseManifestPinArgs(args);
|
|
606
743
|
const config = await loadConfig();
|
|
607
744
|
const vaultPath = expandHome(config.vault_path);
|
|
608
745
|
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
@@ -616,7 +753,7 @@ async function runManifest(subCommand: string, args: string[]): Promise<void> {
|
|
|
616
753
|
} else {
|
|
617
754
|
updated =
|
|
618
755
|
subCommand === "pin"
|
|
619
|
-
? pinProject(manifest, skillId, project!,
|
|
756
|
+
? pinProject(manifest, skillId, project!, paths)
|
|
620
757
|
: unpinProject(manifest, skillId, project!);
|
|
621
758
|
}
|
|
622
759
|
validateManifest(updated, vaultPath, localVaultPaths);
|
|
@@ -685,7 +822,14 @@ async function runSync(args: string[]): Promise<void> {
|
|
|
685
822
|
const { notes } = validateManifest(manifest, vaultPath, localVaultPaths);
|
|
686
823
|
for (const note of notes) console.log(`note: ${note}`);
|
|
687
824
|
|
|
825
|
+
const currentHost = hostname();
|
|
688
826
|
for (const [targetName, target] of Object.entries(manifest.targets)) {
|
|
827
|
+
if (target.host !== undefined && target.host !== currentHost) {
|
|
828
|
+
console.log(
|
|
829
|
+
`${targetName}: skipped (host ${target.host} does not match current host ${currentHost})`,
|
|
830
|
+
);
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
689
833
|
const targetDir = expandHome(target.dir);
|
|
690
834
|
|
|
691
835
|
if (restoreMonolith) {
|
|
@@ -723,8 +867,21 @@ async function runSync(args: string[]): Promise<void> {
|
|
|
723
867
|
}
|
|
724
868
|
}
|
|
725
869
|
|
|
726
|
-
function parseInitArgs(args: string[]): {
|
|
870
|
+
function parseInitArgs(args: string[]): {
|
|
871
|
+
targets: string[];
|
|
872
|
+
clients: string[];
|
|
873
|
+
coreSkillIds: string[];
|
|
874
|
+
customPath?: string;
|
|
875
|
+
migrateFullVault: boolean;
|
|
876
|
+
vaultPath?: string;
|
|
877
|
+
yes: boolean;
|
|
878
|
+
} {
|
|
727
879
|
const targets: string[] = [];
|
|
880
|
+
const clients: string[] = [];
|
|
881
|
+
const coreSkillIds: string[] = [];
|
|
882
|
+
let customPath: string | undefined;
|
|
883
|
+
let migrateFullVault = false;
|
|
884
|
+
let vaultPath: string | undefined;
|
|
728
885
|
let yes = false;
|
|
729
886
|
for (let i = 0; i < args.length; i++) {
|
|
730
887
|
const option = args[i];
|
|
@@ -733,56 +890,379 @@ function parseInitArgs(args: string[]): { targets: string[]; yes: boolean } {
|
|
|
733
890
|
if (!value) throw new Error("--target requires a name");
|
|
734
891
|
targets.push(value);
|
|
735
892
|
i++;
|
|
893
|
+
} else if (option === "--client") {
|
|
894
|
+
const value = args[i + 1];
|
|
895
|
+
if (!value) throw new Error("--client requires a name");
|
|
896
|
+
clients.push(value);
|
|
897
|
+
i++;
|
|
898
|
+
} else if (option === "--vault") {
|
|
899
|
+
const value = args[i + 1];
|
|
900
|
+
if (!value) throw new Error("--vault requires a path");
|
|
901
|
+
vaultPath = value;
|
|
902
|
+
i++;
|
|
903
|
+
} else if (option === "--path") {
|
|
904
|
+
const value = args[i + 1];
|
|
905
|
+
if (!value) throw new Error("--path requires a directory");
|
|
906
|
+
customPath = value;
|
|
907
|
+
i++;
|
|
908
|
+
} else if (option === "--core") {
|
|
909
|
+
const value = args[i + 1];
|
|
910
|
+
if (!value) throw new Error("--core requires a skill_id");
|
|
911
|
+
coreSkillIds.push(value);
|
|
912
|
+
i++;
|
|
913
|
+
} else if (option === "--dry-run" || option === "--json") {
|
|
914
|
+
continue;
|
|
915
|
+
} else if (option === "--migrate-full-vault") {
|
|
916
|
+
migrateFullVault = true;
|
|
736
917
|
} else if (option === "--yes") {
|
|
737
918
|
yes = true;
|
|
738
919
|
} else {
|
|
739
920
|
throw new Error(`unknown init option: ${option}`);
|
|
740
921
|
}
|
|
741
922
|
}
|
|
742
|
-
return { targets, yes };
|
|
923
|
+
return { targets, clients, coreSkillIds, customPath, migrateFullVault, vaultPath, yes };
|
|
743
924
|
}
|
|
744
925
|
|
|
745
|
-
async function runInit(
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
926
|
+
async function runInit(
|
|
927
|
+
args: string[],
|
|
928
|
+
options: { isJson: boolean; dryRun: boolean },
|
|
929
|
+
): Promise<void> {
|
|
930
|
+
const {
|
|
931
|
+
targets: explicitTargets,
|
|
932
|
+
clients: requestedClients,
|
|
933
|
+
coreSkillIds,
|
|
934
|
+
customPath,
|
|
935
|
+
migrateFullVault,
|
|
936
|
+
vaultPath: requestedVaultPath,
|
|
937
|
+
yes,
|
|
938
|
+
} = parseInitArgs(args);
|
|
939
|
+
migrateLegacyPaths();
|
|
940
|
+
const configPath = resolveConfigPath();
|
|
941
|
+
let configPlan: ConfigInitPlan | undefined;
|
|
942
|
+
let vaultPath: string;
|
|
943
|
+
if (!existsSync(configPath)) {
|
|
944
|
+
const bootstrapVaultPath = requestedVaultPath ??
|
|
945
|
+
(!options.isJson && isInteractive() ? "~/skills" : undefined);
|
|
946
|
+
if (!bootstrapVaultPath) {
|
|
947
|
+
throw new Error(`machine config does not exist: ${configPath}; re-run with --vault <path>`);
|
|
948
|
+
}
|
|
949
|
+
configPlan = planConfigInit(configPath, expandHome(bootstrapVaultPath));
|
|
950
|
+
vaultPath = configPlan.vaultPath;
|
|
951
|
+
if (!options.isJson) {
|
|
952
|
+
console.log(`config create: ${configPath}`);
|
|
953
|
+
}
|
|
954
|
+
} else {
|
|
955
|
+
const config = await loadConfig();
|
|
956
|
+
vaultPath = expandHome(config.vault_path);
|
|
957
|
+
if (requestedVaultPath && expandHome(requestedVaultPath) !== vaultPath) {
|
|
958
|
+
throw new Error(
|
|
959
|
+
`machine config already uses vault_path ${vaultPath}; --vault does not overwrite existing config`,
|
|
960
|
+
);
|
|
756
961
|
}
|
|
757
|
-
const kind = candidate.isSymlink ? "symlink" : "real dir";
|
|
758
|
-
const marked = candidate.alreadyMarked ? ", already skillmux-managed" : "";
|
|
759
|
-
console.log(`${name} (${candidate.path}): ${kind}, ${candidate.skillCount} skills${marked}`);
|
|
760
962
|
}
|
|
761
963
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
964
|
+
const vaultHealth = inspectVault(vaultPath);
|
|
965
|
+
if (!vaultHealth.ok) {
|
|
966
|
+
throw new Error(vaultHealth.message);
|
|
765
967
|
}
|
|
766
968
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
969
|
+
const clientPlan = planClientSurfaces(requestedClients, {
|
|
970
|
+
codexHome: process.env.CODEX_HOME ? expandHome(process.env.CODEX_HOME) : undefined,
|
|
971
|
+
});
|
|
972
|
+
const instructionPlan = planInstructionSetup(clientPlan.clients.map((client) => client.id), {
|
|
973
|
+
codexHome: process.env.CODEX_HOME ? expandHome(process.env.CODEX_HOME) : undefined,
|
|
974
|
+
});
|
|
975
|
+
const instructionReadiness: Partial<Record<ClientId, ReadinessAxis>> = {};
|
|
976
|
+
for (const change of instructionPlan.changes) {
|
|
977
|
+
for (const client of change.clients) {
|
|
978
|
+
instructionReadiness[client] = {
|
|
979
|
+
status: change.status === "unchanged" ? "ready" : "planned",
|
|
980
|
+
detail: change.path,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
for (const manual of instructionPlan.manual) {
|
|
985
|
+
instructionReadiness[manual.client] = { status: "manual", detail: manual.reason };
|
|
986
|
+
}
|
|
987
|
+
const builtInNames = new Set(["agent-skills", "claude-code", "codex", "custom", "agents", "claude"]);
|
|
988
|
+
const explicitSurfaceTargets = explicitTargets
|
|
989
|
+
.filter((name) => builtInNames.has(name))
|
|
990
|
+
.map((name) =>
|
|
991
|
+
resolveBuiltInTarget(name, {
|
|
992
|
+
codexHome: process.env.CODEX_HOME ? expandHome(process.env.CODEX_HOME) : undefined,
|
|
993
|
+
customPath: customPath ? expandHome(customPath) : undefined,
|
|
994
|
+
}),
|
|
770
995
|
);
|
|
996
|
+
if (customPath && !explicitTargets.includes("custom")) {
|
|
997
|
+
throw new Error("--path may only be used with --target custom");
|
|
998
|
+
}
|
|
999
|
+
for (const target of explicitSurfaceTargets) {
|
|
1000
|
+
if (target.warning) console.error(`warning: ${target.warning}`);
|
|
1001
|
+
}
|
|
1002
|
+
const targetByPath = new Map(
|
|
1003
|
+
explicitSurfaceTargets.map((target) => [target.path, target.targetName] as const),
|
|
1004
|
+
);
|
|
1005
|
+
for (const surface of clientPlan.surfaces) {
|
|
1006
|
+
if (!targetByPath.has(surface.path)) targetByPath.set(surface.path, surface.targetName);
|
|
1007
|
+
}
|
|
1008
|
+
const candidatePaths = [
|
|
1009
|
+
...new Set([
|
|
1010
|
+
...surfaceCandidates().map(expandHome),
|
|
1011
|
+
...targetByPath.keys(),
|
|
1012
|
+
]),
|
|
1013
|
+
];
|
|
1014
|
+
const candidates = detectSurfaces(candidatePaths, vaultPath);
|
|
1015
|
+
if (!options.isJson) {
|
|
1016
|
+
for (const candidate of candidates) {
|
|
1017
|
+
const name = targetByPath.get(candidate.path) ?? deriveTargetName(candidate.path);
|
|
1018
|
+
if (candidate.state === "missing") {
|
|
1019
|
+
console.log(`${name} (${candidate.path}): not found`);
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
if (candidate.state === "broken-symlink") {
|
|
1023
|
+
console.log(`${name} (${candidate.path}): broken symlink`);
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
if (candidate.state === "full-vault") {
|
|
1027
|
+
console.log(`${name} (${candidate.path}): full-vault -> ${candidate.canonicalPath}`);
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
if (candidate.state === "external-symlink") {
|
|
1031
|
+
console.log(`${name} (${candidate.path}): external symlink -> ${candidate.canonicalPath}`);
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
if (candidate.state === "unsupported") {
|
|
1035
|
+
console.log(`${name} (${candidate.path}): unsupported filesystem entry`);
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
const kind = "real dir";
|
|
1039
|
+
const marked = candidate.alreadyMarked ? ", already skillmux-managed" : "";
|
|
1040
|
+
console.log(`${name} (${candidate.path}): ${kind}, ${candidate.skillCount} skills${marked}`);
|
|
1041
|
+
}
|
|
1042
|
+
for (const readiness of assessClientReadiness(clientPlan, instructionReadiness)) {
|
|
1043
|
+
console.log(`\n${readiness.client} readiness:`);
|
|
1044
|
+
console.log(` skill surface: ${readiness.skillSurface.status} — ${readiness.skillSurface.detail}`);
|
|
1045
|
+
console.log(` MCP registration: ${readiness.mcpRegistration.status} — ${readiness.mcpRegistration.detail}`);
|
|
1046
|
+
console.log(` instructions: ${readiness.instructionSetup.status} — ${readiness.instructionSetup.detail}`);
|
|
1047
|
+
}
|
|
1048
|
+
for (const change of instructionPlan.changes) {
|
|
1049
|
+
console.log(
|
|
1050
|
+
`instructions ${change.status}: ${change.path} (${change.clients.join(", ")})`,
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
for (const manual of instructionPlan.manual) {
|
|
1054
|
+
console.log(`instructions manual: ${manual.client} — ${manual.reason}`);
|
|
1055
|
+
}
|
|
771
1056
|
}
|
|
772
1057
|
|
|
1058
|
+
const requestedTargets = [
|
|
1059
|
+
...new Set([
|
|
1060
|
+
...explicitTargets.filter((name) => !builtInNames.has(name)),
|
|
1061
|
+
...targetByPath.values(),
|
|
1062
|
+
]),
|
|
1063
|
+
];
|
|
1064
|
+
const hasInstructionWrites = instructionPlan.changes.some(
|
|
1065
|
+
(change) => change.status !== "unchanged",
|
|
1066
|
+
);
|
|
1067
|
+
const hasConfigWrite = configPlan?.action === "create";
|
|
1068
|
+
const hasChanges = !(
|
|
1069
|
+
requestedTargets.length === 0 &&
|
|
1070
|
+
!hasInstructionWrites &&
|
|
1071
|
+
coreSkillIds.length === 0 &&
|
|
1072
|
+
!hasConfigWrite
|
|
1073
|
+
);
|
|
1074
|
+
|
|
773
1075
|
const byName = new Map(
|
|
774
|
-
candidates
|
|
1076
|
+
candidates
|
|
1077
|
+
.filter((candidate) =>
|
|
1078
|
+
candidate.deliveryMode === "managed-pins" ||
|
|
1079
|
+
(migrateFullVault && candidate.state === "full-vault"),
|
|
1080
|
+
)
|
|
1081
|
+
.map((candidate) => [
|
|
1082
|
+
targetByPath.get(candidate.path) ?? deriveTargetName(candidate.path),
|
|
1083
|
+
candidate,
|
|
1084
|
+
] as const),
|
|
1085
|
+
);
|
|
1086
|
+
const allCandidatesByName = new Map(
|
|
1087
|
+
candidates.map((candidate) => [
|
|
1088
|
+
targetByPath.get(candidate.path) ?? deriveTargetName(candidate.path),
|
|
1089
|
+
candidate,
|
|
1090
|
+
] as const),
|
|
775
1091
|
);
|
|
776
1092
|
for (const name of requestedTargets) {
|
|
777
|
-
if (!byName.has(name))
|
|
1093
|
+
if (!byName.has(name)) {
|
|
1094
|
+
if (allCandidatesByName.get(name)?.state === "full-vault") {
|
|
1095
|
+
throw new Error(
|
|
1096
|
+
`target "${name}" is a full-vault surface; re-run with --migrate-full-vault to convert it to managed pins`,
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
throw new Error(`unknown --target "${name}": not among detected surfaces`);
|
|
1100
|
+
}
|
|
778
1101
|
}
|
|
779
1102
|
|
|
780
|
-
const confirmedTargets = requestedTargets.map((name) =>
|
|
781
|
-
|
|
1103
|
+
const confirmedTargets = requestedTargets.map((name) => {
|
|
1104
|
+
const candidate = byName.get(name)!;
|
|
1105
|
+
return {
|
|
1106
|
+
name,
|
|
1107
|
+
dir: candidate.path,
|
|
1108
|
+
...(candidate.state === "full-vault" ? { migrateFullVault: true } : {}),
|
|
1109
|
+
};
|
|
1110
|
+
});
|
|
1111
|
+
const plannedManifest = planInitManifest(vaultPath, confirmedTargets, coreSkillIds);
|
|
1112
|
+
const serializedPlan = {
|
|
1113
|
+
vault_path: vaultPath,
|
|
1114
|
+
config: configPlan
|
|
1115
|
+
? { path: configPlan.configPath, action: configPlan.action }
|
|
1116
|
+
: { path: configPath, action: "preserve" },
|
|
1117
|
+
clients: clientPlan.clients.map((client) => client.id),
|
|
1118
|
+
targets: confirmedTargets,
|
|
1119
|
+
core: plannedManifest.core.skills,
|
|
1120
|
+
instructions: instructionPlan.changes.map(({ path, clients, status }) => ({
|
|
1121
|
+
path,
|
|
1122
|
+
clients,
|
|
1123
|
+
status,
|
|
1124
|
+
})),
|
|
1125
|
+
manual: instructionPlan.manual,
|
|
1126
|
+
};
|
|
1127
|
+
if (!hasChanges) {
|
|
1128
|
+
if (options.isJson) {
|
|
1129
|
+
console.log(JSON.stringify({
|
|
1130
|
+
schema_version: 1,
|
|
1131
|
+
ok: true,
|
|
1132
|
+
command: "init",
|
|
1133
|
+
phase: "plan",
|
|
1134
|
+
dry_run: options.dryRun,
|
|
1135
|
+
applied: false,
|
|
1136
|
+
plan: serializedPlan,
|
|
1137
|
+
}));
|
|
1138
|
+
} else {
|
|
1139
|
+
console.log("\nno managed-pins surface selected — nothing written.");
|
|
1140
|
+
}
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
if (!options.isJson) {
|
|
1144
|
+
for (const target of confirmedTargets.filter((target) => target.migrateFullVault)) {
|
|
1145
|
+
console.log(
|
|
1146
|
+
`full-vault migration ${target.name}: ${vaultHealth.skillCount} visible skills -> ` +
|
|
1147
|
+
`${plannedManifest.core.skills.length} core ${plannedManifest.core.skills.length === 1 ? "skill" : "skills"} after sync`,
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
if (options.dryRun) {
|
|
1152
|
+
if (options.isJson) {
|
|
1153
|
+
console.log(JSON.stringify({
|
|
1154
|
+
schema_version: 1,
|
|
1155
|
+
ok: true,
|
|
1156
|
+
command: "init",
|
|
1157
|
+
phase: "plan",
|
|
1158
|
+
dry_run: true,
|
|
1159
|
+
applied: false,
|
|
1160
|
+
plan: serializedPlan,
|
|
1161
|
+
}));
|
|
1162
|
+
} else {
|
|
1163
|
+
console.log(
|
|
1164
|
+
`\ndry-run: ${confirmedTargets.length} target(s), ` +
|
|
1165
|
+
`${instructionPlan.changes.filter((change) => change.status !== "unchanged").length} instruction file(s), ` +
|
|
1166
|
+
`core: ${plannedManifest.core.skills.join(", ") || "(unchanged)"}`,
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
782
1171
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
1172
|
+
if (!yes) {
|
|
1173
|
+
if (!options.isJson && isInteractive()) {
|
|
1174
|
+
const prompts = [
|
|
1175
|
+
...confirmedTargets.map((target) => `Adopt ${target.name} at ${target.dir}?`),
|
|
1176
|
+
...instructionPlan.changes
|
|
1177
|
+
.filter((change) => change.status !== "unchanged")
|
|
1178
|
+
.map((change) => `${change.status} instruction file ${change.path}?`),
|
|
1179
|
+
...(hasConfigWrite ? [`Create machine config ${configPath}?`] : []),
|
|
1180
|
+
...(coreSkillIds.length > 0 ? [`Pin core skills: ${coreSkillIds.join(", ")}?`] : []),
|
|
1181
|
+
];
|
|
1182
|
+
for (const prompt of prompts) {
|
|
1183
|
+
if (!(await confirmAction(prompt))) {
|
|
1184
|
+
console.log("init cancelled; nothing written");
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
} else {
|
|
1189
|
+
throw new Error(
|
|
1190
|
+
"skillmux init requires --yes before applying target, instruction, or core changes non-interactively",
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
let configCreated = false;
|
|
1196
|
+
let instructionsApplied = false;
|
|
1197
|
+
const applyAdditional = () => {
|
|
1198
|
+
try {
|
|
1199
|
+
if (configPlan?.action === "create") {
|
|
1200
|
+
configCreated = applyConfigInit(configPlan) === "created";
|
|
1201
|
+
}
|
|
1202
|
+
if (hasInstructionWrites) {
|
|
1203
|
+
applyInstructionPlan(instructionPlan);
|
|
1204
|
+
instructionsApplied = true;
|
|
1205
|
+
}
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
if (configCreated && configPlan) rollbackConfigInit(configPlan);
|
|
1208
|
+
configCreated = false;
|
|
1209
|
+
throw error;
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
const rollbackAdditional = () => {
|
|
1213
|
+
if (instructionsApplied) rollbackInstructionPlan(instructionPlan);
|
|
1214
|
+
if (configCreated && configPlan) rollbackConfigInit(configPlan);
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
if (confirmedTargets.length === 0 && coreSkillIds.length === 0) {
|
|
1218
|
+
applyAdditional();
|
|
1219
|
+
} else {
|
|
1220
|
+
applyInit(
|
|
1221
|
+
vaultPath,
|
|
1222
|
+
confirmedTargets,
|
|
1223
|
+
hasInstructionWrites || hasConfigWrite
|
|
1224
|
+
? {
|
|
1225
|
+
apply: applyAdditional,
|
|
1226
|
+
rollback: rollbackAdditional,
|
|
1227
|
+
}
|
|
1228
|
+
: undefined,
|
|
1229
|
+
coreSkillIds,
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (options.isJson) {
|
|
1234
|
+
console.log(JSON.stringify({
|
|
1235
|
+
schema_version: 1,
|
|
1236
|
+
ok: true,
|
|
1237
|
+
command: "init",
|
|
1238
|
+
phase: "result",
|
|
1239
|
+
dry_run: false,
|
|
1240
|
+
applied: true,
|
|
1241
|
+
plan: serializedPlan,
|
|
1242
|
+
result: {
|
|
1243
|
+
config_created: configCreated,
|
|
1244
|
+
targets_adopted: confirmedTargets.map((target) => target.name),
|
|
1245
|
+
instructions_changed: instructionPlan.changes
|
|
1246
|
+
.filter((change) => change.status !== "unchanged")
|
|
1247
|
+
.map((change) => change.path),
|
|
1248
|
+
core: plannedManifest.core.skills,
|
|
1249
|
+
},
|
|
1250
|
+
}));
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
if (configCreated) console.log(`created ${configPath}`);
|
|
1254
|
+
if (confirmedTargets.length > 0) {
|
|
1255
|
+
console.log(`\nwrote ${join(vaultPath, "skillmux.toml")}, adopted: ${confirmedTargets.map((t) => t.name).join(", ")}`);
|
|
1256
|
+
} else if (coreSkillIds.length > 0) {
|
|
1257
|
+
console.log(`\nwrote ${join(vaultPath, "skillmux.toml")}`);
|
|
1258
|
+
}
|
|
1259
|
+
if (plannedManifest.core.skills.length === 0 && confirmedTargets.length > 0) {
|
|
1260
|
+
console.log("next: skillmux manifest pin <skill_id> --core");
|
|
1261
|
+
}
|
|
1262
|
+
if (confirmedTargets.length > 0) console.log("next: skillmux sync");
|
|
1263
|
+
if (requestedClients.length === 0 || requestedClients.includes("skillmux-mcp")) {
|
|
1264
|
+
console.log(`\n${printLastMile()}`);
|
|
1265
|
+
}
|
|
786
1266
|
}
|
|
787
1267
|
|
|
788
1268
|
function parseReportArgs(args: string[]): { server?: string; db?: string; since?: string } {
|