skilllib 1.7.0 → 1.11.0
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 +83 -35
- package/dist/{chunk-AYAN7Q2X.js → chunk-LO4FMXDE.js} +446 -148
- package/dist/index.js +12 -6
- package/dist/json.d.cts +222 -0
- package/dist/json.d.ts +222 -0
- package/dist/{tui-RU255WYX.js → tui-3LDZ5GOZ.js} +350 -107
- package/package.json +16 -3
- package/skills/skilllib/SKILL.md +12 -5
|
@@ -24,6 +24,9 @@ function libraryDir() {
|
|
|
24
24
|
function claudeDir() {
|
|
25
25
|
return process.env.CLAUDE_CONFIG_DIR ?? join(userHome(), ".claude");
|
|
26
26
|
}
|
|
27
|
+
function grokDir() {
|
|
28
|
+
return process.env.GROK_HOME?.trim() || join(userHome(), ".grok");
|
|
29
|
+
}
|
|
27
30
|
function codexHome() {
|
|
28
31
|
return process.env.CODEX_HOME?.trim() || join(userHome(), ".codex");
|
|
29
32
|
}
|
|
@@ -32,7 +35,11 @@ function codexSystemDir() {
|
|
|
32
35
|
}
|
|
33
36
|
var PROJECT_SKILLS_DIR = ".claude/skills";
|
|
34
37
|
var AGENTS_SKILLS_DIR = ".agents/skills";
|
|
38
|
+
var GROK_SKILLS_DIR = ".grok/skills";
|
|
35
39
|
var MANIFEST_FILE = "skilllib.json";
|
|
40
|
+
function isRepoSkillLocation(location) {
|
|
41
|
+
return location === AGENTS_SKILLS_DIR || location === GROK_SKILLS_DIR;
|
|
42
|
+
}
|
|
36
43
|
var OWN_SKILL_MARKER = ".managed-by-skilllib";
|
|
37
44
|
|
|
38
45
|
// src/output.ts
|
|
@@ -40,7 +47,7 @@ var jsonMode = false;
|
|
|
40
47
|
function setJsonMode(on) {
|
|
41
48
|
jsonMode = on;
|
|
42
49
|
}
|
|
43
|
-
function json(data) {
|
|
50
|
+
function json(output, data) {
|
|
44
51
|
process.stdout.write(JSON.stringify(data) + "\n");
|
|
45
52
|
}
|
|
46
53
|
var color = (code) => (s) => process.stdout.isTTY && !jsonMode ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
@@ -103,7 +110,7 @@ var HARNESSES = [
|
|
|
103
110
|
icon: "\u2B21",
|
|
104
111
|
color: "#E8E8E8",
|
|
105
112
|
// The first folder is where links for Cursor go (see linkEverywhere). Cursor also reads .grok/skills;
|
|
106
|
-
// skilllib leaves that folder
|
|
113
|
+
// skilllib leaves that folder alone (see Grok). A .grok-only skill stays invisible to Cursor here.
|
|
107
114
|
projectDirs: [".agents/skills", ".cursor/skills", ".claude/skills", ".codex/skills"],
|
|
108
115
|
// Cursor ignores CLAUDE_CONFIG_DIR and CODEX_HOME: it always reads these under your home folder.
|
|
109
116
|
globalDirs: () => [".agents", ".cursor", ".claude", ".codex"].map((d) => join2(home(), d, "skills")),
|
|
@@ -128,6 +135,19 @@ var HARNESSES = [
|
|
|
128
135
|
projectDirs: [".agents/skills"],
|
|
129
136
|
globalDirs: () => [join2(home(), ".agents", "skills")],
|
|
130
137
|
installed: () => existsSync(join2(home(), ".config", "zed")) || onPath("zed") || hasApp("Zed")
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "grok",
|
|
141
|
+
name: "Grok",
|
|
142
|
+
short: "Gk",
|
|
143
|
+
icon: "\u2736",
|
|
144
|
+
color: "#F5C16C",
|
|
145
|
+
// .agents/skills is first so a skill Grok can't already see is linked into the shared folder, not .grok/skills.
|
|
146
|
+
projectDirs: [".agents/skills", ".grok/skills", ".claude/skills", ".cursor/skills"],
|
|
147
|
+
// Grok ignores CLAUDE_CONFIG_DIR: the Claude compat path is always ~/.claude/skills.
|
|
148
|
+
// The first folder is where global links for Grok go, so those links stay out of ~/.grok/skills.
|
|
149
|
+
globalDirs: () => [join2(home(), ".agents", "skills"), join2(grokDir(), "skills"), join2(home(), ".claude", "skills"), join2(home(), ".cursor", "skills")],
|
|
150
|
+
installed: () => existsSync(grokDir()) || onPath("grok") || hasApp("Grok")
|
|
131
151
|
}
|
|
132
152
|
];
|
|
133
153
|
function onPath(bin) {
|
|
@@ -322,7 +342,10 @@ function knownProjects() {
|
|
|
322
342
|
const path = projectsFile();
|
|
323
343
|
if (!existsSync4(path)) return [];
|
|
324
344
|
const list2 = JSON.parse(readFileSync3(path, "utf-8"));
|
|
325
|
-
return onePerFolder(list2.filter((p) => existsSync4(p) && isProjectCandidate(p)));
|
|
345
|
+
return onePerFolder(list2.filter((p) => existsSync4(p) && isProjectCandidate(p) && !isAgentWorktree(p)));
|
|
346
|
+
}
|
|
347
|
+
function isAgentWorktree(path) {
|
|
348
|
+
return /[\\/]\.claude[\\/]worktrees[\\/]/.test(path);
|
|
326
349
|
}
|
|
327
350
|
function onePerFolder(paths) {
|
|
328
351
|
const byReal = /* @__PURE__ */ new Map();
|
|
@@ -337,7 +360,7 @@ function isProjectCandidate(root) {
|
|
|
337
360
|
return realPath(root) !== realPath(userHome());
|
|
338
361
|
}
|
|
339
362
|
function rememberProjects(roots) {
|
|
340
|
-
const incoming = roots.map((r) => resolve3(r)).filter(isProjectCandidate);
|
|
363
|
+
const incoming = roots.map((r) => resolve3(r)).filter((r) => isProjectCandidate(r) && !isAgentWorktree(r));
|
|
341
364
|
const all = onePerFolder([...knownProjects(), ...incoming]).sort();
|
|
342
365
|
mkdirSync2(skilllibHome(), { recursive: true });
|
|
343
366
|
writeFileSync2(projectsFile(), JSON.stringify(all, null, 2) + "\n");
|
|
@@ -550,6 +573,20 @@ function globalFolderSkills(enabled) {
|
|
|
550
573
|
function tildify2(p) {
|
|
551
574
|
return tildify(p).replace(/\\/g, "/");
|
|
552
575
|
}
|
|
576
|
+
function grokBuiltInSkills(enabled) {
|
|
577
|
+
if (!enabled.includes("grok")) return [];
|
|
578
|
+
return skillDirsIn(join7(grokDir(), "bundled", "skills")).map((path) => ({
|
|
579
|
+
name: basename2(path),
|
|
580
|
+
kind: "built-in",
|
|
581
|
+
origin: "Grok",
|
|
582
|
+
path,
|
|
583
|
+
description: readSkillInfo(path).description,
|
|
584
|
+
movable: false,
|
|
585
|
+
broken: false,
|
|
586
|
+
harnesses: ["grok"],
|
|
587
|
+
links: []
|
|
588
|
+
}));
|
|
589
|
+
}
|
|
553
590
|
function cursorBuiltInSkills(enabled) {
|
|
554
591
|
if (!enabled.includes("cursor")) return [];
|
|
555
592
|
return skillDirsIn(join7(userHome(), ".cursor", "skills-cursor")).map((path) => ({
|
|
@@ -565,30 +602,41 @@ function cursorBuiltInSkills(enabled) {
|
|
|
565
602
|
}));
|
|
566
603
|
}
|
|
567
604
|
function claudeAiSkills(enabled) {
|
|
568
|
-
const
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
605
|
+
const byReal = /* @__PURE__ */ new Map();
|
|
606
|
+
for (const skills of [join7(claudeDir(), "skills"), join7(userHome(), ".claude", "skills")]) {
|
|
607
|
+
const synced = join7(skills, "synced");
|
|
608
|
+
const real = realpathOrNull(synced);
|
|
609
|
+
const readers = harnessesReading(skills, enabled);
|
|
610
|
+
if (!real || readers.length === 0) continue;
|
|
611
|
+
const entry = byReal.get(real) ?? { dir: synced, readers: [] };
|
|
612
|
+
for (const id of readers) if (!entry.readers.includes(id)) entry.readers.push(id);
|
|
613
|
+
byReal.set(real, entry);
|
|
614
|
+
}
|
|
615
|
+
return [...byReal.values()].flatMap(
|
|
616
|
+
({ dir, readers }) => readdirSync3(dir).filter((d) => !d.startsWith(".")).flatMap(
|
|
617
|
+
(bucket) => skillDirsIn(join7(dir, bucket)).map((path) => ({
|
|
618
|
+
name: basename2(path),
|
|
619
|
+
kind: "claude.ai",
|
|
620
|
+
origin: "claude.ai account",
|
|
621
|
+
path,
|
|
622
|
+
description: readSkillInfo(path).description,
|
|
623
|
+
movable: false,
|
|
624
|
+
broken: false,
|
|
625
|
+
harnesses: enabled.filter((id) => readers.includes(id)),
|
|
626
|
+
links: []
|
|
627
|
+
}))
|
|
628
|
+
)
|
|
583
629
|
);
|
|
584
630
|
}
|
|
585
|
-
function
|
|
586
|
-
const
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
631
|
+
function pluginDirCandidates(id, installPaths) {
|
|
632
|
+
const [name = "", marketplace = ""] = id.split("@");
|
|
633
|
+
const plugins = join7(claudeDir(), "plugins");
|
|
634
|
+
return [
|
|
635
|
+
...installPaths,
|
|
636
|
+
join7(plugins, "marketplaces", marketplace, "plugins", name),
|
|
637
|
+
join7(plugins, "marketplaces", marketplace, "external_plugins", name),
|
|
638
|
+
join7(plugins, "marketplaces", marketplace)
|
|
639
|
+
];
|
|
592
640
|
}
|
|
593
641
|
function repoSettingsFiles(root) {
|
|
594
642
|
return [join7(root, ".claude", "settings.json"), join7(root, ".claude", "settings.local.json")];
|
|
@@ -606,13 +654,7 @@ function pluginSkills(harnesses, root) {
|
|
|
606
654
|
const user = enabledPlugins();
|
|
607
655
|
const enabled = root ? enabledPlugins(root) : user;
|
|
608
656
|
const fromMarketplaces = [.../* @__PURE__ */ new Set([...Object.keys(enabled), ...Object.keys(user)])].filter((id) => claude2 && enabled[id] || cursor && user[id]).flatMap((id) => {
|
|
609
|
-
const
|
|
610
|
-
const candidates = [
|
|
611
|
-
...(installed[id] ?? []).flatMap((i) => i.installPath ? [i.installPath] : []),
|
|
612
|
-
join7(plugins, "marketplaces", marketplace, "plugins", name),
|
|
613
|
-
join7(plugins, "marketplaces", marketplace, "external_plugins", name),
|
|
614
|
-
join7(plugins, "marketplaces", marketplace)
|
|
615
|
-
];
|
|
657
|
+
const candidates = pluginDirCandidates(id, (installed[id] ?? []).flatMap((i) => i.installPath ? [i.installPath] : []));
|
|
616
658
|
const dir = candidates.find((c) => existsSync6(join7(c, "skills")));
|
|
617
659
|
if (!dir) return [];
|
|
618
660
|
const inCursor = cursor && user[id] && (installed[id] ?? []).some((i) => i.scope === "user" && i.installPath === dir);
|
|
@@ -647,6 +689,7 @@ function machineSkills(enabled = enabledHarnesses()) {
|
|
|
647
689
|
...claudeAiSkills(enabled),
|
|
648
690
|
...pluginSkills(enabled),
|
|
649
691
|
...cursorBuiltInSkills(enabled),
|
|
692
|
+
...grokBuiltInSkills(enabled),
|
|
650
693
|
...systemSkills(enabled)
|
|
651
694
|
].sort(byKindAndName);
|
|
652
695
|
}
|
|
@@ -670,7 +713,7 @@ function recordOrigin(name, origin) {
|
|
|
670
713
|
function originFor(dir) {
|
|
671
714
|
const match = machineSkills().find((s) => resolve5(s.path) === resolve5(dir));
|
|
672
715
|
if (match) return match.kind === "global" ? "global folder" : `${match.kind}: ${match.origin}`;
|
|
673
|
-
const project = dir.split(/[\\/]\.(?:claude|agents)[\\/]skills[\\/]/)[0];
|
|
716
|
+
const project = dir.split(/[\\/]\.(?:claude|agents|grok|cursor|codex)[\\/]skills[\\/]/)[0];
|
|
674
717
|
if (!project || project === dir) return dir;
|
|
675
718
|
const locked = projectSkillsLock(project)[basename2(dir)]?.source;
|
|
676
719
|
return locked ? `skills.sh: ${locked}` : `project: ${basename2(project)}`;
|
|
@@ -716,7 +759,7 @@ function visibilityOf(root, name, enabled = enabledHarnesses()) {
|
|
|
716
759
|
}
|
|
717
760
|
function dependencyDirs(root, name, dep) {
|
|
718
761
|
const wanted = installDirs(enabledHarnesses());
|
|
719
|
-
const untracked = ALL_PROJECT_DIRS.find((d) => existsSync7(join8(root, d, name, "SKILL.md")) && !isLink(join8(root, d, name)));
|
|
762
|
+
const untracked = ALL_PROJECT_DIRS.find((d) => d !== GROK_SKILLS_DIR && existsSync7(join8(root, d, name, "SKILL.md")) && !isLink(join8(root, d, name)));
|
|
720
763
|
const primary = dep ? dep.dir ?? PROJECT_SKILLS_DIR : untracked ?? wanted[0];
|
|
721
764
|
return [primary, .../* @__PURE__ */ new Set([...dep?.links ?? [], ...wanted.filter((d) => d !== primary)])];
|
|
722
765
|
}
|
|
@@ -768,7 +811,7 @@ function projectStatus(root) {
|
|
|
768
811
|
unmanaged.push({
|
|
769
812
|
name,
|
|
770
813
|
managed: false,
|
|
771
|
-
state: unmanagedState(name, real, location
|
|
814
|
+
state: unmanagedState(name, real, isRepoSkillLocation(location), source),
|
|
772
815
|
version: null,
|
|
773
816
|
latest: latestVersion(name)?.version ?? null,
|
|
774
817
|
location,
|
|
@@ -788,10 +831,12 @@ function unmanagedState(name, real, committed, source) {
|
|
|
788
831
|
return latest === null ? "local only" : sameAsLibrary ? "untracked copy of library skill" : "untracked, differs from library";
|
|
789
832
|
}
|
|
790
833
|
var NESTED_READERS = [
|
|
791
|
-
[PROJECT_SKILLS_DIR, ["claude-code", "cursor"]],
|
|
792
|
-
[AGENTS_SKILLS_DIR, ["codex", "cursor"]],
|
|
793
|
-
[".cursor/skills", ["cursor"]],
|
|
794
|
-
[".codex/skills", ["codex", "cursor"]]
|
|
834
|
+
[PROJECT_SKILLS_DIR, ["claude-code", "cursor", "grok"]],
|
|
835
|
+
[AGENTS_SKILLS_DIR, ["codex", "cursor", "grok"]],
|
|
836
|
+
[".cursor/skills", ["cursor", "grok"]],
|
|
837
|
+
[".codex/skills", ["codex", "cursor"]],
|
|
838
|
+
// Cursor reads nested .grok/skills too, but skilllib doesn't count it: that folder stays Grok's.
|
|
839
|
+
[".grok/skills", ["grok"]]
|
|
795
840
|
];
|
|
796
841
|
var NESTED_SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "vendor", "target", "coverage"]);
|
|
797
842
|
function nestedSkills(root, { depth = 3, enabled = enabledHarnesses() } = {}) {
|
|
@@ -813,7 +858,7 @@ function nestedSkills(root, { depth = 3, enabled = enabledHarnesses() } = {}) {
|
|
|
813
858
|
found.push({
|
|
814
859
|
name,
|
|
815
860
|
managed: false,
|
|
816
|
-
state: unmanagedState(name, path, skillsDir
|
|
861
|
+
state: unmanagedState(name, path, isRepoSkillLocation(skillsDir), void 0),
|
|
817
862
|
version: null,
|
|
818
863
|
latest: latestVersion(name)?.version ?? null,
|
|
819
864
|
location,
|
|
@@ -866,10 +911,15 @@ function linkEverywhere(root, name, location, { allowTracked = false, dryRun = f
|
|
|
866
911
|
}
|
|
867
912
|
return result;
|
|
868
913
|
}
|
|
869
|
-
function unlinkEverywhere(root, name) {
|
|
914
|
+
function unlinkEverywhere(root, name, removing) {
|
|
915
|
+
const gone = removing ? realpathOrNull2(removing) : null;
|
|
870
916
|
return ALL_PROJECT_DIRS.filter((dir) => {
|
|
871
917
|
const link2 = join8(root, dir, name);
|
|
872
918
|
if (!isLink(link2)) return false;
|
|
919
|
+
if (dir === GROK_SKILLS_DIR) {
|
|
920
|
+
const target = realpathOrNull2(link2);
|
|
921
|
+
if (target !== null && target !== gone) return false;
|
|
922
|
+
}
|
|
873
923
|
unlinkSync(link2);
|
|
874
924
|
return true;
|
|
875
925
|
});
|
|
@@ -1052,7 +1102,7 @@ function stash(dir, bucket) {
|
|
|
1052
1102
|
return target;
|
|
1053
1103
|
}
|
|
1054
1104
|
function removeUntracked(root, name, path) {
|
|
1055
|
-
unlinkEverywhere(root, name);
|
|
1105
|
+
unlinkEverywhere(root, name, path);
|
|
1056
1106
|
return stash(path, "trash");
|
|
1057
1107
|
}
|
|
1058
1108
|
function projectsUsing(name) {
|
|
@@ -1215,7 +1265,7 @@ function refreshAgentSkill() {
|
|
|
1215
1265
|
|
|
1216
1266
|
// src/commands.ts
|
|
1217
1267
|
import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
|
|
1218
|
-
import { basename as
|
|
1268
|
+
import { basename as basename9, join as join14, resolve as resolve7, sep as sep5 } from "path";
|
|
1219
1269
|
|
|
1220
1270
|
// src/git.ts
|
|
1221
1271
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -1310,21 +1360,109 @@ function claudePlugins(machine, root) {
|
|
|
1310
1360
|
};
|
|
1311
1361
|
});
|
|
1312
1362
|
}
|
|
1363
|
+
function newer(a, b) {
|
|
1364
|
+
const parts = (v) => v.replace(/^v/i, "").split(/[.+-]/).map((x) => parseInt(x, 10) || 0);
|
|
1365
|
+
const pa = parts(a);
|
|
1366
|
+
const pb = parts(b);
|
|
1367
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) > (pb[i] ?? 0);
|
|
1368
|
+
return false;
|
|
1369
|
+
}
|
|
1370
|
+
function updateOf(name, marketplace, entry, catalogs) {
|
|
1371
|
+
const dir = join11(claudeDir(), "plugins", "marketplaces", marketplace);
|
|
1372
|
+
if (!catalogs.has(marketplace)) catalogs.set(marketplace, readJson2(join11(dir, ".claude-plugin", "marketplace.json"))?.plugins ?? []);
|
|
1373
|
+
const listed = catalogs.get(marketplace).find((p) => p.name === name);
|
|
1374
|
+
if (!listed) return void 0;
|
|
1375
|
+
const version = listed.version ?? (typeof listed.source === "string" ? readJson2(join11(dir, listed.source, ".claude-plugin", "plugin.json"))?.version : void 0);
|
|
1376
|
+
if (version && entry.version) return newer(version, entry.version) ? version : void 0;
|
|
1377
|
+
const sha = typeof listed.source === "object" ? listed.source?.sha : void 0;
|
|
1378
|
+
return sha && entry.gitCommitSha && sha !== entry.gitCommitSha ? "newer" : void 0;
|
|
1379
|
+
}
|
|
1380
|
+
function pluginRoot(id, entry) {
|
|
1381
|
+
const candidates = pluginDirCandidates(id, entry.installPath ? [entry.installPath] : []);
|
|
1382
|
+
return candidates.find((c) => existsSync10(join11(c, "skills"))) ?? candidates.find((c) => existsSync10(c)) ?? candidates[0];
|
|
1383
|
+
}
|
|
1384
|
+
function describe(root) {
|
|
1385
|
+
const manifest = readJson2(join11(root, ".claude-plugin", "plugin.json")) ?? {};
|
|
1386
|
+
return { description: manifest.description ?? "", version: manifest.version, skills: skillDirsIn(join11(root, "skills")), extras: extrasOf(root) };
|
|
1387
|
+
}
|
|
1388
|
+
function installedPlugins() {
|
|
1389
|
+
const plugins = join11(claudeDir(), "plugins");
|
|
1390
|
+
const installed = readJson2(join11(plugins, "installed_plugins.json"))?.plugins ?? {};
|
|
1391
|
+
const catalogs = /* @__PURE__ */ new Map();
|
|
1392
|
+
const fromMarketplaces = Object.entries(installed).flatMap(
|
|
1393
|
+
([id, entries]) => entries.map((entry) => {
|
|
1394
|
+
const [name = id, marketplace = ""] = id.split("@");
|
|
1395
|
+
const scope = entry.scope ?? "user";
|
|
1396
|
+
const projectPath = scope === "user" ? void 0 : entry.projectPath;
|
|
1397
|
+
const root = pluginRoot(id, entry);
|
|
1398
|
+
const about = describe(root);
|
|
1399
|
+
const update2 = updateOf(name, marketplace, entry, catalogs);
|
|
1400
|
+
return {
|
|
1401
|
+
id,
|
|
1402
|
+
scope,
|
|
1403
|
+
...projectPath ? { projectPath } : {},
|
|
1404
|
+
synced: false,
|
|
1405
|
+
agent: "claude-code",
|
|
1406
|
+
on: enabledPlugins(projectPath)[id] === true,
|
|
1407
|
+
root,
|
|
1408
|
+
...about,
|
|
1409
|
+
version: entry.version ?? about.version,
|
|
1410
|
+
...update2 ? { update: update2 } : {}
|
|
1411
|
+
};
|
|
1412
|
+
})
|
|
1413
|
+
);
|
|
1414
|
+
const user = enabledPlugins();
|
|
1415
|
+
const fromSettings = Object.keys(user).filter((id) => user[id] === true && id.includes("@") && !id.endsWith("@synced") && !installed[id]).flatMap((id) => {
|
|
1416
|
+
const root = pluginDirCandidates(id, []).find((c) => existsSync10(join11(c, "skills")));
|
|
1417
|
+
return root ? [{ id, scope: "user", synced: false, agent: "claude-code", on: true, settingsOnly: true, root, ...describe(root) }] : [];
|
|
1418
|
+
});
|
|
1419
|
+
const syncedDir = join11(plugins, "synced");
|
|
1420
|
+
const dirs = (p) => existsSync10(p) ? readdirSync5(p, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => join11(p, d.name)) : [];
|
|
1421
|
+
const fromSynced = dirs(syncedDir).flatMap(dirs).map((root) => {
|
|
1422
|
+
const id = `${readJson2(join11(root, ".claude-plugin", "plugin.json"))?.name ?? basename4(root)}@synced`;
|
|
1423
|
+
return { id, scope: null, synced: true, agent: "claude-code", on: user[id] !== false, root, ...describe(root) };
|
|
1424
|
+
});
|
|
1425
|
+
return [...fromMarketplaces, ...fromSettings, ...fromSynced];
|
|
1426
|
+
}
|
|
1427
|
+
function cursorPlugins() {
|
|
1428
|
+
const cache = join11(userHome(), ".cursor", "plugins", "cache");
|
|
1429
|
+
const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") && statSync3(join11(p, d)).isDirectory()) : [];
|
|
1430
|
+
return dirs(cache).flatMap(
|
|
1431
|
+
(marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
|
|
1432
|
+
const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
|
|
1433
|
+
const root = versions.sort((a, b) => statSync3(b).mtimeMs - statSync3(a).mtimeMs)[0];
|
|
1434
|
+
if (!root) return [];
|
|
1435
|
+
const about = describe(root);
|
|
1436
|
+
return [{ id: `${plugin}@${marketplace}`, scope: null, synced: false, agent: "cursor", on: null, root, ...about, version: about.version ?? basename4(root) }];
|
|
1437
|
+
})
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
var binaries = /* @__PURE__ */ new Map();
|
|
1441
|
+
function forgetClaudeBinary() {
|
|
1442
|
+
binaries.clear();
|
|
1443
|
+
}
|
|
1313
1444
|
function claudeBinary() {
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1445
|
+
const key = `${process.env.PATH}|${userHome()}`;
|
|
1446
|
+
if (!binaries.has(key)) {
|
|
1447
|
+
const candidates = ["claude", join11(userHome(), ".local", "bin", "claude"), join11(userHome(), ".claude", "local", "claude")];
|
|
1448
|
+
const found = candidates.find((bin) => {
|
|
1449
|
+
try {
|
|
1450
|
+
return execFileSync3(bin, ["--version"], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e4, encoding: "utf-8" }).includes("Claude Code");
|
|
1451
|
+
} catch {
|
|
1452
|
+
return false;
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
binaries.set(key, found ?? null);
|
|
1456
|
+
}
|
|
1457
|
+
return binaries.get(key);
|
|
1322
1458
|
}
|
|
1323
1459
|
function claude(args, cwd, bin = claudeBinary()) {
|
|
1460
|
+
if (cwd && !existsSync10(cwd)) return { ok: false, reason: `${cwd} no longer exists; run \`claude ${args.join(" ")}\` in the project's new folder, or remove the install with /plugin` };
|
|
1324
1461
|
if (!bin) return { ok: false, reason: `Claude Code's CLI didn't run (tried \`claude\` on your PATH and ~/.local/bin/claude); run \`claude ${args.join(" ")}\` yourself` };
|
|
1462
|
+
const slow = args[1] === "install" || args[1] === "update";
|
|
1325
1463
|
try {
|
|
1326
|
-
execFileSync3(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 12e4, encoding: "utf-8" });
|
|
1327
|
-
return { ok: true };
|
|
1464
|
+
const out2 = execFileSync3(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: slow ? 12e4 : 2e4, encoding: "utf-8" });
|
|
1465
|
+
return { ok: true, out: out2 };
|
|
1328
1466
|
} catch (err) {
|
|
1329
1467
|
const e = err;
|
|
1330
1468
|
return { ok: false, reason: `${(e.stderr || e.stdout || e.message || "failed").trim().split("\n").slice(-1)[0]} (ran ${bin})` };
|
|
@@ -1338,9 +1476,6 @@ function setPluginIn(file, id, on) {
|
|
|
1338
1476
|
writeFileSync7(file, JSON.stringify({ ...settings, enabledPlugins: { ...enabled, [id]: on } }, null, 2) + "\n");
|
|
1339
1477
|
return { ok: true };
|
|
1340
1478
|
}
|
|
1341
|
-
function turnOffSynced(id) {
|
|
1342
|
-
return setPluginIn(join11(claudeDir(), "settings.json"), id, false);
|
|
1343
|
-
}
|
|
1344
1479
|
function ignoreLocalSettings(root) {
|
|
1345
1480
|
const file = ".claude/settings.local.json";
|
|
1346
1481
|
const git2 = (...args) => execFileSync3("git", args, { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
@@ -1380,6 +1515,50 @@ function turnOffIn(plugin, root) {
|
|
|
1380
1515
|
const r = claude(["plugin", "disable", plugin.id, "--scope", "local"], root, bin);
|
|
1381
1516
|
return r.ok ? { ok: true, message: done } : { ok: false, message: `${plugin.id}: ${r.reason}` };
|
|
1382
1517
|
}
|
|
1518
|
+
function settingsFileOf(p) {
|
|
1519
|
+
if (p.synced || !p.scope || p.scope === "user") return join11(claudeDir(), "settings.json");
|
|
1520
|
+
if (!p.projectPath) return null;
|
|
1521
|
+
return join11(p.projectPath, ".claude", p.scope === "local" ? "settings.local.json" : "settings.json");
|
|
1522
|
+
}
|
|
1523
|
+
function whereOf(p) {
|
|
1524
|
+
return p.projectPath ? ` in ${basename4(p.projectPath)} (${p.scope})` : "";
|
|
1525
|
+
}
|
|
1526
|
+
function setPluginOn(p, on) {
|
|
1527
|
+
const done = { ok: true, message: `${p.id} is ${on ? "on" : "off"}${whereOf(p)}${p.synced ? "" : "; Claude Code picks it up in its next session"}` };
|
|
1528
|
+
if (p.scope && p.scope !== "user" && !p.projectPath) return { ok: false, message: `${p.id}: skilllib doesn't know which project it's installed in; run \`claude plugin ${on ? "enable" : "disable"} ${p.id} --scope ${p.scope}\` there` };
|
|
1529
|
+
const bin = p.synced || p.settingsOnly ? null : claudeBinary();
|
|
1530
|
+
if (bin) {
|
|
1531
|
+
const r2 = claude(["plugin", on ? "enable" : "disable", p.id, ...p.scope ? ["--scope", p.scope] : []], p.projectPath, bin);
|
|
1532
|
+
return r2.ok ? done : { ok: false, message: `${p.id}: ${r2.reason}` };
|
|
1533
|
+
}
|
|
1534
|
+
const r = setPluginIn(settingsFileOf(p), p.id, on);
|
|
1535
|
+
if (!r.ok) return { ok: false, message: `${p.id}: ${r.reason}` };
|
|
1536
|
+
return p.scope === "local" && p.projectPath ? { ...done, message: done.message + ignoreLocalSettings(p.projectPath) } : done;
|
|
1537
|
+
}
|
|
1538
|
+
function uninstallPlugin(p) {
|
|
1539
|
+
if (p.synced) return { ok: false, message: `${p.id} is synced from your claude.ai account: remove it there, or turn it off here` };
|
|
1540
|
+
if (p.settingsOnly) return { ok: false, message: `${p.id} isn't installed, only turned on in your settings: turn it off instead` };
|
|
1541
|
+
if (p.scope && p.scope !== "user" && !p.projectPath) return { ok: false, message: `${p.id}: skilllib doesn't know which project it's installed in; run \`claude plugin uninstall ${p.id} --scope ${p.scope}\` there` };
|
|
1542
|
+
const wasOn = enabledPlugins(p.projectPath)[p.id] === true;
|
|
1543
|
+
const r = claude(["plugin", "uninstall", p.id, "--keep-data", "--scope", p.scope ?? "user"], p.projectPath);
|
|
1544
|
+
if (!r.ok) return { ok: false, message: `${p.id}: ${r.reason}` };
|
|
1545
|
+
recordRemovedPlugin(p.id, p.scope ?? "user", p.projectPath, wasOn);
|
|
1546
|
+
return { ok: true, message: `${p.id} is uninstalled${whereOf(p)} (Settings \u203A Backups reinstalls it)` };
|
|
1547
|
+
}
|
|
1548
|
+
function updatePlugin(p) {
|
|
1549
|
+
if (p.synced) return { ok: false, message: `${p.id} is synced from your claude.ai account: it updates from there` };
|
|
1550
|
+
if (p.settingsOnly) return { ok: false, message: `${p.id} isn't installed, only turned on in your settings: install it with /plugin to update it` };
|
|
1551
|
+
const r = claude(["plugin", "update", p.id, ...p.scope ? ["--scope", p.scope] : [], "--json"], p.projectPath);
|
|
1552
|
+
if (!r.ok) return { ok: false, message: `${p.id}: ${r.reason}; to update it yourself, run \`claude plugin update ${p.id}\`${p.projectPath ? ` in ${p.projectPath}` : ""}` };
|
|
1553
|
+
let result = {};
|
|
1554
|
+
try {
|
|
1555
|
+
result = JSON.parse(r.out.trim().split("\n").pop() ?? "{}");
|
|
1556
|
+
} catch {
|
|
1557
|
+
}
|
|
1558
|
+
if (result.updateOutcome === "up_to_date") return { ok: true, message: `${p.id} is already the newest version${result.newVersion ? ` (${result.newVersion})` : ""}${whereOf(p)}` };
|
|
1559
|
+
const versions = result.oldVersion && result.newVersion ? ` from ${result.oldVersion} to ${result.newVersion}` : "";
|
|
1560
|
+
return { ok: true, message: `${p.id} is updated${versions}${whereOf(p)}; Claude Code uses the new version after a restart` };
|
|
1561
|
+
}
|
|
1383
1562
|
function removedFile() {
|
|
1384
1563
|
return join11(skilllibHome(), "plugin-backup.json");
|
|
1385
1564
|
}
|
|
@@ -1392,42 +1571,45 @@ function removePlugin(plugin, how) {
|
|
|
1392
1571
|
}
|
|
1393
1572
|
const imported = plugin.skills.filter((dir) => latestVersion(basename4(dir)) === null).map((dir) => importSkill(dir).name);
|
|
1394
1573
|
const scope = plugin.scope ? ["--scope", plugin.scope] : [];
|
|
1395
|
-
const r = plugin.synced ?
|
|
1574
|
+
const r = plugin.synced ? (
|
|
1575
|
+
// Synced plugins aren't installed, so `claude plugin` can't touch them: their switch is enabledPlugins.
|
|
1576
|
+
setPluginIn(join11(claudeDir(), "settings.json"), plugin.id, false)
|
|
1577
|
+
) : claude(["plugin", how === "delete" ? "uninstall" : "disable", plugin.id, ...scope], plugin.projectPath);
|
|
1396
1578
|
const saved = imported.length ? `; ${imported.join(", ")} copied into Your skills` : "";
|
|
1397
1579
|
if (!r.ok) return { ok: false, message: `${plugin.id}: ${r.reason}${saved}` };
|
|
1398
|
-
if (how === "delete" && !plugin.synced) recordRemovedPlugin(plugin.id, plugin.scope ?? "user");
|
|
1580
|
+
if (how === "delete" && !plugin.synced) recordRemovedPlugin(plugin.id, plugin.scope ?? "user", plugin.projectPath, true);
|
|
1399
1581
|
return { ok: true, message: `${plugin.id} ${how === "delete" && !plugin.synced ? "removed" : "turned off"}${saved}` };
|
|
1400
1582
|
}
|
|
1401
|
-
function recordRemovedPlugin(id, scope) {
|
|
1583
|
+
function recordRemovedPlugin(id, scope, projectPath, on = true) {
|
|
1402
1584
|
const list2 = readJson2(removedFile()) ?? [];
|
|
1403
1585
|
mkdirSync7(skilllibHome(), { recursive: true });
|
|
1404
|
-
writeFileSync7(removedFile(), JSON.stringify([...list2, { id, scope, at: (/* @__PURE__ */ new Date()).toISOString() }], null, 2) + "\n");
|
|
1586
|
+
writeFileSync7(removedFile(), JSON.stringify([...list2, { id, scope, ...projectPath ? { projectPath } : {}, on, at: (/* @__PURE__ */ new Date()).toISOString() }], null, 2) + "\n");
|
|
1405
1587
|
}
|
|
1406
1588
|
function pluginBackups() {
|
|
1407
1589
|
return (readJson2(removedFile()) ?? []).map((r) => ({
|
|
1408
1590
|
name: r.id,
|
|
1409
1591
|
kind: "plugin",
|
|
1410
|
-
|
|
1592
|
+
// Project and local installs go back into their project.
|
|
1593
|
+
path: r.projectPath ? `${r.scope}:${r.projectPath}` : r.scope,
|
|
1411
1594
|
movedAt: r.at.slice(0, 16).replace("T", " "),
|
|
1412
1595
|
from: r.id
|
|
1413
1596
|
}));
|
|
1414
1597
|
}
|
|
1415
1598
|
function restorePlugin(backup) {
|
|
1416
|
-
const
|
|
1599
|
+
const [scope = "user", ...rest] = backup.path.split(":");
|
|
1600
|
+
const projectPath = rest.join(":") || void 0;
|
|
1601
|
+
const r = claude(["plugin", "install", backup.from, "--scope", scope], projectPath);
|
|
1417
1602
|
if (!r.ok) return r;
|
|
1418
|
-
|
|
1419
|
-
|
|
1603
|
+
const records = readJson2(removedFile()) ?? [];
|
|
1604
|
+
const same = (x) => x.id === backup.from && (x.projectPath ?? void 0) === projectPath && x.scope === scope;
|
|
1605
|
+
const wasOff = records.filter(same).pop()?.on === false;
|
|
1606
|
+
const off = wasOff ? claude(["plugin", "disable", backup.from, "--scope", scope], projectPath) : null;
|
|
1607
|
+
writeFileSync7(removedFile(), JSON.stringify(records.filter((x) => !same(x)), null, 2) + "\n");
|
|
1608
|
+
const where = `Claude Code plugins (${scope}${projectPath ? ` in ${basename4(projectPath)}` : ""}${wasOff ? off?.ok ? ", off as it was" : ", but it's on: turn it off from Plugins" : ""})`;
|
|
1609
|
+
return { ok: true, to: where };
|
|
1420
1610
|
}
|
|
1421
|
-
function cursorPluginSkills() {
|
|
1422
|
-
|
|
1423
|
-
const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") && statSync3(join11(p, d)).isDirectory()) : [];
|
|
1424
|
-
return dirs(cache).flatMap(
|
|
1425
|
-
(marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
|
|
1426
|
-
const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
|
|
1427
|
-
const newest = versions.sort((a, b) => statSync3(b).mtimeMs - statSync3(a).mtimeMs)[0];
|
|
1428
|
-
return newest ? skillDirsIn(join11(newest, "skills")).map((path) => ({ name: basename4(path), plugin, path })) : [];
|
|
1429
|
-
})
|
|
1430
|
-
);
|
|
1611
|
+
function cursorPluginSkills(plugins = cursorPlugins()) {
|
|
1612
|
+
return plugins.flatMap((p) => p.skills.map((path) => ({ name: basename4(path), plugin: p.id.split("@")[0], path })));
|
|
1431
1613
|
}
|
|
1432
1614
|
|
|
1433
1615
|
// src/tidy.ts
|
|
@@ -1455,6 +1637,11 @@ function namesIn(dirs) {
|
|
|
1455
1637
|
}
|
|
1456
1638
|
return [...names].sort();
|
|
1457
1639
|
}
|
|
1640
|
+
var PROJECT_TIDY_DIRS = ALL_PROJECT_DIRS.filter((d) => d !== GROK_SKILLS_DIR);
|
|
1641
|
+
function globalTidyDirs() {
|
|
1642
|
+
const own = realPath(join12(grokDir(), "skills"));
|
|
1643
|
+
return globalSkillDirs().filter((d) => realPath(d) !== own);
|
|
1644
|
+
}
|
|
1458
1645
|
function runsFrom(id, dirsWithCopy, readsOf) {
|
|
1459
1646
|
const reads = readsOf(id).filter((d) => dirsWithCopy.includes(d));
|
|
1460
1647
|
if (id === "codex") return reads;
|
|
@@ -1477,8 +1664,8 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1477
1664
|
const readsOf = (id) => HARNESSES.find((h) => h.id === id).projectDirs.map(abs);
|
|
1478
1665
|
let git2;
|
|
1479
1666
|
const skillsLock = projectSkillsLock(root);
|
|
1480
|
-
for (const name of namesIn(
|
|
1481
|
-
const entries = entriesOf(
|
|
1667
|
+
for (const name of namesIn(PROJECT_TIDY_DIRS.map(abs))) {
|
|
1668
|
+
const entries = entriesOf(PROJECT_TIDY_DIRS.map(abs), name);
|
|
1482
1669
|
const external = entries.find((e) => e.link && e.real && !(e.real + sep4).startsWith(realpathOrNull2(root) + sep4));
|
|
1483
1670
|
if (external) {
|
|
1484
1671
|
report.skipped.push({ name, reason: `${rel(external.path)} links outside the repo` });
|
|
@@ -1533,7 +1720,7 @@ function isSkillsDirPlugin(path) {
|
|
|
1533
1720
|
}
|
|
1534
1721
|
function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
|
|
1535
1722
|
const report = { plans: [], conflicts: [], skipped: [] };
|
|
1536
|
-
const dirs =
|
|
1723
|
+
const dirs = globalTidyDirs();
|
|
1537
1724
|
const agents = join12(userHome(), ".agents", "skills");
|
|
1538
1725
|
for (const name of namesIn(dirs)) {
|
|
1539
1726
|
const entries = entriesOf(dirs, name);
|
|
@@ -1607,7 +1794,7 @@ function folderLabel(root, dir) {
|
|
|
1607
1794
|
return root ? relativeTo(root, dir) : tildify(dir);
|
|
1608
1795
|
}
|
|
1609
1796
|
function foldersOf(root, name) {
|
|
1610
|
-
const entries = entriesOf(root ?
|
|
1797
|
+
const entries = entriesOf(root ? PROJECT_TIDY_DIRS.map((d) => join12(root, d)) : globalTidyDirs(), name);
|
|
1611
1798
|
return { all: entries.map((e) => e.dir), real: entries.filter((e) => !e.link).map((e) => e.dir) };
|
|
1612
1799
|
}
|
|
1613
1800
|
|
|
@@ -1805,7 +1992,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1805
1992
|
id: "review:global",
|
|
1806
1993
|
severity: "suggestion",
|
|
1807
1994
|
title: `${plural(unreviewed.length, "global skill")} of yours ${unreviewed.length === 1 ? "loads" : "load"} in every repo`,
|
|
1808
|
-
detail: `${unreviewed.join(", ")}. Move each to the repos that need it (skilllib
|
|
1995
|
+
detail: `${unreviewed.join(", ")}. Move each to the repos that need it (skilllib global move <name> --to <repo>...), or mark the ones you want everywhere as global on purpose (skilllib global keep <name>).`
|
|
1809
1996
|
});
|
|
1810
1997
|
}
|
|
1811
1998
|
const enabled = enabledHarnesses();
|
|
@@ -1831,7 +2018,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1831
2018
|
id: `usable:global${onPurpose ? ":kept" : ""}`,
|
|
1832
2019
|
severity: "suggestion",
|
|
1833
2020
|
title: `${plural(skills.length, "global skill")}${onPurpose ? " you keep global" : ""} not usable by ${agentNames(missing)}`,
|
|
1834
|
-
detail: `${skills.map((m) => `${m.name} (only ${agentNames(m.harnesses)})`).join(", ")}. ${onPurpose ? "A link in each agent's global folder makes the one real copy reach every agent you use." : `Link ${skills.length === 1 ? "it" : "them"} for every agent, or move ${skills.length === 1 ? "it" : "them"} to the repos that need ${skills.length === 1 ? "it" : "them"} (skilllib
|
|
2021
|
+
detail: `${skills.map((m) => `${m.name} (only ${agentNames(m.harnesses)})`).join(", ")}. ${onPurpose ? "A link in each agent's global folder makes the one real copy reach every agent you use." : `Link ${skills.length === 1 ? "it" : "them"} for every agent, or move ${skills.length === 1 ? "it" : "them"} to the repos that need ${skills.length === 1 ? "it" : "them"} (skilllib global move), which works for every agent too.`}`,
|
|
1835
2022
|
...onPurpose ? { fix: link2 } : { choices: [link2] }
|
|
1836
2023
|
});
|
|
1837
2024
|
}
|
|
@@ -2003,17 +2190,17 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2003
2190
|
});
|
|
2004
2191
|
}
|
|
2005
2192
|
if (s.state === "edited locally" || s.state === "edited locally, update available") {
|
|
2006
|
-
const
|
|
2193
|
+
const newer2 = s.state !== "edited locally";
|
|
2007
2194
|
const pinned = s.version ? `v${s.version}` : "the library version";
|
|
2008
2195
|
issues.push({
|
|
2009
2196
|
id: `edited:${root}:${s.name}`,
|
|
2010
2197
|
severity: "suggestion",
|
|
2011
2198
|
title: `${s.name} in ${where} has local edits`,
|
|
2012
|
-
detail: `It no longer matches ${pinned} from Your skills${
|
|
2199
|
+
detail: `It no longer matches ${pinned} from Your skills${newer2 ? `, and v${s.latest} is out` : ""}. Save the edits as a new version so your other repos can get them, or discard them. Keeping them is fine too: skilllib never overwrites edits.`,
|
|
2013
2200
|
choices: [
|
|
2014
2201
|
{
|
|
2015
2202
|
label: "Save as a new version in Your skills",
|
|
2016
|
-
hint:
|
|
2203
|
+
hint: newer2 ? `becomes v${s.latest + 1}; v${s.latest}'s changes aren't merged in` : `becomes v${(s.latest ?? 0) + 1}; ${where} uses it`,
|
|
2017
2204
|
run: () => {
|
|
2018
2205
|
importSkill(s.path, { force: true });
|
|
2019
2206
|
const c = addSkill(root, s.name);
|
|
@@ -2193,6 +2380,9 @@ function topLevel(root) {
|
|
|
2193
2380
|
return [];
|
|
2194
2381
|
}
|
|
2195
2382
|
}
|
|
2383
|
+
function statusIssues(root, machine = machineSkills(enabledHarnesses())) {
|
|
2384
|
+
return findIssues(projectHere(root) === root ? [root] : [], machine, new Set(librarySkills().map((l) => l.name)));
|
|
2385
|
+
}
|
|
2196
2386
|
function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
2197
2387
|
const agents = enabledHarnesses();
|
|
2198
2388
|
const inRepo = projectHere(root) === root;
|
|
@@ -2207,12 +2397,12 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2207
2397
|
const usual = s.state === "ok" || s.state === "local only" || repo && s.state !== "from npx skills";
|
|
2208
2398
|
const attrs = {
|
|
2209
2399
|
source: s.managed ? "library" : repo ? "repo" : "local",
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2400
|
+
dir: s.location !== ".claude/skills" ? s.location : void 0,
|
|
2401
|
+
version: s.version ?? void 0,
|
|
2402
|
+
state: usual ? void 0 : s.state,
|
|
2403
|
+
latest: behind ? s.latest : void 0,
|
|
2404
|
+
git: gitState,
|
|
2405
|
+
agents: sameSet(loadedBy, agents) ? void 0 : loadedBy
|
|
2216
2406
|
};
|
|
2217
2407
|
return [JSON.stringify(attrs), s.name, () => ({ ...attrs, skills: [] })];
|
|
2218
2408
|
})
|
|
@@ -2228,11 +2418,12 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2228
2418
|
})
|
|
2229
2419
|
);
|
|
2230
2420
|
const issues = /* @__PURE__ */ new Map();
|
|
2231
|
-
for (const i of
|
|
2421
|
+
for (const i of statusIssues(root, machine)) {
|
|
2232
2422
|
const choices = i.choices?.map((c) => c.label);
|
|
2233
2423
|
const key = `${i.detail}\0${i.fix?.label ?? ""}\0${choices?.join() ?? ""}`;
|
|
2234
|
-
const issue = issues.get(key) ?? { problems: [], detail: i.detail,
|
|
2424
|
+
const issue = issues.get(key) ?? { problems: [], ids: [], detail: i.detail, fix: i.fix?.label, choices };
|
|
2235
2425
|
issue.problems.push(i.title);
|
|
2426
|
+
issue.ids.push(i.id);
|
|
2236
2427
|
issues.set(key, issue);
|
|
2237
2428
|
}
|
|
2238
2429
|
const usesHere = Object.fromEntries([...uses].filter(([, n]) => n > 0));
|
|
@@ -2241,10 +2432,10 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2241
2432
|
files: topLevel(root),
|
|
2242
2433
|
agents,
|
|
2243
2434
|
skills,
|
|
2244
|
-
|
|
2435
|
+
nested: nested.length > 0 ? nested : void 0,
|
|
2245
2436
|
global,
|
|
2246
2437
|
issues: [...issues.values()],
|
|
2247
|
-
|
|
2438
|
+
usesHere: Object.keys(usesHere).length > 0 ? usesHere : void 0
|
|
2248
2439
|
};
|
|
2249
2440
|
}
|
|
2250
2441
|
function firstSentence(description) {
|
|
@@ -2256,12 +2447,12 @@ function libraryFor(currentProject) {
|
|
|
2256
2447
|
const manifests = visibleProjects().map((p) => ({ name: basename7(p), skills: readManifest(p).skills }));
|
|
2257
2448
|
const here = currentProject ? new Set(projectStatus(currentProject).map((s) => s.name)) : null;
|
|
2258
2449
|
const all = librarySkills();
|
|
2259
|
-
const
|
|
2450
|
+
const describe2 = (s) => {
|
|
2260
2451
|
const projects2 = manifests.filter((m) => s.name in m.skills).map((m) => m.name);
|
|
2261
|
-
return { name: s.name, description: firstSentence(s.description),
|
|
2452
|
+
return { name: s.name, description: firstSentence(s.description), projects: projects2.length > 0 ? projects2 : void 0 };
|
|
2262
2453
|
};
|
|
2263
|
-
if (!here) return { skills: all.map(
|
|
2264
|
-
return { inThisProject: all.filter((s) => here.has(s.name)).map((s) => s.name), skills: all.filter((s) => !here.has(s.name)).map(
|
|
2454
|
+
if (!here) return { skills: all.map(describe2) };
|
|
2455
|
+
return { inThisProject: all.filter((s) => here.has(s.name)).map((s) => s.name), skills: all.filter((s) => !here.has(s.name)).map(describe2) };
|
|
2265
2456
|
}
|
|
2266
2457
|
|
|
2267
2458
|
// src/usage.ts
|
|
@@ -2392,6 +2583,25 @@ function usesByProject(uses, projectOf) {
|
|
|
2392
2583
|
return byProject;
|
|
2393
2584
|
}
|
|
2394
2585
|
|
|
2586
|
+
// src/move.ts
|
|
2587
|
+
import { basename as basename8 } from "path";
|
|
2588
|
+
function moveGlobalSkills(skills, roots, { group, allowTracked = false, label = basename8 } = {}) {
|
|
2589
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2590
|
+
for (const m of skills) byName.set(m.name, [...byName.get(m.name) ?? [], m]);
|
|
2591
|
+
return [...byName].map(([name, copies]) => {
|
|
2592
|
+
if (new Set(copies.map((m) => treeHash(m.path))).size > 1)
|
|
2593
|
+
return { name, moved: false, reason: `your global copies of ${name} differ; nothing moved (keep one first: skilllib tidy --global)`, changes: [] };
|
|
2594
|
+
if (importSkill(copies[0].path).status === "exists")
|
|
2595
|
+
return { name, moved: false, reason: `your library has a different ${name}; nothing moved (update your library from this copy first)`, changes: [] };
|
|
2596
|
+
if (group) recordOrigin(name, `group: ${group}`);
|
|
2597
|
+
const changes = roots.map((root) => [root, addSkill(root, name, { allowTracked })]);
|
|
2598
|
+
const skipped = changes.filter(([, c]) => c.action === "skipped");
|
|
2599
|
+
if (skipped.length) return { name, moved: false, reason: `not added to ${skipped.map(([r, c]) => `${label(r)} (${c.reason})`).join(", ")}, so it still loads globally`, changes };
|
|
2600
|
+
const failed = copies.map((m) => unloadGlobal(m.path, m.links)).find((r) => !r.ok);
|
|
2601
|
+
return failed && !failed.ok ? { name, moved: false, reason: failed.reason, changes } : { name, moved: true, changes };
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2395
2605
|
// src/commands.ts
|
|
2396
2606
|
var USAGE_DAYS = 30;
|
|
2397
2607
|
function parseArgs(argv) {
|
|
@@ -2399,7 +2609,7 @@ function parseArgs(argv) {
|
|
|
2399
2609
|
const days = daysIdx >= 0 ? Number(argv[daysIdx + 1]) : USAGE_DAYS;
|
|
2400
2610
|
const keepIdx = argv.indexOf("--keep");
|
|
2401
2611
|
return {
|
|
2402
|
-
positional: argv.filter((a, i) => !a.startsWith("--") &&
|
|
2612
|
+
positional: argv.filter((a, i) => !a.startsWith("--") && !["--days", "--keep", "--to"].includes(argv[i - 1])),
|
|
2403
2613
|
force: argv.includes("--force"),
|
|
2404
2614
|
all: argv.includes("--all"),
|
|
2405
2615
|
global: argv.includes("--global"),
|
|
@@ -2409,7 +2619,8 @@ function parseArgs(argv) {
|
|
|
2409
2619
|
days: Number.isFinite(days) && days > 0 ? days : USAGE_DAYS,
|
|
2410
2620
|
dryRun: argv.includes("--dry-run"),
|
|
2411
2621
|
allowGit: argv.includes("--allow-git"),
|
|
2412
|
-
...keepIdx >= 0 && argv[keepIdx + 1] ? { keep: argv[keepIdx + 1] } : {}
|
|
2622
|
+
...keepIdx >= 0 && argv[keepIdx + 1] ? { keep: argv[keepIdx + 1] } : {},
|
|
2623
|
+
to: argv.flatMap((a, i) => argv[i - 1] === "--to" && !a.startsWith("--") ? a.split(",").filter(Boolean) : [])
|
|
2413
2624
|
};
|
|
2414
2625
|
}
|
|
2415
2626
|
function colorState(state) {
|
|
@@ -2427,7 +2638,7 @@ function withDirs(root, changes) {
|
|
|
2427
2638
|
}
|
|
2428
2639
|
function printChanges(root, changes, args) {
|
|
2429
2640
|
if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
|
|
2430
|
-
if (args.json) return json(withDirs(root, changes));
|
|
2641
|
+
if (args.json) return json("changes", withDirs(root, changes));
|
|
2431
2642
|
for (const c of changes) {
|
|
2432
2643
|
if (c.blocked?.length) {
|
|
2433
2644
|
warn(`${c.name}: git tracks ${c.blocked.join(", ")} here, so no link was added there. Re-run with --allow-tracked to add it.`);
|
|
@@ -2482,14 +2693,14 @@ async function status(args) {
|
|
|
2482
2693
|
if (args.json) {
|
|
2483
2694
|
const root2 = findProjectRoot();
|
|
2484
2695
|
const usage3 = await usageBySkill(args.days, root2);
|
|
2485
|
-
json(usableHere(root2, new Map([...usage3].map(([name, u]) => [name, u.uses]))));
|
|
2696
|
+
json("status", usableHere(root2, new Map([...usage3].map(([name, u]) => [name, u.uses]))));
|
|
2486
2697
|
if (projectHere()) rememberProjects([root2]);
|
|
2487
2698
|
return;
|
|
2488
2699
|
}
|
|
2489
2700
|
const root = requireProject();
|
|
2490
2701
|
const skills = projectStatus(root);
|
|
2491
2702
|
const nested = nestedSkills(root);
|
|
2492
|
-
info(`${
|
|
2703
|
+
info(`${basename9(root)} ${dim(tildify(root))}
|
|
2493
2704
|
`);
|
|
2494
2705
|
if (skills.length === 0 && nested.length === 0) info("No skills in this project yet. Add one from your library with: skilllib add <name>");
|
|
2495
2706
|
const usage2 = await usageBySkill(args.days, root);
|
|
@@ -2556,7 +2767,7 @@ async function overview(args) {
|
|
|
2556
2767
|
}
|
|
2557
2768
|
async function list(args) {
|
|
2558
2769
|
if (args.json) {
|
|
2559
|
-
return json(libraryFor(projectHere()));
|
|
2770
|
+
return json("list", libraryFor(projectHere()));
|
|
2560
2771
|
}
|
|
2561
2772
|
const skills = librarySkills();
|
|
2562
2773
|
if (skills.length === 0) {
|
|
@@ -2566,7 +2777,7 @@ async function list(args) {
|
|
|
2566
2777
|
const installs = /* @__PURE__ */ new Map();
|
|
2567
2778
|
for (const project of visibleProjects()) {
|
|
2568
2779
|
for (const name of Object.keys(readManifest(project).skills)) {
|
|
2569
|
-
installs.set(name, [...installs.get(name) ?? [],
|
|
2780
|
+
installs.set(name, [...installs.get(name) ?? [], basename9(project)]);
|
|
2570
2781
|
}
|
|
2571
2782
|
}
|
|
2572
2783
|
const usage2 = await usageBySkill(args.days);
|
|
@@ -2600,11 +2811,16 @@ function projects(args) {
|
|
|
2600
2811
|
const roots = visibleProjects();
|
|
2601
2812
|
if (args.json) {
|
|
2602
2813
|
return json(
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2814
|
+
"projects",
|
|
2815
|
+
roots.map(
|
|
2816
|
+
(root) => ({
|
|
2817
|
+
name: basename9(root),
|
|
2818
|
+
path: root,
|
|
2819
|
+
skills: projectStatus(root).map(
|
|
2820
|
+
(s) => ({ name: s.name, managed: s.managed, state: s.state, version: s.version, latest: s.latest })
|
|
2821
|
+
)
|
|
2822
|
+
})
|
|
2823
|
+
)
|
|
2608
2824
|
);
|
|
2609
2825
|
}
|
|
2610
2826
|
if (roots.length === 0) {
|
|
@@ -2617,7 +2833,7 @@ function projects(args) {
|
|
|
2617
2833
|
const managed = skills.filter((s) => s.managed);
|
|
2618
2834
|
const attention = skills.filter((s) => s.state !== "ok" && s.state !== "local only").length;
|
|
2619
2835
|
return {
|
|
2620
|
-
Project:
|
|
2836
|
+
Project: basename9(root),
|
|
2621
2837
|
Path: dim(tildify(root)),
|
|
2622
2838
|
Skills: managed.map((s) => s.name).join(", ") || dim("none"),
|
|
2623
2839
|
"Local only": String(skills.filter((s) => s.state === "local only").length),
|
|
@@ -2651,15 +2867,15 @@ function sync(args) {
|
|
|
2651
2867
|
const changes = syncProject(root, { force: args.force });
|
|
2652
2868
|
if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
|
|
2653
2869
|
if (!args.json) {
|
|
2654
|
-
if (args.all) info(`${
|
|
2870
|
+
if (args.all) info(`${basename9(root)} ${dim(tildify(root))}`);
|
|
2655
2871
|
if (changes.length === 0) {
|
|
2656
2872
|
if (existsSync13(join14(root, MANIFEST_FILE))) success("Installed versions match skilllib.json");
|
|
2657
2873
|
else info(dim("No skilllib.json: nothing to sync"));
|
|
2658
2874
|
} else printChanges(root, changes, args);
|
|
2659
2875
|
}
|
|
2660
|
-
return { project:
|
|
2876
|
+
return { project: basename9(root), path: root, changes: withDirs(root, changes) };
|
|
2661
2877
|
});
|
|
2662
|
-
if (args.json)
|
|
2878
|
+
if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
|
|
2663
2879
|
if (!args.all) rememberProjects(roots);
|
|
2664
2880
|
}
|
|
2665
2881
|
function update(args) {
|
|
@@ -2674,20 +2890,20 @@ function update(args) {
|
|
|
2674
2890
|
}
|
|
2675
2891
|
if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
|
|
2676
2892
|
if (!args.json) {
|
|
2677
|
-
if (args.all) info(`${
|
|
2893
|
+
if (args.all) info(`${basename9(root)} ${dim(tildify(root))}`);
|
|
2678
2894
|
if (changes.length === 0) success("Everything is on the latest version");
|
|
2679
2895
|
else printChanges(root, changes, args);
|
|
2680
2896
|
}
|
|
2681
|
-
return { project:
|
|
2897
|
+
return { project: basename9(root), path: root, changes: withDirs(root, changes) };
|
|
2682
2898
|
});
|
|
2683
|
-
if (args.json)
|
|
2899
|
+
if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
|
|
2684
2900
|
}
|
|
2685
2901
|
function outdated(args) {
|
|
2686
2902
|
const roots = args.all ? visibleProjects() : [requireProject()];
|
|
2687
2903
|
const behind = roots.flatMap(
|
|
2688
|
-
(root) => projectStatus(root).filter((s) => s.managed && s.latest !== null && s.version !== null && s.latest > s.version).map((s) => ({ project:
|
|
2904
|
+
(root) => projectStatus(root).filter((s) => s.managed && s.latest !== null && s.version !== null && s.latest > s.version).map((s) => ({ project: basename9(root), path: root, skill: s.name, installed: s.version, latest: s.latest, state: s.state }))
|
|
2689
2905
|
);
|
|
2690
|
-
if (args.json) return json(behind);
|
|
2906
|
+
if (args.json) return json("outdated", behind);
|
|
2691
2907
|
const rows = behind.map((b) => ({ Project: b.project, Skill: b.skill, Installed: `v${b.installed}`, Latest: `v${b.latest}`, Status: colorState(b.state) }));
|
|
2692
2908
|
if (rows.length === 0) return success("Everything is on the latest version");
|
|
2693
2909
|
table(rows);
|
|
@@ -2719,12 +2935,12 @@ function importSkills(args) {
|
|
|
2719
2935
|
if (skillsDir === join14(root, ".claude", "skills") && isProjectCandidate(root)) {
|
|
2720
2936
|
if (readManifest(root).skills[name]?.hash !== treeHash(dir)) {
|
|
2721
2937
|
const change = addSkill(root, name);
|
|
2722
|
-
if (change.action !== "skipped") info(dim(` ${
|
|
2938
|
+
if (change.action !== "skipped") info(dim(` ${basename9(root)} now depends on ${name} v${change.to}`));
|
|
2723
2939
|
}
|
|
2724
2940
|
rememberProjects([root]);
|
|
2725
2941
|
}
|
|
2726
2942
|
}
|
|
2727
|
-
if (args.json) json(results);
|
|
2943
|
+
if (args.json) json("import", results);
|
|
2728
2944
|
}
|
|
2729
2945
|
function scan(args) {
|
|
2730
2946
|
for (const p of args.positional) addRoot(p);
|
|
@@ -2736,14 +2952,14 @@ function scan(args) {
|
|
|
2736
2952
|
const found = discoverProjects();
|
|
2737
2953
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
2738
2954
|
const skillNames = (root) => [
|
|
2739
|
-
.../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.flatMap((d) => skillDirsIn(join14(root, d))).map((d) =>
|
|
2955
|
+
.../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.flatMap((d) => skillDirsIn(join14(root, d))).map((d) => basename9(d)), ...nestedSkills(root).map((s) => s.name)])
|
|
2740
2956
|
];
|
|
2741
2957
|
const withSkills = found.filter((root) => skillNames(root).length > 0);
|
|
2742
2958
|
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
|
|
2743
2959
|
`);
|
|
2744
2960
|
table(
|
|
2745
2961
|
withSkills.map((root) => ({
|
|
2746
|
-
Project:
|
|
2962
|
+
Project: basename9(root),
|
|
2747
2963
|
Path: dim(tildify(root)),
|
|
2748
2964
|
Skills: skillNames(root).map((n) => library.has(n) ? n : yellow(n)).join(", ")
|
|
2749
2965
|
}))
|
|
@@ -2827,12 +3043,12 @@ function restore(args) {
|
|
|
2827
3043
|
process.exitCode = 1;
|
|
2828
3044
|
}
|
|
2829
3045
|
}
|
|
2830
|
-
async function
|
|
3046
|
+
async function allIssues(args, withUsage = true) {
|
|
2831
3047
|
const projects2 = visibleProjects();
|
|
2832
3048
|
const machine = machineSkills();
|
|
2833
3049
|
const libraryNames = new Set(librarySkills().map((s) => s.name));
|
|
2834
3050
|
const issues = findIssues(projects2, machine, libraryNames);
|
|
2835
|
-
if (enabledHarnesses().includes("claude-code")) {
|
|
3051
|
+
if (withUsage && enabledHarnesses().includes("claude-code")) {
|
|
2836
3052
|
const projectOf = projectOfFactory(projects2);
|
|
2837
3053
|
const uses = await scanUsage(args.days);
|
|
2838
3054
|
const inProject = usesByProject(uses, projectOf);
|
|
@@ -2845,6 +3061,10 @@ async function doctor(args) {
|
|
|
2845
3061
|
})
|
|
2846
3062
|
);
|
|
2847
3063
|
}
|
|
3064
|
+
return issues;
|
|
3065
|
+
}
|
|
3066
|
+
async function doctor(args) {
|
|
3067
|
+
const issues = await allIssues(args);
|
|
2848
3068
|
const fixed = (fix) => {
|
|
2849
3069
|
const r = runFix(fix);
|
|
2850
3070
|
if (!r.ok) process.exitCode = 1;
|
|
@@ -2852,34 +3072,65 @@ async function doctor(args) {
|
|
|
2852
3072
|
};
|
|
2853
3073
|
if (args.json) {
|
|
2854
3074
|
return json(
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
3075
|
+
"doctor",
|
|
3076
|
+
issues.map(
|
|
3077
|
+
({ id, severity, title, detail, fix, choices }) => ({
|
|
3078
|
+
id,
|
|
3079
|
+
severity,
|
|
3080
|
+
title,
|
|
3081
|
+
detail,
|
|
3082
|
+
fix: fix?.label,
|
|
3083
|
+
choices: choices?.map((c) => c.label),
|
|
3084
|
+
fixed: args.fix && fix ? fixed(fix) : void 0
|
|
3085
|
+
})
|
|
3086
|
+
)
|
|
2861
3087
|
);
|
|
2862
3088
|
}
|
|
2863
3089
|
if (issues.length === 0) return success("Everything looks good");
|
|
2864
3090
|
for (const issue of issues) {
|
|
2865
3091
|
info(`${issue.severity === "problem" ? red("\u25CF") : yellow("\u25C6")} ${issue.title}`);
|
|
2866
3092
|
info(dim(` ${issue.detail}${issue.fix ? ` \u2192 ${issue.fix.label}` : ""}`));
|
|
2867
|
-
if (issue.choices) info(dim(`
|
|
3093
|
+
if (issue.choices) info(dim(` your call: ${issue.choices.map((c, i) => `${i + 1}) ${c.label}`).join(" ")} \u2192 skilllib apply ${issue.id} <n>`));
|
|
2868
3094
|
if (args.fix && issue.fix) {
|
|
2869
3095
|
const r = fixed(issue.fix);
|
|
2870
3096
|
info(` ${r.ok ? green("\u2713") : red("\u2717")} ${r.message}`);
|
|
2871
3097
|
}
|
|
2872
3098
|
}
|
|
2873
|
-
if (!args.fix && issues.some((i) => i.fix)) info(dim("\nRun `skilllib doctor --fix` to apply the fixes, or
|
|
3099
|
+
if (!args.fix && issues.some((i) => i.fix)) info(dim("\nRun `skilllib doctor --fix` to apply the fixes, or one at a time with `skilllib apply <id>`."));
|
|
3100
|
+
}
|
|
3101
|
+
async function apply(args) {
|
|
3102
|
+
const [id, pick] = args.positional;
|
|
3103
|
+
if (!id) throw new Error("Usage: skilllib apply <issue-id> [choice] (ids and choices: skilllib status --json or doctor --json)");
|
|
3104
|
+
const found = [...statusIssues(findProjectRoot()), ...await allIssues(args, id.startsWith("unused:"))].filter((i) => i.id === id);
|
|
3105
|
+
if (!found.length) throw new Error(`No issue ${id} right now (it may be fixed already; for an unused:\u2026 issue, pass doctor's --days). See skilllib status --json or doctor --json`);
|
|
3106
|
+
const optionsOf = (i) => i.choices ?? (i.fix ? [i.fix] : []);
|
|
3107
|
+
const labels = (i) => optionsOf(i).map((c) => c.label).join("\0");
|
|
3108
|
+
const byLabel = pick === void 0 ? void 0 : found.find((i) => optionsOf(i).some((c) => c.label.toLowerCase() === pick.toLowerCase()));
|
|
3109
|
+
if (!byLabel && found.some((i) => labels(i) !== labels(found[0]))) {
|
|
3110
|
+
const all = [...new Set(found.flatMap((i) => optionsOf(i).map((c) => c.label)))];
|
|
3111
|
+
throw new Error(`${found[0].title}: its choices differ between this repo and all your repos; pick one by label: ${all.map((l) => `"${l}"`).join(" ")}`);
|
|
3112
|
+
}
|
|
3113
|
+
const issue = byLabel ?? found[0];
|
|
3114
|
+
const options = optionsOf(issue);
|
|
3115
|
+
if (!options.length) throw new Error(`${issue.title}: nothing to apply. ${issue.detail}`);
|
|
3116
|
+
const listed = options.map((c, i) => `${i + 1}) ${c.label}`).join(" ");
|
|
3117
|
+
if (pick === void 0 && options.length > 1) throw new Error(`${issue.title}: pick one: ${listed}`);
|
|
3118
|
+
const choice = pick === void 0 ? options[0] : options.find((c) => c.label.toLowerCase() === pick.toLowerCase()) ?? options[Number(pick) - 1];
|
|
3119
|
+
if (!choice) throw new Error(`${issue.title}: no choice ${pick}. Pick one: ${listed}`);
|
|
3120
|
+
const r = runFix(choice);
|
|
3121
|
+
if (!r.ok) process.exitCode = 1;
|
|
3122
|
+
if (args.json) return json("apply", { id, applied: choice.label, ...r });
|
|
3123
|
+
if (r.ok) success(r.message);
|
|
3124
|
+
else error(r.message);
|
|
2874
3125
|
}
|
|
2875
3126
|
async function usage(args) {
|
|
2876
3127
|
const summaries = summarize(await scanUsage(args.days), projectOfFactory(knownProjects()));
|
|
2877
3128
|
if (args.json) {
|
|
2878
3129
|
const library2 = new Set(librarySkills().map((s) => s.name));
|
|
2879
|
-
return json({
|
|
3130
|
+
return json("usage", {
|
|
2880
3131
|
usageDays: args.days,
|
|
2881
3132
|
source: "Claude Code transcripts",
|
|
2882
|
-
skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) =>
|
|
3133
|
+
skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) => basename9(p)), inLibrary: library2.has(s.skill) })),
|
|
2883
3134
|
unusedLibrarySkills: librarySkills().filter((l) => !summaries.some((u) => u.skill === l.name)).map((l) => l.name)
|
|
2884
3135
|
});
|
|
2885
3136
|
}
|
|
@@ -2894,7 +3145,7 @@ async function usage(args) {
|
|
|
2894
3145
|
summaries.map((s) => ({
|
|
2895
3146
|
Skill: s.skill,
|
|
2896
3147
|
Uses: String(s.uses),
|
|
2897
|
-
Projects: [...s.projects].map((p) =>
|
|
3148
|
+
Projects: [...s.projects].map((p) => basename9(p)).join(", ") || dim("-"),
|
|
2898
3149
|
"Last used": lastUsed(s),
|
|
2899
3150
|
Library: library.has(s.skill) ? green("yes") : dim("no")
|
|
2900
3151
|
}))
|
|
@@ -2914,13 +3165,13 @@ function show(args) {
|
|
|
2914
3165
|
}
|
|
2915
3166
|
const { description } = readSkillInfo(dir);
|
|
2916
3167
|
if (args.json) {
|
|
2917
|
-
return json({
|
|
3168
|
+
return json("show", {
|
|
2918
3169
|
name,
|
|
2919
3170
|
description,
|
|
2920
3171
|
path: dir,
|
|
2921
3172
|
latest: latestVersion(name)?.version ?? null,
|
|
2922
3173
|
origin: libraryOrigins()[name] ?? "",
|
|
2923
|
-
projects: knownProjects().filter((p) => name in readManifest(p).skills).map((p) =>
|
|
3174
|
+
projects: knownProjects().filter((p) => name in readManifest(p).skills).map((p) => basename9(p)),
|
|
2924
3175
|
skillMd: readFileSync9(join14(dir, "SKILL.md"), "utf-8")
|
|
2925
3176
|
});
|
|
2926
3177
|
}
|
|
@@ -2954,7 +3205,7 @@ function link(args) {
|
|
|
2954
3205
|
for (const root of roots) {
|
|
2955
3206
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
2956
3207
|
const res = linkAll(root);
|
|
2957
|
-
const label = args.all ? `${
|
|
3208
|
+
const label = args.all ? `${basename9(root)}: ` : "";
|
|
2958
3209
|
if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
|
|
2959
3210
|
else if (!args.all && !res.blocked.length && !res.uncommitted.length) success("Every skill here is already usable by all your agents");
|
|
2960
3211
|
if (res.blocked.length) {
|
|
@@ -2970,6 +3221,7 @@ function link(args) {
|
|
|
2970
3221
|
function globalCommand(args) {
|
|
2971
3222
|
const [action, ...names] = args.positional;
|
|
2972
3223
|
const yours = machineSkills().filter((m) => m.movable && !m.broken);
|
|
3224
|
+
if (action === "move") return moveGlobal(args, names, yours);
|
|
2973
3225
|
if (action === "keep" || action === "unkeep") {
|
|
2974
3226
|
const unknown = names.filter((n) => !yours.some((m) => m.name === n));
|
|
2975
3227
|
if (!names.length || unknown.length) {
|
|
@@ -2979,7 +3231,7 @@ function globalCommand(args) {
|
|
|
2979
3231
|
setKeepGlobal(names, action === "keep");
|
|
2980
3232
|
success(action === "keep" ? `Keeping ${names.join(", ")} global on purpose` : `${names.join(", ")} will show up in cleanup again`);
|
|
2981
3233
|
} else if (action) {
|
|
2982
|
-
error("Usage: skilllib global [keep|unkeep <name>...]");
|
|
3234
|
+
error("Usage: skilllib global [keep|unkeep <name>...] | skilllib global move <name>... --to <repo>...");
|
|
2983
3235
|
process.exit(1);
|
|
2984
3236
|
}
|
|
2985
3237
|
const kept = keptGlobal();
|
|
@@ -2989,6 +3241,46 @@ function globalCommand(args) {
|
|
|
2989
3241
|
if (unreviewed) info(dim(`
|
|
2990
3242
|
${unreviewed} not reviewed. Keep one global on purpose: skilllib global keep <name>`));
|
|
2991
3243
|
}
|
|
3244
|
+
function repoRoot(repo) {
|
|
3245
|
+
const looksLikePath = /[\\/]/.test(repo) || repo.startsWith(".") || repo.startsWith("~");
|
|
3246
|
+
const named = looksLikePath ? [] : visibleProjects().filter((p) => basename9(p) === repo);
|
|
3247
|
+
if (named.length === 1) return named[0];
|
|
3248
|
+
if (named.length > 1) throw new Error(`Several projects are named ${repo}: ${named.map(tildify).join(", ")}. Pass its path.`);
|
|
3249
|
+
const path = expandHome(repo);
|
|
3250
|
+
if (!existsSync13(path)) throw new Error(`No project ${repo} (pass its path, or see skilllib projects)`);
|
|
3251
|
+
const root = projectHere(path);
|
|
3252
|
+
if (root !== path) throw new Error(root ? `${tildify(path)} is inside ${tildify(root)}: pass the repo's top folder` : `${tildify(path)} isn't in a git repo`);
|
|
3253
|
+
return root;
|
|
3254
|
+
}
|
|
3255
|
+
function moveGlobal(args, names, yours) {
|
|
3256
|
+
if (!names.length || !args.to.length) throw new Error("Usage: skilllib global move <name>... --to <repo>[,<repo>...]");
|
|
3257
|
+
const unknown = names.filter((n) => !yours.some((m) => m.name === n));
|
|
3258
|
+
if (unknown.length) throw new Error(`Not one of your global skills: ${unknown.join(", ")} (see skilllib global)`);
|
|
3259
|
+
const roots = [...new Set(args.to.map(repoRoot))];
|
|
3260
|
+
const skills = yours.filter((m) => names.includes(m.name));
|
|
3261
|
+
const moves = moveGlobalSkills(skills, roots, { allowTracked: args.allowTracked });
|
|
3262
|
+
if (moves.some((m) => !m.moved)) process.exitCode = 1;
|
|
3263
|
+
if (args.json) {
|
|
3264
|
+
return json(
|
|
3265
|
+
"global move",
|
|
3266
|
+
moves.map(({ name, moved, reason, changes }) => ({
|
|
3267
|
+
skill: name,
|
|
3268
|
+
moved,
|
|
3269
|
+
...reason ? { reason } : {},
|
|
3270
|
+
projects: changes.map(([root, c]) => ({ project: basename9(root), path: root, change: withDirs(root, [c])[0] }))
|
|
3271
|
+
}))
|
|
3272
|
+
);
|
|
3273
|
+
}
|
|
3274
|
+
for (const m of moves) {
|
|
3275
|
+
if (m.moved) success(`${m.name} now loads only in ${m.changes.map(([r]) => basename9(r)).join(", ")} (the global copy is in skilllib restore)`);
|
|
3276
|
+
else error(`${m.name}: ${m.reason}`);
|
|
3277
|
+
for (const [root, c] of m.changes) {
|
|
3278
|
+
if (c.blocked?.length) warn(` ${basename9(root)}: git tracks ${c.blocked.join(", ")}, so no link there. Re-run with --allow-tracked to add it.`);
|
|
3279
|
+
if (c.uncommitted?.length) warn(` ${basename9(root)}: no link in ${c.uncommitted.join(", ")}: git tracks it but not the skill's copy, so the link would break for teammates. Commit the copy, then run skilllib link --allow-tracked.`);
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
if (moves.some((m) => m.moved)) info(dim("Commit skilllib.json and the skill folders in each repo."));
|
|
3283
|
+
}
|
|
2992
3284
|
function tidy(args) {
|
|
2993
3285
|
const names = args.positional;
|
|
2994
3286
|
const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve7(base, args.keep) : expandHome(args.keep)])) : {};
|
|
@@ -3001,13 +3293,13 @@ function tidy(args) {
|
|
|
3001
3293
|
for (const name of names) {
|
|
3002
3294
|
const where = targets.map((root) => ({ root, ...foldersOf(root, name) })).filter((w) => w.all.length);
|
|
3003
3295
|
if (!where.length) {
|
|
3004
|
-
problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${
|
|
3296
|
+
problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${basename9(targets[0])}`}`);
|
|
3005
3297
|
continue;
|
|
3006
3298
|
}
|
|
3007
3299
|
if (!args.keep) continue;
|
|
3008
3300
|
for (const { root, real } of where) {
|
|
3009
3301
|
if (real.includes(keepFor(root)[name])) continue;
|
|
3010
|
-
const label = root ? `${
|
|
3302
|
+
const label = root ? `${basename9(root)} \xB7 ` : "";
|
|
3011
3303
|
problems.push(
|
|
3012
3304
|
`${label}${name}: no copy in ${args.keep}; ${real.length ? `pick one of: ${real.map((d) => folderLabel(root, d)).join(", ")}` : "it has no real copy to keep"}`
|
|
3013
3305
|
);
|
|
@@ -3018,7 +3310,7 @@ function tidy(args) {
|
|
|
3018
3310
|
process.exit(1);
|
|
3019
3311
|
}
|
|
3020
3312
|
const reports = targets.map(
|
|
3021
|
-
(root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label:
|
|
3313
|
+
(root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label: basename9(root), report: planProjectTidy(root, { keep: keepFor(root) }) }
|
|
3022
3314
|
);
|
|
3023
3315
|
let held = 0;
|
|
3024
3316
|
let changed = 0;
|
|
@@ -3058,7 +3350,7 @@ function agentSkill(args) {
|
|
|
3058
3350
|
error(`Can't install the skilllib skill: ${r.reason}`);
|
|
3059
3351
|
process.exit(1);
|
|
3060
3352
|
}
|
|
3061
|
-
if (args.json) return json({ state: agentSkillState(), dirs: r.dirs });
|
|
3353
|
+
if (args.json) return json("agent-skill", { state: agentSkillState(), dirs: r.dirs });
|
|
3062
3354
|
success(`Installed in ${r.dirs.map((d) => tildify(d)).join(" and ")}`);
|
|
3063
3355
|
if (!onPath("skilllib")) warn("`skilllib` isn't on your PATH, so agents can't run it. Install it: npm install -g skilllib");
|
|
3064
3356
|
info(dim("New agent sessions can now answer questions like \u201Cwhich skills can you use here?\u201D"));
|
|
@@ -3066,11 +3358,11 @@ function agentSkill(args) {
|
|
|
3066
3358
|
}
|
|
3067
3359
|
if (action === "remove" || action === "rm") {
|
|
3068
3360
|
const removed = removeAgentSkill();
|
|
3069
|
-
if (args.json) return json({ state: agentSkillState(), removed });
|
|
3361
|
+
if (args.json) return json("agent-skill", { state: agentSkillState(), removed });
|
|
3070
3362
|
return removed.length ? success(`Removed from ${removed.map((d) => tildify(d)).join(" and ")}`) : info("The skilllib skill isn't installed.");
|
|
3071
3363
|
}
|
|
3072
3364
|
const state = agentSkillState();
|
|
3073
|
-
if (args.json) return json({ state, dirs: agentSkillDirs() });
|
|
3365
|
+
if (args.json) return json("agent-skill", { state, dirs: agentSkillDirs() });
|
|
3074
3366
|
if (state === "installed") success(`Your agents can use skilllib ${dim(`(${agentSkillDirs().map((d) => tildify(d)).join(", ")})`)}`);
|
|
3075
3367
|
else if (state === "outdated") warn("The skilllib skill is out of date. Run `skilllib agent-skill install` to update it.");
|
|
3076
3368
|
else info("Your agents don't know about skilllib yet. Run `skilllib agent-skill install` so you can ask them about your skills.");
|
|
@@ -3102,7 +3394,6 @@ export {
|
|
|
3102
3394
|
allowTrackedLinks,
|
|
3103
3395
|
keptGlobal,
|
|
3104
3396
|
setKeepGlobal,
|
|
3105
|
-
setPluginEnabled,
|
|
3106
3397
|
machineSkills,
|
|
3107
3398
|
skillsLoadedIn,
|
|
3108
3399
|
libraryOrigins,
|
|
@@ -3130,9 +3421,14 @@ export {
|
|
|
3130
3421
|
gitInfo,
|
|
3131
3422
|
relativeTo,
|
|
3132
3423
|
claudePlugins,
|
|
3424
|
+
installedPlugins,
|
|
3425
|
+
cursorPlugins,
|
|
3426
|
+
forgetClaudeBinary,
|
|
3133
3427
|
claudeBinary,
|
|
3134
3428
|
turnOffIn,
|
|
3135
|
-
|
|
3429
|
+
setPluginOn,
|
|
3430
|
+
uninstallPlugin,
|
|
3431
|
+
updatePlugin,
|
|
3136
3432
|
pluginBackups,
|
|
3137
3433
|
restorePlugin,
|
|
3138
3434
|
cursorPluginSkills,
|
|
@@ -3148,6 +3444,7 @@ export {
|
|
|
3148
3444
|
copyLabel,
|
|
3149
3445
|
addedAt,
|
|
3150
3446
|
scanUsage,
|
|
3447
|
+
moveGlobalSkills,
|
|
3151
3448
|
parseArgs,
|
|
3152
3449
|
projectOfFactory,
|
|
3153
3450
|
status,
|
|
@@ -3166,6 +3463,7 @@ export {
|
|
|
3166
3463
|
newSkill,
|
|
3167
3464
|
restore,
|
|
3168
3465
|
doctor,
|
|
3466
|
+
apply,
|
|
3169
3467
|
usage,
|
|
3170
3468
|
show,
|
|
3171
3469
|
harnessesCommand,
|