skilllib 1.11.2 → 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-OFWHWIER.js → chunk-3XDWEHI4.js} +718 -437
- package/dist/index.js +43 -13
- package/dist/json.d.cts +2 -1
- package/dist/json.d.ts +2 -1
- package/dist/{tui-NGTOOVAQ.js → tui-WSHQRBPF.js} +291 -700
- package/package.json +1 -1
- package/skills/skilllib/SKILL.md +4 -4
|
@@ -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,6 +41,46 @@ 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;
|
|
@@ -71,21 +111,25 @@ function truncate(s, max) {
|
|
|
71
111
|
return line.length > max ? line.slice(0, max - 1) + "\u2026" : line;
|
|
72
112
|
}
|
|
73
113
|
var visible = (s) => s.replace(/\x1b\[\d+m/g, "");
|
|
74
|
-
function table(
|
|
75
|
-
if (
|
|
76
|
-
const keys = Object.keys(
|
|
77
|
-
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)));
|
|
78
118
|
const pad = (s, w) => s + " ".repeat(Math.max(0, w - visible(s).length));
|
|
79
119
|
info(dim(keys.map((k, i) => pad(k, widths[i])).join(" ")));
|
|
80
|
-
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", " ");
|
|
81
125
|
}
|
|
82
126
|
|
|
83
127
|
// src/agentSkill.ts
|
|
84
|
-
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";
|
|
85
129
|
import { join as join9 } from "path";
|
|
86
130
|
|
|
87
131
|
// src/config.ts
|
|
88
|
-
import { existsSync as existsSync5,
|
|
132
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
89
133
|
import { homedir as homedir2 } from "os";
|
|
90
134
|
import { join as join6, resolve as resolve4 } from "path";
|
|
91
135
|
|
|
@@ -185,16 +229,16 @@ function installDirs(enabled, keep) {
|
|
|
185
229
|
}
|
|
186
230
|
|
|
187
231
|
// src/project.ts
|
|
188
|
-
import { existsSync as existsSync4,
|
|
189
|
-
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";
|
|
190
234
|
|
|
191
235
|
// src/versions.ts
|
|
192
|
-
import { existsSync as existsSync3, mkdirSync
|
|
236
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
193
237
|
import { join as join4 } from "path";
|
|
194
238
|
|
|
195
239
|
// src/skills.ts
|
|
196
240
|
import { createHash } from "crypto";
|
|
197
|
-
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";
|
|
198
242
|
import { basename, join as join3, relative, sep as sep2 } from "path";
|
|
199
243
|
import { parse as parseYaml } from "yaml";
|
|
200
244
|
var IGNORED = /* @__PURE__ */ new Set([".DS_Store", ".git", "node_modules"]);
|
|
@@ -202,7 +246,7 @@ function readSkillInfo(dir) {
|
|
|
202
246
|
const skillMd = join3(dir, "SKILL.md");
|
|
203
247
|
const fallback = { name: basename(dir), description: "" };
|
|
204
248
|
if (!existsSync2(skillMd)) return fallback;
|
|
205
|
-
const match =
|
|
249
|
+
const match = readFileSync2(skillMd, "utf-8").match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
206
250
|
if (!match) return fallback;
|
|
207
251
|
try {
|
|
208
252
|
const fm = parseYaml(match[1]);
|
|
@@ -220,22 +264,22 @@ function isSkillDir(dir) {
|
|
|
220
264
|
function listFiles(dir) {
|
|
221
265
|
return readdirSync(dir).filter((entry) => !IGNORED.has(entry)).flatMap((entry) => {
|
|
222
266
|
const full = join3(dir, entry);
|
|
223
|
-
const st =
|
|
267
|
+
const st = statSync2(full, { throwIfNoEntry: false });
|
|
224
268
|
return !st ? [] : st.isDirectory() ? listFiles(full) : [full];
|
|
225
269
|
});
|
|
226
270
|
}
|
|
227
271
|
function treeHash(dir) {
|
|
228
272
|
if (!existsSync2(dir)) return null;
|
|
229
273
|
const hash = createHash("sha256");
|
|
230
|
-
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"));
|
|
231
275
|
for (const file of files) {
|
|
232
|
-
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");
|
|
233
277
|
}
|
|
234
278
|
return hash.digest("hex").slice(0, 16);
|
|
235
279
|
}
|
|
236
280
|
function copySkill(from, to) {
|
|
237
281
|
const source = realpathSync2(from);
|
|
238
|
-
|
|
282
|
+
rmSync2(to, { recursive: true, force: true });
|
|
239
283
|
cpSync(source, to, { recursive: true, dereference: true, filter: (src) => !IGNORED.has(basename(src)) && existsSync2(src) });
|
|
240
284
|
}
|
|
241
285
|
function skillDirsIn(dir) {
|
|
@@ -251,11 +295,7 @@ function indexFile(name) {
|
|
|
251
295
|
return join4(storeDir(name), "versions.json");
|
|
252
296
|
}
|
|
253
297
|
function versionHistory(name) {
|
|
254
|
-
|
|
255
|
-
return JSON.parse(readFileSync2(indexFile(name), "utf-8"));
|
|
256
|
-
} catch {
|
|
257
|
-
return [];
|
|
258
|
-
}
|
|
298
|
+
return readState(indexFile(name)) ?? [];
|
|
259
299
|
}
|
|
260
300
|
function versionDir(name, version) {
|
|
261
301
|
return join4(storeDir(name), String(version));
|
|
@@ -277,9 +317,10 @@ function latestVersion(name) {
|
|
|
277
317
|
result = last;
|
|
278
318
|
} else {
|
|
279
319
|
const next = { version: (last?.version ?? 0) + 1, hash, date: (/* @__PURE__ */ new Date()).toISOString() };
|
|
280
|
-
|
|
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 });
|
|
281
322
|
copySkill(source, versionDir(name, next.version));
|
|
282
|
-
|
|
323
|
+
writeState(indexFile(name), [...history, next]);
|
|
283
324
|
result = next;
|
|
284
325
|
}
|
|
285
326
|
}
|
|
@@ -298,7 +339,7 @@ function findProjectRoot(from = process.cwd()) {
|
|
|
298
339
|
let dir = resolve3(from);
|
|
299
340
|
while (true) {
|
|
300
341
|
if (existsSync4(join5(dir, MANIFEST_FILE)) || existsSync4(join5(dir, ".git"))) return dir;
|
|
301
|
-
const parent =
|
|
342
|
+
const parent = dirname2(dir);
|
|
302
343
|
if (parent === dir) return resolve3(from);
|
|
303
344
|
dir = parent;
|
|
304
345
|
}
|
|
@@ -310,7 +351,12 @@ function projectHere(from = process.cwd()) {
|
|
|
310
351
|
function readManifest(root) {
|
|
311
352
|
const path = join5(root, MANIFEST_FILE);
|
|
312
353
|
if (!existsSync4(path)) return { skills: {} };
|
|
313
|
-
|
|
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
|
+
}
|
|
314
360
|
const skills = Object.fromEntries(
|
|
315
361
|
Object.entries(data.skills ?? {}).flatMap(([name, value]) => {
|
|
316
362
|
if (name.startsWith("@")) return [];
|
|
@@ -335,8 +381,8 @@ function readManifest(root) {
|
|
|
335
381
|
return { ...data, skills };
|
|
336
382
|
}
|
|
337
383
|
function writeManifest(root, manifest) {
|
|
338
|
-
const skills = Object.fromEntries(Object.entries(manifest.skills).sort(([a], [b]) => a.localeCompare(b)));
|
|
339
|
-
|
|
384
|
+
const skills = Object.fromEntries(Object.entries(manifest.skills).sort(([a], [b]) => a.localeCompare(b, "en")));
|
|
385
|
+
writeState(join5(root, MANIFEST_FILE), { ...manifest, skills });
|
|
340
386
|
}
|
|
341
387
|
function projectsFile() {
|
|
342
388
|
return join5(skilllibHome(), "projects.json");
|
|
@@ -344,7 +390,7 @@ function projectsFile() {
|
|
|
344
390
|
function knownProjects() {
|
|
345
391
|
const path = projectsFile();
|
|
346
392
|
if (!existsSync4(path)) return [];
|
|
347
|
-
const list2 =
|
|
393
|
+
const list2 = readState(path) ?? [];
|
|
348
394
|
return onePerFolder(list2.filter((p) => existsSync4(p) && isProjectCandidate(p) && !isAgentWorktree(p)));
|
|
349
395
|
}
|
|
350
396
|
function isAgentWorktree(path) {
|
|
@@ -365,8 +411,27 @@ function isProjectCandidate(root) {
|
|
|
365
411
|
function rememberProjects(roots) {
|
|
366
412
|
const incoming = roots.map((r) => resolve3(r)).filter((r) => isProjectCandidate(r) && !isAgentWorktree(r));
|
|
367
413
|
const all = onePerFolder([...knownProjects(), ...incoming]).sort();
|
|
368
|
-
|
|
369
|
-
|
|
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
|
+
}
|
|
370
435
|
}
|
|
371
436
|
|
|
372
437
|
// src/config.ts
|
|
@@ -374,16 +439,11 @@ function configFile() {
|
|
|
374
439
|
return join6(skilllibHome(), "config.json");
|
|
375
440
|
}
|
|
376
441
|
function readConfig() {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
return { ...data, roots: data.roots ?? [], hidden: data.hidden ?? [] };
|
|
380
|
-
} catch {
|
|
381
|
-
return { roots: [], hidden: [] };
|
|
382
|
-
}
|
|
442
|
+
const data = readState(configFile()) ?? {};
|
|
443
|
+
return { ...data, roots: data.roots ?? [], hidden: data.hidden ?? [] };
|
|
383
444
|
}
|
|
384
445
|
function writeConfig(config) {
|
|
385
|
-
|
|
386
|
-
writeFileSync3(configFile(), JSON.stringify(config, null, 2) + "\n");
|
|
446
|
+
writeState(configFile(), config);
|
|
387
447
|
}
|
|
388
448
|
function expandHome(path) {
|
|
389
449
|
return resolve4(path.replace(/^~(?=$|\/)/, homedir2()));
|
|
@@ -391,7 +451,7 @@ function expandHome(path) {
|
|
|
391
451
|
function addRoot(path) {
|
|
392
452
|
const root = expandHome(path);
|
|
393
453
|
if (!existsSync5(root)) throw new Error(`${tildify(root)} doesn't exist`);
|
|
394
|
-
if (!
|
|
454
|
+
if (!statSync3(root).isDirectory()) throw new Error(`${tildify(root)} isn't a folder`);
|
|
395
455
|
const config = readConfig();
|
|
396
456
|
if (!config.roots.includes(root)) writeConfig({ ...config, roots: [...config.roots, root].sort() });
|
|
397
457
|
return root;
|
|
@@ -464,35 +524,25 @@ function setKeepGlobal(names, keep) {
|
|
|
464
524
|
|
|
465
525
|
// src/library.ts
|
|
466
526
|
import { execFileSync } from "child_process";
|
|
467
|
-
import { cpSync as cpSync2, existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as
|
|
468
|
-
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";
|
|
469
529
|
|
|
470
530
|
// src/sources.ts
|
|
471
|
-
import { existsSync as existsSync6, lstatSync, mkdirSync as
|
|
472
|
-
import { basename as basename2, join as join7, resolve as resolve5 } from "path";
|
|
473
|
-
|
|
474
|
-
try {
|
|
475
|
-
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
476
|
-
} catch {
|
|
477
|
-
return null;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
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";
|
|
480
534
|
function skillsShLock() {
|
|
481
535
|
return readJson(join7(userHome(), ".agents", ".skill-lock.json"))?.skills ?? {};
|
|
482
536
|
}
|
|
483
537
|
function projectSkillsLock(root) {
|
|
484
538
|
return readJson(join7(root, "skills-lock.json"))?.skills ?? {};
|
|
485
539
|
}
|
|
486
|
-
function
|
|
487
|
-
|
|
488
|
-
return realpathSync3(path);
|
|
489
|
-
} catch {
|
|
490
|
-
return null;
|
|
491
|
-
}
|
|
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: [] };
|
|
492
542
|
}
|
|
493
543
|
function harnessesReading(dir, enabled) {
|
|
494
544
|
const real = realPath(dir);
|
|
495
|
-
return enabled.filter((id) =>
|
|
545
|
+
return enabled.filter((id) => harness(id).globalDirs().some((d) => realPath(d) === real));
|
|
496
546
|
}
|
|
497
547
|
var systemDirs = () => [codexSystemDir()];
|
|
498
548
|
function globalSkillDirs() {
|
|
@@ -511,17 +561,7 @@ function globalDirAs(dir) {
|
|
|
511
561
|
}
|
|
512
562
|
function systemSkills(enabled) {
|
|
513
563
|
return systemDirs().flatMap(
|
|
514
|
-
(dir) => skillDirsIn(dir).map((path) => (
|
|
515
|
-
name: basename2(path),
|
|
516
|
-
kind: "system",
|
|
517
|
-
origin: dir.replace(/\\/g, "/"),
|
|
518
|
-
path,
|
|
519
|
-
description: readSkillInfo(path).description,
|
|
520
|
-
movable: false,
|
|
521
|
-
broken: false,
|
|
522
|
-
harnesses: harnessesReading(dir, enabled),
|
|
523
|
-
links: []
|
|
524
|
-
}))
|
|
564
|
+
(dir) => skillDirsIn(dir).map((path) => vendorSkill(path, "system", dir.replace(/\\/g, "/"), harnessesReading(dir, enabled)))
|
|
525
565
|
).filter((s) => s.harnesses.length > 0);
|
|
526
566
|
}
|
|
527
567
|
function globalFolderSkills(enabled) {
|
|
@@ -550,7 +590,7 @@ function globalFolderSkills(enabled) {
|
|
|
550
590
|
const inAgents = e.dir === agentsDir || target_.includes(".agents/skills");
|
|
551
591
|
const locked = lock[e.name]?.source;
|
|
552
592
|
if (existsSync6(join7(e.path, OWN_SKILL_MARKER))) {
|
|
553
|
-
skills.push(
|
|
593
|
+
skills.push(vendorSkill(e.path, "skilllib", "skilllib", harnessesReading(e.dir, enabled)));
|
|
554
594
|
continue;
|
|
555
595
|
}
|
|
556
596
|
skills.push({
|
|
@@ -578,31 +618,11 @@ function tildify2(p) {
|
|
|
578
618
|
}
|
|
579
619
|
function grokBuiltInSkills(enabled) {
|
|
580
620
|
if (!enabled.includes("grok")) return [];
|
|
581
|
-
return skillDirsIn(join7(grokDir(), "bundled", "skills")).map((path) => (
|
|
582
|
-
name: basename2(path),
|
|
583
|
-
kind: "built-in",
|
|
584
|
-
origin: "Grok",
|
|
585
|
-
path,
|
|
586
|
-
description: readSkillInfo(path).description,
|
|
587
|
-
movable: false,
|
|
588
|
-
broken: false,
|
|
589
|
-
harnesses: ["grok"],
|
|
590
|
-
links: []
|
|
591
|
-
}));
|
|
621
|
+
return skillDirsIn(join7(grokDir(), "bundled", "skills")).map((path) => vendorSkill(path, "built-in", "Grok", ["grok"]));
|
|
592
622
|
}
|
|
593
623
|
function cursorBuiltInSkills(enabled) {
|
|
594
624
|
if (!enabled.includes("cursor")) return [];
|
|
595
|
-
return skillDirsIn(join7(userHome(), ".cursor", "skills-cursor")).map((path) => (
|
|
596
|
-
name: basename2(path),
|
|
597
|
-
kind: "built-in",
|
|
598
|
-
origin: "Cursor",
|
|
599
|
-
path,
|
|
600
|
-
description: readSkillInfo(path).description,
|
|
601
|
-
movable: false,
|
|
602
|
-
broken: false,
|
|
603
|
-
harnesses: ["cursor"],
|
|
604
|
-
links: []
|
|
605
|
-
}));
|
|
625
|
+
return skillDirsIn(join7(userHome(), ".cursor", "skills-cursor")).map((path) => vendorSkill(path, "built-in", "Cursor", ["cursor"]));
|
|
606
626
|
}
|
|
607
627
|
function claudeAiSkills(enabled) {
|
|
608
628
|
const byReal = /* @__PURE__ */ new Map();
|
|
@@ -617,20 +637,13 @@ function claudeAiSkills(enabled) {
|
|
|
617
637
|
}
|
|
618
638
|
return [...byReal.values()].flatMap(
|
|
619
639
|
({ dir, readers }) => readdirSync3(dir).filter((d) => !d.startsWith(".")).flatMap(
|
|
620
|
-
(bucket) => skillDirsIn(join7(dir, bucket)).map((path) => (
|
|
621
|
-
name: basename2(path),
|
|
622
|
-
kind: "claude.ai",
|
|
623
|
-
origin: "claude.ai account",
|
|
624
|
-
path,
|
|
625
|
-
description: readSkillInfo(path).description,
|
|
626
|
-
movable: false,
|
|
627
|
-
broken: false,
|
|
628
|
-
harnesses: enabled.filter((id) => readers.includes(id)),
|
|
629
|
-
links: []
|
|
630
|
-
}))
|
|
640
|
+
(bucket) => skillDirsIn(join7(dir, bucket)).map((path) => vendorSkill(path, "claude.ai", "claude.ai account", enabled.filter((id) => readers.includes(id))))
|
|
631
641
|
)
|
|
632
642
|
);
|
|
633
643
|
}
|
|
644
|
+
function installedPluginsJson() {
|
|
645
|
+
return readJson(join7(claudeDir(), "plugins", "installed_plugins.json"))?.plugins ?? {};
|
|
646
|
+
}
|
|
634
647
|
function pluginDirCandidates(id, installPaths) {
|
|
635
648
|
const [name = "", marketplace = ""] = id.split("@");
|
|
636
649
|
const plugins = join7(claudeDir(), "plugins");
|
|
@@ -653,7 +666,7 @@ function pluginSkills(harnesses, root) {
|
|
|
653
666
|
const cursor = harnesses.includes("cursor");
|
|
654
667
|
if (!claude2 && !cursor) return [];
|
|
655
668
|
const plugins = join7(claudeDir(), "plugins");
|
|
656
|
-
const installed =
|
|
669
|
+
const installed = installedPluginsJson();
|
|
657
670
|
const user = enabledPlugins();
|
|
658
671
|
const enabled = root ? enabledPlugins(root) : user;
|
|
659
672
|
const fromMarketplaces = [.../* @__PURE__ */ new Set([...Object.keys(enabled), ...Object.keys(user)])].filter((id) => claude2 && enabled[id] || cursor && user[id]).flatMap((id) => {
|
|
@@ -673,17 +686,7 @@ function pluginSkills(harnesses, root) {
|
|
|
673
686
|
return skillDirsIn(join7(root2, "skills")).map((path) => ({ path, origin: `${name}@synced`, loadedBy: ["claude-code"] }));
|
|
674
687
|
})
|
|
675
688
|
) : [];
|
|
676
|
-
return [...fromMarketplaces, ...fromSynced].map(({ path, origin, loadedBy }) => (
|
|
677
|
-
name: basename2(path),
|
|
678
|
-
kind: "plugin",
|
|
679
|
-
origin,
|
|
680
|
-
path,
|
|
681
|
-
description: readSkillInfo(path).description,
|
|
682
|
-
movable: false,
|
|
683
|
-
broken: false,
|
|
684
|
-
harnesses: loadedBy,
|
|
685
|
-
links: []
|
|
686
|
-
}));
|
|
689
|
+
return [...fromMarketplaces, ...fromSynced].map(({ path, origin, loadedBy }) => vendorSkill(path, "plugin", origin, loadedBy));
|
|
687
690
|
}
|
|
688
691
|
var byKindAndName = (a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name);
|
|
689
692
|
function machineSkills(enabled = enabledHarnesses()) {
|
|
@@ -705,15 +708,18 @@ function originsFile() {
|
|
|
705
708
|
return join7(skilllibHome(), "origins.json");
|
|
706
709
|
}
|
|
707
710
|
function libraryOrigins() {
|
|
708
|
-
return
|
|
711
|
+
return readState(originsFile()) ?? {};
|
|
709
712
|
}
|
|
710
713
|
function recordOrigin(name, origin) {
|
|
711
714
|
const origins = libraryOrigins();
|
|
712
715
|
if (origins[name] === origin) return;
|
|
713
|
-
|
|
714
|
-
|
|
716
|
+
mkdirSync3(skilllibHome(), { recursive: true });
|
|
717
|
+
writeState(originsFile(), { ...origins, [name]: origin });
|
|
715
718
|
}
|
|
716
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 "";
|
|
717
723
|
const match = machineSkills().find((s) => resolve5(s.path) === resolve5(dir));
|
|
718
724
|
if (match) return match.kind === "global" ? "global folder" : `${match.kind}: ${match.origin}`;
|
|
719
725
|
const project = dir.split(/[\\/]\.(?:claude|agents|grok|cursor|codex)[\\/]skills[\\/]/)[0];
|
|
@@ -733,13 +739,6 @@ function librarySkills() {
|
|
|
733
739
|
function librarySkillDir(name) {
|
|
734
740
|
return join8(libraryDir(), name);
|
|
735
741
|
}
|
|
736
|
-
function realpathOrNull2(path) {
|
|
737
|
-
try {
|
|
738
|
-
return realpathSync4(path);
|
|
739
|
-
} catch {
|
|
740
|
-
return null;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
742
|
function isLink(path) {
|
|
744
743
|
try {
|
|
745
744
|
return lstatSync2(path).isSymbolicLink();
|
|
@@ -749,7 +748,7 @@ function isLink(path) {
|
|
|
749
748
|
}
|
|
750
749
|
function linkDir(target, link2) {
|
|
751
750
|
if (process.platform === "win32") symlinkSync(target, link2, "junction");
|
|
752
|
-
else symlinkSync(relative2(
|
|
751
|
+
else symlinkSync(relative2(dirname3(link2), target), link2);
|
|
753
752
|
}
|
|
754
753
|
function entryExists(path) {
|
|
755
754
|
return isLink(path) || existsSync7(path);
|
|
@@ -761,9 +760,9 @@ function visibilityOf(root, name, enabled = enabledHarnesses()) {
|
|
|
761
760
|
}));
|
|
762
761
|
}
|
|
763
762
|
function dependencyDirs(root, name, dep) {
|
|
764
|
-
const wanted = installDirs(enabledHarnesses());
|
|
765
763
|
const untracked = ALL_PROJECT_DIRS.find((d) => d !== GROK_SKILLS_DIR && existsSync7(join8(root, d, name, "SKILL.md")) && !isLink(join8(root, d, name)));
|
|
766
|
-
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);
|
|
767
766
|
return [primary, .../* @__PURE__ */ new Set([...dep?.links ?? [], ...wanted.filter((d) => d !== primary)])];
|
|
768
767
|
}
|
|
769
768
|
function isGitTracked(root, dir) {
|
|
@@ -809,7 +808,7 @@ function projectStatus(root) {
|
|
|
809
808
|
}
|
|
810
809
|
for (const [name, paths] of byName) {
|
|
811
810
|
const real = paths.find((p) => !isLink(p)) ?? paths[0];
|
|
812
|
-
const location = relative2(root, join8(real, "..")).split(
|
|
811
|
+
const location = relative2(root, join8(real, "..")).split(sep4).join("/");
|
|
813
812
|
const source = lock[name]?.source;
|
|
814
813
|
unmanaged.push({
|
|
815
814
|
name,
|
|
@@ -854,9 +853,9 @@ function nestedSkills(root, { depth = 3, enabled = enabledHarnesses() } = {}) {
|
|
|
854
853
|
for (const sub of subs) {
|
|
855
854
|
if (existsSync7(join8(sub, ".git"))) continue;
|
|
856
855
|
for (const [skillsDir, readers] of NESTED_READERS) {
|
|
857
|
-
const location = relative2(root, join8(sub, skillsDir)).split(
|
|
856
|
+
const location = relative2(root, join8(sub, skillsDir)).split(sep4).join("/");
|
|
858
857
|
for (const path of skillDirsIn(join8(sub, skillsDir))) {
|
|
859
|
-
if (isLink(path) && (
|
|
858
|
+
if (isLink(path) && (realpathOrNull(path) ?? "").startsWith((realpathOrNull(root) ?? root) + sep4)) continue;
|
|
860
859
|
const name = basename3(path);
|
|
861
860
|
found.push({
|
|
862
861
|
name,
|
|
@@ -907,7 +906,7 @@ function linkEverywhere(root, name, location, { allowTracked = false, dryRun = f
|
|
|
907
906
|
}
|
|
908
907
|
}
|
|
909
908
|
if (!dryRun) {
|
|
910
|
-
|
|
909
|
+
mkdirSync4(join8(root, dir), { recursive: true });
|
|
911
910
|
linkDir(join8(root, location, name), link2);
|
|
912
911
|
}
|
|
913
912
|
result.created.push(dir);
|
|
@@ -915,12 +914,12 @@ function linkEverywhere(root, name, location, { allowTracked = false, dryRun = f
|
|
|
915
914
|
return result;
|
|
916
915
|
}
|
|
917
916
|
function unlinkEverywhere(root, name, removing) {
|
|
918
|
-
const gone = removing ?
|
|
917
|
+
const gone = removing ? realpathOrNull(removing) : null;
|
|
919
918
|
return ALL_PROJECT_DIRS.filter((dir) => {
|
|
920
919
|
const link2 = join8(root, dir, name);
|
|
921
920
|
if (!isLink(link2)) return false;
|
|
922
921
|
if (dir === GROK_SKILLS_DIR) {
|
|
923
|
-
const target =
|
|
922
|
+
const target = realpathOrNull(link2);
|
|
924
923
|
if (target !== null && target !== gone) return false;
|
|
925
924
|
}
|
|
926
925
|
unlinkSync(link2);
|
|
@@ -960,6 +959,8 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
960
959
|
const to = join8(root, primary, name);
|
|
961
960
|
const local = isLink(to) && !recorded ? null : treeHash(to);
|
|
962
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` };
|
|
963
964
|
if (local !== null && local !== target.hash && !knownContent && !force) {
|
|
964
965
|
return {
|
|
965
966
|
name,
|
|
@@ -969,9 +970,9 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
969
970
|
}
|
|
970
971
|
let backedUp;
|
|
971
972
|
if (local !== target.hash || isLink(to)) {
|
|
972
|
-
|
|
973
|
+
mkdirSync4(join8(root, primary), { recursive: true });
|
|
973
974
|
if (isLink(to)) unlinkSync(to);
|
|
974
|
-
else if (local !== null &&
|
|
975
|
+
else if (local !== null && versionForHash(name, local) === null) backedUp = stash(to, "edit-backup");
|
|
975
976
|
copySkill(versionDir(name, target.version), to);
|
|
976
977
|
}
|
|
977
978
|
const links = [];
|
|
@@ -980,7 +981,7 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
980
981
|
let shared;
|
|
981
982
|
for (const dir of linkDirs) {
|
|
982
983
|
const link2 = join8(root, dir, name);
|
|
983
|
-
if (isLink(link2) &&
|
|
984
|
+
if (isLink(link2) && realpathOrNull(link2) === realpathOrNull(to)) {
|
|
984
985
|
links.push(dir);
|
|
985
986
|
continue;
|
|
986
987
|
}
|
|
@@ -995,7 +996,7 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
995
996
|
continue;
|
|
996
997
|
}
|
|
997
998
|
}
|
|
998
|
-
|
|
999
|
+
mkdirSync4(join8(root, dir), { recursive: true });
|
|
999
1000
|
linkDir(to, link2);
|
|
1000
1001
|
links.push(dir);
|
|
1001
1002
|
}
|
|
@@ -1022,14 +1023,15 @@ function removeSkill(root, name, { force = false } = {}) {
|
|
|
1022
1023
|
if (!recorded) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
|
|
1023
1024
|
const dir = join8(root, recorded.dir ?? PROJECT_SKILLS_DIR, name);
|
|
1024
1025
|
const local = treeHash(dir);
|
|
1025
|
-
const
|
|
1026
|
+
const inLibrary = local !== null && versionForHash(name, local) !== null;
|
|
1027
|
+
const edited = local !== null && local !== recorded.hash && !inLibrary;
|
|
1026
1028
|
if (edited && !force) return { name, action: "skipped", reason: "has local edits (use --force to delete anyway)" };
|
|
1027
1029
|
for (const linkDir2 of recorded.links ?? []) {
|
|
1028
1030
|
const link2 = join8(root, linkDir2, name);
|
|
1029
1031
|
if (isLink(link2)) unlinkSync(link2);
|
|
1030
1032
|
}
|
|
1031
|
-
const backedUp =
|
|
1032
|
-
if (!backedUp)
|
|
1033
|
+
const backedUp = local !== null && !inLibrary ? stash(dir, "edit-backup") : void 0;
|
|
1034
|
+
if (!backedUp) rmSync3(dir, { recursive: true, force: true });
|
|
1033
1035
|
delete manifest.skills[name];
|
|
1034
1036
|
writeManifest(root, manifest);
|
|
1035
1037
|
return { name, action: "removed", ...backedUp ? { backedUp } : {} };
|
|
@@ -1040,23 +1042,41 @@ function syncProject(root, { force = false } = {}) {
|
|
|
1040
1042
|
function syncSkill(root, name, { force = false } = {}) {
|
|
1041
1043
|
const dep = readManifest(root).skills[name];
|
|
1042
1044
|
if (!dep) return [];
|
|
1043
|
-
const
|
|
1044
|
-
const
|
|
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));
|
|
1045
1048
|
if (local === dep.hash && linksOk) return [];
|
|
1049
|
+
if (local === dep.hash) return [relinkPinned(root, name, dep)];
|
|
1046
1050
|
if (local !== null && local !== dep.hash && !force) return [{ name, action: "skipped", reason: "has local edits (use --force to reset them)" }];
|
|
1047
|
-
const
|
|
1048
|
-
if (
|
|
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 });
|
|
1049
1054
|
return [{ ...change, action: local === null ? "installed" : change.action }];
|
|
1050
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
|
+
}
|
|
1051
1068
|
function pinnedVersion(name, dep) {
|
|
1052
|
-
|
|
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`;
|
|
1053
1071
|
}
|
|
1054
1072
|
function discardEdits(root, name) {
|
|
1055
1073
|
const dep = readManifest(root).skills[name];
|
|
1056
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 };
|
|
1057
1077
|
const dir = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
|
|
1058
1078
|
if (existsSync7(dir) && !isLink(dir)) stash(dir, "edit-backup");
|
|
1059
|
-
return addSkill(root, name, { force: true, version
|
|
1079
|
+
return addSkill(root, name, { force: true, version });
|
|
1060
1080
|
}
|
|
1061
1081
|
function updateProject(root, names, { force = false } = {}) {
|
|
1062
1082
|
const { skills } = readManifest(root);
|
|
@@ -1078,7 +1098,7 @@ function importSkill(dir, { force = false } = {}) {
|
|
|
1078
1098
|
const incoming = treeHash(dir);
|
|
1079
1099
|
if (existing === incoming) return { name, status: "unchanged" };
|
|
1080
1100
|
if (existing !== null && !force) return { name, status: "exists" };
|
|
1081
|
-
|
|
1101
|
+
mkdirSync4(libraryDir(), { recursive: true });
|
|
1082
1102
|
const origin = existing !== null && libraryOrigins()[name] ? null : originFor(dir);
|
|
1083
1103
|
copySkill(dir, to);
|
|
1084
1104
|
if (origin) recordOrigin(name, origin);
|
|
@@ -1090,19 +1110,15 @@ function backupIndexFile(bucket) {
|
|
|
1090
1110
|
return join8(skilllibHome(), bucket, "index.json");
|
|
1091
1111
|
}
|
|
1092
1112
|
function readBackupIndex(bucket) {
|
|
1093
|
-
|
|
1094
|
-
return JSON.parse(readFileSync6(backupIndexFile(bucket), "utf-8"));
|
|
1095
|
-
} catch {
|
|
1096
|
-
return {};
|
|
1097
|
-
}
|
|
1113
|
+
return readState(backupIndexFile(bucket)) ?? {};
|
|
1098
1114
|
}
|
|
1099
1115
|
function move(from, to) {
|
|
1100
1116
|
try {
|
|
1101
|
-
|
|
1117
|
+
renameSync2(from, to);
|
|
1102
1118
|
} catch (err) {
|
|
1103
1119
|
if (err.code !== "EXDEV") throw err;
|
|
1104
1120
|
cpSync2(from, to, { recursive: true, verbatimSymlinks: true });
|
|
1105
|
-
|
|
1121
|
+
rmSync3(from, { recursive: true, force: true });
|
|
1106
1122
|
}
|
|
1107
1123
|
}
|
|
1108
1124
|
function stash(dir, bucket) {
|
|
@@ -1111,9 +1127,10 @@ function stash(dir, bucket) {
|
|
|
1111
1127
|
while (entryExists(join8(skilllibHome(), bucket, entryAt(at)))) at++;
|
|
1112
1128
|
const entry = entryAt(at);
|
|
1113
1129
|
const target = join8(skilllibHome(), bucket, entry);
|
|
1114
|
-
|
|
1130
|
+
const index = readBackupIndex(bucket);
|
|
1131
|
+
mkdirSync4(join8(skilllibHome(), bucket), { recursive: true });
|
|
1115
1132
|
move(dir, target);
|
|
1116
|
-
|
|
1133
|
+
writeState(backupIndexFile(bucket), { ...index, [entry]: dir });
|
|
1117
1134
|
return target;
|
|
1118
1135
|
}
|
|
1119
1136
|
function removeUntracked(root, name, path) {
|
|
@@ -1121,7 +1138,7 @@ function removeUntracked(root, name, path) {
|
|
|
1121
1138
|
return stash(path, "trash");
|
|
1122
1139
|
}
|
|
1123
1140
|
function projectsUsing(name) {
|
|
1124
|
-
return knownProjects().filter((p) => name in
|
|
1141
|
+
return knownProjects().filter((p) => name in pinsIn(p));
|
|
1125
1142
|
}
|
|
1126
1143
|
function deleteLibrarySkill(name) {
|
|
1127
1144
|
const using = projectsUsing(name);
|
|
@@ -1150,12 +1167,12 @@ function linkGlobal(path, agents) {
|
|
|
1150
1167
|
const skipped = [];
|
|
1151
1168
|
for (const dir of new Set(agents.map((id) => harness(id).globalDirs()[0]))) {
|
|
1152
1169
|
const link2 = join8(dir, name);
|
|
1153
|
-
if (isLink(link2) &&
|
|
1170
|
+
if (isLink(link2) && realpathOrNull(link2) === null) unlinkSync(link2);
|
|
1154
1171
|
else if (entryExists(link2)) {
|
|
1155
|
-
if (
|
|
1172
|
+
if (realpathOrNull(link2) !== realpathOrNull(path)) skipped.push(link2);
|
|
1156
1173
|
continue;
|
|
1157
1174
|
}
|
|
1158
|
-
|
|
1175
|
+
mkdirSync4(dir, { recursive: true });
|
|
1159
1176
|
linkDir(path, link2);
|
|
1160
1177
|
linked.push(link2);
|
|
1161
1178
|
}
|
|
@@ -1165,8 +1182,8 @@ function createSkill(name, description) {
|
|
|
1165
1182
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) return { ok: false, reason: "use lowercase letters, digits and dashes" };
|
|
1166
1183
|
const dir = librarySkillDir(name);
|
|
1167
1184
|
if (existsSync7(dir)) return { ok: false, reason: `${name} already exists` };
|
|
1168
|
-
|
|
1169
|
-
|
|
1185
|
+
mkdirSync4(dir, { recursive: true });
|
|
1186
|
+
writeFileSync2(
|
|
1170
1187
|
join8(dir, "SKILL.md"),
|
|
1171
1188
|
`---
|
|
1172
1189
|
name: ${name}
|
|
@@ -1190,26 +1207,28 @@ function listBackups() {
|
|
|
1190
1207
|
const match = entry.match(/^(.*)-(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-\d{3}Z$/);
|
|
1191
1208
|
if (!match) return [];
|
|
1192
1209
|
const name = match[1];
|
|
1193
|
-
const from = index[entry] ?? (kind === "trash" ? librarySkillDir(name) : join8(claudeDir(), "skills", name));
|
|
1194
|
-
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 }];
|
|
1195
1212
|
});
|
|
1196
1213
|
}).sort((a, b) => b.path.slice(-24).localeCompare(a.path.slice(-24)));
|
|
1197
1214
|
}
|
|
1198
1215
|
function backupFrom(backup) {
|
|
1199
1216
|
if (backup.kind === "plugin") return "Claude Code plugin";
|
|
1200
|
-
|
|
1217
|
+
if (!backup.from) return "unknown";
|
|
1218
|
+
return backup.from === librarySkillDir(backup.name) ? "library (deleted)" : tildify(dirname3(backup.from));
|
|
1201
1219
|
}
|
|
1202
1220
|
function restoreBackup(backup) {
|
|
1203
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)}` };
|
|
1204
1223
|
if (backup.kind === "tidy-backup" && isLink(to)) unlinkSync(to);
|
|
1205
|
-
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 });
|
|
1206
1225
|
let taken = existsSync7(to);
|
|
1207
1226
|
try {
|
|
1208
1227
|
taken = taken || lstatSync2(to) !== null;
|
|
1209
1228
|
} catch {
|
|
1210
1229
|
}
|
|
1211
1230
|
if (taken) return { ok: false, reason: `${backup.name} already exists there` };
|
|
1212
|
-
|
|
1231
|
+
mkdirSync4(join8(to, ".."), { recursive: true });
|
|
1213
1232
|
move(backup.path, to);
|
|
1214
1233
|
forgetLatest(backup.name);
|
|
1215
1234
|
return { ok: true, to };
|
|
@@ -1218,7 +1237,7 @@ function restoreBackup(backup) {
|
|
|
1218
1237
|
// src/agentSkill.ts
|
|
1219
1238
|
var AGENT_SKILL_NAME = "skilllib";
|
|
1220
1239
|
function shippedSkill() {
|
|
1221
|
-
return
|
|
1240
|
+
return readFileSync4(new URL("../skills/skilllib/SKILL.md", import.meta.url), "utf-8");
|
|
1222
1241
|
}
|
|
1223
1242
|
function agentSkillDirs(enabled = enabledHarnesses()) {
|
|
1224
1243
|
const claudeSkills = join9(claudeDir(), "skills");
|
|
@@ -1244,7 +1263,7 @@ function entryExists2(path) {
|
|
|
1244
1263
|
function agentSkillState(enabled = enabledHarnesses()) {
|
|
1245
1264
|
const [primary, ...links] = agentSkillDirs(enabled);
|
|
1246
1265
|
if (!primary || !isOurs(primary) || links.some((l) => !existsSync8(join9(l, "SKILL.md")))) return "missing";
|
|
1247
|
-
return
|
|
1266
|
+
return readFileSync4(join9(primary, "SKILL.md"), "utf-8") === shippedSkill() ? "installed" : "outdated";
|
|
1248
1267
|
}
|
|
1249
1268
|
function installAgentSkill(enabled = enabledHarnesses()) {
|
|
1250
1269
|
const [primary, ...links] = agentSkillDirs(enabled);
|
|
@@ -1252,12 +1271,12 @@ function installAgentSkill(enabled = enabledHarnesses()) {
|
|
|
1252
1271
|
const foreign = [primary, ...links].find((d) => entryExists2(d) && !isOurs(d) && !isDangling(d));
|
|
1253
1272
|
if (foreign) return { ok: false, reason: `${tildify(foreign)} already exists and isn't skilllib's` };
|
|
1254
1273
|
for (const d of [primary, ...links]) if (isDangling(d)) unlinkSync2(d);
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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");
|
|
1258
1277
|
for (const link2 of links) {
|
|
1259
1278
|
if (entryExists2(link2)) continue;
|
|
1260
|
-
|
|
1279
|
+
mkdirSync5(join9(link2, ".."), { recursive: true });
|
|
1261
1280
|
linkDir(primary, link2);
|
|
1262
1281
|
}
|
|
1263
1282
|
return { ok: true, dirs: [primary, ...links] };
|
|
@@ -1266,8 +1285,9 @@ function removeAgentSkill() {
|
|
|
1266
1285
|
const all = agentSkillDirs(["claude-code", "codex"]);
|
|
1267
1286
|
const removed = all.filter((d) => entryExists2(d) && (isOurs(d) || isDangling(d)));
|
|
1268
1287
|
for (const d of removed) {
|
|
1288
|
+
if (!entryExists2(d)) continue;
|
|
1269
1289
|
if (lstatSync3(d).isSymbolicLink()) unlinkSync2(d);
|
|
1270
|
-
else
|
|
1290
|
+
else rmSync4(d, { recursive: true, force: true });
|
|
1271
1291
|
}
|
|
1272
1292
|
return removed;
|
|
1273
1293
|
}
|
|
@@ -1279,9 +1299,9 @@ function refreshAgentSkill() {
|
|
|
1279
1299
|
}
|
|
1280
1300
|
|
|
1281
1301
|
// src/commands.ts
|
|
1282
|
-
import { existsSync as
|
|
1302
|
+
import { existsSync as existsSync14, readFileSync as readFileSync6 } from "fs";
|
|
1283
1303
|
import { parseArgs as parseFlags } from "util";
|
|
1284
|
-
import { basename as basename9, join as
|
|
1304
|
+
import { basename as basename9, join as join15, resolve as resolve7, sep as sep6 } from "path";
|
|
1285
1305
|
|
|
1286
1306
|
// src/git.ts
|
|
1287
1307
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -1323,24 +1343,17 @@ function relativeTo(root, path) {
|
|
|
1323
1343
|
}
|
|
1324
1344
|
|
|
1325
1345
|
// src/here.ts
|
|
1326
|
-
import { readdirSync as
|
|
1346
|
+
import { readdirSync as readdirSync8 } from "fs";
|
|
1327
1347
|
import { basename as basename7 } from "path";
|
|
1328
1348
|
|
|
1329
1349
|
// src/health.ts
|
|
1330
|
-
import { lstatSync as lstatSync4 } from "fs";
|
|
1331
|
-
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";
|
|
1332
1352
|
|
|
1333
1353
|
// src/plugins.ts
|
|
1334
1354
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1335
|
-
import { existsSync as existsSync10, mkdirSync as
|
|
1336
|
-
import { basename as basename4, dirname as
|
|
1337
|
-
function readJson2(path) {
|
|
1338
|
-
try {
|
|
1339
|
-
return JSON.parse(readFileSync8(path, "utf-8"));
|
|
1340
|
-
} catch {
|
|
1341
|
-
return null;
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
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";
|
|
1344
1357
|
function nonEmptyDir(path) {
|
|
1345
1358
|
try {
|
|
1346
1359
|
return readdirSync5(path).some((f) => !f.startsWith("."));
|
|
@@ -1349,7 +1362,7 @@ function nonEmptyDir(path) {
|
|
|
1349
1362
|
}
|
|
1350
1363
|
}
|
|
1351
1364
|
function extrasOf(root) {
|
|
1352
|
-
const manifest =
|
|
1365
|
+
const manifest = readJson(join11(root, ".claude-plugin", "plugin.json")) ?? {};
|
|
1353
1366
|
return [
|
|
1354
1367
|
...existsSync10(join11(root, ".mcp.json")) || manifest.mcpServers ? ["MCP servers"] : [],
|
|
1355
1368
|
...existsSync10(join11(root, "hooks", "hooks.json")) || manifest.hooks ? ["hooks"] : [],
|
|
@@ -1358,21 +1371,20 @@ function extrasOf(root) {
|
|
|
1358
1371
|
];
|
|
1359
1372
|
}
|
|
1360
1373
|
function claudePlugins(machine, root) {
|
|
1361
|
-
const installed =
|
|
1374
|
+
const installed = installedPluginsJson();
|
|
1362
1375
|
const byOrigin = /* @__PURE__ */ new Map();
|
|
1363
1376
|
for (const m of machine) if (m.kind === "plugin") byOrigin.set(m.origin, [...byOrigin.get(m.origin) ?? [], m.path]);
|
|
1364
1377
|
return [...byOrigin].map(([origin, skills]) => {
|
|
1365
1378
|
const synced = origin.endsWith("@synced");
|
|
1366
|
-
const id = origin;
|
|
1367
1379
|
const entries = installed[origin] ?? [];
|
|
1368
1380
|
const entry = entries.find((e) => root && e.projectPath === root) ?? entries.find((e) => (e.scope ?? "user") === "user") ?? entries[0];
|
|
1369
1381
|
return {
|
|
1370
|
-
id,
|
|
1382
|
+
id: origin,
|
|
1371
1383
|
scope: synced ? null : entry?.scope ?? "user",
|
|
1372
1384
|
...entry?.projectPath ? { projectPath: entry.projectPath } : {},
|
|
1373
1385
|
synced,
|
|
1374
1386
|
skills,
|
|
1375
|
-
extras: extrasOf(
|
|
1387
|
+
extras: extrasOf(dirname4(dirname4(skills[0])))
|
|
1376
1388
|
};
|
|
1377
1389
|
});
|
|
1378
1390
|
}
|
|
@@ -1385,10 +1397,10 @@ function newer(a, b) {
|
|
|
1385
1397
|
}
|
|
1386
1398
|
function updateOf(name, marketplace, entry, catalogs) {
|
|
1387
1399
|
const dir = join11(claudeDir(), "plugins", "marketplaces", marketplace);
|
|
1388
|
-
if (!catalogs.has(marketplace)) catalogs.set(marketplace,
|
|
1400
|
+
if (!catalogs.has(marketplace)) catalogs.set(marketplace, readJson(join11(dir, ".claude-plugin", "marketplace.json"))?.plugins ?? []);
|
|
1389
1401
|
const listed = catalogs.get(marketplace).find((p) => p.name === name);
|
|
1390
1402
|
if (!listed) return void 0;
|
|
1391
|
-
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);
|
|
1392
1404
|
if (version && entry.version) return newer(version, entry.version) ? version : void 0;
|
|
1393
1405
|
const sha = typeof listed.source === "object" ? listed.source?.sha : void 0;
|
|
1394
1406
|
return sha && entry.gitCommitSha && sha !== entry.gitCommitSha ? "newer" : void 0;
|
|
@@ -1398,12 +1410,12 @@ function pluginRoot(id, entry) {
|
|
|
1398
1410
|
return candidates.find((c) => existsSync10(join11(c, "skills"))) ?? candidates.find((c) => existsSync10(c)) ?? candidates[0];
|
|
1399
1411
|
}
|
|
1400
1412
|
function describe(root) {
|
|
1401
|
-
const manifest =
|
|
1413
|
+
const manifest = readJson(join11(root, ".claude-plugin", "plugin.json")) ?? {};
|
|
1402
1414
|
return { description: manifest.description ?? "", version: manifest.version, skills: skillDirsIn(join11(root, "skills")), extras: extrasOf(root) };
|
|
1403
1415
|
}
|
|
1404
1416
|
function installedPlugins() {
|
|
1405
1417
|
const plugins = join11(claudeDir(), "plugins");
|
|
1406
|
-
const installed =
|
|
1418
|
+
const installed = installedPluginsJson();
|
|
1407
1419
|
const catalogs = /* @__PURE__ */ new Map();
|
|
1408
1420
|
const fromMarketplaces = Object.entries(installed).flatMap(
|
|
1409
1421
|
([id, entries]) => entries.map((entry) => {
|
|
@@ -1435,18 +1447,18 @@ function installedPlugins() {
|
|
|
1435
1447
|
const syncedDir = join11(plugins, "synced");
|
|
1436
1448
|
const dirs = (p) => existsSync10(p) ? readdirSync5(p, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => join11(p, d.name)) : [];
|
|
1437
1449
|
const fromSynced = dirs(syncedDir).flatMap(dirs).map((root) => {
|
|
1438
|
-
const id = `${
|
|
1450
|
+
const id = `${readJson(join11(root, ".claude-plugin", "plugin.json"))?.name ?? basename4(root)}@synced`;
|
|
1439
1451
|
return { id, scope: null, synced: true, agent: "claude-code", on: user[id] !== false, root, ...describe(root) };
|
|
1440
1452
|
});
|
|
1441
1453
|
return [...fromMarketplaces, ...fromSettings, ...fromSynced];
|
|
1442
1454
|
}
|
|
1443
1455
|
function cursorPlugins() {
|
|
1444
1456
|
const cache = join11(userHome(), ".cursor", "plugins", "cache");
|
|
1445
|
-
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()) : [];
|
|
1446
1458
|
return dirs(cache).flatMap(
|
|
1447
1459
|
(marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
|
|
1448
1460
|
const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
|
|
1449
|
-
const root = versions.sort((a, b) =>
|
|
1461
|
+
const root = versions.sort((a, b) => statSync4(b).mtimeMs - statSync4(a).mtimeMs)[0];
|
|
1450
1462
|
if (!root) return [];
|
|
1451
1463
|
const about = describe(root);
|
|
1452
1464
|
return [{ id: `${plugin}@${marketplace}`, scope: null, synced: false, agent: "cursor", on: null, root, ...about, version: about.version ?? basename4(root) }];
|
|
@@ -1485,11 +1497,10 @@ function claude(args, cwd, bin = claudeBinary()) {
|
|
|
1485
1497
|
}
|
|
1486
1498
|
}
|
|
1487
1499
|
function setPluginIn(file, id, on) {
|
|
1488
|
-
const settings = existsSync10(file) ?
|
|
1500
|
+
const settings = existsSync10(file) ? readJson(file) : {};
|
|
1489
1501
|
if (!settings) return { ok: false, reason: `can't read ${file}` };
|
|
1490
1502
|
const enabled = settings.enabledPlugins ?? {};
|
|
1491
|
-
|
|
1492
|
-
writeFileSync7(file, JSON.stringify({ ...settings, enabledPlugins: { ...enabled, [id]: on } }, null, 2) + "\n");
|
|
1503
|
+
writeState(file, { ...settings, enabledPlugins: { ...enabled, [id]: on } });
|
|
1493
1504
|
return { ok: true };
|
|
1494
1505
|
}
|
|
1495
1506
|
function ignoreLocalSettings(root) {
|
|
@@ -1512,9 +1523,9 @@ function ignoreLocalSettings(root) {
|
|
|
1512
1523
|
}
|
|
1513
1524
|
try {
|
|
1514
1525
|
const exclude = resolve6(root, git2("rev-parse", "--git-path", "info/exclude"));
|
|
1515
|
-
const before = existsSync10(exclude) ?
|
|
1516
|
-
|
|
1517
|
-
|
|
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}
|
|
1518
1529
|
`);
|
|
1519
1530
|
return ", which git now ignores via .git/info/exclude";
|
|
1520
1531
|
} catch {
|
|
@@ -1582,32 +1593,38 @@ function removePlugin(plugin, how) {
|
|
|
1582
1593
|
if (plugin.scope && plugin.scope !== "user" && !plugin.projectPath) {
|
|
1583
1594
|
return { ok: false, message: `${plugin.id}: installed for one project; run \`claude plugin ${how === "delete" ? "uninstall" : "disable"} ${plugin.id} --scope ${plugin.scope}\` there` };
|
|
1584
1595
|
}
|
|
1585
|
-
|
|
1596
|
+
const viaSettings = plugin.synced || !claudeBinary() && how === "off" && !plugin.projectPath;
|
|
1597
|
+
if (!viaSettings && !claudeBinary()) {
|
|
1586
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)` };
|
|
1587
1599
|
}
|
|
1588
|
-
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);
|
|
1589
1603
|
const scope = plugin.scope ? ["--scope", plugin.scope] : [];
|
|
1590
|
-
const r =
|
|
1604
|
+
const r = viaSettings ? (
|
|
1591
1605
|
// Synced plugins aren't installed, so `claude plugin` can't touch them: their switch is enabledPlugins.
|
|
1592
1606
|
setPluginIn(join11(claudeDir(), "settings.json"), plugin.id, false)
|
|
1593
|
-
) :
|
|
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
|
+
);
|
|
1594
1611
|
const saved = imported.length ? `; ${imported.join(", ")} copied into Your skills` : "";
|
|
1595
1612
|
if (!r.ok) return { ok: false, message: `${plugin.id}: ${r.reason}${saved}` };
|
|
1596
|
-
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);
|
|
1597
1614
|
return { ok: true, message: `${plugin.id} ${how === "delete" && !plugin.synced ? "removed" : "turned off"}${saved}` };
|
|
1598
1615
|
}
|
|
1599
1616
|
function recordRemovedPlugin(id, scope, projectPath, on = true) {
|
|
1600
|
-
const list2 =
|
|
1601
|
-
|
|
1602
|
-
|
|
1617
|
+
const list2 = readState(removedFile()) ?? [];
|
|
1618
|
+
mkdirSync6(skilllibHome(), { recursive: true });
|
|
1619
|
+
writeState(removedFile(), [...list2, { id, scope, ...projectPath ? { projectPath } : {}, on, at: (/* @__PURE__ */ new Date()).toISOString() }]);
|
|
1603
1620
|
}
|
|
1604
1621
|
function pluginBackups() {
|
|
1605
|
-
return (
|
|
1622
|
+
return (readState(removedFile()) ?? []).map((r) => ({
|
|
1606
1623
|
name: r.id,
|
|
1607
1624
|
kind: "plugin",
|
|
1608
1625
|
// Project and local installs go back into their project.
|
|
1609
1626
|
path: r.projectPath ? `${r.scope}:${r.projectPath}` : r.scope,
|
|
1610
|
-
movedAt: r.at
|
|
1627
|
+
movedAt: localStamp(Date.parse(r.at)),
|
|
1611
1628
|
from: r.id
|
|
1612
1629
|
}));
|
|
1613
1630
|
}
|
|
@@ -1616,11 +1633,11 @@ function restorePlugin(backup) {
|
|
|
1616
1633
|
const projectPath = rest.join(":") || void 0;
|
|
1617
1634
|
const r = claude(["plugin", "install", backup.from, "--scope", scope], projectPath);
|
|
1618
1635
|
if (!r.ok) return r;
|
|
1619
|
-
const records =
|
|
1636
|
+
const records = readState(removedFile()) ?? [];
|
|
1620
1637
|
const same = (x) => x.id === backup.from && (x.projectPath ?? void 0) === projectPath && x.scope === scope;
|
|
1621
1638
|
const wasOff = records.filter(same).pop()?.on === false;
|
|
1622
1639
|
const off = wasOff ? claude(["plugin", "disable", backup.from, "--scope", scope], projectPath) : null;
|
|
1623
|
-
|
|
1640
|
+
writeState(removedFile(), records.filter((x) => !same(x)));
|
|
1624
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" : ""})`;
|
|
1625
1642
|
return { ok: true, to: where };
|
|
1626
1643
|
}
|
|
@@ -1629,14 +1646,14 @@ function cursorPluginSkills(plugins = cursorPlugins()) {
|
|
|
1629
1646
|
}
|
|
1630
1647
|
|
|
1631
1648
|
// src/tidy.ts
|
|
1632
|
-
import { existsSync as existsSync11, mkdirSync as
|
|
1633
|
-
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";
|
|
1634
1651
|
function entriesOf(dirs, name) {
|
|
1635
1652
|
return dirs.flatMap((dir) => {
|
|
1636
1653
|
const path = join12(dir, name);
|
|
1637
1654
|
if (!entryExists(path)) return [];
|
|
1638
1655
|
const link2 = isLink(path);
|
|
1639
|
-
const real =
|
|
1656
|
+
const real = realpathOrNull(path);
|
|
1640
1657
|
const skill = existsSync11(join12(path, "SKILL.md"));
|
|
1641
1658
|
if (!link2 && !skill) return [];
|
|
1642
1659
|
return [{ dir, path, link: link2, real, hash: null }];
|
|
@@ -1661,7 +1678,7 @@ function globalTidyDirs() {
|
|
|
1661
1678
|
function runsFrom(id, dirsWithCopy, readsOf) {
|
|
1662
1679
|
const reads = readsOf(id).filter((d) => dirsWithCopy.includes(d));
|
|
1663
1680
|
if (id === "codex") return reads;
|
|
1664
|
-
const pick = (d) => CURSOR_PICKS.indexOf(basename5(
|
|
1681
|
+
const pick = (d) => CURSOR_PICKS.indexOf(basename5(dirname5(d)));
|
|
1665
1682
|
return id === "cursor" ? [...reads].sort((a, b) => pick(a) - pick(b)).slice(0, 1) : reads.slice(0, 1);
|
|
1666
1683
|
}
|
|
1667
1684
|
function conflictOf(name, root, reals, enabled, readsOf) {
|
|
@@ -1677,17 +1694,17 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1677
1694
|
const { skills } = readManifest(root);
|
|
1678
1695
|
const abs = (dir) => join12(root, dir);
|
|
1679
1696
|
const rel = (dir) => relativeTo(root, dir);
|
|
1680
|
-
const readsOf = (id) =>
|
|
1697
|
+
const readsOf = (id) => harness(id).projectDirs.map(abs);
|
|
1681
1698
|
let git2;
|
|
1682
1699
|
const skillsLock = projectSkillsLock(root);
|
|
1683
1700
|
for (const name of namesIn(PROJECT_TIDY_DIRS.map(abs))) {
|
|
1684
1701
|
const entries = entriesOf(PROJECT_TIDY_DIRS.map(abs), name);
|
|
1685
|
-
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));
|
|
1686
1703
|
if (external) {
|
|
1687
1704
|
report.skipped.push({ name, reason: `${rel(external.path)} links outside the repo` });
|
|
1688
1705
|
continue;
|
|
1689
1706
|
}
|
|
1690
|
-
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);
|
|
1691
1708
|
if (reals.length === 0) continue;
|
|
1692
1709
|
hashReals(reals);
|
|
1693
1710
|
const dep = skills[name];
|
|
@@ -1713,10 +1730,11 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1713
1730
|
const wanted = new Set([...installDirs(enabled, rel(primary.dir)), ...dep?.links ?? []].map(abs));
|
|
1714
1731
|
const steps = [];
|
|
1715
1732
|
for (const e of entries) {
|
|
1716
|
-
if (e === primary) continue;
|
|
1733
|
+
if (e === primary || !e.link && e.real === primary.real) continue;
|
|
1717
1734
|
const needed = wanted.has(e.dir);
|
|
1718
1735
|
if (e.link) {
|
|
1719
1736
|
if (e.real === primary.real) {
|
|
1737
|
+
if (!needed && !dep && ["committed", "changed"].includes((git2 ??= gitInfo(root))?.of(rel(e.path)) ?? "")) continue;
|
|
1720
1738
|
if (!needed) steps.push({ kind: "unlink", path: e.path });
|
|
1721
1739
|
} else steps.push(needed ? { kind: "link", path: e.path, target: primary.path } : { kind: "unlink", path: e.path });
|
|
1722
1740
|
} else {
|
|
@@ -1729,7 +1747,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1729
1747
|
return report;
|
|
1730
1748
|
}
|
|
1731
1749
|
function globalReadsOf(id) {
|
|
1732
|
-
return
|
|
1750
|
+
return harness(id).globalDirs().map(globalDirAs);
|
|
1733
1751
|
}
|
|
1734
1752
|
function isSkillsDirPlugin(path) {
|
|
1735
1753
|
return existsSync11(join12(path, ".claude-plugin", "plugin.json"));
|
|
@@ -1744,7 +1762,7 @@ function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
|
|
|
1744
1762
|
report.skipped.push({ name, reason: "a Claude Code plugin kept in a skills folder" });
|
|
1745
1763
|
continue;
|
|
1746
1764
|
}
|
|
1747
|
-
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);
|
|
1748
1766
|
if (reals.length < 2) continue;
|
|
1749
1767
|
hashReals(reals);
|
|
1750
1768
|
const kept = reals.find((r) => r.dir === keep[name]);
|
|
@@ -1770,11 +1788,12 @@ function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
|
|
|
1770
1788
|
const applied = [];
|
|
1771
1789
|
for (const step of plan.steps) {
|
|
1772
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;
|
|
1773
1792
|
if (step.kind === "link" || step.kind === "unlink") {
|
|
1774
1793
|
if (isLink(step.path)) unlinkSync3(step.path);
|
|
1775
1794
|
} else stash(step.path, "tidy-backup");
|
|
1776
1795
|
if (step.kind === "link" || step.kind === "replace") {
|
|
1777
|
-
|
|
1796
|
+
mkdirSync7(dirname5(step.path), { recursive: true });
|
|
1778
1797
|
linkDir(step.target, step.path);
|
|
1779
1798
|
}
|
|
1780
1799
|
applied.push(step);
|
|
@@ -1782,7 +1801,7 @@ function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
|
|
|
1782
1801
|
if (plan.root) {
|
|
1783
1802
|
const manifest = readManifest(plan.root);
|
|
1784
1803
|
const dep = manifest.skills[plan.name];
|
|
1785
|
-
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)));
|
|
1786
1805
|
if (dep && added.length) {
|
|
1787
1806
|
manifest.skills[plan.name] = { ...dep, links: [.../* @__PURE__ */ new Set([...dep.links ?? [], ...added])] };
|
|
1788
1807
|
writeManifest(plan.root, manifest);
|
|
@@ -1826,42 +1845,135 @@ function orThrow(r) {
|
|
|
1826
1845
|
if (!r.ok) throw new Error(r.message);
|
|
1827
1846
|
return r.message;
|
|
1828
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
|
+
}
|
|
1829
1866
|
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1830
|
-
var
|
|
1867
|
+
var rows = (root, skills) => skills.map((skill) => ({ root, skill }));
|
|
1868
|
+
var agentNames = (ids) => ids.map((id) => harness(id).name).join(", ");
|
|
1831
1869
|
function conflictIssue(c) {
|
|
1832
1870
|
const where = c.root ? basename6(c.root) : "your global folders";
|
|
1833
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
|
+
};
|
|
1834
1879
|
return {
|
|
1835
1880
|
id: `conflict:${c.root ?? "global"}:${c.name}`,
|
|
1836
1881
|
severity: "problem",
|
|
1837
1882
|
title: `${c.name} in ${where}: the copies differ`,
|
|
1838
|
-
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
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
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
|
+
})
|
|
1849
1895
|
};
|
|
1850
1896
|
}
|
|
1851
1897
|
function applyAll(plans, git2) {
|
|
1852
1898
|
const results = plans.map((p) => applyTidy(p, { git: git2 }));
|
|
1853
1899
|
return { skills: results.filter((r) => r.applied.length).length, held: results.reduce((n, r) => n + r.held.length, 0) };
|
|
1854
1900
|
}
|
|
1855
|
-
function
|
|
1856
|
-
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
|
+
}));
|
|
1857
1967
|
const statuses = new Map(projects2.map((root) => [root, statusOf(root)]));
|
|
1968
|
+
issues.push(...grokDiffers(projects2, machine, (root) => statuses.get(root)));
|
|
1858
1969
|
const nested = new Map(projects2.map((root) => [root, nestedOf(root)]));
|
|
1859
1970
|
for (const skill of machine.filter((m) => m.broken)) {
|
|
1860
1971
|
issues.push({
|
|
1861
|
-
id: `broken:${skill.
|
|
1972
|
+
id: `broken:${skill.path}`,
|
|
1862
1973
|
severity: "problem",
|
|
1863
|
-
title: `${skill.name}: broken link in ${tildify(
|
|
1974
|
+
title: `${skill.name}: broken link in ${tildify(dirname6(skill.path))}`,
|
|
1864
1975
|
detail: "It points at a folder that no longer exists, so it loads nothing.",
|
|
1976
|
+
at: rows(null, [skill.name]),
|
|
1865
1977
|
fix: {
|
|
1866
1978
|
label: "Remove the link",
|
|
1867
1979
|
run: () => {
|
|
@@ -1884,7 +1996,8 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1884
1996
|
id: `dup:${name}`,
|
|
1885
1997
|
severity: "problem",
|
|
1886
1998
|
title: `${name}: loaded twice in ${harnesses.join(" and ")}`,
|
|
1887
|
-
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])
|
|
1888
2001
|
});
|
|
1889
2002
|
}
|
|
1890
2003
|
const loadedHere = new Map(projects2.map((root) => [root, loadedIn(root)]));
|
|
@@ -1929,7 +2042,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1929
2042
|
const pluginName = plugin.id.split("@")[0];
|
|
1930
2043
|
const remove2 = {
|
|
1931
2044
|
label: `Remove the plugin ${plugin.id}`,
|
|
1932
|
-
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",
|
|
1933
2046
|
run: () => orThrow(removePlugin(plugin, "delete"))
|
|
1934
2047
|
};
|
|
1935
2048
|
const off = { label: `Turn the plugin ${plugin.id} off`, hint: "stays installed", run: () => orThrow(removePlugin(plugin, "off")) };
|
|
@@ -1950,9 +2063,25 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1950
2063
|
plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : "",
|
|
1951
2064
|
inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) => basename6(root)).join(", ")}.` : ""
|
|
1952
2065
|
].filter(Boolean).join(" "),
|
|
1953
|
-
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
|
|
1954
2070
|
});
|
|
1955
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
|
+
}
|
|
1956
2085
|
const machinePluginSkills = new Set(machinePlugins.flatMap((p) => p.skills));
|
|
1957
2086
|
for (const root of projects2) {
|
|
1958
2087
|
const where = basename6(root);
|
|
@@ -1967,7 +2096,9 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1967
2096
|
severity: "problem",
|
|
1968
2097
|
title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}, on in ${where}`,
|
|
1969
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.`,
|
|
1970
|
-
choices: repoChoices(plugin, root, inRepo)
|
|
2099
|
+
choices: repoChoices(plugin, root, inRepo),
|
|
2100
|
+
at: rows(root, dupes),
|
|
2101
|
+
plugin: plugin.id
|
|
1971
2102
|
});
|
|
1972
2103
|
}
|
|
1973
2104
|
}
|
|
@@ -1979,7 +2110,9 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1979
2110
|
id: `cursor-plugin:${plugin}`,
|
|
1980
2111
|
severity: "suggestion",
|
|
1981
2112
|
title: `${names.join(", ")}: also in the Cursor plugin ${plugin}`,
|
|
1982
|
-
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
|
|
1983
2116
|
});
|
|
1984
2117
|
}
|
|
1985
2118
|
}
|
|
@@ -1989,7 +2122,8 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1989
2122
|
id: "tidy:global",
|
|
1990
2123
|
severity: "suggestion",
|
|
1991
2124
|
title: `${plural(global.plans.length, "global skill")} with duplicate copies`,
|
|
1992
|
-
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)),
|
|
1993
2127
|
fix: {
|
|
1994
2128
|
label: "Keep one copy of each",
|
|
1995
2129
|
run: () => {
|
|
@@ -2002,13 +2136,14 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2002
2136
|
}
|
|
2003
2137
|
issues.push(...global.conflicts.map(conflictIssue));
|
|
2004
2138
|
const kept = keptGlobal();
|
|
2005
|
-
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))];
|
|
2006
2140
|
if (unreviewed.length) {
|
|
2007
2141
|
issues.push({
|
|
2008
2142
|
id: "review:global",
|
|
2009
2143
|
severity: "suggestion",
|
|
2010
2144
|
title: `${plural(unreviewed.length, "global skill")} of yours ${unreviewed.length === 1 ? "loads" : "load"} in every repo`,
|
|
2011
|
-
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)
|
|
2012
2147
|
});
|
|
2013
2148
|
}
|
|
2014
2149
|
const enabled = enabledHarnesses();
|
|
@@ -2035,6 +2170,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2035
2170
|
severity: "suggestion",
|
|
2036
2171
|
title: `${plural(skills.length, "global skill")}${onPurpose ? " you keep global" : ""} not usable by ${agentNames(missing)}`,
|
|
2037
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))]),
|
|
2038
2174
|
...onPurpose ? { fix: link2 } : { choices: [link2] }
|
|
2039
2175
|
});
|
|
2040
2176
|
}
|
|
@@ -2044,22 +2180,30 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2044
2180
|
for (const root of projects2) {
|
|
2045
2181
|
const where = basename6(root);
|
|
2046
2182
|
const status2 = statuses.get(root);
|
|
2047
|
-
|
|
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
|
+
});
|
|
2048
2197
|
if (restorable.length > 0) {
|
|
2049
2198
|
issues.push({
|
|
2050
2199
|
id: `sync:${root}`,
|
|
2051
2200
|
severity: "problem",
|
|
2052
2201
|
title: `${where}: ${plural(restorable.length, "skill")} in skilllib.json not installed`,
|
|
2053
2202
|
detail: `${restorable.map((s) => s.name).join(", ")}: the folder is missing`,
|
|
2203
|
+
at: rows(root, restorable.map((s) => s.name)),
|
|
2054
2204
|
fix: {
|
|
2055
2205
|
label: "Sync the project",
|
|
2056
|
-
run: () =>
|
|
2057
|
-
const changes = syncProject(root);
|
|
2058
|
-
const done = changes.filter((c) => c.action !== "skipped");
|
|
2059
|
-
const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
|
|
2060
|
-
if (!done.length) throw new Error(`${where}: nothing restored${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
|
|
2061
|
-
return `${where}: restored ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
|
|
2062
|
-
}
|
|
2206
|
+
run: () => summarize(where, "restored", syncProject(root))
|
|
2063
2207
|
}
|
|
2064
2208
|
});
|
|
2065
2209
|
}
|
|
@@ -2069,6 +2213,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2069
2213
|
severity: "problem",
|
|
2070
2214
|
title: `${s.name}: pinned in ${where}'s skilllib.json, but not in your library`,
|
|
2071
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]),
|
|
2072
2217
|
// Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
|
|
2073
2218
|
choices: [
|
|
2074
2219
|
{
|
|
@@ -2089,15 +2234,10 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2089
2234
|
severity: "suggestion",
|
|
2090
2235
|
title: `${where}: ${plural(behind.length, "skill")} out of date`,
|
|
2091
2236
|
detail: behind.map((s) => `${s.name} (v${s.version} \u2192 v${s.latest})`).join(", "),
|
|
2237
|
+
at: rows(root, behind.map((s) => s.name)),
|
|
2092
2238
|
fix: {
|
|
2093
2239
|
label: "Update to the newest versions",
|
|
2094
|
-
run: () =>
|
|
2095
|
-
const changes = updateProject(root, behind.map((s) => s.name));
|
|
2096
|
-
const done = changes.filter((c) => c.action !== "skipped");
|
|
2097
|
-
const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
|
|
2098
|
-
if (!done.length) throw new Error(`${where}: nothing updated${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
|
|
2099
|
-
return `${where}: updated ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
|
|
2100
|
-
}
|
|
2240
|
+
run: () => summarize(where, "updated", updateProject(root, behind.map((s) => s.name)))
|
|
2101
2241
|
}
|
|
2102
2242
|
});
|
|
2103
2243
|
const tidy2 = planProjectTidy(root);
|
|
@@ -2113,40 +2253,45 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2113
2253
|
id: `tidy:${root}`,
|
|
2114
2254
|
severity: "suggestion",
|
|
2115
2255
|
title: `${where}: ${plural(tidy2.plans.length, "skill")} with extra copies or links`,
|
|
2116
|
-
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)),
|
|
2117
2258
|
...visible2.length ? {
|
|
2118
2259
|
choices: [
|
|
2119
|
-
|
|
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") }] : [],
|
|
2120
2262
|
{ label: "Tidy everything", hint: `${plural(visible2.length, "change")} will show in git status`, run: run("go") }
|
|
2121
2263
|
]
|
|
2122
2264
|
} : { fix: { label: "Tidy", run: run("go") } }
|
|
2123
2265
|
});
|
|
2124
2266
|
}
|
|
2125
2267
|
issues.push(...tidy2.conflicts.map(conflictIssue));
|
|
2126
|
-
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 }) }));
|
|
2127
2270
|
for (const { s, would } of blindHere.filter(({ would: w }) => !w.created.length && !w.blocked.length && w.uncommitted.length))
|
|
2128
2271
|
issues.push({
|
|
2129
2272
|
id: `uncommitted:${root}:${s.name}`,
|
|
2130
2273
|
severity: "suggestion",
|
|
2131
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`,
|
|
2132
|
-
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])
|
|
2133
2277
|
});
|
|
2134
2278
|
const partial = blindHere.filter(({ would }) => would.created.length || would.blocked.length).map(({ s }) => s);
|
|
2135
2279
|
if (partial.length) {
|
|
2136
|
-
const
|
|
2280
|
+
const missing2 = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
|
|
2137
2281
|
const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
|
|
2138
2282
|
const run = (allowTracked) => () => {
|
|
2139
2283
|
if (allowTracked) allowTrackedLinks(root);
|
|
2140
2284
|
const r = linkAll(root, { allowTracked });
|
|
2141
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)` : ""}`;
|
|
2142
2286
|
if (!r.linked.length) throw new Error(`${where}: nothing linked${notes}`);
|
|
2143
|
-
return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(
|
|
2287
|
+
return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing2)}${notes}`;
|
|
2144
2288
|
};
|
|
2145
2289
|
issues.push({
|
|
2146
2290
|
id: `usable:${root}`,
|
|
2147
2291
|
severity: "suggestion",
|
|
2148
|
-
title: `${where}: ${plural(partial.length, "skill")} not usable by ${agentNames(
|
|
2292
|
+
title: `${where}: ${plural(partial.length, "skill")} not usable by ${agentNames(missing2)}`,
|
|
2149
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)),
|
|
2150
2295
|
...tracked.length ? {
|
|
2151
2296
|
choices: [
|
|
2152
2297
|
// Only when some link goes where git doesn't track: otherwise this one would link nothing.
|
|
@@ -2167,12 +2312,13 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2167
2312
|
severity: "suggestion",
|
|
2168
2313
|
title: `${s.name} in ${where} isn't in your library`,
|
|
2169
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]),
|
|
2170
2316
|
choices: [
|
|
2171
2317
|
{
|
|
2172
2318
|
label: "Copy into the library",
|
|
2173
2319
|
hint: `${where}'s copy stays as it is`,
|
|
2174
2320
|
run: () => {
|
|
2175
|
-
|
|
2321
|
+
intoLibrary(s.path);
|
|
2176
2322
|
return `${s.name} copied into your library; ${where}'s copy stays as it is`;
|
|
2177
2323
|
}
|
|
2178
2324
|
}
|
|
@@ -2184,13 +2330,14 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2184
2330
|
severity: "suggestion",
|
|
2185
2331
|
title: `${s.name} in ${where} isn't in your library`,
|
|
2186
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]),
|
|
2187
2334
|
choices: [
|
|
2188
2335
|
{
|
|
2189
2336
|
label: "Import into the library",
|
|
2190
|
-
hint:
|
|
2337
|
+
hint: "copies it into Your skills",
|
|
2191
2338
|
run: () => {
|
|
2192
|
-
|
|
2193
|
-
|
|
2339
|
+
intoLibrary(s.path);
|
|
2340
|
+
track(root, s.name);
|
|
2194
2341
|
return `${s.name} imported and tracked in ${where}`;
|
|
2195
2342
|
}
|
|
2196
2343
|
}
|
|
@@ -2202,9 +2349,81 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2202
2349
|
severity: "suggestion",
|
|
2203
2350
|
title: `${s.name} in ${where} isn't tracked`,
|
|
2204
2351
|
detail: "It's identical to the library copy; track it (in skilllib.json) so future library changes reach it.",
|
|
2205
|
-
|
|
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
|
+
]
|
|
2206
2407
|
});
|
|
2207
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
|
+
});
|
|
2208
2427
|
if (s.state === "edited locally" || s.state === "edited locally, update available") {
|
|
2209
2428
|
const newer2 = s.state !== "edited locally";
|
|
2210
2429
|
const pinned = s.version ? `v${s.version}` : "the library version";
|
|
@@ -2213,6 +2432,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2213
2432
|
severity: "suggestion",
|
|
2214
2433
|
title: `${s.name} in ${where} has local edits`,
|
|
2215
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]),
|
|
2216
2436
|
choices: [
|
|
2217
2437
|
{
|
|
2218
2438
|
label: "Save as a new version in Your skills",
|
|
@@ -2226,7 +2446,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2226
2446
|
},
|
|
2227
2447
|
{
|
|
2228
2448
|
label: "Discard the edits",
|
|
2229
|
-
hint: `back to ${pinned}; the edited copy is restorable
|
|
2449
|
+
hint: `back to ${pinned}; the edited copy is restorable with skilllib restore`,
|
|
2230
2450
|
run: () => {
|
|
2231
2451
|
const c = discardEdits(root, s.name);
|
|
2232
2452
|
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
@@ -2242,13 +2462,14 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2242
2462
|
for (const root of projects2) {
|
|
2243
2463
|
for (const n of nested.get(root)) {
|
|
2244
2464
|
const copies = alsoGlobal.get(n.name) ?? [];
|
|
2245
|
-
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 }]);
|
|
2246
2466
|
}
|
|
2247
2467
|
}
|
|
2248
2468
|
for (const [name, copies] of alsoGlobal) {
|
|
2249
2469
|
const wheres = copies.map((c) => basename6(c.root)).join(", ");
|
|
2250
2470
|
const removable = copies.filter((c) => c.managed).map((c) => c.root);
|
|
2251
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])];
|
|
2252
2473
|
if (kept.has(name)) {
|
|
2253
2474
|
const removeWheres = removable.map((r) => basename6(r)).join(", ");
|
|
2254
2475
|
issues.push({
|
|
@@ -2256,13 +2477,15 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2256
2477
|
severity: "suggestion",
|
|
2257
2478
|
title: `${name}: in ${wheres}, and you keep it global`,
|
|
2258
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,
|
|
2259
2481
|
choices: removable.length ? [
|
|
2260
2482
|
{
|
|
2261
2483
|
label: `Remove the copies in ${removeWheres}`,
|
|
2262
2484
|
hint: "the global copy stays",
|
|
2263
2485
|
run: () => {
|
|
2264
2486
|
const removed = removable.filter((root) => removeSkill(root, name).action === "removed").map((r) => basename6(r));
|
|
2265
|
-
|
|
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`;
|
|
2266
2489
|
}
|
|
2267
2490
|
}
|
|
2268
2491
|
] : void 0
|
|
@@ -2271,18 +2494,23 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2271
2494
|
}
|
|
2272
2495
|
const movable = machine.find((m) => m.name === name && m.movable);
|
|
2273
2496
|
issues.push({
|
|
2497
|
+
at,
|
|
2274
2498
|
id: `twice:${name}`,
|
|
2275
2499
|
severity: "suggestion",
|
|
2276
2500
|
title: `${name}: in ${wheres} and also loaded globally`,
|
|
2277
|
-
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.`}`,
|
|
2278
2502
|
choices: movable ? [
|
|
2279
2503
|
{
|
|
2280
2504
|
label: "Stop loading it globally",
|
|
2281
|
-
hint: "restorable
|
|
2505
|
+
hint: "restorable with skilllib restore",
|
|
2282
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)`);
|
|
2283
2509
|
if (!libraryNames.has(name)) importSkill(movable.path);
|
|
2284
|
-
const
|
|
2285
|
-
|
|
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
|
+
}
|
|
2286
2514
|
return `${name} no longer loads globally`;
|
|
2287
2515
|
}
|
|
2288
2516
|
}
|
|
@@ -2305,6 +2533,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2305
2533
|
severity: "suggestion",
|
|
2306
2534
|
title: agentSkill2 === "missing" ? "Your agents don't know about skilllib" : "The skilllib skill for your agents is out of date",
|
|
2307
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"]),
|
|
2308
2537
|
...agentSkill2 === "missing" ? { choices: [install] } : { fix: install }
|
|
2309
2538
|
});
|
|
2310
2539
|
}
|
|
@@ -2332,6 +2561,7 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
|
|
|
2332
2561
|
severity: "suggestion",
|
|
2333
2562
|
title: `${where}: ${plural(unused.length, "skill")} unused in ${uses.days} days`,
|
|
2334
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)),
|
|
2335
2565
|
choices: removable.length ? [
|
|
2336
2566
|
{
|
|
2337
2567
|
label: `Remove ${removable.length === unused.length ? "them" : `the ${plural(removable.length, "skill")} skilllib installed`} from ${where}`,
|
|
@@ -2340,12 +2570,30 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
|
|
|
2340
2570
|
const changes = removable.map((s) => removeSkill(root, s.name));
|
|
2341
2571
|
const removed = changes.filter((c) => c.action === "removed").map((c) => c.name);
|
|
2342
2572
|
const kept = changes.filter((c) => c.action !== "removed").map((c) => c.name);
|
|
2343
|
-
|
|
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)` : ""}`;
|
|
2344
2575
|
}
|
|
2345
2576
|
}
|
|
2346
2577
|
] : void 0
|
|
2347
2578
|
});
|
|
2348
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
|
+
}
|
|
2349
2597
|
const installed = /* @__PURE__ */ new Set([...projects2.flatMap((root) => statusOf(root).map((s) => s.name)), ...machine.filter((m) => !m.broken).map((m) => m.name)]);
|
|
2350
2598
|
const idle = [...libraryNames].filter((name) => {
|
|
2351
2599
|
const first = versionHistory(name)[0];
|
|
@@ -2357,13 +2605,16 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
|
|
|
2357
2605
|
severity: "suggestion",
|
|
2358
2606
|
title: `${plural(idle.length, "skill")} in Your skills: in no repo, unused in ${uses.days} days`,
|
|
2359
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),
|
|
2360
2610
|
choices: [
|
|
2361
2611
|
{
|
|
2362
2612
|
label: `Delete ${idle.length === 1 ? "it" : `all ${idle.length}`} from Your skills`,
|
|
2363
|
-
hint: "restorable
|
|
2613
|
+
hint: "restorable with skilllib restore",
|
|
2364
2614
|
run: () => {
|
|
2365
2615
|
const done = idle.map((name) => [name, deleteLibrarySkill(name)]);
|
|
2366
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(", ")}`);
|
|
2367
2618
|
return `Deleted ${plural(done.length - failed.length, "skill")} from Your skills${failed.length ? `; kept ${failed.join(", ")}` : ""}`;
|
|
2368
2619
|
}
|
|
2369
2620
|
}
|
|
@@ -2391,7 +2642,7 @@ function globalFrom(kind, origin, path) {
|
|
|
2391
2642
|
var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "target", "vendor", "coverage"]);
|
|
2392
2643
|
function topLevel(root) {
|
|
2393
2644
|
try {
|
|
2394
|
-
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);
|
|
2395
2646
|
} catch {
|
|
2396
2647
|
return [];
|
|
2397
2648
|
}
|
|
@@ -2407,7 +2658,7 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2407
2658
|
const group = (list2) => groupBy(
|
|
2408
2659
|
list2.map((s) => {
|
|
2409
2660
|
const loadedBy = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
|
|
2410
|
-
const behind = s.
|
|
2661
|
+
const behind = s.state.endsWith("update available");
|
|
2411
2662
|
const gitState = git2?.of(relativeTo(root, s.path));
|
|
2412
2663
|
const repo = !s.managed && (s.state.startsWith("repo skill") || gitState === "committed" || gitState === "changed");
|
|
2413
2664
|
const usual = s.state === "ok" || s.state === "local only" || repo && s.state !== "from npx skills";
|
|
@@ -2445,7 +2696,8 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2445
2696
|
const usesHere = Object.fromEntries([...uses].filter(([, n]) => n > 0));
|
|
2446
2697
|
return {
|
|
2447
2698
|
project: tildify(root),
|
|
2448
|
-
files
|
|
2699
|
+
// Outside a repo there are no project files to read (and the folder may be your home).
|
|
2700
|
+
files: inRepo ? topLevel(root) : [],
|
|
2449
2701
|
agents,
|
|
2450
2702
|
skills,
|
|
2451
2703
|
nested: nested.length > 0 ? nested : void 0,
|
|
@@ -2460,7 +2712,7 @@ function firstSentence(description) {
|
|
|
2460
2712
|
return sentence.length > 200 ? sentence.slice(0, 199).trimEnd() + "\u2026" : sentence;
|
|
2461
2713
|
}
|
|
2462
2714
|
function libraryFor(currentProject) {
|
|
2463
|
-
const manifests = visibleProjects().map((p) => ({ name: basename7(p), skills:
|
|
2715
|
+
const manifests = visibleProjects().map((p) => ({ name: basename7(p), skills: pinsIn(p) }));
|
|
2464
2716
|
const here = currentProject ? new Set(projectStatus(currentProject).map((s) => s.name)) : null;
|
|
2465
2717
|
const all = librarySkills();
|
|
2466
2718
|
const describe2 = (s) => {
|
|
@@ -2472,8 +2724,8 @@ function libraryFor(currentProject) {
|
|
|
2472
2724
|
}
|
|
2473
2725
|
|
|
2474
2726
|
// src/usage.ts
|
|
2475
|
-
import { createReadStream, existsSync as
|
|
2476
|
-
import { join as
|
|
2727
|
+
import { createReadStream, existsSync as existsSync13, readdirSync as readdirSync9, statSync as statSync5 } from "fs";
|
|
2728
|
+
import { join as join14 } from "path";
|
|
2477
2729
|
import { createInterface } from "readline";
|
|
2478
2730
|
var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
|
|
2479
2731
|
"add-dir",
|
|
@@ -2553,12 +2805,12 @@ function usesFromTranscriptLine(line) {
|
|
|
2553
2805
|
return [];
|
|
2554
2806
|
}
|
|
2555
2807
|
function transcriptFiles(sinceMs) {
|
|
2556
|
-
const base =
|
|
2557
|
-
if (!
|
|
2558
|
-
return
|
|
2559
|
-
const dir =
|
|
2560
|
-
if (!
|
|
2561
|
-
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);
|
|
2562
2814
|
});
|
|
2563
2815
|
}
|
|
2564
2816
|
async function scanUsage(days) {
|
|
@@ -2574,7 +2826,7 @@ async function scanUsage(days) {
|
|
|
2574
2826
|
}
|
|
2575
2827
|
return [...seen.values()];
|
|
2576
2828
|
}
|
|
2577
|
-
function
|
|
2829
|
+
function summarize2(uses, projectOf = () => null) {
|
|
2578
2830
|
const bySkill = /* @__PURE__ */ new Map();
|
|
2579
2831
|
for (const use of uses) {
|
|
2580
2832
|
const skill = use.skill.split(":").pop();
|
|
@@ -2601,7 +2853,12 @@ function usesByProject(uses, projectOf) {
|
|
|
2601
2853
|
|
|
2602
2854
|
// src/move.ts
|
|
2603
2855
|
import { basename as basename8 } from "path";
|
|
2604
|
-
function moveGlobalSkills(skills, roots, {
|
|
2856
|
+
function moveGlobalSkills(skills, roots, {
|
|
2857
|
+
group,
|
|
2858
|
+
allowTracked = false,
|
|
2859
|
+
label = basename8,
|
|
2860
|
+
allowBlind = false
|
|
2861
|
+
} = {}) {
|
|
2605
2862
|
const byName = /* @__PURE__ */ new Map();
|
|
2606
2863
|
for (const m of skills) byName.set(m.name, [...byName.get(m.name) ?? [], m]);
|
|
2607
2864
|
return [...byName].map(([name, copies]) => {
|
|
@@ -2612,7 +2869,14 @@ function moveGlobalSkills(skills, roots, { group, allowTracked = false, label =
|
|
|
2612
2869
|
if (group) recordOrigin(name, `group: ${group}`);
|
|
2613
2870
|
const changes = roots.map((root) => [root, addSkill(root, name, { allowTracked })]);
|
|
2614
2871
|
const skipped = changes.filter(([, c]) => c.action === "skipped");
|
|
2615
|
-
|
|
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
|
+
}
|
|
2616
2880
|
const failed = copies.map((m) => unloadGlobal(m.path, m.links)).find((r) => !r.ok);
|
|
2617
2881
|
return failed && !failed.ok ? { name, moved: false, reason: failed.reason, changes } : { name, moved: true, changes };
|
|
2618
2882
|
});
|
|
@@ -2636,6 +2900,7 @@ var FLAGS = {
|
|
|
2636
2900
|
function parseArgs(argv) {
|
|
2637
2901
|
const { values: v, positionals } = parseFlags({ args: argv, options: FLAGS, allowPositionals: true, strict: true });
|
|
2638
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})`);
|
|
2639
2904
|
return {
|
|
2640
2905
|
positional: positionals,
|
|
2641
2906
|
force: !!v.force,
|
|
@@ -2644,7 +2909,7 @@ function parseArgs(argv) {
|
|
|
2644
2909
|
fix: !!v.fix,
|
|
2645
2910
|
allowTracked: !!v["allow-tracked"],
|
|
2646
2911
|
json: !!v.json,
|
|
2647
|
-
days
|
|
2912
|
+
days,
|
|
2648
2913
|
dryRun: !!v["dry-run"],
|
|
2649
2914
|
allowGit: !!v["allow-git"],
|
|
2650
2915
|
...v.keep ? { keep: v.keep } : {},
|
|
@@ -2660,25 +2925,24 @@ function colorState(state) {
|
|
|
2660
2925
|
function withDirs(root, changes) {
|
|
2661
2926
|
const { skills } = readManifest(root);
|
|
2662
2927
|
return changes.map((c) => {
|
|
2663
|
-
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;
|
|
2664
2929
|
return dep ? { ...c, dirs: [dep.dir ?? ".claude/skills", ...dep.links ?? []] } : c;
|
|
2665
2930
|
});
|
|
2666
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
|
+
}
|
|
2667
2938
|
function printChanges(root, changes, args) {
|
|
2668
|
-
if (changes.some(
|
|
2939
|
+
if (changes.some(incomplete)) process.exitCode = 1;
|
|
2669
2940
|
if (args.json) return json("changes", withDirs(root, changes));
|
|
2670
2941
|
for (const c of changes) {
|
|
2671
|
-
|
|
2672
|
-
warn(`${c.name}: git tracks ${c.blocked.join(", ")} here, so no link was added there. Re-run with --allow-tracked to add it.`);
|
|
2673
|
-
}
|
|
2674
|
-
if (c.uncommitted?.length) {
|
|
2675
|
-
warn(
|
|
2676
|
-
`${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.`
|
|
2677
|
-
);
|
|
2678
|
-
}
|
|
2942
|
+
warnHeldLinks(c);
|
|
2679
2943
|
if (c.action === "skipped") error(`${c.name}: ${c.reason}`);
|
|
2680
2944
|
else {
|
|
2681
|
-
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");
|
|
2682
2946
|
const versions = c.action === "removed" ? "" : c.from && c.from !== c.to ? ` v${c.from} \u2192 v${c.to}` : c.to ? ` v${c.to}` : "";
|
|
2683
2947
|
info(` ${icon} ${c.name}${versions} ${dim(c.action)}`);
|
|
2684
2948
|
}
|
|
@@ -2693,9 +2957,9 @@ function projectOfFactory(projects2) {
|
|
|
2693
2957
|
const sorted = [...projects2].sort((a, b) => b.length - a.length);
|
|
2694
2958
|
return (cwd) => {
|
|
2695
2959
|
if (!cwd) return null;
|
|
2696
|
-
const known = sorted.find((p) => cwd === p || cwd.startsWith(p +
|
|
2960
|
+
const known = sorted.find((p) => cwd === p || cwd.startsWith(p + sep6));
|
|
2697
2961
|
if (known) return known;
|
|
2698
|
-
const repo = cwd.split(`${
|
|
2962
|
+
const repo = cwd.split(`${sep6}.claude${sep6}worktrees${sep6}`)[0];
|
|
2699
2963
|
return isProjectCandidate(repo) ? repo : null;
|
|
2700
2964
|
};
|
|
2701
2965
|
}
|
|
@@ -2703,13 +2967,23 @@ async function usageBySkill(days, onlyProject) {
|
|
|
2703
2967
|
const projectOf = projectOfFactory(onlyProject ? [onlyProject, ...knownProjects()] : knownProjects());
|
|
2704
2968
|
const uses = await scanUsage(days);
|
|
2705
2969
|
const scoped = onlyProject ? uses.filter((u) => projectOf(u.cwd) === onlyProject) : uses;
|
|
2706
|
-
return new Map(
|
|
2970
|
+
return new Map(summarize2(scoped, projectOf).map((s) => [s.skill, s]));
|
|
2707
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);
|
|
2708
2982
|
function requireProject() {
|
|
2709
2983
|
const root = projectHere();
|
|
2710
2984
|
if (root) return root;
|
|
2711
2985
|
if (!isProjectCandidate(findProjectRoot())) {
|
|
2712
|
-
const stray =
|
|
2986
|
+
const stray = existsSync14(join15(userHome(), MANIFEST_FILE)) ? `, though ${tildify(join15(userHome(), MANIFEST_FILE))} makes it look like one` : "";
|
|
2713
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.`);
|
|
2714
2988
|
}
|
|
2715
2989
|
throw new Error("Not in a git repo. Run this inside a project (or `git init` first).");
|
|
@@ -2732,14 +3006,8 @@ async function status(args) {
|
|
|
2732
3006
|
`);
|
|
2733
3007
|
if (skills.length === 0 && nested.length === 0) info("No skills in this project yet. Add one from your library with: skilllib add <name>");
|
|
2734
3008
|
const usage2 = await usageBySkill(args.days, root);
|
|
2735
|
-
const
|
|
2736
|
-
const
|
|
2737
|
-
const asRepo = {
|
|
2738
|
-
"local only": "repo skill",
|
|
2739
|
-
"untracked copy of library skill": "repo skill, in library",
|
|
2740
|
-
"untracked, differs from library": "repo skill, differs from library"
|
|
2741
|
-
};
|
|
2742
|
-
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));
|
|
2743
3011
|
const agentsOf = (s) => {
|
|
2744
3012
|
const ids = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
|
|
2745
3013
|
return ids.length === s.visibility.length ? dim("all") : ids.length ? ids.map((id) => harness(id).name).join(", ") : red("none");
|
|
@@ -2804,7 +3072,7 @@ async function list(args) {
|
|
|
2804
3072
|
} else {
|
|
2805
3073
|
const installs = /* @__PURE__ */ new Map();
|
|
2806
3074
|
for (const project of visibleProjects()) {
|
|
2807
|
-
for (const name of Object.keys(
|
|
3075
|
+
for (const name of Object.keys(pinsIn(project))) {
|
|
2808
3076
|
installs.set(name, [...installs.get(name) ?? [], basename9(project)]);
|
|
2809
3077
|
}
|
|
2810
3078
|
}
|
|
@@ -2823,7 +3091,7 @@ async function list(args) {
|
|
|
2823
3091
|
);
|
|
2824
3092
|
}
|
|
2825
3093
|
const libraryNames = new Set(skills.map((s) => s.name));
|
|
2826
|
-
const globals = [...new Set(
|
|
3094
|
+
const globals = [...new Set(yourGlobals().map((m) => m.name))];
|
|
2827
3095
|
if (globals.length > 0) {
|
|
2828
3096
|
info("");
|
|
2829
3097
|
warn(`${globals.length} of your global ${globals.length === 1 ? "skill still loads" : "skills still load"} in every project`);
|
|
@@ -2835,20 +3103,33 @@ async function list(args) {
|
|
|
2835
3103
|
);
|
|
2836
3104
|
}
|
|
2837
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
|
+
}
|
|
2838
3116
|
function projects(args) {
|
|
2839
|
-
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
|
+
}
|
|
2840
3122
|
if (args.json) {
|
|
2841
3123
|
return json(
|
|
2842
3124
|
"projects",
|
|
2843
|
-
roots.map(
|
|
2844
|
-
(root)
|
|
3125
|
+
roots.map((root) => {
|
|
3126
|
+
const teamState = teamStateIn(root);
|
|
3127
|
+
return {
|
|
2845
3128
|
name: basename9(root),
|
|
2846
3129
|
path: root,
|
|
2847
|
-
skills: projectStatus(root).map(
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
})
|
|
2851
|
-
)
|
|
3130
|
+
skills: projectStatus(root).map((s) => ({ name: s.name, managed: s.managed, state: teamState(s), version: s.version, latest: s.latest }))
|
|
3131
|
+
};
|
|
3132
|
+
})
|
|
2852
3133
|
);
|
|
2853
3134
|
}
|
|
2854
3135
|
if (roots.length === 0) {
|
|
@@ -2886,59 +3167,60 @@ function remove(args) {
|
|
|
2886
3167
|
const root = requireProject();
|
|
2887
3168
|
printChanges(root, args.positional.map((name) => removeSkill(root, name, { force: args.force })), args);
|
|
2888
3169
|
}
|
|
2889
|
-
function
|
|
2890
|
-
|
|
2891
|
-
const roots = args.all ? visibleProjects() : [requireProject()];
|
|
3170
|
+
function changeEach(args, change, nothing) {
|
|
3171
|
+
const roots = projectsFor(args);
|
|
2892
3172
|
const results = roots.map((root) => {
|
|
2893
3173
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
2894
|
-
const changes =
|
|
2895
|
-
if (changes.some(
|
|
3174
|
+
const changes = change(root);
|
|
3175
|
+
if (changes.some(incomplete)) process.exitCode = 1;
|
|
2896
3176
|
if (!args.json) {
|
|
2897
3177
|
if (args.all) info(`${basename9(root)} ${dim(tildify(root))}`);
|
|
2898
|
-
if (changes.length === 0)
|
|
2899
|
-
|
|
2900
|
-
else info(dim("No skilllib.json: nothing to sync"));
|
|
2901
|
-
} else printChanges(root, changes, args);
|
|
3178
|
+
if (changes.length === 0) nothing(root);
|
|
3179
|
+
else printChanges(root, changes, args);
|
|
2902
3180
|
}
|
|
2903
3181
|
return { project: basename9(root), path: root, changes: withDirs(root, changes) };
|
|
2904
3182
|
});
|
|
2905
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
|
+
);
|
|
2906
3193
|
if (!args.all) rememberProjects(roots);
|
|
2907
3194
|
}
|
|
2908
3195
|
function update(args) {
|
|
2909
|
-
const
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
if (changes.length === 0) success("Everything is on the latest version");
|
|
2922
|
-
else printChanges(root, changes, args);
|
|
2923
|
-
}
|
|
2924
|
-
return { project: basename9(root), path: root, changes: withDirs(root, changes) };
|
|
2925
|
-
});
|
|
2926
|
-
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
|
+
);
|
|
2927
3208
|
}
|
|
2928
3209
|
function outdated(args) {
|
|
2929
|
-
const roots = args
|
|
3210
|
+
const roots = projectsFor(args);
|
|
2930
3211
|
const behind = roots.flatMap(
|
|
2931
|
-
(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 }))
|
|
2932
3213
|
);
|
|
2933
3214
|
if (args.json) return json("outdated", behind);
|
|
2934
|
-
const
|
|
2935
|
-
if (
|
|
2936
|
-
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);
|
|
2937
3218
|
info(dim(`
|
|
2938
3219
|
\u2192 skilllib update${args.all ? " --all" : ""}`));
|
|
2939
3220
|
}
|
|
2940
3221
|
function importSkills(args) {
|
|
2941
|
-
|
|
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));
|
|
2942
3224
|
if (dirs.length === 0) {
|
|
2943
3225
|
throw new Error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
|
|
2944
3226
|
}
|
|
@@ -2958,14 +3240,11 @@ function importSkills(args) {
|
|
|
2958
3240
|
continue;
|
|
2959
3241
|
}
|
|
2960
3242
|
success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
|
|
2961
|
-
const
|
|
2962
|
-
const
|
|
2963
|
-
if (
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
if (change.action !== "skipped") info(dim(` ${basename9(root)} now depends on ${name} v${change.to}`));
|
|
2967
|
-
}
|
|
2968
|
-
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}`));
|
|
2969
3248
|
}
|
|
2970
3249
|
}
|
|
2971
3250
|
if (args.json) json("import", results);
|
|
@@ -2980,7 +3259,7 @@ function scan(args) {
|
|
|
2980
3259
|
const found = discoverProjects();
|
|
2981
3260
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
2982
3261
|
const skillNames = (root) => [
|
|
2983
|
-
.../* @__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)])
|
|
2984
3263
|
];
|
|
2985
3264
|
const withSkills = found.filter((root) => skillNames(root).length > 0);
|
|
2986
3265
|
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
|
|
@@ -2997,6 +3276,7 @@ ${yellow("Yellow")} skills aren't in your library yet (\`skilllib import <skill-
|
|
|
2997
3276
|
}
|
|
2998
3277
|
function folders(args) {
|
|
2999
3278
|
const [action, ...paths] = args.positional;
|
|
3279
|
+
if (action && !["add", "remove", "rm"].includes(action)) throw new Error("Usage: skilllib folders [add|remove <folder>...]");
|
|
3000
3280
|
let failed = false;
|
|
3001
3281
|
if (action === "add") {
|
|
3002
3282
|
for (const p of paths) {
|
|
@@ -3011,7 +3291,10 @@ function folders(args) {
|
|
|
3011
3291
|
} else if (action === "remove" || action === "rm") {
|
|
3012
3292
|
for (const p of paths) {
|
|
3013
3293
|
if (removeRoot(p)) success(`Removed ${tildify(expandHome(p))}`);
|
|
3014
|
-
else
|
|
3294
|
+
else {
|
|
3295
|
+
warn(`${tildify(expandHome(p))} wasn't a project folder`);
|
|
3296
|
+
failed = true;
|
|
3297
|
+
}
|
|
3015
3298
|
}
|
|
3016
3299
|
}
|
|
3017
3300
|
const { roots, hidden } = readConfig();
|
|
@@ -3045,7 +3328,7 @@ function newSkill(args) {
|
|
|
3045
3328
|
if (!r.ok) {
|
|
3046
3329
|
throw new Error(`Can't create ${name}: ${r.reason}`);
|
|
3047
3330
|
}
|
|
3048
|
-
success(`Created ${tildify(
|
|
3331
|
+
success(`Created ${tildify(join15(r.dir, "SKILL.md"))}`);
|
|
3049
3332
|
}
|
|
3050
3333
|
function restore(args) {
|
|
3051
3334
|
const backups = [...listBackups(), ...pluginBackups()];
|
|
@@ -3056,15 +3339,17 @@ function restore(args) {
|
|
|
3056
3339
|
info(dim("\n\u2192 skilllib restore <name>"));
|
|
3057
3340
|
return;
|
|
3058
3341
|
}
|
|
3059
|
-
const
|
|
3060
|
-
if (!
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
const
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
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
|
+
}
|
|
3068
3353
|
}
|
|
3069
3354
|
}
|
|
3070
3355
|
async function allIssues(args, withUsage = true) {
|
|
@@ -3076,11 +3361,14 @@ async function allIssues(args, withUsage = true) {
|
|
|
3076
3361
|
const projectOf = projectOfFactory(projects2);
|
|
3077
3362
|
const uses = await scanUsage(args.days);
|
|
3078
3363
|
const inProject = usesByProject(uses, projectOf);
|
|
3079
|
-
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);
|
|
3080
3367
|
issues.push(
|
|
3081
|
-
...usageIssues(projects2, machine, libraryNames, {
|
|
3368
|
+
...usageIssues(readableRoots(projects2).readable, machine, libraryNames, {
|
|
3082
3369
|
inProject: (root, skill) => inProject.get(root)?.get(skill) ?? 0,
|
|
3083
3370
|
total: (skill) => total.get(skill) ?? 0,
|
|
3371
|
+
asUsed: (key) => asUsed.get(key) ?? 0,
|
|
3084
3372
|
days: args.days
|
|
3085
3373
|
})
|
|
3086
3374
|
);
|
|
@@ -3125,7 +3413,7 @@ async function doctor(args) {
|
|
|
3125
3413
|
async function apply(args) {
|
|
3126
3414
|
const [id, pick] = args.positional;
|
|
3127
3415
|
if (!id) throw new Error("Usage: skilllib apply <issue-id> [choice] (ids and choices: skilllib status --json or doctor --json)");
|
|
3128
|
-
const found = [...statusIssues(findProjectRoot()), ...await allIssues(args,
|
|
3416
|
+
const found = [...statusIssues(findProjectRoot()), ...await allIssues(args, /^(plugin-)?unused:/.test(id))].filter((i) => i.id === id);
|
|
3129
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`);
|
|
3130
3418
|
const optionsOf = (i) => i.choices ?? (i.fix ? [i.fix] : []);
|
|
3131
3419
|
const labels = (i) => optionsOf(i).map((c) => c.label).join("\0");
|
|
@@ -3148,22 +3436,22 @@ async function apply(args) {
|
|
|
3148
3436
|
else error(r.message);
|
|
3149
3437
|
}
|
|
3150
3438
|
async function usage(args) {
|
|
3151
|
-
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));
|
|
3152
3442
|
if (args.json) {
|
|
3153
|
-
const library2 = new Set(librarySkills().map((s) => s.name));
|
|
3154
3443
|
return json("usage", {
|
|
3155
3444
|
usageDays: args.days,
|
|
3156
3445
|
source: "Claude Code transcripts",
|
|
3157
|
-
skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) => basename9(p)), inLibrary:
|
|
3158
|
-
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
|
|
3159
3448
|
});
|
|
3160
3449
|
}
|
|
3161
3450
|
if (summaries.length === 0) {
|
|
3162
3451
|
info(`No skill usage in Claude Code transcripts from the last ${args.days} days.`);
|
|
3163
3452
|
return;
|
|
3164
3453
|
}
|
|
3165
|
-
|
|
3166
|
-
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"))})`)}
|
|
3167
3455
|
`);
|
|
3168
3456
|
table(
|
|
3169
3457
|
summaries.map((s) => ({
|
|
@@ -3174,15 +3462,14 @@ async function usage(args) {
|
|
|
3174
3462
|
Library: library.has(s.skill) ? green("yes") : dim("no")
|
|
3175
3463
|
}))
|
|
3176
3464
|
);
|
|
3177
|
-
const unused = librarySkills().filter((s) => !summaries.some((u) => u.skill === s.name));
|
|
3178
3465
|
if (unused.length > 0) {
|
|
3179
3466
|
info("");
|
|
3180
|
-
warn(`Library skills unused in the last ${args.days} days: ${unused.
|
|
3467
|
+
warn(`Library skills unused in the last ${args.days} days: ${unused.join(", ")}`);
|
|
3181
3468
|
}
|
|
3182
3469
|
}
|
|
3183
3470
|
function show(args) {
|
|
3184
3471
|
const name = args.positional[0];
|
|
3185
|
-
const dir = name ?
|
|
3472
|
+
const dir = name ? join15(libraryDir(), name) : "";
|
|
3186
3473
|
if (!name || !isSkillDir(dir)) {
|
|
3187
3474
|
throw new Error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
|
|
3188
3475
|
}
|
|
@@ -3194,14 +3481,14 @@ function show(args) {
|
|
|
3194
3481
|
path: dir,
|
|
3195
3482
|
latest: latestVersion(name)?.version ?? null,
|
|
3196
3483
|
origin: libraryOrigins()[name] ?? "",
|
|
3197
|
-
projects: knownProjects().filter((p) => name in
|
|
3198
|
-
skillMd:
|
|
3484
|
+
projects: knownProjects().filter((p) => name in pinsIn(p)).map((p) => basename9(p)),
|
|
3485
|
+
skillMd: readFileSync6(join15(dir, "SKILL.md"), "utf-8")
|
|
3199
3486
|
});
|
|
3200
3487
|
}
|
|
3201
3488
|
info(`${name} ${dim(tildify(dir))}`);
|
|
3202
3489
|
if (description) info(`
|
|
3203
3490
|
${description}`);
|
|
3204
|
-
const using = knownProjects().filter((p) => name in
|
|
3491
|
+
const using = knownProjects().filter((p) => name in pinsIn(p));
|
|
3205
3492
|
info(`
|
|
3206
3493
|
Installed in: ${using.map((p) => tildify(p)).join(", ") || "no projects"}`);
|
|
3207
3494
|
}
|
|
@@ -3223,7 +3510,7 @@ function harnessesCommand(args) {
|
|
|
3223
3510
|
New installs go to ${installDirs(enabled).join(" (real copy) + ")}${installDirs(enabled).length > 1 ? " (link)" : ""}.`));
|
|
3224
3511
|
}
|
|
3225
3512
|
function link(args) {
|
|
3226
|
-
const roots = args
|
|
3513
|
+
const roots = projectsFor(args);
|
|
3227
3514
|
for (const root of roots) {
|
|
3228
3515
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
3229
3516
|
const res = linkAll(root);
|
|
@@ -3234,6 +3521,7 @@ function link(args) {
|
|
|
3234
3521
|
process.exitCode = 1;
|
|
3235
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`);
|
|
3236
3523
|
}
|
|
3524
|
+
if (res.uncommitted.length) process.exitCode = 1;
|
|
3237
3525
|
if (res.uncommitted.length)
|
|
3238
3526
|
warn(
|
|
3239
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`
|
|
@@ -3242,7 +3530,7 @@ function link(args) {
|
|
|
3242
3530
|
}
|
|
3243
3531
|
function globalCommand(args) {
|
|
3244
3532
|
const [action, ...names] = args.positional;
|
|
3245
|
-
const yours =
|
|
3533
|
+
const yours = yourGlobals();
|
|
3246
3534
|
if (action === "move") return moveGlobal(args, names, yours);
|
|
3247
3535
|
if (action === "keep" || action === "unkeep") {
|
|
3248
3536
|
const unknown = names.filter((n) => !yours.some((m) => m.name === n));
|
|
@@ -3267,7 +3555,7 @@ function repoRoot(repo) {
|
|
|
3267
3555
|
if (named.length === 1) return named[0];
|
|
3268
3556
|
if (named.length > 1) throw new Error(`Several projects are named ${repo}: ${named.map(tildify).join(", ")}. Pass its path.`);
|
|
3269
3557
|
const path = expandHome(repo);
|
|
3270
|
-
if (!
|
|
3558
|
+
if (!existsSync14(path)) throw new Error(`No project ${repo} (pass its path, or see skilllib projects)`);
|
|
3271
3559
|
const root = projectHere(path);
|
|
3272
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`);
|
|
3273
3561
|
return root;
|
|
@@ -3279,6 +3567,7 @@ function moveGlobal(args, names, yours) {
|
|
|
3279
3567
|
const roots = [...new Set(args.to.map(repoRoot))];
|
|
3280
3568
|
const skills = yours.filter((m) => names.includes(m.name));
|
|
3281
3569
|
const moves = moveGlobalSkills(skills, roots, { allowTracked: args.allowTracked });
|
|
3570
|
+
rememberProjects(roots);
|
|
3282
3571
|
if (moves.some((m) => !m.moved)) process.exitCode = 1;
|
|
3283
3572
|
if (args.json) {
|
|
3284
3573
|
return json(
|
|
@@ -3294,10 +3583,7 @@ function moveGlobal(args, names, yours) {
|
|
|
3294
3583
|
for (const m of moves) {
|
|
3295
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)`);
|
|
3296
3585
|
else error(`${m.name}: ${m.reason}`);
|
|
3297
|
-
for (const [root, c] of m.changes) {
|
|
3298
|
-
if (c.blocked?.length) warn(` ${basename9(root)}: git tracks ${c.blocked.join(", ")}, so no link there. Re-run with --allow-tracked to add it.`);
|
|
3299
|
-
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.`);
|
|
3300
|
-
}
|
|
3586
|
+
for (const [root, c] of m.changes) warnHeldLinks(c, `in ${basename9(root)}`);
|
|
3301
3587
|
}
|
|
3302
3588
|
if (moves.some((m) => m.moved)) info(dim("Commit skilllib.json and the skill folders in each repo."));
|
|
3303
3589
|
}
|
|
@@ -3307,7 +3593,7 @@ function tidy(args) {
|
|
|
3307
3593
|
if (args.keep && !names.length) {
|
|
3308
3594
|
throw new Error("Usage: skilllib tidy <name>... --keep <folder>");
|
|
3309
3595
|
}
|
|
3310
|
-
const targets = args.global ? [null] : args
|
|
3596
|
+
const targets = args.global ? [null] : projectsFor(args);
|
|
3311
3597
|
const problems = [];
|
|
3312
3598
|
for (const name of names) {
|
|
3313
3599
|
const where = targets.map((root) => ({ root, ...foldersOf(root, name) })).filter((w) => w.all.length);
|
|
@@ -3335,20 +3621,20 @@ function tidy(args) {
|
|
|
3335
3621
|
for (const { label, report } of reports) {
|
|
3336
3622
|
const plans = report.plans.filter((p) => !names.length || names.includes(p.name));
|
|
3337
3623
|
const conflicts = report.conflicts.filter((c) => !names.length || names.includes(c.name));
|
|
3338
|
-
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)`);
|
|
3339
3625
|
for (const plan of plans) {
|
|
3340
|
-
const
|
|
3341
|
-
const
|
|
3342
|
-
if (
|
|
3343
|
-
|
|
3344
|
-
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;
|
|
3345
3631
|
changed += result.applied.length;
|
|
3346
3632
|
}
|
|
3347
3633
|
if (args.keep && conflicts.length) process.exitCode = 1;
|
|
3348
3634
|
for (const c of conflicts) {
|
|
3349
3635
|
warn(`${label} \xB7 ${c.name}: the copies differ`);
|
|
3350
3636
|
for (const copy of c.copies) {
|
|
3351
|
-
const runs = copy.runs.map((id) =>
|
|
3637
|
+
const runs = copy.runs.map((id) => harness(id).name);
|
|
3352
3638
|
info(` ${copyLabel(c, copy.dir)}${runs.length ? dim(` run by ${runs.join(", ")}`) : ""}${c.managed === copy.dir ? dim(" (skilllib's copy)") : ""}`);
|
|
3353
3639
|
}
|
|
3354
3640
|
const pick = copyLabel(c, c.managed ?? c.copies[0].dir);
|
|
@@ -3378,6 +3664,7 @@ function agentSkill(args) {
|
|
|
3378
3664
|
if (args.json) return json("agent-skill", { state: agentSkillState(), removed });
|
|
3379
3665
|
return removed.length ? success(`Removed from ${removed.map((d) => tildify(d)).join(" and ")}`) : info("The skilllib skill isn't installed.");
|
|
3380
3666
|
}
|
|
3667
|
+
if (action) throw new Error("Usage: skilllib agent-skill [install|remove]");
|
|
3381
3668
|
const state = agentSkillState();
|
|
3382
3669
|
if (args.json) return json("agent-skill", { state, dirs: agentSkillDirs() });
|
|
3383
3670
|
if (state === "installed") success(`Your agents can use skilllib ${dim(`(${agentSkillDirs().map((d) => tildify(d)).join(", ")})`)}`);
|
|
@@ -3391,16 +3678,19 @@ export {
|
|
|
3391
3678
|
HARNESSES,
|
|
3392
3679
|
harness,
|
|
3393
3680
|
readSkillInfo,
|
|
3681
|
+
treeHash,
|
|
3394
3682
|
versionHistory,
|
|
3395
3683
|
forgetLatest,
|
|
3396
3684
|
latestVersion,
|
|
3397
3685
|
projectHere,
|
|
3398
3686
|
readManifest,
|
|
3687
|
+
readableRoots,
|
|
3399
3688
|
setJsonMode,
|
|
3400
3689
|
isJsonMode,
|
|
3401
3690
|
json,
|
|
3402
3691
|
error,
|
|
3403
3692
|
tildify,
|
|
3693
|
+
localStamp,
|
|
3404
3694
|
readConfig,
|
|
3405
3695
|
addRoot,
|
|
3406
3696
|
removeRoot,
|
|
@@ -3425,43 +3715,34 @@ export {
|
|
|
3425
3715
|
relinkDependency,
|
|
3426
3716
|
addSkill,
|
|
3427
3717
|
removeSkill,
|
|
3428
|
-
syncSkill,
|
|
3429
3718
|
updateProject,
|
|
3430
3719
|
importSkill,
|
|
3431
3720
|
removeUntracked,
|
|
3432
3721
|
projectsUsing,
|
|
3433
3722
|
deleteLibrarySkill,
|
|
3434
3723
|
deleteGlobal,
|
|
3435
|
-
unloadGlobal,
|
|
3436
|
-
linkGlobal,
|
|
3437
3724
|
createSkill,
|
|
3438
3725
|
listBackups,
|
|
3439
3726
|
restoreBackup,
|
|
3440
3727
|
gitInfo,
|
|
3441
3728
|
relativeTo,
|
|
3442
|
-
claudePlugins,
|
|
3443
3729
|
installedPlugins,
|
|
3444
3730
|
cursorPlugins,
|
|
3445
3731
|
forgetClaudeBinary,
|
|
3446
3732
|
claudeBinary,
|
|
3447
|
-
turnOffIn,
|
|
3448
3733
|
setPluginOn,
|
|
3449
3734
|
uninstallPlugin,
|
|
3450
3735
|
updatePlugin,
|
|
3451
3736
|
pluginBackups,
|
|
3452
3737
|
restorePlugin,
|
|
3453
|
-
cursorPluginSkills,
|
|
3454
3738
|
agentSkillState,
|
|
3455
3739
|
installAgentSkill,
|
|
3456
3740
|
removeAgentSkill,
|
|
3457
3741
|
refreshAgentSkill,
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
describeStep,
|
|
3463
|
-
copyLabel,
|
|
3464
|
-
addedAt,
|
|
3742
|
+
foldersOf,
|
|
3743
|
+
runFix,
|
|
3744
|
+
findIssues,
|
|
3745
|
+
usageIssues,
|
|
3465
3746
|
scanUsage,
|
|
3466
3747
|
moveGlobalSkills,
|
|
3467
3748
|
parseArgs,
|