skilllib 1.0.2 → 1.2.1
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 +51 -13
- package/dist/{chunk-67BDL5SN.js → chunk-O5TY4NWZ.js} +696 -88
- package/dist/index.js +14 -3
- package/dist/{tui-CQCVL6IK.js → tui-6BOQIKAU.js} +200 -149
- package/package.json +1 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ALL_PROJECT_DIRS,
|
|
3
3
|
HARNESSES,
|
|
4
|
-
MANIFEST_FILE,
|
|
5
4
|
addRoot,
|
|
6
5
|
addSkill,
|
|
7
6
|
allowTrackedLinks,
|
|
@@ -13,10 +12,12 @@ import {
|
|
|
13
12
|
findIssues,
|
|
14
13
|
findProjectRoot,
|
|
15
14
|
forgetLatest,
|
|
15
|
+
gitInfo,
|
|
16
16
|
harnessesChosen,
|
|
17
17
|
importSkill,
|
|
18
18
|
installDirs,
|
|
19
19
|
isProjectCandidate,
|
|
20
|
+
keptGlobal,
|
|
20
21
|
latestVersion,
|
|
21
22
|
libraryOrigins,
|
|
22
23
|
librarySkillDir,
|
|
@@ -25,17 +26,22 @@ import {
|
|
|
25
26
|
linkEverywhere,
|
|
26
27
|
listBackups,
|
|
27
28
|
machineSkills,
|
|
29
|
+
pluginBackups,
|
|
28
30
|
projectOfFactory,
|
|
29
31
|
projectStatus,
|
|
30
32
|
readConfig,
|
|
31
33
|
readSkillInfo,
|
|
34
|
+
relativeTo,
|
|
32
35
|
relinkDependency,
|
|
33
36
|
removeRoot,
|
|
34
37
|
removeSkill,
|
|
35
38
|
restoreBackup,
|
|
39
|
+
restorePlugin,
|
|
40
|
+
runFix,
|
|
36
41
|
scanUsage,
|
|
37
42
|
setHarnesses,
|
|
38
43
|
setHidden,
|
|
44
|
+
setKeepGlobal,
|
|
39
45
|
skilllibHome,
|
|
40
46
|
summarize,
|
|
41
47
|
syncProject,
|
|
@@ -46,7 +52,7 @@ import {
|
|
|
46
52
|
versionHistory,
|
|
47
53
|
visibilityOf,
|
|
48
54
|
visibleProjects
|
|
49
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-O5TY4NWZ.js";
|
|
50
56
|
|
|
51
57
|
// src/tui/index.tsx
|
|
52
58
|
import { render } from "ink";
|
|
@@ -54,7 +60,7 @@ import { render } from "ink";
|
|
|
54
60
|
// src/tui/App.tsx
|
|
55
61
|
import { spawn, spawnSync as spawnSync2 } from "child_process";
|
|
56
62
|
import { homedir } from "os";
|
|
57
|
-
import { basename as basename3, dirname, join as
|
|
63
|
+
import { basename as basename3, dirname, join as join3, sep } from "path";
|
|
58
64
|
import { useEffect, useRef, useState as useState2 } from "react";
|
|
59
65
|
import { Box as Box3, Text as Text3, useApp, useInput, useWindowSize } from "ink";
|
|
60
66
|
import { lstatSync } from "fs";
|
|
@@ -102,6 +108,8 @@ function stateBadge(state) {
|
|
|
102
108
|
return { icon: "\u2713", label: "in your skills too", color: color.green };
|
|
103
109
|
case "repo skill, differs from library":
|
|
104
110
|
return { icon: "\u2260", label: "differs from yours", color: color.yellow };
|
|
111
|
+
case "from npx skills":
|
|
112
|
+
return { icon: "\u2193", label: "npx skills", color: color.magenta };
|
|
105
113
|
case "available":
|
|
106
114
|
return { icon: " ", label: "", color: color.faint };
|
|
107
115
|
}
|
|
@@ -260,10 +268,11 @@ function loadSnapshot() {
|
|
|
260
268
|
library,
|
|
261
269
|
machine,
|
|
262
270
|
issues: findIssues(projects, machine, new Set(library.map((l) => l.name)), statusOf),
|
|
263
|
-
backups: listBackups(),
|
|
271
|
+
backups: [...listBackups(), ...pluginBackups()].sort((a, b) => b.movedAt.localeCompare(a.movedAt)),
|
|
264
272
|
roots: readConfig().roots,
|
|
265
273
|
harnesses: enabledHarnesses(),
|
|
266
|
-
harnessesChosen: harnessesChosen()
|
|
274
|
+
harnessesChosen: harnessesChosen(),
|
|
275
|
+
keptGlobal: keptGlobal()
|
|
267
276
|
};
|
|
268
277
|
}
|
|
269
278
|
function projectRows(root, library, status = projectStatus(root)) {
|
|
@@ -281,6 +290,7 @@ function projectRows(root, library, status = projectStatus(root)) {
|
|
|
281
290
|
location: s.location,
|
|
282
291
|
path: s.path,
|
|
283
292
|
visibility: s.visibility,
|
|
293
|
+
...s.source ? { source: s.source } : {},
|
|
284
294
|
description: library.find((l) => l.name === s.name)?.description ?? readSkillInfo(s.path).description
|
|
285
295
|
})
|
|
286
296
|
);
|
|
@@ -351,6 +361,7 @@ var NEXT_MODE = { move: "keep", keep: "delete", delete: "move" };
|
|
|
351
361
|
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
352
362
|
function CleanupWizard({
|
|
353
363
|
skills,
|
|
364
|
+
kept,
|
|
354
365
|
projects,
|
|
355
366
|
usesIn,
|
|
356
367
|
usesTotal,
|
|
@@ -367,11 +378,19 @@ function CleanupWizard({
|
|
|
367
378
|
const [step, setStep] = useState("choose");
|
|
368
379
|
const [focus, setFocus] = useState("skills");
|
|
369
380
|
const [modes, setModes] = useState(
|
|
370
|
-
() => new Map(skills.map((s) => [s.path,
|
|
381
|
+
() => new Map(skills.map((s) => [s.path, s.name === initialSkill || !initialSkill && !kept.has(s.name) ? "move" : "keep"]))
|
|
371
382
|
);
|
|
372
383
|
const modeOf = (s) => modes.get(s.path) ?? "keep";
|
|
373
|
-
const
|
|
374
|
-
const
|
|
384
|
+
const [touched, setTouched] = useState(() => new Set(initialSkill ? [] : skills.map((s) => s.path)));
|
|
385
|
+
const setMode = (s, mode) => {
|
|
386
|
+
setModes((prev) => new Map(prev).set(s.path, mode));
|
|
387
|
+
setTouched((prev) => new Set(prev).add(s.path));
|
|
388
|
+
};
|
|
389
|
+
const [showKept, setShowKept] = useState(false);
|
|
390
|
+
const shown = (on) => skills.filter((s) => on || !kept.has(s.name) || s.name === initialSkill);
|
|
391
|
+
const list = shown(showKept);
|
|
392
|
+
const hiddenKept = skills.length - shown(false).length;
|
|
393
|
+
const [skillIndex, setSkillIndex] = useState(() => Math.max(0, list.findIndex((s) => s.name === initialSkill)));
|
|
375
394
|
const [projectIndex, setProjectIndex] = useState(0);
|
|
376
395
|
const [status, setStatus] = useState("");
|
|
377
396
|
const [targets, setTargets] = useState(
|
|
@@ -379,9 +398,10 @@ function CleanupWizard({
|
|
|
379
398
|
skills.map((s) => [s.path, new Set(projects.filter((p) => installedIn(s.name).includes(p) || (usesIn(s.name, p) ?? 0) > 0))])
|
|
380
399
|
)
|
|
381
400
|
);
|
|
382
|
-
const skill =
|
|
401
|
+
const skill = list[skillIndex];
|
|
383
402
|
const chosen = skills.filter((s) => modeOf(s) === "move");
|
|
384
403
|
const toDelete = skills.filter((s) => modeOf(s) === "delete");
|
|
404
|
+
const toKeep = skills.filter((s) => modeOf(s) === "keep" && touched.has(s.path) && !kept.has(s.name));
|
|
385
405
|
const totalInstalls = chosen.reduce((n, s) => n + (targets.get(s.path)?.size ?? 0), 0);
|
|
386
406
|
const orphans = chosen.filter((s) => (targets.get(s.path)?.size ?? 0) === 0);
|
|
387
407
|
const toggleTarget = (s, project) => setTargets((prev) => {
|
|
@@ -417,7 +437,9 @@ function CleanupWizard({
|
|
|
417
437
|
if (r.ok) unloaded++;
|
|
418
438
|
else problems.push(`${s.name}: ${r.reason}`);
|
|
419
439
|
}
|
|
420
|
-
|
|
440
|
+
setKeepGlobal(toKeep.map((s) => s.name), true);
|
|
441
|
+
setKeepGlobal([...chosen, ...toDelete].map((s) => s.name).filter((n) => kept.has(n)), false);
|
|
442
|
+
onDone({ unloaded, installs, deleted, kept: toKeep.length, problems });
|
|
421
443
|
};
|
|
422
444
|
inputRef.current = (input, key) => {
|
|
423
445
|
if (key.ctrl && input === "c") return onCancel();
|
|
@@ -427,14 +449,19 @@ function CleanupWizard({
|
|
|
427
449
|
return;
|
|
428
450
|
}
|
|
429
451
|
if (key.escape) return onCancel();
|
|
430
|
-
if (input === "p") return setStatus(onReviewPrompt(
|
|
431
|
-
if (
|
|
452
|
+
if (input === "p") return setStatus(onReviewPrompt(list));
|
|
453
|
+
if (input === "k" && hiddenKept) {
|
|
454
|
+
const next = shown(!showKept);
|
|
455
|
+
setShowKept(!showKept);
|
|
456
|
+
return setSkillIndex(Math.max(0, next.findIndex((s) => s.path === skill?.path)));
|
|
457
|
+
}
|
|
458
|
+
if (key.return) return chosen.length || toDelete.length || toKeep.length ? setStep("review") : void 0;
|
|
432
459
|
if (key.tab || key.rightArrow || key.leftArrow) {
|
|
433
460
|
return setFocus(key.leftArrow ? "skills" : key.rightArrow ? "projects" : focus === "skills" ? "projects" : "skills");
|
|
434
461
|
}
|
|
435
462
|
if (focus === "skills") {
|
|
436
463
|
if (key.upArrow) return setSkillIndex((i) => Math.max(0, i - 1));
|
|
437
|
-
if (key.downArrow) return setSkillIndex((i) => Math.min(
|
|
464
|
+
if (key.downArrow) return setSkillIndex((i) => Math.min(list.length - 1, i + 1));
|
|
438
465
|
if (input === " " && skill) return setMode(skill, NEXT_MODE[modeOf(skill)]);
|
|
439
466
|
if (input === "d" && skill) return setMode(skill, modeOf(skill) === "delete" ? "keep" : "delete");
|
|
440
467
|
} else {
|
|
@@ -460,8 +487,9 @@ function CleanupWizard({
|
|
|
460
487
|
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: color.accent, paddingX: 2, paddingY: 1, height: bodyHeight + 2, children: [
|
|
461
488
|
/* @__PURE__ */ jsx2(Text2, { color: color.text, children: [
|
|
462
489
|
chosen.length ? `${plural(chosen.length, "skill")} ${chosen.length === 1 ? "moves" : "move"} into your projects (${plural(totalInstalls, "install")})` : "",
|
|
463
|
-
toDelete.length ? `${plural(toDelete.length, "skill")} ${toDelete.length === 1 ? "is" : "are"} deleted` : ""
|
|
464
|
-
|
|
490
|
+
toDelete.length ? `${plural(toDelete.length, "skill")} ${toDelete.length === 1 ? "is" : "are"} deleted` : "",
|
|
491
|
+
toKeep.length ? `${plural(toKeep.length, "skill")} ${toKeep.length === 1 ? "stays" : "stay"} global on purpose` : ""
|
|
492
|
+
].filter(Boolean).join(" \xB7 ") + "." }),
|
|
465
493
|
/* @__PURE__ */ jsx2(Text2, { color: color.faint, children: "Moved skills are copied into Your skills first. Every original goes to a backup you can restore from Health." }),
|
|
466
494
|
/* @__PURE__ */ jsx2(Text2, { children: " " }),
|
|
467
495
|
lines.slice(0, Math.max(1, bodyHeight - 7)).map(({ s, where }) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate-end", children: [
|
|
@@ -471,6 +499,7 @@ function CleanupWizard({
|
|
|
471
499
|
] }, s.path)),
|
|
472
500
|
lines.length > bodyHeight - 7 ? /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: `\u2026 and ${lines.length - (bodyHeight - 7)} more` }) : null,
|
|
473
501
|
toDelete.length ? /* @__PURE__ */ jsx2(Text2, { color: color.red, wrap: "truncate-end", children: `\u2715 delete (not kept in Your skills): ${toDelete.map((s) => s.name).join(", ")}` }) : null,
|
|
502
|
+
toKeep.length ? /* @__PURE__ */ jsx2(Text2, { color: color.muted, wrap: "truncate-end", children: `\u2713 keep global, and skip next cleanup: ${toKeep.map((s) => s.name).join(", ")}` }) : null,
|
|
474
503
|
/* @__PURE__ */ jsx2(Box2, { flexGrow: 1 }),
|
|
475
504
|
orphans.length ? /* @__PURE__ */ jsx2(Text2, { color: color.yellow, children: `${plural(orphans.length, "skill")} won't be in any project \u2014 add them later from "Add skills".` }) : null
|
|
476
505
|
] }),
|
|
@@ -490,7 +519,7 @@ function CleanupWizard({
|
|
|
490
519
|
const detailHeight = height >= 26 ? 7 : height >= 20 ? 5 : 0;
|
|
491
520
|
const listHeight = bodyHeight - detailHeight;
|
|
492
521
|
const usesText = (n) => n === null ? "\u2026" : n === 0 ? "unused" : `${n} use${n === 1 ? "" : "s"}`;
|
|
493
|
-
const skillRows =
|
|
522
|
+
const skillRows = list.map((s) => {
|
|
494
523
|
const mode = modeOf(s);
|
|
495
524
|
const n = targets.get(s.path)?.size ?? 0;
|
|
496
525
|
return {
|
|
@@ -500,7 +529,7 @@ function CleanupWizard({
|
|
|
500
529
|
{ text: s.name, grow: true, color: mode === "keep" ? color.muted : mode === "delete" ? color.red : void 0 },
|
|
501
530
|
{ text: usesText(usesTotal(s.name)).padStart(9), width: 9, color: usesTotal(s.name) ? color.text : color.faint },
|
|
502
531
|
{
|
|
503
|
-
text: mode === "keep" ? " keep global" : mode === "delete" ? " delete" : n ? ` \u2192 ${plural(n, "project")}` : " \u2192 no project",
|
|
532
|
+
text: mode === "keep" ? kept.has(s.name) ? " \u2713 kept global" : " keep global" : mode === "delete" ? " delete" : n ? ` \u2192 ${plural(n, "project")}` : " \u2192 no project",
|
|
504
533
|
width: 15,
|
|
505
534
|
color: mode === "keep" ? color.faint : mode === "delete" ? color.red : n ? color.green : color.yellow
|
|
506
535
|
}
|
|
@@ -526,19 +555,19 @@ function CleanupWizard({
|
|
|
526
555
|
] }),
|
|
527
556
|
/* @__PURE__ */ jsxs2(Box2, { height: 2, paddingX: 1, flexDirection: "column", children: [
|
|
528
557
|
/* @__PURE__ */ jsx2(Text2, { color: color.text, wrap: "truncate-end", children: "These load in every repo, so every agent reads them all the time. Pick the projects that should keep each one." }),
|
|
529
|
-
/* @__PURE__ */ jsx2(Text2, { color: color.faint, wrap: "truncate-end", children: status ? `\u2713 ${status}` : "\u25C9 move into the ticked projects \xB7 \u25CB keep global \xB7 \u2715 delete. Pre-ticked: projects that have it or used it. p: ask an agent." })
|
|
558
|
+
/* @__PURE__ */ jsx2(Text2, { color: color.faint, wrap: "truncate-end", children: status ? `\u2713 ${status}` : "\u25C9 move into the ticked projects \xB7 \u25CB keep global (remembered) \xB7 \u2715 delete. Pre-ticked: projects that have it or used it. p: ask an agent." })
|
|
530
559
|
] }),
|
|
531
560
|
/* @__PURE__ */ jsxs2(Box2, { height: listHeight, children: [
|
|
532
561
|
/* @__PURE__ */ jsx2(
|
|
533
562
|
ListPanel,
|
|
534
563
|
{
|
|
535
|
-
title: `Global skills \xB7 ${chosen.length} move \xB7 ${toDelete.length} delete \xB7 uses ${usageDays}d`,
|
|
564
|
+
title: `Global skills \xB7 ${chosen.length} move \xB7 ${toDelete.length} delete${hiddenKept && !showKept ? ` \xB7 ${hiddenKept} kept hidden` : ""} \xB7 uses ${usageDays}d`,
|
|
536
565
|
focused: focus === "skills",
|
|
537
566
|
width: leftWidth,
|
|
538
567
|
height: listHeight,
|
|
539
568
|
rows: skillRows,
|
|
540
569
|
selected: skillIndex,
|
|
541
|
-
empty: "None of your skills load globally. \u{1F389}"
|
|
570
|
+
empty: hiddenKept ? `All reviewed: you keep ${plural(hiddenKept, "skill")} global on purpose. k shows them.` : "None of your skills load globally. \u{1F389}"
|
|
542
571
|
}
|
|
543
572
|
),
|
|
544
573
|
/* @__PURE__ */ jsx2(
|
|
@@ -571,6 +600,7 @@ function CleanupWizard({
|
|
|
571
600
|
["space", focus === "skills" ? "move / keep / delete" : "tick project"],
|
|
572
601
|
...focus === "skills" ? [["d", "delete"]] : [],
|
|
573
602
|
["tab", "switch side"],
|
|
603
|
+
...hiddenKept ? [["k", showKept ? "hide kept" : `show kept (${hiddenKept})`]] : [],
|
|
574
604
|
["enter", `review (${plural(totalInstalls, "install")})`],
|
|
575
605
|
["p", "ask an agent"],
|
|
576
606
|
["esc", "cancel"]
|
|
@@ -602,17 +632,18 @@ var HELP_TOPICS = [
|
|
|
602
632
|
"tab switch lists inside a project",
|
|
603
633
|
"esc clear the search, then go back to the left pane",
|
|
604
634
|
"ctrl+r reload ctrl+c quit",
|
|
605
|
-
"p in the cleanup wizard: copy a prompt asking an agent to review the skills"
|
|
635
|
+
"p in the cleanup wizard: copy a prompt asking an agent to review the skills",
|
|
636
|
+
"k in the cleanup wizard: show / hide the skills you keep global on purpose"
|
|
606
637
|
]
|
|
607
638
|
},
|
|
608
639
|
{
|
|
609
640
|
heading: "Badges",
|
|
610
641
|
lines: [
|
|
611
642
|
"\u273B \u2B21 \u25CE \u2124 Claude Code, Cursor, Codex, Zed \u2014 lit when that agent loads the skill here",
|
|
612
|
-
"\
|
|
613
|
-
"\
|
|
614
|
-
"\u29C9 N copies the same skill reaches agents from N places \u2014
|
|
615
|
-
"\u25CF in sync \xB7 \u2191 update \xB7 \u270E edited \xB7 \u25CB only here",
|
|
643
|
+
"\u26A0 loaded globally loads in every repo on this machine, and you haven't decided about it yet",
|
|
644
|
+
"\u2713 global loads in every repo, and you keep it global on purpose (Enter \u2192 Keep global on purpose)",
|
|
645
|
+
"\u29C9 N copies the same skill reaches agents from N places \u2014 Health has the fix",
|
|
646
|
+
"\u25CF in sync \xB7 \u2191 update \xB7 \u270E edited \xB7 \u25CB only here \xB7 \u2193 owner/repo installed here by npx skills",
|
|
616
647
|
"\u2713 committed in git, so teammates get it \xB1 changed committed, with local edits",
|
|
617
648
|
"+ not added not committed yet \u2205 ignored gitignored \u2014 only on your machine"
|
|
618
649
|
]
|
|
@@ -629,8 +660,9 @@ var HELP_TOPICS = [
|
|
|
629
660
|
{
|
|
630
661
|
heading: "In a repo (only agents working in that repo see them)",
|
|
631
662
|
lines: [
|
|
632
|
-
".claude/skills Claude Code (Cursor reads it too). Where skilllib installs your skills.",
|
|
633
663
|
".agents/skills the shared folder: Codex, Cursor, Zed and most other agents. Repos often commit their team skills here.",
|
|
664
|
+
" skilllib (like `npx skills`) keeps the real copy here whenever one of your agents needs it.",
|
|
665
|
+
".claude/skills Claude Code (Cursor reads it too). A link to .agents/skills, or the copy itself if only Claude Code needs it.",
|
|
634
666
|
".cursor/skills Cursor only. .codex/skills Cursor only (compatibility)."
|
|
635
667
|
]
|
|
636
668
|
},
|
|
@@ -710,7 +742,7 @@ var HELP_TOPICS = [
|
|
|
710
742
|
"Compat .claude/skills, .codex/skills, ~/.claude/skills, ~/.codex/skills",
|
|
711
743
|
"Plus plugins from its marketplace, and Cursor's built-in skills",
|
|
712
744
|
"Cloud Agents only see ~/.cursor/skills.",
|
|
713
|
-
"
|
|
745
|
+
"A skill in two folders is listed once (tested). A plugin's copy and yours are listed separately."
|
|
714
746
|
]
|
|
715
747
|
},
|
|
716
748
|
{
|
|
@@ -736,7 +768,7 @@ var HELP_TOPICS = [
|
|
|
736
768
|
"User ~/.agents/skills",
|
|
737
769
|
"Admin /etc/codex/skills",
|
|
738
770
|
"System skills bundled by OpenAI",
|
|
739
|
-
"It does NOT read .claude/skills \u2014 skilllib
|
|
771
|
+
"It does NOT read .claude/skills \u2014 skilllib keeps your skills' real copies in .agents/skills."
|
|
740
772
|
]
|
|
741
773
|
},
|
|
742
774
|
{
|
|
@@ -777,57 +809,18 @@ var HELP_TOPICS = [
|
|
|
777
809
|
}
|
|
778
810
|
];
|
|
779
811
|
|
|
780
|
-
// src/git.ts
|
|
781
|
-
import { execFileSync } from "child_process";
|
|
782
|
-
import { existsSync as existsSync2 } from "fs";
|
|
783
|
-
import { join as join2, relative } from "path";
|
|
784
|
-
function git(root, args, input) {
|
|
785
|
-
try {
|
|
786
|
-
return execFileSync("git", args, { cwd: root, encoding: "utf-8", input, stdio: ["pipe", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 });
|
|
787
|
-
} catch (err) {
|
|
788
|
-
const out = err.stdout;
|
|
789
|
-
return typeof out === "string" ? out : null;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
function gitInfo(root) {
|
|
793
|
-
if (!existsSync2(join2(root, ".git"))) return null;
|
|
794
|
-
const dirs = [...ALL_PROJECT_DIRS, MANIFEST_FILE].filter((d) => existsSync2(join2(root, d)));
|
|
795
|
-
if (dirs.length === 0) return { of: () => "new" };
|
|
796
|
-
const tracked = (git(root, ["ls-files", "-z", "--", ...dirs]) ?? "").split("\0").filter(Boolean);
|
|
797
|
-
const dirty = (git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...dirs]) ?? "").split("\0").filter(Boolean).map((entry) => entry.slice(3));
|
|
798
|
-
const ignoredCache = /* @__PURE__ */ new Map();
|
|
799
|
-
const isIgnored = (rel) => {
|
|
800
|
-
if (!ignoredCache.has(rel)) {
|
|
801
|
-
const out = git(root, ["check-ignore", "--no-index", "--", rel]) ?? "";
|
|
802
|
-
ignoredCache.set(rel, out.trim().length > 0);
|
|
803
|
-
}
|
|
804
|
-
return ignoredCache.get(rel);
|
|
805
|
-
};
|
|
806
|
-
const under = (list, rel) => list.some((p) => p === rel || p.startsWith(rel + "/"));
|
|
807
|
-
return {
|
|
808
|
-
of: (relPath) => {
|
|
809
|
-
const rel = relPath.replace(/\/+$/, "");
|
|
810
|
-
if (under(tracked, rel)) return under(dirty, rel) ? "changed" : "committed";
|
|
811
|
-
return isIgnored(rel) ? "ignored" : "new";
|
|
812
|
-
}
|
|
813
|
-
};
|
|
814
|
-
}
|
|
815
|
-
function relativeTo(root, path) {
|
|
816
|
-
return relative(root, path).split("\\").join("/");
|
|
817
|
-
}
|
|
818
|
-
|
|
819
812
|
// src/review.ts
|
|
820
813
|
import { spawnSync } from "child_process";
|
|
821
|
-
import { existsSync as
|
|
822
|
-
import { join as
|
|
814
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
815
|
+
import { join as join2 } from "path";
|
|
823
816
|
var EXCERPT_CHARS = 1200;
|
|
824
817
|
function tildify(p) {
|
|
825
818
|
const home = userHome();
|
|
826
819
|
return p.startsWith(home) ? "~" + p.slice(home.length) : p;
|
|
827
820
|
}
|
|
828
821
|
function skillExcerpt(dir, chars) {
|
|
829
|
-
const file =
|
|
830
|
-
if (!
|
|
822
|
+
const file = join2(dir, "SKILL.md");
|
|
823
|
+
if (!existsSync2(file)) return { text: "(SKILL.md not found)", files: 0, size: 0 };
|
|
831
824
|
const content = readFileSync2(file, "utf-8").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
|
|
832
825
|
let files = 0;
|
|
833
826
|
try {
|
|
@@ -851,6 +844,7 @@ function buildReviewPrompt(skills, ctx) {
|
|
|
851
844
|
`- Location: ${tildify(s.path)}${s.links.length ? ` (also linked from ${s.links.map(tildify).join(", ")})` : ""}`,
|
|
852
845
|
`- Loaded by: ${s.loadedBy}`,
|
|
853
846
|
...s.vendor ? [`- Vendor-managed: ${s.vendor}`] : [],
|
|
847
|
+
...s.keptGlobal ? ["- I marked this as global on purpose: recommend changing that only for a strong reason (unused, duplicated, low quality)"] : [],
|
|
854
848
|
`- Usage (Claude Code transcripts only): ${usage}`,
|
|
855
849
|
`- Installed in projects: ${s.installedIn.length ? s.installedIn.join(", ") : "none"}`,
|
|
856
850
|
`- Other copies agents can see: ${s.otherCopies.length ? s.otherCopies.join("; ") : "none"}`,
|
|
@@ -897,7 +891,7 @@ Reply with a short table (skill \xB7 decision \xB7 projects \xB7 one-line reason
|
|
|
897
891
|
`;
|
|
898
892
|
}
|
|
899
893
|
function copyText(text) {
|
|
900
|
-
const savedTo =
|
|
894
|
+
const savedTo = join2(skilllibHome(), "review-prompt.md");
|
|
901
895
|
mkdirSync(skilllibHome(), { recursive: true });
|
|
902
896
|
writeFileSync(savedTo, text);
|
|
903
897
|
const tools = process.platform === "darwin" ? [["pbcopy", []]] : process.platform === "win32" ? [["clip", []]] : [
|
|
@@ -1037,15 +1031,18 @@ function versionText(r) {
|
|
|
1037
1031
|
}
|
|
1038
1032
|
var harnessName = (id) => HARNESSES.find((h) => h.id === id)?.name ?? id;
|
|
1039
1033
|
function harnessChips(visibility, installed) {
|
|
1040
|
-
return visibility.
|
|
1034
|
+
return visibility.map((v) => {
|
|
1041
1035
|
const h = HARNESSES.find((x) => x.id === v.id);
|
|
1042
1036
|
const on = installed && v.paths > 0;
|
|
1043
|
-
return
|
|
1044
|
-
{ text: ` ${h?.icon ?? "?"}`, color: on ? h?.color : color.faint, dim: !on },
|
|
1045
|
-
{ text: v.paths > 1 && installed ? "\xB2" : " ", color: color.yellow }
|
|
1046
|
-
];
|
|
1037
|
+
return { text: ` ${h?.icon ?? "?"} `, color: on ? h?.color : color.faint, dim: !on };
|
|
1047
1038
|
});
|
|
1048
1039
|
}
|
|
1040
|
+
var BACKUP_LABEL = {
|
|
1041
|
+
trash: "deleted from library",
|
|
1042
|
+
"global-backup": "unloaded from global",
|
|
1043
|
+
"tidy-backup": "duplicate replaced by a link",
|
|
1044
|
+
plugin: "plugin removed"
|
|
1045
|
+
};
|
|
1049
1046
|
var GIT_LABEL = {
|
|
1050
1047
|
committed: { text: "\u2713 committed", color: color.green },
|
|
1051
1048
|
changed: { text: "\xB1 changed", color: color.yellow },
|
|
@@ -1091,7 +1088,7 @@ function wrapAligned(line, width) {
|
|
|
1091
1088
|
function hasLinks(root, name) {
|
|
1092
1089
|
return ALL_PROJECT_DIRS.some((d) => {
|
|
1093
1090
|
try {
|
|
1094
|
-
return lstatSync(
|
|
1091
|
+
return lstatSync(join3(root, d, name)).isSymbolicLink();
|
|
1095
1092
|
} catch {
|
|
1096
1093
|
return false;
|
|
1097
1094
|
}
|
|
@@ -1256,8 +1253,8 @@ function App() {
|
|
|
1256
1253
|
const pKey = placeKey(place);
|
|
1257
1254
|
function projectSections(root) {
|
|
1258
1255
|
const where = basename3(root);
|
|
1259
|
-
const
|
|
1260
|
-
const gitOf = (path) =>
|
|
1256
|
+
const git = gitFor(root);
|
|
1257
|
+
const gitOf = (path) => git ? git.of(relativeTo(root, path)) : null;
|
|
1261
1258
|
const rows = snapshot.rows.get(root) ?? [];
|
|
1262
1259
|
const projectActions = [
|
|
1263
1260
|
{
|
|
@@ -1310,7 +1307,8 @@ function App() {
|
|
|
1310
1307
|
const dir = r.path;
|
|
1311
1308
|
const badge = stateBadge(r.state);
|
|
1312
1309
|
const edited = r.state.startsWith("edited");
|
|
1313
|
-
const
|
|
1310
|
+
const npx = r.state === "from npx skills";
|
|
1311
|
+
const inRepo = r.location === ".agents/skills" && !r.managed && !npx;
|
|
1314
1312
|
const missing = r.visibility.filter((v) => v.paths === 0);
|
|
1315
1313
|
const seenBy = r.visibility.filter((v) => v.paths > 0);
|
|
1316
1314
|
const notInLibrary = r.state === "local only" || r.state === "untracked, differs from library" || r.state === "repo skill" || r.state === "repo skill, differs from library";
|
|
@@ -1351,7 +1349,10 @@ function App() {
|
|
|
1351
1349
|
...r.installed && missing.length ? [{ label: `Make visible to ${missing.map((v) => harnessName(v.id)).join(", ")}`, hint: "adds links; one real copy", run: () => makeVisible() }] : [],
|
|
1352
1350
|
...r.installed && !r.managed && r.visibility.some((v) => v.paths > 0) && hasLinks(root, r.name) ? [{ label: "Remove links", hint: "keeps the real folder", run: removeLinks }] : []
|
|
1353
1351
|
];
|
|
1354
|
-
if (
|
|
1352
|
+
if (npx) {
|
|
1353
|
+
primary = { label: "copy to your skills", run: copyRepoSkillToLibrary };
|
|
1354
|
+
actions.push({ label: "Copy to Your skills", hint: "so other repos can use it; npx skills keeps this copy", run: copyRepoSkillToLibrary });
|
|
1355
|
+
} else if (inRepo) {
|
|
1355
1356
|
const libraryAction = r.state === "repo skill" ? { label: "Copy to Your skills", hint: "so other repos can use it; this repo keeps its copy", run: copyRepoSkillToLibrary } : r.state === "repo skill, differs from library" ? { label: "Update Your skills from this repo's copy", hint: "becomes a new version", run: copyRepoSkillToLibrary } : { label: "Already in Your skills", hint: `v${r.latest}`, run: () => say(`${r.name} matches library v${r.latest}`) };
|
|
1356
1357
|
primary = r.state === "repo skill, in library" ? missing.length ? { label: "make visible to all", run: () => makeVisible() } : void 0 : { label: "copy to your skills", run: copyRepoSkillToLibrary };
|
|
1357
1358
|
actions.push(libraryAction, ...visibilityActions);
|
|
@@ -1411,7 +1412,7 @@ function App() {
|
|
|
1411
1412
|
}
|
|
1412
1413
|
if (!inRepo) actions.push(...visibilityActions);
|
|
1413
1414
|
actions.push(
|
|
1414
|
-
{ label: "Edit SKILL.md", run: () => void edit(
|
|
1415
|
+
{ label: "Edit SKILL.md", run: () => void edit(join3(dir, "SKILL.md")) },
|
|
1415
1416
|
{ label: "Open folder", run: () => openFolder(dir) },
|
|
1416
1417
|
...projectActions
|
|
1417
1418
|
);
|
|
@@ -1420,11 +1421,11 @@ function App() {
|
|
|
1420
1421
|
return {
|
|
1421
1422
|
key,
|
|
1422
1423
|
search: r.name,
|
|
1423
|
-
group: prefixGroup(r.name),
|
|
1424
|
+
group: npx ? { key: `repo:${r.source}`, label: r.source, about: `Installed with \`npx skills add ${r.source}\` into this repo (recorded in skills-lock.json).` } : prefixGroup(r.name),
|
|
1424
1425
|
skill: { name: r.name, path: dir },
|
|
1425
1426
|
tags: [
|
|
1426
1427
|
r.installed ? "in this project" : "installable",
|
|
1427
|
-
...r.installed && r.state !== "ok" && r.state !== "repo skill, in library" ? ["needs attention"] : []
|
|
1428
|
+
...r.installed && r.state !== "ok" && r.state !== "repo skill, in library" && !npx ? ["needs attention"] : []
|
|
1428
1429
|
],
|
|
1429
1430
|
row: {
|
|
1430
1431
|
key,
|
|
@@ -1435,11 +1436,11 @@ function App() {
|
|
|
1435
1436
|
},
|
|
1436
1437
|
{ text: r.name, grow: true, color: r.installed ? void 0 : color.muted },
|
|
1437
1438
|
{ text: whereLabel.padStart(8), width: 8, color: color.faint },
|
|
1438
|
-
...gitCell(r.installed ? gitOf(r.path) : null, !!
|
|
1439
|
+
...gitCell(r.installed ? gitOf(r.path) : null, !!git),
|
|
1439
1440
|
...harnessChips(r.visibility.length ? r.visibility : enabledIds.map((id) => ({ id, paths: 0 })), r.installed),
|
|
1440
1441
|
{ text: versionText(r).padStart(8), width: 8, color: behind ? color.yellow : color.muted },
|
|
1441
1442
|
{
|
|
1442
|
-
text: r.installed && dupes(r.name) > 1 ? ` \u29C9 ${dupes(r.name)} copies` : !r.installed && globalCopy ? " \u2197 already global" : ` ${badge.icon} ${badge.label}`,
|
|
1443
|
+
text: r.installed && dupes(r.name) > 1 ? ` \u29C9 ${dupes(r.name)} copies` : !r.installed && globalCopy ? " \u2197 already global" : ` ${badge.icon} ${npx ? r.source : badge.label}`,
|
|
1443
1444
|
width: 19,
|
|
1444
1445
|
color: r.installed && dupes(r.name) > 1 ? color.red : !r.installed && globalCopy ? color.yellow : badge.color
|
|
1445
1446
|
},
|
|
@@ -1453,12 +1454,12 @@ function App() {
|
|
|
1453
1454
|
dir,
|
|
1454
1455
|
meta: [
|
|
1455
1456
|
r.installed ? `${seenBy.length ? `loaded by ${seenBy.map((v) => `${harnessName(v.id)}${v.paths > 1 ? ` (via ${v.paths} folders)` : ""}`).join(", ")}` : "not loaded by your harnesses"}${missing.length ? ` \xB7 not ${missing.map((v) => harnessName(v.id)).join(", ")}` : ""}` : "not installed",
|
|
1456
|
-
inRepo ? `committed in ${r.location}` : r.installed ? r.managed ? `v${r.version} installed in .
|
|
1457
|
-
...r.installed &&
|
|
1457
|
+
npx ? `installed by npx skills from ${r.source} into ${r.location}` : inRepo ? `committed in ${r.location}` : r.installed ? r.managed ? `v${r.version} installed in ${r.location}` : notInLibrary ? "only in this project" : "untracked copy" : "not installed",
|
|
1458
|
+
...r.installed && git ? [GIT_ABOUT[gitOf(r.path)]] : [],
|
|
1458
1459
|
r.inLibrary ? `in Your skills (v${r.latest}${origin ? `, from ${origin}` : ""})` : "not in Your skills",
|
|
1459
1460
|
`${usesText(r.name, root)} Claude Code uses here (${USAGE_DAYS}d)`
|
|
1460
1461
|
],
|
|
1461
|
-
...r.installed && dupes(r.name) > 1 ? { description: `
|
|
1462
|
+
...r.installed && dupes(r.name) > 1 ? { description: `Reaches your agents from ${dupes(r.name)} places: ${copiesOf(r.name).join(" \xB7 ")}. Health has the fix (your skill wins over plugins).` } : {}
|
|
1462
1463
|
}
|
|
1463
1464
|
};
|
|
1464
1465
|
};
|
|
@@ -1470,6 +1471,7 @@ function App() {
|
|
|
1470
1471
|
const dupes = (name) => copiesOf(name).length;
|
|
1471
1472
|
const globalItem = (m) => {
|
|
1472
1473
|
const inLib = libraryNames.has(m.name);
|
|
1474
|
+
const kept = m.movable && snapshot.keptGlobal.has(m.name);
|
|
1473
1475
|
const alsoLocal = rows.some((r) => r.installed && r.name === m.name);
|
|
1474
1476
|
const moveHere = () => confirm(
|
|
1475
1477
|
`Use ${m.name} only where you add it? It's ${inLib ? "" : "imported into the library, "}added to ${where}, and stops loading globally \u2014 other projects lose it until you add it there. Restorable from Health.`,
|
|
@@ -1483,6 +1485,7 @@ function App() {
|
|
|
1483
1485
|
);
|
|
1484
1486
|
const actions = m.movable ? [
|
|
1485
1487
|
{ label: "Choose which repos keep it\u2026", hint: "cleanup wizard", run: () => setWizard({ initialSkill: m.name }) },
|
|
1488
|
+
keepAction(m),
|
|
1486
1489
|
{ label: `Use only in ${where}`, hint: "add here, stop loading everywhere", run: moveHere },
|
|
1487
1490
|
{ label: "Delete", hint: "not kept in Your skills \xB7 restorable from Health", run: () => confirmDeleteGlobal([m]) },
|
|
1488
1491
|
...inLib ? [] : [
|
|
@@ -1507,13 +1510,13 @@ function App() {
|
|
|
1507
1510
|
{ text: "\u25C8 ", color: color.accentDim },
|
|
1508
1511
|
{ text: m.name, grow: true },
|
|
1509
1512
|
{ text: (m.kind === "global" || m.kind === "skills.sh" ? "global" : m.kind).padStart(8), width: 8, color: color.faint },
|
|
1510
|
-
...gitCell(null, !!
|
|
1513
|
+
...gitCell(null, !!git),
|
|
1511
1514
|
...harnessChips(enabledIds.map((id) => ({ id, paths: m.harnesses.includes(id) ? 1 : 0 })), true),
|
|
1512
1515
|
{ text: "", width: 8 },
|
|
1513
1516
|
{
|
|
1514
|
-
text: ` ${dupes(m.name) > 1 ? `\u29C9 ${dupes(m.name)} copies` : m.movable ? "\u26A0 loaded globally" : "vendor"}`,
|
|
1517
|
+
text: ` ${dupes(m.name) > 1 ? `\u29C9 ${dupes(m.name)} copies` : kept ? "\u2713 global" : m.movable ? "\u26A0 loaded globally" : "vendor"}`,
|
|
1515
1518
|
width: 19,
|
|
1516
|
-
color: dupes(m.name) > 1 ? color.red : m.movable ? color.yellow : color.faint
|
|
1519
|
+
color: dupes(m.name) > 1 ? color.red : kept ? color.green : m.movable ? color.yellow : color.faint
|
|
1517
1520
|
},
|
|
1518
1521
|
{ text: usesText(m.name, root).padStart(4), color: color.muted }
|
|
1519
1522
|
]
|
|
@@ -1526,9 +1529,9 @@ function App() {
|
|
|
1526
1529
|
meta: [
|
|
1527
1530
|
`loaded in every project by ${m.harnesses.map(harnessName).join(", ")}`,
|
|
1528
1531
|
`${m.kind}: ${m.origin}`,
|
|
1529
|
-
m.movable ? "yours: the cleanup wizard can scope it to projects" : "managed by the vendor"
|
|
1532
|
+
kept ? "yours: kept global on purpose" : m.movable ? "yours: the cleanup wizard can scope it to projects" : "managed by the vendor"
|
|
1530
1533
|
],
|
|
1531
|
-
description: dupes(m.name) > 1 ? `
|
|
1534
|
+
description: dupes(m.name) > 1 ? `Reaches your agents from ${dupes(m.name)} places: ${copiesOf(m.name).join(" \xB7 ")}. Health has the fix (your skill wins over plugins).` : sourceGroup(m)?.about
|
|
1532
1535
|
}
|
|
1533
1536
|
};
|
|
1534
1537
|
};
|
|
@@ -1573,15 +1576,28 @@ function App() {
|
|
|
1573
1576
|
preview: {
|
|
1574
1577
|
title: "Make every skill usable by all your agents",
|
|
1575
1578
|
meta: [`${plural2(partial.length, "skill")} missing for ${missingAgents.map(harnessName).join(", ")}`],
|
|
1576
|
-
description: `Adds relative symlinks so each agent's folder reaches the one real copy (e.g. .claude/skills/<name> \u2192 .agents/skills/<name>). The repo's files aren't changed; folders git tracks need your OK first. Cursor
|
|
1579
|
+
description: `Adds relative symlinks so each agent's folder reaches the one real copy (e.g. .claude/skills/<name> \u2192 .agents/skills/<name>). The repo's files aren't changed; folders git tracks need your OK first. Cursor lists a skill it reaches through several folders once.`
|
|
1577
1580
|
}
|
|
1578
1581
|
}
|
|
1579
1582
|
] : [];
|
|
1580
1583
|
const fromLibrary = local.filter((r) => r.managed || r.state === "untracked copy of library skill");
|
|
1581
|
-
const
|
|
1584
|
+
const fromNpx = local.filter((r) => r.state === "from npx skills");
|
|
1585
|
+
const repoOwn = local.filter((r) => !fromLibrary.includes(r) && !fromNpx.includes(r));
|
|
1586
|
+
const copyAllToLibrary = (members) => [
|
|
1587
|
+
{
|
|
1588
|
+
label: `Copy all ${members.length} into your library`,
|
|
1589
|
+
hint: "the repo keeps its copies",
|
|
1590
|
+
run: () => act(() => {
|
|
1591
|
+
const added = members.map((m) => importSkill(m.skill.path, { force: true })).filter((r) => r.status !== "unchanged").length;
|
|
1592
|
+
return ok(`${plural2(added, "skill")} copied into your library`);
|
|
1593
|
+
})
|
|
1594
|
+
}
|
|
1595
|
+
];
|
|
1582
1596
|
const yours = globals.filter((m) => m.movable);
|
|
1597
|
+
const keptYours = yours.filter((m) => snapshot.keptGlobal.has(m.name));
|
|
1598
|
+
const unreviewed = yours.filter((m) => !snapshot.keptGlobal.has(m.name));
|
|
1583
1599
|
const vendor = globals.filter((m) => !m.movable);
|
|
1584
|
-
const cleanupItem =
|
|
1600
|
+
const cleanupItem = unreviewed.length ? [
|
|
1585
1601
|
{
|
|
1586
1602
|
key: "__cleanup",
|
|
1587
1603
|
search: "clean up global",
|
|
@@ -1590,7 +1606,7 @@ function App() {
|
|
|
1590
1606
|
key: "__cleanup",
|
|
1591
1607
|
cells: [
|
|
1592
1608
|
{ text: "\u26A0 ", color: color.yellow },
|
|
1593
|
-
{ text: `${plural2(
|
|
1609
|
+
{ text: `${plural2(unreviewed.length, "skill")} of yours load in every repo \u2014 choose where each should live\u2026`, grow: true, color: color.yellow }
|
|
1594
1610
|
]
|
|
1595
1611
|
},
|
|
1596
1612
|
primary: { label: "clean up", run: () => setWizard({}) },
|
|
@@ -1619,43 +1635,17 @@ function App() {
|
|
|
1619
1635
|
}
|
|
1620
1636
|
]
|
|
1621
1637
|
},
|
|
1638
|
+
{ title: `From npx skills (${fromNpx.length})`, items: fromNpx.map(item), groupActions: copyAllToLibrary },
|
|
1639
|
+
{ title: `The repo's own (${repoOwn.length})`, items: repoOwn.map(item), groupActions: copyAllToLibrary },
|
|
1622
1640
|
{
|
|
1623
|
-
title:
|
|
1624
|
-
items:
|
|
1625
|
-
groupActions: (members) => [
|
|
1626
|
-
{
|
|
1627
|
-
label: `Copy all ${members.length} into your library`,
|
|
1628
|
-
hint: "the repo keeps its copies",
|
|
1629
|
-
run: () => act(() => {
|
|
1630
|
-
const added = members.map((m) => importSkill(m.skill.path, { force: true })).filter((r) => r.status !== "unchanged").length;
|
|
1631
|
-
return ok(`${plural2(added, "skill")} copied into your library`);
|
|
1632
|
-
})
|
|
1633
|
-
}
|
|
1634
|
-
]
|
|
1641
|
+
title: `\u26A0 Global \xB7 yours, not reviewed, loaded in every repo (${unreviewed.length})`,
|
|
1642
|
+
items: [...cleanupItem, ...unreviewed.map(globalItem)],
|
|
1643
|
+
groupActions: (members) => [...keepGroupActions(members), ...yourGlobalActions(members)]
|
|
1635
1644
|
},
|
|
1636
1645
|
{
|
|
1637
|
-
title: `\
|
|
1638
|
-
items:
|
|
1639
|
-
groupActions: (members) => [
|
|
1640
|
-
{
|
|
1641
|
-
label: `Copy review prompt for all ${members.length}`,
|
|
1642
|
-
hint: "for an agent to decide keep / move / delete",
|
|
1643
|
-
run: () => copyReviewPrompt(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)).map(reviewOfGlobal), `These are global skills (group ${members[0]?.group?.label ?? ""}).`)
|
|
1644
|
-
},
|
|
1645
|
-
{
|
|
1646
|
-
label: `Copy all ${members.length} to Your skills`,
|
|
1647
|
-
hint: "they keep loading globally",
|
|
1648
|
-
run: () => act(() => {
|
|
1649
|
-
const added = members.map((m) => importSkill(m.skill.path)).filter((r) => r.status === "added").length;
|
|
1650
|
-
return ok(`${plural2(added, "skill")} imported into your library`);
|
|
1651
|
-
})
|
|
1652
|
-
},
|
|
1653
|
-
{
|
|
1654
|
-
label: `Delete all ${members.length}`,
|
|
1655
|
-
hint: "not kept in Your skills \xB7 restorable from Health",
|
|
1656
|
-
run: () => confirmDeleteGlobal(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)))
|
|
1657
|
-
}
|
|
1658
|
-
]
|
|
1646
|
+
title: `\u2713 Global \xB7 yours, on purpose (${keptYours.length})`,
|
|
1647
|
+
items: keptYours.map(globalItem),
|
|
1648
|
+
groupActions: (members) => [...keepGroupActions(members), ...yourGlobalActions(members)]
|
|
1659
1649
|
},
|
|
1660
1650
|
{
|
|
1661
1651
|
title: `Global \xB7 from vendors: plugins, claude.ai, built-in (${vendor.length})`,
|
|
@@ -1738,7 +1728,7 @@ function App() {
|
|
|
1738
1728
|
primary: { label: "choose projects", run: toggleProjects.run },
|
|
1739
1729
|
actions: [
|
|
1740
1730
|
toggleProjects,
|
|
1741
|
-
{ label: "Edit SKILL.md", hint: "saving creates a new version", run: () => void edit(
|
|
1731
|
+
{ label: "Edit SKILL.md", hint: "saving creates a new version", run: () => void edit(join3(librarySkillDir(l.name), "SKILL.md")) },
|
|
1742
1732
|
{ label: "Open folder", run: () => openFolder(librarySkillDir(l.name)) },
|
|
1743
1733
|
{
|
|
1744
1734
|
label: "Copy review prompt",
|
|
@@ -1795,6 +1785,8 @@ function App() {
|
|
|
1795
1785
|
}
|
|
1796
1786
|
function globalSections() {
|
|
1797
1787
|
const yours = snapshot.machine.filter((m) => m.movable);
|
|
1788
|
+
const unreviewed = yours.filter((m) => !m.broken && !snapshot.keptGlobal.has(m.name));
|
|
1789
|
+
const keptCount = yours.filter((m) => !m.broken && snapshot.keptGlobal.has(m.name)).length;
|
|
1798
1790
|
const importAll = {
|
|
1799
1791
|
label: "Copy all of yours to Your skills",
|
|
1800
1792
|
run: () => {
|
|
@@ -1833,12 +1825,16 @@ function App() {
|
|
|
1833
1825
|
key: "__cleanup",
|
|
1834
1826
|
cells: [
|
|
1835
1827
|
{ text: "\u26A0 ", color: color.yellow },
|
|
1836
|
-
{ text: `Clean up: choose which repos keep each of your ${
|
|
1828
|
+
{ text: `Clean up: choose which repos keep each of your ${unreviewed.length} unreviewed global skills\u2026`, grow: true, color: color.yellow }
|
|
1837
1829
|
]
|
|
1838
1830
|
},
|
|
1839
1831
|
primary: { label: "clean up", run: () => setWizard({}) },
|
|
1840
1832
|
actions: [{ label: "Open the cleanup wizard", run: () => setWizard({}) }],
|
|
1841
|
-
preview: {
|
|
1833
|
+
preview: {
|
|
1834
|
+
title: "Clean up global skills",
|
|
1835
|
+
meta: ["every agent reads global skills in every repo", ...keptCount ? [`skips the ${keptCount} you keep global on purpose (k shows them)`] : []],
|
|
1836
|
+
description: "Install each skill only where it's needed, keep it global on purpose, or delete it. Originals are kept in backup."
|
|
1837
|
+
}
|
|
1842
1838
|
};
|
|
1843
1839
|
const grouped = groups.map(([title, test]) => ({
|
|
1844
1840
|
title,
|
|
@@ -1849,6 +1845,7 @@ function App() {
|
|
|
1849
1845
|
run: () => copyReviewPrompt(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)).map(reviewOfGlobal), `These are global skills (group ${members[0]?.group?.label ?? ""}).`)
|
|
1850
1846
|
},
|
|
1851
1847
|
...title.startsWith("Yours") ? [
|
|
1848
|
+
...keepGroupActions(members),
|
|
1852
1849
|
{
|
|
1853
1850
|
label: `Delete all ${members.length}`,
|
|
1854
1851
|
hint: "not kept in Your skills \xB7 restorable from Health",
|
|
@@ -1879,7 +1876,7 @@ function App() {
|
|
|
1879
1876
|
actions.push({ label: "Remove the broken link", run: unload });
|
|
1880
1877
|
} else {
|
|
1881
1878
|
if (!inLib) actions.push({ label: "Copy to Your skills", hint: "keeps loading globally", run: importIt });
|
|
1882
|
-
if (m.movable) actions.push({ label: "Stop loading it globally", hint: "use it per project instead", run: unload });
|
|
1879
|
+
if (m.movable) actions.push(keepAction(m), { label: "Stop loading it globally", hint: "use it per project instead", run: unload });
|
|
1883
1880
|
primary = m.movable ? { label: "choose repos\u2026", run: () => setWizard({ initialSkill: m.name }) } : !inLib ? { label: "copy to your skills", run: importIt } : void 0;
|
|
1884
1881
|
if (m.movable) {
|
|
1885
1882
|
actions.unshift({ label: "Choose which repos keep it\u2026", hint: "cleanup wizard", run: () => setWizard({ initialSkill: m.name }) });
|
|
@@ -1901,6 +1898,7 @@ function App() {
|
|
|
1901
1898
|
{ text: m.name.padEnd(30).slice(0, 30), color: m.broken ? color.red : void 0 },
|
|
1902
1899
|
{ text: m.broken ? "broken link" : m.origin, grow: true, color: m.broken ? color.red : kindColor[m.kind] },
|
|
1903
1900
|
...harnessChips(enabledIds.map((id) => ({ id, paths: m.harnesses.includes(id) ? 1 : 0 })), !m.broken),
|
|
1901
|
+
{ text: m.movable && snapshot.keptGlobal.has(m.name) ? " \u2713 kept" : " ", color: color.green },
|
|
1904
1902
|
{ text: inLib ? " \u2713 library" : " ", color: color.green },
|
|
1905
1903
|
{ text: usesText(m.name).padStart(5), color: color.muted }
|
|
1906
1904
|
]
|
|
@@ -1913,7 +1911,7 @@ function App() {
|
|
|
1913
1911
|
meta: [
|
|
1914
1912
|
m.harnesses.length ? `loaded everywhere by ${m.harnesses.map(harnessName).join(", ")}` : "not loaded by your harnesses",
|
|
1915
1913
|
`${m.kind}: ${m.origin}`,
|
|
1916
|
-
m.movable ? "yours to manage" : "managed by the vendor",
|
|
1914
|
+
m.movable ? snapshot.keptGlobal.has(m.name) ? "yours \xB7 kept global on purpose" : "yours to manage" : "managed by the vendor",
|
|
1917
1915
|
tildify2(m.path) + (m.links.length ? ` (+ ${m.links.length} link${m.links.length > 1 ? "s" : ""})` : "")
|
|
1918
1916
|
],
|
|
1919
1917
|
description: m.broken ? "Broken link: it points at a folder that no longer exists, so it loads nothing." : void 0
|
|
@@ -1952,7 +1950,48 @@ function App() {
|
|
|
1952
1950
|
description: "The prompt includes each skill's description, a SKILL.md excerpt, how it was installed (vendor, skills.sh, plugin\u2026), which agents load it, usage per project, duplicates and your project list. Paste it into Claude Code, Cursor or Codex; it replies with keep / move / delete per skill."
|
|
1953
1951
|
}
|
|
1954
1952
|
};
|
|
1955
|
-
return [{ title: "", items: [...
|
|
1953
|
+
return [{ title: "", items: [...unreviewed.length ? [launcher] : [], reviewAll] }, ...grouped];
|
|
1954
|
+
}
|
|
1955
|
+
function markKept(names, keep) {
|
|
1956
|
+
act(() => {
|
|
1957
|
+
setKeepGlobal(names, keep);
|
|
1958
|
+
const label = names.length === 1 ? names[0] : plural2(names.length, "skill");
|
|
1959
|
+
return ok(keep ? `${label} kept global on purpose \u2014 cleanup skips ${names.length === 1 ? "it" : "them"}` : `${label} back in cleanup`);
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
function keepAction(m) {
|
|
1963
|
+
return snapshot.keptGlobal.has(m.name) ? { label: "Unmark: not global on purpose", hint: "cleanup asks about it again", run: () => markKept([m.name], false) } : { label: "\u2713 Keep global on purpose", hint: "stops the \u26A0 and skips it in cleanup", run: () => markKept([m.name], true) };
|
|
1964
|
+
}
|
|
1965
|
+
function keepGroupActions(members) {
|
|
1966
|
+
const names = [...new Set(members.flatMap((m) => m.skill ? [m.skill.name] : []))];
|
|
1967
|
+
const unkept = names.filter((n) => !snapshot.keptGlobal.has(n));
|
|
1968
|
+
const kept = names.filter((n) => snapshot.keptGlobal.has(n));
|
|
1969
|
+
return [
|
|
1970
|
+
...unkept.length ? [{ label: `\u2713 Keep all ${unkept.length} global on purpose`, hint: "cleanup skips them", run: () => markKept(unkept, true) }] : [],
|
|
1971
|
+
...kept.length ? [{ label: `Unmark all ${kept.length}`, hint: "cleanup asks about them again", run: () => markKept(kept, false) }] : []
|
|
1972
|
+
];
|
|
1973
|
+
}
|
|
1974
|
+
function yourGlobalActions(members) {
|
|
1975
|
+
return [
|
|
1976
|
+
{
|
|
1977
|
+
label: `Copy review prompt for all ${members.length}`,
|
|
1978
|
+
hint: "for an agent to decide keep / move / delete",
|
|
1979
|
+
run: () => copyReviewPrompt(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)).map(reviewOfGlobal), `These are global skills (group ${members[0]?.group?.label ?? ""}).`)
|
|
1980
|
+
},
|
|
1981
|
+
{
|
|
1982
|
+
label: `Copy all ${members.length} to Your skills`,
|
|
1983
|
+
hint: "they keep loading globally",
|
|
1984
|
+
run: () => act(() => {
|
|
1985
|
+
const added = members.map((m) => importSkill(m.skill.path)).filter((r) => r.status === "added").length;
|
|
1986
|
+
return ok(`${plural2(added, "skill")} imported into your library`);
|
|
1987
|
+
})
|
|
1988
|
+
},
|
|
1989
|
+
{
|
|
1990
|
+
label: `Delete all ${members.length}`,
|
|
1991
|
+
hint: "not kept in Your skills \xB7 restorable from Health",
|
|
1992
|
+
run: () => confirmDeleteGlobal(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)))
|
|
1993
|
+
}
|
|
1994
|
+
];
|
|
1956
1995
|
}
|
|
1957
1996
|
function confirmDeleteGlobal(skills) {
|
|
1958
1997
|
if (skills.length === 0) return;
|
|
@@ -2001,7 +2040,8 @@ function App() {
|
|
|
2001
2040
|
usesByProject: u.byProject,
|
|
2002
2041
|
installedIn: projectsWith(m.name),
|
|
2003
2042
|
otherCopies: snapshot.machine.filter((x) => x.name === m.name && x.path !== m.path).map((x) => `${x.kind} ${x.origin} (${tildify2(x.path)})`),
|
|
2004
|
-
inYourSkills: lib ? `yes, v${lib.version}` : null
|
|
2043
|
+
inYourSkills: lib ? `yes, v${lib.version}` : null,
|
|
2044
|
+
keptGlobal: m.movable && snapshot.keptGlobal.has(m.name)
|
|
2005
2045
|
};
|
|
2006
2046
|
}
|
|
2007
2047
|
function reviewOfLibrary(l) {
|
|
@@ -2116,7 +2156,12 @@ function App() {
|
|
|
2116
2156
|
title: "Issues",
|
|
2117
2157
|
items: snapshot.issues.map((issue) => {
|
|
2118
2158
|
const fix = issue.fix;
|
|
2119
|
-
const
|
|
2159
|
+
const result = (c) => {
|
|
2160
|
+
const r = runFix(c);
|
|
2161
|
+
return r.ok ? ok(r.message) : warn(r.message);
|
|
2162
|
+
};
|
|
2163
|
+
const choices = (issue.choices ?? []).map((c) => ({ label: c.label, hint: c.hint, run: () => act(() => result(c)) }));
|
|
2164
|
+
const run = fix ? () => confirm(`${fix.label}?`, () => result(fix)) : choices.length ? () => setOverlay({ type: "menu", title: issue.title, index: 0, build: () => choices }) : void 0;
|
|
2120
2165
|
return {
|
|
2121
2166
|
key: issue.id,
|
|
2122
2167
|
search: issue.title,
|
|
@@ -2130,10 +2175,10 @@ function App() {
|
|
|
2130
2175
|
]
|
|
2131
2176
|
},
|
|
2132
2177
|
primary: run ? { label: "fix", run } : void 0,
|
|
2133
|
-
actions:
|
|
2178
|
+
actions: fix && run ? [{ label: fix.label, run }] : choices,
|
|
2134
2179
|
preview: {
|
|
2135
2180
|
title: issue.title,
|
|
2136
|
-
meta: [issue.severity, fix ? `fix: ${fix.label.toLowerCase()}` : "no automatic fix"],
|
|
2181
|
+
meta: [issue.severity, fix ? `fix: ${fix.label.toLowerCase()}` : choices.length ? `${choices.length} ways to fix it: pick one` : "no automatic fix"],
|
|
2137
2182
|
description: issue.detail
|
|
2138
2183
|
}
|
|
2139
2184
|
};
|
|
@@ -2143,7 +2188,7 @@ function App() {
|
|
|
2143
2188
|
title: "Moved out \xB7 restorable",
|
|
2144
2189
|
items: snapshot.backups.map((b) => {
|
|
2145
2190
|
const restore = () => act(() => {
|
|
2146
|
-
const r = restoreBackup(b);
|
|
2191
|
+
const r = b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b);
|
|
2147
2192
|
return r.ok ? ok(`${b.name} restored to ${tildify2(r.to)}`) : warn(`Can't restore ${b.name}: ${r.reason}`);
|
|
2148
2193
|
});
|
|
2149
2194
|
return {
|
|
@@ -2154,7 +2199,7 @@ function App() {
|
|
|
2154
2199
|
key: b.path,
|
|
2155
2200
|
cells: [
|
|
2156
2201
|
{ text: b.name, grow: true },
|
|
2157
|
-
{ text: b.kind
|
|
2202
|
+
{ text: ` ${BACKUP_LABEL[b.kind]}`, color: color.muted },
|
|
2158
2203
|
{ text: ` ${b.movedAt}`, color: color.faint }
|
|
2159
2204
|
]
|
|
2160
2205
|
},
|
|
@@ -2163,7 +2208,7 @@ function App() {
|
|
|
2163
2208
|
preview: {
|
|
2164
2209
|
title: b.name,
|
|
2165
2210
|
dir: b.path,
|
|
2166
|
-
meta: [b.kind === "
|
|
2211
|
+
meta: [b.kind === "plugin" ? "Claude Code plugin; restoring reinstalls it" : `from ${tildify2(b.from)}`, `moved ${b.movedAt}`]
|
|
2167
2212
|
}
|
|
2168
2213
|
};
|
|
2169
2214
|
})
|
|
@@ -2214,7 +2259,7 @@ function App() {
|
|
|
2214
2259
|
if (!r.ok) return say(`Can't create ${value}: ${r.reason}`, false);
|
|
2215
2260
|
setSelected((s) => ({ ...s, "right:library": value.trim() }));
|
|
2216
2261
|
reload();
|
|
2217
|
-
void edit(
|
|
2262
|
+
void edit(join3(r.dir, "SKILL.md"));
|
|
2218
2263
|
}
|
|
2219
2264
|
});
|
|
2220
2265
|
}
|
|
@@ -2406,7 +2451,7 @@ function App() {
|
|
|
2406
2451
|
"Skills your harnesses load in EVERY project (~/.claude/skills, ~/.agents/skills, plugins\u2026). Move yours into the library and add them only where needed."
|
|
2407
2452
|
],
|
|
2408
2453
|
help: ["Help", "Where skills come from, and how Claude Code, Cursor, Codex and Zed find and use them. Press enter (or ? anywhere)."],
|
|
2409
|
-
health: ["Health", "Broken links, skills
|
|
2454
|
+
health: ["Health", "Broken links, duplicates, copies that differ, plugins that repeat your skills, skills some agents can't reach, out-of-date projects \u2014 and everything skilllib moved out, ready to restore."],
|
|
2410
2455
|
settings: ["Settings", `Harnesses: ${snapshot.harnesses.map(harnessName).join(", ") || "none"} \xB7 Folders: ${snapshot.roots.map(tildify2).join(", ") || "none yet"}`]
|
|
2411
2456
|
};
|
|
2412
2457
|
const [title, description] = about[place.kind] ?? ["", ""];
|
|
@@ -2496,6 +2541,7 @@ function App() {
|
|
|
2496
2541
|
CleanupWizard,
|
|
2497
2542
|
{
|
|
2498
2543
|
skills: yours,
|
|
2544
|
+
kept: snapshot.keptGlobal,
|
|
2499
2545
|
projects: snapshot.projects,
|
|
2500
2546
|
usesIn: (skill, project) => usage ? usage.byProject.get(project)?.get(skill) ?? 0 : null,
|
|
2501
2547
|
usesTotal: (skill) => usage ? usage.total.get(skill) ?? 0 : null,
|
|
@@ -2524,7 +2570,12 @@ function App() {
|
|
|
2524
2570
|
reload();
|
|
2525
2571
|
setToast({
|
|
2526
2572
|
ok: r.problems.length === 0,
|
|
2527
|
-
text:
|
|
2573
|
+
text: [
|
|
2574
|
+
r.unloaded || r.installs ? `${plural2(r.unloaded, "skill")} moved into projects (${plural2(r.installs, "install")})` : "",
|
|
2575
|
+
r.deleted ? `${r.deleted} deleted` : "",
|
|
2576
|
+
r.kept ? `${plural2(r.kept, "skill")} kept global on purpose` : "",
|
|
2577
|
+
r.problems.length ? `${r.problems.length} left global: ${r.problems[0]}` : ""
|
|
2578
|
+
].filter(Boolean).join(" \xB7 ")
|
|
2528
2579
|
});
|
|
2529
2580
|
}
|
|
2530
2581
|
}
|