skilllib 1.12.0 → 1.12.2

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
@@ -195,7 +195,7 @@ A skill's pin goes in one of two places:
195
195
  - **`skilllib.json` in the repo (shared).** Commit it with the skill folders, and teammates get the same skills. This is where pins go when the repo already has a `skilllib.json`.
196
196
  - **Your personal pins (`~/.skilllib/pins/`).** One file per repo, matched by its `origin` remote, so every clone and worktree of the repo shares it. skilllib keeps the copies and links out of git through the repo's `.git/info/exclude` (only your clone; the repo's `.gitignore` doesn't change). Nothing shows up in `git status`. This is where pins go when the repo has no `skilllib.json`.
197
197
 
198
- `skilllib add <name> --personal` or `--shared` picks the place, and moves an existing pin without changing its version. In the app, a library skill's actions offer *Make it personal* or *Pin it for the team*, and its tag says `lib v2 · personal`. A copy git already has can't become personal. A skill pinned in both places uses the repo's pin; doctor offers to drop yours (a choice: yours would keep the skill if the team unpinned it).
198
+ `skilllib add <name> --personal` or `--shared` picks the place, and moves an existing pin without changing its version. In the app, a library skill's actions offer *Make it personal* or *Pin it for the team*, and its tag says `personal v2`. A copy git already has can't become personal. A skill pinned in both places uses the repo's pin; doctor offers to drop yours (a choice: yours would keep the skill if the team unpinned it).
199
199
 
200
200
  **On another machine:** keep `~/.skilllib/pins/` and `~/.skilllib/library/` in git (a dotfiles repo, say). Clone a repo there and run `skilllib sync`: your personal skills come back, out of git again. A repo without a remote keeps its pins by folder, so they stay on that machine until it gets one: adding a remote (or renaming it) keeps them. Only a repo's top folder can have personal pins, not a monorepo package's.
201
201
 
@@ -235,7 +235,7 @@ function installDirs(enabled, keep) {
235
235
  import { execFileSync } from "child_process";
236
236
  import { createHash as createHash2 } from "crypto";
237
237
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
238
- import { basename as basename2, dirname as dirname2, join as join5, resolve as resolve3 } from "path";
238
+ import { basename as basename2, dirname as dirname2, isAbsolute, join as join5, relative as relative2, resolve as resolve3, sep as sep3 } from "path";
239
239
 
240
240
  // src/versions.ts
241
241
  import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
@@ -425,7 +425,7 @@ function writeManifest(root, manifest) {
425
425
  for (const [name, dep] of Object.entries(readPins(file).skills)) if (onDisk[name] && manifest.skills[name] && !manifest.skills[name].personal) mine[name] = dep;
426
426
  if (Object.keys(mine).length) writeChanged(file, { ...pinsHeader(repoOf(root)), skills: sorted(mine) });
427
427
  else rmSync3(file, { force: true });
428
- excludePersonal(root, mine);
428
+ excludePersonal(root, Object.fromEntries(Object.entries(mine).filter(([name]) => !shared[name])));
429
429
  }
430
430
  function writeChanged(path, data) {
431
431
  try {
@@ -478,7 +478,7 @@ function repoOf(root) {
478
478
  const seen = changedAt(config);
479
479
  let repo = null;
480
480
  if (out2[0] === "") {
481
- const urls = (gitIn(root, ["config", "--get-regexp", "^remote\\..*\\.url$"]) ?? "").split("\n").filter(Boolean).map((l) => l.split(" "));
481
+ const urls = (gitIn(root, ["config", "--get-regexp", "^remote\\..*\\.url$"]) ?? "").split("\n").filter(Boolean).map((l) => [l.slice(0, l.indexOf(" ")), l.slice(l.indexOf(" ") + 1)]);
482
482
  const url = (urls.find(([k]) => k === "remote.origin.url") ?? urls[0])?.[1];
483
483
  const remote = url ? normalizeRemote(url) : void 0;
484
484
  repo = { file: pinsFileFor(root, remote, folder), folder, common, ...remote ? { remote } : {}, root };
@@ -530,7 +530,15 @@ function excludePersonal(root, skills = Object.fromEntries(Object.entries(readMa
530
530
  const end = start === -1 ? -1 : lines.indexOf(EXCLUDE_END, start);
531
531
  const rest = start === -1 ? lines : [...lines.slice(0, start), ...lines.slice(end === -1 ? start + 1 : end + 1)];
532
532
  while (rest.length && rest[rest.length - 1] === "") rest.pop();
533
- const paths = Object.entries(skills).flatMap(([name, d]) => [d.dir ?? PROJECT_SKILLS_DIR, ...d.links ?? []].map((dir) => `/${dir}/${name.replace(/[[\]*?\\]/g, "\\$&")}`));
533
+ const top = realNative(root) ?? root;
534
+ const places = (dir) => {
535
+ const real = realNative(join5(root, dir));
536
+ const rel = real ? relative2(top, real).split(sep3).join("/") : "";
537
+ return rel && rel !== ".." && !rel.startsWith("../") && !isAbsolute(rel) && rel !== dir ? [dir, rel] : [dir];
538
+ };
539
+ const paths = Object.entries(skills).flatMap(
540
+ ([name, d]) => [d.dir ?? PROJECT_SKILLS_DIR, ...d.links ?? []].flatMap(places).map((dir) => `/${dir}/${name.replace(/[[\]*?\\]/g, "\\$&")}`)
541
+ );
534
542
  const block = paths.length ? [EXCLUDE_START, ...[...new Set(paths)].sort(), EXCLUDE_END] : [];
535
543
  const next = [...rest, ...rest.length && block.length ? [""] : [], ...block].join("\n") + (rest.length || block.length ? "\n" : "");
536
544
  if (next === text) return;
@@ -688,11 +696,11 @@ import { join as join9 } from "path";
688
696
  // src/library.ts
689
697
  import { execFileSync as execFileSync2 } from "child_process";
690
698
  import { cpSync as cpSync2, existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync4, readlinkSync as readlinkSync2, renameSync as renameSync3, rmSync as rmSync4, symlinkSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
691
- import { basename as basename4, dirname as dirname3, isAbsolute, join as join8, posix, relative as relative2, resolve as resolve6, sep as sep4 } from "path";
699
+ import { basename as basename4, dirname as dirname3, isAbsolute as isAbsolute2, join as join8, posix, relative as relative3, resolve as resolve6, sep as sep5 } from "path";
692
700
 
693
701
  // src/sources.ts
694
702
  import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readlinkSync } from "fs";
695
- import { basename as basename3, join as join7, resolve as resolve5, sep as sep3 } from "path";
703
+ import { basename as basename3, join as join7, resolve as resolve5, sep as sep4 } from "path";
696
704
  import { tmpdir } from "os";
697
705
  function skillsShLock() {
698
706
  return readJson(join7(userHome(), ".agents", ".skill-lock.json"))?.skills ?? {};
@@ -884,7 +892,7 @@ function recordOrigin(name, origin) {
884
892
  writeState(originsFile(), { ...origins, [name]: origin });
885
893
  }
886
894
  function originFor(dir) {
887
- const under = (path, folder) => path.startsWith((realpathOrNull(folder) ?? folder) + sep3);
895
+ const under = (path, folder) => path.startsWith((realpathOrNull(folder) ?? folder) + sep4);
888
896
  const real = realpathOrNull(dir) ?? dir;
889
897
  const temps = [tmpdir(), "/tmp", "/var/tmp"];
890
898
  const homeInTemp = temps.some((t) => under(realpathOrNull(userHome()) ?? userHome(), t));
@@ -922,7 +930,7 @@ function notWritable(root, dir) {
922
930
  p = join8(p, part);
923
931
  if (!entryExists(p)) return null;
924
932
  const real = realpathOrNull(p);
925
- if (!isDir(p) || real === null || real !== top && !real.startsWith(top + sep4)) return relative2(root, p).split(sep4).join("/");
933
+ if (!isDir(p) || real === null || real !== top && !real.startsWith(top + sep5)) return relative3(root, p).split(sep5).join("/");
926
934
  }
927
935
  return null;
928
936
  }
@@ -973,7 +981,7 @@ function syncBlocker(root, name, dep = readManifest(root).skills[name]) {
973
981
  }
974
982
  function linkDir(target, link2) {
975
983
  if (process.platform === "win32") symlinkSync(target, link2, "junction");
976
- else symlinkSync(relative2(dirname3(link2), target), link2);
984
+ else symlinkSync(relative3(dirname3(link2), target), link2);
977
985
  }
978
986
  function entryExists(path) {
979
987
  return isLink(path) || existsSync7(path);
@@ -1005,7 +1013,7 @@ function lostAt(path, gone) {
1005
1013
  let last = null;
1006
1014
  for (let hop = 0; hop < 40; hop++) {
1007
1015
  const here = canon(at);
1008
- if (moved.some((m) => key(here) === m || key(here).startsWith(m + sep4))) return last ?? here;
1016
+ if (moved.some((m) => key(here) === m || key(here).startsWith(m + sep5))) return last ?? here;
1009
1017
  if (!isLink(here)) return existsSync7(join8(here, "SKILL.md")) ? null : here;
1010
1018
  last = here;
1011
1019
  at = resolve6(dirname3(here), readlinkSync2(here).replace(/^\\\\\?\\(UNC\\)?/, (_m, unc) => unc ? "\\\\" : ""));
@@ -1019,7 +1027,7 @@ function blindWithoutGlobal(name, copies, roots, nestedOf = (root) => nestedEntr
1019
1027
  const holds = (root) => ALL_PROJECT_DIRS.some((d) => reposChoice(root, d) && entryExists(join8(root, d, name))) || pinsSkill(root, name);
1020
1028
  const keepsTop = (root, id) => harness(id).projectDirs.some((d) => existsSync7(join8(root, d, name, "SKILL.md")) && survives(join8(root, d, name), gone));
1021
1029
  const shown = (root, p) => {
1022
- const under = (dir) => ((d) => p.startsWith(d + sep4) ? relative2(d, p).split(sep4).join("/") : null)(realNative(dir) ?? dir);
1030
+ const under = (dir) => ((d) => p.startsWith(d + sep5) ? relative3(d, p).split(sep5).join("/") : null)(realNative(dir) ?? dir);
1023
1031
  return under(root) ?? ((h) => h === null ? p : `~/${h}`)(under(userHome()));
1024
1032
  };
1025
1033
  const blindIn = (root) => enabled.flatMap((id) => {
@@ -1047,16 +1055,16 @@ function blindWithoutGlobal(name, copies, roots, nestedOf = (root) => nestedEntr
1047
1055
  const had = byWho.get(k);
1048
1056
  if (!had || !(had.link || had.folder || had.shared) && (b.link || b.folder || b.shared)) byWho.set(k, b);
1049
1057
  }
1050
- const within = (d) => unique.some((r) => d === r || d.startsWith(r + sep4));
1058
+ const within = (d) => unique.some((r) => d === r || d.startsWith(r + sep5));
1051
1059
  const home2 = realNative(userHome()) ?? userHome();
1052
- const belowHome = (d) => (realNative(d) ?? d) !== home2 && !home2.startsWith((realNative(d) ?? d) + sep4);
1060
+ const belowHome = (d) => (realNative(d) ?? d) !== home2 && !home2.startsWith((realNative(d) ?? d) + sep5);
1053
1061
  const gitTop = (from) => {
1054
1062
  for (let d = from; d !== dirname3(d) && belowHome(d); d = dirname3(d)) if (existsSync7(join8(d, ".git"))) return d;
1055
1063
  return null;
1056
1064
  };
1057
1065
  const servedAbove = (b) => {
1058
1066
  const top = gitTop(b.root);
1059
- const inside = (d) => belowHome(d) && (top ? d === top || d.startsWith(top + sep4) : within(d));
1067
+ const inside = (d) => belowHome(d) && (top ? d === top || d.startsWith(top + sep5) : within(d));
1060
1068
  for (let d = dirname3(b.root); inside(d) && d !== dirname3(d); d = dirname3(d)) if (keepsTop(d, b.id)) return true;
1061
1069
  return false;
1062
1070
  };
@@ -1073,7 +1081,7 @@ function strandedLinks(name, copies, roots, label = basename4) {
1073
1081
  ).map(
1074
1082
  (d) => `${d}/${name} in ${label(root)}`
1075
1083
  ),
1076
- ...nestedEntries(root).filter((n) => n.name === name && isLink(n.path) && !survives(n.path, gone) && once(n.path)).map((n) => `${relative2(root, n.path).split(sep4).join("/")} in ${label(root)}`)
1084
+ ...nestedEntries(root).filter((n) => n.name === name && isLink(n.path) && !survives(n.path, gone) && once(n.path)).map((n) => `${relative3(root, n.path).split(sep5).join("/")} in ${label(root)}`)
1077
1085
  ]);
1078
1086
  }
1079
1087
  function copiesDiffer(paths) {
@@ -1082,7 +1090,7 @@ function copiesDiffer(paths) {
1082
1090
  function blindSaid(name, blind, label = basename4) {
1083
1091
  const who = blind.map((b) => `${harness(b.id).name} in ${label(b.root)}`).join(", ");
1084
1092
  const folders2 = [...new Set(blind.flatMap((b) => b.folder ? [`${b.folder} in ${label(b.root)}`] : []))];
1085
- const links = [...new Set(blind.flatMap((b) => b.link ? [b.link.startsWith("~") || isAbsolute(b.link) ? b.link : `${b.link} in ${label(b.root)}`] : []))];
1093
+ const links = [...new Set(blind.flatMap((b) => b.link ? [b.link.startsWith("~") || isAbsolute2(b.link) ? b.link : `${b.link} in ${label(b.root)}`] : []))];
1086
1094
  const sharedBy = /* @__PURE__ */ new Map();
1087
1095
  for (const b of blind.filter((x) => x.shared)) {
1088
1096
  if (!sharedBy.has(b.shared.to)) sharedBy.set(b.shared.to, { to: b.shared.to, where: /* @__PURE__ */ new Set(), dirs: /* @__PURE__ */ new Set() });
@@ -1178,7 +1186,7 @@ function projectStatus(root) {
1178
1186
  }
1179
1187
  for (const [name, paths] of byName) {
1180
1188
  const real = paths.find((p) => !isLink(p)) ?? paths[0];
1181
- const location = relative2(root, join8(real, "..")).split(sep4).join("/");
1189
+ const location = relative3(root, join8(real, "..")).split(sep5).join("/");
1182
1190
  const source = lock[name]?.source;
1183
1191
  unmanaged.push({
1184
1192
  name,
@@ -1226,7 +1234,7 @@ function nestedEntries(root, { depth = PACKAGE_DEPTH, enabled = enabledHarnesses
1226
1234
  const top = realpathOrNull(root) ?? root;
1227
1235
  const found = packageSkillFolders(root, subs ?? packageDirs(root, depth)).flatMap(
1228
1236
  ({ sub, skillsDir, location }) => skillDirsIn(join8(sub, skillsDir)).flatMap((path) => {
1229
- if (isLink(path) && (realpathOrNull(path) ?? "").startsWith(top + sep4)) return [];
1237
+ if (isLink(path) && (realpathOrNull(path) ?? "").startsWith(top + sep5)) return [];
1230
1238
  const name = basename4(path);
1231
1239
  const readers = NESTED_READERS.filter(([d]) => realpathOrNull(join8(sub, d, name)) === realpathOrNull(path)).flatMap(([, r]) => r);
1232
1240
  return [{ name, path, location, skillsDir, readers: enabled.filter((id) => readers.includes(id)) }];
@@ -1257,7 +1265,7 @@ function packageSkillFolders(root, subs = packageDirs(root)) {
1257
1265
  return subs.flatMap(
1258
1266
  (sub) => NESTED_READERS.flatMap(([skillsDir]) => {
1259
1267
  if (!existsSync7(join8(sub, skillsDir))) return [];
1260
- const location = relative2(root, join8(sub, skillsDir)).split(sep4).join("/");
1268
+ const location = relative3(root, join8(sub, skillsDir)).split(sep5).join("/");
1261
1269
  return reposChoice(root, location) ? [{ sub, skillsDir, location }] : [];
1262
1270
  })
1263
1271
  );
@@ -1387,8 +1395,8 @@ function addSkill(root, name, { force = false, version, allowTracked = false, pe
1387
1395
  if (moving && !keep) return { name, action: "skipped", reason: `your library has no copy of the version it pins, so its pin can't move as it is: skilllib update ${name} first` };
1388
1396
  const target = keep ?? (version === void 0 ? latestVersion(name) : getVersion(name, version));
1389
1397
  if (!target) return { name, action: "skipped", reason: version === void 0 ? "not in the library" : `version ${version} doesn't exist` };
1390
- const plan = addPlan(root, name, target.hash, force);
1391
- if ("reason" in plan) return { name, action: "skipped", reason: plan.reason };
1398
+ const plan = addPlan(root, name, target.hash, moving ? false : force);
1399
+ if ("reason" in plan) return { name, action: "skipped", reason: moving ? `${withoutForceHint(plan.reason)}, so its pin can't move: save the edits to your library first` : plan.reason };
1392
1400
  const { manifest, recorded, primary, wantedLinks, to, local } = plan;
1393
1401
  const mine = pinsWhere(root, recorded, personal);
1394
1402
  if (typeof mine === "string") return { name, action: "skipped", reason: mine };
@@ -1636,7 +1644,7 @@ function stash(dir, bucket) {
1636
1644
  return target;
1637
1645
  }
1638
1646
  function removeUntracked(root, name, path) {
1639
- const stuck = notWritable(root, relative2(root, dirname3(path)));
1647
+ const stuck = notWritable(root, relative3(root, dirname3(path)));
1640
1648
  if (stuck) throw new Error(`${stuck} isn't a folder in the repo: skilllib leaves what's in it alone`);
1641
1649
  const to = stash(path, "trash");
1642
1650
  unlinkEverywhere(root, name);
@@ -1801,12 +1809,12 @@ function refreshAgentSkill() {
1801
1809
  // src/commands.ts
1802
1810
  import { existsSync as existsSync14, readFileSync as readFileSync7 } from "fs";
1803
1811
  import { parseArgs as parseFlags } from "util";
1804
- import { basename as basename10, join as join15, resolve as resolve8, sep as sep8 } from "path";
1812
+ import { basename as basename10, join as join15, resolve as resolve8, sep as sep9 } from "path";
1805
1813
 
1806
1814
  // src/git.ts
1807
1815
  import { execFileSync as execFileSync3 } from "child_process";
1808
1816
  import { existsSync as existsSync9, realpathSync as realpathSync3 } from "fs";
1809
- import { isAbsolute as isAbsolute2, join as join10, relative as relative3, sep as sep5 } from "path";
1817
+ import { isAbsolute as isAbsolute3, join as join10, relative as relative4, sep as sep6 } from "path";
1810
1818
  function isShared(state) {
1811
1819
  return state === "committed" || state === "changed";
1812
1820
  }
@@ -1835,7 +1843,7 @@ function gitInfo(root, extraDirs = []) {
1835
1843
  };
1836
1844
  const realRoot = real(root);
1837
1845
  const places = new Map(
1838
- [.../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS, MANIFEST_FILE, ...extraDirs])].filter((d) => existsSync9(join10(root, d))).map((d) => [d, relative3(realRoot, real(join10(root, d))).split(sep5).join("/")]).filter(([, place]) => place !== "" && place !== ".." && !place.startsWith("../") && !isAbsolute2(place))
1846
+ [.../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS, MANIFEST_FILE, ...extraDirs])].filter((d) => existsSync9(join10(root, d))).map((d) => [d, relative4(realRoot, real(join10(root, d))).split(sep6).join("/")]).filter(([, place]) => place !== "" && place !== ".." && !place.startsWith("../") && !isAbsolute3(place))
1839
1847
  );
1840
1848
  const dirs = [...new Set(places.values())];
1841
1849
  if (dirs.length === 0) return { of: () => "new" };
@@ -1863,7 +1871,7 @@ function gitInfo(root, extraDirs = []) {
1863
1871
  };
1864
1872
  }
1865
1873
  function relativeTo(root, path) {
1866
- return relative3(root, path).split("\\").join("/");
1874
+ return relative4(root, path).split("\\").join("/");
1867
1875
  }
1868
1876
 
1869
1877
  // src/here.ts
@@ -1872,7 +1880,7 @@ import { basename as basename8 } from "path";
1872
1880
 
1873
1881
  // src/health.ts
1874
1882
  import { existsSync as existsSync12, lstatSync as lstatSync4, readdirSync as readdirSync8 } from "fs";
1875
- import { basename as basename7, dirname as dirname6, isAbsolute as isAbsolute3, join as join13, relative as relative4, sep as sep7 } from "path";
1883
+ import { basename as basename7, dirname as dirname6, isAbsolute as isAbsolute4, join as join13, relative as relative5, sep as sep8 } from "path";
1876
1884
 
1877
1885
  // src/plugins.ts
1878
1886
  import { execFileSync as execFileSync4 } from "child_process";
@@ -2175,7 +2183,7 @@ function cursorPluginSkills(plugins = cursorPlugins()) {
2175
2183
 
2176
2184
  // src/tidy.ts
2177
2185
  import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as readdirSync7, unlinkSync as unlinkSync3 } from "fs";
2178
- import { basename as basename6, dirname as dirname5, join as join12, sep as sep6 } from "path";
2186
+ import { basename as basename6, dirname as dirname5, join as join12, sep as sep7 } from "path";
2179
2187
  function entriesOf(dirs, name) {
2180
2188
  return dirs.flatMap((dir) => {
2181
2189
  const path = join12(dir, name);
@@ -2233,7 +2241,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
2233
2241
  const realRoot = realpathOrNull(root) ?? root;
2234
2242
  for (const name of namesIn(dirs)) {
2235
2243
  const entries = entriesOf(dirs, name);
2236
- const external = entries.find((e) => e.link && e.real && !(e.real + sep6).startsWith(realRoot + sep6));
2244
+ const external = entries.find((e) => e.link && e.real && !(e.real + sep7).startsWith(realRoot + sep7));
2237
2245
  if (external) {
2238
2246
  const differs = entries.some((e) => e !== external && e.real && existsSync11(join12(e.real, "SKILL.md")) && treeHash(e.real) !== treeHash(external.real));
2239
2247
  report.skipped.push({ name, reason: `${rel(external.path)} links outside the repo`, ...differs ? { differs } : {} });
@@ -2392,6 +2400,8 @@ function intoLibrary(path, force = false) {
2392
2400
  if (r.status === "exists") throw new Error(`${r.name}: your library has a different ${r.name}; update or delete it first`);
2393
2401
  return r.name;
2394
2402
  }
2403
+ var pinsOf = (where, personal) => personal ? "your personal pins" : `${where}'s skilllib.json`;
2404
+ var newPinsOf = (root, where) => pinsOf(where, !existsSync12(join13(root, MANIFEST_FILE)) && personalPinsFile(root) !== null);
2395
2405
  function track(root, name) {
2396
2406
  const c = addSkill(root, name);
2397
2407
  if (c.action === "skipped") throw new Error(`${name}: ${c.reason}`);
@@ -2465,7 +2475,7 @@ function brokenLinksIn(root, opened) {
2465
2475
  const realRoot = realNative(root) ?? root;
2466
2476
  const ownerOf = (real) => {
2467
2477
  let nearest = null;
2468
- for (let d = dirname6(dirname6(real)); d.startsWith(realRoot + sep7); d = dirname6(d)) {
2478
+ for (let d = dirname6(dirname6(real)); d.startsWith(realRoot + sep8); d = dirname6(d)) {
2469
2479
  if (!existsSync12(join13(d, MANIFEST_FILE))) continue;
2470
2480
  if (opened.has(d)) return { opened: true, own: d };
2471
2481
  nearest ??= d;
@@ -2497,13 +2507,13 @@ function brokenLinksIn(root, opened) {
2497
2507
  return `Removed the broken link ${dir}/${name} in ${where} (restorable with skilllib restore)`;
2498
2508
  }
2499
2509
  };
2500
- const placeOf = (p) => relative4(realRoot, p).split(sep7).join("/");
2510
+ const placeOf = (p) => relative5(realRoot, p).split(sep8).join("/");
2501
2511
  git2 ??= gitInfo(
2502
2512
  root,
2503
- folders2.map((f) => placeOf(f.real)).filter((p) => p !== "" && p !== ".." && !p.startsWith("../") && !isAbsolute3(p))
2513
+ folders2.map((f) => placeOf(f.real)).filter((p) => p !== "" && p !== ".." && !p.startsWith("../") && !isAbsolute4(p))
2504
2514
  );
2505
2515
  const tracked = isShared(git2?.of(placeOf(join13(real, name))));
2506
- const recorded = !!own && mends(own, relative4(own, real).split(sep7).join("/"), name);
2516
+ const recorded = !!own && mends(own, relative5(own, real).split(sep8).join("/"), name);
2507
2517
  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` : ""}.` : "";
2508
2518
  return [
2509
2519
  {
@@ -2911,7 +2921,7 @@ function repoIssues(root, ctx) {
2911
2921
  ctx.issues.push({
2912
2922
  id: `stuck:${root}:${name}`,
2913
2923
  severity: "problem",
2914
- title: `${name} in ${where}: sync can't put it back as skilllib.json pins it`,
2924
+ title: `${name} in ${where}: sync can't put it back as ${pins[name]?.personal ? "your personal pins have" : "skilllib.json pins"} it`,
2915
2925
  // What to do depends on what's there: a folder that links out of the repo is a setup of yours, and a pin
2916
2926
  // your library has no version of can't come back by sync anyway.
2917
2927
  detail: `${blocker.said}, so sync can't put it back there.${blocker.outside ? " skilllib only installs inside the repo: point that folder back into the repo, or remove the skill here (skilllib remove)." : versionForHash(name, pins[name]?.hash ?? "") !== null ? " Move it out of the way, then run skilllib sync." : " Move it out of the way; your library has no copy of the pinned version either."}`,
@@ -2925,11 +2935,11 @@ function repoIssues(root, ctx) {
2925
2935
  ctx.issues.push({
2926
2936
  id: `pin:${root}:${s.name}`,
2927
2937
  severity: "problem",
2928
- title: `${s.name}: the version ${where}'s skilllib.json pins isn't in your library`,
2929
- detail: `${s.state === "folder missing" ? "The folder is missing, and no" : "The copy is another version, and no"} version in Your skills has the content it pins (pinned from another library?). ${untrackable(s) ? `${untrackable(s)}, so skilllib can't install it here.` : `Install your newest v${s.latest}; ${where}'s skilllib.json then pins it.`}`,
2938
+ title: `${s.name}: the version ${s.personal ? "your personal pins" : `${where}'s skilllib.json`} pin${s.personal ? "" : "s"} isn't in your library`,
2939
+ detail: `${s.state === "folder missing" ? "The folder is missing, and no" : "The copy is another version, and no"} version in Your skills has the content it pins (pinned from another library?). ${untrackable(s) ? `${untrackable(s)}, so skilllib can't install it here.` : `Install your newest v${s.latest}; ${s.personal ? "your personal pins" : `${where}'s skilllib.json`} then pin${s.personal ? "" : "s"} it.`}`,
2930
2940
  at: rows(root, [s.name]),
2931
2941
  // It changes what the shared skilllib.json pins: a choice, never run by `doctor --fix`.
2932
- ...untrackable(s) ? {} : { choices: [{ label: `Install your newest (v${s.latest})`, hint: `${where}'s skilllib.json pins it`, run: () => `${s.name}: installed v${track(root, s.name).to}` }] }
2942
+ ...untrackable(s) ? {} : { choices: [{ label: `Install your newest (v${s.latest})`, hint: s.personal ? "your personal pins pin it" : `${where}'s skilllib.json pins it`, run: () => `${s.name}: installed v${track(root, s.name).to}` }] }
2933
2943
  });
2934
2944
  if (restorable.length > 0) {
2935
2945
  ctx.issues.push({
@@ -2967,18 +2977,18 @@ function repoIssues(root, ctx) {
2967
2977
  ctx.issues.push({
2968
2978
  id: `unavailable:${root}:${s.name}`,
2969
2979
  severity: "problem",
2970
- title: `${s.name}: pinned in ${where}'s skilllib.json, but not in your library`,
2980
+ title: `${s.name}: pinned in ${s.personal ? "your personal pins" : `${where}'s skilllib.json`}, but not in your library`,
2971
2981
  detail: `Its folder is missing, and your library (${tildify(libraryDir())}) has no copy to restore: import it, or set SKILLLIB_HOME to the skilllib folder whose library has it.`,
2972
2982
  at: rows(root, [s.name]),
2973
2983
  // Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
2974
2984
  choices: [
2975
2985
  {
2976
- label: "Remove it from skilllib.json",
2986
+ label: s.personal ? "Remove it from your personal pins" : "Remove it from skilllib.json",
2977
2987
  hint: `${where} stops pinning it`,
2978
2988
  run: () => {
2979
2989
  const r = removeSkill(root, s.name);
2980
2990
  if (r.action !== "removed") throw new Error(`${s.name}: ${r.reason}`);
2981
- return `${s.name}: removed from ${where}'s skilllib.json`;
2991
+ return `${s.name}: removed from ${pinsOf(where, s.personal)}`;
2982
2992
  }
2983
2993
  }
2984
2994
  ]
@@ -2995,7 +3005,7 @@ function repoIssues(root, ctx) {
2995
3005
  choices: [
2996
3006
  {
2997
3007
  label: "Update to the newest versions",
2998
- hint: isShared(gitOf(root)?.of(MANIFEST_FILE)) ? "git will see the new copies and skilllib.json" : "replaces each copy with your library's newest",
3008
+ hint: behind.some((s) => !s.personal) && isShared(gitOf(root)?.of(MANIFEST_FILE)) ? "git will see the new copies and skilllib.json" : "replaces each copy with your library's newest",
2999
3009
  run: () => summarize(where, "updated", updateProject(root, behind.map((s) => s.name)))
3000
3010
  }
3001
3011
  ]
@@ -3078,7 +3088,7 @@ function repoIssues(root, ctx) {
3078
3088
  if (partial.length) {
3079
3089
  const missing2 = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
3080
3090
  const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
3081
- const manifestShared = partial.some((s) => s.managed) && isShared(gitOf(root)?.of(MANIFEST_FILE));
3091
+ const manifestShared = partial.some((s) => s.managed && !s.personal) && isShared(gitOf(root)?.of(MANIFEST_FILE));
3082
3092
  const run = (allowTracked) => () => {
3083
3093
  if (allowTracked) allowTrackedLinks(root);
3084
3094
  const r = linkAll(root, { allowTracked, names: partial.map((s) => s.name) });
@@ -3134,7 +3144,7 @@ function repoIssues(root, ctx) {
3134
3144
  id: `local:${root}:${s.name}`,
3135
3145
  severity: "suggestion",
3136
3146
  title: `${s.name} in ${where} isn't in your library`,
3137
- 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.",
3147
+ 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, or your personal pins when it has none).",
3138
3148
  at: rows(root, [s.name]),
3139
3149
  choices: [
3140
3150
  {
@@ -3154,9 +3164,9 @@ function repoIssues(root, ctx) {
3154
3164
  id: `adopt:${root}:${s.name}`,
3155
3165
  severity: "suggestion",
3156
3166
  title: `${s.name} in ${where} isn't tracked`,
3157
- detail: "It's identical to the library copy; track it (in skilllib.json) so future library changes reach it.",
3167
+ detail: `It's identical to the library copy; track it (in ${newPinsOf(root, where)}) so future library changes reach it.`,
3158
3168
  at: rows(root, [s.name]),
3159
- choices: [{ label: "Track it", hint: `adds it to ${where}'s skilllib.json`, run: () => `${track(root, s.name).name} tracked in ${where}` }]
3169
+ choices: [{ label: "Track it", hint: `adds it to ${newPinsOf(root, where)}`, run: () => `${track(root, s.name).name} tracked in ${where}` }]
3160
3170
  });
3161
3171
  } else if (s.state === "untracked, differs from library" || s.state === "repo skill, differs from library") {
3162
3172
  const teams = s.state.startsWith("repo skill") || committed(s) || !!untrackable(s);
@@ -3197,7 +3207,7 @@ function repoIssues(root, ctx) {
3197
3207
  id: `npx:${root}:${s.name}`,
3198
3208
  severity: "suggestion",
3199
3209
  title: `${s.name} in ${where} came from npx skills, not your library`,
3200
- detail: `Installed with \`npx skills add\`${s.source ? ` (${s.source})` : ""}. Import it into your library to reuse it in other projects and keep it backed up${teams ? `; ${where}'s copy stays as it is (${committed(s) ? "committed" : `${untrackable(s)}, so skilllib can't track it here`})` : `; ${where} then tracks it in skilllib.json`}.`,
3210
+ detail: `Installed with \`npx skills add\`${s.source ? ` (${s.source})` : ""}. Import it into your library to reuse it in other projects and keep it backed up${teams ? `; ${where}'s copy stays as it is (${committed(s) ? "committed" : `${untrackable(s)}, so skilllib can't track it here`})` : `; ${where} then tracks it (in skilllib.json, or your personal pins when it has none)`}.`,
3201
3211
  at: rows(root, [s.name]),
3202
3212
  choices: [
3203
3213
  {
@@ -3218,7 +3228,7 @@ function repoIssues(root, ctx) {
3218
3228
  id: `not-in-lib:${root}:${s.name}`,
3219
3229
  severity: "suggestion",
3220
3230
  title: `${s.name} in ${where} is tracked, but no longer in your library`,
3221
- detail: `${where}'s skilllib.json pins it, but your library has no copy: if the folder goes, sync can't restore it, and your other repos can't add it. Copy ${where}'s copy back into the library.`,
3231
+ detail: `${s.personal ? "Your personal pins have" : `${where}'s skilllib.json pins`} it, but your library has no copy: if the folder goes, sync can't restore it, and your other repos can't add it. Copy ${where}'s copy back into the library.`,
3222
3232
  at: rows(root, [s.name]),
3223
3233
  choices: [
3224
3234
  {
@@ -3739,9 +3749,9 @@ function projectOfFactory(projects2) {
3739
3749
  const sorted2 = [...projects2].sort((a, b) => b.length - a.length);
3740
3750
  return (cwd) => {
3741
3751
  if (!cwd) return null;
3742
- const known = sorted2.find((p) => cwd === p || cwd.startsWith(p + sep8));
3752
+ const known = sorted2.find((p) => cwd === p || cwd.startsWith(p + sep9));
3743
3753
  if (known) return known;
3744
- const repo = cwd.split(`${sep8}.claude${sep8}worktrees${sep8}`)[0];
3754
+ const repo = cwd.split(`${sep9}.claude${sep9}worktrees${sep9}`)[0];
3745
3755
  return isProjectCandidate(repo) ? repo : null;
3746
3756
  };
3747
3757
  }
@@ -3950,7 +3960,7 @@ function add(args) {
3950
3960
  printChanges(root, changes, args);
3951
3961
  const pins = readManifest(root).skills;
3952
3962
  const done = changes.filter((c) => c.action !== "skipped" && pins[c.name]);
3953
- if (!args.json && done.some((c) => pins[c.name].personal))
3963
+ if (!args.json && args.personal === void 0 && done.some((c) => c.action === "installed" && !c.moved && pins[c.name].personal))
3954
3964
  info(dim(`Pinned in your personal pins (${tildify(personalPinsDir())}) and kept out of git. --shared pins in skilllib.json for the team.`));
3955
3965
  rememberProjects([root]);
3956
3966
  }
@@ -4382,7 +4392,7 @@ function moveGlobal(args, names, yours) {
4382
4392
  for (const [root, c] of m.changes) warnHeldLinks(root, c, `in ${basename10(root)}`);
4383
4393
  if (m.left) warn(`${m.name}: ${m.left.join(", ")} now point${m.left.length === 1 ? "s" : ""} at nothing (the repo's own copy serves that package): remove ${m.left.length === 1 ? "it" : "them"}`);
4384
4394
  }
4385
- const toCommit = [...new Set(moves.filter((m) => m.moved).flatMap((m) => m.changes.map(([r]) => r)))].filter((r) => existsSync14(join15(r, MANIFEST_FILE)));
4395
+ const toCommit = [...new Set(moves.filter((m) => m.moved).flatMap((m) => m.changes.filter(([r]) => pinsIn(r)[m.name] && !pinsIn(r)[m.name].personal).map(([r]) => r)))];
4386
4396
  if (toCommit.length) info(dim(`Commit skilllib.json and the skill folders in ${toCommit.map((r) => basename10(r)).join(", ")}.`));
4387
4397
  }
4388
4398
  function tidy(args) {
@@ -4489,11 +4499,14 @@ export {
4489
4499
  versionHistory,
4490
4500
  forgetLatest,
4491
4501
  latestVersion,
4502
+ versionForHash,
4492
4503
  projectHere,
4493
4504
  readManifest,
4505
+ personalPinsFile,
4494
4506
  knownProjects,
4495
4507
  rememberProjects,
4496
4508
  readableRoots,
4509
+ pinsIn,
4497
4510
  setJsonMode,
4498
4511
  isJsonMode,
4499
4512
  json,
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  unhide,
32
32
  update,
33
33
  usage
34
- } from "./chunk-57EEPVXJ.js";
34
+ } from "./chunk-YQFLFWAY.js";
35
35
 
36
36
  // src/index.ts
37
37
  import { readFileSync } from "fs";
@@ -140,10 +140,10 @@ async function main() {
140
140
  if (!["--version", "-v", "version", "help", "--help", "-h"].includes(command ?? "")) refreshAgentSkill();
141
141
  switch (command) {
142
142
  case void 0:
143
- return process.stdin.isTTY && process.stdout.isTTY ? (await import("./tui-BCCAP7GJ.js")).tui() : overview(args);
143
+ return process.stdin.isTTY && process.stdout.isTTY ? (await import("./tui-P3HKRNHI.js")).tui() : overview(args);
144
144
  case "ui":
145
145
  if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("`skilllib ui` needs an interactive terminal. Agents and scripts: use `skilllib status --json`.");
146
- return (await import("./tui-BCCAP7GJ.js")).tui();
146
+ return (await import("./tui-P3HKRNHI.js")).tui();
147
147
  case "status":
148
148
  return status(args);
149
149
  case "add":
@@ -43,6 +43,8 @@ import {
43
43
  moveGlobalSkills,
44
44
  movedTogether,
45
45
  notWritable,
46
+ personalPinsFile,
47
+ pinsIn,
46
48
  pluginBackups,
47
49
  projectHere,
48
50
  projectOfFactory,
@@ -81,10 +83,11 @@ import {
81
83
  updatePlugin,
82
84
  updateProject,
83
85
  usageIssues,
86
+ versionForHash,
84
87
  versionHistory,
85
88
  visibleProjects,
86
89
  withoutForceHint
87
- } from "./chunk-57EEPVXJ.js";
90
+ } from "./chunk-YQFLFWAY.js";
88
91
 
89
92
  // src/tui/index.tsx
90
93
  import { render } from "ink";
@@ -445,7 +448,7 @@ function globalFixes(m) {
445
448
  ];
446
449
  }
447
450
  function pinFix(w, repo, s) {
448
- if (s.source !== "lib" || !w.library.some((l) => l.name === s.name)) return null;
451
+ if (s.source !== "lib" || !s.pinMoves || !w.library.some((l) => l.name === s.name)) return null;
449
452
  if (s.personal)
450
453
  return { label: "Pin it for the team", preview: `Move ${s.name}'s pin into ${repo}'s skilllib.json. Git will see skilllib.json and ${s.dir}/${s.name}: commit them to share it.`, run: (w2) => w2.ops.pin(repo, s.name, false) };
451
454
  if (s.inGit || isShared(s.git)) return null;
@@ -2095,7 +2098,7 @@ function uniqueNames(roots) {
2095
2098
  })
2096
2099
  );
2097
2100
  }
2098
- function localSkill(root, s, git) {
2101
+ function localSkill(root, s, git, pins, canBePersonal) {
2099
2102
  const managed = {
2100
2103
  "edited locally": { edited: true },
2101
2104
  "edited locally, update available": { edited: true },
@@ -2114,7 +2117,9 @@ function localSkill(root, s, git) {
2114
2117
  const state = git ? git.of(relativeTo(root, s.path)) : null;
2115
2118
  const others = otherCopies(root, s.name, s.location);
2116
2119
  const own = other[s.state]?.source === "untracked" && isShared(state) ? { source: "repo" } : {};
2117
- const inGit = s.managed && (isShared(state) || (readManifest(root).skills[s.name]?.links ?? []).some((d) => isShared(git?.of(`${d}/${s.name}`))));
2120
+ const dep = pins[s.name];
2121
+ const inGit = s.managed && (isShared(state) || (dep?.links ?? []).some((d) => isShared(git?.of(`${d}/${s.name}`))));
2122
+ const pinMoves = s.managed && !!dep && versionForHash(s.name, dep.hash) !== null && !s.state.startsWith("edited") && (s.personal || canBePersonal);
2118
2123
  return {
2119
2124
  name: s.name,
2120
2125
  source: "lib",
@@ -2122,7 +2127,7 @@ function localSkill(root, s, git) {
2122
2127
  path: s.path,
2123
2128
  // Agents that actually load it here: visibility lists every agent you use, with `paths: 0` for the blind ones.
2124
2129
  agents: s.visibility.filter((v) => v.paths > 0).map((v) => v.id),
2125
- ...s.managed ? { version: s.version ?? void 0, ...s.personal ? { personal: true } : {}, ...inGit ? { inGit: true } : {}, ...managed[s.state] } : { ...other[s.state], ...own },
2130
+ ...s.managed ? { version: s.version ?? void 0, ...s.personal ? { personal: true } : {}, ...inGit ? { inGit: true } : {}, ...pinMoves ? { pinMoves: true } : {}, ...managed[s.state] } : { ...other[s.state], ...own },
2126
2131
  ...s.source ? { origin: `npx skills: ${s.source}` } : {},
2127
2132
  ...others.length ? { others } : {},
2128
2133
  git: state
@@ -2219,11 +2224,11 @@ function loadWorld() {
2219
2224
  name: names.get(root),
2220
2225
  path: root,
2221
2226
  manifest: hasManifest ? git ? git.of("skilllib.json") : "no git" : "none",
2222
- skills: status.map((s) => {
2223
- const local = localSkill(root, s, git);
2227
+ skills: ((pins, canBePersonal) => status.map((s) => {
2228
+ const local = localSkill(root, s, git, pins, canBePersonal);
2224
2229
  const own = s.state.includes("missing") ? "" : readSkillInfo(s.path).description;
2225
2230
  return own && own !== descriptions[s.name] ? { ...local, description: own } : local;
2226
- }),
2231
+ }))(pinsIn(root), personalPinsFile(root) !== null),
2227
2232
  ...loadedHere(root)
2228
2233
  };
2229
2234
  });
@@ -2376,8 +2381,14 @@ function realOps(roots, backups, machine, libraryNames) {
2376
2381
  },
2377
2382
  pin: (repo, name, personal) => {
2378
2383
  const c = addSkill(rootOf(repo), name, { personal });
2379
- if (c.action === "skipped") return failed(said3(c, repo));
2380
- return personal ? `${name} is now personal in ${repo}: only on your machines, kept out of git` : `${name} is now pinned in ${repo}'s skilllib.json for the team`;
2384
+ if (c.action === "skipped") return failed(`${name}: ${withoutForceHint(c.reason ?? "skipped")}`);
2385
+ const moved = personal ? `${name} is now personal in ${repo}: only on your machines, kept out of git` : `${name} is now pinned in ${repo}'s skilllib.json for the team`;
2386
+ if (c.uncommitted?.length) {
2387
+ const copy = `${copyDir(repo, name)}/${name}`;
2388
+ return failed(`${moved}, but not linked in ${c.uncommitted.join(", ")}: git tracks ${c.uncommitted.length === 1 ? "it" : "them"} and ${copy} isn't committed, so agents reading ${c.uncommitted.length === 1 ? "it" : "them"} can't use ${name}. Commit ${copy}, then run skilllib link --allow-tracked`);
2389
+ }
2390
+ if (c.blocked?.length || c.inTheWay?.length) return withLinks(c, repo, name);
2391
+ return moved;
2381
2392
  },
2382
2393
  copyToLibrary: (repo, name) => ((s) => s ? toLibrary(s.path) : failed(`${name} isn't in ${repo} any more`))(find(repo, name)),
2383
2394
  deleteGlobal: (m) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skilllib",
3
- "version": "1.12.0",
3
+ "version": "1.12.2",
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",
@@ -25,7 +25,7 @@ Also: `usage --json` (Claude Code usage only, so "unused" is a hint; each skill
25
25
 
26
26
  ## Changing (ask first unless the user asked for this exact change)
27
27
 
28
- `add <name>...` (pins in `skilllib.json` when the repo has one, else in personal pins; `--personal` / `--shared` choose, and move an existing pin without changing its version) · `remove <name>...` (skills skilllib manages, `source: library`) · `update [name...]` · `sync` (installs what `skilllib.json` pins; takes no names). Pass several names in one call. Their result is `[{name, action, from, to, dirs, reason, blocked, uncommitted, inTheWay, backedUp}]`, which is the whole result: there's no need to check files afterwards. `import <dir>...` (into the library) prints `[{path, name, status}]` and exits 1 if a folder was skipped (`exists`, `no SKILL.md`). `doctor --fix` prints doctor's issues, each fix with `fixed: {ok, message}`.
28
+ `add <name>...` (pins in `skilllib.json` when the repo has one, else in personal pins; `--personal` / `--shared` choose, and move an existing pin without changing its version) · `remove <name>...` (skills skilllib manages, `source: library`) · `update [name...]` · `sync` (installs what's pinned, in `skilllib.json` and your personal pins; takes no names). Pass several names in one call. Their result is `[{name, action, from, to, dirs, personal, moved, reason, blocked, uncommitted, inTheWay, backedUp}]` (`personal: true` when the pin is in your personal pins; `moved` when `--personal`/`--shared` moved it), which is the whole result: there's no need to check files afterwards. `import <dir>...` (into the library) prints `[{path, name, status}]` and exits 1 if a folder was skipped (`exists`, `no SKILL.md`). `doctor --fix` prints doctor's issues, each fix with `fixed: {ok, message}`.
29
29
 
30
30
  - `skipped` + `reason`: nothing changed for that skill, and the command exits 1; tell the user. `--force` overwrites local edits (`backedUp` says where they went; `skilllib restore <name>` brings them back), but only use it with their OK.
31
31
  - `removed` + `reason`: removed from skilllib.json, but something was left as it is (its copy behind a folder that links out of the repo, a file of the user's, or a link in such a folder). Tell the user what the reason says is still there.