skilllib 1.11.3 → 1.11.4
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 +22 -19
- package/dist/{chunk-3XDWEHI4.js → chunk-CS6T66EE.js} +961 -355
- package/dist/index.js +6 -4
- package/dist/json.d.cts +20 -8
- package/dist/json.d.ts +20 -8
- package/dist/{tui-WSHQRBPF.js → tui-BDLJOMSC.js} +205 -128
- package/package.json +2 -2
- package/skills/skilllib/SKILL.md +9 -6
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
HARNESSES,
|
|
3
3
|
PROJECT_SKILLS_DIR,
|
|
4
|
+
addRefusal,
|
|
4
5
|
addRoot,
|
|
5
6
|
addSkill,
|
|
6
7
|
agentSkillState,
|
|
7
8
|
allowTrackedLinks,
|
|
9
|
+
blindSaid,
|
|
10
|
+
blindWithoutGlobal,
|
|
8
11
|
claudeBinary,
|
|
9
12
|
createSkill,
|
|
10
13
|
cursorPlugins,
|
|
@@ -12,6 +15,7 @@ import {
|
|
|
12
15
|
deleteLibrarySkill,
|
|
13
16
|
discoverProjects,
|
|
14
17
|
enabledHarnesses,
|
|
18
|
+
entryExists,
|
|
15
19
|
findIssues,
|
|
16
20
|
foldersOf,
|
|
17
21
|
forgetClaudeBinary,
|
|
@@ -20,9 +24,13 @@ import {
|
|
|
20
24
|
harness,
|
|
21
25
|
harnessesChosen,
|
|
22
26
|
importSkill,
|
|
27
|
+
inTheWaySaid,
|
|
23
28
|
installAgentSkill,
|
|
29
|
+
installDirs,
|
|
24
30
|
installedPlugins,
|
|
31
|
+
isDir,
|
|
25
32
|
keptGlobal,
|
|
33
|
+
knownProjects,
|
|
26
34
|
latestVersion,
|
|
27
35
|
libraryOrigins,
|
|
28
36
|
librarySkillDir,
|
|
@@ -32,6 +40,8 @@ import {
|
|
|
32
40
|
localStamp,
|
|
33
41
|
machineSkills,
|
|
34
42
|
moveGlobalSkills,
|
|
43
|
+
movedTogether,
|
|
44
|
+
notWritable,
|
|
35
45
|
pluginBackups,
|
|
36
46
|
projectHere,
|
|
37
47
|
projectOfFactory,
|
|
@@ -41,9 +51,11 @@ import {
|
|
|
41
51
|
readManifest,
|
|
42
52
|
readSkillInfo,
|
|
43
53
|
readableRoots,
|
|
54
|
+
realPath,
|
|
44
55
|
recordOrigin,
|
|
45
56
|
relativeTo,
|
|
46
57
|
relinkDependency,
|
|
58
|
+
rememberProjects,
|
|
47
59
|
removeAgentSkill,
|
|
48
60
|
removeRoot,
|
|
49
61
|
removeSkill,
|
|
@@ -56,18 +68,21 @@ import {
|
|
|
56
68
|
setHidden,
|
|
57
69
|
setKeepGlobal,
|
|
58
70
|
setPluginOn,
|
|
71
|
+
skillOf,
|
|
59
72
|
skilllibHome,
|
|
60
73
|
skillsLoadedIn,
|
|
74
|
+
strandedLinks,
|
|
75
|
+
survives,
|
|
61
76
|
tildify,
|
|
77
|
+
trackBlocker,
|
|
62
78
|
treeHash,
|
|
63
79
|
uninstallPlugin,
|
|
64
80
|
updatePlugin,
|
|
65
81
|
updateProject,
|
|
66
82
|
usageIssues,
|
|
67
83
|
versionHistory,
|
|
68
|
-
visibilityOf,
|
|
69
84
|
visibleProjects
|
|
70
|
-
} from "./chunk-
|
|
85
|
+
} from "./chunk-CS6T66EE.js";
|
|
71
86
|
|
|
72
87
|
// src/tui/index.tsx
|
|
73
88
|
import { render } from "ink";
|
|
@@ -186,6 +201,9 @@ function wrap(text, width, max) {
|
|
|
186
201
|
return lines;
|
|
187
202
|
}
|
|
188
203
|
|
|
204
|
+
// src/tui/prompts.ts
|
|
205
|
+
import { dirname } from "path";
|
|
206
|
+
|
|
189
207
|
// src/review.ts
|
|
190
208
|
import { spawnSync } from "child_process";
|
|
191
209
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -194,7 +212,7 @@ var EXCERPT_CHARS = 1200;
|
|
|
194
212
|
function skillExcerpt(dir, chars) {
|
|
195
213
|
const file = join(dir, "SKILL.md");
|
|
196
214
|
if (!existsSync(file)) return { text: "(SKILL.md not found)", files: 0, size: 0 };
|
|
197
|
-
const content = readFileSync(file, "utf-8").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
|
|
215
|
+
const content = readFileSync(file, "utf-8").replace(/^\uFEFF/, "").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
|
|
198
216
|
let files = 0;
|
|
199
217
|
try {
|
|
200
218
|
files = readdirSync(dir, { recursive: true, withFileTypes: true }).filter((d) => d.isFile()).length;
|
|
@@ -296,8 +314,9 @@ var isFailure = (r) => typeof r !== "string" && "failed" in r;
|
|
|
296
314
|
var followUp = (r) => typeof r !== "string" && "then" in r ? r.then : void 0;
|
|
297
315
|
var said = (r) => typeof r === "string" ? r : r.message;
|
|
298
316
|
var joined = (results) => {
|
|
299
|
-
const
|
|
300
|
-
|
|
317
|
+
const bad = results.filter(isFailure);
|
|
318
|
+
const message = [...bad.length && bad.length < results.length ? [`${results.length - bad.length} of ${results.length} done`] : [], ...results.map(said)].join(" \xB7 ");
|
|
319
|
+
return bad.length ? failed(message) : message;
|
|
301
320
|
};
|
|
302
321
|
var SEVERITY_RANK = { problem: 0, suggestion: 1 };
|
|
303
322
|
function fixOf(c, found) {
|
|
@@ -325,6 +344,7 @@ function shortOf(found) {
|
|
|
325
344
|
sync: "Folder missing",
|
|
326
345
|
unavailable: "Pinned, not in your library",
|
|
327
346
|
pin: "Pinned version not in your library",
|
|
347
|
+
"out-of-sync": "Not the pinned version",
|
|
328
348
|
update: "Out of date",
|
|
329
349
|
uncommitted: "Commit it first",
|
|
330
350
|
local: "Not in your library",
|
|
@@ -353,7 +373,7 @@ function viewOf(found) {
|
|
|
353
373
|
title: found.title,
|
|
354
374
|
short: shortOf(found),
|
|
355
375
|
detail: found.detail,
|
|
356
|
-
skills: [...new Set(found.at?.
|
|
376
|
+
skills: [...new Set(found.at?.flatMap((a) => a.skill ? [a.skill] : []))],
|
|
357
377
|
decision: !found.fix,
|
|
358
378
|
fixes: (found.fix ? [found.fix] : found.choices ?? []).map((c) => fixOf(c, found))
|
|
359
379
|
};
|
|
@@ -364,6 +384,9 @@ function filterOf(source) {
|
|
|
364
384
|
function project(w, name) {
|
|
365
385
|
return w.projects.find((p) => p.name === name);
|
|
366
386
|
}
|
|
387
|
+
function usesKey(m) {
|
|
388
|
+
return m.source === "plugin" ? `${m.where.split("@")[0]}:${m.name}` : m.name;
|
|
389
|
+
}
|
|
367
390
|
function totalUses(w, name) {
|
|
368
391
|
return Object.values(w.usage).reduce((n, u) => n + (u[name] ?? 0), 0);
|
|
369
392
|
}
|
|
@@ -383,7 +406,7 @@ function usable(w, projectName) {
|
|
|
383
406
|
const uses = (n) => w.usage[projectName]?.[n] ?? 0;
|
|
384
407
|
return [
|
|
385
408
|
...p.skills.map((s) => ({ name: s.name, source: s.source, where: s.dir, agents: s.agents, uses: uses(s.name), local: s })),
|
|
386
|
-
...machineIn(w, p).map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: uses(m
|
|
409
|
+
...machineIn(w, p).map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: uses(usesKey(m)), machine: m }))
|
|
387
410
|
];
|
|
388
411
|
}
|
|
389
412
|
function issuesAt(w, repo, skill) {
|
|
@@ -397,7 +420,7 @@ function repoIssues(w, repo) {
|
|
|
397
420
|
function unlisted(w, repo) {
|
|
398
421
|
const root = project(w, repo).path;
|
|
399
422
|
const listed = new Set(usable(w, repo).map((u) => u.name));
|
|
400
|
-
return [...new Set(w.issues.flatMap((i) => (i.at ?? []).filter((a) => a.root === root && !listed.has(a.skill)).map((a) => a.skill)))];
|
|
423
|
+
return [...new Set(w.issues.flatMap((i) => (i.at ?? []).filter((a) => a.root === root && a.skill && !listed.has(a.skill)).map((a) => a.skill)))];
|
|
401
424
|
}
|
|
402
425
|
function libraryIssues(w, name) {
|
|
403
426
|
return w.issues.filter((i) => i.id === "unused:library" && i.at?.some((a) => a.skill === name)).map(issueOf);
|
|
@@ -410,12 +433,12 @@ function globalFixes(m) {
|
|
|
410
433
|
return [
|
|
411
434
|
{
|
|
412
435
|
label: "Move it to the repos that need it\u2026",
|
|
413
|
-
preview: `Pick repos. ${m.name} goes into your library and those repos, and stops loading globally (
|
|
436
|
+
preview: `Pick repos. ${m.name} goes into your library and those repos, and stops loading globally (every global copy backed up).`,
|
|
414
437
|
run: () => "",
|
|
415
438
|
pickRepos: (w, repos) => w.ops.moveGlobal([m], repos)
|
|
416
439
|
},
|
|
417
440
|
m.kept ? { label: "Stop marking it as global on purpose", preview: `${m.name} shows as Not reviewed again, like any global skill.`, run: (w) => w.ops.keepGlobal(m.name, false) } : { label: "Keep it global on purpose", preview: `Mark ${m.name} as global on purpose: it no longer shows as Not reviewed. Its other issues stay.`, run: (w) => w.ops.keepGlobal(m.name, true) },
|
|
418
|
-
{ label: "Delete it", preview: `Remove ${m.
|
|
441
|
+
{ label: "Delete it", preview: `Remove ${m.where}/${m.name}. A backup is kept in Settings \u203A Backups.`, run: (w) => w.ops.deleteGlobal(m) }
|
|
419
442
|
];
|
|
420
443
|
}
|
|
421
444
|
function machineActions(m) {
|
|
@@ -466,7 +489,7 @@ function deleteLibraryFix(w, names) {
|
|
|
466
489
|
`Delete ${what} and ${names.length === 1 ? "its" : "their"} versions from your library.`,
|
|
467
490
|
repos.length ? `Removes ${names.length === 1 ? "it" : "them"} from ${repos.length === 1 ? repos[0] : `these ${repos.length} repos: ${repos.join(", ")}`}.` : "No repo uses it.",
|
|
468
491
|
edited.length ? `${edited.join(", ")} ${edited.length === 1 ? "has" : "have"} local edits: backed up too.` : "",
|
|
469
|
-
"Every copy goes to Settings \u203A Backups.",
|
|
492
|
+
"Every copy goes to Settings \u203A Backups; where a repo commits its copy, git will see it.",
|
|
470
493
|
others.length ? `Untracked or the team's own copies in ${others.join(", ")} stay.` : ""
|
|
471
494
|
].filter(Boolean).join(" "),
|
|
472
495
|
run: (w2) => joined(names.map((n) => w2.ops.deleteLibrary(n)))
|
|
@@ -495,7 +518,8 @@ function replacePluginFix(p) {
|
|
|
495
518
|
`Copies its ${plural(names.length, "skill")} into your library and adds them to the repos you tick (or none),`,
|
|
496
519
|
synced ? `then turns ${p.id} off in Claude Code. It's synced from your claude.ai account: to delete it for good, remove it there.` : p.settingsOnly ? `then turns ${p.id} off in ~/.claude/settings.json. It isn't installed, only turned on there, so there's nothing to uninstall.` : `then uninstalls ${p.id}${p.repo ? ` from ${p.repo}` : ""} (its saved data is kept).`,
|
|
497
520
|
p.parts.length ? `It also brings ${p.parts.join(", ")}: those ${offOnly ? "stop" : "go"} too.` : "",
|
|
498
|
-
|
|
521
|
+
// A plugin only turned on in settings isn't listed in Plugins once it's off: say where it comes back.
|
|
522
|
+
offOnly ? p.settingsOnly ? "Turn it back on with /plugin in Claude Code." : "Turn it back on from Plugins." : "Settings \u203A Backups reinstalls it."
|
|
499
523
|
].filter(Boolean).join(" "),
|
|
500
524
|
run: () => "",
|
|
501
525
|
allowNone: true,
|
|
@@ -638,14 +662,16 @@ var agentNames = (agents) => agents.map((a) => harness(a).name).join(", ") || "n
|
|
|
638
662
|
var localSource = (s) => s.source === "lib" ? "Your skills (skilllib library), installed in the repo" : s.source === "repo" ? "a repo's own skill (committed by the team)" : "a copy only on this machine (not committed)";
|
|
639
663
|
var localInstalled = (s) => s.origin ?? (s.source === "lib" ? "added from my library with skilllib" : s.source === "repo" ? "committed in the repo" : "copied into the repo by hand (not committed)");
|
|
640
664
|
function reviewPrompt(w, names, scope, repo) {
|
|
665
|
+
const project2 = repo ? w.projects.find((p) => p.name === repo) : void 0;
|
|
666
|
+
const machine = project2 ? machineIn(w, project2) : w.machine;
|
|
641
667
|
const skills = names.map((name) => {
|
|
642
|
-
const copies =
|
|
643
|
-
const here =
|
|
668
|
+
const copies = machine.filter((m2) => m2.name === name);
|
|
669
|
+
const here = project2?.skills.find((s) => s.name === name);
|
|
644
670
|
if (here) {
|
|
645
671
|
const lib2 = w.library.find((l) => l.name === name);
|
|
646
672
|
return {
|
|
647
673
|
name,
|
|
648
|
-
description: w.descriptions[name] ?? "",
|
|
674
|
+
description: here.description ?? w.descriptions[name] ?? "",
|
|
649
675
|
source: localSource(here),
|
|
650
676
|
installedHow: localInstalled(here),
|
|
651
677
|
path: here.path,
|
|
@@ -669,12 +695,13 @@ function reviewPrompt(w, names, scope, repo) {
|
|
|
669
695
|
description: w.descriptions[name] ?? "",
|
|
670
696
|
source: m ? m.source === "global" ? "my global skills" : `vendor: ${m.source} (${m.where})` : lib ? "Your skills (skilllib library)" : local ? localSource(local) : "a repo's own skill",
|
|
671
697
|
installedHow: m?.origin ?? (m ? m.where : lib ? "created or imported into skilllib" : local ? localInstalled(local) : "committed in the repo"),
|
|
672
|
-
path: m?.path ?? (lib ? w.ops.libraryFile(name)
|
|
698
|
+
path: m?.path ?? (lib ? dirname(w.ops.libraryFile(name)) : local?.path ?? ""),
|
|
673
699
|
links: m?.links ?? [],
|
|
674
|
-
loadedBy: m ? `${agentNames(m.agents)} \u2014 in every repo` : repos.map((p) => `${agentNames(p.skills.find((s) => s.name === name).agents)} \u2014 in ${p.name}`).join("; ") || "none of my agents",
|
|
700
|
+
loadedBy: m ? `${agentNames(m.agents)} \u2014 ${w.machine.some((g) => g.path === m.path) ? "in every repo" : `in ${repo}`}` : repos.map((p) => `${agentNames(p.skills.find((s) => s.name === name).agents)} \u2014 in ${p.name}`).join("; ") || "none of my agents",
|
|
675
701
|
vendor: m && m.source !== "global" ? "turn it off at its source (/plugin in Claude Code, claude.ai settings, or Cursor)" : null,
|
|
676
|
-
|
|
677
|
-
|
|
702
|
+
// A plugin's copy is counted under plugin:skill.
|
|
703
|
+
usesTotal: w.days ? totalUses(w, m ? usesKey(m) : name) : null,
|
|
704
|
+
usesByProject: Object.entries(w.usage).flatMap(([p, u]) => /* @__PURE__ */ ((n) => n ? [[p, n]] : [])(u[m ? usesKey(m) : name])),
|
|
678
705
|
installedIn: repos.map((p) => p.name),
|
|
679
706
|
otherCopies: copies.filter((c) => c !== m).map((c) => `${c.source} ${c.where}`),
|
|
680
707
|
inYourSkills: lib ? `yes, v${lib.latest}` : null,
|
|
@@ -690,7 +717,7 @@ function reviewPrompt(w, names, scope, repo) {
|
|
|
690
717
|
}
|
|
691
718
|
function writeSkillPrompt(w, name, repo) {
|
|
692
719
|
const p = repo ? w.projects.find((x) => x.name === repo) : void 0;
|
|
693
|
-
const file = p ? `${p.path}
|
|
720
|
+
const file = p ? `${p.path}/${installDirs(w.agents)[0]}/${name || "<name>"}/SKILL.md` : w.ops.libraryFile(name || "<name>");
|
|
694
721
|
return `Write a new agent skill${name ? ` named "${name}"` : ""}${p ? ` for the repo at ${p.path}` : " for my skill library"}.
|
|
695
722
|
|
|
696
723
|
Create ${file} with:
|
|
@@ -739,7 +766,7 @@ function tag(u) {
|
|
|
739
766
|
return [u.source, color.muted];
|
|
740
767
|
}
|
|
741
768
|
function isNameChar(input, key) {
|
|
742
|
-
return !key.ctrl && !key.meta && /^[a-zA-Z0-9._-]
|
|
769
|
+
return !key.ctrl && !key.meta && /^[a-zA-Z0-9._-]+$/.test(input);
|
|
743
770
|
}
|
|
744
771
|
function clip(text, width) {
|
|
745
772
|
return text.length > width - 1 ? text.slice(0, width - 2) + "\u2026" : text;
|
|
@@ -755,7 +782,7 @@ function builder(title) {
|
|
|
755
782
|
issues(issues) {
|
|
756
783
|
if (!issues.length) return d.body.push({ segs: [["\u2713 No issues", color.green]] });
|
|
757
784
|
for (const issue of [...issues].sort(bySeverity)) {
|
|
758
|
-
d.body.push({ segs: [[`${SEV[issue.severity].icon} `, SEV[issue.severity].color], [issue.title, color.text], [issue.decision ? " \xB7 your call" : "", color.faint]] });
|
|
785
|
+
d.body.push({ segs: [[`${SEV[issue.severity].icon} `, SEV[issue.severity].color], [issue.title, color.text], [issue.decision && issue.fixes.length ? " \xB7 your call" : "", color.faint]] });
|
|
759
786
|
issue.fixes.forEach((fix, i) => d.body.push({ segs: [], option: d.options.push({ label: fix.label, fix, issue, recommended: !issue.decision && i === 0 }) - 1 }));
|
|
760
787
|
}
|
|
761
788
|
return 0;
|
|
@@ -848,13 +875,14 @@ function skillItem(w, copies, issues, nameW, actions, repo) {
|
|
|
848
875
|
],
|
|
849
876
|
detail: (width) => {
|
|
850
877
|
const b = builder(u.name);
|
|
851
|
-
for (const l of wrap(w.descriptions[u.name] || "(no description)", width - 4, 3)) b.line([l, color.muted]);
|
|
878
|
+
for (const l of wrap(u.local?.description || w.descriptions[u.name] || "(no description)", width - 4, 3)) b.line([l, color.muted]);
|
|
852
879
|
b.line();
|
|
853
|
-
|
|
880
|
+
const here = !!u.machine && !w.machine.some((m) => m.path === u.machine.path && m.where === u.machine.where);
|
|
881
|
+
if (repo && !u.local) b.line([here ? "Loads in this repo (its settings turn it on)" : "Loads in every repo", color.text], [u.source === "global" || u.source === "skilllib" ? " issues and cleanup live in Global" : " managed at its source", color.faint]);
|
|
854
882
|
else b.issues(issues);
|
|
855
883
|
b.actions(actions, width);
|
|
856
884
|
b.line();
|
|
857
|
-
for (const segs of usageLines(w, u.name, width, repo)) b.line(...segs);
|
|
885
|
+
for (const segs of usageLines(w, u.machine ? usesKey(u.machine) : u.name, width, repo)) b.line(...segs);
|
|
858
886
|
b.line();
|
|
859
887
|
b.line(["Loaded from", color.accent]);
|
|
860
888
|
for (const c of copies) {
|
|
@@ -904,12 +932,13 @@ var reviewOption = (w, name, ui, repo) => ({
|
|
|
904
932
|
label: "Review prompt",
|
|
905
933
|
action: () => ui.copy(reviewPrompt(w, [name], `Review the skill ${name}${repo ? ` in ${repo}` : ""}.`, repo), `a review prompt for ${name}`)
|
|
906
934
|
});
|
|
935
|
+
var gitSees = (s) => s?.git === "committed" || s?.git === "changed" ? " Git will see it." : "";
|
|
907
936
|
function versionsOption(w, repo, name, installed, ui) {
|
|
908
937
|
return {
|
|
909
938
|
label: "Other versions\u2026",
|
|
910
939
|
action: () => ui.menu(
|
|
911
940
|
`${name}: versions`,
|
|
912
|
-
w.ops.versions(name).reverse().map((v) => fixOption({ label: `v${v.version} ${v.date}${v.version === installed ? " (installed)" : ""}`, preview: `Install ${name} v${v.version} in ${repo}
|
|
941
|
+
w.ops.versions(name).reverse().map((v) => fixOption({ label: `v${v.version} ${v.date}${v.version === installed ? " (installed)" : ""}`, preview: `Install ${name} v${v.version} in ${repo}.${gitSees(project(w, repo).skills.find((x) => x.name === name))}`, run: (w2) => w2.ops.installVersion(repo, name, v.version) }))
|
|
913
942
|
)
|
|
914
943
|
};
|
|
915
944
|
}
|
|
@@ -919,14 +948,15 @@ function skillActions(w, p, u, ui) {
|
|
|
919
948
|
const remove = {
|
|
920
949
|
// Its folder gone: all that's left to remove is the skilllib.json entry (the label Health offers too).
|
|
921
950
|
label: s?.missing ? "Remove it from skilllib.json" : "Remove from repo",
|
|
922
|
-
preview: (s?.missing ? `${p}'s skilllib.json stops pinning ${u.name}. There's no folder to delete.` : s?.source === "lib" ? `Delete ${s.dir}/${u.name} in ${p}.${inLibrary ? " Your library keeps it." : ""}` : `Move ${s?.dir}/${u.name} in ${p} to Settings \u203A Backups.`) + (s?.others ? ` The copy in ${s.others.join(", ")} stays.` : "")
|
|
951
|
+
preview: (s?.missing ? `${p}'s skilllib.json stops pinning ${u.name}. There's no folder to delete.` : s?.source === "lib" ? `Delete ${s.dir}/${u.name} in ${p}.${inLibrary ? " Your library keeps it." : ""}` : `Move ${s?.dir}/${u.name} in ${p} to Settings \u203A Backups.`) + (s?.others ? ` The copy in ${s.others.join(", ")} stays.` : "") + // A managed skill's removal edits skilllib.json too.
|
|
952
|
+
(gitSees(s) || (s?.source === "lib" && project(w, p).manifest === "committed" ? " Git will see skilllib.json change." : "")),
|
|
923
953
|
run: (w2) => w2.ops.remove(p, u.name)
|
|
924
954
|
};
|
|
925
955
|
if (s?.source === "lib")
|
|
926
956
|
return [
|
|
927
957
|
// A tracked skill your library doesn't have: nothing to edit there, no versions to pick.
|
|
928
958
|
...inLibrary ? [
|
|
929
|
-
...s.version < inLibrary.latest ? [fixOption({ label: `Update to v${inLibrary.latest}`, preview: `Replace ${s.dir}/${u.name} with library v${inLibrary.latest}
|
|
959
|
+
...s.version < inLibrary.latest ? [fixOption({ label: `Update to v${inLibrary.latest}`, preview: `Replace ${s.dir}/${u.name} with library v${inLibrary.latest}.${gitSees(s)}`, run: (w2) => w2.ops.update(p, u.name) })] : [],
|
|
930
960
|
{ label: "Edit in library", action: () => ui.edit(w.ops.libraryFile(u.name)) },
|
|
931
961
|
versionsOption(w, p, u.name, s.version, ui)
|
|
932
962
|
] : [],
|
|
@@ -945,7 +975,8 @@ function skillActions(w, p, u, ui) {
|
|
|
945
975
|
fixOption(remove),
|
|
946
976
|
reviewOption(w, u.name, ui, p)
|
|
947
977
|
];
|
|
948
|
-
|
|
978
|
+
const everywhere = !!u.machine && w.machine.some((m) => m.path === u.machine.path && m.where === u.machine.where);
|
|
979
|
+
return everywhere ? [{ label: "Open in Global", action: () => ui.openInGlobal(u.name) }, reviewOption(w, u.name, ui)] : [reviewOption(w, u.name, ui, p)];
|
|
949
980
|
}
|
|
950
981
|
function overview(w, p) {
|
|
951
982
|
const project2 = project(w, p);
|
|
@@ -992,7 +1023,7 @@ function projectItems(w) {
|
|
|
992
1023
|
});
|
|
993
1024
|
}
|
|
994
1025
|
function globalItems(w, nameW, ui) {
|
|
995
|
-
const copies = w.machine.map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: totalUses(w, m
|
|
1026
|
+
const copies = w.machine.map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: totalUses(w, usesKey(m)), machine: m }));
|
|
996
1027
|
return byName(copies).map((g) => skillItem(w, g, issuesAt(w, null, g[0].name), nameW, globalActions(w, g[0], ui), null));
|
|
997
1028
|
}
|
|
998
1029
|
var AT_SOURCE = {
|
|
@@ -1055,7 +1086,7 @@ function libraryItems(w, nameW, ui) {
|
|
|
1055
1086
|
...removable.length ? [
|
|
1056
1087
|
fixOption({
|
|
1057
1088
|
label: "Remove from repos\u2026",
|
|
1058
|
-
preview: `Pick repos to remove ${l.name} from. Your library keeps it.`,
|
|
1089
|
+
preview: `Pick repos to remove ${l.name} from. Your library keeps it. Where a repo commits its copy, git will see it.`,
|
|
1059
1090
|
run: () => "",
|
|
1060
1091
|
candidates: () => removable.map((p) => p.name),
|
|
1061
1092
|
pickRepos: (w2, rs) => joined(rs.map((r) => w2.ops.remove(r, l.name)))
|
|
@@ -1149,7 +1180,7 @@ function pluginItems(w, columns, skillW, ui, issuesOf) {
|
|
|
1149
1180
|
const skills = p.skills.map((s) => cursor || !w.days ? s.name : `${s.name} (${uses2(totalUses(w, pluginSkillKey(p, s.name)))})`);
|
|
1150
1181
|
wrap(skills.join(" \xB7 ") || "none", width - 16, 4).forEach((l, i) => b.line(label(i ? "" : "Skills"), [l, color.text]));
|
|
1151
1182
|
b.line(label("Also brings"), [p.parts.join(", ") || "nothing else", p.parts.length ? color.text : color.faint]);
|
|
1152
|
-
const installed = cursor ? "in Cursor (whether it's on is only in Cursor)" : p.scope === "claude.ai" ? "synced from your claude.ai account" : p.repo ? `${p.scope} scope, in ${p.repo} only` : `user scope, in every repo`;
|
|
1183
|
+
const installed = cursor ? "in Cursor (whether it's on is only in Cursor)" : p.scope === "claude.ai" ? "synced from your claude.ai account" : p.settingsOnly ? "not installed: turned on in your settings, loaded from its marketplace's copy" : p.repo ? `${p.scope} scope, in ${p.repo} only` : `user scope, in every repo`;
|
|
1153
1184
|
b.line(label("Installed"), [installed, color.text], [p.version ? ` v${p.version.replace(/^v/, "")}` : "", color.muted], [p.id.includes("@") && p.scope !== "claude.ai" ? ` from ${p.id.split("@")[1]}` : "", color.faint]);
|
|
1154
1185
|
b.line(label("Folder"), [tildify(p.path), color.faint]);
|
|
1155
1186
|
return b.d;
|
|
@@ -1174,7 +1205,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1174
1205
|
const world = { ...withUsage, issues: [...base.issues, ...usageIssues2] };
|
|
1175
1206
|
useEffect(() => {
|
|
1176
1207
|
let live = true;
|
|
1177
|
-
void loadUsage2?.(base).then((u) => live && setUsed(u));
|
|
1208
|
+
void loadUsage2?.(base).then((u) => live && setUsed(u)).catch(() => live && setToast("\u2717 Couldn't read your Claude Code transcripts: usage isn't shown"));
|
|
1178
1209
|
return () => {
|
|
1179
1210
|
live = false;
|
|
1180
1211
|
};
|
|
@@ -1253,7 +1284,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1253
1284
|
}
|
|
1254
1285
|
function choose(o) {
|
|
1255
1286
|
if (o.action) return o.action();
|
|
1256
|
-
if (!o.fix) return
|
|
1287
|
+
if (!o.fix) return;
|
|
1257
1288
|
if (o.fix.pickRepos) {
|
|
1258
1289
|
const name = current?.name ?? "";
|
|
1259
1290
|
const only = o.fix.candidates?.(world);
|
|
@@ -1319,7 +1350,8 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1319
1350
|
cells: [
|
|
1320
1351
|
{ text: top ? SEV[top.severity].icon : " ", width: 2, color: top ? SEV[top.severity].color : void 0 },
|
|
1321
1352
|
{ text: `${isOpen ? "\u25BE" : "\u25B8"} ${label} (${members.length})`, grow: true, color: color.accent },
|
|
1322
|
-
|
|
1353
|
+
// A leading space keeps it apart from a name cut to fit.
|
|
1354
|
+
{ text: flagged ? ` ${flagged} to fix` : "", width: 11, color: top ? SEV[top.severity].color : color.faint }
|
|
1323
1355
|
],
|
|
1324
1356
|
detail: (width) => {
|
|
1325
1357
|
const b = builder(label);
|
|
@@ -1398,7 +1430,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1398
1430
|
() => ui.menu(repo, [
|
|
1399
1431
|
{ label: "Open its folder", action: () => setToast(said2(world.ops.openFolder(project(world, repo).path))) },
|
|
1400
1432
|
{ label: "Hide it from the list", action: () => (apply((w) => w.ops.hide(repo)), setOpen(null), setFocus(sidebar ? "sidebar" : "list")) },
|
|
1401
|
-
...skills.length ? [{ label: `Review prompt for its ${skills.length} skills`, action: () => ui.copy(reviewPrompt(world, skills, `Review the skills in ${repo}.`, repo), `a review prompt for ${repo}`) }] : []
|
|
1433
|
+
...skills.length ? [{ label: `Review prompt for its ${skills.length === 1 ? "skill" : `${skills.length} skills`}`, action: () => ui.copy(reviewPrompt(world, skills, `Review the skills in ${repo}.`, repo), `a review prompt for ${repo}`) }] : []
|
|
1402
1434
|
])
|
|
1403
1435
|
)
|
|
1404
1436
|
];
|
|
@@ -1446,8 +1478,16 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1446
1478
|
setToast(`Saved ${tildify(file)}`);
|
|
1447
1479
|
});
|
|
1448
1480
|
}
|
|
1449
|
-
function askForFolder(title) {
|
|
1450
|
-
|
|
1481
|
+
function askForFolder(title, problem) {
|
|
1482
|
+
const submit = (v) => apply((w) => {
|
|
1483
|
+
try {
|
|
1484
|
+
return w.ops.addRoot(v);
|
|
1485
|
+
} catch (e) {
|
|
1486
|
+
askForFolder(title, e.message);
|
|
1487
|
+
throw e;
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
setModal({ kind: "input", title: problem ? `${problem}. ${title}` : title, value: "", placeholder: "~/Projects", submit });
|
|
1451
1491
|
}
|
|
1452
1492
|
useEffect(() => {
|
|
1453
1493
|
const folder = () => !initial.roots.length && askForFolder("Where are your projects? skilllib finds the git repos in there");
|
|
@@ -1701,7 +1741,8 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1701
1741
|
if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
|
|
1702
1742
|
}
|
|
1703
1743
|
}
|
|
1704
|
-
const
|
|
1744
|
+
const toastLines = toast && modal?.kind !== "confirm" ? wrap(toast, termCols - 2, 3).map((l) => " " + l) : [];
|
|
1745
|
+
const bottomH = Math.max(1, toastLines.length);
|
|
1705
1746
|
const confirmLines = modal?.kind === "confirm" ? wrap(modal.fix.preview, columns - 6, 6) : [];
|
|
1706
1747
|
const confirmH = modal?.kind === "confirm" ? confirmLines.length + 2 : 0;
|
|
1707
1748
|
const bodyH = Math.max(8, termRows - (sidebar ? 0 : 1) - bottomH - confirmH);
|
|
@@ -1952,9 +1993,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1952
1993
|
modal?.kind === "confirm" ? /* @__PURE__ */ jsx2(Panel, { title: modal.fix.label, focused: true, width: columns, height: confirmH, children: confirmLines.map((l, i) => /* @__PURE__ */ jsx2(Text2, { color: color.text, children: l }, i)) }) : null
|
|
1953
1994
|
] })
|
|
1954
1995
|
] }),
|
|
1955
|
-
/* @__PURE__ */ jsxs2(Box2, { height:
|
|
1956
|
-
/* @__PURE__ */ jsx2(Text2, { color: toast.startsWith("\u2713") ? color.green : toast.startsWith("\u2717") ? color.red : toast.startsWith("!") ? color.yellow : color.muted, wrap: "truncate-end", children: " " + toast }),
|
|
1957
|
-
toast ? null : /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: hints + " " })
|
|
1996
|
+
/* @__PURE__ */ jsxs2(Box2, { height: bottomH, width: termCols, justifyContent: "space-between", children: [
|
|
1997
|
+
toastLines.length ? /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: toastLines.map((l, i) => /* @__PURE__ */ jsx2(Text2, { wrap: "truncate-end", color: toast.startsWith("\u2713") ? color.green : toast.startsWith("\u2717") ? color.red : toast.startsWith("!") ? color.yellow : color.muted, children: l }, i)) }) : /* @__PURE__ */ jsx2(Text2, { color: toast.startsWith("\u2713") ? color.green : toast.startsWith("\u2717") ? color.red : toast.startsWith("!") ? color.yellow : color.muted, wrap: "truncate-end", children: " " + toast }),
|
|
1998
|
+
toast && modal?.kind !== "confirm" ? null : /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: " " + hints + " " })
|
|
1958
1999
|
] })
|
|
1959
2000
|
] });
|
|
1960
2001
|
}
|
|
@@ -1999,7 +2040,7 @@ function Help({ width, height }) {
|
|
|
1999
2040
|
const notes = [
|
|
2000
2041
|
'\u2715 problem \xB7 \u26A0 suggestion \xB7 \u2605 recommended fix \xB7 "your call" = a decision, never auto-fixed',
|
|
2001
2042
|
"Source: lib = from your library \xB7 repo = committed by the team \xB7 untracked = only on this machine",
|
|
2002
|
-
"Usage counts Claude Code sessions only:
|
|
2043
|
+
"Usage counts Claude Code sessions only: skilllib reads no other agent's transcripts."
|
|
2003
2044
|
];
|
|
2004
2045
|
const lines = [
|
|
2005
2046
|
...keys.flatMap(([k, d]) => wrap(d, width - 16, 3).map((l, i) => [[(i ? "" : k).padEnd(12), color.accent], [l, color.text]])),
|
|
@@ -2010,12 +2051,9 @@ function Help({ width, height }) {
|
|
|
2010
2051
|
}
|
|
2011
2052
|
|
|
2012
2053
|
// src/tui/load.ts
|
|
2013
|
-
import {
|
|
2014
|
-
import { existsSync as existsSync3, lstatSync
|
|
2015
|
-
import { basename, dirname, join as join2 } from "path";
|
|
2016
|
-
function tildify2(p) {
|
|
2017
|
-
return tildify(p).replace(/\\/g, "/");
|
|
2018
|
-
}
|
|
2054
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
2055
|
+
import { existsSync as existsSync3, lstatSync } from "fs";
|
|
2056
|
+
import { basename, dirname as dirname2, join as join2, relative, sep } from "path";
|
|
2019
2057
|
function uniqueNames(roots) {
|
|
2020
2058
|
const tail = (r, n) => r.split(/[\\/]/).filter(Boolean).slice(-n).join("/");
|
|
2021
2059
|
return new Map(
|
|
@@ -2059,7 +2097,8 @@ function localSkill(root, s, git) {
|
|
|
2059
2097
|
};
|
|
2060
2098
|
}
|
|
2061
2099
|
function otherCopies(root, name, dir) {
|
|
2062
|
-
|
|
2100
|
+
const self = realPath(join2(root, dir, name));
|
|
2101
|
+
return foldersOf(root, name).real.filter((d) => realPath(join2(d, name)) !== self).map((d) => relativeTo(root, d));
|
|
2063
2102
|
}
|
|
2064
2103
|
function installedAt(path) {
|
|
2065
2104
|
try {
|
|
@@ -2073,8 +2112,8 @@ function machineSkill(s, kept) {
|
|
|
2073
2112
|
const global = s.kind === "global" || s.kind === "skills.sh";
|
|
2074
2113
|
return {
|
|
2075
2114
|
name: s.name,
|
|
2076
|
-
source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : s.kind === "system" ? "system" : s.kind === "skilllib" ? "skilllib" : s.kind === "built-in" && s.harnesses[0] === "grok" ? "grok" : "cursor",
|
|
2077
|
-
where: global || s.kind === "skilllib" ?
|
|
2115
|
+
source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : s.kind === "system" || s.kind === "built-in" && s.harnesses[0] === "codex" ? "system" : s.kind === "skilllib" ? "skilllib" : s.kind === "built-in" && s.harnesses[0] === "grok" ? "grok" : "cursor",
|
|
2116
|
+
where: global || s.kind === "skilllib" ? tildify(dirname2(s.path)) : s.kind === "claude.ai" ? "claude.ai account" : s.kind === "built-in" ? `${s.origin} built-in` : s.origin,
|
|
2078
2117
|
...s.kind === "skills.sh" ? { origin: `npx skills: ${s.origin}` } : {},
|
|
2079
2118
|
path: s.path,
|
|
2080
2119
|
links: s.links,
|
|
@@ -2111,13 +2150,8 @@ function loadWorld() {
|
|
|
2111
2150
|
forgetClaudeBinary();
|
|
2112
2151
|
const agents = enabledHarnesses();
|
|
2113
2152
|
const here = projectHere();
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
return realpathSync(p);
|
|
2117
|
-
} catch {
|
|
2118
|
-
return p;
|
|
2119
|
-
}
|
|
2120
|
-
};
|
|
2153
|
+
if (here) rememberProjects([here]);
|
|
2154
|
+
const real = realPath;
|
|
2121
2155
|
const visible = visibleProjects();
|
|
2122
2156
|
const cwd = here ? visible.find((p) => real(p) === real(here)) ?? here : null;
|
|
2123
2157
|
const listed = [.../* @__PURE__ */ new Set([...cwd ? [cwd] : [], ...visible])];
|
|
@@ -2132,7 +2166,7 @@ function loadWorld() {
|
|
|
2132
2166
|
const libraryNames = new Set(library.map((l) => l.name));
|
|
2133
2167
|
const kept = keptGlobal();
|
|
2134
2168
|
const cursorList = agents.includes("cursor") ? cursorPlugins() : [];
|
|
2135
|
-
const sourced = machineSkills(agents);
|
|
2169
|
+
const sourced = machineSkills(agents).filter((s) => s.harnesses.length || s.broken);
|
|
2136
2170
|
const machine = sourced.map((s) => {
|
|
2137
2171
|
descriptions[s.name] ??= s.description;
|
|
2138
2172
|
return machineSkill(s, kept);
|
|
@@ -2153,7 +2187,11 @@ function loadWorld() {
|
|
|
2153
2187
|
name: names.get(root),
|
|
2154
2188
|
path: root,
|
|
2155
2189
|
manifest: hasManifest ? git ? git.of("skilllib.json") : "no git" : "none",
|
|
2156
|
-
skills: status.map((s) =>
|
|
2190
|
+
skills: status.map((s) => {
|
|
2191
|
+
const local = localSkill(root, s, git);
|
|
2192
|
+
const own = s.state.includes("missing") ? "" : readSkillInfo(s.path).description;
|
|
2193
|
+
return own && own !== descriptions[s.name] ? { ...local, description: own } : local;
|
|
2194
|
+
}),
|
|
2157
2195
|
...loadedHere(root)
|
|
2158
2196
|
};
|
|
2159
2197
|
});
|
|
@@ -2169,7 +2207,7 @@ function loadWorld() {
|
|
|
2169
2207
|
library,
|
|
2170
2208
|
usage: {},
|
|
2171
2209
|
descriptions,
|
|
2172
|
-
backups: backups.map((b) => ({ name: b.name, from:
|
|
2210
|
+
backups: backups.map((b) => ({ name: b.name, from: tildify(b.from), at: b.movedAt })),
|
|
2173
2211
|
...(({ roots: roots2, hidden }) => ({ roots: roots2, hidden }))(readConfig()),
|
|
2174
2212
|
agentsChosen: harnessesChosen(),
|
|
2175
2213
|
agentSkill: agentSkillState(agents),
|
|
@@ -2186,7 +2224,7 @@ async function loadUsage(w) {
|
|
|
2186
2224
|
const today = midnight(Date.now());
|
|
2187
2225
|
const totals = {};
|
|
2188
2226
|
for (const use of await scanUsage(30)) {
|
|
2189
|
-
const skill = use.skill
|
|
2227
|
+
const skill = skillOf(use.skill);
|
|
2190
2228
|
totals[skill] = (totals[skill] ?? 0) + 1;
|
|
2191
2229
|
if (skill !== use.skill) totals[use.skill] = (totals[use.skill] ?? 0) + 1;
|
|
2192
2230
|
const root = projectOf(use.cwd);
|
|
@@ -2200,7 +2238,9 @@ async function loadUsage(w) {
|
|
|
2200
2238
|
}
|
|
2201
2239
|
function said3(c, repo) {
|
|
2202
2240
|
if (c.action === "skipped") return `${c.name}: ${c.reason ?? "skipped"}`;
|
|
2203
|
-
if (c.action === "removed") return `Removed ${c.name} from ${repo}`;
|
|
2241
|
+
if (c.action === "removed") return `Removed ${c.name} from ${repo}${c.reason ? ` (${c.reason})` : ""}`;
|
|
2242
|
+
if (c.action === "unchanged") return `${c.name}${c.to ? ` v${c.to}` : ""} is already in ${repo}`;
|
|
2243
|
+
if (c.action === "relinked") return `Linked ${c.name}${c.to ? ` v${c.to}` : ""} for your other agents in ${repo}`;
|
|
2204
2244
|
const back = c.action === "updated" && c.from !== void 0 && c.to !== void 0 && c.to < c.from;
|
|
2205
2245
|
const done = back ? `Pinned ${c.name} to v${c.to} in ${repo} (was v${c.from})` : `${c.action === "installed" ? "Added" : c.action === "reset" ? "Reset" : "Updated"} ${c.name}${c.to ? ` v${c.to}` : ""} in ${repo}`;
|
|
2206
2246
|
return c.backedUp ? `${done} (your edits are in Settings \u203A Backups)` : done;
|
|
@@ -2212,10 +2252,11 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2212
2252
|
const copyDir = (repo, name) => readManifest(rootOf(repo)).skills[name]?.dir ?? PROJECT_SKILLS_DIR;
|
|
2213
2253
|
const withLinks = (c, repo, name) => {
|
|
2214
2254
|
const it = (dirs) => dirs.length === 1 ? "it" : "them";
|
|
2255
|
+
const way = c.inTheWay?.length ? `; not linked everywhere: ${inTheWaySaid(rootOf(repo), c.inTheWay)} (clear the way, then link again)` : "";
|
|
2215
2256
|
if (c.uncommitted?.length)
|
|
2216
|
-
return `${said3(c, repo)}; not linked in ${c.uncommitted.join(", ")}: git tracks ${it(c.uncommitted)} and ${copyDir(repo, name)}/${name} isn't committed, so teammates would get broken links`;
|
|
2217
|
-
if (!c.blocked?.length) return result(c, repo);
|
|
2218
|
-
return { message: `${said3(c, repo)}; not linked in ${c.blocked.join(", ")}: git tracks ${it(c.blocked)}`, then: allowTracked([{ repo, name, blocked: c.blocked }]) };
|
|
2257
|
+
return `${said3(c, repo)}${way}; not linked in ${c.uncommitted.join(", ")}: git tracks ${it(c.uncommitted)} and ${copyDir(repo, name)}/${name} isn't committed, so teammates would get broken links`;
|
|
2258
|
+
if (!c.blocked?.length) return way ? failed(`${said3(c, repo)}${way}`) : result(c, repo);
|
|
2259
|
+
return { message: `${said3(c, repo)}${way}; not linked in ${c.blocked.join(", ")}: git tracks ${it(c.blocked)}`, then: allowTracked([{ repo, name, blocked: c.blocked }]) };
|
|
2219
2260
|
};
|
|
2220
2261
|
const addTo = (repos, names) => {
|
|
2221
2262
|
if (repos.length === 1 && names.length === 1) return withLinks(addSkill(rootOf(repos[0]), names[0]), repos[0], names[0]);
|
|
@@ -2237,10 +2278,11 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2237
2278
|
const why = kind === "blocked" ? `git tracks ${dirs.length === 1 ? "it" : "them"}` : `${copies.join(", ")} ${copies.length === 1 ? "isn't" : "aren't"} committed, so a link would break for teammates`;
|
|
2238
2279
|
notes.push(`${who}not linked in ${dirs.join(", ")} in ${repo}: ${why}`);
|
|
2239
2280
|
}
|
|
2281
|
+
for (const x of done.filter((d) => d.c.inTheWay?.length)) notes.push(`${x.name} not linked everywhere in ${x.repo}: ${inTheWaySaid(rootOf(x.repo), x.c.inTheWay)}`);
|
|
2240
2282
|
const skipped = changes.filter((x) => x.c.action === "skipped").map((x) => `${x.name} not added to ${x.repo}: ${x.c.reason}`);
|
|
2241
2283
|
const text = [summary, ...notes, ...skipped].filter(Boolean).join("; ");
|
|
2242
2284
|
const held = done.flatMap((x) => x.c.blocked?.length ? [{ repo: x.repo, name: x.name, blocked: x.c.blocked }] : []);
|
|
2243
|
-
const result2 = !done.length ? failed(text) : held.length ? { message: text, then: allowTracked(held) } : skipped.length ? failed(text) : text;
|
|
2285
|
+
const result2 = !done.length ? failed(text) : held.length ? { message: text, then: allowTracked(held) } : skipped.length || done.some((x) => x.c.inTheWay?.length) ? failed(text) : text;
|
|
2244
2286
|
return { result: result2, skipped, notes };
|
|
2245
2287
|
};
|
|
2246
2288
|
const allowTracked = (held) => {
|
|
@@ -2255,7 +2297,7 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2255
2297
|
const linked = results.filter((x) => x.r.created.length);
|
|
2256
2298
|
const names = [...new Set(linked.map((x) => x.name))];
|
|
2257
2299
|
const summary = linked.length ? `Linked ${names.length === 1 ? names[0] : `${names.length} skills`} in ${[...new Set(linked.flatMap((x) => x.r.created))].join(", ")} in ${[...new Set(linked.map((x) => x.repo))].join(", ")}` : "";
|
|
2258
|
-
const others = results.filter((x) => !x.r.created.length || x.r.uncommitted.length).map((x) => `${x.repo}: ${linkSaid(x.name, x.r)}`);
|
|
2300
|
+
const others = results.filter((x) => !x.r.created.length || x.r.uncommitted.length || x.r.inTheWay.length).map((x) => `${x.repo}: ${linkSaid(x.repo, x.name, x.r)}`);
|
|
2259
2301
|
return [summary, ...others].join("; ");
|
|
2260
2302
|
}
|
|
2261
2303
|
};
|
|
@@ -2264,15 +2306,19 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2264
2306
|
const s = find(repo, name);
|
|
2265
2307
|
return s.managed ? relinkDependency(rootOf(repo), name, { allowTracked: allow }) : linkEverywhere(rootOf(repo), name, s.location, { allowTracked: allow });
|
|
2266
2308
|
};
|
|
2267
|
-
const linkSaid = (name, r) => {
|
|
2309
|
+
const linkSaid = (repo, name, r) => {
|
|
2268
2310
|
const linked = r.created.length ? `linked in ${r.created.join(", ")}` : "";
|
|
2269
2311
|
const notLinked = r.uncommitted.length ? `not linked in ${r.uncommitted.join(", ")}: git tracks ${r.uncommitted.length === 1 ? "it" : "them"} and ${name} isn't committed, so teammates would get broken links` : "";
|
|
2270
|
-
|
|
2312
|
+
const inTheWay = r.inTheWay.length ? inTheWaySaid(rootOf(repo), r.inTheWay) : "";
|
|
2313
|
+
return `${name}: ${[linked, notLinked, inTheWay].filter(Boolean).join("; ") || "nothing to link"}`;
|
|
2271
2314
|
};
|
|
2272
|
-
const orForce = (c, repo, label, force) =>
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2315
|
+
const orForce = (c, repo, label, force) => (
|
|
2316
|
+
// Whatever --force would get past (edits, a pin from another library, another copy in the way): offer it here.
|
|
2317
|
+
c.action === "skipped" && !!c.reason?.includes("--force") ? {
|
|
2318
|
+
message: `${c.name} in ${repo}: ${c.reason.replace(/ \(use --force[^)]*\)$/, "")}`,
|
|
2319
|
+
then: { label, preview: `What's there now in ${repo} goes to Settings \u203A Backups first.`, run: () => result(force(), repo) }
|
|
2320
|
+
} : result(c, repo)
|
|
2321
|
+
);
|
|
2276
2322
|
const toLibrary = (path) => {
|
|
2277
2323
|
const r = importSkill(path, { force: true });
|
|
2278
2324
|
return r.status === "unchanged" ? `${r.name} is already the same in your library` : `${r.name} v${latestVersion(r.name)?.version} is in your library`;
|
|
@@ -2296,48 +2342,64 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2296
2342
|
},
|
|
2297
2343
|
copyToLibrary: (repo, name) => toLibrary(find(repo, name).path),
|
|
2298
2344
|
deleteGlobal: (m) => {
|
|
2299
|
-
|
|
2300
|
-
|
|
2345
|
+
if (!entryExists(m.path)) return `${tildify(m.path)} is already gone`;
|
|
2346
|
+
const others = machine.filter((o) => o.name === m.name && o.path !== m.path && o.kind !== "plugin" && !o.broken);
|
|
2347
|
+
const dependents = others.filter((o) => !survives(o.path, [m.path, ...m.links]));
|
|
2348
|
+
const stays = others.filter((o) => !dependents.includes(o));
|
|
2349
|
+
if (!m.broken) {
|
|
2350
|
+
const readers = [.../* @__PURE__ */ new Set([...m.agents, ...dependents.flatMap((o) => o.harnesses)])].filter((id) => !stays.some((o) => o.harnesses.includes(id)));
|
|
2351
|
+
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, (root) => [...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root))}`);
|
|
2353
|
+
}
|
|
2354
|
+
const left = m.broken ? [] : strandedLinks(m.name, [{ path: m.path, links: [...m.links, ...dependents.map((o) => o.path)] }], [...roots.values(), ...knownProjects()], (root) => [...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root));
|
|
2355
|
+
const r = deleteGlobal(m.path, [...m.links, ...dependents.map((o) => o.path)]);
|
|
2356
|
+
if (!r.ok) return failed(`${m.name}: ${r.reason}`);
|
|
2357
|
+
if (m.broken) return `Removed the broken link ${m.name}`;
|
|
2358
|
+
const still = stays.map((o) => tildify(o.path));
|
|
2359
|
+
return `Deleted ${tildify(m.path)} (in Settings \u203A Backups)${still.length ? `; ${m.name} still loads globally from ${still.join(", ")}` : ""}${left.length ? `; ${left.join(", ")} now point${left.length === 1 ? "s" : ""} at nothing: remove ${left.length === 1 ? "it" : "them"}` : ""}`;
|
|
2301
2360
|
},
|
|
2302
2361
|
keepGlobal: (name, keep) => (setKeepGlobal([name], keep), keep ? `${name} marked as global on purpose` : `${name} is no longer marked as global on purpose`),
|
|
2303
2362
|
moveGlobal: (skills, repos, group) => {
|
|
2304
|
-
const moved = [];
|
|
2305
|
-
const notes = [];
|
|
2306
|
-
const held = [];
|
|
2307
2363
|
const repoRoots = repos.map(rootOf);
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
moved.
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
}
|
|
2364
|
+
const label = (root) => repos[repoRoots.indexOf(root)] ?? [...roots].find(([, r]) => r === root)?.[0] ?? basename(root);
|
|
2365
|
+
const picked = new Set(skills.map((m) => m.name));
|
|
2366
|
+
const sourced = machine.filter((o) => picked.has(o.name) && o.movable && !o.broken);
|
|
2367
|
+
const attempt = (names) => moveGlobalSkills(names ? sourced.filter((m) => names.includes(m.name)) : sourced, repoRoots, { group, label, allowTrackedHint: "allow links there, then move it again" });
|
|
2368
|
+
const said4 = (results2) => {
|
|
2369
|
+
const moved2 = results2.filter((mv) => mv.moved).map((mv) => mv.name);
|
|
2370
|
+
const which = moved2.length <= 4 ? moved2.join(", ") : `${moved2.length} skills`;
|
|
2371
|
+
const summary = moved2.length ? `${which} now load${moved2.length === 1 ? "s" : ""} only in ${repos.join(", ")}` : "";
|
|
2372
|
+
const left = results2.flatMap((mv) => mv.left ?? []);
|
|
2373
|
+
return {
|
|
2374
|
+
moved: moved2,
|
|
2375
|
+
text: [summary, ...left.length ? [`${left.join(", ")} now point${left.length === 1 ? "s" : ""} at nothing: remove ${left.length === 1 ? "it" : "them"}`] : [], ...results2.filter((mv) => !mv.moved).map((mv) => `${mv.name}: ${mv.reason}`)].filter(Boolean).join("; ") || "Nothing moved"
|
|
2376
|
+
};
|
|
2377
|
+
};
|
|
2378
|
+
const results = attempt();
|
|
2379
|
+
const { moved, text } = said4(results);
|
|
2380
|
+
const finishable = (mv) => !mv.moved && mv.changes.length > 0 && mv.changes.every(([, c]) => c.action !== "skipped" && !c.inTheWay?.length && !c.uncommitted?.length) && mv.changes.some(([, c]) => c.blocked?.length);
|
|
2381
|
+
const held = results.filter(finishable).flatMap((mv) => mv.changes.flatMap(([root, c]) => c.blocked?.length ? [{ repo: label(root), name: mv.name, blocked: c.blocked }] : []));
|
|
2382
|
+
if (held.length) {
|
|
2383
|
+
const ask = allowTracked(held);
|
|
2384
|
+
const names = [...new Set(held.map((h) => h.name))];
|
|
2385
|
+
return {
|
|
2386
|
+
message: text,
|
|
2387
|
+
then: {
|
|
2388
|
+
...ask,
|
|
2389
|
+
label: `${ask.label}, then stop loading ${names.length === 1 ? "it" : "them"} globally`,
|
|
2390
|
+
// The retry is what says whether it's done: a skill still global after it is a failure, said as one.
|
|
2391
|
+
run: (w) => {
|
|
2392
|
+
const linked = said(ask.run(w));
|
|
2393
|
+
const retry = said4(attempt(names));
|
|
2394
|
+
const before = said4(results.filter((mv) => mv.moved));
|
|
2395
|
+
const others = results.filter((mv) => !mv.moved && !names.includes(mv.name)).map((mv) => `${mv.name}: ${mv.reason}`);
|
|
2396
|
+
const text2 = [before.moved.length ? before.text : "", linked, retry.text, ...others].filter(Boolean).join("; ");
|
|
2397
|
+
return retry.moved.length === names.length ? text2 : failed(text2);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
};
|
|
2321
2401
|
}
|
|
2322
|
-
|
|
2323
|
-
for (const repo of new Set(held.map((h) => h.repo)))
|
|
2324
|
-
for (const shared of [true, false]) {
|
|
2325
|
-
const here = held.filter((h) => h.repo === repo && h.shared === shared);
|
|
2326
|
-
if (!here.length) continue;
|
|
2327
|
-
const agents = [...new Set(here.flatMap((h) => h.blind))].map((id) => harness(id).name).join(", ");
|
|
2328
|
-
const what = skills.length === 1 ? "it" : here.length === moved.length ? "them" : here.map((h) => h.name).join(", ");
|
|
2329
|
-
const dirs = [...new Set(here.flatMap((h) => h.blocked))].join(", ");
|
|
2330
|
-
const why = shared ? `git tracks ${dirs}` : `${[...new Set(here.map((h) => h.copy))].join(", ")} isn't committed, so a link in ${dirs} would break for teammates`;
|
|
2331
|
-
const key = `${agents}|${what}|${why}`;
|
|
2332
|
-
if (!gaps.has(key)) gaps.set(key, { who: `${agents} can't see ${what}`, why, repos: [] });
|
|
2333
|
-
gaps.get(key).repos.push(repo);
|
|
2334
|
-
}
|
|
2335
|
-
const blindNotes = [...gaps.values()].map((g) => `${g.who} in ${g.repos.join(", ")}: ${g.why}`);
|
|
2336
|
-
const summary = moved.length ? `${moved.length === 1 ? moved[0] : `${moved.length} skills`} now load${moved.length === 1 ? "s" : ""} only in ${repos.join(", ")}` : "";
|
|
2337
|
-
const text = [summary, ...blindNotes, ...notes].filter(Boolean).join("; ") || "Nothing moved";
|
|
2338
|
-
const askable = held.filter((h) => h.shared);
|
|
2339
|
-
if (!moved.length && notes.length) return failed(text);
|
|
2340
|
-
return askable.length ? { message: text, then: allowTracked(askable) } : text;
|
|
2402
|
+
return moved.length ? text : failed(text);
|
|
2341
2403
|
},
|
|
2342
2404
|
createSkill: (name) => {
|
|
2343
2405
|
const r = createSkill(name, "");
|
|
@@ -2350,6 +2412,10 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2350
2412
|
return mine !== null && mine !== treeHash(s.path);
|
|
2351
2413
|
});
|
|
2352
2414
|
if (clashes.length) return failed(`${id} kept: your library has different ${clashes.map((s) => s.name).join(", ")}; update or delete ${clashes.length === 1 ? "it" : "them"} first`);
|
|
2415
|
+
const cant = repos.flatMap(
|
|
2416
|
+
(repo) => p.skills.flatMap((s) => ((why) => why ? [`${s.name} in ${repo}: ${why.replace(/ \(use --force[^)]*\)$/, "")}`] : [])(trackBlocker(rootOf(repo), s.name) ?? addRefusal(rootOf(repo), s.name, treeHash(s.path) ?? "")))
|
|
2417
|
+
);
|
|
2418
|
+
if (cant.length) return failed(`${id} kept, nothing changed: ${cant.join("; ")}`);
|
|
2353
2419
|
const imported = p.skills.map((s) => ({ name: s.name, status: importSkill(s.path).status }));
|
|
2354
2420
|
for (const r of imported) if (r.status !== "unchanged") recordOrigin(r.name, `plugin: ${id}`);
|
|
2355
2421
|
const added = repos.length ? addMany(repos, p.skills.map((s) => s.name)) : { result: "", skipped: [], notes: [] };
|
|
@@ -2386,21 +2452,31 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2386
2452
|
return r.ok ? r.message : failed(r.message);
|
|
2387
2453
|
},
|
|
2388
2454
|
deleteLibrary: (name) => {
|
|
2455
|
+
const using = projectsUsing(name);
|
|
2456
|
+
const unreadable = readableRoots(using).unreadable.map((u) => u.root);
|
|
2457
|
+
if (unreadable.length) return failed(`${name} is pinned in ${unreadable.map((r2) => basename(r2)).join(", ")}, whose skilllib.json can't be read: fix it first, then delete`);
|
|
2458
|
+
const hidden = using.filter((root) => ![...roots.values()].includes(root));
|
|
2459
|
+
if (hidden.length) return failed(`${name} is also in ${hidden.map((r2) => basename(r2)).join(", ")}, which ${hidden.length === 1 ? "is" : "are"} hidden: unhide ${hidden.length === 1 ? "it" : "them"} (Settings) to delete it everywhere`);
|
|
2389
2460
|
const from = [];
|
|
2390
|
-
|
|
2461
|
+
const left = [];
|
|
2462
|
+
for (const root of using) {
|
|
2391
2463
|
const s = projectStatus(root).find((x) => x.name === name && x.managed);
|
|
2392
|
-
if (s &&
|
|
2393
|
-
removeSkill(root, name, { force: true });
|
|
2394
|
-
|
|
2464
|
+
if (s && isDir(s.path) && !notWritable(root, relative(root, dirname2(s.path)).split(sep).join("/"))) removeUntracked(root, name, s.path);
|
|
2465
|
+
const c = removeSkill(root, name, { force: true });
|
|
2466
|
+
if (c.action === "skipped") return failed(`${name}: ${c.reason}${from.length ? `; removed from ${from.join(", ")} so far` : ""}`);
|
|
2467
|
+
const repo = [...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root);
|
|
2468
|
+
from.push(repo);
|
|
2469
|
+
if (c.reason) left.push(`in ${repo}, ${c.reason}`);
|
|
2395
2470
|
}
|
|
2396
2471
|
const r = deleteLibrarySkill(name);
|
|
2397
2472
|
if (!r.ok) return failed(`${name}: ${r.reason}`);
|
|
2398
|
-
return `Deleted ${name} from your library${from.length ? ` and from ${from.join(", ")}` : ""} (in Settings \u203A Backups)`;
|
|
2473
|
+
return `Deleted ${name} from your library${from.length ? ` and from ${from.join(", ")}` : ""} (in Settings \u203A Backups)${left.length ? `; ${left.join("; ")}` : ""}`;
|
|
2399
2474
|
},
|
|
2400
2475
|
restoreBackup: (i) => {
|
|
2401
|
-
const
|
|
2402
|
-
const
|
|
2403
|
-
|
|
2476
|
+
const done = movedTogether(backups, backups[i]).map((b) => [b, b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b)]);
|
|
2477
|
+
const bad = done.filter(([, r]) => !r.ok);
|
|
2478
|
+
if (bad.length) return failed(bad.map(([b, r]) => `${b.name}: ${!r.ok ? r.reason : ""}`).join("; "));
|
|
2479
|
+
return done.map(([b, r]) => `Restored ${b.name} to ${r.ok ? tildify(r.to) : ""}`).join(" \xB7 ");
|
|
2404
2480
|
},
|
|
2405
2481
|
setAgents: (ids) => (setHarnesses(ids), "Saved your agents"),
|
|
2406
2482
|
installVersion: (repo, name, version) => withLinks(addSkill(rootOf(repo), name, { version, force: true }), repo, name),
|
|
@@ -2409,28 +2485,28 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2409
2485
|
addRoot: (path) => {
|
|
2410
2486
|
const root = addRoot(path);
|
|
2411
2487
|
discoverProjects();
|
|
2412
|
-
return `Scanning ${
|
|
2488
|
+
return `Scanning ${tildify(root)} for repos`;
|
|
2413
2489
|
},
|
|
2414
|
-
removeRoot: (path) => (removeRoot(path), `Stopped scanning ${
|
|
2490
|
+
removeRoot: (path) => (removeRoot(path), `Stopped scanning ${tildify(path)}; repos already found stay until you hide them`),
|
|
2415
2491
|
rescan: () => (discoverProjects(), "Looked for repos in your project folders again"),
|
|
2416
2492
|
hide: (repo) => (setHidden(rootOf(repo), true), `Hid ${repo}: Settings \u203A Hidden repos brings it back`),
|
|
2417
|
-
unhide: (path) => (setHidden(path, false), `${
|
|
2493
|
+
unhide: (path) => (setHidden(path, false), `${tildify(path)} is back in the list`),
|
|
2418
2494
|
openFolder: (path) => {
|
|
2419
2495
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
2420
|
-
|
|
2421
|
-
return `Opened ${
|
|
2496
|
+
const r = spawnSync3(cmd, [path], { stdio: "ignore", timeout: 1e4 });
|
|
2497
|
+
return r.error || r.status !== 0 && process.platform !== "win32" ? failed(`Couldn't open ${tildify(path)} (${cmd} ${r.error ? "isn't available" : "failed"})`) : `Opened ${tildify(path)}`;
|
|
2422
2498
|
},
|
|
2423
2499
|
copy: (text, what) => {
|
|
2424
2500
|
const r = copyText(text);
|
|
2425
|
-
return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor, Codex or Grok` : `Couldn't reach the clipboard; ${what} is in ${
|
|
2501
|
+
return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor, Codex or Grok` : `Couldn't reach the clipboard; ${what} is in ${tildify(r.savedTo)}`;
|
|
2426
2502
|
},
|
|
2427
2503
|
installAgentSkill: () => {
|
|
2428
2504
|
const r = installAgentSkill();
|
|
2429
|
-
return r.ok ? `Your agents can now use skilllib (${r.dirs.map(
|
|
2505
|
+
return r.ok ? `Your agents can now use skilllib (${r.dirs.map(tildify).join(", ")})` : failed(`skilllib skill: ${r.reason}`);
|
|
2430
2506
|
},
|
|
2431
2507
|
removeAgentSkill: () => {
|
|
2432
2508
|
const removed = removeAgentSkill();
|
|
2433
|
-
return removed.length ? `Removed the skilllib skill (${removed.map(
|
|
2509
|
+
return removed.length ? `Removed the skilllib skill (${removed.map(tildify).join(", ")})` : failed("The skilllib skill isn't installed");
|
|
2434
2510
|
},
|
|
2435
2511
|
usageIssues: ({ usage, totals = {} }) => {
|
|
2436
2512
|
const repoOf = new Map([...roots].map(([name, root]) => [root, name]));
|
|
@@ -2447,6 +2523,7 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2447
2523
|
// src/tui/index.tsx
|
|
2448
2524
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
2449
2525
|
async function tui() {
|
|
2526
|
+
discoverProjects();
|
|
2450
2527
|
const instance = render(/* @__PURE__ */ jsx3(App, { initial: loadWorld(), reload: loadWorld, loadUsage }), { alternateScreen: true, exitOnCtrlC: true });
|
|
2451
2528
|
await instance.waitUntilExit();
|
|
2452
2529
|
}
|