skilllib 1.1.0 → 1.3.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/README.md +83 -76
- package/dist/{chunk-HLCJQVHB.js → chunk-TWQQHXVG.js} +676 -95
- package/dist/index.js +10 -3
- package/dist/tui-H5IQAGAL.js +2184 -0
- package/package.json +1 -1
- package/dist/tui-YF7BQ7CD.js +0 -2763
|
@@ -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
|
|
@@ -400,6 +416,9 @@ function readJson(path) {
|
|
|
400
416
|
function skillsShLock() {
|
|
401
417
|
return readJson(join7(resolve3(claudeDir(), ".."), ".agents", ".skill-lock.json"))?.skills ?? {};
|
|
402
418
|
}
|
|
419
|
+
function projectSkillsLock(root) {
|
|
420
|
+
return readJson(join7(root, "skills-lock.json"))?.skills ?? {};
|
|
421
|
+
}
|
|
403
422
|
function realpathOrNull(path) {
|
|
404
423
|
try {
|
|
405
424
|
return realpathSync2(path);
|
|
@@ -493,6 +512,14 @@ function claudeAiSkills() {
|
|
|
493
512
|
}))
|
|
494
513
|
);
|
|
495
514
|
}
|
|
515
|
+
function setPluginEnabled(id, on) {
|
|
516
|
+
const file = join7(claudeDir(), "settings.json");
|
|
517
|
+
const settings = existsSync6(file) ? readJson(file) : {};
|
|
518
|
+
if (!settings) throw new Error(`${file} isn't valid JSON, so skilllib won't rewrite it`);
|
|
519
|
+
settings.enabledPlugins = { ...settings.enabledPlugins, [id]: on };
|
|
520
|
+
mkdirSync4(claudeDir(), { recursive: true });
|
|
521
|
+
writeFileSync4(file, JSON.stringify(settings, null, 2) + "\n");
|
|
522
|
+
}
|
|
496
523
|
function pluginSkills() {
|
|
497
524
|
const plugins = join7(claudeDir(), "plugins");
|
|
498
525
|
const enabled = readJson(join7(claudeDir(), "settings.json"))?.enabledPlugins ?? {};
|
|
@@ -513,7 +540,8 @@ function pluginSkills() {
|
|
|
513
540
|
(bucket) => readdirSync3(join7(syncedDir, bucket), { withFileTypes: true }).filter((d) => d.isDirectory()).flatMap((d) => {
|
|
514
541
|
const root = join7(syncedDir, bucket, d.name);
|
|
515
542
|
const name = readJson(join7(root, ".claude-plugin", "plugin.json"))?.name ?? d.name;
|
|
516
|
-
|
|
543
|
+
if (enabled[`${name}@synced`] === false) return [];
|
|
544
|
+
return skillDirsIn(join7(root, "skills")).map((path) => ({ path, origin: `${name}@synced` }));
|
|
517
545
|
})
|
|
518
546
|
) : [];
|
|
519
547
|
return [...fromMarketplaces, ...fromSynced].map(({ path, origin }) => ({
|
|
@@ -529,11 +557,11 @@ function pluginSkills() {
|
|
|
529
557
|
}));
|
|
530
558
|
}
|
|
531
559
|
function machineSkills(enabled = enabledHarnesses()) {
|
|
532
|
-
const
|
|
560
|
+
const claude2 = enabled.includes("claude-code");
|
|
533
561
|
return [
|
|
534
562
|
...globalFolderSkills(enabled),
|
|
535
|
-
...
|
|
536
|
-
...
|
|
563
|
+
...claude2 ? claudeAiSkills() : [],
|
|
564
|
+
...claude2 ? pluginSkills() : [],
|
|
537
565
|
...cursorBuiltInSkills(enabled)
|
|
538
566
|
].sort(
|
|
539
567
|
(a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name)
|
|
@@ -555,7 +583,9 @@ function originFor(dir) {
|
|
|
555
583
|
const match = machineSkills().find((s) => resolve3(s.path) === resolve3(dir));
|
|
556
584
|
if (match) return match.kind === "global" ? "global folder" : `${match.kind}: ${match.origin}`;
|
|
557
585
|
const project = dir.split(/[\\/]\.(?:claude|agents)[\\/]skills[\\/]/)[0];
|
|
558
|
-
|
|
586
|
+
if (!project || project === dir) return dir;
|
|
587
|
+
const locked = projectSkillsLock(project)[basename2(dir)]?.source;
|
|
588
|
+
return locked ? `skills.sh: ${locked}` : `project: ${basename2(project)}`;
|
|
559
589
|
}
|
|
560
590
|
|
|
561
591
|
// src/library.ts
|
|
@@ -596,11 +626,11 @@ function visibilityOf(root, name, enabled = enabledHarnesses()) {
|
|
|
596
626
|
paths: harness(id).projectDirs.filter((dir) => existsSync7(join8(root, dir, name, "SKILL.md"))).length
|
|
597
627
|
}));
|
|
598
628
|
}
|
|
599
|
-
function dependencyDirs(dep) {
|
|
629
|
+
function dependencyDirs(root, name, dep) {
|
|
600
630
|
const wanted = installDirs(enabledHarnesses());
|
|
601
|
-
|
|
602
|
-
const primary = dep.dir ?? PROJECT_SKILLS_DIR;
|
|
603
|
-
return [primary, .../* @__PURE__ */ new Set([...dep
|
|
631
|
+
const untracked = ALL_PROJECT_DIRS.find((d) => existsSync7(join8(root, d, name, "SKILL.md")) && !isLink(join8(root, d, name)));
|
|
632
|
+
const primary = dep ? dep.dir ?? PROJECT_SKILLS_DIR : untracked ?? wanted[0];
|
|
633
|
+
return [primary, .../* @__PURE__ */ new Set([...dep?.links ?? [], ...wanted.filter((d) => d !== primary)])];
|
|
604
634
|
}
|
|
605
635
|
function isGitTracked(root, dir) {
|
|
606
636
|
try {
|
|
@@ -633,6 +663,7 @@ function projectStatus(root) {
|
|
|
633
663
|
};
|
|
634
664
|
});
|
|
635
665
|
const seen = new Set(Object.keys(skills));
|
|
666
|
+
const lock = projectSkillsLock(root);
|
|
636
667
|
const unmanaged = [];
|
|
637
668
|
const byName = /* @__PURE__ */ new Map();
|
|
638
669
|
for (const dir of ALL_PROJECT_DIRS) {
|
|
@@ -649,7 +680,8 @@ function projectStatus(root) {
|
|
|
649
680
|
const local = treeHash(real);
|
|
650
681
|
const sameAsLibrary = latest !== null && (latest.hash === local || versionForHash(name, local ?? "") !== null);
|
|
651
682
|
const committed = location === AGENTS_SKILLS_DIR;
|
|
652
|
-
const
|
|
683
|
+
const source = lock[name]?.source;
|
|
684
|
+
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";
|
|
653
685
|
unmanaged.push({
|
|
654
686
|
name,
|
|
655
687
|
managed: false,
|
|
@@ -658,7 +690,8 @@ function projectStatus(root) {
|
|
|
658
690
|
latest: latest?.version ?? null,
|
|
659
691
|
location,
|
|
660
692
|
path: real,
|
|
661
|
-
visibility: visibilityOf(root, name, enabled)
|
|
693
|
+
visibility: visibilityOf(root, name, enabled),
|
|
694
|
+
...source ? { source } : {}
|
|
662
695
|
});
|
|
663
696
|
}
|
|
664
697
|
return [...managed, ...unmanaged].sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -716,7 +749,7 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
716
749
|
if (!target) return { name, action: "skipped", reason: version === void 0 ? "not in the library" : `version ${version} doesn't exist` };
|
|
717
750
|
const manifest = readManifest(root);
|
|
718
751
|
const recorded = manifest.skills[name];
|
|
719
|
-
const [primary = PROJECT_SKILLS_DIR, ...linkDirs] = dependencyDirs(recorded);
|
|
752
|
+
const [primary = PROJECT_SKILLS_DIR, ...linkDirs] = dependencyDirs(root, name, recorded);
|
|
720
753
|
const to = join8(root, primary, name);
|
|
721
754
|
const local = isLink(to) && !recorded ? null : treeHash(to);
|
|
722
755
|
const knownContent = local !== null && (local === recorded?.hash || versionForHash(name, local) !== null);
|
|
@@ -842,6 +875,10 @@ function stash(dir, bucket) {
|
|
|
842
875
|
writeFileSync5(backupIndexFile(bucket), JSON.stringify({ ...readBackupIndex(bucket), [entry]: dir }, null, 2) + "\n");
|
|
843
876
|
return target;
|
|
844
877
|
}
|
|
878
|
+
function removeUntracked(root, name, path) {
|
|
879
|
+
unlinkEverywhere(root, name);
|
|
880
|
+
return stash(path, "trash");
|
|
881
|
+
}
|
|
845
882
|
function projectsUsing(name) {
|
|
846
883
|
return knownProjects().filter((p) => name in readManifest(p).skills);
|
|
847
884
|
}
|
|
@@ -887,7 +924,7 @@ Instructions for Claude go here.
|
|
|
887
924
|
return { ok: true, dir };
|
|
888
925
|
}
|
|
889
926
|
function listBackups() {
|
|
890
|
-
return ["trash", "global-backup"].flatMap((kind) => {
|
|
927
|
+
return ["trash", "global-backup", "tidy-backup"].flatMap((kind) => {
|
|
891
928
|
const dir = join8(skilllibHome(), kind);
|
|
892
929
|
if (!existsSync7(dir)) return [];
|
|
893
930
|
const index = readBackupIndex(kind);
|
|
@@ -902,6 +939,7 @@ function listBackups() {
|
|
|
902
939
|
}
|
|
903
940
|
function restoreBackup(backup) {
|
|
904
941
|
const to = backup.from;
|
|
942
|
+
if (backup.kind === "tidy-backup" && isLink(to)) unlinkSync(to);
|
|
905
943
|
let taken = existsSync7(to);
|
|
906
944
|
try {
|
|
907
945
|
taken = taken || lstatSync2(to) !== null;
|
|
@@ -1020,20 +1058,395 @@ async function scanUsage(days) {
|
|
|
1020
1058
|
function summarize(uses, projectOf = () => null) {
|
|
1021
1059
|
const bySkill = /* @__PURE__ */ new Map();
|
|
1022
1060
|
for (const use of uses) {
|
|
1023
|
-
const
|
|
1061
|
+
const skill = use.skill.split(":").pop();
|
|
1062
|
+
const summary = bySkill.get(skill) ?? { skill, uses: 0, lastUsed: "", projects: /* @__PURE__ */ new Set() };
|
|
1024
1063
|
summary.uses += 1;
|
|
1025
1064
|
if (use.at > summary.lastUsed) summary.lastUsed = use.at;
|
|
1026
1065
|
const project = projectOf(use.cwd);
|
|
1027
1066
|
if (project) summary.projects.add(project);
|
|
1028
|
-
bySkill.set(
|
|
1067
|
+
bySkill.set(skill, summary);
|
|
1029
1068
|
}
|
|
1030
1069
|
return [...bySkill.values()].sort((a, b) => b.uses - a.uses);
|
|
1031
1070
|
}
|
|
1032
1071
|
|
|
1033
1072
|
// src/health.ts
|
|
1034
|
-
import { basename as
|
|
1073
|
+
import { basename as basename5 } from "path";
|
|
1074
|
+
|
|
1075
|
+
// src/git.ts
|
|
1076
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1077
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1078
|
+
import { join as join10, relative as relative3 } from "path";
|
|
1079
|
+
function git(root, args, input) {
|
|
1080
|
+
try {
|
|
1081
|
+
return execFileSync2("git", args, { cwd: root, encoding: "utf-8", input, stdio: ["pipe", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 });
|
|
1082
|
+
} catch (err) {
|
|
1083
|
+
const out = err.stdout;
|
|
1084
|
+
return typeof out === "string" ? out : null;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
function gitInfo(root) {
|
|
1088
|
+
if (!existsSync9(join10(root, ".git"))) return null;
|
|
1089
|
+
const dirs = [...ALL_PROJECT_DIRS, MANIFEST_FILE].filter((d) => existsSync9(join10(root, d)));
|
|
1090
|
+
if (dirs.length === 0) return { of: () => "new" };
|
|
1091
|
+
const tracked = (git(root, ["ls-files", "-z", "--", ...dirs]) ?? "").split("\0").filter(Boolean);
|
|
1092
|
+
const dirty = (git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...dirs]) ?? "").split("\0").filter(Boolean).map((entry) => entry.slice(3));
|
|
1093
|
+
const ignoredCache = /* @__PURE__ */ new Map();
|
|
1094
|
+
const isIgnored = (rel) => {
|
|
1095
|
+
if (!ignoredCache.has(rel)) {
|
|
1096
|
+
const out = git(root, ["check-ignore", "--no-index", "--", rel]) ?? "";
|
|
1097
|
+
ignoredCache.set(rel, out.trim().length > 0);
|
|
1098
|
+
}
|
|
1099
|
+
return ignoredCache.get(rel);
|
|
1100
|
+
};
|
|
1101
|
+
const under = (list2, rel) => list2.some((p) => p === rel || p.startsWith(rel + "/"));
|
|
1102
|
+
return {
|
|
1103
|
+
of: (relPath) => {
|
|
1104
|
+
const rel = relPath.replace(/\/+$/, "");
|
|
1105
|
+
if (under(tracked, rel)) return under(dirty, rel) ? "changed" : "committed";
|
|
1106
|
+
return isIgnored(rel) ? "ignored" : "new";
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
function relativeTo(root, path) {
|
|
1111
|
+
return relative3(root, path).split("\\").join("/");
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// src/plugins.ts
|
|
1115
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1116
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
1117
|
+
import { basename as basename4, dirname as dirname3, join as join11 } from "path";
|
|
1118
|
+
function readJson2(path) {
|
|
1119
|
+
try {
|
|
1120
|
+
return JSON.parse(readFileSync7(path, "utf-8"));
|
|
1121
|
+
} catch {
|
|
1122
|
+
return null;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
function nonEmptyDir(path) {
|
|
1126
|
+
try {
|
|
1127
|
+
return readdirSync6(path).some((f) => !f.startsWith("."));
|
|
1128
|
+
} catch {
|
|
1129
|
+
return false;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
function extrasOf(root) {
|
|
1133
|
+
const manifest = readJson2(join11(root, ".claude-plugin", "plugin.json")) ?? {};
|
|
1134
|
+
return [
|
|
1135
|
+
...existsSync10(join11(root, ".mcp.json")) || manifest.mcpServers ? ["MCP servers"] : [],
|
|
1136
|
+
...existsSync10(join11(root, "hooks", "hooks.json")) || manifest.hooks ? ["hooks"] : [],
|
|
1137
|
+
...nonEmptyDir(join11(root, "agents")) || manifest.agents ? ["agents"] : [],
|
|
1138
|
+
...nonEmptyDir(join11(root, "commands")) || manifest.commands ? ["commands"] : []
|
|
1139
|
+
];
|
|
1140
|
+
}
|
|
1141
|
+
function claudePlugins(machine) {
|
|
1142
|
+
const installed = readJson2(join11(claudeDir(), "plugins", "installed_plugins.json"))?.plugins ?? {};
|
|
1143
|
+
const byOrigin = /* @__PURE__ */ new Map();
|
|
1144
|
+
for (const m of machine) if (m.kind === "plugin") byOrigin.set(m.origin, [...byOrigin.get(m.origin) ?? [], m.path]);
|
|
1145
|
+
return [...byOrigin].map(([origin, skills]) => {
|
|
1146
|
+
const synced = origin.endsWith("@synced");
|
|
1147
|
+
const id = origin;
|
|
1148
|
+
const entry = installed[origin]?.[0];
|
|
1149
|
+
return {
|
|
1150
|
+
id,
|
|
1151
|
+
scope: synced ? null : entry?.scope ?? "user",
|
|
1152
|
+
...entry?.projectPath ? { projectPath: entry.projectPath } : {},
|
|
1153
|
+
synced,
|
|
1154
|
+
skills,
|
|
1155
|
+
extras: extrasOf(dirname3(dirname3(skills[0])))
|
|
1156
|
+
};
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
function claudeBinary() {
|
|
1160
|
+
const candidates = ["claude", join11(userHome(), ".local", "bin", "claude"), join11(userHome(), ".claude", "local", "claude")];
|
|
1161
|
+
return candidates.find((bin) => {
|
|
1162
|
+
try {
|
|
1163
|
+
return execFileSync3(bin, ["--version"], { stdio: ["ignore", "pipe", "ignore"], timeout: 3e4, encoding: "utf-8" }).includes("Claude Code");
|
|
1164
|
+
} catch {
|
|
1165
|
+
return false;
|
|
1166
|
+
}
|
|
1167
|
+
}) ?? null;
|
|
1168
|
+
}
|
|
1169
|
+
function claude(args, cwd) {
|
|
1170
|
+
const bin = claudeBinary();
|
|
1171
|
+
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` };
|
|
1172
|
+
try {
|
|
1173
|
+
execFileSync3(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 12e4, encoding: "utf-8" });
|
|
1174
|
+
return { ok: true };
|
|
1175
|
+
} catch (err) {
|
|
1176
|
+
const e = err;
|
|
1177
|
+
return { ok: false, reason: `${(e.stderr || e.stdout || e.message || "failed").trim().split("\n").slice(-1)[0]} (ran ${bin})` };
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
function turnOffSynced(id) {
|
|
1181
|
+
const file = join11(claudeDir(), "settings.json");
|
|
1182
|
+
const settings = existsSync10(file) ? readJson2(file) : {};
|
|
1183
|
+
if (!settings) return { ok: false, reason: `can't read ${file}` };
|
|
1184
|
+
const enabled = settings.enabledPlugins ?? {};
|
|
1185
|
+
mkdirSync6(claudeDir(), { recursive: true });
|
|
1186
|
+
writeFileSync6(file, JSON.stringify({ ...settings, enabledPlugins: { ...enabled, [id]: false } }, null, 2) + "\n");
|
|
1187
|
+
return { ok: true };
|
|
1188
|
+
}
|
|
1189
|
+
function removedFile() {
|
|
1190
|
+
return join11(skilllibHome(), "plugin-backup.json");
|
|
1191
|
+
}
|
|
1192
|
+
function removePlugin(plugin, how) {
|
|
1193
|
+
if (plugin.scope && plugin.scope !== "user" && !plugin.projectPath) {
|
|
1194
|
+
return { ok: false, message: `${plugin.id}: installed for one project; run \`claude plugin ${how === "delete" ? "uninstall" : "disable"} ${plugin.id} --scope ${plugin.scope}\` there` };
|
|
1195
|
+
}
|
|
1196
|
+
if (!plugin.synced && !claudeBinary()) {
|
|
1197
|
+
return { ok: false, message: `${plugin.id}: nothing changed; Claude Code's CLI didn't run (tried \`claude\` on your PATH and ~/.local/bin/claude)` };
|
|
1198
|
+
}
|
|
1199
|
+
const imported = plugin.skills.filter((dir) => latestVersion(basename4(dir)) === null).map((dir) => importSkill(dir).name);
|
|
1200
|
+
const scope = plugin.scope ? ["--scope", plugin.scope] : [];
|
|
1201
|
+
const r = plugin.synced ? turnOffSynced(plugin.id) : claude(["plugin", how === "delete" ? "uninstall" : "disable", plugin.id, ...scope], plugin.projectPath);
|
|
1202
|
+
const saved = imported.length ? `; ${imported.join(", ")} copied into Your skills` : "";
|
|
1203
|
+
if (!r.ok) return { ok: false, message: `${plugin.id}: ${r.reason}${saved}` };
|
|
1204
|
+
if (how === "delete" && !plugin.synced) recordRemovedPlugin(plugin.id, plugin.scope ?? "user");
|
|
1205
|
+
return { ok: true, message: `${plugin.id} ${how === "delete" && !plugin.synced ? "removed" : "turned off"}${saved}` };
|
|
1206
|
+
}
|
|
1207
|
+
function recordRemovedPlugin(id, scope) {
|
|
1208
|
+
const list2 = readJson2(removedFile()) ?? [];
|
|
1209
|
+
mkdirSync6(skilllibHome(), { recursive: true });
|
|
1210
|
+
writeFileSync6(removedFile(), JSON.stringify([...list2, { id, scope, at: (/* @__PURE__ */ new Date()).toISOString() }], null, 2) + "\n");
|
|
1211
|
+
}
|
|
1212
|
+
function pluginBackups() {
|
|
1213
|
+
return (readJson2(removedFile()) ?? []).map((r) => ({
|
|
1214
|
+
name: r.id,
|
|
1215
|
+
kind: "plugin",
|
|
1216
|
+
path: r.scope,
|
|
1217
|
+
movedAt: r.at.slice(0, 16).replace("T", " "),
|
|
1218
|
+
from: r.id
|
|
1219
|
+
}));
|
|
1220
|
+
}
|
|
1221
|
+
function restorePlugin(backup) {
|
|
1222
|
+
const r = claude(["plugin", "install", backup.from, "--scope", backup.path]);
|
|
1223
|
+
if (!r.ok) return r;
|
|
1224
|
+
writeFileSync6(removedFile(), JSON.stringify((readJson2(removedFile()) ?? []).filter((x) => x.id !== backup.from), null, 2) + "\n");
|
|
1225
|
+
return { ok: true, to: `Claude Code plugins (${backup.path})` };
|
|
1226
|
+
}
|
|
1227
|
+
function cursorPluginSkills() {
|
|
1228
|
+
const cache = join11(userHome(), ".cursor", "plugins", "cache");
|
|
1229
|
+
const dirs = (p) => existsSync10(p) ? readdirSync6(p).filter((d) => !d.startsWith(".") && statSync3(join11(p, d)).isDirectory()) : [];
|
|
1230
|
+
return dirs(cache).flatMap(
|
|
1231
|
+
(marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
|
|
1232
|
+
const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
|
|
1233
|
+
const newest = versions.sort((a, b) => statSync3(b).mtimeMs - statSync3(a).mtimeMs)[0];
|
|
1234
|
+
return newest ? skillDirsIn(join11(newest, "skills")).map((path) => ({ name: basename4(path), plugin, path })) : [];
|
|
1235
|
+
})
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// src/tidy.ts
|
|
1240
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync7, readdirSync as readdirSync7, unlinkSync as unlinkSync2 } from "fs";
|
|
1241
|
+
import { dirname as dirname4, join as join12, sep as sep4 } from "path";
|
|
1242
|
+
function entriesOf(dirs, name) {
|
|
1243
|
+
return dirs.flatMap((dir) => {
|
|
1244
|
+
const path = join12(dir, name);
|
|
1245
|
+
if (!entryExists(path)) return [];
|
|
1246
|
+
const link2 = isLink(path);
|
|
1247
|
+
const real = realpathOrNull2(path);
|
|
1248
|
+
const skill = existsSync11(join12(path, "SKILL.md"));
|
|
1249
|
+
if (!link2 && !skill) return [];
|
|
1250
|
+
return [{ dir, path, link: link2, real, hash: null }];
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
function hashReals(reals) {
|
|
1254
|
+
if (reals.length > 1) for (const r of reals) r.hash = treeHash(r.path);
|
|
1255
|
+
}
|
|
1256
|
+
function namesIn(dirs) {
|
|
1257
|
+
const names = /* @__PURE__ */ new Set();
|
|
1258
|
+
for (const dir of dirs) {
|
|
1259
|
+
if (!existsSync11(dir)) continue;
|
|
1260
|
+
for (const name of readdirSync7(dir)) if (!name.startsWith(".") && name !== "synced") names.add(name);
|
|
1261
|
+
}
|
|
1262
|
+
return [...names].sort();
|
|
1263
|
+
}
|
|
1264
|
+
function runsFrom(id, dirsWithCopy, readsOf) {
|
|
1265
|
+
const reads = readsOf(id).filter((d) => dirsWithCopy.includes(d));
|
|
1266
|
+
return reads.find((d) => d.endsWith(join12(".claude", "skills"))) ?? reads[0];
|
|
1267
|
+
}
|
|
1268
|
+
function conflictOf(name, root, reals, enabled, readsOf) {
|
|
1269
|
+
const dirs = reals.map((r) => r.dir);
|
|
1270
|
+
return {
|
|
1271
|
+
name,
|
|
1272
|
+
root,
|
|
1273
|
+
copies: reals.map((r) => ({ dir: r.dir, path: r.path, runs: enabled.filter((id) => runsFrom(id, dirs, readsOf) === r.dir) }))
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {}) {
|
|
1277
|
+
const report = { plans: [], conflicts: [], skipped: [] };
|
|
1278
|
+
const { skills } = readManifest(root);
|
|
1279
|
+
const abs = (dir) => join12(root, dir);
|
|
1280
|
+
const rel = (dir) => relativeTo(root, dir);
|
|
1281
|
+
const readsOf = (id) => HARNESSES.find((h) => h.id === id).projectDirs.map(abs);
|
|
1282
|
+
let git2;
|
|
1283
|
+
const skillsLock = projectSkillsLock(root);
|
|
1284
|
+
for (const name of namesIn(ALL_PROJECT_DIRS.map(abs))) {
|
|
1285
|
+
const entries = entriesOf(ALL_PROJECT_DIRS.map(abs), name);
|
|
1286
|
+
const external = entries.find((e) => e.link && e.real && !(e.real + sep4).startsWith(realpathOrNull2(root) + sep4));
|
|
1287
|
+
if (external) {
|
|
1288
|
+
report.skipped.push({ name, reason: `${rel(external.path)} links outside the repo` });
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
const reals = entries.filter((e) => !e.link);
|
|
1292
|
+
if (reals.length === 0) continue;
|
|
1293
|
+
hashReals(reals);
|
|
1294
|
+
const dep = skills[name];
|
|
1295
|
+
const chosen = keep[name];
|
|
1296
|
+
let primary;
|
|
1297
|
+
if (dep) {
|
|
1298
|
+
primary = reals.find((r) => rel(r.dir) === (dep.dir ?? PROJECT_SKILLS_DIR));
|
|
1299
|
+
if (!primary) continue;
|
|
1300
|
+
} else if (chosen) {
|
|
1301
|
+
primary = reals.find((r) => r.dir === chosen);
|
|
1302
|
+
} else {
|
|
1303
|
+
if (git2 === void 0) git2 = gitInfo(root);
|
|
1304
|
+
const planned = installDirs(enabled).map(abs);
|
|
1305
|
+
const fromSkillsSh = name in skillsLock;
|
|
1306
|
+
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);
|
|
1307
|
+
primary = [...reals].sort((a, b) => rank(a) - rank(b))[0];
|
|
1308
|
+
}
|
|
1309
|
+
if (!primary) continue;
|
|
1310
|
+
if (chosen !== primary.dir && reals.some((r) => r.hash !== primary.hash)) {
|
|
1311
|
+
report.conflicts.push({ ...conflictOf(name, root, reals, enabled, readsOf), ...dep ? { managed: primary.dir } : {} });
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
const wanted = new Set([...installDirs(enabled, rel(primary.dir)), ...dep?.links ?? []].map(abs));
|
|
1315
|
+
const steps = [];
|
|
1316
|
+
for (const e of entries) {
|
|
1317
|
+
if (e === primary) continue;
|
|
1318
|
+
const needed = wanted.has(e.dir);
|
|
1319
|
+
if (e.link) {
|
|
1320
|
+
if (e.real === primary.real) {
|
|
1321
|
+
if (!needed) steps.push({ kind: "unlink", path: e.path });
|
|
1322
|
+
} else steps.push(needed ? { kind: "link", path: e.path, target: primary.path } : { kind: "unlink", path: e.path });
|
|
1323
|
+
} else {
|
|
1324
|
+
steps.push(needed ? { kind: "replace", path: e.path, target: primary.path } : { kind: "remove", path: e.path });
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
if (steps.length) report.plans.push({ name, root, primary: primary.path, steps });
|
|
1328
|
+
}
|
|
1329
|
+
return report;
|
|
1330
|
+
}
|
|
1331
|
+
function globalReadsOf(id) {
|
|
1332
|
+
return HARNESSES.find((h) => h.id === id).globalDirs();
|
|
1333
|
+
}
|
|
1334
|
+
function isSkillsDirPlugin(path) {
|
|
1335
|
+
return existsSync11(join12(path, ".claude-plugin", "plugin.json"));
|
|
1336
|
+
}
|
|
1337
|
+
function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
|
|
1338
|
+
const report = { plans: [], conflicts: [], skipped: [] };
|
|
1339
|
+
const dirs = globalSkillDirs();
|
|
1340
|
+
const agents = join12(userHome(), ".agents", "skills");
|
|
1341
|
+
for (const name of namesIn(dirs)) {
|
|
1342
|
+
const entries = entriesOf(dirs, name);
|
|
1343
|
+
if (entries.some((e) => isSkillsDirPlugin(e.path))) {
|
|
1344
|
+
report.skipped.push({ name, reason: "a Claude Code plugin kept in a skills folder" });
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
const reals = entries.filter((e) => !e.link);
|
|
1348
|
+
if (reals.length < 2) continue;
|
|
1349
|
+
hashReals(reals);
|
|
1350
|
+
const primary = reals.find((r) => r.dir === keep[name]) ?? reals.find((r) => r.dir === agents) ?? reals[0];
|
|
1351
|
+
if (!keep[name] && reals.some((r) => r.hash !== primary.hash)) {
|
|
1352
|
+
report.conflicts.push(conflictOf(name, null, reals, enabled, globalReadsOf));
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
const steps = reals.filter((r) => r !== primary).map((r) => ({ kind: "replace", path: r.path, target: primary.path }));
|
|
1356
|
+
report.plans.push({ name, root: null, primary: primary.path, steps });
|
|
1357
|
+
}
|
|
1358
|
+
return report;
|
|
1359
|
+
}
|
|
1360
|
+
function gitVisibleSteps(plan, git2 = plan.root ? gitInfo(plan.root) : null) {
|
|
1361
|
+
if (!plan.root || !git2) return [];
|
|
1362
|
+
return plan.steps.filter((s) => {
|
|
1363
|
+
const state = git2.of(relativeTo(plan.root, s.path));
|
|
1364
|
+
return state === "committed" || state === "changed";
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
|
|
1368
|
+
const held = git2 === "keep" ? gitVisibleSteps(plan, info2) : [];
|
|
1369
|
+
const applied = [];
|
|
1370
|
+
for (const step of plan.steps) {
|
|
1371
|
+
if (held.includes(step) || !entryExists(step.path) || "target" in step && !entryExists(step.target)) continue;
|
|
1372
|
+
if (step.kind === "link" || step.kind === "unlink") {
|
|
1373
|
+
if (isLink(step.path)) unlinkSync2(step.path);
|
|
1374
|
+
} else stash(step.path, "tidy-backup");
|
|
1375
|
+
if (step.kind === "link" || step.kind === "replace") {
|
|
1376
|
+
mkdirSync7(dirname4(step.path), { recursive: true });
|
|
1377
|
+
linkDir(step.target, step.path);
|
|
1378
|
+
}
|
|
1379
|
+
applied.push(step);
|
|
1380
|
+
}
|
|
1381
|
+
if (plan.root) {
|
|
1382
|
+
const manifest = readManifest(plan.root);
|
|
1383
|
+
const dep = manifest.skills[plan.name];
|
|
1384
|
+
const added = applied.filter((s) => s.kind === "link" || s.kind === "replace").map((s) => relativeTo(plan.root, dirname4(s.path)));
|
|
1385
|
+
if (dep && added.length) {
|
|
1386
|
+
manifest.skills[plan.name] = { ...dep, links: [.../* @__PURE__ */ new Set([...dep.links ?? [], ...added])] };
|
|
1387
|
+
writeManifest(plan.root, manifest);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
return { name: plan.name, root: plan.root, applied, held };
|
|
1391
|
+
}
|
|
1392
|
+
function describeStep(step, root) {
|
|
1393
|
+
const shown = root ? relativeTo(root, step.path) : step.path.replace(userHome(), "~");
|
|
1394
|
+
switch (step.kind) {
|
|
1395
|
+
case "link":
|
|
1396
|
+
return `link ${shown}`;
|
|
1397
|
+
case "unlink":
|
|
1398
|
+
return `remove extra link ${shown}`;
|
|
1399
|
+
case "replace":
|
|
1400
|
+
return `${shown}: duplicate copy \u2192 link`;
|
|
1401
|
+
case "remove":
|
|
1402
|
+
return `remove duplicate copy ${shown}`;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
function copyLabel(conflict, dir) {
|
|
1406
|
+
return conflict.root ? relativeTo(conflict.root, dir) : dir.replace(userHome(), "~");
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// src/health.ts
|
|
1410
|
+
function runFix(choice) {
|
|
1411
|
+
try {
|
|
1412
|
+
return { ok: true, message: choice.run() };
|
|
1413
|
+
} catch (err) {
|
|
1414
|
+
return { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
function orThrow(r) {
|
|
1418
|
+
if (!r.ok) throw new Error(r.message);
|
|
1419
|
+
return r.message;
|
|
1420
|
+
}
|
|
1421
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1422
|
+
var agentNames = (ids) => ids.map((id) => HARNESSES.find((h) => h.id === id).name).join(", ");
|
|
1423
|
+
function conflictIssue(c) {
|
|
1424
|
+
const where = c.root ? basename5(c.root) : "your global folders";
|
|
1425
|
+
const winners = c.managed ? c.copies.filter((x) => x.dir === c.managed) : c.copies;
|
|
1426
|
+
return {
|
|
1427
|
+
id: `conflict:${c.root ?? "global"}:${c.name}`,
|
|
1428
|
+
severity: "problem",
|
|
1429
|
+
title: `${c.name} in ${where}: the copies differ`,
|
|
1430
|
+
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.`,
|
|
1431
|
+
choices: winners.map((x) => ({
|
|
1432
|
+
label: `Keep the ${copyLabel(c, x.dir)} copy`,
|
|
1433
|
+
hint: x.dir === c.managed ? "skilllib's version" : "the others become links to it",
|
|
1434
|
+
run: () => {
|
|
1435
|
+
const keep = { [c.name]: x.dir };
|
|
1436
|
+
const plan = (c.root ? planProjectTidy(c.root, { keep }) : planGlobalTidy({ keep })).plans.find((p) => p.name === c.name);
|
|
1437
|
+
if (plan) applyTidy(plan, { git: "go" });
|
|
1438
|
+
return `${c.name}: kept the ${copyLabel(c, x.dir)} copy`;
|
|
1439
|
+
}
|
|
1440
|
+
}))
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
function applyAll(plans, git2) {
|
|
1444
|
+
const results = plans.map((p) => applyTidy(p, { git: git2 }));
|
|
1445
|
+
return { skills: results.filter((r) => r.applied.length).length, held: results.reduce((n, r) => n + r.held.length, 0) };
|
|
1446
|
+
}
|
|
1035
1447
|
function findIssues(projects2, machine, libraryNames, statusOf = projectStatus) {
|
|
1036
1448
|
const issues = [];
|
|
1449
|
+
const statuses = new Map(projects2.map((root) => [root, statusOf(root)]));
|
|
1037
1450
|
for (const skill of machine.filter((m) => m.broken)) {
|
|
1038
1451
|
issues.push({
|
|
1039
1452
|
id: `broken:${skill.name}`,
|
|
@@ -1051,36 +1464,85 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
|
|
|
1051
1464
|
}
|
|
1052
1465
|
const loaded = machine.filter((m) => !m.broken);
|
|
1053
1466
|
const byName = /* @__PURE__ */ new Map();
|
|
1054
|
-
for (const m of loaded) byName.set(m.name, [...byName.get(m.name) ?? [], m]);
|
|
1467
|
+
for (const m of loaded) if (m.kind !== "plugin") byName.set(m.name, [...byName.get(m.name) ?? [], m]);
|
|
1055
1468
|
for (const [name, copies] of byName) {
|
|
1469
|
+
if (copies.every((c) => c.movable) || !copies.some((c) => c.movable)) continue;
|
|
1056
1470
|
const harnesses = HARNESSES.filter((h) => copies.filter((c) => c.harnesses.includes(h.id)).length > 1).map((h) => h.name);
|
|
1057
1471
|
if (harnesses.length === 0) continue;
|
|
1058
|
-
const
|
|
1472
|
+
const vendor = copies.filter((c) => !c.movable);
|
|
1059
1473
|
issues.push({
|
|
1060
1474
|
id: `dup:${name}`,
|
|
1061
1475
|
severity: "problem",
|
|
1062
1476
|
title: `${name}: loaded twice in ${harnesses.join(" and ")}`,
|
|
1063
|
-
detail: copies.map((c) => `${c.kind} (${c.origin})`).join(" and ")
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1477
|
+
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)" : ""}.`
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
const loadedYours = /* @__PURE__ */ new Set([...loaded.filter((m) => m.movable).map((m) => m.name), ...[...statuses.values()].flat().map((s) => s.name)]);
|
|
1481
|
+
const yours = /* @__PURE__ */ new Set([...libraryNames, ...loadedYours]);
|
|
1482
|
+
for (const plugin of claudePlugins(machine)) {
|
|
1483
|
+
const dupes = plugin.skills.map((p) => basename5(p)).filter((n) => yours.has(n));
|
|
1484
|
+
if (!dupes.length) continue;
|
|
1485
|
+
const others = plugin.skills.length - dupes.length;
|
|
1486
|
+
const pluginName = plugin.id.split("@")[0];
|
|
1487
|
+
const remove2 = {
|
|
1488
|
+
label: `Remove the plugin ${plugin.id}`,
|
|
1489
|
+
hint: plugin.extras.length ? `also removes its ${plugin.extras.join(", ")}` : "restorable from Health",
|
|
1490
|
+
run: () => orThrow(removePlugin(plugin, "delete"))
|
|
1491
|
+
};
|
|
1492
|
+
const off = { label: `Turn the plugin ${plugin.id} off`, hint: "stays installed", run: () => orThrow(removePlugin(plugin, "off")) };
|
|
1493
|
+
const notLoaded = dupes.filter((n) => !loadedYours.has(n));
|
|
1494
|
+
issues.push({
|
|
1495
|
+
id: `plugin:${plugin.id}`,
|
|
1496
|
+
severity: notLoaded.length === dupes.length ? "suggestion" : "problem",
|
|
1497
|
+
title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}`,
|
|
1498
|
+
detail: [
|
|
1499
|
+
notLoaded.length < dupes.length ? `Claude Code loads both (the plugin's as /${pluginName}:${dupes[0]}). Your skill wins.` : "",
|
|
1500
|
+
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"}.` : "",
|
|
1501
|
+
others ? `The plugin's other ${plural(others, "skill")} are copied into Your skills first, so nothing is lost.` : "",
|
|
1502
|
+
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"}.` : "",
|
|
1503
|
+
plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : ""
|
|
1504
|
+
].filter(Boolean).join(" "),
|
|
1505
|
+
choices: plugin.synced ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off]
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
if (enabledHarnesses().includes("cursor")) {
|
|
1509
|
+
const byPlugin = /* @__PURE__ */ new Map();
|
|
1510
|
+
for (const s of cursorPluginSkills().filter((x) => loadedYours.has(x.name))) byPlugin.set(s.plugin, [...byPlugin.get(s.plugin) ?? [], s.name]);
|
|
1511
|
+
for (const [plugin, names] of byPlugin) {
|
|
1512
|
+
issues.push({
|
|
1513
|
+
id: `cursor-plugin:${plugin}`,
|
|
1514
|
+
severity: "suggestion",
|
|
1515
|
+
title: `${names.join(", ")}: also in the Cursor plugin ${plugin}`,
|
|
1516
|
+
detail: `Cursor lists both copies when that plugin is on. Your skill wins: turn the plugin off in Cursor (Settings \u2192 Plugins).`
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
const global = planGlobalTidy();
|
|
1521
|
+
if (global.plans.length) {
|
|
1522
|
+
issues.push({
|
|
1523
|
+
id: "tidy:global",
|
|
1524
|
+
severity: "suggestion",
|
|
1525
|
+
title: `${plural(global.plans.length, "global skill")} with duplicate copies`,
|
|
1526
|
+
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.`,
|
|
1527
|
+
fix: {
|
|
1528
|
+
label: "Keep one copy of each",
|
|
1529
|
+
run: () => `${plural(applyAll(global.plans, "go").skills, "global skill")} now have one copy`
|
|
1530
|
+
}
|
|
1072
1531
|
});
|
|
1073
1532
|
}
|
|
1074
|
-
|
|
1533
|
+
issues.push(...global.conflicts.map(conflictIssue));
|
|
1534
|
+
const kept = keptGlobal();
|
|
1535
|
+
const alsoGlobal = /* @__PURE__ */ new Map();
|
|
1536
|
+
const globalNames = new Set(loaded.filter((m) => m.kind !== "plugin").map((m) => m.name));
|
|
1075
1537
|
for (const root of projects2) {
|
|
1076
|
-
const where =
|
|
1077
|
-
const status2 =
|
|
1538
|
+
const where = basename5(root);
|
|
1539
|
+
const status2 = statuses.get(root);
|
|
1078
1540
|
const behind = status2.filter((s) => s.state === "update available" || s.state === "folder missing");
|
|
1079
1541
|
if (behind.length > 0) {
|
|
1080
1542
|
issues.push({
|
|
1081
1543
|
id: `sync:${root}`,
|
|
1082
1544
|
severity: "problem",
|
|
1083
|
-
title: `${where}: ${behind.length
|
|
1545
|
+
title: `${where}: ${plural(behind.length, "skill")} out of date`,
|
|
1084
1546
|
detail: behind.map((s) => s.name).join(", "),
|
|
1085
1547
|
fix: {
|
|
1086
1548
|
label: "Sync the project",
|
|
@@ -1091,49 +1553,123 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
|
|
|
1091
1553
|
}
|
|
1092
1554
|
});
|
|
1093
1555
|
}
|
|
1556
|
+
const tidy2 = planProjectTidy(root);
|
|
1557
|
+
if (tidy2.plans.length) {
|
|
1558
|
+
const git2 = gitInfo(root);
|
|
1559
|
+
const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p, git2).map((s) => describeStep(s, root)));
|
|
1560
|
+
const run = (mode) => () => {
|
|
1561
|
+
const r = applyAll(tidy2.plans, mode);
|
|
1562
|
+
return `${where}: tidied ${plural(r.skills, "skill")}${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`;
|
|
1563
|
+
};
|
|
1564
|
+
issues.push({
|
|
1565
|
+
id: `tidy:${root}`,
|
|
1566
|
+
severity: "suggestion",
|
|
1567
|
+
title: `${where}: ${plural(tidy2.plans.length, "skill")} with extra copies or links`,
|
|
1568
|
+
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."),
|
|
1569
|
+
...visible2.length ? {
|
|
1570
|
+
choices: [
|
|
1571
|
+
{ label: "Tidy, but keep git as it is", hint: `skips the ${plural(visible2.length, "change")} git would see`, run: run("keep") },
|
|
1572
|
+
{ label: "Tidy everything", hint: `${plural(visible2.length, "change")} will show in git status`, run: run("go") }
|
|
1573
|
+
]
|
|
1574
|
+
} : { fix: { label: "Tidy", run: run("go") } }
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
issues.push(...tidy2.conflicts.map(conflictIssue));
|
|
1578
|
+
const partial = status2.filter((s) => s.state !== "folder missing" && s.visibility.some((v) => v.paths === 0));
|
|
1579
|
+
if (partial.length) {
|
|
1580
|
+
const missing = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
|
|
1581
|
+
const tracked = readConfig().agentsDirOk?.includes(root) ? [] : [...new Set(missing.map((id) => harness(id).projectDirs[0]))].filter((dir) => isGitTracked(root, dir));
|
|
1582
|
+
const run = (allowTracked) => () => {
|
|
1583
|
+
if (allowTracked) allowTrackedLinks(root);
|
|
1584
|
+
const r = linkAll(root, { allowTracked });
|
|
1585
|
+
return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing)}${r.blocked.length ? `; skipped ${r.blocked.join(", ")} (git tracks it)` : ""}`;
|
|
1586
|
+
};
|
|
1587
|
+
issues.push({
|
|
1588
|
+
id: `usable:${root}`,
|
|
1589
|
+
severity: "suggestion",
|
|
1590
|
+
title: `${where}: ${plural(partial.length, "skill")} not usable by ${agentNames(missing)}`,
|
|
1591
|
+
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.` : ""}`,
|
|
1592
|
+
...tracked.length ? {
|
|
1593
|
+
choices: [
|
|
1594
|
+
{ label: "Add links, but not where git tracks the folder", hint: `skips ${tracked.join(", ")}`, run: run(false) },
|
|
1595
|
+
{ label: "Add links everywhere", hint: `remembered for ${where}; git will see them`, run: run(true) }
|
|
1596
|
+
]
|
|
1597
|
+
} : { fix: { label: "Add the links", run: run(false) } }
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
const differing = new Set(tidy2.conflicts.map((c) => c.name));
|
|
1094
1601
|
for (const s of status2) {
|
|
1602
|
+
if (isLink(s.path) || differing.has(s.name)) continue;
|
|
1095
1603
|
if (s.state === "local only") {
|
|
1096
1604
|
issues.push({
|
|
1097
1605
|
id: `local:${root}:${s.name}`,
|
|
1098
1606
|
severity: "suggestion",
|
|
1099
1607
|
title: `${s.name}: only exists in ${where}`,
|
|
1100
|
-
detail: "Import it into the library to reuse it in other projects and keep it backed up.",
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1608
|
+
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.",
|
|
1609
|
+
choices: [
|
|
1610
|
+
{
|
|
1611
|
+
label: "Import into the library",
|
|
1612
|
+
hint: `${where} then tracks it`,
|
|
1613
|
+
run: () => {
|
|
1614
|
+
importSkill(s.path);
|
|
1615
|
+
addSkill(root, s.name);
|
|
1616
|
+
return `${s.name} imported and tracked in ${where}`;
|
|
1617
|
+
}
|
|
1107
1618
|
}
|
|
1108
|
-
|
|
1619
|
+
]
|
|
1109
1620
|
});
|
|
1110
1621
|
} else if (s.state === "untracked copy of library skill") {
|
|
1111
1622
|
issues.push({
|
|
1112
1623
|
id: `adopt:${root}:${s.name}`,
|
|
1113
1624
|
severity: "suggestion",
|
|
1114
1625
|
title: `${s.name} in ${where} isn't tracked`,
|
|
1115
|
-
detail: "It's identical to the library copy; track it so future library changes reach it.",
|
|
1116
|
-
|
|
1626
|
+
detail: "It's identical to the library copy; track it (in skilllib.json) so future library changes reach it.",
|
|
1627
|
+
choices: [{ label: "Track it", hint: `adds it to ${where}'s skilllib.json`, run: () => `${addSkill(root, s.name).name} tracked in ${where}` }]
|
|
1117
1628
|
});
|
|
1118
1629
|
}
|
|
1119
|
-
if (globalNames.has(s.name) && s.managed)
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1630
|
+
if (globalNames.has(s.name) && s.managed) alsoGlobal.set(s.name, [...alsoGlobal.get(s.name) ?? [], root]);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
for (const [name, roots] of alsoGlobal) {
|
|
1634
|
+
const wheres = roots.map((r) => basename5(r)).join(", ");
|
|
1635
|
+
if (kept.has(name)) {
|
|
1636
|
+
issues.push({
|
|
1637
|
+
id: `twice:${name}`,
|
|
1638
|
+
severity: "suggestion",
|
|
1639
|
+
title: `${name}: in ${wheres}, and you keep it global`,
|
|
1640
|
+
detail: "The global copy already loads there, so the project copies are extra. Claude Code runs the global one anyway.",
|
|
1641
|
+
choices: [
|
|
1642
|
+
{
|
|
1643
|
+
label: `Remove the copies in ${wheres}`,
|
|
1644
|
+
hint: "the global copy stays",
|
|
1127
1645
|
run: () => {
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1130
|
-
const r = unloadGlobal(g.path, g.links);
|
|
1131
|
-
return r.ok ? `${s.name} no longer loads globally` : `${s.name}: ${r.reason}`;
|
|
1646
|
+
const removed = roots.filter((root) => removeSkill(root, name).action === "removed").map((r) => basename5(r));
|
|
1647
|
+
return `${name}: removed from ${removed.join(", ") || "no project"}; the global copy stays`;
|
|
1132
1648
|
}
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
}
|
|
1649
|
+
}
|
|
1650
|
+
]
|
|
1651
|
+
});
|
|
1652
|
+
continue;
|
|
1136
1653
|
}
|
|
1654
|
+
const movable = machine.find((m) => m.name === name && m.movable);
|
|
1655
|
+
issues.push({
|
|
1656
|
+
id: `twice:${name}`,
|
|
1657
|
+
severity: "suggestion",
|
|
1658
|
+
title: `${name}: in ${wheres} and also loaded globally`,
|
|
1659
|
+
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).",
|
|
1660
|
+
choices: movable ? [
|
|
1661
|
+
{
|
|
1662
|
+
label: "Stop loading it globally",
|
|
1663
|
+
hint: "restorable from Health",
|
|
1664
|
+
run: () => {
|
|
1665
|
+
if (!libraryNames.has(name)) importSkill(movable.path);
|
|
1666
|
+
const r = unloadGlobal(movable.path, movable.links);
|
|
1667
|
+
if (!r.ok) throw new Error(`${name}: ${r.reason}`);
|
|
1668
|
+
return `${name} no longer loads globally`;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
] : void 0
|
|
1672
|
+
});
|
|
1137
1673
|
}
|
|
1138
1674
|
return issues;
|
|
1139
1675
|
}
|
|
@@ -1143,20 +1679,24 @@ var USAGE_DAYS = 30;
|
|
|
1143
1679
|
function parseArgs(argv) {
|
|
1144
1680
|
const daysIdx = argv.indexOf("--days");
|
|
1145
1681
|
const days = daysIdx >= 0 ? Number(argv[daysIdx + 1]) : USAGE_DAYS;
|
|
1682
|
+
const keepIdx = argv.indexOf("--keep");
|
|
1146
1683
|
return {
|
|
1147
|
-
positional: argv.filter((a, i) => !a.startsWith("--") && argv[i - 1] !== "--days"),
|
|
1684
|
+
positional: argv.filter((a, i) => !a.startsWith("--") && argv[i - 1] !== "--days" && argv[i - 1] !== "--keep"),
|
|
1148
1685
|
force: argv.includes("--force"),
|
|
1149
1686
|
all: argv.includes("--all"),
|
|
1150
1687
|
global: argv.includes("--global"),
|
|
1151
1688
|
fix: argv.includes("--fix"),
|
|
1152
1689
|
allowTracked: argv.includes("--allow-tracked"),
|
|
1153
|
-
days: Number.isFinite(days) && days > 0 ? days : USAGE_DAYS
|
|
1690
|
+
days: Number.isFinite(days) && days > 0 ? days : USAGE_DAYS,
|
|
1691
|
+
dryRun: argv.includes("--dry-run"),
|
|
1692
|
+
allowGit: argv.includes("--allow-git"),
|
|
1693
|
+
...keepIdx >= 0 && argv[keepIdx + 1] ? { keep: argv[keepIdx + 1] } : {}
|
|
1154
1694
|
};
|
|
1155
1695
|
}
|
|
1156
1696
|
function colorState(state) {
|
|
1157
1697
|
if (state === "ok") return green(state);
|
|
1158
1698
|
if (state === "folder missing" || state === "not in library") return red(state);
|
|
1159
|
-
if (state === "local only") return dim(state);
|
|
1699
|
+
if (state === "local only" || state === "from npx skills") return dim(state);
|
|
1160
1700
|
return yellow(state);
|
|
1161
1701
|
}
|
|
1162
1702
|
function printChanges(changes) {
|
|
@@ -1176,9 +1716,9 @@ function projectOfFactory(projects2) {
|
|
|
1176
1716
|
const sorted = [...projects2].sort((a, b) => b.length - a.length);
|
|
1177
1717
|
return (cwd) => {
|
|
1178
1718
|
if (!cwd) return null;
|
|
1179
|
-
const known = sorted.find((p) => cwd === p || cwd.startsWith(p +
|
|
1719
|
+
const known = sorted.find((p) => cwd === p || cwd.startsWith(p + sep5));
|
|
1180
1720
|
if (known) return known;
|
|
1181
|
-
const repo = cwd.split(`${
|
|
1721
|
+
const repo = cwd.split(`${sep5}.claude${sep5}worktrees${sep5}`)[0];
|
|
1182
1722
|
return isProjectCandidate(repo) ? repo : null;
|
|
1183
1723
|
};
|
|
1184
1724
|
}
|
|
@@ -1194,7 +1734,7 @@ function lastUsed(summary) {
|
|
|
1194
1734
|
async function status(args) {
|
|
1195
1735
|
const root = findProjectRoot();
|
|
1196
1736
|
const skills = projectStatus(root);
|
|
1197
|
-
info(`${
|
|
1737
|
+
info(`${basename6(root)} ${dim(tildify(root))}
|
|
1198
1738
|
`);
|
|
1199
1739
|
if (skills.length === 0) {
|
|
1200
1740
|
info("No skills in this project yet. Add one from your library with: skilllib add <name>");
|
|
@@ -1204,7 +1744,7 @@ async function status(args) {
|
|
|
1204
1744
|
table(
|
|
1205
1745
|
skills.map((s) => ({
|
|
1206
1746
|
Skill: s.name,
|
|
1207
|
-
Status: colorState(s.state),
|
|
1747
|
+
Status: s.source ? dim(`npx skills: ${s.source}`) : colorState(s.state),
|
|
1208
1748
|
[`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0),
|
|
1209
1749
|
"Last used": lastUsed(usage2.get(s.name))
|
|
1210
1750
|
}))
|
|
@@ -1230,7 +1770,7 @@ async function list(args) {
|
|
|
1230
1770
|
const installs = /* @__PURE__ */ new Map();
|
|
1231
1771
|
for (const project of visibleProjects()) {
|
|
1232
1772
|
for (const name of Object.keys(readManifest(project).skills)) {
|
|
1233
|
-
installs.set(name, [...installs.get(name) ?? [],
|
|
1773
|
+
installs.set(name, [...installs.get(name) ?? [], basename6(project)]);
|
|
1234
1774
|
}
|
|
1235
1775
|
}
|
|
1236
1776
|
const usage2 = await usageBySkill(args.days);
|
|
@@ -1272,7 +1812,7 @@ function projects() {
|
|
|
1272
1812
|
const managed = skills.filter((s) => s.managed);
|
|
1273
1813
|
const attention = skills.filter((s) => s.state !== "ok" && s.state !== "local only").length;
|
|
1274
1814
|
return {
|
|
1275
|
-
Project:
|
|
1815
|
+
Project: basename6(root),
|
|
1276
1816
|
Path: dim(tildify(root)),
|
|
1277
1817
|
Skills: managed.map((s) => s.name).join(", ") || dim("none"),
|
|
1278
1818
|
"Local only": String(skills.filter((s) => s.state === "local only").length),
|
|
@@ -1304,7 +1844,7 @@ function sync(args) {
|
|
|
1304
1844
|
for (const root of roots) {
|
|
1305
1845
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
1306
1846
|
const changes = syncProject(root, { force: args.force });
|
|
1307
|
-
if (args.all) info(`${
|
|
1847
|
+
if (args.all) info(`${basename6(root)} ${dim(tildify(root))}`);
|
|
1308
1848
|
if (changes.length === 0) success("Installed versions match skilllib.json");
|
|
1309
1849
|
else printChanges(changes);
|
|
1310
1850
|
}
|
|
@@ -1315,7 +1855,7 @@ function update(args) {
|
|
|
1315
1855
|
for (const root of roots) {
|
|
1316
1856
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
1317
1857
|
const changes = updateProject(root, args.positional.length ? args.positional : void 0, { force: args.force });
|
|
1318
|
-
if (args.all) info(`${
|
|
1858
|
+
if (args.all) info(`${basename6(root)} ${dim(tildify(root))}`);
|
|
1319
1859
|
if (changes.length === 0) success("Everything is on the latest version");
|
|
1320
1860
|
else printChanges(changes);
|
|
1321
1861
|
}
|
|
@@ -1323,7 +1863,7 @@ function update(args) {
|
|
|
1323
1863
|
function outdated(args) {
|
|
1324
1864
|
const roots = args.all ? visibleProjects() : [findProjectRoot()];
|
|
1325
1865
|
const rows = roots.flatMap(
|
|
1326
|
-
(root) => projectStatus(root).filter((s) => s.managed && s.latest !== null && s.version !== null && s.latest > s.version).map((s) => ({ Project:
|
|
1866
|
+
(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) }))
|
|
1327
1867
|
);
|
|
1328
1868
|
if (rows.length === 0) return success("Everything is on the latest version");
|
|
1329
1869
|
table(rows);
|
|
@@ -1349,10 +1889,10 @@ function importSkills(args) {
|
|
|
1349
1889
|
success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
|
|
1350
1890
|
const skillsDir = resolve4(dir, "..");
|
|
1351
1891
|
const root = resolve4(skillsDir, "..", "..");
|
|
1352
|
-
if (skillsDir ===
|
|
1892
|
+
if (skillsDir === join13(root, ".claude", "skills") && isProjectCandidate(root)) {
|
|
1353
1893
|
if (readManifest(root).skills[name]?.hash !== treeHash(dir)) {
|
|
1354
1894
|
const change = addSkill(root, name);
|
|
1355
|
-
if (change.action !== "skipped") info(dim(` ${
|
|
1895
|
+
if (change.action !== "skipped") info(dim(` ${basename6(root)} now depends on ${name} v${change.to}`));
|
|
1356
1896
|
}
|
|
1357
1897
|
rememberProjects([root]);
|
|
1358
1898
|
}
|
|
@@ -1367,13 +1907,13 @@ function scan(args) {
|
|
|
1367
1907
|
}
|
|
1368
1908
|
const found = discoverProjects();
|
|
1369
1909
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
1370
|
-
const skillNames = (root) => [...skillDirsIn(projectSkillsDir(root)), ...skillDirsIn(agentsSkillsDir(root))].map((d) =>
|
|
1910
|
+
const skillNames = (root) => [...skillDirsIn(projectSkillsDir(root)), ...skillDirsIn(agentsSkillsDir(root))].map((d) => basename6(d));
|
|
1371
1911
|
const withSkills = found.filter((root) => skillNames(root).length > 0);
|
|
1372
1912
|
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project(s), ${withSkills.length} with skills
|
|
1373
1913
|
`);
|
|
1374
1914
|
table(
|
|
1375
1915
|
withSkills.map((root) => ({
|
|
1376
|
-
Project:
|
|
1916
|
+
Project: basename6(root),
|
|
1377
1917
|
Path: dim(tildify(root)),
|
|
1378
1918
|
Skills: [...new Set(skillNames(root))].map((n) => library.has(n) ? n : yellow(n)).join(", ")
|
|
1379
1919
|
}))
|
|
@@ -1414,14 +1954,14 @@ function newSkill(args) {
|
|
|
1414
1954
|
error(`Can't create ${name}: ${r.reason}`);
|
|
1415
1955
|
process.exit(1);
|
|
1416
1956
|
}
|
|
1417
|
-
success(`Created ${tildify(
|
|
1957
|
+
success(`Created ${tildify(join13(r.dir, "SKILL.md"))}`);
|
|
1418
1958
|
}
|
|
1419
1959
|
function restore(args) {
|
|
1420
|
-
const backups = listBackups();
|
|
1960
|
+
const backups = [...listBackups(), ...pluginBackups()];
|
|
1421
1961
|
const name = args.positional[0];
|
|
1422
1962
|
if (!name) {
|
|
1423
1963
|
if (backups.length === 0) return info("Nothing to restore.");
|
|
1424
|
-
table(backups.map((b) => ({ Skill: b.name, From: b.kind === "trash" ? "library (deleted)" : "
|
|
1964
|
+
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 })));
|
|
1425
1965
|
info(dim("\n\u2192 skilllib restore <name>"));
|
|
1426
1966
|
return;
|
|
1427
1967
|
}
|
|
@@ -1430,7 +1970,7 @@ function restore(args) {
|
|
|
1430
1970
|
error(`No backup named ${name}`);
|
|
1431
1971
|
process.exit(1);
|
|
1432
1972
|
}
|
|
1433
|
-
const r = restoreBackup(backup);
|
|
1973
|
+
const r = backup.kind === "plugin" ? restorePlugin(backup) : restoreBackup(backup);
|
|
1434
1974
|
if (r.ok) success(`Restored ${name} to ${tildify(r.to)}`);
|
|
1435
1975
|
else error(`Can't restore ${name}: ${r.reason}`);
|
|
1436
1976
|
}
|
|
@@ -1441,9 +1981,13 @@ function doctor(args) {
|
|
|
1441
1981
|
for (const issue of issues) {
|
|
1442
1982
|
info(`${issue.severity === "problem" ? red("\u25CF") : yellow("\u25C6")} ${issue.title}`);
|
|
1443
1983
|
info(dim(` ${issue.detail}${issue.fix ? ` \u2192 ${issue.fix.label}` : ""}`));
|
|
1444
|
-
if (
|
|
1984
|
+
if (issue.choices) info(dim(` choose in skilllib \u2192 Health: ${issue.choices.map((c) => c.label).join(" / ")}`));
|
|
1985
|
+
if (args.fix && issue.fix) {
|
|
1986
|
+
const r = runFix(issue.fix);
|
|
1987
|
+
info(` ${r.ok ? green("\u2713") : red("\u2717")} ${r.message}`);
|
|
1988
|
+
}
|
|
1445
1989
|
}
|
|
1446
|
-
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
|
|
1990
|
+
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."));
|
|
1447
1991
|
}
|
|
1448
1992
|
async function usage(args) {
|
|
1449
1993
|
const summaries = summarize(await scanUsage(args.days), projectOfFactory(knownProjects()));
|
|
@@ -1452,13 +1996,13 @@ async function usage(args) {
|
|
|
1452
1996
|
return;
|
|
1453
1997
|
}
|
|
1454
1998
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
1455
|
-
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(
|
|
1999
|
+
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join13(claudeDir(), "projects"))})`)}
|
|
1456
2000
|
`);
|
|
1457
2001
|
table(
|
|
1458
2002
|
summaries.map((s) => ({
|
|
1459
2003
|
Skill: s.skill,
|
|
1460
2004
|
Uses: String(s.uses),
|
|
1461
|
-
Projects: [...s.projects].map((p) =>
|
|
2005
|
+
Projects: [...s.projects].map((p) => basename6(p)).join(", ") || dim("-"),
|
|
1462
2006
|
"Last used": lastUsed(s),
|
|
1463
2007
|
Library: library.has(s.skill) ? green("yes") : dim("no")
|
|
1464
2008
|
}))
|
|
@@ -1471,7 +2015,7 @@ async function usage(args) {
|
|
|
1471
2015
|
}
|
|
1472
2016
|
function show(args) {
|
|
1473
2017
|
const name = args.positional[0];
|
|
1474
|
-
const dir = name ?
|
|
2018
|
+
const dir = name ? join13(libraryDir(), name) : "";
|
|
1475
2019
|
if (!name || !isSkillDir(dir)) {
|
|
1476
2020
|
error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
|
|
1477
2021
|
process.exit(1);
|
|
@@ -1507,7 +2051,7 @@ function link(args) {
|
|
|
1507
2051
|
for (const root of roots) {
|
|
1508
2052
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
1509
2053
|
const res = linkAll(root);
|
|
1510
|
-
const label = args.all ? `${
|
|
2054
|
+
const label = args.all ? `${basename6(root)}: ` : "";
|
|
1511
2055
|
if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
|
|
1512
2056
|
else if (!args.all) success("Every skill here is already usable by all your agents");
|
|
1513
2057
|
if (res.blocked.length) warn(`${label}git tracks ${res.blocked.join(", ")}; re-run with --allow-tracked to link there too`);
|
|
@@ -1535,14 +2079,45 @@ function globalCommand(args) {
|
|
|
1535
2079
|
if (unreviewed) info(dim(`
|
|
1536
2080
|
${unreviewed} not reviewed. Keep one global on purpose: skilllib global keep <name>`));
|
|
1537
2081
|
}
|
|
2082
|
+
function tidy(args) {
|
|
2083
|
+
const names = args.positional;
|
|
2084
|
+
const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve4(base, args.keep) : expandHome(args.keep)])) : {};
|
|
2085
|
+
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) }) }));
|
|
2086
|
+
let held = 0;
|
|
2087
|
+
let changed = 0;
|
|
2088
|
+
for (const { label, report } of reports) {
|
|
2089
|
+
const plans = report.plans.filter((p) => !names.length || names.includes(p.name));
|
|
2090
|
+
const conflicts = report.conflicts.filter((c) => !names.length || names.includes(c.name));
|
|
2091
|
+
if (!plans.length && !conflicts.length && !args.all) success(`${label}: every skill has one copy and the links your agents need`);
|
|
2092
|
+
for (const plan of plans) {
|
|
2093
|
+
const result = args.dryRun ? { applied: [], held: gitVisibleSteps(plan) } : applyTidy(plan, { git: args.allowGit ? "go" : "keep" });
|
|
2094
|
+
const show2 = args.dryRun ? plan.steps : result.applied;
|
|
2095
|
+
if (show2.length) info(`${args.dryRun ? dim("would") : green("\u2713")} ${label} \xB7 ${plan.name}: ${show2.map((s) => describeStep(s, plan.root)).join(", ")}`);
|
|
2096
|
+
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)}`);
|
|
2097
|
+
held += args.dryRun ? 0 : result.held.length;
|
|
2098
|
+
changed += result.applied.length;
|
|
2099
|
+
}
|
|
2100
|
+
for (const c of conflicts) {
|
|
2101
|
+
warn(`${label} \xB7 ${c.name}: the copies differ`);
|
|
2102
|
+
for (const copy of c.copies) {
|
|
2103
|
+
const runs = copy.runs.map((id) => HARNESSES.find((h) => h.id === id).name);
|
|
2104
|
+
info(` ${copyLabel(c, copy.dir)}${runs.length ? dim(` run by ${runs.join(", ")}`) : ""}${c.managed === copy.dir ? dim(" (skilllib's copy)") : ""}`);
|
|
2105
|
+
}
|
|
2106
|
+
const pick = copyLabel(c, c.managed ?? c.copies[0].dir);
|
|
2107
|
+
info(dim(` keep one: skilllib tidy ${c.name}${args.global ? " --global" : ""} --keep ${pick}`));
|
|
2108
|
+
}
|
|
2109
|
+
for (const s of report.skipped.filter((x) => !names.length || names.includes(x.name))) info(dim(`- ${label} \xB7 ${s.name}: skipped, ${s.reason}`));
|
|
2110
|
+
}
|
|
2111
|
+
if (args.dryRun) info(dim("\nNothing changed (--dry-run)."));
|
|
2112
|
+
else if (changed) info(dim("\nReplaced copies are in ~/.skilllib/tidy-backup; `skilllib restore` puts them back."));
|
|
2113
|
+
if (held) info(dim(`${held} change${held === 1 ? "" : "s"} held back because git would see them. Re-run with --allow-git to make them.`));
|
|
2114
|
+
}
|
|
1538
2115
|
|
|
1539
2116
|
export {
|
|
1540
2117
|
userHome,
|
|
1541
2118
|
skilllibHome,
|
|
1542
|
-
MANIFEST_FILE,
|
|
1543
2119
|
HARNESSES,
|
|
1544
|
-
|
|
1545
|
-
installDirs,
|
|
2120
|
+
harness,
|
|
1546
2121
|
readSkillInfo,
|
|
1547
2122
|
versionHistory,
|
|
1548
2123
|
forgetLatest,
|
|
@@ -1561,21 +2136,22 @@ export {
|
|
|
1561
2136
|
allowTrackedLinks,
|
|
1562
2137
|
keptGlobal,
|
|
1563
2138
|
setKeepGlobal,
|
|
2139
|
+
setPluginEnabled,
|
|
1564
2140
|
machineSkills,
|
|
1565
2141
|
libraryOrigins,
|
|
2142
|
+
recordOrigin,
|
|
1566
2143
|
librarySkills,
|
|
1567
2144
|
librarySkillDir,
|
|
1568
|
-
visibilityOf,
|
|
1569
2145
|
projectStatus,
|
|
1570
2146
|
linkEverywhere,
|
|
1571
|
-
unlinkEverywhere,
|
|
1572
2147
|
relinkDependency,
|
|
1573
|
-
linkAll,
|
|
1574
2148
|
addSkill,
|
|
1575
2149
|
removeSkill,
|
|
1576
2150
|
syncProject,
|
|
1577
2151
|
updateProject,
|
|
1578
2152
|
importSkill,
|
|
2153
|
+
removeUntracked,
|
|
2154
|
+
projectsUsing,
|
|
1579
2155
|
deleteLibrarySkill,
|
|
1580
2156
|
deleteGlobal,
|
|
1581
2157
|
unloadGlobal,
|
|
@@ -1584,8 +2160,12 @@ export {
|
|
|
1584
2160
|
restoreBackup,
|
|
1585
2161
|
error,
|
|
1586
2162
|
scanUsage,
|
|
1587
|
-
|
|
1588
|
-
|
|
2163
|
+
gitInfo,
|
|
2164
|
+
relativeTo,
|
|
2165
|
+
claudeBinary,
|
|
2166
|
+
recordRemovedPlugin,
|
|
2167
|
+
pluginBackups,
|
|
2168
|
+
restorePlugin,
|
|
1589
2169
|
parseArgs,
|
|
1590
2170
|
projectOfFactory,
|
|
1591
2171
|
status,
|
|
@@ -1607,5 +2187,6 @@ export {
|
|
|
1607
2187
|
show,
|
|
1608
2188
|
harnessesCommand,
|
|
1609
2189
|
link,
|
|
1610
|
-
globalCommand
|
|
2190
|
+
globalCommand,
|
|
2191
|
+
tidy
|
|
1611
2192
|
};
|