skilllib 1.11.6 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -5
- package/dist/{chunk-TXQQQ73J.js → chunk-57EEPVXJ.js} +781 -596
- package/dist/index.js +6 -4
- package/dist/json.d.cts +6 -0
- package/dist/json.d.ts +6 -0
- package/dist/{tui-OS2VUBGF.js → tui-BCCAP7GJ.js} +166 -135
- package/package.json +1 -1
- package/skills/skilllib/SKILL.md +3 -3
|
@@ -48,6 +48,13 @@ function readJson(path) {
|
|
|
48
48
|
return null;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
function realNative(p) {
|
|
52
|
+
try {
|
|
53
|
+
return realpathSync.native(p);
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
51
58
|
function realpathOrNull(path) {
|
|
52
59
|
try {
|
|
53
60
|
return realpathSync(path);
|
|
@@ -126,7 +133,7 @@ function localStamp(ms) {
|
|
|
126
133
|
}
|
|
127
134
|
|
|
128
135
|
// src/config.ts
|
|
129
|
-
import { existsSync as existsSync5, readdirSync as
|
|
136
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
130
137
|
import { join as join6, resolve as resolve4 } from "path";
|
|
131
138
|
|
|
132
139
|
// src/harnesses.ts
|
|
@@ -225,8 +232,10 @@ function installDirs(enabled, keep) {
|
|
|
225
232
|
}
|
|
226
233
|
|
|
227
234
|
// src/project.ts
|
|
228
|
-
import {
|
|
229
|
-
import {
|
|
235
|
+
import { execFileSync } from "child_process";
|
|
236
|
+
import { createHash as createHash2 } from "crypto";
|
|
237
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
238
|
+
import { basename as basename2, dirname as dirname2, join as join5, resolve as resolve3 } from "path";
|
|
230
239
|
|
|
231
240
|
// src/versions.ts
|
|
232
241
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
@@ -356,8 +365,7 @@ function projectHere(from = process.cwd()) {
|
|
|
356
365
|
const root = findProjectRoot(from);
|
|
357
366
|
return isProjectCandidate(root) && (existsSync4(join5(root, ".git")) || existsSync4(join5(root, MANIFEST_FILE))) ? root : null;
|
|
358
367
|
}
|
|
359
|
-
function
|
|
360
|
-
const path = join5(root, MANIFEST_FILE);
|
|
368
|
+
function readPins(path) {
|
|
361
369
|
if (!existsSync4(path)) return { skills: {} };
|
|
362
370
|
let data;
|
|
363
371
|
try {
|
|
@@ -393,9 +401,141 @@ function readManifest(root) {
|
|
|
393
401
|
);
|
|
394
402
|
return { ...data, skills };
|
|
395
403
|
}
|
|
404
|
+
function readManifest(root) {
|
|
405
|
+
const shared = readPins(join5(root, MANIFEST_FILE));
|
|
406
|
+
const file = personalPinsFile(root);
|
|
407
|
+
const personal = file ? readPins(file).skills : {};
|
|
408
|
+
const mine = Object.fromEntries(Object.entries(personal).map(([name, dep]) => [name, { ...dep, personal: true }]));
|
|
409
|
+
return { ...shared, skills: { ...mine, ...shared.skills } };
|
|
410
|
+
}
|
|
411
|
+
var sorted = (skills) => Object.fromEntries(Object.entries(skills).sort(([a], [b]) => a.localeCompare(b, "en")));
|
|
396
412
|
function writeManifest(root, manifest) {
|
|
397
|
-
const
|
|
398
|
-
|
|
413
|
+
const shared = Object.fromEntries(Object.entries(manifest.skills).filter(([, d]) => !d.personal));
|
|
414
|
+
const mine = Object.fromEntries(Object.entries(manifest.skills).flatMap(([name, d]) => d.personal ? [[name, withoutMark(d)]] : []));
|
|
415
|
+
const path = join5(root, MANIFEST_FILE);
|
|
416
|
+
const onDisk = readPins(path).skills;
|
|
417
|
+
const leftEmpty = !Object.keys(shared).length && Object.keys(mine).length > 0 && existsSync4(path) && gitIn(root, ["ls-files", "--", MANIFEST_FILE]) === "";
|
|
418
|
+
if (leftEmpty) rmSync3(path, { force: true });
|
|
419
|
+
else if (existsSync4(path) || Object.keys(shared).length) writeChanged(path, { ...manifest, skills: sorted(shared) });
|
|
420
|
+
const file = personalPinsFile(root);
|
|
421
|
+
if (!file) {
|
|
422
|
+
if (Object.keys(mine).length) throw new Error(`${root} isn't the top folder of a git repo, so it can't have personal pins`);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
for (const [name, dep] of Object.entries(readPins(file).skills)) if (onDisk[name] && manifest.skills[name] && !manifest.skills[name].personal) mine[name] = dep;
|
|
426
|
+
if (Object.keys(mine).length) writeChanged(file, { ...pinsHeader(repoOf(root)), skills: sorted(mine) });
|
|
427
|
+
else rmSync3(file, { force: true });
|
|
428
|
+
excludePersonal(root, mine);
|
|
429
|
+
}
|
|
430
|
+
function writeChanged(path, data) {
|
|
431
|
+
try {
|
|
432
|
+
if (JSON.stringify(JSON.parse(readFileSync3(path, "utf-8"))) === JSON.stringify(data)) return;
|
|
433
|
+
} catch {
|
|
434
|
+
}
|
|
435
|
+
writeState(path, data);
|
|
436
|
+
}
|
|
437
|
+
function withoutMark(dep) {
|
|
438
|
+
const { personal: _, ...rest } = dep;
|
|
439
|
+
return rest;
|
|
440
|
+
}
|
|
441
|
+
function hasPins(root) {
|
|
442
|
+
if (existsSync4(join5(root, MANIFEST_FILE))) return true;
|
|
443
|
+
const file = personalPinsFile(root);
|
|
444
|
+
return !!file && existsSync4(file);
|
|
445
|
+
}
|
|
446
|
+
function personalPinsDir() {
|
|
447
|
+
return join5(skilllibHome(), "pins");
|
|
448
|
+
}
|
|
449
|
+
function gitIn(root, args) {
|
|
450
|
+
try {
|
|
451
|
+
return execFileSync("git", args, { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
452
|
+
} catch {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function normalizeRemote(url) {
|
|
457
|
+
let s = url.trim();
|
|
458
|
+
const scp = s.match(/^(?:[^/@:]+@)?([^/:\\]{2,}):(?!\/)(.*)$/);
|
|
459
|
+
if (scp) s = `${scp[1]}/${scp[2]}`;
|
|
460
|
+
else s = s.replace(/^[a-z+]+:\/\//i, "").replace(/^[^/@]+@/, "").replace(/^([^/:]+):\d+\//, "$1/");
|
|
461
|
+
s = s.replace(/\.git\/?$/, "").replace(/\/+$/, "");
|
|
462
|
+
return s.replace(/^[^/]*/, (host) => host.toLowerCase());
|
|
463
|
+
}
|
|
464
|
+
var repos = /* @__PURE__ */ new Map();
|
|
465
|
+
var changedAt = (path) => statSync3(path, { throwIfNoEntry: false })?.mtimeMs ?? -1;
|
|
466
|
+
function repoOf(root) {
|
|
467
|
+
const real = realPath(root);
|
|
468
|
+
const hit = repos.get(real);
|
|
469
|
+
if (hit && (hit.config ? changedAt(hit.config) === hit.seen : !existsSync4(join5(root, ".git")))) return hit.repo;
|
|
470
|
+
const out2 = gitIn(root, ["rev-parse", "--show-prefix", "--git-common-dir"])?.split("\n");
|
|
471
|
+
if (!out2) {
|
|
472
|
+
repos.set(real, { repo: null, config: null, seen: 0 });
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
const common = realNative(resolve3(root, out2[1].trim())) ?? resolve3(root, out2[1].trim());
|
|
476
|
+
const folder = basename2(common) === ".git" ? dirname2(common) : common;
|
|
477
|
+
const config = join5(common, "config");
|
|
478
|
+
const seen = changedAt(config);
|
|
479
|
+
let repo = null;
|
|
480
|
+
if (out2[0] === "") {
|
|
481
|
+
const urls = (gitIn(root, ["config", "--get-regexp", "^remote\\..*\\.url$"]) ?? "").split("\n").filter(Boolean).map((l) => l.split(" "));
|
|
482
|
+
const url = (urls.find(([k]) => k === "remote.origin.url") ?? urls[0])?.[1];
|
|
483
|
+
const remote = url ? normalizeRemote(url) : void 0;
|
|
484
|
+
repo = { file: pinsFileFor(root, remote, folder), folder, common, ...remote ? { remote } : {}, root };
|
|
485
|
+
}
|
|
486
|
+
repos.set(real, { repo, config, seen });
|
|
487
|
+
return repo;
|
|
488
|
+
}
|
|
489
|
+
var firstCommit = (root) => gitIn(root, ["rev-list", "--max-parents=0", "HEAD"])?.split("\n")[0] || null;
|
|
490
|
+
function pinsFileFor(root, remote, folder) {
|
|
491
|
+
const name = remote ?? `local-${basename2(folder)}`;
|
|
492
|
+
const slug = name.replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
493
|
+
const file = join5(personalPinsDir(), `${slug}-${createHash2("sha1").update(remote ?? folder).digest("hex").slice(0, 8)}.json`);
|
|
494
|
+
if (existsSync4(file) || !existsSync4(personalPinsDir())) return file;
|
|
495
|
+
for (const other of readdirSync2(personalPinsDir()).filter((f) => f.endsWith(".json"))) {
|
|
496
|
+
const at = join5(personalPinsDir(), other);
|
|
497
|
+
const theirs = readJson(at);
|
|
498
|
+
if (theirs?.path === folder && (theirs.first ? theirs.first === firstCommit(root) : !theirs.remote)) {
|
|
499
|
+
renameSync2(at, file);
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return file;
|
|
504
|
+
}
|
|
505
|
+
var pinsHeader = (repo) => ((first) => ({ ...repo.remote ? { remote: repo.remote } : {}, path: repo.folder, ...first ? { first } : {} }))(firstCommit(repo.root));
|
|
506
|
+
function personalPinsFile(root) {
|
|
507
|
+
return repoOf(root)?.file ?? null;
|
|
508
|
+
}
|
|
509
|
+
function personalPins(root) {
|
|
510
|
+
const file = personalPinsFile(root);
|
|
511
|
+
return file ? readPins(file).skills : {};
|
|
512
|
+
}
|
|
513
|
+
function dropPersonalPin(root, name) {
|
|
514
|
+
const file = personalPinsFile(root);
|
|
515
|
+
if (!file) return;
|
|
516
|
+
const { [name]: _, ...rest } = readPins(file).skills;
|
|
517
|
+
if (Object.keys(rest).length) writeChanged(file, { ...pinsHeader(repoOf(root)), skills: sorted(rest) });
|
|
518
|
+
else rmSync3(file, { force: true });
|
|
519
|
+
excludePersonal(root);
|
|
520
|
+
}
|
|
521
|
+
var EXCLUDE_START = "# skilllib: personal skills (kept by skilllib; don't edit)";
|
|
522
|
+
var EXCLUDE_END = "# end skilllib";
|
|
523
|
+
function excludePersonal(root, skills = Object.fromEntries(Object.entries(readManifest(root).skills).filter(([, d]) => d.personal))) {
|
|
524
|
+
const repo = repoOf(root);
|
|
525
|
+
if (!repo?.file) return;
|
|
526
|
+
const file = join5(repo.common, "info", "exclude");
|
|
527
|
+
const text = existsSync4(file) ? readFileSync3(file, "utf-8") : "";
|
|
528
|
+
const lines = text.split("\n");
|
|
529
|
+
const start = lines.indexOf(EXCLUDE_START);
|
|
530
|
+
const end = start === -1 ? -1 : lines.indexOf(EXCLUDE_END, start);
|
|
531
|
+
const rest = start === -1 ? lines : [...lines.slice(0, start), ...lines.slice(end === -1 ? start + 1 : end + 1)];
|
|
532
|
+
while (rest.length && rest[rest.length - 1] === "") rest.pop();
|
|
533
|
+
const paths = Object.entries(skills).flatMap(([name, d]) => [d.dir ?? PROJECT_SKILLS_DIR, ...d.links ?? []].map((dir) => `/${dir}/${name.replace(/[[\]*?\\]/g, "\\$&")}`));
|
|
534
|
+
const block = paths.length ? [EXCLUDE_START, ...[...new Set(paths)].sort(), EXCLUDE_END] : [];
|
|
535
|
+
const next = [...rest, ...rest.length && block.length ? [""] : [], ...block].join("\n") + (rest.length || block.length ? "\n" : "");
|
|
536
|
+
if (next === text) return;
|
|
537
|
+
mkdirSync3(dirname2(file), { recursive: true });
|
|
538
|
+
writeFileSync2(file, next);
|
|
399
539
|
}
|
|
400
540
|
function projectsFile() {
|
|
401
541
|
return join5(skilllibHome(), "projects.json");
|
|
@@ -454,8 +594,9 @@ function configFile() {
|
|
|
454
594
|
return join6(skilllibHome(), "config.json");
|
|
455
595
|
}
|
|
456
596
|
function readConfig() {
|
|
457
|
-
const data = readState(configFile()) ?? {};
|
|
458
|
-
|
|
597
|
+
const { agentsDirOk, ...data } = readState(configFile()) ?? {};
|
|
598
|
+
const allowTracked = [.../* @__PURE__ */ new Set([...data.allowTracked ?? [], ...agentsDirOk ?? []])].sort();
|
|
599
|
+
return { ...data, roots: data.roots ?? [], hidden: data.hidden ?? [], ...allowTracked.length ? { allowTracked } : {} };
|
|
459
600
|
}
|
|
460
601
|
function writeConfig(config) {
|
|
461
602
|
writeState(configFile(), config);
|
|
@@ -466,7 +607,7 @@ function expandHome(path) {
|
|
|
466
607
|
function addRoot(path) {
|
|
467
608
|
const root = expandHome(path);
|
|
468
609
|
if (!existsSync5(root)) throw new Error(`${tildify(root)} doesn't exist`);
|
|
469
|
-
if (!
|
|
610
|
+
if (!statSync4(root).isDirectory()) throw new Error(`${tildify(root)} isn't a folder`);
|
|
470
611
|
const config = readConfig();
|
|
471
612
|
if (!config.roots.includes(root)) writeConfig({ ...config, roots: [...config.roots, root].sort() });
|
|
472
613
|
return root;
|
|
@@ -495,7 +636,7 @@ function findProjects(dir, depth = 4) {
|
|
|
495
636
|
}
|
|
496
637
|
let entries = [];
|
|
497
638
|
try {
|
|
498
|
-
entries =
|
|
639
|
+
entries = readdirSync3(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && !SKIP_DIRS.has(d.name)).map((d) => join6(dir, d.name));
|
|
499
640
|
} catch {
|
|
500
641
|
return [];
|
|
501
642
|
}
|
|
@@ -521,7 +662,10 @@ function setHarnesses(ids) {
|
|
|
521
662
|
}
|
|
522
663
|
function allowTrackedLinks(root) {
|
|
523
664
|
const config = readConfig();
|
|
524
|
-
writeConfig({ ...config,
|
|
665
|
+
writeConfig({ ...config, allowTracked: [.../* @__PURE__ */ new Set([...config.allowTracked ?? [], root])].sort() });
|
|
666
|
+
}
|
|
667
|
+
function allowsTrackedLinks(root) {
|
|
668
|
+
return readConfig().allowTracked?.includes(root) ?? false;
|
|
525
669
|
}
|
|
526
670
|
function keptGlobal() {
|
|
527
671
|
return new Set(readConfig().keepGlobal ?? []);
|
|
@@ -538,17 +682,17 @@ function setKeepGlobal(names, keep) {
|
|
|
538
682
|
}
|
|
539
683
|
|
|
540
684
|
// src/agentSkill.ts
|
|
541
|
-
import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as
|
|
685
|
+
import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirSync6, readFileSync as readFileSync5, rmSync as rmSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
542
686
|
import { join as join9 } from "path";
|
|
543
687
|
|
|
544
688
|
// src/library.ts
|
|
545
|
-
import { execFileSync } from "child_process";
|
|
546
|
-
import { cpSync as cpSync2, existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as
|
|
547
|
-
import { basename as
|
|
689
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
690
|
+
import { cpSync as cpSync2, existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync4, readlinkSync as readlinkSync2, renameSync as renameSync3, rmSync as rmSync4, symlinkSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
691
|
+
import { basename as basename4, dirname as dirname3, isAbsolute, join as join8, posix, relative as relative2, resolve as resolve6, sep as sep4 } from "path";
|
|
548
692
|
|
|
549
693
|
// src/sources.ts
|
|
550
|
-
import { existsSync as existsSync6, lstatSync, mkdirSync as
|
|
551
|
-
import { basename as
|
|
694
|
+
import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readlinkSync } from "fs";
|
|
695
|
+
import { basename as basename3, join as join7, resolve as resolve5, sep as sep3 } from "path";
|
|
552
696
|
import { tmpdir } from "os";
|
|
553
697
|
function skillsShLock() {
|
|
554
698
|
return readJson(join7(userHome(), ".agents", ".skill-lock.json"))?.skills ?? {};
|
|
@@ -557,7 +701,7 @@ function projectSkillsLock(root) {
|
|
|
557
701
|
return readJson(join7(root, "skills-lock.json"))?.skills ?? {};
|
|
558
702
|
}
|
|
559
703
|
function vendorSkill(path, kind, origin, harnesses) {
|
|
560
|
-
return { name:
|
|
704
|
+
return { name: basename3(path), kind, origin, path, description: readSkillInfo(path).description, movable: false, broken: false, harnesses, links: [] };
|
|
561
705
|
}
|
|
562
706
|
function harnessesReading(dir, enabled) {
|
|
563
707
|
const real = realPath(dir);
|
|
@@ -588,7 +732,7 @@ function globalFolderSkills(enabled) {
|
|
|
588
732
|
const agentsDir = join7(userHome(), ".agents", "skills");
|
|
589
733
|
const entries = globalSkillDirs().flatMap((dir) => {
|
|
590
734
|
if (!isDir(dir)) return [];
|
|
591
|
-
return
|
|
735
|
+
return readdirSync4(dir).filter((name) => !name.startsWith(".") && name !== "synced").flatMap((name) => {
|
|
592
736
|
const path = join7(dir, name);
|
|
593
737
|
const isLink2 = lstatSync(path).isSymbolicLink();
|
|
594
738
|
const broken = !existsSync6(join7(path, "SKILL.md"));
|
|
@@ -656,7 +800,7 @@ function claudeAiSkills(enabled) {
|
|
|
656
800
|
byReal.set(real, entry);
|
|
657
801
|
}
|
|
658
802
|
return [...byReal.values()].flatMap(
|
|
659
|
-
({ dir, readers }) => (isDir(dir) ?
|
|
803
|
+
({ dir, readers }) => (isDir(dir) ? readdirSync4(dir) : []).filter((d) => !d.startsWith(".")).flatMap(
|
|
660
804
|
(bucket) => skillDirsIn(join7(dir, bucket)).map((path) => vendorSkill(path, "claude.ai", "claude.ai account", enabled.filter((id) => readers.includes(id))))
|
|
661
805
|
)
|
|
662
806
|
);
|
|
@@ -700,8 +844,8 @@ function pluginSkills(harnesses, root) {
|
|
|
700
844
|
return loadedBy.length ? skillDirsIn(join7(dir, "skills")).map((path) => ({ path, origin: id, loadedBy })) : [];
|
|
701
845
|
});
|
|
702
846
|
const syncedDir = join7(plugins, "synced");
|
|
703
|
-
const fromSynced = claude2 && existsSync6(syncedDir) ?
|
|
704
|
-
(bucket) =>
|
|
847
|
+
const fromSynced = claude2 && existsSync6(syncedDir) ? readdirSync4(syncedDir).filter((b) => !b.startsWith(".") && isDir(join7(syncedDir, b))).flatMap(
|
|
848
|
+
(bucket) => readdirSync4(join7(syncedDir, bucket), { withFileTypes: true }).filter((d) => d.isDirectory()).flatMap((d) => {
|
|
705
849
|
const root2 = join7(syncedDir, bucket, d.name);
|
|
706
850
|
const name = readJson(join7(root2, ".claude-plugin", "plugin.json"))?.name ?? d.name;
|
|
707
851
|
if (enabled[`${name}@synced`] === false) return [];
|
|
@@ -736,7 +880,7 @@ function libraryOrigins() {
|
|
|
736
880
|
function recordOrigin(name, origin) {
|
|
737
881
|
const origins = libraryOrigins();
|
|
738
882
|
if (origins[name] === origin) return;
|
|
739
|
-
|
|
883
|
+
mkdirSync4(skilllibHome(), { recursive: true });
|
|
740
884
|
writeState(originsFile(), { ...origins, [name]: origin });
|
|
741
885
|
}
|
|
742
886
|
function originFor(dir) {
|
|
@@ -749,14 +893,14 @@ function originFor(dir) {
|
|
|
749
893
|
if (match) return match.kind === "global" ? "global folder" : `${match.kind}: ${match.origin}`;
|
|
750
894
|
const project = dir.split(/[\\/]\.(?:claude|agents|grok|cursor|codex)[\\/]skills[\\/]/)[0];
|
|
751
895
|
if (!project || project === dir) return dir;
|
|
752
|
-
const locked = projectSkillsLock(project)[
|
|
753
|
-
return locked ? `skills.sh: ${locked}` : `project: ${
|
|
896
|
+
const locked = projectSkillsLock(project)[basename3(dir)]?.source;
|
|
897
|
+
return locked ? `skills.sh: ${locked}` : `project: ${basename3(project)}`;
|
|
754
898
|
}
|
|
755
899
|
|
|
756
900
|
// src/library.ts
|
|
757
901
|
function librarySkills() {
|
|
758
902
|
return skillDirsIn(libraryDir()).map((dir) => ({
|
|
759
|
-
name:
|
|
903
|
+
name: basename4(dir),
|
|
760
904
|
description: readSkillInfo(dir).description,
|
|
761
905
|
dir
|
|
762
906
|
}));
|
|
@@ -840,13 +984,6 @@ function visibilityOf(root, name, enabled = enabledHarnesses()) {
|
|
|
840
984
|
paths: harness(id).projectDirs.filter((dir) => existsSync7(join8(root, dir, name, "SKILL.md"))).length
|
|
841
985
|
}));
|
|
842
986
|
}
|
|
843
|
-
function realNative(p) {
|
|
844
|
-
try {
|
|
845
|
-
return realpathSync3.native(p);
|
|
846
|
-
} catch {
|
|
847
|
-
return null;
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
987
|
function survives(path, gone) {
|
|
851
988
|
return lostAt(path, gone) === null;
|
|
852
989
|
}
|
|
@@ -854,10 +991,10 @@ function lostAt(path, gone) {
|
|
|
854
991
|
const key = (p) => process.platform === "win32" ? p.toLowerCase() : p;
|
|
855
992
|
const canon = (p) => {
|
|
856
993
|
const dir = realNative(dirname3(p)) ?? dirname3(p);
|
|
857
|
-
const leaf =
|
|
994
|
+
const leaf = basename4(p);
|
|
858
995
|
try {
|
|
859
996
|
lstatSync2(join8(dir, leaf));
|
|
860
|
-
const names =
|
|
997
|
+
const names = readdirSync5(dir);
|
|
861
998
|
return join8(dir, names.includes(leaf) ? leaf : names.find((n) => n.toLowerCase() === leaf.toLowerCase()) ?? leaf);
|
|
862
999
|
} catch {
|
|
863
1000
|
return join8(dir, leaf);
|
|
@@ -925,11 +1062,11 @@ function blindWithoutGlobal(name, copies, roots, nestedOf = (root) => nestedEntr
|
|
|
925
1062
|
};
|
|
926
1063
|
return [...byWho.values()].filter((b) => !servedAbove(b));
|
|
927
1064
|
}
|
|
928
|
-
function strandedLinks(name, copies, roots, label =
|
|
1065
|
+
function strandedLinks(name, copies, roots, label = basename4) {
|
|
929
1066
|
const gone = copies.flatMap((c) => [c.path, ...c.links ?? []]);
|
|
930
1067
|
const unique = [...new Map(roots.map((r) => [realpathOrNull(r) ?? r, r])).values()];
|
|
931
1068
|
const seen = /* @__PURE__ */ new Set();
|
|
932
|
-
const once = (p) => ((k) => seen.has(k) ? false : (seen.add(k), true))(join8(realNative(dirname3(p)) ?? dirname3(p),
|
|
1069
|
+
const once = (p) => ((k) => seen.has(k) ? false : (seen.add(k), true))(join8(realNative(dirname3(p)) ?? dirname3(p), basename4(p)));
|
|
933
1070
|
return unique.flatMap((root) => [
|
|
934
1071
|
...ALL_PROJECT_DIRS.filter(
|
|
935
1072
|
(d) => writableDir(root, d) && isLink(join8(root, d, name)) && existsSync7(join8(root, d, name, "SKILL.md")) && !survives(join8(root, d, name), gone) && once(join8(root, d, name))
|
|
@@ -942,7 +1079,7 @@ function strandedLinks(name, copies, roots, label = basename3) {
|
|
|
942
1079
|
function copiesDiffer(paths) {
|
|
943
1080
|
return new Set(paths.map((p) => treeHash(p)).filter((h) => h !== null)).size > 1;
|
|
944
1081
|
}
|
|
945
|
-
function blindSaid(name, blind, label =
|
|
1082
|
+
function blindSaid(name, blind, label = basename4) {
|
|
946
1083
|
const who = blind.map((b) => `${harness(b.id).name} in ${label(b.root)}`).join(", ");
|
|
947
1084
|
const folders2 = [...new Set(blind.flatMap((b) => b.folder ? [`${b.folder} in ${label(b.root)}`] : []))];
|
|
948
1085
|
const links = [...new Set(blind.flatMap((b) => b.link ? [b.link.startsWith("~") || isAbsolute(b.link) ? b.link : `${b.link} in ${label(b.root)}`] : []))];
|
|
@@ -955,14 +1092,14 @@ function blindSaid(name, blind, label = basename3) {
|
|
|
955
1092
|
const shared = [...sharedBy.values()].map(
|
|
956
1093
|
(g) => `put a copy of ${name} in ${g.to} (${[...g.where].join(", ")} link${g.where.size === 1 ? "s" : ""} there), or point ${g.where.size === 1 ? `${[...g.dirs][0]} back into the repo` : "those folders back into their repos"}`
|
|
957
1094
|
);
|
|
958
|
-
const
|
|
1095
|
+
const repos2 = blind.filter((b) => !b.folder && !b.link && !b.shared);
|
|
959
1096
|
const fixes = [
|
|
960
1097
|
...folders2.length ? [`point ${folders2.join(", ")} back into the repo`] : [],
|
|
961
1098
|
// A package's link to the global copy: add or link at the repo's top doesn't touch it.
|
|
962
1099
|
...links.length ? [`give ${links.join(", ")} ${links.length === 1 ? "a copy of its own" : "each a copy of its own"} instead of a link to the global one`] : [],
|
|
963
1100
|
...shared,
|
|
964
1101
|
// Named, so with other advice beside it, it's clear which repo (or package) this is about.
|
|
965
|
-
...
|
|
1102
|
+
...repos2.length ? ((names) => [`give ${names.join(", ")} ${names.length === 1 ? "a copy of its own" : "their own copies"} (skilllib add, sync or link there)`])([...new Set(repos2.map((b) => label(b.root)))]) : []
|
|
966
1103
|
];
|
|
967
1104
|
return `${who} only ${blind.length === 1 ? "sees" : "see"} the global copy: ${fixes.join("; ")}, or keep ${name} global (skilllib global keep)`;
|
|
968
1105
|
}
|
|
@@ -997,7 +1134,7 @@ function pinsSkill(root, name) {
|
|
|
997
1134
|
}
|
|
998
1135
|
function isGitTracked(root, dir) {
|
|
999
1136
|
try {
|
|
1000
|
-
const out2 =
|
|
1137
|
+
const out2 = execFileSync2("git", ["ls-files", "--", dir], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
1001
1138
|
return out2.trim().length > 0;
|
|
1002
1139
|
} catch {
|
|
1003
1140
|
return false;
|
|
@@ -1024,7 +1161,8 @@ function projectStatus(root) {
|
|
|
1024
1161
|
latest: latest?.version ?? null,
|
|
1025
1162
|
location,
|
|
1026
1163
|
path,
|
|
1027
|
-
visibility: visibilityOf(root, name, enabled)
|
|
1164
|
+
visibility: visibilityOf(root, name, enabled),
|
|
1165
|
+
...dep.personal ? { personal: true } : {}
|
|
1028
1166
|
};
|
|
1029
1167
|
});
|
|
1030
1168
|
const seen = new Set(Object.keys(skills));
|
|
@@ -1033,7 +1171,7 @@ function projectStatus(root) {
|
|
|
1033
1171
|
const byName = /* @__PURE__ */ new Map();
|
|
1034
1172
|
for (const dir of ALL_PROJECT_DIRS.filter((d) => writableDir(root, d))) {
|
|
1035
1173
|
for (const path of skillDirsIn(join8(root, dir))) {
|
|
1036
|
-
const name =
|
|
1174
|
+
const name = basename4(path);
|
|
1037
1175
|
if (seen.has(name)) continue;
|
|
1038
1176
|
byName.set(name, [...byName.get(name) ?? [], path]);
|
|
1039
1177
|
}
|
|
@@ -1089,7 +1227,7 @@ function nestedEntries(root, { depth = PACKAGE_DEPTH, enabled = enabledHarnesses
|
|
|
1089
1227
|
const found = packageSkillFolders(root, subs ?? packageDirs(root, depth)).flatMap(
|
|
1090
1228
|
({ sub, skillsDir, location }) => skillDirsIn(join8(sub, skillsDir)).flatMap((path) => {
|
|
1091
1229
|
if (isLink(path) && (realpathOrNull(path) ?? "").startsWith(top + sep4)) return [];
|
|
1092
|
-
const name =
|
|
1230
|
+
const name = basename4(path);
|
|
1093
1231
|
const readers = NESTED_READERS.filter(([d]) => realpathOrNull(join8(sub, d, name)) === realpathOrNull(path)).flatMap(([, r]) => r);
|
|
1094
1232
|
return [{ name, path, location, skillsDir, readers: enabled.filter((id) => readers.includes(id)) }];
|
|
1095
1233
|
})
|
|
@@ -1102,7 +1240,7 @@ function packageDirs(root, depth = PACKAGE_DEPTH) {
|
|
|
1102
1240
|
const walk = (dir, left) => {
|
|
1103
1241
|
let subs = [];
|
|
1104
1242
|
try {
|
|
1105
|
-
subs =
|
|
1243
|
+
subs = readdirSync5(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && !BUILD_DIRS.has(d.name)).map((d) => join8(dir, d.name));
|
|
1106
1244
|
} catch {
|
|
1107
1245
|
return;
|
|
1108
1246
|
}
|
|
@@ -1128,11 +1266,14 @@ function sharedByGit(root, location, name) {
|
|
|
1128
1266
|
const copy = `${location}/${name}`;
|
|
1129
1267
|
if (isGitTracked(root, copy)) return true;
|
|
1130
1268
|
if (!isGitTracked(root, location)) return false;
|
|
1269
|
+
return !gitIgnores(root, copy);
|
|
1270
|
+
}
|
|
1271
|
+
function gitIgnores(root, rel) {
|
|
1131
1272
|
try {
|
|
1132
|
-
|
|
1133
|
-
return false;
|
|
1134
|
-
} catch {
|
|
1273
|
+
execFileSync2("git", ["check-ignore", "-q", "--no-index", "--", rel], { cwd: root, stdio: "ignore" });
|
|
1135
1274
|
return true;
|
|
1275
|
+
} catch {
|
|
1276
|
+
return false;
|
|
1136
1277
|
}
|
|
1137
1278
|
}
|
|
1138
1279
|
function linkEverywhere(root, name, location, { allowTracked = false, dryRun = false } = {}) {
|
|
@@ -1155,19 +1296,19 @@ function linkEverywhere(root, name, location, { allowTracked = false, dryRun = f
|
|
|
1155
1296
|
stuck(`${dir}/${name}`);
|
|
1156
1297
|
continue;
|
|
1157
1298
|
}
|
|
1158
|
-
if (isGitTracked(root, dir)) {
|
|
1299
|
+
if (isGitTracked(root, dir) && !gitIgnores(root, `${dir}/${name}`)) {
|
|
1159
1300
|
if (!(shared ??= sharedByGit(root, location, name))) {
|
|
1160
1301
|
if (!result.uncommitted.includes(dir)) result.uncommitted.push(dir);
|
|
1161
1302
|
continue;
|
|
1162
1303
|
}
|
|
1163
|
-
if (!allowTracked && !
|
|
1304
|
+
if (!allowTracked && !allowsTrackedLinks(root)) {
|
|
1164
1305
|
if (!result.blocked.includes(dir)) result.blocked.push(dir);
|
|
1165
1306
|
continue;
|
|
1166
1307
|
}
|
|
1167
1308
|
}
|
|
1168
1309
|
if (!dryRun) {
|
|
1169
1310
|
if (broken) unlinkSync(link2);
|
|
1170
|
-
|
|
1311
|
+
mkdirSync5(join8(root, dir), { recursive: true });
|
|
1171
1312
|
linkDir(join8(root, location, name), link2);
|
|
1172
1313
|
}
|
|
1173
1314
|
result.created.push(dir);
|
|
@@ -1186,11 +1327,12 @@ function relinkDependency(root, name, { allowTracked = false } = {}) {
|
|
|
1186
1327
|
const manifest = readManifest(root);
|
|
1187
1328
|
const dep = manifest.skills[name];
|
|
1188
1329
|
if (!dep) return { created: [], blocked: [], uncommitted: [], inTheWay: [] };
|
|
1330
|
+
if (dep.personal) excludePersonal(root, { ...personalOf(manifest), [name]: { ...dep, links: [...ALL_PROJECT_DIRS] } });
|
|
1189
1331
|
const result = linkEverywhere(root, name, dep.dir ?? PROJECT_SKILLS_DIR, { allowTracked });
|
|
1190
1332
|
if (result.created.length) {
|
|
1191
1333
|
manifest.skills[name] = { ...dep, links: [.../* @__PURE__ */ new Set([...dep.links ?? [], ...result.created])] };
|
|
1192
1334
|
writeManifest(root, manifest);
|
|
1193
|
-
}
|
|
1335
|
+
} else if (dep.personal) excludePersonal(root);
|
|
1194
1336
|
return result;
|
|
1195
1337
|
}
|
|
1196
1338
|
function linkAll(root, { allowTracked = false, names } = {}) {
|
|
@@ -1237,17 +1379,30 @@ function addRefusal(root, name, hash) {
|
|
|
1237
1379
|
const plan = addPlan(root, name, hash, false);
|
|
1238
1380
|
return "reason" in plan ? plan.reason : null;
|
|
1239
1381
|
}
|
|
1240
|
-
function addSkill(root, name, { force = false, version, allowTracked = false } = {}) {
|
|
1382
|
+
function addSkill(root, name, { force = false, version, allowTracked = false, personal } = {}) {
|
|
1241
1383
|
if (!name || name === "." || name === ".." || /[\\/]/.test(name)) return { name, action: "skipped", reason: "not a skill name" };
|
|
1242
|
-
const
|
|
1384
|
+
const before = personal !== void 0 && version === void 0 ? readManifest(root).skills[name] : void 0;
|
|
1385
|
+
const moving = !!before && !!before.personal !== personal;
|
|
1386
|
+
const keep = moving ? versionForHash(name, before.hash) : null;
|
|
1387
|
+
if (moving && !keep) return { name, action: "skipped", reason: `your library has no copy of the version it pins, so its pin can't move as it is: skilllib update ${name} first` };
|
|
1388
|
+
const target = keep ?? (version === void 0 ? latestVersion(name) : getVersion(name, version));
|
|
1243
1389
|
if (!target) return { name, action: "skipped", reason: version === void 0 ? "not in the library" : `version ${version} doesn't exist` };
|
|
1244
1390
|
const plan = addPlan(root, name, target.hash, force);
|
|
1245
1391
|
if ("reason" in plan) return { name, action: "skipped", reason: plan.reason };
|
|
1246
1392
|
const { manifest, recorded, primary, wantedLinks, to, local } = plan;
|
|
1393
|
+
const mine = pinsWhere(root, recorded, personal);
|
|
1394
|
+
if (typeof mine === "string") return { name, action: "skipped", reason: mine };
|
|
1395
|
+
const committed = [primary, ...recorded?.links ?? []].find((d) => isGitTracked(root, `${d}/${name}`));
|
|
1396
|
+
if (mine && !recorded?.personal && committed)
|
|
1397
|
+
return { name, action: "skipped", reason: `${committed}/${name} is committed to git, so it can't be personal: remove it from git first (git rm -r --cached '${`${committed}/${name}`.replace(/'/g, "'\\''")}')` };
|
|
1398
|
+
if (mine || recorded?.personal) {
|
|
1399
|
+
const { [name]: _, ...others } = personalOf(manifest);
|
|
1400
|
+
excludePersonal(root, mine ? { ...others, [name]: { version: target.version, hash: target.hash, dir: primary, links: wantedLinks } } : others);
|
|
1401
|
+
}
|
|
1247
1402
|
let backedUp;
|
|
1248
1403
|
const copied = local !== target.hash || isLink(to);
|
|
1249
1404
|
if (copied) {
|
|
1250
|
-
|
|
1405
|
+
mkdirSync5(join8(root, primary), { recursive: true });
|
|
1251
1406
|
if (isLink(to)) unlinkSync(to);
|
|
1252
1407
|
else if (local !== null && versionForHash(name, local) === null) backedUp = stash(to, "edit-backup");
|
|
1253
1408
|
copySkill(versionDir(name, target.version), to);
|
|
@@ -1258,11 +1413,19 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
1258
1413
|
const inTheWay = [];
|
|
1259
1414
|
let shared;
|
|
1260
1415
|
let relinked = false;
|
|
1416
|
+
const toShared = !!recorded?.personal && !mine;
|
|
1417
|
+
const heldBack = (dir) => {
|
|
1418
|
+
if (!isGitTracked(root, dir) || gitIgnores(root, `${dir}/${name}`)) return false;
|
|
1419
|
+
if (!(shared ??= sharedByGit(root, primary, name))) return uncommitted.push(dir), true;
|
|
1420
|
+
if (!allowTracked && !allowsTrackedLinks(root)) return blocked.push(dir), true;
|
|
1421
|
+
return false;
|
|
1422
|
+
};
|
|
1261
1423
|
for (const dir of wantedLinks) {
|
|
1262
1424
|
const link2 = join8(root, dir, name);
|
|
1263
1425
|
const slot = slotAt(link2, to);
|
|
1264
1426
|
if (slot === "ok") {
|
|
1265
|
-
|
|
1427
|
+
if (toShared && isLink(link2) && heldBack(dir)) unlinkSync(link2);
|
|
1428
|
+
else links.push(dir);
|
|
1266
1429
|
continue;
|
|
1267
1430
|
}
|
|
1268
1431
|
if (slot === "taken") {
|
|
@@ -1270,18 +1433,9 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
1270
1433
|
if (recorded?.links?.includes(dir)) links.push(dir);
|
|
1271
1434
|
continue;
|
|
1272
1435
|
}
|
|
1273
|
-
if (!recorded?.links?.includes(dir) &&
|
|
1274
|
-
if (!(shared ??= sharedByGit(root, primary, name))) {
|
|
1275
|
-
uncommitted.push(dir);
|
|
1276
|
-
continue;
|
|
1277
|
-
}
|
|
1278
|
-
if (!allowTracked && !readConfig().agentsDirOk?.includes(root)) {
|
|
1279
|
-
blocked.push(dir);
|
|
1280
|
-
continue;
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1436
|
+
if ((toShared || !recorded?.links?.includes(dir)) && heldBack(dir)) continue;
|
|
1283
1437
|
if (isLink(link2)) unlinkSync(link2);
|
|
1284
|
-
|
|
1438
|
+
mkdirSync5(join8(root, dir), { recursive: true });
|
|
1285
1439
|
linkDir(to, link2);
|
|
1286
1440
|
links.push(dir);
|
|
1287
1441
|
relinked = true;
|
|
@@ -1290,9 +1444,11 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
1290
1444
|
version: target.version,
|
|
1291
1445
|
hash: target.hash,
|
|
1292
1446
|
...primary !== PROJECT_SKILLS_DIR ? { dir: primary } : {},
|
|
1293
|
-
...links.length ? { links } : {}
|
|
1447
|
+
...links.length ? { links } : {},
|
|
1448
|
+
...mine ? { personal: true } : {}
|
|
1294
1449
|
};
|
|
1295
1450
|
writeManifest(root, manifest);
|
|
1451
|
+
const moved = recorded && !!recorded.personal !== mine ? mine ? "personal" : "shared" : void 0;
|
|
1296
1452
|
const from = recorded ? versionForHash(name, recorded.hash)?.version ?? recorded.version : void 0;
|
|
1297
1453
|
return {
|
|
1298
1454
|
name,
|
|
@@ -1308,9 +1464,16 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
1308
1464
|
...blocked.length ? { blocked } : {},
|
|
1309
1465
|
...uncommitted.length ? { uncommitted } : {},
|
|
1310
1466
|
...inTheWay.length ? { inTheWay } : {},
|
|
1311
|
-
...backedUp ? { backedUp } : {}
|
|
1467
|
+
...backedUp ? { backedUp } : {},
|
|
1468
|
+
...moved ? { moved } : {}
|
|
1312
1469
|
};
|
|
1313
1470
|
}
|
|
1471
|
+
function pinsWhere(root, recorded, personal) {
|
|
1472
|
+
const want = personal ?? (recorded ? !!recorded.personal : !existsSync7(join8(root, MANIFEST_FILE)));
|
|
1473
|
+
if (!want || personalPinsFile(root)) return want;
|
|
1474
|
+
return personal ? "only the top folder of a git repo can have personal pins" : false;
|
|
1475
|
+
}
|
|
1476
|
+
var personalOf = (manifest) => Object.fromEntries(Object.entries(manifest.skills).filter(([, d]) => d.personal));
|
|
1314
1477
|
function removeSkill(root, name, { force = false } = {}) {
|
|
1315
1478
|
const manifest = readManifest(root);
|
|
1316
1479
|
const recorded = manifest.skills[name];
|
|
@@ -1352,13 +1515,14 @@ function removeSkill(root, name, { force = false } = {}) {
|
|
|
1352
1515
|
};
|
|
1353
1516
|
unlinkOwn();
|
|
1354
1517
|
const backedUp = local !== null && !inLibrary ? stash(dir, "edit-backup") : void 0;
|
|
1355
|
-
if (!backedUp)
|
|
1518
|
+
if (!backedUp) rmSync4(dir, { recursive: true, force: true });
|
|
1356
1519
|
delete manifest.skills[name];
|
|
1357
1520
|
writeManifest(root, manifest);
|
|
1358
1521
|
const left = [...leftLinks, ...takenLinks];
|
|
1359
1522
|
return { name, action: "removed", ...left.length ? { reason: left.join("; ") } : {}, ...backedUp ? { backedUp } : {} };
|
|
1360
1523
|
}
|
|
1361
1524
|
function syncProject(root, { force = false, names } = {}) {
|
|
1525
|
+
excludePersonal(root);
|
|
1362
1526
|
return Object.keys(readManifest(root).skills).filter((name) => !names || names.includes(name)).flatMap((name) => syncSkill(root, name, { force }));
|
|
1363
1527
|
}
|
|
1364
1528
|
function syncSkill(root, name, { force = false } = {}) {
|
|
@@ -1374,7 +1538,7 @@ function syncSkill(root, name, { force = false } = {}) {
|
|
|
1374
1538
|
if (typeof version === "string") return [{ name, action: "skipped", reason: version }];
|
|
1375
1539
|
if (local !== null && local !== dep.hash && versionForHash(name, local) === null && !force) return [{ name, action: "skipped", reason: "has local edits (use --force to reset them)" }];
|
|
1376
1540
|
const backedUp = local !== null && versionForHash(name, local) === null ? stash(copy, "edit-backup") : void 0;
|
|
1377
|
-
|
|
1541
|
+
mkdirSync5(dirname3(copy), { recursive: true });
|
|
1378
1542
|
copySkill(versionDir(name, version), copy);
|
|
1379
1543
|
const links = relinkPinned(root, name, dep);
|
|
1380
1544
|
const from = (local !== null ? versionForHash(name, local)?.version : void 0) ?? version;
|
|
@@ -1396,7 +1560,7 @@ function relinkPinned(root, name, dep) {
|
|
|
1396
1560
|
if (realpathOrNull(link2) === realpathOrNull(to)) continue;
|
|
1397
1561
|
if (isLink(link2)) unlinkSync(link2);
|
|
1398
1562
|
else if (entryExists(link2)) return { name, action: "skipped", reason: `${dir}/${name} is in the way of its link: move it, then sync` };
|
|
1399
|
-
|
|
1563
|
+
mkdirSync5(join8(root, dir), { recursive: true });
|
|
1400
1564
|
linkDir(to, link2);
|
|
1401
1565
|
}
|
|
1402
1566
|
return { name, action: "relinked", to: versionForHash(name, dep.hash)?.version ?? dep.version };
|
|
@@ -1426,13 +1590,13 @@ function updateProject(root, names, { force = false, allowTracked = false } = {}
|
|
|
1426
1590
|
});
|
|
1427
1591
|
}
|
|
1428
1592
|
function importSkill(dir, { force = false } = {}) {
|
|
1429
|
-
const name =
|
|
1593
|
+
const name = basename4(dir);
|
|
1430
1594
|
const to = librarySkillDir(name);
|
|
1431
1595
|
const existing = treeHash(to);
|
|
1432
1596
|
const incoming = treeHash(dir);
|
|
1433
1597
|
if (existing === incoming) return { name, status: "unchanged" };
|
|
1434
1598
|
if (existing !== null && !force) return { name, status: "exists" };
|
|
1435
|
-
|
|
1599
|
+
mkdirSync5(libraryDir(), { recursive: true });
|
|
1436
1600
|
const origin = existing !== null && libraryOrigins()[name] ? null : originFor(dir);
|
|
1437
1601
|
if (existing !== null) {
|
|
1438
1602
|
forgetLatest(name);
|
|
@@ -1452,21 +1616,21 @@ function readBackupIndex(bucket) {
|
|
|
1452
1616
|
}
|
|
1453
1617
|
function move(from, to) {
|
|
1454
1618
|
try {
|
|
1455
|
-
|
|
1619
|
+
renameSync3(from, to);
|
|
1456
1620
|
} catch (err) {
|
|
1457
1621
|
if (err.code !== "EXDEV") throw err;
|
|
1458
1622
|
cpSync2(from, to, { recursive: true, verbatimSymlinks: true });
|
|
1459
|
-
|
|
1623
|
+
rmSync4(from, { recursive: true, force: true });
|
|
1460
1624
|
}
|
|
1461
1625
|
}
|
|
1462
1626
|
function stash(dir, bucket) {
|
|
1463
1627
|
let at = Date.now();
|
|
1464
|
-
const entryAt = (ms) => `${
|
|
1628
|
+
const entryAt = (ms) => `${basename4(dir)}-${new Date(ms).toISOString().replace(/[:.]/g, "-")}`;
|
|
1465
1629
|
while (entryExists(join8(skilllibHome(), bucket, entryAt(at)))) at++;
|
|
1466
1630
|
const entry = entryAt(at);
|
|
1467
1631
|
const target = join8(skilllibHome(), bucket, entry);
|
|
1468
1632
|
const index = readBackupIndex(bucket);
|
|
1469
|
-
|
|
1633
|
+
mkdirSync5(join8(skilllibHome(), bucket), { recursive: true });
|
|
1470
1634
|
move(dir, target);
|
|
1471
1635
|
writeState(backupIndexFile(bucket), { ...index, [entry]: dir });
|
|
1472
1636
|
return target;
|
|
@@ -1483,7 +1647,7 @@ function projectsUsing(name) {
|
|
|
1483
1647
|
}
|
|
1484
1648
|
function deleteLibrarySkill(name) {
|
|
1485
1649
|
const using = projectsUsing(name);
|
|
1486
|
-
if (using.length > 0) return { ok: false, reason: `still installed in ${using.map((p) =>
|
|
1650
|
+
if (using.length > 0) return { ok: false, reason: `still installed in ${using.map((p) => basename4(p)).join(", ")}` };
|
|
1487
1651
|
const dir = librarySkillDir(name);
|
|
1488
1652
|
if (!existsSync7(dir)) return { ok: false, reason: "not in the library" };
|
|
1489
1653
|
forgetLatest(name);
|
|
@@ -1493,7 +1657,7 @@ function deleteGlobal(path, links = []) {
|
|
|
1493
1657
|
return unloadGlobal(path, links, { requireLibrary: false });
|
|
1494
1658
|
}
|
|
1495
1659
|
function unloadGlobal(path, links = [], { requireLibrary = true } = {}) {
|
|
1496
|
-
const name =
|
|
1660
|
+
const name = basename4(path);
|
|
1497
1661
|
if (!entryExists(path) || name === "synced") return { ok: false, reason: "not a global skill" };
|
|
1498
1662
|
const broken = !existsSync7(join8(path, "SKILL.md"));
|
|
1499
1663
|
if (requireLibrary && !broken && latestVersion(name) === null) return { ok: false, reason: "import it into the library first" };
|
|
@@ -1503,7 +1667,7 @@ function unloadGlobal(path, links = [], { requireLibrary = true } = {}) {
|
|
|
1503
1667
|
return { ok: true, movedTo };
|
|
1504
1668
|
}
|
|
1505
1669
|
function linkGlobal(path, agents) {
|
|
1506
|
-
const name =
|
|
1670
|
+
const name = basename4(path);
|
|
1507
1671
|
const linked = [];
|
|
1508
1672
|
const skipped = [];
|
|
1509
1673
|
for (const dir of new Set(agents.map((id) => harness(id).globalDirs()[0]))) {
|
|
@@ -1513,7 +1677,7 @@ function linkGlobal(path, agents) {
|
|
|
1513
1677
|
if (realpathOrNull(link2) !== realpathOrNull(path)) skipped.push(link2);
|
|
1514
1678
|
continue;
|
|
1515
1679
|
}
|
|
1516
|
-
|
|
1680
|
+
mkdirSync5(dir, { recursive: true });
|
|
1517
1681
|
linkDir(realpathOrNull(path) ?? path, join8(realpathOrNull(dir) ?? dir, name));
|
|
1518
1682
|
linked.push(link2);
|
|
1519
1683
|
}
|
|
@@ -1523,8 +1687,8 @@ function createSkill(name, description) {
|
|
|
1523
1687
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) return { ok: false, reason: "use lowercase letters, digits and dashes" };
|
|
1524
1688
|
const dir = librarySkillDir(name);
|
|
1525
1689
|
if (existsSync7(dir)) return { ok: false, reason: `${name} already exists` };
|
|
1526
|
-
|
|
1527
|
-
|
|
1690
|
+
mkdirSync5(dir, { recursive: true });
|
|
1691
|
+
writeFileSync3(
|
|
1528
1692
|
join8(dir, "SKILL.md"),
|
|
1529
1693
|
// JSON strings are valid YAML: a description with ": " or "#" stays one value.
|
|
1530
1694
|
`---
|
|
@@ -1545,7 +1709,7 @@ function listBackups() {
|
|
|
1545
1709
|
const dir = join8(skilllibHome(), kind);
|
|
1546
1710
|
if (!existsSync7(dir)) return [];
|
|
1547
1711
|
const index = readBackupIndex(kind);
|
|
1548
|
-
return
|
|
1712
|
+
return readdirSync5(dir).flatMap((entry) => {
|
|
1549
1713
|
const match = entry.match(/^(.*)-(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-\d{3}Z$/);
|
|
1550
1714
|
if (!match) return [];
|
|
1551
1715
|
const name = match[1];
|
|
@@ -1568,9 +1732,9 @@ function restoreBackup(backup) {
|
|
|
1568
1732
|
const to = backup.from;
|
|
1569
1733
|
if (!to) return { ok: false, reason: `skilllib doesn't know where it came from; its files are in ${tildify(backup.path)}` };
|
|
1570
1734
|
if (backup.kind === "tidy-backup" && isLink(to)) unlinkSync(to);
|
|
1571
|
-
if (backup.kind === "edit-backup" && !isLink(to) && versionForHash(backup.name, treeHash(to) ?? "") !== null)
|
|
1735
|
+
if (backup.kind === "edit-backup" && !isLink(to) && versionForHash(backup.name, treeHash(to) ?? "") !== null) rmSync4(to, { recursive: true, force: true });
|
|
1572
1736
|
if (entryExists(to)) return { ok: false, reason: `${backup.name} already exists there` };
|
|
1573
|
-
|
|
1737
|
+
mkdirSync5(join8(to, ".."), { recursive: true });
|
|
1574
1738
|
move(backup.path, to);
|
|
1575
1739
|
forgetLatest(backup.name);
|
|
1576
1740
|
return { ok: true, to };
|
|
@@ -1606,13 +1770,13 @@ function installAgentSkill(enabled = enabledHarnesses()) {
|
|
|
1606
1770
|
const foreign = [primary, ...links].find((d) => entryExists(d) && !isOurs(d) && !isDangling(d));
|
|
1607
1771
|
if (foreign) return { ok: false, reason: `${tildify(foreign)} already exists and isn't skilllib's` };
|
|
1608
1772
|
for (const d of [primary, ...links]) if (isDangling(d)) unlinkSync2(d);
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1773
|
+
mkdirSync6(primary, { recursive: true });
|
|
1774
|
+
writeFileSync4(join9(primary, "SKILL.md"), shippedSkill());
|
|
1775
|
+
writeFileSync4(join9(primary, OWN_SKILL_MARKER), "Installed by skilllib. `skilllib agent-skill remove` removes it.\n");
|
|
1612
1776
|
for (const link2 of links) {
|
|
1613
|
-
if (entryExists(link2) && !isLink(link2) && isOurs(link2))
|
|
1777
|
+
if (entryExists(link2) && !isLink(link2) && isOurs(link2)) rmSync5(link2, { recursive: true, force: true });
|
|
1614
1778
|
if (entryExists(link2)) continue;
|
|
1615
|
-
|
|
1779
|
+
mkdirSync6(join9(link2, ".."), { recursive: true });
|
|
1616
1780
|
linkDir(primary, link2);
|
|
1617
1781
|
}
|
|
1618
1782
|
return { ok: true, dirs: [primary, ...links] };
|
|
@@ -1623,7 +1787,7 @@ function removeAgentSkill() {
|
|
|
1623
1787
|
for (const d of removed) {
|
|
1624
1788
|
if (!entryExists(d)) continue;
|
|
1625
1789
|
if (lstatSync3(d).isSymbolicLink()) unlinkSync2(d);
|
|
1626
|
-
else
|
|
1790
|
+
else rmSync5(d, { recursive: true, force: true });
|
|
1627
1791
|
}
|
|
1628
1792
|
return removed;
|
|
1629
1793
|
}
|
|
@@ -1637,18 +1801,18 @@ function refreshAgentSkill() {
|
|
|
1637
1801
|
// src/commands.ts
|
|
1638
1802
|
import { existsSync as existsSync14, readFileSync as readFileSync7 } from "fs";
|
|
1639
1803
|
import { parseArgs as parseFlags } from "util";
|
|
1640
|
-
import { basename as
|
|
1804
|
+
import { basename as basename10, join as join15, resolve as resolve8, sep as sep8 } from "path";
|
|
1641
1805
|
|
|
1642
1806
|
// src/git.ts
|
|
1643
|
-
import { execFileSync as
|
|
1644
|
-
import { existsSync as existsSync9, realpathSync as
|
|
1807
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1808
|
+
import { existsSync as existsSync9, realpathSync as realpathSync3 } from "fs";
|
|
1645
1809
|
import { isAbsolute as isAbsolute2, join as join10, relative as relative3, sep as sep5 } from "path";
|
|
1646
1810
|
function isShared(state) {
|
|
1647
1811
|
return state === "committed" || state === "changed";
|
|
1648
1812
|
}
|
|
1649
1813
|
function git(root, args, input) {
|
|
1650
1814
|
try {
|
|
1651
|
-
return
|
|
1815
|
+
return execFileSync3("git", args, { cwd: root, encoding: "utf-8", input, stdio: ["pipe", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 });
|
|
1652
1816
|
} catch (err) {
|
|
1653
1817
|
const out2 = err.stdout;
|
|
1654
1818
|
return typeof out2 === "string" ? out2 : null;
|
|
@@ -1657,14 +1821,14 @@ function git(root, args, input) {
|
|
|
1657
1821
|
function gitInfo(root, extraDirs = []) {
|
|
1658
1822
|
let inRepo;
|
|
1659
1823
|
try {
|
|
1660
|
-
inRepo =
|
|
1824
|
+
inRepo = execFileSync3("git", ["rev-parse", "--show-prefix"], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1661
1825
|
} catch {
|
|
1662
1826
|
if (!existsSync9(join10(root, ".git"))) return null;
|
|
1663
1827
|
inRepo = "";
|
|
1664
1828
|
}
|
|
1665
1829
|
const real = (p) => {
|
|
1666
1830
|
try {
|
|
1667
|
-
return
|
|
1831
|
+
return realpathSync3.native(p);
|
|
1668
1832
|
} catch {
|
|
1669
1833
|
return p;
|
|
1670
1834
|
}
|
|
@@ -1703,23 +1867,23 @@ function relativeTo(root, path) {
|
|
|
1703
1867
|
}
|
|
1704
1868
|
|
|
1705
1869
|
// src/here.ts
|
|
1706
|
-
import { readdirSync as
|
|
1707
|
-
import { basename as
|
|
1870
|
+
import { readdirSync as readdirSync9 } from "fs";
|
|
1871
|
+
import { basename as basename8 } from "path";
|
|
1708
1872
|
|
|
1709
1873
|
// src/health.ts
|
|
1710
|
-
import { existsSync as existsSync12, lstatSync as lstatSync4, readdirSync as
|
|
1711
|
-
import { basename as
|
|
1874
|
+
import { existsSync as existsSync12, lstatSync as lstatSync4, readdirSync as readdirSync8 } from "fs";
|
|
1875
|
+
import { basename as basename7, dirname as dirname6, isAbsolute as isAbsolute3, join as join13, relative as relative4, sep as sep7 } from "path";
|
|
1712
1876
|
|
|
1713
1877
|
// src/plugins.ts
|
|
1714
|
-
import { execFileSync as
|
|
1715
|
-
import { existsSync as existsSync10, mkdirSync as
|
|
1716
|
-
import { basename as
|
|
1878
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
1879
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync6, readFileSync as readFileSync6, statSync as statSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
1880
|
+
import { basename as basename5, dirname as dirname4, join as join11, resolve as resolve7 } from "path";
|
|
1717
1881
|
function pluginNameOf(id) {
|
|
1718
1882
|
return id.split("@")[0];
|
|
1719
1883
|
}
|
|
1720
1884
|
function nonEmptyDir(path) {
|
|
1721
1885
|
try {
|
|
1722
|
-
return
|
|
1886
|
+
return readdirSync6(path).some((f) => !f.startsWith("."));
|
|
1723
1887
|
} catch {
|
|
1724
1888
|
return false;
|
|
1725
1889
|
}
|
|
@@ -1810,23 +1974,23 @@ function installedPlugins() {
|
|
|
1810
1974
|
return root ? [{ id, scope: "user", synced: false, agent: "claude-code", on: true, settingsOnly: true, root, ...describe(root) }] : [];
|
|
1811
1975
|
});
|
|
1812
1976
|
const syncedDir = join11(plugins, "synced");
|
|
1813
|
-
const dirs = (p) => existsSync10(p) ?
|
|
1977
|
+
const dirs = (p) => existsSync10(p) ? readdirSync6(p, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => join11(p, d.name)) : [];
|
|
1814
1978
|
const fromSynced = dirs(syncedDir).flatMap(dirs).map((root) => {
|
|
1815
|
-
const id = `${readJson(join11(root, ".claude-plugin", "plugin.json"))?.name ??
|
|
1979
|
+
const id = `${readJson(join11(root, ".claude-plugin", "plugin.json"))?.name ?? basename5(root)}@synced`;
|
|
1816
1980
|
return { id, scope: null, synced: true, agent: "claude-code", on: user[id] !== false, root, ...describe(root) };
|
|
1817
1981
|
});
|
|
1818
1982
|
return [...fromMarketplaces, ...fromSettings, ...fromSynced];
|
|
1819
1983
|
}
|
|
1820
1984
|
function cursorPlugins() {
|
|
1821
1985
|
const cache = join11(userHome(), ".cursor", "plugins", "cache");
|
|
1822
|
-
const dirs = (p) => existsSync10(p) ?
|
|
1986
|
+
const dirs = (p) => existsSync10(p) ? readdirSync6(p).filter((d) => !d.startsWith(".") && statSync5(join11(p, d)).isDirectory()) : [];
|
|
1823
1987
|
return dirs(cache).flatMap(
|
|
1824
1988
|
(marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
|
|
1825
1989
|
const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
|
|
1826
|
-
const root = versions.sort((a, b) =>
|
|
1990
|
+
const root = versions.sort((a, b) => statSync5(b).mtimeMs - statSync5(a).mtimeMs)[0];
|
|
1827
1991
|
if (!root) return [];
|
|
1828
1992
|
const about = describe(root);
|
|
1829
|
-
return [{ id: `${plugin}@${marketplace}`, scope: null, synced: false, agent: "cursor", on: null, root, ...about, version: about.version ??
|
|
1993
|
+
return [{ id: `${plugin}@${marketplace}`, scope: null, synced: false, agent: "cursor", on: null, root, ...about, version: about.version ?? basename5(root) }];
|
|
1830
1994
|
})
|
|
1831
1995
|
);
|
|
1832
1996
|
}
|
|
@@ -1840,7 +2004,7 @@ function claudeBinary() {
|
|
|
1840
2004
|
const candidates = ["claude", join11(userHome(), ".local", "bin", "claude"), join11(userHome(), ".claude", "local", "claude")];
|
|
1841
2005
|
const found = candidates.find((bin) => {
|
|
1842
2006
|
try {
|
|
1843
|
-
return
|
|
2007
|
+
return execFileSync4(bin, ["--version"], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e4, encoding: "utf-8" }).includes("Claude Code");
|
|
1844
2008
|
} catch {
|
|
1845
2009
|
return false;
|
|
1846
2010
|
}
|
|
@@ -1854,7 +2018,7 @@ function claude(args, cwd, bin = claudeBinary()) {
|
|
|
1854
2018
|
if (!bin) return { ok: false, reason: `Claude Code's CLI didn't run (tried \`claude\` on your PATH and ~/.local/bin/claude); run \`claude ${args.join(" ")}\` yourself` };
|
|
1855
2019
|
const slow = args[1] === "install" || args[1] === "update";
|
|
1856
2020
|
try {
|
|
1857
|
-
const out2 =
|
|
2021
|
+
const out2 = execFileSync4(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: slow ? 12e4 : 2e4, encoding: "utf-8" });
|
|
1858
2022
|
return { ok: true, out: out2 };
|
|
1859
2023
|
} catch (err) {
|
|
1860
2024
|
const e = err;
|
|
@@ -1870,7 +2034,7 @@ function setPluginIn(file, id, on) {
|
|
|
1870
2034
|
}
|
|
1871
2035
|
function ignoreLocalSettings(root) {
|
|
1872
2036
|
const file = ".claude/settings.local.json";
|
|
1873
|
-
const git2 = (...args) =>
|
|
2037
|
+
const git2 = (...args) => execFileSync4("git", args, { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1874
2038
|
try {
|
|
1875
2039
|
git2("rev-parse", "--git-dir");
|
|
1876
2040
|
} catch {
|
|
@@ -1889,8 +2053,8 @@ function ignoreLocalSettings(root) {
|
|
|
1889
2053
|
try {
|
|
1890
2054
|
const exclude = resolve7(root, git2("rev-parse", "--git-path", "info/exclude"));
|
|
1891
2055
|
const before = existsSync10(exclude) ? readFileSync6(exclude, "utf-8") : "";
|
|
1892
|
-
|
|
1893
|
-
|
|
2056
|
+
mkdirSync7(dirname4(exclude), { recursive: true });
|
|
2057
|
+
writeFileSync5(exclude, `${before}${before && !before.endsWith("\n") ? "\n" : ""}/${git2("rev-parse", "--show-prefix")}${file}
|
|
1894
2058
|
`);
|
|
1895
2059
|
return ", which git now ignores via .git/info/exclude";
|
|
1896
2060
|
} catch {
|
|
@@ -1898,7 +2062,7 @@ function ignoreLocalSettings(root) {
|
|
|
1898
2062
|
}
|
|
1899
2063
|
}
|
|
1900
2064
|
function turnOffIn(plugin, root) {
|
|
1901
|
-
const done = `${plugin.id} turned off in ${
|
|
2065
|
+
const done = `${plugin.id} turned off in ${basename5(root)}; other repos keep it`;
|
|
1902
2066
|
const bin = plugin.synced || plugin.settingsOnly ? null : claudeBinary();
|
|
1903
2067
|
if (!bin) {
|
|
1904
2068
|
const r2 = setPluginIn(join11(root, ".claude", "settings.local.json"), plugin.id, false);
|
|
@@ -1917,7 +2081,7 @@ function settingsFileOf(p) {
|
|
|
1917
2081
|
return join11(p.projectPath, ".claude", p.scope === "local" ? "settings.local.json" : "settings.json");
|
|
1918
2082
|
}
|
|
1919
2083
|
function whereOf(p) {
|
|
1920
|
-
return p.projectPath ? ` in ${
|
|
2084
|
+
return p.projectPath ? ` in ${basename5(p.projectPath)} (${p.scope})` : "";
|
|
1921
2085
|
}
|
|
1922
2086
|
function setPluginOn(p, on) {
|
|
1923
2087
|
const shared = p.scope === "project" && p.projectPath ? "; it's in .claude/settings.json, which git tracks, so commit it if the team should get it" : "";
|
|
@@ -1931,7 +2095,7 @@ function setPluginOn(p, on) {
|
|
|
1931
2095
|
}
|
|
1932
2096
|
const r = setPluginIn(settingsFileOf(p), p.id, on);
|
|
1933
2097
|
if (!r.ok) return { ok: false, message: `${p.id}: ${r.reason}` };
|
|
1934
|
-
if (p.scope === "local" && p.projectPath) return { ...done, message: done.message
|
|
2098
|
+
if (p.scope === "local" && p.projectPath) return { ...done, message: `${done.message}; it's in .claude/settings.local.json${ignoreLocalSettings(p.projectPath)}` };
|
|
1935
2099
|
return done;
|
|
1936
2100
|
}
|
|
1937
2101
|
function uninstallPlugin(p) {
|
|
@@ -1966,31 +2130,20 @@ function removePlugin(plugin, how) {
|
|
|
1966
2130
|
if (plugin.settingsOnly && how === "delete") return { ok: false, message: `${plugin.id} isn't installed, only turned on in your settings: turn it off instead` };
|
|
1967
2131
|
const lost = unplaced(plugin, how === "delete" ? "uninstall" : "disable");
|
|
1968
2132
|
if (lost) return lost;
|
|
1969
|
-
const
|
|
1970
|
-
if (
|
|
1971
|
-
return { ok: false, message: `${plugin.id}: nothing changed; Claude Code's CLI didn't run (tried \`claude\` on your PATH and ~/.local/bin/claude)` };
|
|
1972
|
-
}
|
|
1973
|
-
const wasOn = enabledPlugins(plugin.projectPath)[plugin.id] === true;
|
|
2133
|
+
const uninstall = how === "delete" && !plugin.synced;
|
|
2134
|
+
if (uninstall && !claudeBinary()) return { ok: false, message: `${plugin.id}: nothing changed; Claude Code's CLI didn't run (tried \`claude\` on your PATH and ~/.local/bin/claude)` };
|
|
1974
2135
|
const yours = new Set(machineSkills().filter((m) => m.movable).map((m) => m.name));
|
|
1975
|
-
const imported = plugin.skills.filter((dir) => !yours.has(
|
|
1976
|
-
const scope = plugin.scope ? ["--scope", plugin.scope] : [];
|
|
1977
|
-
const r = viaSettings ? (
|
|
1978
|
-
// Synced and settings-only plugins aren't installed, so `claude plugin` can't touch them: their switch is enabledPlugins.
|
|
1979
|
-
setPluginIn(join11(claudeDir(), "settings.json"), plugin.id, false)
|
|
1980
|
-
) : (
|
|
1981
|
-
// --keep-data, as uninstallPlugin: its saved data stays, so a restore brings it back as it was.
|
|
1982
|
-
claude(["plugin", how === "delete" ? "uninstall" : "disable", plugin.id, ...how === "delete" ? ["--keep-data"] : [], ...scope], plugin.projectPath)
|
|
1983
|
-
);
|
|
2136
|
+
const imported = plugin.skills.filter((dir) => !yours.has(basename5(dir)) && latestVersion(basename5(dir)) === null).map((dir) => importSkill(dir).name);
|
|
1984
2137
|
const saved = imported.length ? `; ${imported.join(", ")} copied into Your skills` : "";
|
|
1985
|
-
|
|
1986
|
-
if (
|
|
1987
|
-
const others =
|
|
1988
|
-
const left = others.length ? `; its ${others.map((e) => `${e.scope ?? "user"} install${e.projectPath ? ` in ${
|
|
1989
|
-
return { ok: true, message: `${plugin.id}
|
|
2138
|
+
const r = uninstall ? uninstallPlugin(plugin) : setPluginOn(plugin, false);
|
|
2139
|
+
if (!r.ok) return { ok: false, message: `${r.message}${saved}` };
|
|
2140
|
+
const others = uninstall ? (installedPluginsJson()[plugin.id] ?? []).filter((e) => (e.scope ?? "user") !== (plugin.scope ?? "user") || e.projectPath !== plugin.projectPath) : [];
|
|
2141
|
+
const left = others.length ? `; its ${others.map((e) => `${e.scope ?? "user"} install${e.projectPath ? ` in ${basename5(e.projectPath)}` : ""}`).join(", ")} stays` : "";
|
|
2142
|
+
return { ok: true, message: uninstall ? `${plugin.id} removed (${plugin.scope ?? "user"} install)${saved}${left}` : `${r.message}${saved}` };
|
|
1990
2143
|
}
|
|
1991
2144
|
function recordRemovedPlugin(id, scope, projectPath, on = true) {
|
|
1992
2145
|
const list2 = readState(removedFile()) ?? [];
|
|
1993
|
-
|
|
2146
|
+
mkdirSync7(skilllibHome(), { recursive: true });
|
|
1994
2147
|
writeState(removedFile(), [...list2, { id, scope, ...projectPath ? { projectPath } : {}, on, at: (/* @__PURE__ */ new Date()).toISOString() }]);
|
|
1995
2148
|
}
|
|
1996
2149
|
function pluginBackups() {
|
|
@@ -2013,16 +2166,16 @@ function restorePlugin(backup) {
|
|
|
2013
2166
|
const wasOff = records.filter(same).pop()?.on === false;
|
|
2014
2167
|
const off = wasOff ? claude(["plugin", "disable", backup.from, "--scope", scope], projectPath) : null;
|
|
2015
2168
|
writeState(removedFile(), records.filter((x) => !same(x)));
|
|
2016
|
-
const where = `Claude Code plugins (${scope}${projectPath ? ` in ${
|
|
2169
|
+
const where = `Claude Code plugins (${scope}${projectPath ? ` in ${basename5(projectPath)}` : ""}${wasOff ? off?.ok ? ", off as it was" : ", but it's on: turn it off from Plugins" : ""})`;
|
|
2017
2170
|
return { ok: true, to: where };
|
|
2018
2171
|
}
|
|
2019
2172
|
function cursorPluginSkills(plugins = cursorPlugins()) {
|
|
2020
|
-
return plugins.flatMap((p) => p.skills.map((path) => ({ name:
|
|
2173
|
+
return plugins.flatMap((p) => p.skills.map((path) => ({ name: basename5(path), plugin: pluginNameOf(p.id), path })));
|
|
2021
2174
|
}
|
|
2022
2175
|
|
|
2023
2176
|
// src/tidy.ts
|
|
2024
|
-
import { existsSync as existsSync11, mkdirSync as
|
|
2025
|
-
import { basename as
|
|
2177
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as readdirSync7, unlinkSync as unlinkSync3 } from "fs";
|
|
2178
|
+
import { basename as basename6, dirname as dirname5, join as join12, sep as sep6 } from "path";
|
|
2026
2179
|
function entriesOf(dirs, name) {
|
|
2027
2180
|
return dirs.flatMap((dir) => {
|
|
2028
2181
|
const path = join12(dir, name);
|
|
@@ -2044,7 +2197,7 @@ function namesIn(dirs) {
|
|
|
2044
2197
|
const names = /* @__PURE__ */ new Set();
|
|
2045
2198
|
for (const dir of dirs) {
|
|
2046
2199
|
if (!isDir(dir)) continue;
|
|
2047
|
-
for (const name of
|
|
2200
|
+
for (const name of readdirSync7(dir)) if (!name.startsWith(".") && name !== "synced") names.add(name);
|
|
2048
2201
|
}
|
|
2049
2202
|
return [...names].sort();
|
|
2050
2203
|
}
|
|
@@ -2057,7 +2210,7 @@ function globalTidyDirs() {
|
|
|
2057
2210
|
function runsFrom(id, dirsWithCopy, readsOf) {
|
|
2058
2211
|
const reads = readsOf(id).filter((d) => dirsWithCopy.includes(d));
|
|
2059
2212
|
if (id === "codex") return reads;
|
|
2060
|
-
const pick = (d) => CURSOR_PICKS.indexOf(
|
|
2213
|
+
const pick = (d) => CURSOR_PICKS.indexOf(basename6(dirname5(d)));
|
|
2061
2214
|
return id === "cursor" ? [...reads].sort((a, b) => pick(a) - pick(b)).slice(0, 1) : reads.slice(0, 1);
|
|
2062
2215
|
}
|
|
2063
2216
|
function conflictOf(name, root, reals, enabled, readsOf) {
|
|
@@ -2168,7 +2321,7 @@ function gitVisibleSteps(plan, git2 = plan.root ? gitInfo(plan.root) : null) {
|
|
|
2168
2321
|
if (!plan.root || !git2) return [];
|
|
2169
2322
|
const shared = (rel) => isShared(git2.of(rel));
|
|
2170
2323
|
const dep = readManifest(plan.root).skills[plan.name];
|
|
2171
|
-
const recordsLink = (s) => !!dep && (s.kind === "link" || s.kind === "replace") && !dep.links?.includes(relativeTo(plan.root, dirname5(s.path)));
|
|
2324
|
+
const recordsLink = (s) => !!dep && !dep.personal && (s.kind === "link" || s.kind === "replace") && !dep.links?.includes(relativeTo(plan.root, dirname5(s.path)));
|
|
2172
2325
|
return plan.steps.filter((s) => shared(relativeTo(plan.root, s.path)) || recordsLink(s) && shared(MANIFEST_FILE));
|
|
2173
2326
|
}
|
|
2174
2327
|
function applyTidy(plan, { git: git2 = "keep" } = {}) {
|
|
@@ -2181,7 +2334,7 @@ function applyTidy(plan, { git: git2 = "keep" } = {}) {
|
|
|
2181
2334
|
if (isLink(step.path)) unlinkSync3(step.path);
|
|
2182
2335
|
} else stash(step.path, "tidy-backup");
|
|
2183
2336
|
if (step.kind === "link" || step.kind === "replace") {
|
|
2184
|
-
|
|
2337
|
+
mkdirSync8(dirname5(step.path), { recursive: true });
|
|
2185
2338
|
linkDir(step.target, step.path);
|
|
2186
2339
|
}
|
|
2187
2340
|
applied.push(step);
|
|
@@ -2244,7 +2397,7 @@ function track(root, name) {
|
|
|
2244
2397
|
if (c.action === "skipped") throw new Error(`${name}: ${c.reason}`);
|
|
2245
2398
|
return c;
|
|
2246
2399
|
}
|
|
2247
|
-
var installedIn = (p) => p.projectPath ? ` in ${
|
|
2400
|
+
var installedIn = (p) => p.projectPath ? ` in ${basename7(p.projectPath)}` : "";
|
|
2248
2401
|
var installId = (p) => `${p.projectPath ? `${p.projectPath}:` : ""}${p.id}`;
|
|
2249
2402
|
function summarize(where, verb, changes) {
|
|
2250
2403
|
const done = changes.filter((c) => c.action !== "skipped");
|
|
@@ -2266,7 +2419,7 @@ var gitNote = (root, paths) => {
|
|
|
2266
2419
|
};
|
|
2267
2420
|
var agentNames = (ids) => ids.map((id) => harness(id).name).join(", ");
|
|
2268
2421
|
function conflictIssue(c) {
|
|
2269
|
-
const where = c.root ?
|
|
2422
|
+
const where = c.root ? basename7(c.root) : "your global folders";
|
|
2270
2423
|
const winners = c.managed ? c.copies.filter((x) => x.dir === c.managed) : c.copies;
|
|
2271
2424
|
const planFor = (dir) => (c.root ? planProjectTidy(c.root, { keep: { [c.name]: dir } }) : planGlobalTidy({ keep: { [c.name]: dir } })).plans.find((p) => p.name === c.name);
|
|
2272
2425
|
const keep = (dir, git2) => () => {
|
|
@@ -2300,7 +2453,7 @@ function applyAll(plans, git2) {
|
|
|
2300
2453
|
return { skills: results.filter((r) => r.applied.length).length, held: results.reduce((n, r) => n + r.held.length, 0) };
|
|
2301
2454
|
}
|
|
2302
2455
|
function brokenLinksIn(root, opened) {
|
|
2303
|
-
const where =
|
|
2456
|
+
const where = basename7(root);
|
|
2304
2457
|
const tidyDirs = repoTidyDirs(root);
|
|
2305
2458
|
const hasCopy = (name) => tidyDirs.some((d) => !isLink(join13(root, d, name)) && existsSync12(join13(root, d, name, "SKILL.md")));
|
|
2306
2459
|
const mends = (project, dir, name) => {
|
|
@@ -2332,7 +2485,7 @@ function brokenLinksIn(root, opened) {
|
|
|
2332
2485
|
return folders2.flatMap(({ dir, top, own, real }) => {
|
|
2333
2486
|
const folder = join13(root, dir);
|
|
2334
2487
|
if (!isDir(folder) || notWritable(root, dir)) return [];
|
|
2335
|
-
return
|
|
2488
|
+
return readdirSync8(folder).flatMap((name) => {
|
|
2336
2489
|
const path = join13(folder, name);
|
|
2337
2490
|
if (!isLink(path) || isDir(path) || top && (dir !== GROK_SKILLS_DIR && hasCopy(name) || syncMends(dir, name))) return [];
|
|
2338
2491
|
const remove2 = {
|
|
@@ -2371,24 +2524,24 @@ function grokDiffers(projects2, machine, statusOf) {
|
|
|
2371
2524
|
const issue = (root, name, grokCopy, other) => ({
|
|
2372
2525
|
id: `grok-differs:${root ?? "global"}:${name}`,
|
|
2373
2526
|
severity: "suggestion",
|
|
2374
|
-
title: `${name}${root ? ` in ${
|
|
2527
|
+
title: `${name}${root ? ` in ${basename7(root)}` : ""}: Grok runs a different copy`,
|
|
2375
2528
|
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.`,
|
|
2376
2529
|
at: rows(root, [name])
|
|
2377
2530
|
});
|
|
2378
2531
|
const grokGlobal = join13(grokDir(), "skills");
|
|
2379
2532
|
const global = skillDirsIn(grokGlobal).filter((path) => !isLink(path)).flatMap((path) => {
|
|
2380
|
-
const name =
|
|
2533
|
+
const name = basename7(path);
|
|
2381
2534
|
const other = machine.find((m) => m.name === name && m.movable && !m.broken && m.harnesses.some((id) => id !== "grok") && realPath(dirname6(m.path)) !== realPath(grokGlobal));
|
|
2382
2535
|
return other && treeHash(other.path) !== treeHash(path) ? [issue(null, name, tildify(path), tildify(other.path))] : [];
|
|
2383
2536
|
});
|
|
2384
|
-
const
|
|
2537
|
+
const repos2 = projects2.flatMap(
|
|
2385
2538
|
(root) => skillDirsIn(join13(root, GROK_SKILLS_DIR)).filter((path) => !isLink(path)).flatMap((path) => {
|
|
2386
|
-
const name =
|
|
2539
|
+
const name = basename7(path);
|
|
2387
2540
|
const other = statusOf(root).find((s) => s.name === name && !s.path.startsWith(join13(root, GROK_SKILLS_DIR)));
|
|
2388
2541
|
return other && treeHash(other.path) !== treeHash(path) ? [issue(root, name, `${GROK_SKILLS_DIR}/${name}`, relativeTo(root, other.path))] : [];
|
|
2389
2542
|
})
|
|
2390
2543
|
);
|
|
2391
|
-
return [...global, ...
|
|
2544
|
+
return [...global, ...repos2];
|
|
2392
2545
|
}
|
|
2393
2546
|
function findIssues(roots, machine, libraryNames, statusOf = projectStatus, loadedIn = (root) => skillsLoadedIn(root, machine), nestedOf = nestedSkills) {
|
|
2394
2547
|
gitMemo = /* @__PURE__ */ new Map();
|
|
@@ -2396,8 +2549,8 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2396
2549
|
const issues = unreadable.map(({ root, reason }) => ({
|
|
2397
2550
|
id: `manifest:${root}`,
|
|
2398
2551
|
severity: "problem",
|
|
2399
|
-
title: `${
|
|
2400
|
-
detail: `${reason}. Until it's fixed, skilllib leaves ${
|
|
2552
|
+
title: `${basename7(root)}: skilllib.json can't be read`,
|
|
2553
|
+
detail: `${reason}. Until it's fixed, skilllib leaves ${basename7(root)} out.`,
|
|
2401
2554
|
at: [{ root, skill: "" }]
|
|
2402
2555
|
}));
|
|
2403
2556
|
const statuses = new Map(projects2.map((root) => [root, statusOf(root)]));
|
|
@@ -2448,7 +2601,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2448
2601
|
const globalYours = new Set(loaded.filter((m) => m.movable).map((m) => m.name));
|
|
2449
2602
|
const installedYours = /* @__PURE__ */ new Set([...globalYours, ...[...statuses.values()].flat().map((s) => s.name), ...knownProjects().flatMap((p) => Object.keys(pinsIn(p)))]);
|
|
2450
2603
|
const repoChoices = (plugin, root, names) => {
|
|
2451
|
-
const where =
|
|
2604
|
+
const where = basename7(root);
|
|
2452
2605
|
const copies = statuses.get(root).filter((s) => names.includes(s.name));
|
|
2453
2606
|
const allManaged = names.every((n) => copies.some((c) => c.name === n && removesClean(root, c)));
|
|
2454
2607
|
const committed = gitNote(root, copies.map((s) => s.path)) !== "";
|
|
@@ -2474,7 +2627,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2474
2627
|
};
|
|
2475
2628
|
const machinePlugins = claudePlugins(machine);
|
|
2476
2629
|
for (const plugin of machinePlugins) {
|
|
2477
|
-
const names = plugin.skills.map((p) =>
|
|
2630
|
+
const names = plugin.skills.map((p) => basename7(p));
|
|
2478
2631
|
const inRepos = projects2.filter((root) => pluginAgentsIn(plugin, root).has("claude-code")).map((root) => [root, namesIn2(root).filter((n) => names.includes(n))]).filter(([, d]) => d.length);
|
|
2479
2632
|
const cursorRepos = enabledHarnesses().includes("claude-code") ? projects2.filter((root) => !pluginAgentsIn(plugin, root).has("claude-code") && pluginAgentsIn(plugin, root).has("cursor")).map((root) => [root, namesIn2(root, "cursor").filter((n) => names.includes(n))]).filter(([, d]) => d.length) : [];
|
|
2480
2633
|
const claudeYours = /* @__PURE__ */ new Set([...globalYours, ...inRepos.flatMap(([, d]) => d)]);
|
|
@@ -2491,7 +2644,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2491
2644
|
const off = { label: `Turn the plugin ${plugin.id} off`, hint: "stays installed", run: () => orThrow(removePlugin(plugin, "off")) };
|
|
2492
2645
|
const bothLoad = dupes.filter((n) => loadedYours.has(n));
|
|
2493
2646
|
const claudeBoth = dupes.filter((n) => claudeYours.has(n));
|
|
2494
|
-
const cursorWheres = cursorRepos.map(([root]) =>
|
|
2647
|
+
const cursorWheres = cursorRepos.map(([root]) => basename7(root));
|
|
2495
2648
|
const notInstalled = dupes.filter((n) => !installedYours.has(n));
|
|
2496
2649
|
issues.push({
|
|
2497
2650
|
id: `plugin:${plugin.id}`,
|
|
@@ -2505,7 +2658,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2505
2658
|
plugin.extras.length ? `It also brings ${plugin.extras.join(", ")}: ${plugin.synced || plugin.settingsOnly ? "turning it off pauses those" : "removing it drops those, turning it off pauses them"}.` : "",
|
|
2506
2659
|
plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : "",
|
|
2507
2660
|
plugin.settingsOnly ? "It's only turned on in your settings, not installed, so it can only be turned off here." : "",
|
|
2508
|
-
inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) =>
|
|
2661
|
+
inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) => basename7(root)).join(", ")}.` : ""
|
|
2509
2662
|
].filter(Boolean).join(" "),
|
|
2510
2663
|
choices: [...plugin.synced || plugin.settingsOnly ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off], ...inRepos.flatMap(([root, d]) => repoChoices(plugin, root, d))],
|
|
2511
2664
|
// Global's rows (yours and the plugin's), and the repos where both load.
|
|
@@ -2528,10 +2681,10 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2528
2681
|
}
|
|
2529
2682
|
const machinePluginSkills = new Set(machinePlugins.flatMap((p) => p.skills));
|
|
2530
2683
|
for (const root of projects2) {
|
|
2531
|
-
const where =
|
|
2684
|
+
const where = basename7(root);
|
|
2532
2685
|
const repoOnly = loadedHere.get(root).filter((m) => m.kind === "plugin" && !machinePluginSkills.has(m.path));
|
|
2533
2686
|
for (const plugin of enabledHarnesses().includes("claude-code") ? claudePlugins(repoOnly, root) : []) {
|
|
2534
|
-
const names = plugin.skills.map((p) =>
|
|
2687
|
+
const names = plugin.skills.map((p) => basename7(p));
|
|
2535
2688
|
const inRepo = namesIn2(root).filter((n) => names.includes(n));
|
|
2536
2689
|
const dupes = names.filter((n) => inRepo.includes(n) || globalYours.has(n));
|
|
2537
2690
|
if (!dupes.length) continue;
|
|
@@ -2621,385 +2774,8 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2621
2774
|
const alsoGlobal = /* @__PURE__ */ new Map();
|
|
2622
2775
|
const globalCopies = loaded.filter((m) => m.kind !== "plugin");
|
|
2623
2776
|
const alsoLoadedGlobally = (s) => globalCopies.some((m) => m.name === s.name && m.harnesses.some((id) => s.visibility.some((v) => v.id === id && v.paths > 0)));
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
const status2 = statuses.get(root);
|
|
2627
|
-
issues.push(...brokenLinksIn(root, openedProjects));
|
|
2628
|
-
const pins = readManifest(root).skills;
|
|
2629
|
-
const blockedFor = /* @__PURE__ */ new Map();
|
|
2630
|
-
const untrackable = (s) => {
|
|
2631
|
-
if (!blockedFor.has(s.name)) blockedFor.set(s.name, trackBlocker(root, s.name));
|
|
2632
|
-
return blockedFor.get(s.name);
|
|
2633
|
-
};
|
|
2634
|
-
const blockers = new Map(status2.filter((s) => s.managed).flatMap((s) => ((b) => b ? [[s.name, b]] : [])(syncBlocker(root, s.name, pins[s.name]))));
|
|
2635
|
-
for (const [name, blocker] of blockers)
|
|
2636
|
-
issues.push({
|
|
2637
|
-
id: `stuck:${root}:${name}`,
|
|
2638
|
-
severity: "problem",
|
|
2639
|
-
title: `${name} in ${where}: sync can't put it back as skilllib.json pins it`,
|
|
2640
|
-
// What to do depends on what's there: a folder that links out of the repo is a setup of yours, and a pin
|
|
2641
|
-
// your library has no version of can't come back by sync anyway.
|
|
2642
|
-
detail: `${blocker.said}, so sync can't put it back there.${blocker.outside ? " skilllib only installs inside the repo: point that folder back into the repo, or remove the skill here (skilllib remove)." : versionForHash(name, pins[name]?.hash ?? "") !== null ? " Move it out of the way, then run skilllib sync." : " Move it out of the way; your library has no copy of the pinned version either."}`,
|
|
2643
|
-
at: rows(root, [name])
|
|
2644
|
-
});
|
|
2645
|
-
const unstuck = status2.filter((s) => !blockers.has(s.name));
|
|
2646
|
-
const missing = unstuck.filter((s) => s.state === "folder missing" && s.latest !== null);
|
|
2647
|
-
const knownPin = (s) => versionForHash(s.name, pins[s.name]?.hash ?? "") !== null;
|
|
2648
|
-
const restorable = missing.filter(knownPin);
|
|
2649
|
-
for (const s of unstuck.filter((m) => (m.state === "folder missing" || m.state === "out of sync") && m.latest !== null && !knownPin(m)))
|
|
2650
|
-
issues.push({
|
|
2651
|
-
id: `pin:${root}:${s.name}`,
|
|
2652
|
-
severity: "problem",
|
|
2653
|
-
title: `${s.name}: the version ${where}'s skilllib.json pins isn't in your library`,
|
|
2654
|
-
detail: `${s.state === "folder missing" ? "The folder is missing, and no" : "The copy is another version, and no"} version in Your skills has the content it pins (pinned from another library?). ${untrackable(s) ? `${untrackable(s)}, so skilllib can't install it here.` : `Install your newest v${s.latest}; ${where}'s skilllib.json then pins it.`}`,
|
|
2655
|
-
at: rows(root, [s.name]),
|
|
2656
|
-
// It changes what the shared skilllib.json pins: a choice, never run by `doctor --fix`.
|
|
2657
|
-
...untrackable(s) ? {} : { 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}` }] }
|
|
2658
|
-
});
|
|
2659
|
-
if (restorable.length > 0) {
|
|
2660
|
-
issues.push({
|
|
2661
|
-
id: `sync:${root}`,
|
|
2662
|
-
severity: "problem",
|
|
2663
|
-
title: `${where}: ${plural(restorable.length, "skill")} in skilllib.json not installed`,
|
|
2664
|
-
detail: `${restorable.map((s) => s.name).join(", ")}: the folder is missing`,
|
|
2665
|
-
at: rows(root, restorable.map((s) => s.name)),
|
|
2666
|
-
fix: {
|
|
2667
|
-
label: "Sync the project",
|
|
2668
|
-
// Only the missing folders: an out-of-sync copy (maybe committed) is its own issue's choice.
|
|
2669
|
-
run: () => summarize(where, "restored", syncProject(root, { names: restorable.map((s) => s.name) }))
|
|
2670
|
-
}
|
|
2671
|
-
});
|
|
2672
|
-
}
|
|
2673
|
-
const drifted = unstuck.filter((s) => s.state === "out of sync" && knownPin(s));
|
|
2674
|
-
if (drifted.length) {
|
|
2675
|
-
const seen = drifted.some((s) => isShared(gitOf(root)?.of(relativeTo(root, s.path))));
|
|
2676
|
-
const yours = drifted.some((s) => untrackable(s));
|
|
2677
|
-
const install = {
|
|
2678
|
-
label: "Install the pinned versions",
|
|
2679
|
-
hint: seen ? "git will see the copies change" : "your library keeps every version",
|
|
2680
|
-
run: () => summarize(where, "synced", syncProject(root, { names: drifted.map((s) => s.name) }))
|
|
2681
|
-
};
|
|
2682
|
-
issues.push({
|
|
2683
|
-
id: `out-of-sync:${root}`,
|
|
2684
|
-
severity: "suggestion",
|
|
2685
|
-
title: `${where}: ${plural(drifted.length, "skill")} not at the version skilllib.json pins`,
|
|
2686
|
-
detail: `${drifted.map((s) => s.name).join(", ")}: each copy is another version from your library. sync installs the pinned one.${yours ? ` ${[...new Set(drifted.flatMap((s) => untrackable(s) ? [untrackable(s)] : []))].map((why) => `${drifted.filter((s) => untrackable(s) === why).map((s) => s.name).join(", ")}: ${why}`).join("; ")}, so skilllib can't pin the version that's there; ${drifted.filter((s) => untrackable(s)).length === 1 ? "it may be edits you copied into Your skills on purpose, and stays" : "they may be edits you copied into Your skills on purpose, and stay"} unless you choose this.` : ""}`,
|
|
2687
|
-
at: rows(root, drifted.map((s) => s.name)),
|
|
2688
|
-
...seen || yours ? { choices: [install] } : { fix: install }
|
|
2689
|
-
});
|
|
2690
|
-
}
|
|
2691
|
-
for (const s of unstuck.filter((s2) => s2.state === "folder missing" && s2.latest === null))
|
|
2692
|
-
issues.push({
|
|
2693
|
-
id: `unavailable:${root}:${s.name}`,
|
|
2694
|
-
severity: "problem",
|
|
2695
|
-
title: `${s.name}: pinned in ${where}'s skilllib.json, but not in your library`,
|
|
2696
|
-
detail: `Its folder is missing, and your library (${tildify(libraryDir())}) has no copy to restore: import it, or set SKILLLIB_HOME to the skilllib folder whose library has it.`,
|
|
2697
|
-
at: rows(root, [s.name]),
|
|
2698
|
-
// Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
|
|
2699
|
-
choices: [
|
|
2700
|
-
{
|
|
2701
|
-
label: "Remove it from skilllib.json",
|
|
2702
|
-
hint: `${where} stops pinning it`,
|
|
2703
|
-
run: () => {
|
|
2704
|
-
const r = removeSkill(root, s.name);
|
|
2705
|
-
if (r.action !== "removed") throw new Error(`${s.name}: ${r.reason}`);
|
|
2706
|
-
return `${s.name}: removed from ${where}'s skilllib.json`;
|
|
2707
|
-
}
|
|
2708
|
-
}
|
|
2709
|
-
]
|
|
2710
|
-
});
|
|
2711
|
-
const behind = unstuck.filter((s) => s.state === "update available" && !untrackable(s));
|
|
2712
|
-
if (behind.length > 0)
|
|
2713
|
-
issues.push({
|
|
2714
|
-
id: `update:${root}`,
|
|
2715
|
-
severity: "suggestion",
|
|
2716
|
-
title: `${where}: ${plural(behind.length, "skill")} out of date`,
|
|
2717
|
-
detail: behind.map((s) => `${s.name} (v${s.version} \u2192 v${s.latest})`).join(", "),
|
|
2718
|
-
at: rows(root, behind.map((s) => s.name)),
|
|
2719
|
-
// It changes what the repo pins and the copies its agents load (often committed): a choice, never `doctor --fix`.
|
|
2720
|
-
choices: [
|
|
2721
|
-
{
|
|
2722
|
-
label: "Update to the newest versions",
|
|
2723
|
-
hint: isShared(gitOf(root)?.of(MANIFEST_FILE)) ? "git will see the new copies and skilllib.json" : "replaces each copy with your library's newest",
|
|
2724
|
-
run: () => summarize(where, "updated", updateProject(root, behind.map((s) => s.name)))
|
|
2725
|
-
}
|
|
2726
|
-
]
|
|
2727
|
-
});
|
|
2728
|
-
const tidy2 = planProjectTidy(root);
|
|
2729
|
-
if (tidy2.plans.length) {
|
|
2730
|
-
const git3 = gitOf(root);
|
|
2731
|
-
const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p, git3).map((s) => describeStep(s, root)));
|
|
2732
|
-
const run = (mode) => () => {
|
|
2733
|
-
const r = applyAll(tidy2.plans, mode);
|
|
2734
|
-
if (!r.skills) throw new Error(`${where}: nothing tidied${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`);
|
|
2735
|
-
return `${where}: tidied ${plural(r.skills, "skill")}${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`;
|
|
2736
|
-
};
|
|
2737
|
-
issues.push({
|
|
2738
|
-
id: `tidy:${root}`,
|
|
2739
|
-
severity: "suggestion",
|
|
2740
|
-
title: `${where}: ${plural(tidy2.plans.length, "skill")} with extra copies or links`,
|
|
2741
|
-
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(", ")}.` : tidy2.plans.some((p) => p.steps.some((s) => s.kind === "replace" || s.kind === "remove")) ? ". Replaced copies are restorable with skilllib restore." : "."),
|
|
2742
|
-
at: rows(root, tidy2.plans.map((p) => p.name)),
|
|
2743
|
-
...visible2.length ? {
|
|
2744
|
-
choices: [
|
|
2745
|
-
// "Keep git as it is" only when it leaves something to do.
|
|
2746
|
-
...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") }] : [],
|
|
2747
|
-
{ label: "Tidy everything", hint: `${plural(visible2.length, "change")} will show in git status`, run: run("go") }
|
|
2748
|
-
]
|
|
2749
|
-
} : { fix: { label: "Tidy", run: run("go") } }
|
|
2750
|
-
});
|
|
2751
|
-
}
|
|
2752
|
-
issues.push(...tidy2.conflicts.map(conflictIssue));
|
|
2753
|
-
for (const s of tidy2.skipped.filter((x) => x.differs))
|
|
2754
|
-
issues.push({
|
|
2755
|
-
id: `linkdiffers:${root}:${s.name}`,
|
|
2756
|
-
severity: "problem",
|
|
2757
|
-
title: `${s.name} in ${where}: the copies differ`,
|
|
2758
|
-
detail: `${s.reason}, with other content than the copy beside it: your agents run different versions. Make them the same, or remove the link; skilllib doesn't change a link to another copy.`,
|
|
2759
|
-
at: rows(root, [s.name])
|
|
2760
|
-
});
|
|
2761
|
-
const loadsGlobally = (name, id) => {
|
|
2762
|
-
const here = status2.find((s) => s.name === name);
|
|
2763
|
-
return globalCopies.some((m) => m.name === name && m.harnesses.includes(id) && (!here || treeHash(m.path) === treeHash(here.path)));
|
|
2764
|
-
};
|
|
2765
|
-
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 }) }));
|
|
2766
|
-
const enabledIds = enabledHarnesses();
|
|
2767
|
-
const notFolders = /* @__PURE__ */ new Set();
|
|
2768
|
-
for (const dir of new Set(enabledIds.map((id) => harness(id).projectDirs[0]))) {
|
|
2769
|
-
const blocker = notWritable(root, dir);
|
|
2770
|
-
if (!blocker || notFolders.has(blocker)) continue;
|
|
2771
|
-
notFolders.add(blocker);
|
|
2772
|
-
const who = agentNames(enabledIds.filter((id) => notWritable(root, harness(id).projectDirs[0]) === blocker));
|
|
2773
|
-
const out2 = linksOut(root, blocker);
|
|
2774
|
-
issues.push({
|
|
2775
|
-
id: `notdir:${root}:${blocker}`,
|
|
2776
|
-
severity: out2 ? "suggestion" : "problem",
|
|
2777
|
-
title: out2 ? `${where}: ${blocker} links out of the repo` : `${where}: ${blocker} isn't a folder`,
|
|
2778
|
-
detail: out2 ? `${blockerSaid(root, blocker)}. ${who} ${who.includes(",") || who.includes(" and ") ? "load" : "loads"} what's there, but skilllib treats it as outside ${where}: it adds, links and removes nothing there.` : `${blockerSaid(root, blocker)}: ${who} can't load skills from ${where}, and skilllib adds none there. Move it out of the way, then run skilllib link.`,
|
|
2779
|
-
at: [{ root, skill: "" }]
|
|
2780
|
-
// the repo itself: its row in Places
|
|
2781
|
-
});
|
|
2782
|
-
}
|
|
2783
|
-
for (const { s, would } of blindHere) {
|
|
2784
|
-
const way = would.inTheWay.filter((p) => !notFolders.has(p));
|
|
2785
|
-
if (!way.length || blockers.has(s.name)) continue;
|
|
2786
|
-
issues.push({
|
|
2787
|
-
id: `intheway:${root}:${s.name}`,
|
|
2788
|
-
severity: "problem",
|
|
2789
|
-
title: `${s.name} in ${where}: ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))} can't use it here`,
|
|
2790
|
-
detail: `${inTheWaySaid(root, way)}. skilllib won't link past what's there: clear the way, then run skilllib link.`,
|
|
2791
|
-
at: rows(root, [s.name])
|
|
2792
|
-
});
|
|
2793
|
-
}
|
|
2794
|
-
for (const { s, would } of blindHere.filter(({ would: w }) => !w.created.length && !w.blocked.length && w.uncommitted.length))
|
|
2795
|
-
issues.push({
|
|
2796
|
-
id: `uncommitted:${root}:${s.name}`,
|
|
2797
|
-
severity: "suggestion",
|
|
2798
|
-
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`,
|
|
2799
|
-
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\`).`,
|
|
2800
|
-
at: rows(root, [s.name])
|
|
2801
|
-
});
|
|
2802
|
-
const partial = blindHere.filter(({ would }) => would.created.length || would.blocked.length).map(({ s }) => s);
|
|
2803
|
-
if (partial.length) {
|
|
2804
|
-
const missing2 = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
|
|
2805
|
-
const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
|
|
2806
|
-
const manifestShared = partial.some((s) => s.managed) && isShared(gitOf(root)?.of(MANIFEST_FILE));
|
|
2807
|
-
const run = (allowTracked) => () => {
|
|
2808
|
-
if (allowTracked) allowTrackedLinks(root);
|
|
2809
|
-
const r = linkAll(root, { allowTracked, names: partial.map((s) => s.name) });
|
|
2810
|
-
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)` : ""}`;
|
|
2811
|
-
if (!r.linked.length) {
|
|
2812
|
-
const still = partial.filter((s) => visibilityOf(root, s.name).some((v) => v.paths === 0 && !loadsGlobally(s.name, v.id)));
|
|
2813
|
-
if (!still.length) return `${where}: already usable by ${agentNames(missing2)}`;
|
|
2814
|
-
throw new Error(`${where}: nothing linked${notes}`);
|
|
2815
|
-
}
|
|
2816
|
-
return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing2)}${notes}`;
|
|
2817
|
-
};
|
|
2818
|
-
issues.push({
|
|
2819
|
-
id: `usable:${root}`,
|
|
2820
|
-
severity: "suggestion",
|
|
2821
|
-
title: `${where}: ${plural(partial.length, "skill")} not usable by ${agentNames(missing2)}`,
|
|
2822
|
-
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.` : ""}${manifestShared ? " The new links are recorded in skilllib.json, which git tracks." : ""}`,
|
|
2823
|
-
at: rows(root, partial.map((s) => s.name)),
|
|
2824
|
-
...manifestShared && !tracked.length ? { choices: [{ label: "Add the links", hint: "git will see skilllib.json change", run: run(false) }] } : tracked.length ? {
|
|
2825
|
-
choices: [
|
|
2826
|
-
// Only when some link goes where git doesn't track: otherwise this one would link nothing.
|
|
2827
|
-
...blindHere.some(({ would }) => would.created.length) ? [{ label: "Add links, but not where git tracks the folder", hint: `skips ${tracked.join(", ")}`, run: run(false) }] : [],
|
|
2828
|
-
{ label: "Add links everywhere", hint: `remembered for ${where}; git will see them`, run: run(true) }
|
|
2829
|
-
]
|
|
2830
|
-
} : { fix: { label: "Add the links", run: run(false) } }
|
|
2831
|
-
});
|
|
2832
|
-
}
|
|
2833
|
-
const differing = new Set(tidy2.conflicts.map((c) => c.name));
|
|
2834
|
-
const git2 = gitOf(root);
|
|
2835
|
-
const committed = (s) => isShared(git2?.of(relativeTo(root, s.path)));
|
|
2836
|
-
for (const s of unstuck) {
|
|
2837
|
-
if (isLink(s.path) || differing.has(s.name)) continue;
|
|
2838
|
-
if (s.state === "local only" && (committed(s) || untrackable(s))) {
|
|
2839
|
-
const why = committed(s) ? "it's the team's (committed)" : `${untrackable(s)}, so skilllib can't track it here`;
|
|
2840
|
-
issues.push({
|
|
2841
|
-
id: `local:${root}:${s.name}`,
|
|
2842
|
-
severity: "suggestion",
|
|
2843
|
-
title: `${s.name} in ${where} isn't in your library`,
|
|
2844
|
-
detail: `Copy it into the library to reuse it in other projects. The repo's copy stays as it is: ${why}.`,
|
|
2845
|
-
at: rows(root, [s.name]),
|
|
2846
|
-
choices: [
|
|
2847
|
-
{
|
|
2848
|
-
label: "Copy into the library",
|
|
2849
|
-
hint: `${where}'s copy stays as it is`,
|
|
2850
|
-
run: () => {
|
|
2851
|
-
intoLibrary(s.path);
|
|
2852
|
-
return `${s.name} copied into your library; ${where}'s copy stays as it is`;
|
|
2853
|
-
}
|
|
2854
|
-
}
|
|
2855
|
-
]
|
|
2856
|
-
});
|
|
2857
|
-
} else if (s.state === "local only") {
|
|
2858
|
-
issues.push({
|
|
2859
|
-
id: `local:${root}:${s.name}`,
|
|
2860
|
-
severity: "suggestion",
|
|
2861
|
-
title: `${s.name} in ${where} isn't in your library`,
|
|
2862
|
-
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.",
|
|
2863
|
-
at: rows(root, [s.name]),
|
|
2864
|
-
choices: [
|
|
2865
|
-
{
|
|
2866
|
-
label: "Import into the library",
|
|
2867
|
-
// Tracking it writes skilllib.json.
|
|
2868
|
-
hint: `copies it into Your skills${gitNote(root, [join13(root, MANIFEST_FILE)])}`,
|
|
2869
|
-
run: () => {
|
|
2870
|
-
intoLibrary(s.path);
|
|
2871
|
-
track(root, s.name);
|
|
2872
|
-
return `${s.name} imported and tracked in ${where}`;
|
|
2873
|
-
}
|
|
2874
|
-
}
|
|
2875
|
-
]
|
|
2876
|
-
});
|
|
2877
|
-
} else if (s.state === "untracked copy of library skill" && !committed(s) && !untrackable(s)) {
|
|
2878
|
-
issues.push({
|
|
2879
|
-
id: `adopt:${root}:${s.name}`,
|
|
2880
|
-
severity: "suggestion",
|
|
2881
|
-
title: `${s.name} in ${where} isn't tracked`,
|
|
2882
|
-
detail: "It's identical to the library copy; track it (in skilllib.json) so future library changes reach it.",
|
|
2883
|
-
at: rows(root, [s.name]),
|
|
2884
|
-
choices: [{ label: "Track it", hint: `adds it to ${where}'s skilllib.json`, run: () => `${track(root, s.name).name} tracked in ${where}` }]
|
|
2885
|
-
});
|
|
2886
|
-
} else if (s.state === "untracked, differs from library" || s.state === "repo skill, differs from library") {
|
|
2887
|
-
const teams = s.state.startsWith("repo skill") || committed(s) || !!untrackable(s);
|
|
2888
|
-
const whose = s.state.startsWith("repo skill") || committed(s) ? "It's the team's copy (committed, or in the repo's own .agents/skills or .grok/skills)" : `${untrackable(s)}, so skilllib can't track it here`;
|
|
2889
|
-
issues.push({
|
|
2890
|
-
id: `differs:${root}:${s.name}`,
|
|
2891
|
-
severity: "suggestion",
|
|
2892
|
-
title: `${s.name} in ${where} differs from your library`,
|
|
2893
|
-
detail: teams ? `${whose}, 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.`,
|
|
2894
|
-
at: rows(root, [s.name]),
|
|
2895
|
-
choices: [
|
|
2896
|
-
{
|
|
2897
|
-
label: "Update your library from it",
|
|
2898
|
-
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`,
|
|
2899
|
-
run: () => {
|
|
2900
|
-
intoLibrary(s.path, true);
|
|
2901
|
-
if (teams) return `${s.name}: your library has ${where}'s copy; ${where}'s copy stays as it is`;
|
|
2902
|
-
const c = track(root, s.name);
|
|
2903
|
-
return `${s.name}: your library has ${where}'s copy as v${c.to}; ${where} tracks it`;
|
|
2904
|
-
}
|
|
2905
|
-
},
|
|
2906
|
-
...teams ? [] : [
|
|
2907
|
-
{
|
|
2908
|
-
label: "Replace it with your library's",
|
|
2909
|
-
hint: "this copy is restorable with skilllib restore",
|
|
2910
|
-
run: () => {
|
|
2911
|
-
const c = addSkill(root, s.name, { force: true });
|
|
2912
|
-
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
2913
|
-
return `${s.name} in ${where} is your library's v${c.to} now; the old copy is restorable with skilllib restore`;
|
|
2914
|
-
}
|
|
2915
|
-
}
|
|
2916
|
-
]
|
|
2917
|
-
]
|
|
2918
|
-
});
|
|
2919
|
-
} else if (s.state === "from npx skills" && !((committed(s) || untrackable(s)) && versionForHash(s.name, treeHash(s.path) ?? "") !== null)) {
|
|
2920
|
-
const teams = committed(s) || !!untrackable(s);
|
|
2921
|
-
issues.push({
|
|
2922
|
-
id: `npx:${root}:${s.name}`,
|
|
2923
|
-
severity: "suggestion",
|
|
2924
|
-
title: `${s.name} in ${where} came from npx skills, not your library`,
|
|
2925
|
-
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(s) ? "committed" : `${untrackable(s)}, so skilllib can't track it here`})` : `; ${where} then tracks it in skilllib.json`}.`,
|
|
2926
|
-
at: rows(root, [s.name]),
|
|
2927
|
-
choices: [
|
|
2928
|
-
{
|
|
2929
|
-
label: "Import it into your library",
|
|
2930
|
-
hint: teams ? `${where}'s copy stays as it is` : `${where} then tracks it`,
|
|
2931
|
-
run: () => {
|
|
2932
|
-
intoLibrary(s.path);
|
|
2933
|
-
if (teams) return `${s.name} copied into your library; ${where}'s copy stays as it is`;
|
|
2934
|
-
track(root, s.name);
|
|
2935
|
-
return `${s.name} imported and tracked in ${where}`;
|
|
2936
|
-
}
|
|
2937
|
-
}
|
|
2938
|
-
]
|
|
2939
|
-
});
|
|
2940
|
-
}
|
|
2941
|
-
if (s.state === "not in library")
|
|
2942
|
-
issues.push({
|
|
2943
|
-
id: `not-in-lib:${root}:${s.name}`,
|
|
2944
|
-
severity: "suggestion",
|
|
2945
|
-
title: `${s.name} in ${where} is tracked, but no longer in your library`,
|
|
2946
|
-
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.`,
|
|
2947
|
-
at: rows(root, [s.name]),
|
|
2948
|
-
choices: [
|
|
2949
|
-
{
|
|
2950
|
-
label: "Copy it back into your library",
|
|
2951
|
-
hint: `${where}'s copy stays as it is`,
|
|
2952
|
-
run: () => {
|
|
2953
|
-
intoLibrary(s.path);
|
|
2954
|
-
return `${s.name} copied back into your library`;
|
|
2955
|
-
}
|
|
2956
|
-
}
|
|
2957
|
-
]
|
|
2958
|
-
});
|
|
2959
|
-
if (s.state === "edited locally" || s.state === "edited locally, update available") {
|
|
2960
|
-
const newer2 = s.state !== "edited locally";
|
|
2961
|
-
const pinned = s.version ? `v${s.version}` : "the library version";
|
|
2962
|
-
issues.push({
|
|
2963
|
-
id: `edited:${root}:${s.name}`,
|
|
2964
|
-
severity: "suggestion",
|
|
2965
|
-
title: `${s.name} in ${where} has local edits`,
|
|
2966
|
-
detail: `It no longer matches ${pinned} from Your skills${newer2 ? `, and v${s.latest} is out` : ""}. ${untrackable(s) ? `${untrackable(s)}, so skilllib can't re-pin or discard them here; copy them into Your skills to keep them for your other repos.` : "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."}`,
|
|
2967
|
-
at: rows(root, [s.name]),
|
|
2968
|
-
choices: untrackable(s) ? [
|
|
2969
|
-
{
|
|
2970
|
-
label: "Copy the edits into Your skills",
|
|
2971
|
-
hint: `${newer2 ? `becomes v${s.latest + 1}; v${s.latest}'s changes aren't merged in` : "a new version there"}; ${where}'s copy and pin stay as they are (skilllib sync there would put the pinned version back)`,
|
|
2972
|
-
run: () => {
|
|
2973
|
-
intoLibrary(s.path, true);
|
|
2974
|
-
return `${s.name}: the edits are in Your skills as v${latestVersion(s.name)?.version}; ${where} stays as it is`;
|
|
2975
|
-
}
|
|
2976
|
-
}
|
|
2977
|
-
] : [
|
|
2978
|
-
{
|
|
2979
|
-
label: "Save as a new version in Your skills",
|
|
2980
|
-
hint: `${newer2 ? `becomes v${s.latest + 1}; v${s.latest}'s changes aren't merged in` : `becomes v${(s.latest ?? 0) + 1}; ${where} uses it`}${gitNote(root, [join13(root, MANIFEST_FILE)])}`,
|
|
2981
|
-
run: () => {
|
|
2982
|
-
importSkill(s.path, { force: true });
|
|
2983
|
-
const c = addSkill(root, s.name);
|
|
2984
|
-
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
2985
|
-
return `${s.name}: saved as v${c.to}; ${where} uses it`;
|
|
2986
|
-
}
|
|
2987
|
-
},
|
|
2988
|
-
{
|
|
2989
|
-
label: "Discard the edits",
|
|
2990
|
-
hint: `back to ${pinned}; the edited copy is restorable with skilllib restore${gitNote(root, [s.path, join13(root, MANIFEST_FILE)])}`,
|
|
2991
|
-
run: () => {
|
|
2992
|
-
const c = discardEdits(root, s.name);
|
|
2993
|
-
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
2994
|
-
return `${s.name}: edits discarded, back to v${c.to}`;
|
|
2995
|
-
}
|
|
2996
|
-
}
|
|
2997
|
-
]
|
|
2998
|
-
});
|
|
2999
|
-
}
|
|
3000
|
-
if (alsoLoadedGlobally(s)) alsoGlobal.set(s.name, [...alsoGlobal.get(s.name) ?? [], { root, managed: s.managed }]);
|
|
3001
|
-
}
|
|
3002
|
-
}
|
|
2777
|
+
const ctx = { issues, statuses, openedProjects, globalCopies, alsoLoadedGlobally, alsoGlobal };
|
|
2778
|
+
for (const root of projects2) repoIssues(root, ctx);
|
|
3003
2779
|
for (const root of projects2) {
|
|
3004
2780
|
for (const n of nested.get(root)) {
|
|
3005
2781
|
const copies = alsoGlobal.get(n.name) ?? [];
|
|
@@ -3012,23 +2788,23 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
3012
2788
|
const subsOf = (root) => subsMemo.get(root) ?? (subsMemo.set(root, packageDirs(root)), subsMemo.get(root));
|
|
3013
2789
|
const entriesOf2 = (root) => nestedMemo.get(root) ?? (nestedMemo.set(root, nestedEntries(root, { subs: subsOf(root) })), nestedMemo.get(root));
|
|
3014
2790
|
for (const [name, copies] of alsoGlobal) {
|
|
3015
|
-
const wheres = copies.map((c) =>
|
|
2791
|
+
const wheres = copies.map((c) => basename7(c.root)).join(", ");
|
|
3016
2792
|
const removable = copies.filter((c) => c.managed && statuses.get(c.root)?.some((s) => s.name === name && removesClean(c.root, s))).map((c) => c.root);
|
|
3017
|
-
const repoOwn = copies.filter((c) => !c.managed).map((c) =>
|
|
2793
|
+
const repoOwn = copies.filter((c) => !c.managed).map((c) => basename7(c.root));
|
|
3018
2794
|
const at = [...copies.flatMap((c) => rows(c.root, [name])), ...rows(null, [name])];
|
|
3019
2795
|
if (kept.has(name)) {
|
|
3020
2796
|
const globalLoaders = new Set(globalCopies.filter((m) => m.name === name).flatMap((m) => m.harnesses));
|
|
3021
2797
|
const onlyHere = (root) => visibilityOf(root, name).filter((v) => v.paths > 0 && !globalLoaders.has(v.id)).map((v) => harness(v.id).name);
|
|
3022
2798
|
const needed = removable.filter((root) => onlyHere(root).length);
|
|
3023
2799
|
const extra = removable.filter((root) => !onlyHere(root).length);
|
|
3024
|
-
const removeWheres = extra.map((r) =>
|
|
2800
|
+
const removeWheres = extra.map((r) => basename7(r)).join(", ");
|
|
3025
2801
|
issues.push({
|
|
3026
2802
|
id: `twice:${name}`,
|
|
3027
2803
|
severity: "suggestion",
|
|
3028
2804
|
title: `${name}: in ${wheres}, and you keep it global`,
|
|
3029
2805
|
detail: [
|
|
3030
2806
|
extra.length ? `The global copy already loads in ${removeWheres} for every agent that reads the project copy, so those are extra.` : "",
|
|
3031
|
-
needed.length ? `In ${needed.map((r) => `${
|
|
2807
|
+
needed.length ? `In ${needed.map((r) => `${basename7(r)} (${onlyHere(r).join(", ")})`).join(", ")}, some agents read only the repo's copy: keep it there, or link the global one for them.` : "",
|
|
3032
2808
|
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.` : ""
|
|
3033
2809
|
].filter(Boolean).join(" "),
|
|
3034
2810
|
at,
|
|
@@ -3039,8 +2815,8 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
3039
2815
|
hint: `the global copy stays${extra.some((r) => gitNote(r, [...statuses.get(r).filter((s) => s.name === name).map((s) => s.path), join13(r, MANIFEST_FILE)])) ? "; git will see it" : ""}`,
|
|
3040
2816
|
run: () => {
|
|
3041
2817
|
const results = extra.map((root) => [root, removeSkill(root, name)]);
|
|
3042
|
-
const removed = results.filter(([, c]) => c.action === "removed").map(([r]) =>
|
|
3043
|
-
const kept2 = results.filter(([, c]) => c.action !== "removed").map(([r, c]) => `${
|
|
2818
|
+
const removed = results.filter(([, c]) => c.action === "removed").map(([r]) => basename7(r));
|
|
2819
|
+
const kept2 = results.filter(([, c]) => c.action !== "removed").map(([r, c]) => `${basename7(r)} (${c.reason})`);
|
|
3044
2820
|
if (kept2.length) throw new Error(`${name}: ${removed.length ? `removed from ${removed.join(", ")}; ` : ""}kept in ${kept2.join(", ")}`);
|
|
3045
2821
|
return `${name}: removed from ${removed.join(", ")}; the global copy stays`;
|
|
3046
2822
|
}
|
|
@@ -3101,6 +2877,404 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
3101
2877
|
}
|
|
3102
2878
|
return issues;
|
|
3103
2879
|
}
|
|
2880
|
+
function repoIssues(root, ctx) {
|
|
2881
|
+
const where = basename7(root);
|
|
2882
|
+
const status2 = ctx.statuses.get(root);
|
|
2883
|
+
ctx.issues.push(...brokenLinksIn(root, ctx.openedProjects));
|
|
2884
|
+
const pins = readManifest(root).skills;
|
|
2885
|
+
const shadowed = Object.keys(personalPins(root)).filter((name) => pins[name] && !pins[name].personal);
|
|
2886
|
+
if (shadowed.length)
|
|
2887
|
+
ctx.issues.push({
|
|
2888
|
+
id: `shadowed:${root}`,
|
|
2889
|
+
severity: "suggestion",
|
|
2890
|
+
title: `${where}: ${plural(shadowed.length, "skill")} pinned in both skilllib.json and your personal pins`,
|
|
2891
|
+
detail: `${shadowed.join(", ")}: ${where}'s skilllib.json pins ${shadowed.length === 1 ? "it" : "them"} for the team, and that pin wins. Your personal pin does nothing.`,
|
|
2892
|
+
at: rows(root, shadowed),
|
|
2893
|
+
choices: [
|
|
2894
|
+
{
|
|
2895
|
+
label: "Drop the personal pins",
|
|
2896
|
+
hint: "skilllib.json's pins stay; nothing that loads changes",
|
|
2897
|
+
run: () => {
|
|
2898
|
+
for (const name of shadowed) dropPersonalPin(root, name);
|
|
2899
|
+
return `${where}: ${shadowed.join(", ")} now pinned only in skilllib.json`;
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
]
|
|
2903
|
+
});
|
|
2904
|
+
const blockedFor = /* @__PURE__ */ new Map();
|
|
2905
|
+
const untrackable = (s) => {
|
|
2906
|
+
if (!blockedFor.has(s.name)) blockedFor.set(s.name, trackBlocker(root, s.name));
|
|
2907
|
+
return blockedFor.get(s.name);
|
|
2908
|
+
};
|
|
2909
|
+
const blockers = new Map(status2.filter((s) => s.managed).flatMap((s) => ((b) => b ? [[s.name, b]] : [])(syncBlocker(root, s.name, pins[s.name]))));
|
|
2910
|
+
for (const [name, blocker] of blockers)
|
|
2911
|
+
ctx.issues.push({
|
|
2912
|
+
id: `stuck:${root}:${name}`,
|
|
2913
|
+
severity: "problem",
|
|
2914
|
+
title: `${name} in ${where}: sync can't put it back as skilllib.json pins it`,
|
|
2915
|
+
// What to do depends on what's there: a folder that links out of the repo is a setup of yours, and a pin
|
|
2916
|
+
// your library has no version of can't come back by sync anyway.
|
|
2917
|
+
detail: `${blocker.said}, so sync can't put it back there.${blocker.outside ? " skilllib only installs inside the repo: point that folder back into the repo, or remove the skill here (skilllib remove)." : versionForHash(name, pins[name]?.hash ?? "") !== null ? " Move it out of the way, then run skilllib sync." : " Move it out of the way; your library has no copy of the pinned version either."}`,
|
|
2918
|
+
at: rows(root, [name])
|
|
2919
|
+
});
|
|
2920
|
+
const unstuck = status2.filter((s) => !blockers.has(s.name));
|
|
2921
|
+
const missing = unstuck.filter((s) => s.state === "folder missing" && s.latest !== null);
|
|
2922
|
+
const knownPin = (s) => versionForHash(s.name, pins[s.name]?.hash ?? "") !== null;
|
|
2923
|
+
const restorable = missing.filter(knownPin);
|
|
2924
|
+
for (const s of unstuck.filter((m) => (m.state === "folder missing" || m.state === "out of sync") && m.latest !== null && !knownPin(m)))
|
|
2925
|
+
ctx.issues.push({
|
|
2926
|
+
id: `pin:${root}:${s.name}`,
|
|
2927
|
+
severity: "problem",
|
|
2928
|
+
title: `${s.name}: the version ${where}'s skilllib.json pins isn't in your library`,
|
|
2929
|
+
detail: `${s.state === "folder missing" ? "The folder is missing, and no" : "The copy is another version, and no"} version in Your skills has the content it pins (pinned from another library?). ${untrackable(s) ? `${untrackable(s)}, so skilllib can't install it here.` : `Install your newest v${s.latest}; ${where}'s skilllib.json then pins it.`}`,
|
|
2930
|
+
at: rows(root, [s.name]),
|
|
2931
|
+
// It changes what the shared skilllib.json pins: a choice, never run by `doctor --fix`.
|
|
2932
|
+
...untrackable(s) ? {} : { 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}` }] }
|
|
2933
|
+
});
|
|
2934
|
+
if (restorable.length > 0) {
|
|
2935
|
+
ctx.issues.push({
|
|
2936
|
+
id: `sync:${root}`,
|
|
2937
|
+
severity: "problem",
|
|
2938
|
+
title: `${where}: ${plural(restorable.length, "skill")} pinned but not installed`,
|
|
2939
|
+
detail: `${restorable.map((s) => s.name).join(", ")}: the folder is missing`,
|
|
2940
|
+
at: rows(root, restorable.map((s) => s.name)),
|
|
2941
|
+
fix: {
|
|
2942
|
+
label: "Sync the project",
|
|
2943
|
+
// Only the missing folders: an out-of-sync copy (maybe committed) is its own issue's choice.
|
|
2944
|
+
run: () => summarize(where, "restored", syncProject(root, { names: restorable.map((s) => s.name) }))
|
|
2945
|
+
}
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
const drifted = unstuck.filter((s) => s.state === "out of sync" && knownPin(s));
|
|
2949
|
+
if (drifted.length) {
|
|
2950
|
+
const seen = drifted.some((s) => isShared(gitOf(root)?.of(relativeTo(root, s.path))));
|
|
2951
|
+
const yours = drifted.some((s) => untrackable(s));
|
|
2952
|
+
const install = {
|
|
2953
|
+
label: "Install the pinned versions",
|
|
2954
|
+
hint: seen ? "git will see the copies change" : "your library keeps every version",
|
|
2955
|
+
run: () => summarize(where, "synced", syncProject(root, { names: drifted.map((s) => s.name) }))
|
|
2956
|
+
};
|
|
2957
|
+
ctx.issues.push({
|
|
2958
|
+
id: `out-of-sync:${root}`,
|
|
2959
|
+
severity: "suggestion",
|
|
2960
|
+
title: `${where}: ${plural(drifted.length, "skill")} not at the pinned version`,
|
|
2961
|
+
detail: `${drifted.map((s) => s.name).join(", ")}: each copy is another version from your library. sync installs the pinned one.${yours ? ` ${[...new Set(drifted.flatMap((s) => untrackable(s) ? [untrackable(s)] : []))].map((why) => `${drifted.filter((s) => untrackable(s) === why).map((s) => s.name).join(", ")}: ${why}`).join("; ")}, so skilllib can't pin the version that's there; ${drifted.filter((s) => untrackable(s)).length === 1 ? "it may be edits you copied into Your skills on purpose, and stays" : "they may be edits you copied into Your skills on purpose, and stay"} unless you choose this.` : ""}`,
|
|
2962
|
+
at: rows(root, drifted.map((s) => s.name)),
|
|
2963
|
+
...seen || yours ? { choices: [install] } : { fix: install }
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
for (const s of unstuck.filter((s2) => s2.state === "folder missing" && s2.latest === null))
|
|
2967
|
+
ctx.issues.push({
|
|
2968
|
+
id: `unavailable:${root}:${s.name}`,
|
|
2969
|
+
severity: "problem",
|
|
2970
|
+
title: `${s.name}: pinned in ${where}'s skilllib.json, but not in your library`,
|
|
2971
|
+
detail: `Its folder is missing, and your library (${tildify(libraryDir())}) has no copy to restore: import it, or set SKILLLIB_HOME to the skilllib folder whose library has it.`,
|
|
2972
|
+
at: rows(root, [s.name]),
|
|
2973
|
+
// Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
|
|
2974
|
+
choices: [
|
|
2975
|
+
{
|
|
2976
|
+
label: "Remove it from skilllib.json",
|
|
2977
|
+
hint: `${where} stops pinning it`,
|
|
2978
|
+
run: () => {
|
|
2979
|
+
const r = removeSkill(root, s.name);
|
|
2980
|
+
if (r.action !== "removed") throw new Error(`${s.name}: ${r.reason}`);
|
|
2981
|
+
return `${s.name}: removed from ${where}'s skilllib.json`;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
]
|
|
2985
|
+
});
|
|
2986
|
+
const behind = unstuck.filter((s) => s.state === "update available" && !untrackable(s));
|
|
2987
|
+
if (behind.length > 0)
|
|
2988
|
+
ctx.issues.push({
|
|
2989
|
+
id: `update:${root}`,
|
|
2990
|
+
severity: "suggestion",
|
|
2991
|
+
title: `${where}: ${plural(behind.length, "skill")} out of date`,
|
|
2992
|
+
detail: behind.map((s) => `${s.name} (v${s.version} \u2192 v${s.latest})`).join(", "),
|
|
2993
|
+
at: rows(root, behind.map((s) => s.name)),
|
|
2994
|
+
// It changes what the repo pins and the copies its agents load (often committed): a choice, never `doctor --fix`.
|
|
2995
|
+
choices: [
|
|
2996
|
+
{
|
|
2997
|
+
label: "Update to the newest versions",
|
|
2998
|
+
hint: isShared(gitOf(root)?.of(MANIFEST_FILE)) ? "git will see the new copies and skilllib.json" : "replaces each copy with your library's newest",
|
|
2999
|
+
run: () => summarize(where, "updated", updateProject(root, behind.map((s) => s.name)))
|
|
3000
|
+
}
|
|
3001
|
+
]
|
|
3002
|
+
});
|
|
3003
|
+
const tidy2 = planProjectTidy(root);
|
|
3004
|
+
if (tidy2.plans.length) {
|
|
3005
|
+
const git3 = gitOf(root);
|
|
3006
|
+
const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p, git3).map((s) => describeStep(s, root)));
|
|
3007
|
+
const run = (mode) => () => {
|
|
3008
|
+
const r = applyAll(tidy2.plans, mode);
|
|
3009
|
+
if (!r.skills) throw new Error(`${where}: nothing tidied${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`);
|
|
3010
|
+
return `${where}: tidied ${plural(r.skills, "skill")}${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`;
|
|
3011
|
+
};
|
|
3012
|
+
ctx.issues.push({
|
|
3013
|
+
id: `tidy:${root}`,
|
|
3014
|
+
severity: "suggestion",
|
|
3015
|
+
title: `${where}: ${plural(tidy2.plans.length, "skill")} with extra copies or links`,
|
|
3016
|
+
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(", ")}.` : tidy2.plans.some((p) => p.steps.some((s) => s.kind === "replace" || s.kind === "remove")) ? ". Replaced copies are restorable with skilllib restore." : "."),
|
|
3017
|
+
at: rows(root, tidy2.plans.map((p) => p.name)),
|
|
3018
|
+
...visible2.length ? {
|
|
3019
|
+
choices: [
|
|
3020
|
+
// "Keep git as it is" only when it leaves something to do.
|
|
3021
|
+
...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") }] : [],
|
|
3022
|
+
{ label: "Tidy everything", hint: `${plural(visible2.length, "change")} will show in git status`, run: run("go") }
|
|
3023
|
+
]
|
|
3024
|
+
} : { fix: { label: "Tidy", run: run("go") } }
|
|
3025
|
+
});
|
|
3026
|
+
}
|
|
3027
|
+
ctx.issues.push(...tidy2.conflicts.map(conflictIssue));
|
|
3028
|
+
for (const s of tidy2.skipped.filter((x) => x.differs))
|
|
3029
|
+
ctx.issues.push({
|
|
3030
|
+
id: `linkdiffers:${root}:${s.name}`,
|
|
3031
|
+
severity: "problem",
|
|
3032
|
+
title: `${s.name} in ${where}: the copies differ`,
|
|
3033
|
+
detail: `${s.reason}, with other content than the copy beside it: your agents run different versions. Make them the same, or remove the link; skilllib doesn't change a link to another copy.`,
|
|
3034
|
+
at: rows(root, [s.name])
|
|
3035
|
+
});
|
|
3036
|
+
const loadsGlobally = (name, id) => {
|
|
3037
|
+
const here = status2.find((s) => s.name === name);
|
|
3038
|
+
return ctx.globalCopies.some((m) => m.name === name && m.harnesses.includes(id) && (!here || treeHash(m.path) === treeHash(here.path)));
|
|
3039
|
+
};
|
|
3040
|
+
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 }) }));
|
|
3041
|
+
const enabledIds = enabledHarnesses();
|
|
3042
|
+
const notFolders = /* @__PURE__ */ new Set();
|
|
3043
|
+
for (const dir of new Set(enabledIds.map((id) => harness(id).projectDirs[0]))) {
|
|
3044
|
+
const blocker = notWritable(root, dir);
|
|
3045
|
+
if (!blocker || notFolders.has(blocker)) continue;
|
|
3046
|
+
notFolders.add(blocker);
|
|
3047
|
+
const who = agentNames(enabledIds.filter((id) => notWritable(root, harness(id).projectDirs[0]) === blocker));
|
|
3048
|
+
const out2 = linksOut(root, blocker);
|
|
3049
|
+
ctx.issues.push({
|
|
3050
|
+
id: `notdir:${root}:${blocker}`,
|
|
3051
|
+
severity: out2 ? "suggestion" : "problem",
|
|
3052
|
+
title: out2 ? `${where}: ${blocker} links out of the repo` : `${where}: ${blocker} isn't a folder`,
|
|
3053
|
+
detail: out2 ? `${blockerSaid(root, blocker)}. ${who} ${who.includes(",") || who.includes(" and ") ? "load" : "loads"} what's there, but skilllib treats it as outside ${where}: it adds, links and removes nothing there.` : `${blockerSaid(root, blocker)}: ${who} can't load skills from ${where}, and skilllib adds none there. Move it out of the way, then run skilllib link.`,
|
|
3054
|
+
at: [{ root, skill: "" }]
|
|
3055
|
+
// the repo itself: its row in Places
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
for (const { s, would } of blindHere) {
|
|
3059
|
+
const way = would.inTheWay.filter((p) => !notFolders.has(p));
|
|
3060
|
+
if (!way.length || blockers.has(s.name)) continue;
|
|
3061
|
+
ctx.issues.push({
|
|
3062
|
+
id: `intheway:${root}:${s.name}`,
|
|
3063
|
+
severity: "problem",
|
|
3064
|
+
title: `${s.name} in ${where}: ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))} can't use it here`,
|
|
3065
|
+
detail: `${inTheWaySaid(root, way)}. skilllib won't link past what's there: clear the way, then run skilllib link.`,
|
|
3066
|
+
at: rows(root, [s.name])
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
3069
|
+
for (const { s, would } of blindHere.filter(({ would: w }) => !w.created.length && !w.blocked.length && w.uncommitted.length))
|
|
3070
|
+
ctx.issues.push({
|
|
3071
|
+
id: `uncommitted:${root}:${s.name}`,
|
|
3072
|
+
severity: "suggestion",
|
|
3073
|
+
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`,
|
|
3074
|
+
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\`).`,
|
|
3075
|
+
at: rows(root, [s.name])
|
|
3076
|
+
});
|
|
3077
|
+
const partial = blindHere.filter(({ would }) => would.created.length || would.blocked.length).map(({ s }) => s);
|
|
3078
|
+
if (partial.length) {
|
|
3079
|
+
const missing2 = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
|
|
3080
|
+
const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
|
|
3081
|
+
const manifestShared = partial.some((s) => s.managed) && isShared(gitOf(root)?.of(MANIFEST_FILE));
|
|
3082
|
+
const run = (allowTracked) => () => {
|
|
3083
|
+
if (allowTracked) allowTrackedLinks(root);
|
|
3084
|
+
const r = linkAll(root, { allowTracked, names: partial.map((s) => s.name) });
|
|
3085
|
+
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)` : ""}`;
|
|
3086
|
+
if (!r.linked.length) {
|
|
3087
|
+
const still = partial.filter((s) => visibilityOf(root, s.name).some((v) => v.paths === 0 && !loadsGlobally(s.name, v.id)));
|
|
3088
|
+
if (!still.length) return `${where}: already usable by ${agentNames(missing2)}`;
|
|
3089
|
+
throw new Error(`${where}: nothing linked${notes}`);
|
|
3090
|
+
}
|
|
3091
|
+
return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing2)}${notes}`;
|
|
3092
|
+
};
|
|
3093
|
+
ctx.issues.push({
|
|
3094
|
+
id: `usable:${root}`,
|
|
3095
|
+
severity: "suggestion",
|
|
3096
|
+
title: `${where}: ${plural(partial.length, "skill")} not usable by ${agentNames(missing2)}`,
|
|
3097
|
+
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.` : ""}${manifestShared ? " The new links are recorded in skilllib.json, which git tracks." : ""}`,
|
|
3098
|
+
at: rows(root, partial.map((s) => s.name)),
|
|
3099
|
+
...manifestShared && !tracked.length ? { choices: [{ label: "Add the links", hint: "git will see skilllib.json change", run: run(false) }] } : tracked.length ? {
|
|
3100
|
+
choices: [
|
|
3101
|
+
// Only when some link goes where git doesn't track: otherwise this one would link nothing.
|
|
3102
|
+
...blindHere.some(({ would }) => would.created.length) ? [{ label: "Add links, but not where git tracks the folder", hint: `skips ${tracked.join(", ")}`, run: run(false) }] : [],
|
|
3103
|
+
{ label: "Add links everywhere", hint: `remembered for ${where}; git will see them`, run: run(true) }
|
|
3104
|
+
]
|
|
3105
|
+
} : { fix: { label: "Add the links", run: run(false) } }
|
|
3106
|
+
});
|
|
3107
|
+
}
|
|
3108
|
+
const differing = new Set(tidy2.conflicts.map((c) => c.name));
|
|
3109
|
+
const git2 = gitOf(root);
|
|
3110
|
+
const committed = (s) => isShared(git2?.of(relativeTo(root, s.path)));
|
|
3111
|
+
for (const s of unstuck) {
|
|
3112
|
+
if (isLink(s.path) || differing.has(s.name)) continue;
|
|
3113
|
+
if (s.state === "local only" && (committed(s) || untrackable(s))) {
|
|
3114
|
+
const why = committed(s) ? "it's the team's (committed)" : `${untrackable(s)}, so skilllib can't track it here`;
|
|
3115
|
+
ctx.issues.push({
|
|
3116
|
+
id: `local:${root}:${s.name}`,
|
|
3117
|
+
severity: "suggestion",
|
|
3118
|
+
title: `${s.name} in ${where} isn't in your library`,
|
|
3119
|
+
detail: `Copy it into the library to reuse it in other projects. The repo's copy stays as it is: ${why}.`,
|
|
3120
|
+
at: rows(root, [s.name]),
|
|
3121
|
+
choices: [
|
|
3122
|
+
{
|
|
3123
|
+
label: "Copy into the library",
|
|
3124
|
+
hint: `${where}'s copy stays as it is`,
|
|
3125
|
+
run: () => {
|
|
3126
|
+
intoLibrary(s.path);
|
|
3127
|
+
return `${s.name} copied into your library; ${where}'s copy stays as it is`;
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
]
|
|
3131
|
+
});
|
|
3132
|
+
} else if (s.state === "local only") {
|
|
3133
|
+
ctx.issues.push({
|
|
3134
|
+
id: `local:${root}:${s.name}`,
|
|
3135
|
+
severity: "suggestion",
|
|
3136
|
+
title: `${s.name} in ${where} isn't in your library`,
|
|
3137
|
+
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.",
|
|
3138
|
+
at: rows(root, [s.name]),
|
|
3139
|
+
choices: [
|
|
3140
|
+
{
|
|
3141
|
+
label: "Import into the library",
|
|
3142
|
+
// Tracking it writes skilllib.json.
|
|
3143
|
+
hint: `copies it into Your skills${gitNote(root, [join13(root, MANIFEST_FILE)])}`,
|
|
3144
|
+
run: () => {
|
|
3145
|
+
intoLibrary(s.path);
|
|
3146
|
+
track(root, s.name);
|
|
3147
|
+
return `${s.name} imported and tracked in ${where}`;
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
]
|
|
3151
|
+
});
|
|
3152
|
+
} else if (s.state === "untracked copy of library skill" && !committed(s) && !untrackable(s)) {
|
|
3153
|
+
ctx.issues.push({
|
|
3154
|
+
id: `adopt:${root}:${s.name}`,
|
|
3155
|
+
severity: "suggestion",
|
|
3156
|
+
title: `${s.name} in ${where} isn't tracked`,
|
|
3157
|
+
detail: "It's identical to the library copy; track it (in skilllib.json) so future library changes reach it.",
|
|
3158
|
+
at: rows(root, [s.name]),
|
|
3159
|
+
choices: [{ label: "Track it", hint: `adds it to ${where}'s skilllib.json`, run: () => `${track(root, s.name).name} tracked in ${where}` }]
|
|
3160
|
+
});
|
|
3161
|
+
} else if (s.state === "untracked, differs from library" || s.state === "repo skill, differs from library") {
|
|
3162
|
+
const teams = s.state.startsWith("repo skill") || committed(s) || !!untrackable(s);
|
|
3163
|
+
const whose = s.state.startsWith("repo skill") || committed(s) ? "It's the team's copy (committed, or in the repo's own .agents/skills or .grok/skills)" : `${untrackable(s)}, so skilllib can't track it here`;
|
|
3164
|
+
ctx.issues.push({
|
|
3165
|
+
id: `differs:${root}:${s.name}`,
|
|
3166
|
+
severity: "suggestion",
|
|
3167
|
+
title: `${s.name} in ${where} differs from your library`,
|
|
3168
|
+
detail: teams ? `${whose}, 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.`,
|
|
3169
|
+
at: rows(root, [s.name]),
|
|
3170
|
+
choices: [
|
|
3171
|
+
{
|
|
3172
|
+
label: "Update your library from it",
|
|
3173
|
+
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`,
|
|
3174
|
+
run: () => {
|
|
3175
|
+
intoLibrary(s.path, true);
|
|
3176
|
+
if (teams) return `${s.name}: your library has ${where}'s copy; ${where}'s copy stays as it is`;
|
|
3177
|
+
const c = track(root, s.name);
|
|
3178
|
+
return `${s.name}: your library has ${where}'s copy as v${c.to}; ${where} tracks it`;
|
|
3179
|
+
}
|
|
3180
|
+
},
|
|
3181
|
+
...teams ? [] : [
|
|
3182
|
+
{
|
|
3183
|
+
label: "Replace it with your library's",
|
|
3184
|
+
hint: "this copy is restorable with skilllib restore",
|
|
3185
|
+
run: () => {
|
|
3186
|
+
const c = addSkill(root, s.name, { force: true });
|
|
3187
|
+
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
3188
|
+
return `${s.name} in ${where} is your library's v${c.to} now; the old copy is restorable with skilllib restore`;
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
]
|
|
3192
|
+
]
|
|
3193
|
+
});
|
|
3194
|
+
} else if (s.state === "from npx skills" && !((committed(s) || untrackable(s)) && versionForHash(s.name, treeHash(s.path) ?? "") !== null)) {
|
|
3195
|
+
const teams = committed(s) || !!untrackable(s);
|
|
3196
|
+
ctx.issues.push({
|
|
3197
|
+
id: `npx:${root}:${s.name}`,
|
|
3198
|
+
severity: "suggestion",
|
|
3199
|
+
title: `${s.name} in ${where} came from npx skills, not your library`,
|
|
3200
|
+
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(s) ? "committed" : `${untrackable(s)}, so skilllib can't track it here`})` : `; ${where} then tracks it in skilllib.json`}.`,
|
|
3201
|
+
at: rows(root, [s.name]),
|
|
3202
|
+
choices: [
|
|
3203
|
+
{
|
|
3204
|
+
label: "Import it into your library",
|
|
3205
|
+
hint: teams ? `${where}'s copy stays as it is` : `${where} then tracks it`,
|
|
3206
|
+
run: () => {
|
|
3207
|
+
intoLibrary(s.path);
|
|
3208
|
+
if (teams) return `${s.name} copied into your library; ${where}'s copy stays as it is`;
|
|
3209
|
+
track(root, s.name);
|
|
3210
|
+
return `${s.name} imported and tracked in ${where}`;
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
]
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
if (s.state === "not in library")
|
|
3217
|
+
ctx.issues.push({
|
|
3218
|
+
id: `not-in-lib:${root}:${s.name}`,
|
|
3219
|
+
severity: "suggestion",
|
|
3220
|
+
title: `${s.name} in ${where} is tracked, but no longer in your library`,
|
|
3221
|
+
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.`,
|
|
3222
|
+
at: rows(root, [s.name]),
|
|
3223
|
+
choices: [
|
|
3224
|
+
{
|
|
3225
|
+
label: "Copy it back into your library",
|
|
3226
|
+
hint: `${where}'s copy stays as it is`,
|
|
3227
|
+
run: () => {
|
|
3228
|
+
intoLibrary(s.path);
|
|
3229
|
+
return `${s.name} copied back into your library`;
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
]
|
|
3233
|
+
});
|
|
3234
|
+
if (s.state === "edited locally" || s.state === "edited locally, update available") {
|
|
3235
|
+
const newer2 = s.state !== "edited locally";
|
|
3236
|
+
const pinned = s.version ? `v${s.version}` : "the library version";
|
|
3237
|
+
ctx.issues.push({
|
|
3238
|
+
id: `edited:${root}:${s.name}`,
|
|
3239
|
+
severity: "suggestion",
|
|
3240
|
+
title: `${s.name} in ${where} has local edits`,
|
|
3241
|
+
detail: `It no longer matches ${pinned} from Your skills${newer2 ? `, and v${s.latest} is out` : ""}. ${untrackable(s) ? `${untrackable(s)}, so skilllib can't re-pin or discard them here; copy them into Your skills to keep them for your other repos.` : "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."}`,
|
|
3242
|
+
at: rows(root, [s.name]),
|
|
3243
|
+
choices: untrackable(s) ? [
|
|
3244
|
+
{
|
|
3245
|
+
label: "Copy the edits into Your skills",
|
|
3246
|
+
hint: `${newer2 ? `becomes v${s.latest + 1}; v${s.latest}'s changes aren't merged in` : "a new version there"}; ${where}'s copy and pin stay as they are (skilllib sync there would put the pinned version back)`,
|
|
3247
|
+
run: () => {
|
|
3248
|
+
intoLibrary(s.path, true);
|
|
3249
|
+
return `${s.name}: the edits are in Your skills as v${latestVersion(s.name)?.version}; ${where} stays as it is`;
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
] : [
|
|
3253
|
+
{
|
|
3254
|
+
label: "Save as a new version in Your skills",
|
|
3255
|
+
hint: `${newer2 ? `becomes v${s.latest + 1}; v${s.latest}'s changes aren't merged in` : `becomes v${(s.latest ?? 0) + 1}; ${where} uses it`}${gitNote(root, [join13(root, MANIFEST_FILE)])}`,
|
|
3256
|
+
run: () => {
|
|
3257
|
+
importSkill(s.path, { force: true });
|
|
3258
|
+
const c = addSkill(root, s.name);
|
|
3259
|
+
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
3260
|
+
return `${s.name}: saved as v${c.to}; ${where} uses it`;
|
|
3261
|
+
}
|
|
3262
|
+
},
|
|
3263
|
+
{
|
|
3264
|
+
label: "Discard the edits",
|
|
3265
|
+
hint: `back to ${pinned}; the edited copy is restorable with skilllib restore${gitNote(root, [s.path, join13(root, MANIFEST_FILE)])}`,
|
|
3266
|
+
run: () => {
|
|
3267
|
+
const c = discardEdits(root, s.name);
|
|
3268
|
+
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
3269
|
+
return `${s.name}: edits discarded, back to v${c.to}`;
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
]
|
|
3273
|
+
});
|
|
3274
|
+
}
|
|
3275
|
+
if (ctx.alsoLoadedGlobally(s)) ctx.alsoGlobal.set(s.name, [...ctx.alsoGlobal.get(s.name) ?? [], { root, managed: s.managed }]);
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3104
3278
|
function addedAt(path) {
|
|
3105
3279
|
try {
|
|
3106
3280
|
const st = lstatSync4(path);
|
|
@@ -3115,7 +3289,7 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
|
|
|
3115
3289
|
const issues = [];
|
|
3116
3290
|
const since = Date.now() - uses.days * 24 * 60 * 60 * 1e3;
|
|
3117
3291
|
for (const root of projects2) {
|
|
3118
|
-
const where =
|
|
3292
|
+
const where = basename7(root);
|
|
3119
3293
|
const unused = statusOf(root).filter((s) => s.state !== "folder missing" && uses.inProject(root, s.name) === 0 && addedAt(s.path) <= since);
|
|
3120
3294
|
if (!unused.length) continue;
|
|
3121
3295
|
const removable = unused.filter((s) => removesClean(root, s));
|
|
@@ -3143,15 +3317,15 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
|
|
|
3143
3317
|
if (uses.asUsed)
|
|
3144
3318
|
for (const p of installedPlugins().filter((x) => x.agent === "claude-code" && x.on && x.skills.length)) {
|
|
3145
3319
|
const name = pluginNameOf(p.id);
|
|
3146
|
-
if (p.skills.some((path) => uses.asUsed(`${name}:${
|
|
3320
|
+
if (p.skills.some((path) => uses.asUsed(`${name}:${basename7(path)}`) > 0)) continue;
|
|
3147
3321
|
issues.push({
|
|
3148
3322
|
id: `plugin-unused:${installId(p)}`,
|
|
3149
3323
|
severity: "suggestion",
|
|
3150
3324
|
title: `${p.id}${installedIn(p)}: its skills weren't used in ${plural(uses.days, "day")}`,
|
|
3151
|
-
detail: `No Claude Code session used ${p.skills.map((path) => `/${name}:${
|
|
3325
|
+
detail: `No Claude Code session used ${p.skills.map((path) => `/${name}:${basename7(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.` : ""}`,
|
|
3152
3326
|
choices: [
|
|
3153
|
-
{ label: `Turn the plugin ${p.id} off`, hint: p.scope === "project" ? `stays installed; edits ${
|
|
3154
|
-
...p.synced || p.settingsOnly ? [] : [{ label: `Uninstall the plugin ${p.id}`, hint: `its data stays; restorable with skilllib restore${p.scope === "project" ? `; edits ${
|
|
3327
|
+
{ label: `Turn the plugin ${p.id} off`, hint: p.scope === "project" ? `stays installed; edits ${basename7(p.projectPath ?? "the repo")}'s committed .claude/settings.json` : "stays installed", run: () => orThrow(setPluginOn(p, false)) },
|
|
3328
|
+
...p.synced || p.settingsOnly ? [] : [{ label: `Uninstall the plugin ${p.id}`, hint: `its data stays; restorable with skilllib restore${p.scope === "project" ? `; edits ${basename7(p.projectPath ?? "the repo")}'s committed .claude/settings.json` : ""}`, run: () => orThrow(uninstallPlugin(p)) }]
|
|
3155
3329
|
],
|
|
3156
3330
|
plugin: p.id,
|
|
3157
3331
|
pluginAt: p.projectPath ?? null
|
|
@@ -3204,7 +3378,7 @@ function globalFrom(kind, origin, path) {
|
|
|
3204
3378
|
}
|
|
3205
3379
|
function topLevel(root) {
|
|
3206
3380
|
try {
|
|
3207
|
-
return
|
|
3381
|
+
return readdirSync9(root, { withFileTypes: true }).filter((d) => !d.name.startsWith(".") && !BUILD_DIRS.has(d.name)).map((d) => d.isDirectory() ? `${d.name}/` : d.name).sort().slice(0, 60);
|
|
3208
3382
|
} catch {
|
|
3209
3383
|
return [];
|
|
3210
3384
|
}
|
|
@@ -3231,7 +3405,8 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
3231
3405
|
state: usual ? void 0 : s.state,
|
|
3232
3406
|
latest: behind ? s.latest : void 0,
|
|
3233
3407
|
git: gitState,
|
|
3234
|
-
agents: sameSet(loadedBy, agents) ? void 0 : loadedBy
|
|
3408
|
+
agents: sameSet(loadedBy, agents) ? void 0 : loadedBy,
|
|
3409
|
+
personal: s.personal ? true : void 0
|
|
3235
3410
|
};
|
|
3236
3411
|
return [JSON.stringify(attrs), s.name, () => ({ ...attrs, skills: [] })];
|
|
3237
3412
|
})
|
|
@@ -3281,7 +3456,7 @@ function firstSentence(description) {
|
|
|
3281
3456
|
return sentence.length > 200 ? sentence.slice(0, 199).trimEnd() + "\u2026" : sentence;
|
|
3282
3457
|
}
|
|
3283
3458
|
function libraryFor(currentProject) {
|
|
3284
|
-
const manifests = visibleProjects().map((p) => ({ name:
|
|
3459
|
+
const manifests = visibleProjects().map((p) => ({ name: basename8(p), skills: pinsIn(p) }));
|
|
3285
3460
|
const here = currentProject ? new Set(projectStatus(currentProject).map((s) => s.name)) : null;
|
|
3286
3461
|
const all = librarySkills();
|
|
3287
3462
|
const describe2 = (s) => {
|
|
@@ -3293,7 +3468,7 @@ function libraryFor(currentProject) {
|
|
|
3293
3468
|
}
|
|
3294
3469
|
|
|
3295
3470
|
// src/usage.ts
|
|
3296
|
-
import { createReadStream, existsSync as existsSync13, readdirSync as
|
|
3471
|
+
import { createReadStream, existsSync as existsSync13, readdirSync as readdirSync10, statSync as statSync6 } from "fs";
|
|
3297
3472
|
import { join as join14 } from "path";
|
|
3298
3473
|
import { createInterface } from "readline";
|
|
3299
3474
|
var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
|
|
@@ -3378,17 +3553,17 @@ function transcriptFiles(sinceMs) {
|
|
|
3378
3553
|
if (!existsSync13(base)) return [];
|
|
3379
3554
|
const list2 = (dir) => {
|
|
3380
3555
|
try {
|
|
3381
|
-
return
|
|
3556
|
+
return readdirSync10(dir);
|
|
3382
3557
|
} catch {
|
|
3383
3558
|
return [];
|
|
3384
3559
|
}
|
|
3385
3560
|
};
|
|
3386
3561
|
return list2(base).flatMap((project) => {
|
|
3387
3562
|
const dir = join14(base, project);
|
|
3388
|
-
if (!
|
|
3563
|
+
if (!statSync6(dir, { throwIfNoEntry: false })?.isDirectory()) return [];
|
|
3389
3564
|
return list2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join14(dir, f)).filter((f) => {
|
|
3390
3565
|
try {
|
|
3391
|
-
const st =
|
|
3566
|
+
const st = statSync6(f);
|
|
3392
3567
|
return st.isFile() && st.mtimeMs >= sinceMs;
|
|
3393
3568
|
} catch {
|
|
3394
3569
|
return false;
|
|
@@ -3441,11 +3616,11 @@ function usesByProject(uses, projectOf) {
|
|
|
3441
3616
|
}
|
|
3442
3617
|
|
|
3443
3618
|
// src/move.ts
|
|
3444
|
-
import { basename as
|
|
3619
|
+
import { basename as basename9 } from "path";
|
|
3445
3620
|
function moveGlobalSkills(skills, roots, {
|
|
3446
3621
|
group,
|
|
3447
3622
|
allowTracked = false,
|
|
3448
|
-
label =
|
|
3623
|
+
label = basename9,
|
|
3449
3624
|
allowTrackedHint = "re-run with --allow-tracked"
|
|
3450
3625
|
} = {}) {
|
|
3451
3626
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -3493,6 +3668,8 @@ var FLAGS = {
|
|
|
3493
3668
|
json: { type: "boolean" },
|
|
3494
3669
|
"dry-run": { type: "boolean" },
|
|
3495
3670
|
"allow-git": { type: "boolean" },
|
|
3671
|
+
personal: { type: "boolean" },
|
|
3672
|
+
shared: { type: "boolean" },
|
|
3496
3673
|
days: { type: "string" },
|
|
3497
3674
|
keep: { type: "string" },
|
|
3498
3675
|
to: { type: "string", multiple: true }
|
|
@@ -3500,6 +3677,7 @@ var FLAGS = {
|
|
|
3500
3677
|
var VALUE_FLAGS = Object.entries(FLAGS).flatMap(([name, f]) => f.type === "string" ? [`--${name}`] : []);
|
|
3501
3678
|
function parseArgs(argv) {
|
|
3502
3679
|
const { values: v, positionals } = parseFlags({ args: argv, options: FLAGS, allowPositionals: true, strict: true });
|
|
3680
|
+
if (v.personal && v.shared) throw new Error("--personal and --shared don't go together: a pin is in one place");
|
|
3503
3681
|
const days = Number(v.days ?? USAGE_DAYS);
|
|
3504
3682
|
if (!Number.isInteger(days) || days <= 0) throw new Error(`--days takes a whole number of days, like --days 30 (got ${v.days})`);
|
|
3505
3683
|
return {
|
|
@@ -3513,6 +3691,7 @@ function parseArgs(argv) {
|
|
|
3513
3691
|
days,
|
|
3514
3692
|
dryRun: !!v["dry-run"],
|
|
3515
3693
|
allowGit: !!v["allow-git"],
|
|
3694
|
+
...v.personal || v.shared ? { personal: !!v.personal } : {},
|
|
3516
3695
|
...v.keep ? { keep: v.keep } : {},
|
|
3517
3696
|
to: (v.to ?? []).flatMap((t) => t.split(",").filter(Boolean))
|
|
3518
3697
|
};
|
|
@@ -3527,7 +3706,7 @@ function withDirs(root, changes) {
|
|
|
3527
3706
|
const { skills } = readManifest(root);
|
|
3528
3707
|
return changes.map((c) => {
|
|
3529
3708
|
const dep = c.action !== "removed" && c.action !== "skipped" ? skills[c.name] : void 0;
|
|
3530
|
-
return dep ? { ...c, dirs: [dep.dir ?? PROJECT_SKILLS_DIR, ...(dep.links ?? []).filter((d) => !c.inTheWay?.includes(`${d}/${c.name}`))] } : c;
|
|
3709
|
+
return dep ? { ...c, dirs: [dep.dir ?? PROJECT_SKILLS_DIR, ...(dep.links ?? []).filter((d) => !c.inTheWay?.includes(`${d}/${c.name}`))], ...dep.personal ? { personal: true } : {} } : c;
|
|
3531
3710
|
});
|
|
3532
3711
|
}
|
|
3533
3712
|
var incomplete = (c) => c.action === "skipped" || !!c.blocked?.length || !!c.uncommitted?.length || !!c.inTheWay?.length;
|
|
@@ -3546,7 +3725,7 @@ function printChanges(root, changes, args) {
|
|
|
3546
3725
|
else {
|
|
3547
3726
|
const icon = c.action === "removed" ? red("-") : c.action === "reset" ? yellow("\u21BA") : c.action === "unchanged" ? dim("=") : green(c.action === "installed" ? "+" : c.action === "relinked" ? "\u21E2" : "\u2191");
|
|
3548
3727
|
const versions = c.action === "removed" ? "" : c.from && c.from !== c.to ? ` v${c.from} \u2192 v${c.to}` : c.to ? ` v${c.to}` : "";
|
|
3549
|
-
info(` ${icon} ${c.name}${versions} ${dim(c.action)}`);
|
|
3728
|
+
info(` ${icon} ${c.name}${versions} ${dim(c.action)}${c.moved ? dim(c.moved === "personal" ? ", now in your personal pins" : ", now in skilllib.json") : ""}`);
|
|
3550
3729
|
if (c.reason) info(dim(` ${c.reason}`));
|
|
3551
3730
|
}
|
|
3552
3731
|
if (c.backedUp) info(dim(` local edits saved to ${tildify(c.backedUp)}; \`skilllib restore ${c.name}\` brings them back`));
|
|
@@ -3557,10 +3736,10 @@ function printChanges(root, changes, args) {
|
|
|
3557
3736
|
${done} done, ${skipped} skipped`));
|
|
3558
3737
|
}
|
|
3559
3738
|
function projectOfFactory(projects2) {
|
|
3560
|
-
const
|
|
3739
|
+
const sorted2 = [...projects2].sort((a, b) => b.length - a.length);
|
|
3561
3740
|
return (cwd) => {
|
|
3562
3741
|
if (!cwd) return null;
|
|
3563
|
-
const known =
|
|
3742
|
+
const known = sorted2.find((p) => cwd === p || cwd.startsWith(p + sep8));
|
|
3564
3743
|
if (known) return known;
|
|
3565
3744
|
const repo = cwd.split(`${sep8}.claude${sep8}worktrees${sep8}`)[0];
|
|
3566
3745
|
return isProjectCandidate(repo) ? repo : null;
|
|
@@ -3576,7 +3755,7 @@ var projectsFor = (args) => {
|
|
|
3576
3755
|
if (!args.all) return [requireProject()];
|
|
3577
3756
|
const { readable, unreadable } = readableRoots(visibleProjects());
|
|
3578
3757
|
for (const u of unreadable) {
|
|
3579
|
-
error(`${
|
|
3758
|
+
error(`${basename10(u.root)}: ${u.reason}`);
|
|
3580
3759
|
process.exitCode = 1;
|
|
3581
3760
|
}
|
|
3582
3761
|
return readable;
|
|
@@ -3610,7 +3789,7 @@ async function status(args) {
|
|
|
3610
3789
|
const root = requireProject();
|
|
3611
3790
|
const skills = projectStatus(root);
|
|
3612
3791
|
const nested = nestedSkills(root);
|
|
3613
|
-
info(`${
|
|
3792
|
+
info(`${basename10(root)} ${dim(tildify(root))}
|
|
3614
3793
|
`);
|
|
3615
3794
|
if (skills.length === 0 && nested.length === 0) info("No skills in this project yet. Add one from your library with: skilllib add <name>");
|
|
3616
3795
|
const usage2 = await usageBySkill(args.days, root);
|
|
@@ -3683,7 +3862,7 @@ async function list(args) {
|
|
|
3683
3862
|
const installs = /* @__PURE__ */ new Map();
|
|
3684
3863
|
for (const project of visibleProjects()) {
|
|
3685
3864
|
for (const name of Object.keys(pinsIn(project))) {
|
|
3686
|
-
installs.set(name, [...installs.get(name) ?? [],
|
|
3865
|
+
installs.set(name, [...installs.get(name) ?? [], basename10(project)]);
|
|
3687
3866
|
}
|
|
3688
3867
|
}
|
|
3689
3868
|
const usage2 = await usageBySkill(args.days);
|
|
@@ -3726,7 +3905,7 @@ function teamStateIn(root, extraDirs = []) {
|
|
|
3726
3905
|
function projects(args) {
|
|
3727
3906
|
const { readable: roots, unreadable } = readableRoots(visibleProjects());
|
|
3728
3907
|
for (const u of unreadable) {
|
|
3729
|
-
error(`${
|
|
3908
|
+
error(`${basename10(u.root)}: ${u.reason}`);
|
|
3730
3909
|
process.exitCode = 1;
|
|
3731
3910
|
}
|
|
3732
3911
|
if (args.json) {
|
|
@@ -3735,7 +3914,7 @@ function projects(args) {
|
|
|
3735
3914
|
roots.map((root) => {
|
|
3736
3915
|
const teamState = teamStateIn(root);
|
|
3737
3916
|
return {
|
|
3738
|
-
name:
|
|
3917
|
+
name: basename10(root),
|
|
3739
3918
|
path: root,
|
|
3740
3919
|
skills: projectStatus(root).map((s) => ({ name: s.name, managed: s.managed, state: teamState(s), version: s.version, latest: s.latest }))
|
|
3741
3920
|
};
|
|
@@ -3753,7 +3932,7 @@ function projects(args) {
|
|
|
3753
3932
|
const usual = /* @__PURE__ */ new Set(["ok", "local only", "repo skill", "repo skill, in library", "from npx skills"]);
|
|
3754
3933
|
const attention = skills.filter((s) => !usual.has(teamState(s))).length;
|
|
3755
3934
|
return {
|
|
3756
|
-
Project:
|
|
3935
|
+
Project: basename10(root),
|
|
3757
3936
|
Path: dim(tildify(root)),
|
|
3758
3937
|
Skills: skills.map((s) => s.name).join(", ") || dim("none"),
|
|
3759
3938
|
"Local only": String(skills.filter((s) => teamState(s) === "local only").length),
|
|
@@ -3764,10 +3943,15 @@ function projects(args) {
|
|
|
3764
3943
|
}
|
|
3765
3944
|
function add(args) {
|
|
3766
3945
|
if (args.positional.length === 0) {
|
|
3767
|
-
throw new Error("Usage: skilllib add <skill-name>... [--force]");
|
|
3946
|
+
throw new Error("Usage: skilllib add <skill-name>... [--force] [--personal|--shared]");
|
|
3768
3947
|
}
|
|
3769
3948
|
const root = requireProject();
|
|
3770
|
-
|
|
3949
|
+
const changes = args.positional.map((name) => addSkill(root, name, { force: args.force, allowTracked: args.allowTracked, personal: args.personal }));
|
|
3950
|
+
printChanges(root, changes, args);
|
|
3951
|
+
const pins = readManifest(root).skills;
|
|
3952
|
+
const done = changes.filter((c) => c.action !== "skipped" && pins[c.name]);
|
|
3953
|
+
if (!args.json && done.some((c) => pins[c.name].personal))
|
|
3954
|
+
info(dim(`Pinned in your personal pins (${tildify(personalPinsDir())}) and kept out of git. --shared pins in skilllib.json for the team.`));
|
|
3771
3955
|
rememberProjects([root]);
|
|
3772
3956
|
}
|
|
3773
3957
|
function remove(args) {
|
|
@@ -3783,11 +3967,11 @@ function changeEach(args, change, nothing) {
|
|
|
3783
3967
|
const changes = change(root);
|
|
3784
3968
|
if (changes.some(incomplete)) process.exitCode = 1;
|
|
3785
3969
|
if (!args.json) {
|
|
3786
|
-
if (args.all) info(`${
|
|
3970
|
+
if (args.all) info(`${basename10(root)} ${dim(tildify(root))}`);
|
|
3787
3971
|
if (changes.length === 0) nothing(root);
|
|
3788
3972
|
else printChanges(root, changes, args);
|
|
3789
3973
|
}
|
|
3790
|
-
return { project:
|
|
3974
|
+
return { project: basename10(root), path: root, changes: withDirs(root, changes) };
|
|
3791
3975
|
});
|
|
3792
3976
|
if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
|
|
3793
3977
|
return roots;
|
|
@@ -3797,7 +3981,7 @@ function sync(args) {
|
|
|
3797
3981
|
const roots = changeEach(
|
|
3798
3982
|
args,
|
|
3799
3983
|
(root) => syncProject(root, { force: args.force }),
|
|
3800
|
-
(root) =>
|
|
3984
|
+
(root) => hasPins(root) ? success("Installed versions match what's pinned") : info(dim("No skilllib.json or personal pins: nothing to sync"))
|
|
3801
3985
|
);
|
|
3802
3986
|
if (!args.all) rememberProjects(roots);
|
|
3803
3987
|
}
|
|
@@ -3818,7 +4002,7 @@ function update(args) {
|
|
|
3818
4002
|
function outdated(args) {
|
|
3819
4003
|
const roots = projectsFor(args);
|
|
3820
4004
|
const behind = roots.flatMap(
|
|
3821
|
-
(root) => projectStatus(root).filter((s) => s.state.endsWith("update available") && s.latest !== null && s.version !== null).map((s) => ({ project:
|
|
4005
|
+
(root) => projectStatus(root).filter((s) => s.state.endsWith("update available") && s.latest !== null && s.version !== null).map((s) => ({ project: basename10(root), path: root, skill: s.name, installed: s.version, latest: s.latest, state: s.state }))
|
|
3822
4006
|
);
|
|
3823
4007
|
if (args.json) return json("outdated", behind);
|
|
3824
4008
|
const rows2 = behind.map((b) => ({ Project: b.project, Skill: b.skill, Installed: `v${b.installed}`, Latest: `v${b.latest}`, Status: colorState(b.state) }));
|
|
@@ -3853,7 +4037,7 @@ function importSkills(args) {
|
|
|
3853
4037
|
const dep = projectHere(root) === root ? readManifest(root).skills[name] : void 0;
|
|
3854
4038
|
if (dep && realpathOrNull(join15(root, dep.dir ?? PROJECT_SKILLS_DIR, name)) === realpathOrNull(dir) && dep.hash !== treeHash(dir)) {
|
|
3855
4039
|
const change = addSkill(root, name);
|
|
3856
|
-
if (change.action !== "skipped") info(dim(` ${
|
|
4040
|
+
if (change.action !== "skipped") info(dim(` ${basename10(root)} now depends on ${name} v${change.to}`));
|
|
3857
4041
|
}
|
|
3858
4042
|
}
|
|
3859
4043
|
if (args.json) json("import", results);
|
|
@@ -3869,14 +4053,14 @@ function scan(args) {
|
|
|
3869
4053
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
3870
4054
|
const skillNames = (root) => [
|
|
3871
4055
|
// Not a folder that links out of the repo: what's in it isn't the repo's (as in status).
|
|
3872
|
-
.../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.filter((d) => !notWritable(root, d)).flatMap((d) => skillDirsIn(join15(root, d))).map((d) =>
|
|
4056
|
+
.../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.filter((d) => !notWritable(root, d)).flatMap((d) => skillDirsIn(join15(root, d))).map((d) => basename10(d)), ...nestedSkills(root).map((s) => s.name)])
|
|
3873
4057
|
];
|
|
3874
4058
|
const withSkills = found.filter((root) => skillNames(root).length > 0);
|
|
3875
4059
|
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
|
|
3876
4060
|
`);
|
|
3877
4061
|
table(
|
|
3878
4062
|
withSkills.map((root) => ({
|
|
3879
|
-
Project:
|
|
4063
|
+
Project: basename10(root),
|
|
3880
4064
|
Path: dim(tildify(root)),
|
|
3881
4065
|
Skills: skillNames(root).map((n) => library.has(n) ? n : yellow(n)).join(", ")
|
|
3882
4066
|
}))
|
|
@@ -4046,7 +4230,7 @@ async function usage(args) {
|
|
|
4046
4230
|
return json("usage", {
|
|
4047
4231
|
usageDays: args.days,
|
|
4048
4232
|
source: "Claude Code transcripts",
|
|
4049
|
-
skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) =>
|
|
4233
|
+
skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) => basename10(p)), inLibrary: library.has(s.skill) })),
|
|
4050
4234
|
unusedLibrarySkills: unused
|
|
4051
4235
|
});
|
|
4052
4236
|
}
|
|
@@ -4060,7 +4244,7 @@ async function usage(args) {
|
|
|
4060
4244
|
summaries.map((s) => ({
|
|
4061
4245
|
Skill: s.skill,
|
|
4062
4246
|
Uses: String(s.uses),
|
|
4063
|
-
Projects: [...s.projects].map((p) =>
|
|
4247
|
+
Projects: [...s.projects].map((p) => basename10(p)).join(", ") || dim("-"),
|
|
4064
4248
|
"Last used": lastUsed(s),
|
|
4065
4249
|
Library: library.has(s.skill) ? green("yes") : dim("no")
|
|
4066
4250
|
}))
|
|
@@ -4084,7 +4268,7 @@ function show(args) {
|
|
|
4084
4268
|
path: dir,
|
|
4085
4269
|
latest: latestVersion(name)?.version ?? null,
|
|
4086
4270
|
origin: libraryOrigins()[name] ?? "",
|
|
4087
|
-
projects: projectsUsing(name).map((p) =>
|
|
4271
|
+
projects: projectsUsing(name).map((p) => basename10(p)),
|
|
4088
4272
|
skillMd: readFileSync7(join15(dir, "SKILL.md"), "utf-8")
|
|
4089
4273
|
});
|
|
4090
4274
|
}
|
|
@@ -4116,7 +4300,7 @@ function link(args) {
|
|
|
4116
4300
|
const roots = projectsFor(args);
|
|
4117
4301
|
for (const root of roots) {
|
|
4118
4302
|
const res = linkAll(root, { allowTracked: args.allowTracked });
|
|
4119
|
-
const label = args.all ? `${
|
|
4303
|
+
const label = args.all ? `${basename10(root)}: ` : "";
|
|
4120
4304
|
if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
|
|
4121
4305
|
else if (!args.all && !res.blocked.length && !res.uncommitted.length && !res.inTheWay.length) {
|
|
4122
4306
|
const missing = projectStatus(root).filter((s) => s.state === "folder missing").map((s) => s.name);
|
|
@@ -4162,7 +4346,7 @@ ${unreviewed} not reviewed. Keep one global on purpose: skilllib global keep <na
|
|
|
4162
4346
|
}
|
|
4163
4347
|
function repoRoot(repo) {
|
|
4164
4348
|
const looksLikePath = /[\\/]/.test(repo) || repo.startsWith(".") || repo.startsWith("~");
|
|
4165
|
-
const named = looksLikePath ? [] : visibleProjects().filter((p) =>
|
|
4349
|
+
const named = looksLikePath ? [] : visibleProjects().filter((p) => basename10(p) === repo);
|
|
4166
4350
|
if (named.length === 1) return named[0];
|
|
4167
4351
|
if (named.length > 1) throw new Error(`Several projects are named ${repo}: ${named.map(tildify).join(", ")}. Pass its path.`);
|
|
4168
4352
|
const path = expandHome(repo);
|
|
@@ -4188,17 +4372,18 @@ function moveGlobal(args, names, yours) {
|
|
|
4188
4372
|
moved,
|
|
4189
4373
|
...reason ? { reason } : {},
|
|
4190
4374
|
...left ? { left } : {},
|
|
4191
|
-
projects: changes.map(([root, c]) => ({ project:
|
|
4375
|
+
projects: changes.map(([root, c]) => ({ project: basename10(root), path: root, change: withDirs(root, [c])[0] }))
|
|
4192
4376
|
}))
|
|
4193
4377
|
);
|
|
4194
4378
|
}
|
|
4195
4379
|
for (const m of moves) {
|
|
4196
|
-
if (m.moved) success(`${m.name} now loads only in ${m.changes.map(([r]) =>
|
|
4380
|
+
if (m.moved) success(`${m.name} now loads only in ${m.changes.map(([r]) => basename10(r)).join(", ")} (the global copy is in skilllib restore)`);
|
|
4197
4381
|
else error(`${m.name}: ${m.reason}`);
|
|
4198
|
-
for (const [root, c] of m.changes) warnHeldLinks(root, c, `in ${
|
|
4382
|
+
for (const [root, c] of m.changes) warnHeldLinks(root, c, `in ${basename10(root)}`);
|
|
4199
4383
|
if (m.left) warn(`${m.name}: ${m.left.join(", ")} now point${m.left.length === 1 ? "s" : ""} at nothing (the repo's own copy serves that package): remove ${m.left.length === 1 ? "it" : "them"}`);
|
|
4200
4384
|
}
|
|
4201
|
-
|
|
4385
|
+
const toCommit = [...new Set(moves.filter((m) => m.moved).flatMap((m) => m.changes.map(([r]) => r)))].filter((r) => existsSync14(join15(r, MANIFEST_FILE)));
|
|
4386
|
+
if (toCommit.length) info(dim(`Commit skilllib.json and the skill folders in ${toCommit.map((r) => basename10(r)).join(", ")}.`));
|
|
4202
4387
|
}
|
|
4203
4388
|
function tidy(args) {
|
|
4204
4389
|
const names = args.positional;
|
|
@@ -4211,13 +4396,13 @@ function tidy(args) {
|
|
|
4211
4396
|
for (const name of names) {
|
|
4212
4397
|
const where = targets.map((root) => ({ root, ...foldersOf(root, name) })).filter((w) => w.all.length);
|
|
4213
4398
|
if (!where.length) {
|
|
4214
|
-
problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${
|
|
4399
|
+
problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${basename10(targets[0])}`}`);
|
|
4215
4400
|
continue;
|
|
4216
4401
|
}
|
|
4217
4402
|
if (!args.keep) continue;
|
|
4218
4403
|
for (const { root, real } of where) {
|
|
4219
4404
|
if (real.includes(keepFor(root)[name])) continue;
|
|
4220
|
-
const label = root ? `${
|
|
4405
|
+
const label = root ? `${basename10(root)} \xB7 ` : "";
|
|
4221
4406
|
problems.push(
|
|
4222
4407
|
`${label}${name}: no copy in ${args.keep}; ${real.length ? `pick one of: ${real.map((d) => folderLabel(root, d)).join(", ")}` : "it has no real copy to keep"}`
|
|
4223
4408
|
);
|
|
@@ -4227,7 +4412,7 @@ function tidy(args) {
|
|
|
4227
4412
|
throw new Error(problems.join("\n"));
|
|
4228
4413
|
}
|
|
4229
4414
|
const reports = targets.map(
|
|
4230
|
-
(root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label:
|
|
4415
|
+
(root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label: basename10(root), report: planProjectTidy(root, { keep: keepFor(root) }) }
|
|
4231
4416
|
);
|
|
4232
4417
|
let held = 0;
|
|
4233
4418
|
let changed = 0;
|