skilllib 1.0.2 → 1.2.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/README.md +51 -13
- package/dist/{chunk-67BDL5SN.js → chunk-O5TY4NWZ.js} +696 -88
- package/dist/index.js +14 -3
- package/dist/{tui-CQCVL6IK.js → tui-6BOQIKAU.js} +200 -149
- package/package.json +1 -1
|
@@ -29,7 +29,7 @@ function table(rows) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// src/commands.ts
|
|
32
|
-
import { basename as
|
|
32
|
+
import { basename as basename6, dirname as dirname5, join as join13, resolve as resolve4, sep as sep5 } from "path";
|
|
33
33
|
|
|
34
34
|
// src/library.ts
|
|
35
35
|
import { execFileSync } from "child_process";
|
|
@@ -112,9 +112,25 @@ function harness(id) {
|
|
|
112
112
|
return HARNESSES.find((h) => h.id === id);
|
|
113
113
|
}
|
|
114
114
|
var ALL_PROJECT_DIRS = [...new Set(HARNESSES.flatMap((h) => h.projectDirs))];
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
var PREFERRED_DIRS = [AGENTS_SKILLS_DIR, PROJECT_SKILLS_DIR];
|
|
116
|
+
function preference(dir) {
|
|
117
|
+
const i = PREFERRED_DIRS.indexOf(dir);
|
|
118
|
+
return i === -1 ? PREFERRED_DIRS.length : i;
|
|
119
|
+
}
|
|
120
|
+
function cost(dirs) {
|
|
121
|
+
return dirs.length * 100 + dirs.reduce((sum, d) => sum + preference(d), 0);
|
|
122
|
+
}
|
|
123
|
+
function installDirs(enabled, keep) {
|
|
124
|
+
if (enabled.length === 0) return [keep ?? PROJECT_SKILLS_DIR];
|
|
125
|
+
let best = [];
|
|
126
|
+
for (let mask = 1; mask < 1 << ALL_PROJECT_DIRS.length; mask++) {
|
|
127
|
+
const dirs = ALL_PROJECT_DIRS.filter((_, i) => mask & 1 << i).sort((a, b) => preference(a) - preference(b));
|
|
128
|
+
if (keep && !dirs.includes(keep)) continue;
|
|
129
|
+
if (!enabled.every((id) => harness(id).projectDirs.some((d) => dirs.includes(d)))) continue;
|
|
130
|
+
if (best.length === 0 || cost(dirs) < cost(best)) best = dirs;
|
|
131
|
+
}
|
|
132
|
+
const primary = keep ?? best[0];
|
|
133
|
+
return [primary, ...best.filter((d) => d !== primary)];
|
|
118
134
|
}
|
|
119
135
|
|
|
120
136
|
// src/config.ts
|
|
@@ -373,6 +389,19 @@ function allowTrackedLinks(root) {
|
|
|
373
389
|
const config = readConfig();
|
|
374
390
|
writeConfig({ ...config, agentsDirOk: [.../* @__PURE__ */ new Set([...config.agentsDirOk ?? [], root])].sort() });
|
|
375
391
|
}
|
|
392
|
+
function keptGlobal() {
|
|
393
|
+
return new Set(readConfig().keepGlobal ?? []);
|
|
394
|
+
}
|
|
395
|
+
function setKeepGlobal(names, keep) {
|
|
396
|
+
const kept = keptGlobal();
|
|
397
|
+
const before = kept.size;
|
|
398
|
+
for (const n of names) {
|
|
399
|
+
if (keep) kept.add(n);
|
|
400
|
+
else kept.delete(n);
|
|
401
|
+
}
|
|
402
|
+
if (kept.size === before) return;
|
|
403
|
+
writeConfig({ ...readConfig(), keepGlobal: [...kept].sort() });
|
|
404
|
+
}
|
|
376
405
|
|
|
377
406
|
// src/sources.ts
|
|
378
407
|
import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
@@ -387,6 +416,9 @@ function readJson(path) {
|
|
|
387
416
|
function skillsShLock() {
|
|
388
417
|
return readJson(join7(resolve3(claudeDir(), ".."), ".agents", ".skill-lock.json"))?.skills ?? {};
|
|
389
418
|
}
|
|
419
|
+
function projectSkillsLock(root) {
|
|
420
|
+
return readJson(join7(root, "skills-lock.json"))?.skills ?? {};
|
|
421
|
+
}
|
|
390
422
|
function realpathOrNull(path) {
|
|
391
423
|
try {
|
|
392
424
|
return realpathSync2(path);
|
|
@@ -516,11 +548,11 @@ function pluginSkills() {
|
|
|
516
548
|
}));
|
|
517
549
|
}
|
|
518
550
|
function machineSkills(enabled = enabledHarnesses()) {
|
|
519
|
-
const
|
|
551
|
+
const claude2 = enabled.includes("claude-code");
|
|
520
552
|
return [
|
|
521
553
|
...globalFolderSkills(enabled),
|
|
522
|
-
...
|
|
523
|
-
...
|
|
554
|
+
...claude2 ? claudeAiSkills() : [],
|
|
555
|
+
...claude2 ? pluginSkills() : [],
|
|
524
556
|
...cursorBuiltInSkills(enabled)
|
|
525
557
|
].sort(
|
|
526
558
|
(a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name)
|
|
@@ -542,7 +574,9 @@ function originFor(dir) {
|
|
|
542
574
|
const match = machineSkills().find((s) => resolve3(s.path) === resolve3(dir));
|
|
543
575
|
if (match) return match.kind === "global" ? "global folder" : `${match.kind}: ${match.origin}`;
|
|
544
576
|
const project = dir.split(/[\\/]\.(?:claude|agents)[\\/]skills[\\/]/)[0];
|
|
545
|
-
|
|
577
|
+
if (!project || project === dir) return dir;
|
|
578
|
+
const locked = projectSkillsLock(project)[basename2(dir)]?.source;
|
|
579
|
+
return locked ? `skills.sh: ${locked}` : `project: ${basename2(project)}`;
|
|
546
580
|
}
|
|
547
581
|
|
|
548
582
|
// src/library.ts
|
|
@@ -583,11 +617,11 @@ function visibilityOf(root, name, enabled = enabledHarnesses()) {
|
|
|
583
617
|
paths: harness(id).projectDirs.filter((dir) => existsSync7(join8(root, dir, name, "SKILL.md"))).length
|
|
584
618
|
}));
|
|
585
619
|
}
|
|
586
|
-
function dependencyDirs(dep) {
|
|
620
|
+
function dependencyDirs(root, name, dep) {
|
|
587
621
|
const wanted = installDirs(enabledHarnesses());
|
|
588
|
-
|
|
589
|
-
const primary = dep.dir ?? PROJECT_SKILLS_DIR;
|
|
590
|
-
return [primary, .../* @__PURE__ */ new Set([...dep
|
|
622
|
+
const untracked = ALL_PROJECT_DIRS.find((d) => existsSync7(join8(root, d, name, "SKILL.md")) && !isLink(join8(root, d, name)));
|
|
623
|
+
const primary = dep ? dep.dir ?? PROJECT_SKILLS_DIR : untracked ?? wanted[0];
|
|
624
|
+
return [primary, .../* @__PURE__ */ new Set([...dep?.links ?? [], ...wanted.filter((d) => d !== primary)])];
|
|
591
625
|
}
|
|
592
626
|
function isGitTracked(root, dir) {
|
|
593
627
|
try {
|
|
@@ -620,6 +654,7 @@ function projectStatus(root) {
|
|
|
620
654
|
};
|
|
621
655
|
});
|
|
622
656
|
const seen = new Set(Object.keys(skills));
|
|
657
|
+
const lock = projectSkillsLock(root);
|
|
623
658
|
const unmanaged = [];
|
|
624
659
|
const byName = /* @__PURE__ */ new Map();
|
|
625
660
|
for (const dir of ALL_PROJECT_DIRS) {
|
|
@@ -636,7 +671,8 @@ function projectStatus(root) {
|
|
|
636
671
|
const local = treeHash(real);
|
|
637
672
|
const sameAsLibrary = latest !== null && (latest.hash === local || versionForHash(name, local ?? "") !== null);
|
|
638
673
|
const committed = location === AGENTS_SKILLS_DIR;
|
|
639
|
-
const
|
|
674
|
+
const source = lock[name]?.source;
|
|
675
|
+
const state = source ? "from npx skills" : committed ? latest === null ? "repo skill" : sameAsLibrary ? "repo skill, in library" : "repo skill, differs from library" : latest === null ? "local only" : sameAsLibrary ? "untracked copy of library skill" : "untracked, differs from library";
|
|
640
676
|
unmanaged.push({
|
|
641
677
|
name,
|
|
642
678
|
managed: false,
|
|
@@ -645,7 +681,8 @@ function projectStatus(root) {
|
|
|
645
681
|
latest: latest?.version ?? null,
|
|
646
682
|
location,
|
|
647
683
|
path: real,
|
|
648
|
-
visibility: visibilityOf(root, name, enabled)
|
|
684
|
+
visibility: visibilityOf(root, name, enabled),
|
|
685
|
+
...source ? { source } : {}
|
|
649
686
|
});
|
|
650
687
|
}
|
|
651
688
|
return [...managed, ...unmanaged].sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -703,7 +740,7 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
703
740
|
if (!target) return { name, action: "skipped", reason: version === void 0 ? "not in the library" : `version ${version} doesn't exist` };
|
|
704
741
|
const manifest = readManifest(root);
|
|
705
742
|
const recorded = manifest.skills[name];
|
|
706
|
-
const [primary = PROJECT_SKILLS_DIR, ...linkDirs] = dependencyDirs(recorded);
|
|
743
|
+
const [primary = PROJECT_SKILLS_DIR, ...linkDirs] = dependencyDirs(root, name, recorded);
|
|
707
744
|
const to = join8(root, primary, name);
|
|
708
745
|
const local = isLink(to) && !recorded ? null : treeHash(to);
|
|
709
746
|
const knownContent = local !== null && (local === recorded?.hash || versionForHash(name, local) !== null);
|
|
@@ -849,7 +886,9 @@ function unloadGlobal(path, links = [], { requireLibrary = true } = {}) {
|
|
|
849
886
|
const broken = !existsSync7(join8(path, "SKILL.md"));
|
|
850
887
|
if (requireLibrary && !broken && latestVersion(name) === null) return { ok: false, reason: "import it into the library first" };
|
|
851
888
|
for (const link2 of links) if (isLink(link2)) stash(link2, "global-backup");
|
|
852
|
-
|
|
889
|
+
const movedTo = stash(path, "global-backup");
|
|
890
|
+
if (!globalSkillDirs().some((d) => entryExists(join8(d, name)))) setKeepGlobal([name], false);
|
|
891
|
+
return { ok: true, movedTo };
|
|
853
892
|
}
|
|
854
893
|
function createSkill(name, description) {
|
|
855
894
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) return { ok: false, reason: "use lowercase letters, digits and dashes" };
|
|
@@ -872,7 +911,7 @@ Instructions for Claude go here.
|
|
|
872
911
|
return { ok: true, dir };
|
|
873
912
|
}
|
|
874
913
|
function listBackups() {
|
|
875
|
-
return ["trash", "global-backup"].flatMap((kind) => {
|
|
914
|
+
return ["trash", "global-backup", "tidy-backup"].flatMap((kind) => {
|
|
876
915
|
const dir = join8(skilllibHome(), kind);
|
|
877
916
|
if (!existsSync7(dir)) return [];
|
|
878
917
|
const index = readBackupIndex(kind);
|
|
@@ -887,6 +926,7 @@ function listBackups() {
|
|
|
887
926
|
}
|
|
888
927
|
function restoreBackup(backup) {
|
|
889
928
|
const to = backup.from;
|
|
929
|
+
if (backup.kind === "tidy-backup" && isLink(to)) unlinkSync(to);
|
|
890
930
|
let taken = existsSync7(to);
|
|
891
931
|
try {
|
|
892
932
|
taken = taken || lstatSync2(to) !== null;
|
|
@@ -1005,20 +1045,394 @@ async function scanUsage(days) {
|
|
|
1005
1045
|
function summarize(uses, projectOf = () => null) {
|
|
1006
1046
|
const bySkill = /* @__PURE__ */ new Map();
|
|
1007
1047
|
for (const use of uses) {
|
|
1008
|
-
const
|
|
1048
|
+
const skill = use.skill.split(":").pop();
|
|
1049
|
+
const summary = bySkill.get(skill) ?? { skill, uses: 0, lastUsed: "", projects: /* @__PURE__ */ new Set() };
|
|
1009
1050
|
summary.uses += 1;
|
|
1010
1051
|
if (use.at > summary.lastUsed) summary.lastUsed = use.at;
|
|
1011
1052
|
const project = projectOf(use.cwd);
|
|
1012
1053
|
if (project) summary.projects.add(project);
|
|
1013
|
-
bySkill.set(
|
|
1054
|
+
bySkill.set(skill, summary);
|
|
1014
1055
|
}
|
|
1015
1056
|
return [...bySkill.values()].sort((a, b) => b.uses - a.uses);
|
|
1016
1057
|
}
|
|
1017
1058
|
|
|
1018
1059
|
// src/health.ts
|
|
1019
|
-
import { basename as
|
|
1060
|
+
import { basename as basename5 } from "path";
|
|
1061
|
+
|
|
1062
|
+
// src/git.ts
|
|
1063
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1064
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1065
|
+
import { join as join10, relative as relative3 } from "path";
|
|
1066
|
+
function git(root, args, input) {
|
|
1067
|
+
try {
|
|
1068
|
+
return execFileSync2("git", args, { cwd: root, encoding: "utf-8", input, stdio: ["pipe", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 });
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
const out = err.stdout;
|
|
1071
|
+
return typeof out === "string" ? out : null;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
function gitInfo(root) {
|
|
1075
|
+
if (!existsSync9(join10(root, ".git"))) return null;
|
|
1076
|
+
const dirs = [...ALL_PROJECT_DIRS, MANIFEST_FILE].filter((d) => existsSync9(join10(root, d)));
|
|
1077
|
+
if (dirs.length === 0) return { of: () => "new" };
|
|
1078
|
+
const tracked = (git(root, ["ls-files", "-z", "--", ...dirs]) ?? "").split("\0").filter(Boolean);
|
|
1079
|
+
const dirty = (git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...dirs]) ?? "").split("\0").filter(Boolean).map((entry) => entry.slice(3));
|
|
1080
|
+
const ignoredCache = /* @__PURE__ */ new Map();
|
|
1081
|
+
const isIgnored = (rel) => {
|
|
1082
|
+
if (!ignoredCache.has(rel)) {
|
|
1083
|
+
const out = git(root, ["check-ignore", "--no-index", "--", rel]) ?? "";
|
|
1084
|
+
ignoredCache.set(rel, out.trim().length > 0);
|
|
1085
|
+
}
|
|
1086
|
+
return ignoredCache.get(rel);
|
|
1087
|
+
};
|
|
1088
|
+
const under = (list2, rel) => list2.some((p) => p === rel || p.startsWith(rel + "/"));
|
|
1089
|
+
return {
|
|
1090
|
+
of: (relPath) => {
|
|
1091
|
+
const rel = relPath.replace(/\/+$/, "");
|
|
1092
|
+
if (under(tracked, rel)) return under(dirty, rel) ? "changed" : "committed";
|
|
1093
|
+
return isIgnored(rel) ? "ignored" : "new";
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
function relativeTo(root, path) {
|
|
1098
|
+
return relative3(root, path).split("\\").join("/");
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// src/plugins.ts
|
|
1102
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1103
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
1104
|
+
import { basename as basename4, dirname as dirname3, join as join11 } from "path";
|
|
1105
|
+
function readJson2(path) {
|
|
1106
|
+
try {
|
|
1107
|
+
return JSON.parse(readFileSync7(path, "utf-8"));
|
|
1108
|
+
} catch {
|
|
1109
|
+
return null;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
function nonEmptyDir(path) {
|
|
1113
|
+
try {
|
|
1114
|
+
return readdirSync6(path).some((f) => !f.startsWith("."));
|
|
1115
|
+
} catch {
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
function extrasOf(root) {
|
|
1120
|
+
const manifest = readJson2(join11(root, ".claude-plugin", "plugin.json")) ?? {};
|
|
1121
|
+
return [
|
|
1122
|
+
...existsSync10(join11(root, ".mcp.json")) || manifest.mcpServers ? ["MCP servers"] : [],
|
|
1123
|
+
...existsSync10(join11(root, "hooks", "hooks.json")) || manifest.hooks ? ["hooks"] : [],
|
|
1124
|
+
...nonEmptyDir(join11(root, "agents")) || manifest.agents ? ["agents"] : [],
|
|
1125
|
+
...nonEmptyDir(join11(root, "commands")) || manifest.commands ? ["commands"] : []
|
|
1126
|
+
];
|
|
1127
|
+
}
|
|
1128
|
+
function claudePlugins(machine) {
|
|
1129
|
+
const installed = readJson2(join11(claudeDir(), "plugins", "installed_plugins.json"))?.plugins ?? {};
|
|
1130
|
+
const byOrigin = /* @__PURE__ */ new Map();
|
|
1131
|
+
for (const m of machine) if (m.kind === "plugin") byOrigin.set(m.origin, [...byOrigin.get(m.origin) ?? [], m.path]);
|
|
1132
|
+
return [...byOrigin].map(([origin, skills]) => {
|
|
1133
|
+
const synced = origin.endsWith(" (claude.ai)");
|
|
1134
|
+
const id = synced ? `${origin.replace(" (claude.ai)", "")}@synced` : origin;
|
|
1135
|
+
const entry = installed[origin]?.[0];
|
|
1136
|
+
return {
|
|
1137
|
+
id,
|
|
1138
|
+
scope: synced ? null : entry?.scope ?? "user",
|
|
1139
|
+
...entry?.projectPath ? { projectPath: entry.projectPath } : {},
|
|
1140
|
+
synced,
|
|
1141
|
+
skills,
|
|
1142
|
+
extras: extrasOf(dirname3(dirname3(skills[0])))
|
|
1143
|
+
};
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
function claudeBinary() {
|
|
1147
|
+
const candidates = ["claude", join11(userHome(), ".local", "bin", "claude"), join11(userHome(), ".claude", "local", "claude")];
|
|
1148
|
+
return candidates.find((bin) => {
|
|
1149
|
+
try {
|
|
1150
|
+
return execFileSync3(bin, ["--version"], { stdio: ["ignore", "pipe", "ignore"], timeout: 3e4, encoding: "utf-8" }).includes("Claude Code");
|
|
1151
|
+
} catch {
|
|
1152
|
+
return false;
|
|
1153
|
+
}
|
|
1154
|
+
}) ?? null;
|
|
1155
|
+
}
|
|
1156
|
+
function claude(args, cwd) {
|
|
1157
|
+
const bin = claudeBinary();
|
|
1158
|
+
if (!bin) return { ok: false, reason: `Claude Code's CLI didn't run (tried \`claude\` on your PATH and ~/.local/bin/claude); run \`claude ${args.join(" ")}\` yourself` };
|
|
1159
|
+
try {
|
|
1160
|
+
execFileSync3(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 12e4, encoding: "utf-8" });
|
|
1161
|
+
return { ok: true };
|
|
1162
|
+
} catch (err) {
|
|
1163
|
+
const e = err;
|
|
1164
|
+
return { ok: false, reason: `${(e.stderr || e.stdout || e.message || "failed").trim().split("\n").slice(-1)[0]} (ran ${bin})` };
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
function turnOffSynced(id) {
|
|
1168
|
+
const file = join11(claudeDir(), "settings.json");
|
|
1169
|
+
const settings = existsSync10(file) ? readJson2(file) : {};
|
|
1170
|
+
if (!settings) return { ok: false, reason: `can't read ${file}` };
|
|
1171
|
+
const enabled = settings.enabledPlugins ?? {};
|
|
1172
|
+
mkdirSync6(claudeDir(), { recursive: true });
|
|
1173
|
+
writeFileSync6(file, JSON.stringify({ ...settings, enabledPlugins: { ...enabled, [id]: false } }, null, 2) + "\n");
|
|
1174
|
+
return { ok: true };
|
|
1175
|
+
}
|
|
1176
|
+
function removedFile() {
|
|
1177
|
+
return join11(skilllibHome(), "plugin-backup.json");
|
|
1178
|
+
}
|
|
1179
|
+
function removePlugin(plugin, how) {
|
|
1180
|
+
if (plugin.scope && plugin.scope !== "user" && !plugin.projectPath) {
|
|
1181
|
+
return { ok: false, message: `${plugin.id}: installed for one project; run \`claude plugin ${how === "delete" ? "uninstall" : "disable"} ${plugin.id} --scope ${plugin.scope}\` there` };
|
|
1182
|
+
}
|
|
1183
|
+
if (!plugin.synced && !claudeBinary()) {
|
|
1184
|
+
return { ok: false, message: `${plugin.id}: nothing changed; Claude Code's CLI didn't run (tried \`claude\` on your PATH and ~/.local/bin/claude)` };
|
|
1185
|
+
}
|
|
1186
|
+
const imported = plugin.skills.filter((dir) => latestVersion(basename4(dir)) === null).map((dir) => importSkill(dir).name);
|
|
1187
|
+
const scope = plugin.scope ? ["--scope", plugin.scope] : [];
|
|
1188
|
+
const r = plugin.synced ? turnOffSynced(plugin.id) : claude(["plugin", how === "delete" ? "uninstall" : "disable", plugin.id, ...scope], plugin.projectPath);
|
|
1189
|
+
const saved = imported.length ? `; ${imported.join(", ")} copied into Your skills` : "";
|
|
1190
|
+
if (!r.ok) return { ok: false, message: `${plugin.id}: ${r.reason}${saved}` };
|
|
1191
|
+
if (how === "delete" && !plugin.synced) {
|
|
1192
|
+
const list2 = readJson2(removedFile()) ?? [];
|
|
1193
|
+
mkdirSync6(skilllibHome(), { recursive: true });
|
|
1194
|
+
writeFileSync6(removedFile(), JSON.stringify([...list2, { id: plugin.id, scope: plugin.scope ?? "user", at: (/* @__PURE__ */ new Date()).toISOString() }], null, 2) + "\n");
|
|
1195
|
+
}
|
|
1196
|
+
return { ok: true, message: `${plugin.id} ${how === "delete" && !plugin.synced ? "removed" : "turned off"}${saved}` };
|
|
1197
|
+
}
|
|
1198
|
+
function pluginBackups() {
|
|
1199
|
+
return (readJson2(removedFile()) ?? []).map((r) => ({
|
|
1200
|
+
name: r.id,
|
|
1201
|
+
kind: "plugin",
|
|
1202
|
+
path: r.scope,
|
|
1203
|
+
movedAt: r.at.slice(0, 16).replace("T", " "),
|
|
1204
|
+
from: r.id
|
|
1205
|
+
}));
|
|
1206
|
+
}
|
|
1207
|
+
function restorePlugin(backup) {
|
|
1208
|
+
const r = claude(["plugin", "install", backup.from, "--scope", backup.path]);
|
|
1209
|
+
if (!r.ok) return r;
|
|
1210
|
+
writeFileSync6(removedFile(), JSON.stringify((readJson2(removedFile()) ?? []).filter((x) => x.id !== backup.from), null, 2) + "\n");
|
|
1211
|
+
return { ok: true, to: `Claude Code plugins (${backup.path})` };
|
|
1212
|
+
}
|
|
1213
|
+
function cursorPluginSkills() {
|
|
1214
|
+
const cache = join11(userHome(), ".cursor", "plugins", "cache");
|
|
1215
|
+
const dirs = (p) => existsSync10(p) ? readdirSync6(p).filter((d) => !d.startsWith(".") && statSync3(join11(p, d)).isDirectory()) : [];
|
|
1216
|
+
return dirs(cache).flatMap(
|
|
1217
|
+
(marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
|
|
1218
|
+
const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
|
|
1219
|
+
const newest = versions.sort((a, b) => statSync3(b).mtimeMs - statSync3(a).mtimeMs)[0];
|
|
1220
|
+
return newest ? skillDirsIn(join11(newest, "skills")).map((path) => ({ name: basename4(path), plugin, path })) : [];
|
|
1221
|
+
})
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// src/tidy.ts
|
|
1226
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync7, readdirSync as readdirSync7, unlinkSync as unlinkSync2 } from "fs";
|
|
1227
|
+
import { dirname as dirname4, join as join12, sep as sep4 } from "path";
|
|
1228
|
+
function entriesOf(dirs, name) {
|
|
1229
|
+
return dirs.flatMap((dir) => {
|
|
1230
|
+
const path = join12(dir, name);
|
|
1231
|
+
if (!entryExists(path)) return [];
|
|
1232
|
+
const link2 = isLink(path);
|
|
1233
|
+
const real = realpathOrNull2(path);
|
|
1234
|
+
const skill = existsSync11(join12(path, "SKILL.md"));
|
|
1235
|
+
if (!link2 && !skill) return [];
|
|
1236
|
+
return [{ dir, path, link: link2, real, hash: null }];
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
function hashReals(reals) {
|
|
1240
|
+
if (reals.length > 1) for (const r of reals) r.hash = treeHash(r.path);
|
|
1241
|
+
}
|
|
1242
|
+
function namesIn(dirs) {
|
|
1243
|
+
const names = /* @__PURE__ */ new Set();
|
|
1244
|
+
for (const dir of dirs) {
|
|
1245
|
+
if (!existsSync11(dir)) continue;
|
|
1246
|
+
for (const name of readdirSync7(dir)) if (!name.startsWith(".") && name !== "synced") names.add(name);
|
|
1247
|
+
}
|
|
1248
|
+
return [...names].sort();
|
|
1249
|
+
}
|
|
1250
|
+
function runsFrom(id, dirsWithCopy, readsOf) {
|
|
1251
|
+
const reads = readsOf(id).filter((d) => dirsWithCopy.includes(d));
|
|
1252
|
+
return reads.find((d) => d.endsWith(join12(".claude", "skills"))) ?? reads[0];
|
|
1253
|
+
}
|
|
1254
|
+
function conflictOf(name, root, reals, enabled, readsOf) {
|
|
1255
|
+
const dirs = reals.map((r) => r.dir);
|
|
1256
|
+
return {
|
|
1257
|
+
name,
|
|
1258
|
+
root,
|
|
1259
|
+
copies: reals.map((r) => ({ dir: r.dir, path: r.path, runs: enabled.filter((id) => runsFrom(id, dirs, readsOf) === r.dir) }))
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {}) {
|
|
1263
|
+
const report = { plans: [], conflicts: [], skipped: [] };
|
|
1264
|
+
const { skills } = readManifest(root);
|
|
1265
|
+
const abs = (dir) => join12(root, dir);
|
|
1266
|
+
const rel = (dir) => relativeTo(root, dir);
|
|
1267
|
+
const readsOf = (id) => HARNESSES.find((h) => h.id === id).projectDirs.map(abs);
|
|
1268
|
+
let git2;
|
|
1269
|
+
const skillsLock = projectSkillsLock(root);
|
|
1270
|
+
for (const name of namesIn(ALL_PROJECT_DIRS.map(abs))) {
|
|
1271
|
+
const entries = entriesOf(ALL_PROJECT_DIRS.map(abs), name);
|
|
1272
|
+
const external = entries.find((e) => e.link && e.real && !(e.real + sep4).startsWith(realpathOrNull2(root) + sep4));
|
|
1273
|
+
if (external) {
|
|
1274
|
+
report.skipped.push({ name, reason: `${rel(external.path)} links outside the repo` });
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
const reals = entries.filter((e) => !e.link);
|
|
1278
|
+
if (reals.length === 0) continue;
|
|
1279
|
+
hashReals(reals);
|
|
1280
|
+
const dep = skills[name];
|
|
1281
|
+
const chosen = keep[name];
|
|
1282
|
+
let primary;
|
|
1283
|
+
if (dep) {
|
|
1284
|
+
primary = reals.find((r) => rel(r.dir) === (dep.dir ?? PROJECT_SKILLS_DIR));
|
|
1285
|
+
if (!primary) continue;
|
|
1286
|
+
} else if (chosen) {
|
|
1287
|
+
primary = reals.find((r) => r.dir === chosen);
|
|
1288
|
+
} else {
|
|
1289
|
+
if (git2 === void 0) git2 = gitInfo(root);
|
|
1290
|
+
const planned = installDirs(enabled).map(abs);
|
|
1291
|
+
const fromSkillsSh = name in skillsLock;
|
|
1292
|
+
const rank = (e) => (git2 && ["committed", "changed"].includes(git2.of(rel(e.path))) ? 0 : 1e3) + (fromSkillsSh && e.dir === abs(AGENTS_SKILLS_DIR) ? 0 : 100) + (planned.includes(e.dir) ? planned.indexOf(e.dir) : 50);
|
|
1293
|
+
primary = [...reals].sort((a, b) => rank(a) - rank(b))[0];
|
|
1294
|
+
}
|
|
1295
|
+
if (!primary) continue;
|
|
1296
|
+
if (chosen !== primary.dir && reals.some((r) => r.hash !== primary.hash)) {
|
|
1297
|
+
report.conflicts.push({ ...conflictOf(name, root, reals, enabled, readsOf), ...dep ? { managed: primary.dir } : {} });
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
const wanted = new Set([...installDirs(enabled, rel(primary.dir)), ...dep?.links ?? []].map(abs));
|
|
1301
|
+
const steps = [];
|
|
1302
|
+
for (const e of entries) {
|
|
1303
|
+
if (e === primary) continue;
|
|
1304
|
+
const needed = wanted.has(e.dir);
|
|
1305
|
+
if (e.link) {
|
|
1306
|
+
if (e.real === primary.real) {
|
|
1307
|
+
if (!needed) steps.push({ kind: "unlink", path: e.path });
|
|
1308
|
+
} else steps.push(needed ? { kind: "link", path: e.path, target: primary.path } : { kind: "unlink", path: e.path });
|
|
1309
|
+
} else {
|
|
1310
|
+
steps.push(needed ? { kind: "replace", path: e.path, target: primary.path } : { kind: "remove", path: e.path });
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
if (steps.length) report.plans.push({ name, root, primary: primary.path, steps });
|
|
1314
|
+
}
|
|
1315
|
+
return report;
|
|
1316
|
+
}
|
|
1317
|
+
function globalReadsOf(id) {
|
|
1318
|
+
return HARNESSES.find((h) => h.id === id).globalDirs();
|
|
1319
|
+
}
|
|
1320
|
+
function isSkillsDirPlugin(path) {
|
|
1321
|
+
return existsSync11(join12(path, ".claude-plugin", "plugin.json"));
|
|
1322
|
+
}
|
|
1323
|
+
function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
|
|
1324
|
+
const report = { plans: [], conflicts: [], skipped: [] };
|
|
1325
|
+
const dirs = globalSkillDirs();
|
|
1326
|
+
const agents = join12(userHome(), ".agents", "skills");
|
|
1327
|
+
for (const name of namesIn(dirs)) {
|
|
1328
|
+
const entries = entriesOf(dirs, name);
|
|
1329
|
+
if (entries.some((e) => isSkillsDirPlugin(e.path))) {
|
|
1330
|
+
report.skipped.push({ name, reason: "a Claude Code plugin kept in a skills folder" });
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
const reals = entries.filter((e) => !e.link);
|
|
1334
|
+
if (reals.length < 2) continue;
|
|
1335
|
+
hashReals(reals);
|
|
1336
|
+
const primary = reals.find((r) => r.dir === keep[name]) ?? reals.find((r) => r.dir === agents) ?? reals[0];
|
|
1337
|
+
if (!keep[name] && reals.some((r) => r.hash !== primary.hash)) {
|
|
1338
|
+
report.conflicts.push(conflictOf(name, null, reals, enabled, globalReadsOf));
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
const steps = reals.filter((r) => r !== primary).map((r) => ({ kind: "replace", path: r.path, target: primary.path }));
|
|
1342
|
+
report.plans.push({ name, root: null, primary: primary.path, steps });
|
|
1343
|
+
}
|
|
1344
|
+
return report;
|
|
1345
|
+
}
|
|
1346
|
+
function gitVisibleSteps(plan, git2 = plan.root ? gitInfo(plan.root) : null) {
|
|
1347
|
+
if (!plan.root || !git2) return [];
|
|
1348
|
+
return plan.steps.filter((s) => {
|
|
1349
|
+
const state = git2.of(relativeTo(plan.root, s.path));
|
|
1350
|
+
return state === "committed" || state === "changed";
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
|
|
1354
|
+
const held = git2 === "keep" ? gitVisibleSteps(plan, info2) : [];
|
|
1355
|
+
const applied = [];
|
|
1356
|
+
for (const step of plan.steps) {
|
|
1357
|
+
if (held.includes(step) || !entryExists(step.path) || "target" in step && !entryExists(step.target)) continue;
|
|
1358
|
+
if (step.kind === "link" || step.kind === "unlink") {
|
|
1359
|
+
if (isLink(step.path)) unlinkSync2(step.path);
|
|
1360
|
+
} else stash(step.path, "tidy-backup");
|
|
1361
|
+
if (step.kind === "link" || step.kind === "replace") {
|
|
1362
|
+
mkdirSync7(dirname4(step.path), { recursive: true });
|
|
1363
|
+
linkDir(step.target, step.path);
|
|
1364
|
+
}
|
|
1365
|
+
applied.push(step);
|
|
1366
|
+
}
|
|
1367
|
+
if (plan.root) {
|
|
1368
|
+
const manifest = readManifest(plan.root);
|
|
1369
|
+
const dep = manifest.skills[plan.name];
|
|
1370
|
+
const added = applied.filter((s) => s.kind === "link" || s.kind === "replace").map((s) => relativeTo(plan.root, dirname4(s.path)));
|
|
1371
|
+
if (dep && added.length) {
|
|
1372
|
+
manifest.skills[plan.name] = { ...dep, links: [.../* @__PURE__ */ new Set([...dep.links ?? [], ...added])] };
|
|
1373
|
+
writeManifest(plan.root, manifest);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
return { name: plan.name, root: plan.root, applied, held };
|
|
1377
|
+
}
|
|
1378
|
+
function describeStep(step, root) {
|
|
1379
|
+
const shown = root ? relativeTo(root, step.path) : step.path.replace(userHome(), "~");
|
|
1380
|
+
switch (step.kind) {
|
|
1381
|
+
case "link":
|
|
1382
|
+
return `link ${shown}`;
|
|
1383
|
+
case "unlink":
|
|
1384
|
+
return `remove extra link ${shown}`;
|
|
1385
|
+
case "replace":
|
|
1386
|
+
return `${shown}: duplicate copy \u2192 link`;
|
|
1387
|
+
case "remove":
|
|
1388
|
+
return `remove duplicate copy ${shown}`;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
function copyLabel(conflict, dir) {
|
|
1392
|
+
return conflict.root ? relativeTo(conflict.root, dir) : dir.replace(userHome(), "~");
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// src/health.ts
|
|
1396
|
+
function runFix(choice) {
|
|
1397
|
+
try {
|
|
1398
|
+
return { ok: true, message: choice.run() };
|
|
1399
|
+
} catch (err) {
|
|
1400
|
+
return { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
function orThrow(r) {
|
|
1404
|
+
if (!r.ok) throw new Error(r.message);
|
|
1405
|
+
return r.message;
|
|
1406
|
+
}
|
|
1407
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1408
|
+
var agentNames = (ids) => ids.map((id) => HARNESSES.find((h) => h.id === id).name).join(", ");
|
|
1409
|
+
function conflictIssue(c) {
|
|
1410
|
+
const where = c.root ? basename5(c.root) : "your global folders";
|
|
1411
|
+
const winners = c.managed ? c.copies.filter((x) => x.dir === c.managed) : c.copies;
|
|
1412
|
+
return {
|
|
1413
|
+
id: `conflict:${c.root ?? "global"}:${c.name}`,
|
|
1414
|
+
severity: "problem",
|
|
1415
|
+
title: `${c.name} in ${where}: the copies differ`,
|
|
1416
|
+
detail: `Your agents run different versions: ${c.copies.map((x) => `${copyLabel(c, x.dir)}${x.runs.length ? ` (${agentNames(x.runs)})` : ""}`).join(" vs ")}. Keep one; the others become links to it and are restorable from Health.`,
|
|
1417
|
+
choices: winners.map((x) => ({
|
|
1418
|
+
label: `Keep the ${copyLabel(c, x.dir)} copy`,
|
|
1419
|
+
hint: x.dir === c.managed ? "skilllib's version" : "the others become links to it",
|
|
1420
|
+
run: () => {
|
|
1421
|
+
const keep = { [c.name]: x.dir };
|
|
1422
|
+
const plan = (c.root ? planProjectTidy(c.root, { keep }) : planGlobalTidy({ keep })).plans.find((p) => p.name === c.name);
|
|
1423
|
+
if (plan) applyTidy(plan, { git: "go" });
|
|
1424
|
+
return `${c.name}: kept the ${copyLabel(c, x.dir)} copy`;
|
|
1425
|
+
}
|
|
1426
|
+
}))
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
function applyAll(plans, git2) {
|
|
1430
|
+
const results = plans.map((p) => applyTidy(p, { git: git2 }));
|
|
1431
|
+
return { skills: results.filter((r) => r.applied.length).length, held: results.reduce((n, r) => n + r.held.length, 0) };
|
|
1432
|
+
}
|
|
1020
1433
|
function findIssues(projects2, machine, libraryNames, statusOf = projectStatus) {
|
|
1021
1434
|
const issues = [];
|
|
1435
|
+
const statuses = new Map(projects2.map((root) => [root, statusOf(root)]));
|
|
1022
1436
|
for (const skill of machine.filter((m) => m.broken)) {
|
|
1023
1437
|
issues.push({
|
|
1024
1438
|
id: `broken:${skill.name}`,
|
|
@@ -1036,36 +1450,85 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
|
|
|
1036
1450
|
}
|
|
1037
1451
|
const loaded = machine.filter((m) => !m.broken);
|
|
1038
1452
|
const byName = /* @__PURE__ */ new Map();
|
|
1039
|
-
for (const m of loaded) byName.set(m.name, [...byName.get(m.name) ?? [], m]);
|
|
1453
|
+
for (const m of loaded) if (m.kind !== "plugin") byName.set(m.name, [...byName.get(m.name) ?? [], m]);
|
|
1040
1454
|
for (const [name, copies] of byName) {
|
|
1455
|
+
if (copies.every((c) => c.movable) || !copies.some((c) => c.movable)) continue;
|
|
1041
1456
|
const harnesses = HARNESSES.filter((h) => copies.filter((c) => c.harnesses.includes(h.id)).length > 1).map((h) => h.name);
|
|
1042
1457
|
if (harnesses.length === 0) continue;
|
|
1043
|
-
const
|
|
1458
|
+
const vendor = copies.filter((c) => !c.movable);
|
|
1044
1459
|
issues.push({
|
|
1045
1460
|
id: `dup:${name}`,
|
|
1046
1461
|
severity: "problem",
|
|
1047
1462
|
title: `${name}: loaded twice in ${harnesses.join(" and ")}`,
|
|
1048
|
-
detail: copies.map((c) => `${c.kind} (${c.origin})`).join(" and ")
|
|
1049
|
-
fix: movable && copies.some((c) => !c.movable) ? {
|
|
1050
|
-
label: "Keep the vendor copy, stop loading yours globally",
|
|
1051
|
-
run: () => {
|
|
1052
|
-
if (!libraryNames.has(name)) importSkill(movable.path);
|
|
1053
|
-
const r = unloadGlobal(movable.path, movable.links);
|
|
1054
|
-
return r.ok ? `${name} now loads once` : `${name}: ${r.reason}`;
|
|
1055
|
-
}
|
|
1056
|
-
} : void 0
|
|
1463
|
+
detail: `${copies.map((c) => `${c.kind} (${c.origin})`).join(" and ")}. Your skill wins: turn the ${vendor.map((c) => c.kind).join(" and ")} copy off at its source${vendor.some((c) => c.kind === "claude.ai") ? " (claude.ai \u2192 Settings \u2192 Skills)" : ""}.`
|
|
1057
1464
|
});
|
|
1058
1465
|
}
|
|
1059
|
-
const
|
|
1466
|
+
const loadedYours = /* @__PURE__ */ new Set([...loaded.filter((m) => m.movable).map((m) => m.name), ...[...statuses.values()].flat().map((s) => s.name)]);
|
|
1467
|
+
const yours = /* @__PURE__ */ new Set([...libraryNames, ...loadedYours]);
|
|
1468
|
+
for (const plugin of claudePlugins(machine)) {
|
|
1469
|
+
const dupes = plugin.skills.map((p) => basename5(p)).filter((n) => yours.has(n));
|
|
1470
|
+
if (!dupes.length) continue;
|
|
1471
|
+
const others = plugin.skills.length - dupes.length;
|
|
1472
|
+
const pluginName = plugin.id.split("@")[0];
|
|
1473
|
+
const remove2 = {
|
|
1474
|
+
label: `Remove the plugin ${plugin.id}`,
|
|
1475
|
+
hint: plugin.extras.length ? `also removes its ${plugin.extras.join(", ")}` : "restorable from Health",
|
|
1476
|
+
run: () => orThrow(removePlugin(plugin, "delete"))
|
|
1477
|
+
};
|
|
1478
|
+
const off = { label: `Turn the plugin ${plugin.id} off`, hint: "stays installed", run: () => orThrow(removePlugin(plugin, "off")) };
|
|
1479
|
+
const notLoaded = dupes.filter((n) => !loadedYours.has(n));
|
|
1480
|
+
issues.push({
|
|
1481
|
+
id: `plugin:${plugin.id}`,
|
|
1482
|
+
severity: notLoaded.length === dupes.length ? "suggestion" : "problem",
|
|
1483
|
+
title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}`,
|
|
1484
|
+
detail: [
|
|
1485
|
+
notLoaded.length < dupes.length ? `Claude Code loads both (the plugin's as /${pluginName}:${dupes[0]}). Your skill wins.` : "",
|
|
1486
|
+
notLoaded.length ? `Your ${notLoaded.join(", ")} ${notLoaded.length === 1 ? "is" : "are"} only in Your skills, not installed anywhere: once the plugin is gone, add ${notLoaded.length === 1 ? "it" : "them"} where you need ${notLoaded.length === 1 ? "it" : "them"}.` : "",
|
|
1487
|
+
others ? `The plugin's other ${plural(others, "skill")} are copied into Your skills first, so nothing is lost.` : "",
|
|
1488
|
+
plugin.extras.length ? `It also brings ${plugin.extras.join(", ")}: ${plugin.synced ? "turning it off pauses those" : "removing it drops those, turning it off pauses them"}.` : "",
|
|
1489
|
+
plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : ""
|
|
1490
|
+
].filter(Boolean).join(" "),
|
|
1491
|
+
choices: plugin.synced ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off]
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
if (enabledHarnesses().includes("cursor")) {
|
|
1495
|
+
const byPlugin = /* @__PURE__ */ new Map();
|
|
1496
|
+
for (const s of cursorPluginSkills().filter((x) => loadedYours.has(x.name))) byPlugin.set(s.plugin, [...byPlugin.get(s.plugin) ?? [], s.name]);
|
|
1497
|
+
for (const [plugin, names] of byPlugin) {
|
|
1498
|
+
issues.push({
|
|
1499
|
+
id: `cursor-plugin:${plugin}`,
|
|
1500
|
+
severity: "suggestion",
|
|
1501
|
+
title: `${names.join(", ")}: also in the Cursor plugin ${plugin}`,
|
|
1502
|
+
detail: `Cursor lists both copies when that plugin is on. Your skill wins: turn the plugin off in Cursor (Settings \u2192 Plugins).`
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
const global = planGlobalTidy();
|
|
1507
|
+
if (global.plans.length) {
|
|
1508
|
+
issues.push({
|
|
1509
|
+
id: "tidy:global",
|
|
1510
|
+
severity: "suggestion",
|
|
1511
|
+
title: `${plural(global.plans.length, "global skill")} with duplicate copies`,
|
|
1512
|
+
detail: `${global.plans.map((p) => p.name).join(", ")}: each keeps one copy (in ~/.agents/skills when there's one) and the others become links. Restorable from Health.`,
|
|
1513
|
+
fix: {
|
|
1514
|
+
label: "Keep one copy of each",
|
|
1515
|
+
run: () => `${plural(applyAll(global.plans, "go").skills, "global skill")} now have one copy`
|
|
1516
|
+
}
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
issues.push(...global.conflicts.map(conflictIssue));
|
|
1520
|
+
const kept = keptGlobal();
|
|
1521
|
+
const alsoGlobal = /* @__PURE__ */ new Map();
|
|
1522
|
+
const globalNames = new Set(loaded.filter((m) => m.kind !== "plugin").map((m) => m.name));
|
|
1060
1523
|
for (const root of projects2) {
|
|
1061
|
-
const where =
|
|
1062
|
-
const status2 =
|
|
1524
|
+
const where = basename5(root);
|
|
1525
|
+
const status2 = statuses.get(root);
|
|
1063
1526
|
const behind = status2.filter((s) => s.state === "update available" || s.state === "folder missing");
|
|
1064
1527
|
if (behind.length > 0) {
|
|
1065
1528
|
issues.push({
|
|
1066
1529
|
id: `sync:${root}`,
|
|
1067
1530
|
severity: "problem",
|
|
1068
|
-
title: `${where}: ${behind.length
|
|
1531
|
+
title: `${where}: ${plural(behind.length, "skill")} out of date`,
|
|
1069
1532
|
detail: behind.map((s) => s.name).join(", "),
|
|
1070
1533
|
fix: {
|
|
1071
1534
|
label: "Sync the project",
|
|
@@ -1076,49 +1539,123 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
|
|
|
1076
1539
|
}
|
|
1077
1540
|
});
|
|
1078
1541
|
}
|
|
1542
|
+
const tidy2 = planProjectTidy(root);
|
|
1543
|
+
if (tidy2.plans.length) {
|
|
1544
|
+
const git2 = gitInfo(root);
|
|
1545
|
+
const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p, git2).map((s) => describeStep(s, root)));
|
|
1546
|
+
const run = (mode) => () => {
|
|
1547
|
+
const r = applyAll(tidy2.plans, mode);
|
|
1548
|
+
return `${where}: tidied ${plural(r.skills, "skill")}${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`;
|
|
1549
|
+
};
|
|
1550
|
+
issues.push({
|
|
1551
|
+
id: `tidy:${root}`,
|
|
1552
|
+
severity: "suggestion",
|
|
1553
|
+
title: `${where}: ${plural(tidy2.plans.length, "skill")} with extra copies or links`,
|
|
1554
|
+
detail: tidy2.plans.map((p) => `${p.name}: ${p.steps.map((s) => describeStep(s, root)).join(", ")}`).join(" \xB7 ") + (visible2.length ? `. Git would see ${plural(visible2.length, "of these change")}: ${visible2.join(", ")}.` : ". Replaced copies are restorable from Health."),
|
|
1555
|
+
...visible2.length ? {
|
|
1556
|
+
choices: [
|
|
1557
|
+
{ label: "Tidy, but keep git as it is", hint: `skips the ${plural(visible2.length, "change")} git would see`, run: run("keep") },
|
|
1558
|
+
{ label: "Tidy everything", hint: `${plural(visible2.length, "change")} will show in git status`, run: run("go") }
|
|
1559
|
+
]
|
|
1560
|
+
} : { fix: { label: "Tidy", run: run("go") } }
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
issues.push(...tidy2.conflicts.map(conflictIssue));
|
|
1564
|
+
const partial = status2.filter((s) => s.state !== "folder missing" && s.visibility.some((v) => v.paths === 0));
|
|
1565
|
+
if (partial.length) {
|
|
1566
|
+
const missing = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
|
|
1567
|
+
const tracked = readConfig().agentsDirOk?.includes(root) ? [] : [...new Set(missing.map((id) => harness(id).projectDirs[0]))].filter((dir) => isGitTracked(root, dir));
|
|
1568
|
+
const run = (allowTracked) => () => {
|
|
1569
|
+
if (allowTracked) allowTrackedLinks(root);
|
|
1570
|
+
const r = linkAll(root, { allowTracked });
|
|
1571
|
+
return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing)}${r.blocked.length ? `; skipped ${r.blocked.join(", ")} (git tracks it)` : ""}`;
|
|
1572
|
+
};
|
|
1573
|
+
issues.push({
|
|
1574
|
+
id: `usable:${root}`,
|
|
1575
|
+
severity: "suggestion",
|
|
1576
|
+
title: `${where}: ${plural(partial.length, "skill")} not usable by ${agentNames(missing)}`,
|
|
1577
|
+
detail: `${partial.map((s) => `${s.name} (not ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))})`).join(", ")}. Adding links makes the one real copy reach every agent you use; nothing is copied or moved.${tracked.length ? ` Git tracks ${tracked.join(", ")}, so links there show in git status.` : ""}`,
|
|
1578
|
+
...tracked.length ? {
|
|
1579
|
+
choices: [
|
|
1580
|
+
{ label: "Add links, but not where git tracks the folder", hint: `skips ${tracked.join(", ")}`, run: run(false) },
|
|
1581
|
+
{ label: "Add links everywhere", hint: `remembered for ${where}; git will see them`, run: run(true) }
|
|
1582
|
+
]
|
|
1583
|
+
} : { fix: { label: "Add the links", run: run(false) } }
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
const differing = new Set(tidy2.conflicts.map((c) => c.name));
|
|
1079
1587
|
for (const s of status2) {
|
|
1588
|
+
if (isLink(s.path) || differing.has(s.name)) continue;
|
|
1080
1589
|
if (s.state === "local only") {
|
|
1081
1590
|
issues.push({
|
|
1082
1591
|
id: `local:${root}:${s.name}`,
|
|
1083
1592
|
severity: "suggestion",
|
|
1084
1593
|
title: `${s.name}: only exists in ${where}`,
|
|
1085
|
-
detail: "Import it into the library to reuse it in other projects and keep it backed up.",
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1594
|
+
detail: "Import it into the library to reuse it in other projects and keep it backed up. The repo then tracks it in skilllib.json.",
|
|
1595
|
+
choices: [
|
|
1596
|
+
{
|
|
1597
|
+
label: "Import into the library",
|
|
1598
|
+
hint: `${where} then tracks it`,
|
|
1599
|
+
run: () => {
|
|
1600
|
+
importSkill(s.path);
|
|
1601
|
+
addSkill(root, s.name);
|
|
1602
|
+
return `${s.name} imported and tracked in ${where}`;
|
|
1603
|
+
}
|
|
1092
1604
|
}
|
|
1093
|
-
|
|
1605
|
+
]
|
|
1094
1606
|
});
|
|
1095
1607
|
} else if (s.state === "untracked copy of library skill") {
|
|
1096
1608
|
issues.push({
|
|
1097
1609
|
id: `adopt:${root}:${s.name}`,
|
|
1098
1610
|
severity: "suggestion",
|
|
1099
1611
|
title: `${s.name} in ${where} isn't tracked`,
|
|
1100
|
-
detail: "It's identical to the library copy; track it so future library changes reach it.",
|
|
1101
|
-
|
|
1612
|
+
detail: "It's identical to the library copy; track it (in skilllib.json) so future library changes reach it.",
|
|
1613
|
+
choices: [{ label: "Track it", hint: `adds it to ${where}'s skilllib.json`, run: () => `${addSkill(root, s.name).name} tracked in ${where}` }]
|
|
1102
1614
|
});
|
|
1103
1615
|
}
|
|
1104
|
-
if (globalNames.has(s.name) && s.managed)
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1616
|
+
if (globalNames.has(s.name) && s.managed) alsoGlobal.set(s.name, [...alsoGlobal.get(s.name) ?? [], root]);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
for (const [name, roots] of alsoGlobal) {
|
|
1620
|
+
const wheres = roots.map((r) => basename5(r)).join(", ");
|
|
1621
|
+
if (kept.has(name)) {
|
|
1622
|
+
issues.push({
|
|
1623
|
+
id: `twice:${name}`,
|
|
1624
|
+
severity: "suggestion",
|
|
1625
|
+
title: `${name}: in ${wheres}, and you keep it global`,
|
|
1626
|
+
detail: "The global copy already loads there, so the project copies are extra. Claude Code runs the global one anyway.",
|
|
1627
|
+
choices: [
|
|
1628
|
+
{
|
|
1629
|
+
label: `Remove the copies in ${wheres}`,
|
|
1630
|
+
hint: "the global copy stays",
|
|
1112
1631
|
run: () => {
|
|
1113
|
-
const
|
|
1114
|
-
|
|
1115
|
-
const r = unloadGlobal(g.path, g.links);
|
|
1116
|
-
return r.ok ? `${s.name} no longer loads globally` : `${s.name}: ${r.reason}`;
|
|
1632
|
+
const removed = roots.filter((root) => removeSkill(root, name).action === "removed").map((r) => basename5(r));
|
|
1633
|
+
return `${name}: removed from ${removed.join(", ") || "no project"}; the global copy stays`;
|
|
1117
1634
|
}
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
}
|
|
1635
|
+
}
|
|
1636
|
+
]
|
|
1637
|
+
});
|
|
1638
|
+
continue;
|
|
1121
1639
|
}
|
|
1640
|
+
const movable = machine.find((m) => m.name === name && m.movable);
|
|
1641
|
+
issues.push({
|
|
1642
|
+
id: `twice:${name}`,
|
|
1643
|
+
severity: "suggestion",
|
|
1644
|
+
title: `${name}: in ${wheres} and also loaded globally`,
|
|
1645
|
+
detail: "The project copies are enough; the global one loads it in every other project too. Or keep it global on purpose (Global \u2192 Enter).",
|
|
1646
|
+
choices: movable ? [
|
|
1647
|
+
{
|
|
1648
|
+
label: "Stop loading it globally",
|
|
1649
|
+
hint: "restorable from Health",
|
|
1650
|
+
run: () => {
|
|
1651
|
+
if (!libraryNames.has(name)) importSkill(movable.path);
|
|
1652
|
+
const r = unloadGlobal(movable.path, movable.links);
|
|
1653
|
+
if (!r.ok) throw new Error(`${name}: ${r.reason}`);
|
|
1654
|
+
return `${name} no longer loads globally`;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
] : void 0
|
|
1658
|
+
});
|
|
1122
1659
|
}
|
|
1123
1660
|
return issues;
|
|
1124
1661
|
}
|
|
@@ -1128,20 +1665,24 @@ var USAGE_DAYS = 30;
|
|
|
1128
1665
|
function parseArgs(argv) {
|
|
1129
1666
|
const daysIdx = argv.indexOf("--days");
|
|
1130
1667
|
const days = daysIdx >= 0 ? Number(argv[daysIdx + 1]) : USAGE_DAYS;
|
|
1668
|
+
const keepIdx = argv.indexOf("--keep");
|
|
1131
1669
|
return {
|
|
1132
|
-
positional: argv.filter((a, i) => !a.startsWith("--") && argv[i - 1] !== "--days"),
|
|
1670
|
+
positional: argv.filter((a, i) => !a.startsWith("--") && argv[i - 1] !== "--days" && argv[i - 1] !== "--keep"),
|
|
1133
1671
|
force: argv.includes("--force"),
|
|
1134
1672
|
all: argv.includes("--all"),
|
|
1135
1673
|
global: argv.includes("--global"),
|
|
1136
1674
|
fix: argv.includes("--fix"),
|
|
1137
1675
|
allowTracked: argv.includes("--allow-tracked"),
|
|
1138
|
-
days: Number.isFinite(days) && days > 0 ? days : USAGE_DAYS
|
|
1676
|
+
days: Number.isFinite(days) && days > 0 ? days : USAGE_DAYS,
|
|
1677
|
+
dryRun: argv.includes("--dry-run"),
|
|
1678
|
+
allowGit: argv.includes("--allow-git"),
|
|
1679
|
+
...keepIdx >= 0 && argv[keepIdx + 1] ? { keep: argv[keepIdx + 1] } : {}
|
|
1139
1680
|
};
|
|
1140
1681
|
}
|
|
1141
1682
|
function colorState(state) {
|
|
1142
1683
|
if (state === "ok") return green(state);
|
|
1143
1684
|
if (state === "folder missing" || state === "not in library") return red(state);
|
|
1144
|
-
if (state === "local only") return dim(state);
|
|
1685
|
+
if (state === "local only" || state === "from npx skills") return dim(state);
|
|
1145
1686
|
return yellow(state);
|
|
1146
1687
|
}
|
|
1147
1688
|
function printChanges(changes) {
|
|
@@ -1161,9 +1702,9 @@ function projectOfFactory(projects2) {
|
|
|
1161
1702
|
const sorted = [...projects2].sort((a, b) => b.length - a.length);
|
|
1162
1703
|
return (cwd) => {
|
|
1163
1704
|
if (!cwd) return null;
|
|
1164
|
-
const known = sorted.find((p) => cwd === p || cwd.startsWith(p +
|
|
1705
|
+
const known = sorted.find((p) => cwd === p || cwd.startsWith(p + sep5));
|
|
1165
1706
|
if (known) return known;
|
|
1166
|
-
const repo = cwd.split(`${
|
|
1707
|
+
const repo = cwd.split(`${sep5}.claude${sep5}worktrees${sep5}`)[0];
|
|
1167
1708
|
return isProjectCandidate(repo) ? repo : null;
|
|
1168
1709
|
};
|
|
1169
1710
|
}
|
|
@@ -1179,7 +1720,7 @@ function lastUsed(summary) {
|
|
|
1179
1720
|
async function status(args) {
|
|
1180
1721
|
const root = findProjectRoot();
|
|
1181
1722
|
const skills = projectStatus(root);
|
|
1182
|
-
info(`${
|
|
1723
|
+
info(`${basename6(root)} ${dim(tildify(root))}
|
|
1183
1724
|
`);
|
|
1184
1725
|
if (skills.length === 0) {
|
|
1185
1726
|
info("No skills in this project yet. Add one from your library with: skilllib add <name>");
|
|
@@ -1189,7 +1730,7 @@ async function status(args) {
|
|
|
1189
1730
|
table(
|
|
1190
1731
|
skills.map((s) => ({
|
|
1191
1732
|
Skill: s.name,
|
|
1192
|
-
Status: colorState(s.state),
|
|
1733
|
+
Status: s.source ? dim(`npx skills: ${s.source}`) : colorState(s.state),
|
|
1193
1734
|
[`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0),
|
|
1194
1735
|
"Last used": lastUsed(usage2.get(s.name))
|
|
1195
1736
|
}))
|
|
@@ -1215,7 +1756,7 @@ async function list(args) {
|
|
|
1215
1756
|
const installs = /* @__PURE__ */ new Map();
|
|
1216
1757
|
for (const project of visibleProjects()) {
|
|
1217
1758
|
for (const name of Object.keys(readManifest(project).skills)) {
|
|
1218
|
-
installs.set(name, [...installs.get(name) ?? [],
|
|
1759
|
+
installs.set(name, [...installs.get(name) ?? [], basename6(project)]);
|
|
1219
1760
|
}
|
|
1220
1761
|
}
|
|
1221
1762
|
const usage2 = await usageBySkill(args.days);
|
|
@@ -1257,7 +1798,7 @@ function projects() {
|
|
|
1257
1798
|
const managed = skills.filter((s) => s.managed);
|
|
1258
1799
|
const attention = skills.filter((s) => s.state !== "ok" && s.state !== "local only").length;
|
|
1259
1800
|
return {
|
|
1260
|
-
Project:
|
|
1801
|
+
Project: basename6(root),
|
|
1261
1802
|
Path: dim(tildify(root)),
|
|
1262
1803
|
Skills: managed.map((s) => s.name).join(", ") || dim("none"),
|
|
1263
1804
|
"Local only": String(skills.filter((s) => s.state === "local only").length),
|
|
@@ -1289,7 +1830,7 @@ function sync(args) {
|
|
|
1289
1830
|
for (const root of roots) {
|
|
1290
1831
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
1291
1832
|
const changes = syncProject(root, { force: args.force });
|
|
1292
|
-
if (args.all) info(`${
|
|
1833
|
+
if (args.all) info(`${basename6(root)} ${dim(tildify(root))}`);
|
|
1293
1834
|
if (changes.length === 0) success("Installed versions match skilllib.json");
|
|
1294
1835
|
else printChanges(changes);
|
|
1295
1836
|
}
|
|
@@ -1300,7 +1841,7 @@ function update(args) {
|
|
|
1300
1841
|
for (const root of roots) {
|
|
1301
1842
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
1302
1843
|
const changes = updateProject(root, args.positional.length ? args.positional : void 0, { force: args.force });
|
|
1303
|
-
if (args.all) info(`${
|
|
1844
|
+
if (args.all) info(`${basename6(root)} ${dim(tildify(root))}`);
|
|
1304
1845
|
if (changes.length === 0) success("Everything is on the latest version");
|
|
1305
1846
|
else printChanges(changes);
|
|
1306
1847
|
}
|
|
@@ -1308,7 +1849,7 @@ function update(args) {
|
|
|
1308
1849
|
function outdated(args) {
|
|
1309
1850
|
const roots = args.all ? visibleProjects() : [findProjectRoot()];
|
|
1310
1851
|
const rows = roots.flatMap(
|
|
1311
|
-
(root) => projectStatus(root).filter((s) => s.managed && s.latest !== null && s.version !== null && s.latest > s.version).map((s) => ({ Project:
|
|
1852
|
+
(root) => projectStatus(root).filter((s) => s.managed && s.latest !== null && s.version !== null && s.latest > s.version).map((s) => ({ Project: basename6(root), Skill: s.name, Installed: `v${s.version}`, Latest: `v${s.latest}`, Status: colorState(s.state) }))
|
|
1312
1853
|
);
|
|
1313
1854
|
if (rows.length === 0) return success("Everything is on the latest version");
|
|
1314
1855
|
table(rows);
|
|
@@ -1334,10 +1875,10 @@ function importSkills(args) {
|
|
|
1334
1875
|
success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
|
|
1335
1876
|
const skillsDir = resolve4(dir, "..");
|
|
1336
1877
|
const root = resolve4(skillsDir, "..", "..");
|
|
1337
|
-
if (skillsDir ===
|
|
1878
|
+
if (skillsDir === join13(root, ".claude", "skills") && isProjectCandidate(root)) {
|
|
1338
1879
|
if (readManifest(root).skills[name]?.hash !== treeHash(dir)) {
|
|
1339
1880
|
const change = addSkill(root, name);
|
|
1340
|
-
if (change.action !== "skipped") info(dim(` ${
|
|
1881
|
+
if (change.action !== "skipped") info(dim(` ${basename6(root)} now depends on ${name} v${change.to}`));
|
|
1341
1882
|
}
|
|
1342
1883
|
rememberProjects([root]);
|
|
1343
1884
|
}
|
|
@@ -1352,13 +1893,13 @@ function scan(args) {
|
|
|
1352
1893
|
}
|
|
1353
1894
|
const found = discoverProjects();
|
|
1354
1895
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
1355
|
-
const skillNames = (root) => [...skillDirsIn(projectSkillsDir(root)), ...skillDirsIn(agentsSkillsDir(root))].map((d) =>
|
|
1896
|
+
const skillNames = (root) => [...skillDirsIn(projectSkillsDir(root)), ...skillDirsIn(agentsSkillsDir(root))].map((d) => basename6(d));
|
|
1356
1897
|
const withSkills = found.filter((root) => skillNames(root).length > 0);
|
|
1357
1898
|
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project(s), ${withSkills.length} with skills
|
|
1358
1899
|
`);
|
|
1359
1900
|
table(
|
|
1360
1901
|
withSkills.map((root) => ({
|
|
1361
|
-
Project:
|
|
1902
|
+
Project: basename6(root),
|
|
1362
1903
|
Path: dim(tildify(root)),
|
|
1363
1904
|
Skills: [...new Set(skillNames(root))].map((n) => library.has(n) ? n : yellow(n)).join(", ")
|
|
1364
1905
|
}))
|
|
@@ -1399,14 +1940,14 @@ function newSkill(args) {
|
|
|
1399
1940
|
error(`Can't create ${name}: ${r.reason}`);
|
|
1400
1941
|
process.exit(1);
|
|
1401
1942
|
}
|
|
1402
|
-
success(`Created ${tildify(
|
|
1943
|
+
success(`Created ${tildify(join13(r.dir, "SKILL.md"))}`);
|
|
1403
1944
|
}
|
|
1404
1945
|
function restore(args) {
|
|
1405
|
-
const backups = listBackups();
|
|
1946
|
+
const backups = [...listBackups(), ...pluginBackups()];
|
|
1406
1947
|
const name = args.positional[0];
|
|
1407
1948
|
if (!name) {
|
|
1408
1949
|
if (backups.length === 0) return info("Nothing to restore.");
|
|
1409
|
-
table(backups.map((b) => ({ Skill: b.name, From: b.kind === "trash" ? "library (deleted)" : "
|
|
1950
|
+
table(backups.map((b) => ({ Skill: b.name, From: b.kind === "trash" ? "library (deleted)" : b.kind === "plugin" ? "Claude Code plugin" : tildify(dirname5(b.from)), Moved: b.movedAt })));
|
|
1410
1951
|
info(dim("\n\u2192 skilllib restore <name>"));
|
|
1411
1952
|
return;
|
|
1412
1953
|
}
|
|
@@ -1415,7 +1956,7 @@ function restore(args) {
|
|
|
1415
1956
|
error(`No backup named ${name}`);
|
|
1416
1957
|
process.exit(1);
|
|
1417
1958
|
}
|
|
1418
|
-
const r = restoreBackup(backup);
|
|
1959
|
+
const r = backup.kind === "plugin" ? restorePlugin(backup) : restoreBackup(backup);
|
|
1419
1960
|
if (r.ok) success(`Restored ${name} to ${tildify(r.to)}`);
|
|
1420
1961
|
else error(`Can't restore ${name}: ${r.reason}`);
|
|
1421
1962
|
}
|
|
@@ -1426,9 +1967,13 @@ function doctor(args) {
|
|
|
1426
1967
|
for (const issue of issues) {
|
|
1427
1968
|
info(`${issue.severity === "problem" ? red("\u25CF") : yellow("\u25C6")} ${issue.title}`);
|
|
1428
1969
|
info(dim(` ${issue.detail}${issue.fix ? ` \u2192 ${issue.fix.label}` : ""}`));
|
|
1429
|
-
if (
|
|
1970
|
+
if (issue.choices) info(dim(` choose in skilllib \u2192 Health: ${issue.choices.map((c) => c.label).join(" / ")}`));
|
|
1971
|
+
if (args.fix && issue.fix) {
|
|
1972
|
+
const r = runFix(issue.fix);
|
|
1973
|
+
info(` ${r.ok ? green("\u2713") : red("\u2717")} ${r.message}`);
|
|
1974
|
+
}
|
|
1430
1975
|
}
|
|
1431
|
-
if (!args.fix && issues.some((i) => i.fix)) info(dim("\nRun `skilllib doctor --fix` to apply the fixes, or fix them one by one in `skilllib` \u2192
|
|
1976
|
+
if (!args.fix && issues.some((i) => i.fix)) info(dim("\nRun `skilllib doctor --fix` to apply the fixes, or fix them one by one in `skilllib` \u2192 Health."));
|
|
1432
1977
|
}
|
|
1433
1978
|
async function usage(args) {
|
|
1434
1979
|
const summaries = summarize(await scanUsage(args.days), projectOfFactory(knownProjects()));
|
|
@@ -1437,13 +1982,13 @@ async function usage(args) {
|
|
|
1437
1982
|
return;
|
|
1438
1983
|
}
|
|
1439
1984
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
1440
|
-
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(
|
|
1985
|
+
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join13(claudeDir(), "projects"))})`)}
|
|
1441
1986
|
`);
|
|
1442
1987
|
table(
|
|
1443
1988
|
summaries.map((s) => ({
|
|
1444
1989
|
Skill: s.skill,
|
|
1445
1990
|
Uses: String(s.uses),
|
|
1446
|
-
Projects: [...s.projects].map((p) =>
|
|
1991
|
+
Projects: [...s.projects].map((p) => basename6(p)).join(", ") || dim("-"),
|
|
1447
1992
|
"Last used": lastUsed(s),
|
|
1448
1993
|
Library: library.has(s.skill) ? green("yes") : dim("no")
|
|
1449
1994
|
}))
|
|
@@ -1456,7 +2001,7 @@ async function usage(args) {
|
|
|
1456
2001
|
}
|
|
1457
2002
|
function show(args) {
|
|
1458
2003
|
const name = args.positional[0];
|
|
1459
|
-
const dir = name ?
|
|
2004
|
+
const dir = name ? join13(libraryDir(), name) : "";
|
|
1460
2005
|
if (!name || !isSkillDir(dir)) {
|
|
1461
2006
|
error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
|
|
1462
2007
|
process.exit(1);
|
|
@@ -1492,17 +2037,71 @@ function link(args) {
|
|
|
1492
2037
|
for (const root of roots) {
|
|
1493
2038
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
1494
2039
|
const res = linkAll(root);
|
|
1495
|
-
const label = args.all ? `${
|
|
2040
|
+
const label = args.all ? `${basename6(root)}: ` : "";
|
|
1496
2041
|
if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
|
|
1497
2042
|
else if (!args.all) success("Every skill here is already usable by all your agents");
|
|
1498
2043
|
if (res.blocked.length) warn(`${label}git tracks ${res.blocked.join(", ")}; re-run with --allow-tracked to link there too`);
|
|
1499
2044
|
}
|
|
1500
2045
|
}
|
|
2046
|
+
function globalCommand(args) {
|
|
2047
|
+
const [action, ...names] = args.positional;
|
|
2048
|
+
const yours = machineSkills().filter((m) => m.movable && !m.broken);
|
|
2049
|
+
if (action === "keep" || action === "unkeep") {
|
|
2050
|
+
const unknown = names.filter((n) => !yours.some((m) => m.name === n));
|
|
2051
|
+
if (!names.length || unknown.length) {
|
|
2052
|
+
error(names.length ? `Not one of your global skills: ${unknown.join(", ")}` : `Usage: skilllib global ${action} <name>...`);
|
|
2053
|
+
process.exit(1);
|
|
2054
|
+
}
|
|
2055
|
+
setKeepGlobal(names, action === "keep");
|
|
2056
|
+
success(action === "keep" ? `Keeping ${names.join(", ")} global on purpose` : `${names.join(", ")} will show up in cleanup again`);
|
|
2057
|
+
} else if (action) {
|
|
2058
|
+
error("Usage: skilllib global [keep|unkeep <name>...]");
|
|
2059
|
+
process.exit(1);
|
|
2060
|
+
}
|
|
2061
|
+
const kept = keptGlobal();
|
|
2062
|
+
if (!yours.length) return info("None of your skills load globally.");
|
|
2063
|
+
for (const m of yours) info(`${kept.has(m.name) ? green("\u2713") : yellow("\u26A0")} ${m.name.padEnd(32)} ${dim(tildify(m.path))}`);
|
|
2064
|
+
const unreviewed = yours.filter((m) => !kept.has(m.name)).length;
|
|
2065
|
+
if (unreviewed) info(dim(`
|
|
2066
|
+
${unreviewed} not reviewed. Keep one global on purpose: skilllib global keep <name>`));
|
|
2067
|
+
}
|
|
2068
|
+
function tidy(args) {
|
|
2069
|
+
const names = args.positional;
|
|
2070
|
+
const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve4(base, args.keep) : expandHome(args.keep)])) : {};
|
|
2071
|
+
const reports = args.global ? [{ label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) }] : (args.all ? visibleProjects() : [findProjectRoot()]).map((root) => ({ label: basename6(root), report: planProjectTidy(root, { keep: keepFor(root) }) }));
|
|
2072
|
+
let held = 0;
|
|
2073
|
+
let changed = 0;
|
|
2074
|
+
for (const { label, report } of reports) {
|
|
2075
|
+
const plans = report.plans.filter((p) => !names.length || names.includes(p.name));
|
|
2076
|
+
const conflicts = report.conflicts.filter((c) => !names.length || names.includes(c.name));
|
|
2077
|
+
if (!plans.length && !conflicts.length && !args.all) success(`${label}: every skill has one copy and the links your agents need`);
|
|
2078
|
+
for (const plan of plans) {
|
|
2079
|
+
const result = args.dryRun ? { applied: [], held: gitVisibleSteps(plan) } : applyTidy(plan, { git: args.allowGit ? "go" : "keep" });
|
|
2080
|
+
const show2 = args.dryRun ? plan.steps : result.applied;
|
|
2081
|
+
if (show2.length) info(`${args.dryRun ? dim("would") : green("\u2713")} ${label} \xB7 ${plan.name}: ${show2.map((s) => describeStep(s, plan.root)).join(", ")}`);
|
|
2082
|
+
if (!args.dryRun) for (const step of result.held) info(`${yellow("!")} ${label} \xB7 ${plan.name}: held back, git would see it: ${describeStep(step, plan.root)}`);
|
|
2083
|
+
held += args.dryRun ? 0 : result.held.length;
|
|
2084
|
+
changed += result.applied.length;
|
|
2085
|
+
}
|
|
2086
|
+
for (const c of conflicts) {
|
|
2087
|
+
warn(`${label} \xB7 ${c.name}: the copies differ`);
|
|
2088
|
+
for (const copy of c.copies) {
|
|
2089
|
+
const runs = copy.runs.map((id) => HARNESSES.find((h) => h.id === id).name);
|
|
2090
|
+
info(` ${copyLabel(c, copy.dir)}${runs.length ? dim(` run by ${runs.join(", ")}`) : ""}${c.managed === copy.dir ? dim(" (skilllib's copy)") : ""}`);
|
|
2091
|
+
}
|
|
2092
|
+
const pick = copyLabel(c, c.managed ?? c.copies[0].dir);
|
|
2093
|
+
info(dim(` keep one: skilllib tidy ${c.name}${args.global ? " --global" : ""} --keep ${pick}`));
|
|
2094
|
+
}
|
|
2095
|
+
for (const s of report.skipped.filter((x) => !names.length || names.includes(x.name))) info(dim(`- ${label} \xB7 ${s.name}: skipped, ${s.reason}`));
|
|
2096
|
+
}
|
|
2097
|
+
if (args.dryRun) info(dim("\nNothing changed (--dry-run)."));
|
|
2098
|
+
else if (changed) info(dim("\nReplaced copies are in ~/.skilllib/tidy-backup; `skilllib restore` puts them back."));
|
|
2099
|
+
if (held) info(dim(`${held} change${held === 1 ? "" : "s"} held back because git would see them. Re-run with --allow-git to make them.`));
|
|
2100
|
+
}
|
|
1501
2101
|
|
|
1502
2102
|
export {
|
|
1503
2103
|
userHome,
|
|
1504
2104
|
skilllibHome,
|
|
1505
|
-
MANIFEST_FILE,
|
|
1506
2105
|
HARNESSES,
|
|
1507
2106
|
ALL_PROJECT_DIRS,
|
|
1508
2107
|
installDirs,
|
|
@@ -1522,6 +2121,8 @@ export {
|
|
|
1522
2121
|
harnessesChosen,
|
|
1523
2122
|
setHarnesses,
|
|
1524
2123
|
allowTrackedLinks,
|
|
2124
|
+
keptGlobal,
|
|
2125
|
+
setKeepGlobal,
|
|
1525
2126
|
machineSkills,
|
|
1526
2127
|
libraryOrigins,
|
|
1527
2128
|
librarySkills,
|
|
@@ -1546,6 +2147,11 @@ export {
|
|
|
1546
2147
|
error,
|
|
1547
2148
|
scanUsage,
|
|
1548
2149
|
summarize,
|
|
2150
|
+
gitInfo,
|
|
2151
|
+
relativeTo,
|
|
2152
|
+
pluginBackups,
|
|
2153
|
+
restorePlugin,
|
|
2154
|
+
runFix,
|
|
1549
2155
|
findIssues,
|
|
1550
2156
|
parseArgs,
|
|
1551
2157
|
projectOfFactory,
|
|
@@ -1567,5 +2173,7 @@ export {
|
|
|
1567
2173
|
usage,
|
|
1568
2174
|
show,
|
|
1569
2175
|
harnessesCommand,
|
|
1570
|
-
link
|
|
2176
|
+
link,
|
|
2177
|
+
globalCommand,
|
|
2178
|
+
tidy
|
|
1571
2179
|
};
|