skilllib 1.11.7 → 1.12.1
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-PKQPUIN3.js → chunk-LORSEDNW.js} +805 -601
- package/dist/index.js +6 -4
- package/dist/json.d.cts +6 -0
- package/dist/json.d.ts +6 -0
- package/dist/{tui-B5PF3J3E.js → tui-3QD6RQGL.js} +165 -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, isAbsolute, join as join5, relative as relative2, resolve as resolve3, sep as sep3 } 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,149 @@ 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, Object.fromEntries(Object.entries(mine).filter(([name]) => !shared[name])));
|
|
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 top = realNative(root) ?? root;
|
|
534
|
+
const places = (dir) => {
|
|
535
|
+
const real = realNative(join5(root, dir));
|
|
536
|
+
const rel = real ? relative2(top, real).split(sep3).join("/") : "";
|
|
537
|
+
return rel && rel !== ".." && !rel.startsWith("../") && !isAbsolute(rel) && rel !== dir ? [dir, rel] : [dir];
|
|
538
|
+
};
|
|
539
|
+
const paths = Object.entries(skills).flatMap(
|
|
540
|
+
([name, d]) => [d.dir ?? PROJECT_SKILLS_DIR, ...d.links ?? []].flatMap(places).map((dir) => `/${dir}/${name.replace(/[[\]*?\\]/g, "\\$&")}`)
|
|
541
|
+
);
|
|
542
|
+
const block = paths.length ? [EXCLUDE_START, ...[...new Set(paths)].sort(), EXCLUDE_END] : [];
|
|
543
|
+
const next = [...rest, ...rest.length && block.length ? [""] : [], ...block].join("\n") + (rest.length || block.length ? "\n" : "");
|
|
544
|
+
if (next === text) return;
|
|
545
|
+
mkdirSync3(dirname2(file), { recursive: true });
|
|
546
|
+
writeFileSync2(file, next);
|
|
399
547
|
}
|
|
400
548
|
function projectsFile() {
|
|
401
549
|
return join5(skilllibHome(), "projects.json");
|
|
@@ -454,8 +602,9 @@ function configFile() {
|
|
|
454
602
|
return join6(skilllibHome(), "config.json");
|
|
455
603
|
}
|
|
456
604
|
function readConfig() {
|
|
457
|
-
const data = readState(configFile()) ?? {};
|
|
458
|
-
|
|
605
|
+
const { agentsDirOk, ...data } = readState(configFile()) ?? {};
|
|
606
|
+
const allowTracked = [.../* @__PURE__ */ new Set([...data.allowTracked ?? [], ...agentsDirOk ?? []])].sort();
|
|
607
|
+
return { ...data, roots: data.roots ?? [], hidden: data.hidden ?? [], ...allowTracked.length ? { allowTracked } : {} };
|
|
459
608
|
}
|
|
460
609
|
function writeConfig(config) {
|
|
461
610
|
writeState(configFile(), config);
|
|
@@ -466,7 +615,7 @@ function expandHome(path) {
|
|
|
466
615
|
function addRoot(path) {
|
|
467
616
|
const root = expandHome(path);
|
|
468
617
|
if (!existsSync5(root)) throw new Error(`${tildify(root)} doesn't exist`);
|
|
469
|
-
if (!
|
|
618
|
+
if (!statSync4(root).isDirectory()) throw new Error(`${tildify(root)} isn't a folder`);
|
|
470
619
|
const config = readConfig();
|
|
471
620
|
if (!config.roots.includes(root)) writeConfig({ ...config, roots: [...config.roots, root].sort() });
|
|
472
621
|
return root;
|
|
@@ -495,7 +644,7 @@ function findProjects(dir, depth = 4) {
|
|
|
495
644
|
}
|
|
496
645
|
let entries = [];
|
|
497
646
|
try {
|
|
498
|
-
entries =
|
|
647
|
+
entries = readdirSync3(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && !SKIP_DIRS.has(d.name)).map((d) => join6(dir, d.name));
|
|
499
648
|
} catch {
|
|
500
649
|
return [];
|
|
501
650
|
}
|
|
@@ -521,7 +670,10 @@ function setHarnesses(ids) {
|
|
|
521
670
|
}
|
|
522
671
|
function allowTrackedLinks(root) {
|
|
523
672
|
const config = readConfig();
|
|
524
|
-
writeConfig({ ...config,
|
|
673
|
+
writeConfig({ ...config, allowTracked: [.../* @__PURE__ */ new Set([...config.allowTracked ?? [], root])].sort() });
|
|
674
|
+
}
|
|
675
|
+
function allowsTrackedLinks(root) {
|
|
676
|
+
return readConfig().allowTracked?.includes(root) ?? false;
|
|
525
677
|
}
|
|
526
678
|
function keptGlobal() {
|
|
527
679
|
return new Set(readConfig().keepGlobal ?? []);
|
|
@@ -538,17 +690,17 @@ function setKeepGlobal(names, keep) {
|
|
|
538
690
|
}
|
|
539
691
|
|
|
540
692
|
// src/agentSkill.ts
|
|
541
|
-
import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as
|
|
693
|
+
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
694
|
import { join as join9 } from "path";
|
|
543
695
|
|
|
544
696
|
// 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
|
|
697
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
698
|
+
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";
|
|
699
|
+
import { basename as basename4, dirname as dirname3, isAbsolute as isAbsolute2, join as join8, posix, relative as relative3, resolve as resolve6, sep as sep5 } from "path";
|
|
548
700
|
|
|
549
701
|
// src/sources.ts
|
|
550
|
-
import { existsSync as existsSync6, lstatSync, mkdirSync as
|
|
551
|
-
import { basename as
|
|
702
|
+
import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readlinkSync } from "fs";
|
|
703
|
+
import { basename as basename3, join as join7, resolve as resolve5, sep as sep4 } from "path";
|
|
552
704
|
import { tmpdir } from "os";
|
|
553
705
|
function skillsShLock() {
|
|
554
706
|
return readJson(join7(userHome(), ".agents", ".skill-lock.json"))?.skills ?? {};
|
|
@@ -557,7 +709,7 @@ function projectSkillsLock(root) {
|
|
|
557
709
|
return readJson(join7(root, "skills-lock.json"))?.skills ?? {};
|
|
558
710
|
}
|
|
559
711
|
function vendorSkill(path, kind, origin, harnesses) {
|
|
560
|
-
return { name:
|
|
712
|
+
return { name: basename3(path), kind, origin, path, description: readSkillInfo(path).description, movable: false, broken: false, harnesses, links: [] };
|
|
561
713
|
}
|
|
562
714
|
function harnessesReading(dir, enabled) {
|
|
563
715
|
const real = realPath(dir);
|
|
@@ -588,7 +740,7 @@ function globalFolderSkills(enabled) {
|
|
|
588
740
|
const agentsDir = join7(userHome(), ".agents", "skills");
|
|
589
741
|
const entries = globalSkillDirs().flatMap((dir) => {
|
|
590
742
|
if (!isDir(dir)) return [];
|
|
591
|
-
return
|
|
743
|
+
return readdirSync4(dir).filter((name) => !name.startsWith(".") && name !== "synced").flatMap((name) => {
|
|
592
744
|
const path = join7(dir, name);
|
|
593
745
|
const isLink2 = lstatSync(path).isSymbolicLink();
|
|
594
746
|
const broken = !existsSync6(join7(path, "SKILL.md"));
|
|
@@ -656,7 +808,7 @@ function claudeAiSkills(enabled) {
|
|
|
656
808
|
byReal.set(real, entry);
|
|
657
809
|
}
|
|
658
810
|
return [...byReal.values()].flatMap(
|
|
659
|
-
({ dir, readers }) => (isDir(dir) ?
|
|
811
|
+
({ dir, readers }) => (isDir(dir) ? readdirSync4(dir) : []).filter((d) => !d.startsWith(".")).flatMap(
|
|
660
812
|
(bucket) => skillDirsIn(join7(dir, bucket)).map((path) => vendorSkill(path, "claude.ai", "claude.ai account", enabled.filter((id) => readers.includes(id))))
|
|
661
813
|
)
|
|
662
814
|
);
|
|
@@ -700,8 +852,8 @@ function pluginSkills(harnesses, root) {
|
|
|
700
852
|
return loadedBy.length ? skillDirsIn(join7(dir, "skills")).map((path) => ({ path, origin: id, loadedBy })) : [];
|
|
701
853
|
});
|
|
702
854
|
const syncedDir = join7(plugins, "synced");
|
|
703
|
-
const fromSynced = claude2 && existsSync6(syncedDir) ?
|
|
704
|
-
(bucket) =>
|
|
855
|
+
const fromSynced = claude2 && existsSync6(syncedDir) ? readdirSync4(syncedDir).filter((b) => !b.startsWith(".") && isDir(join7(syncedDir, b))).flatMap(
|
|
856
|
+
(bucket) => readdirSync4(join7(syncedDir, bucket), { withFileTypes: true }).filter((d) => d.isDirectory()).flatMap((d) => {
|
|
705
857
|
const root2 = join7(syncedDir, bucket, d.name);
|
|
706
858
|
const name = readJson(join7(root2, ".claude-plugin", "plugin.json"))?.name ?? d.name;
|
|
707
859
|
if (enabled[`${name}@synced`] === false) return [];
|
|
@@ -736,11 +888,11 @@ function libraryOrigins() {
|
|
|
736
888
|
function recordOrigin(name, origin) {
|
|
737
889
|
const origins = libraryOrigins();
|
|
738
890
|
if (origins[name] === origin) return;
|
|
739
|
-
|
|
891
|
+
mkdirSync4(skilllibHome(), { recursive: true });
|
|
740
892
|
writeState(originsFile(), { ...origins, [name]: origin });
|
|
741
893
|
}
|
|
742
894
|
function originFor(dir) {
|
|
743
|
-
const under = (path, folder) => path.startsWith((realpathOrNull(folder) ?? folder) +
|
|
895
|
+
const under = (path, folder) => path.startsWith((realpathOrNull(folder) ?? folder) + sep4);
|
|
744
896
|
const real = realpathOrNull(dir) ?? dir;
|
|
745
897
|
const temps = [tmpdir(), "/tmp", "/var/tmp"];
|
|
746
898
|
const homeInTemp = temps.some((t) => under(realpathOrNull(userHome()) ?? userHome(), t));
|
|
@@ -749,14 +901,14 @@ function originFor(dir) {
|
|
|
749
901
|
if (match) return match.kind === "global" ? "global folder" : `${match.kind}: ${match.origin}`;
|
|
750
902
|
const project = dir.split(/[\\/]\.(?:claude|agents|grok|cursor|codex)[\\/]skills[\\/]/)[0];
|
|
751
903
|
if (!project || project === dir) return dir;
|
|
752
|
-
const locked = projectSkillsLock(project)[
|
|
753
|
-
return locked ? `skills.sh: ${locked}` : `project: ${
|
|
904
|
+
const locked = projectSkillsLock(project)[basename3(dir)]?.source;
|
|
905
|
+
return locked ? `skills.sh: ${locked}` : `project: ${basename3(project)}`;
|
|
754
906
|
}
|
|
755
907
|
|
|
756
908
|
// src/library.ts
|
|
757
909
|
function librarySkills() {
|
|
758
910
|
return skillDirsIn(libraryDir()).map((dir) => ({
|
|
759
|
-
name:
|
|
911
|
+
name: basename4(dir),
|
|
760
912
|
description: readSkillInfo(dir).description,
|
|
761
913
|
dir
|
|
762
914
|
}));
|
|
@@ -778,7 +930,7 @@ function notWritable(root, dir) {
|
|
|
778
930
|
p = join8(p, part);
|
|
779
931
|
if (!entryExists(p)) return null;
|
|
780
932
|
const real = realpathOrNull(p);
|
|
781
|
-
if (!isDir(p) || real === null || real !== top && !real.startsWith(top +
|
|
933
|
+
if (!isDir(p) || real === null || real !== top && !real.startsWith(top + sep5)) return relative3(root, p).split(sep5).join("/");
|
|
782
934
|
}
|
|
783
935
|
return null;
|
|
784
936
|
}
|
|
@@ -829,7 +981,7 @@ function syncBlocker(root, name, dep = readManifest(root).skills[name]) {
|
|
|
829
981
|
}
|
|
830
982
|
function linkDir(target, link2) {
|
|
831
983
|
if (process.platform === "win32") symlinkSync(target, link2, "junction");
|
|
832
|
-
else symlinkSync(
|
|
984
|
+
else symlinkSync(relative3(dirname3(link2), target), link2);
|
|
833
985
|
}
|
|
834
986
|
function entryExists(path) {
|
|
835
987
|
return isLink(path) || existsSync7(path);
|
|
@@ -840,13 +992,6 @@ function visibilityOf(root, name, enabled = enabledHarnesses()) {
|
|
|
840
992
|
paths: harness(id).projectDirs.filter((dir) => existsSync7(join8(root, dir, name, "SKILL.md"))).length
|
|
841
993
|
}));
|
|
842
994
|
}
|
|
843
|
-
function realNative(p) {
|
|
844
|
-
try {
|
|
845
|
-
return realpathSync3.native(p);
|
|
846
|
-
} catch {
|
|
847
|
-
return null;
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
995
|
function survives(path, gone) {
|
|
851
996
|
return lostAt(path, gone) === null;
|
|
852
997
|
}
|
|
@@ -854,10 +999,10 @@ function lostAt(path, gone) {
|
|
|
854
999
|
const key = (p) => process.platform === "win32" ? p.toLowerCase() : p;
|
|
855
1000
|
const canon = (p) => {
|
|
856
1001
|
const dir = realNative(dirname3(p)) ?? dirname3(p);
|
|
857
|
-
const leaf =
|
|
1002
|
+
const leaf = basename4(p);
|
|
858
1003
|
try {
|
|
859
1004
|
lstatSync2(join8(dir, leaf));
|
|
860
|
-
const names =
|
|
1005
|
+
const names = readdirSync5(dir);
|
|
861
1006
|
return join8(dir, names.includes(leaf) ? leaf : names.find((n) => n.toLowerCase() === leaf.toLowerCase()) ?? leaf);
|
|
862
1007
|
} catch {
|
|
863
1008
|
return join8(dir, leaf);
|
|
@@ -868,7 +1013,7 @@ function lostAt(path, gone) {
|
|
|
868
1013
|
let last = null;
|
|
869
1014
|
for (let hop = 0; hop < 40; hop++) {
|
|
870
1015
|
const here = canon(at);
|
|
871
|
-
if (moved.some((m) => key(here) === m || key(here).startsWith(m +
|
|
1016
|
+
if (moved.some((m) => key(here) === m || key(here).startsWith(m + sep5))) return last ?? here;
|
|
872
1017
|
if (!isLink(here)) return existsSync7(join8(here, "SKILL.md")) ? null : here;
|
|
873
1018
|
last = here;
|
|
874
1019
|
at = resolve6(dirname3(here), readlinkSync2(here).replace(/^\\\\\?\\(UNC\\)?/, (_m, unc) => unc ? "\\\\" : ""));
|
|
@@ -882,7 +1027,7 @@ function blindWithoutGlobal(name, copies, roots, nestedOf = (root) => nestedEntr
|
|
|
882
1027
|
const holds = (root) => ALL_PROJECT_DIRS.some((d) => reposChoice(root, d) && entryExists(join8(root, d, name))) || pinsSkill(root, name);
|
|
883
1028
|
const keepsTop = (root, id) => harness(id).projectDirs.some((d) => existsSync7(join8(root, d, name, "SKILL.md")) && survives(join8(root, d, name), gone));
|
|
884
1029
|
const shown = (root, p) => {
|
|
885
|
-
const under = (dir) => ((d) => p.startsWith(d +
|
|
1030
|
+
const under = (dir) => ((d) => p.startsWith(d + sep5) ? relative3(d, p).split(sep5).join("/") : null)(realNative(dir) ?? dir);
|
|
886
1031
|
return under(root) ?? ((h) => h === null ? p : `~/${h}`)(under(userHome()));
|
|
887
1032
|
};
|
|
888
1033
|
const blindIn = (root) => enabled.flatMap((id) => {
|
|
@@ -910,42 +1055,42 @@ function blindWithoutGlobal(name, copies, roots, nestedOf = (root) => nestedEntr
|
|
|
910
1055
|
const had = byWho.get(k);
|
|
911
1056
|
if (!had || !(had.link || had.folder || had.shared) && (b.link || b.folder || b.shared)) byWho.set(k, b);
|
|
912
1057
|
}
|
|
913
|
-
const within = (d) => unique.some((r) => d === r || d.startsWith(r +
|
|
1058
|
+
const within = (d) => unique.some((r) => d === r || d.startsWith(r + sep5));
|
|
914
1059
|
const home2 = realNative(userHome()) ?? userHome();
|
|
915
|
-
const belowHome = (d) => (realNative(d) ?? d) !== home2 && !home2.startsWith((realNative(d) ?? d) +
|
|
1060
|
+
const belowHome = (d) => (realNative(d) ?? d) !== home2 && !home2.startsWith((realNative(d) ?? d) + sep5);
|
|
916
1061
|
const gitTop = (from) => {
|
|
917
1062
|
for (let d = from; d !== dirname3(d) && belowHome(d); d = dirname3(d)) if (existsSync7(join8(d, ".git"))) return d;
|
|
918
1063
|
return null;
|
|
919
1064
|
};
|
|
920
1065
|
const servedAbove = (b) => {
|
|
921
1066
|
const top = gitTop(b.root);
|
|
922
|
-
const inside = (d) => belowHome(d) && (top ? d === top || d.startsWith(top +
|
|
1067
|
+
const inside = (d) => belowHome(d) && (top ? d === top || d.startsWith(top + sep5) : within(d));
|
|
923
1068
|
for (let d = dirname3(b.root); inside(d) && d !== dirname3(d); d = dirname3(d)) if (keepsTop(d, b.id)) return true;
|
|
924
1069
|
return false;
|
|
925
1070
|
};
|
|
926
1071
|
return [...byWho.values()].filter((b) => !servedAbove(b));
|
|
927
1072
|
}
|
|
928
|
-
function strandedLinks(name, copies, roots, label =
|
|
1073
|
+
function strandedLinks(name, copies, roots, label = basename4) {
|
|
929
1074
|
const gone = copies.flatMap((c) => [c.path, ...c.links ?? []]);
|
|
930
1075
|
const unique = [...new Map(roots.map((r) => [realpathOrNull(r) ?? r, r])).values()];
|
|
931
1076
|
const seen = /* @__PURE__ */ new Set();
|
|
932
|
-
const once = (p) => ((k) => seen.has(k) ? false : (seen.add(k), true))(join8(realNative(dirname3(p)) ?? dirname3(p),
|
|
1077
|
+
const once = (p) => ((k) => seen.has(k) ? false : (seen.add(k), true))(join8(realNative(dirname3(p)) ?? dirname3(p), basename4(p)));
|
|
933
1078
|
return unique.flatMap((root) => [
|
|
934
1079
|
...ALL_PROJECT_DIRS.filter(
|
|
935
1080
|
(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))
|
|
936
1081
|
).map(
|
|
937
1082
|
(d) => `${d}/${name} in ${label(root)}`
|
|
938
1083
|
),
|
|
939
|
-
...nestedEntries(root).filter((n) => n.name === name && isLink(n.path) && !survives(n.path, gone) && once(n.path)).map((n) => `${
|
|
1084
|
+
...nestedEntries(root).filter((n) => n.name === name && isLink(n.path) && !survives(n.path, gone) && once(n.path)).map((n) => `${relative3(root, n.path).split(sep5).join("/")} in ${label(root)}`)
|
|
940
1085
|
]);
|
|
941
1086
|
}
|
|
942
1087
|
function copiesDiffer(paths) {
|
|
943
1088
|
return new Set(paths.map((p) => treeHash(p)).filter((h) => h !== null)).size > 1;
|
|
944
1089
|
}
|
|
945
|
-
function blindSaid(name, blind, label =
|
|
1090
|
+
function blindSaid(name, blind, label = basename4) {
|
|
946
1091
|
const who = blind.map((b) => `${harness(b.id).name} in ${label(b.root)}`).join(", ");
|
|
947
1092
|
const folders2 = [...new Set(blind.flatMap((b) => b.folder ? [`${b.folder} in ${label(b.root)}`] : []))];
|
|
948
|
-
const links = [...new Set(blind.flatMap((b) => b.link ? [b.link.startsWith("~") ||
|
|
1093
|
+
const links = [...new Set(blind.flatMap((b) => b.link ? [b.link.startsWith("~") || isAbsolute2(b.link) ? b.link : `${b.link} in ${label(b.root)}`] : []))];
|
|
949
1094
|
const sharedBy = /* @__PURE__ */ new Map();
|
|
950
1095
|
for (const b of blind.filter((x) => x.shared)) {
|
|
951
1096
|
if (!sharedBy.has(b.shared.to)) sharedBy.set(b.shared.to, { to: b.shared.to, where: /* @__PURE__ */ new Set(), dirs: /* @__PURE__ */ new Set() });
|
|
@@ -955,14 +1100,14 @@ function blindSaid(name, blind, label = basename3) {
|
|
|
955
1100
|
const shared = [...sharedBy.values()].map(
|
|
956
1101
|
(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
1102
|
);
|
|
958
|
-
const
|
|
1103
|
+
const repos2 = blind.filter((b) => !b.folder && !b.link && !b.shared);
|
|
959
1104
|
const fixes = [
|
|
960
1105
|
...folders2.length ? [`point ${folders2.join(", ")} back into the repo`] : [],
|
|
961
1106
|
// A package's link to the global copy: add or link at the repo's top doesn't touch it.
|
|
962
1107
|
...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
1108
|
...shared,
|
|
964
1109
|
// Named, so with other advice beside it, it's clear which repo (or package) this is about.
|
|
965
|
-
...
|
|
1110
|
+
...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
1111
|
];
|
|
967
1112
|
return `${who} only ${blind.length === 1 ? "sees" : "see"} the global copy: ${fixes.join("; ")}, or keep ${name} global (skilllib global keep)`;
|
|
968
1113
|
}
|
|
@@ -997,7 +1142,7 @@ function pinsSkill(root, name) {
|
|
|
997
1142
|
}
|
|
998
1143
|
function isGitTracked(root, dir) {
|
|
999
1144
|
try {
|
|
1000
|
-
const out2 =
|
|
1145
|
+
const out2 = execFileSync2("git", ["ls-files", "--", dir], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
1001
1146
|
return out2.trim().length > 0;
|
|
1002
1147
|
} catch {
|
|
1003
1148
|
return false;
|
|
@@ -1024,7 +1169,8 @@ function projectStatus(root) {
|
|
|
1024
1169
|
latest: latest?.version ?? null,
|
|
1025
1170
|
location,
|
|
1026
1171
|
path,
|
|
1027
|
-
visibility: visibilityOf(root, name, enabled)
|
|
1172
|
+
visibility: visibilityOf(root, name, enabled),
|
|
1173
|
+
...dep.personal ? { personal: true } : {}
|
|
1028
1174
|
};
|
|
1029
1175
|
});
|
|
1030
1176
|
const seen = new Set(Object.keys(skills));
|
|
@@ -1033,14 +1179,14 @@ function projectStatus(root) {
|
|
|
1033
1179
|
const byName = /* @__PURE__ */ new Map();
|
|
1034
1180
|
for (const dir of ALL_PROJECT_DIRS.filter((d) => writableDir(root, d))) {
|
|
1035
1181
|
for (const path of skillDirsIn(join8(root, dir))) {
|
|
1036
|
-
const name =
|
|
1182
|
+
const name = basename4(path);
|
|
1037
1183
|
if (seen.has(name)) continue;
|
|
1038
1184
|
byName.set(name, [...byName.get(name) ?? [], path]);
|
|
1039
1185
|
}
|
|
1040
1186
|
}
|
|
1041
1187
|
for (const [name, paths] of byName) {
|
|
1042
1188
|
const real = paths.find((p) => !isLink(p)) ?? paths[0];
|
|
1043
|
-
const location =
|
|
1189
|
+
const location = relative3(root, join8(real, "..")).split(sep5).join("/");
|
|
1044
1190
|
const source = lock[name]?.source;
|
|
1045
1191
|
unmanaged.push({
|
|
1046
1192
|
name,
|
|
@@ -1088,8 +1234,8 @@ function nestedEntries(root, { depth = PACKAGE_DEPTH, enabled = enabledHarnesses
|
|
|
1088
1234
|
const top = realpathOrNull(root) ?? root;
|
|
1089
1235
|
const found = packageSkillFolders(root, subs ?? packageDirs(root, depth)).flatMap(
|
|
1090
1236
|
({ sub, skillsDir, location }) => skillDirsIn(join8(sub, skillsDir)).flatMap((path) => {
|
|
1091
|
-
if (isLink(path) && (realpathOrNull(path) ?? "").startsWith(top +
|
|
1092
|
-
const name =
|
|
1237
|
+
if (isLink(path) && (realpathOrNull(path) ?? "").startsWith(top + sep5)) return [];
|
|
1238
|
+
const name = basename4(path);
|
|
1093
1239
|
const readers = NESTED_READERS.filter(([d]) => realpathOrNull(join8(sub, d, name)) === realpathOrNull(path)).flatMap(([, r]) => r);
|
|
1094
1240
|
return [{ name, path, location, skillsDir, readers: enabled.filter((id) => readers.includes(id)) }];
|
|
1095
1241
|
})
|
|
@@ -1102,7 +1248,7 @@ function packageDirs(root, depth = PACKAGE_DEPTH) {
|
|
|
1102
1248
|
const walk = (dir, left) => {
|
|
1103
1249
|
let subs = [];
|
|
1104
1250
|
try {
|
|
1105
|
-
subs =
|
|
1251
|
+
subs = readdirSync5(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && !BUILD_DIRS.has(d.name)).map((d) => join8(dir, d.name));
|
|
1106
1252
|
} catch {
|
|
1107
1253
|
return;
|
|
1108
1254
|
}
|
|
@@ -1119,7 +1265,7 @@ function packageSkillFolders(root, subs = packageDirs(root)) {
|
|
|
1119
1265
|
return subs.flatMap(
|
|
1120
1266
|
(sub) => NESTED_READERS.flatMap(([skillsDir]) => {
|
|
1121
1267
|
if (!existsSync7(join8(sub, skillsDir))) return [];
|
|
1122
|
-
const location =
|
|
1268
|
+
const location = relative3(root, join8(sub, skillsDir)).split(sep5).join("/");
|
|
1123
1269
|
return reposChoice(root, location) ? [{ sub, skillsDir, location }] : [];
|
|
1124
1270
|
})
|
|
1125
1271
|
);
|
|
@@ -1128,11 +1274,14 @@ function sharedByGit(root, location, name) {
|
|
|
1128
1274
|
const copy = `${location}/${name}`;
|
|
1129
1275
|
if (isGitTracked(root, copy)) return true;
|
|
1130
1276
|
if (!isGitTracked(root, location)) return false;
|
|
1277
|
+
return !gitIgnores(root, copy);
|
|
1278
|
+
}
|
|
1279
|
+
function gitIgnores(root, rel) {
|
|
1131
1280
|
try {
|
|
1132
|
-
|
|
1133
|
-
return false;
|
|
1134
|
-
} catch {
|
|
1281
|
+
execFileSync2("git", ["check-ignore", "-q", "--no-index", "--", rel], { cwd: root, stdio: "ignore" });
|
|
1135
1282
|
return true;
|
|
1283
|
+
} catch {
|
|
1284
|
+
return false;
|
|
1136
1285
|
}
|
|
1137
1286
|
}
|
|
1138
1287
|
function linkEverywhere(root, name, location, { allowTracked = false, dryRun = false } = {}) {
|
|
@@ -1155,19 +1304,19 @@ function linkEverywhere(root, name, location, { allowTracked = false, dryRun = f
|
|
|
1155
1304
|
stuck(`${dir}/${name}`);
|
|
1156
1305
|
continue;
|
|
1157
1306
|
}
|
|
1158
|
-
if (isGitTracked(root, dir)) {
|
|
1307
|
+
if (isGitTracked(root, dir) && !gitIgnores(root, `${dir}/${name}`)) {
|
|
1159
1308
|
if (!(shared ??= sharedByGit(root, location, name))) {
|
|
1160
1309
|
if (!result.uncommitted.includes(dir)) result.uncommitted.push(dir);
|
|
1161
1310
|
continue;
|
|
1162
1311
|
}
|
|
1163
|
-
if (!allowTracked && !
|
|
1312
|
+
if (!allowTracked && !allowsTrackedLinks(root)) {
|
|
1164
1313
|
if (!result.blocked.includes(dir)) result.blocked.push(dir);
|
|
1165
1314
|
continue;
|
|
1166
1315
|
}
|
|
1167
1316
|
}
|
|
1168
1317
|
if (!dryRun) {
|
|
1169
1318
|
if (broken) unlinkSync(link2);
|
|
1170
|
-
|
|
1319
|
+
mkdirSync5(join8(root, dir), { recursive: true });
|
|
1171
1320
|
linkDir(join8(root, location, name), link2);
|
|
1172
1321
|
}
|
|
1173
1322
|
result.created.push(dir);
|
|
@@ -1186,11 +1335,12 @@ function relinkDependency(root, name, { allowTracked = false } = {}) {
|
|
|
1186
1335
|
const manifest = readManifest(root);
|
|
1187
1336
|
const dep = manifest.skills[name];
|
|
1188
1337
|
if (!dep) return { created: [], blocked: [], uncommitted: [], inTheWay: [] };
|
|
1338
|
+
if (dep.personal) excludePersonal(root, { ...personalOf(manifest), [name]: { ...dep, links: [...ALL_PROJECT_DIRS] } });
|
|
1189
1339
|
const result = linkEverywhere(root, name, dep.dir ?? PROJECT_SKILLS_DIR, { allowTracked });
|
|
1190
1340
|
if (result.created.length) {
|
|
1191
1341
|
manifest.skills[name] = { ...dep, links: [.../* @__PURE__ */ new Set([...dep.links ?? [], ...result.created])] };
|
|
1192
1342
|
writeManifest(root, manifest);
|
|
1193
|
-
}
|
|
1343
|
+
} else if (dep.personal) excludePersonal(root);
|
|
1194
1344
|
return result;
|
|
1195
1345
|
}
|
|
1196
1346
|
function linkAll(root, { allowTracked = false, names } = {}) {
|
|
@@ -1237,17 +1387,30 @@ function addRefusal(root, name, hash) {
|
|
|
1237
1387
|
const plan = addPlan(root, name, hash, false);
|
|
1238
1388
|
return "reason" in plan ? plan.reason : null;
|
|
1239
1389
|
}
|
|
1240
|
-
function addSkill(root, name, { force = false, version, allowTracked = false } = {}) {
|
|
1390
|
+
function addSkill(root, name, { force = false, version, allowTracked = false, personal } = {}) {
|
|
1241
1391
|
if (!name || name === "." || name === ".." || /[\\/]/.test(name)) return { name, action: "skipped", reason: "not a skill name" };
|
|
1242
|
-
const
|
|
1392
|
+
const before = personal !== void 0 && version === void 0 ? readManifest(root).skills[name] : void 0;
|
|
1393
|
+
const moving = !!before && !!before.personal !== personal;
|
|
1394
|
+
const keep = moving ? versionForHash(name, before.hash) : null;
|
|
1395
|
+
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` };
|
|
1396
|
+
const target = keep ?? (version === void 0 ? latestVersion(name) : getVersion(name, version));
|
|
1243
1397
|
if (!target) return { name, action: "skipped", reason: version === void 0 ? "not in the library" : `version ${version} doesn't exist` };
|
|
1244
1398
|
const plan = addPlan(root, name, target.hash, force);
|
|
1245
1399
|
if ("reason" in plan) return { name, action: "skipped", reason: plan.reason };
|
|
1246
1400
|
const { manifest, recorded, primary, wantedLinks, to, local } = plan;
|
|
1401
|
+
const mine = pinsWhere(root, recorded, personal);
|
|
1402
|
+
if (typeof mine === "string") return { name, action: "skipped", reason: mine };
|
|
1403
|
+
const committed = [primary, ...recorded?.links ?? []].find((d) => isGitTracked(root, `${d}/${name}`));
|
|
1404
|
+
if (mine && !recorded?.personal && committed)
|
|
1405
|
+
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, "'\\''")}')` };
|
|
1406
|
+
if (mine || recorded?.personal) {
|
|
1407
|
+
const { [name]: _, ...others } = personalOf(manifest);
|
|
1408
|
+
excludePersonal(root, mine ? { ...others, [name]: { version: target.version, hash: target.hash, dir: primary, links: wantedLinks } } : others);
|
|
1409
|
+
}
|
|
1247
1410
|
let backedUp;
|
|
1248
1411
|
const copied = local !== target.hash || isLink(to);
|
|
1249
1412
|
if (copied) {
|
|
1250
|
-
|
|
1413
|
+
mkdirSync5(join8(root, primary), { recursive: true });
|
|
1251
1414
|
if (isLink(to)) unlinkSync(to);
|
|
1252
1415
|
else if (local !== null && versionForHash(name, local) === null) backedUp = stash(to, "edit-backup");
|
|
1253
1416
|
copySkill(versionDir(name, target.version), to);
|
|
@@ -1258,11 +1421,19 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
1258
1421
|
const inTheWay = [];
|
|
1259
1422
|
let shared;
|
|
1260
1423
|
let relinked = false;
|
|
1424
|
+
const toShared = !!recorded?.personal && !mine;
|
|
1425
|
+
const heldBack = (dir) => {
|
|
1426
|
+
if (!isGitTracked(root, dir) || gitIgnores(root, `${dir}/${name}`)) return false;
|
|
1427
|
+
if (!(shared ??= sharedByGit(root, primary, name))) return uncommitted.push(dir), true;
|
|
1428
|
+
if (!allowTracked && !allowsTrackedLinks(root)) return blocked.push(dir), true;
|
|
1429
|
+
return false;
|
|
1430
|
+
};
|
|
1261
1431
|
for (const dir of wantedLinks) {
|
|
1262
1432
|
const link2 = join8(root, dir, name);
|
|
1263
1433
|
const slot = slotAt(link2, to);
|
|
1264
1434
|
if (slot === "ok") {
|
|
1265
|
-
|
|
1435
|
+
if (toShared && isLink(link2) && heldBack(dir)) unlinkSync(link2);
|
|
1436
|
+
else links.push(dir);
|
|
1266
1437
|
continue;
|
|
1267
1438
|
}
|
|
1268
1439
|
if (slot === "taken") {
|
|
@@ -1270,18 +1441,9 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
1270
1441
|
if (recorded?.links?.includes(dir)) links.push(dir);
|
|
1271
1442
|
continue;
|
|
1272
1443
|
}
|
|
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
|
-
}
|
|
1444
|
+
if ((toShared || !recorded?.links?.includes(dir)) && heldBack(dir)) continue;
|
|
1283
1445
|
if (isLink(link2)) unlinkSync(link2);
|
|
1284
|
-
|
|
1446
|
+
mkdirSync5(join8(root, dir), { recursive: true });
|
|
1285
1447
|
linkDir(to, link2);
|
|
1286
1448
|
links.push(dir);
|
|
1287
1449
|
relinked = true;
|
|
@@ -1290,9 +1452,11 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
1290
1452
|
version: target.version,
|
|
1291
1453
|
hash: target.hash,
|
|
1292
1454
|
...primary !== PROJECT_SKILLS_DIR ? { dir: primary } : {},
|
|
1293
|
-
...links.length ? { links } : {}
|
|
1455
|
+
...links.length ? { links } : {},
|
|
1456
|
+
...mine ? { personal: true } : {}
|
|
1294
1457
|
};
|
|
1295
1458
|
writeManifest(root, manifest);
|
|
1459
|
+
const moved = recorded && !!recorded.personal !== mine ? mine ? "personal" : "shared" : void 0;
|
|
1296
1460
|
const from = recorded ? versionForHash(name, recorded.hash)?.version ?? recorded.version : void 0;
|
|
1297
1461
|
return {
|
|
1298
1462
|
name,
|
|
@@ -1308,9 +1472,16 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
1308
1472
|
...blocked.length ? { blocked } : {},
|
|
1309
1473
|
...uncommitted.length ? { uncommitted } : {},
|
|
1310
1474
|
...inTheWay.length ? { inTheWay } : {},
|
|
1311
|
-
...backedUp ? { backedUp } : {}
|
|
1475
|
+
...backedUp ? { backedUp } : {},
|
|
1476
|
+
...moved ? { moved } : {}
|
|
1312
1477
|
};
|
|
1313
1478
|
}
|
|
1479
|
+
function pinsWhere(root, recorded, personal) {
|
|
1480
|
+
const want = personal ?? (recorded ? !!recorded.personal : !existsSync7(join8(root, MANIFEST_FILE)));
|
|
1481
|
+
if (!want || personalPinsFile(root)) return want;
|
|
1482
|
+
return personal ? "only the top folder of a git repo can have personal pins" : false;
|
|
1483
|
+
}
|
|
1484
|
+
var personalOf = (manifest) => Object.fromEntries(Object.entries(manifest.skills).filter(([, d]) => d.personal));
|
|
1314
1485
|
function removeSkill(root, name, { force = false } = {}) {
|
|
1315
1486
|
const manifest = readManifest(root);
|
|
1316
1487
|
const recorded = manifest.skills[name];
|
|
@@ -1352,13 +1523,14 @@ function removeSkill(root, name, { force = false } = {}) {
|
|
|
1352
1523
|
};
|
|
1353
1524
|
unlinkOwn();
|
|
1354
1525
|
const backedUp = local !== null && !inLibrary ? stash(dir, "edit-backup") : void 0;
|
|
1355
|
-
if (!backedUp)
|
|
1526
|
+
if (!backedUp) rmSync4(dir, { recursive: true, force: true });
|
|
1356
1527
|
delete manifest.skills[name];
|
|
1357
1528
|
writeManifest(root, manifest);
|
|
1358
1529
|
const left = [...leftLinks, ...takenLinks];
|
|
1359
1530
|
return { name, action: "removed", ...left.length ? { reason: left.join("; ") } : {}, ...backedUp ? { backedUp } : {} };
|
|
1360
1531
|
}
|
|
1361
1532
|
function syncProject(root, { force = false, names } = {}) {
|
|
1533
|
+
excludePersonal(root);
|
|
1362
1534
|
return Object.keys(readManifest(root).skills).filter((name) => !names || names.includes(name)).flatMap((name) => syncSkill(root, name, { force }));
|
|
1363
1535
|
}
|
|
1364
1536
|
function syncSkill(root, name, { force = false } = {}) {
|
|
@@ -1374,7 +1546,7 @@ function syncSkill(root, name, { force = false } = {}) {
|
|
|
1374
1546
|
if (typeof version === "string") return [{ name, action: "skipped", reason: version }];
|
|
1375
1547
|
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
1548
|
const backedUp = local !== null && versionForHash(name, local) === null ? stash(copy, "edit-backup") : void 0;
|
|
1377
|
-
|
|
1549
|
+
mkdirSync5(dirname3(copy), { recursive: true });
|
|
1378
1550
|
copySkill(versionDir(name, version), copy);
|
|
1379
1551
|
const links = relinkPinned(root, name, dep);
|
|
1380
1552
|
const from = (local !== null ? versionForHash(name, local)?.version : void 0) ?? version;
|
|
@@ -1396,7 +1568,7 @@ function relinkPinned(root, name, dep) {
|
|
|
1396
1568
|
if (realpathOrNull(link2) === realpathOrNull(to)) continue;
|
|
1397
1569
|
if (isLink(link2)) unlinkSync(link2);
|
|
1398
1570
|
else if (entryExists(link2)) return { name, action: "skipped", reason: `${dir}/${name} is in the way of its link: move it, then sync` };
|
|
1399
|
-
|
|
1571
|
+
mkdirSync5(join8(root, dir), { recursive: true });
|
|
1400
1572
|
linkDir(to, link2);
|
|
1401
1573
|
}
|
|
1402
1574
|
return { name, action: "relinked", to: versionForHash(name, dep.hash)?.version ?? dep.version };
|
|
@@ -1426,13 +1598,13 @@ function updateProject(root, names, { force = false, allowTracked = false } = {}
|
|
|
1426
1598
|
});
|
|
1427
1599
|
}
|
|
1428
1600
|
function importSkill(dir, { force = false } = {}) {
|
|
1429
|
-
const name =
|
|
1601
|
+
const name = basename4(dir);
|
|
1430
1602
|
const to = librarySkillDir(name);
|
|
1431
1603
|
const existing = treeHash(to);
|
|
1432
1604
|
const incoming = treeHash(dir);
|
|
1433
1605
|
if (existing === incoming) return { name, status: "unchanged" };
|
|
1434
1606
|
if (existing !== null && !force) return { name, status: "exists" };
|
|
1435
|
-
|
|
1607
|
+
mkdirSync5(libraryDir(), { recursive: true });
|
|
1436
1608
|
const origin = existing !== null && libraryOrigins()[name] ? null : originFor(dir);
|
|
1437
1609
|
if (existing !== null) {
|
|
1438
1610
|
forgetLatest(name);
|
|
@@ -1452,27 +1624,27 @@ function readBackupIndex(bucket) {
|
|
|
1452
1624
|
}
|
|
1453
1625
|
function move(from, to) {
|
|
1454
1626
|
try {
|
|
1455
|
-
|
|
1627
|
+
renameSync3(from, to);
|
|
1456
1628
|
} catch (err) {
|
|
1457
1629
|
if (err.code !== "EXDEV") throw err;
|
|
1458
1630
|
cpSync2(from, to, { recursive: true, verbatimSymlinks: true });
|
|
1459
|
-
|
|
1631
|
+
rmSync4(from, { recursive: true, force: true });
|
|
1460
1632
|
}
|
|
1461
1633
|
}
|
|
1462
1634
|
function stash(dir, bucket) {
|
|
1463
1635
|
let at = Date.now();
|
|
1464
|
-
const entryAt = (ms) => `${
|
|
1636
|
+
const entryAt = (ms) => `${basename4(dir)}-${new Date(ms).toISOString().replace(/[:.]/g, "-")}`;
|
|
1465
1637
|
while (entryExists(join8(skilllibHome(), bucket, entryAt(at)))) at++;
|
|
1466
1638
|
const entry = entryAt(at);
|
|
1467
1639
|
const target = join8(skilllibHome(), bucket, entry);
|
|
1468
1640
|
const index = readBackupIndex(bucket);
|
|
1469
|
-
|
|
1641
|
+
mkdirSync5(join8(skilllibHome(), bucket), { recursive: true });
|
|
1470
1642
|
move(dir, target);
|
|
1471
1643
|
writeState(backupIndexFile(bucket), { ...index, [entry]: dir });
|
|
1472
1644
|
return target;
|
|
1473
1645
|
}
|
|
1474
1646
|
function removeUntracked(root, name, path) {
|
|
1475
|
-
const stuck = notWritable(root,
|
|
1647
|
+
const stuck = notWritable(root, relative3(root, dirname3(path)));
|
|
1476
1648
|
if (stuck) throw new Error(`${stuck} isn't a folder in the repo: skilllib leaves what's in it alone`);
|
|
1477
1649
|
const to = stash(path, "trash");
|
|
1478
1650
|
unlinkEverywhere(root, name);
|
|
@@ -1483,7 +1655,7 @@ function projectsUsing(name) {
|
|
|
1483
1655
|
}
|
|
1484
1656
|
function deleteLibrarySkill(name) {
|
|
1485
1657
|
const using = projectsUsing(name);
|
|
1486
|
-
if (using.length > 0) return { ok: false, reason: `still installed in ${using.map((p) =>
|
|
1658
|
+
if (using.length > 0) return { ok: false, reason: `still installed in ${using.map((p) => basename4(p)).join(", ")}` };
|
|
1487
1659
|
const dir = librarySkillDir(name);
|
|
1488
1660
|
if (!existsSync7(dir)) return { ok: false, reason: "not in the library" };
|
|
1489
1661
|
forgetLatest(name);
|
|
@@ -1493,7 +1665,7 @@ function deleteGlobal(path, links = []) {
|
|
|
1493
1665
|
return unloadGlobal(path, links, { requireLibrary: false });
|
|
1494
1666
|
}
|
|
1495
1667
|
function unloadGlobal(path, links = [], { requireLibrary = true } = {}) {
|
|
1496
|
-
const name =
|
|
1668
|
+
const name = basename4(path);
|
|
1497
1669
|
if (!entryExists(path) || name === "synced") return { ok: false, reason: "not a global skill" };
|
|
1498
1670
|
const broken = !existsSync7(join8(path, "SKILL.md"));
|
|
1499
1671
|
if (requireLibrary && !broken && latestVersion(name) === null) return { ok: false, reason: "import it into the library first" };
|
|
@@ -1503,7 +1675,7 @@ function unloadGlobal(path, links = [], { requireLibrary = true } = {}) {
|
|
|
1503
1675
|
return { ok: true, movedTo };
|
|
1504
1676
|
}
|
|
1505
1677
|
function linkGlobal(path, agents) {
|
|
1506
|
-
const name =
|
|
1678
|
+
const name = basename4(path);
|
|
1507
1679
|
const linked = [];
|
|
1508
1680
|
const skipped = [];
|
|
1509
1681
|
for (const dir of new Set(agents.map((id) => harness(id).globalDirs()[0]))) {
|
|
@@ -1513,7 +1685,7 @@ function linkGlobal(path, agents) {
|
|
|
1513
1685
|
if (realpathOrNull(link2) !== realpathOrNull(path)) skipped.push(link2);
|
|
1514
1686
|
continue;
|
|
1515
1687
|
}
|
|
1516
|
-
|
|
1688
|
+
mkdirSync5(dir, { recursive: true });
|
|
1517
1689
|
linkDir(realpathOrNull(path) ?? path, join8(realpathOrNull(dir) ?? dir, name));
|
|
1518
1690
|
linked.push(link2);
|
|
1519
1691
|
}
|
|
@@ -1523,8 +1695,8 @@ function createSkill(name, description) {
|
|
|
1523
1695
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) return { ok: false, reason: "use lowercase letters, digits and dashes" };
|
|
1524
1696
|
const dir = librarySkillDir(name);
|
|
1525
1697
|
if (existsSync7(dir)) return { ok: false, reason: `${name} already exists` };
|
|
1526
|
-
|
|
1527
|
-
|
|
1698
|
+
mkdirSync5(dir, { recursive: true });
|
|
1699
|
+
writeFileSync3(
|
|
1528
1700
|
join8(dir, "SKILL.md"),
|
|
1529
1701
|
// JSON strings are valid YAML: a description with ": " or "#" stays one value.
|
|
1530
1702
|
`---
|
|
@@ -1545,7 +1717,7 @@ function listBackups() {
|
|
|
1545
1717
|
const dir = join8(skilllibHome(), kind);
|
|
1546
1718
|
if (!existsSync7(dir)) return [];
|
|
1547
1719
|
const index = readBackupIndex(kind);
|
|
1548
|
-
return
|
|
1720
|
+
return readdirSync5(dir).flatMap((entry) => {
|
|
1549
1721
|
const match = entry.match(/^(.*)-(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-\d{3}Z$/);
|
|
1550
1722
|
if (!match) return [];
|
|
1551
1723
|
const name = match[1];
|
|
@@ -1568,9 +1740,9 @@ function restoreBackup(backup) {
|
|
|
1568
1740
|
const to = backup.from;
|
|
1569
1741
|
if (!to) return { ok: false, reason: `skilllib doesn't know where it came from; its files are in ${tildify(backup.path)}` };
|
|
1570
1742
|
if (backup.kind === "tidy-backup" && isLink(to)) unlinkSync(to);
|
|
1571
|
-
if (backup.kind === "edit-backup" && !isLink(to) && versionForHash(backup.name, treeHash(to) ?? "") !== null)
|
|
1743
|
+
if (backup.kind === "edit-backup" && !isLink(to) && versionForHash(backup.name, treeHash(to) ?? "") !== null) rmSync4(to, { recursive: true, force: true });
|
|
1572
1744
|
if (entryExists(to)) return { ok: false, reason: `${backup.name} already exists there` };
|
|
1573
|
-
|
|
1745
|
+
mkdirSync5(join8(to, ".."), { recursive: true });
|
|
1574
1746
|
move(backup.path, to);
|
|
1575
1747
|
forgetLatest(backup.name);
|
|
1576
1748
|
return { ok: true, to };
|
|
@@ -1606,13 +1778,13 @@ function installAgentSkill(enabled = enabledHarnesses()) {
|
|
|
1606
1778
|
const foreign = [primary, ...links].find((d) => entryExists(d) && !isOurs(d) && !isDangling(d));
|
|
1607
1779
|
if (foreign) return { ok: false, reason: `${tildify(foreign)} already exists and isn't skilllib's` };
|
|
1608
1780
|
for (const d of [primary, ...links]) if (isDangling(d)) unlinkSync2(d);
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1781
|
+
mkdirSync6(primary, { recursive: true });
|
|
1782
|
+
writeFileSync4(join9(primary, "SKILL.md"), shippedSkill());
|
|
1783
|
+
writeFileSync4(join9(primary, OWN_SKILL_MARKER), "Installed by skilllib. `skilllib agent-skill remove` removes it.\n");
|
|
1612
1784
|
for (const link2 of links) {
|
|
1613
|
-
if (entryExists(link2) && !isLink(link2) && isOurs(link2))
|
|
1785
|
+
if (entryExists(link2) && !isLink(link2) && isOurs(link2)) rmSync5(link2, { recursive: true, force: true });
|
|
1614
1786
|
if (entryExists(link2)) continue;
|
|
1615
|
-
|
|
1787
|
+
mkdirSync6(join9(link2, ".."), { recursive: true });
|
|
1616
1788
|
linkDir(primary, link2);
|
|
1617
1789
|
}
|
|
1618
1790
|
return { ok: true, dirs: [primary, ...links] };
|
|
@@ -1623,7 +1795,7 @@ function removeAgentSkill() {
|
|
|
1623
1795
|
for (const d of removed) {
|
|
1624
1796
|
if (!entryExists(d)) continue;
|
|
1625
1797
|
if (lstatSync3(d).isSymbolicLink()) unlinkSync2(d);
|
|
1626
|
-
else
|
|
1798
|
+
else rmSync5(d, { recursive: true, force: true });
|
|
1627
1799
|
}
|
|
1628
1800
|
return removed;
|
|
1629
1801
|
}
|
|
@@ -1637,18 +1809,18 @@ function refreshAgentSkill() {
|
|
|
1637
1809
|
// src/commands.ts
|
|
1638
1810
|
import { existsSync as existsSync14, readFileSync as readFileSync7 } from "fs";
|
|
1639
1811
|
import { parseArgs as parseFlags } from "util";
|
|
1640
|
-
import { basename as
|
|
1812
|
+
import { basename as basename10, join as join15, resolve as resolve8, sep as sep9 } from "path";
|
|
1641
1813
|
|
|
1642
1814
|
// src/git.ts
|
|
1643
|
-
import { execFileSync as
|
|
1644
|
-
import { existsSync as existsSync9, realpathSync as
|
|
1645
|
-
import { isAbsolute as
|
|
1815
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1816
|
+
import { existsSync as existsSync9, realpathSync as realpathSync3 } from "fs";
|
|
1817
|
+
import { isAbsolute as isAbsolute3, join as join10, relative as relative4, sep as sep6 } from "path";
|
|
1646
1818
|
function isShared(state) {
|
|
1647
1819
|
return state === "committed" || state === "changed";
|
|
1648
1820
|
}
|
|
1649
1821
|
function git(root, args, input) {
|
|
1650
1822
|
try {
|
|
1651
|
-
return
|
|
1823
|
+
return execFileSync3("git", args, { cwd: root, encoding: "utf-8", input, stdio: ["pipe", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 });
|
|
1652
1824
|
} catch (err) {
|
|
1653
1825
|
const out2 = err.stdout;
|
|
1654
1826
|
return typeof out2 === "string" ? out2 : null;
|
|
@@ -1657,21 +1829,21 @@ function git(root, args, input) {
|
|
|
1657
1829
|
function gitInfo(root, extraDirs = []) {
|
|
1658
1830
|
let inRepo;
|
|
1659
1831
|
try {
|
|
1660
|
-
inRepo =
|
|
1832
|
+
inRepo = execFileSync3("git", ["rev-parse", "--show-prefix"], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1661
1833
|
} catch {
|
|
1662
1834
|
if (!existsSync9(join10(root, ".git"))) return null;
|
|
1663
1835
|
inRepo = "";
|
|
1664
1836
|
}
|
|
1665
1837
|
const real = (p) => {
|
|
1666
1838
|
try {
|
|
1667
|
-
return
|
|
1839
|
+
return realpathSync3.native(p);
|
|
1668
1840
|
} catch {
|
|
1669
1841
|
return p;
|
|
1670
1842
|
}
|
|
1671
1843
|
};
|
|
1672
1844
|
const realRoot = real(root);
|
|
1673
1845
|
const places = new Map(
|
|
1674
|
-
[.../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS, MANIFEST_FILE, ...extraDirs])].filter((d) => existsSync9(join10(root, d))).map((d) => [d,
|
|
1846
|
+
[.../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS, MANIFEST_FILE, ...extraDirs])].filter((d) => existsSync9(join10(root, d))).map((d) => [d, relative4(realRoot, real(join10(root, d))).split(sep6).join("/")]).filter(([, place]) => place !== "" && place !== ".." && !place.startsWith("../") && !isAbsolute3(place))
|
|
1675
1847
|
);
|
|
1676
1848
|
const dirs = [...new Set(places.values())];
|
|
1677
1849
|
if (dirs.length === 0) return { of: () => "new" };
|
|
@@ -1699,27 +1871,27 @@ function gitInfo(root, extraDirs = []) {
|
|
|
1699
1871
|
};
|
|
1700
1872
|
}
|
|
1701
1873
|
function relativeTo(root, path) {
|
|
1702
|
-
return
|
|
1874
|
+
return relative4(root, path).split("\\").join("/");
|
|
1703
1875
|
}
|
|
1704
1876
|
|
|
1705
1877
|
// src/here.ts
|
|
1706
|
-
import { readdirSync as
|
|
1707
|
-
import { basename as
|
|
1878
|
+
import { readdirSync as readdirSync9 } from "fs";
|
|
1879
|
+
import { basename as basename8 } from "path";
|
|
1708
1880
|
|
|
1709
1881
|
// src/health.ts
|
|
1710
|
-
import { existsSync as existsSync12, lstatSync as lstatSync4, readdirSync as
|
|
1711
|
-
import { basename as
|
|
1882
|
+
import { existsSync as existsSync12, lstatSync as lstatSync4, readdirSync as readdirSync8 } from "fs";
|
|
1883
|
+
import { basename as basename7, dirname as dirname6, isAbsolute as isAbsolute4, join as join13, relative as relative5, sep as sep8 } from "path";
|
|
1712
1884
|
|
|
1713
1885
|
// src/plugins.ts
|
|
1714
|
-
import { execFileSync as
|
|
1715
|
-
import { existsSync as existsSync10, mkdirSync as
|
|
1716
|
-
import { basename as
|
|
1886
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
1887
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync6, readFileSync as readFileSync6, statSync as statSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
1888
|
+
import { basename as basename5, dirname as dirname4, join as join11, resolve as resolve7 } from "path";
|
|
1717
1889
|
function pluginNameOf(id) {
|
|
1718
1890
|
return id.split("@")[0];
|
|
1719
1891
|
}
|
|
1720
1892
|
function nonEmptyDir(path) {
|
|
1721
1893
|
try {
|
|
1722
|
-
return
|
|
1894
|
+
return readdirSync6(path).some((f) => !f.startsWith("."));
|
|
1723
1895
|
} catch {
|
|
1724
1896
|
return false;
|
|
1725
1897
|
}
|
|
@@ -1810,23 +1982,23 @@ function installedPlugins() {
|
|
|
1810
1982
|
return root ? [{ id, scope: "user", synced: false, agent: "claude-code", on: true, settingsOnly: true, root, ...describe(root) }] : [];
|
|
1811
1983
|
});
|
|
1812
1984
|
const syncedDir = join11(plugins, "synced");
|
|
1813
|
-
const dirs = (p) => existsSync10(p) ?
|
|
1985
|
+
const dirs = (p) => existsSync10(p) ? readdirSync6(p, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => join11(p, d.name)) : [];
|
|
1814
1986
|
const fromSynced = dirs(syncedDir).flatMap(dirs).map((root) => {
|
|
1815
|
-
const id = `${readJson(join11(root, ".claude-plugin", "plugin.json"))?.name ??
|
|
1987
|
+
const id = `${readJson(join11(root, ".claude-plugin", "plugin.json"))?.name ?? basename5(root)}@synced`;
|
|
1816
1988
|
return { id, scope: null, synced: true, agent: "claude-code", on: user[id] !== false, root, ...describe(root) };
|
|
1817
1989
|
});
|
|
1818
1990
|
return [...fromMarketplaces, ...fromSettings, ...fromSynced];
|
|
1819
1991
|
}
|
|
1820
1992
|
function cursorPlugins() {
|
|
1821
1993
|
const cache = join11(userHome(), ".cursor", "plugins", "cache");
|
|
1822
|
-
const dirs = (p) => existsSync10(p) ?
|
|
1994
|
+
const dirs = (p) => existsSync10(p) ? readdirSync6(p).filter((d) => !d.startsWith(".") && statSync5(join11(p, d)).isDirectory()) : [];
|
|
1823
1995
|
return dirs(cache).flatMap(
|
|
1824
1996
|
(marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
|
|
1825
1997
|
const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
|
|
1826
|
-
const root = versions.sort((a, b) =>
|
|
1998
|
+
const root = versions.sort((a, b) => statSync5(b).mtimeMs - statSync5(a).mtimeMs)[0];
|
|
1827
1999
|
if (!root) return [];
|
|
1828
2000
|
const about = describe(root);
|
|
1829
|
-
return [{ id: `${plugin}@${marketplace}`, scope: null, synced: false, agent: "cursor", on: null, root, ...about, version: about.version ??
|
|
2001
|
+
return [{ id: `${plugin}@${marketplace}`, scope: null, synced: false, agent: "cursor", on: null, root, ...about, version: about.version ?? basename5(root) }];
|
|
1830
2002
|
})
|
|
1831
2003
|
);
|
|
1832
2004
|
}
|
|
@@ -1840,7 +2012,7 @@ function claudeBinary() {
|
|
|
1840
2012
|
const candidates = ["claude", join11(userHome(), ".local", "bin", "claude"), join11(userHome(), ".claude", "local", "claude")];
|
|
1841
2013
|
const found = candidates.find((bin) => {
|
|
1842
2014
|
try {
|
|
1843
|
-
return
|
|
2015
|
+
return execFileSync4(bin, ["--version"], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e4, encoding: "utf-8" }).includes("Claude Code");
|
|
1844
2016
|
} catch {
|
|
1845
2017
|
return false;
|
|
1846
2018
|
}
|
|
@@ -1854,7 +2026,7 @@ function claude(args, cwd, bin = claudeBinary()) {
|
|
|
1854
2026
|
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
2027
|
const slow = args[1] === "install" || args[1] === "update";
|
|
1856
2028
|
try {
|
|
1857
|
-
const out2 =
|
|
2029
|
+
const out2 = execFileSync4(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: slow ? 12e4 : 2e4, encoding: "utf-8" });
|
|
1858
2030
|
return { ok: true, out: out2 };
|
|
1859
2031
|
} catch (err) {
|
|
1860
2032
|
const e = err;
|
|
@@ -1870,7 +2042,7 @@ function setPluginIn(file, id, on) {
|
|
|
1870
2042
|
}
|
|
1871
2043
|
function ignoreLocalSettings(root) {
|
|
1872
2044
|
const file = ".claude/settings.local.json";
|
|
1873
|
-
const git2 = (...args) =>
|
|
2045
|
+
const git2 = (...args) => execFileSync4("git", args, { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1874
2046
|
try {
|
|
1875
2047
|
git2("rev-parse", "--git-dir");
|
|
1876
2048
|
} catch {
|
|
@@ -1889,8 +2061,8 @@ function ignoreLocalSettings(root) {
|
|
|
1889
2061
|
try {
|
|
1890
2062
|
const exclude = resolve7(root, git2("rev-parse", "--git-path", "info/exclude"));
|
|
1891
2063
|
const before = existsSync10(exclude) ? readFileSync6(exclude, "utf-8") : "";
|
|
1892
|
-
|
|
1893
|
-
|
|
2064
|
+
mkdirSync7(dirname4(exclude), { recursive: true });
|
|
2065
|
+
writeFileSync5(exclude, `${before}${before && !before.endsWith("\n") ? "\n" : ""}/${git2("rev-parse", "--show-prefix")}${file}
|
|
1894
2066
|
`);
|
|
1895
2067
|
return ", which git now ignores via .git/info/exclude";
|
|
1896
2068
|
} catch {
|
|
@@ -1898,7 +2070,7 @@ function ignoreLocalSettings(root) {
|
|
|
1898
2070
|
}
|
|
1899
2071
|
}
|
|
1900
2072
|
function turnOffIn(plugin, root) {
|
|
1901
|
-
const done = `${plugin.id} turned off in ${
|
|
2073
|
+
const done = `${plugin.id} turned off in ${basename5(root)}; other repos keep it`;
|
|
1902
2074
|
const bin = plugin.synced || plugin.settingsOnly ? null : claudeBinary();
|
|
1903
2075
|
if (!bin) {
|
|
1904
2076
|
const r2 = setPluginIn(join11(root, ".claude", "settings.local.json"), plugin.id, false);
|
|
@@ -1917,7 +2089,7 @@ function settingsFileOf(p) {
|
|
|
1917
2089
|
return join11(p.projectPath, ".claude", p.scope === "local" ? "settings.local.json" : "settings.json");
|
|
1918
2090
|
}
|
|
1919
2091
|
function whereOf(p) {
|
|
1920
|
-
return p.projectPath ? ` in ${
|
|
2092
|
+
return p.projectPath ? ` in ${basename5(p.projectPath)} (${p.scope})` : "";
|
|
1921
2093
|
}
|
|
1922
2094
|
function setPluginOn(p, on) {
|
|
1923
2095
|
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" : "";
|
|
@@ -1969,17 +2141,17 @@ function removePlugin(plugin, how) {
|
|
|
1969
2141
|
const uninstall = how === "delete" && !plugin.synced;
|
|
1970
2142
|
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)` };
|
|
1971
2143
|
const yours = new Set(machineSkills().filter((m) => m.movable).map((m) => m.name));
|
|
1972
|
-
const imported = plugin.skills.filter((dir) => !yours.has(
|
|
2144
|
+
const imported = plugin.skills.filter((dir) => !yours.has(basename5(dir)) && latestVersion(basename5(dir)) === null).map((dir) => importSkill(dir).name);
|
|
1973
2145
|
const saved = imported.length ? `; ${imported.join(", ")} copied into Your skills` : "";
|
|
1974
2146
|
const r = uninstall ? uninstallPlugin(plugin) : setPluginOn(plugin, false);
|
|
1975
2147
|
if (!r.ok) return { ok: false, message: `${r.message}${saved}` };
|
|
1976
2148
|
const others = uninstall ? (installedPluginsJson()[plugin.id] ?? []).filter((e) => (e.scope ?? "user") !== (plugin.scope ?? "user") || e.projectPath !== plugin.projectPath) : [];
|
|
1977
|
-
const left = others.length ? `; its ${others.map((e) => `${e.scope ?? "user"} install${e.projectPath ? ` in ${
|
|
2149
|
+
const left = others.length ? `; its ${others.map((e) => `${e.scope ?? "user"} install${e.projectPath ? ` in ${basename5(e.projectPath)}` : ""}`).join(", ")} stays` : "";
|
|
1978
2150
|
return { ok: true, message: uninstall ? `${plugin.id} removed (${plugin.scope ?? "user"} install)${saved}${left}` : `${r.message}${saved}` };
|
|
1979
2151
|
}
|
|
1980
2152
|
function recordRemovedPlugin(id, scope, projectPath, on = true) {
|
|
1981
2153
|
const list2 = readState(removedFile()) ?? [];
|
|
1982
|
-
|
|
2154
|
+
mkdirSync7(skilllibHome(), { recursive: true });
|
|
1983
2155
|
writeState(removedFile(), [...list2, { id, scope, ...projectPath ? { projectPath } : {}, on, at: (/* @__PURE__ */ new Date()).toISOString() }]);
|
|
1984
2156
|
}
|
|
1985
2157
|
function pluginBackups() {
|
|
@@ -2002,16 +2174,16 @@ function restorePlugin(backup) {
|
|
|
2002
2174
|
const wasOff = records.filter(same).pop()?.on === false;
|
|
2003
2175
|
const off = wasOff ? claude(["plugin", "disable", backup.from, "--scope", scope], projectPath) : null;
|
|
2004
2176
|
writeState(removedFile(), records.filter((x) => !same(x)));
|
|
2005
|
-
const where = `Claude Code plugins (${scope}${projectPath ? ` in ${
|
|
2177
|
+
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" : ""})`;
|
|
2006
2178
|
return { ok: true, to: where };
|
|
2007
2179
|
}
|
|
2008
2180
|
function cursorPluginSkills(plugins = cursorPlugins()) {
|
|
2009
|
-
return plugins.flatMap((p) => p.skills.map((path) => ({ name:
|
|
2181
|
+
return plugins.flatMap((p) => p.skills.map((path) => ({ name: basename5(path), plugin: pluginNameOf(p.id), path })));
|
|
2010
2182
|
}
|
|
2011
2183
|
|
|
2012
2184
|
// src/tidy.ts
|
|
2013
|
-
import { existsSync as existsSync11, mkdirSync as
|
|
2014
|
-
import { basename as
|
|
2185
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as readdirSync7, unlinkSync as unlinkSync3 } from "fs";
|
|
2186
|
+
import { basename as basename6, dirname as dirname5, join as join12, sep as sep7 } from "path";
|
|
2015
2187
|
function entriesOf(dirs, name) {
|
|
2016
2188
|
return dirs.flatMap((dir) => {
|
|
2017
2189
|
const path = join12(dir, name);
|
|
@@ -2033,7 +2205,7 @@ function namesIn(dirs) {
|
|
|
2033
2205
|
const names = /* @__PURE__ */ new Set();
|
|
2034
2206
|
for (const dir of dirs) {
|
|
2035
2207
|
if (!isDir(dir)) continue;
|
|
2036
|
-
for (const name of
|
|
2208
|
+
for (const name of readdirSync7(dir)) if (!name.startsWith(".") && name !== "synced") names.add(name);
|
|
2037
2209
|
}
|
|
2038
2210
|
return [...names].sort();
|
|
2039
2211
|
}
|
|
@@ -2046,7 +2218,7 @@ function globalTidyDirs() {
|
|
|
2046
2218
|
function runsFrom(id, dirsWithCopy, readsOf) {
|
|
2047
2219
|
const reads = readsOf(id).filter((d) => dirsWithCopy.includes(d));
|
|
2048
2220
|
if (id === "codex") return reads;
|
|
2049
|
-
const pick = (d) => CURSOR_PICKS.indexOf(
|
|
2221
|
+
const pick = (d) => CURSOR_PICKS.indexOf(basename6(dirname5(d)));
|
|
2050
2222
|
return id === "cursor" ? [...reads].sort((a, b) => pick(a) - pick(b)).slice(0, 1) : reads.slice(0, 1);
|
|
2051
2223
|
}
|
|
2052
2224
|
function conflictOf(name, root, reals, enabled, readsOf) {
|
|
@@ -2069,7 +2241,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
2069
2241
|
const realRoot = realpathOrNull(root) ?? root;
|
|
2070
2242
|
for (const name of namesIn(dirs)) {
|
|
2071
2243
|
const entries = entriesOf(dirs, name);
|
|
2072
|
-
const external = entries.find((e) => e.link && e.real && !(e.real +
|
|
2244
|
+
const external = entries.find((e) => e.link && e.real && !(e.real + sep7).startsWith(realRoot + sep7));
|
|
2073
2245
|
if (external) {
|
|
2074
2246
|
const differs = entries.some((e) => e !== external && e.real && existsSync11(join12(e.real, "SKILL.md")) && treeHash(e.real) !== treeHash(external.real));
|
|
2075
2247
|
report.skipped.push({ name, reason: `${rel(external.path)} links outside the repo`, ...differs ? { differs } : {} });
|
|
@@ -2157,7 +2329,7 @@ function gitVisibleSteps(plan, git2 = plan.root ? gitInfo(plan.root) : null) {
|
|
|
2157
2329
|
if (!plan.root || !git2) return [];
|
|
2158
2330
|
const shared = (rel) => isShared(git2.of(rel));
|
|
2159
2331
|
const dep = readManifest(plan.root).skills[plan.name];
|
|
2160
|
-
const recordsLink = (s) => !!dep && (s.kind === "link" || s.kind === "replace") && !dep.links?.includes(relativeTo(plan.root, dirname5(s.path)));
|
|
2332
|
+
const recordsLink = (s) => !!dep && !dep.personal && (s.kind === "link" || s.kind === "replace") && !dep.links?.includes(relativeTo(plan.root, dirname5(s.path)));
|
|
2161
2333
|
return plan.steps.filter((s) => shared(relativeTo(plan.root, s.path)) || recordsLink(s) && shared(MANIFEST_FILE));
|
|
2162
2334
|
}
|
|
2163
2335
|
function applyTidy(plan, { git: git2 = "keep" } = {}) {
|
|
@@ -2170,7 +2342,7 @@ function applyTidy(plan, { git: git2 = "keep" } = {}) {
|
|
|
2170
2342
|
if (isLink(step.path)) unlinkSync3(step.path);
|
|
2171
2343
|
} else stash(step.path, "tidy-backup");
|
|
2172
2344
|
if (step.kind === "link" || step.kind === "replace") {
|
|
2173
|
-
|
|
2345
|
+
mkdirSync8(dirname5(step.path), { recursive: true });
|
|
2174
2346
|
linkDir(step.target, step.path);
|
|
2175
2347
|
}
|
|
2176
2348
|
applied.push(step);
|
|
@@ -2233,7 +2405,7 @@ function track(root, name) {
|
|
|
2233
2405
|
if (c.action === "skipped") throw new Error(`${name}: ${c.reason}`);
|
|
2234
2406
|
return c;
|
|
2235
2407
|
}
|
|
2236
|
-
var installedIn = (p) => p.projectPath ? ` in ${
|
|
2408
|
+
var installedIn = (p) => p.projectPath ? ` in ${basename7(p.projectPath)}` : "";
|
|
2237
2409
|
var installId = (p) => `${p.projectPath ? `${p.projectPath}:` : ""}${p.id}`;
|
|
2238
2410
|
function summarize(where, verb, changes) {
|
|
2239
2411
|
const done = changes.filter((c) => c.action !== "skipped");
|
|
@@ -2255,7 +2427,7 @@ var gitNote = (root, paths) => {
|
|
|
2255
2427
|
};
|
|
2256
2428
|
var agentNames = (ids) => ids.map((id) => harness(id).name).join(", ");
|
|
2257
2429
|
function conflictIssue(c) {
|
|
2258
|
-
const where = c.root ?
|
|
2430
|
+
const where = c.root ? basename7(c.root) : "your global folders";
|
|
2259
2431
|
const winners = c.managed ? c.copies.filter((x) => x.dir === c.managed) : c.copies;
|
|
2260
2432
|
const planFor = (dir) => (c.root ? planProjectTidy(c.root, { keep: { [c.name]: dir } }) : planGlobalTidy({ keep: { [c.name]: dir } })).plans.find((p) => p.name === c.name);
|
|
2261
2433
|
const keep = (dir, git2) => () => {
|
|
@@ -2289,7 +2461,7 @@ function applyAll(plans, git2) {
|
|
|
2289
2461
|
return { skills: results.filter((r) => r.applied.length).length, held: results.reduce((n, r) => n + r.held.length, 0) };
|
|
2290
2462
|
}
|
|
2291
2463
|
function brokenLinksIn(root, opened) {
|
|
2292
|
-
const where =
|
|
2464
|
+
const where = basename7(root);
|
|
2293
2465
|
const tidyDirs = repoTidyDirs(root);
|
|
2294
2466
|
const hasCopy = (name) => tidyDirs.some((d) => !isLink(join13(root, d, name)) && existsSync12(join13(root, d, name, "SKILL.md")));
|
|
2295
2467
|
const mends = (project, dir, name) => {
|
|
@@ -2301,7 +2473,7 @@ function brokenLinksIn(root, opened) {
|
|
|
2301
2473
|
const realRoot = realNative(root) ?? root;
|
|
2302
2474
|
const ownerOf = (real) => {
|
|
2303
2475
|
let nearest = null;
|
|
2304
|
-
for (let d = dirname6(dirname6(real)); d.startsWith(realRoot +
|
|
2476
|
+
for (let d = dirname6(dirname6(real)); d.startsWith(realRoot + sep8); d = dirname6(d)) {
|
|
2305
2477
|
if (!existsSync12(join13(d, MANIFEST_FILE))) continue;
|
|
2306
2478
|
if (opened.has(d)) return { opened: true, own: d };
|
|
2307
2479
|
nearest ??= d;
|
|
@@ -2321,7 +2493,7 @@ function brokenLinksIn(root, opened) {
|
|
|
2321
2493
|
return folders2.flatMap(({ dir, top, own, real }) => {
|
|
2322
2494
|
const folder = join13(root, dir);
|
|
2323
2495
|
if (!isDir(folder) || notWritable(root, dir)) return [];
|
|
2324
|
-
return
|
|
2496
|
+
return readdirSync8(folder).flatMap((name) => {
|
|
2325
2497
|
const path = join13(folder, name);
|
|
2326
2498
|
if (!isLink(path) || isDir(path) || top && (dir !== GROK_SKILLS_DIR && hasCopy(name) || syncMends(dir, name))) return [];
|
|
2327
2499
|
const remove2 = {
|
|
@@ -2333,13 +2505,13 @@ function brokenLinksIn(root, opened) {
|
|
|
2333
2505
|
return `Removed the broken link ${dir}/${name} in ${where} (restorable with skilllib restore)`;
|
|
2334
2506
|
}
|
|
2335
2507
|
};
|
|
2336
|
-
const placeOf = (p) =>
|
|
2508
|
+
const placeOf = (p) => relative5(realRoot, p).split(sep8).join("/");
|
|
2337
2509
|
git2 ??= gitInfo(
|
|
2338
2510
|
root,
|
|
2339
|
-
folders2.map((f) => placeOf(f.real)).filter((p) => p !== "" && p !== ".." && !p.startsWith("../") && !
|
|
2511
|
+
folders2.map((f) => placeOf(f.real)).filter((p) => p !== "" && p !== ".." && !p.startsWith("../") && !isAbsolute4(p))
|
|
2340
2512
|
);
|
|
2341
2513
|
const tracked = isShared(git2?.of(placeOf(join13(real, name))));
|
|
2342
|
-
const recorded = !!own && mends(own,
|
|
2514
|
+
const recorded = !!own && mends(own, relative5(own, real).split(sep8).join("/"), name);
|
|
2343
2515
|
const pinnedThere = own ? ` ${placeOf(own)} has its own skilllib.json${recorded ? `, which records this link: skilllib sync in ${placeOf(own)} brings back what it points to` : ""}.` : "";
|
|
2344
2516
|
return [
|
|
2345
2517
|
{
|
|
@@ -2360,24 +2532,24 @@ function grokDiffers(projects2, machine, statusOf) {
|
|
|
2360
2532
|
const issue = (root, name, grokCopy, other) => ({
|
|
2361
2533
|
id: `grok-differs:${root ?? "global"}:${name}`,
|
|
2362
2534
|
severity: "suggestion",
|
|
2363
|
-
title: `${name}${root ? ` in ${
|
|
2535
|
+
title: `${name}${root ? ` in ${basename7(root)}` : ""}: Grok runs a different copy`,
|
|
2364
2536
|
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.`,
|
|
2365
2537
|
at: rows(root, [name])
|
|
2366
2538
|
});
|
|
2367
2539
|
const grokGlobal = join13(grokDir(), "skills");
|
|
2368
2540
|
const global = skillDirsIn(grokGlobal).filter((path) => !isLink(path)).flatMap((path) => {
|
|
2369
|
-
const name =
|
|
2541
|
+
const name = basename7(path);
|
|
2370
2542
|
const other = machine.find((m) => m.name === name && m.movable && !m.broken && m.harnesses.some((id) => id !== "grok") && realPath(dirname6(m.path)) !== realPath(grokGlobal));
|
|
2371
2543
|
return other && treeHash(other.path) !== treeHash(path) ? [issue(null, name, tildify(path), tildify(other.path))] : [];
|
|
2372
2544
|
});
|
|
2373
|
-
const
|
|
2545
|
+
const repos2 = projects2.flatMap(
|
|
2374
2546
|
(root) => skillDirsIn(join13(root, GROK_SKILLS_DIR)).filter((path) => !isLink(path)).flatMap((path) => {
|
|
2375
|
-
const name =
|
|
2547
|
+
const name = basename7(path);
|
|
2376
2548
|
const other = statusOf(root).find((s) => s.name === name && !s.path.startsWith(join13(root, GROK_SKILLS_DIR)));
|
|
2377
2549
|
return other && treeHash(other.path) !== treeHash(path) ? [issue(root, name, `${GROK_SKILLS_DIR}/${name}`, relativeTo(root, other.path))] : [];
|
|
2378
2550
|
})
|
|
2379
2551
|
);
|
|
2380
|
-
return [...global, ...
|
|
2552
|
+
return [...global, ...repos2];
|
|
2381
2553
|
}
|
|
2382
2554
|
function findIssues(roots, machine, libraryNames, statusOf = projectStatus, loadedIn = (root) => skillsLoadedIn(root, machine), nestedOf = nestedSkills) {
|
|
2383
2555
|
gitMemo = /* @__PURE__ */ new Map();
|
|
@@ -2385,8 +2557,8 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2385
2557
|
const issues = unreadable.map(({ root, reason }) => ({
|
|
2386
2558
|
id: `manifest:${root}`,
|
|
2387
2559
|
severity: "problem",
|
|
2388
|
-
title: `${
|
|
2389
|
-
detail: `${reason}. Until it's fixed, skilllib leaves ${
|
|
2560
|
+
title: `${basename7(root)}: skilllib.json can't be read`,
|
|
2561
|
+
detail: `${reason}. Until it's fixed, skilllib leaves ${basename7(root)} out.`,
|
|
2390
2562
|
at: [{ root, skill: "" }]
|
|
2391
2563
|
}));
|
|
2392
2564
|
const statuses = new Map(projects2.map((root) => [root, statusOf(root)]));
|
|
@@ -2437,7 +2609,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2437
2609
|
const globalYours = new Set(loaded.filter((m) => m.movable).map((m) => m.name));
|
|
2438
2610
|
const installedYours = /* @__PURE__ */ new Set([...globalYours, ...[...statuses.values()].flat().map((s) => s.name), ...knownProjects().flatMap((p) => Object.keys(pinsIn(p)))]);
|
|
2439
2611
|
const repoChoices = (plugin, root, names) => {
|
|
2440
|
-
const where =
|
|
2612
|
+
const where = basename7(root);
|
|
2441
2613
|
const copies = statuses.get(root).filter((s) => names.includes(s.name));
|
|
2442
2614
|
const allManaged = names.every((n) => copies.some((c) => c.name === n && removesClean(root, c)));
|
|
2443
2615
|
const committed = gitNote(root, copies.map((s) => s.path)) !== "";
|
|
@@ -2463,7 +2635,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2463
2635
|
};
|
|
2464
2636
|
const machinePlugins = claudePlugins(machine);
|
|
2465
2637
|
for (const plugin of machinePlugins) {
|
|
2466
|
-
const names = plugin.skills.map((p) =>
|
|
2638
|
+
const names = plugin.skills.map((p) => basename7(p));
|
|
2467
2639
|
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);
|
|
2468
2640
|
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) : [];
|
|
2469
2641
|
const claudeYours = /* @__PURE__ */ new Set([...globalYours, ...inRepos.flatMap(([, d]) => d)]);
|
|
@@ -2480,7 +2652,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2480
2652
|
const off = { label: `Turn the plugin ${plugin.id} off`, hint: "stays installed", run: () => orThrow(removePlugin(plugin, "off")) };
|
|
2481
2653
|
const bothLoad = dupes.filter((n) => loadedYours.has(n));
|
|
2482
2654
|
const claudeBoth = dupes.filter((n) => claudeYours.has(n));
|
|
2483
|
-
const cursorWheres = cursorRepos.map(([root]) =>
|
|
2655
|
+
const cursorWheres = cursorRepos.map(([root]) => basename7(root));
|
|
2484
2656
|
const notInstalled = dupes.filter((n) => !installedYours.has(n));
|
|
2485
2657
|
issues.push({
|
|
2486
2658
|
id: `plugin:${plugin.id}`,
|
|
@@ -2494,7 +2666,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2494
2666
|
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"}.` : "",
|
|
2495
2667
|
plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : "",
|
|
2496
2668
|
plugin.settingsOnly ? "It's only turned on in your settings, not installed, so it can only be turned off here." : "",
|
|
2497
|
-
inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) =>
|
|
2669
|
+
inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) => basename7(root)).join(", ")}.` : ""
|
|
2498
2670
|
].filter(Boolean).join(" "),
|
|
2499
2671
|
choices: [...plugin.synced || plugin.settingsOnly ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off], ...inRepos.flatMap(([root, d]) => repoChoices(plugin, root, d))],
|
|
2500
2672
|
// Global's rows (yours and the plugin's), and the repos where both load.
|
|
@@ -2517,10 +2689,10 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2517
2689
|
}
|
|
2518
2690
|
const machinePluginSkills = new Set(machinePlugins.flatMap((p) => p.skills));
|
|
2519
2691
|
for (const root of projects2) {
|
|
2520
|
-
const where =
|
|
2692
|
+
const where = basename7(root);
|
|
2521
2693
|
const repoOnly = loadedHere.get(root).filter((m) => m.kind === "plugin" && !machinePluginSkills.has(m.path));
|
|
2522
2694
|
for (const plugin of enabledHarnesses().includes("claude-code") ? claudePlugins(repoOnly, root) : []) {
|
|
2523
|
-
const names = plugin.skills.map((p) =>
|
|
2695
|
+
const names = plugin.skills.map((p) => basename7(p));
|
|
2524
2696
|
const inRepo = namesIn2(root).filter((n) => names.includes(n));
|
|
2525
2697
|
const dupes = names.filter((n) => inRepo.includes(n) || globalYours.has(n));
|
|
2526
2698
|
if (!dupes.length) continue;
|
|
@@ -2610,385 +2782,8 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2610
2782
|
const alsoGlobal = /* @__PURE__ */ new Map();
|
|
2611
2783
|
const globalCopies = loaded.filter((m) => m.kind !== "plugin");
|
|
2612
2784
|
const alsoLoadedGlobally = (s) => globalCopies.some((m) => m.name === s.name && m.harnesses.some((id) => s.visibility.some((v) => v.id === id && v.paths > 0)));
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
const status2 = statuses.get(root);
|
|
2616
|
-
issues.push(...brokenLinksIn(root, openedProjects));
|
|
2617
|
-
const pins = readManifest(root).skills;
|
|
2618
|
-
const blockedFor = /* @__PURE__ */ new Map();
|
|
2619
|
-
const untrackable = (s) => {
|
|
2620
|
-
if (!blockedFor.has(s.name)) blockedFor.set(s.name, trackBlocker(root, s.name));
|
|
2621
|
-
return blockedFor.get(s.name);
|
|
2622
|
-
};
|
|
2623
|
-
const blockers = new Map(status2.filter((s) => s.managed).flatMap((s) => ((b) => b ? [[s.name, b]] : [])(syncBlocker(root, s.name, pins[s.name]))));
|
|
2624
|
-
for (const [name, blocker] of blockers)
|
|
2625
|
-
issues.push({
|
|
2626
|
-
id: `stuck:${root}:${name}`,
|
|
2627
|
-
severity: "problem",
|
|
2628
|
-
title: `${name} in ${where}: sync can't put it back as skilllib.json pins it`,
|
|
2629
|
-
// What to do depends on what's there: a folder that links out of the repo is a setup of yours, and a pin
|
|
2630
|
-
// your library has no version of can't come back by sync anyway.
|
|
2631
|
-
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."}`,
|
|
2632
|
-
at: rows(root, [name])
|
|
2633
|
-
});
|
|
2634
|
-
const unstuck = status2.filter((s) => !blockers.has(s.name));
|
|
2635
|
-
const missing = unstuck.filter((s) => s.state === "folder missing" && s.latest !== null);
|
|
2636
|
-
const knownPin = (s) => versionForHash(s.name, pins[s.name]?.hash ?? "") !== null;
|
|
2637
|
-
const restorable = missing.filter(knownPin);
|
|
2638
|
-
for (const s of unstuck.filter((m) => (m.state === "folder missing" || m.state === "out of sync") && m.latest !== null && !knownPin(m)))
|
|
2639
|
-
issues.push({
|
|
2640
|
-
id: `pin:${root}:${s.name}`,
|
|
2641
|
-
severity: "problem",
|
|
2642
|
-
title: `${s.name}: the version ${where}'s skilllib.json pins isn't in your library`,
|
|
2643
|
-
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.`}`,
|
|
2644
|
-
at: rows(root, [s.name]),
|
|
2645
|
-
// It changes what the shared skilllib.json pins: a choice, never run by `doctor --fix`.
|
|
2646
|
-
...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}` }] }
|
|
2647
|
-
});
|
|
2648
|
-
if (restorable.length > 0) {
|
|
2649
|
-
issues.push({
|
|
2650
|
-
id: `sync:${root}`,
|
|
2651
|
-
severity: "problem",
|
|
2652
|
-
title: `${where}: ${plural(restorable.length, "skill")} in skilllib.json not installed`,
|
|
2653
|
-
detail: `${restorable.map((s) => s.name).join(", ")}: the folder is missing`,
|
|
2654
|
-
at: rows(root, restorable.map((s) => s.name)),
|
|
2655
|
-
fix: {
|
|
2656
|
-
label: "Sync the project",
|
|
2657
|
-
// Only the missing folders: an out-of-sync copy (maybe committed) is its own issue's choice.
|
|
2658
|
-
run: () => summarize(where, "restored", syncProject(root, { names: restorable.map((s) => s.name) }))
|
|
2659
|
-
}
|
|
2660
|
-
});
|
|
2661
|
-
}
|
|
2662
|
-
const drifted = unstuck.filter((s) => s.state === "out of sync" && knownPin(s));
|
|
2663
|
-
if (drifted.length) {
|
|
2664
|
-
const seen = drifted.some((s) => isShared(gitOf(root)?.of(relativeTo(root, s.path))));
|
|
2665
|
-
const yours = drifted.some((s) => untrackable(s));
|
|
2666
|
-
const install = {
|
|
2667
|
-
label: "Install the pinned versions",
|
|
2668
|
-
hint: seen ? "git will see the copies change" : "your library keeps every version",
|
|
2669
|
-
run: () => summarize(where, "synced", syncProject(root, { names: drifted.map((s) => s.name) }))
|
|
2670
|
-
};
|
|
2671
|
-
issues.push({
|
|
2672
|
-
id: `out-of-sync:${root}`,
|
|
2673
|
-
severity: "suggestion",
|
|
2674
|
-
title: `${where}: ${plural(drifted.length, "skill")} not at the version skilllib.json pins`,
|
|
2675
|
-
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.` : ""}`,
|
|
2676
|
-
at: rows(root, drifted.map((s) => s.name)),
|
|
2677
|
-
...seen || yours ? { choices: [install] } : { fix: install }
|
|
2678
|
-
});
|
|
2679
|
-
}
|
|
2680
|
-
for (const s of unstuck.filter((s2) => s2.state === "folder missing" && s2.latest === null))
|
|
2681
|
-
issues.push({
|
|
2682
|
-
id: `unavailable:${root}:${s.name}`,
|
|
2683
|
-
severity: "problem",
|
|
2684
|
-
title: `${s.name}: pinned in ${where}'s skilllib.json, but not in your library`,
|
|
2685
|
-
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.`,
|
|
2686
|
-
at: rows(root, [s.name]),
|
|
2687
|
-
// Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
|
|
2688
|
-
choices: [
|
|
2689
|
-
{
|
|
2690
|
-
label: "Remove it from skilllib.json",
|
|
2691
|
-
hint: `${where} stops pinning it`,
|
|
2692
|
-
run: () => {
|
|
2693
|
-
const r = removeSkill(root, s.name);
|
|
2694
|
-
if (r.action !== "removed") throw new Error(`${s.name}: ${r.reason}`);
|
|
2695
|
-
return `${s.name}: removed from ${where}'s skilllib.json`;
|
|
2696
|
-
}
|
|
2697
|
-
}
|
|
2698
|
-
]
|
|
2699
|
-
});
|
|
2700
|
-
const behind = unstuck.filter((s) => s.state === "update available" && !untrackable(s));
|
|
2701
|
-
if (behind.length > 0)
|
|
2702
|
-
issues.push({
|
|
2703
|
-
id: `update:${root}`,
|
|
2704
|
-
severity: "suggestion",
|
|
2705
|
-
title: `${where}: ${plural(behind.length, "skill")} out of date`,
|
|
2706
|
-
detail: behind.map((s) => `${s.name} (v${s.version} \u2192 v${s.latest})`).join(", "),
|
|
2707
|
-
at: rows(root, behind.map((s) => s.name)),
|
|
2708
|
-
// It changes what the repo pins and the copies its agents load (often committed): a choice, never `doctor --fix`.
|
|
2709
|
-
choices: [
|
|
2710
|
-
{
|
|
2711
|
-
label: "Update to the newest versions",
|
|
2712
|
-
hint: isShared(gitOf(root)?.of(MANIFEST_FILE)) ? "git will see the new copies and skilllib.json" : "replaces each copy with your library's newest",
|
|
2713
|
-
run: () => summarize(where, "updated", updateProject(root, behind.map((s) => s.name)))
|
|
2714
|
-
}
|
|
2715
|
-
]
|
|
2716
|
-
});
|
|
2717
|
-
const tidy2 = planProjectTidy(root);
|
|
2718
|
-
if (tidy2.plans.length) {
|
|
2719
|
-
const git3 = gitOf(root);
|
|
2720
|
-
const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p, git3).map((s) => describeStep(s, root)));
|
|
2721
|
-
const run = (mode) => () => {
|
|
2722
|
-
const r = applyAll(tidy2.plans, mode);
|
|
2723
|
-
if (!r.skills) throw new Error(`${where}: nothing tidied${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`);
|
|
2724
|
-
return `${where}: tidied ${plural(r.skills, "skill")}${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`;
|
|
2725
|
-
};
|
|
2726
|
-
issues.push({
|
|
2727
|
-
id: `tidy:${root}`,
|
|
2728
|
-
severity: "suggestion",
|
|
2729
|
-
title: `${where}: ${plural(tidy2.plans.length, "skill")} with extra copies or links`,
|
|
2730
|
-
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." : "."),
|
|
2731
|
-
at: rows(root, tidy2.plans.map((p) => p.name)),
|
|
2732
|
-
...visible2.length ? {
|
|
2733
|
-
choices: [
|
|
2734
|
-
// "Keep git as it is" only when it leaves something to do.
|
|
2735
|
-
...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") }] : [],
|
|
2736
|
-
{ label: "Tidy everything", hint: `${plural(visible2.length, "change")} will show in git status`, run: run("go") }
|
|
2737
|
-
]
|
|
2738
|
-
} : { fix: { label: "Tidy", run: run("go") } }
|
|
2739
|
-
});
|
|
2740
|
-
}
|
|
2741
|
-
issues.push(...tidy2.conflicts.map(conflictIssue));
|
|
2742
|
-
for (const s of tidy2.skipped.filter((x) => x.differs))
|
|
2743
|
-
issues.push({
|
|
2744
|
-
id: `linkdiffers:${root}:${s.name}`,
|
|
2745
|
-
severity: "problem",
|
|
2746
|
-
title: `${s.name} in ${where}: the copies differ`,
|
|
2747
|
-
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.`,
|
|
2748
|
-
at: rows(root, [s.name])
|
|
2749
|
-
});
|
|
2750
|
-
const loadsGlobally = (name, id) => {
|
|
2751
|
-
const here = status2.find((s) => s.name === name);
|
|
2752
|
-
return globalCopies.some((m) => m.name === name && m.harnesses.includes(id) && (!here || treeHash(m.path) === treeHash(here.path)));
|
|
2753
|
-
};
|
|
2754
|
-
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 }) }));
|
|
2755
|
-
const enabledIds = enabledHarnesses();
|
|
2756
|
-
const notFolders = /* @__PURE__ */ new Set();
|
|
2757
|
-
for (const dir of new Set(enabledIds.map((id) => harness(id).projectDirs[0]))) {
|
|
2758
|
-
const blocker = notWritable(root, dir);
|
|
2759
|
-
if (!blocker || notFolders.has(blocker)) continue;
|
|
2760
|
-
notFolders.add(blocker);
|
|
2761
|
-
const who = agentNames(enabledIds.filter((id) => notWritable(root, harness(id).projectDirs[0]) === blocker));
|
|
2762
|
-
const out2 = linksOut(root, blocker);
|
|
2763
|
-
issues.push({
|
|
2764
|
-
id: `notdir:${root}:${blocker}`,
|
|
2765
|
-
severity: out2 ? "suggestion" : "problem",
|
|
2766
|
-
title: out2 ? `${where}: ${blocker} links out of the repo` : `${where}: ${blocker} isn't a folder`,
|
|
2767
|
-
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.`,
|
|
2768
|
-
at: [{ root, skill: "" }]
|
|
2769
|
-
// the repo itself: its row in Places
|
|
2770
|
-
});
|
|
2771
|
-
}
|
|
2772
|
-
for (const { s, would } of blindHere) {
|
|
2773
|
-
const way = would.inTheWay.filter((p) => !notFolders.has(p));
|
|
2774
|
-
if (!way.length || blockers.has(s.name)) continue;
|
|
2775
|
-
issues.push({
|
|
2776
|
-
id: `intheway:${root}:${s.name}`,
|
|
2777
|
-
severity: "problem",
|
|
2778
|
-
title: `${s.name} in ${where}: ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))} can't use it here`,
|
|
2779
|
-
detail: `${inTheWaySaid(root, way)}. skilllib won't link past what's there: clear the way, then run skilllib link.`,
|
|
2780
|
-
at: rows(root, [s.name])
|
|
2781
|
-
});
|
|
2782
|
-
}
|
|
2783
|
-
for (const { s, would } of blindHere.filter(({ would: w }) => !w.created.length && !w.blocked.length && w.uncommitted.length))
|
|
2784
|
-
issues.push({
|
|
2785
|
-
id: `uncommitted:${root}:${s.name}`,
|
|
2786
|
-
severity: "suggestion",
|
|
2787
|
-
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`,
|
|
2788
|
-
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\`).`,
|
|
2789
|
-
at: rows(root, [s.name])
|
|
2790
|
-
});
|
|
2791
|
-
const partial = blindHere.filter(({ would }) => would.created.length || would.blocked.length).map(({ s }) => s);
|
|
2792
|
-
if (partial.length) {
|
|
2793
|
-
const missing2 = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
|
|
2794
|
-
const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
|
|
2795
|
-
const manifestShared = partial.some((s) => s.managed) && isShared(gitOf(root)?.of(MANIFEST_FILE));
|
|
2796
|
-
const run = (allowTracked) => () => {
|
|
2797
|
-
if (allowTracked) allowTrackedLinks(root);
|
|
2798
|
-
const r = linkAll(root, { allowTracked, names: partial.map((s) => s.name) });
|
|
2799
|
-
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)` : ""}`;
|
|
2800
|
-
if (!r.linked.length) {
|
|
2801
|
-
const still = partial.filter((s) => visibilityOf(root, s.name).some((v) => v.paths === 0 && !loadsGlobally(s.name, v.id)));
|
|
2802
|
-
if (!still.length) return `${where}: already usable by ${agentNames(missing2)}`;
|
|
2803
|
-
throw new Error(`${where}: nothing linked${notes}`);
|
|
2804
|
-
}
|
|
2805
|
-
return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing2)}${notes}`;
|
|
2806
|
-
};
|
|
2807
|
-
issues.push({
|
|
2808
|
-
id: `usable:${root}`,
|
|
2809
|
-
severity: "suggestion",
|
|
2810
|
-
title: `${where}: ${plural(partial.length, "skill")} not usable by ${agentNames(missing2)}`,
|
|
2811
|
-
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." : ""}`,
|
|
2812
|
-
at: rows(root, partial.map((s) => s.name)),
|
|
2813
|
-
...manifestShared && !tracked.length ? { choices: [{ label: "Add the links", hint: "git will see skilllib.json change", run: run(false) }] } : tracked.length ? {
|
|
2814
|
-
choices: [
|
|
2815
|
-
// Only when some link goes where git doesn't track: otherwise this one would link nothing.
|
|
2816
|
-
...blindHere.some(({ would }) => would.created.length) ? [{ label: "Add links, but not where git tracks the folder", hint: `skips ${tracked.join(", ")}`, run: run(false) }] : [],
|
|
2817
|
-
{ label: "Add links everywhere", hint: `remembered for ${where}; git will see them`, run: run(true) }
|
|
2818
|
-
]
|
|
2819
|
-
} : { fix: { label: "Add the links", run: run(false) } }
|
|
2820
|
-
});
|
|
2821
|
-
}
|
|
2822
|
-
const differing = new Set(tidy2.conflicts.map((c) => c.name));
|
|
2823
|
-
const git2 = gitOf(root);
|
|
2824
|
-
const committed = (s) => isShared(git2?.of(relativeTo(root, s.path)));
|
|
2825
|
-
for (const s of unstuck) {
|
|
2826
|
-
if (isLink(s.path) || differing.has(s.name)) continue;
|
|
2827
|
-
if (s.state === "local only" && (committed(s) || untrackable(s))) {
|
|
2828
|
-
const why = committed(s) ? "it's the team's (committed)" : `${untrackable(s)}, so skilllib can't track it here`;
|
|
2829
|
-
issues.push({
|
|
2830
|
-
id: `local:${root}:${s.name}`,
|
|
2831
|
-
severity: "suggestion",
|
|
2832
|
-
title: `${s.name} in ${where} isn't in your library`,
|
|
2833
|
-
detail: `Copy it into the library to reuse it in other projects. The repo's copy stays as it is: ${why}.`,
|
|
2834
|
-
at: rows(root, [s.name]),
|
|
2835
|
-
choices: [
|
|
2836
|
-
{
|
|
2837
|
-
label: "Copy into the library",
|
|
2838
|
-
hint: `${where}'s copy stays as it is`,
|
|
2839
|
-
run: () => {
|
|
2840
|
-
intoLibrary(s.path);
|
|
2841
|
-
return `${s.name} copied into your library; ${where}'s copy stays as it is`;
|
|
2842
|
-
}
|
|
2843
|
-
}
|
|
2844
|
-
]
|
|
2845
|
-
});
|
|
2846
|
-
} else if (s.state === "local only") {
|
|
2847
|
-
issues.push({
|
|
2848
|
-
id: `local:${root}:${s.name}`,
|
|
2849
|
-
severity: "suggestion",
|
|
2850
|
-
title: `${s.name} in ${where} isn't in your library`,
|
|
2851
|
-
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.",
|
|
2852
|
-
at: rows(root, [s.name]),
|
|
2853
|
-
choices: [
|
|
2854
|
-
{
|
|
2855
|
-
label: "Import into the library",
|
|
2856
|
-
// Tracking it writes skilllib.json.
|
|
2857
|
-
hint: `copies it into Your skills${gitNote(root, [join13(root, MANIFEST_FILE)])}`,
|
|
2858
|
-
run: () => {
|
|
2859
|
-
intoLibrary(s.path);
|
|
2860
|
-
track(root, s.name);
|
|
2861
|
-
return `${s.name} imported and tracked in ${where}`;
|
|
2862
|
-
}
|
|
2863
|
-
}
|
|
2864
|
-
]
|
|
2865
|
-
});
|
|
2866
|
-
} else if (s.state === "untracked copy of library skill" && !committed(s) && !untrackable(s)) {
|
|
2867
|
-
issues.push({
|
|
2868
|
-
id: `adopt:${root}:${s.name}`,
|
|
2869
|
-
severity: "suggestion",
|
|
2870
|
-
title: `${s.name} in ${where} isn't tracked`,
|
|
2871
|
-
detail: "It's identical to the library copy; track it (in skilllib.json) so future library changes reach it.",
|
|
2872
|
-
at: rows(root, [s.name]),
|
|
2873
|
-
choices: [{ label: "Track it", hint: `adds it to ${where}'s skilllib.json`, run: () => `${track(root, s.name).name} tracked in ${where}` }]
|
|
2874
|
-
});
|
|
2875
|
-
} else if (s.state === "untracked, differs from library" || s.state === "repo skill, differs from library") {
|
|
2876
|
-
const teams = s.state.startsWith("repo skill") || committed(s) || !!untrackable(s);
|
|
2877
|
-
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`;
|
|
2878
|
-
issues.push({
|
|
2879
|
-
id: `differs:${root}:${s.name}`,
|
|
2880
|
-
severity: "suggestion",
|
|
2881
|
-
title: `${s.name} in ${where} differs from your library`,
|
|
2882
|
-
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.`,
|
|
2883
|
-
at: rows(root, [s.name]),
|
|
2884
|
-
choices: [
|
|
2885
|
-
{
|
|
2886
|
-
label: "Update your library from it",
|
|
2887
|
-
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`,
|
|
2888
|
-
run: () => {
|
|
2889
|
-
intoLibrary(s.path, true);
|
|
2890
|
-
if (teams) return `${s.name}: your library has ${where}'s copy; ${where}'s copy stays as it is`;
|
|
2891
|
-
const c = track(root, s.name);
|
|
2892
|
-
return `${s.name}: your library has ${where}'s copy as v${c.to}; ${where} tracks it`;
|
|
2893
|
-
}
|
|
2894
|
-
},
|
|
2895
|
-
...teams ? [] : [
|
|
2896
|
-
{
|
|
2897
|
-
label: "Replace it with your library's",
|
|
2898
|
-
hint: "this copy is restorable with skilllib restore",
|
|
2899
|
-
run: () => {
|
|
2900
|
-
const c = addSkill(root, s.name, { force: true });
|
|
2901
|
-
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
2902
|
-
return `${s.name} in ${where} is your library's v${c.to} now; the old copy is restorable with skilllib restore`;
|
|
2903
|
-
}
|
|
2904
|
-
}
|
|
2905
|
-
]
|
|
2906
|
-
]
|
|
2907
|
-
});
|
|
2908
|
-
} else if (s.state === "from npx skills" && !((committed(s) || untrackable(s)) && versionForHash(s.name, treeHash(s.path) ?? "") !== null)) {
|
|
2909
|
-
const teams = committed(s) || !!untrackable(s);
|
|
2910
|
-
issues.push({
|
|
2911
|
-
id: `npx:${root}:${s.name}`,
|
|
2912
|
-
severity: "suggestion",
|
|
2913
|
-
title: `${s.name} in ${where} came from npx skills, not your library`,
|
|
2914
|
-
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`}.`,
|
|
2915
|
-
at: rows(root, [s.name]),
|
|
2916
|
-
choices: [
|
|
2917
|
-
{
|
|
2918
|
-
label: "Import it into your library",
|
|
2919
|
-
hint: teams ? `${where}'s copy stays as it is` : `${where} then tracks it`,
|
|
2920
|
-
run: () => {
|
|
2921
|
-
intoLibrary(s.path);
|
|
2922
|
-
if (teams) return `${s.name} copied into your library; ${where}'s copy stays as it is`;
|
|
2923
|
-
track(root, s.name);
|
|
2924
|
-
return `${s.name} imported and tracked in ${where}`;
|
|
2925
|
-
}
|
|
2926
|
-
}
|
|
2927
|
-
]
|
|
2928
|
-
});
|
|
2929
|
-
}
|
|
2930
|
-
if (s.state === "not in library")
|
|
2931
|
-
issues.push({
|
|
2932
|
-
id: `not-in-lib:${root}:${s.name}`,
|
|
2933
|
-
severity: "suggestion",
|
|
2934
|
-
title: `${s.name} in ${where} is tracked, but no longer in your library`,
|
|
2935
|
-
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.`,
|
|
2936
|
-
at: rows(root, [s.name]),
|
|
2937
|
-
choices: [
|
|
2938
|
-
{
|
|
2939
|
-
label: "Copy it back into your library",
|
|
2940
|
-
hint: `${where}'s copy stays as it is`,
|
|
2941
|
-
run: () => {
|
|
2942
|
-
intoLibrary(s.path);
|
|
2943
|
-
return `${s.name} copied back into your library`;
|
|
2944
|
-
}
|
|
2945
|
-
}
|
|
2946
|
-
]
|
|
2947
|
-
});
|
|
2948
|
-
if (s.state === "edited locally" || s.state === "edited locally, update available") {
|
|
2949
|
-
const newer2 = s.state !== "edited locally";
|
|
2950
|
-
const pinned = s.version ? `v${s.version}` : "the library version";
|
|
2951
|
-
issues.push({
|
|
2952
|
-
id: `edited:${root}:${s.name}`,
|
|
2953
|
-
severity: "suggestion",
|
|
2954
|
-
title: `${s.name} in ${where} has local edits`,
|
|
2955
|
-
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."}`,
|
|
2956
|
-
at: rows(root, [s.name]),
|
|
2957
|
-
choices: untrackable(s) ? [
|
|
2958
|
-
{
|
|
2959
|
-
label: "Copy the edits into Your skills",
|
|
2960
|
-
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)`,
|
|
2961
|
-
run: () => {
|
|
2962
|
-
intoLibrary(s.path, true);
|
|
2963
|
-
return `${s.name}: the edits are in Your skills as v${latestVersion(s.name)?.version}; ${where} stays as it is`;
|
|
2964
|
-
}
|
|
2965
|
-
}
|
|
2966
|
-
] : [
|
|
2967
|
-
{
|
|
2968
|
-
label: "Save as a new version in Your skills",
|
|
2969
|
-
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)])}`,
|
|
2970
|
-
run: () => {
|
|
2971
|
-
importSkill(s.path, { force: true });
|
|
2972
|
-
const c = addSkill(root, s.name);
|
|
2973
|
-
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
2974
|
-
return `${s.name}: saved as v${c.to}; ${where} uses it`;
|
|
2975
|
-
}
|
|
2976
|
-
},
|
|
2977
|
-
{
|
|
2978
|
-
label: "Discard the edits",
|
|
2979
|
-
hint: `back to ${pinned}; the edited copy is restorable with skilllib restore${gitNote(root, [s.path, join13(root, MANIFEST_FILE)])}`,
|
|
2980
|
-
run: () => {
|
|
2981
|
-
const c = discardEdits(root, s.name);
|
|
2982
|
-
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
2983
|
-
return `${s.name}: edits discarded, back to v${c.to}`;
|
|
2984
|
-
}
|
|
2985
|
-
}
|
|
2986
|
-
]
|
|
2987
|
-
});
|
|
2988
|
-
}
|
|
2989
|
-
if (alsoLoadedGlobally(s)) alsoGlobal.set(s.name, [...alsoGlobal.get(s.name) ?? [], { root, managed: s.managed }]);
|
|
2990
|
-
}
|
|
2991
|
-
}
|
|
2785
|
+
const ctx = { issues, statuses, openedProjects, globalCopies, alsoLoadedGlobally, alsoGlobal };
|
|
2786
|
+
for (const root of projects2) repoIssues(root, ctx);
|
|
2992
2787
|
for (const root of projects2) {
|
|
2993
2788
|
for (const n of nested.get(root)) {
|
|
2994
2789
|
const copies = alsoGlobal.get(n.name) ?? [];
|
|
@@ -3001,23 +2796,23 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
3001
2796
|
const subsOf = (root) => subsMemo.get(root) ?? (subsMemo.set(root, packageDirs(root)), subsMemo.get(root));
|
|
3002
2797
|
const entriesOf2 = (root) => nestedMemo.get(root) ?? (nestedMemo.set(root, nestedEntries(root, { subs: subsOf(root) })), nestedMemo.get(root));
|
|
3003
2798
|
for (const [name, copies] of alsoGlobal) {
|
|
3004
|
-
const wheres = copies.map((c) =>
|
|
2799
|
+
const wheres = copies.map((c) => basename7(c.root)).join(", ");
|
|
3005
2800
|
const removable = copies.filter((c) => c.managed && statuses.get(c.root)?.some((s) => s.name === name && removesClean(c.root, s))).map((c) => c.root);
|
|
3006
|
-
const repoOwn = copies.filter((c) => !c.managed).map((c) =>
|
|
2801
|
+
const repoOwn = copies.filter((c) => !c.managed).map((c) => basename7(c.root));
|
|
3007
2802
|
const at = [...copies.flatMap((c) => rows(c.root, [name])), ...rows(null, [name])];
|
|
3008
2803
|
if (kept.has(name)) {
|
|
3009
2804
|
const globalLoaders = new Set(globalCopies.filter((m) => m.name === name).flatMap((m) => m.harnesses));
|
|
3010
2805
|
const onlyHere = (root) => visibilityOf(root, name).filter((v) => v.paths > 0 && !globalLoaders.has(v.id)).map((v) => harness(v.id).name);
|
|
3011
2806
|
const needed = removable.filter((root) => onlyHere(root).length);
|
|
3012
2807
|
const extra = removable.filter((root) => !onlyHere(root).length);
|
|
3013
|
-
const removeWheres = extra.map((r) =>
|
|
2808
|
+
const removeWheres = extra.map((r) => basename7(r)).join(", ");
|
|
3014
2809
|
issues.push({
|
|
3015
2810
|
id: `twice:${name}`,
|
|
3016
2811
|
severity: "suggestion",
|
|
3017
2812
|
title: `${name}: in ${wheres}, and you keep it global`,
|
|
3018
2813
|
detail: [
|
|
3019
2814
|
extra.length ? `The global copy already loads in ${removeWheres} for every agent that reads the project copy, so those are extra.` : "",
|
|
3020
|
-
needed.length ? `In ${needed.map((r) => `${
|
|
2815
|
+
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.` : "",
|
|
3021
2816
|
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.` : ""
|
|
3022
2817
|
].filter(Boolean).join(" "),
|
|
3023
2818
|
at,
|
|
@@ -3028,8 +2823,8 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
3028
2823
|
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" : ""}`,
|
|
3029
2824
|
run: () => {
|
|
3030
2825
|
const results = extra.map((root) => [root, removeSkill(root, name)]);
|
|
3031
|
-
const removed = results.filter(([, c]) => c.action === "removed").map(([r]) =>
|
|
3032
|
-
const kept2 = results.filter(([, c]) => c.action !== "removed").map(([r, c]) => `${
|
|
2826
|
+
const removed = results.filter(([, c]) => c.action === "removed").map(([r]) => basename7(r));
|
|
2827
|
+
const kept2 = results.filter(([, c]) => c.action !== "removed").map(([r, c]) => `${basename7(r)} (${c.reason})`);
|
|
3033
2828
|
if (kept2.length) throw new Error(`${name}: ${removed.length ? `removed from ${removed.join(", ")}; ` : ""}kept in ${kept2.join(", ")}`);
|
|
3034
2829
|
return `${name}: removed from ${removed.join(", ")}; the global copy stays`;
|
|
3035
2830
|
}
|
|
@@ -3090,6 +2885,404 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
3090
2885
|
}
|
|
3091
2886
|
return issues;
|
|
3092
2887
|
}
|
|
2888
|
+
function repoIssues(root, ctx) {
|
|
2889
|
+
const where = basename7(root);
|
|
2890
|
+
const status2 = ctx.statuses.get(root);
|
|
2891
|
+
ctx.issues.push(...brokenLinksIn(root, ctx.openedProjects));
|
|
2892
|
+
const pins = readManifest(root).skills;
|
|
2893
|
+
const shadowed = Object.keys(personalPins(root)).filter((name) => pins[name] && !pins[name].personal);
|
|
2894
|
+
if (shadowed.length)
|
|
2895
|
+
ctx.issues.push({
|
|
2896
|
+
id: `shadowed:${root}`,
|
|
2897
|
+
severity: "suggestion",
|
|
2898
|
+
title: `${where}: ${plural(shadowed.length, "skill")} pinned in both skilllib.json and your personal pins`,
|
|
2899
|
+
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.`,
|
|
2900
|
+
at: rows(root, shadowed),
|
|
2901
|
+
choices: [
|
|
2902
|
+
{
|
|
2903
|
+
label: "Drop the personal pins",
|
|
2904
|
+
hint: "skilllib.json's pins stay; nothing that loads changes",
|
|
2905
|
+
run: () => {
|
|
2906
|
+
for (const name of shadowed) dropPersonalPin(root, name);
|
|
2907
|
+
return `${where}: ${shadowed.join(", ")} now pinned only in skilllib.json`;
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
]
|
|
2911
|
+
});
|
|
2912
|
+
const blockedFor = /* @__PURE__ */ new Map();
|
|
2913
|
+
const untrackable = (s) => {
|
|
2914
|
+
if (!blockedFor.has(s.name)) blockedFor.set(s.name, trackBlocker(root, s.name));
|
|
2915
|
+
return blockedFor.get(s.name);
|
|
2916
|
+
};
|
|
2917
|
+
const blockers = new Map(status2.filter((s) => s.managed).flatMap((s) => ((b) => b ? [[s.name, b]] : [])(syncBlocker(root, s.name, pins[s.name]))));
|
|
2918
|
+
for (const [name, blocker] of blockers)
|
|
2919
|
+
ctx.issues.push({
|
|
2920
|
+
id: `stuck:${root}:${name}`,
|
|
2921
|
+
severity: "problem",
|
|
2922
|
+
title: `${name} in ${where}: sync can't put it back as ${pins[name]?.personal ? "your personal pins have" : "skilllib.json pins"} it`,
|
|
2923
|
+
// What to do depends on what's there: a folder that links out of the repo is a setup of yours, and a pin
|
|
2924
|
+
// your library has no version of can't come back by sync anyway.
|
|
2925
|
+
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."}`,
|
|
2926
|
+
at: rows(root, [name])
|
|
2927
|
+
});
|
|
2928
|
+
const unstuck = status2.filter((s) => !blockers.has(s.name));
|
|
2929
|
+
const missing = unstuck.filter((s) => s.state === "folder missing" && s.latest !== null);
|
|
2930
|
+
const knownPin = (s) => versionForHash(s.name, pins[s.name]?.hash ?? "") !== null;
|
|
2931
|
+
const restorable = missing.filter(knownPin);
|
|
2932
|
+
for (const s of unstuck.filter((m) => (m.state === "folder missing" || m.state === "out of sync") && m.latest !== null && !knownPin(m)))
|
|
2933
|
+
ctx.issues.push({
|
|
2934
|
+
id: `pin:${root}:${s.name}`,
|
|
2935
|
+
severity: "problem",
|
|
2936
|
+
title: `${s.name}: the version ${s.personal ? "your personal pins" : `${where}'s skilllib.json`} pin${s.personal ? "" : "s"} isn't in your library`,
|
|
2937
|
+
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}; ${s.personal ? "your personal pins" : `${where}'s skilllib.json`} then pin${s.personal ? "" : "s"} it.`}`,
|
|
2938
|
+
at: rows(root, [s.name]),
|
|
2939
|
+
// It changes what the shared skilllib.json pins: a choice, never run by `doctor --fix`.
|
|
2940
|
+
...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}` }] }
|
|
2941
|
+
});
|
|
2942
|
+
if (restorable.length > 0) {
|
|
2943
|
+
ctx.issues.push({
|
|
2944
|
+
id: `sync:${root}`,
|
|
2945
|
+
severity: "problem",
|
|
2946
|
+
title: `${where}: ${plural(restorable.length, "skill")} pinned but not installed`,
|
|
2947
|
+
detail: `${restorable.map((s) => s.name).join(", ")}: the folder is missing`,
|
|
2948
|
+
at: rows(root, restorable.map((s) => s.name)),
|
|
2949
|
+
fix: {
|
|
2950
|
+
label: "Sync the project",
|
|
2951
|
+
// Only the missing folders: an out-of-sync copy (maybe committed) is its own issue's choice.
|
|
2952
|
+
run: () => summarize(where, "restored", syncProject(root, { names: restorable.map((s) => s.name) }))
|
|
2953
|
+
}
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
const drifted = unstuck.filter((s) => s.state === "out of sync" && knownPin(s));
|
|
2957
|
+
if (drifted.length) {
|
|
2958
|
+
const seen = drifted.some((s) => isShared(gitOf(root)?.of(relativeTo(root, s.path))));
|
|
2959
|
+
const yours = drifted.some((s) => untrackable(s));
|
|
2960
|
+
const install = {
|
|
2961
|
+
label: "Install the pinned versions",
|
|
2962
|
+
hint: seen ? "git will see the copies change" : "your library keeps every version",
|
|
2963
|
+
run: () => summarize(where, "synced", syncProject(root, { names: drifted.map((s) => s.name) }))
|
|
2964
|
+
};
|
|
2965
|
+
ctx.issues.push({
|
|
2966
|
+
id: `out-of-sync:${root}`,
|
|
2967
|
+
severity: "suggestion",
|
|
2968
|
+
title: `${where}: ${plural(drifted.length, "skill")} not at the pinned version`,
|
|
2969
|
+
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.` : ""}`,
|
|
2970
|
+
at: rows(root, drifted.map((s) => s.name)),
|
|
2971
|
+
...seen || yours ? { choices: [install] } : { fix: install }
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
for (const s of unstuck.filter((s2) => s2.state === "folder missing" && s2.latest === null))
|
|
2975
|
+
ctx.issues.push({
|
|
2976
|
+
id: `unavailable:${root}:${s.name}`,
|
|
2977
|
+
severity: "problem",
|
|
2978
|
+
title: `${s.name}: pinned in ${s.personal ? "your personal pins" : `${where}'s skilllib.json`}, but not in your library`,
|
|
2979
|
+
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.`,
|
|
2980
|
+
at: rows(root, [s.name]),
|
|
2981
|
+
// Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
|
|
2982
|
+
choices: [
|
|
2983
|
+
{
|
|
2984
|
+
label: "Remove it from skilllib.json",
|
|
2985
|
+
hint: `${where} stops pinning it`,
|
|
2986
|
+
run: () => {
|
|
2987
|
+
const r = removeSkill(root, s.name);
|
|
2988
|
+
if (r.action !== "removed") throw new Error(`${s.name}: ${r.reason}`);
|
|
2989
|
+
return `${s.name}: removed from ${where}'s skilllib.json`;
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
]
|
|
2993
|
+
});
|
|
2994
|
+
const behind = unstuck.filter((s) => s.state === "update available" && !untrackable(s));
|
|
2995
|
+
if (behind.length > 0)
|
|
2996
|
+
ctx.issues.push({
|
|
2997
|
+
id: `update:${root}`,
|
|
2998
|
+
severity: "suggestion",
|
|
2999
|
+
title: `${where}: ${plural(behind.length, "skill")} out of date`,
|
|
3000
|
+
detail: behind.map((s) => `${s.name} (v${s.version} \u2192 v${s.latest})`).join(", "),
|
|
3001
|
+
at: rows(root, behind.map((s) => s.name)),
|
|
3002
|
+
// It changes what the repo pins and the copies its agents load (often committed): a choice, never `doctor --fix`.
|
|
3003
|
+
choices: [
|
|
3004
|
+
{
|
|
3005
|
+
label: "Update to the newest versions",
|
|
3006
|
+
hint: isShared(gitOf(root)?.of(MANIFEST_FILE)) ? "git will see the new copies and skilllib.json" : "replaces each copy with your library's newest",
|
|
3007
|
+
run: () => summarize(where, "updated", updateProject(root, behind.map((s) => s.name)))
|
|
3008
|
+
}
|
|
3009
|
+
]
|
|
3010
|
+
});
|
|
3011
|
+
const tidy2 = planProjectTidy(root);
|
|
3012
|
+
if (tidy2.plans.length) {
|
|
3013
|
+
const git3 = gitOf(root);
|
|
3014
|
+
const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p, git3).map((s) => describeStep(s, root)));
|
|
3015
|
+
const run = (mode) => () => {
|
|
3016
|
+
const r = applyAll(tidy2.plans, mode);
|
|
3017
|
+
if (!r.skills) throw new Error(`${where}: nothing tidied${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`);
|
|
3018
|
+
return `${where}: tidied ${plural(r.skills, "skill")}${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`;
|
|
3019
|
+
};
|
|
3020
|
+
ctx.issues.push({
|
|
3021
|
+
id: `tidy:${root}`,
|
|
3022
|
+
severity: "suggestion",
|
|
3023
|
+
title: `${where}: ${plural(tidy2.plans.length, "skill")} with extra copies or links`,
|
|
3024
|
+
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." : "."),
|
|
3025
|
+
at: rows(root, tidy2.plans.map((p) => p.name)),
|
|
3026
|
+
...visible2.length ? {
|
|
3027
|
+
choices: [
|
|
3028
|
+
// "Keep git as it is" only when it leaves something to do.
|
|
3029
|
+
...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") }] : [],
|
|
3030
|
+
{ label: "Tidy everything", hint: `${plural(visible2.length, "change")} will show in git status`, run: run("go") }
|
|
3031
|
+
]
|
|
3032
|
+
} : { fix: { label: "Tidy", run: run("go") } }
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
3035
|
+
ctx.issues.push(...tidy2.conflicts.map(conflictIssue));
|
|
3036
|
+
for (const s of tidy2.skipped.filter((x) => x.differs))
|
|
3037
|
+
ctx.issues.push({
|
|
3038
|
+
id: `linkdiffers:${root}:${s.name}`,
|
|
3039
|
+
severity: "problem",
|
|
3040
|
+
title: `${s.name} in ${where}: the copies differ`,
|
|
3041
|
+
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.`,
|
|
3042
|
+
at: rows(root, [s.name])
|
|
3043
|
+
});
|
|
3044
|
+
const loadsGlobally = (name, id) => {
|
|
3045
|
+
const here = status2.find((s) => s.name === name);
|
|
3046
|
+
return ctx.globalCopies.some((m) => m.name === name && m.harnesses.includes(id) && (!here || treeHash(m.path) === treeHash(here.path)));
|
|
3047
|
+
};
|
|
3048
|
+
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 }) }));
|
|
3049
|
+
const enabledIds = enabledHarnesses();
|
|
3050
|
+
const notFolders = /* @__PURE__ */ new Set();
|
|
3051
|
+
for (const dir of new Set(enabledIds.map((id) => harness(id).projectDirs[0]))) {
|
|
3052
|
+
const blocker = notWritable(root, dir);
|
|
3053
|
+
if (!blocker || notFolders.has(blocker)) continue;
|
|
3054
|
+
notFolders.add(blocker);
|
|
3055
|
+
const who = agentNames(enabledIds.filter((id) => notWritable(root, harness(id).projectDirs[0]) === blocker));
|
|
3056
|
+
const out2 = linksOut(root, blocker);
|
|
3057
|
+
ctx.issues.push({
|
|
3058
|
+
id: `notdir:${root}:${blocker}`,
|
|
3059
|
+
severity: out2 ? "suggestion" : "problem",
|
|
3060
|
+
title: out2 ? `${where}: ${blocker} links out of the repo` : `${where}: ${blocker} isn't a folder`,
|
|
3061
|
+
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.`,
|
|
3062
|
+
at: [{ root, skill: "" }]
|
|
3063
|
+
// the repo itself: its row in Places
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
3066
|
+
for (const { s, would } of blindHere) {
|
|
3067
|
+
const way = would.inTheWay.filter((p) => !notFolders.has(p));
|
|
3068
|
+
if (!way.length || blockers.has(s.name)) continue;
|
|
3069
|
+
ctx.issues.push({
|
|
3070
|
+
id: `intheway:${root}:${s.name}`,
|
|
3071
|
+
severity: "problem",
|
|
3072
|
+
title: `${s.name} in ${where}: ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))} can't use it here`,
|
|
3073
|
+
detail: `${inTheWaySaid(root, way)}. skilllib won't link past what's there: clear the way, then run skilllib link.`,
|
|
3074
|
+
at: rows(root, [s.name])
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
3077
|
+
for (const { s, would } of blindHere.filter(({ would: w }) => !w.created.length && !w.blocked.length && w.uncommitted.length))
|
|
3078
|
+
ctx.issues.push({
|
|
3079
|
+
id: `uncommitted:${root}:${s.name}`,
|
|
3080
|
+
severity: "suggestion",
|
|
3081
|
+
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`,
|
|
3082
|
+
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\`).`,
|
|
3083
|
+
at: rows(root, [s.name])
|
|
3084
|
+
});
|
|
3085
|
+
const partial = blindHere.filter(({ would }) => would.created.length || would.blocked.length).map(({ s }) => s);
|
|
3086
|
+
if (partial.length) {
|
|
3087
|
+
const missing2 = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
|
|
3088
|
+
const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
|
|
3089
|
+
const manifestShared = partial.some((s) => s.managed) && isShared(gitOf(root)?.of(MANIFEST_FILE));
|
|
3090
|
+
const run = (allowTracked) => () => {
|
|
3091
|
+
if (allowTracked) allowTrackedLinks(root);
|
|
3092
|
+
const r = linkAll(root, { allowTracked, names: partial.map((s) => s.name) });
|
|
3093
|
+
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)` : ""}`;
|
|
3094
|
+
if (!r.linked.length) {
|
|
3095
|
+
const still = partial.filter((s) => visibilityOf(root, s.name).some((v) => v.paths === 0 && !loadsGlobally(s.name, v.id)));
|
|
3096
|
+
if (!still.length) return `${where}: already usable by ${agentNames(missing2)}`;
|
|
3097
|
+
throw new Error(`${where}: nothing linked${notes}`);
|
|
3098
|
+
}
|
|
3099
|
+
return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing2)}${notes}`;
|
|
3100
|
+
};
|
|
3101
|
+
ctx.issues.push({
|
|
3102
|
+
id: `usable:${root}`,
|
|
3103
|
+
severity: "suggestion",
|
|
3104
|
+
title: `${where}: ${plural(partial.length, "skill")} not usable by ${agentNames(missing2)}`,
|
|
3105
|
+
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." : ""}`,
|
|
3106
|
+
at: rows(root, partial.map((s) => s.name)),
|
|
3107
|
+
...manifestShared && !tracked.length ? { choices: [{ label: "Add the links", hint: "git will see skilllib.json change", run: run(false) }] } : tracked.length ? {
|
|
3108
|
+
choices: [
|
|
3109
|
+
// Only when some link goes where git doesn't track: otherwise this one would link nothing.
|
|
3110
|
+
...blindHere.some(({ would }) => would.created.length) ? [{ label: "Add links, but not where git tracks the folder", hint: `skips ${tracked.join(", ")}`, run: run(false) }] : [],
|
|
3111
|
+
{ label: "Add links everywhere", hint: `remembered for ${where}; git will see them`, run: run(true) }
|
|
3112
|
+
]
|
|
3113
|
+
} : { fix: { label: "Add the links", run: run(false) } }
|
|
3114
|
+
});
|
|
3115
|
+
}
|
|
3116
|
+
const differing = new Set(tidy2.conflicts.map((c) => c.name));
|
|
3117
|
+
const git2 = gitOf(root);
|
|
3118
|
+
const committed = (s) => isShared(git2?.of(relativeTo(root, s.path)));
|
|
3119
|
+
for (const s of unstuck) {
|
|
3120
|
+
if (isLink(s.path) || differing.has(s.name)) continue;
|
|
3121
|
+
if (s.state === "local only" && (committed(s) || untrackable(s))) {
|
|
3122
|
+
const why = committed(s) ? "it's the team's (committed)" : `${untrackable(s)}, so skilllib can't track it here`;
|
|
3123
|
+
ctx.issues.push({
|
|
3124
|
+
id: `local:${root}:${s.name}`,
|
|
3125
|
+
severity: "suggestion",
|
|
3126
|
+
title: `${s.name} in ${where} isn't in your library`,
|
|
3127
|
+
detail: `Copy it into the library to reuse it in other projects. The repo's copy stays as it is: ${why}.`,
|
|
3128
|
+
at: rows(root, [s.name]),
|
|
3129
|
+
choices: [
|
|
3130
|
+
{
|
|
3131
|
+
label: "Copy into the library",
|
|
3132
|
+
hint: `${where}'s copy stays as it is`,
|
|
3133
|
+
run: () => {
|
|
3134
|
+
intoLibrary(s.path);
|
|
3135
|
+
return `${s.name} copied into your library; ${where}'s copy stays as it is`;
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
]
|
|
3139
|
+
});
|
|
3140
|
+
} else if (s.state === "local only") {
|
|
3141
|
+
ctx.issues.push({
|
|
3142
|
+
id: `local:${root}:${s.name}`,
|
|
3143
|
+
severity: "suggestion",
|
|
3144
|
+
title: `${s.name} in ${where} isn't in your library`,
|
|
3145
|
+
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, or your personal pins when it has none).",
|
|
3146
|
+
at: rows(root, [s.name]),
|
|
3147
|
+
choices: [
|
|
3148
|
+
{
|
|
3149
|
+
label: "Import into the library",
|
|
3150
|
+
// Tracking it writes skilllib.json.
|
|
3151
|
+
hint: `copies it into Your skills${gitNote(root, [join13(root, MANIFEST_FILE)])}`,
|
|
3152
|
+
run: () => {
|
|
3153
|
+
intoLibrary(s.path);
|
|
3154
|
+
track(root, s.name);
|
|
3155
|
+
return `${s.name} imported and tracked in ${where}`;
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
]
|
|
3159
|
+
});
|
|
3160
|
+
} else if (s.state === "untracked copy of library skill" && !committed(s) && !untrackable(s)) {
|
|
3161
|
+
ctx.issues.push({
|
|
3162
|
+
id: `adopt:${root}:${s.name}`,
|
|
3163
|
+
severity: "suggestion",
|
|
3164
|
+
title: `${s.name} in ${where} isn't tracked`,
|
|
3165
|
+
detail: "It's identical to the library copy; track it (in skilllib.json) so future library changes reach it.",
|
|
3166
|
+
at: rows(root, [s.name]),
|
|
3167
|
+
choices: [{ label: "Track it", hint: `adds it to ${where}'s skilllib.json`, run: () => `${track(root, s.name).name} tracked in ${where}` }]
|
|
3168
|
+
});
|
|
3169
|
+
} else if (s.state === "untracked, differs from library" || s.state === "repo skill, differs from library") {
|
|
3170
|
+
const teams = s.state.startsWith("repo skill") || committed(s) || !!untrackable(s);
|
|
3171
|
+
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`;
|
|
3172
|
+
ctx.issues.push({
|
|
3173
|
+
id: `differs:${root}:${s.name}`,
|
|
3174
|
+
severity: "suggestion",
|
|
3175
|
+
title: `${s.name} in ${where} differs from your library`,
|
|
3176
|
+
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.`,
|
|
3177
|
+
at: rows(root, [s.name]),
|
|
3178
|
+
choices: [
|
|
3179
|
+
{
|
|
3180
|
+
label: "Update your library from it",
|
|
3181
|
+
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`,
|
|
3182
|
+
run: () => {
|
|
3183
|
+
intoLibrary(s.path, true);
|
|
3184
|
+
if (teams) return `${s.name}: your library has ${where}'s copy; ${where}'s copy stays as it is`;
|
|
3185
|
+
const c = track(root, s.name);
|
|
3186
|
+
return `${s.name}: your library has ${where}'s copy as v${c.to}; ${where} tracks it`;
|
|
3187
|
+
}
|
|
3188
|
+
},
|
|
3189
|
+
...teams ? [] : [
|
|
3190
|
+
{
|
|
3191
|
+
label: "Replace it with your library's",
|
|
3192
|
+
hint: "this copy is restorable with skilllib restore",
|
|
3193
|
+
run: () => {
|
|
3194
|
+
const c = addSkill(root, s.name, { force: true });
|
|
3195
|
+
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
3196
|
+
return `${s.name} in ${where} is your library's v${c.to} now; the old copy is restorable with skilllib restore`;
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
]
|
|
3200
|
+
]
|
|
3201
|
+
});
|
|
3202
|
+
} else if (s.state === "from npx skills" && !((committed(s) || untrackable(s)) && versionForHash(s.name, treeHash(s.path) ?? "") !== null)) {
|
|
3203
|
+
const teams = committed(s) || !!untrackable(s);
|
|
3204
|
+
ctx.issues.push({
|
|
3205
|
+
id: `npx:${root}:${s.name}`,
|
|
3206
|
+
severity: "suggestion",
|
|
3207
|
+
title: `${s.name} in ${where} came from npx skills, not your library`,
|
|
3208
|
+
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, or your personal pins when it has none)`}.`,
|
|
3209
|
+
at: rows(root, [s.name]),
|
|
3210
|
+
choices: [
|
|
3211
|
+
{
|
|
3212
|
+
label: "Import it into your library",
|
|
3213
|
+
hint: teams ? `${where}'s copy stays as it is` : `${where} then tracks it`,
|
|
3214
|
+
run: () => {
|
|
3215
|
+
intoLibrary(s.path);
|
|
3216
|
+
if (teams) return `${s.name} copied into your library; ${where}'s copy stays as it is`;
|
|
3217
|
+
track(root, s.name);
|
|
3218
|
+
return `${s.name} imported and tracked in ${where}`;
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
]
|
|
3222
|
+
});
|
|
3223
|
+
}
|
|
3224
|
+
if (s.state === "not in library")
|
|
3225
|
+
ctx.issues.push({
|
|
3226
|
+
id: `not-in-lib:${root}:${s.name}`,
|
|
3227
|
+
severity: "suggestion",
|
|
3228
|
+
title: `${s.name} in ${where} is tracked, but no longer in your library`,
|
|
3229
|
+
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.`,
|
|
3230
|
+
at: rows(root, [s.name]),
|
|
3231
|
+
choices: [
|
|
3232
|
+
{
|
|
3233
|
+
label: "Copy it back into your library",
|
|
3234
|
+
hint: `${where}'s copy stays as it is`,
|
|
3235
|
+
run: () => {
|
|
3236
|
+
intoLibrary(s.path);
|
|
3237
|
+
return `${s.name} copied back into your library`;
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
]
|
|
3241
|
+
});
|
|
3242
|
+
if (s.state === "edited locally" || s.state === "edited locally, update available") {
|
|
3243
|
+
const newer2 = s.state !== "edited locally";
|
|
3244
|
+
const pinned = s.version ? `v${s.version}` : "the library version";
|
|
3245
|
+
ctx.issues.push({
|
|
3246
|
+
id: `edited:${root}:${s.name}`,
|
|
3247
|
+
severity: "suggestion",
|
|
3248
|
+
title: `${s.name} in ${where} has local edits`,
|
|
3249
|
+
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."}`,
|
|
3250
|
+
at: rows(root, [s.name]),
|
|
3251
|
+
choices: untrackable(s) ? [
|
|
3252
|
+
{
|
|
3253
|
+
label: "Copy the edits into Your skills",
|
|
3254
|
+
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)`,
|
|
3255
|
+
run: () => {
|
|
3256
|
+
intoLibrary(s.path, true);
|
|
3257
|
+
return `${s.name}: the edits are in Your skills as v${latestVersion(s.name)?.version}; ${where} stays as it is`;
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
] : [
|
|
3261
|
+
{
|
|
3262
|
+
label: "Save as a new version in Your skills",
|
|
3263
|
+
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)])}`,
|
|
3264
|
+
run: () => {
|
|
3265
|
+
importSkill(s.path, { force: true });
|
|
3266
|
+
const c = addSkill(root, s.name);
|
|
3267
|
+
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
3268
|
+
return `${s.name}: saved as v${c.to}; ${where} uses it`;
|
|
3269
|
+
}
|
|
3270
|
+
},
|
|
3271
|
+
{
|
|
3272
|
+
label: "Discard the edits",
|
|
3273
|
+
hint: `back to ${pinned}; the edited copy is restorable with skilllib restore${gitNote(root, [s.path, join13(root, MANIFEST_FILE)])}`,
|
|
3274
|
+
run: () => {
|
|
3275
|
+
const c = discardEdits(root, s.name);
|
|
3276
|
+
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
3277
|
+
return `${s.name}: edits discarded, back to v${c.to}`;
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
]
|
|
3281
|
+
});
|
|
3282
|
+
}
|
|
3283
|
+
if (ctx.alsoLoadedGlobally(s)) ctx.alsoGlobal.set(s.name, [...ctx.alsoGlobal.get(s.name) ?? [], { root, managed: s.managed }]);
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3093
3286
|
function addedAt(path) {
|
|
3094
3287
|
try {
|
|
3095
3288
|
const st = lstatSync4(path);
|
|
@@ -3104,7 +3297,7 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
|
|
|
3104
3297
|
const issues = [];
|
|
3105
3298
|
const since = Date.now() - uses.days * 24 * 60 * 60 * 1e3;
|
|
3106
3299
|
for (const root of projects2) {
|
|
3107
|
-
const where =
|
|
3300
|
+
const where = basename7(root);
|
|
3108
3301
|
const unused = statusOf(root).filter((s) => s.state !== "folder missing" && uses.inProject(root, s.name) === 0 && addedAt(s.path) <= since);
|
|
3109
3302
|
if (!unused.length) continue;
|
|
3110
3303
|
const removable = unused.filter((s) => removesClean(root, s));
|
|
@@ -3132,15 +3325,15 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
|
|
|
3132
3325
|
if (uses.asUsed)
|
|
3133
3326
|
for (const p of installedPlugins().filter((x) => x.agent === "claude-code" && x.on && x.skills.length)) {
|
|
3134
3327
|
const name = pluginNameOf(p.id);
|
|
3135
|
-
if (p.skills.some((path) => uses.asUsed(`${name}:${
|
|
3328
|
+
if (p.skills.some((path) => uses.asUsed(`${name}:${basename7(path)}`) > 0)) continue;
|
|
3136
3329
|
issues.push({
|
|
3137
3330
|
id: `plugin-unused:${installId(p)}`,
|
|
3138
3331
|
severity: "suggestion",
|
|
3139
3332
|
title: `${p.id}${installedIn(p)}: its skills weren't used in ${plural(uses.days, "day")}`,
|
|
3140
|
-
detail: `No Claude Code session used ${p.skills.map((path) => `/${name}:${
|
|
3333
|
+
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.` : ""}`,
|
|
3141
3334
|
choices: [
|
|
3142
|
-
{ label: `Turn the plugin ${p.id} off`, hint: p.scope === "project" ? `stays installed; edits ${
|
|
3143
|
-
...p.synced || p.settingsOnly ? [] : [{ label: `Uninstall the plugin ${p.id}`, hint: `its data stays; restorable with skilllib restore${p.scope === "project" ? `; edits ${
|
|
3335
|
+
{ 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)) },
|
|
3336
|
+
...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)) }]
|
|
3144
3337
|
],
|
|
3145
3338
|
plugin: p.id,
|
|
3146
3339
|
pluginAt: p.projectPath ?? null
|
|
@@ -3193,7 +3386,7 @@ function globalFrom(kind, origin, path) {
|
|
|
3193
3386
|
}
|
|
3194
3387
|
function topLevel(root) {
|
|
3195
3388
|
try {
|
|
3196
|
-
return
|
|
3389
|
+
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);
|
|
3197
3390
|
} catch {
|
|
3198
3391
|
return [];
|
|
3199
3392
|
}
|
|
@@ -3220,7 +3413,8 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
3220
3413
|
state: usual ? void 0 : s.state,
|
|
3221
3414
|
latest: behind ? s.latest : void 0,
|
|
3222
3415
|
git: gitState,
|
|
3223
|
-
agents: sameSet(loadedBy, agents) ? void 0 : loadedBy
|
|
3416
|
+
agents: sameSet(loadedBy, agents) ? void 0 : loadedBy,
|
|
3417
|
+
personal: s.personal ? true : void 0
|
|
3224
3418
|
};
|
|
3225
3419
|
return [JSON.stringify(attrs), s.name, () => ({ ...attrs, skills: [] })];
|
|
3226
3420
|
})
|
|
@@ -3270,7 +3464,7 @@ function firstSentence(description) {
|
|
|
3270
3464
|
return sentence.length > 200 ? sentence.slice(0, 199).trimEnd() + "\u2026" : sentence;
|
|
3271
3465
|
}
|
|
3272
3466
|
function libraryFor(currentProject) {
|
|
3273
|
-
const manifests = visibleProjects().map((p) => ({ name:
|
|
3467
|
+
const manifests = visibleProjects().map((p) => ({ name: basename8(p), skills: pinsIn(p) }));
|
|
3274
3468
|
const here = currentProject ? new Set(projectStatus(currentProject).map((s) => s.name)) : null;
|
|
3275
3469
|
const all = librarySkills();
|
|
3276
3470
|
const describe2 = (s) => {
|
|
@@ -3282,7 +3476,7 @@ function libraryFor(currentProject) {
|
|
|
3282
3476
|
}
|
|
3283
3477
|
|
|
3284
3478
|
// src/usage.ts
|
|
3285
|
-
import { createReadStream, existsSync as existsSync13, readdirSync as
|
|
3479
|
+
import { createReadStream, existsSync as existsSync13, readdirSync as readdirSync10, statSync as statSync6 } from "fs";
|
|
3286
3480
|
import { join as join14 } from "path";
|
|
3287
3481
|
import { createInterface } from "readline";
|
|
3288
3482
|
var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
|
|
@@ -3367,17 +3561,17 @@ function transcriptFiles(sinceMs) {
|
|
|
3367
3561
|
if (!existsSync13(base)) return [];
|
|
3368
3562
|
const list2 = (dir) => {
|
|
3369
3563
|
try {
|
|
3370
|
-
return
|
|
3564
|
+
return readdirSync10(dir);
|
|
3371
3565
|
} catch {
|
|
3372
3566
|
return [];
|
|
3373
3567
|
}
|
|
3374
3568
|
};
|
|
3375
3569
|
return list2(base).flatMap((project) => {
|
|
3376
3570
|
const dir = join14(base, project);
|
|
3377
|
-
if (!
|
|
3571
|
+
if (!statSync6(dir, { throwIfNoEntry: false })?.isDirectory()) return [];
|
|
3378
3572
|
return list2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join14(dir, f)).filter((f) => {
|
|
3379
3573
|
try {
|
|
3380
|
-
const st =
|
|
3574
|
+
const st = statSync6(f);
|
|
3381
3575
|
return st.isFile() && st.mtimeMs >= sinceMs;
|
|
3382
3576
|
} catch {
|
|
3383
3577
|
return false;
|
|
@@ -3430,11 +3624,11 @@ function usesByProject(uses, projectOf) {
|
|
|
3430
3624
|
}
|
|
3431
3625
|
|
|
3432
3626
|
// src/move.ts
|
|
3433
|
-
import { basename as
|
|
3627
|
+
import { basename as basename9 } from "path";
|
|
3434
3628
|
function moveGlobalSkills(skills, roots, {
|
|
3435
3629
|
group,
|
|
3436
3630
|
allowTracked = false,
|
|
3437
|
-
label =
|
|
3631
|
+
label = basename9,
|
|
3438
3632
|
allowTrackedHint = "re-run with --allow-tracked"
|
|
3439
3633
|
} = {}) {
|
|
3440
3634
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -3482,6 +3676,8 @@ var FLAGS = {
|
|
|
3482
3676
|
json: { type: "boolean" },
|
|
3483
3677
|
"dry-run": { type: "boolean" },
|
|
3484
3678
|
"allow-git": { type: "boolean" },
|
|
3679
|
+
personal: { type: "boolean" },
|
|
3680
|
+
shared: { type: "boolean" },
|
|
3485
3681
|
days: { type: "string" },
|
|
3486
3682
|
keep: { type: "string" },
|
|
3487
3683
|
to: { type: "string", multiple: true }
|
|
@@ -3489,6 +3685,7 @@ var FLAGS = {
|
|
|
3489
3685
|
var VALUE_FLAGS = Object.entries(FLAGS).flatMap(([name, f]) => f.type === "string" ? [`--${name}`] : []);
|
|
3490
3686
|
function parseArgs(argv) {
|
|
3491
3687
|
const { values: v, positionals } = parseFlags({ args: argv, options: FLAGS, allowPositionals: true, strict: true });
|
|
3688
|
+
if (v.personal && v.shared) throw new Error("--personal and --shared don't go together: a pin is in one place");
|
|
3492
3689
|
const days = Number(v.days ?? USAGE_DAYS);
|
|
3493
3690
|
if (!Number.isInteger(days) || days <= 0) throw new Error(`--days takes a whole number of days, like --days 30 (got ${v.days})`);
|
|
3494
3691
|
return {
|
|
@@ -3502,6 +3699,7 @@ function parseArgs(argv) {
|
|
|
3502
3699
|
days,
|
|
3503
3700
|
dryRun: !!v["dry-run"],
|
|
3504
3701
|
allowGit: !!v["allow-git"],
|
|
3702
|
+
...v.personal || v.shared ? { personal: !!v.personal } : {},
|
|
3505
3703
|
...v.keep ? { keep: v.keep } : {},
|
|
3506
3704
|
to: (v.to ?? []).flatMap((t) => t.split(",").filter(Boolean))
|
|
3507
3705
|
};
|
|
@@ -3516,7 +3714,7 @@ function withDirs(root, changes) {
|
|
|
3516
3714
|
const { skills } = readManifest(root);
|
|
3517
3715
|
return changes.map((c) => {
|
|
3518
3716
|
const dep = c.action !== "removed" && c.action !== "skipped" ? skills[c.name] : void 0;
|
|
3519
|
-
return dep ? { ...c, dirs: [dep.dir ?? PROJECT_SKILLS_DIR, ...(dep.links ?? []).filter((d) => !c.inTheWay?.includes(`${d}/${c.name}`))] } : c;
|
|
3717
|
+
return dep ? { ...c, dirs: [dep.dir ?? PROJECT_SKILLS_DIR, ...(dep.links ?? []).filter((d) => !c.inTheWay?.includes(`${d}/${c.name}`))], ...dep.personal ? { personal: true } : {} } : c;
|
|
3520
3718
|
});
|
|
3521
3719
|
}
|
|
3522
3720
|
var incomplete = (c) => c.action === "skipped" || !!c.blocked?.length || !!c.uncommitted?.length || !!c.inTheWay?.length;
|
|
@@ -3535,7 +3733,7 @@ function printChanges(root, changes, args) {
|
|
|
3535
3733
|
else {
|
|
3536
3734
|
const icon = c.action === "removed" ? red("-") : c.action === "reset" ? yellow("\u21BA") : c.action === "unchanged" ? dim("=") : green(c.action === "installed" ? "+" : c.action === "relinked" ? "\u21E2" : "\u2191");
|
|
3537
3735
|
const versions = c.action === "removed" ? "" : c.from && c.from !== c.to ? ` v${c.from} \u2192 v${c.to}` : c.to ? ` v${c.to}` : "";
|
|
3538
|
-
info(` ${icon} ${c.name}${versions} ${dim(c.action)}`);
|
|
3736
|
+
info(` ${icon} ${c.name}${versions} ${dim(c.action)}${c.moved ? dim(c.moved === "personal" ? ", now in your personal pins" : ", now in skilllib.json") : ""}`);
|
|
3539
3737
|
if (c.reason) info(dim(` ${c.reason}`));
|
|
3540
3738
|
}
|
|
3541
3739
|
if (c.backedUp) info(dim(` local edits saved to ${tildify(c.backedUp)}; \`skilllib restore ${c.name}\` brings them back`));
|
|
@@ -3546,12 +3744,12 @@ function printChanges(root, changes, args) {
|
|
|
3546
3744
|
${done} done, ${skipped} skipped`));
|
|
3547
3745
|
}
|
|
3548
3746
|
function projectOfFactory(projects2) {
|
|
3549
|
-
const
|
|
3747
|
+
const sorted2 = [...projects2].sort((a, b) => b.length - a.length);
|
|
3550
3748
|
return (cwd) => {
|
|
3551
3749
|
if (!cwd) return null;
|
|
3552
|
-
const known =
|
|
3750
|
+
const known = sorted2.find((p) => cwd === p || cwd.startsWith(p + sep9));
|
|
3553
3751
|
if (known) return known;
|
|
3554
|
-
const repo = cwd.split(`${
|
|
3752
|
+
const repo = cwd.split(`${sep9}.claude${sep9}worktrees${sep9}`)[0];
|
|
3555
3753
|
return isProjectCandidate(repo) ? repo : null;
|
|
3556
3754
|
};
|
|
3557
3755
|
}
|
|
@@ -3565,7 +3763,7 @@ var projectsFor = (args) => {
|
|
|
3565
3763
|
if (!args.all) return [requireProject()];
|
|
3566
3764
|
const { readable, unreadable } = readableRoots(visibleProjects());
|
|
3567
3765
|
for (const u of unreadable) {
|
|
3568
|
-
error(`${
|
|
3766
|
+
error(`${basename10(u.root)}: ${u.reason}`);
|
|
3569
3767
|
process.exitCode = 1;
|
|
3570
3768
|
}
|
|
3571
3769
|
return readable;
|
|
@@ -3599,7 +3797,7 @@ async function status(args) {
|
|
|
3599
3797
|
const root = requireProject();
|
|
3600
3798
|
const skills = projectStatus(root);
|
|
3601
3799
|
const nested = nestedSkills(root);
|
|
3602
|
-
info(`${
|
|
3800
|
+
info(`${basename10(root)} ${dim(tildify(root))}
|
|
3603
3801
|
`);
|
|
3604
3802
|
if (skills.length === 0 && nested.length === 0) info("No skills in this project yet. Add one from your library with: skilllib add <name>");
|
|
3605
3803
|
const usage2 = await usageBySkill(args.days, root);
|
|
@@ -3672,7 +3870,7 @@ async function list(args) {
|
|
|
3672
3870
|
const installs = /* @__PURE__ */ new Map();
|
|
3673
3871
|
for (const project of visibleProjects()) {
|
|
3674
3872
|
for (const name of Object.keys(pinsIn(project))) {
|
|
3675
|
-
installs.set(name, [...installs.get(name) ?? [],
|
|
3873
|
+
installs.set(name, [...installs.get(name) ?? [], basename10(project)]);
|
|
3676
3874
|
}
|
|
3677
3875
|
}
|
|
3678
3876
|
const usage2 = await usageBySkill(args.days);
|
|
@@ -3715,7 +3913,7 @@ function teamStateIn(root, extraDirs = []) {
|
|
|
3715
3913
|
function projects(args) {
|
|
3716
3914
|
const { readable: roots, unreadable } = readableRoots(visibleProjects());
|
|
3717
3915
|
for (const u of unreadable) {
|
|
3718
|
-
error(`${
|
|
3916
|
+
error(`${basename10(u.root)}: ${u.reason}`);
|
|
3719
3917
|
process.exitCode = 1;
|
|
3720
3918
|
}
|
|
3721
3919
|
if (args.json) {
|
|
@@ -3724,7 +3922,7 @@ function projects(args) {
|
|
|
3724
3922
|
roots.map((root) => {
|
|
3725
3923
|
const teamState = teamStateIn(root);
|
|
3726
3924
|
return {
|
|
3727
|
-
name:
|
|
3925
|
+
name: basename10(root),
|
|
3728
3926
|
path: root,
|
|
3729
3927
|
skills: projectStatus(root).map((s) => ({ name: s.name, managed: s.managed, state: teamState(s), version: s.version, latest: s.latest }))
|
|
3730
3928
|
};
|
|
@@ -3742,7 +3940,7 @@ function projects(args) {
|
|
|
3742
3940
|
const usual = /* @__PURE__ */ new Set(["ok", "local only", "repo skill", "repo skill, in library", "from npx skills"]);
|
|
3743
3941
|
const attention = skills.filter((s) => !usual.has(teamState(s))).length;
|
|
3744
3942
|
return {
|
|
3745
|
-
Project:
|
|
3943
|
+
Project: basename10(root),
|
|
3746
3944
|
Path: dim(tildify(root)),
|
|
3747
3945
|
Skills: skills.map((s) => s.name).join(", ") || dim("none"),
|
|
3748
3946
|
"Local only": String(skills.filter((s) => teamState(s) === "local only").length),
|
|
@@ -3753,10 +3951,15 @@ function projects(args) {
|
|
|
3753
3951
|
}
|
|
3754
3952
|
function add(args) {
|
|
3755
3953
|
if (args.positional.length === 0) {
|
|
3756
|
-
throw new Error("Usage: skilllib add <skill-name>... [--force]");
|
|
3954
|
+
throw new Error("Usage: skilllib add <skill-name>... [--force] [--personal|--shared]");
|
|
3757
3955
|
}
|
|
3758
3956
|
const root = requireProject();
|
|
3759
|
-
|
|
3957
|
+
const changes = args.positional.map((name) => addSkill(root, name, { force: args.force, allowTracked: args.allowTracked, personal: args.personal }));
|
|
3958
|
+
printChanges(root, changes, args);
|
|
3959
|
+
const pins = readManifest(root).skills;
|
|
3960
|
+
const done = changes.filter((c) => c.action !== "skipped" && pins[c.name]);
|
|
3961
|
+
if (!args.json && done.some((c) => pins[c.name].personal))
|
|
3962
|
+
info(dim(`Pinned in your personal pins (${tildify(personalPinsDir())}) and kept out of git. --shared pins in skilllib.json for the team.`));
|
|
3760
3963
|
rememberProjects([root]);
|
|
3761
3964
|
}
|
|
3762
3965
|
function remove(args) {
|
|
@@ -3772,11 +3975,11 @@ function changeEach(args, change, nothing) {
|
|
|
3772
3975
|
const changes = change(root);
|
|
3773
3976
|
if (changes.some(incomplete)) process.exitCode = 1;
|
|
3774
3977
|
if (!args.json) {
|
|
3775
|
-
if (args.all) info(`${
|
|
3978
|
+
if (args.all) info(`${basename10(root)} ${dim(tildify(root))}`);
|
|
3776
3979
|
if (changes.length === 0) nothing(root);
|
|
3777
3980
|
else printChanges(root, changes, args);
|
|
3778
3981
|
}
|
|
3779
|
-
return { project:
|
|
3982
|
+
return { project: basename10(root), path: root, changes: withDirs(root, changes) };
|
|
3780
3983
|
});
|
|
3781
3984
|
if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
|
|
3782
3985
|
return roots;
|
|
@@ -3786,7 +3989,7 @@ function sync(args) {
|
|
|
3786
3989
|
const roots = changeEach(
|
|
3787
3990
|
args,
|
|
3788
3991
|
(root) => syncProject(root, { force: args.force }),
|
|
3789
|
-
(root) =>
|
|
3992
|
+
(root) => hasPins(root) ? success("Installed versions match what's pinned") : info(dim("No skilllib.json or personal pins: nothing to sync"))
|
|
3790
3993
|
);
|
|
3791
3994
|
if (!args.all) rememberProjects(roots);
|
|
3792
3995
|
}
|
|
@@ -3807,7 +4010,7 @@ function update(args) {
|
|
|
3807
4010
|
function outdated(args) {
|
|
3808
4011
|
const roots = projectsFor(args);
|
|
3809
4012
|
const behind = roots.flatMap(
|
|
3810
|
-
(root) => projectStatus(root).filter((s) => s.state.endsWith("update available") && s.latest !== null && s.version !== null).map((s) => ({ project:
|
|
4013
|
+
(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 }))
|
|
3811
4014
|
);
|
|
3812
4015
|
if (args.json) return json("outdated", behind);
|
|
3813
4016
|
const rows2 = behind.map((b) => ({ Project: b.project, Skill: b.skill, Installed: `v${b.installed}`, Latest: `v${b.latest}`, Status: colorState(b.state) }));
|
|
@@ -3842,7 +4045,7 @@ function importSkills(args) {
|
|
|
3842
4045
|
const dep = projectHere(root) === root ? readManifest(root).skills[name] : void 0;
|
|
3843
4046
|
if (dep && realpathOrNull(join15(root, dep.dir ?? PROJECT_SKILLS_DIR, name)) === realpathOrNull(dir) && dep.hash !== treeHash(dir)) {
|
|
3844
4047
|
const change = addSkill(root, name);
|
|
3845
|
-
if (change.action !== "skipped") info(dim(` ${
|
|
4048
|
+
if (change.action !== "skipped") info(dim(` ${basename10(root)} now depends on ${name} v${change.to}`));
|
|
3846
4049
|
}
|
|
3847
4050
|
}
|
|
3848
4051
|
if (args.json) json("import", results);
|
|
@@ -3858,14 +4061,14 @@ function scan(args) {
|
|
|
3858
4061
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
3859
4062
|
const skillNames = (root) => [
|
|
3860
4063
|
// Not a folder that links out of the repo: what's in it isn't the repo's (as in status).
|
|
3861
|
-
.../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.filter((d) => !notWritable(root, d)).flatMap((d) => skillDirsIn(join15(root, d))).map((d) =>
|
|
4064
|
+
.../* @__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)])
|
|
3862
4065
|
];
|
|
3863
4066
|
const withSkills = found.filter((root) => skillNames(root).length > 0);
|
|
3864
4067
|
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
|
|
3865
4068
|
`);
|
|
3866
4069
|
table(
|
|
3867
4070
|
withSkills.map((root) => ({
|
|
3868
|
-
Project:
|
|
4071
|
+
Project: basename10(root),
|
|
3869
4072
|
Path: dim(tildify(root)),
|
|
3870
4073
|
Skills: skillNames(root).map((n) => library.has(n) ? n : yellow(n)).join(", ")
|
|
3871
4074
|
}))
|
|
@@ -4035,7 +4238,7 @@ async function usage(args) {
|
|
|
4035
4238
|
return json("usage", {
|
|
4036
4239
|
usageDays: args.days,
|
|
4037
4240
|
source: "Claude Code transcripts",
|
|
4038
|
-
skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) =>
|
|
4241
|
+
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) })),
|
|
4039
4242
|
unusedLibrarySkills: unused
|
|
4040
4243
|
});
|
|
4041
4244
|
}
|
|
@@ -4049,7 +4252,7 @@ async function usage(args) {
|
|
|
4049
4252
|
summaries.map((s) => ({
|
|
4050
4253
|
Skill: s.skill,
|
|
4051
4254
|
Uses: String(s.uses),
|
|
4052
|
-
Projects: [...s.projects].map((p) =>
|
|
4255
|
+
Projects: [...s.projects].map((p) => basename10(p)).join(", ") || dim("-"),
|
|
4053
4256
|
"Last used": lastUsed(s),
|
|
4054
4257
|
Library: library.has(s.skill) ? green("yes") : dim("no")
|
|
4055
4258
|
}))
|
|
@@ -4073,7 +4276,7 @@ function show(args) {
|
|
|
4073
4276
|
path: dir,
|
|
4074
4277
|
latest: latestVersion(name)?.version ?? null,
|
|
4075
4278
|
origin: libraryOrigins()[name] ?? "",
|
|
4076
|
-
projects: projectsUsing(name).map((p) =>
|
|
4279
|
+
projects: projectsUsing(name).map((p) => basename10(p)),
|
|
4077
4280
|
skillMd: readFileSync7(join15(dir, "SKILL.md"), "utf-8")
|
|
4078
4281
|
});
|
|
4079
4282
|
}
|
|
@@ -4105,7 +4308,7 @@ function link(args) {
|
|
|
4105
4308
|
const roots = projectsFor(args);
|
|
4106
4309
|
for (const root of roots) {
|
|
4107
4310
|
const res = linkAll(root, { allowTracked: args.allowTracked });
|
|
4108
|
-
const label = args.all ? `${
|
|
4311
|
+
const label = args.all ? `${basename10(root)}: ` : "";
|
|
4109
4312
|
if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
|
|
4110
4313
|
else if (!args.all && !res.blocked.length && !res.uncommitted.length && !res.inTheWay.length) {
|
|
4111
4314
|
const missing = projectStatus(root).filter((s) => s.state === "folder missing").map((s) => s.name);
|
|
@@ -4151,7 +4354,7 @@ ${unreviewed} not reviewed. Keep one global on purpose: skilllib global keep <na
|
|
|
4151
4354
|
}
|
|
4152
4355
|
function repoRoot(repo) {
|
|
4153
4356
|
const looksLikePath = /[\\/]/.test(repo) || repo.startsWith(".") || repo.startsWith("~");
|
|
4154
|
-
const named = looksLikePath ? [] : visibleProjects().filter((p) =>
|
|
4357
|
+
const named = looksLikePath ? [] : visibleProjects().filter((p) => basename10(p) === repo);
|
|
4155
4358
|
if (named.length === 1) return named[0];
|
|
4156
4359
|
if (named.length > 1) throw new Error(`Several projects are named ${repo}: ${named.map(tildify).join(", ")}. Pass its path.`);
|
|
4157
4360
|
const path = expandHome(repo);
|
|
@@ -4177,17 +4380,18 @@ function moveGlobal(args, names, yours) {
|
|
|
4177
4380
|
moved,
|
|
4178
4381
|
...reason ? { reason } : {},
|
|
4179
4382
|
...left ? { left } : {},
|
|
4180
|
-
projects: changes.map(([root, c]) => ({ project:
|
|
4383
|
+
projects: changes.map(([root, c]) => ({ project: basename10(root), path: root, change: withDirs(root, [c])[0] }))
|
|
4181
4384
|
}))
|
|
4182
4385
|
);
|
|
4183
4386
|
}
|
|
4184
4387
|
for (const m of moves) {
|
|
4185
|
-
if (m.moved) success(`${m.name} now loads only in ${m.changes.map(([r]) =>
|
|
4388
|
+
if (m.moved) success(`${m.name} now loads only in ${m.changes.map(([r]) => basename10(r)).join(", ")} (the global copy is in skilllib restore)`);
|
|
4186
4389
|
else error(`${m.name}: ${m.reason}`);
|
|
4187
|
-
for (const [root, c] of m.changes) warnHeldLinks(root, c, `in ${
|
|
4390
|
+
for (const [root, c] of m.changes) warnHeldLinks(root, c, `in ${basename10(root)}`);
|
|
4188
4391
|
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"}`);
|
|
4189
4392
|
}
|
|
4190
|
-
|
|
4393
|
+
const toCommit = [...new Set(moves.filter((m) => m.moved).flatMap((m) => m.changes.map(([r]) => r)))].filter((r) => existsSync14(join15(r, MANIFEST_FILE)));
|
|
4394
|
+
if (toCommit.length) info(dim(`Commit skilllib.json and the skill folders in ${toCommit.map((r) => basename10(r)).join(", ")}.`));
|
|
4191
4395
|
}
|
|
4192
4396
|
function tidy(args) {
|
|
4193
4397
|
const names = args.positional;
|
|
@@ -4200,13 +4404,13 @@ function tidy(args) {
|
|
|
4200
4404
|
for (const name of names) {
|
|
4201
4405
|
const where = targets.map((root) => ({ root, ...foldersOf(root, name) })).filter((w) => w.all.length);
|
|
4202
4406
|
if (!where.length) {
|
|
4203
|
-
problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${
|
|
4407
|
+
problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${basename10(targets[0])}`}`);
|
|
4204
4408
|
continue;
|
|
4205
4409
|
}
|
|
4206
4410
|
if (!args.keep) continue;
|
|
4207
4411
|
for (const { root, real } of where) {
|
|
4208
4412
|
if (real.includes(keepFor(root)[name])) continue;
|
|
4209
|
-
const label = root ? `${
|
|
4413
|
+
const label = root ? `${basename10(root)} \xB7 ` : "";
|
|
4210
4414
|
problems.push(
|
|
4211
4415
|
`${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"}`
|
|
4212
4416
|
);
|
|
@@ -4216,7 +4420,7 @@ function tidy(args) {
|
|
|
4216
4420
|
throw new Error(problems.join("\n"));
|
|
4217
4421
|
}
|
|
4218
4422
|
const reports = targets.map(
|
|
4219
|
-
(root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label:
|
|
4423
|
+
(root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label: basename10(root), report: planProjectTidy(root, { keep: keepFor(root) }) }
|
|
4220
4424
|
);
|
|
4221
4425
|
let held = 0;
|
|
4222
4426
|
let changed = 0;
|