skilllib 1.6.0 → 1.10.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 +93 -38
- package/dist/{chunk-PHRUPZGX.js → chunk-Z2F5W3DY.js} +852 -325
- package/dist/index.js +9 -8
- package/dist/json.d.cts +200 -0
- package/dist/json.d.ts +200 -0
- package/dist/{tui-WYVSDGHD.js → tui-7CMGG6HX.js} +715 -235
- package/package.json +16 -3
- package/skills/skilllib/SKILL.md +5 -4
|
@@ -1,11 +1,53 @@
|
|
|
1
1
|
// src/output.ts
|
|
2
|
-
import { sep } from "path";
|
|
2
|
+
import { resolve as resolve2, sep } from "path";
|
|
3
|
+
|
|
4
|
+
// src/paths.ts
|
|
5
|
+
import { realpathSync } from "fs";
|
|
3
6
|
import { homedir } from "os";
|
|
7
|
+
import { join, resolve } from "path";
|
|
8
|
+
function userHome() {
|
|
9
|
+
return process.env.HOME || homedir();
|
|
10
|
+
}
|
|
11
|
+
function realPath(path) {
|
|
12
|
+
try {
|
|
13
|
+
return realpathSync(resolve(path));
|
|
14
|
+
} catch {
|
|
15
|
+
return resolve(path);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function skilllibHome() {
|
|
19
|
+
return process.env.SKILLLIB_HOME ?? join(userHome(), ".skilllib");
|
|
20
|
+
}
|
|
21
|
+
function libraryDir() {
|
|
22
|
+
return join(skilllibHome(), "library");
|
|
23
|
+
}
|
|
24
|
+
function claudeDir() {
|
|
25
|
+
return process.env.CLAUDE_CONFIG_DIR ?? join(userHome(), ".claude");
|
|
26
|
+
}
|
|
27
|
+
function grokDir() {
|
|
28
|
+
return process.env.GROK_HOME?.trim() || join(userHome(), ".grok");
|
|
29
|
+
}
|
|
30
|
+
function codexHome() {
|
|
31
|
+
return process.env.CODEX_HOME?.trim() || join(userHome(), ".codex");
|
|
32
|
+
}
|
|
33
|
+
function codexSystemDir() {
|
|
34
|
+
return process.env.SKILLLIB_CODEX_SYSTEM_DIR ?? "/etc/codex/skills";
|
|
35
|
+
}
|
|
36
|
+
var PROJECT_SKILLS_DIR = ".claude/skills";
|
|
37
|
+
var AGENTS_SKILLS_DIR = ".agents/skills";
|
|
38
|
+
var GROK_SKILLS_DIR = ".grok/skills";
|
|
39
|
+
var MANIFEST_FILE = "skilllib.json";
|
|
40
|
+
function isRepoSkillLocation(location) {
|
|
41
|
+
return location === AGENTS_SKILLS_DIR || location === GROK_SKILLS_DIR;
|
|
42
|
+
}
|
|
43
|
+
var OWN_SKILL_MARKER = ".managed-by-skilllib";
|
|
44
|
+
|
|
45
|
+
// src/output.ts
|
|
4
46
|
var jsonMode = false;
|
|
5
47
|
function setJsonMode(on) {
|
|
6
48
|
jsonMode = on;
|
|
7
49
|
}
|
|
8
|
-
function json(data) {
|
|
50
|
+
function json(output, data) {
|
|
9
51
|
process.stdout.write(JSON.stringify(data) + "\n");
|
|
10
52
|
}
|
|
11
53
|
var color = (code) => (s) => process.stdout.isTTY && !jsonMode ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
@@ -19,8 +61,9 @@ var success = (msg) => out(`${green("\u2713")} ${msg}`);
|
|
|
19
61
|
var warn = (msg) => out(`${yellow("!")} ${msg}`);
|
|
20
62
|
var error = (msg) => console.error(`${red("\u2717")} ${msg}`);
|
|
21
63
|
function tildify(path) {
|
|
22
|
-
const home2
|
|
23
|
-
|
|
64
|
+
for (const home2 of /* @__PURE__ */ new Set([resolve2(userHome()), realPath(userHome())]))
|
|
65
|
+
if (path === home2 || path.startsWith(home2 + sep)) return ("~" + path.slice(home2.length)).replace(/\\/g, "/");
|
|
66
|
+
return path;
|
|
24
67
|
}
|
|
25
68
|
function truncate(s, max) {
|
|
26
69
|
const line = s.replace(/\s+/g, " ");
|
|
@@ -41,35 +84,9 @@ import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirS
|
|
|
41
84
|
import { join as join9 } from "path";
|
|
42
85
|
|
|
43
86
|
// src/config.ts
|
|
44
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
45
|
-
import { homedir as homedir4 } from "os";
|
|
46
|
-
import { join as join6, resolve as resolve2 } from "path";
|
|
47
|
-
|
|
48
|
-
// src/paths.ts
|
|
87
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
49
88
|
import { homedir as homedir2 } from "os";
|
|
50
|
-
import { join } from "path";
|
|
51
|
-
function userHome() {
|
|
52
|
-
return process.env.HOME || homedir2();
|
|
53
|
-
}
|
|
54
|
-
function skilllibHome() {
|
|
55
|
-
return process.env.SKILLLIB_HOME ?? join(userHome(), ".skilllib");
|
|
56
|
-
}
|
|
57
|
-
function libraryDir() {
|
|
58
|
-
return join(skilllibHome(), "library");
|
|
59
|
-
}
|
|
60
|
-
function claudeDir() {
|
|
61
|
-
return process.env.CLAUDE_CONFIG_DIR ?? join(userHome(), ".claude");
|
|
62
|
-
}
|
|
63
|
-
function codexHome() {
|
|
64
|
-
return process.env.CODEX_HOME?.trim() || join(userHome(), ".codex");
|
|
65
|
-
}
|
|
66
|
-
function codexSystemDir() {
|
|
67
|
-
return process.env.SKILLLIB_CODEX_SYSTEM_DIR ?? "/etc/codex/skills";
|
|
68
|
-
}
|
|
69
|
-
var PROJECT_SKILLS_DIR = ".claude/skills";
|
|
70
|
-
var AGENTS_SKILLS_DIR = ".agents/skills";
|
|
71
|
-
var MANIFEST_FILE = "skilllib.json";
|
|
72
|
-
var OWN_SKILL_MARKER = ".managed-by-skilllib";
|
|
89
|
+
import { join as join6, resolve as resolve4 } from "path";
|
|
73
90
|
|
|
74
91
|
// src/harnesses.ts
|
|
75
92
|
import { existsSync } from "fs";
|
|
@@ -93,7 +110,7 @@ var HARNESSES = [
|
|
|
93
110
|
icon: "\u2B21",
|
|
94
111
|
color: "#E8E8E8",
|
|
95
112
|
// The first folder is where links for Cursor go (see linkEverywhere). Cursor also reads .grok/skills;
|
|
96
|
-
// skilllib leaves that folder
|
|
113
|
+
// skilllib leaves that folder alone (see Grok). A .grok-only skill stays invisible to Cursor here.
|
|
97
114
|
projectDirs: [".agents/skills", ".cursor/skills", ".claude/skills", ".codex/skills"],
|
|
98
115
|
// Cursor ignores CLAUDE_CONFIG_DIR and CODEX_HOME: it always reads these under your home folder.
|
|
99
116
|
globalDirs: () => [".agents", ".cursor", ".claude", ".codex"].map((d) => join2(home(), d, "skills")),
|
|
@@ -118,6 +135,19 @@ var HARNESSES = [
|
|
|
118
135
|
projectDirs: [".agents/skills"],
|
|
119
136
|
globalDirs: () => [join2(home(), ".agents", "skills")],
|
|
120
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")
|
|
121
151
|
}
|
|
122
152
|
];
|
|
123
153
|
function onPath(bin) {
|
|
@@ -155,8 +185,7 @@ function installDirs(enabled, keep) {
|
|
|
155
185
|
|
|
156
186
|
// src/project.ts
|
|
157
187
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
158
|
-
import {
|
|
159
|
-
import { dirname, join as join5, resolve } from "path";
|
|
188
|
+
import { dirname, join as join5, resolve as resolve3 } from "path";
|
|
160
189
|
|
|
161
190
|
// src/versions.ts
|
|
162
191
|
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
@@ -164,7 +193,7 @@ import { join as join4 } from "path";
|
|
|
164
193
|
|
|
165
194
|
// src/skills.ts
|
|
166
195
|
import { createHash } from "crypto";
|
|
167
|
-
import { cpSync, existsSync as existsSync2, readdirSync, readFileSync, realpathSync, rmSync, statSync } from "fs";
|
|
196
|
+
import { cpSync, existsSync as existsSync2, readdirSync, readFileSync, realpathSync as realpathSync2, rmSync, statSync } from "fs";
|
|
168
197
|
import { basename, join as join3, relative, sep as sep2 } from "path";
|
|
169
198
|
import { parse as parseYaml } from "yaml";
|
|
170
199
|
var IGNORED = /* @__PURE__ */ new Set([".DS_Store", ".git", "node_modules"]);
|
|
@@ -204,7 +233,7 @@ function treeHash(dir) {
|
|
|
204
233
|
}
|
|
205
234
|
function copySkill(from, to) {
|
|
206
235
|
rmSync(to, { recursive: true, force: true });
|
|
207
|
-
cpSync(
|
|
236
|
+
cpSync(realpathSync2(from), to, { recursive: true, dereference: true, filter: (src) => !IGNORED.has(basename(src)) });
|
|
208
237
|
}
|
|
209
238
|
function skillDirsIn(dir) {
|
|
210
239
|
if (!existsSync2(dir)) return [];
|
|
@@ -263,19 +292,17 @@ function getVersion(name, version) {
|
|
|
263
292
|
|
|
264
293
|
// src/project.ts
|
|
265
294
|
function findProjectRoot(from = process.cwd()) {
|
|
266
|
-
let dir =
|
|
295
|
+
let dir = resolve3(from);
|
|
267
296
|
while (true) {
|
|
268
297
|
if (existsSync4(join5(dir, MANIFEST_FILE)) || existsSync4(join5(dir, ".git"))) return dir;
|
|
269
298
|
const parent = dirname(dir);
|
|
270
|
-
if (parent === dir) return
|
|
299
|
+
if (parent === dir) return resolve3(from);
|
|
271
300
|
dir = parent;
|
|
272
301
|
}
|
|
273
302
|
}
|
|
274
|
-
function
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
function agentsSkillsDir(root) {
|
|
278
|
-
return join5(root, AGENTS_SKILLS_DIR);
|
|
303
|
+
function projectHere(from = process.cwd()) {
|
|
304
|
+
const root = findProjectRoot(from);
|
|
305
|
+
return isProjectCandidate(root) && (existsSync4(join5(root, ".git")) || existsSync4(join5(root, MANIFEST_FILE))) ? root : null;
|
|
279
306
|
}
|
|
280
307
|
function readManifest(root) {
|
|
281
308
|
const path = join5(root, MANIFEST_FILE);
|
|
@@ -315,14 +342,23 @@ function knownProjects() {
|
|
|
315
342
|
const path = projectsFile();
|
|
316
343
|
if (!existsSync4(path)) return [];
|
|
317
344
|
const list2 = JSON.parse(readFileSync3(path, "utf-8"));
|
|
318
|
-
return list2.filter((p) => existsSync4(p) && isProjectCandidate(p));
|
|
345
|
+
return onePerFolder(list2.filter((p) => existsSync4(p) && isProjectCandidate(p)));
|
|
346
|
+
}
|
|
347
|
+
function onePerFolder(paths) {
|
|
348
|
+
const byReal = /* @__PURE__ */ new Map();
|
|
349
|
+
for (const p of paths) {
|
|
350
|
+
const real = realPath(p);
|
|
351
|
+
const seen = byReal.get(real);
|
|
352
|
+
if (seen === void 0 || seen === real && p !== real) byReal.set(real, p);
|
|
353
|
+
}
|
|
354
|
+
return [...new Set(byReal.values())];
|
|
319
355
|
}
|
|
320
356
|
function isProjectCandidate(root) {
|
|
321
|
-
return
|
|
357
|
+
return realPath(root) !== realPath(userHome());
|
|
322
358
|
}
|
|
323
359
|
function rememberProjects(roots) {
|
|
324
|
-
const incoming = roots.map((r) =>
|
|
325
|
-
const all =
|
|
360
|
+
const incoming = roots.map((r) => resolve3(r)).filter(isProjectCandidate);
|
|
361
|
+
const all = onePerFolder([...knownProjects(), ...incoming]).sort();
|
|
326
362
|
mkdirSync2(skilllibHome(), { recursive: true });
|
|
327
363
|
writeFileSync2(projectsFile(), JSON.stringify(all, null, 2) + "\n");
|
|
328
364
|
}
|
|
@@ -344,10 +380,12 @@ function writeConfig(config) {
|
|
|
344
380
|
writeFileSync3(configFile(), JSON.stringify(config, null, 2) + "\n");
|
|
345
381
|
}
|
|
346
382
|
function expandHome(path) {
|
|
347
|
-
return
|
|
383
|
+
return resolve4(path.replace(/^~(?=$|\/)/, homedir2()));
|
|
348
384
|
}
|
|
349
385
|
function addRoot(path) {
|
|
350
386
|
const root = expandHome(path);
|
|
387
|
+
if (!existsSync5(root)) throw new Error(`${tildify(root)} doesn't exist`);
|
|
388
|
+
if (!statSync2(root).isDirectory()) throw new Error(`${tildify(root)} isn't a folder`);
|
|
351
389
|
const config = readConfig();
|
|
352
390
|
if (!config.roots.includes(root)) writeConfig({ ...config, roots: [...config.roots, root].sort() });
|
|
353
391
|
return root;
|
|
@@ -361,8 +399,10 @@ function removeRoot(path) {
|
|
|
361
399
|
}
|
|
362
400
|
function setHidden(project, hidden) {
|
|
363
401
|
const config = readConfig();
|
|
402
|
+
if (config.hidden.includes(project) === hidden) return false;
|
|
364
403
|
const rest = config.hidden.filter((h) => h !== project);
|
|
365
404
|
writeConfig({ ...config, hidden: hidden ? [...rest, project].sort() : rest });
|
|
405
|
+
return true;
|
|
366
406
|
}
|
|
367
407
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "vendor", "target", "Library", "Applications", "Pictures", "Music", "Movies"]);
|
|
368
408
|
function findProjects(dir, depth = 4) {
|
|
@@ -386,8 +426,8 @@ function discoverProjects() {
|
|
|
386
426
|
return visibleProjects();
|
|
387
427
|
}
|
|
388
428
|
function visibleProjects() {
|
|
389
|
-
const hidden = new Set(readConfig().hidden);
|
|
390
|
-
return knownProjects().filter((p) => !hidden.has(p));
|
|
429
|
+
const hidden = new Set(readConfig().hidden.map(realPath));
|
|
430
|
+
return knownProjects().filter((p) => !hidden.has(realPath(p)));
|
|
391
431
|
}
|
|
392
432
|
function enabledHarnesses() {
|
|
393
433
|
return readConfig().harnesses ?? HARNESSES.filter((h) => h.installed()).map((h) => h.id);
|
|
@@ -418,12 +458,12 @@ function setKeepGlobal(names, keep) {
|
|
|
418
458
|
|
|
419
459
|
// src/library.ts
|
|
420
460
|
import { execFileSync } from "child_process";
|
|
421
|
-
import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as
|
|
461
|
+
import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync4, renameSync, rmSync as rmSync2, symlinkSync, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
|
|
422
462
|
import { basename as basename3, dirname as dirname2, join as join8, relative as relative2, sep as sep3 } from "path";
|
|
423
463
|
|
|
424
464
|
// src/sources.ts
|
|
425
|
-
import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as
|
|
426
|
-
import { basename as basename2, join as join7, resolve as
|
|
465
|
+
import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
466
|
+
import { basename as basename2, join as join7, resolve as resolve5 } from "path";
|
|
427
467
|
function readJson(path) {
|
|
428
468
|
try {
|
|
429
469
|
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
@@ -432,24 +472,36 @@ function readJson(path) {
|
|
|
432
472
|
}
|
|
433
473
|
}
|
|
434
474
|
function skillsShLock() {
|
|
435
|
-
return readJson(join7(
|
|
475
|
+
return readJson(join7(userHome(), ".agents", ".skill-lock.json"))?.skills ?? {};
|
|
436
476
|
}
|
|
437
477
|
function projectSkillsLock(root) {
|
|
438
478
|
return readJson(join7(root, "skills-lock.json"))?.skills ?? {};
|
|
439
479
|
}
|
|
440
480
|
function realpathOrNull(path) {
|
|
441
481
|
try {
|
|
442
|
-
return
|
|
482
|
+
return realpathSync3(path);
|
|
443
483
|
} catch {
|
|
444
484
|
return null;
|
|
445
485
|
}
|
|
446
486
|
}
|
|
447
487
|
function harnessesReading(dir, enabled) {
|
|
448
|
-
|
|
488
|
+
const real = realPath(dir);
|
|
489
|
+
return enabled.filter((id) => HARNESSES.find((h) => h.id === id)?.globalDirs().some((d) => realPath(d) === real));
|
|
449
490
|
}
|
|
450
491
|
var systemDirs = () => [codexSystemDir()];
|
|
451
492
|
function globalSkillDirs() {
|
|
452
|
-
|
|
493
|
+
const seen = /* @__PURE__ */ new Set();
|
|
494
|
+
return [...new Set(HARNESSES.flatMap((h) => h.globalDirs()))].filter((d) => {
|
|
495
|
+
if (systemDirs().includes(d)) return false;
|
|
496
|
+
const real = realPath(d);
|
|
497
|
+
if (seen.has(real)) return false;
|
|
498
|
+
seen.add(real);
|
|
499
|
+
return true;
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
function globalDirAs(dir) {
|
|
503
|
+
const real = realPath(dir);
|
|
504
|
+
return globalSkillDirs().find((d) => realPath(d) === real) ?? dir;
|
|
453
505
|
}
|
|
454
506
|
function systemSkills(enabled) {
|
|
455
507
|
return systemDirs().flatMap(
|
|
@@ -492,7 +544,7 @@ function globalFolderSkills(enabled) {
|
|
|
492
544
|
const inAgents = e.dir === agentsDir || target_.includes(".agents/skills");
|
|
493
545
|
const locked = lock[e.name]?.source;
|
|
494
546
|
if (existsSync6(join7(e.path, OWN_SKILL_MARKER))) {
|
|
495
|
-
skills.push({ name: e.name, kind: "
|
|
547
|
+
skills.push({ name: e.name, kind: "skilllib", origin: "skilllib", path: e.path, description: readSkillInfo(e.path).description, movable: false, broken: false, harnesses: harnessesReading(e.dir, enabled), links: [] });
|
|
496
548
|
continue;
|
|
497
549
|
}
|
|
498
550
|
skills.push({
|
|
@@ -516,8 +568,21 @@ function globalFolderSkills(enabled) {
|
|
|
516
568
|
return skills;
|
|
517
569
|
}
|
|
518
570
|
function tildify2(p) {
|
|
519
|
-
|
|
520
|
-
|
|
571
|
+
return tildify(p).replace(/\\/g, "/");
|
|
572
|
+
}
|
|
573
|
+
function grokBuiltInSkills(enabled) {
|
|
574
|
+
if (!enabled.includes("grok")) return [];
|
|
575
|
+
return skillDirsIn(join7(grokDir(), "bundled", "skills")).map((path) => ({
|
|
576
|
+
name: basename2(path),
|
|
577
|
+
kind: "built-in",
|
|
578
|
+
origin: "Grok",
|
|
579
|
+
path,
|
|
580
|
+
description: readSkillInfo(path).description,
|
|
581
|
+
movable: false,
|
|
582
|
+
broken: false,
|
|
583
|
+
harnesses: ["grok"],
|
|
584
|
+
links: []
|
|
585
|
+
}));
|
|
521
586
|
}
|
|
522
587
|
function cursorBuiltInSkills(enabled) {
|
|
523
588
|
if (!enabled.includes("cursor")) return [];
|
|
@@ -534,30 +599,41 @@ function cursorBuiltInSkills(enabled) {
|
|
|
534
599
|
}));
|
|
535
600
|
}
|
|
536
601
|
function claudeAiSkills(enabled) {
|
|
537
|
-
const
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
602
|
+
const byReal = /* @__PURE__ */ new Map();
|
|
603
|
+
for (const skills of [join7(claudeDir(), "skills"), join7(userHome(), ".claude", "skills")]) {
|
|
604
|
+
const synced = join7(skills, "synced");
|
|
605
|
+
const real = realpathOrNull(synced);
|
|
606
|
+
const readers = harnessesReading(skills, enabled);
|
|
607
|
+
if (!real || readers.length === 0) continue;
|
|
608
|
+
const entry = byReal.get(real) ?? { dir: synced, readers: [] };
|
|
609
|
+
for (const id of readers) if (!entry.readers.includes(id)) entry.readers.push(id);
|
|
610
|
+
byReal.set(real, entry);
|
|
611
|
+
}
|
|
612
|
+
return [...byReal.values()].flatMap(
|
|
613
|
+
({ dir, readers }) => readdirSync3(dir).filter((d) => !d.startsWith(".")).flatMap(
|
|
614
|
+
(bucket) => skillDirsIn(join7(dir, bucket)).map((path) => ({
|
|
615
|
+
name: basename2(path),
|
|
616
|
+
kind: "claude.ai",
|
|
617
|
+
origin: "claude.ai account",
|
|
618
|
+
path,
|
|
619
|
+
description: readSkillInfo(path).description,
|
|
620
|
+
movable: false,
|
|
621
|
+
broken: false,
|
|
622
|
+
harnesses: enabled.filter((id) => readers.includes(id)),
|
|
623
|
+
links: []
|
|
624
|
+
}))
|
|
625
|
+
)
|
|
552
626
|
);
|
|
553
627
|
}
|
|
554
|
-
function
|
|
555
|
-
const
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
628
|
+
function pluginDirCandidates(id, installPaths) {
|
|
629
|
+
const [name = "", marketplace = ""] = id.split("@");
|
|
630
|
+
const plugins = join7(claudeDir(), "plugins");
|
|
631
|
+
return [
|
|
632
|
+
...installPaths,
|
|
633
|
+
join7(plugins, "marketplaces", marketplace, "plugins", name),
|
|
634
|
+
join7(plugins, "marketplaces", marketplace, "external_plugins", name),
|
|
635
|
+
join7(plugins, "marketplaces", marketplace)
|
|
636
|
+
];
|
|
561
637
|
}
|
|
562
638
|
function repoSettingsFiles(root) {
|
|
563
639
|
return [join7(root, ".claude", "settings.json"), join7(root, ".claude", "settings.local.json")];
|
|
@@ -575,13 +651,7 @@ function pluginSkills(harnesses, root) {
|
|
|
575
651
|
const user = enabledPlugins();
|
|
576
652
|
const enabled = root ? enabledPlugins(root) : user;
|
|
577
653
|
const fromMarketplaces = [.../* @__PURE__ */ new Set([...Object.keys(enabled), ...Object.keys(user)])].filter((id) => claude2 && enabled[id] || cursor && user[id]).flatMap((id) => {
|
|
578
|
-
const
|
|
579
|
-
const candidates = [
|
|
580
|
-
...(installed[id] ?? []).flatMap((i) => i.installPath ? [i.installPath] : []),
|
|
581
|
-
join7(plugins, "marketplaces", marketplace, "plugins", name),
|
|
582
|
-
join7(plugins, "marketplaces", marketplace, "external_plugins", name),
|
|
583
|
-
join7(plugins, "marketplaces", marketplace)
|
|
584
|
-
];
|
|
654
|
+
const candidates = pluginDirCandidates(id, (installed[id] ?? []).flatMap((i) => i.installPath ? [i.installPath] : []));
|
|
585
655
|
const dir = candidates.find((c) => existsSync6(join7(c, "skills")));
|
|
586
656
|
if (!dir) return [];
|
|
587
657
|
const inCursor = cursor && user[id] && (installed[id] ?? []).some((i) => i.scope === "user" && i.installPath === dir);
|
|
@@ -616,6 +686,7 @@ function machineSkills(enabled = enabledHarnesses()) {
|
|
|
616
686
|
...claudeAiSkills(enabled),
|
|
617
687
|
...pluginSkills(enabled),
|
|
618
688
|
...cursorBuiltInSkills(enabled),
|
|
689
|
+
...grokBuiltInSkills(enabled),
|
|
619
690
|
...systemSkills(enabled)
|
|
620
691
|
].sort(byKindAndName);
|
|
621
692
|
}
|
|
@@ -637,9 +708,9 @@ function recordOrigin(name, origin) {
|
|
|
637
708
|
writeFileSync4(originsFile(), JSON.stringify({ ...origins, [name]: origin }, null, 2) + "\n");
|
|
638
709
|
}
|
|
639
710
|
function originFor(dir) {
|
|
640
|
-
const match = machineSkills().find((s) =>
|
|
711
|
+
const match = machineSkills().find((s) => resolve5(s.path) === resolve5(dir));
|
|
641
712
|
if (match) return match.kind === "global" ? "global folder" : `${match.kind}: ${match.origin}`;
|
|
642
|
-
const project = dir.split(/[\\/]\.(?:claude|agents)[\\/]skills[\\/]/)[0];
|
|
713
|
+
const project = dir.split(/[\\/]\.(?:claude|agents|grok|cursor|codex)[\\/]skills[\\/]/)[0];
|
|
643
714
|
if (!project || project === dir) return dir;
|
|
644
715
|
const locked = projectSkillsLock(project)[basename2(dir)]?.source;
|
|
645
716
|
return locked ? `skills.sh: ${locked}` : `project: ${basename2(project)}`;
|
|
@@ -658,7 +729,7 @@ function librarySkillDir(name) {
|
|
|
658
729
|
}
|
|
659
730
|
function realpathOrNull2(path) {
|
|
660
731
|
try {
|
|
661
|
-
return
|
|
732
|
+
return realpathSync4(path);
|
|
662
733
|
} catch {
|
|
663
734
|
return null;
|
|
664
735
|
}
|
|
@@ -685,7 +756,7 @@ function visibilityOf(root, name, enabled = enabledHarnesses()) {
|
|
|
685
756
|
}
|
|
686
757
|
function dependencyDirs(root, name, dep) {
|
|
687
758
|
const wanted = installDirs(enabledHarnesses());
|
|
688
|
-
const untracked = ALL_PROJECT_DIRS.find((d) => existsSync7(join8(root, d, name, "SKILL.md")) && !isLink(join8(root, d, name)));
|
|
759
|
+
const untracked = ALL_PROJECT_DIRS.find((d) => d !== GROK_SKILLS_DIR && existsSync7(join8(root, d, name, "SKILL.md")) && !isLink(join8(root, d, name)));
|
|
689
760
|
const primary = dep ? dep.dir ?? PROJECT_SKILLS_DIR : untracked ?? wanted[0];
|
|
690
761
|
return [primary, .../* @__PURE__ */ new Set([...dep?.links ?? [], ...wanted.filter((d) => d !== primary)])];
|
|
691
762
|
}
|
|
@@ -737,7 +808,7 @@ function projectStatus(root) {
|
|
|
737
808
|
unmanaged.push({
|
|
738
809
|
name,
|
|
739
810
|
managed: false,
|
|
740
|
-
state: unmanagedState(name, real, location
|
|
811
|
+
state: unmanagedState(name, real, isRepoSkillLocation(location), source),
|
|
741
812
|
version: null,
|
|
742
813
|
latest: latestVersion(name)?.version ?? null,
|
|
743
814
|
location,
|
|
@@ -757,10 +828,12 @@ function unmanagedState(name, real, committed, source) {
|
|
|
757
828
|
return latest === null ? "local only" : sameAsLibrary ? "untracked copy of library skill" : "untracked, differs from library";
|
|
758
829
|
}
|
|
759
830
|
var NESTED_READERS = [
|
|
760
|
-
[PROJECT_SKILLS_DIR, ["claude-code", "cursor"]],
|
|
761
|
-
[AGENTS_SKILLS_DIR, ["codex", "cursor"]],
|
|
762
|
-
[".cursor/skills", ["cursor"]],
|
|
763
|
-
[".codex/skills", ["codex", "cursor"]]
|
|
831
|
+
[PROJECT_SKILLS_DIR, ["claude-code", "cursor", "grok"]],
|
|
832
|
+
[AGENTS_SKILLS_DIR, ["codex", "cursor", "grok"]],
|
|
833
|
+
[".cursor/skills", ["cursor", "grok"]],
|
|
834
|
+
[".codex/skills", ["codex", "cursor"]],
|
|
835
|
+
// Cursor reads nested .grok/skills too, but skilllib doesn't count it: that folder stays Grok's.
|
|
836
|
+
[".grok/skills", ["grok"]]
|
|
764
837
|
];
|
|
765
838
|
var NESTED_SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "vendor", "target", "coverage"]);
|
|
766
839
|
function nestedSkills(root, { depth = 3, enabled = enabledHarnesses() } = {}) {
|
|
@@ -782,7 +855,7 @@ function nestedSkills(root, { depth = 3, enabled = enabledHarnesses() } = {}) {
|
|
|
782
855
|
found.push({
|
|
783
856
|
name,
|
|
784
857
|
managed: false,
|
|
785
|
-
state: unmanagedState(name, path, skillsDir
|
|
858
|
+
state: unmanagedState(name, path, isRepoSkillLocation(skillsDir), void 0),
|
|
786
859
|
version: null,
|
|
787
860
|
latest: latestVersion(name)?.version ?? null,
|
|
788
861
|
location,
|
|
@@ -797,28 +870,53 @@ function nestedSkills(root, { depth = 3, enabled = enabledHarnesses() } = {}) {
|
|
|
797
870
|
walk(root, depth);
|
|
798
871
|
return found.sort((a, b) => a.location.localeCompare(b.location) || a.name.localeCompare(b.name));
|
|
799
872
|
}
|
|
800
|
-
function
|
|
873
|
+
function sharedByGit(root, location, name) {
|
|
874
|
+
const copy = `${location}/${name}`;
|
|
875
|
+
if (isGitTracked(root, copy)) return true;
|
|
876
|
+
if (!isGitTracked(root, location)) return false;
|
|
877
|
+
try {
|
|
878
|
+
execFileSync("git", ["check-ignore", "-q", "--no-index", "--", copy], { cwd: root, stdio: "ignore" });
|
|
879
|
+
return false;
|
|
880
|
+
} catch {
|
|
881
|
+
return true;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
function linkEverywhere(root, name, location, { allowTracked = false, dryRun = false } = {}) {
|
|
801
885
|
const enabled = enabledHarnesses();
|
|
802
|
-
const result = { created: [], blocked: [] };
|
|
886
|
+
const result = { created: [], blocked: [], uncommitted: [] };
|
|
887
|
+
let shared;
|
|
803
888
|
for (const id of enabled) {
|
|
804
889
|
if (visibilityOf(root, name, [id])[0].paths > 0) continue;
|
|
805
890
|
const dir = harness(id).projectDirs[0];
|
|
806
891
|
const link2 = join8(root, dir, name);
|
|
807
|
-
if (entryExists(link2)) continue;
|
|
808
|
-
if (
|
|
809
|
-
|
|
810
|
-
|
|
892
|
+
if (entryExists(link2) || Object.values(result).some((dirs) => dirs.includes(dir))) continue;
|
|
893
|
+
if (isGitTracked(root, dir)) {
|
|
894
|
+
if (!(shared ??= sharedByGit(root, location, name))) {
|
|
895
|
+
if (!result.uncommitted.includes(dir)) result.uncommitted.push(dir);
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
if (!allowTracked && !readConfig().agentsDirOk?.includes(root)) {
|
|
899
|
+
if (!result.blocked.includes(dir)) result.blocked.push(dir);
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (!dryRun) {
|
|
904
|
+
mkdirSync5(join8(root, dir), { recursive: true });
|
|
905
|
+
linkDir(join8(root, location, name), link2);
|
|
811
906
|
}
|
|
812
|
-
mkdirSync5(join8(root, dir), { recursive: true });
|
|
813
|
-
linkDir(join8(root, location, name), link2);
|
|
814
907
|
result.created.push(dir);
|
|
815
908
|
}
|
|
816
909
|
return result;
|
|
817
910
|
}
|
|
818
|
-
function unlinkEverywhere(root, name) {
|
|
911
|
+
function unlinkEverywhere(root, name, removing) {
|
|
912
|
+
const gone = removing ? realpathOrNull2(removing) : null;
|
|
819
913
|
return ALL_PROJECT_DIRS.filter((dir) => {
|
|
820
914
|
const link2 = join8(root, dir, name);
|
|
821
915
|
if (!isLink(link2)) return false;
|
|
916
|
+
if (dir === GROK_SKILLS_DIR) {
|
|
917
|
+
const target = realpathOrNull2(link2);
|
|
918
|
+
if (target !== null && target !== gone) return false;
|
|
919
|
+
}
|
|
822
920
|
unlinkSync(link2);
|
|
823
921
|
return true;
|
|
824
922
|
});
|
|
@@ -826,7 +924,7 @@ function unlinkEverywhere(root, name) {
|
|
|
826
924
|
function relinkDependency(root, name, { allowTracked = false } = {}) {
|
|
827
925
|
const manifest = readManifest(root);
|
|
828
926
|
const dep = manifest.skills[name];
|
|
829
|
-
if (!dep) return { created: [], blocked: [] };
|
|
927
|
+
if (!dep) return { created: [], blocked: [], uncommitted: [] };
|
|
830
928
|
const result = linkEverywhere(root, name, dep.dir ?? PROJECT_SKILLS_DIR, { allowTracked });
|
|
831
929
|
if (result.created.length) {
|
|
832
930
|
manifest.skills[name] = { ...dep, links: [.../* @__PURE__ */ new Set([...dep.links ?? [], ...result.created])] };
|
|
@@ -837,13 +935,15 @@ function relinkDependency(root, name, { allowTracked = false } = {}) {
|
|
|
837
935
|
function linkAll(root, { allowTracked = false } = {}) {
|
|
838
936
|
const linked = [];
|
|
839
937
|
const blocked = /* @__PURE__ */ new Set();
|
|
938
|
+
const uncommitted = [];
|
|
840
939
|
for (const skill of projectStatus(root)) {
|
|
841
940
|
if (!skill.visibility.some((v) => v.paths === 0) || skill.state === "folder missing") continue;
|
|
842
941
|
const res = skill.managed ? relinkDependency(root, skill.name, { allowTracked }) : linkEverywhere(root, skill.name, skill.location, { allowTracked });
|
|
843
942
|
if (res.created.length) linked.push({ name: skill.name, into: res.created });
|
|
844
943
|
for (const dir of res.blocked) blocked.add(dir);
|
|
944
|
+
if (res.uncommitted.length) uncommitted.push(skill.name);
|
|
845
945
|
}
|
|
846
|
-
return { linked, blocked: [...blocked] };
|
|
946
|
+
return { linked, blocked: [...blocked], uncommitted };
|
|
847
947
|
}
|
|
848
948
|
function addSkill(root, name, { force = false, version, allowTracked = false } = {}) {
|
|
849
949
|
const target = version === void 0 ? latestVersion(name) : getVersion(name, version);
|
|
@@ -861,13 +961,17 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
861
961
|
reason: recorded ? "has local edits (use --force to overwrite, or save them to the library first)" : `${primary}/${name} already exists with different content (use --force to replace it)`
|
|
862
962
|
};
|
|
863
963
|
}
|
|
964
|
+
let backedUp;
|
|
864
965
|
if (local !== target.hash || isLink(to)) {
|
|
865
966
|
mkdirSync5(join8(root, primary), { recursive: true });
|
|
866
967
|
if (isLink(to)) unlinkSync(to);
|
|
968
|
+
else if (local !== null && !knownContent) backedUp = stash(to, "edit-backup");
|
|
867
969
|
copySkill(versionDir(name, target.version), to);
|
|
868
970
|
}
|
|
869
971
|
const links = [];
|
|
870
972
|
const blocked = [];
|
|
973
|
+
const uncommitted = [];
|
|
974
|
+
let shared;
|
|
871
975
|
for (const dir of linkDirs) {
|
|
872
976
|
const link2 = join8(root, dir, name);
|
|
873
977
|
if (isLink(link2) && realpathOrNull2(link2) === realpathOrNull2(to)) {
|
|
@@ -875,9 +979,15 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
875
979
|
continue;
|
|
876
980
|
}
|
|
877
981
|
if (entryExists(link2)) continue;
|
|
878
|
-
if (!
|
|
879
|
-
|
|
880
|
-
|
|
982
|
+
if (!recorded?.links?.includes(dir) && isGitTracked(root, dir)) {
|
|
983
|
+
if (!(shared ??= sharedByGit(root, primary, name))) {
|
|
984
|
+
uncommitted.push(dir);
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
if (!allowTracked && !readConfig().agentsDirOk?.includes(root)) {
|
|
988
|
+
blocked.push(dir);
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
881
991
|
}
|
|
882
992
|
mkdirSync5(join8(root, dir), { recursive: true });
|
|
883
993
|
linkDir(to, link2);
|
|
@@ -892,10 +1002,12 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
892
1002
|
writeManifest(root, manifest);
|
|
893
1003
|
return {
|
|
894
1004
|
name,
|
|
895
|
-
action: recorded
|
|
1005
|
+
action: !recorded && local === null ? "installed" : backedUp && recorded?.version === target.version ? "reset" : "updated",
|
|
896
1006
|
...recorded ? { from: recorded.version } : {},
|
|
897
1007
|
to: target.version,
|
|
898
|
-
...blocked.length ? { blocked } : {}
|
|
1008
|
+
...blocked.length ? { blocked } : {},
|
|
1009
|
+
...uncommitted.length ? { uncommitted } : {},
|
|
1010
|
+
...backedUp ? { backedUp } : {}
|
|
899
1011
|
};
|
|
900
1012
|
}
|
|
901
1013
|
function removeSkill(root, name, { force = false } = {}) {
|
|
@@ -904,17 +1016,17 @@ function removeSkill(root, name, { force = false } = {}) {
|
|
|
904
1016
|
if (!recorded) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
|
|
905
1017
|
const dir = join8(root, recorded.dir ?? PROJECT_SKILLS_DIR, name);
|
|
906
1018
|
const local = treeHash(dir);
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
}
|
|
1019
|
+
const edited = local !== null && local !== recorded.hash && !versionForHash(name, local);
|
|
1020
|
+
if (edited && !force) return { name, action: "skipped", reason: "has local edits (use --force to delete anyway)" };
|
|
910
1021
|
for (const linkDir2 of recorded.links ?? []) {
|
|
911
1022
|
const link2 = join8(root, linkDir2, name);
|
|
912
1023
|
if (isLink(link2)) unlinkSync(link2);
|
|
913
1024
|
}
|
|
914
|
-
|
|
1025
|
+
const backedUp = edited ? stash(dir, "edit-backup") : void 0;
|
|
1026
|
+
if (!backedUp) rmSync2(dir, { recursive: true, force: true });
|
|
915
1027
|
delete manifest.skills[name];
|
|
916
1028
|
writeManifest(root, manifest);
|
|
917
|
-
return { name, action: "removed" };
|
|
1029
|
+
return { name, action: "removed", ...backedUp ? { backedUp } : {} };
|
|
918
1030
|
}
|
|
919
1031
|
function syncProject(root, { force = false } = {}) {
|
|
920
1032
|
const { skills } = readManifest(root);
|
|
@@ -925,7 +1037,8 @@ function syncProject(root, { force = false } = {}) {
|
|
|
925
1037
|
const pinned = dep.version > 0 && getVersion(name, dep.version) ? dep.version : void 0;
|
|
926
1038
|
if (local !== null && local !== dep.hash && !force) return [{ name, action: "skipped", reason: "has local edits (use --force to reset them)" }];
|
|
927
1039
|
const change = addSkill(root, name, { force: true, version: pinned });
|
|
928
|
-
|
|
1040
|
+
if (change.action === "skipped") return [{ ...change, reason: `not in your library (${tildify(libraryDir())}); import it, or set SKILLLIB_HOME to the library that has it` }];
|
|
1041
|
+
return [{ ...change, action: local === null ? "installed" : change.action }];
|
|
929
1042
|
});
|
|
930
1043
|
}
|
|
931
1044
|
function discardEdits(root, name) {
|
|
@@ -944,6 +1057,7 @@ function updateProject(root, names, { force = false } = {}) {
|
|
|
944
1057
|
const dep = skills[name];
|
|
945
1058
|
const local = treeHash(join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name));
|
|
946
1059
|
if (dep.hash === latest.hash && local === latest.hash) return [];
|
|
1060
|
+
if (dep.hash === latest.hash && local !== null && !force) return [];
|
|
947
1061
|
const change = addSkill(root, name, { force });
|
|
948
1062
|
return change.action === "skipped" || change.from !== change.to || local !== latest.hash ? [change] : [];
|
|
949
1063
|
});
|
|
@@ -985,7 +1099,7 @@ function stash(dir, bucket) {
|
|
|
985
1099
|
return target;
|
|
986
1100
|
}
|
|
987
1101
|
function removeUntracked(root, name, path) {
|
|
988
|
-
unlinkEverywhere(root, name);
|
|
1102
|
+
unlinkEverywhere(root, name, path);
|
|
989
1103
|
return stash(path, "trash");
|
|
990
1104
|
}
|
|
991
1105
|
function projectsUsing(name) {
|
|
@@ -1038,12 +1152,12 @@ function createSkill(name, description) {
|
|
|
1038
1152
|
join8(dir, "SKILL.md"),
|
|
1039
1153
|
`---
|
|
1040
1154
|
name: ${name}
|
|
1041
|
-
description: ${description || "Describe when
|
|
1155
|
+
description: ${description || "Describe when an agent should use this skill."}
|
|
1042
1156
|
---
|
|
1043
1157
|
|
|
1044
1158
|
# ${name}
|
|
1045
1159
|
|
|
1046
|
-
Instructions for
|
|
1160
|
+
Instructions for the agent go here.
|
|
1047
1161
|
`
|
|
1048
1162
|
);
|
|
1049
1163
|
forgetLatest(name);
|
|
@@ -1061,7 +1175,11 @@ function listBackups() {
|
|
|
1061
1175
|
const from = index[entry] ?? (kind === "trash" ? librarySkillDir(name) : join8(claudeDir(), "skills", name));
|
|
1062
1176
|
return [{ name, kind, path: join8(dir, entry), movedAt: `${match[2]} ${match[3]}:${match[4]}`, from }];
|
|
1063
1177
|
});
|
|
1064
|
-
}).sort((a, b) => b.
|
|
1178
|
+
}).sort((a, b) => b.path.slice(-24).localeCompare(a.path.slice(-24)));
|
|
1179
|
+
}
|
|
1180
|
+
function backupFrom(backup) {
|
|
1181
|
+
if (backup.kind === "plugin") return "Claude Code plugin";
|
|
1182
|
+
return backup.from === librarySkillDir(backup.name) ? "library (deleted)" : tildify(dirname2(backup.from));
|
|
1065
1183
|
}
|
|
1066
1184
|
function restoreBackup(backup) {
|
|
1067
1185
|
const to = backup.from;
|
|
@@ -1114,7 +1232,7 @@ function installAgentSkill(enabled = enabledHarnesses()) {
|
|
|
1114
1232
|
const [primary, ...links] = agentSkillDirs(enabled);
|
|
1115
1233
|
if (!primary) return { ok: false, reason: "no agents chosen yet (skilllib harnesses <id>...)" };
|
|
1116
1234
|
const foreign = [primary, ...links].find((d) => entryExists2(d) && !isOurs(d) && !isDangling(d));
|
|
1117
|
-
if (foreign) return { ok: false, reason: `${foreign} already exists and isn't skilllib's` };
|
|
1235
|
+
if (foreign) return { ok: false, reason: `${tildify(foreign)} already exists and isn't skilllib's` };
|
|
1118
1236
|
for (const d of [primary, ...links]) if (isDangling(d)) unlinkSync2(d);
|
|
1119
1237
|
mkdirSync6(primary, { recursive: true });
|
|
1120
1238
|
writeFileSync6(join9(primary, "SKILL.md"), shippedSkill());
|
|
@@ -1143,12 +1261,8 @@ function refreshAgentSkill() {
|
|
|
1143
1261
|
}
|
|
1144
1262
|
|
|
1145
1263
|
// src/commands.ts
|
|
1146
|
-
import { existsSync as
|
|
1147
|
-
import { basename as basename8,
|
|
1148
|
-
|
|
1149
|
-
// src/here.ts
|
|
1150
|
-
import { existsSync as existsSync12, readdirSync as readdirSync7 } from "fs";
|
|
1151
|
-
import { basename as basename7, join as join13 } from "path";
|
|
1264
|
+
import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
|
|
1265
|
+
import { basename as basename8, join as join14, resolve as resolve7, sep as sep5 } from "path";
|
|
1152
1266
|
|
|
1153
1267
|
// src/git.ts
|
|
1154
1268
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -1162,9 +1276,9 @@ function git(root, args, input) {
|
|
|
1162
1276
|
return typeof out2 === "string" ? out2 : null;
|
|
1163
1277
|
}
|
|
1164
1278
|
}
|
|
1165
|
-
function gitInfo(root) {
|
|
1279
|
+
function gitInfo(root, extraDirs = []) {
|
|
1166
1280
|
if (!existsSync9(join10(root, ".git"))) return null;
|
|
1167
|
-
const dirs = [...ALL_PROJECT_DIRS, MANIFEST_FILE].filter((d) => existsSync9(join10(root, d)));
|
|
1281
|
+
const dirs = [...ALL_PROJECT_DIRS, MANIFEST_FILE, ...extraDirs].filter((d) => existsSync9(join10(root, d)));
|
|
1168
1282
|
if (dirs.length === 0) return { of: () => "new" };
|
|
1169
1283
|
const tracked = (git(root, ["ls-files", "-z", "--", ...dirs]) ?? "").split("\0").filter(Boolean);
|
|
1170
1284
|
const dirty = (git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...dirs]) ?? "").split("\0").filter(Boolean).map((entry) => entry.slice(3));
|
|
@@ -1189,14 +1303,18 @@ function relativeTo(root, path) {
|
|
|
1189
1303
|
return relative3(root, path).split("\\").join("/");
|
|
1190
1304
|
}
|
|
1191
1305
|
|
|
1306
|
+
// src/here.ts
|
|
1307
|
+
import { readdirSync as readdirSync7 } from "fs";
|
|
1308
|
+
import { basename as basename7 } from "path";
|
|
1309
|
+
|
|
1192
1310
|
// src/health.ts
|
|
1193
1311
|
import { lstatSync as lstatSync4 } from "fs";
|
|
1194
|
-
import { basename as basename6 } from "path";
|
|
1312
|
+
import { basename as basename6, dirname as dirname5 } from "path";
|
|
1195
1313
|
|
|
1196
1314
|
// src/plugins.ts
|
|
1197
1315
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1198
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as
|
|
1199
|
-
import { basename as basename4, dirname as dirname3, join as join11 } from "path";
|
|
1316
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync7 } from "fs";
|
|
1317
|
+
import { basename as basename4, dirname as dirname3, join as join11, resolve as resolve6 } from "path";
|
|
1200
1318
|
function readJson2(path) {
|
|
1201
1319
|
try {
|
|
1202
1320
|
return JSON.parse(readFileSync8(path, "utf-8"));
|
|
@@ -1239,22 +1357,109 @@ function claudePlugins(machine, root) {
|
|
|
1239
1357
|
};
|
|
1240
1358
|
});
|
|
1241
1359
|
}
|
|
1360
|
+
function newer(a, b) {
|
|
1361
|
+
const parts = (v) => v.replace(/^v/i, "").split(/[.+-]/).map((x) => parseInt(x, 10) || 0);
|
|
1362
|
+
const pa = parts(a);
|
|
1363
|
+
const pb = parts(b);
|
|
1364
|
+
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);
|
|
1365
|
+
return false;
|
|
1366
|
+
}
|
|
1367
|
+
function updateOf(name, marketplace, entry, catalogs) {
|
|
1368
|
+
const dir = join11(claudeDir(), "plugins", "marketplaces", marketplace);
|
|
1369
|
+
if (!catalogs.has(marketplace)) catalogs.set(marketplace, readJson2(join11(dir, ".claude-plugin", "marketplace.json"))?.plugins ?? []);
|
|
1370
|
+
const listed = catalogs.get(marketplace).find((p) => p.name === name);
|
|
1371
|
+
if (!listed) return void 0;
|
|
1372
|
+
const version = listed.version ?? (typeof listed.source === "string" ? readJson2(join11(dir, listed.source, ".claude-plugin", "plugin.json"))?.version : void 0);
|
|
1373
|
+
if (version && entry.version) return newer(version, entry.version) ? version : void 0;
|
|
1374
|
+
const sha = typeof listed.source === "object" ? listed.source?.sha : void 0;
|
|
1375
|
+
return sha && entry.gitCommitSha && sha !== entry.gitCommitSha ? "newer" : void 0;
|
|
1376
|
+
}
|
|
1377
|
+
function pluginRoot(id, entry) {
|
|
1378
|
+
const candidates = pluginDirCandidates(id, entry.installPath ? [entry.installPath] : []);
|
|
1379
|
+
return candidates.find((c) => existsSync10(join11(c, "skills"))) ?? candidates.find((c) => existsSync10(c)) ?? candidates[0];
|
|
1380
|
+
}
|
|
1381
|
+
function describe(root) {
|
|
1382
|
+
const manifest = readJson2(join11(root, ".claude-plugin", "plugin.json")) ?? {};
|
|
1383
|
+
return { description: manifest.description ?? "", version: manifest.version, skills: skillDirsIn(join11(root, "skills")), extras: extrasOf(root) };
|
|
1384
|
+
}
|
|
1385
|
+
function installedPlugins() {
|
|
1386
|
+
const plugins = join11(claudeDir(), "plugins");
|
|
1387
|
+
const installed = readJson2(join11(plugins, "installed_plugins.json"))?.plugins ?? {};
|
|
1388
|
+
const catalogs = /* @__PURE__ */ new Map();
|
|
1389
|
+
const fromMarketplaces = Object.entries(installed).flatMap(
|
|
1390
|
+
([id, entries]) => entries.map((entry) => {
|
|
1391
|
+
const [name = id, marketplace = ""] = id.split("@");
|
|
1392
|
+
const scope = entry.scope ?? "user";
|
|
1393
|
+
const projectPath = scope === "user" ? void 0 : entry.projectPath;
|
|
1394
|
+
const root = pluginRoot(id, entry);
|
|
1395
|
+
const about = describe(root);
|
|
1396
|
+
const update2 = updateOf(name, marketplace, entry, catalogs);
|
|
1397
|
+
return {
|
|
1398
|
+
id,
|
|
1399
|
+
scope,
|
|
1400
|
+
...projectPath ? { projectPath } : {},
|
|
1401
|
+
synced: false,
|
|
1402
|
+
agent: "claude-code",
|
|
1403
|
+
on: enabledPlugins(projectPath)[id] === true,
|
|
1404
|
+
root,
|
|
1405
|
+
...about,
|
|
1406
|
+
version: entry.version ?? about.version,
|
|
1407
|
+
...update2 ? { update: update2 } : {}
|
|
1408
|
+
};
|
|
1409
|
+
})
|
|
1410
|
+
);
|
|
1411
|
+
const user = enabledPlugins();
|
|
1412
|
+
const fromSettings = Object.keys(user).filter((id) => user[id] === true && id.includes("@") && !id.endsWith("@synced") && !installed[id]).flatMap((id) => {
|
|
1413
|
+
const root = pluginDirCandidates(id, []).find((c) => existsSync10(join11(c, "skills")));
|
|
1414
|
+
return root ? [{ id, scope: "user", synced: false, agent: "claude-code", on: true, settingsOnly: true, root, ...describe(root) }] : [];
|
|
1415
|
+
});
|
|
1416
|
+
const syncedDir = join11(plugins, "synced");
|
|
1417
|
+
const dirs = (p) => existsSync10(p) ? readdirSync5(p, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => join11(p, d.name)) : [];
|
|
1418
|
+
const fromSynced = dirs(syncedDir).flatMap(dirs).map((root) => {
|
|
1419
|
+
const id = `${readJson2(join11(root, ".claude-plugin", "plugin.json"))?.name ?? basename4(root)}@synced`;
|
|
1420
|
+
return { id, scope: null, synced: true, agent: "claude-code", on: user[id] !== false, root, ...describe(root) };
|
|
1421
|
+
});
|
|
1422
|
+
return [...fromMarketplaces, ...fromSettings, ...fromSynced];
|
|
1423
|
+
}
|
|
1424
|
+
function cursorPlugins() {
|
|
1425
|
+
const cache = join11(userHome(), ".cursor", "plugins", "cache");
|
|
1426
|
+
const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") && statSync3(join11(p, d)).isDirectory()) : [];
|
|
1427
|
+
return dirs(cache).flatMap(
|
|
1428
|
+
(marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
|
|
1429
|
+
const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
|
|
1430
|
+
const root = versions.sort((a, b) => statSync3(b).mtimeMs - statSync3(a).mtimeMs)[0];
|
|
1431
|
+
if (!root) return [];
|
|
1432
|
+
const about = describe(root);
|
|
1433
|
+
return [{ id: `${plugin}@${marketplace}`, scope: null, synced: false, agent: "cursor", on: null, root, ...about, version: about.version ?? basename4(root) }];
|
|
1434
|
+
})
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
var binaries = /* @__PURE__ */ new Map();
|
|
1438
|
+
function forgetClaudeBinary() {
|
|
1439
|
+
binaries.clear();
|
|
1440
|
+
}
|
|
1242
1441
|
function claudeBinary() {
|
|
1243
|
-
const
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1442
|
+
const key = `${process.env.PATH}|${userHome()}`;
|
|
1443
|
+
if (!binaries.has(key)) {
|
|
1444
|
+
const candidates = ["claude", join11(userHome(), ".local", "bin", "claude"), join11(userHome(), ".claude", "local", "claude")];
|
|
1445
|
+
const found = candidates.find((bin) => {
|
|
1446
|
+
try {
|
|
1447
|
+
return execFileSync3(bin, ["--version"], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e4, encoding: "utf-8" }).includes("Claude Code");
|
|
1448
|
+
} catch {
|
|
1449
|
+
return false;
|
|
1450
|
+
}
|
|
1451
|
+
});
|
|
1452
|
+
binaries.set(key, found ?? null);
|
|
1453
|
+
}
|
|
1454
|
+
return binaries.get(key);
|
|
1251
1455
|
}
|
|
1252
|
-
function claude(args, cwd) {
|
|
1253
|
-
|
|
1456
|
+
function claude(args, cwd, bin = claudeBinary()) {
|
|
1457
|
+
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` };
|
|
1254
1458
|
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` };
|
|
1459
|
+
const slow = args[1] === "install" || args[1] === "update";
|
|
1255
1460
|
try {
|
|
1256
|
-
execFileSync3(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 12e4, encoding: "utf-8" });
|
|
1257
|
-
return { ok: true };
|
|
1461
|
+
const out2 = execFileSync3(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: slow ? 12e4 : 2e4, encoding: "utf-8" });
|
|
1462
|
+
return { ok: true, out: out2 };
|
|
1258
1463
|
} catch (err) {
|
|
1259
1464
|
const e = err;
|
|
1260
1465
|
return { ok: false, reason: `${(e.stderr || e.stdout || e.message || "failed").trim().split("\n").slice(-1)[0]} (ran ${bin})` };
|
|
@@ -1268,12 +1473,88 @@ function setPluginIn(file, id, on) {
|
|
|
1268
1473
|
writeFileSync7(file, JSON.stringify({ ...settings, enabledPlugins: { ...enabled, [id]: on } }, null, 2) + "\n");
|
|
1269
1474
|
return { ok: true };
|
|
1270
1475
|
}
|
|
1271
|
-
function
|
|
1272
|
-
|
|
1476
|
+
function ignoreLocalSettings(root) {
|
|
1477
|
+
const file = ".claude/settings.local.json";
|
|
1478
|
+
const git2 = (...args) => execFileSync3("git", args, { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1479
|
+
try {
|
|
1480
|
+
git2("rev-parse", "--git-dir");
|
|
1481
|
+
} catch {
|
|
1482
|
+
return "";
|
|
1483
|
+
}
|
|
1484
|
+
try {
|
|
1485
|
+
git2("ls-files", "--error-unmatch", "--", file);
|
|
1486
|
+
return "; git tracks that file, so this shows in git status";
|
|
1487
|
+
} catch {
|
|
1488
|
+
}
|
|
1489
|
+
try {
|
|
1490
|
+
git2("check-ignore", "-q", "--", file);
|
|
1491
|
+
return ", which git ignores";
|
|
1492
|
+
} catch {
|
|
1493
|
+
}
|
|
1494
|
+
try {
|
|
1495
|
+
const exclude = resolve6(root, git2("rev-parse", "--git-path", "info/exclude"));
|
|
1496
|
+
const before = existsSync10(exclude) ? readFileSync8(exclude, "utf-8") : "";
|
|
1497
|
+
mkdirSync7(dirname3(exclude), { recursive: true });
|
|
1498
|
+
writeFileSync7(exclude, `${before}${before && !before.endsWith("\n") ? "\n" : ""}/${git2("rev-parse", "--show-prefix")}${file}
|
|
1499
|
+
`);
|
|
1500
|
+
return ", which git now ignores via .git/info/exclude";
|
|
1501
|
+
} catch {
|
|
1502
|
+
return "; git doesn't ignore that file yet: it's local only, don't commit it";
|
|
1503
|
+
}
|
|
1273
1504
|
}
|
|
1274
1505
|
function turnOffIn(plugin, root) {
|
|
1275
|
-
const
|
|
1276
|
-
|
|
1506
|
+
const done = `${plugin.id} turned off in ${basename4(root)}; other repos keep it`;
|
|
1507
|
+
const bin = plugin.synced ? null : claudeBinary();
|
|
1508
|
+
if (!bin) {
|
|
1509
|
+
const r2 = setPluginIn(join11(root, ".claude", "settings.local.json"), plugin.id, false);
|
|
1510
|
+
return r2.ok ? { ok: true, message: `${done} (in .claude/settings.local.json${ignoreLocalSettings(root)})` } : { ok: false, message: `${plugin.id}: ${r2.reason}` };
|
|
1511
|
+
}
|
|
1512
|
+
const r = claude(["plugin", "disable", plugin.id, "--scope", "local"], root, bin);
|
|
1513
|
+
return r.ok ? { ok: true, message: done } : { ok: false, message: `${plugin.id}: ${r.reason}` };
|
|
1514
|
+
}
|
|
1515
|
+
function settingsFileOf(p) {
|
|
1516
|
+
if (p.synced || !p.scope || p.scope === "user") return join11(claudeDir(), "settings.json");
|
|
1517
|
+
if (!p.projectPath) return null;
|
|
1518
|
+
return join11(p.projectPath, ".claude", p.scope === "local" ? "settings.local.json" : "settings.json");
|
|
1519
|
+
}
|
|
1520
|
+
function whereOf(p) {
|
|
1521
|
+
return p.projectPath ? ` in ${basename4(p.projectPath)} (${p.scope})` : "";
|
|
1522
|
+
}
|
|
1523
|
+
function setPluginOn(p, on) {
|
|
1524
|
+
const done = { ok: true, message: `${p.id} is ${on ? "on" : "off"}${whereOf(p)}${p.synced ? "" : "; Claude Code picks it up in its next session"}` };
|
|
1525
|
+
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` };
|
|
1526
|
+
const bin = p.synced || p.settingsOnly ? null : claudeBinary();
|
|
1527
|
+
if (bin) {
|
|
1528
|
+
const r2 = claude(["plugin", on ? "enable" : "disable", p.id, ...p.scope ? ["--scope", p.scope] : []], p.projectPath, bin);
|
|
1529
|
+
return r2.ok ? done : { ok: false, message: `${p.id}: ${r2.reason}` };
|
|
1530
|
+
}
|
|
1531
|
+
const r = setPluginIn(settingsFileOf(p), p.id, on);
|
|
1532
|
+
if (!r.ok) return { ok: false, message: `${p.id}: ${r.reason}` };
|
|
1533
|
+
return p.scope === "local" && p.projectPath ? { ...done, message: done.message + ignoreLocalSettings(p.projectPath) } : done;
|
|
1534
|
+
}
|
|
1535
|
+
function uninstallPlugin(p) {
|
|
1536
|
+
if (p.synced) return { ok: false, message: `${p.id} is synced from your claude.ai account: remove it there, or turn it off here` };
|
|
1537
|
+
if (p.settingsOnly) return { ok: false, message: `${p.id} isn't installed, only turned on in your settings: turn it off instead` };
|
|
1538
|
+
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` };
|
|
1539
|
+
const wasOn = enabledPlugins(p.projectPath)[p.id] === true;
|
|
1540
|
+
const r = claude(["plugin", "uninstall", p.id, "--keep-data", "--scope", p.scope ?? "user"], p.projectPath);
|
|
1541
|
+
if (!r.ok) return { ok: false, message: `${p.id}: ${r.reason}` };
|
|
1542
|
+
recordRemovedPlugin(p.id, p.scope ?? "user", p.projectPath, wasOn);
|
|
1543
|
+
return { ok: true, message: `${p.id} is uninstalled${whereOf(p)} (Settings \u203A Backups reinstalls it)` };
|
|
1544
|
+
}
|
|
1545
|
+
function updatePlugin(p) {
|
|
1546
|
+
if (p.synced) return { ok: false, message: `${p.id} is synced from your claude.ai account: it updates from there` };
|
|
1547
|
+
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` };
|
|
1548
|
+
const r = claude(["plugin", "update", p.id, ...p.scope ? ["--scope", p.scope] : [], "--json"], p.projectPath);
|
|
1549
|
+
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}` : ""}` };
|
|
1550
|
+
let result = {};
|
|
1551
|
+
try {
|
|
1552
|
+
result = JSON.parse(r.out.trim().split("\n").pop() ?? "{}");
|
|
1553
|
+
} catch {
|
|
1554
|
+
}
|
|
1555
|
+
if (result.updateOutcome === "up_to_date") return { ok: true, message: `${p.id} is already the newest version${result.newVersion ? ` (${result.newVersion})` : ""}${whereOf(p)}` };
|
|
1556
|
+
const versions = result.oldVersion && result.newVersion ? ` from ${result.oldVersion} to ${result.newVersion}` : "";
|
|
1557
|
+
return { ok: true, message: `${p.id} is updated${versions}${whereOf(p)}; Claude Code uses the new version after a restart` };
|
|
1277
1558
|
}
|
|
1278
1559
|
function removedFile() {
|
|
1279
1560
|
return join11(skilllibHome(), "plugin-backup.json");
|
|
@@ -1287,42 +1568,45 @@ function removePlugin(plugin, how) {
|
|
|
1287
1568
|
}
|
|
1288
1569
|
const imported = plugin.skills.filter((dir) => latestVersion(basename4(dir)) === null).map((dir) => importSkill(dir).name);
|
|
1289
1570
|
const scope = plugin.scope ? ["--scope", plugin.scope] : [];
|
|
1290
|
-
const r = plugin.synced ?
|
|
1571
|
+
const r = plugin.synced ? (
|
|
1572
|
+
// Synced plugins aren't installed, so `claude plugin` can't touch them: their switch is enabledPlugins.
|
|
1573
|
+
setPluginIn(join11(claudeDir(), "settings.json"), plugin.id, false)
|
|
1574
|
+
) : claude(["plugin", how === "delete" ? "uninstall" : "disable", plugin.id, ...scope], plugin.projectPath);
|
|
1291
1575
|
const saved = imported.length ? `; ${imported.join(", ")} copied into Your skills` : "";
|
|
1292
1576
|
if (!r.ok) return { ok: false, message: `${plugin.id}: ${r.reason}${saved}` };
|
|
1293
|
-
if (how === "delete" && !plugin.synced) recordRemovedPlugin(plugin.id, plugin.scope ?? "user");
|
|
1577
|
+
if (how === "delete" && !plugin.synced) recordRemovedPlugin(plugin.id, plugin.scope ?? "user", plugin.projectPath, true);
|
|
1294
1578
|
return { ok: true, message: `${plugin.id} ${how === "delete" && !plugin.synced ? "removed" : "turned off"}${saved}` };
|
|
1295
1579
|
}
|
|
1296
|
-
function recordRemovedPlugin(id, scope) {
|
|
1580
|
+
function recordRemovedPlugin(id, scope, projectPath, on = true) {
|
|
1297
1581
|
const list2 = readJson2(removedFile()) ?? [];
|
|
1298
1582
|
mkdirSync7(skilllibHome(), { recursive: true });
|
|
1299
|
-
writeFileSync7(removedFile(), JSON.stringify([...list2, { id, scope, at: (/* @__PURE__ */ new Date()).toISOString() }], null, 2) + "\n");
|
|
1583
|
+
writeFileSync7(removedFile(), JSON.stringify([...list2, { id, scope, ...projectPath ? { projectPath } : {}, on, at: (/* @__PURE__ */ new Date()).toISOString() }], null, 2) + "\n");
|
|
1300
1584
|
}
|
|
1301
1585
|
function pluginBackups() {
|
|
1302
1586
|
return (readJson2(removedFile()) ?? []).map((r) => ({
|
|
1303
1587
|
name: r.id,
|
|
1304
1588
|
kind: "plugin",
|
|
1305
|
-
|
|
1589
|
+
// Project and local installs go back into their project.
|
|
1590
|
+
path: r.projectPath ? `${r.scope}:${r.projectPath}` : r.scope,
|
|
1306
1591
|
movedAt: r.at.slice(0, 16).replace("T", " "),
|
|
1307
1592
|
from: r.id
|
|
1308
1593
|
}));
|
|
1309
1594
|
}
|
|
1310
1595
|
function restorePlugin(backup) {
|
|
1311
|
-
const
|
|
1596
|
+
const [scope = "user", ...rest] = backup.path.split(":");
|
|
1597
|
+
const projectPath = rest.join(":") || void 0;
|
|
1598
|
+
const r = claude(["plugin", "install", backup.from, "--scope", scope], projectPath);
|
|
1312
1599
|
if (!r.ok) return r;
|
|
1313
|
-
|
|
1314
|
-
|
|
1600
|
+
const records = readJson2(removedFile()) ?? [];
|
|
1601
|
+
const same = (x) => x.id === backup.from && (x.projectPath ?? void 0) === projectPath && x.scope === scope;
|
|
1602
|
+
const wasOff = records.filter(same).pop()?.on === false;
|
|
1603
|
+
const off = wasOff ? claude(["plugin", "disable", backup.from, "--scope", scope], projectPath) : null;
|
|
1604
|
+
writeFileSync7(removedFile(), JSON.stringify(records.filter((x) => !same(x)), null, 2) + "\n");
|
|
1605
|
+
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" : ""})`;
|
|
1606
|
+
return { ok: true, to: where };
|
|
1315
1607
|
}
|
|
1316
|
-
function cursorPluginSkills() {
|
|
1317
|
-
|
|
1318
|
-
const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") && statSync2(join11(p, d)).isDirectory()) : [];
|
|
1319
|
-
return dirs(cache).flatMap(
|
|
1320
|
-
(marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
|
|
1321
|
-
const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
|
|
1322
|
-
const newest = versions.sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs)[0];
|
|
1323
|
-
return newest ? skillDirsIn(join11(newest, "skills")).map((path) => ({ name: basename4(path), plugin, path })) : [];
|
|
1324
|
-
})
|
|
1325
|
-
);
|
|
1608
|
+
function cursorPluginSkills(plugins = cursorPlugins()) {
|
|
1609
|
+
return plugins.flatMap((p) => p.skills.map((path) => ({ name: basename4(path), plugin: p.id.split("@")[0], path })));
|
|
1326
1610
|
}
|
|
1327
1611
|
|
|
1328
1612
|
// src/tidy.ts
|
|
@@ -1350,6 +1634,11 @@ function namesIn(dirs) {
|
|
|
1350
1634
|
}
|
|
1351
1635
|
return [...names].sort();
|
|
1352
1636
|
}
|
|
1637
|
+
var PROJECT_TIDY_DIRS = ALL_PROJECT_DIRS.filter((d) => d !== GROK_SKILLS_DIR);
|
|
1638
|
+
function globalTidyDirs() {
|
|
1639
|
+
const own = realPath(join12(grokDir(), "skills"));
|
|
1640
|
+
return globalSkillDirs().filter((d) => realPath(d) !== own);
|
|
1641
|
+
}
|
|
1353
1642
|
function runsFrom(id, dirsWithCopy, readsOf) {
|
|
1354
1643
|
const reads = readsOf(id).filter((d) => dirsWithCopy.includes(d));
|
|
1355
1644
|
if (id === "codex") return reads;
|
|
@@ -1372,8 +1661,8 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1372
1661
|
const readsOf = (id) => HARNESSES.find((h) => h.id === id).projectDirs.map(abs);
|
|
1373
1662
|
let git2;
|
|
1374
1663
|
const skillsLock = projectSkillsLock(root);
|
|
1375
|
-
for (const name of namesIn(
|
|
1376
|
-
const entries = entriesOf(
|
|
1664
|
+
for (const name of namesIn(PROJECT_TIDY_DIRS.map(abs))) {
|
|
1665
|
+
const entries = entriesOf(PROJECT_TIDY_DIRS.map(abs), name);
|
|
1377
1666
|
const external = entries.find((e) => e.link && e.real && !(e.real + sep4).startsWith(realpathOrNull2(root) + sep4));
|
|
1378
1667
|
if (external) {
|
|
1379
1668
|
report.skipped.push({ name, reason: `${rel(external.path)} links outside the repo` });
|
|
@@ -1388,7 +1677,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1388
1677
|
if (dep) {
|
|
1389
1678
|
primary = reals.find((r) => rel(r.dir) === (dep.dir ?? PROJECT_SKILLS_DIR));
|
|
1390
1679
|
if (!primary) continue;
|
|
1391
|
-
} else if (chosen) {
|
|
1680
|
+
} else if (chosen && reals.some((r) => r.dir === chosen)) {
|
|
1392
1681
|
primary = reals.find((r) => r.dir === chosen);
|
|
1393
1682
|
} else {
|
|
1394
1683
|
if (git2 === void 0) git2 = gitInfo(root);
|
|
@@ -1412,7 +1701,8 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1412
1701
|
if (!needed) steps.push({ kind: "unlink", path: e.path });
|
|
1413
1702
|
} else steps.push(needed ? { kind: "link", path: e.path, target: primary.path } : { kind: "unlink", path: e.path });
|
|
1414
1703
|
} else {
|
|
1415
|
-
|
|
1704
|
+
const differs = e.hash !== primary.hash ? { differs: true } : {};
|
|
1705
|
+
steps.push(needed ? { kind: "replace", path: e.path, target: primary.path, ...differs } : { kind: "remove", path: e.path, ...differs });
|
|
1416
1706
|
}
|
|
1417
1707
|
}
|
|
1418
1708
|
if (steps.length) report.plans.push({ name, root, primary: primary.path, steps });
|
|
@@ -1420,14 +1710,14 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1420
1710
|
return report;
|
|
1421
1711
|
}
|
|
1422
1712
|
function globalReadsOf(id) {
|
|
1423
|
-
return HARNESSES.find((h) => h.id === id).globalDirs();
|
|
1713
|
+
return HARNESSES.find((h) => h.id === id).globalDirs().map(globalDirAs);
|
|
1424
1714
|
}
|
|
1425
1715
|
function isSkillsDirPlugin(path) {
|
|
1426
1716
|
return existsSync11(join12(path, ".claude-plugin", "plugin.json"));
|
|
1427
1717
|
}
|
|
1428
1718
|
function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
|
|
1429
1719
|
const report = { plans: [], conflicts: [], skipped: [] };
|
|
1430
|
-
const dirs =
|
|
1720
|
+
const dirs = globalTidyDirs();
|
|
1431
1721
|
const agents = join12(userHome(), ".agents", "skills");
|
|
1432
1722
|
for (const name of namesIn(dirs)) {
|
|
1433
1723
|
const entries = entriesOf(dirs, name);
|
|
@@ -1435,15 +1725,16 @@ function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
|
|
|
1435
1725
|
report.skipped.push({ name, reason: "a Claude Code plugin kept in a skills folder" });
|
|
1436
1726
|
continue;
|
|
1437
1727
|
}
|
|
1438
|
-
const reals = entries.filter((e) => !e.link);
|
|
1728
|
+
const reals = entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link && realpathOrNull2(o.path) === realpathOrNull2(e.path)) === i);
|
|
1439
1729
|
if (reals.length < 2) continue;
|
|
1440
1730
|
hashReals(reals);
|
|
1441
|
-
const
|
|
1442
|
-
|
|
1731
|
+
const kept = reals.find((r) => r.dir === keep[name]);
|
|
1732
|
+
const primary = kept ?? reals.find((r) => r.dir === agents) ?? reals[0];
|
|
1733
|
+
if (!kept && reals.some((r) => r.hash !== primary.hash)) {
|
|
1443
1734
|
report.conflicts.push(conflictOf(name, null, reals, enabled, globalReadsOf));
|
|
1444
1735
|
continue;
|
|
1445
1736
|
}
|
|
1446
|
-
const steps = reals.filter((r) => r !== primary).map((r) => ({ kind: "replace", path: r.path, target: primary.path }));
|
|
1737
|
+
const steps = reals.filter((r) => r !== primary).map((r) => ({ kind: "replace", path: r.path, target: primary.path, ...r.hash !== primary.hash ? { differs: true } : {} }));
|
|
1447
1738
|
report.plans.push({ name, root: null, primary: primary.path, steps });
|
|
1448
1739
|
}
|
|
1449
1740
|
return report;
|
|
@@ -1481,20 +1772,27 @@ function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
|
|
|
1481
1772
|
return { name: plan.name, root: plan.root, applied, held };
|
|
1482
1773
|
}
|
|
1483
1774
|
function describeStep(step, root) {
|
|
1484
|
-
const shown = root ? relativeTo(root, step.path) : step.path
|
|
1775
|
+
const shown = root ? relativeTo(root, step.path) : tildify(step.path);
|
|
1485
1776
|
switch (step.kind) {
|
|
1486
1777
|
case "link":
|
|
1487
1778
|
return `link ${shown}`;
|
|
1488
1779
|
case "unlink":
|
|
1489
1780
|
return `remove extra link ${shown}`;
|
|
1490
1781
|
case "replace":
|
|
1491
|
-
return `${shown}: duplicate copy \u2192 link`;
|
|
1782
|
+
return `${shown}: ${step.differs ? "differing" : "duplicate"} copy \u2192 link`;
|
|
1492
1783
|
case "remove":
|
|
1493
|
-
return `remove duplicate copy ${shown}`;
|
|
1784
|
+
return `remove ${step.differs ? "differing" : "duplicate"} copy ${shown}`;
|
|
1494
1785
|
}
|
|
1495
1786
|
}
|
|
1496
1787
|
function copyLabel(conflict, dir) {
|
|
1497
|
-
return
|
|
1788
|
+
return folderLabel(conflict.root, dir);
|
|
1789
|
+
}
|
|
1790
|
+
function folderLabel(root, dir) {
|
|
1791
|
+
return root ? relativeTo(root, dir) : tildify(dir);
|
|
1792
|
+
}
|
|
1793
|
+
function foldersOf(root, name) {
|
|
1794
|
+
const entries = entriesOf(root ? PROJECT_TIDY_DIRS.map((d) => join12(root, d)) : globalTidyDirs(), name);
|
|
1795
|
+
return { all: entries.map((e) => e.dir), real: entries.filter((e) => !e.link).map((e) => e.dir) };
|
|
1498
1796
|
}
|
|
1499
1797
|
|
|
1500
1798
|
// src/health.ts
|
|
@@ -1543,13 +1841,14 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1543
1841
|
issues.push({
|
|
1544
1842
|
id: `broken:${skill.name}`,
|
|
1545
1843
|
severity: "problem",
|
|
1546
|
-
title: `${skill.name}: broken link in ${skill.path
|
|
1844
|
+
title: `${skill.name}: broken link in ${tildify(dirname5(skill.path))}`,
|
|
1547
1845
|
detail: "It points at a folder that no longer exists, so it loads nothing.",
|
|
1548
1846
|
fix: {
|
|
1549
1847
|
label: "Remove the link",
|
|
1550
1848
|
run: () => {
|
|
1551
1849
|
const r = unloadGlobal(skill.path);
|
|
1552
|
-
|
|
1850
|
+
if (!r.ok) throw new Error(`${skill.name}: ${r.reason}`);
|
|
1851
|
+
return `Removed broken link ${skill.name}`;
|
|
1553
1852
|
}
|
|
1554
1853
|
}
|
|
1555
1854
|
});
|
|
@@ -1570,8 +1869,10 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1570
1869
|
});
|
|
1571
1870
|
}
|
|
1572
1871
|
const loadedHere = new Map(projects2.map((root) => [root, loadedIn(root)]));
|
|
1573
|
-
const
|
|
1574
|
-
|
|
1872
|
+
const namesIn2 = (root, agent = "claude-code") => [
|
|
1873
|
+
...new Set([...statuses.get(root), ...nested.get(root)].filter((s) => s.visibility.some((v) => v.id === agent && v.paths > 0)).map((s) => s.name))
|
|
1874
|
+
];
|
|
1875
|
+
const pluginAgentsIn = (plugin, root) => new Set(loadedHere.get(root).filter((m) => plugin.skills.includes(m.path)).flatMap((m) => m.harnesses));
|
|
1575
1876
|
const globalYours = new Set(loaded.filter((m) => m.movable).map((m) => m.name));
|
|
1576
1877
|
const installedYours = /* @__PURE__ */ new Set([...globalYours, ...[...statuses.values()].flat().map((s) => s.name)]);
|
|
1577
1878
|
const repoChoices = (plugin, root, names) => {
|
|
@@ -1579,7 +1880,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1579
1880
|
const copies = statuses.get(root).filter((s) => names.includes(s.name));
|
|
1580
1881
|
const allManaged = names.every((n) => copies.some((c) => c.name === n && c.managed));
|
|
1581
1882
|
return [
|
|
1582
|
-
{ label: `Turn the plugin ${plugin.id} off in ${where} only`, hint: "other repos keep it", run: () => orThrow(turnOffIn(plugin, root)) },
|
|
1883
|
+
{ label: `Turn the plugin ${plugin.id} off in ${where} only`, hint: "other repos keep it; in .claude/settings.local.json, a local-only file", run: () => orThrow(turnOffIn(plugin, root)) },
|
|
1583
1884
|
// Only copies skilllib installed: Your skills keeps them. A repo's own skill is the team's, never removed from here.
|
|
1584
1885
|
...names.length && allManaged ? [
|
|
1585
1886
|
{
|
|
@@ -1599,8 +1900,10 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1599
1900
|
const machinePlugins = claudePlugins(machine);
|
|
1600
1901
|
for (const plugin of machinePlugins) {
|
|
1601
1902
|
const names = plugin.skills.map((p) => basename6(p));
|
|
1602
|
-
const inRepos = projects2.filter((root) =>
|
|
1603
|
-
const
|
|
1903
|
+
const inRepos = projects2.filter((root) => pluginAgentsIn(plugin, root).has("claude-code")).map((root) => [root, namesIn2(root).filter((n) => names.includes(n))]).filter(([, d]) => d.length);
|
|
1904
|
+
const cursorRepos = enabledHarnesses().includes("claude-code") ? projects2.filter((root) => !pluginAgentsIn(plugin, root).has("claude-code") && pluginAgentsIn(plugin, root).has("cursor")).map((root) => [root, namesIn2(root, "cursor").filter((n) => names.includes(n))]).filter(([, d]) => d.length) : [];
|
|
1905
|
+
const claudeYours = /* @__PURE__ */ new Set([...globalYours, ...inRepos.flatMap(([, d]) => d)]);
|
|
1906
|
+
const loadedYours = /* @__PURE__ */ new Set([...claudeYours, ...cursorRepos.flatMap(([, d]) => d)]);
|
|
1604
1907
|
const dupes = names.filter((n) => libraryNames.has(n) || loadedYours.has(n));
|
|
1605
1908
|
if (!dupes.length) continue;
|
|
1606
1909
|
const others = plugin.skills.length - dupes.length;
|
|
@@ -1612,15 +1915,18 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1612
1915
|
};
|
|
1613
1916
|
const off = { label: `Turn the plugin ${plugin.id} off`, hint: "stays installed", run: () => orThrow(removePlugin(plugin, "off")) };
|
|
1614
1917
|
const bothLoad = dupes.filter((n) => loadedYours.has(n));
|
|
1918
|
+
const claudeBoth = dupes.filter((n) => claudeYours.has(n));
|
|
1919
|
+
const cursorWheres = cursorRepos.map(([root]) => basename6(root));
|
|
1615
1920
|
const notInstalled = dupes.filter((n) => !installedYours.has(n));
|
|
1616
1921
|
issues.push({
|
|
1617
1922
|
id: `plugin:${plugin.id}`,
|
|
1618
1923
|
severity: bothLoad.length ? "problem" : "suggestion",
|
|
1619
1924
|
title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}`,
|
|
1620
1925
|
detail: [
|
|
1621
|
-
|
|
1926
|
+
claudeBoth.length ? `Claude Code loads both (the plugin's as /${pluginName}:${claudeBoth[0]}). Your skill wins.` : "",
|
|
1927
|
+
cursorWheres.length ? `${cursorWheres.join(", ")} ${cursorWheres.length === 1 ? "turns" : "turn"} it off for Claude Code, but Cursor ignores repo settings and lists both there: only turning the plugin off or removing it changes that.` : "",
|
|
1622
1928
|
notInstalled.length ? `Your ${notInstalled.join(", ")} ${notInstalled.length === 1 ? "is" : "are"} only in Your skills, not installed anywhere: once the plugin is gone, add ${notInstalled.length === 1 ? "it" : "them"} where you need ${notInstalled.length === 1 ? "it" : "them"}.` : "",
|
|
1623
|
-
others ? `The plugin's other ${
|
|
1929
|
+
others ? `The plugin's other ${others === 1 ? "skill is" : `${others} skills are`} copied into Your skills first, so nothing is lost.` : "",
|
|
1624
1930
|
plugin.extras.length ? `It also brings ${plugin.extras.join(", ")}: ${plugin.synced ? "turning it off pauses those" : "removing it drops those, turning it off pauses them"}.` : "",
|
|
1625
1931
|
plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : "",
|
|
1626
1932
|
inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) => basename6(root)).join(", ")}.` : ""
|
|
@@ -1667,7 +1973,11 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1667
1973
|
detail: `${global.plans.map((p) => p.name).join(", ")}: each keeps one copy (in ~/.agents/skills when there's one) and the others become links. Restorable from Health.`,
|
|
1668
1974
|
fix: {
|
|
1669
1975
|
label: "Keep one copy of each",
|
|
1670
|
-
run: () =>
|
|
1976
|
+
run: () => {
|
|
1977
|
+
const r = applyAll(global.plans, "go");
|
|
1978
|
+
if (!r.skills) throw new Error("No global skill changed");
|
|
1979
|
+
return `${plural(r.skills, "global skill")} now ${r.skills === 1 ? "has" : "have"} one copy`;
|
|
1980
|
+
}
|
|
1671
1981
|
}
|
|
1672
1982
|
});
|
|
1673
1983
|
}
|
|
@@ -1679,7 +1989,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1679
1989
|
id: "review:global",
|
|
1680
1990
|
severity: "suggestion",
|
|
1681
1991
|
title: `${plural(unreviewed.length, "global skill")} of yours ${unreviewed.length === 1 ? "loads" : "load"} in every repo`,
|
|
1682
|
-
detail: `${unreviewed.join(", ")}.
|
|
1992
|
+
detail: `${unreviewed.join(", ")}. Move each to the repos that need it (skilllib \u2192 Global \u2192 Move it to the repos that need it\u2026), or mark the ones you want everywhere as global on purpose (skilllib global keep <name>).`
|
|
1683
1993
|
});
|
|
1684
1994
|
}
|
|
1685
1995
|
const enabled = enabledHarnesses();
|
|
@@ -1697,6 +2007,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1697
2007
|
run: () => {
|
|
1698
2008
|
const results = skills.map((m) => ({ m, ...linkGlobal(m.path, blindOf(m)) }));
|
|
1699
2009
|
const skipped = results.flatMap((r) => r.skipped);
|
|
2010
|
+
if (!results.some((r) => r.linked.length)) throw new Error(`Nothing linked${skipped.length ? `; skipped ${skipped.join(", ")} (a different skill has that name)` : ""}`);
|
|
1700
2011
|
return `Linked ${plural(results.filter((r) => r.linked.length).length, "global skill")} for ${agentNames(missing)}${skipped.length ? `; skipped ${skipped.join(", ")} (a different skill has that name)` : ""}`;
|
|
1701
2012
|
}
|
|
1702
2013
|
};
|
|
@@ -1714,28 +2025,69 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1714
2025
|
for (const root of projects2) {
|
|
1715
2026
|
const where = basename6(root);
|
|
1716
2027
|
const status2 = statuses.get(root);
|
|
1717
|
-
const
|
|
1718
|
-
if (
|
|
2028
|
+
const restorable = status2.filter((s) => s.state === "folder missing" && s.latest !== null);
|
|
2029
|
+
if (restorable.length > 0) {
|
|
1719
2030
|
issues.push({
|
|
1720
2031
|
id: `sync:${root}`,
|
|
1721
2032
|
severity: "problem",
|
|
1722
|
-
title: `${where}: ${plural(
|
|
1723
|
-
detail:
|
|
2033
|
+
title: `${where}: ${plural(restorable.length, "skill")} in skilllib.json not installed`,
|
|
2034
|
+
detail: `${restorable.map((s) => s.name).join(", ")}: the folder is missing`,
|
|
1724
2035
|
fix: {
|
|
1725
2036
|
label: "Sync the project",
|
|
1726
2037
|
run: () => {
|
|
1727
2038
|
const changes = syncProject(root);
|
|
1728
|
-
|
|
2039
|
+
const done = changes.filter((c) => c.action !== "skipped");
|
|
2040
|
+
const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
|
|
2041
|
+
if (!done.length) throw new Error(`${where}: nothing restored${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
|
|
2042
|
+
return `${where}: restored ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
|
|
1729
2043
|
}
|
|
1730
2044
|
}
|
|
1731
2045
|
});
|
|
1732
2046
|
}
|
|
2047
|
+
for (const s of status2.filter((s2) => s2.state === "folder missing" && s2.latest === null))
|
|
2048
|
+
issues.push({
|
|
2049
|
+
id: `unavailable:${root}:${s.name}`,
|
|
2050
|
+
severity: "problem",
|
|
2051
|
+
title: `${s.name}: pinned in ${where}'s skilllib.json, but not in your library`,
|
|
2052
|
+
detail: `Its folder is missing, and your library (${tildify(libraryDir())}) has no copy to restore: import it, or set SKILLLIB_HOME to the library that has it.`,
|
|
2053
|
+
// Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
|
|
2054
|
+
choices: [
|
|
2055
|
+
{
|
|
2056
|
+
label: "Remove it from skilllib.json",
|
|
2057
|
+
hint: `${where} stops pinning it`,
|
|
2058
|
+
run: () => {
|
|
2059
|
+
const r = removeSkill(root, s.name);
|
|
2060
|
+
if (r.action !== "removed") throw new Error(`${s.name}: ${r.reason}`);
|
|
2061
|
+
return `${s.name}: removed from ${where}'s skilllib.json`;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
]
|
|
2065
|
+
});
|
|
2066
|
+
const behind = status2.filter((s) => s.state === "update available");
|
|
2067
|
+
if (behind.length > 0)
|
|
2068
|
+
issues.push({
|
|
2069
|
+
id: `update:${root}`,
|
|
2070
|
+
severity: "suggestion",
|
|
2071
|
+
title: `${where}: ${plural(behind.length, "skill")} out of date`,
|
|
2072
|
+
detail: behind.map((s) => `${s.name} (v${s.version} \u2192 v${s.latest})`).join(", "),
|
|
2073
|
+
fix: {
|
|
2074
|
+
label: "Update to the newest versions",
|
|
2075
|
+
run: () => {
|
|
2076
|
+
const changes = updateProject(root, behind.map((s) => s.name));
|
|
2077
|
+
const done = changes.filter((c) => c.action !== "skipped");
|
|
2078
|
+
const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
|
|
2079
|
+
if (!done.length) throw new Error(`${where}: nothing updated${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
|
|
2080
|
+
return `${where}: updated ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
});
|
|
1733
2084
|
const tidy2 = planProjectTidy(root);
|
|
1734
2085
|
if (tidy2.plans.length) {
|
|
1735
|
-
const
|
|
1736
|
-
const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p,
|
|
2086
|
+
const git3 = gitInfo(root);
|
|
2087
|
+
const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p, git3).map((s) => describeStep(s, root)));
|
|
1737
2088
|
const run = (mode) => () => {
|
|
1738
2089
|
const r = applyAll(tidy2.plans, mode);
|
|
2090
|
+
if (!r.skills) throw new Error(`${where}: nothing tidied${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`);
|
|
1739
2091
|
return `${where}: tidied ${plural(r.skills, "skill")}${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`;
|
|
1740
2092
|
};
|
|
1741
2093
|
issues.push({
|
|
@@ -1752,14 +2104,24 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1752
2104
|
});
|
|
1753
2105
|
}
|
|
1754
2106
|
issues.push(...tidy2.conflicts.map(conflictIssue));
|
|
1755
|
-
const
|
|
2107
|
+
const blindHere = status2.filter((s) => s.state !== "folder missing" && s.visibility.some((v) => v.paths === 0)).map((s) => ({ s, would: linkEverywhere(root, s.name, s.location, { dryRun: true }) }));
|
|
2108
|
+
for (const { s, would } of blindHere.filter(({ would: w }) => !w.created.length && !w.blocked.length && w.uncommitted.length))
|
|
2109
|
+
issues.push({
|
|
2110
|
+
id: `uncommitted:${root}:${s.name}`,
|
|
2111
|
+
severity: "suggestion",
|
|
2112
|
+
title: `${s.name} in ${where}: ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))} can't use it until it's committed`,
|
|
2113
|
+
detail: `Git tracks ${would.uncommitted.join(", ")}, but not ${s.location}/${s.name}: a link there would be broken for teammates. Commit it, then link it (\`skilllib link --allow-tracked\`, or from Health).`
|
|
2114
|
+
});
|
|
2115
|
+
const partial = blindHere.filter(({ would }) => would.created.length || would.blocked.length).map(({ s }) => s);
|
|
1756
2116
|
if (partial.length) {
|
|
1757
2117
|
const missing = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
|
|
1758
|
-
const tracked =
|
|
2118
|
+
const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
|
|
1759
2119
|
const run = (allowTracked) => () => {
|
|
1760
2120
|
if (allowTracked) allowTrackedLinks(root);
|
|
1761
2121
|
const r = linkAll(root, { allowTracked });
|
|
1762
|
-
|
|
2122
|
+
const notes = `${r.blocked.length ? `; skipped ${r.blocked.join(", ")} (git tracks ${r.blocked.length === 1 ? "it" : "them"})` : ""}${r.uncommitted.length ? `; ${r.uncommitted.join(", ")} not linked into folders git tracks (not committed, so teammates would get broken links)` : ""}`;
|
|
2123
|
+
if (!r.linked.length) throw new Error(`${where}: nothing linked${notes}`);
|
|
2124
|
+
return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing)}${notes}`;
|
|
1763
2125
|
};
|
|
1764
2126
|
issues.push({
|
|
1765
2127
|
id: `usable:${root}`,
|
|
@@ -1768,20 +2130,40 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1768
2130
|
detail: `${partial.map((s) => `${s.name} (not ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))})`).join(", ")}. Adding links makes the one real copy reach every agent you use; nothing is copied or moved.${tracked.length ? ` Git tracks ${tracked.join(", ")}, so links there show in git status.` : ""}`,
|
|
1769
2131
|
...tracked.length ? {
|
|
1770
2132
|
choices: [
|
|
1771
|
-
|
|
2133
|
+
// Only when some link goes where git doesn't track: otherwise this one would link nothing.
|
|
2134
|
+
...blindHere.some(({ would }) => would.created.length) ? [{ label: "Add links, but not where git tracks the folder", hint: `skips ${tracked.join(", ")}`, run: run(false) }] : [],
|
|
1772
2135
|
{ label: "Add links everywhere", hint: `remembered for ${where}; git will see them`, run: run(true) }
|
|
1773
2136
|
]
|
|
1774
2137
|
} : { fix: { label: "Add the links", run: run(false) } }
|
|
1775
2138
|
});
|
|
1776
2139
|
}
|
|
1777
2140
|
const differing = new Set(tidy2.conflicts.map((c) => c.name));
|
|
2141
|
+
const git2 = gitInfo(root);
|
|
2142
|
+
const committed = (s) => ["committed", "changed"].includes(git2?.of(relativeTo(root, s.path)) ?? "");
|
|
1778
2143
|
for (const s of status2) {
|
|
1779
2144
|
if (isLink(s.path) || differing.has(s.name)) continue;
|
|
1780
|
-
if (s.state === "local only") {
|
|
2145
|
+
if (s.state === "local only" && committed(s)) {
|
|
1781
2146
|
issues.push({
|
|
1782
2147
|
id: `local:${root}:${s.name}`,
|
|
1783
2148
|
severity: "suggestion",
|
|
1784
|
-
title: `${s.name}
|
|
2149
|
+
title: `${s.name} in ${where} isn't in your library`,
|
|
2150
|
+
detail: "Copy it into the library to reuse it in other projects. The repo's copy stays as it is: it's the team's (committed).",
|
|
2151
|
+
choices: [
|
|
2152
|
+
{
|
|
2153
|
+
label: "Copy into the library",
|
|
2154
|
+
hint: `${where}'s copy stays as it is`,
|
|
2155
|
+
run: () => {
|
|
2156
|
+
importSkill(s.path);
|
|
2157
|
+
return `${s.name} copied into your library; ${where}'s copy stays as it is`;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
]
|
|
2161
|
+
});
|
|
2162
|
+
} else if (s.state === "local only") {
|
|
2163
|
+
issues.push({
|
|
2164
|
+
id: `local:${root}:${s.name}`,
|
|
2165
|
+
severity: "suggestion",
|
|
2166
|
+
title: `${s.name} in ${where} isn't in your library`,
|
|
1785
2167
|
detail: "Import it into the library to reuse it in other projects and keep it backed up. The repo then tracks it in skilllib.json.",
|
|
1786
2168
|
choices: [
|
|
1787
2169
|
{
|
|
@@ -1805,17 +2187,17 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1805
2187
|
});
|
|
1806
2188
|
}
|
|
1807
2189
|
if (s.state === "edited locally" || s.state === "edited locally, update available") {
|
|
1808
|
-
const
|
|
2190
|
+
const newer2 = s.state !== "edited locally";
|
|
1809
2191
|
const pinned = s.version ? `v${s.version}` : "the library version";
|
|
1810
2192
|
issues.push({
|
|
1811
2193
|
id: `edited:${root}:${s.name}`,
|
|
1812
2194
|
severity: "suggestion",
|
|
1813
2195
|
title: `${s.name} in ${where} has local edits`,
|
|
1814
|
-
detail: `It no longer matches ${pinned} from Your skills${
|
|
2196
|
+
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.`,
|
|
1815
2197
|
choices: [
|
|
1816
2198
|
{
|
|
1817
2199
|
label: "Save as a new version in Your skills",
|
|
1818
|
-
hint:
|
|
2200
|
+
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`,
|
|
1819
2201
|
run: () => {
|
|
1820
2202
|
importSkill(s.path, { force: true });
|
|
1821
2203
|
const c = addSkill(root, s.name);
|
|
@@ -1984,7 +2366,7 @@ function groupBy(items) {
|
|
|
1984
2366
|
}
|
|
1985
2367
|
var sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
|
|
1986
2368
|
function globalFrom(kind, origin, path) {
|
|
1987
|
-
if (kind === "global") return tildify(path.replace(/[\\/][^\\/]+$/, "")).replace(/\\/g, "/");
|
|
2369
|
+
if (kind === "global" || kind === "skilllib") return tildify(path.replace(/[\\/][^\\/]+$/, "")).replace(/\\/g, "/");
|
|
1988
2370
|
return origin;
|
|
1989
2371
|
}
|
|
1990
2372
|
var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "target", "vendor", "coverage"]);
|
|
@@ -1997,28 +2379,34 @@ function topLevel(root) {
|
|
|
1997
2379
|
}
|
|
1998
2380
|
function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
1999
2381
|
const agents = enabledHarnesses();
|
|
2000
|
-
const
|
|
2001
|
-
const
|
|
2002
|
-
const
|
|
2003
|
-
const
|
|
2004
|
-
|
|
2382
|
+
const inRepo = projectHere(root) === root;
|
|
2383
|
+
const nestedHere = inRepo ? nestedSkills(root, { enabled: agents }) : [];
|
|
2384
|
+
const git2 = gitInfo(root, [...new Set(nestedHere.map((s) => s.location))]);
|
|
2385
|
+
const group = (list2) => groupBy(
|
|
2386
|
+
list2.map((s) => {
|
|
2005
2387
|
const loadedBy = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
|
|
2006
2388
|
const behind = s.managed && s.latest !== null && s.version !== null && s.latest > s.version;
|
|
2389
|
+
const gitState = git2?.of(relativeTo(root, s.path));
|
|
2390
|
+
const repo = !s.managed && (s.state.startsWith("repo skill") || gitState === "committed" || gitState === "changed");
|
|
2391
|
+
const usual = s.state === "ok" || s.state === "local only" || repo && s.state !== "from npx skills";
|
|
2007
2392
|
const attrs = {
|
|
2008
|
-
source: s.managed ? "library" :
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2393
|
+
source: s.managed ? "library" : repo ? "repo" : "local",
|
|
2394
|
+
dir: s.location !== ".claude/skills" ? s.location : void 0,
|
|
2395
|
+
version: s.version ?? void 0,
|
|
2396
|
+
state: usual ? void 0 : s.state,
|
|
2397
|
+
latest: behind ? s.latest : void 0,
|
|
2398
|
+
git: gitState,
|
|
2399
|
+
agents: sameSet(loadedBy, agents) ? void 0 : loadedBy
|
|
2015
2400
|
};
|
|
2016
2401
|
return [JSON.stringify(attrs), s.name, () => ({ ...attrs, skills: [] })];
|
|
2017
2402
|
})
|
|
2018
2403
|
);
|
|
2404
|
+
const skills = group(inRepo ? projectStatus(root) : []);
|
|
2405
|
+
const nested = group(nestedHere);
|
|
2019
2406
|
const machine = machineSkills(agents);
|
|
2407
|
+
const loaded = inRepo ? skillsLoadedIn(root, machine, agents) : machine;
|
|
2020
2408
|
const global = groupBy(
|
|
2021
|
-
|
|
2409
|
+
loaded.filter((m) => !m.broken && m.harnesses.length > 0).map((m) => {
|
|
2022
2410
|
const from = globalFrom(m.kind, m.origin, m.path);
|
|
2023
2411
|
return [`${m.kind}\0${from}\0${m.harnesses.join()}`, m.name, () => ({ source: m.kind, from, agents: m.harnesses, skills: [] })];
|
|
2024
2412
|
})
|
|
@@ -2027,7 +2415,7 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2027
2415
|
for (const i of findIssues(inRepo ? [root] : [], machine, new Set(librarySkills().map((l) => l.name)))) {
|
|
2028
2416
|
const choices = i.choices?.map((c) => c.label);
|
|
2029
2417
|
const key = `${i.detail}\0${i.fix?.label ?? ""}\0${choices?.join() ?? ""}`;
|
|
2030
|
-
const issue = issues.get(key) ?? { problems: [], detail: i.detail,
|
|
2418
|
+
const issue = issues.get(key) ?? { problems: [], detail: i.detail, fix: i.fix?.label, choices };
|
|
2031
2419
|
issue.problems.push(i.title);
|
|
2032
2420
|
issues.set(key, issue);
|
|
2033
2421
|
}
|
|
@@ -2037,9 +2425,10 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2037
2425
|
files: topLevel(root),
|
|
2038
2426
|
agents,
|
|
2039
2427
|
skills,
|
|
2428
|
+
nested: nested.length > 0 ? nested : void 0,
|
|
2040
2429
|
global,
|
|
2041
2430
|
issues: [...issues.values()],
|
|
2042
|
-
|
|
2431
|
+
usesHere: Object.keys(usesHere).length > 0 ? usesHere : void 0
|
|
2043
2432
|
};
|
|
2044
2433
|
}
|
|
2045
2434
|
function firstSentence(description) {
|
|
@@ -2047,21 +2436,21 @@ function firstSentence(description) {
|
|
|
2047
2436
|
const sentence = line.match(/^(.{40,}?[.!?])(?:\s|$)/)?.[1] ?? line;
|
|
2048
2437
|
return sentence.length > 200 ? sentence.slice(0, 199).trimEnd() + "\u2026" : sentence;
|
|
2049
2438
|
}
|
|
2050
|
-
function libraryFor(
|
|
2439
|
+
function libraryFor(currentProject) {
|
|
2051
2440
|
const manifests = visibleProjects().map((p) => ({ name: basename7(p), skills: readManifest(p).skills }));
|
|
2052
|
-
const here =
|
|
2441
|
+
const here = currentProject ? new Set(projectStatus(currentProject).map((s) => s.name)) : null;
|
|
2053
2442
|
const all = librarySkills();
|
|
2054
|
-
const
|
|
2443
|
+
const describe2 = (s) => {
|
|
2055
2444
|
const projects2 = manifests.filter((m) => s.name in m.skills).map((m) => m.name);
|
|
2056
|
-
return { name: s.name, description: firstSentence(s.description),
|
|
2445
|
+
return { name: s.name, description: firstSentence(s.description), projects: projects2.length > 0 ? projects2 : void 0 };
|
|
2057
2446
|
};
|
|
2058
|
-
if (!here) return { skills: all.map(
|
|
2059
|
-
return { inThisProject: all.filter((s) => here.has(s.name)).map((s) => s.name), skills: all.filter((s) => !here.has(s.name)).map(
|
|
2447
|
+
if (!here) return { skills: all.map(describe2) };
|
|
2448
|
+
return { inThisProject: all.filter((s) => here.has(s.name)).map((s) => s.name), skills: all.filter((s) => !here.has(s.name)).map(describe2) };
|
|
2060
2449
|
}
|
|
2061
2450
|
|
|
2062
2451
|
// src/usage.ts
|
|
2063
|
-
import { createReadStream, existsSync as
|
|
2064
|
-
import { join as
|
|
2452
|
+
import { createReadStream, existsSync as existsSync12, readdirSync as readdirSync8, statSync as statSync4 } from "fs";
|
|
2453
|
+
import { join as join13 } from "path";
|
|
2065
2454
|
import { createInterface } from "readline";
|
|
2066
2455
|
var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
|
|
2067
2456
|
"add-dir",
|
|
@@ -2141,12 +2530,12 @@ function usesFromTranscriptLine(line) {
|
|
|
2141
2530
|
return [];
|
|
2142
2531
|
}
|
|
2143
2532
|
function transcriptFiles(sinceMs) {
|
|
2144
|
-
const base =
|
|
2145
|
-
if (!
|
|
2533
|
+
const base = join13(claudeDir(), "projects");
|
|
2534
|
+
if (!existsSync12(base)) return [];
|
|
2146
2535
|
return readdirSync8(base).flatMap((project) => {
|
|
2147
|
-
const dir =
|
|
2148
|
-
if (!
|
|
2149
|
-
return readdirSync8(dir).filter((f) => f.endsWith(".jsonl")).map((f) =>
|
|
2536
|
+
const dir = join13(base, project);
|
|
2537
|
+
if (!statSync4(dir).isDirectory()) return [];
|
|
2538
|
+
return readdirSync8(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join13(dir, f)).filter((f) => statSync4(f).mtimeMs >= sinceMs);
|
|
2150
2539
|
});
|
|
2151
2540
|
}
|
|
2152
2541
|
async function scanUsage(days) {
|
|
@@ -2210,29 +2599,40 @@ function parseArgs(argv) {
|
|
|
2210
2599
|
function colorState(state) {
|
|
2211
2600
|
if (state === "ok") return green(state);
|
|
2212
2601
|
if (state === "folder missing" || state === "not in library") return red(state);
|
|
2213
|
-
if (state === "local only" || state === "from npx skills") return dim(state);
|
|
2602
|
+
if (state === "local only" || state === "from npx skills" || state === "repo skill") return dim(state);
|
|
2214
2603
|
return yellow(state);
|
|
2215
2604
|
}
|
|
2216
2605
|
function withDirs(root, changes) {
|
|
2217
2606
|
const { skills } = readManifest(root);
|
|
2218
2607
|
return changes.map((c) => {
|
|
2219
|
-
const dep = c.action === "installed" || c.action === "updated" ? skills[c.name] : void 0;
|
|
2608
|
+
const dep = c.action === "installed" || c.action === "updated" || c.action === "reset" ? skills[c.name] : void 0;
|
|
2220
2609
|
return dep ? { ...c, dirs: [dep.dir ?? ".claude/skills", ...dep.links ?? []] } : c;
|
|
2221
2610
|
});
|
|
2222
2611
|
}
|
|
2223
2612
|
function printChanges(root, changes, args) {
|
|
2224
|
-
if (
|
|
2613
|
+
if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
|
|
2614
|
+
if (args.json) return json("changes", withDirs(root, changes));
|
|
2225
2615
|
for (const c of changes) {
|
|
2226
2616
|
if (c.blocked?.length) {
|
|
2227
2617
|
warn(`${c.name}: git tracks ${c.blocked.join(", ")} here, so no link was added there. Re-run with --allow-tracked to add it.`);
|
|
2228
2618
|
}
|
|
2229
|
-
if (c.
|
|
2619
|
+
if (c.uncommitted?.length) {
|
|
2620
|
+
warn(
|
|
2621
|
+
`${c.name}: git tracks ${c.uncommitted.join(", ")} here but not ${c.name}'s real copy, so no link was added there (teammates would get a broken link). Commit ${c.name} first, then run skilllib link --allow-tracked.`
|
|
2622
|
+
);
|
|
2623
|
+
}
|
|
2624
|
+
if (c.action === "skipped") error(`${c.name}: ${c.reason}`);
|
|
2230
2625
|
else {
|
|
2231
|
-
const icon = c.action === "removed" ? red("-") : green(c.action === "installed" ? "+" : "\u2191");
|
|
2626
|
+
const icon = c.action === "removed" ? red("-") : c.action === "reset" ? yellow("\u21BA") : green(c.action === "installed" ? "+" : "\u2191");
|
|
2232
2627
|
const versions = c.action === "removed" ? "" : c.from && c.from !== c.to ? ` v${c.from} \u2192 v${c.to}` : c.to ? ` v${c.to}` : "";
|
|
2233
2628
|
info(` ${icon} ${c.name}${versions} ${dim(c.action)}`);
|
|
2234
2629
|
}
|
|
2630
|
+
if (c.backedUp) info(dim(` local edits saved to ${tildify(c.backedUp)}; \`skilllib restore ${c.name}\` brings them back`));
|
|
2235
2631
|
}
|
|
2632
|
+
const done = changes.filter((c) => c.action !== "skipped").length;
|
|
2633
|
+
const skipped = changes.length - done;
|
|
2634
|
+
if (skipped && done) info(dim(`
|
|
2635
|
+
${done} done, ${skipped} skipped`));
|
|
2236
2636
|
}
|
|
2237
2637
|
function projectOfFactory(projects2) {
|
|
2238
2638
|
const sorted = [...projects2].sort((a, b) => b.length - a.length);
|
|
@@ -2250,35 +2650,51 @@ async function usageBySkill(days, onlyProject) {
|
|
|
2250
2650
|
const scoped = onlyProject ? uses.filter((u) => projectOf(u.cwd) === onlyProject) : uses;
|
|
2251
2651
|
return new Map(summarize(scoped, projectOf).map((s) => [s.skill, s]));
|
|
2252
2652
|
}
|
|
2253
|
-
function
|
|
2254
|
-
const root =
|
|
2255
|
-
|
|
2653
|
+
function requireProject() {
|
|
2654
|
+
const root = projectHere();
|
|
2655
|
+
if (root) return root;
|
|
2656
|
+
if (!isProjectCandidate(findProjectRoot())) {
|
|
2657
|
+
const stray = existsSync13(join14(userHome(), MANIFEST_FILE)) ? `, though ${tildify(join14(userHome(), MANIFEST_FILE))} makes it look like one` : "";
|
|
2658
|
+
throw new Error(`Your home folder isn't a project${stray}: skills added here would load in every repo. Run this inside a project.`);
|
|
2659
|
+
}
|
|
2660
|
+
throw new Error("Not in a git repo. Run this inside a project (or `git init` first).");
|
|
2256
2661
|
}
|
|
2257
2662
|
function lastUsed(summary) {
|
|
2258
2663
|
return summary ? summary.lastUsed.slice(0, 10) : "-";
|
|
2259
2664
|
}
|
|
2260
2665
|
async function status(args) {
|
|
2261
|
-
const root = findProjectRoot();
|
|
2262
2666
|
if (args.json) {
|
|
2263
|
-
const
|
|
2264
|
-
|
|
2265
|
-
|
|
2667
|
+
const root2 = findProjectRoot();
|
|
2668
|
+
const usage3 = await usageBySkill(args.days, root2);
|
|
2669
|
+
json("status", usableHere(root2, new Map([...usage3].map(([name, u]) => [name, u.uses]))));
|
|
2670
|
+
if (projectHere()) rememberProjects([root2]);
|
|
2266
2671
|
return;
|
|
2267
2672
|
}
|
|
2673
|
+
const root = requireProject();
|
|
2268
2674
|
const skills = projectStatus(root);
|
|
2269
2675
|
const nested = nestedSkills(root);
|
|
2270
2676
|
info(`${basename8(root)} ${dim(tildify(root))}
|
|
2271
2677
|
`);
|
|
2272
|
-
if (skills.length === 0 && nested.length === 0)
|
|
2273
|
-
info("No skills in this project yet. Add one from your library with: skilllib add <name>");
|
|
2274
|
-
return;
|
|
2275
|
-
}
|
|
2678
|
+
if (skills.length === 0 && nested.length === 0) info("No skills in this project yet. Add one from your library with: skilllib add <name>");
|
|
2276
2679
|
const usage2 = await usageBySkill(args.days, root);
|
|
2680
|
+
const git2 = gitInfo(root, [...new Set(nested.map((s) => s.location))]);
|
|
2681
|
+
const committed = (path) => ["committed", "changed"].includes(git2?.of(relativeTo(root, path)) ?? "");
|
|
2682
|
+
const asRepo = {
|
|
2683
|
+
"local only": "repo skill",
|
|
2684
|
+
"untracked copy of library skill": "repo skill, in library",
|
|
2685
|
+
"untracked, differs from library": "repo skill, differs from library"
|
|
2686
|
+
};
|
|
2687
|
+
const stateOf = (s) => colorState(committed(s.path) && asRepo[s.state] || s.state);
|
|
2688
|
+
const agentsOf = (s) => {
|
|
2689
|
+
const ids = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
|
|
2690
|
+
return ids.length === s.visibility.length ? dim("all") : ids.length ? ids.map((id) => harness(id).name).join(", ") : red("none");
|
|
2691
|
+
};
|
|
2277
2692
|
if (skills.length) {
|
|
2278
2693
|
table(
|
|
2279
2694
|
skills.map((s) => ({
|
|
2280
2695
|
Skill: s.name,
|
|
2281
|
-
Status: s.source ? dim(`npx skills: ${s.source}`) :
|
|
2696
|
+
Status: s.source ? dim(`npx skills: ${s.source}`) : stateOf(s),
|
|
2697
|
+
Agents: agentsOf(s),
|
|
2282
2698
|
[`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0),
|
|
2283
2699
|
"Last used": lastUsed(usage2.get(s.name))
|
|
2284
2700
|
}))
|
|
@@ -2290,7 +2706,8 @@ async function status(args) {
|
|
|
2290
2706
|
nested.map((s) => ({
|
|
2291
2707
|
Skill: s.name,
|
|
2292
2708
|
Folder: s.location,
|
|
2293
|
-
Status:
|
|
2709
|
+
Status: stateOf(s),
|
|
2710
|
+
Agents: agentsOf(s),
|
|
2294
2711
|
[`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0)
|
|
2295
2712
|
}))
|
|
2296
2713
|
);
|
|
@@ -2298,18 +2715,32 @@ async function status(args) {
|
|
|
2298
2715
|
const states = new Set(skills.map((s) => s.state));
|
|
2299
2716
|
const hints = [];
|
|
2300
2717
|
if (states.has("update available")) hints.push("`skilllib update` to install the newest versions");
|
|
2301
|
-
|
|
2718
|
+
const missing = skills.filter((s) => s.state === "folder missing");
|
|
2719
|
+
if (missing.some((s) => s.latest !== null)) hints.push("`skilllib sync` to restore missing skills");
|
|
2720
|
+
const notInLibrary = missing.filter((s) => s.latest === null).map((s) => s.name);
|
|
2721
|
+
if (notInLibrary.length)
|
|
2722
|
+
hints.push(`${notInLibrary.join(", ")} ${notInLibrary.length === 1 ? "isn't" : "aren't"} in your library (${tildify(libraryDir())}): \`skilllib import <dir>\` to add, or set SKILLLIB_HOME to the library that has ${notInLibrary.length === 1 ? "it" : "them"}`);
|
|
2302
2723
|
if (states.has("edited locally") || states.has("edited locally, update available"))
|
|
2303
2724
|
hints.push("`skilllib import .claude/skills/<name> --force` to save local edits to the library");
|
|
2304
2725
|
if (states.has("local only")) hints.push("`skilllib import .claude/skills/<name>` to add a local skill to the library");
|
|
2305
2726
|
if (states.has("untracked copy of library skill") || states.has("untracked, differs from library"))
|
|
2306
2727
|
hints.push("`skilllib add <name>` to manage an untracked skill (--force if it differs)");
|
|
2307
2728
|
if (hints.length > 0) info("\n" + hints.map((h) => dim(`\u2192 ${h}`)).join("\n"));
|
|
2729
|
+
const here = usableHere(root);
|
|
2730
|
+
const globals = new Set(here.global.flatMap((g) => g.skills)).size;
|
|
2731
|
+
if (globals) info(`
|
|
2732
|
+
${globals} global ${globals === 1 ? "skill also loads" : "skills also load"} here ${dim("(your global folders, plugins, claude.ai): `skilllib` lists them")}`);
|
|
2733
|
+
const problems = here.issues.reduce((n, i) => n + i.problems.length, 0);
|
|
2734
|
+
if (problems) warn(`${problems} ${problems === 1 ? "issue" : "issues"} here or in your global skills: \`skilllib doctor\` lists them with fixes`);
|
|
2308
2735
|
rememberProjects([root]);
|
|
2309
2736
|
}
|
|
2737
|
+
async function overview(args) {
|
|
2738
|
+
if (args.json || projectHere()) return status(args);
|
|
2739
|
+
info("Not in a project. `skilllib projects` lists yours, `skilllib list` your library, `skilllib global` your global skills.");
|
|
2740
|
+
}
|
|
2310
2741
|
async function list(args) {
|
|
2311
2742
|
if (args.json) {
|
|
2312
|
-
return json(libraryFor(
|
|
2743
|
+
return json("list", libraryFor(projectHere()));
|
|
2313
2744
|
}
|
|
2314
2745
|
const skills = librarySkills();
|
|
2315
2746
|
if (skills.length === 0) {
|
|
@@ -2337,14 +2768,14 @@ async function list(args) {
|
|
|
2337
2768
|
);
|
|
2338
2769
|
}
|
|
2339
2770
|
const libraryNames = new Set(skills.map((s) => s.name));
|
|
2340
|
-
const globals = machineSkills().filter((m) => m.movable && !m.broken);
|
|
2771
|
+
const globals = [...new Set(machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.name))];
|
|
2341
2772
|
if (globals.length > 0) {
|
|
2342
2773
|
info("");
|
|
2343
|
-
warn(`${globals.length} of your global skill
|
|
2344
|
-
const missing = globals.filter((g) => !libraryNames.has(g
|
|
2774
|
+
warn(`${globals.length} of your global ${globals.length === 1 ? "skill still loads" : "skills still load"} in every project`);
|
|
2775
|
+
const missing = globals.filter((g) => !libraryNames.has(g));
|
|
2345
2776
|
info(
|
|
2346
2777
|
dim(
|
|
2347
|
-
missing.length > 0 ? `\u2192 \`skilllib import --global\` copies ${missing.length} of them into the library` : "\u2192 all are in your library; run `skilllib` \u2192 Global
|
|
2778
|
+
missing.length > 0 ? `\u2192 \`skilllib import --global\` copies ${missing.length} of them into the library` : "\u2192 all are in your library; run `skilllib` \u2192 Global to stop loading them everywhere"
|
|
2348
2779
|
)
|
|
2349
2780
|
);
|
|
2350
2781
|
}
|
|
@@ -2353,11 +2784,16 @@ function projects(args) {
|
|
|
2353
2784
|
const roots = visibleProjects();
|
|
2354
2785
|
if (args.json) {
|
|
2355
2786
|
return json(
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2787
|
+
"projects",
|
|
2788
|
+
roots.map(
|
|
2789
|
+
(root) => ({
|
|
2790
|
+
name: basename8(root),
|
|
2791
|
+
path: root,
|
|
2792
|
+
skills: projectStatus(root).map(
|
|
2793
|
+
(s) => ({ name: s.name, managed: s.managed, state: s.state, version: s.version, latest: s.latest })
|
|
2794
|
+
)
|
|
2795
|
+
})
|
|
2796
|
+
)
|
|
2361
2797
|
);
|
|
2362
2798
|
}
|
|
2363
2799
|
if (roots.length === 0) {
|
|
@@ -2384,7 +2820,7 @@ function add(args) {
|
|
|
2384
2820
|
error("Usage: skilllib add <skill-name>... [--force]");
|
|
2385
2821
|
process.exit(1);
|
|
2386
2822
|
}
|
|
2387
|
-
const root =
|
|
2823
|
+
const root = requireProject();
|
|
2388
2824
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
2389
2825
|
printChanges(root, args.positional.map((name) => addSkill(root, name, { force: args.force })), args);
|
|
2390
2826
|
rememberProjects([root]);
|
|
@@ -2394,29 +2830,38 @@ function remove(args) {
|
|
|
2394
2830
|
error("Usage: skilllib remove <skill-name>... [--force]");
|
|
2395
2831
|
process.exit(1);
|
|
2396
2832
|
}
|
|
2397
|
-
const root =
|
|
2833
|
+
const root = requireProject();
|
|
2398
2834
|
printChanges(root, args.positional.map((name) => removeSkill(root, name, { force: args.force })), args);
|
|
2399
2835
|
}
|
|
2400
2836
|
function sync(args) {
|
|
2401
|
-
const roots = args.all ? visibleProjects() : [
|
|
2837
|
+
const roots = args.all ? visibleProjects() : [requireProject()];
|
|
2402
2838
|
const results = roots.map((root) => {
|
|
2403
2839
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
2404
2840
|
const changes = syncProject(root, { force: args.force });
|
|
2841
|
+
if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
|
|
2405
2842
|
if (!args.json) {
|
|
2406
2843
|
if (args.all) info(`${basename8(root)} ${dim(tildify(root))}`);
|
|
2407
|
-
if (changes.length === 0)
|
|
2408
|
-
|
|
2844
|
+
if (changes.length === 0) {
|
|
2845
|
+
if (existsSync13(join14(root, MANIFEST_FILE))) success("Installed versions match skilllib.json");
|
|
2846
|
+
else info(dim("No skilllib.json: nothing to sync"));
|
|
2847
|
+
} else printChanges(root, changes, args);
|
|
2409
2848
|
}
|
|
2410
2849
|
return { project: basename8(root), path: root, changes: withDirs(root, changes) };
|
|
2411
2850
|
});
|
|
2412
|
-
if (args.json)
|
|
2851
|
+
if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
|
|
2413
2852
|
if (!args.all) rememberProjects(roots);
|
|
2414
2853
|
}
|
|
2415
2854
|
function update(args) {
|
|
2416
|
-
const roots = args.all ? visibleProjects() : [
|
|
2855
|
+
const roots = args.all ? visibleProjects() : [requireProject()];
|
|
2417
2856
|
const results = roots.map((root) => {
|
|
2418
2857
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
2419
|
-
const
|
|
2858
|
+
const names = args.positional.length ? args.positional : void 0;
|
|
2859
|
+
const changes = updateProject(root, names, { force: args.force });
|
|
2860
|
+
if (!args.all) {
|
|
2861
|
+
const { skills } = readManifest(root);
|
|
2862
|
+
for (const name of names ?? []) if (!(name in skills)) changes.push({ name, action: "skipped", reason: "not managed by skilllib in this project" });
|
|
2863
|
+
}
|
|
2864
|
+
if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
|
|
2420
2865
|
if (!args.json) {
|
|
2421
2866
|
if (args.all) info(`${basename8(root)} ${dim(tildify(root))}`);
|
|
2422
2867
|
if (changes.length === 0) success("Everything is on the latest version");
|
|
@@ -2424,14 +2869,14 @@ function update(args) {
|
|
|
2424
2869
|
}
|
|
2425
2870
|
return { project: basename8(root), path: root, changes: withDirs(root, changes) };
|
|
2426
2871
|
});
|
|
2427
|
-
if (args.json)
|
|
2872
|
+
if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
|
|
2428
2873
|
}
|
|
2429
2874
|
function outdated(args) {
|
|
2430
|
-
const roots = args.all ? visibleProjects() : [
|
|
2875
|
+
const roots = args.all ? visibleProjects() : [requireProject()];
|
|
2431
2876
|
const behind = roots.flatMap(
|
|
2432
2877
|
(root) => projectStatus(root).filter((s) => s.managed && s.latest !== null && s.version !== null && s.latest > s.version).map((s) => ({ project: basename8(root), path: root, skill: s.name, installed: s.version, latest: s.latest, state: s.state }))
|
|
2433
2878
|
);
|
|
2434
|
-
if (args.json) return json(behind);
|
|
2879
|
+
if (args.json) return json("outdated", behind);
|
|
2435
2880
|
const rows = behind.map((b) => ({ Project: b.project, Skill: b.skill, Installed: `v${b.installed}`, Latest: `v${b.latest}`, Status: colorState(b.state) }));
|
|
2436
2881
|
if (rows.length === 0) return success("Everything is on the latest version");
|
|
2437
2882
|
table(rows);
|
|
@@ -2439,7 +2884,7 @@ function outdated(args) {
|
|
|
2439
2884
|
\u2192 skilllib update${args.all ? " --all" : ""}`));
|
|
2440
2885
|
}
|
|
2441
2886
|
function importSkills(args) {
|
|
2442
|
-
const dirs = args.global ? machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.path) : args.positional.map((p) =>
|
|
2887
|
+
const dirs = args.global ? machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.path) : args.positional.map((p) => resolve7(p));
|
|
2443
2888
|
if (dirs.length === 0) {
|
|
2444
2889
|
error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
|
|
2445
2890
|
process.exit(1);
|
|
@@ -2458,9 +2903,9 @@ function importSkills(args) {
|
|
|
2458
2903
|
continue;
|
|
2459
2904
|
}
|
|
2460
2905
|
success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
|
|
2461
|
-
const skillsDir =
|
|
2462
|
-
const root =
|
|
2463
|
-
if (skillsDir ===
|
|
2906
|
+
const skillsDir = resolve7(dir, "..");
|
|
2907
|
+
const root = resolve7(skillsDir, "..", "..");
|
|
2908
|
+
if (skillsDir === join14(root, ".claude", "skills") && isProjectCandidate(root)) {
|
|
2464
2909
|
if (readManifest(root).skills[name]?.hash !== treeHash(dir)) {
|
|
2465
2910
|
const change = addSkill(root, name);
|
|
2466
2911
|
if (change.action !== "skipped") info(dim(` ${basename8(root)} now depends on ${name} v${change.to}`));
|
|
@@ -2468,7 +2913,7 @@ function importSkills(args) {
|
|
|
2468
2913
|
rememberProjects([root]);
|
|
2469
2914
|
}
|
|
2470
2915
|
}
|
|
2471
|
-
if (args.json) json(results);
|
|
2916
|
+
if (args.json) json("import", results);
|
|
2472
2917
|
}
|
|
2473
2918
|
function scan(args) {
|
|
2474
2919
|
for (const p of args.positional) addRoot(p);
|
|
@@ -2479,24 +2924,34 @@ function scan(args) {
|
|
|
2479
2924
|
}
|
|
2480
2925
|
const found = discoverProjects();
|
|
2481
2926
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
2482
|
-
const skillNames = (root) => [
|
|
2927
|
+
const skillNames = (root) => [
|
|
2928
|
+
.../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.flatMap((d) => skillDirsIn(join14(root, d))).map((d) => basename8(d)), ...nestedSkills(root).map((s) => s.name)])
|
|
2929
|
+
];
|
|
2483
2930
|
const withSkills = found.filter((root) => skillNames(root).length > 0);
|
|
2484
|
-
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project
|
|
2931
|
+
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
|
|
2485
2932
|
`);
|
|
2486
2933
|
table(
|
|
2487
2934
|
withSkills.map((root) => ({
|
|
2488
2935
|
Project: basename8(root),
|
|
2489
2936
|
Path: dim(tildify(root)),
|
|
2490
|
-
Skills:
|
|
2937
|
+
Skills: skillNames(root).map((n) => library.has(n) ? n : yellow(n)).join(", ")
|
|
2491
2938
|
}))
|
|
2492
2939
|
);
|
|
2493
2940
|
if (withSkills.length > 0) info(`
|
|
2494
|
-
${yellow("Yellow")} skills aren't in your library yet (import
|
|
2941
|
+
${yellow("Yellow")} skills aren't in your library yet (\`skilllib import <skill-dir>\`, or \`skilllib\` \u2192 Health).`);
|
|
2495
2942
|
}
|
|
2496
2943
|
function folders(args) {
|
|
2497
2944
|
const [action, ...paths] = args.positional;
|
|
2945
|
+
let failed = false;
|
|
2498
2946
|
if (action === "add") {
|
|
2499
|
-
for (const p of paths)
|
|
2947
|
+
for (const p of paths) {
|
|
2948
|
+
try {
|
|
2949
|
+
success(`Added ${tildify(addRoot(p))}`);
|
|
2950
|
+
} catch (e) {
|
|
2951
|
+
error(e.message);
|
|
2952
|
+
failed = true;
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2500
2955
|
discoverProjects();
|
|
2501
2956
|
} else if (action === "remove" || action === "rm") {
|
|
2502
2957
|
for (const p of paths) {
|
|
@@ -2508,12 +2963,24 @@ function folders(args) {
|
|
|
2508
2963
|
info(roots.length ? `Project folders:
|
|
2509
2964
|
${roots.map((r) => ` ${tildify(r)}`).join("\n")}` : "No project folders. Add one: skilllib folders add ~/Projects");
|
|
2510
2965
|
if (hidden.length) info(dim(`Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")} (skilllib unhide <path>)`));
|
|
2966
|
+
if (failed) process.exit(1);
|
|
2511
2967
|
}
|
|
2512
2968
|
function unhide(args) {
|
|
2969
|
+
if (args.positional.length === 0) {
|
|
2970
|
+
error("Usage: skilllib unhide <path>...");
|
|
2971
|
+
process.exit(1);
|
|
2972
|
+
}
|
|
2973
|
+
let failed = false;
|
|
2513
2974
|
for (const p of args.positional) {
|
|
2514
|
-
|
|
2515
|
-
success(`${tildify(
|
|
2975
|
+
const path = expandHome(p);
|
|
2976
|
+
if (setHidden(path, false)) success(`${tildify(path)} is visible again`);
|
|
2977
|
+
else {
|
|
2978
|
+
const { hidden } = readConfig();
|
|
2979
|
+
error(`${tildify(path)} wasn't hidden. ${hidden.length ? `Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")}` : "No projects are hidden."}`);
|
|
2980
|
+
failed = true;
|
|
2981
|
+
}
|
|
2516
2982
|
}
|
|
2983
|
+
if (failed) process.exit(1);
|
|
2517
2984
|
}
|
|
2518
2985
|
function newSkill(args) {
|
|
2519
2986
|
const [name, ...rest] = args.positional;
|
|
@@ -2526,14 +2993,14 @@ function newSkill(args) {
|
|
|
2526
2993
|
error(`Can't create ${name}: ${r.reason}`);
|
|
2527
2994
|
process.exit(1);
|
|
2528
2995
|
}
|
|
2529
|
-
success(`Created ${tildify(
|
|
2996
|
+
success(`Created ${tildify(join14(r.dir, "SKILL.md"))}`);
|
|
2530
2997
|
}
|
|
2531
2998
|
function restore(args) {
|
|
2532
2999
|
const backups = [...listBackups(), ...pluginBackups()];
|
|
2533
3000
|
const name = args.positional[0];
|
|
2534
3001
|
if (!name) {
|
|
2535
3002
|
if (backups.length === 0) return info("Nothing to restore.");
|
|
2536
|
-
table(backups.map((b) => ({ Skill: b.name, From:
|
|
3003
|
+
table(backups.map((b) => ({ Skill: b.name, From: backupFrom(b), Moved: b.movedAt })));
|
|
2537
3004
|
info(dim("\n\u2192 skilllib restore <name>"));
|
|
2538
3005
|
return;
|
|
2539
3006
|
}
|
|
@@ -2544,7 +3011,10 @@ function restore(args) {
|
|
|
2544
3011
|
}
|
|
2545
3012
|
const r = backup.kind === "plugin" ? restorePlugin(backup) : restoreBackup(backup);
|
|
2546
3013
|
if (r.ok) success(`Restored ${name} to ${tildify(r.to)}`);
|
|
2547
|
-
else
|
|
3014
|
+
else {
|
|
3015
|
+
error(`Can't restore ${name}: ${r.reason}`);
|
|
3016
|
+
process.exitCode = 1;
|
|
3017
|
+
}
|
|
2548
3018
|
}
|
|
2549
3019
|
async function doctor(args) {
|
|
2550
3020
|
const projects2 = visibleProjects();
|
|
@@ -2564,14 +3034,25 @@ async function doctor(args) {
|
|
|
2564
3034
|
})
|
|
2565
3035
|
);
|
|
2566
3036
|
}
|
|
3037
|
+
const fixed = (fix) => {
|
|
3038
|
+
const r = runFix(fix);
|
|
3039
|
+
if (!r.ok) process.exitCode = 1;
|
|
3040
|
+
return r;
|
|
3041
|
+
};
|
|
2567
3042
|
if (args.json) {
|
|
2568
3043
|
return json(
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
3044
|
+
"doctor",
|
|
3045
|
+
issues.map(
|
|
3046
|
+
({ id, severity, title, detail, fix, choices }) => ({
|
|
3047
|
+
id,
|
|
3048
|
+
severity,
|
|
3049
|
+
title,
|
|
3050
|
+
detail,
|
|
3051
|
+
fix: fix?.label,
|
|
3052
|
+
choices: choices?.map((c) => c.label),
|
|
3053
|
+
fixed: args.fix && fix ? fixed(fix) : void 0
|
|
3054
|
+
})
|
|
3055
|
+
)
|
|
2575
3056
|
);
|
|
2576
3057
|
}
|
|
2577
3058
|
if (issues.length === 0) return success("Everything looks good");
|
|
@@ -2580,7 +3061,7 @@ async function doctor(args) {
|
|
|
2580
3061
|
info(dim(` ${issue.detail}${issue.fix ? ` \u2192 ${issue.fix.label}` : ""}`));
|
|
2581
3062
|
if (issue.choices) info(dim(` choose in skilllib \u2192 Health: ${issue.choices.map((c) => c.label).join(" / ")}`));
|
|
2582
3063
|
if (args.fix && issue.fix) {
|
|
2583
|
-
const r =
|
|
3064
|
+
const r = fixed(issue.fix);
|
|
2584
3065
|
info(` ${r.ok ? green("\u2713") : red("\u2717")} ${r.message}`);
|
|
2585
3066
|
}
|
|
2586
3067
|
}
|
|
@@ -2590,7 +3071,7 @@ async function usage(args) {
|
|
|
2590
3071
|
const summaries = summarize(await scanUsage(args.days), projectOfFactory(knownProjects()));
|
|
2591
3072
|
if (args.json) {
|
|
2592
3073
|
const library2 = new Set(librarySkills().map((s) => s.name));
|
|
2593
|
-
return json({
|
|
3074
|
+
return json("usage", {
|
|
2594
3075
|
usageDays: args.days,
|
|
2595
3076
|
source: "Claude Code transcripts",
|
|
2596
3077
|
skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) => basename8(p)), inLibrary: library2.has(s.skill) })),
|
|
@@ -2602,7 +3083,7 @@ async function usage(args) {
|
|
|
2602
3083
|
return;
|
|
2603
3084
|
}
|
|
2604
3085
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
2605
|
-
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(
|
|
3086
|
+
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join14(claudeDir(), "projects"))})`)}
|
|
2606
3087
|
`);
|
|
2607
3088
|
table(
|
|
2608
3089
|
summaries.map((s) => ({
|
|
@@ -2621,21 +3102,21 @@ async function usage(args) {
|
|
|
2621
3102
|
}
|
|
2622
3103
|
function show(args) {
|
|
2623
3104
|
const name = args.positional[0];
|
|
2624
|
-
const dir = name ?
|
|
3105
|
+
const dir = name ? join14(libraryDir(), name) : "";
|
|
2625
3106
|
if (!name || !isSkillDir(dir)) {
|
|
2626
3107
|
error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
|
|
2627
3108
|
process.exit(1);
|
|
2628
3109
|
}
|
|
2629
3110
|
const { description } = readSkillInfo(dir);
|
|
2630
3111
|
if (args.json) {
|
|
2631
|
-
return json({
|
|
3112
|
+
return json("show", {
|
|
2632
3113
|
name,
|
|
2633
3114
|
description,
|
|
2634
3115
|
path: dir,
|
|
2635
3116
|
latest: latestVersion(name)?.version ?? null,
|
|
2636
3117
|
origin: libraryOrigins()[name] ?? "",
|
|
2637
3118
|
projects: knownProjects().filter((p) => name in readManifest(p).skills).map((p) => basename8(p)),
|
|
2638
|
-
skillMd: readFileSync9(
|
|
3119
|
+
skillMd: readFileSync9(join14(dir, "SKILL.md"), "utf-8")
|
|
2639
3120
|
});
|
|
2640
3121
|
}
|
|
2641
3122
|
info(`${name} ${dim(tildify(dir))}`);
|
|
@@ -2664,14 +3145,21 @@ function harnessesCommand(args) {
|
|
|
2664
3145
|
New installs go to ${installDirs(enabled).join(" (real copy) + ")}${installDirs(enabled).length > 1 ? " (link)" : ""}.`));
|
|
2665
3146
|
}
|
|
2666
3147
|
function link(args) {
|
|
2667
|
-
const roots = args.all ? visibleProjects() : [
|
|
3148
|
+
const roots = args.all ? visibleProjects() : [requireProject()];
|
|
2668
3149
|
for (const root of roots) {
|
|
2669
3150
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
2670
3151
|
const res = linkAll(root);
|
|
2671
3152
|
const label = args.all ? `${basename8(root)}: ` : "";
|
|
2672
3153
|
if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
|
|
2673
|
-
else if (!args.all) success("Every skill here is already usable by all your agents");
|
|
2674
|
-
if (res.blocked.length)
|
|
3154
|
+
else if (!args.all && !res.blocked.length && !res.uncommitted.length) success("Every skill here is already usable by all your agents");
|
|
3155
|
+
if (res.blocked.length) {
|
|
3156
|
+
process.exitCode = 1;
|
|
3157
|
+
error(`${label}git tracks ${res.blocked.join(", ")}, so ${res.linked.length ? "some skills weren't" : "nothing was"} linked there; re-run with --allow-tracked to link there too`);
|
|
3158
|
+
}
|
|
3159
|
+
if (res.uncommitted.length)
|
|
3160
|
+
warn(
|
|
3161
|
+
`${label}${res.uncommitted.join(", ")} ${res.uncommitted.length === 1 ? "isn't" : "aren't"} committed, so not linked into folders git tracks (teammates would get broken links); commit ${res.uncommitted.length === 1 ? "it" : "them"} first`
|
|
3162
|
+
);
|
|
2675
3163
|
}
|
|
2676
3164
|
}
|
|
2677
3165
|
function globalCommand(args) {
|
|
@@ -2692,14 +3180,41 @@ function globalCommand(args) {
|
|
|
2692
3180
|
const kept = keptGlobal();
|
|
2693
3181
|
if (!yours.length) return info("None of your skills load globally.");
|
|
2694
3182
|
for (const m of yours) info(`${kept.has(m.name) ? green("\u2713") : yellow("\u26A0")} ${m.name.padEnd(32)} ${dim(tildify(m.path))}`);
|
|
2695
|
-
const unreviewed = yours.filter((m) => !kept.has(m.name)).
|
|
3183
|
+
const unreviewed = new Set(yours.filter((m) => !kept.has(m.name)).map((m) => m.name)).size;
|
|
2696
3184
|
if (unreviewed) info(dim(`
|
|
2697
3185
|
${unreviewed} not reviewed. Keep one global on purpose: skilllib global keep <name>`));
|
|
2698
3186
|
}
|
|
2699
3187
|
function tidy(args) {
|
|
2700
3188
|
const names = args.positional;
|
|
2701
|
-
const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ?
|
|
2702
|
-
|
|
3189
|
+
const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve7(base, args.keep) : expandHome(args.keep)])) : {};
|
|
3190
|
+
if (args.keep && !names.length) {
|
|
3191
|
+
error("Usage: skilllib tidy <name>... --keep <folder>");
|
|
3192
|
+
process.exit(1);
|
|
3193
|
+
}
|
|
3194
|
+
const targets = args.global ? [null] : args.all ? visibleProjects() : [requireProject()];
|
|
3195
|
+
const problems = [];
|
|
3196
|
+
for (const name of names) {
|
|
3197
|
+
const where = targets.map((root) => ({ root, ...foldersOf(root, name) })).filter((w) => w.all.length);
|
|
3198
|
+
if (!where.length) {
|
|
3199
|
+
problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${basename8(targets[0])}`}`);
|
|
3200
|
+
continue;
|
|
3201
|
+
}
|
|
3202
|
+
if (!args.keep) continue;
|
|
3203
|
+
for (const { root, real } of where) {
|
|
3204
|
+
if (real.includes(keepFor(root)[name])) continue;
|
|
3205
|
+
const label = root ? `${basename8(root)} \xB7 ` : "";
|
|
3206
|
+
problems.push(
|
|
3207
|
+
`${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"}`
|
|
3208
|
+
);
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
if (problems.length) {
|
|
3212
|
+
for (const p of problems) error(p);
|
|
3213
|
+
process.exit(1);
|
|
3214
|
+
}
|
|
3215
|
+
const reports = targets.map(
|
|
3216
|
+
(root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label: basename8(root), report: planProjectTidy(root, { keep: keepFor(root) }) }
|
|
3217
|
+
);
|
|
2703
3218
|
let held = 0;
|
|
2704
3219
|
let changed = 0;
|
|
2705
3220
|
for (const { label, report } of reports) {
|
|
@@ -2714,6 +3229,7 @@ function tidy(args) {
|
|
|
2714
3229
|
held += args.dryRun ? 0 : result.held.length;
|
|
2715
3230
|
changed += result.applied.length;
|
|
2716
3231
|
}
|
|
3232
|
+
if (args.keep && conflicts.length) process.exitCode = 1;
|
|
2717
3233
|
for (const c of conflicts) {
|
|
2718
3234
|
warn(`${label} \xB7 ${c.name}: the copies differ`);
|
|
2719
3235
|
for (const copy of c.copies) {
|
|
@@ -2737,7 +3253,7 @@ function agentSkill(args) {
|
|
|
2737
3253
|
error(`Can't install the skilllib skill: ${r.reason}`);
|
|
2738
3254
|
process.exit(1);
|
|
2739
3255
|
}
|
|
2740
|
-
if (args.json) return json({ state: agentSkillState(), dirs: r.dirs });
|
|
3256
|
+
if (args.json) return json("agent-skill", { state: agentSkillState(), dirs: r.dirs });
|
|
2741
3257
|
success(`Installed in ${r.dirs.map((d) => tildify(d)).join(" and ")}`);
|
|
2742
3258
|
if (!onPath("skilllib")) warn("`skilllib` isn't on your PATH, so agents can't run it. Install it: npm install -g skilllib");
|
|
2743
3259
|
info(dim("New agent sessions can now answer questions like \u201Cwhich skills can you use here?\u201D"));
|
|
@@ -2745,27 +3261,30 @@ function agentSkill(args) {
|
|
|
2745
3261
|
}
|
|
2746
3262
|
if (action === "remove" || action === "rm") {
|
|
2747
3263
|
const removed = removeAgentSkill();
|
|
2748
|
-
if (args.json) return json({ state: agentSkillState(), removed });
|
|
3264
|
+
if (args.json) return json("agent-skill", { state: agentSkillState(), removed });
|
|
2749
3265
|
return removed.length ? success(`Removed from ${removed.map((d) => tildify(d)).join(" and ")}`) : info("The skilllib skill isn't installed.");
|
|
2750
3266
|
}
|
|
2751
3267
|
const state = agentSkillState();
|
|
2752
|
-
if (args.json) return json({ state, dirs: agentSkillDirs() });
|
|
3268
|
+
if (args.json) return json("agent-skill", { state, dirs: agentSkillDirs() });
|
|
2753
3269
|
if (state === "installed") success(`Your agents can use skilllib ${dim(`(${agentSkillDirs().map((d) => tildify(d)).join(", ")})`)}`);
|
|
2754
3270
|
else if (state === "outdated") warn("The skilllib skill is out of date. Run `skilllib agent-skill install` to update it.");
|
|
2755
3271
|
else info("Your agents don't know about skilllib yet. Run `skilllib agent-skill install` so you can ask them about your skills.");
|
|
2756
3272
|
}
|
|
2757
3273
|
|
|
2758
3274
|
export {
|
|
2759
|
-
userHome,
|
|
2760
3275
|
skilllibHome,
|
|
3276
|
+
PROJECT_SKILLS_DIR,
|
|
2761
3277
|
HARNESSES,
|
|
2762
3278
|
harness,
|
|
2763
3279
|
readSkillInfo,
|
|
2764
3280
|
versionHistory,
|
|
2765
3281
|
forgetLatest,
|
|
2766
3282
|
latestVersion,
|
|
2767
|
-
|
|
2768
|
-
|
|
3283
|
+
projectHere,
|
|
3284
|
+
readManifest,
|
|
3285
|
+
setJsonMode,
|
|
3286
|
+
error,
|
|
3287
|
+
tildify,
|
|
2769
3288
|
readConfig,
|
|
2770
3289
|
addRoot,
|
|
2771
3290
|
removeRoot,
|
|
@@ -2778,13 +3297,13 @@ export {
|
|
|
2778
3297
|
allowTrackedLinks,
|
|
2779
3298
|
keptGlobal,
|
|
2780
3299
|
setKeepGlobal,
|
|
2781
|
-
setPluginEnabled,
|
|
2782
3300
|
machineSkills,
|
|
2783
3301
|
skillsLoadedIn,
|
|
2784
3302
|
libraryOrigins,
|
|
2785
3303
|
recordOrigin,
|
|
2786
3304
|
librarySkills,
|
|
2787
3305
|
librarySkillDir,
|
|
3306
|
+
visibilityOf,
|
|
2788
3307
|
projectStatus,
|
|
2789
3308
|
linkEverywhere,
|
|
2790
3309
|
relinkDependency,
|
|
@@ -2802,17 +3321,23 @@ export {
|
|
|
2802
3321
|
createSkill,
|
|
2803
3322
|
listBackups,
|
|
2804
3323
|
restoreBackup,
|
|
2805
|
-
setJsonMode,
|
|
2806
|
-
error,
|
|
2807
3324
|
gitInfo,
|
|
2808
3325
|
relativeTo,
|
|
2809
3326
|
claudePlugins,
|
|
3327
|
+
installedPlugins,
|
|
3328
|
+
cursorPlugins,
|
|
3329
|
+
forgetClaudeBinary,
|
|
2810
3330
|
claudeBinary,
|
|
2811
3331
|
turnOffIn,
|
|
2812
|
-
|
|
3332
|
+
setPluginOn,
|
|
3333
|
+
uninstallPlugin,
|
|
3334
|
+
updatePlugin,
|
|
2813
3335
|
pluginBackups,
|
|
2814
3336
|
restorePlugin,
|
|
2815
3337
|
cursorPluginSkills,
|
|
3338
|
+
agentSkillState,
|
|
3339
|
+
installAgentSkill,
|
|
3340
|
+
removeAgentSkill,
|
|
2816
3341
|
refreshAgentSkill,
|
|
2817
3342
|
planProjectTidy,
|
|
2818
3343
|
planGlobalTidy,
|
|
@@ -2820,10 +3345,12 @@ export {
|
|
|
2820
3345
|
applyTidy,
|
|
2821
3346
|
describeStep,
|
|
2822
3347
|
copyLabel,
|
|
3348
|
+
addedAt,
|
|
2823
3349
|
scanUsage,
|
|
2824
3350
|
parseArgs,
|
|
2825
3351
|
projectOfFactory,
|
|
2826
3352
|
status,
|
|
3353
|
+
overview,
|
|
2827
3354
|
list,
|
|
2828
3355
|
projects,
|
|
2829
3356
|
add,
|