skilllib 1.11.4 → 1.11.6
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
CHANGED
|
@@ -270,7 +270,7 @@ The same skill often reaches your agents more than once: a copy in `.claude/skil
|
|
|
270
270
|
| Links no agent you use needs | Removes them. It keeps links that `skilllib.json` records, because teammates may use other agents. |
|
|
271
271
|
| **Copies that differ** | Shows which agent runs which copy (e.g. Claude Code and Cursor run `.claude`, Codex runs `.agents`), and lets you pick the one to keep. It's your call, so **Fix all** never does it. |
|
|
272
272
|
| Copies that differ behind a link (a link to another copy, in or out of the repo) | Reported only: skilllib never changes what a link points to. Make the copies the same, or remove the link. |
|
|
273
|
-
| A Claude Code plugin with the same skill | Your skill wins. skilllib copies
|
|
273
|
+
| A Claude Code plugin with the same skill | Your skill wins. skilllib copies the plugin's other skills into Your skills, then removes the plugin or turns it off (`claude plugin uninstall` / `disable`; a plugin only turned on in your settings, or synced from claude.ai, can only be turned off). If the plugin also brings MCP servers, hooks, agents or commands, turning it off is the default. When the overlap is in one repo, you can instead turn the plugin off only there (in its `.claude/settings.local.json`: Claude Code makes git ignore it when it writes it; without the `claude` command, skilllib writes the file itself and makes git ignore it), or remove your copy from that repo. If the repo commits its copy, it's the team's: skilllib never offers to remove it. A plugin that a repo's own `.claude/settings.json` turns on counts only in that repo. Cursor ignores repo settings: after a repo turns the plugin off, Cursor still lists both there, and only turning the plugin off or removing it changes that. |
|
|
274
274
|
| A Cursor plugin with the same skill | Reported only. Turn the plugin off in Cursor. |
|
|
275
275
|
| A repo copy of a skill you keep global | The repo copy is the extra one: remove it, or stop loading the skill globally after all. Claude Code runs the global copy anyway. |
|
|
276
276
|
| A repo's own skill that also loads globally | Stop loading it globally (when no agent in any of your repos would then lack it). The repo's copy is the team's, so skilllib never offers to remove it. |
|
|
@@ -774,7 +774,7 @@ function isLink(path) {
|
|
|
774
774
|
function notWritable(root, dir) {
|
|
775
775
|
const top = realpathOrNull(root) ?? root;
|
|
776
776
|
let p = root;
|
|
777
|
-
for (const part of dir.split(
|
|
777
|
+
for (const part of dir.split(/[\\/]/)) {
|
|
778
778
|
p = join8(p, part);
|
|
779
779
|
if (!entryExists(p)) return null;
|
|
780
780
|
const real = realpathOrNull(p);
|
|
@@ -1230,6 +1230,9 @@ function addPlan(root, name, hash, force) {
|
|
|
1230
1230
|
}
|
|
1231
1231
|
return { manifest, recorded, primary, wantedLinks, to, local };
|
|
1232
1232
|
}
|
|
1233
|
+
function withoutForceHint(reason) {
|
|
1234
|
+
return reason.replace(/ \(use --force[^)]*\)$/, "");
|
|
1235
|
+
}
|
|
1233
1236
|
function addRefusal(root, name, hash) {
|
|
1234
1237
|
const plan = addPlan(root, name, hash, false);
|
|
1235
1238
|
return "reason" in plan ? plan.reason : null;
|
|
@@ -1640,6 +1643,9 @@ import { basename as basename9, join as join15, resolve as resolve8, sep as sep8
|
|
|
1640
1643
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
1641
1644
|
import { existsSync as existsSync9, realpathSync as realpathSync4 } from "fs";
|
|
1642
1645
|
import { isAbsolute as isAbsolute2, join as join10, relative as relative3, sep as sep5 } from "path";
|
|
1646
|
+
function isShared(state) {
|
|
1647
|
+
return state === "committed" || state === "changed";
|
|
1648
|
+
}
|
|
1643
1649
|
function git(root, args, input) {
|
|
1644
1650
|
try {
|
|
1645
1651
|
return execFileSync2("git", args, { cwd: root, encoding: "utf-8", input, stdio: ["pipe", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 });
|
|
@@ -1708,6 +1714,9 @@ import { basename as basename6, dirname as dirname6, isAbsolute as isAbsolute3,
|
|
|
1708
1714
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1709
1715
|
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
1710
1716
|
import { basename as basename4, dirname as dirname4, join as join11, resolve as resolve7 } from "path";
|
|
1717
|
+
function pluginNameOf(id) {
|
|
1718
|
+
return id.split("@")[0];
|
|
1719
|
+
}
|
|
1711
1720
|
function nonEmptyDir(path) {
|
|
1712
1721
|
try {
|
|
1713
1722
|
return readdirSync5(path).some((f) => !f.startsWith("."));
|
|
@@ -1898,6 +1907,10 @@ function turnOffIn(plugin, root) {
|
|
|
1898
1907
|
const r = claude(["plugin", "disable", plugin.id, "--scope", "local"], root, bin);
|
|
1899
1908
|
return r.ok ? { ok: true, message: done } : { ok: false, message: `${plugin.id}: ${r.reason}` };
|
|
1900
1909
|
}
|
|
1910
|
+
function unplaced(p, verb) {
|
|
1911
|
+
if (!p.scope || p.scope === "user" || p.projectPath) return null;
|
|
1912
|
+
return { ok: false, message: `${p.id}: skilllib doesn't know which project it's installed in; run \`claude plugin ${verb} ${p.id} --scope ${p.scope}\` there` };
|
|
1913
|
+
}
|
|
1901
1914
|
function settingsFileOf(p) {
|
|
1902
1915
|
if (p.synced || !p.scope || p.scope === "user") return join11(claudeDir(), "settings.json");
|
|
1903
1916
|
if (!p.projectPath) return null;
|
|
@@ -1909,7 +1922,8 @@ function whereOf(p) {
|
|
|
1909
1922
|
function setPluginOn(p, on) {
|
|
1910
1923
|
const shared = p.scope === "project" && p.projectPath ? "; it's in .claude/settings.json, which git tracks, so commit it if the team should get it" : "";
|
|
1911
1924
|
const done = { ok: true, message: `${p.id} is ${on ? "on" : "off"}${whereOf(p)}${p.synced ? "" : "; Claude Code picks it up in its next session"}${shared}` };
|
|
1912
|
-
|
|
1925
|
+
const lost = unplaced(p, on ? "enable" : "disable");
|
|
1926
|
+
if (lost) return lost;
|
|
1913
1927
|
const bin = p.synced || p.settingsOnly ? null : claudeBinary();
|
|
1914
1928
|
if (bin) {
|
|
1915
1929
|
const r2 = claude(["plugin", on ? "enable" : "disable", p.id, ...p.scope ? ["--scope", p.scope] : []], p.projectPath, bin);
|
|
@@ -1923,7 +1937,8 @@ function setPluginOn(p, on) {
|
|
|
1923
1937
|
function uninstallPlugin(p) {
|
|
1924
1938
|
if (p.synced) return { ok: false, message: `${p.id} is synced from your claude.ai account: remove it there, or turn it off here` };
|
|
1925
1939
|
if (p.settingsOnly) return { ok: false, message: `${p.id} isn't installed, only turned on in your settings: turn it off instead` };
|
|
1926
|
-
|
|
1940
|
+
const lost = unplaced(p, "uninstall");
|
|
1941
|
+
if (lost) return lost;
|
|
1927
1942
|
const wasOn = enabledPlugins(p.projectPath)[p.id] === true;
|
|
1928
1943
|
const r = claude(["plugin", "uninstall", p.id, "--keep-data", "--scope", p.scope ?? "user"], p.projectPath);
|
|
1929
1944
|
if (!r.ok) return { ok: false, message: `${p.id}: ${r.reason}` };
|
|
@@ -1949,9 +1964,8 @@ function removedFile() {
|
|
|
1949
1964
|
}
|
|
1950
1965
|
function removePlugin(plugin, how) {
|
|
1951
1966
|
if (plugin.settingsOnly && how === "delete") return { ok: false, message: `${plugin.id} isn't installed, only turned on in your settings: turn it off instead` };
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
}
|
|
1967
|
+
const lost = unplaced(plugin, how === "delete" ? "uninstall" : "disable");
|
|
1968
|
+
if (lost) return lost;
|
|
1955
1969
|
const viaSettings = plugin.synced || plugin.settingsOnly || !claudeBinary() && how === "off" && !plugin.projectPath;
|
|
1956
1970
|
if (!viaSettings && !claudeBinary()) {
|
|
1957
1971
|
return { ok: false, message: `${plugin.id}: nothing changed; Claude Code's CLI didn't run (tried \`claude\` on your PATH and ~/.local/bin/claude)` };
|
|
@@ -2003,7 +2017,7 @@ function restorePlugin(backup) {
|
|
|
2003
2017
|
return { ok: true, to: where };
|
|
2004
2018
|
}
|
|
2005
2019
|
function cursorPluginSkills(plugins = cursorPlugins()) {
|
|
2006
|
-
return plugins.flatMap((p) => p.skills.map((path) => ({ name: basename4(path), plugin: p.id
|
|
2020
|
+
return plugins.flatMap((p) => p.skills.map((path) => ({ name: basename4(path), plugin: pluginNameOf(p.id), path })));
|
|
2007
2021
|
}
|
|
2008
2022
|
|
|
2009
2023
|
// src/tidy.ts
|
|
@@ -2020,6 +2034,9 @@ function entriesOf(dirs, name) {
|
|
|
2020
2034
|
return [{ dir, path, link: link2, real, hash: null }];
|
|
2021
2035
|
});
|
|
2022
2036
|
}
|
|
2037
|
+
function uniqueReals(entries) {
|
|
2038
|
+
return entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link && o.real === e.real) === i);
|
|
2039
|
+
}
|
|
2023
2040
|
function hashReals(reals) {
|
|
2024
2041
|
if (reals.length > 1) for (const r of reals) r.hash = treeHash(r.path);
|
|
2025
2042
|
}
|
|
@@ -2060,15 +2077,16 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
2060
2077
|
let git2;
|
|
2061
2078
|
const skillsLock = projectSkillsLock(root);
|
|
2062
2079
|
const dirs = repoTidyDirs(root).map(abs);
|
|
2080
|
+
const realRoot = realpathOrNull(root) ?? root;
|
|
2063
2081
|
for (const name of namesIn(dirs)) {
|
|
2064
2082
|
const entries = entriesOf(dirs, name);
|
|
2065
|
-
const external = entries.find((e) => e.link && e.real && !(e.real + sep6).startsWith(
|
|
2083
|
+
const external = entries.find((e) => e.link && e.real && !(e.real + sep6).startsWith(realRoot + sep6));
|
|
2066
2084
|
if (external) {
|
|
2067
2085
|
const differs = entries.some((e) => e !== external && e.real && existsSync11(join12(e.real, "SKILL.md")) && treeHash(e.real) !== treeHash(external.real));
|
|
2068
2086
|
report.skipped.push({ name, reason: `${rel(external.path)} links outside the repo`, ...differs ? { differs } : {} });
|
|
2069
2087
|
continue;
|
|
2070
2088
|
}
|
|
2071
|
-
const reals = entries
|
|
2089
|
+
const reals = uniqueReals(entries);
|
|
2072
2090
|
if (reals.length === 0) continue;
|
|
2073
2091
|
hashReals(reals);
|
|
2074
2092
|
const dep = skills[name];
|
|
@@ -2083,7 +2101,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
2083
2101
|
if (git2 === void 0) git2 = gitInfo(root);
|
|
2084
2102
|
const planned = installDirs(enabled).map(abs);
|
|
2085
2103
|
const fromSkillsSh = name in skillsLock;
|
|
2086
|
-
const rank = (e) => (
|
|
2104
|
+
const rank = (e) => (isShared(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);
|
|
2087
2105
|
primary = [...reals].sort((a, b) => rank(a) - rank(b))[0];
|
|
2088
2106
|
}
|
|
2089
2107
|
if (!primary) continue;
|
|
@@ -2103,12 +2121,12 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
2103
2121
|
const needed = wanted.has(e.dir);
|
|
2104
2122
|
if (e.link) {
|
|
2105
2123
|
if (e.real === primary.real) {
|
|
2106
|
-
if (!needed && !dep &&
|
|
2124
|
+
if (!needed && !dep && isShared((git2 ??= gitInfo(root))?.of(rel(e.path)))) continue;
|
|
2107
2125
|
if (!needed) steps.push({ kind: "unlink", path: e.path });
|
|
2108
2126
|
} else steps.push(needed ? { kind: "link", path: e.path, target: primary.path } : { kind: "unlink", path: e.path });
|
|
2109
2127
|
} else {
|
|
2110
2128
|
const differs = e.hash !== primary.hash ? { differs: true } : {};
|
|
2111
|
-
if (!needed &&
|
|
2129
|
+
if (!needed && !chosen && isShared((git2 ??= gitInfo(root))?.of(rel(e.path)))) continue;
|
|
2112
2130
|
steps.push(needed ? { kind: "replace", path: e.path, target: primary.path, ...differs } : { kind: "remove", path: e.path, ...differs });
|
|
2113
2131
|
}
|
|
2114
2132
|
}
|
|
@@ -2132,7 +2150,7 @@ function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
|
|
|
2132
2150
|
report.skipped.push({ name, reason: "a Claude Code plugin kept in a skills folder" });
|
|
2133
2151
|
continue;
|
|
2134
2152
|
}
|
|
2135
|
-
const reals = entries
|
|
2153
|
+
const reals = uniqueReals(entries);
|
|
2136
2154
|
if (reals.length < 2) continue;
|
|
2137
2155
|
hashReals(reals);
|
|
2138
2156
|
const kept = reals.find((r) => r.dir === keep[name]);
|
|
@@ -2148,13 +2166,13 @@ function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
|
|
|
2148
2166
|
}
|
|
2149
2167
|
function gitVisibleSteps(plan, git2 = plan.root ? gitInfo(plan.root) : null) {
|
|
2150
2168
|
if (!plan.root || !git2) return [];
|
|
2151
|
-
const shared = (rel) =>
|
|
2169
|
+
const shared = (rel) => isShared(git2.of(rel));
|
|
2152
2170
|
const dep = readManifest(plan.root).skills[plan.name];
|
|
2153
2171
|
const recordsLink = (s) => !!dep && (s.kind === "link" || s.kind === "replace") && !dep.links?.includes(relativeTo(plan.root, dirname5(s.path)));
|
|
2154
2172
|
return plan.steps.filter((s) => shared(relativeTo(plan.root, s.path)) || recordsLink(s) && shared(MANIFEST_FILE));
|
|
2155
2173
|
}
|
|
2156
|
-
function applyTidy(plan, { git: git2 = "keep"
|
|
2157
|
-
const held = git2 === "keep" ? gitVisibleSteps(plan
|
|
2174
|
+
function applyTidy(plan, { git: git2 = "keep" } = {}) {
|
|
2175
|
+
const held = git2 === "keep" ? gitVisibleSteps(plan) : [];
|
|
2158
2176
|
const applied = [];
|
|
2159
2177
|
for (const step of plan.steps) {
|
|
2160
2178
|
if (held.includes(step) || !entryExists(step.path) || "target" in step && !entryExists(step.target)) continue;
|
|
@@ -2244,7 +2262,7 @@ var gitOf = (root) => {
|
|
|
2244
2262
|
};
|
|
2245
2263
|
var gitNote = (root, paths) => {
|
|
2246
2264
|
const git2 = gitOf(root);
|
|
2247
|
-
return paths.some((p) =>
|
|
2265
|
+
return paths.some((p) => isShared(git2?.of(relativeTo(root, p)))) ? "; git will see it" : "";
|
|
2248
2266
|
};
|
|
2249
2267
|
var agentNames = (ids) => ids.map((id) => harness(id).name).join(", ");
|
|
2250
2268
|
function conflictIssue(c) {
|
|
@@ -2265,6 +2283,7 @@ function conflictIssue(c) {
|
|
|
2265
2283
|
at: rows(c.root, [c.name]),
|
|
2266
2284
|
choices: winners.flatMap((x) => {
|
|
2267
2285
|
const plan = planFor(x.dir);
|
|
2286
|
+
if (!plan?.steps.length) return [];
|
|
2268
2287
|
const visible2 = plan ? gitVisibleSteps(plan).map((s) => describeStep(s, c.root)) : [];
|
|
2269
2288
|
const label = `Keep the ${folderLabel(c.root, x.dir)} copy`;
|
|
2270
2289
|
if (!visible2.length) return [{ label, hint: x.dir === c.managed ? "skilllib's version" : "the others become links to it", run: keep(x.dir, "go") }];
|
|
@@ -2330,7 +2349,7 @@ function brokenLinksIn(root, opened) {
|
|
|
2330
2349
|
root,
|
|
2331
2350
|
folders2.map((f) => placeOf(f.real)).filter((p) => p !== "" && p !== ".." && !p.startsWith("../") && !isAbsolute3(p))
|
|
2332
2351
|
);
|
|
2333
|
-
const tracked =
|
|
2352
|
+
const tracked = isShared(git2?.of(placeOf(join13(real, name))));
|
|
2334
2353
|
const recorded = !!own && mends(own, relative4(own, real).split(sep7).join("/"), name);
|
|
2335
2354
|
const pinnedThere = own ? ` ${placeOf(own)} has its own skilllib.json${recorded ? `, which records this link: skilllib sync in ${placeOf(own)} brings back what it points to` : ""}.` : "";
|
|
2336
2355
|
return [
|
|
@@ -2463,7 +2482,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2463
2482
|
const dupes = names.filter((n) => libraryNames.has(n) || loadedYours.has(n));
|
|
2464
2483
|
if (!dupes.length) continue;
|
|
2465
2484
|
const others = plugin.skills.length - dupes.length;
|
|
2466
|
-
const pluginName = plugin.id
|
|
2485
|
+
const pluginName = pluginNameOf(plugin.id);
|
|
2467
2486
|
const remove2 = {
|
|
2468
2487
|
label: `Remove the plugin ${plugin.id}`,
|
|
2469
2488
|
hint: plugin.extras.length ? `also removes its ${plugin.extras.join(", ")}` : "restorable with skilllib restore",
|
|
@@ -2520,7 +2539,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2520
2539
|
id: `plugin:${root}:${plugin.id}`,
|
|
2521
2540
|
severity: "problem",
|
|
2522
2541
|
title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}, on in ${where}`,
|
|
2523
|
-
detail: `${where}'s Claude Code settings turn the plugin on, so Claude Code loads both there (the plugin's as /${plugin.id
|
|
2542
|
+
detail: `${where}'s Claude Code settings turn the plugin on, so Claude Code loads both there (the plugin's as /${pluginNameOf(plugin.id)}:${dupes[0]}). Your skill wins.`,
|
|
2524
2543
|
choices: repoChoices(plugin, root, inRepo),
|
|
2525
2544
|
at: rows(root, dupes),
|
|
2526
2545
|
plugin: plugin.id
|
|
@@ -2653,7 +2672,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2653
2672
|
}
|
|
2654
2673
|
const drifted = unstuck.filter((s) => s.state === "out of sync" && knownPin(s));
|
|
2655
2674
|
if (drifted.length) {
|
|
2656
|
-
const seen = drifted.some((s) =>
|
|
2675
|
+
const seen = drifted.some((s) => isShared(gitOf(root)?.of(relativeTo(root, s.path))));
|
|
2657
2676
|
const yours = drifted.some((s) => untrackable(s));
|
|
2658
2677
|
const install = {
|
|
2659
2678
|
label: "Install the pinned versions",
|
|
@@ -2701,7 +2720,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2701
2720
|
choices: [
|
|
2702
2721
|
{
|
|
2703
2722
|
label: "Update to the newest versions",
|
|
2704
|
-
hint:
|
|
2723
|
+
hint: isShared(gitOf(root)?.of(MANIFEST_FILE)) ? "git will see the new copies and skilllib.json" : "replaces each copy with your library's newest",
|
|
2705
2724
|
run: () => summarize(where, "updated", updateProject(root, behind.map((s) => s.name)))
|
|
2706
2725
|
}
|
|
2707
2726
|
]
|
|
@@ -2784,7 +2803,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2784
2803
|
if (partial.length) {
|
|
2785
2804
|
const missing2 = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
|
|
2786
2805
|
const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
|
|
2787
|
-
const manifestShared = partial.some((s) => s.managed) &&
|
|
2806
|
+
const manifestShared = partial.some((s) => s.managed) && isShared(gitOf(root)?.of(MANIFEST_FILE));
|
|
2788
2807
|
const run = (allowTracked) => () => {
|
|
2789
2808
|
if (allowTracked) allowTrackedLinks(root);
|
|
2790
2809
|
const r = linkAll(root, { allowTracked, names: partial.map((s) => s.name) });
|
|
@@ -2813,7 +2832,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2813
2832
|
}
|
|
2814
2833
|
const differing = new Set(tidy2.conflicts.map((c) => c.name));
|
|
2815
2834
|
const git2 = gitOf(root);
|
|
2816
|
-
const committed = (s) =>
|
|
2835
|
+
const committed = (s) => isShared(git2?.of(relativeTo(root, s.path)));
|
|
2817
2836
|
for (const s of unstuck) {
|
|
2818
2837
|
if (isLink(s.path) || differing.has(s.name)) continue;
|
|
2819
2838
|
if (s.state === "local only" && (committed(s) || untrackable(s))) {
|
|
@@ -2866,7 +2885,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2866
2885
|
});
|
|
2867
2886
|
} else if (s.state === "untracked, differs from library" || s.state === "repo skill, differs from library") {
|
|
2868
2887
|
const teams = s.state.startsWith("repo skill") || committed(s) || !!untrackable(s);
|
|
2869
|
-
const whose = s.state.startsWith("repo skill") || committed(s) ? "It's the team's copy (committed, or in the repo's own .agents/skills)" : `${untrackable(s)}, so skilllib can't track it here`;
|
|
2888
|
+
const whose = s.state.startsWith("repo skill") || committed(s) ? "It's the team's copy (committed, or in the repo's own .agents/skills or .grok/skills)" : `${untrackable(s)}, so skilllib can't track it here`;
|
|
2870
2889
|
issues.push({
|
|
2871
2890
|
id: `differs:${root}:${s.name}`,
|
|
2872
2891
|
severity: "suggestion",
|
|
@@ -3123,7 +3142,7 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
|
|
|
3123
3142
|
}
|
|
3124
3143
|
if (uses.asUsed)
|
|
3125
3144
|
for (const p of installedPlugins().filter((x) => x.agent === "claude-code" && x.on && x.skills.length)) {
|
|
3126
|
-
const name = p.id
|
|
3145
|
+
const name = pluginNameOf(p.id);
|
|
3127
3146
|
if (p.skills.some((path) => uses.asUsed(`${name}:${basename6(path)}`) > 0)) continue;
|
|
3128
3147
|
issues.push({
|
|
3129
3148
|
id: `plugin-unused:${installId(p)}`,
|
|
@@ -3203,7 +3222,7 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
3203
3222
|
const loadedBy = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
|
|
3204
3223
|
const behind = s.state.endsWith("update available");
|
|
3205
3224
|
const gitState = git2?.of(relativeTo(root, s.path));
|
|
3206
|
-
const repo = !s.managed && (s.state.startsWith("repo skill") || gitState
|
|
3225
|
+
const repo = !s.managed && (s.state.startsWith("repo skill") || isShared(gitState));
|
|
3207
3226
|
const usual = s.state === "ok" || s.state === "local only" || repo && s.state !== "from npx skills";
|
|
3208
3227
|
const attrs = {
|
|
3209
3228
|
source: s.managed ? "library" : repo ? "repo" : "local",
|
|
@@ -3367,7 +3386,14 @@ function transcriptFiles(sinceMs) {
|
|
|
3367
3386
|
return list2(base).flatMap((project) => {
|
|
3368
3387
|
const dir = join14(base, project);
|
|
3369
3388
|
if (!statSync5(dir, { throwIfNoEntry: false })?.isDirectory()) return [];
|
|
3370
|
-
return list2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join14(dir, f)).filter((f) =>
|
|
3389
|
+
return list2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join14(dir, f)).filter((f) => {
|
|
3390
|
+
try {
|
|
3391
|
+
const st = statSync5(f);
|
|
3392
|
+
return st.isFile() && st.mtimeMs >= sinceMs;
|
|
3393
|
+
} catch {
|
|
3394
|
+
return false;
|
|
3395
|
+
}
|
|
3396
|
+
});
|
|
3371
3397
|
});
|
|
3372
3398
|
}
|
|
3373
3399
|
async function scanUsage(days) {
|
|
@@ -3430,7 +3456,7 @@ function moveGlobalSkills(skills, roots, {
|
|
|
3430
3456
|
const elsewhere = blindWithoutGlobal(name, copies, knownProjects().filter((p) => !roots.some((r) => realPath(r) === realPath(p))));
|
|
3431
3457
|
if (elsewhere.length) return { name, moved: false, reason: `nothing moved: ${blindSaid(name, elsewhere, label)}`, changes: [] };
|
|
3432
3458
|
const hash = treeHash(copies[0].path) ?? "";
|
|
3433
|
-
const cant = roots.flatMap((root) => ((b) => b ? [`${label(root)} (${b
|
|
3459
|
+
const cant = roots.flatMap((root) => ((b) => b ? [`${label(root)} (${withoutForceHint(b)})`] : [])(trackBlocker(root, name) ?? addRefusal(root, name, hash)));
|
|
3434
3460
|
if (cant.length) return { name, moved: false, reason: `nothing moved: it can't be added to ${cant.join(", ")}`, changes: [] };
|
|
3435
3461
|
const imported = importSkill(copies[0].path).status;
|
|
3436
3462
|
if (imported === "exists")
|
|
@@ -3438,7 +3464,7 @@ function moveGlobalSkills(skills, roots, {
|
|
|
3438
3464
|
if (group) recordOrigin(name, `group: ${group}`);
|
|
3439
3465
|
const changes = roots.map((root) => [root, addSkill(root, name, { allowTracked })]);
|
|
3440
3466
|
const skipped = changes.filter(([, c]) => c.action === "skipped");
|
|
3441
|
-
const why = (reason = "") => reason
|
|
3467
|
+
const why = (reason = "") => withoutForceHint(reason) === reason ? reason : `${withoutForceHint(reason)}: move or remove it there first`;
|
|
3442
3468
|
const kept = imported === "added" ? `; it's in Your skills now` : "";
|
|
3443
3469
|
if (skipped.length) return { name, moved: false, reason: `not added to ${skipped.map(([r, c]) => `${label(r)} (${why(c.reason)})`).join(", ")}, so it still loads globally${kept}`, changes };
|
|
3444
3470
|
const blindIn = changes.flatMap(([root, c]) => ((b) => b.length ? [{ root, c, b }] : [])(blindWithoutGlobal(name, copies, [root])));
|
|
@@ -3471,6 +3497,7 @@ var FLAGS = {
|
|
|
3471
3497
|
keep: { type: "string" },
|
|
3472
3498
|
to: { type: "string", multiple: true }
|
|
3473
3499
|
};
|
|
3500
|
+
var VALUE_FLAGS = Object.entries(FLAGS).flatMap(([name, f]) => f.type === "string" ? [`--${name}`] : []);
|
|
3474
3501
|
function parseArgs(argv) {
|
|
3475
3502
|
const { values: v, positionals } = parseFlags({ args: argv, options: FLAGS, allowPositionals: true, strict: true });
|
|
3476
3503
|
const days = Number(v.days ?? USAGE_DAYS);
|
|
@@ -3688,7 +3715,7 @@ async function list(args) {
|
|
|
3688
3715
|
}
|
|
3689
3716
|
function teamStateIn(root, extraDirs = []) {
|
|
3690
3717
|
const git2 = gitInfo(root, [...new Set(extraDirs)]);
|
|
3691
|
-
const committed = (path) =>
|
|
3718
|
+
const committed = (path) => isShared(git2?.of(relativeTo(root, path)));
|
|
3692
3719
|
const asRepo = {
|
|
3693
3720
|
"local only": "repo skill",
|
|
3694
3721
|
"untracked copy of library skill": "repo skill, in library",
|
|
@@ -4231,7 +4258,8 @@ function tidy(args) {
|
|
|
4231
4258
|
for (const s of report.skipped.filter((x) => !names.length || names.includes(x.name))) info(dim(`- ${label} \xB7 ${s.name}: skipped, ${s.reason}`));
|
|
4232
4259
|
}
|
|
4233
4260
|
if (args.dryRun) info(dim("\nNothing changed (--dry-run)."));
|
|
4234
|
-
else if (changed) info(dim(
|
|
4261
|
+
else if (changed) info(dim(`
|
|
4262
|
+
Replaced copies are in ${tildify(join15(skilllibHome(), "tidy-backup"))}; \`skilllib restore\` puts them back.`));
|
|
4235
4263
|
if (held) {
|
|
4236
4264
|
process.exitCode = 1;
|
|
4237
4265
|
info(dim(`${held} change${held === 1 ? "" : "s"} held back because git would see them. Re-run with --allow-git to make them.`));
|
|
@@ -4316,6 +4344,7 @@ export {
|
|
|
4316
4344
|
projectStatus,
|
|
4317
4345
|
linkEverywhere,
|
|
4318
4346
|
relinkDependency,
|
|
4347
|
+
withoutForceHint,
|
|
4319
4348
|
addRefusal,
|
|
4320
4349
|
addSkill,
|
|
4321
4350
|
removeSkill,
|
|
@@ -4329,6 +4358,7 @@ export {
|
|
|
4329
4358
|
listBackups,
|
|
4330
4359
|
movedTogether,
|
|
4331
4360
|
restoreBackup,
|
|
4361
|
+
isShared,
|
|
4332
4362
|
gitInfo,
|
|
4333
4363
|
relativeTo,
|
|
4334
4364
|
installedPlugins,
|
|
@@ -4351,6 +4381,7 @@ export {
|
|
|
4351
4381
|
scanUsage,
|
|
4352
4382
|
skillOf,
|
|
4353
4383
|
moveGlobalSkills,
|
|
4384
|
+
VALUE_FLAGS,
|
|
4354
4385
|
parseArgs,
|
|
4355
4386
|
projectOfFactory,
|
|
4356
4387
|
status,
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
VALUE_FLAGS,
|
|
3
4
|
add,
|
|
4
5
|
agentSkill,
|
|
5
6
|
apply,
|
|
@@ -30,7 +31,7 @@ import {
|
|
|
30
31
|
unhide,
|
|
31
32
|
update,
|
|
32
33
|
usage
|
|
33
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-TXQQQ73J.js";
|
|
34
35
|
|
|
35
36
|
// src/index.ts
|
|
36
37
|
import { readFileSync } from "fs";
|
|
@@ -125,7 +126,7 @@ async function main() {
|
|
|
125
126
|
const argv = process.argv.slice(2);
|
|
126
127
|
setJsonMode(argv.includes("--json"));
|
|
127
128
|
let at = 0;
|
|
128
|
-
while (argv[at]?.startsWith("--") && !["--version", "--help"].includes(argv[at])) at +=
|
|
129
|
+
while (argv[at]?.startsWith("--") && !["--version", "--help"].includes(argv[at])) at += VALUE_FLAGS.includes(argv[at]) ? 2 : 1;
|
|
129
130
|
const command = argv[at];
|
|
130
131
|
const rest = [...argv.slice(0, at), ...argv.slice(at + 1)];
|
|
131
132
|
const args = parseArgs(rest);
|
|
@@ -137,10 +138,10 @@ async function main() {
|
|
|
137
138
|
if (!["--version", "-v", "version", "help", "--help", "-h"].includes(command ?? "")) refreshAgentSkill();
|
|
138
139
|
switch (command) {
|
|
139
140
|
case void 0:
|
|
140
|
-
return process.stdin.isTTY && process.stdout.isTTY ? (await import("./tui-
|
|
141
|
+
return process.stdin.isTTY && process.stdout.isTTY ? (await import("./tui-OS2VUBGF.js")).tui() : overview(args);
|
|
141
142
|
case "ui":
|
|
142
143
|
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("`skilllib ui` needs an interactive terminal. Agents and scripts: use `skilllib status --json`.");
|
|
143
|
-
return (await import("./tui-
|
|
144
|
+
return (await import("./tui-OS2VUBGF.js")).tui();
|
|
144
145
|
case "status":
|
|
145
146
|
return status(args);
|
|
146
147
|
case "add":
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
installDirs,
|
|
30
30
|
installedPlugins,
|
|
31
31
|
isDir,
|
|
32
|
+
isShared,
|
|
32
33
|
keptGlobal,
|
|
33
34
|
knownProjects,
|
|
34
35
|
latestVersion,
|
|
@@ -81,8 +82,9 @@ import {
|
|
|
81
82
|
updateProject,
|
|
82
83
|
usageIssues,
|
|
83
84
|
versionHistory,
|
|
84
|
-
visibleProjects
|
|
85
|
-
|
|
85
|
+
visibleProjects,
|
|
86
|
+
withoutForceHint
|
|
87
|
+
} from "./chunk-TXQQQ73J.js";
|
|
86
88
|
|
|
87
89
|
// src/tui/index.tsx
|
|
88
90
|
import { render } from "ink";
|
|
@@ -932,7 +934,7 @@ var reviewOption = (w, name, ui, repo) => ({
|
|
|
932
934
|
label: "Review prompt",
|
|
933
935
|
action: () => ui.copy(reviewPrompt(w, [name], `Review the skill ${name}${repo ? ` in ${repo}` : ""}.`, repo), `a review prompt for ${name}`)
|
|
934
936
|
});
|
|
935
|
-
var gitSees = (s) => s?.git
|
|
937
|
+
var gitSees = (s) => isShared(s?.git) ? " Git will see it." : "";
|
|
936
938
|
function versionsOption(w, repo, name, installed, ui) {
|
|
937
939
|
return {
|
|
938
940
|
label: "Other versions\u2026",
|
|
@@ -1741,7 +1743,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1741
1743
|
if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
|
|
1742
1744
|
}
|
|
1743
1745
|
}
|
|
1744
|
-
const toastLines = toast && modal?.kind !== "confirm" ? wrap(toast, termCols - 2, 3).map((l) => " " + l) : [];
|
|
1746
|
+
const toastLines = toast && modal?.kind !== "confirm" ? wrap(toast, termCols - 2, Math.max(3, Math.floor(termRows / 3))).map((l) => " " + l) : [];
|
|
1745
1747
|
const bottomH = Math.max(1, toastLines.length);
|
|
1746
1748
|
const confirmLines = modal?.kind === "confirm" ? wrap(modal.fix.preview, columns - 6, 6) : [];
|
|
1747
1749
|
const confirmH = modal?.kind === "confirm" ? confirmLines.length + 2 : 0;
|
|
@@ -1784,10 +1786,14 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1784
1786
|
}), here: onHelp }
|
|
1785
1787
|
];
|
|
1786
1788
|
const sideAt = sideRows.findIndex((r) => r.here);
|
|
1789
|
+
const brand = " \u25C6 skilllib ";
|
|
1790
|
+
const tabsWidth = (short) => PLACES.reduce((n, [p, label]) => n + (short && p === "library" ? 6 : label.length) + 2 + (p === "health" && healthCount ? ` ${healthCount}`.length : 0), 0);
|
|
1791
|
+
const showBrand = brand.length + tabsWidth(false) <= columns;
|
|
1792
|
+
const shortTabs = !showBrand && tabsWidth(false) > columns;
|
|
1787
1793
|
const topBar = /* @__PURE__ */ jsx2(Box2, { height: 1, width: columns, justifyContent: "space-between", children: /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1788
|
-
/* @__PURE__ */ jsx2(Text2, { color: color.accent, bold: true, children:
|
|
1794
|
+
/* @__PURE__ */ jsx2(Text2, { color: color.accent, bold: true, children: showBrand ? brand : "" }),
|
|
1789
1795
|
PLACES.map(([p, label]) => /* @__PURE__ */ jsxs2(Text2, { backgroundColor: place === p ? color.accent : void 0, color: place === p ? "#0B1020" : color.muted, bold: place === p, children: [
|
|
1790
|
-
` ${label}`,
|
|
1796
|
+
` ${shortTabs && p === "library" ? "Skills" : label}`,
|
|
1791
1797
|
/* @__PURE__ */ jsx2(Text2, { color: place === p ? "#0B1020" : healthWorst ? SEV[healthWorst.severity].color : color.green, children: p === "health" && healthCount ? ` ${healthCount}` : "" }),
|
|
1792
1798
|
" "
|
|
1793
1799
|
] }, p))
|
|
@@ -2082,7 +2088,7 @@ function localSkill(root, s, git) {
|
|
|
2082
2088
|
};
|
|
2083
2089
|
const state = git ? git.of(relativeTo(root, s.path)) : null;
|
|
2084
2090
|
const others = otherCopies(root, s.name, s.location);
|
|
2085
|
-
const own = other[s.state]?.source === "untracked" && (state
|
|
2091
|
+
const own = other[s.state]?.source === "untracked" && isShared(state) ? { source: "repo" } : {};
|
|
2086
2092
|
return {
|
|
2087
2093
|
name: s.name,
|
|
2088
2094
|
source: "lib",
|
|
@@ -2247,6 +2253,7 @@ function said3(c, repo) {
|
|
|
2247
2253
|
}
|
|
2248
2254
|
function realOps(roots, backups, machine, libraryNames) {
|
|
2249
2255
|
const rootOf = (repo) => roots.get(repo);
|
|
2256
|
+
const nameOf = (root) => [...roots].find(([, r]) => r === root)?.[0] ?? basename(root);
|
|
2250
2257
|
const find = (repo, name) => projectStatus(rootOf(repo)).find((s) => s.name === name);
|
|
2251
2258
|
const result = (c, repo) => c.action === "skipped" ? failed(said3(c, repo)) : said3(c, repo);
|
|
2252
2259
|
const copyDir = (repo, name) => readManifest(rootOf(repo)).skills[name]?.dir ?? PROJECT_SKILLS_DIR;
|
|
@@ -2304,6 +2311,7 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2304
2311
|
};
|
|
2305
2312
|
const linkIn = (repo, name, allow) => {
|
|
2306
2313
|
const s = find(repo, name);
|
|
2314
|
+
if (!s) throw new Error(`${name} isn't in ${repo} any more`);
|
|
2307
2315
|
return s.managed ? relinkDependency(rootOf(repo), name, { allowTracked: allow }) : linkEverywhere(rootOf(repo), name, s.location, { allowTracked: allow });
|
|
2308
2316
|
};
|
|
2309
2317
|
const linkSaid = (repo, name, r) => {
|
|
@@ -2315,7 +2323,7 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2315
2323
|
const orForce = (c, repo, label, force) => (
|
|
2316
2324
|
// Whatever --force would get past (edits, a pin from another library, another copy in the way): offer it here.
|
|
2317
2325
|
c.action === "skipped" && !!c.reason?.includes("--force") ? {
|
|
2318
|
-
message: `${c.name} in ${repo}: ${c.reason
|
|
2326
|
+
message: `${c.name} in ${repo}: ${withoutForceHint(c.reason)}`,
|
|
2319
2327
|
then: { label, preview: `What's there now in ${repo} goes to Settings \u203A Backups first.`, run: () => result(force(), repo) }
|
|
2320
2328
|
} : result(c, repo)
|
|
2321
2329
|
);
|
|
@@ -2329,7 +2337,7 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2329
2337
|
remove: (repo, name) => {
|
|
2330
2338
|
const s = find(repo, name);
|
|
2331
2339
|
if (!s) return failed(`${name} isn't in ${repo}`);
|
|
2332
|
-
const committed = !s.managed &&
|
|
2340
|
+
const committed = !s.managed && isShared(gitInfo(rootOf(repo))?.of(relativeTo(rootOf(repo), s.path)));
|
|
2333
2341
|
if (s.state.startsWith("repo skill") || committed) return failed(`${name} is the repo's own (committed by your team): skilllib doesn't delete it`);
|
|
2334
2342
|
const others = otherCopies(rootOf(repo), name, s.location);
|
|
2335
2343
|
const left = (r) => typeof r === "string" && others.length ? `${r}; the copy in ${others.join(", ")} stays` : r;
|
|
@@ -2340,7 +2348,7 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2340
2348
|
const c = updateProject(rootOf(repo), [name])[0];
|
|
2341
2349
|
return c ? orForce(c, repo, "Update anyway", () => updateProject(rootOf(repo), [name], { force: true })[0]) : `${name} is up to date`;
|
|
2342
2350
|
},
|
|
2343
|
-
copyToLibrary: (repo, name) => toLibrary(find(repo, name)
|
|
2351
|
+
copyToLibrary: (repo, name) => ((s) => s ? toLibrary(s.path) : failed(`${name} isn't in ${repo} any more`))(find(repo, name)),
|
|
2344
2352
|
deleteGlobal: (m) => {
|
|
2345
2353
|
if (!entryExists(m.path)) return `${tildify(m.path)} is already gone`;
|
|
2346
2354
|
const others = machine.filter((o) => o.name === m.name && o.path !== m.path && o.kind !== "plugin" && !o.broken);
|
|
@@ -2349,9 +2357,9 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2349
2357
|
if (!m.broken) {
|
|
2350
2358
|
const readers = [.../* @__PURE__ */ new Set([...m.agents, ...dependents.flatMap((o) => o.harnesses)])].filter((id) => !stays.some((o) => o.harnesses.includes(id)));
|
|
2351
2359
|
const blind = blindWithoutGlobal(m.name, [{ path: m.path, harnesses: readers, links: [...m.links, ...dependents.map((o) => o.path)] }], [...roots.values(), ...knownProjects()]);
|
|
2352
|
-
if (blind.length) return failed(`${m.name} kept: ${blindSaid(m.name, blind,
|
|
2360
|
+
if (blind.length) return failed(`${m.name} kept: ${blindSaid(m.name, blind, nameOf)}`);
|
|
2353
2361
|
}
|
|
2354
|
-
const left = m.broken ? [] : strandedLinks(m.name, [{ path: m.path, links: [...m.links, ...dependents.map((o) => o.path)] }], [...roots.values(), ...knownProjects()],
|
|
2362
|
+
const left = m.broken ? [] : strandedLinks(m.name, [{ path: m.path, links: [...m.links, ...dependents.map((o) => o.path)] }], [...roots.values(), ...knownProjects()], nameOf);
|
|
2355
2363
|
const r = deleteGlobal(m.path, [...m.links, ...dependents.map((o) => o.path)]);
|
|
2356
2364
|
if (!r.ok) return failed(`${m.name}: ${r.reason}`);
|
|
2357
2365
|
if (m.broken) return `Removed the broken link ${m.name}`;
|
|
@@ -2361,7 +2369,7 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2361
2369
|
keepGlobal: (name, keep) => (setKeepGlobal([name], keep), keep ? `${name} marked as global on purpose` : `${name} is no longer marked as global on purpose`),
|
|
2362
2370
|
moveGlobal: (skills, repos, group) => {
|
|
2363
2371
|
const repoRoots = repos.map(rootOf);
|
|
2364
|
-
const label = (root) => repos[repoRoots.indexOf(root)] ??
|
|
2372
|
+
const label = (root) => repos[repoRoots.indexOf(root)] ?? nameOf(root);
|
|
2365
2373
|
const picked = new Set(skills.map((m) => m.name));
|
|
2366
2374
|
const sourced = machine.filter((o) => picked.has(o.name) && o.movable && !o.broken);
|
|
2367
2375
|
const attempt = (names) => moveGlobalSkills(names ? sourced.filter((m) => names.includes(m.name)) : sourced, repoRoots, { group, label, allowTrackedHint: "allow links there, then move it again" });
|
|
@@ -2408,12 +2416,13 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2408
2416
|
replacePlugin: (p, repos) => {
|
|
2409
2417
|
const id = p.id;
|
|
2410
2418
|
const clashes = p.skills.filter((s) => {
|
|
2411
|
-
const
|
|
2412
|
-
|
|
2419
|
+
const theirs = treeHash(s.path);
|
|
2420
|
+
const mine = [librarySkillDir(s.name), ...machine.filter((m) => m.movable && m.name === s.name).map((m) => m.path)].map(treeHash);
|
|
2421
|
+
return mine.some((h) => h !== null && h !== theirs);
|
|
2413
2422
|
});
|
|
2414
|
-
if (clashes.length) return failed(`${id} kept:
|
|
2423
|
+
if (clashes.length) return failed(`${id} kept: you have different ${clashes.map((s) => s.name).join(", ")} (in your library or global skills); keep yours, or update or delete ${clashes.length === 1 ? "it" : "them"} first`);
|
|
2415
2424
|
const cant = repos.flatMap(
|
|
2416
|
-
(repo) => p.skills.flatMap((s) => ((why) => why ? [`${s.name} in ${repo}: ${why
|
|
2425
|
+
(repo) => p.skills.flatMap((s) => ((why) => why ? [`${s.name} in ${repo}: ${withoutForceHint(why)}`] : [])(trackBlocker(rootOf(repo), s.name) ?? addRefusal(rootOf(repo), s.name, treeHash(s.path) ?? "")))
|
|
2417
2426
|
);
|
|
2418
2427
|
if (cant.length) return failed(`${id} kept, nothing changed: ${cant.join("; ")}`);
|
|
2419
2428
|
const imported = p.skills.map((s) => ({ name: s.name, status: importSkill(s.path).status }));
|
|
@@ -2464,7 +2473,7 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2464
2473
|
if (s && isDir(s.path) && !notWritable(root, relative(root, dirname2(s.path)).split(sep).join("/"))) removeUntracked(root, name, s.path);
|
|
2465
2474
|
const c = removeSkill(root, name, { force: true });
|
|
2466
2475
|
if (c.action === "skipped") return failed(`${name}: ${c.reason}${from.length ? `; removed from ${from.join(", ")} so far` : ""}`);
|
|
2467
|
-
const repo =
|
|
2476
|
+
const repo = nameOf(root);
|
|
2468
2477
|
from.push(repo);
|
|
2469
2478
|
if (c.reason) left.push(`in ${repo}, ${c.reason}`);
|
|
2470
2479
|
}
|
|
@@ -2511,7 +2520,8 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2511
2520
|
usageIssues: ({ usage, totals = {} }) => {
|
|
2512
2521
|
const repoOf = new Map([...roots].map(([name, root]) => [root, name]));
|
|
2513
2522
|
return usageIssues([...roots.values()], machine, libraryNames, {
|
|
2514
|
-
|
|
2523
|
+
// As doctor counts them: a plugin's copy (plugin:name, kept apart for the Plugins rows) counts for the skill.
|
|
2524
|
+
inProject: (root, skill) => Object.entries(usage[repoOf.get(root)] ?? {}).reduce((n, [key, count]) => n + (skillOf(key) === skill ? count : 0), 0),
|
|
2515
2525
|
total: (skill) => totals[skill] ?? 0,
|
|
2516
2526
|
asUsed: (key) => totals[key] ?? 0,
|
|
2517
2527
|
days: 30
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skilllib",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.6",
|
|
4
4
|
"description": "Manage your agent skills like dependencies. One library for all your skills; each repo gets only the ones it needs, and every agent can see them. Works with Claude Code, Cursor, Codex, Zed and Grok.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-skills",
|