skilllib 1.11.4 → 1.11.5
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
|
|
@@ -2083,7 +2097,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
2083
2097
|
if (git2 === void 0) git2 = gitInfo(root);
|
|
2084
2098
|
const planned = installDirs(enabled).map(abs);
|
|
2085
2099
|
const fromSkillsSh = name in skillsLock;
|
|
2086
|
-
const rank = (e) => (
|
|
2100
|
+
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
2101
|
primary = [...reals].sort((a, b) => rank(a) - rank(b))[0];
|
|
2088
2102
|
}
|
|
2089
2103
|
if (!primary) continue;
|
|
@@ -2103,12 +2117,12 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
2103
2117
|
const needed = wanted.has(e.dir);
|
|
2104
2118
|
if (e.link) {
|
|
2105
2119
|
if (e.real === primary.real) {
|
|
2106
|
-
if (!needed && !dep &&
|
|
2120
|
+
if (!needed && !dep && isShared((git2 ??= gitInfo(root))?.of(rel(e.path)))) continue;
|
|
2107
2121
|
if (!needed) steps.push({ kind: "unlink", path: e.path });
|
|
2108
2122
|
} else steps.push(needed ? { kind: "link", path: e.path, target: primary.path } : { kind: "unlink", path: e.path });
|
|
2109
2123
|
} else {
|
|
2110
2124
|
const differs = e.hash !== primary.hash ? { differs: true } : {};
|
|
2111
|
-
if (!needed &&
|
|
2125
|
+
if (!needed && !chosen && isShared((git2 ??= gitInfo(root))?.of(rel(e.path)))) continue;
|
|
2112
2126
|
steps.push(needed ? { kind: "replace", path: e.path, target: primary.path, ...differs } : { kind: "remove", path: e.path, ...differs });
|
|
2113
2127
|
}
|
|
2114
2128
|
}
|
|
@@ -2148,13 +2162,13 @@ function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
|
|
|
2148
2162
|
}
|
|
2149
2163
|
function gitVisibleSteps(plan, git2 = plan.root ? gitInfo(plan.root) : null) {
|
|
2150
2164
|
if (!plan.root || !git2) return [];
|
|
2151
|
-
const shared = (rel) =>
|
|
2165
|
+
const shared = (rel) => isShared(git2.of(rel));
|
|
2152
2166
|
const dep = readManifest(plan.root).skills[plan.name];
|
|
2153
2167
|
const recordsLink = (s) => !!dep && (s.kind === "link" || s.kind === "replace") && !dep.links?.includes(relativeTo(plan.root, dirname5(s.path)));
|
|
2154
2168
|
return plan.steps.filter((s) => shared(relativeTo(plan.root, s.path)) || recordsLink(s) && shared(MANIFEST_FILE));
|
|
2155
2169
|
}
|
|
2156
|
-
function applyTidy(plan, { git: git2 = "keep"
|
|
2157
|
-
const held = git2 === "keep" ? gitVisibleSteps(plan
|
|
2170
|
+
function applyTidy(plan, { git: git2 = "keep" } = {}) {
|
|
2171
|
+
const held = git2 === "keep" ? gitVisibleSteps(plan) : [];
|
|
2158
2172
|
const applied = [];
|
|
2159
2173
|
for (const step of plan.steps) {
|
|
2160
2174
|
if (held.includes(step) || !entryExists(step.path) || "target" in step && !entryExists(step.target)) continue;
|
|
@@ -2244,7 +2258,7 @@ var gitOf = (root) => {
|
|
|
2244
2258
|
};
|
|
2245
2259
|
var gitNote = (root, paths) => {
|
|
2246
2260
|
const git2 = gitOf(root);
|
|
2247
|
-
return paths.some((p) =>
|
|
2261
|
+
return paths.some((p) => isShared(git2?.of(relativeTo(root, p)))) ? "; git will see it" : "";
|
|
2248
2262
|
};
|
|
2249
2263
|
var agentNames = (ids) => ids.map((id) => harness(id).name).join(", ");
|
|
2250
2264
|
function conflictIssue(c) {
|
|
@@ -2265,6 +2279,7 @@ function conflictIssue(c) {
|
|
|
2265
2279
|
at: rows(c.root, [c.name]),
|
|
2266
2280
|
choices: winners.flatMap((x) => {
|
|
2267
2281
|
const plan = planFor(x.dir);
|
|
2282
|
+
if (!plan?.steps.length) return [];
|
|
2268
2283
|
const visible2 = plan ? gitVisibleSteps(plan).map((s) => describeStep(s, c.root)) : [];
|
|
2269
2284
|
const label = `Keep the ${folderLabel(c.root, x.dir)} copy`;
|
|
2270
2285
|
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 +2345,7 @@ function brokenLinksIn(root, opened) {
|
|
|
2330
2345
|
root,
|
|
2331
2346
|
folders2.map((f) => placeOf(f.real)).filter((p) => p !== "" && p !== ".." && !p.startsWith("../") && !isAbsolute3(p))
|
|
2332
2347
|
);
|
|
2333
|
-
const tracked =
|
|
2348
|
+
const tracked = isShared(git2?.of(placeOf(join13(real, name))));
|
|
2334
2349
|
const recorded = !!own && mends(own, relative4(own, real).split(sep7).join("/"), name);
|
|
2335
2350
|
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
2351
|
return [
|
|
@@ -2463,7 +2478,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2463
2478
|
const dupes = names.filter((n) => libraryNames.has(n) || loadedYours.has(n));
|
|
2464
2479
|
if (!dupes.length) continue;
|
|
2465
2480
|
const others = plugin.skills.length - dupes.length;
|
|
2466
|
-
const pluginName = plugin.id
|
|
2481
|
+
const pluginName = pluginNameOf(plugin.id);
|
|
2467
2482
|
const remove2 = {
|
|
2468
2483
|
label: `Remove the plugin ${plugin.id}`,
|
|
2469
2484
|
hint: plugin.extras.length ? `also removes its ${plugin.extras.join(", ")}` : "restorable with skilllib restore",
|
|
@@ -2520,7 +2535,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2520
2535
|
id: `plugin:${root}:${plugin.id}`,
|
|
2521
2536
|
severity: "problem",
|
|
2522
2537
|
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
|
|
2538
|
+
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
2539
|
choices: repoChoices(plugin, root, inRepo),
|
|
2525
2540
|
at: rows(root, dupes),
|
|
2526
2541
|
plugin: plugin.id
|
|
@@ -2653,7 +2668,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2653
2668
|
}
|
|
2654
2669
|
const drifted = unstuck.filter((s) => s.state === "out of sync" && knownPin(s));
|
|
2655
2670
|
if (drifted.length) {
|
|
2656
|
-
const seen = drifted.some((s) =>
|
|
2671
|
+
const seen = drifted.some((s) => isShared(gitOf(root)?.of(relativeTo(root, s.path))));
|
|
2657
2672
|
const yours = drifted.some((s) => untrackable(s));
|
|
2658
2673
|
const install = {
|
|
2659
2674
|
label: "Install the pinned versions",
|
|
@@ -2701,7 +2716,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2701
2716
|
choices: [
|
|
2702
2717
|
{
|
|
2703
2718
|
label: "Update to the newest versions",
|
|
2704
|
-
hint:
|
|
2719
|
+
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
2720
|
run: () => summarize(where, "updated", updateProject(root, behind.map((s) => s.name)))
|
|
2706
2721
|
}
|
|
2707
2722
|
]
|
|
@@ -2784,7 +2799,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2784
2799
|
if (partial.length) {
|
|
2785
2800
|
const missing2 = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
|
|
2786
2801
|
const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
|
|
2787
|
-
const manifestShared = partial.some((s) => s.managed) &&
|
|
2802
|
+
const manifestShared = partial.some((s) => s.managed) && isShared(gitOf(root)?.of(MANIFEST_FILE));
|
|
2788
2803
|
const run = (allowTracked) => () => {
|
|
2789
2804
|
if (allowTracked) allowTrackedLinks(root);
|
|
2790
2805
|
const r = linkAll(root, { allowTracked, names: partial.map((s) => s.name) });
|
|
@@ -2813,7 +2828,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2813
2828
|
}
|
|
2814
2829
|
const differing = new Set(tidy2.conflicts.map((c) => c.name));
|
|
2815
2830
|
const git2 = gitOf(root);
|
|
2816
|
-
const committed = (s) =>
|
|
2831
|
+
const committed = (s) => isShared(git2?.of(relativeTo(root, s.path)));
|
|
2817
2832
|
for (const s of unstuck) {
|
|
2818
2833
|
if (isLink(s.path) || differing.has(s.name)) continue;
|
|
2819
2834
|
if (s.state === "local only" && (committed(s) || untrackable(s))) {
|
|
@@ -2866,7 +2881,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2866
2881
|
});
|
|
2867
2882
|
} else if (s.state === "untracked, differs from library" || s.state === "repo skill, differs from library") {
|
|
2868
2883
|
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`;
|
|
2884
|
+
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
2885
|
issues.push({
|
|
2871
2886
|
id: `differs:${root}:${s.name}`,
|
|
2872
2887
|
severity: "suggestion",
|
|
@@ -3123,7 +3138,7 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
|
|
|
3123
3138
|
}
|
|
3124
3139
|
if (uses.asUsed)
|
|
3125
3140
|
for (const p of installedPlugins().filter((x) => x.agent === "claude-code" && x.on && x.skills.length)) {
|
|
3126
|
-
const name = p.id
|
|
3141
|
+
const name = pluginNameOf(p.id);
|
|
3127
3142
|
if (p.skills.some((path) => uses.asUsed(`${name}:${basename6(path)}`) > 0)) continue;
|
|
3128
3143
|
issues.push({
|
|
3129
3144
|
id: `plugin-unused:${installId(p)}`,
|
|
@@ -3203,7 +3218,7 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
3203
3218
|
const loadedBy = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
|
|
3204
3219
|
const behind = s.state.endsWith("update available");
|
|
3205
3220
|
const gitState = git2?.of(relativeTo(root, s.path));
|
|
3206
|
-
const repo = !s.managed && (s.state.startsWith("repo skill") || gitState
|
|
3221
|
+
const repo = !s.managed && (s.state.startsWith("repo skill") || isShared(gitState));
|
|
3207
3222
|
const usual = s.state === "ok" || s.state === "local only" || repo && s.state !== "from npx skills";
|
|
3208
3223
|
const attrs = {
|
|
3209
3224
|
source: s.managed ? "library" : repo ? "repo" : "local",
|
|
@@ -3367,7 +3382,14 @@ function transcriptFiles(sinceMs) {
|
|
|
3367
3382
|
return list2(base).flatMap((project) => {
|
|
3368
3383
|
const dir = join14(base, project);
|
|
3369
3384
|
if (!statSync5(dir, { throwIfNoEntry: false })?.isDirectory()) return [];
|
|
3370
|
-
return list2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join14(dir, f)).filter((f) =>
|
|
3385
|
+
return list2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join14(dir, f)).filter((f) => {
|
|
3386
|
+
try {
|
|
3387
|
+
const st = statSync5(f);
|
|
3388
|
+
return st.isFile() && st.mtimeMs >= sinceMs;
|
|
3389
|
+
} catch {
|
|
3390
|
+
return false;
|
|
3391
|
+
}
|
|
3392
|
+
});
|
|
3371
3393
|
});
|
|
3372
3394
|
}
|
|
3373
3395
|
async function scanUsage(days) {
|
|
@@ -3430,7 +3452,7 @@ function moveGlobalSkills(skills, roots, {
|
|
|
3430
3452
|
const elsewhere = blindWithoutGlobal(name, copies, knownProjects().filter((p) => !roots.some((r) => realPath(r) === realPath(p))));
|
|
3431
3453
|
if (elsewhere.length) return { name, moved: false, reason: `nothing moved: ${blindSaid(name, elsewhere, label)}`, changes: [] };
|
|
3432
3454
|
const hash = treeHash(copies[0].path) ?? "";
|
|
3433
|
-
const cant = roots.flatMap((root) => ((b) => b ? [`${label(root)} (${b
|
|
3455
|
+
const cant = roots.flatMap((root) => ((b) => b ? [`${label(root)} (${withoutForceHint(b)})`] : [])(trackBlocker(root, name) ?? addRefusal(root, name, hash)));
|
|
3434
3456
|
if (cant.length) return { name, moved: false, reason: `nothing moved: it can't be added to ${cant.join(", ")}`, changes: [] };
|
|
3435
3457
|
const imported = importSkill(copies[0].path).status;
|
|
3436
3458
|
if (imported === "exists")
|
|
@@ -3438,7 +3460,7 @@ function moveGlobalSkills(skills, roots, {
|
|
|
3438
3460
|
if (group) recordOrigin(name, `group: ${group}`);
|
|
3439
3461
|
const changes = roots.map((root) => [root, addSkill(root, name, { allowTracked })]);
|
|
3440
3462
|
const skipped = changes.filter(([, c]) => c.action === "skipped");
|
|
3441
|
-
const why = (reason = "") => reason
|
|
3463
|
+
const why = (reason = "") => withoutForceHint(reason) === reason ? reason : `${withoutForceHint(reason)}: move or remove it there first`;
|
|
3442
3464
|
const kept = imported === "added" ? `; it's in Your skills now` : "";
|
|
3443
3465
|
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
3466
|
const blindIn = changes.flatMap(([root, c]) => ((b) => b.length ? [{ root, c, b }] : [])(blindWithoutGlobal(name, copies, [root])));
|
|
@@ -3471,6 +3493,7 @@ var FLAGS = {
|
|
|
3471
3493
|
keep: { type: "string" },
|
|
3472
3494
|
to: { type: "string", multiple: true }
|
|
3473
3495
|
};
|
|
3496
|
+
var VALUE_FLAGS = Object.entries(FLAGS).flatMap(([name, f]) => f.type === "string" ? [`--${name}`] : []);
|
|
3474
3497
|
function parseArgs(argv) {
|
|
3475
3498
|
const { values: v, positionals } = parseFlags({ args: argv, options: FLAGS, allowPositionals: true, strict: true });
|
|
3476
3499
|
const days = Number(v.days ?? USAGE_DAYS);
|
|
@@ -3688,7 +3711,7 @@ async function list(args) {
|
|
|
3688
3711
|
}
|
|
3689
3712
|
function teamStateIn(root, extraDirs = []) {
|
|
3690
3713
|
const git2 = gitInfo(root, [...new Set(extraDirs)]);
|
|
3691
|
-
const committed = (path) =>
|
|
3714
|
+
const committed = (path) => isShared(git2?.of(relativeTo(root, path)));
|
|
3692
3715
|
const asRepo = {
|
|
3693
3716
|
"local only": "repo skill",
|
|
3694
3717
|
"untracked copy of library skill": "repo skill, in library",
|
|
@@ -4231,7 +4254,8 @@ function tidy(args) {
|
|
|
4231
4254
|
for (const s of report.skipped.filter((x) => !names.length || names.includes(x.name))) info(dim(`- ${label} \xB7 ${s.name}: skipped, ${s.reason}`));
|
|
4232
4255
|
}
|
|
4233
4256
|
if (args.dryRun) info(dim("\nNothing changed (--dry-run)."));
|
|
4234
|
-
else if (changed) info(dim(
|
|
4257
|
+
else if (changed) info(dim(`
|
|
4258
|
+
Replaced copies are in ${tildify(join15(skilllibHome(), "tidy-backup"))}; \`skilllib restore\` puts them back.`));
|
|
4235
4259
|
if (held) {
|
|
4236
4260
|
process.exitCode = 1;
|
|
4237
4261
|
info(dim(`${held} change${held === 1 ? "" : "s"} held back because git would see them. Re-run with --allow-git to make them.`));
|
|
@@ -4316,6 +4340,7 @@ export {
|
|
|
4316
4340
|
projectStatus,
|
|
4317
4341
|
linkEverywhere,
|
|
4318
4342
|
relinkDependency,
|
|
4343
|
+
withoutForceHint,
|
|
4319
4344
|
addRefusal,
|
|
4320
4345
|
addSkill,
|
|
4321
4346
|
removeSkill,
|
|
@@ -4329,6 +4354,7 @@ export {
|
|
|
4329
4354
|
listBackups,
|
|
4330
4355
|
movedTogether,
|
|
4331
4356
|
restoreBackup,
|
|
4357
|
+
isShared,
|
|
4332
4358
|
gitInfo,
|
|
4333
4359
|
relativeTo,
|
|
4334
4360
|
installedPlugins,
|
|
@@ -4351,6 +4377,7 @@ export {
|
|
|
4351
4377
|
scanUsage,
|
|
4352
4378
|
skillOf,
|
|
4353
4379
|
moveGlobalSkills,
|
|
4380
|
+
VALUE_FLAGS,
|
|
4354
4381
|
parseArgs,
|
|
4355
4382
|
projectOfFactory,
|
|
4356
4383
|
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-TE2EDZ7L.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-GFQUHPEC.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-GFQUHPEC.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-TE2EDZ7L.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",
|
|
@@ -2082,7 +2084,7 @@ function localSkill(root, s, git) {
|
|
|
2082
2084
|
};
|
|
2083
2085
|
const state = git ? git.of(relativeTo(root, s.path)) : null;
|
|
2084
2086
|
const others = otherCopies(root, s.name, s.location);
|
|
2085
|
-
const own = other[s.state]?.source === "untracked" && (state
|
|
2087
|
+
const own = other[s.state]?.source === "untracked" && isShared(state) ? { source: "repo" } : {};
|
|
2086
2088
|
return {
|
|
2087
2089
|
name: s.name,
|
|
2088
2090
|
source: "lib",
|
|
@@ -2247,6 +2249,7 @@ function said3(c, repo) {
|
|
|
2247
2249
|
}
|
|
2248
2250
|
function realOps(roots, backups, machine, libraryNames) {
|
|
2249
2251
|
const rootOf = (repo) => roots.get(repo);
|
|
2252
|
+
const nameOf = (root) => [...roots].find(([, r]) => r === root)?.[0] ?? basename(root);
|
|
2250
2253
|
const find = (repo, name) => projectStatus(rootOf(repo)).find((s) => s.name === name);
|
|
2251
2254
|
const result = (c, repo) => c.action === "skipped" ? failed(said3(c, repo)) : said3(c, repo);
|
|
2252
2255
|
const copyDir = (repo, name) => readManifest(rootOf(repo)).skills[name]?.dir ?? PROJECT_SKILLS_DIR;
|
|
@@ -2304,6 +2307,7 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2304
2307
|
};
|
|
2305
2308
|
const linkIn = (repo, name, allow) => {
|
|
2306
2309
|
const s = find(repo, name);
|
|
2310
|
+
if (!s) throw new Error(`${name} isn't in ${repo} any more`);
|
|
2307
2311
|
return s.managed ? relinkDependency(rootOf(repo), name, { allowTracked: allow }) : linkEverywhere(rootOf(repo), name, s.location, { allowTracked: allow });
|
|
2308
2312
|
};
|
|
2309
2313
|
const linkSaid = (repo, name, r) => {
|
|
@@ -2315,7 +2319,7 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2315
2319
|
const orForce = (c, repo, label, force) => (
|
|
2316
2320
|
// Whatever --force would get past (edits, a pin from another library, another copy in the way): offer it here.
|
|
2317
2321
|
c.action === "skipped" && !!c.reason?.includes("--force") ? {
|
|
2318
|
-
message: `${c.name} in ${repo}: ${c.reason
|
|
2322
|
+
message: `${c.name} in ${repo}: ${withoutForceHint(c.reason)}`,
|
|
2319
2323
|
then: { label, preview: `What's there now in ${repo} goes to Settings \u203A Backups first.`, run: () => result(force(), repo) }
|
|
2320
2324
|
} : result(c, repo)
|
|
2321
2325
|
);
|
|
@@ -2329,7 +2333,7 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2329
2333
|
remove: (repo, name) => {
|
|
2330
2334
|
const s = find(repo, name);
|
|
2331
2335
|
if (!s) return failed(`${name} isn't in ${repo}`);
|
|
2332
|
-
const committed = !s.managed &&
|
|
2336
|
+
const committed = !s.managed && isShared(gitInfo(rootOf(repo))?.of(relativeTo(rootOf(repo), s.path)));
|
|
2333
2337
|
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
2338
|
const others = otherCopies(rootOf(repo), name, s.location);
|
|
2335
2339
|
const left = (r) => typeof r === "string" && others.length ? `${r}; the copy in ${others.join(", ")} stays` : r;
|
|
@@ -2340,7 +2344,7 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2340
2344
|
const c = updateProject(rootOf(repo), [name])[0];
|
|
2341
2345
|
return c ? orForce(c, repo, "Update anyway", () => updateProject(rootOf(repo), [name], { force: true })[0]) : `${name} is up to date`;
|
|
2342
2346
|
},
|
|
2343
|
-
copyToLibrary: (repo, name) => toLibrary(find(repo, name)
|
|
2347
|
+
copyToLibrary: (repo, name) => ((s) => s ? toLibrary(s.path) : failed(`${name} isn't in ${repo} any more`))(find(repo, name)),
|
|
2344
2348
|
deleteGlobal: (m) => {
|
|
2345
2349
|
if (!entryExists(m.path)) return `${tildify(m.path)} is already gone`;
|
|
2346
2350
|
const others = machine.filter((o) => o.name === m.name && o.path !== m.path && o.kind !== "plugin" && !o.broken);
|
|
@@ -2349,9 +2353,9 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2349
2353
|
if (!m.broken) {
|
|
2350
2354
|
const readers = [.../* @__PURE__ */ new Set([...m.agents, ...dependents.flatMap((o) => o.harnesses)])].filter((id) => !stays.some((o) => o.harnesses.includes(id)));
|
|
2351
2355
|
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,
|
|
2356
|
+
if (blind.length) return failed(`${m.name} kept: ${blindSaid(m.name, blind, nameOf)}`);
|
|
2353
2357
|
}
|
|
2354
|
-
const left = m.broken ? [] : strandedLinks(m.name, [{ path: m.path, links: [...m.links, ...dependents.map((o) => o.path)] }], [...roots.values(), ...knownProjects()],
|
|
2358
|
+
const left = m.broken ? [] : strandedLinks(m.name, [{ path: m.path, links: [...m.links, ...dependents.map((o) => o.path)] }], [...roots.values(), ...knownProjects()], nameOf);
|
|
2355
2359
|
const r = deleteGlobal(m.path, [...m.links, ...dependents.map((o) => o.path)]);
|
|
2356
2360
|
if (!r.ok) return failed(`${m.name}: ${r.reason}`);
|
|
2357
2361
|
if (m.broken) return `Removed the broken link ${m.name}`;
|
|
@@ -2361,7 +2365,7 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2361
2365
|
keepGlobal: (name, keep) => (setKeepGlobal([name], keep), keep ? `${name} marked as global on purpose` : `${name} is no longer marked as global on purpose`),
|
|
2362
2366
|
moveGlobal: (skills, repos, group) => {
|
|
2363
2367
|
const repoRoots = repos.map(rootOf);
|
|
2364
|
-
const label = (root) => repos[repoRoots.indexOf(root)] ??
|
|
2368
|
+
const label = (root) => repos[repoRoots.indexOf(root)] ?? nameOf(root);
|
|
2365
2369
|
const picked = new Set(skills.map((m) => m.name));
|
|
2366
2370
|
const sourced = machine.filter((o) => picked.has(o.name) && o.movable && !o.broken);
|
|
2367
2371
|
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 +2412,13 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2408
2412
|
replacePlugin: (p, repos) => {
|
|
2409
2413
|
const id = p.id;
|
|
2410
2414
|
const clashes = p.skills.filter((s) => {
|
|
2411
|
-
const
|
|
2412
|
-
|
|
2415
|
+
const theirs = treeHash(s.path);
|
|
2416
|
+
const mine = [librarySkillDir(s.name), ...machine.filter((m) => m.movable && m.name === s.name).map((m) => m.path)].map(treeHash);
|
|
2417
|
+
return mine.some((h) => h !== null && h !== theirs);
|
|
2413
2418
|
});
|
|
2414
|
-
if (clashes.length) return failed(`${id} kept:
|
|
2419
|
+
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
2420
|
const cant = repos.flatMap(
|
|
2416
|
-
(repo) => p.skills.flatMap((s) => ((why) => why ? [`${s.name} in ${repo}: ${why
|
|
2421
|
+
(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
2422
|
);
|
|
2418
2423
|
if (cant.length) return failed(`${id} kept, nothing changed: ${cant.join("; ")}`);
|
|
2419
2424
|
const imported = p.skills.map((s) => ({ name: s.name, status: importSkill(s.path).status }));
|
|
@@ -2464,7 +2469,7 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2464
2469
|
if (s && isDir(s.path) && !notWritable(root, relative(root, dirname2(s.path)).split(sep).join("/"))) removeUntracked(root, name, s.path);
|
|
2465
2470
|
const c = removeSkill(root, name, { force: true });
|
|
2466
2471
|
if (c.action === "skipped") return failed(`${name}: ${c.reason}${from.length ? `; removed from ${from.join(", ")} so far` : ""}`);
|
|
2467
|
-
const repo =
|
|
2472
|
+
const repo = nameOf(root);
|
|
2468
2473
|
from.push(repo);
|
|
2469
2474
|
if (c.reason) left.push(`in ${repo}, ${c.reason}`);
|
|
2470
2475
|
}
|
|
@@ -2511,7 +2516,8 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2511
2516
|
usageIssues: ({ usage, totals = {} }) => {
|
|
2512
2517
|
const repoOf = new Map([...roots].map(([name, root]) => [root, name]));
|
|
2513
2518
|
return usageIssues([...roots.values()], machine, libraryNames, {
|
|
2514
|
-
|
|
2519
|
+
// As doctor counts them: a plugin's copy (plugin:name, kept apart for the Plugins rows) counts for the skill.
|
|
2520
|
+
inProject: (root, skill) => Object.entries(usage[repoOf.get(root)] ?? {}).reduce((n, [key, count]) => n + (skillOf(key) === skill ? count : 0), 0),
|
|
2515
2521
|
total: (skill) => totals[skill] ?? 0,
|
|
2516
2522
|
asUsed: (key) => totals[key] ?? 0,
|
|
2517
2523
|
days: 30
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skilllib",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.5",
|
|
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",
|