skilllib 1.6.0 → 1.7.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 +20 -15
- package/dist/{chunk-PHRUPZGX.js → chunk-AYAN7Q2X.js} +546 -218
- package/dist/index.js +7 -6
- package/dist/{tui-WYVSDGHD.js → tui-RU255WYX.js} +445 -168
- package/package.json +1 -1
- package/skills/skilllib/SKILL.md +5 -4
|
@@ -1,6 +1,41 @@
|
|
|
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 codexHome() {
|
|
28
|
+
return process.env.CODEX_HOME?.trim() || join(userHome(), ".codex");
|
|
29
|
+
}
|
|
30
|
+
function codexSystemDir() {
|
|
31
|
+
return process.env.SKILLLIB_CODEX_SYSTEM_DIR ?? "/etc/codex/skills";
|
|
32
|
+
}
|
|
33
|
+
var PROJECT_SKILLS_DIR = ".claude/skills";
|
|
34
|
+
var AGENTS_SKILLS_DIR = ".agents/skills";
|
|
35
|
+
var MANIFEST_FILE = "skilllib.json";
|
|
36
|
+
var OWN_SKILL_MARKER = ".managed-by-skilllib";
|
|
37
|
+
|
|
38
|
+
// src/output.ts
|
|
4
39
|
var jsonMode = false;
|
|
5
40
|
function setJsonMode(on) {
|
|
6
41
|
jsonMode = on;
|
|
@@ -19,8 +54,9 @@ var success = (msg) => out(`${green("\u2713")} ${msg}`);
|
|
|
19
54
|
var warn = (msg) => out(`${yellow("!")} ${msg}`);
|
|
20
55
|
var error = (msg) => console.error(`${red("\u2717")} ${msg}`);
|
|
21
56
|
function tildify(path) {
|
|
22
|
-
const home2
|
|
23
|
-
|
|
57
|
+
for (const home2 of /* @__PURE__ */ new Set([resolve2(userHome()), realPath(userHome())]))
|
|
58
|
+
if (path === home2 || path.startsWith(home2 + sep)) return ("~" + path.slice(home2.length)).replace(/\\/g, "/");
|
|
59
|
+
return path;
|
|
24
60
|
}
|
|
25
61
|
function truncate(s, max) {
|
|
26
62
|
const line = s.replace(/\s+/g, " ");
|
|
@@ -41,35 +77,9 @@ import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirS
|
|
|
41
77
|
import { join as join9 } from "path";
|
|
42
78
|
|
|
43
79
|
// 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
|
|
80
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
49
81
|
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";
|
|
82
|
+
import { join as join6, resolve as resolve4 } from "path";
|
|
73
83
|
|
|
74
84
|
// src/harnesses.ts
|
|
75
85
|
import { existsSync } from "fs";
|
|
@@ -155,8 +165,7 @@ function installDirs(enabled, keep) {
|
|
|
155
165
|
|
|
156
166
|
// src/project.ts
|
|
157
167
|
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";
|
|
168
|
+
import { dirname, join as join5, resolve as resolve3 } from "path";
|
|
160
169
|
|
|
161
170
|
// src/versions.ts
|
|
162
171
|
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
@@ -164,7 +173,7 @@ import { join as join4 } from "path";
|
|
|
164
173
|
|
|
165
174
|
// src/skills.ts
|
|
166
175
|
import { createHash } from "crypto";
|
|
167
|
-
import { cpSync, existsSync as existsSync2, readdirSync, readFileSync, realpathSync, rmSync, statSync } from "fs";
|
|
176
|
+
import { cpSync, existsSync as existsSync2, readdirSync, readFileSync, realpathSync as realpathSync2, rmSync, statSync } from "fs";
|
|
168
177
|
import { basename, join as join3, relative, sep as sep2 } from "path";
|
|
169
178
|
import { parse as parseYaml } from "yaml";
|
|
170
179
|
var IGNORED = /* @__PURE__ */ new Set([".DS_Store", ".git", "node_modules"]);
|
|
@@ -204,7 +213,7 @@ function treeHash(dir) {
|
|
|
204
213
|
}
|
|
205
214
|
function copySkill(from, to) {
|
|
206
215
|
rmSync(to, { recursive: true, force: true });
|
|
207
|
-
cpSync(
|
|
216
|
+
cpSync(realpathSync2(from), to, { recursive: true, dereference: true, filter: (src) => !IGNORED.has(basename(src)) });
|
|
208
217
|
}
|
|
209
218
|
function skillDirsIn(dir) {
|
|
210
219
|
if (!existsSync2(dir)) return [];
|
|
@@ -263,19 +272,17 @@ function getVersion(name, version) {
|
|
|
263
272
|
|
|
264
273
|
// src/project.ts
|
|
265
274
|
function findProjectRoot(from = process.cwd()) {
|
|
266
|
-
let dir =
|
|
275
|
+
let dir = resolve3(from);
|
|
267
276
|
while (true) {
|
|
268
277
|
if (existsSync4(join5(dir, MANIFEST_FILE)) || existsSync4(join5(dir, ".git"))) return dir;
|
|
269
278
|
const parent = dirname(dir);
|
|
270
|
-
if (parent === dir) return
|
|
279
|
+
if (parent === dir) return resolve3(from);
|
|
271
280
|
dir = parent;
|
|
272
281
|
}
|
|
273
282
|
}
|
|
274
|
-
function
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
function agentsSkillsDir(root) {
|
|
278
|
-
return join5(root, AGENTS_SKILLS_DIR);
|
|
283
|
+
function projectHere(from = process.cwd()) {
|
|
284
|
+
const root = findProjectRoot(from);
|
|
285
|
+
return isProjectCandidate(root) && (existsSync4(join5(root, ".git")) || existsSync4(join5(root, MANIFEST_FILE))) ? root : null;
|
|
279
286
|
}
|
|
280
287
|
function readManifest(root) {
|
|
281
288
|
const path = join5(root, MANIFEST_FILE);
|
|
@@ -315,14 +322,23 @@ function knownProjects() {
|
|
|
315
322
|
const path = projectsFile();
|
|
316
323
|
if (!existsSync4(path)) return [];
|
|
317
324
|
const list2 = JSON.parse(readFileSync3(path, "utf-8"));
|
|
318
|
-
return list2.filter((p) => existsSync4(p) && isProjectCandidate(p));
|
|
325
|
+
return onePerFolder(list2.filter((p) => existsSync4(p) && isProjectCandidate(p)));
|
|
326
|
+
}
|
|
327
|
+
function onePerFolder(paths) {
|
|
328
|
+
const byReal = /* @__PURE__ */ new Map();
|
|
329
|
+
for (const p of paths) {
|
|
330
|
+
const real = realPath(p);
|
|
331
|
+
const seen = byReal.get(real);
|
|
332
|
+
if (seen === void 0 || seen === real && p !== real) byReal.set(real, p);
|
|
333
|
+
}
|
|
334
|
+
return [...new Set(byReal.values())];
|
|
319
335
|
}
|
|
320
336
|
function isProjectCandidate(root) {
|
|
321
|
-
return
|
|
337
|
+
return realPath(root) !== realPath(userHome());
|
|
322
338
|
}
|
|
323
339
|
function rememberProjects(roots) {
|
|
324
|
-
const incoming = roots.map((r) =>
|
|
325
|
-
const all =
|
|
340
|
+
const incoming = roots.map((r) => resolve3(r)).filter(isProjectCandidate);
|
|
341
|
+
const all = onePerFolder([...knownProjects(), ...incoming]).sort();
|
|
326
342
|
mkdirSync2(skilllibHome(), { recursive: true });
|
|
327
343
|
writeFileSync2(projectsFile(), JSON.stringify(all, null, 2) + "\n");
|
|
328
344
|
}
|
|
@@ -344,10 +360,12 @@ function writeConfig(config) {
|
|
|
344
360
|
writeFileSync3(configFile(), JSON.stringify(config, null, 2) + "\n");
|
|
345
361
|
}
|
|
346
362
|
function expandHome(path) {
|
|
347
|
-
return
|
|
363
|
+
return resolve4(path.replace(/^~(?=$|\/)/, homedir2()));
|
|
348
364
|
}
|
|
349
365
|
function addRoot(path) {
|
|
350
366
|
const root = expandHome(path);
|
|
367
|
+
if (!existsSync5(root)) throw new Error(`${tildify(root)} doesn't exist`);
|
|
368
|
+
if (!statSync2(root).isDirectory()) throw new Error(`${tildify(root)} isn't a folder`);
|
|
351
369
|
const config = readConfig();
|
|
352
370
|
if (!config.roots.includes(root)) writeConfig({ ...config, roots: [...config.roots, root].sort() });
|
|
353
371
|
return root;
|
|
@@ -361,8 +379,10 @@ function removeRoot(path) {
|
|
|
361
379
|
}
|
|
362
380
|
function setHidden(project, hidden) {
|
|
363
381
|
const config = readConfig();
|
|
382
|
+
if (config.hidden.includes(project) === hidden) return false;
|
|
364
383
|
const rest = config.hidden.filter((h) => h !== project);
|
|
365
384
|
writeConfig({ ...config, hidden: hidden ? [...rest, project].sort() : rest });
|
|
385
|
+
return true;
|
|
366
386
|
}
|
|
367
387
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "vendor", "target", "Library", "Applications", "Pictures", "Music", "Movies"]);
|
|
368
388
|
function findProjects(dir, depth = 4) {
|
|
@@ -386,8 +406,8 @@ function discoverProjects() {
|
|
|
386
406
|
return visibleProjects();
|
|
387
407
|
}
|
|
388
408
|
function visibleProjects() {
|
|
389
|
-
const hidden = new Set(readConfig().hidden);
|
|
390
|
-
return knownProjects().filter((p) => !hidden.has(p));
|
|
409
|
+
const hidden = new Set(readConfig().hidden.map(realPath));
|
|
410
|
+
return knownProjects().filter((p) => !hidden.has(realPath(p)));
|
|
391
411
|
}
|
|
392
412
|
function enabledHarnesses() {
|
|
393
413
|
return readConfig().harnesses ?? HARNESSES.filter((h) => h.installed()).map((h) => h.id);
|
|
@@ -418,12 +438,12 @@ function setKeepGlobal(names, keep) {
|
|
|
418
438
|
|
|
419
439
|
// src/library.ts
|
|
420
440
|
import { execFileSync } from "child_process";
|
|
421
|
-
import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as
|
|
441
|
+
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
442
|
import { basename as basename3, dirname as dirname2, join as join8, relative as relative2, sep as sep3 } from "path";
|
|
423
443
|
|
|
424
444
|
// 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
|
|
445
|
+
import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
446
|
+
import { basename as basename2, join as join7, resolve as resolve5 } from "path";
|
|
427
447
|
function readJson(path) {
|
|
428
448
|
try {
|
|
429
449
|
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
@@ -432,24 +452,36 @@ function readJson(path) {
|
|
|
432
452
|
}
|
|
433
453
|
}
|
|
434
454
|
function skillsShLock() {
|
|
435
|
-
return readJson(join7(
|
|
455
|
+
return readJson(join7(userHome(), ".agents", ".skill-lock.json"))?.skills ?? {};
|
|
436
456
|
}
|
|
437
457
|
function projectSkillsLock(root) {
|
|
438
458
|
return readJson(join7(root, "skills-lock.json"))?.skills ?? {};
|
|
439
459
|
}
|
|
440
460
|
function realpathOrNull(path) {
|
|
441
461
|
try {
|
|
442
|
-
return
|
|
462
|
+
return realpathSync3(path);
|
|
443
463
|
} catch {
|
|
444
464
|
return null;
|
|
445
465
|
}
|
|
446
466
|
}
|
|
447
467
|
function harnessesReading(dir, enabled) {
|
|
448
|
-
|
|
468
|
+
const real = realPath(dir);
|
|
469
|
+
return enabled.filter((id) => HARNESSES.find((h) => h.id === id)?.globalDirs().some((d) => realPath(d) === real));
|
|
449
470
|
}
|
|
450
471
|
var systemDirs = () => [codexSystemDir()];
|
|
451
472
|
function globalSkillDirs() {
|
|
452
|
-
|
|
473
|
+
const seen = /* @__PURE__ */ new Set();
|
|
474
|
+
return [...new Set(HARNESSES.flatMap((h) => h.globalDirs()))].filter((d) => {
|
|
475
|
+
if (systemDirs().includes(d)) return false;
|
|
476
|
+
const real = realPath(d);
|
|
477
|
+
if (seen.has(real)) return false;
|
|
478
|
+
seen.add(real);
|
|
479
|
+
return true;
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
function globalDirAs(dir) {
|
|
483
|
+
const real = realPath(dir);
|
|
484
|
+
return globalSkillDirs().find((d) => realPath(d) === real) ?? dir;
|
|
453
485
|
}
|
|
454
486
|
function systemSkills(enabled) {
|
|
455
487
|
return systemDirs().flatMap(
|
|
@@ -492,7 +524,7 @@ function globalFolderSkills(enabled) {
|
|
|
492
524
|
const inAgents = e.dir === agentsDir || target_.includes(".agents/skills");
|
|
493
525
|
const locked = lock[e.name]?.source;
|
|
494
526
|
if (existsSync6(join7(e.path, OWN_SKILL_MARKER))) {
|
|
495
|
-
skills.push({ name: e.name, kind: "
|
|
527
|
+
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
528
|
continue;
|
|
497
529
|
}
|
|
498
530
|
skills.push({
|
|
@@ -516,8 +548,7 @@ function globalFolderSkills(enabled) {
|
|
|
516
548
|
return skills;
|
|
517
549
|
}
|
|
518
550
|
function tildify2(p) {
|
|
519
|
-
|
|
520
|
-
return shown.replace(/\\/g, "/");
|
|
551
|
+
return tildify(p).replace(/\\/g, "/");
|
|
521
552
|
}
|
|
522
553
|
function cursorBuiltInSkills(enabled) {
|
|
523
554
|
if (!enabled.includes("cursor")) return [];
|
|
@@ -637,7 +668,7 @@ function recordOrigin(name, origin) {
|
|
|
637
668
|
writeFileSync4(originsFile(), JSON.stringify({ ...origins, [name]: origin }, null, 2) + "\n");
|
|
638
669
|
}
|
|
639
670
|
function originFor(dir) {
|
|
640
|
-
const match = machineSkills().find((s) =>
|
|
671
|
+
const match = machineSkills().find((s) => resolve5(s.path) === resolve5(dir));
|
|
641
672
|
if (match) return match.kind === "global" ? "global folder" : `${match.kind}: ${match.origin}`;
|
|
642
673
|
const project = dir.split(/[\\/]\.(?:claude|agents)[\\/]skills[\\/]/)[0];
|
|
643
674
|
if (!project || project === dir) return dir;
|
|
@@ -658,7 +689,7 @@ function librarySkillDir(name) {
|
|
|
658
689
|
}
|
|
659
690
|
function realpathOrNull2(path) {
|
|
660
691
|
try {
|
|
661
|
-
return
|
|
692
|
+
return realpathSync4(path);
|
|
662
693
|
} catch {
|
|
663
694
|
return null;
|
|
664
695
|
}
|
|
@@ -797,20 +828,40 @@ function nestedSkills(root, { depth = 3, enabled = enabledHarnesses() } = {}) {
|
|
|
797
828
|
walk(root, depth);
|
|
798
829
|
return found.sort((a, b) => a.location.localeCompare(b.location) || a.name.localeCompare(b.name));
|
|
799
830
|
}
|
|
800
|
-
function
|
|
831
|
+
function sharedByGit(root, location, name) {
|
|
832
|
+
const copy = `${location}/${name}`;
|
|
833
|
+
if (isGitTracked(root, copy)) return true;
|
|
834
|
+
if (!isGitTracked(root, location)) return false;
|
|
835
|
+
try {
|
|
836
|
+
execFileSync("git", ["check-ignore", "-q", "--no-index", "--", copy], { cwd: root, stdio: "ignore" });
|
|
837
|
+
return false;
|
|
838
|
+
} catch {
|
|
839
|
+
return true;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
function linkEverywhere(root, name, location, { allowTracked = false, dryRun = false } = {}) {
|
|
801
843
|
const enabled = enabledHarnesses();
|
|
802
|
-
const result = { created: [], blocked: [] };
|
|
844
|
+
const result = { created: [], blocked: [], uncommitted: [] };
|
|
845
|
+
let shared;
|
|
803
846
|
for (const id of enabled) {
|
|
804
847
|
if (visibilityOf(root, name, [id])[0].paths > 0) continue;
|
|
805
848
|
const dir = harness(id).projectDirs[0];
|
|
806
849
|
const link2 = join8(root, dir, name);
|
|
807
|
-
if (entryExists(link2)) continue;
|
|
808
|
-
if (
|
|
809
|
-
|
|
810
|
-
|
|
850
|
+
if (entryExists(link2) || Object.values(result).some((dirs) => dirs.includes(dir))) continue;
|
|
851
|
+
if (isGitTracked(root, dir)) {
|
|
852
|
+
if (!(shared ??= sharedByGit(root, location, name))) {
|
|
853
|
+
if (!result.uncommitted.includes(dir)) result.uncommitted.push(dir);
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
if (!allowTracked && !readConfig().agentsDirOk?.includes(root)) {
|
|
857
|
+
if (!result.blocked.includes(dir)) result.blocked.push(dir);
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (!dryRun) {
|
|
862
|
+
mkdirSync5(join8(root, dir), { recursive: true });
|
|
863
|
+
linkDir(join8(root, location, name), link2);
|
|
811
864
|
}
|
|
812
|
-
mkdirSync5(join8(root, dir), { recursive: true });
|
|
813
|
-
linkDir(join8(root, location, name), link2);
|
|
814
865
|
result.created.push(dir);
|
|
815
866
|
}
|
|
816
867
|
return result;
|
|
@@ -826,7 +877,7 @@ function unlinkEverywhere(root, name) {
|
|
|
826
877
|
function relinkDependency(root, name, { allowTracked = false } = {}) {
|
|
827
878
|
const manifest = readManifest(root);
|
|
828
879
|
const dep = manifest.skills[name];
|
|
829
|
-
if (!dep) return { created: [], blocked: [] };
|
|
880
|
+
if (!dep) return { created: [], blocked: [], uncommitted: [] };
|
|
830
881
|
const result = linkEverywhere(root, name, dep.dir ?? PROJECT_SKILLS_DIR, { allowTracked });
|
|
831
882
|
if (result.created.length) {
|
|
832
883
|
manifest.skills[name] = { ...dep, links: [.../* @__PURE__ */ new Set([...dep.links ?? [], ...result.created])] };
|
|
@@ -837,13 +888,15 @@ function relinkDependency(root, name, { allowTracked = false } = {}) {
|
|
|
837
888
|
function linkAll(root, { allowTracked = false } = {}) {
|
|
838
889
|
const linked = [];
|
|
839
890
|
const blocked = /* @__PURE__ */ new Set();
|
|
891
|
+
const uncommitted = [];
|
|
840
892
|
for (const skill of projectStatus(root)) {
|
|
841
893
|
if (!skill.visibility.some((v) => v.paths === 0) || skill.state === "folder missing") continue;
|
|
842
894
|
const res = skill.managed ? relinkDependency(root, skill.name, { allowTracked }) : linkEverywhere(root, skill.name, skill.location, { allowTracked });
|
|
843
895
|
if (res.created.length) linked.push({ name: skill.name, into: res.created });
|
|
844
896
|
for (const dir of res.blocked) blocked.add(dir);
|
|
897
|
+
if (res.uncommitted.length) uncommitted.push(skill.name);
|
|
845
898
|
}
|
|
846
|
-
return { linked, blocked: [...blocked] };
|
|
899
|
+
return { linked, blocked: [...blocked], uncommitted };
|
|
847
900
|
}
|
|
848
901
|
function addSkill(root, name, { force = false, version, allowTracked = false } = {}) {
|
|
849
902
|
const target = version === void 0 ? latestVersion(name) : getVersion(name, version);
|
|
@@ -861,13 +914,17 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
861
914
|
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
915
|
};
|
|
863
916
|
}
|
|
917
|
+
let backedUp;
|
|
864
918
|
if (local !== target.hash || isLink(to)) {
|
|
865
919
|
mkdirSync5(join8(root, primary), { recursive: true });
|
|
866
920
|
if (isLink(to)) unlinkSync(to);
|
|
921
|
+
else if (local !== null && !knownContent) backedUp = stash(to, "edit-backup");
|
|
867
922
|
copySkill(versionDir(name, target.version), to);
|
|
868
923
|
}
|
|
869
924
|
const links = [];
|
|
870
925
|
const blocked = [];
|
|
926
|
+
const uncommitted = [];
|
|
927
|
+
let shared;
|
|
871
928
|
for (const dir of linkDirs) {
|
|
872
929
|
const link2 = join8(root, dir, name);
|
|
873
930
|
if (isLink(link2) && realpathOrNull2(link2) === realpathOrNull2(to)) {
|
|
@@ -875,9 +932,15 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
875
932
|
continue;
|
|
876
933
|
}
|
|
877
934
|
if (entryExists(link2)) continue;
|
|
878
|
-
if (!
|
|
879
|
-
|
|
880
|
-
|
|
935
|
+
if (!recorded?.links?.includes(dir) && isGitTracked(root, dir)) {
|
|
936
|
+
if (!(shared ??= sharedByGit(root, primary, name))) {
|
|
937
|
+
uncommitted.push(dir);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
if (!allowTracked && !readConfig().agentsDirOk?.includes(root)) {
|
|
941
|
+
blocked.push(dir);
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
881
944
|
}
|
|
882
945
|
mkdirSync5(join8(root, dir), { recursive: true });
|
|
883
946
|
linkDir(to, link2);
|
|
@@ -892,10 +955,12 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
892
955
|
writeManifest(root, manifest);
|
|
893
956
|
return {
|
|
894
957
|
name,
|
|
895
|
-
action: recorded
|
|
958
|
+
action: !recorded && local === null ? "installed" : backedUp && recorded?.version === target.version ? "reset" : "updated",
|
|
896
959
|
...recorded ? { from: recorded.version } : {},
|
|
897
960
|
to: target.version,
|
|
898
|
-
...blocked.length ? { blocked } : {}
|
|
961
|
+
...blocked.length ? { blocked } : {},
|
|
962
|
+
...uncommitted.length ? { uncommitted } : {},
|
|
963
|
+
...backedUp ? { backedUp } : {}
|
|
899
964
|
};
|
|
900
965
|
}
|
|
901
966
|
function removeSkill(root, name, { force = false } = {}) {
|
|
@@ -904,17 +969,17 @@ function removeSkill(root, name, { force = false } = {}) {
|
|
|
904
969
|
if (!recorded) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
|
|
905
970
|
const dir = join8(root, recorded.dir ?? PROJECT_SKILLS_DIR, name);
|
|
906
971
|
const local = treeHash(dir);
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
}
|
|
972
|
+
const edited = local !== null && local !== recorded.hash && !versionForHash(name, local);
|
|
973
|
+
if (edited && !force) return { name, action: "skipped", reason: "has local edits (use --force to delete anyway)" };
|
|
910
974
|
for (const linkDir2 of recorded.links ?? []) {
|
|
911
975
|
const link2 = join8(root, linkDir2, name);
|
|
912
976
|
if (isLink(link2)) unlinkSync(link2);
|
|
913
977
|
}
|
|
914
|
-
|
|
978
|
+
const backedUp = edited ? stash(dir, "edit-backup") : void 0;
|
|
979
|
+
if (!backedUp) rmSync2(dir, { recursive: true, force: true });
|
|
915
980
|
delete manifest.skills[name];
|
|
916
981
|
writeManifest(root, manifest);
|
|
917
|
-
return { name, action: "removed" };
|
|
982
|
+
return { name, action: "removed", ...backedUp ? { backedUp } : {} };
|
|
918
983
|
}
|
|
919
984
|
function syncProject(root, { force = false } = {}) {
|
|
920
985
|
const { skills } = readManifest(root);
|
|
@@ -925,7 +990,8 @@ function syncProject(root, { force = false } = {}) {
|
|
|
925
990
|
const pinned = dep.version > 0 && getVersion(name, dep.version) ? dep.version : void 0;
|
|
926
991
|
if (local !== null && local !== dep.hash && !force) return [{ name, action: "skipped", reason: "has local edits (use --force to reset them)" }];
|
|
927
992
|
const change = addSkill(root, name, { force: true, version: pinned });
|
|
928
|
-
|
|
993
|
+
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` }];
|
|
994
|
+
return [{ ...change, action: local === null ? "installed" : change.action }];
|
|
929
995
|
});
|
|
930
996
|
}
|
|
931
997
|
function discardEdits(root, name) {
|
|
@@ -944,6 +1010,7 @@ function updateProject(root, names, { force = false } = {}) {
|
|
|
944
1010
|
const dep = skills[name];
|
|
945
1011
|
const local = treeHash(join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name));
|
|
946
1012
|
if (dep.hash === latest.hash && local === latest.hash) return [];
|
|
1013
|
+
if (dep.hash === latest.hash && local !== null && !force) return [];
|
|
947
1014
|
const change = addSkill(root, name, { force });
|
|
948
1015
|
return change.action === "skipped" || change.from !== change.to || local !== latest.hash ? [change] : [];
|
|
949
1016
|
});
|
|
@@ -1038,12 +1105,12 @@ function createSkill(name, description) {
|
|
|
1038
1105
|
join8(dir, "SKILL.md"),
|
|
1039
1106
|
`---
|
|
1040
1107
|
name: ${name}
|
|
1041
|
-
description: ${description || "Describe when
|
|
1108
|
+
description: ${description || "Describe when an agent should use this skill."}
|
|
1042
1109
|
---
|
|
1043
1110
|
|
|
1044
1111
|
# ${name}
|
|
1045
1112
|
|
|
1046
|
-
Instructions for
|
|
1113
|
+
Instructions for the agent go here.
|
|
1047
1114
|
`
|
|
1048
1115
|
);
|
|
1049
1116
|
forgetLatest(name);
|
|
@@ -1061,7 +1128,11 @@ function listBackups() {
|
|
|
1061
1128
|
const from = index[entry] ?? (kind === "trash" ? librarySkillDir(name) : join8(claudeDir(), "skills", name));
|
|
1062
1129
|
return [{ name, kind, path: join8(dir, entry), movedAt: `${match[2]} ${match[3]}:${match[4]}`, from }];
|
|
1063
1130
|
});
|
|
1064
|
-
}).sort((a, b) => b.
|
|
1131
|
+
}).sort((a, b) => b.path.slice(-24).localeCompare(a.path.slice(-24)));
|
|
1132
|
+
}
|
|
1133
|
+
function backupFrom(backup) {
|
|
1134
|
+
if (backup.kind === "plugin") return "Claude Code plugin";
|
|
1135
|
+
return backup.from === librarySkillDir(backup.name) ? "library (deleted)" : tildify(dirname2(backup.from));
|
|
1065
1136
|
}
|
|
1066
1137
|
function restoreBackup(backup) {
|
|
1067
1138
|
const to = backup.from;
|
|
@@ -1114,7 +1185,7 @@ function installAgentSkill(enabled = enabledHarnesses()) {
|
|
|
1114
1185
|
const [primary, ...links] = agentSkillDirs(enabled);
|
|
1115
1186
|
if (!primary) return { ok: false, reason: "no agents chosen yet (skilllib harnesses <id>...)" };
|
|
1116
1187
|
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` };
|
|
1188
|
+
if (foreign) return { ok: false, reason: `${tildify(foreign)} already exists and isn't skilllib's` };
|
|
1118
1189
|
for (const d of [primary, ...links]) if (isDangling(d)) unlinkSync2(d);
|
|
1119
1190
|
mkdirSync6(primary, { recursive: true });
|
|
1120
1191
|
writeFileSync6(join9(primary, "SKILL.md"), shippedSkill());
|
|
@@ -1143,12 +1214,8 @@ function refreshAgentSkill() {
|
|
|
1143
1214
|
}
|
|
1144
1215
|
|
|
1145
1216
|
// 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";
|
|
1217
|
+
import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
|
|
1218
|
+
import { basename as basename8, join as join14, resolve as resolve7, sep as sep5 } from "path";
|
|
1152
1219
|
|
|
1153
1220
|
// src/git.ts
|
|
1154
1221
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -1162,9 +1229,9 @@ function git(root, args, input) {
|
|
|
1162
1229
|
return typeof out2 === "string" ? out2 : null;
|
|
1163
1230
|
}
|
|
1164
1231
|
}
|
|
1165
|
-
function gitInfo(root) {
|
|
1232
|
+
function gitInfo(root, extraDirs = []) {
|
|
1166
1233
|
if (!existsSync9(join10(root, ".git"))) return null;
|
|
1167
|
-
const dirs = [...ALL_PROJECT_DIRS, MANIFEST_FILE].filter((d) => existsSync9(join10(root, d)));
|
|
1234
|
+
const dirs = [...ALL_PROJECT_DIRS, MANIFEST_FILE, ...extraDirs].filter((d) => existsSync9(join10(root, d)));
|
|
1168
1235
|
if (dirs.length === 0) return { of: () => "new" };
|
|
1169
1236
|
const tracked = (git(root, ["ls-files", "-z", "--", ...dirs]) ?? "").split("\0").filter(Boolean);
|
|
1170
1237
|
const dirty = (git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...dirs]) ?? "").split("\0").filter(Boolean).map((entry) => entry.slice(3));
|
|
@@ -1189,14 +1256,18 @@ function relativeTo(root, path) {
|
|
|
1189
1256
|
return relative3(root, path).split("\\").join("/");
|
|
1190
1257
|
}
|
|
1191
1258
|
|
|
1259
|
+
// src/here.ts
|
|
1260
|
+
import { readdirSync as readdirSync7 } from "fs";
|
|
1261
|
+
import { basename as basename7 } from "path";
|
|
1262
|
+
|
|
1192
1263
|
// src/health.ts
|
|
1193
1264
|
import { lstatSync as lstatSync4 } from "fs";
|
|
1194
|
-
import { basename as basename6 } from "path";
|
|
1265
|
+
import { basename as basename6, dirname as dirname5 } from "path";
|
|
1195
1266
|
|
|
1196
1267
|
// src/plugins.ts
|
|
1197
1268
|
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";
|
|
1269
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync7 } from "fs";
|
|
1270
|
+
import { basename as basename4, dirname as dirname3, join as join11, resolve as resolve6 } from "path";
|
|
1200
1271
|
function readJson2(path) {
|
|
1201
1272
|
try {
|
|
1202
1273
|
return JSON.parse(readFileSync8(path, "utf-8"));
|
|
@@ -1249,8 +1320,7 @@ function claudeBinary() {
|
|
|
1249
1320
|
}
|
|
1250
1321
|
}) ?? null;
|
|
1251
1322
|
}
|
|
1252
|
-
function claude(args, cwd) {
|
|
1253
|
-
const bin = claudeBinary();
|
|
1323
|
+
function claude(args, cwd, bin = claudeBinary()) {
|
|
1254
1324
|
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` };
|
|
1255
1325
|
try {
|
|
1256
1326
|
execFileSync3(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 12e4, encoding: "utf-8" });
|
|
@@ -1271,9 +1341,44 @@ function setPluginIn(file, id, on) {
|
|
|
1271
1341
|
function turnOffSynced(id) {
|
|
1272
1342
|
return setPluginIn(join11(claudeDir(), "settings.json"), id, false);
|
|
1273
1343
|
}
|
|
1344
|
+
function ignoreLocalSettings(root) {
|
|
1345
|
+
const file = ".claude/settings.local.json";
|
|
1346
|
+
const git2 = (...args) => execFileSync3("git", args, { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1347
|
+
try {
|
|
1348
|
+
git2("rev-parse", "--git-dir");
|
|
1349
|
+
} catch {
|
|
1350
|
+
return "";
|
|
1351
|
+
}
|
|
1352
|
+
try {
|
|
1353
|
+
git2("ls-files", "--error-unmatch", "--", file);
|
|
1354
|
+
return "; git tracks that file, so this shows in git status";
|
|
1355
|
+
} catch {
|
|
1356
|
+
}
|
|
1357
|
+
try {
|
|
1358
|
+
git2("check-ignore", "-q", "--", file);
|
|
1359
|
+
return ", which git ignores";
|
|
1360
|
+
} catch {
|
|
1361
|
+
}
|
|
1362
|
+
try {
|
|
1363
|
+
const exclude = resolve6(root, git2("rev-parse", "--git-path", "info/exclude"));
|
|
1364
|
+
const before = existsSync10(exclude) ? readFileSync8(exclude, "utf-8") : "";
|
|
1365
|
+
mkdirSync7(dirname3(exclude), { recursive: true });
|
|
1366
|
+
writeFileSync7(exclude, `${before}${before && !before.endsWith("\n") ? "\n" : ""}/${git2("rev-parse", "--show-prefix")}${file}
|
|
1367
|
+
`);
|
|
1368
|
+
return ", which git now ignores via .git/info/exclude";
|
|
1369
|
+
} catch {
|
|
1370
|
+
return "; git doesn't ignore that file yet: it's local only, don't commit it";
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1274
1373
|
function turnOffIn(plugin, root) {
|
|
1275
|
-
const
|
|
1276
|
-
|
|
1374
|
+
const done = `${plugin.id} turned off in ${basename4(root)}; other repos keep it`;
|
|
1375
|
+
const bin = plugin.synced ? null : claudeBinary();
|
|
1376
|
+
if (!bin) {
|
|
1377
|
+
const r2 = setPluginIn(join11(root, ".claude", "settings.local.json"), plugin.id, false);
|
|
1378
|
+
return r2.ok ? { ok: true, message: `${done} (in .claude/settings.local.json${ignoreLocalSettings(root)})` } : { ok: false, message: `${plugin.id}: ${r2.reason}` };
|
|
1379
|
+
}
|
|
1380
|
+
const r = claude(["plugin", "disable", plugin.id, "--scope", "local"], root, bin);
|
|
1381
|
+
return r.ok ? { ok: true, message: done } : { ok: false, message: `${plugin.id}: ${r.reason}` };
|
|
1277
1382
|
}
|
|
1278
1383
|
function removedFile() {
|
|
1279
1384
|
return join11(skilllibHome(), "plugin-backup.json");
|
|
@@ -1315,11 +1420,11 @@ function restorePlugin(backup) {
|
|
|
1315
1420
|
}
|
|
1316
1421
|
function cursorPluginSkills() {
|
|
1317
1422
|
const cache = join11(userHome(), ".cursor", "plugins", "cache");
|
|
1318
|
-
const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") &&
|
|
1423
|
+
const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") && statSync3(join11(p, d)).isDirectory()) : [];
|
|
1319
1424
|
return dirs(cache).flatMap(
|
|
1320
1425
|
(marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
|
|
1321
1426
|
const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
|
|
1322
|
-
const newest = versions.sort((a, b) =>
|
|
1427
|
+
const newest = versions.sort((a, b) => statSync3(b).mtimeMs - statSync3(a).mtimeMs)[0];
|
|
1323
1428
|
return newest ? skillDirsIn(join11(newest, "skills")).map((path) => ({ name: basename4(path), plugin, path })) : [];
|
|
1324
1429
|
})
|
|
1325
1430
|
);
|
|
@@ -1388,7 +1493,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1388
1493
|
if (dep) {
|
|
1389
1494
|
primary = reals.find((r) => rel(r.dir) === (dep.dir ?? PROJECT_SKILLS_DIR));
|
|
1390
1495
|
if (!primary) continue;
|
|
1391
|
-
} else if (chosen) {
|
|
1496
|
+
} else if (chosen && reals.some((r) => r.dir === chosen)) {
|
|
1392
1497
|
primary = reals.find((r) => r.dir === chosen);
|
|
1393
1498
|
} else {
|
|
1394
1499
|
if (git2 === void 0) git2 = gitInfo(root);
|
|
@@ -1412,7 +1517,8 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1412
1517
|
if (!needed) steps.push({ kind: "unlink", path: e.path });
|
|
1413
1518
|
} else steps.push(needed ? { kind: "link", path: e.path, target: primary.path } : { kind: "unlink", path: e.path });
|
|
1414
1519
|
} else {
|
|
1415
|
-
|
|
1520
|
+
const differs = e.hash !== primary.hash ? { differs: true } : {};
|
|
1521
|
+
steps.push(needed ? { kind: "replace", path: e.path, target: primary.path, ...differs } : { kind: "remove", path: e.path, ...differs });
|
|
1416
1522
|
}
|
|
1417
1523
|
}
|
|
1418
1524
|
if (steps.length) report.plans.push({ name, root, primary: primary.path, steps });
|
|
@@ -1420,7 +1526,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1420
1526
|
return report;
|
|
1421
1527
|
}
|
|
1422
1528
|
function globalReadsOf(id) {
|
|
1423
|
-
return HARNESSES.find((h) => h.id === id).globalDirs();
|
|
1529
|
+
return HARNESSES.find((h) => h.id === id).globalDirs().map(globalDirAs);
|
|
1424
1530
|
}
|
|
1425
1531
|
function isSkillsDirPlugin(path) {
|
|
1426
1532
|
return existsSync11(join12(path, ".claude-plugin", "plugin.json"));
|
|
@@ -1435,15 +1541,16 @@ function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
|
|
|
1435
1541
|
report.skipped.push({ name, reason: "a Claude Code plugin kept in a skills folder" });
|
|
1436
1542
|
continue;
|
|
1437
1543
|
}
|
|
1438
|
-
const reals = entries.filter((e) => !e.link);
|
|
1544
|
+
const reals = entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link && realpathOrNull2(o.path) === realpathOrNull2(e.path)) === i);
|
|
1439
1545
|
if (reals.length < 2) continue;
|
|
1440
1546
|
hashReals(reals);
|
|
1441
|
-
const
|
|
1442
|
-
|
|
1547
|
+
const kept = reals.find((r) => r.dir === keep[name]);
|
|
1548
|
+
const primary = kept ?? reals.find((r) => r.dir === agents) ?? reals[0];
|
|
1549
|
+
if (!kept && reals.some((r) => r.hash !== primary.hash)) {
|
|
1443
1550
|
report.conflicts.push(conflictOf(name, null, reals, enabled, globalReadsOf));
|
|
1444
1551
|
continue;
|
|
1445
1552
|
}
|
|
1446
|
-
const steps = reals.filter((r) => r !== primary).map((r) => ({ kind: "replace", path: r.path, target: primary.path }));
|
|
1553
|
+
const steps = reals.filter((r) => r !== primary).map((r) => ({ kind: "replace", path: r.path, target: primary.path, ...r.hash !== primary.hash ? { differs: true } : {} }));
|
|
1447
1554
|
report.plans.push({ name, root: null, primary: primary.path, steps });
|
|
1448
1555
|
}
|
|
1449
1556
|
return report;
|
|
@@ -1481,20 +1588,27 @@ function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
|
|
|
1481
1588
|
return { name: plan.name, root: plan.root, applied, held };
|
|
1482
1589
|
}
|
|
1483
1590
|
function describeStep(step, root) {
|
|
1484
|
-
const shown = root ? relativeTo(root, step.path) : step.path
|
|
1591
|
+
const shown = root ? relativeTo(root, step.path) : tildify(step.path);
|
|
1485
1592
|
switch (step.kind) {
|
|
1486
1593
|
case "link":
|
|
1487
1594
|
return `link ${shown}`;
|
|
1488
1595
|
case "unlink":
|
|
1489
1596
|
return `remove extra link ${shown}`;
|
|
1490
1597
|
case "replace":
|
|
1491
|
-
return `${shown}: duplicate copy \u2192 link`;
|
|
1598
|
+
return `${shown}: ${step.differs ? "differing" : "duplicate"} copy \u2192 link`;
|
|
1492
1599
|
case "remove":
|
|
1493
|
-
return `remove duplicate copy ${shown}`;
|
|
1600
|
+
return `remove ${step.differs ? "differing" : "duplicate"} copy ${shown}`;
|
|
1494
1601
|
}
|
|
1495
1602
|
}
|
|
1496
1603
|
function copyLabel(conflict, dir) {
|
|
1497
|
-
return
|
|
1604
|
+
return folderLabel(conflict.root, dir);
|
|
1605
|
+
}
|
|
1606
|
+
function folderLabel(root, dir) {
|
|
1607
|
+
return root ? relativeTo(root, dir) : tildify(dir);
|
|
1608
|
+
}
|
|
1609
|
+
function foldersOf(root, name) {
|
|
1610
|
+
const entries = entriesOf(root ? ALL_PROJECT_DIRS.map((d) => join12(root, d)) : globalSkillDirs(), name);
|
|
1611
|
+
return { all: entries.map((e) => e.dir), real: entries.filter((e) => !e.link).map((e) => e.dir) };
|
|
1498
1612
|
}
|
|
1499
1613
|
|
|
1500
1614
|
// src/health.ts
|
|
@@ -1543,13 +1657,14 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1543
1657
|
issues.push({
|
|
1544
1658
|
id: `broken:${skill.name}`,
|
|
1545
1659
|
severity: "problem",
|
|
1546
|
-
title: `${skill.name}: broken link in ${skill.path
|
|
1660
|
+
title: `${skill.name}: broken link in ${tildify(dirname5(skill.path))}`,
|
|
1547
1661
|
detail: "It points at a folder that no longer exists, so it loads nothing.",
|
|
1548
1662
|
fix: {
|
|
1549
1663
|
label: "Remove the link",
|
|
1550
1664
|
run: () => {
|
|
1551
1665
|
const r = unloadGlobal(skill.path);
|
|
1552
|
-
|
|
1666
|
+
if (!r.ok) throw new Error(`${skill.name}: ${r.reason}`);
|
|
1667
|
+
return `Removed broken link ${skill.name}`;
|
|
1553
1668
|
}
|
|
1554
1669
|
}
|
|
1555
1670
|
});
|
|
@@ -1570,8 +1685,10 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1570
1685
|
});
|
|
1571
1686
|
}
|
|
1572
1687
|
const loadedHere = new Map(projects2.map((root) => [root, loadedIn(root)]));
|
|
1573
|
-
const
|
|
1574
|
-
|
|
1688
|
+
const namesIn2 = (root, agent = "claude-code") => [
|
|
1689
|
+
...new Set([...statuses.get(root), ...nested.get(root)].filter((s) => s.visibility.some((v) => v.id === agent && v.paths > 0)).map((s) => s.name))
|
|
1690
|
+
];
|
|
1691
|
+
const pluginAgentsIn = (plugin, root) => new Set(loadedHere.get(root).filter((m) => plugin.skills.includes(m.path)).flatMap((m) => m.harnesses));
|
|
1575
1692
|
const globalYours = new Set(loaded.filter((m) => m.movable).map((m) => m.name));
|
|
1576
1693
|
const installedYours = /* @__PURE__ */ new Set([...globalYours, ...[...statuses.values()].flat().map((s) => s.name)]);
|
|
1577
1694
|
const repoChoices = (plugin, root, names) => {
|
|
@@ -1579,7 +1696,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1579
1696
|
const copies = statuses.get(root).filter((s) => names.includes(s.name));
|
|
1580
1697
|
const allManaged = names.every((n) => copies.some((c) => c.name === n && c.managed));
|
|
1581
1698
|
return [
|
|
1582
|
-
{ label: `Turn the plugin ${plugin.id} off in ${where} only`, hint: "other repos keep it", run: () => orThrow(turnOffIn(plugin, root)) },
|
|
1699
|
+
{ 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
1700
|
// Only copies skilllib installed: Your skills keeps them. A repo's own skill is the team's, never removed from here.
|
|
1584
1701
|
...names.length && allManaged ? [
|
|
1585
1702
|
{
|
|
@@ -1599,8 +1716,10 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1599
1716
|
const machinePlugins = claudePlugins(machine);
|
|
1600
1717
|
for (const plugin of machinePlugins) {
|
|
1601
1718
|
const names = plugin.skills.map((p) => basename6(p));
|
|
1602
|
-
const inRepos = projects2.filter((root) =>
|
|
1603
|
-
const
|
|
1719
|
+
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);
|
|
1720
|
+
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) : [];
|
|
1721
|
+
const claudeYours = /* @__PURE__ */ new Set([...globalYours, ...inRepos.flatMap(([, d]) => d)]);
|
|
1722
|
+
const loadedYours = /* @__PURE__ */ new Set([...claudeYours, ...cursorRepos.flatMap(([, d]) => d)]);
|
|
1604
1723
|
const dupes = names.filter((n) => libraryNames.has(n) || loadedYours.has(n));
|
|
1605
1724
|
if (!dupes.length) continue;
|
|
1606
1725
|
const others = plugin.skills.length - dupes.length;
|
|
@@ -1612,15 +1731,18 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1612
1731
|
};
|
|
1613
1732
|
const off = { label: `Turn the plugin ${plugin.id} off`, hint: "stays installed", run: () => orThrow(removePlugin(plugin, "off")) };
|
|
1614
1733
|
const bothLoad = dupes.filter((n) => loadedYours.has(n));
|
|
1734
|
+
const claudeBoth = dupes.filter((n) => claudeYours.has(n));
|
|
1735
|
+
const cursorWheres = cursorRepos.map(([root]) => basename6(root));
|
|
1615
1736
|
const notInstalled = dupes.filter((n) => !installedYours.has(n));
|
|
1616
1737
|
issues.push({
|
|
1617
1738
|
id: `plugin:${plugin.id}`,
|
|
1618
1739
|
severity: bothLoad.length ? "problem" : "suggestion",
|
|
1619
1740
|
title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}`,
|
|
1620
1741
|
detail: [
|
|
1621
|
-
|
|
1742
|
+
claudeBoth.length ? `Claude Code loads both (the plugin's as /${pluginName}:${claudeBoth[0]}). Your skill wins.` : "",
|
|
1743
|
+
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
1744
|
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 ${
|
|
1745
|
+
others ? `The plugin's other ${others === 1 ? "skill is" : `${others} skills are`} copied into Your skills first, so nothing is lost.` : "",
|
|
1624
1746
|
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
1747
|
plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : "",
|
|
1626
1748
|
inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) => basename6(root)).join(", ")}.` : ""
|
|
@@ -1667,7 +1789,11 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1667
1789
|
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
1790
|
fix: {
|
|
1669
1791
|
label: "Keep one copy of each",
|
|
1670
|
-
run: () =>
|
|
1792
|
+
run: () => {
|
|
1793
|
+
const r = applyAll(global.plans, "go");
|
|
1794
|
+
if (!r.skills) throw new Error("No global skill changed");
|
|
1795
|
+
return `${plural(r.skills, "global skill")} now ${r.skills === 1 ? "has" : "have"} one copy`;
|
|
1796
|
+
}
|
|
1671
1797
|
}
|
|
1672
1798
|
});
|
|
1673
1799
|
}
|
|
@@ -1679,7 +1805,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1679
1805
|
id: "review:global",
|
|
1680
1806
|
severity: "suggestion",
|
|
1681
1807
|
title: `${plural(unreviewed.length, "global skill")} of yours ${unreviewed.length === 1 ? "loads" : "load"} in every repo`,
|
|
1682
|
-
detail: `${unreviewed.join(", ")}.
|
|
1808
|
+
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
1809
|
});
|
|
1684
1810
|
}
|
|
1685
1811
|
const enabled = enabledHarnesses();
|
|
@@ -1697,6 +1823,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1697
1823
|
run: () => {
|
|
1698
1824
|
const results = skills.map((m) => ({ m, ...linkGlobal(m.path, blindOf(m)) }));
|
|
1699
1825
|
const skipped = results.flatMap((r) => r.skipped);
|
|
1826
|
+
if (!results.some((r) => r.linked.length)) throw new Error(`Nothing linked${skipped.length ? `; skipped ${skipped.join(", ")} (a different skill has that name)` : ""}`);
|
|
1700
1827
|
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
1828
|
}
|
|
1702
1829
|
};
|
|
@@ -1714,28 +1841,69 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1714
1841
|
for (const root of projects2) {
|
|
1715
1842
|
const where = basename6(root);
|
|
1716
1843
|
const status2 = statuses.get(root);
|
|
1717
|
-
const
|
|
1718
|
-
if (
|
|
1844
|
+
const restorable = status2.filter((s) => s.state === "folder missing" && s.latest !== null);
|
|
1845
|
+
if (restorable.length > 0) {
|
|
1719
1846
|
issues.push({
|
|
1720
1847
|
id: `sync:${root}`,
|
|
1721
1848
|
severity: "problem",
|
|
1722
|
-
title: `${where}: ${plural(
|
|
1723
|
-
detail:
|
|
1849
|
+
title: `${where}: ${plural(restorable.length, "skill")} in skilllib.json not installed`,
|
|
1850
|
+
detail: `${restorable.map((s) => s.name).join(", ")}: the folder is missing`,
|
|
1724
1851
|
fix: {
|
|
1725
1852
|
label: "Sync the project",
|
|
1726
1853
|
run: () => {
|
|
1727
1854
|
const changes = syncProject(root);
|
|
1728
|
-
|
|
1855
|
+
const done = changes.filter((c) => c.action !== "skipped");
|
|
1856
|
+
const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
|
|
1857
|
+
if (!done.length) throw new Error(`${where}: nothing restored${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
|
|
1858
|
+
return `${where}: restored ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
|
|
1729
1859
|
}
|
|
1730
1860
|
}
|
|
1731
1861
|
});
|
|
1732
1862
|
}
|
|
1863
|
+
for (const s of status2.filter((s2) => s2.state === "folder missing" && s2.latest === null))
|
|
1864
|
+
issues.push({
|
|
1865
|
+
id: `unavailable:${root}:${s.name}`,
|
|
1866
|
+
severity: "problem",
|
|
1867
|
+
title: `${s.name}: pinned in ${where}'s skilllib.json, but not in your library`,
|
|
1868
|
+
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.`,
|
|
1869
|
+
// Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
|
|
1870
|
+
choices: [
|
|
1871
|
+
{
|
|
1872
|
+
label: "Remove it from skilllib.json",
|
|
1873
|
+
hint: `${where} stops pinning it`,
|
|
1874
|
+
run: () => {
|
|
1875
|
+
const r = removeSkill(root, s.name);
|
|
1876
|
+
if (r.action !== "removed") throw new Error(`${s.name}: ${r.reason}`);
|
|
1877
|
+
return `${s.name}: removed from ${where}'s skilllib.json`;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
]
|
|
1881
|
+
});
|
|
1882
|
+
const behind = status2.filter((s) => s.state === "update available");
|
|
1883
|
+
if (behind.length > 0)
|
|
1884
|
+
issues.push({
|
|
1885
|
+
id: `update:${root}`,
|
|
1886
|
+
severity: "suggestion",
|
|
1887
|
+
title: `${where}: ${plural(behind.length, "skill")} out of date`,
|
|
1888
|
+
detail: behind.map((s) => `${s.name} (v${s.version} \u2192 v${s.latest})`).join(", "),
|
|
1889
|
+
fix: {
|
|
1890
|
+
label: "Update to the newest versions",
|
|
1891
|
+
run: () => {
|
|
1892
|
+
const changes = updateProject(root, behind.map((s) => s.name));
|
|
1893
|
+
const done = changes.filter((c) => c.action !== "skipped");
|
|
1894
|
+
const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
|
|
1895
|
+
if (!done.length) throw new Error(`${where}: nothing updated${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
|
|
1896
|
+
return `${where}: updated ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1733
1900
|
const tidy2 = planProjectTidy(root);
|
|
1734
1901
|
if (tidy2.plans.length) {
|
|
1735
|
-
const
|
|
1736
|
-
const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p,
|
|
1902
|
+
const git3 = gitInfo(root);
|
|
1903
|
+
const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p, git3).map((s) => describeStep(s, root)));
|
|
1737
1904
|
const run = (mode) => () => {
|
|
1738
1905
|
const r = applyAll(tidy2.plans, mode);
|
|
1906
|
+
if (!r.skills) throw new Error(`${where}: nothing tidied${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`);
|
|
1739
1907
|
return `${where}: tidied ${plural(r.skills, "skill")}${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`;
|
|
1740
1908
|
};
|
|
1741
1909
|
issues.push({
|
|
@@ -1752,14 +1920,24 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1752
1920
|
});
|
|
1753
1921
|
}
|
|
1754
1922
|
issues.push(...tidy2.conflicts.map(conflictIssue));
|
|
1755
|
-
const
|
|
1923
|
+
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 }) }));
|
|
1924
|
+
for (const { s, would } of blindHere.filter(({ would: w }) => !w.created.length && !w.blocked.length && w.uncommitted.length))
|
|
1925
|
+
issues.push({
|
|
1926
|
+
id: `uncommitted:${root}:${s.name}`,
|
|
1927
|
+
severity: "suggestion",
|
|
1928
|
+
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`,
|
|
1929
|
+
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).`
|
|
1930
|
+
});
|
|
1931
|
+
const partial = blindHere.filter(({ would }) => would.created.length || would.blocked.length).map(({ s }) => s);
|
|
1756
1932
|
if (partial.length) {
|
|
1757
1933
|
const missing = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
|
|
1758
|
-
const tracked =
|
|
1934
|
+
const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
|
|
1759
1935
|
const run = (allowTracked) => () => {
|
|
1760
1936
|
if (allowTracked) allowTrackedLinks(root);
|
|
1761
1937
|
const r = linkAll(root, { allowTracked });
|
|
1762
|
-
|
|
1938
|
+
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)` : ""}`;
|
|
1939
|
+
if (!r.linked.length) throw new Error(`${where}: nothing linked${notes}`);
|
|
1940
|
+
return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing)}${notes}`;
|
|
1763
1941
|
};
|
|
1764
1942
|
issues.push({
|
|
1765
1943
|
id: `usable:${root}`,
|
|
@@ -1768,20 +1946,40 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1768
1946
|
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
1947
|
...tracked.length ? {
|
|
1770
1948
|
choices: [
|
|
1771
|
-
|
|
1949
|
+
// Only when some link goes where git doesn't track: otherwise this one would link nothing.
|
|
1950
|
+
...blindHere.some(({ would }) => would.created.length) ? [{ label: "Add links, but not where git tracks the folder", hint: `skips ${tracked.join(", ")}`, run: run(false) }] : [],
|
|
1772
1951
|
{ label: "Add links everywhere", hint: `remembered for ${where}; git will see them`, run: run(true) }
|
|
1773
1952
|
]
|
|
1774
1953
|
} : { fix: { label: "Add the links", run: run(false) } }
|
|
1775
1954
|
});
|
|
1776
1955
|
}
|
|
1777
1956
|
const differing = new Set(tidy2.conflicts.map((c) => c.name));
|
|
1957
|
+
const git2 = gitInfo(root);
|
|
1958
|
+
const committed = (s) => ["committed", "changed"].includes(git2?.of(relativeTo(root, s.path)) ?? "");
|
|
1778
1959
|
for (const s of status2) {
|
|
1779
1960
|
if (isLink(s.path) || differing.has(s.name)) continue;
|
|
1780
|
-
if (s.state === "local only") {
|
|
1961
|
+
if (s.state === "local only" && committed(s)) {
|
|
1962
|
+
issues.push({
|
|
1963
|
+
id: `local:${root}:${s.name}`,
|
|
1964
|
+
severity: "suggestion",
|
|
1965
|
+
title: `${s.name} in ${where} isn't in your library`,
|
|
1966
|
+
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).",
|
|
1967
|
+
choices: [
|
|
1968
|
+
{
|
|
1969
|
+
label: "Copy into the library",
|
|
1970
|
+
hint: `${where}'s copy stays as it is`,
|
|
1971
|
+
run: () => {
|
|
1972
|
+
importSkill(s.path);
|
|
1973
|
+
return `${s.name} copied into your library; ${where}'s copy stays as it is`;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
]
|
|
1977
|
+
});
|
|
1978
|
+
} else if (s.state === "local only") {
|
|
1781
1979
|
issues.push({
|
|
1782
1980
|
id: `local:${root}:${s.name}`,
|
|
1783
1981
|
severity: "suggestion",
|
|
1784
|
-
title: `${s.name}
|
|
1982
|
+
title: `${s.name} in ${where} isn't in your library`,
|
|
1785
1983
|
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
1984
|
choices: [
|
|
1787
1985
|
{
|
|
@@ -1984,7 +2182,7 @@ function groupBy(items) {
|
|
|
1984
2182
|
}
|
|
1985
2183
|
var sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
|
|
1986
2184
|
function globalFrom(kind, origin, path) {
|
|
1987
|
-
if (kind === "global") return tildify(path.replace(/[\\/][^\\/]+$/, "")).replace(/\\/g, "/");
|
|
2185
|
+
if (kind === "global" || kind === "skilllib") return tildify(path.replace(/[\\/][^\\/]+$/, "")).replace(/\\/g, "/");
|
|
1988
2186
|
return origin;
|
|
1989
2187
|
}
|
|
1990
2188
|
var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "target", "vendor", "coverage"]);
|
|
@@ -1997,28 +2195,34 @@ function topLevel(root) {
|
|
|
1997
2195
|
}
|
|
1998
2196
|
function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
1999
2197
|
const agents = enabledHarnesses();
|
|
2000
|
-
const
|
|
2001
|
-
const
|
|
2002
|
-
const
|
|
2003
|
-
const
|
|
2004
|
-
|
|
2198
|
+
const inRepo = projectHere(root) === root;
|
|
2199
|
+
const nestedHere = inRepo ? nestedSkills(root, { enabled: agents }) : [];
|
|
2200
|
+
const git2 = gitInfo(root, [...new Set(nestedHere.map((s) => s.location))]);
|
|
2201
|
+
const group = (list2) => groupBy(
|
|
2202
|
+
list2.map((s) => {
|
|
2005
2203
|
const loadedBy = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
|
|
2006
2204
|
const behind = s.managed && s.latest !== null && s.version !== null && s.latest > s.version;
|
|
2205
|
+
const gitState = git2?.of(relativeTo(root, s.path));
|
|
2206
|
+
const repo = !s.managed && (s.state.startsWith("repo skill") || gitState === "committed" || gitState === "changed");
|
|
2207
|
+
const usual = s.state === "ok" || s.state === "local only" || repo && s.state !== "from npx skills";
|
|
2007
2208
|
const attrs = {
|
|
2008
|
-
source: s.managed ? "library" :
|
|
2209
|
+
source: s.managed ? "library" : repo ? "repo" : "local",
|
|
2009
2210
|
...s.location !== ".claude/skills" && { dir: s.location },
|
|
2010
2211
|
...s.version !== null && { version: s.version },
|
|
2011
|
-
|
|
2212
|
+
...!usual && { state: s.state },
|
|
2012
2213
|
...behind && { latest: s.latest },
|
|
2013
|
-
...
|
|
2214
|
+
...gitState && { git: gitState },
|
|
2014
2215
|
...!sameSet(loadedBy, agents) && { agents: loadedBy }
|
|
2015
2216
|
};
|
|
2016
2217
|
return [JSON.stringify(attrs), s.name, () => ({ ...attrs, skills: [] })];
|
|
2017
2218
|
})
|
|
2018
2219
|
);
|
|
2220
|
+
const skills = group(inRepo ? projectStatus(root) : []);
|
|
2221
|
+
const nested = group(nestedHere);
|
|
2019
2222
|
const machine = machineSkills(agents);
|
|
2223
|
+
const loaded = inRepo ? skillsLoadedIn(root, machine, agents) : machine;
|
|
2020
2224
|
const global = groupBy(
|
|
2021
|
-
|
|
2225
|
+
loaded.filter((m) => !m.broken && m.harnesses.length > 0).map((m) => {
|
|
2022
2226
|
const from = globalFrom(m.kind, m.origin, m.path);
|
|
2023
2227
|
return [`${m.kind}\0${from}\0${m.harnesses.join()}`, m.name, () => ({ source: m.kind, from, agents: m.harnesses, skills: [] })];
|
|
2024
2228
|
})
|
|
@@ -2037,6 +2241,7 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2037
2241
|
files: topLevel(root),
|
|
2038
2242
|
agents,
|
|
2039
2243
|
skills,
|
|
2244
|
+
...nested.length > 0 && { nested },
|
|
2040
2245
|
global,
|
|
2041
2246
|
issues: [...issues.values()],
|
|
2042
2247
|
...Object.keys(usesHere).length > 0 && { usesHere }
|
|
@@ -2047,9 +2252,9 @@ function firstSentence(description) {
|
|
|
2047
2252
|
const sentence = line.match(/^(.{40,}?[.!?])(?:\s|$)/)?.[1] ?? line;
|
|
2048
2253
|
return sentence.length > 200 ? sentence.slice(0, 199).trimEnd() + "\u2026" : sentence;
|
|
2049
2254
|
}
|
|
2050
|
-
function libraryFor(
|
|
2255
|
+
function libraryFor(currentProject) {
|
|
2051
2256
|
const manifests = visibleProjects().map((p) => ({ name: basename7(p), skills: readManifest(p).skills }));
|
|
2052
|
-
const here =
|
|
2257
|
+
const here = currentProject ? new Set(projectStatus(currentProject).map((s) => s.name)) : null;
|
|
2053
2258
|
const all = librarySkills();
|
|
2054
2259
|
const describe = (s) => {
|
|
2055
2260
|
const projects2 = manifests.filter((m) => s.name in m.skills).map((m) => m.name);
|
|
@@ -2060,8 +2265,8 @@ function libraryFor(currentProject2) {
|
|
|
2060
2265
|
}
|
|
2061
2266
|
|
|
2062
2267
|
// src/usage.ts
|
|
2063
|
-
import { createReadStream, existsSync as
|
|
2064
|
-
import { join as
|
|
2268
|
+
import { createReadStream, existsSync as existsSync12, readdirSync as readdirSync8, statSync as statSync4 } from "fs";
|
|
2269
|
+
import { join as join13 } from "path";
|
|
2065
2270
|
import { createInterface } from "readline";
|
|
2066
2271
|
var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
|
|
2067
2272
|
"add-dir",
|
|
@@ -2141,12 +2346,12 @@ function usesFromTranscriptLine(line) {
|
|
|
2141
2346
|
return [];
|
|
2142
2347
|
}
|
|
2143
2348
|
function transcriptFiles(sinceMs) {
|
|
2144
|
-
const base =
|
|
2145
|
-
if (!
|
|
2349
|
+
const base = join13(claudeDir(), "projects");
|
|
2350
|
+
if (!existsSync12(base)) return [];
|
|
2146
2351
|
return readdirSync8(base).flatMap((project) => {
|
|
2147
|
-
const dir =
|
|
2148
|
-
if (!
|
|
2149
|
-
return readdirSync8(dir).filter((f) => f.endsWith(".jsonl")).map((f) =>
|
|
2352
|
+
const dir = join13(base, project);
|
|
2353
|
+
if (!statSync4(dir).isDirectory()) return [];
|
|
2354
|
+
return readdirSync8(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join13(dir, f)).filter((f) => statSync4(f).mtimeMs >= sinceMs);
|
|
2150
2355
|
});
|
|
2151
2356
|
}
|
|
2152
2357
|
async function scanUsage(days) {
|
|
@@ -2210,29 +2415,40 @@ function parseArgs(argv) {
|
|
|
2210
2415
|
function colorState(state) {
|
|
2211
2416
|
if (state === "ok") return green(state);
|
|
2212
2417
|
if (state === "folder missing" || state === "not in library") return red(state);
|
|
2213
|
-
if (state === "local only" || state === "from npx skills") return dim(state);
|
|
2418
|
+
if (state === "local only" || state === "from npx skills" || state === "repo skill") return dim(state);
|
|
2214
2419
|
return yellow(state);
|
|
2215
2420
|
}
|
|
2216
2421
|
function withDirs(root, changes) {
|
|
2217
2422
|
const { skills } = readManifest(root);
|
|
2218
2423
|
return changes.map((c) => {
|
|
2219
|
-
const dep = c.action === "installed" || c.action === "updated" ? skills[c.name] : void 0;
|
|
2424
|
+
const dep = c.action === "installed" || c.action === "updated" || c.action === "reset" ? skills[c.name] : void 0;
|
|
2220
2425
|
return dep ? { ...c, dirs: [dep.dir ?? ".claude/skills", ...dep.links ?? []] } : c;
|
|
2221
2426
|
});
|
|
2222
2427
|
}
|
|
2223
2428
|
function printChanges(root, changes, args) {
|
|
2429
|
+
if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
|
|
2224
2430
|
if (args.json) return json(withDirs(root, changes));
|
|
2225
2431
|
for (const c of changes) {
|
|
2226
2432
|
if (c.blocked?.length) {
|
|
2227
2433
|
warn(`${c.name}: git tracks ${c.blocked.join(", ")} here, so no link was added there. Re-run with --allow-tracked to add it.`);
|
|
2228
2434
|
}
|
|
2229
|
-
if (c.
|
|
2435
|
+
if (c.uncommitted?.length) {
|
|
2436
|
+
warn(
|
|
2437
|
+
`${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.`
|
|
2438
|
+
);
|
|
2439
|
+
}
|
|
2440
|
+
if (c.action === "skipped") error(`${c.name}: ${c.reason}`);
|
|
2230
2441
|
else {
|
|
2231
|
-
const icon = c.action === "removed" ? red("-") : green(c.action === "installed" ? "+" : "\u2191");
|
|
2442
|
+
const icon = c.action === "removed" ? red("-") : c.action === "reset" ? yellow("\u21BA") : green(c.action === "installed" ? "+" : "\u2191");
|
|
2232
2443
|
const versions = c.action === "removed" ? "" : c.from && c.from !== c.to ? ` v${c.from} \u2192 v${c.to}` : c.to ? ` v${c.to}` : "";
|
|
2233
2444
|
info(` ${icon} ${c.name}${versions} ${dim(c.action)}`);
|
|
2234
2445
|
}
|
|
2446
|
+
if (c.backedUp) info(dim(` local edits saved to ${tildify(c.backedUp)}; \`skilllib restore ${c.name}\` brings them back`));
|
|
2235
2447
|
}
|
|
2448
|
+
const done = changes.filter((c) => c.action !== "skipped").length;
|
|
2449
|
+
const skipped = changes.length - done;
|
|
2450
|
+
if (skipped && done) info(dim(`
|
|
2451
|
+
${done} done, ${skipped} skipped`));
|
|
2236
2452
|
}
|
|
2237
2453
|
function projectOfFactory(projects2) {
|
|
2238
2454
|
const sorted = [...projects2].sort((a, b) => b.length - a.length);
|
|
@@ -2250,35 +2466,51 @@ async function usageBySkill(days, onlyProject) {
|
|
|
2250
2466
|
const scoped = onlyProject ? uses.filter((u) => projectOf(u.cwd) === onlyProject) : uses;
|
|
2251
2467
|
return new Map(summarize(scoped, projectOf).map((s) => [s.skill, s]));
|
|
2252
2468
|
}
|
|
2253
|
-
function
|
|
2254
|
-
const root =
|
|
2255
|
-
|
|
2469
|
+
function requireProject() {
|
|
2470
|
+
const root = projectHere();
|
|
2471
|
+
if (root) return root;
|
|
2472
|
+
if (!isProjectCandidate(findProjectRoot())) {
|
|
2473
|
+
const stray = existsSync13(join14(userHome(), MANIFEST_FILE)) ? `, though ${tildify(join14(userHome(), MANIFEST_FILE))} makes it look like one` : "";
|
|
2474
|
+
throw new Error(`Your home folder isn't a project${stray}: skills added here would load in every repo. Run this inside a project.`);
|
|
2475
|
+
}
|
|
2476
|
+
throw new Error("Not in a git repo. Run this inside a project (or `git init` first).");
|
|
2256
2477
|
}
|
|
2257
2478
|
function lastUsed(summary) {
|
|
2258
2479
|
return summary ? summary.lastUsed.slice(0, 10) : "-";
|
|
2259
2480
|
}
|
|
2260
2481
|
async function status(args) {
|
|
2261
|
-
const root = findProjectRoot();
|
|
2262
2482
|
if (args.json) {
|
|
2263
|
-
const
|
|
2264
|
-
|
|
2265
|
-
|
|
2483
|
+
const root2 = findProjectRoot();
|
|
2484
|
+
const usage3 = await usageBySkill(args.days, root2);
|
|
2485
|
+
json(usableHere(root2, new Map([...usage3].map(([name, u]) => [name, u.uses]))));
|
|
2486
|
+
if (projectHere()) rememberProjects([root2]);
|
|
2266
2487
|
return;
|
|
2267
2488
|
}
|
|
2489
|
+
const root = requireProject();
|
|
2268
2490
|
const skills = projectStatus(root);
|
|
2269
2491
|
const nested = nestedSkills(root);
|
|
2270
2492
|
info(`${basename8(root)} ${dim(tildify(root))}
|
|
2271
2493
|
`);
|
|
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
|
-
}
|
|
2494
|
+
if (skills.length === 0 && nested.length === 0) info("No skills in this project yet. Add one from your library with: skilllib add <name>");
|
|
2276
2495
|
const usage2 = await usageBySkill(args.days, root);
|
|
2496
|
+
const git2 = gitInfo(root, [...new Set(nested.map((s) => s.location))]);
|
|
2497
|
+
const committed = (path) => ["committed", "changed"].includes(git2?.of(relativeTo(root, path)) ?? "");
|
|
2498
|
+
const asRepo = {
|
|
2499
|
+
"local only": "repo skill",
|
|
2500
|
+
"untracked copy of library skill": "repo skill, in library",
|
|
2501
|
+
"untracked, differs from library": "repo skill, differs from library"
|
|
2502
|
+
};
|
|
2503
|
+
const stateOf = (s) => colorState(committed(s.path) && asRepo[s.state] || s.state);
|
|
2504
|
+
const agentsOf = (s) => {
|
|
2505
|
+
const ids = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
|
|
2506
|
+
return ids.length === s.visibility.length ? dim("all") : ids.length ? ids.map((id) => harness(id).name).join(", ") : red("none");
|
|
2507
|
+
};
|
|
2277
2508
|
if (skills.length) {
|
|
2278
2509
|
table(
|
|
2279
2510
|
skills.map((s) => ({
|
|
2280
2511
|
Skill: s.name,
|
|
2281
|
-
Status: s.source ? dim(`npx skills: ${s.source}`) :
|
|
2512
|
+
Status: s.source ? dim(`npx skills: ${s.source}`) : stateOf(s),
|
|
2513
|
+
Agents: agentsOf(s),
|
|
2282
2514
|
[`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0),
|
|
2283
2515
|
"Last used": lastUsed(usage2.get(s.name))
|
|
2284
2516
|
}))
|
|
@@ -2290,7 +2522,8 @@ async function status(args) {
|
|
|
2290
2522
|
nested.map((s) => ({
|
|
2291
2523
|
Skill: s.name,
|
|
2292
2524
|
Folder: s.location,
|
|
2293
|
-
Status:
|
|
2525
|
+
Status: stateOf(s),
|
|
2526
|
+
Agents: agentsOf(s),
|
|
2294
2527
|
[`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0)
|
|
2295
2528
|
}))
|
|
2296
2529
|
);
|
|
@@ -2298,18 +2531,32 @@ async function status(args) {
|
|
|
2298
2531
|
const states = new Set(skills.map((s) => s.state));
|
|
2299
2532
|
const hints = [];
|
|
2300
2533
|
if (states.has("update available")) hints.push("`skilllib update` to install the newest versions");
|
|
2301
|
-
|
|
2534
|
+
const missing = skills.filter((s) => s.state === "folder missing");
|
|
2535
|
+
if (missing.some((s) => s.latest !== null)) hints.push("`skilllib sync` to restore missing skills");
|
|
2536
|
+
const notInLibrary = missing.filter((s) => s.latest === null).map((s) => s.name);
|
|
2537
|
+
if (notInLibrary.length)
|
|
2538
|
+
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
2539
|
if (states.has("edited locally") || states.has("edited locally, update available"))
|
|
2303
2540
|
hints.push("`skilllib import .claude/skills/<name> --force` to save local edits to the library");
|
|
2304
2541
|
if (states.has("local only")) hints.push("`skilllib import .claude/skills/<name>` to add a local skill to the library");
|
|
2305
2542
|
if (states.has("untracked copy of library skill") || states.has("untracked, differs from library"))
|
|
2306
2543
|
hints.push("`skilllib add <name>` to manage an untracked skill (--force if it differs)");
|
|
2307
2544
|
if (hints.length > 0) info("\n" + hints.map((h) => dim(`\u2192 ${h}`)).join("\n"));
|
|
2545
|
+
const here = usableHere(root);
|
|
2546
|
+
const globals = new Set(here.global.flatMap((g) => g.skills)).size;
|
|
2547
|
+
if (globals) info(`
|
|
2548
|
+
${globals} global ${globals === 1 ? "skill also loads" : "skills also load"} here ${dim("(your global folders, plugins, claude.ai): `skilllib` lists them")}`);
|
|
2549
|
+
const problems = here.issues.reduce((n, i) => n + i.problems.length, 0);
|
|
2550
|
+
if (problems) warn(`${problems} ${problems === 1 ? "issue" : "issues"} here or in your global skills: \`skilllib doctor\` lists them with fixes`);
|
|
2308
2551
|
rememberProjects([root]);
|
|
2309
2552
|
}
|
|
2553
|
+
async function overview(args) {
|
|
2554
|
+
if (args.json || projectHere()) return status(args);
|
|
2555
|
+
info("Not in a project. `skilllib projects` lists yours, `skilllib list` your library, `skilllib global` your global skills.");
|
|
2556
|
+
}
|
|
2310
2557
|
async function list(args) {
|
|
2311
2558
|
if (args.json) {
|
|
2312
|
-
return json(libraryFor(
|
|
2559
|
+
return json(libraryFor(projectHere()));
|
|
2313
2560
|
}
|
|
2314
2561
|
const skills = librarySkills();
|
|
2315
2562
|
if (skills.length === 0) {
|
|
@@ -2337,14 +2584,14 @@ async function list(args) {
|
|
|
2337
2584
|
);
|
|
2338
2585
|
}
|
|
2339
2586
|
const libraryNames = new Set(skills.map((s) => s.name));
|
|
2340
|
-
const globals = machineSkills().filter((m) => m.movable && !m.broken);
|
|
2587
|
+
const globals = [...new Set(machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.name))];
|
|
2341
2588
|
if (globals.length > 0) {
|
|
2342
2589
|
info("");
|
|
2343
|
-
warn(`${globals.length} of your global skill
|
|
2344
|
-
const missing = globals.filter((g) => !libraryNames.has(g
|
|
2590
|
+
warn(`${globals.length} of your global ${globals.length === 1 ? "skill still loads" : "skills still load"} in every project`);
|
|
2591
|
+
const missing = globals.filter((g) => !libraryNames.has(g));
|
|
2345
2592
|
info(
|
|
2346
2593
|
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
|
|
2594
|
+
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
2595
|
)
|
|
2349
2596
|
);
|
|
2350
2597
|
}
|
|
@@ -2384,7 +2631,7 @@ function add(args) {
|
|
|
2384
2631
|
error("Usage: skilllib add <skill-name>... [--force]");
|
|
2385
2632
|
process.exit(1);
|
|
2386
2633
|
}
|
|
2387
|
-
const root =
|
|
2634
|
+
const root = requireProject();
|
|
2388
2635
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
2389
2636
|
printChanges(root, args.positional.map((name) => addSkill(root, name, { force: args.force })), args);
|
|
2390
2637
|
rememberProjects([root]);
|
|
@@ -2394,18 +2641,21 @@ function remove(args) {
|
|
|
2394
2641
|
error("Usage: skilllib remove <skill-name>... [--force]");
|
|
2395
2642
|
process.exit(1);
|
|
2396
2643
|
}
|
|
2397
|
-
const root =
|
|
2644
|
+
const root = requireProject();
|
|
2398
2645
|
printChanges(root, args.positional.map((name) => removeSkill(root, name, { force: args.force })), args);
|
|
2399
2646
|
}
|
|
2400
2647
|
function sync(args) {
|
|
2401
|
-
const roots = args.all ? visibleProjects() : [
|
|
2648
|
+
const roots = args.all ? visibleProjects() : [requireProject()];
|
|
2402
2649
|
const results = roots.map((root) => {
|
|
2403
2650
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
2404
2651
|
const changes = syncProject(root, { force: args.force });
|
|
2652
|
+
if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
|
|
2405
2653
|
if (!args.json) {
|
|
2406
2654
|
if (args.all) info(`${basename8(root)} ${dim(tildify(root))}`);
|
|
2407
|
-
if (changes.length === 0)
|
|
2408
|
-
|
|
2655
|
+
if (changes.length === 0) {
|
|
2656
|
+
if (existsSync13(join14(root, MANIFEST_FILE))) success("Installed versions match skilllib.json");
|
|
2657
|
+
else info(dim("No skilllib.json: nothing to sync"));
|
|
2658
|
+
} else printChanges(root, changes, args);
|
|
2409
2659
|
}
|
|
2410
2660
|
return { project: basename8(root), path: root, changes: withDirs(root, changes) };
|
|
2411
2661
|
});
|
|
@@ -2413,10 +2663,16 @@ function sync(args) {
|
|
|
2413
2663
|
if (!args.all) rememberProjects(roots);
|
|
2414
2664
|
}
|
|
2415
2665
|
function update(args) {
|
|
2416
|
-
const roots = args.all ? visibleProjects() : [
|
|
2666
|
+
const roots = args.all ? visibleProjects() : [requireProject()];
|
|
2417
2667
|
const results = roots.map((root) => {
|
|
2418
2668
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
2419
|
-
const
|
|
2669
|
+
const names = args.positional.length ? args.positional : void 0;
|
|
2670
|
+
const changes = updateProject(root, names, { force: args.force });
|
|
2671
|
+
if (!args.all) {
|
|
2672
|
+
const { skills } = readManifest(root);
|
|
2673
|
+
for (const name of names ?? []) if (!(name in skills)) changes.push({ name, action: "skipped", reason: "not managed by skilllib in this project" });
|
|
2674
|
+
}
|
|
2675
|
+
if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
|
|
2420
2676
|
if (!args.json) {
|
|
2421
2677
|
if (args.all) info(`${basename8(root)} ${dim(tildify(root))}`);
|
|
2422
2678
|
if (changes.length === 0) success("Everything is on the latest version");
|
|
@@ -2427,7 +2683,7 @@ function update(args) {
|
|
|
2427
2683
|
if (args.json) json(args.all ? results : results[0].changes);
|
|
2428
2684
|
}
|
|
2429
2685
|
function outdated(args) {
|
|
2430
|
-
const roots = args.all ? visibleProjects() : [
|
|
2686
|
+
const roots = args.all ? visibleProjects() : [requireProject()];
|
|
2431
2687
|
const behind = roots.flatMap(
|
|
2432
2688
|
(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
2689
|
);
|
|
@@ -2439,7 +2695,7 @@ function outdated(args) {
|
|
|
2439
2695
|
\u2192 skilllib update${args.all ? " --all" : ""}`));
|
|
2440
2696
|
}
|
|
2441
2697
|
function importSkills(args) {
|
|
2442
|
-
const dirs = args.global ? machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.path) : args.positional.map((p) =>
|
|
2698
|
+
const dirs = args.global ? machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.path) : args.positional.map((p) => resolve7(p));
|
|
2443
2699
|
if (dirs.length === 0) {
|
|
2444
2700
|
error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
|
|
2445
2701
|
process.exit(1);
|
|
@@ -2458,9 +2714,9 @@ function importSkills(args) {
|
|
|
2458
2714
|
continue;
|
|
2459
2715
|
}
|
|
2460
2716
|
success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
|
|
2461
|
-
const skillsDir =
|
|
2462
|
-
const root =
|
|
2463
|
-
if (skillsDir ===
|
|
2717
|
+
const skillsDir = resolve7(dir, "..");
|
|
2718
|
+
const root = resolve7(skillsDir, "..", "..");
|
|
2719
|
+
if (skillsDir === join14(root, ".claude", "skills") && isProjectCandidate(root)) {
|
|
2464
2720
|
if (readManifest(root).skills[name]?.hash !== treeHash(dir)) {
|
|
2465
2721
|
const change = addSkill(root, name);
|
|
2466
2722
|
if (change.action !== "skipped") info(dim(` ${basename8(root)} now depends on ${name} v${change.to}`));
|
|
@@ -2479,24 +2735,34 @@ function scan(args) {
|
|
|
2479
2735
|
}
|
|
2480
2736
|
const found = discoverProjects();
|
|
2481
2737
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
2482
|
-
const skillNames = (root) => [
|
|
2738
|
+
const skillNames = (root) => [
|
|
2739
|
+
.../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.flatMap((d) => skillDirsIn(join14(root, d))).map((d) => basename8(d)), ...nestedSkills(root).map((s) => s.name)])
|
|
2740
|
+
];
|
|
2483
2741
|
const withSkills = found.filter((root) => skillNames(root).length > 0);
|
|
2484
|
-
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project
|
|
2742
|
+
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
|
|
2485
2743
|
`);
|
|
2486
2744
|
table(
|
|
2487
2745
|
withSkills.map((root) => ({
|
|
2488
2746
|
Project: basename8(root),
|
|
2489
2747
|
Path: dim(tildify(root)),
|
|
2490
|
-
Skills:
|
|
2748
|
+
Skills: skillNames(root).map((n) => library.has(n) ? n : yellow(n)).join(", ")
|
|
2491
2749
|
}))
|
|
2492
2750
|
);
|
|
2493
2751
|
if (withSkills.length > 0) info(`
|
|
2494
|
-
${yellow("Yellow")} skills aren't in your library yet (import
|
|
2752
|
+
${yellow("Yellow")} skills aren't in your library yet (\`skilllib import <skill-dir>\`, or \`skilllib\` \u2192 Health).`);
|
|
2495
2753
|
}
|
|
2496
2754
|
function folders(args) {
|
|
2497
2755
|
const [action, ...paths] = args.positional;
|
|
2756
|
+
let failed = false;
|
|
2498
2757
|
if (action === "add") {
|
|
2499
|
-
for (const p of paths)
|
|
2758
|
+
for (const p of paths) {
|
|
2759
|
+
try {
|
|
2760
|
+
success(`Added ${tildify(addRoot(p))}`);
|
|
2761
|
+
} catch (e) {
|
|
2762
|
+
error(e.message);
|
|
2763
|
+
failed = true;
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2500
2766
|
discoverProjects();
|
|
2501
2767
|
} else if (action === "remove" || action === "rm") {
|
|
2502
2768
|
for (const p of paths) {
|
|
@@ -2508,12 +2774,24 @@ function folders(args) {
|
|
|
2508
2774
|
info(roots.length ? `Project folders:
|
|
2509
2775
|
${roots.map((r) => ` ${tildify(r)}`).join("\n")}` : "No project folders. Add one: skilllib folders add ~/Projects");
|
|
2510
2776
|
if (hidden.length) info(dim(`Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")} (skilllib unhide <path>)`));
|
|
2777
|
+
if (failed) process.exit(1);
|
|
2511
2778
|
}
|
|
2512
2779
|
function unhide(args) {
|
|
2780
|
+
if (args.positional.length === 0) {
|
|
2781
|
+
error("Usage: skilllib unhide <path>...");
|
|
2782
|
+
process.exit(1);
|
|
2783
|
+
}
|
|
2784
|
+
let failed = false;
|
|
2513
2785
|
for (const p of args.positional) {
|
|
2514
|
-
|
|
2515
|
-
success(`${tildify(
|
|
2786
|
+
const path = expandHome(p);
|
|
2787
|
+
if (setHidden(path, false)) success(`${tildify(path)} is visible again`);
|
|
2788
|
+
else {
|
|
2789
|
+
const { hidden } = readConfig();
|
|
2790
|
+
error(`${tildify(path)} wasn't hidden. ${hidden.length ? `Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")}` : "No projects are hidden."}`);
|
|
2791
|
+
failed = true;
|
|
2792
|
+
}
|
|
2516
2793
|
}
|
|
2794
|
+
if (failed) process.exit(1);
|
|
2517
2795
|
}
|
|
2518
2796
|
function newSkill(args) {
|
|
2519
2797
|
const [name, ...rest] = args.positional;
|
|
@@ -2526,14 +2804,14 @@ function newSkill(args) {
|
|
|
2526
2804
|
error(`Can't create ${name}: ${r.reason}`);
|
|
2527
2805
|
process.exit(1);
|
|
2528
2806
|
}
|
|
2529
|
-
success(`Created ${tildify(
|
|
2807
|
+
success(`Created ${tildify(join14(r.dir, "SKILL.md"))}`);
|
|
2530
2808
|
}
|
|
2531
2809
|
function restore(args) {
|
|
2532
2810
|
const backups = [...listBackups(), ...pluginBackups()];
|
|
2533
2811
|
const name = args.positional[0];
|
|
2534
2812
|
if (!name) {
|
|
2535
2813
|
if (backups.length === 0) return info("Nothing to restore.");
|
|
2536
|
-
table(backups.map((b) => ({ Skill: b.name, From:
|
|
2814
|
+
table(backups.map((b) => ({ Skill: b.name, From: backupFrom(b), Moved: b.movedAt })));
|
|
2537
2815
|
info(dim("\n\u2192 skilllib restore <name>"));
|
|
2538
2816
|
return;
|
|
2539
2817
|
}
|
|
@@ -2544,7 +2822,10 @@ function restore(args) {
|
|
|
2544
2822
|
}
|
|
2545
2823
|
const r = backup.kind === "plugin" ? restorePlugin(backup) : restoreBackup(backup);
|
|
2546
2824
|
if (r.ok) success(`Restored ${name} to ${tildify(r.to)}`);
|
|
2547
|
-
else
|
|
2825
|
+
else {
|
|
2826
|
+
error(`Can't restore ${name}: ${r.reason}`);
|
|
2827
|
+
process.exitCode = 1;
|
|
2828
|
+
}
|
|
2548
2829
|
}
|
|
2549
2830
|
async function doctor(args) {
|
|
2550
2831
|
const projects2 = visibleProjects();
|
|
@@ -2564,13 +2845,18 @@ async function doctor(args) {
|
|
|
2564
2845
|
})
|
|
2565
2846
|
);
|
|
2566
2847
|
}
|
|
2848
|
+
const fixed = (fix) => {
|
|
2849
|
+
const r = runFix(fix);
|
|
2850
|
+
if (!r.ok) process.exitCode = 1;
|
|
2851
|
+
return r;
|
|
2852
|
+
};
|
|
2567
2853
|
if (args.json) {
|
|
2568
2854
|
return json(
|
|
2569
2855
|
issues.map(({ fix, choices, ...issue }) => ({
|
|
2570
2856
|
...issue,
|
|
2571
2857
|
...fix && { fix: fix.label },
|
|
2572
2858
|
...choices && { choices: choices.map((c) => c.label) },
|
|
2573
|
-
...args.fix && fix && { fixed:
|
|
2859
|
+
...args.fix && fix && { fixed: fixed(fix) }
|
|
2574
2860
|
}))
|
|
2575
2861
|
);
|
|
2576
2862
|
}
|
|
@@ -2580,7 +2866,7 @@ async function doctor(args) {
|
|
|
2580
2866
|
info(dim(` ${issue.detail}${issue.fix ? ` \u2192 ${issue.fix.label}` : ""}`));
|
|
2581
2867
|
if (issue.choices) info(dim(` choose in skilllib \u2192 Health: ${issue.choices.map((c) => c.label).join(" / ")}`));
|
|
2582
2868
|
if (args.fix && issue.fix) {
|
|
2583
|
-
const r =
|
|
2869
|
+
const r = fixed(issue.fix);
|
|
2584
2870
|
info(` ${r.ok ? green("\u2713") : red("\u2717")} ${r.message}`);
|
|
2585
2871
|
}
|
|
2586
2872
|
}
|
|
@@ -2602,7 +2888,7 @@ async function usage(args) {
|
|
|
2602
2888
|
return;
|
|
2603
2889
|
}
|
|
2604
2890
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
2605
|
-
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(
|
|
2891
|
+
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join14(claudeDir(), "projects"))})`)}
|
|
2606
2892
|
`);
|
|
2607
2893
|
table(
|
|
2608
2894
|
summaries.map((s) => ({
|
|
@@ -2621,7 +2907,7 @@ async function usage(args) {
|
|
|
2621
2907
|
}
|
|
2622
2908
|
function show(args) {
|
|
2623
2909
|
const name = args.positional[0];
|
|
2624
|
-
const dir = name ?
|
|
2910
|
+
const dir = name ? join14(libraryDir(), name) : "";
|
|
2625
2911
|
if (!name || !isSkillDir(dir)) {
|
|
2626
2912
|
error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
|
|
2627
2913
|
process.exit(1);
|
|
@@ -2635,7 +2921,7 @@ function show(args) {
|
|
|
2635
2921
|
latest: latestVersion(name)?.version ?? null,
|
|
2636
2922
|
origin: libraryOrigins()[name] ?? "",
|
|
2637
2923
|
projects: knownProjects().filter((p) => name in readManifest(p).skills).map((p) => basename8(p)),
|
|
2638
|
-
skillMd: readFileSync9(
|
|
2924
|
+
skillMd: readFileSync9(join14(dir, "SKILL.md"), "utf-8")
|
|
2639
2925
|
});
|
|
2640
2926
|
}
|
|
2641
2927
|
info(`${name} ${dim(tildify(dir))}`);
|
|
@@ -2664,14 +2950,21 @@ function harnessesCommand(args) {
|
|
|
2664
2950
|
New installs go to ${installDirs(enabled).join(" (real copy) + ")}${installDirs(enabled).length > 1 ? " (link)" : ""}.`));
|
|
2665
2951
|
}
|
|
2666
2952
|
function link(args) {
|
|
2667
|
-
const roots = args.all ? visibleProjects() : [
|
|
2953
|
+
const roots = args.all ? visibleProjects() : [requireProject()];
|
|
2668
2954
|
for (const root of roots) {
|
|
2669
2955
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
2670
2956
|
const res = linkAll(root);
|
|
2671
2957
|
const label = args.all ? `${basename8(root)}: ` : "";
|
|
2672
2958
|
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)
|
|
2959
|
+
else if (!args.all && !res.blocked.length && !res.uncommitted.length) success("Every skill here is already usable by all your agents");
|
|
2960
|
+
if (res.blocked.length) {
|
|
2961
|
+
process.exitCode = 1;
|
|
2962
|
+
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`);
|
|
2963
|
+
}
|
|
2964
|
+
if (res.uncommitted.length)
|
|
2965
|
+
warn(
|
|
2966
|
+
`${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`
|
|
2967
|
+
);
|
|
2675
2968
|
}
|
|
2676
2969
|
}
|
|
2677
2970
|
function globalCommand(args) {
|
|
@@ -2692,14 +2985,41 @@ function globalCommand(args) {
|
|
|
2692
2985
|
const kept = keptGlobal();
|
|
2693
2986
|
if (!yours.length) return info("None of your skills load globally.");
|
|
2694
2987
|
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)).
|
|
2988
|
+
const unreviewed = new Set(yours.filter((m) => !kept.has(m.name)).map((m) => m.name)).size;
|
|
2696
2989
|
if (unreviewed) info(dim(`
|
|
2697
2990
|
${unreviewed} not reviewed. Keep one global on purpose: skilllib global keep <name>`));
|
|
2698
2991
|
}
|
|
2699
2992
|
function tidy(args) {
|
|
2700
2993
|
const names = args.positional;
|
|
2701
|
-
const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ?
|
|
2702
|
-
|
|
2994
|
+
const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve7(base, args.keep) : expandHome(args.keep)])) : {};
|
|
2995
|
+
if (args.keep && !names.length) {
|
|
2996
|
+
error("Usage: skilllib tidy <name>... --keep <folder>");
|
|
2997
|
+
process.exit(1);
|
|
2998
|
+
}
|
|
2999
|
+
const targets = args.global ? [null] : args.all ? visibleProjects() : [requireProject()];
|
|
3000
|
+
const problems = [];
|
|
3001
|
+
for (const name of names) {
|
|
3002
|
+
const where = targets.map((root) => ({ root, ...foldersOf(root, name) })).filter((w) => w.all.length);
|
|
3003
|
+
if (!where.length) {
|
|
3004
|
+
problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${basename8(targets[0])}`}`);
|
|
3005
|
+
continue;
|
|
3006
|
+
}
|
|
3007
|
+
if (!args.keep) continue;
|
|
3008
|
+
for (const { root, real } of where) {
|
|
3009
|
+
if (real.includes(keepFor(root)[name])) continue;
|
|
3010
|
+
const label = root ? `${basename8(root)} \xB7 ` : "";
|
|
3011
|
+
problems.push(
|
|
3012
|
+
`${label}${name}: no copy in ${args.keep}; ${real.length ? `pick one of: ${real.map((d) => folderLabel(root, d)).join(", ")}` : "it has no real copy to keep"}`
|
|
3013
|
+
);
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
if (problems.length) {
|
|
3017
|
+
for (const p of problems) error(p);
|
|
3018
|
+
process.exit(1);
|
|
3019
|
+
}
|
|
3020
|
+
const reports = targets.map(
|
|
3021
|
+
(root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label: basename8(root), report: planProjectTidy(root, { keep: keepFor(root) }) }
|
|
3022
|
+
);
|
|
2703
3023
|
let held = 0;
|
|
2704
3024
|
let changed = 0;
|
|
2705
3025
|
for (const { label, report } of reports) {
|
|
@@ -2714,6 +3034,7 @@ function tidy(args) {
|
|
|
2714
3034
|
held += args.dryRun ? 0 : result.held.length;
|
|
2715
3035
|
changed += result.applied.length;
|
|
2716
3036
|
}
|
|
3037
|
+
if (args.keep && conflicts.length) process.exitCode = 1;
|
|
2717
3038
|
for (const c of conflicts) {
|
|
2718
3039
|
warn(`${label} \xB7 ${c.name}: the copies differ`);
|
|
2719
3040
|
for (const copy of c.copies) {
|
|
@@ -2756,16 +3077,19 @@ function agentSkill(args) {
|
|
|
2756
3077
|
}
|
|
2757
3078
|
|
|
2758
3079
|
export {
|
|
2759
|
-
userHome,
|
|
2760
3080
|
skilllibHome,
|
|
3081
|
+
PROJECT_SKILLS_DIR,
|
|
2761
3082
|
HARNESSES,
|
|
2762
3083
|
harness,
|
|
2763
3084
|
readSkillInfo,
|
|
2764
3085
|
versionHistory,
|
|
2765
3086
|
forgetLatest,
|
|
2766
3087
|
latestVersion,
|
|
2767
|
-
|
|
2768
|
-
|
|
3088
|
+
projectHere,
|
|
3089
|
+
readManifest,
|
|
3090
|
+
setJsonMode,
|
|
3091
|
+
error,
|
|
3092
|
+
tildify,
|
|
2769
3093
|
readConfig,
|
|
2770
3094
|
addRoot,
|
|
2771
3095
|
removeRoot,
|
|
@@ -2785,6 +3109,7 @@ export {
|
|
|
2785
3109
|
recordOrigin,
|
|
2786
3110
|
librarySkills,
|
|
2787
3111
|
librarySkillDir,
|
|
3112
|
+
visibilityOf,
|
|
2788
3113
|
projectStatus,
|
|
2789
3114
|
linkEverywhere,
|
|
2790
3115
|
relinkDependency,
|
|
@@ -2802,8 +3127,6 @@ export {
|
|
|
2802
3127
|
createSkill,
|
|
2803
3128
|
listBackups,
|
|
2804
3129
|
restoreBackup,
|
|
2805
|
-
setJsonMode,
|
|
2806
|
-
error,
|
|
2807
3130
|
gitInfo,
|
|
2808
3131
|
relativeTo,
|
|
2809
3132
|
claudePlugins,
|
|
@@ -2813,6 +3136,9 @@ export {
|
|
|
2813
3136
|
pluginBackups,
|
|
2814
3137
|
restorePlugin,
|
|
2815
3138
|
cursorPluginSkills,
|
|
3139
|
+
agentSkillState,
|
|
3140
|
+
installAgentSkill,
|
|
3141
|
+
removeAgentSkill,
|
|
2816
3142
|
refreshAgentSkill,
|
|
2817
3143
|
planProjectTidy,
|
|
2818
3144
|
planGlobalTidy,
|
|
@@ -2820,10 +3146,12 @@ export {
|
|
|
2820
3146
|
applyTidy,
|
|
2821
3147
|
describeStep,
|
|
2822
3148
|
copyLabel,
|
|
3149
|
+
addedAt,
|
|
2823
3150
|
scanUsage,
|
|
2824
3151
|
parseArgs,
|
|
2825
3152
|
projectOfFactory,
|
|
2826
3153
|
status,
|
|
3154
|
+
overview,
|
|
2827
3155
|
list,
|
|
2828
3156
|
projects,
|
|
2829
3157
|
add,
|