skilllib 1.11.1 → 1.11.3
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 +31 -25
- package/dist/{chunk-LO4FMXDE.js → chunk-3XDWEHI4.js} +785 -485
- package/dist/index.js +49 -15
- package/dist/json.d.cts +7 -2
- package/dist/json.d.ts +7 -2
- package/dist/{tui-3LDZ5GOZ.js → tui-WSHQRBPF.js} +295 -703
- package/package.json +1 -1
- package/skills/skilllib/SKILL.md +5 -5
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import { resolve as resolve2, sep } from "path";
|
|
3
3
|
|
|
4
4
|
// src/paths.ts
|
|
5
|
-
import { realpathSync } from "fs";
|
|
5
|
+
import { mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
|
|
6
6
|
import { homedir } from "os";
|
|
7
|
-
import { join, resolve } from "path";
|
|
7
|
+
import { dirname, join, resolve } from "path";
|
|
8
8
|
function userHome() {
|
|
9
9
|
return process.env.HOME || homedir();
|
|
10
10
|
}
|
|
@@ -41,12 +41,53 @@ function isRepoSkillLocation(location) {
|
|
|
41
41
|
return location === AGENTS_SKILLS_DIR || location === GROK_SKILLS_DIR;
|
|
42
42
|
}
|
|
43
43
|
var OWN_SKILL_MARKER = ".managed-by-skilllib";
|
|
44
|
+
function readJson(path) {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function realpathOrNull(path) {
|
|
52
|
+
try {
|
|
53
|
+
return realpathSync(path);
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function readState(path) {
|
|
59
|
+
let text;
|
|
60
|
+
try {
|
|
61
|
+
text = readFileSync(path, "utf-8");
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (err.code === "ENOENT") return null;
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(text);
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error(`${path} isn't valid JSON: fix it, or move it away to start that file over`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function writeState(path, data) {
|
|
73
|
+
const target = realpathOrNull(path) ?? path;
|
|
74
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
75
|
+
const temp = `${target}.${process.pid}.tmp`;
|
|
76
|
+
try {
|
|
77
|
+
writeFileSync(temp, JSON.stringify(data, null, 2) + "\n", { mode: statSync(target, { throwIfNoEntry: false })?.mode ?? 420 });
|
|
78
|
+
renameSync(temp, target);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
rmSync(temp, { force: true });
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
44
84
|
|
|
45
85
|
// src/output.ts
|
|
46
86
|
var jsonMode = false;
|
|
47
87
|
function setJsonMode(on) {
|
|
48
88
|
jsonMode = on;
|
|
49
89
|
}
|
|
90
|
+
var isJsonMode = () => jsonMode;
|
|
50
91
|
function json(output, data) {
|
|
51
92
|
process.stdout.write(JSON.stringify(data) + "\n");
|
|
52
93
|
}
|
|
@@ -70,21 +111,25 @@ function truncate(s, max) {
|
|
|
70
111
|
return line.length > max ? line.slice(0, max - 1) + "\u2026" : line;
|
|
71
112
|
}
|
|
72
113
|
var visible = (s) => s.replace(/\x1b\[\d+m/g, "");
|
|
73
|
-
function table(
|
|
74
|
-
if (
|
|
75
|
-
const keys = Object.keys(
|
|
76
|
-
const widths = keys.map((k) => Math.max(k.length, ...
|
|
114
|
+
function table(rows2) {
|
|
115
|
+
if (rows2.length === 0) return;
|
|
116
|
+
const keys = Object.keys(rows2[0]);
|
|
117
|
+
const widths = keys.map((k) => Math.max(k.length, ...rows2.map((r) => visible(r[k] ?? "").length)));
|
|
77
118
|
const pad = (s, w) => s + " ".repeat(Math.max(0, w - visible(s).length));
|
|
78
119
|
info(dim(keys.map((k, i) => pad(k, widths[i])).join(" ")));
|
|
79
|
-
for (const row of
|
|
120
|
+
for (const row of rows2) info(keys.map((k, i) => pad(row[k] ?? "", widths[i])).join(" ").trimEnd());
|
|
121
|
+
}
|
|
122
|
+
function localStamp(ms) {
|
|
123
|
+
const at = new Date(ms);
|
|
124
|
+
return new Date(ms - at.getTimezoneOffset() * 6e4).toISOString().slice(0, 16).replace("T", " ");
|
|
80
125
|
}
|
|
81
126
|
|
|
82
127
|
// src/agentSkill.ts
|
|
83
|
-
import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as
|
|
128
|
+
import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync4, rmSync as rmSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
84
129
|
import { join as join9 } from "path";
|
|
85
130
|
|
|
86
131
|
// src/config.ts
|
|
87
|
-
import { existsSync as existsSync5,
|
|
132
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
88
133
|
import { homedir as homedir2 } from "os";
|
|
89
134
|
import { join as join6, resolve as resolve4 } from "path";
|
|
90
135
|
|
|
@@ -184,16 +229,16 @@ function installDirs(enabled, keep) {
|
|
|
184
229
|
}
|
|
185
230
|
|
|
186
231
|
// src/project.ts
|
|
187
|
-
import { existsSync as existsSync4,
|
|
188
|
-
import { dirname, join as join5, resolve as resolve3 } from "path";
|
|
232
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
233
|
+
import { dirname as dirname2, join as join5, resolve as resolve3 } from "path";
|
|
189
234
|
|
|
190
235
|
// src/versions.ts
|
|
191
|
-
import { existsSync as existsSync3, mkdirSync
|
|
236
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
192
237
|
import { join as join4 } from "path";
|
|
193
238
|
|
|
194
239
|
// src/skills.ts
|
|
195
240
|
import { createHash } from "crypto";
|
|
196
|
-
import { cpSync, existsSync as existsSync2, readdirSync, readFileSync, realpathSync as realpathSync2, rmSync, statSync } from "fs";
|
|
241
|
+
import { cpSync, existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, realpathSync as realpathSync2, rmSync as rmSync2, statSync as statSync2 } from "fs";
|
|
197
242
|
import { basename, join as join3, relative, sep as sep2 } from "path";
|
|
198
243
|
import { parse as parseYaml } from "yaml";
|
|
199
244
|
var IGNORED = /* @__PURE__ */ new Set([".DS_Store", ".git", "node_modules"]);
|
|
@@ -201,7 +246,7 @@ function readSkillInfo(dir) {
|
|
|
201
246
|
const skillMd = join3(dir, "SKILL.md");
|
|
202
247
|
const fallback = { name: basename(dir), description: "" };
|
|
203
248
|
if (!existsSync2(skillMd)) return fallback;
|
|
204
|
-
const match =
|
|
249
|
+
const match = readFileSync2(skillMd, "utf-8").match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
205
250
|
if (!match) return fallback;
|
|
206
251
|
try {
|
|
207
252
|
const fm = parseYaml(match[1]);
|
|
@@ -219,21 +264,23 @@ function isSkillDir(dir) {
|
|
|
219
264
|
function listFiles(dir) {
|
|
220
265
|
return readdirSync(dir).filter((entry) => !IGNORED.has(entry)).flatMap((entry) => {
|
|
221
266
|
const full = join3(dir, entry);
|
|
222
|
-
|
|
267
|
+
const st = statSync2(full, { throwIfNoEntry: false });
|
|
268
|
+
return !st ? [] : st.isDirectory() ? listFiles(full) : [full];
|
|
223
269
|
});
|
|
224
270
|
}
|
|
225
271
|
function treeHash(dir) {
|
|
226
272
|
if (!existsSync2(dir)) return null;
|
|
227
273
|
const hash = createHash("sha256");
|
|
228
|
-
const files = listFiles(dir).map((f) => ({ path: relative(dir, f).split(sep2).join("/"), full: f })).sort((a, b) => a.path.localeCompare(b.path));
|
|
274
|
+
const files = listFiles(dir).map((f) => ({ path: relative(dir, f).split(sep2).join("/"), full: f })).sort((a, b) => a.path.localeCompare(b.path, "en"));
|
|
229
275
|
for (const file of files) {
|
|
230
|
-
hash.update(file.path).update("\0").update(createHash("sha256").update(
|
|
276
|
+
hash.update(file.path).update("\0").update(createHash("sha256").update(readFileSync2(file.full)).digest("hex")).update("\n");
|
|
231
277
|
}
|
|
232
278
|
return hash.digest("hex").slice(0, 16);
|
|
233
279
|
}
|
|
234
280
|
function copySkill(from, to) {
|
|
235
|
-
|
|
236
|
-
|
|
281
|
+
const source = realpathSync2(from);
|
|
282
|
+
rmSync2(to, { recursive: true, force: true });
|
|
283
|
+
cpSync(source, to, { recursive: true, dereference: true, filter: (src) => !IGNORED.has(basename(src)) && existsSync2(src) });
|
|
237
284
|
}
|
|
238
285
|
function skillDirsIn(dir) {
|
|
239
286
|
if (!existsSync2(dir)) return [];
|
|
@@ -248,11 +295,7 @@ function indexFile(name) {
|
|
|
248
295
|
return join4(storeDir(name), "versions.json");
|
|
249
296
|
}
|
|
250
297
|
function versionHistory(name) {
|
|
251
|
-
|
|
252
|
-
return JSON.parse(readFileSync2(indexFile(name), "utf-8"));
|
|
253
|
-
} catch {
|
|
254
|
-
return [];
|
|
255
|
-
}
|
|
298
|
+
return readState(indexFile(name)) ?? [];
|
|
256
299
|
}
|
|
257
300
|
function versionDir(name, version) {
|
|
258
301
|
return join4(storeDir(name), String(version));
|
|
@@ -274,9 +317,10 @@ function latestVersion(name) {
|
|
|
274
317
|
result = last;
|
|
275
318
|
} else {
|
|
276
319
|
const next = { version: (last?.version ?? 0) + 1, hash, date: (/* @__PURE__ */ new Date()).toISOString() };
|
|
277
|
-
|
|
320
|
+
if (existsSync3(versionDir(name, next.version))) throw new Error(`${versionDir(name, next.version)} exists but ${indexFile(name)} doesn't list it: fix the index before adding versions`);
|
|
321
|
+
mkdirSync2(storeDir(name), { recursive: true });
|
|
278
322
|
copySkill(source, versionDir(name, next.version));
|
|
279
|
-
|
|
323
|
+
writeState(indexFile(name), [...history, next]);
|
|
280
324
|
result = next;
|
|
281
325
|
}
|
|
282
326
|
}
|
|
@@ -295,7 +339,7 @@ function findProjectRoot(from = process.cwd()) {
|
|
|
295
339
|
let dir = resolve3(from);
|
|
296
340
|
while (true) {
|
|
297
341
|
if (existsSync4(join5(dir, MANIFEST_FILE)) || existsSync4(join5(dir, ".git"))) return dir;
|
|
298
|
-
const parent =
|
|
342
|
+
const parent = dirname2(dir);
|
|
299
343
|
if (parent === dir) return resolve3(from);
|
|
300
344
|
dir = parent;
|
|
301
345
|
}
|
|
@@ -307,7 +351,12 @@ function projectHere(from = process.cwd()) {
|
|
|
307
351
|
function readManifest(root) {
|
|
308
352
|
const path = join5(root, MANIFEST_FILE);
|
|
309
353
|
if (!existsSync4(path)) return { skills: {} };
|
|
310
|
-
|
|
354
|
+
let data;
|
|
355
|
+
try {
|
|
356
|
+
data = JSON.parse(readFileSync3(path, "utf-8"));
|
|
357
|
+
} catch {
|
|
358
|
+
throw new Error(`${path} isn't valid JSON (a merge conflict?): fix it, then run skilllib again`);
|
|
359
|
+
}
|
|
311
360
|
const skills = Object.fromEntries(
|
|
312
361
|
Object.entries(data.skills ?? {}).flatMap(([name, value]) => {
|
|
313
362
|
if (name.startsWith("@")) return [];
|
|
@@ -332,8 +381,8 @@ function readManifest(root) {
|
|
|
332
381
|
return { ...data, skills };
|
|
333
382
|
}
|
|
334
383
|
function writeManifest(root, manifest) {
|
|
335
|
-
const skills = Object.fromEntries(Object.entries(manifest.skills).sort(([a], [b]) => a.localeCompare(b)));
|
|
336
|
-
|
|
384
|
+
const skills = Object.fromEntries(Object.entries(manifest.skills).sort(([a], [b]) => a.localeCompare(b, "en")));
|
|
385
|
+
writeState(join5(root, MANIFEST_FILE), { ...manifest, skills });
|
|
337
386
|
}
|
|
338
387
|
function projectsFile() {
|
|
339
388
|
return join5(skilllibHome(), "projects.json");
|
|
@@ -341,7 +390,7 @@ function projectsFile() {
|
|
|
341
390
|
function knownProjects() {
|
|
342
391
|
const path = projectsFile();
|
|
343
392
|
if (!existsSync4(path)) return [];
|
|
344
|
-
const list2 =
|
|
393
|
+
const list2 = readState(path) ?? [];
|
|
345
394
|
return onePerFolder(list2.filter((p) => existsSync4(p) && isProjectCandidate(p) && !isAgentWorktree(p)));
|
|
346
395
|
}
|
|
347
396
|
function isAgentWorktree(path) {
|
|
@@ -362,8 +411,27 @@ function isProjectCandidate(root) {
|
|
|
362
411
|
function rememberProjects(roots) {
|
|
363
412
|
const incoming = roots.map((r) => resolve3(r)).filter((r) => isProjectCandidate(r) && !isAgentWorktree(r));
|
|
364
413
|
const all = onePerFolder([...knownProjects(), ...incoming]).sort();
|
|
365
|
-
|
|
366
|
-
|
|
414
|
+
writeState(projectsFile(), all);
|
|
415
|
+
}
|
|
416
|
+
function readableRoots(roots) {
|
|
417
|
+
const unreadable = [];
|
|
418
|
+
const readable = roots.filter((root) => {
|
|
419
|
+
try {
|
|
420
|
+
readManifest(root);
|
|
421
|
+
return true;
|
|
422
|
+
} catch (err) {
|
|
423
|
+
unreadable.push({ root, reason: err.message });
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
return { readable, unreadable };
|
|
428
|
+
}
|
|
429
|
+
function pinsIn(root) {
|
|
430
|
+
try {
|
|
431
|
+
return readManifest(root).skills;
|
|
432
|
+
} catch {
|
|
433
|
+
return {};
|
|
434
|
+
}
|
|
367
435
|
}
|
|
368
436
|
|
|
369
437
|
// src/config.ts
|
|
@@ -371,16 +439,11 @@ function configFile() {
|
|
|
371
439
|
return join6(skilllibHome(), "config.json");
|
|
372
440
|
}
|
|
373
441
|
function readConfig() {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
return { ...data, roots: data.roots ?? [], hidden: data.hidden ?? [] };
|
|
377
|
-
} catch {
|
|
378
|
-
return { roots: [], hidden: [] };
|
|
379
|
-
}
|
|
442
|
+
const data = readState(configFile()) ?? {};
|
|
443
|
+
return { ...data, roots: data.roots ?? [], hidden: data.hidden ?? [] };
|
|
380
444
|
}
|
|
381
445
|
function writeConfig(config) {
|
|
382
|
-
|
|
383
|
-
writeFileSync3(configFile(), JSON.stringify(config, null, 2) + "\n");
|
|
446
|
+
writeState(configFile(), config);
|
|
384
447
|
}
|
|
385
448
|
function expandHome(path) {
|
|
386
449
|
return resolve4(path.replace(/^~(?=$|\/)/, homedir2()));
|
|
@@ -388,7 +451,7 @@ function expandHome(path) {
|
|
|
388
451
|
function addRoot(path) {
|
|
389
452
|
const root = expandHome(path);
|
|
390
453
|
if (!existsSync5(root)) throw new Error(`${tildify(root)} doesn't exist`);
|
|
391
|
-
if (!
|
|
454
|
+
if (!statSync3(root).isDirectory()) throw new Error(`${tildify(root)} isn't a folder`);
|
|
392
455
|
const config = readConfig();
|
|
393
456
|
if (!config.roots.includes(root)) writeConfig({ ...config, roots: [...config.roots, root].sort() });
|
|
394
457
|
return root;
|
|
@@ -461,35 +524,25 @@ function setKeepGlobal(names, keep) {
|
|
|
461
524
|
|
|
462
525
|
// src/library.ts
|
|
463
526
|
import { execFileSync } from "child_process";
|
|
464
|
-
import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as
|
|
465
|
-
import { basename as basename3, dirname as
|
|
527
|
+
import { cpSync as cpSync2, existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readdirSync as readdirSync4, renameSync as renameSync2, rmSync as rmSync3, symlinkSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
528
|
+
import { basename as basename3, dirname as dirname3, join as join8, relative as relative2, sep as sep4 } from "path";
|
|
466
529
|
|
|
467
530
|
// src/sources.ts
|
|
468
|
-
import { existsSync as existsSync6, lstatSync, mkdirSync as
|
|
469
|
-
import { basename as basename2, join as join7, resolve as resolve5 } from "path";
|
|
470
|
-
|
|
471
|
-
try {
|
|
472
|
-
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
473
|
-
} catch {
|
|
474
|
-
return null;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
531
|
+
import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readlinkSync } from "fs";
|
|
532
|
+
import { basename as basename2, join as join7, resolve as resolve5, sep as sep3 } from "path";
|
|
533
|
+
import { tmpdir } from "os";
|
|
477
534
|
function skillsShLock() {
|
|
478
535
|
return readJson(join7(userHome(), ".agents", ".skill-lock.json"))?.skills ?? {};
|
|
479
536
|
}
|
|
480
537
|
function projectSkillsLock(root) {
|
|
481
538
|
return readJson(join7(root, "skills-lock.json"))?.skills ?? {};
|
|
482
539
|
}
|
|
483
|
-
function
|
|
484
|
-
|
|
485
|
-
return realpathSync3(path);
|
|
486
|
-
} catch {
|
|
487
|
-
return null;
|
|
488
|
-
}
|
|
540
|
+
function vendorSkill(path, kind, origin, harnesses) {
|
|
541
|
+
return { name: basename2(path), kind, origin, path, description: readSkillInfo(path).description, movable: false, broken: false, harnesses, links: [] };
|
|
489
542
|
}
|
|
490
543
|
function harnessesReading(dir, enabled) {
|
|
491
544
|
const real = realPath(dir);
|
|
492
|
-
return enabled.filter((id) =>
|
|
545
|
+
return enabled.filter((id) => harness(id).globalDirs().some((d) => realPath(d) === real));
|
|
493
546
|
}
|
|
494
547
|
var systemDirs = () => [codexSystemDir()];
|
|
495
548
|
function globalSkillDirs() {
|
|
@@ -508,17 +561,7 @@ function globalDirAs(dir) {
|
|
|
508
561
|
}
|
|
509
562
|
function systemSkills(enabled) {
|
|
510
563
|
return systemDirs().flatMap(
|
|
511
|
-
(dir) => skillDirsIn(dir).map((path) => (
|
|
512
|
-
name: basename2(path),
|
|
513
|
-
kind: "system",
|
|
514
|
-
origin: dir.replace(/\\/g, "/"),
|
|
515
|
-
path,
|
|
516
|
-
description: readSkillInfo(path).description,
|
|
517
|
-
movable: false,
|
|
518
|
-
broken: false,
|
|
519
|
-
harnesses: harnessesReading(dir, enabled),
|
|
520
|
-
links: []
|
|
521
|
-
}))
|
|
564
|
+
(dir) => skillDirsIn(dir).map((path) => vendorSkill(path, "system", dir.replace(/\\/g, "/"), harnessesReading(dir, enabled)))
|
|
522
565
|
).filter((s) => s.harnesses.length > 0);
|
|
523
566
|
}
|
|
524
567
|
function globalFolderSkills(enabled) {
|
|
@@ -547,7 +590,7 @@ function globalFolderSkills(enabled) {
|
|
|
547
590
|
const inAgents = e.dir === agentsDir || target_.includes(".agents/skills");
|
|
548
591
|
const locked = lock[e.name]?.source;
|
|
549
592
|
if (existsSync6(join7(e.path, OWN_SKILL_MARKER))) {
|
|
550
|
-
skills.push(
|
|
593
|
+
skills.push(vendorSkill(e.path, "skilllib", "skilllib", harnessesReading(e.dir, enabled)));
|
|
551
594
|
continue;
|
|
552
595
|
}
|
|
553
596
|
skills.push({
|
|
@@ -575,31 +618,11 @@ function tildify2(p) {
|
|
|
575
618
|
}
|
|
576
619
|
function grokBuiltInSkills(enabled) {
|
|
577
620
|
if (!enabled.includes("grok")) return [];
|
|
578
|
-
return skillDirsIn(join7(grokDir(), "bundled", "skills")).map((path) => (
|
|
579
|
-
name: basename2(path),
|
|
580
|
-
kind: "built-in",
|
|
581
|
-
origin: "Grok",
|
|
582
|
-
path,
|
|
583
|
-
description: readSkillInfo(path).description,
|
|
584
|
-
movable: false,
|
|
585
|
-
broken: false,
|
|
586
|
-
harnesses: ["grok"],
|
|
587
|
-
links: []
|
|
588
|
-
}));
|
|
621
|
+
return skillDirsIn(join7(grokDir(), "bundled", "skills")).map((path) => vendorSkill(path, "built-in", "Grok", ["grok"]));
|
|
589
622
|
}
|
|
590
623
|
function cursorBuiltInSkills(enabled) {
|
|
591
624
|
if (!enabled.includes("cursor")) return [];
|
|
592
|
-
return skillDirsIn(join7(userHome(), ".cursor", "skills-cursor")).map((path) => (
|
|
593
|
-
name: basename2(path),
|
|
594
|
-
kind: "built-in",
|
|
595
|
-
origin: "Cursor",
|
|
596
|
-
path,
|
|
597
|
-
description: readSkillInfo(path).description,
|
|
598
|
-
movable: false,
|
|
599
|
-
broken: false,
|
|
600
|
-
harnesses: ["cursor"],
|
|
601
|
-
links: []
|
|
602
|
-
}));
|
|
625
|
+
return skillDirsIn(join7(userHome(), ".cursor", "skills-cursor")).map((path) => vendorSkill(path, "built-in", "Cursor", ["cursor"]));
|
|
603
626
|
}
|
|
604
627
|
function claudeAiSkills(enabled) {
|
|
605
628
|
const byReal = /* @__PURE__ */ new Map();
|
|
@@ -614,20 +637,13 @@ function claudeAiSkills(enabled) {
|
|
|
614
637
|
}
|
|
615
638
|
return [...byReal.values()].flatMap(
|
|
616
639
|
({ dir, readers }) => readdirSync3(dir).filter((d) => !d.startsWith(".")).flatMap(
|
|
617
|
-
(bucket) => skillDirsIn(join7(dir, bucket)).map((path) => (
|
|
618
|
-
name: basename2(path),
|
|
619
|
-
kind: "claude.ai",
|
|
620
|
-
origin: "claude.ai account",
|
|
621
|
-
path,
|
|
622
|
-
description: readSkillInfo(path).description,
|
|
623
|
-
movable: false,
|
|
624
|
-
broken: false,
|
|
625
|
-
harnesses: enabled.filter((id) => readers.includes(id)),
|
|
626
|
-
links: []
|
|
627
|
-
}))
|
|
640
|
+
(bucket) => skillDirsIn(join7(dir, bucket)).map((path) => vendorSkill(path, "claude.ai", "claude.ai account", enabled.filter((id) => readers.includes(id))))
|
|
628
641
|
)
|
|
629
642
|
);
|
|
630
643
|
}
|
|
644
|
+
function installedPluginsJson() {
|
|
645
|
+
return readJson(join7(claudeDir(), "plugins", "installed_plugins.json"))?.plugins ?? {};
|
|
646
|
+
}
|
|
631
647
|
function pluginDirCandidates(id, installPaths) {
|
|
632
648
|
const [name = "", marketplace = ""] = id.split("@");
|
|
633
649
|
const plugins = join7(claudeDir(), "plugins");
|
|
@@ -650,7 +666,7 @@ function pluginSkills(harnesses, root) {
|
|
|
650
666
|
const cursor = harnesses.includes("cursor");
|
|
651
667
|
if (!claude2 && !cursor) return [];
|
|
652
668
|
const plugins = join7(claudeDir(), "plugins");
|
|
653
|
-
const installed =
|
|
669
|
+
const installed = installedPluginsJson();
|
|
654
670
|
const user = enabledPlugins();
|
|
655
671
|
const enabled = root ? enabledPlugins(root) : user;
|
|
656
672
|
const fromMarketplaces = [.../* @__PURE__ */ new Set([...Object.keys(enabled), ...Object.keys(user)])].filter((id) => claude2 && enabled[id] || cursor && user[id]).flatMap((id) => {
|
|
@@ -670,17 +686,7 @@ function pluginSkills(harnesses, root) {
|
|
|
670
686
|
return skillDirsIn(join7(root2, "skills")).map((path) => ({ path, origin: `${name}@synced`, loadedBy: ["claude-code"] }));
|
|
671
687
|
})
|
|
672
688
|
) : [];
|
|
673
|
-
return [...fromMarketplaces, ...fromSynced].map(({ path, origin, loadedBy }) => (
|
|
674
|
-
name: basename2(path),
|
|
675
|
-
kind: "plugin",
|
|
676
|
-
origin,
|
|
677
|
-
path,
|
|
678
|
-
description: readSkillInfo(path).description,
|
|
679
|
-
movable: false,
|
|
680
|
-
broken: false,
|
|
681
|
-
harnesses: loadedBy,
|
|
682
|
-
links: []
|
|
683
|
-
}));
|
|
689
|
+
return [...fromMarketplaces, ...fromSynced].map(({ path, origin, loadedBy }) => vendorSkill(path, "plugin", origin, loadedBy));
|
|
684
690
|
}
|
|
685
691
|
var byKindAndName = (a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name);
|
|
686
692
|
function machineSkills(enabled = enabledHarnesses()) {
|
|
@@ -702,15 +708,18 @@ function originsFile() {
|
|
|
702
708
|
return join7(skilllibHome(), "origins.json");
|
|
703
709
|
}
|
|
704
710
|
function libraryOrigins() {
|
|
705
|
-
return
|
|
711
|
+
return readState(originsFile()) ?? {};
|
|
706
712
|
}
|
|
707
713
|
function recordOrigin(name, origin) {
|
|
708
714
|
const origins = libraryOrigins();
|
|
709
715
|
if (origins[name] === origin) return;
|
|
710
|
-
|
|
711
|
-
|
|
716
|
+
mkdirSync3(skilllibHome(), { recursive: true });
|
|
717
|
+
writeState(originsFile(), { ...origins, [name]: origin });
|
|
712
718
|
}
|
|
713
719
|
function originFor(dir) {
|
|
720
|
+
const under = (path, folder) => path.startsWith((realpathOrNull(folder) ?? folder) + sep3);
|
|
721
|
+
const real = realpathOrNull(dir) ?? dir;
|
|
722
|
+
if ([tmpdir(), "/tmp", "/var/tmp"].some((t) => under(real, t)) && !under(real, userHome())) return "";
|
|
714
723
|
const match = machineSkills().find((s) => resolve5(s.path) === resolve5(dir));
|
|
715
724
|
if (match) return match.kind === "global" ? "global folder" : `${match.kind}: ${match.origin}`;
|
|
716
725
|
const project = dir.split(/[\\/]\.(?:claude|agents|grok|cursor|codex)[\\/]skills[\\/]/)[0];
|
|
@@ -730,13 +739,6 @@ function librarySkills() {
|
|
|
730
739
|
function librarySkillDir(name) {
|
|
731
740
|
return join8(libraryDir(), name);
|
|
732
741
|
}
|
|
733
|
-
function realpathOrNull2(path) {
|
|
734
|
-
try {
|
|
735
|
-
return realpathSync4(path);
|
|
736
|
-
} catch {
|
|
737
|
-
return null;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
742
|
function isLink(path) {
|
|
741
743
|
try {
|
|
742
744
|
return lstatSync2(path).isSymbolicLink();
|
|
@@ -746,7 +748,7 @@ function isLink(path) {
|
|
|
746
748
|
}
|
|
747
749
|
function linkDir(target, link2) {
|
|
748
750
|
if (process.platform === "win32") symlinkSync(target, link2, "junction");
|
|
749
|
-
else symlinkSync(relative2(
|
|
751
|
+
else symlinkSync(relative2(dirname3(link2), target), link2);
|
|
750
752
|
}
|
|
751
753
|
function entryExists(path) {
|
|
752
754
|
return isLink(path) || existsSync7(path);
|
|
@@ -758,9 +760,9 @@ function visibilityOf(root, name, enabled = enabledHarnesses()) {
|
|
|
758
760
|
}));
|
|
759
761
|
}
|
|
760
762
|
function dependencyDirs(root, name, dep) {
|
|
761
|
-
const wanted = installDirs(enabledHarnesses());
|
|
762
763
|
const untracked = ALL_PROJECT_DIRS.find((d) => d !== GROK_SKILLS_DIR && existsSync7(join8(root, d, name, "SKILL.md")) && !isLink(join8(root, d, name)));
|
|
763
|
-
const primary = dep ? dep.dir ?? PROJECT_SKILLS_DIR : untracked ??
|
|
764
|
+
const primary = dep ? dep.dir ?? PROJECT_SKILLS_DIR : untracked ?? installDirs(enabledHarnesses())[0];
|
|
765
|
+
const wanted = installDirs(enabledHarnesses(), primary);
|
|
764
766
|
return [primary, .../* @__PURE__ */ new Set([...dep?.links ?? [], ...wanted.filter((d) => d !== primary)])];
|
|
765
767
|
}
|
|
766
768
|
function isGitTracked(root, dir) {
|
|
@@ -806,7 +808,7 @@ function projectStatus(root) {
|
|
|
806
808
|
}
|
|
807
809
|
for (const [name, paths] of byName) {
|
|
808
810
|
const real = paths.find((p) => !isLink(p)) ?? paths[0];
|
|
809
|
-
const location = relative2(root, join8(real, "..")).split(
|
|
811
|
+
const location = relative2(root, join8(real, "..")).split(sep4).join("/");
|
|
810
812
|
const source = lock[name]?.source;
|
|
811
813
|
unmanaged.push({
|
|
812
814
|
name,
|
|
@@ -851,9 +853,9 @@ function nestedSkills(root, { depth = 3, enabled = enabledHarnesses() } = {}) {
|
|
|
851
853
|
for (const sub of subs) {
|
|
852
854
|
if (existsSync7(join8(sub, ".git"))) continue;
|
|
853
855
|
for (const [skillsDir, readers] of NESTED_READERS) {
|
|
854
|
-
const location = relative2(root, join8(sub, skillsDir)).split(
|
|
856
|
+
const location = relative2(root, join8(sub, skillsDir)).split(sep4).join("/");
|
|
855
857
|
for (const path of skillDirsIn(join8(sub, skillsDir))) {
|
|
856
|
-
if (isLink(path) && (
|
|
858
|
+
if (isLink(path) && (realpathOrNull(path) ?? "").startsWith((realpathOrNull(root) ?? root) + sep4)) continue;
|
|
857
859
|
const name = basename3(path);
|
|
858
860
|
found.push({
|
|
859
861
|
name,
|
|
@@ -904,7 +906,7 @@ function linkEverywhere(root, name, location, { allowTracked = false, dryRun = f
|
|
|
904
906
|
}
|
|
905
907
|
}
|
|
906
908
|
if (!dryRun) {
|
|
907
|
-
|
|
909
|
+
mkdirSync4(join8(root, dir), { recursive: true });
|
|
908
910
|
linkDir(join8(root, location, name), link2);
|
|
909
911
|
}
|
|
910
912
|
result.created.push(dir);
|
|
@@ -912,12 +914,12 @@ function linkEverywhere(root, name, location, { allowTracked = false, dryRun = f
|
|
|
912
914
|
return result;
|
|
913
915
|
}
|
|
914
916
|
function unlinkEverywhere(root, name, removing) {
|
|
915
|
-
const gone = removing ?
|
|
917
|
+
const gone = removing ? realpathOrNull(removing) : null;
|
|
916
918
|
return ALL_PROJECT_DIRS.filter((dir) => {
|
|
917
919
|
const link2 = join8(root, dir, name);
|
|
918
920
|
if (!isLink(link2)) return false;
|
|
919
921
|
if (dir === GROK_SKILLS_DIR) {
|
|
920
|
-
const target =
|
|
922
|
+
const target = realpathOrNull(link2);
|
|
921
923
|
if (target !== null && target !== gone) return false;
|
|
922
924
|
}
|
|
923
925
|
unlinkSync(link2);
|
|
@@ -957,6 +959,8 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
957
959
|
const to = join8(root, primary, name);
|
|
958
960
|
const local = isLink(to) && !recorded ? null : treeHash(to);
|
|
959
961
|
const knownContent = local !== null && (local === recorded?.hash || versionForHash(name, local) !== null);
|
|
962
|
+
if (local !== null && !recorded && local !== target.hash && isGitTracked(root, `${primary}/${name}`))
|
|
963
|
+
return { name, action: "skipped", reason: `${primary}/${name} is the repo's own (committed): skilllib doesn't replace it; remove it in the repo first if the library's should win` };
|
|
960
964
|
if (local !== null && local !== target.hash && !knownContent && !force) {
|
|
961
965
|
return {
|
|
962
966
|
name,
|
|
@@ -966,9 +970,9 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
966
970
|
}
|
|
967
971
|
let backedUp;
|
|
968
972
|
if (local !== target.hash || isLink(to)) {
|
|
969
|
-
|
|
973
|
+
mkdirSync4(join8(root, primary), { recursive: true });
|
|
970
974
|
if (isLink(to)) unlinkSync(to);
|
|
971
|
-
else if (local !== null &&
|
|
975
|
+
else if (local !== null && versionForHash(name, local) === null) backedUp = stash(to, "edit-backup");
|
|
972
976
|
copySkill(versionDir(name, target.version), to);
|
|
973
977
|
}
|
|
974
978
|
const links = [];
|
|
@@ -977,7 +981,7 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
977
981
|
let shared;
|
|
978
982
|
for (const dir of linkDirs) {
|
|
979
983
|
const link2 = join8(root, dir, name);
|
|
980
|
-
if (isLink(link2) &&
|
|
984
|
+
if (isLink(link2) && realpathOrNull(link2) === realpathOrNull(to)) {
|
|
981
985
|
links.push(dir);
|
|
982
986
|
continue;
|
|
983
987
|
}
|
|
@@ -992,7 +996,7 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
992
996
|
continue;
|
|
993
997
|
}
|
|
994
998
|
}
|
|
995
|
-
|
|
999
|
+
mkdirSync4(join8(root, dir), { recursive: true });
|
|
996
1000
|
linkDir(to, link2);
|
|
997
1001
|
links.push(dir);
|
|
998
1002
|
}
|
|
@@ -1019,38 +1023,60 @@ function removeSkill(root, name, { force = false } = {}) {
|
|
|
1019
1023
|
if (!recorded) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
|
|
1020
1024
|
const dir = join8(root, recorded.dir ?? PROJECT_SKILLS_DIR, name);
|
|
1021
1025
|
const local = treeHash(dir);
|
|
1022
|
-
const
|
|
1026
|
+
const inLibrary = local !== null && versionForHash(name, local) !== null;
|
|
1027
|
+
const edited = local !== null && local !== recorded.hash && !inLibrary;
|
|
1023
1028
|
if (edited && !force) return { name, action: "skipped", reason: "has local edits (use --force to delete anyway)" };
|
|
1024
1029
|
for (const linkDir2 of recorded.links ?? []) {
|
|
1025
1030
|
const link2 = join8(root, linkDir2, name);
|
|
1026
1031
|
if (isLink(link2)) unlinkSync(link2);
|
|
1027
1032
|
}
|
|
1028
|
-
const backedUp =
|
|
1029
|
-
if (!backedUp)
|
|
1033
|
+
const backedUp = local !== null && !inLibrary ? stash(dir, "edit-backup") : void 0;
|
|
1034
|
+
if (!backedUp) rmSync3(dir, { recursive: true, force: true });
|
|
1030
1035
|
delete manifest.skills[name];
|
|
1031
1036
|
writeManifest(root, manifest);
|
|
1032
1037
|
return { name, action: "removed", ...backedUp ? { backedUp } : {} };
|
|
1033
1038
|
}
|
|
1034
1039
|
function syncProject(root, { force = false } = {}) {
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
}
|
|
1040
|
+
return Object.keys(readManifest(root).skills).flatMap((name) => syncSkill(root, name, { force }));
|
|
1041
|
+
}
|
|
1042
|
+
function syncSkill(root, name, { force = false } = {}) {
|
|
1043
|
+
const dep = readManifest(root).skills[name];
|
|
1044
|
+
if (!dep) return [];
|
|
1045
|
+
const copy = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
|
|
1046
|
+
const local = treeHash(copy);
|
|
1047
|
+
const linksOk = (dep.links ?? []).every((d) => realpathOrNull(join8(root, d, name)) === realpathOrNull(copy));
|
|
1048
|
+
if (local === dep.hash && linksOk) return [];
|
|
1049
|
+
if (local === dep.hash) return [relinkPinned(root, name, dep)];
|
|
1050
|
+
if (local !== null && local !== dep.hash && !force) return [{ name, action: "skipped", reason: "has local edits (use --force to reset them)" }];
|
|
1051
|
+
const version = pinnedVersion(name, dep);
|
|
1052
|
+
if (typeof version === "string") return [{ name, action: "skipped", reason: version }];
|
|
1053
|
+
const change = addSkill(root, name, { force: true, version });
|
|
1054
|
+
return [{ ...change, action: local === null ? "installed" : change.action }];
|
|
1055
|
+
}
|
|
1056
|
+
function relinkPinned(root, name, dep) {
|
|
1057
|
+
const to = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
|
|
1058
|
+
for (const dir of dep.links ?? []) {
|
|
1059
|
+
const link2 = join8(root, dir, name);
|
|
1060
|
+
if (realpathOrNull(link2) === realpathOrNull(to)) continue;
|
|
1061
|
+
if (isLink(link2)) unlinkSync(link2);
|
|
1062
|
+
else if (entryExists(link2)) return { name, action: "skipped", reason: `${dir}/${name} is in the way of its link: move it, then sync` };
|
|
1063
|
+
mkdirSync4(join8(root, dir), { recursive: true });
|
|
1064
|
+
linkDir(to, link2);
|
|
1065
|
+
}
|
|
1066
|
+
return { name, action: "relinked", to: dep.version };
|
|
1067
|
+
}
|
|
1068
|
+
function pinnedVersion(name, dep) {
|
|
1069
|
+
if (!latestVersion(name)) return `not in your library (${tildify(libraryDir())}); import it, or set SKILLLIB_HOME to the library that has it`;
|
|
1070
|
+
return versionForHash(name, dep.hash)?.version ?? `your library has no copy of the version skilllib.json pins (pinned from another library?); skilllib update ${name} installs your newest`;
|
|
1046
1071
|
}
|
|
1047
1072
|
function discardEdits(root, name) {
|
|
1048
1073
|
const dep = readManifest(root).skills[name];
|
|
1049
1074
|
if (!dep) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
|
|
1075
|
+
const version = pinnedVersion(name, dep);
|
|
1076
|
+
if (typeof version === "string") return { name, action: "skipped", reason: version };
|
|
1050
1077
|
const dir = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
|
|
1051
|
-
const pinned = dep.version > 0 && getVersion(name, dep.version) ? dep.version : void 0;
|
|
1052
1078
|
if (existsSync7(dir) && !isLink(dir)) stash(dir, "edit-backup");
|
|
1053
|
-
return addSkill(root, name, { force: true, version
|
|
1079
|
+
return addSkill(root, name, { force: true, version });
|
|
1054
1080
|
}
|
|
1055
1081
|
function updateProject(root, names, { force = false } = {}) {
|
|
1056
1082
|
const { skills } = readManifest(root);
|
|
@@ -1072,7 +1098,7 @@ function importSkill(dir, { force = false } = {}) {
|
|
|
1072
1098
|
const incoming = treeHash(dir);
|
|
1073
1099
|
if (existing === incoming) return { name, status: "unchanged" };
|
|
1074
1100
|
if (existing !== null && !force) return { name, status: "exists" };
|
|
1075
|
-
|
|
1101
|
+
mkdirSync4(libraryDir(), { recursive: true });
|
|
1076
1102
|
const origin = existing !== null && libraryOrigins()[name] ? null : originFor(dir);
|
|
1077
1103
|
copySkill(dir, to);
|
|
1078
1104
|
if (origin) recordOrigin(name, origin);
|
|
@@ -1084,10 +1110,15 @@ function backupIndexFile(bucket) {
|
|
|
1084
1110
|
return join8(skilllibHome(), bucket, "index.json");
|
|
1085
1111
|
}
|
|
1086
1112
|
function readBackupIndex(bucket) {
|
|
1113
|
+
return readState(backupIndexFile(bucket)) ?? {};
|
|
1114
|
+
}
|
|
1115
|
+
function move(from, to) {
|
|
1087
1116
|
try {
|
|
1088
|
-
|
|
1089
|
-
} catch {
|
|
1090
|
-
|
|
1117
|
+
renameSync2(from, to);
|
|
1118
|
+
} catch (err) {
|
|
1119
|
+
if (err.code !== "EXDEV") throw err;
|
|
1120
|
+
cpSync2(from, to, { recursive: true, verbatimSymlinks: true });
|
|
1121
|
+
rmSync3(from, { recursive: true, force: true });
|
|
1091
1122
|
}
|
|
1092
1123
|
}
|
|
1093
1124
|
function stash(dir, bucket) {
|
|
@@ -1096,9 +1127,10 @@ function stash(dir, bucket) {
|
|
|
1096
1127
|
while (entryExists(join8(skilllibHome(), bucket, entryAt(at)))) at++;
|
|
1097
1128
|
const entry = entryAt(at);
|
|
1098
1129
|
const target = join8(skilllibHome(), bucket, entry);
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1130
|
+
const index = readBackupIndex(bucket);
|
|
1131
|
+
mkdirSync4(join8(skilllibHome(), bucket), { recursive: true });
|
|
1132
|
+
move(dir, target);
|
|
1133
|
+
writeState(backupIndexFile(bucket), { ...index, [entry]: dir });
|
|
1102
1134
|
return target;
|
|
1103
1135
|
}
|
|
1104
1136
|
function removeUntracked(root, name, path) {
|
|
@@ -1106,7 +1138,7 @@ function removeUntracked(root, name, path) {
|
|
|
1106
1138
|
return stash(path, "trash");
|
|
1107
1139
|
}
|
|
1108
1140
|
function projectsUsing(name) {
|
|
1109
|
-
return knownProjects().filter((p) => name in
|
|
1141
|
+
return knownProjects().filter((p) => name in pinsIn(p));
|
|
1110
1142
|
}
|
|
1111
1143
|
function deleteLibrarySkill(name) {
|
|
1112
1144
|
const using = projectsUsing(name);
|
|
@@ -1135,12 +1167,12 @@ function linkGlobal(path, agents) {
|
|
|
1135
1167
|
const skipped = [];
|
|
1136
1168
|
for (const dir of new Set(agents.map((id) => harness(id).globalDirs()[0]))) {
|
|
1137
1169
|
const link2 = join8(dir, name);
|
|
1138
|
-
if (isLink(link2) &&
|
|
1170
|
+
if (isLink(link2) && realpathOrNull(link2) === null) unlinkSync(link2);
|
|
1139
1171
|
else if (entryExists(link2)) {
|
|
1140
|
-
if (
|
|
1172
|
+
if (realpathOrNull(link2) !== realpathOrNull(path)) skipped.push(link2);
|
|
1141
1173
|
continue;
|
|
1142
1174
|
}
|
|
1143
|
-
|
|
1175
|
+
mkdirSync4(dir, { recursive: true });
|
|
1144
1176
|
linkDir(path, link2);
|
|
1145
1177
|
linked.push(link2);
|
|
1146
1178
|
}
|
|
@@ -1150,8 +1182,8 @@ function createSkill(name, description) {
|
|
|
1150
1182
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) return { ok: false, reason: "use lowercase letters, digits and dashes" };
|
|
1151
1183
|
const dir = librarySkillDir(name);
|
|
1152
1184
|
if (existsSync7(dir)) return { ok: false, reason: `${name} already exists` };
|
|
1153
|
-
|
|
1154
|
-
|
|
1185
|
+
mkdirSync4(dir, { recursive: true });
|
|
1186
|
+
writeFileSync2(
|
|
1155
1187
|
join8(dir, "SKILL.md"),
|
|
1156
1188
|
`---
|
|
1157
1189
|
name: ${name}
|
|
@@ -1175,27 +1207,29 @@ function listBackups() {
|
|
|
1175
1207
|
const match = entry.match(/^(.*)-(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-\d{3}Z$/);
|
|
1176
1208
|
if (!match) return [];
|
|
1177
1209
|
const name = match[1];
|
|
1178
|
-
const from = index[entry] ?? (kind === "trash" ? librarySkillDir(name) : join8(claudeDir(), "skills", name));
|
|
1179
|
-
return [{ name, kind, path: join8(dir, entry), movedAt: `${match[2]}
|
|
1210
|
+
const from = index[entry] ?? (kind === "trash" ? librarySkillDir(name) : kind === "global-backup" ? join8(claudeDir(), "skills", name) : "");
|
|
1211
|
+
return [{ name, kind, path: join8(dir, entry), movedAt: localStamp(Date.parse(`${match[2]}T${match[3]}:${match[4]}:${match[5]}Z`)), from }];
|
|
1180
1212
|
});
|
|
1181
1213
|
}).sort((a, b) => b.path.slice(-24).localeCompare(a.path.slice(-24)));
|
|
1182
1214
|
}
|
|
1183
1215
|
function backupFrom(backup) {
|
|
1184
1216
|
if (backup.kind === "plugin") return "Claude Code plugin";
|
|
1185
|
-
|
|
1217
|
+
if (!backup.from) return "unknown";
|
|
1218
|
+
return backup.from === librarySkillDir(backup.name) ? "library (deleted)" : tildify(dirname3(backup.from));
|
|
1186
1219
|
}
|
|
1187
1220
|
function restoreBackup(backup) {
|
|
1188
1221
|
const to = backup.from;
|
|
1222
|
+
if (!to) return { ok: false, reason: `skilllib doesn't know where it came from; its files are in ${tildify(backup.path)}` };
|
|
1189
1223
|
if (backup.kind === "tidy-backup" && isLink(to)) unlinkSync(to);
|
|
1190
|
-
if (backup.kind === "edit-backup" && !isLink(to) && versionForHash(backup.name, treeHash(to) ?? "") !== null)
|
|
1224
|
+
if (backup.kind === "edit-backup" && !isLink(to) && versionForHash(backup.name, treeHash(to) ?? "") !== null) rmSync3(to, { recursive: true, force: true });
|
|
1191
1225
|
let taken = existsSync7(to);
|
|
1192
1226
|
try {
|
|
1193
1227
|
taken = taken || lstatSync2(to) !== null;
|
|
1194
1228
|
} catch {
|
|
1195
1229
|
}
|
|
1196
1230
|
if (taken) return { ok: false, reason: `${backup.name} already exists there` };
|
|
1197
|
-
|
|
1198
|
-
|
|
1231
|
+
mkdirSync4(join8(to, ".."), { recursive: true });
|
|
1232
|
+
move(backup.path, to);
|
|
1199
1233
|
forgetLatest(backup.name);
|
|
1200
1234
|
return { ok: true, to };
|
|
1201
1235
|
}
|
|
@@ -1203,7 +1237,7 @@ function restoreBackup(backup) {
|
|
|
1203
1237
|
// src/agentSkill.ts
|
|
1204
1238
|
var AGENT_SKILL_NAME = "skilllib";
|
|
1205
1239
|
function shippedSkill() {
|
|
1206
|
-
return
|
|
1240
|
+
return readFileSync4(new URL("../skills/skilllib/SKILL.md", import.meta.url), "utf-8");
|
|
1207
1241
|
}
|
|
1208
1242
|
function agentSkillDirs(enabled = enabledHarnesses()) {
|
|
1209
1243
|
const claudeSkills = join9(claudeDir(), "skills");
|
|
@@ -1229,7 +1263,7 @@ function entryExists2(path) {
|
|
|
1229
1263
|
function agentSkillState(enabled = enabledHarnesses()) {
|
|
1230
1264
|
const [primary, ...links] = agentSkillDirs(enabled);
|
|
1231
1265
|
if (!primary || !isOurs(primary) || links.some((l) => !existsSync8(join9(l, "SKILL.md")))) return "missing";
|
|
1232
|
-
return
|
|
1266
|
+
return readFileSync4(join9(primary, "SKILL.md"), "utf-8") === shippedSkill() ? "installed" : "outdated";
|
|
1233
1267
|
}
|
|
1234
1268
|
function installAgentSkill(enabled = enabledHarnesses()) {
|
|
1235
1269
|
const [primary, ...links] = agentSkillDirs(enabled);
|
|
@@ -1237,12 +1271,12 @@ function installAgentSkill(enabled = enabledHarnesses()) {
|
|
|
1237
1271
|
const foreign = [primary, ...links].find((d) => entryExists2(d) && !isOurs(d) && !isDangling(d));
|
|
1238
1272
|
if (foreign) return { ok: false, reason: `${tildify(foreign)} already exists and isn't skilllib's` };
|
|
1239
1273
|
for (const d of [primary, ...links]) if (isDangling(d)) unlinkSync2(d);
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1274
|
+
mkdirSync5(primary, { recursive: true });
|
|
1275
|
+
writeFileSync3(join9(primary, "SKILL.md"), shippedSkill());
|
|
1276
|
+
writeFileSync3(join9(primary, OWN_SKILL_MARKER), "Installed by skilllib. `skilllib agent-skill remove` removes it.\n");
|
|
1243
1277
|
for (const link2 of links) {
|
|
1244
1278
|
if (entryExists2(link2)) continue;
|
|
1245
|
-
|
|
1279
|
+
mkdirSync5(join9(link2, ".."), { recursive: true });
|
|
1246
1280
|
linkDir(primary, link2);
|
|
1247
1281
|
}
|
|
1248
1282
|
return { ok: true, dirs: [primary, ...links] };
|
|
@@ -1251,8 +1285,9 @@ function removeAgentSkill() {
|
|
|
1251
1285
|
const all = agentSkillDirs(["claude-code", "codex"]);
|
|
1252
1286
|
const removed = all.filter((d) => entryExists2(d) && (isOurs(d) || isDangling(d)));
|
|
1253
1287
|
for (const d of removed) {
|
|
1288
|
+
if (!entryExists2(d)) continue;
|
|
1254
1289
|
if (lstatSync3(d).isSymbolicLink()) unlinkSync2(d);
|
|
1255
|
-
else
|
|
1290
|
+
else rmSync4(d, { recursive: true, force: true });
|
|
1256
1291
|
}
|
|
1257
1292
|
return removed;
|
|
1258
1293
|
}
|
|
@@ -1264,8 +1299,9 @@ function refreshAgentSkill() {
|
|
|
1264
1299
|
}
|
|
1265
1300
|
|
|
1266
1301
|
// src/commands.ts
|
|
1267
|
-
import { existsSync as
|
|
1268
|
-
import {
|
|
1302
|
+
import { existsSync as existsSync14, readFileSync as readFileSync6 } from "fs";
|
|
1303
|
+
import { parseArgs as parseFlags } from "util";
|
|
1304
|
+
import { basename as basename9, join as join15, resolve as resolve7, sep as sep6 } from "path";
|
|
1269
1305
|
|
|
1270
1306
|
// src/git.ts
|
|
1271
1307
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -1307,24 +1343,17 @@ function relativeTo(root, path) {
|
|
|
1307
1343
|
}
|
|
1308
1344
|
|
|
1309
1345
|
// src/here.ts
|
|
1310
|
-
import { readdirSync as
|
|
1346
|
+
import { readdirSync as readdirSync8 } from "fs";
|
|
1311
1347
|
import { basename as basename7 } from "path";
|
|
1312
1348
|
|
|
1313
1349
|
// src/health.ts
|
|
1314
|
-
import { lstatSync as lstatSync4 } from "fs";
|
|
1315
|
-
import { basename as basename6, dirname as
|
|
1350
|
+
import { existsSync as existsSync12, lstatSync as lstatSync4, readdirSync as readdirSync7, unlinkSync as unlinkSync4 } from "fs";
|
|
1351
|
+
import { basename as basename6, dirname as dirname6, join as join13 } from "path";
|
|
1316
1352
|
|
|
1317
1353
|
// src/plugins.ts
|
|
1318
1354
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1319
|
-
import { existsSync as existsSync10, mkdirSync as
|
|
1320
|
-
import { basename as basename4, dirname as
|
|
1321
|
-
function readJson2(path) {
|
|
1322
|
-
try {
|
|
1323
|
-
return JSON.parse(readFileSync8(path, "utf-8"));
|
|
1324
|
-
} catch {
|
|
1325
|
-
return null;
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1355
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync5, readFileSync as readFileSync5, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
1356
|
+
import { basename as basename4, dirname as dirname4, join as join11, resolve as resolve6 } from "path";
|
|
1328
1357
|
function nonEmptyDir(path) {
|
|
1329
1358
|
try {
|
|
1330
1359
|
return readdirSync5(path).some((f) => !f.startsWith("."));
|
|
@@ -1333,7 +1362,7 @@ function nonEmptyDir(path) {
|
|
|
1333
1362
|
}
|
|
1334
1363
|
}
|
|
1335
1364
|
function extrasOf(root) {
|
|
1336
|
-
const manifest =
|
|
1365
|
+
const manifest = readJson(join11(root, ".claude-plugin", "plugin.json")) ?? {};
|
|
1337
1366
|
return [
|
|
1338
1367
|
...existsSync10(join11(root, ".mcp.json")) || manifest.mcpServers ? ["MCP servers"] : [],
|
|
1339
1368
|
...existsSync10(join11(root, "hooks", "hooks.json")) || manifest.hooks ? ["hooks"] : [],
|
|
@@ -1342,21 +1371,20 @@ function extrasOf(root) {
|
|
|
1342
1371
|
];
|
|
1343
1372
|
}
|
|
1344
1373
|
function claudePlugins(machine, root) {
|
|
1345
|
-
const installed =
|
|
1374
|
+
const installed = installedPluginsJson();
|
|
1346
1375
|
const byOrigin = /* @__PURE__ */ new Map();
|
|
1347
1376
|
for (const m of machine) if (m.kind === "plugin") byOrigin.set(m.origin, [...byOrigin.get(m.origin) ?? [], m.path]);
|
|
1348
1377
|
return [...byOrigin].map(([origin, skills]) => {
|
|
1349
1378
|
const synced = origin.endsWith("@synced");
|
|
1350
|
-
const id = origin;
|
|
1351
1379
|
const entries = installed[origin] ?? [];
|
|
1352
1380
|
const entry = entries.find((e) => root && e.projectPath === root) ?? entries.find((e) => (e.scope ?? "user") === "user") ?? entries[0];
|
|
1353
1381
|
return {
|
|
1354
|
-
id,
|
|
1382
|
+
id: origin,
|
|
1355
1383
|
scope: synced ? null : entry?.scope ?? "user",
|
|
1356
1384
|
...entry?.projectPath ? { projectPath: entry.projectPath } : {},
|
|
1357
1385
|
synced,
|
|
1358
1386
|
skills,
|
|
1359
|
-
extras: extrasOf(
|
|
1387
|
+
extras: extrasOf(dirname4(dirname4(skills[0])))
|
|
1360
1388
|
};
|
|
1361
1389
|
});
|
|
1362
1390
|
}
|
|
@@ -1369,10 +1397,10 @@ function newer(a, b) {
|
|
|
1369
1397
|
}
|
|
1370
1398
|
function updateOf(name, marketplace, entry, catalogs) {
|
|
1371
1399
|
const dir = join11(claudeDir(), "plugins", "marketplaces", marketplace);
|
|
1372
|
-
if (!catalogs.has(marketplace)) catalogs.set(marketplace,
|
|
1400
|
+
if (!catalogs.has(marketplace)) catalogs.set(marketplace, readJson(join11(dir, ".claude-plugin", "marketplace.json"))?.plugins ?? []);
|
|
1373
1401
|
const listed = catalogs.get(marketplace).find((p) => p.name === name);
|
|
1374
1402
|
if (!listed) return void 0;
|
|
1375
|
-
const version = listed.version ?? (typeof listed.source === "string" ?
|
|
1403
|
+
const version = listed.version ?? (typeof listed.source === "string" ? readJson(join11(dir, listed.source, ".claude-plugin", "plugin.json"))?.version : void 0);
|
|
1376
1404
|
if (version && entry.version) return newer(version, entry.version) ? version : void 0;
|
|
1377
1405
|
const sha = typeof listed.source === "object" ? listed.source?.sha : void 0;
|
|
1378
1406
|
return sha && entry.gitCommitSha && sha !== entry.gitCommitSha ? "newer" : void 0;
|
|
@@ -1382,12 +1410,12 @@ function pluginRoot(id, entry) {
|
|
|
1382
1410
|
return candidates.find((c) => existsSync10(join11(c, "skills"))) ?? candidates.find((c) => existsSync10(c)) ?? candidates[0];
|
|
1383
1411
|
}
|
|
1384
1412
|
function describe(root) {
|
|
1385
|
-
const manifest =
|
|
1413
|
+
const manifest = readJson(join11(root, ".claude-plugin", "plugin.json")) ?? {};
|
|
1386
1414
|
return { description: manifest.description ?? "", version: manifest.version, skills: skillDirsIn(join11(root, "skills")), extras: extrasOf(root) };
|
|
1387
1415
|
}
|
|
1388
1416
|
function installedPlugins() {
|
|
1389
1417
|
const plugins = join11(claudeDir(), "plugins");
|
|
1390
|
-
const installed =
|
|
1418
|
+
const installed = installedPluginsJson();
|
|
1391
1419
|
const catalogs = /* @__PURE__ */ new Map();
|
|
1392
1420
|
const fromMarketplaces = Object.entries(installed).flatMap(
|
|
1393
1421
|
([id, entries]) => entries.map((entry) => {
|
|
@@ -1419,18 +1447,18 @@ function installedPlugins() {
|
|
|
1419
1447
|
const syncedDir = join11(plugins, "synced");
|
|
1420
1448
|
const dirs = (p) => existsSync10(p) ? readdirSync5(p, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => join11(p, d.name)) : [];
|
|
1421
1449
|
const fromSynced = dirs(syncedDir).flatMap(dirs).map((root) => {
|
|
1422
|
-
const id = `${
|
|
1450
|
+
const id = `${readJson(join11(root, ".claude-plugin", "plugin.json"))?.name ?? basename4(root)}@synced`;
|
|
1423
1451
|
return { id, scope: null, synced: true, agent: "claude-code", on: user[id] !== false, root, ...describe(root) };
|
|
1424
1452
|
});
|
|
1425
1453
|
return [...fromMarketplaces, ...fromSettings, ...fromSynced];
|
|
1426
1454
|
}
|
|
1427
1455
|
function cursorPlugins() {
|
|
1428
1456
|
const cache = join11(userHome(), ".cursor", "plugins", "cache");
|
|
1429
|
-
const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") &&
|
|
1457
|
+
const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") && statSync4(join11(p, d)).isDirectory()) : [];
|
|
1430
1458
|
return dirs(cache).flatMap(
|
|
1431
1459
|
(marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
|
|
1432
1460
|
const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
|
|
1433
|
-
const root = versions.sort((a, b) =>
|
|
1461
|
+
const root = versions.sort((a, b) => statSync4(b).mtimeMs - statSync4(a).mtimeMs)[0];
|
|
1434
1462
|
if (!root) return [];
|
|
1435
1463
|
const about = describe(root);
|
|
1436
1464
|
return [{ id: `${plugin}@${marketplace}`, scope: null, synced: false, agent: "cursor", on: null, root, ...about, version: about.version ?? basename4(root) }];
|
|
@@ -1469,11 +1497,10 @@ function claude(args, cwd, bin = claudeBinary()) {
|
|
|
1469
1497
|
}
|
|
1470
1498
|
}
|
|
1471
1499
|
function setPluginIn(file, id, on) {
|
|
1472
|
-
const settings = existsSync10(file) ?
|
|
1500
|
+
const settings = existsSync10(file) ? readJson(file) : {};
|
|
1473
1501
|
if (!settings) return { ok: false, reason: `can't read ${file}` };
|
|
1474
1502
|
const enabled = settings.enabledPlugins ?? {};
|
|
1475
|
-
|
|
1476
|
-
writeFileSync7(file, JSON.stringify({ ...settings, enabledPlugins: { ...enabled, [id]: on } }, null, 2) + "\n");
|
|
1503
|
+
writeState(file, { ...settings, enabledPlugins: { ...enabled, [id]: on } });
|
|
1477
1504
|
return { ok: true };
|
|
1478
1505
|
}
|
|
1479
1506
|
function ignoreLocalSettings(root) {
|
|
@@ -1496,9 +1523,9 @@ function ignoreLocalSettings(root) {
|
|
|
1496
1523
|
}
|
|
1497
1524
|
try {
|
|
1498
1525
|
const exclude = resolve6(root, git2("rev-parse", "--git-path", "info/exclude"));
|
|
1499
|
-
const before = existsSync10(exclude) ?
|
|
1500
|
-
|
|
1501
|
-
|
|
1526
|
+
const before = existsSync10(exclude) ? readFileSync5(exclude, "utf-8") : "";
|
|
1527
|
+
mkdirSync6(dirname4(exclude), { recursive: true });
|
|
1528
|
+
writeFileSync4(exclude, `${before}${before && !before.endsWith("\n") ? "\n" : ""}/${git2("rev-parse", "--show-prefix")}${file}
|
|
1502
1529
|
`);
|
|
1503
1530
|
return ", which git now ignores via .git/info/exclude";
|
|
1504
1531
|
} catch {
|
|
@@ -1566,32 +1593,38 @@ function removePlugin(plugin, how) {
|
|
|
1566
1593
|
if (plugin.scope && plugin.scope !== "user" && !plugin.projectPath) {
|
|
1567
1594
|
return { ok: false, message: `${plugin.id}: installed for one project; run \`claude plugin ${how === "delete" ? "uninstall" : "disable"} ${plugin.id} --scope ${plugin.scope}\` there` };
|
|
1568
1595
|
}
|
|
1569
|
-
|
|
1596
|
+
const viaSettings = plugin.synced || !claudeBinary() && how === "off" && !plugin.projectPath;
|
|
1597
|
+
if (!viaSettings && !claudeBinary()) {
|
|
1570
1598
|
return { ok: false, message: `${plugin.id}: nothing changed; Claude Code's CLI didn't run (tried \`claude\` on your PATH and ~/.local/bin/claude)` };
|
|
1571
1599
|
}
|
|
1572
|
-
const
|
|
1600
|
+
const wasOn = enabledPlugins(plugin.projectPath)[plugin.id] === true;
|
|
1601
|
+
const yours = new Set(machineSkills().filter((m) => m.movable).map((m) => m.name));
|
|
1602
|
+
const imported = plugin.skills.filter((dir) => !yours.has(basename4(dir)) && latestVersion(basename4(dir)) === null).map((dir) => importSkill(dir).name);
|
|
1573
1603
|
const scope = plugin.scope ? ["--scope", plugin.scope] : [];
|
|
1574
|
-
const r =
|
|
1604
|
+
const r = viaSettings ? (
|
|
1575
1605
|
// Synced plugins aren't installed, so `claude plugin` can't touch them: their switch is enabledPlugins.
|
|
1576
1606
|
setPluginIn(join11(claudeDir(), "settings.json"), plugin.id, false)
|
|
1577
|
-
) :
|
|
1607
|
+
) : (
|
|
1608
|
+
// --keep-data, as uninstallPlugin: its saved data stays, so a restore brings it back as it was.
|
|
1609
|
+
claude(["plugin", how === "delete" ? "uninstall" : "disable", plugin.id, ...how === "delete" ? ["--keep-data"] : [], ...scope], plugin.projectPath)
|
|
1610
|
+
);
|
|
1578
1611
|
const saved = imported.length ? `; ${imported.join(", ")} copied into Your skills` : "";
|
|
1579
1612
|
if (!r.ok) return { ok: false, message: `${plugin.id}: ${r.reason}${saved}` };
|
|
1580
|
-
if (how === "delete" && !plugin.synced) recordRemovedPlugin(plugin.id, plugin.scope ?? "user", plugin.projectPath,
|
|
1613
|
+
if (how === "delete" && !plugin.synced) recordRemovedPlugin(plugin.id, plugin.scope ?? "user", plugin.projectPath, wasOn);
|
|
1581
1614
|
return { ok: true, message: `${plugin.id} ${how === "delete" && !plugin.synced ? "removed" : "turned off"}${saved}` };
|
|
1582
1615
|
}
|
|
1583
1616
|
function recordRemovedPlugin(id, scope, projectPath, on = true) {
|
|
1584
|
-
const list2 =
|
|
1585
|
-
|
|
1586
|
-
|
|
1617
|
+
const list2 = readState(removedFile()) ?? [];
|
|
1618
|
+
mkdirSync6(skilllibHome(), { recursive: true });
|
|
1619
|
+
writeState(removedFile(), [...list2, { id, scope, ...projectPath ? { projectPath } : {}, on, at: (/* @__PURE__ */ new Date()).toISOString() }]);
|
|
1587
1620
|
}
|
|
1588
1621
|
function pluginBackups() {
|
|
1589
|
-
return (
|
|
1622
|
+
return (readState(removedFile()) ?? []).map((r) => ({
|
|
1590
1623
|
name: r.id,
|
|
1591
1624
|
kind: "plugin",
|
|
1592
1625
|
// Project and local installs go back into their project.
|
|
1593
1626
|
path: r.projectPath ? `${r.scope}:${r.projectPath}` : r.scope,
|
|
1594
|
-
movedAt: r.at
|
|
1627
|
+
movedAt: localStamp(Date.parse(r.at)),
|
|
1595
1628
|
from: r.id
|
|
1596
1629
|
}));
|
|
1597
1630
|
}
|
|
@@ -1600,11 +1633,11 @@ function restorePlugin(backup) {
|
|
|
1600
1633
|
const projectPath = rest.join(":") || void 0;
|
|
1601
1634
|
const r = claude(["plugin", "install", backup.from, "--scope", scope], projectPath);
|
|
1602
1635
|
if (!r.ok) return r;
|
|
1603
|
-
const records =
|
|
1636
|
+
const records = readState(removedFile()) ?? [];
|
|
1604
1637
|
const same = (x) => x.id === backup.from && (x.projectPath ?? void 0) === projectPath && x.scope === scope;
|
|
1605
1638
|
const wasOff = records.filter(same).pop()?.on === false;
|
|
1606
1639
|
const off = wasOff ? claude(["plugin", "disable", backup.from, "--scope", scope], projectPath) : null;
|
|
1607
|
-
|
|
1640
|
+
writeState(removedFile(), records.filter((x) => !same(x)));
|
|
1608
1641
|
const where = `Claude Code plugins (${scope}${projectPath ? ` in ${basename4(projectPath)}` : ""}${wasOff ? off?.ok ? ", off as it was" : ", but it's on: turn it off from Plugins" : ""})`;
|
|
1609
1642
|
return { ok: true, to: where };
|
|
1610
1643
|
}
|
|
@@ -1613,14 +1646,14 @@ function cursorPluginSkills(plugins = cursorPlugins()) {
|
|
|
1613
1646
|
}
|
|
1614
1647
|
|
|
1615
1648
|
// src/tidy.ts
|
|
1616
|
-
import { existsSync as existsSync11, mkdirSync as
|
|
1617
|
-
import { basename as basename5, dirname as
|
|
1649
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync7, readdirSync as readdirSync6, unlinkSync as unlinkSync3 } from "fs";
|
|
1650
|
+
import { basename as basename5, dirname as dirname5, join as join12, sep as sep5 } from "path";
|
|
1618
1651
|
function entriesOf(dirs, name) {
|
|
1619
1652
|
return dirs.flatMap((dir) => {
|
|
1620
1653
|
const path = join12(dir, name);
|
|
1621
1654
|
if (!entryExists(path)) return [];
|
|
1622
1655
|
const link2 = isLink(path);
|
|
1623
|
-
const real =
|
|
1656
|
+
const real = realpathOrNull(path);
|
|
1624
1657
|
const skill = existsSync11(join12(path, "SKILL.md"));
|
|
1625
1658
|
if (!link2 && !skill) return [];
|
|
1626
1659
|
return [{ dir, path, link: link2, real, hash: null }];
|
|
@@ -1645,7 +1678,7 @@ function globalTidyDirs() {
|
|
|
1645
1678
|
function runsFrom(id, dirsWithCopy, readsOf) {
|
|
1646
1679
|
const reads = readsOf(id).filter((d) => dirsWithCopy.includes(d));
|
|
1647
1680
|
if (id === "codex") return reads;
|
|
1648
|
-
const pick = (d) => CURSOR_PICKS.indexOf(basename5(
|
|
1681
|
+
const pick = (d) => CURSOR_PICKS.indexOf(basename5(dirname5(d)));
|
|
1649
1682
|
return id === "cursor" ? [...reads].sort((a, b) => pick(a) - pick(b)).slice(0, 1) : reads.slice(0, 1);
|
|
1650
1683
|
}
|
|
1651
1684
|
function conflictOf(name, root, reals, enabled, readsOf) {
|
|
@@ -1661,17 +1694,17 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1661
1694
|
const { skills } = readManifest(root);
|
|
1662
1695
|
const abs = (dir) => join12(root, dir);
|
|
1663
1696
|
const rel = (dir) => relativeTo(root, dir);
|
|
1664
|
-
const readsOf = (id) =>
|
|
1697
|
+
const readsOf = (id) => harness(id).projectDirs.map(abs);
|
|
1665
1698
|
let git2;
|
|
1666
1699
|
const skillsLock = projectSkillsLock(root);
|
|
1667
1700
|
for (const name of namesIn(PROJECT_TIDY_DIRS.map(abs))) {
|
|
1668
1701
|
const entries = entriesOf(PROJECT_TIDY_DIRS.map(abs), name);
|
|
1669
|
-
const external = entries.find((e) => e.link && e.real && !(e.real +
|
|
1702
|
+
const external = entries.find((e) => e.link && e.real && !(e.real + sep5).startsWith(realpathOrNull(root) + sep5));
|
|
1670
1703
|
if (external) {
|
|
1671
1704
|
report.skipped.push({ name, reason: `${rel(external.path)} links outside the repo` });
|
|
1672
1705
|
continue;
|
|
1673
1706
|
}
|
|
1674
|
-
const reals = entries.filter((e) => !e.link);
|
|
1707
|
+
const reals = entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link && realpathOrNull(o.path) === realpathOrNull(e.path)) === i);
|
|
1675
1708
|
if (reals.length === 0) continue;
|
|
1676
1709
|
hashReals(reals);
|
|
1677
1710
|
const dep = skills[name];
|
|
@@ -1697,10 +1730,11 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1697
1730
|
const wanted = new Set([...installDirs(enabled, rel(primary.dir)), ...dep?.links ?? []].map(abs));
|
|
1698
1731
|
const steps = [];
|
|
1699
1732
|
for (const e of entries) {
|
|
1700
|
-
if (e === primary) continue;
|
|
1733
|
+
if (e === primary || !e.link && e.real === primary.real) continue;
|
|
1701
1734
|
const needed = wanted.has(e.dir);
|
|
1702
1735
|
if (e.link) {
|
|
1703
1736
|
if (e.real === primary.real) {
|
|
1737
|
+
if (!needed && !dep && ["committed", "changed"].includes((git2 ??= gitInfo(root))?.of(rel(e.path)) ?? "")) continue;
|
|
1704
1738
|
if (!needed) steps.push({ kind: "unlink", path: e.path });
|
|
1705
1739
|
} else steps.push(needed ? { kind: "link", path: e.path, target: primary.path } : { kind: "unlink", path: e.path });
|
|
1706
1740
|
} else {
|
|
@@ -1713,7 +1747,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1713
1747
|
return report;
|
|
1714
1748
|
}
|
|
1715
1749
|
function globalReadsOf(id) {
|
|
1716
|
-
return
|
|
1750
|
+
return harness(id).globalDirs().map(globalDirAs);
|
|
1717
1751
|
}
|
|
1718
1752
|
function isSkillsDirPlugin(path) {
|
|
1719
1753
|
return existsSync11(join12(path, ".claude-plugin", "plugin.json"));
|
|
@@ -1728,7 +1762,7 @@ function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
|
|
|
1728
1762
|
report.skipped.push({ name, reason: "a Claude Code plugin kept in a skills folder" });
|
|
1729
1763
|
continue;
|
|
1730
1764
|
}
|
|
1731
|
-
const reals = entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link &&
|
|
1765
|
+
const reals = entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link && realpathOrNull(o.path) === realpathOrNull(e.path)) === i);
|
|
1732
1766
|
if (reals.length < 2) continue;
|
|
1733
1767
|
hashReals(reals);
|
|
1734
1768
|
const kept = reals.find((r) => r.dir === keep[name]);
|
|
@@ -1754,11 +1788,12 @@ function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
|
|
|
1754
1788
|
const applied = [];
|
|
1755
1789
|
for (const step of plan.steps) {
|
|
1756
1790
|
if (held.includes(step) || !entryExists(step.path) || "target" in step && !entryExists(step.target)) continue;
|
|
1791
|
+
if ("target" in step && realpathOrNull(step.path) === realpathOrNull(step.target)) continue;
|
|
1757
1792
|
if (step.kind === "link" || step.kind === "unlink") {
|
|
1758
1793
|
if (isLink(step.path)) unlinkSync3(step.path);
|
|
1759
1794
|
} else stash(step.path, "tidy-backup");
|
|
1760
1795
|
if (step.kind === "link" || step.kind === "replace") {
|
|
1761
|
-
|
|
1796
|
+
mkdirSync7(dirname5(step.path), { recursive: true });
|
|
1762
1797
|
linkDir(step.target, step.path);
|
|
1763
1798
|
}
|
|
1764
1799
|
applied.push(step);
|
|
@@ -1766,7 +1801,7 @@ function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
|
|
|
1766
1801
|
if (plan.root) {
|
|
1767
1802
|
const manifest = readManifest(plan.root);
|
|
1768
1803
|
const dep = manifest.skills[plan.name];
|
|
1769
|
-
const added = applied.filter((s) => s.kind === "link" || s.kind === "replace").map((s) => relativeTo(plan.root,
|
|
1804
|
+
const added = applied.filter((s) => s.kind === "link" || s.kind === "replace").map((s) => relativeTo(plan.root, dirname5(s.path)));
|
|
1770
1805
|
if (dep && added.length) {
|
|
1771
1806
|
manifest.skills[plan.name] = { ...dep, links: [.../* @__PURE__ */ new Set([...dep.links ?? [], ...added])] };
|
|
1772
1807
|
writeManifest(plan.root, manifest);
|
|
@@ -1810,42 +1845,135 @@ function orThrow(r) {
|
|
|
1810
1845
|
if (!r.ok) throw new Error(r.message);
|
|
1811
1846
|
return r.message;
|
|
1812
1847
|
}
|
|
1848
|
+
function intoLibrary(path, force = false) {
|
|
1849
|
+
const r = importSkill(path, { force });
|
|
1850
|
+
if (r.status === "exists") throw new Error(`${r.name}: your library has a different ${r.name}; update or delete it first`);
|
|
1851
|
+
return r.name;
|
|
1852
|
+
}
|
|
1853
|
+
function track(root, name) {
|
|
1854
|
+
const c = addSkill(root, name);
|
|
1855
|
+
if (c.action === "skipped") throw new Error(`${name}: ${c.reason}`);
|
|
1856
|
+
return c;
|
|
1857
|
+
}
|
|
1858
|
+
var installedIn = (p) => p.projectPath ? ` in ${basename6(p.projectPath)}` : "";
|
|
1859
|
+
var installId = (p) => `${p.projectPath ? `${p.projectPath}:` : ""}${p.id}`;
|
|
1860
|
+
function summarize(where, verb, changes) {
|
|
1861
|
+
const done = changes.filter((c) => c.action !== "skipped");
|
|
1862
|
+
const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
|
|
1863
|
+
if (!done.length) throw new Error(`${where}: nothing ${verb}${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
|
|
1864
|
+
return `${where}: ${verb} ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
|
|
1865
|
+
}
|
|
1813
1866
|
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1814
|
-
var
|
|
1867
|
+
var rows = (root, skills) => skills.map((skill) => ({ root, skill }));
|
|
1868
|
+
var agentNames = (ids) => ids.map((id) => harness(id).name).join(", ");
|
|
1815
1869
|
function conflictIssue(c) {
|
|
1816
1870
|
const where = c.root ? basename6(c.root) : "your global folders";
|
|
1817
1871
|
const winners = c.managed ? c.copies.filter((x) => x.dir === c.managed) : c.copies;
|
|
1872
|
+
const planFor = (dir) => (c.root ? planProjectTidy(c.root, { keep: { [c.name]: dir } }) : planGlobalTidy({ keep: { [c.name]: dir } })).plans.find((p) => p.name === c.name);
|
|
1873
|
+
const keep = (dir, git2) => () => {
|
|
1874
|
+
const plan = planFor(dir);
|
|
1875
|
+
const r = plan ? applyTidy(plan, { git: git2 }) : null;
|
|
1876
|
+
if (!r?.applied.length) throw new Error(`${c.name}: nothing changed${r?.held.length ? " (git would see every change)" : ""}`);
|
|
1877
|
+
return `${c.name}: kept the ${copyLabel(c, dir)} copy${r.held.length ? `; held back ${plural(r.held.length, "change")} git would see` : ""}`;
|
|
1878
|
+
};
|
|
1818
1879
|
return {
|
|
1819
1880
|
id: `conflict:${c.root ?? "global"}:${c.name}`,
|
|
1820
1881
|
severity: "problem",
|
|
1821
1882
|
title: `${c.name} in ${where}: the copies differ`,
|
|
1822
|
-
detail: `Your agents run different versions: ${c.copies.map((x) => `${copyLabel(c, x.dir)}${x.runs.length ? ` (${agentNames(x.runs)})` : ""}`).join(" vs ")}. Keep one; the others become links to it and are restorable
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1883
|
+
detail: `Your agents run different versions: ${c.copies.map((x) => `${copyLabel(c, x.dir)}${x.runs.length ? ` (${agentNames(x.runs)})` : ""}`).join(" vs ")}. Keep one; the others become links to it and are restorable with skilllib restore (Settings \u203A Backups in the app).`,
|
|
1884
|
+
at: rows(c.root, [c.name]),
|
|
1885
|
+
choices: winners.flatMap((x) => {
|
|
1886
|
+
const plan = planFor(x.dir);
|
|
1887
|
+
const visible2 = plan ? gitVisibleSteps(plan).map((s) => describeStep(s, c.root)) : [];
|
|
1888
|
+
const label = `Keep the ${copyLabel(c, x.dir)} copy`;
|
|
1889
|
+
if (!visible2.length) return [{ label, hint: x.dir === c.managed ? "skilllib's version" : "the others become links to it", run: keep(x.dir, "go") }];
|
|
1890
|
+
return [
|
|
1891
|
+
{ label: `${label}, but keep git as it is`, hint: `skips what git would see: ${visible2.join(", ")}`, run: keep(x.dir, "keep") },
|
|
1892
|
+
{ label: `${label}, and change what git tracks`, hint: `git will see: ${visible2.join(", ")}`, run: keep(x.dir, "go") }
|
|
1893
|
+
];
|
|
1894
|
+
})
|
|
1833
1895
|
};
|
|
1834
1896
|
}
|
|
1835
1897
|
function applyAll(plans, git2) {
|
|
1836
1898
|
const results = plans.map((p) => applyTidy(p, { git: git2 }));
|
|
1837
1899
|
return { skills: results.filter((r) => r.applied.length).length, held: results.reduce((n, r) => n + r.held.length, 0) };
|
|
1838
1900
|
}
|
|
1839
|
-
function
|
|
1840
|
-
const
|
|
1901
|
+
function brokenLinksIn(root) {
|
|
1902
|
+
const where = basename6(root);
|
|
1903
|
+
const tidied = ALL_PROJECT_DIRS.filter((d) => d !== GROK_SKILLS_DIR);
|
|
1904
|
+
const hasCopy = (name) => tidied.some((d) => !isLink(join13(root, d, name)) && existsSync12(join13(root, d, name, "SKILL.md")));
|
|
1905
|
+
let git2;
|
|
1906
|
+
return ALL_PROJECT_DIRS.flatMap((dir) => {
|
|
1907
|
+
const folder = join13(root, dir);
|
|
1908
|
+
if (!existsSync12(folder)) return [];
|
|
1909
|
+
return readdirSync7(folder).flatMap((name) => {
|
|
1910
|
+
const path = join13(folder, name);
|
|
1911
|
+
if (!isLink(path) || existsSync12(path) || dir !== GROK_SKILLS_DIR && hasCopy(name)) return [];
|
|
1912
|
+
const remove2 = {
|
|
1913
|
+
label: "Remove the link",
|
|
1914
|
+
run: () => {
|
|
1915
|
+
unlinkSync4(path);
|
|
1916
|
+
return `Removed the broken link ${dir}/${name} in ${where}`;
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
git2 ??= gitInfo(root);
|
|
1920
|
+
const tracked = ["committed", "changed"].includes(git2?.of(relativeTo(root, path)) ?? "");
|
|
1921
|
+
return [
|
|
1922
|
+
{
|
|
1923
|
+
id: `broken:${root}:${dir}/${name}`,
|
|
1924
|
+
severity: "problem",
|
|
1925
|
+
title: `${name}: broken link in ${where}/${dir}`,
|
|
1926
|
+
detail: `It points at a folder that no longer exists, so it loads nothing.${tracked ? " Git tracks the link, so removing it shows in git status." : ""}`,
|
|
1927
|
+
at: rows(root, [name]),
|
|
1928
|
+
...tracked ? { choices: [remove2] } : { fix: remove2 }
|
|
1929
|
+
}
|
|
1930
|
+
];
|
|
1931
|
+
});
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
function grokDiffers(projects2, machine, statusOf) {
|
|
1935
|
+
if (!enabledHarnesses().includes("grok")) return [];
|
|
1936
|
+
const issue = (root, name, grokCopy, other) => ({
|
|
1937
|
+
id: `grok-differs:${root ?? "global"}:${name}`,
|
|
1938
|
+
severity: "suggestion",
|
|
1939
|
+
title: `${name}${root ? ` in ${basename6(root)}` : ""}: Grok runs a different copy`,
|
|
1940
|
+
detail: `Grok reads ${grokCopy} first; your other agents run ${other}, and the two differ. skilllib doesn't change Grok's folder: update or remove that copy there to make them match.`,
|
|
1941
|
+
at: rows(root, [name])
|
|
1942
|
+
});
|
|
1943
|
+
const grokGlobal = join13(grokDir(), "skills");
|
|
1944
|
+
const global = skillDirsIn(grokGlobal).filter((path) => !isLink(path)).flatMap((path) => {
|
|
1945
|
+
const name = basename6(path);
|
|
1946
|
+
const other = machine.find((m) => m.name === name && m.movable && !m.broken && m.harnesses.some((id) => id !== "grok") && realPath(dirname6(m.path)) !== realPath(grokGlobal));
|
|
1947
|
+
return other && treeHash(other.path) !== treeHash(path) ? [issue(null, name, tildify(path), tildify(other.path))] : [];
|
|
1948
|
+
});
|
|
1949
|
+
const repos = projects2.flatMap(
|
|
1950
|
+
(root) => skillDirsIn(join13(root, GROK_SKILLS_DIR)).filter((path) => !isLink(path)).flatMap((path) => {
|
|
1951
|
+
const name = basename6(path);
|
|
1952
|
+
const other = statusOf(root).find((s) => s.name === name && !s.path.startsWith(join13(root, GROK_SKILLS_DIR)));
|
|
1953
|
+
return other && treeHash(other.path) !== treeHash(path) ? [issue(root, name, `${GROK_SKILLS_DIR}/${name}`, relativeTo(root, other.path))] : [];
|
|
1954
|
+
})
|
|
1955
|
+
);
|
|
1956
|
+
return [...global, ...repos];
|
|
1957
|
+
}
|
|
1958
|
+
function findIssues(roots, machine, libraryNames, statusOf = projectStatus, loadedIn = (root) => skillsLoadedIn(root, machine), nestedOf = nestedSkills) {
|
|
1959
|
+
const { readable: projects2, unreadable } = readableRoots(roots);
|
|
1960
|
+
const issues = unreadable.map(({ root, reason }) => ({
|
|
1961
|
+
id: `manifest:${root}`,
|
|
1962
|
+
severity: "problem",
|
|
1963
|
+
title: `${basename6(root)}: skilllib.json can't be read`,
|
|
1964
|
+
detail: `${reason}. Until it's fixed, skilllib leaves ${basename6(root)} out.`,
|
|
1965
|
+
at: []
|
|
1966
|
+
}));
|
|
1841
1967
|
const statuses = new Map(projects2.map((root) => [root, statusOf(root)]));
|
|
1968
|
+
issues.push(...grokDiffers(projects2, machine, (root) => statuses.get(root)));
|
|
1842
1969
|
const nested = new Map(projects2.map((root) => [root, nestedOf(root)]));
|
|
1843
1970
|
for (const skill of machine.filter((m) => m.broken)) {
|
|
1844
1971
|
issues.push({
|
|
1845
|
-
id: `broken:${skill.
|
|
1972
|
+
id: `broken:${skill.path}`,
|
|
1846
1973
|
severity: "problem",
|
|
1847
|
-
title: `${skill.name}: broken link in ${tildify(
|
|
1974
|
+
title: `${skill.name}: broken link in ${tildify(dirname6(skill.path))}`,
|
|
1848
1975
|
detail: "It points at a folder that no longer exists, so it loads nothing.",
|
|
1976
|
+
at: rows(null, [skill.name]),
|
|
1849
1977
|
fix: {
|
|
1850
1978
|
label: "Remove the link",
|
|
1851
1979
|
run: () => {
|
|
@@ -1868,7 +1996,8 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1868
1996
|
id: `dup:${name}`,
|
|
1869
1997
|
severity: "problem",
|
|
1870
1998
|
title: `${name}: loaded twice in ${harnesses.join(" and ")}`,
|
|
1871
|
-
detail: `${copies.map((c) => `${c.kind} (${c.origin})`).join(" and ")}. Your skill wins: turn the ${vendor.map((c) => c.kind).join(" and ")} copy off at its source${vendor.some((c) => c.kind === "claude.ai") ? " (claude.ai \u2192 Settings \u2192 Skills)" : ""}${vendor.some((c) => c.kind === "system") ? ` (whoever manages ${vendor.find((c) => c.kind === "system").origin})` : ""}
|
|
1999
|
+
detail: `${copies.map((c) => `${c.kind} (${c.origin})`).join(" and ")}. Your skill wins: turn the ${vendor.map((c) => c.kind).join(" and ")} copy off at its source${vendor.some((c) => c.kind === "claude.ai") ? " (claude.ai \u2192 Settings \u2192 Skills)" : ""}${vendor.some((c) => c.kind === "system") ? ` (whoever manages ${vendor.find((c) => c.kind === "system").origin})` : ""}.`,
|
|
2000
|
+
at: rows(null, [name])
|
|
1872
2001
|
});
|
|
1873
2002
|
}
|
|
1874
2003
|
const loadedHere = new Map(projects2.map((root) => [root, loadedIn(root)]));
|
|
@@ -1913,7 +2042,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1913
2042
|
const pluginName = plugin.id.split("@")[0];
|
|
1914
2043
|
const remove2 = {
|
|
1915
2044
|
label: `Remove the plugin ${plugin.id}`,
|
|
1916
|
-
hint: plugin.extras.length ? `also removes its ${plugin.extras.join(", ")}` : "restorable
|
|
2045
|
+
hint: plugin.extras.length ? `also removes its ${plugin.extras.join(", ")}` : "restorable with skilllib restore",
|
|
1917
2046
|
run: () => orThrow(removePlugin(plugin, "delete"))
|
|
1918
2047
|
};
|
|
1919
2048
|
const off = { label: `Turn the plugin ${plugin.id} off`, hint: "stays installed", run: () => orThrow(removePlugin(plugin, "off")) };
|
|
@@ -1934,9 +2063,25 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1934
2063
|
plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : "",
|
|
1935
2064
|
inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) => basename6(root)).join(", ")}.` : ""
|
|
1936
2065
|
].filter(Boolean).join(" "),
|
|
1937
|
-
choices: [...plugin.synced ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off], ...inRepos.flatMap(([root, d]) => repoChoices(plugin, root, d))]
|
|
2066
|
+
choices: [...plugin.synced ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off], ...inRepos.flatMap(([root, d]) => repoChoices(plugin, root, d))],
|
|
2067
|
+
// Global's rows (yours and the plugin's), and the repos where both load.
|
|
2068
|
+
at: [...rows(null, dupes), ...[...inRepos, ...cursorRepos].flatMap(([root, d]) => rows(root, d))],
|
|
2069
|
+
plugin: plugin.id
|
|
1938
2070
|
});
|
|
1939
2071
|
}
|
|
2072
|
+
if (enabledHarnesses().includes("claude-code"))
|
|
2073
|
+
for (const p of installedPlugins().filter((x) => x.update && !x.synced && !x.settingsOnly)) {
|
|
2074
|
+
const update2 = { label: p.update === "newer" ? "Update it" : `Update to ${p.update}`, hint: `runs claude plugin update ${p.id}`, run: () => orThrow(updatePlugin(p)) };
|
|
2075
|
+
issues.push({
|
|
2076
|
+
id: `plugin-update:${installId(p)}`,
|
|
2077
|
+
severity: "suggestion",
|
|
2078
|
+
title: `${p.id}${installedIn(p)}: ${p.update === "newer" ? "its marketplace has a newer commit" : `update available (${p.version ?? "?"} \u2192 ${p.update})`}`,
|
|
2079
|
+
detail: "Updates can bring new commands, hooks or MCP servers. If the marketplace asks to run a command, skilllib stops and you update it yourself. Claude Code uses the new version after a restart.",
|
|
2080
|
+
choices: [update2],
|
|
2081
|
+
plugin: p.id,
|
|
2082
|
+
pluginAt: p.projectPath ?? null
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
1940
2085
|
const machinePluginSkills = new Set(machinePlugins.flatMap((p) => p.skills));
|
|
1941
2086
|
for (const root of projects2) {
|
|
1942
2087
|
const where = basename6(root);
|
|
@@ -1951,7 +2096,9 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1951
2096
|
severity: "problem",
|
|
1952
2097
|
title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}, on in ${where}`,
|
|
1953
2098
|
detail: `${where}'s Claude Code settings turn the plugin on, so Claude Code loads both there (the plugin's as /${plugin.id.split("@")[0]}:${dupes[0]}). Your skill wins.`,
|
|
1954
|
-
choices: repoChoices(plugin, root, inRepo)
|
|
2099
|
+
choices: repoChoices(plugin, root, inRepo),
|
|
2100
|
+
at: rows(root, dupes),
|
|
2101
|
+
plugin: plugin.id
|
|
1955
2102
|
});
|
|
1956
2103
|
}
|
|
1957
2104
|
}
|
|
@@ -1963,7 +2110,9 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1963
2110
|
id: `cursor-plugin:${plugin}`,
|
|
1964
2111
|
severity: "suggestion",
|
|
1965
2112
|
title: `${names.join(", ")}: also in the Cursor plugin ${plugin}`,
|
|
1966
|
-
detail: `Cursor lists both copies when that plugin is on. Your skill wins: turn the plugin off in Cursor (Settings \u2192 Plugins)
|
|
2113
|
+
detail: `Cursor lists both copies when that plugin is on. Your skill wins: turn the plugin off in Cursor (Settings \u2192 Plugins).`,
|
|
2114
|
+
at: [...rows(null, names.filter((n) => globalYours.has(n))), ...projects2.flatMap((root) => rows(root, names.filter((n) => statuses.get(root).some((s) => s.name === n))))],
|
|
2115
|
+
plugin
|
|
1967
2116
|
});
|
|
1968
2117
|
}
|
|
1969
2118
|
}
|
|
@@ -1973,7 +2122,8 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1973
2122
|
id: "tidy:global",
|
|
1974
2123
|
severity: "suggestion",
|
|
1975
2124
|
title: `${plural(global.plans.length, "global skill")} with duplicate copies`,
|
|
1976
|
-
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
|
|
2125
|
+
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 with skilllib restore.`,
|
|
2126
|
+
at: rows(null, global.plans.map((p) => p.name)),
|
|
1977
2127
|
fix: {
|
|
1978
2128
|
label: "Keep one copy of each",
|
|
1979
2129
|
run: () => {
|
|
@@ -1986,13 +2136,14 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1986
2136
|
}
|
|
1987
2137
|
issues.push(...global.conflicts.map(conflictIssue));
|
|
1988
2138
|
const kept = keptGlobal();
|
|
1989
|
-
const unreviewed = [...new Set(loaded.filter((m) => m.movable && !kept.has(m.name)).map((m) => m.name))];
|
|
2139
|
+
const unreviewed = [...new Set(loaded.filter((m) => m.movable && m.harnesses.length && !kept.has(m.name)).map((m) => m.name))];
|
|
1990
2140
|
if (unreviewed.length) {
|
|
1991
2141
|
issues.push({
|
|
1992
2142
|
id: "review:global",
|
|
1993
2143
|
severity: "suggestion",
|
|
1994
2144
|
title: `${plural(unreviewed.length, "global skill")} of yours ${unreviewed.length === 1 ? "loads" : "load"} in every repo`,
|
|
1995
|
-
detail: `${unreviewed.join(", ")}. Move each to the repos that need it (skilllib global move <name> --to <repo>...), or mark the ones you want everywhere as global on purpose (skilllib global keep <name>)
|
|
2145
|
+
detail: `${unreviewed.join(", ")}. Move each to the repos that need it (skilllib global move <name> --to <repo>...), or mark the ones you want everywhere as global on purpose (skilllib global keep <name>).`,
|
|
2146
|
+
at: rows(null, unreviewed)
|
|
1996
2147
|
});
|
|
1997
2148
|
}
|
|
1998
2149
|
const enabled = enabledHarnesses();
|
|
@@ -2019,6 +2170,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2019
2170
|
severity: "suggestion",
|
|
2020
2171
|
title: `${plural(skills.length, "global skill")}${onPurpose ? " you keep global" : ""} not usable by ${agentNames(missing)}`,
|
|
2021
2172
|
detail: `${skills.map((m) => `${m.name} (only ${agentNames(m.harnesses)})`).join(", ")}. ${onPurpose ? "A link in each agent's global folder makes the one real copy reach every agent you use." : `Link ${skills.length === 1 ? "it" : "them"} for every agent, or move ${skills.length === 1 ? "it" : "them"} to the repos that need ${skills.length === 1 ? "it" : "them"} (skilllib global move), which works for every agent too.`}`,
|
|
2173
|
+
at: rows(null, [...new Set(skills.map((m) => m.name))]),
|
|
2022
2174
|
...onPurpose ? { fix: link2 } : { choices: [link2] }
|
|
2023
2175
|
});
|
|
2024
2176
|
}
|
|
@@ -2028,22 +2180,30 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2028
2180
|
for (const root of projects2) {
|
|
2029
2181
|
const where = basename6(root);
|
|
2030
2182
|
const status2 = statuses.get(root);
|
|
2031
|
-
|
|
2183
|
+
issues.push(...brokenLinksIn(root));
|
|
2184
|
+
const pins = readManifest(root).skills;
|
|
2185
|
+
const missing = status2.filter((s) => s.state === "folder missing" && s.latest !== null);
|
|
2186
|
+
const restorable = missing.filter((s) => versionForHash(s.name, pins[s.name]?.hash ?? "") !== null);
|
|
2187
|
+
for (const s of missing.filter((m) => !restorable.includes(m)))
|
|
2188
|
+
issues.push({
|
|
2189
|
+
id: `pin:${root}:${s.name}`,
|
|
2190
|
+
severity: "problem",
|
|
2191
|
+
title: `${s.name}: the version ${where}'s skilllib.json pins isn't in your library`,
|
|
2192
|
+
detail: `The folder is missing, and no version in Your skills has the content it pins (pinned from another library?). Install your newest v${s.latest}; ${where}'s skilllib.json then pins it.`,
|
|
2193
|
+
at: rows(root, [s.name]),
|
|
2194
|
+
// It changes what the shared skilllib.json pins: a choice, never run by `doctor --fix`.
|
|
2195
|
+
choices: [{ label: `Install your newest (v${s.latest})`, hint: `${where}'s skilllib.json pins it`, run: () => `${s.name}: installed v${track(root, s.name).to}` }]
|
|
2196
|
+
});
|
|
2032
2197
|
if (restorable.length > 0) {
|
|
2033
2198
|
issues.push({
|
|
2034
2199
|
id: `sync:${root}`,
|
|
2035
2200
|
severity: "problem",
|
|
2036
2201
|
title: `${where}: ${plural(restorable.length, "skill")} in skilllib.json not installed`,
|
|
2037
2202
|
detail: `${restorable.map((s) => s.name).join(", ")}: the folder is missing`,
|
|
2203
|
+
at: rows(root, restorable.map((s) => s.name)),
|
|
2038
2204
|
fix: {
|
|
2039
2205
|
label: "Sync the project",
|
|
2040
|
-
run: () =>
|
|
2041
|
-
const changes = syncProject(root);
|
|
2042
|
-
const done = changes.filter((c) => c.action !== "skipped");
|
|
2043
|
-
const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
|
|
2044
|
-
if (!done.length) throw new Error(`${where}: nothing restored${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
|
|
2045
|
-
return `${where}: restored ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
|
|
2046
|
-
}
|
|
2206
|
+
run: () => summarize(where, "restored", syncProject(root))
|
|
2047
2207
|
}
|
|
2048
2208
|
});
|
|
2049
2209
|
}
|
|
@@ -2053,6 +2213,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2053
2213
|
severity: "problem",
|
|
2054
2214
|
title: `${s.name}: pinned in ${where}'s skilllib.json, but not in your library`,
|
|
2055
2215
|
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.`,
|
|
2216
|
+
at: rows(root, [s.name]),
|
|
2056
2217
|
// Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
|
|
2057
2218
|
choices: [
|
|
2058
2219
|
{
|
|
@@ -2073,15 +2234,10 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2073
2234
|
severity: "suggestion",
|
|
2074
2235
|
title: `${where}: ${plural(behind.length, "skill")} out of date`,
|
|
2075
2236
|
detail: behind.map((s) => `${s.name} (v${s.version} \u2192 v${s.latest})`).join(", "),
|
|
2237
|
+
at: rows(root, behind.map((s) => s.name)),
|
|
2076
2238
|
fix: {
|
|
2077
2239
|
label: "Update to the newest versions",
|
|
2078
|
-
run: () =>
|
|
2079
|
-
const changes = updateProject(root, behind.map((s) => s.name));
|
|
2080
|
-
const done = changes.filter((c) => c.action !== "skipped");
|
|
2081
|
-
const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
|
|
2082
|
-
if (!done.length) throw new Error(`${where}: nothing updated${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
|
|
2083
|
-
return `${where}: updated ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
|
|
2084
|
-
}
|
|
2240
|
+
run: () => summarize(where, "updated", updateProject(root, behind.map((s) => s.name)))
|
|
2085
2241
|
}
|
|
2086
2242
|
});
|
|
2087
2243
|
const tidy2 = planProjectTidy(root);
|
|
@@ -2097,40 +2253,45 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2097
2253
|
id: `tidy:${root}`,
|
|
2098
2254
|
severity: "suggestion",
|
|
2099
2255
|
title: `${where}: ${plural(tidy2.plans.length, "skill")} with extra copies or links`,
|
|
2100
|
-
detail: tidy2.plans.map((p) => `${p.name}: ${p.steps.map((s) => describeStep(s, root)).join(", ")}`).join(" \xB7 ") + (visible2.length ? `. Git would see ${plural(visible2.length, "of these change")}: ${visible2.join(", ")}.` : ". Replaced copies are restorable
|
|
2256
|
+
detail: tidy2.plans.map((p) => `${p.name}: ${p.steps.map((s) => describeStep(s, root)).join(", ")}`).join(" \xB7 ") + (visible2.length ? `. Git would see ${plural(visible2.length, "of these change")}: ${visible2.join(", ")}.` : ". Replaced copies are restorable with skilllib restore."),
|
|
2257
|
+
at: rows(root, tidy2.plans.map((p) => p.name)),
|
|
2101
2258
|
...visible2.length ? {
|
|
2102
2259
|
choices: [
|
|
2103
|
-
|
|
2260
|
+
// "Keep git as it is" only when it leaves something to do.
|
|
2261
|
+
...visible2.length < tidy2.plans.reduce((n, p) => n + p.steps.length, 0) ? [{ label: "Tidy, but keep git as it is", hint: `skips the ${plural(visible2.length, "change")} git would see`, run: run("keep") }] : [],
|
|
2104
2262
|
{ label: "Tidy everything", hint: `${plural(visible2.length, "change")} will show in git status`, run: run("go") }
|
|
2105
2263
|
]
|
|
2106
2264
|
} : { fix: { label: "Tidy", run: run("go") } }
|
|
2107
2265
|
});
|
|
2108
2266
|
}
|
|
2109
2267
|
issues.push(...tidy2.conflicts.map(conflictIssue));
|
|
2110
|
-
const
|
|
2268
|
+
const loadsGlobally = (name, id) => globalCopies.some((m) => m.name === name && m.harnesses.includes(id));
|
|
2269
|
+
const blindHere = status2.filter((s) => s.state !== "folder missing" && s.visibility.some((v) => v.paths === 0 && !loadsGlobally(s.name, v.id))).map((s) => ({ s, would: linkEverywhere(root, s.name, s.location, { dryRun: true }) }));
|
|
2111
2270
|
for (const { s, would } of blindHere.filter(({ would: w }) => !w.created.length && !w.blocked.length && w.uncommitted.length))
|
|
2112
2271
|
issues.push({
|
|
2113
2272
|
id: `uncommitted:${root}:${s.name}`,
|
|
2114
2273
|
severity: "suggestion",
|
|
2115
2274
|
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`,
|
|
2116
|
-
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
|
|
2275
|
+
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\`).`,
|
|
2276
|
+
at: rows(root, [s.name])
|
|
2117
2277
|
});
|
|
2118
2278
|
const partial = blindHere.filter(({ would }) => would.created.length || would.blocked.length).map(({ s }) => s);
|
|
2119
2279
|
if (partial.length) {
|
|
2120
|
-
const
|
|
2280
|
+
const missing2 = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
|
|
2121
2281
|
const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
|
|
2122
2282
|
const run = (allowTracked) => () => {
|
|
2123
2283
|
if (allowTracked) allowTrackedLinks(root);
|
|
2124
2284
|
const r = linkAll(root, { allowTracked });
|
|
2125
2285
|
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)` : ""}`;
|
|
2126
2286
|
if (!r.linked.length) throw new Error(`${where}: nothing linked${notes}`);
|
|
2127
|
-
return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(
|
|
2287
|
+
return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing2)}${notes}`;
|
|
2128
2288
|
};
|
|
2129
2289
|
issues.push({
|
|
2130
2290
|
id: `usable:${root}`,
|
|
2131
2291
|
severity: "suggestion",
|
|
2132
|
-
title: `${where}: ${plural(partial.length, "skill")} not usable by ${agentNames(
|
|
2292
|
+
title: `${where}: ${plural(partial.length, "skill")} not usable by ${agentNames(missing2)}`,
|
|
2133
2293
|
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.` : ""}`,
|
|
2294
|
+
at: rows(root, partial.map((s) => s.name)),
|
|
2134
2295
|
...tracked.length ? {
|
|
2135
2296
|
choices: [
|
|
2136
2297
|
// Only when some link goes where git doesn't track: otherwise this one would link nothing.
|
|
@@ -2151,12 +2312,13 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2151
2312
|
severity: "suggestion",
|
|
2152
2313
|
title: `${s.name} in ${where} isn't in your library`,
|
|
2153
2314
|
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).",
|
|
2315
|
+
at: rows(root, [s.name]),
|
|
2154
2316
|
choices: [
|
|
2155
2317
|
{
|
|
2156
2318
|
label: "Copy into the library",
|
|
2157
2319
|
hint: `${where}'s copy stays as it is`,
|
|
2158
2320
|
run: () => {
|
|
2159
|
-
|
|
2321
|
+
intoLibrary(s.path);
|
|
2160
2322
|
return `${s.name} copied into your library; ${where}'s copy stays as it is`;
|
|
2161
2323
|
}
|
|
2162
2324
|
}
|
|
@@ -2168,13 +2330,14 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2168
2330
|
severity: "suggestion",
|
|
2169
2331
|
title: `${s.name} in ${where} isn't in your library`,
|
|
2170
2332
|
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.",
|
|
2333
|
+
at: rows(root, [s.name]),
|
|
2171
2334
|
choices: [
|
|
2172
2335
|
{
|
|
2173
2336
|
label: "Import into the library",
|
|
2174
|
-
hint:
|
|
2337
|
+
hint: "copies it into Your skills",
|
|
2175
2338
|
run: () => {
|
|
2176
|
-
|
|
2177
|
-
|
|
2339
|
+
intoLibrary(s.path);
|
|
2340
|
+
track(root, s.name);
|
|
2178
2341
|
return `${s.name} imported and tracked in ${where}`;
|
|
2179
2342
|
}
|
|
2180
2343
|
}
|
|
@@ -2186,9 +2349,81 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2186
2349
|
severity: "suggestion",
|
|
2187
2350
|
title: `${s.name} in ${where} isn't tracked`,
|
|
2188
2351
|
detail: "It's identical to the library copy; track it (in skilllib.json) so future library changes reach it.",
|
|
2189
|
-
|
|
2352
|
+
at: rows(root, [s.name]),
|
|
2353
|
+
choices: [{ label: "Track it", hint: `adds it to ${where}'s skilllib.json`, run: () => `${track(root, s.name).name} tracked in ${where}` }]
|
|
2354
|
+
});
|
|
2355
|
+
} else if (s.state === "untracked, differs from library" || s.state === "repo skill, differs from library") {
|
|
2356
|
+
const teams = s.state.startsWith("repo skill") || committed(s);
|
|
2357
|
+
issues.push({
|
|
2358
|
+
id: `differs:${root}:${s.name}`,
|
|
2359
|
+
severity: "suggestion",
|
|
2360
|
+
title: `${s.name} in ${where} differs from your library`,
|
|
2361
|
+
detail: teams ? `It's the team's copy (committed, or in the repo's own .agents/skills), and no version in Your skills matches it. Update your library from it if it's the one to keep; ${where}'s copy stays as it is.` : `No version in Your skills matches it. Update your library from it (${where} then tracks it), or replace it with your library's.`,
|
|
2362
|
+
at: rows(root, [s.name]),
|
|
2363
|
+
choices: [
|
|
2364
|
+
{
|
|
2365
|
+
label: "Update your library from it",
|
|
2366
|
+
hint: teams ? `a new version in Your skills; ${where}'s copy stays as it is` : `a new version in Your skills; ${where} then tracks it`,
|
|
2367
|
+
run: () => {
|
|
2368
|
+
intoLibrary(s.path, true);
|
|
2369
|
+
if (teams) return `${s.name}: your library has ${where}'s copy; ${where}'s copy stays as it is`;
|
|
2370
|
+
const c = track(root, s.name);
|
|
2371
|
+
return `${s.name}: your library has ${where}'s copy as v${c.to}; ${where} tracks it`;
|
|
2372
|
+
}
|
|
2373
|
+
},
|
|
2374
|
+
...teams ? [] : [
|
|
2375
|
+
{
|
|
2376
|
+
label: "Replace it with your library's",
|
|
2377
|
+
hint: "this copy is restorable with skilllib restore",
|
|
2378
|
+
run: () => {
|
|
2379
|
+
const c = addSkill(root, s.name, { force: true });
|
|
2380
|
+
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
2381
|
+
return `${s.name} in ${where} is your library's v${c.to} now; the old copy is restorable with skilllib restore`;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
]
|
|
2385
|
+
]
|
|
2386
|
+
});
|
|
2387
|
+
} else if (s.state === "from npx skills") {
|
|
2388
|
+
const teams = committed(s);
|
|
2389
|
+
issues.push({
|
|
2390
|
+
id: `npx:${root}:${s.name}`,
|
|
2391
|
+
severity: "suggestion",
|
|
2392
|
+
title: `${s.name} in ${where} came from npx skills, not your library`,
|
|
2393
|
+
detail: `Installed with \`npx skills add\`${s.source ? ` (${s.source})` : ""}. Import it into your library to reuse it in other projects and keep it backed up${teams ? `; ${where}'s copy stays as it is (committed)` : `; ${where} then tracks it in skilllib.json`}.`,
|
|
2394
|
+
at: rows(root, [s.name]),
|
|
2395
|
+
choices: [
|
|
2396
|
+
{
|
|
2397
|
+
label: "Import it into your library",
|
|
2398
|
+
hint: teams ? `${where}'s copy stays as it is` : `${where} then tracks it`,
|
|
2399
|
+
run: () => {
|
|
2400
|
+
intoLibrary(s.path);
|
|
2401
|
+
if (teams) return `${s.name} copied into your library; ${where}'s copy stays as it is`;
|
|
2402
|
+
track(root, s.name);
|
|
2403
|
+
return `${s.name} imported and tracked in ${where}`;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
]
|
|
2190
2407
|
});
|
|
2191
2408
|
}
|
|
2409
|
+
if (s.state === "not in library")
|
|
2410
|
+
issues.push({
|
|
2411
|
+
id: `not-in-lib:${root}:${s.name}`,
|
|
2412
|
+
severity: "suggestion",
|
|
2413
|
+
title: `${s.name} in ${where} is tracked, but no longer in your library`,
|
|
2414
|
+
detail: `${where}'s skilllib.json pins it, but your library has no copy: if the folder goes, sync can't restore it, and your other repos can't add it. Copy ${where}'s copy back into the library.`,
|
|
2415
|
+
at: rows(root, [s.name]),
|
|
2416
|
+
choices: [
|
|
2417
|
+
{
|
|
2418
|
+
label: "Copy it back into your library",
|
|
2419
|
+
hint: `${where}'s copy stays as it is`,
|
|
2420
|
+
run: () => {
|
|
2421
|
+
intoLibrary(s.path);
|
|
2422
|
+
return `${s.name} copied back into your library`;
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
]
|
|
2426
|
+
});
|
|
2192
2427
|
if (s.state === "edited locally" || s.state === "edited locally, update available") {
|
|
2193
2428
|
const newer2 = s.state !== "edited locally";
|
|
2194
2429
|
const pinned = s.version ? `v${s.version}` : "the library version";
|
|
@@ -2197,6 +2432,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2197
2432
|
severity: "suggestion",
|
|
2198
2433
|
title: `${s.name} in ${where} has local edits`,
|
|
2199
2434
|
detail: `It no longer matches ${pinned} from Your skills${newer2 ? `, and v${s.latest} is out` : ""}. Save the edits as a new version so your other repos can get them, or discard them. Keeping them is fine too: skilllib never overwrites edits.`,
|
|
2435
|
+
at: rows(root, [s.name]),
|
|
2200
2436
|
choices: [
|
|
2201
2437
|
{
|
|
2202
2438
|
label: "Save as a new version in Your skills",
|
|
@@ -2210,7 +2446,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2210
2446
|
},
|
|
2211
2447
|
{
|
|
2212
2448
|
label: "Discard the edits",
|
|
2213
|
-
hint: `back to ${pinned}; the edited copy is restorable
|
|
2449
|
+
hint: `back to ${pinned}; the edited copy is restorable with skilllib restore`,
|
|
2214
2450
|
run: () => {
|
|
2215
2451
|
const c = discardEdits(root, s.name);
|
|
2216
2452
|
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
@@ -2226,13 +2462,14 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2226
2462
|
for (const root of projects2) {
|
|
2227
2463
|
for (const n of nested.get(root)) {
|
|
2228
2464
|
const copies = alsoGlobal.get(n.name) ?? [];
|
|
2229
|
-
if (alsoLoadedGlobally(n) && !copies.some((c) => c.root === root)) alsoGlobal.set(n.name, [...copies, { root, managed: false }]);
|
|
2465
|
+
if (alsoLoadedGlobally(n) && !copies.some((c) => c.root === root)) alsoGlobal.set(n.name, [...copies, { root, managed: false, nested: true }]);
|
|
2230
2466
|
}
|
|
2231
2467
|
}
|
|
2232
2468
|
for (const [name, copies] of alsoGlobal) {
|
|
2233
2469
|
const wheres = copies.map((c) => basename6(c.root)).join(", ");
|
|
2234
2470
|
const removable = copies.filter((c) => c.managed).map((c) => c.root);
|
|
2235
2471
|
const repoOwn = copies.filter((c) => !c.managed).map((c) => basename6(c.root));
|
|
2472
|
+
const at = [...copies.flatMap((c) => rows(c.root, [name])), ...rows(null, [name])];
|
|
2236
2473
|
if (kept.has(name)) {
|
|
2237
2474
|
const removeWheres = removable.map((r) => basename6(r)).join(", ");
|
|
2238
2475
|
issues.push({
|
|
@@ -2240,13 +2477,15 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2240
2477
|
severity: "suggestion",
|
|
2241
2478
|
title: `${name}: in ${wheres}, and you keep it global`,
|
|
2242
2479
|
detail: `The global copy already loads there, so the project copies are extra. Claude Code runs the global one anyway.${repoOwn.length ? ` ${repoOwn.join(", ")} ${repoOwn.length === 1 ? "has its own copy" : "have their own copies"}: remove ${repoOwn.length === 1 ? "it" : "them"} in the repo, or stop keeping ${name} global.` : ""}`,
|
|
2480
|
+
at,
|
|
2243
2481
|
choices: removable.length ? [
|
|
2244
2482
|
{
|
|
2245
2483
|
label: `Remove the copies in ${removeWheres}`,
|
|
2246
2484
|
hint: "the global copy stays",
|
|
2247
2485
|
run: () => {
|
|
2248
2486
|
const removed = removable.filter((root) => removeSkill(root, name).action === "removed").map((r) => basename6(r));
|
|
2249
|
-
|
|
2487
|
+
if (!removed.length) throw new Error(`${name}: removed from no project (local edits?)`);
|
|
2488
|
+
return `${name}: removed from ${removed.join(", ")}; the global copy stays`;
|
|
2250
2489
|
}
|
|
2251
2490
|
}
|
|
2252
2491
|
] : void 0
|
|
@@ -2255,18 +2494,23 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2255
2494
|
}
|
|
2256
2495
|
const movable = machine.find((m) => m.name === name && m.movable);
|
|
2257
2496
|
issues.push({
|
|
2497
|
+
at,
|
|
2258
2498
|
id: `twice:${name}`,
|
|
2259
2499
|
severity: "suggestion",
|
|
2260
2500
|
title: `${name}: in ${wheres} and also loaded globally`,
|
|
2261
|
-
detail: `${repoOwn.length === copies.length ? `Agents there load both the repo's copy and the global one.` : "The project copies are enough; the global one loads it in every other project too."} ${movable ? "Stop loading it globally, or keep it global on purpose (
|
|
2501
|
+
detail: `${repoOwn.length === copies.length ? `Agents there load both the repo's copy and the global one.` : "The project copies are enough; the global one loads it in every other project too."} ${movable ? "Stop loading it globally, or keep it global on purpose (skilllib global keep)." : `The global copy comes from ${machine.find((m) => m.name === name && !m.movable)?.origin ?? "a vendor"}: turn it off there.`}`,
|
|
2262
2502
|
choices: movable ? [
|
|
2263
2503
|
{
|
|
2264
2504
|
label: "Stop loading it globally",
|
|
2265
|
-
hint: "restorable
|
|
2505
|
+
hint: "restorable with skilllib restore",
|
|
2266
2506
|
run: () => {
|
|
2507
|
+
const blind = copies.filter((c) => !c.nested).flatMap((c) => visibilityOf(c.root, name).flatMap((v) => v.paths === 0 ? [`${harness(v.id).name} in ${basename6(c.root)}`] : []));
|
|
2508
|
+
if (blind.length) throw new Error(`${name}: ${blind.join(", ")} only see the global copy; link the repo copies first (skilllib link)`);
|
|
2267
2509
|
if (!libraryNames.has(name)) importSkill(movable.path);
|
|
2268
|
-
const
|
|
2269
|
-
|
|
2510
|
+
for (const m of machine.filter((x) => x.name === name && x.movable)) {
|
|
2511
|
+
const r = unloadGlobal(m.path, m.links);
|
|
2512
|
+
if (!r.ok) throw new Error(`${name}: ${r.reason}`);
|
|
2513
|
+
}
|
|
2270
2514
|
return `${name} no longer loads globally`;
|
|
2271
2515
|
}
|
|
2272
2516
|
}
|
|
@@ -2289,6 +2533,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2289
2533
|
severity: "suggestion",
|
|
2290
2534
|
title: agentSkill2 === "missing" ? "Your agents don't know about skilllib" : "The skilllib skill for your agents is out of date",
|
|
2291
2535
|
detail: "The skilllib skill lets you ask your agents which skills they can use here, where each comes from, and which of your skills a repo should add.",
|
|
2536
|
+
at: rows(null, ["skilllib"]),
|
|
2292
2537
|
...agentSkill2 === "missing" ? { choices: [install] } : { fix: install }
|
|
2293
2538
|
});
|
|
2294
2539
|
}
|
|
@@ -2316,6 +2561,7 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
|
|
|
2316
2561
|
severity: "suggestion",
|
|
2317
2562
|
title: `${where}: ${plural(unused.length, "skill")} unused in ${uses.days} days`,
|
|
2318
2563
|
detail: `${unused.map((s) => s.name).join(", ")}: no Claude Code session in ${where} used ${unused.length === 1 ? "it" : "them"}. Each one's name and description still takes context in every session there.${unused.length > removable.length ? " The repo's own skills are the team's: remove those in the repo if nobody needs them." : ""}`,
|
|
2564
|
+
at: rows(root, unused.map((s) => s.name)),
|
|
2319
2565
|
choices: removable.length ? [
|
|
2320
2566
|
{
|
|
2321
2567
|
label: `Remove ${removable.length === unused.length ? "them" : `the ${plural(removable.length, "skill")} skilllib installed`} from ${where}`,
|
|
@@ -2324,12 +2570,30 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
|
|
|
2324
2570
|
const changes = removable.map((s) => removeSkill(root, s.name));
|
|
2325
2571
|
const removed = changes.filter((c) => c.action === "removed").map((c) => c.name);
|
|
2326
2572
|
const kept = changes.filter((c) => c.action !== "removed").map((c) => c.name);
|
|
2327
|
-
|
|
2573
|
+
if (!removed.length) throw new Error(`${where}: removed nothing; kept ${kept.join(", ")} (local edits)`);
|
|
2574
|
+
return `${where}: removed ${removed.join(", ")}${kept.length ? `; kept ${kept.join(", ")} (local edits)` : ""}`;
|
|
2328
2575
|
}
|
|
2329
2576
|
}
|
|
2330
2577
|
] : void 0
|
|
2331
2578
|
});
|
|
2332
2579
|
}
|
|
2580
|
+
if (uses.asUsed)
|
|
2581
|
+
for (const p of installedPlugins().filter((x) => x.agent === "claude-code" && x.on && x.skills.length)) {
|
|
2582
|
+
const name = p.id.split("@")[0];
|
|
2583
|
+
if (p.skills.some((path) => uses.asUsed(`${name}:${basename6(path)}`) > 0)) continue;
|
|
2584
|
+
issues.push({
|
|
2585
|
+
id: `plugin-unused:${installId(p)}`,
|
|
2586
|
+
severity: "suggestion",
|
|
2587
|
+
title: `${p.id}${installedIn(p)}: its skills weren't used in ${uses.days} days`,
|
|
2588
|
+
detail: `No Claude Code session used ${p.skills.map((path) => `/${name}:${basename6(path)}`).join(", ")}. Each one's name and description still takes context in every session.${p.extras.length ? ` It also brings ${p.extras.join(", ")}: turning it off pauses those too.` : ""}`,
|
|
2589
|
+
choices: [
|
|
2590
|
+
{ label: `Turn the plugin ${p.id} off`, hint: "stays installed", run: () => orThrow(setPluginOn(p, false)) },
|
|
2591
|
+
...p.synced || p.settingsOnly ? [] : [{ label: `Uninstall the plugin ${p.id}`, hint: "its data stays; restorable with skilllib restore", run: () => orThrow(uninstallPlugin(p)) }]
|
|
2592
|
+
],
|
|
2593
|
+
plugin: p.id,
|
|
2594
|
+
pluginAt: p.projectPath ?? null
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2333
2597
|
const installed = /* @__PURE__ */ new Set([...projects2.flatMap((root) => statusOf(root).map((s) => s.name)), ...machine.filter((m) => !m.broken).map((m) => m.name)]);
|
|
2334
2598
|
const idle = [...libraryNames].filter((name) => {
|
|
2335
2599
|
const first = versionHistory(name)[0];
|
|
@@ -2341,13 +2605,16 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
|
|
|
2341
2605
|
severity: "suggestion",
|
|
2342
2606
|
title: `${plural(idle.length, "skill")} in Your skills: in no repo, unused in ${uses.days} days`,
|
|
2343
2607
|
detail: `${idle.sort().join(", ")}. They load nowhere, so they take no context: add the ones you want to a repo, or delete the ones you've outgrown.`,
|
|
2608
|
+
// Your skills' rows (none loads globally, so none is in Global).
|
|
2609
|
+
at: rows(null, idle),
|
|
2344
2610
|
choices: [
|
|
2345
2611
|
{
|
|
2346
2612
|
label: `Delete ${idle.length === 1 ? "it" : `all ${idle.length}`} from Your skills`,
|
|
2347
|
-
hint: "restorable
|
|
2613
|
+
hint: "restorable with skilllib restore",
|
|
2348
2614
|
run: () => {
|
|
2349
2615
|
const done = idle.map((name) => [name, deleteLibrarySkill(name)]);
|
|
2350
2616
|
const failed = done.filter(([, r]) => !r.ok).map(([name]) => name);
|
|
2617
|
+
if (failed.length === done.length) throw new Error(`Deleted nothing; kept ${failed.join(", ")}`);
|
|
2351
2618
|
return `Deleted ${plural(done.length - failed.length, "skill")} from Your skills${failed.length ? `; kept ${failed.join(", ")}` : ""}`;
|
|
2352
2619
|
}
|
|
2353
2620
|
}
|
|
@@ -2375,7 +2642,7 @@ function globalFrom(kind, origin, path) {
|
|
|
2375
2642
|
var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "target", "vendor", "coverage"]);
|
|
2376
2643
|
function topLevel(root) {
|
|
2377
2644
|
try {
|
|
2378
|
-
return
|
|
2645
|
+
return readdirSync8(root, { withFileTypes: true }).filter((d) => !d.name.startsWith(".") && !SKIP.has(d.name)).map((d) => d.isDirectory() ? `${d.name}/` : d.name).sort().slice(0, 60);
|
|
2379
2646
|
} catch {
|
|
2380
2647
|
return [];
|
|
2381
2648
|
}
|
|
@@ -2391,7 +2658,7 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2391
2658
|
const group = (list2) => groupBy(
|
|
2392
2659
|
list2.map((s) => {
|
|
2393
2660
|
const loadedBy = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
|
|
2394
|
-
const behind = s.
|
|
2661
|
+
const behind = s.state.endsWith("update available");
|
|
2395
2662
|
const gitState = git2?.of(relativeTo(root, s.path));
|
|
2396
2663
|
const repo = !s.managed && (s.state.startsWith("repo skill") || gitState === "committed" || gitState === "changed");
|
|
2397
2664
|
const usual = s.state === "ok" || s.state === "local only" || repo && s.state !== "from npx skills";
|
|
@@ -2429,7 +2696,8 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2429
2696
|
const usesHere = Object.fromEntries([...uses].filter(([, n]) => n > 0));
|
|
2430
2697
|
return {
|
|
2431
2698
|
project: tildify(root),
|
|
2432
|
-
files
|
|
2699
|
+
// Outside a repo there are no project files to read (and the folder may be your home).
|
|
2700
|
+
files: inRepo ? topLevel(root) : [],
|
|
2433
2701
|
agents,
|
|
2434
2702
|
skills,
|
|
2435
2703
|
nested: nested.length > 0 ? nested : void 0,
|
|
@@ -2444,7 +2712,7 @@ function firstSentence(description) {
|
|
|
2444
2712
|
return sentence.length > 200 ? sentence.slice(0, 199).trimEnd() + "\u2026" : sentence;
|
|
2445
2713
|
}
|
|
2446
2714
|
function libraryFor(currentProject) {
|
|
2447
|
-
const manifests = visibleProjects().map((p) => ({ name: basename7(p), skills:
|
|
2715
|
+
const manifests = visibleProjects().map((p) => ({ name: basename7(p), skills: pinsIn(p) }));
|
|
2448
2716
|
const here = currentProject ? new Set(projectStatus(currentProject).map((s) => s.name)) : null;
|
|
2449
2717
|
const all = librarySkills();
|
|
2450
2718
|
const describe2 = (s) => {
|
|
@@ -2456,8 +2724,8 @@ function libraryFor(currentProject) {
|
|
|
2456
2724
|
}
|
|
2457
2725
|
|
|
2458
2726
|
// src/usage.ts
|
|
2459
|
-
import { createReadStream, existsSync as
|
|
2460
|
-
import { join as
|
|
2727
|
+
import { createReadStream, existsSync as existsSync13, readdirSync as readdirSync9, statSync as statSync5 } from "fs";
|
|
2728
|
+
import { join as join14 } from "path";
|
|
2461
2729
|
import { createInterface } from "readline";
|
|
2462
2730
|
var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
|
|
2463
2731
|
"add-dir",
|
|
@@ -2537,12 +2805,12 @@ function usesFromTranscriptLine(line) {
|
|
|
2537
2805
|
return [];
|
|
2538
2806
|
}
|
|
2539
2807
|
function transcriptFiles(sinceMs) {
|
|
2540
|
-
const base =
|
|
2541
|
-
if (!
|
|
2542
|
-
return
|
|
2543
|
-
const dir =
|
|
2544
|
-
if (!
|
|
2545
|
-
return
|
|
2808
|
+
const base = join14(claudeDir(), "projects");
|
|
2809
|
+
if (!existsSync13(base)) return [];
|
|
2810
|
+
return readdirSync9(base).flatMap((project) => {
|
|
2811
|
+
const dir = join14(base, project);
|
|
2812
|
+
if (!statSync5(dir).isDirectory()) return [];
|
|
2813
|
+
return readdirSync9(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join14(dir, f)).filter((f) => statSync5(f).mtimeMs >= sinceMs);
|
|
2546
2814
|
});
|
|
2547
2815
|
}
|
|
2548
2816
|
async function scanUsage(days) {
|
|
@@ -2558,7 +2826,7 @@ async function scanUsage(days) {
|
|
|
2558
2826
|
}
|
|
2559
2827
|
return [...seen.values()];
|
|
2560
2828
|
}
|
|
2561
|
-
function
|
|
2829
|
+
function summarize2(uses, projectOf = () => null) {
|
|
2562
2830
|
const bySkill = /* @__PURE__ */ new Map();
|
|
2563
2831
|
for (const use of uses) {
|
|
2564
2832
|
const skill = use.skill.split(":").pop();
|
|
@@ -2585,7 +2853,12 @@ function usesByProject(uses, projectOf) {
|
|
|
2585
2853
|
|
|
2586
2854
|
// src/move.ts
|
|
2587
2855
|
import { basename as basename8 } from "path";
|
|
2588
|
-
function moveGlobalSkills(skills, roots, {
|
|
2856
|
+
function moveGlobalSkills(skills, roots, {
|
|
2857
|
+
group,
|
|
2858
|
+
allowTracked = false,
|
|
2859
|
+
label = basename8,
|
|
2860
|
+
allowBlind = false
|
|
2861
|
+
} = {}) {
|
|
2589
2862
|
const byName = /* @__PURE__ */ new Map();
|
|
2590
2863
|
for (const m of skills) byName.set(m.name, [...byName.get(m.name) ?? [], m]);
|
|
2591
2864
|
return [...byName].map(([name, copies]) => {
|
|
@@ -2596,7 +2869,14 @@ function moveGlobalSkills(skills, roots, { group, allowTracked = false, label =
|
|
|
2596
2869
|
if (group) recordOrigin(name, `group: ${group}`);
|
|
2597
2870
|
const changes = roots.map((root) => [root, addSkill(root, name, { allowTracked })]);
|
|
2598
2871
|
const skipped = changes.filter(([, c]) => c.action === "skipped");
|
|
2599
|
-
|
|
2872
|
+
const why = (reason = "") => reason.replace(/ \(use --force to replace it\)$/, ": move or remove it there first");
|
|
2873
|
+
if (skipped.length) return { name, moved: false, reason: `not added to ${skipped.map(([r, c]) => `${label(r)} (${why(c.reason)})`).join(", ")}, so it still loads globally`, changes };
|
|
2874
|
+
const held = changes.filter(([, c]) => c.blocked?.length || c.uncommitted?.length);
|
|
2875
|
+
const blind = held.flatMap(([root]) => visibilityOf(root, name).flatMap((v) => v.paths === 0 ? [`${harness(v.id).name} in ${label(root)}`] : []));
|
|
2876
|
+
if (blind.length && !allowBlind) {
|
|
2877
|
+
const next = held.some(([, c]) => c.uncommitted?.length) ? "commit the skill's copy in the repo, then run skilllib link --allow-tracked there" : "re-run with --allow-tracked";
|
|
2878
|
+
return { name, moved: false, reason: `added, but ${blind.join(", ")} couldn't see it (git tracks the folder its link goes in), so it still loads globally; ${next}`, changes };
|
|
2879
|
+
}
|
|
2600
2880
|
const failed = copies.map((m) => unloadGlobal(m.path, m.links)).find((r) => !r.ok);
|
|
2601
2881
|
return failed && !failed.ok ? { name, moved: false, reason: failed.reason, changes } : { name, moved: true, changes };
|
|
2602
2882
|
});
|
|
@@ -2604,23 +2884,36 @@ function moveGlobalSkills(skills, roots, { group, allowTracked = false, label =
|
|
|
2604
2884
|
|
|
2605
2885
|
// src/commands.ts
|
|
2606
2886
|
var USAGE_DAYS = 30;
|
|
2887
|
+
var FLAGS = {
|
|
2888
|
+
force: { type: "boolean" },
|
|
2889
|
+
all: { type: "boolean" },
|
|
2890
|
+
global: { type: "boolean" },
|
|
2891
|
+
fix: { type: "boolean" },
|
|
2892
|
+
"allow-tracked": { type: "boolean" },
|
|
2893
|
+
json: { type: "boolean" },
|
|
2894
|
+
"dry-run": { type: "boolean" },
|
|
2895
|
+
"allow-git": { type: "boolean" },
|
|
2896
|
+
days: { type: "string" },
|
|
2897
|
+
keep: { type: "string" },
|
|
2898
|
+
to: { type: "string", multiple: true }
|
|
2899
|
+
};
|
|
2607
2900
|
function parseArgs(argv) {
|
|
2608
|
-
const
|
|
2609
|
-
const days =
|
|
2610
|
-
|
|
2901
|
+
const { values: v, positionals } = parseFlags({ args: argv, options: FLAGS, allowPositionals: true, strict: true });
|
|
2902
|
+
const days = Number(v.days ?? USAGE_DAYS);
|
|
2903
|
+
if (!Number.isInteger(days) || days <= 0) throw new Error(`--days takes a whole number of days, like --days 30 (got ${v.days})`);
|
|
2611
2904
|
return {
|
|
2612
|
-
positional:
|
|
2613
|
-
force:
|
|
2614
|
-
all:
|
|
2615
|
-
global:
|
|
2616
|
-
fix:
|
|
2617
|
-
allowTracked:
|
|
2618
|
-
json:
|
|
2619
|
-
days
|
|
2620
|
-
dryRun:
|
|
2621
|
-
allowGit:
|
|
2622
|
-
...
|
|
2623
|
-
to:
|
|
2905
|
+
positional: positionals,
|
|
2906
|
+
force: !!v.force,
|
|
2907
|
+
all: !!v.all,
|
|
2908
|
+
global: !!v.global,
|
|
2909
|
+
fix: !!v.fix,
|
|
2910
|
+
allowTracked: !!v["allow-tracked"],
|
|
2911
|
+
json: !!v.json,
|
|
2912
|
+
days,
|
|
2913
|
+
dryRun: !!v["dry-run"],
|
|
2914
|
+
allowGit: !!v["allow-git"],
|
|
2915
|
+
...v.keep ? { keep: v.keep } : {},
|
|
2916
|
+
to: (v.to ?? []).flatMap((t) => t.split(",").filter(Boolean))
|
|
2624
2917
|
};
|
|
2625
2918
|
}
|
|
2626
2919
|
function colorState(state) {
|
|
@@ -2632,25 +2925,24 @@ function colorState(state) {
|
|
|
2632
2925
|
function withDirs(root, changes) {
|
|
2633
2926
|
const { skills } = readManifest(root);
|
|
2634
2927
|
return changes.map((c) => {
|
|
2635
|
-
const dep = c.action === "installed" || c.action === "updated" || c.action === "reset" ? skills[c.name] : void 0;
|
|
2928
|
+
const dep = c.action === "installed" || c.action === "updated" || c.action === "reset" || c.action === "relinked" ? skills[c.name] : void 0;
|
|
2636
2929
|
return dep ? { ...c, dirs: [dep.dir ?? ".claude/skills", ...dep.links ?? []] } : c;
|
|
2637
2930
|
});
|
|
2638
2931
|
}
|
|
2932
|
+
var incomplete = (c) => c.action === "skipped" || !!c.blocked?.length || !!c.uncommitted?.length;
|
|
2933
|
+
function warnHeldLinks(c, where = "here") {
|
|
2934
|
+
if (c.blocked?.length) warn(`${c.name}: git tracks ${c.blocked.join(", ")} ${where}, so no link was added there. Re-run with --allow-tracked to add it.`);
|
|
2935
|
+
if (c.uncommitted?.length)
|
|
2936
|
+
warn(`${c.name}: git tracks ${c.uncommitted.join(", ")} ${where} 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.`);
|
|
2937
|
+
}
|
|
2639
2938
|
function printChanges(root, changes, args) {
|
|
2640
|
-
if (changes.some(
|
|
2939
|
+
if (changes.some(incomplete)) process.exitCode = 1;
|
|
2641
2940
|
if (args.json) return json("changes", withDirs(root, changes));
|
|
2642
2941
|
for (const c of changes) {
|
|
2643
|
-
|
|
2644
|
-
warn(`${c.name}: git tracks ${c.blocked.join(", ")} here, so no link was added there. Re-run with --allow-tracked to add it.`);
|
|
2645
|
-
}
|
|
2646
|
-
if (c.uncommitted?.length) {
|
|
2647
|
-
warn(
|
|
2648
|
-
`${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.`
|
|
2649
|
-
);
|
|
2650
|
-
}
|
|
2942
|
+
warnHeldLinks(c);
|
|
2651
2943
|
if (c.action === "skipped") error(`${c.name}: ${c.reason}`);
|
|
2652
2944
|
else {
|
|
2653
|
-
const icon = c.action === "removed" ? red("-") : c.action === "reset" ? yellow("\u21BA") : green(c.action === "installed" ? "+" : "\u2191");
|
|
2945
|
+
const icon = c.action === "removed" ? red("-") : c.action === "reset" ? yellow("\u21BA") : green(c.action === "installed" ? "+" : c.action === "relinked" ? "\u21E2" : "\u2191");
|
|
2654
2946
|
const versions = c.action === "removed" ? "" : c.from && c.from !== c.to ? ` v${c.from} \u2192 v${c.to}` : c.to ? ` v${c.to}` : "";
|
|
2655
2947
|
info(` ${icon} ${c.name}${versions} ${dim(c.action)}`);
|
|
2656
2948
|
}
|
|
@@ -2665,9 +2957,9 @@ function projectOfFactory(projects2) {
|
|
|
2665
2957
|
const sorted = [...projects2].sort((a, b) => b.length - a.length);
|
|
2666
2958
|
return (cwd) => {
|
|
2667
2959
|
if (!cwd) return null;
|
|
2668
|
-
const known = sorted.find((p) => cwd === p || cwd.startsWith(p +
|
|
2960
|
+
const known = sorted.find((p) => cwd === p || cwd.startsWith(p + sep6));
|
|
2669
2961
|
if (known) return known;
|
|
2670
|
-
const repo = cwd.split(`${
|
|
2962
|
+
const repo = cwd.split(`${sep6}.claude${sep6}worktrees${sep6}`)[0];
|
|
2671
2963
|
return isProjectCandidate(repo) ? repo : null;
|
|
2672
2964
|
};
|
|
2673
2965
|
}
|
|
@@ -2675,13 +2967,23 @@ async function usageBySkill(days, onlyProject) {
|
|
|
2675
2967
|
const projectOf = projectOfFactory(onlyProject ? [onlyProject, ...knownProjects()] : knownProjects());
|
|
2676
2968
|
const uses = await scanUsage(days);
|
|
2677
2969
|
const scoped = onlyProject ? uses.filter((u) => projectOf(u.cwd) === onlyProject) : uses;
|
|
2678
|
-
return new Map(
|
|
2970
|
+
return new Map(summarize2(scoped, projectOf).map((s) => [s.skill, s]));
|
|
2679
2971
|
}
|
|
2972
|
+
var projectsFor = (args) => {
|
|
2973
|
+
if (!args.all) return [requireProject()];
|
|
2974
|
+
const { readable, unreadable } = readableRoots(visibleProjects());
|
|
2975
|
+
for (const u of unreadable) {
|
|
2976
|
+
error(`${basename9(u.root)}: ${u.reason}`);
|
|
2977
|
+
process.exitCode = 1;
|
|
2978
|
+
}
|
|
2979
|
+
return readable;
|
|
2980
|
+
};
|
|
2981
|
+
var yourGlobals = () => machineSkills().filter((m) => m.movable && !m.broken);
|
|
2680
2982
|
function requireProject() {
|
|
2681
2983
|
const root = projectHere();
|
|
2682
2984
|
if (root) return root;
|
|
2683
2985
|
if (!isProjectCandidate(findProjectRoot())) {
|
|
2684
|
-
const stray =
|
|
2986
|
+
const stray = existsSync14(join15(userHome(), MANIFEST_FILE)) ? `, though ${tildify(join15(userHome(), MANIFEST_FILE))} makes it look like one` : "";
|
|
2685
2987
|
throw new Error(`Your home folder isn't a project${stray}: skills added here would load in every repo. Run this inside a project.`);
|
|
2686
2988
|
}
|
|
2687
2989
|
throw new Error("Not in a git repo. Run this inside a project (or `git init` first).");
|
|
@@ -2704,14 +3006,8 @@ async function status(args) {
|
|
|
2704
3006
|
`);
|
|
2705
3007
|
if (skills.length === 0 && nested.length === 0) info("No skills in this project yet. Add one from your library with: skilllib add <name>");
|
|
2706
3008
|
const usage2 = await usageBySkill(args.days, root);
|
|
2707
|
-
const
|
|
2708
|
-
const
|
|
2709
|
-
const asRepo = {
|
|
2710
|
-
"local only": "repo skill",
|
|
2711
|
-
"untracked copy of library skill": "repo skill, in library",
|
|
2712
|
-
"untracked, differs from library": "repo skill, differs from library"
|
|
2713
|
-
};
|
|
2714
|
-
const stateOf = (s) => colorState(committed(s.path) && asRepo[s.state] || s.state);
|
|
3009
|
+
const teamState = teamStateIn(root, nested.map((s) => s.location));
|
|
3010
|
+
const stateOf = (s) => colorState(teamState(s));
|
|
2715
3011
|
const agentsOf = (s) => {
|
|
2716
3012
|
const ids = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
|
|
2717
3013
|
return ids.length === s.visibility.length ? dim("all") : ids.length ? ids.map((id) => harness(id).name).join(", ") : red("none");
|
|
@@ -2776,7 +3072,7 @@ async function list(args) {
|
|
|
2776
3072
|
} else {
|
|
2777
3073
|
const installs = /* @__PURE__ */ new Map();
|
|
2778
3074
|
for (const project of visibleProjects()) {
|
|
2779
|
-
for (const name of Object.keys(
|
|
3075
|
+
for (const name of Object.keys(pinsIn(project))) {
|
|
2780
3076
|
installs.set(name, [...installs.get(name) ?? [], basename9(project)]);
|
|
2781
3077
|
}
|
|
2782
3078
|
}
|
|
@@ -2795,7 +3091,7 @@ async function list(args) {
|
|
|
2795
3091
|
);
|
|
2796
3092
|
}
|
|
2797
3093
|
const libraryNames = new Set(skills.map((s) => s.name));
|
|
2798
|
-
const globals = [...new Set(
|
|
3094
|
+
const globals = [...new Set(yourGlobals().map((m) => m.name))];
|
|
2799
3095
|
if (globals.length > 0) {
|
|
2800
3096
|
info("");
|
|
2801
3097
|
warn(`${globals.length} of your global ${globals.length === 1 ? "skill still loads" : "skills still load"} in every project`);
|
|
@@ -2807,20 +3103,33 @@ async function list(args) {
|
|
|
2807
3103
|
);
|
|
2808
3104
|
}
|
|
2809
3105
|
}
|
|
3106
|
+
function teamStateIn(root, extraDirs = []) {
|
|
3107
|
+
const git2 = gitInfo(root, [...new Set(extraDirs)]);
|
|
3108
|
+
const committed = (path) => ["committed", "changed"].includes(git2?.of(relativeTo(root, path)) ?? "");
|
|
3109
|
+
const asRepo = {
|
|
3110
|
+
"local only": "repo skill",
|
|
3111
|
+
"untracked copy of library skill": "repo skill, in library",
|
|
3112
|
+
"untracked, differs from library": "repo skill, differs from library"
|
|
3113
|
+
};
|
|
3114
|
+
return (s) => committed(s.path) && asRepo[s.state] || s.state;
|
|
3115
|
+
}
|
|
2810
3116
|
function projects(args) {
|
|
2811
|
-
const roots = visibleProjects();
|
|
3117
|
+
const { readable: roots, unreadable } = readableRoots(visibleProjects());
|
|
3118
|
+
for (const u of unreadable) {
|
|
3119
|
+
error(`${basename9(u.root)}: ${u.reason}`);
|
|
3120
|
+
process.exitCode = 1;
|
|
3121
|
+
}
|
|
2812
3122
|
if (args.json) {
|
|
2813
3123
|
return json(
|
|
2814
3124
|
"projects",
|
|
2815
|
-
roots.map(
|
|
2816
|
-
(root)
|
|
3125
|
+
roots.map((root) => {
|
|
3126
|
+
const teamState = teamStateIn(root);
|
|
3127
|
+
return {
|
|
2817
3128
|
name: basename9(root),
|
|
2818
3129
|
path: root,
|
|
2819
|
-
skills: projectStatus(root).map(
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
})
|
|
2823
|
-
)
|
|
3130
|
+
skills: projectStatus(root).map((s) => ({ name: s.name, managed: s.managed, state: teamState(s), version: s.version, latest: s.latest }))
|
|
3131
|
+
};
|
|
3132
|
+
})
|
|
2824
3133
|
);
|
|
2825
3134
|
}
|
|
2826
3135
|
if (roots.length === 0) {
|
|
@@ -2844,8 +3153,7 @@ function projects(args) {
|
|
|
2844
3153
|
}
|
|
2845
3154
|
function add(args) {
|
|
2846
3155
|
if (args.positional.length === 0) {
|
|
2847
|
-
|
|
2848
|
-
process.exit(1);
|
|
3156
|
+
throw new Error("Usage: skilllib add <skill-name>... [--force]");
|
|
2849
3157
|
}
|
|
2850
3158
|
const root = requireProject();
|
|
2851
3159
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
@@ -2854,90 +3162,89 @@ function add(args) {
|
|
|
2854
3162
|
}
|
|
2855
3163
|
function remove(args) {
|
|
2856
3164
|
if (args.positional.length === 0) {
|
|
2857
|
-
|
|
2858
|
-
process.exit(1);
|
|
3165
|
+
throw new Error("Usage: skilllib remove <skill-name>... [--force]");
|
|
2859
3166
|
}
|
|
2860
3167
|
const root = requireProject();
|
|
2861
3168
|
printChanges(root, args.positional.map((name) => removeSkill(root, name, { force: args.force })), args);
|
|
2862
3169
|
}
|
|
2863
|
-
function
|
|
2864
|
-
const roots = args
|
|
3170
|
+
function changeEach(args, change, nothing) {
|
|
3171
|
+
const roots = projectsFor(args);
|
|
2865
3172
|
const results = roots.map((root) => {
|
|
2866
3173
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
2867
|
-
const changes =
|
|
2868
|
-
if (changes.some(
|
|
3174
|
+
const changes = change(root);
|
|
3175
|
+
if (changes.some(incomplete)) process.exitCode = 1;
|
|
2869
3176
|
if (!args.json) {
|
|
2870
3177
|
if (args.all) info(`${basename9(root)} ${dim(tildify(root))}`);
|
|
2871
|
-
if (changes.length === 0)
|
|
2872
|
-
|
|
2873
|
-
else info(dim("No skilllib.json: nothing to sync"));
|
|
2874
|
-
} else printChanges(root, changes, args);
|
|
3178
|
+
if (changes.length === 0) nothing(root);
|
|
3179
|
+
else printChanges(root, changes, args);
|
|
2875
3180
|
}
|
|
2876
3181
|
return { project: basename9(root), path: root, changes: withDirs(root, changes) };
|
|
2877
3182
|
});
|
|
2878
3183
|
if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
|
|
3184
|
+
return roots;
|
|
3185
|
+
}
|
|
3186
|
+
function sync(args) {
|
|
3187
|
+
if (args.positional.length) throw new Error(`sync takes no skill names: it installs exactly what skilllib.json pins. To add a skill: skilllib add ${args.positional.join(" ")}`);
|
|
3188
|
+
const roots = changeEach(
|
|
3189
|
+
args,
|
|
3190
|
+
(root) => syncProject(root, { force: args.force }),
|
|
3191
|
+
(root) => existsSync14(join15(root, MANIFEST_FILE)) ? success("Installed versions match skilllib.json") : info(dim("No skilllib.json: nothing to sync"))
|
|
3192
|
+
);
|
|
2879
3193
|
if (!args.all) rememberProjects(roots);
|
|
2880
3194
|
}
|
|
2881
3195
|
function update(args) {
|
|
2882
|
-
const
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
if (changes.length === 0) success("Everything is on the latest version");
|
|
2895
|
-
else printChanges(root, changes, args);
|
|
2896
|
-
}
|
|
2897
|
-
return { project: basename9(root), path: root, changes: withDirs(root, changes) };
|
|
2898
|
-
});
|
|
2899
|
-
if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
|
|
3196
|
+
const names = args.positional.length ? args.positional : void 0;
|
|
3197
|
+
changeEach(
|
|
3198
|
+
args,
|
|
3199
|
+
(root) => {
|
|
3200
|
+
const changes = updateProject(root, names, { force: args.force });
|
|
3201
|
+
if (!args.all) {
|
|
3202
|
+
for (const name of names ?? []) if (!(name in readManifest(root).skills)) changes.push({ name, action: "skipped", reason: "not managed by skilllib in this project" });
|
|
3203
|
+
}
|
|
3204
|
+
return changes;
|
|
3205
|
+
},
|
|
3206
|
+
() => success("Everything is on the latest version")
|
|
3207
|
+
);
|
|
2900
3208
|
}
|
|
2901
3209
|
function outdated(args) {
|
|
2902
|
-
const roots = args
|
|
3210
|
+
const roots = projectsFor(args);
|
|
2903
3211
|
const behind = roots.flatMap(
|
|
2904
|
-
(root) => projectStatus(root).filter((s) => s.
|
|
3212
|
+
(root) => projectStatus(root).filter((s) => s.state.endsWith("update available") && s.latest !== null && s.version !== null).map((s) => ({ project: basename9(root), path: root, skill: s.name, installed: s.version, latest: s.latest, state: s.state }))
|
|
2905
3213
|
);
|
|
2906
3214
|
if (args.json) return json("outdated", behind);
|
|
2907
|
-
const
|
|
2908
|
-
if (
|
|
2909
|
-
table(
|
|
3215
|
+
const rows2 = behind.map((b) => ({ Project: b.project, Skill: b.skill, Installed: `v${b.installed}`, Latest: `v${b.latest}`, Status: colorState(b.state) }));
|
|
3216
|
+
if (rows2.length === 0) return success("Everything is on the latest version");
|
|
3217
|
+
table(rows2);
|
|
2910
3218
|
info(dim(`
|
|
2911
3219
|
\u2192 skilllib update${args.all ? " --all" : ""}`));
|
|
2912
3220
|
}
|
|
2913
3221
|
function importSkills(args) {
|
|
2914
|
-
|
|
3222
|
+
if (args.global && args.positional.length) throw new Error("skilllib import --global takes no folders: it imports all your global skills");
|
|
3223
|
+
const dirs = args.global ? yourGlobals().map((m) => m.path) : args.positional.map((p) => resolve7(p));
|
|
2915
3224
|
if (dirs.length === 0) {
|
|
2916
|
-
|
|
2917
|
-
process.exit(1);
|
|
3225
|
+
throw new Error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
|
|
2918
3226
|
}
|
|
2919
3227
|
const results = [];
|
|
2920
3228
|
for (const dir of dirs) {
|
|
2921
3229
|
if (!isSkillDir(dir)) {
|
|
2922
3230
|
warn(`${tildify(dir)}: no SKILL.md, skipping`);
|
|
2923
3231
|
results.push({ path: dir, status: "no SKILL.md" });
|
|
3232
|
+
process.exitCode = 1;
|
|
2924
3233
|
continue;
|
|
2925
3234
|
}
|
|
2926
3235
|
const { name, status: result } = importSkill(dir, { force: args.force });
|
|
2927
3236
|
results.push({ path: dir, name, status: result });
|
|
2928
3237
|
if (result === "exists") {
|
|
2929
3238
|
warn(`${name}: the library already has a different version (use --force to replace it)`);
|
|
3239
|
+
process.exitCode = 1;
|
|
2930
3240
|
continue;
|
|
2931
3241
|
}
|
|
2932
3242
|
success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
|
|
2933
|
-
const
|
|
2934
|
-
const
|
|
2935
|
-
if (
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
if (change.action !== "skipped") info(dim(` ${basename9(root)} now depends on ${name} v${change.to}`));
|
|
2939
|
-
}
|
|
2940
|
-
rememberProjects([root]);
|
|
3243
|
+
const root = resolve7(dir, "..", "..", "..");
|
|
3244
|
+
const dep = projectHere(root) === root ? readManifest(root).skills[name] : void 0;
|
|
3245
|
+
if (dep && realpathOrNull(join15(root, dep.dir ?? PROJECT_SKILLS_DIR, name)) === realpathOrNull(dir) && dep.hash !== treeHash(dir)) {
|
|
3246
|
+
const change = addSkill(root, name);
|
|
3247
|
+
if (change.action !== "skipped") info(dim(` ${basename9(root)} now depends on ${name} v${change.to}`));
|
|
2941
3248
|
}
|
|
2942
3249
|
}
|
|
2943
3250
|
if (args.json) json("import", results);
|
|
@@ -2952,7 +3259,7 @@ function scan(args) {
|
|
|
2952
3259
|
const found = discoverProjects();
|
|
2953
3260
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
2954
3261
|
const skillNames = (root) => [
|
|
2955
|
-
.../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.flatMap((d) => skillDirsIn(
|
|
3262
|
+
.../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.flatMap((d) => skillDirsIn(join15(root, d))).map((d) => basename9(d)), ...nestedSkills(root).map((s) => s.name)])
|
|
2956
3263
|
];
|
|
2957
3264
|
const withSkills = found.filter((root) => skillNames(root).length > 0);
|
|
2958
3265
|
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
|
|
@@ -2969,6 +3276,7 @@ ${yellow("Yellow")} skills aren't in your library yet (\`skilllib import <skill-
|
|
|
2969
3276
|
}
|
|
2970
3277
|
function folders(args) {
|
|
2971
3278
|
const [action, ...paths] = args.positional;
|
|
3279
|
+
if (action && !["add", "remove", "rm"].includes(action)) throw new Error("Usage: skilllib folders [add|remove <folder>...]");
|
|
2972
3280
|
let failed = false;
|
|
2973
3281
|
if (action === "add") {
|
|
2974
3282
|
for (const p of paths) {
|
|
@@ -2983,19 +3291,21 @@ function folders(args) {
|
|
|
2983
3291
|
} else if (action === "remove" || action === "rm") {
|
|
2984
3292
|
for (const p of paths) {
|
|
2985
3293
|
if (removeRoot(p)) success(`Removed ${tildify(expandHome(p))}`);
|
|
2986
|
-
else
|
|
3294
|
+
else {
|
|
3295
|
+
warn(`${tildify(expandHome(p))} wasn't a project folder`);
|
|
3296
|
+
failed = true;
|
|
3297
|
+
}
|
|
2987
3298
|
}
|
|
2988
3299
|
}
|
|
2989
3300
|
const { roots, hidden } = readConfig();
|
|
2990
3301
|
info(roots.length ? `Project folders:
|
|
2991
3302
|
${roots.map((r) => ` ${tildify(r)}`).join("\n")}` : "No project folders. Add one: skilllib folders add ~/Projects");
|
|
2992
3303
|
if (hidden.length) info(dim(`Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")} (skilllib unhide <path>)`));
|
|
2993
|
-
if (failed) process.
|
|
3304
|
+
if (failed) process.exitCode = 1;
|
|
2994
3305
|
}
|
|
2995
3306
|
function unhide(args) {
|
|
2996
3307
|
if (args.positional.length === 0) {
|
|
2997
|
-
|
|
2998
|
-
process.exit(1);
|
|
3308
|
+
throw new Error("Usage: skilllib unhide <path>...");
|
|
2999
3309
|
}
|
|
3000
3310
|
let failed = false;
|
|
3001
3311
|
for (const p of args.positional) {
|
|
@@ -3007,20 +3317,18 @@ function unhide(args) {
|
|
|
3007
3317
|
failed = true;
|
|
3008
3318
|
}
|
|
3009
3319
|
}
|
|
3010
|
-
if (failed) process.
|
|
3320
|
+
if (failed) process.exitCode = 1;
|
|
3011
3321
|
}
|
|
3012
3322
|
function newSkill(args) {
|
|
3013
3323
|
const [name, ...rest] = args.positional;
|
|
3014
3324
|
if (!name) {
|
|
3015
|
-
|
|
3016
|
-
process.exit(1);
|
|
3325
|
+
throw new Error("Usage: skilllib new <name> [description]");
|
|
3017
3326
|
}
|
|
3018
3327
|
const r = createSkill(name, rest.join(" "));
|
|
3019
3328
|
if (!r.ok) {
|
|
3020
|
-
|
|
3021
|
-
process.exit(1);
|
|
3329
|
+
throw new Error(`Can't create ${name}: ${r.reason}`);
|
|
3022
3330
|
}
|
|
3023
|
-
success(`Created ${tildify(
|
|
3331
|
+
success(`Created ${tildify(join15(r.dir, "SKILL.md"))}`);
|
|
3024
3332
|
}
|
|
3025
3333
|
function restore(args) {
|
|
3026
3334
|
const backups = [...listBackups(), ...pluginBackups()];
|
|
@@ -3031,16 +3339,17 @@ function restore(args) {
|
|
|
3031
3339
|
info(dim("\n\u2192 skilllib restore <name>"));
|
|
3032
3340
|
return;
|
|
3033
3341
|
}
|
|
3034
|
-
const
|
|
3035
|
-
if (!
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3342
|
+
const newest = backups.find((b) => b.name === name);
|
|
3343
|
+
if (!newest) throw new Error(`No backup named ${name} (skilllib restore lists them; a skill removed from a repo that your library has comes back with skilllib add ${name})`);
|
|
3344
|
+
const at = (b) => Date.parse(b.path.slice(-24).replace(/^(.{13})-(\d\d)-(\d\d)-(\d{3})Z$/, "$1:$2:$3.$4Z"));
|
|
3345
|
+
const together = newest.kind === "plugin" ? [newest] : backups.filter((b) => b.name === name && b.kind === newest.kind && Math.abs(at(b) - at(newest)) <= 2e3);
|
|
3346
|
+
for (const backup of together) {
|
|
3347
|
+
const r = backup.kind === "plugin" ? restorePlugin(backup) : restoreBackup(backup);
|
|
3348
|
+
if (r.ok) success(`Restored ${name} to ${tildify(r.to)}`);
|
|
3349
|
+
else {
|
|
3350
|
+
error(`Can't restore ${name}: ${r.reason}`);
|
|
3351
|
+
process.exitCode = 1;
|
|
3352
|
+
}
|
|
3044
3353
|
}
|
|
3045
3354
|
}
|
|
3046
3355
|
async function allIssues(args, withUsage = true) {
|
|
@@ -3052,11 +3361,14 @@ async function allIssues(args, withUsage = true) {
|
|
|
3052
3361
|
const projectOf = projectOfFactory(projects2);
|
|
3053
3362
|
const uses = await scanUsage(args.days);
|
|
3054
3363
|
const inProject = usesByProject(uses, projectOf);
|
|
3055
|
-
const total = new Map(
|
|
3364
|
+
const total = new Map(summarize2(uses, projectOf).map((s) => [s.skill, s.uses]));
|
|
3365
|
+
const asUsed = /* @__PURE__ */ new Map();
|
|
3366
|
+
for (const u of uses) asUsed.set(u.skill, (asUsed.get(u.skill) ?? 0) + 1);
|
|
3056
3367
|
issues.push(
|
|
3057
|
-
...usageIssues(projects2, machine, libraryNames, {
|
|
3368
|
+
...usageIssues(readableRoots(projects2).readable, machine, libraryNames, {
|
|
3058
3369
|
inProject: (root, skill) => inProject.get(root)?.get(skill) ?? 0,
|
|
3059
3370
|
total: (skill) => total.get(skill) ?? 0,
|
|
3371
|
+
asUsed: (key) => asUsed.get(key) ?? 0,
|
|
3060
3372
|
days: args.days
|
|
3061
3373
|
})
|
|
3062
3374
|
);
|
|
@@ -3101,7 +3413,7 @@ async function doctor(args) {
|
|
|
3101
3413
|
async function apply(args) {
|
|
3102
3414
|
const [id, pick] = args.positional;
|
|
3103
3415
|
if (!id) throw new Error("Usage: skilllib apply <issue-id> [choice] (ids and choices: skilllib status --json or doctor --json)");
|
|
3104
|
-
const found = [...statusIssues(findProjectRoot()), ...await allIssues(args,
|
|
3416
|
+
const found = [...statusIssues(findProjectRoot()), ...await allIssues(args, /^(plugin-)?unused:/.test(id))].filter((i) => i.id === id);
|
|
3105
3417
|
if (!found.length) throw new Error(`No issue ${id} right now (it may be fixed already; for an unused:\u2026 issue, pass doctor's --days). See skilllib status --json or doctor --json`);
|
|
3106
3418
|
const optionsOf = (i) => i.choices ?? (i.fix ? [i.fix] : []);
|
|
3107
3419
|
const labels = (i) => optionsOf(i).map((c) => c.label).join("\0");
|
|
@@ -3124,22 +3436,22 @@ async function apply(args) {
|
|
|
3124
3436
|
else error(r.message);
|
|
3125
3437
|
}
|
|
3126
3438
|
async function usage(args) {
|
|
3127
|
-
const summaries =
|
|
3439
|
+
const summaries = summarize2(await scanUsage(args.days), projectOfFactory(knownProjects()));
|
|
3440
|
+
const library = new Set(librarySkills().map((s) => s.name));
|
|
3441
|
+
const unused = [...library].filter((name) => !summaries.some((u) => u.skill === name));
|
|
3128
3442
|
if (args.json) {
|
|
3129
|
-
const library2 = new Set(librarySkills().map((s) => s.name));
|
|
3130
3443
|
return json("usage", {
|
|
3131
3444
|
usageDays: args.days,
|
|
3132
3445
|
source: "Claude Code transcripts",
|
|
3133
|
-
skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) => basename9(p)), inLibrary:
|
|
3134
|
-
unusedLibrarySkills:
|
|
3446
|
+
skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) => basename9(p)), inLibrary: library.has(s.skill) })),
|
|
3447
|
+
unusedLibrarySkills: unused
|
|
3135
3448
|
});
|
|
3136
3449
|
}
|
|
3137
3450
|
if (summaries.length === 0) {
|
|
3138
3451
|
info(`No skill usage in Claude Code transcripts from the last ${args.days} days.`);
|
|
3139
3452
|
return;
|
|
3140
3453
|
}
|
|
3141
|
-
|
|
3142
|
-
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join14(claudeDir(), "projects"))})`)}
|
|
3454
|
+
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join15(claudeDir(), "projects"))})`)}
|
|
3143
3455
|
`);
|
|
3144
3456
|
table(
|
|
3145
3457
|
summaries.map((s) => ({
|
|
@@ -3150,18 +3462,16 @@ async function usage(args) {
|
|
|
3150
3462
|
Library: library.has(s.skill) ? green("yes") : dim("no")
|
|
3151
3463
|
}))
|
|
3152
3464
|
);
|
|
3153
|
-
const unused = librarySkills().filter((s) => !summaries.some((u) => u.skill === s.name));
|
|
3154
3465
|
if (unused.length > 0) {
|
|
3155
3466
|
info("");
|
|
3156
|
-
warn(`Library skills unused in the last ${args.days} days: ${unused.
|
|
3467
|
+
warn(`Library skills unused in the last ${args.days} days: ${unused.join(", ")}`);
|
|
3157
3468
|
}
|
|
3158
3469
|
}
|
|
3159
3470
|
function show(args) {
|
|
3160
3471
|
const name = args.positional[0];
|
|
3161
|
-
const dir = name ?
|
|
3472
|
+
const dir = name ? join15(libraryDir(), name) : "";
|
|
3162
3473
|
if (!name || !isSkillDir(dir)) {
|
|
3163
|
-
|
|
3164
|
-
process.exit(1);
|
|
3474
|
+
throw new Error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
|
|
3165
3475
|
}
|
|
3166
3476
|
const { description } = readSkillInfo(dir);
|
|
3167
3477
|
if (args.json) {
|
|
@@ -3171,14 +3481,14 @@ function show(args) {
|
|
|
3171
3481
|
path: dir,
|
|
3172
3482
|
latest: latestVersion(name)?.version ?? null,
|
|
3173
3483
|
origin: libraryOrigins()[name] ?? "",
|
|
3174
|
-
projects: knownProjects().filter((p) => name in
|
|
3175
|
-
skillMd:
|
|
3484
|
+
projects: knownProjects().filter((p) => name in pinsIn(p)).map((p) => basename9(p)),
|
|
3485
|
+
skillMd: readFileSync6(join15(dir, "SKILL.md"), "utf-8")
|
|
3176
3486
|
});
|
|
3177
3487
|
}
|
|
3178
3488
|
info(`${name} ${dim(tildify(dir))}`);
|
|
3179
3489
|
if (description) info(`
|
|
3180
3490
|
${description}`);
|
|
3181
|
-
const using = knownProjects().filter((p) => name in
|
|
3491
|
+
const using = knownProjects().filter((p) => name in pinsIn(p));
|
|
3182
3492
|
info(`
|
|
3183
3493
|
Installed in: ${using.map((p) => tildify(p)).join(", ") || "no projects"}`);
|
|
3184
3494
|
}
|
|
@@ -3187,8 +3497,7 @@ function harnessesCommand(args) {
|
|
|
3187
3497
|
if (args.positional.length) {
|
|
3188
3498
|
const unknown = args.positional.filter((p) => !ids.includes(p));
|
|
3189
3499
|
if (unknown.length) {
|
|
3190
|
-
|
|
3191
|
-
process.exit(1);
|
|
3500
|
+
throw new Error(`Unknown harness: ${unknown.join(", ")}. Choose from: ${ids.join(", ")}`);
|
|
3192
3501
|
}
|
|
3193
3502
|
setHarnesses(args.positional);
|
|
3194
3503
|
success(`Using ${args.positional.join(", ")}`);
|
|
@@ -3201,7 +3510,7 @@ function harnessesCommand(args) {
|
|
|
3201
3510
|
New installs go to ${installDirs(enabled).join(" (real copy) + ")}${installDirs(enabled).length > 1 ? " (link)" : ""}.`));
|
|
3202
3511
|
}
|
|
3203
3512
|
function link(args) {
|
|
3204
|
-
const roots = args
|
|
3513
|
+
const roots = projectsFor(args);
|
|
3205
3514
|
for (const root of roots) {
|
|
3206
3515
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
3207
3516
|
const res = linkAll(root);
|
|
@@ -3212,6 +3521,7 @@ function link(args) {
|
|
|
3212
3521
|
process.exitCode = 1;
|
|
3213
3522
|
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`);
|
|
3214
3523
|
}
|
|
3524
|
+
if (res.uncommitted.length) process.exitCode = 1;
|
|
3215
3525
|
if (res.uncommitted.length)
|
|
3216
3526
|
warn(
|
|
3217
3527
|
`${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`
|
|
@@ -3220,19 +3530,17 @@ function link(args) {
|
|
|
3220
3530
|
}
|
|
3221
3531
|
function globalCommand(args) {
|
|
3222
3532
|
const [action, ...names] = args.positional;
|
|
3223
|
-
const yours =
|
|
3533
|
+
const yours = yourGlobals();
|
|
3224
3534
|
if (action === "move") return moveGlobal(args, names, yours);
|
|
3225
3535
|
if (action === "keep" || action === "unkeep") {
|
|
3226
3536
|
const unknown = names.filter((n) => !yours.some((m) => m.name === n));
|
|
3227
3537
|
if (!names.length || unknown.length) {
|
|
3228
|
-
|
|
3229
|
-
process.exit(1);
|
|
3538
|
+
throw new Error(names.length ? `Not one of your global skills: ${unknown.join(", ")}` : `Usage: skilllib global ${action} <name>...`);
|
|
3230
3539
|
}
|
|
3231
3540
|
setKeepGlobal(names, action === "keep");
|
|
3232
3541
|
success(action === "keep" ? `Keeping ${names.join(", ")} global on purpose` : `${names.join(", ")} will show up in cleanup again`);
|
|
3233
3542
|
} else if (action) {
|
|
3234
|
-
|
|
3235
|
-
process.exit(1);
|
|
3543
|
+
throw new Error("Usage: skilllib global [keep|unkeep <name>...] | skilllib global move <name>... --to <repo>...");
|
|
3236
3544
|
}
|
|
3237
3545
|
const kept = keptGlobal();
|
|
3238
3546
|
if (!yours.length) return info("None of your skills load globally.");
|
|
@@ -3247,7 +3555,7 @@ function repoRoot(repo) {
|
|
|
3247
3555
|
if (named.length === 1) return named[0];
|
|
3248
3556
|
if (named.length > 1) throw new Error(`Several projects are named ${repo}: ${named.map(tildify).join(", ")}. Pass its path.`);
|
|
3249
3557
|
const path = expandHome(repo);
|
|
3250
|
-
if (!
|
|
3558
|
+
if (!existsSync14(path)) throw new Error(`No project ${repo} (pass its path, or see skilllib projects)`);
|
|
3251
3559
|
const root = projectHere(path);
|
|
3252
3560
|
if (root !== path) throw new Error(root ? `${tildify(path)} is inside ${tildify(root)}: pass the repo's top folder` : `${tildify(path)} isn't in a git repo`);
|
|
3253
3561
|
return root;
|
|
@@ -3259,6 +3567,7 @@ function moveGlobal(args, names, yours) {
|
|
|
3259
3567
|
const roots = [...new Set(args.to.map(repoRoot))];
|
|
3260
3568
|
const skills = yours.filter((m) => names.includes(m.name));
|
|
3261
3569
|
const moves = moveGlobalSkills(skills, roots, { allowTracked: args.allowTracked });
|
|
3570
|
+
rememberProjects(roots);
|
|
3262
3571
|
if (moves.some((m) => !m.moved)) process.exitCode = 1;
|
|
3263
3572
|
if (args.json) {
|
|
3264
3573
|
return json(
|
|
@@ -3274,10 +3583,7 @@ function moveGlobal(args, names, yours) {
|
|
|
3274
3583
|
for (const m of moves) {
|
|
3275
3584
|
if (m.moved) success(`${m.name} now loads only in ${m.changes.map(([r]) => basename9(r)).join(", ")} (the global copy is in skilllib restore)`);
|
|
3276
3585
|
else error(`${m.name}: ${m.reason}`);
|
|
3277
|
-
for (const [root, c] of m.changes) {
|
|
3278
|
-
if (c.blocked?.length) warn(` ${basename9(root)}: git tracks ${c.blocked.join(", ")}, so no link there. Re-run with --allow-tracked to add it.`);
|
|
3279
|
-
if (c.uncommitted?.length) warn(` ${basename9(root)}: no link in ${c.uncommitted.join(", ")}: git tracks it but not the skill's copy, so the link would break for teammates. Commit the copy, then run skilllib link --allow-tracked.`);
|
|
3280
|
-
}
|
|
3586
|
+
for (const [root, c] of m.changes) warnHeldLinks(c, `in ${basename9(root)}`);
|
|
3281
3587
|
}
|
|
3282
3588
|
if (moves.some((m) => m.moved)) info(dim("Commit skilllib.json and the skill folders in each repo."));
|
|
3283
3589
|
}
|
|
@@ -3285,10 +3591,9 @@ function tidy(args) {
|
|
|
3285
3591
|
const names = args.positional;
|
|
3286
3592
|
const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve7(base, args.keep) : expandHome(args.keep)])) : {};
|
|
3287
3593
|
if (args.keep && !names.length) {
|
|
3288
|
-
|
|
3289
|
-
process.exit(1);
|
|
3594
|
+
throw new Error("Usage: skilllib tidy <name>... --keep <folder>");
|
|
3290
3595
|
}
|
|
3291
|
-
const targets = args.global ? [null] : args
|
|
3596
|
+
const targets = args.global ? [null] : projectsFor(args);
|
|
3292
3597
|
const problems = [];
|
|
3293
3598
|
for (const name of names) {
|
|
3294
3599
|
const where = targets.map((root) => ({ root, ...foldersOf(root, name) })).filter((w) => w.all.length);
|
|
@@ -3306,8 +3611,7 @@ function tidy(args) {
|
|
|
3306
3611
|
}
|
|
3307
3612
|
}
|
|
3308
3613
|
if (problems.length) {
|
|
3309
|
-
|
|
3310
|
-
process.exit(1);
|
|
3614
|
+
throw new Error(problems.join("\n"));
|
|
3311
3615
|
}
|
|
3312
3616
|
const reports = targets.map(
|
|
3313
3617
|
(root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label: basename9(root), report: planProjectTidy(root, { keep: keepFor(root) }) }
|
|
@@ -3317,20 +3621,20 @@ function tidy(args) {
|
|
|
3317
3621
|
for (const { label, report } of reports) {
|
|
3318
3622
|
const plans = report.plans.filter((p) => !names.length || names.includes(p.name));
|
|
3319
3623
|
const conflicts = report.conflicts.filter((c) => !names.length || names.includes(c.name));
|
|
3320
|
-
if (!plans.length && !conflicts.length && !args.all) success(`${label}:
|
|
3624
|
+
if (!plans.length && !conflicts.length && !args.all) success(`${label}: nothing to tidy (no extra copies or links)`);
|
|
3321
3625
|
for (const plan of plans) {
|
|
3322
|
-
const
|
|
3323
|
-
const
|
|
3324
|
-
if (
|
|
3325
|
-
|
|
3326
|
-
held +=
|
|
3626
|
+
const wouldHold = args.allowGit ? [] : gitVisibleSteps(plan);
|
|
3627
|
+
const result = args.dryRun ? { applied: plan.steps.filter((s) => !wouldHold.includes(s)), held: wouldHold } : applyTidy(plan, { git: args.allowGit ? "go" : "keep" });
|
|
3628
|
+
if (result.applied.length) info(`${args.dryRun ? dim("would") : green("\u2713")} ${label} \xB7 ${plan.name}: ${result.applied.map((s) => describeStep(s, plan.root)).join(", ")}`);
|
|
3629
|
+
for (const step of result.held) info(`${yellow("!")} ${label} \xB7 ${plan.name}: ${args.dryRun ? "would be held" : "held"} back, git would see it: ${describeStep(step, plan.root)}`);
|
|
3630
|
+
held += result.held.length;
|
|
3327
3631
|
changed += result.applied.length;
|
|
3328
3632
|
}
|
|
3329
3633
|
if (args.keep && conflicts.length) process.exitCode = 1;
|
|
3330
3634
|
for (const c of conflicts) {
|
|
3331
3635
|
warn(`${label} \xB7 ${c.name}: the copies differ`);
|
|
3332
3636
|
for (const copy of c.copies) {
|
|
3333
|
-
const runs = copy.runs.map((id) =>
|
|
3637
|
+
const runs = copy.runs.map((id) => harness(id).name);
|
|
3334
3638
|
info(` ${copyLabel(c, copy.dir)}${runs.length ? dim(` run by ${runs.join(", ")}`) : ""}${c.managed === copy.dir ? dim(" (skilllib's copy)") : ""}`);
|
|
3335
3639
|
}
|
|
3336
3640
|
const pick = copyLabel(c, c.managed ?? c.copies[0].dir);
|
|
@@ -3347,8 +3651,7 @@ function agentSkill(args) {
|
|
|
3347
3651
|
if (action === "install") {
|
|
3348
3652
|
const r = installAgentSkill();
|
|
3349
3653
|
if (!r.ok) {
|
|
3350
|
-
|
|
3351
|
-
process.exit(1);
|
|
3654
|
+
throw new Error(`Can't install the skilllib skill: ${r.reason}`);
|
|
3352
3655
|
}
|
|
3353
3656
|
if (args.json) return json("agent-skill", { state: agentSkillState(), dirs: r.dirs });
|
|
3354
3657
|
success(`Installed in ${r.dirs.map((d) => tildify(d)).join(" and ")}`);
|
|
@@ -3361,6 +3664,7 @@ function agentSkill(args) {
|
|
|
3361
3664
|
if (args.json) return json("agent-skill", { state: agentSkillState(), removed });
|
|
3362
3665
|
return removed.length ? success(`Removed from ${removed.map((d) => tildify(d)).join(" and ")}`) : info("The skilllib skill isn't installed.");
|
|
3363
3666
|
}
|
|
3667
|
+
if (action) throw new Error("Usage: skilllib agent-skill [install|remove]");
|
|
3364
3668
|
const state = agentSkillState();
|
|
3365
3669
|
if (args.json) return json("agent-skill", { state, dirs: agentSkillDirs() });
|
|
3366
3670
|
if (state === "installed") success(`Your agents can use skilllib ${dim(`(${agentSkillDirs().map((d) => tildify(d)).join(", ")})`)}`);
|
|
@@ -3374,14 +3678,19 @@ export {
|
|
|
3374
3678
|
HARNESSES,
|
|
3375
3679
|
harness,
|
|
3376
3680
|
readSkillInfo,
|
|
3681
|
+
treeHash,
|
|
3377
3682
|
versionHistory,
|
|
3378
3683
|
forgetLatest,
|
|
3379
3684
|
latestVersion,
|
|
3380
3685
|
projectHere,
|
|
3381
3686
|
readManifest,
|
|
3687
|
+
readableRoots,
|
|
3382
3688
|
setJsonMode,
|
|
3689
|
+
isJsonMode,
|
|
3690
|
+
json,
|
|
3383
3691
|
error,
|
|
3384
3692
|
tildify,
|
|
3693
|
+
localStamp,
|
|
3385
3694
|
readConfig,
|
|
3386
3695
|
addRoot,
|
|
3387
3696
|
removeRoot,
|
|
@@ -3406,43 +3715,34 @@ export {
|
|
|
3406
3715
|
relinkDependency,
|
|
3407
3716
|
addSkill,
|
|
3408
3717
|
removeSkill,
|
|
3409
|
-
syncProject,
|
|
3410
3718
|
updateProject,
|
|
3411
3719
|
importSkill,
|
|
3412
3720
|
removeUntracked,
|
|
3413
3721
|
projectsUsing,
|
|
3414
3722
|
deleteLibrarySkill,
|
|
3415
3723
|
deleteGlobal,
|
|
3416
|
-
unloadGlobal,
|
|
3417
|
-
linkGlobal,
|
|
3418
3724
|
createSkill,
|
|
3419
3725
|
listBackups,
|
|
3420
3726
|
restoreBackup,
|
|
3421
3727
|
gitInfo,
|
|
3422
3728
|
relativeTo,
|
|
3423
|
-
claudePlugins,
|
|
3424
3729
|
installedPlugins,
|
|
3425
3730
|
cursorPlugins,
|
|
3426
3731
|
forgetClaudeBinary,
|
|
3427
3732
|
claudeBinary,
|
|
3428
|
-
turnOffIn,
|
|
3429
3733
|
setPluginOn,
|
|
3430
3734
|
uninstallPlugin,
|
|
3431
3735
|
updatePlugin,
|
|
3432
3736
|
pluginBackups,
|
|
3433
3737
|
restorePlugin,
|
|
3434
|
-
cursorPluginSkills,
|
|
3435
3738
|
agentSkillState,
|
|
3436
3739
|
installAgentSkill,
|
|
3437
3740
|
removeAgentSkill,
|
|
3438
3741
|
refreshAgentSkill,
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
describeStep,
|
|
3444
|
-
copyLabel,
|
|
3445
|
-
addedAt,
|
|
3742
|
+
foldersOf,
|
|
3743
|
+
runFix,
|
|
3744
|
+
findIssues,
|
|
3745
|
+
usageIssues,
|
|
3446
3746
|
scanUsage,
|
|
3447
3747
|
moveGlobalSkills,
|
|
3448
3748
|
parseArgs,
|