skilllib 1.11.3 → 1.11.4
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 +22 -19
- package/dist/{chunk-3XDWEHI4.js → chunk-CS6T66EE.js} +961 -355
- package/dist/index.js +6 -4
- package/dist/json.d.cts +20 -8
- package/dist/json.d.ts +20 -8
- package/dist/{tui-WSHQRBPF.js → tui-BDLJOMSC.js} +205 -128
- package/package.json +2 -2
- package/skills/skilllib/SKILL.md +9 -6
|
@@ -16,13 +16,13 @@ function realPath(path) {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
function skilllibHome() {
|
|
19
|
-
return process.env.SKILLLIB_HOME
|
|
19
|
+
return process.env.SKILLLIB_HOME?.trim() || join(userHome(), ".skilllib");
|
|
20
20
|
}
|
|
21
21
|
function libraryDir() {
|
|
22
22
|
return join(skilllibHome(), "library");
|
|
23
23
|
}
|
|
24
24
|
function claudeDir() {
|
|
25
|
-
return process.env.CLAUDE_CONFIG_DIR
|
|
25
|
+
return process.env.CLAUDE_CONFIG_DIR?.trim() || join(userHome(), ".claude");
|
|
26
26
|
}
|
|
27
27
|
function grokDir() {
|
|
28
28
|
return process.env.GROK_HOME?.trim() || join(userHome(), ".grok");
|
|
@@ -81,6 +81,7 @@ function writeState(path, data) {
|
|
|
81
81
|
throw err;
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
var BUILD_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "vendor", "target", "coverage"]);
|
|
84
85
|
|
|
85
86
|
// src/output.ts
|
|
86
87
|
var jsonMode = false;
|
|
@@ -103,8 +104,8 @@ var warn = (msg) => out(`${yellow("!")} ${msg}`);
|
|
|
103
104
|
var error = (msg) => console.error(`${red("\u2717")} ${msg}`);
|
|
104
105
|
function tildify(path) {
|
|
105
106
|
for (const home2 of /* @__PURE__ */ new Set([resolve2(userHome()), realPath(userHome())]))
|
|
106
|
-
if (path === home2 || path.startsWith(home2 + sep))
|
|
107
|
-
return path;
|
|
107
|
+
if (path === home2 || path.startsWith(home2 + sep)) path = "~" + path.slice(home2.length);
|
|
108
|
+
return path.replace(/\\/g, "/");
|
|
108
109
|
}
|
|
109
110
|
function truncate(s, max) {
|
|
110
111
|
const line = s.replace(/\s+/g, " ");
|
|
@@ -124,13 +125,8 @@ function localStamp(ms) {
|
|
|
124
125
|
return new Date(ms - at.getTimezoneOffset() * 6e4).toISOString().slice(0, 16).replace("T", " ");
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
// src/agentSkill.ts
|
|
128
|
-
import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync4, rmSync as rmSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
129
|
-
import { join as join9 } from "path";
|
|
130
|
-
|
|
131
128
|
// src/config.ts
|
|
132
129
|
import { existsSync as existsSync5, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
133
|
-
import { homedir as homedir2 } from "os";
|
|
134
130
|
import { join as join6, resolve as resolve4 } from "path";
|
|
135
131
|
|
|
136
132
|
// src/harnesses.ts
|
|
@@ -246,7 +242,7 @@ function readSkillInfo(dir) {
|
|
|
246
242
|
const skillMd = join3(dir, "SKILL.md");
|
|
247
243
|
const fallback = { name: basename(dir), description: "" };
|
|
248
244
|
if (!existsSync2(skillMd)) return fallback;
|
|
249
|
-
const match = readFileSync2(skillMd, "utf-8").match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
245
|
+
const match = readFileSync2(skillMd, "utf-8").replace(/^\uFEFF/, "").match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
250
246
|
if (!match) return fallback;
|
|
251
247
|
try {
|
|
252
248
|
const fm = parseYaml(match[1]);
|
|
@@ -255,21 +251,25 @@ function readSkillInfo(dir) {
|
|
|
255
251
|
description: typeof fm?.description === "string" ? fm.description.trim() : ""
|
|
256
252
|
};
|
|
257
253
|
} catch {
|
|
258
|
-
|
|
254
|
+
const line = (key) => match[1].match(new RegExp(`^${key}:[ ]*(.*)$`, "m"))?.[1]?.trim().replace(/^(["'])(.*)\1$/, "$2") ?? "";
|
|
255
|
+
return { name: line("name") || fallback.name, description: line("description") };
|
|
259
256
|
}
|
|
260
257
|
}
|
|
261
258
|
function isSkillDir(dir) {
|
|
262
259
|
return existsSync2(join3(dir, "SKILL.md"));
|
|
263
260
|
}
|
|
264
|
-
function listFiles(dir) {
|
|
261
|
+
function listFiles(dir, seen = /* @__PURE__ */ new Set()) {
|
|
262
|
+
const real = realpathSync2(dir);
|
|
263
|
+
if (seen.has(real)) return [];
|
|
264
|
+
seen.add(real);
|
|
265
265
|
return readdirSync(dir).filter((entry) => !IGNORED.has(entry)).flatMap((entry) => {
|
|
266
266
|
const full = join3(dir, entry);
|
|
267
267
|
const st = statSync2(full, { throwIfNoEntry: false });
|
|
268
|
-
return !st ? [] : st.isDirectory() ? listFiles(full) : [full];
|
|
268
|
+
return !st ? [] : st.isDirectory() ? listFiles(full, seen) : [full];
|
|
269
269
|
});
|
|
270
270
|
}
|
|
271
271
|
function treeHash(dir) {
|
|
272
|
-
if (!
|
|
272
|
+
if (!isDir(dir)) return null;
|
|
273
273
|
const hash = createHash("sha256");
|
|
274
274
|
const files = listFiles(dir).map((f) => ({ path: relative(dir, f).split(sep2).join("/"), full: f })).sort((a, b) => a.path.localeCompare(b.path, "en"));
|
|
275
275
|
for (const file of files) {
|
|
@@ -280,10 +280,18 @@ function treeHash(dir) {
|
|
|
280
280
|
function copySkill(from, to) {
|
|
281
281
|
const source = realpathSync2(from);
|
|
282
282
|
rmSync2(to, { recursive: true, force: true });
|
|
283
|
-
|
|
283
|
+
const loops = (src) => src !== source && ((r) => r === source || source.startsWith(r + sep2))(realpathSync2(src));
|
|
284
|
+
cpSync(source, to, { recursive: true, dereference: true, filter: (src) => !IGNORED.has(basename(src)) && existsSync2(src) && !loops(src) });
|
|
285
|
+
}
|
|
286
|
+
function isDir(path) {
|
|
287
|
+
try {
|
|
288
|
+
return statSync2(path, { throwIfNoEntry: false })?.isDirectory() === true;
|
|
289
|
+
} catch {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
284
292
|
}
|
|
285
293
|
function skillDirsIn(dir) {
|
|
286
|
-
if (!
|
|
294
|
+
if (!isDir(dir)) return [];
|
|
287
295
|
return readdirSync(dir, { withFileTypes: true }).filter((d) => (d.isDirectory() || d.isSymbolicLink()) && isSkillDir(join3(dir, d.name))).map((d) => join3(dir, d.name)).sort();
|
|
288
296
|
}
|
|
289
297
|
|
|
@@ -357,12 +365,17 @@ function readManifest(root) {
|
|
|
357
365
|
} catch {
|
|
358
366
|
throw new Error(`${path} isn't valid JSON (a merge conflict?): fix it, then run skilllib again`);
|
|
359
367
|
}
|
|
368
|
+
if (!data || typeof data !== "object" || Array.isArray(data) || data.skills !== void 0 && (typeof data.skills !== "object" || data.skills === null || Array.isArray(data.skills)))
|
|
369
|
+
throw new Error(`${path} isn't a skilllib.json (it should hold an object with "skills": { name: pin })`);
|
|
360
370
|
const skills = Object.fromEntries(
|
|
361
371
|
Object.entries(data.skills ?? {}).flatMap(([name, value]) => {
|
|
362
|
-
if (name.startsWith("@")) return [];
|
|
372
|
+
if (name.startsWith("@") || !name || name === "." || name === ".." || /[\\/]/.test(name)) return [];
|
|
363
373
|
if (typeof value === "string") return [[name, { version: versionForHash(name, value)?.version ?? 0, hash: value }]];
|
|
364
374
|
if (value && typeof value === "object" && "hash" in value) {
|
|
365
375
|
const dep = value;
|
|
376
|
+
const folders2 = [...typeof dep.dir === "string" ? [dep.dir] : [], ...Array.isArray(dep.links) ? dep.links.map(String) : []];
|
|
377
|
+
const bad = folders2.find((d) => !ALL_PROJECT_DIRS.includes(d));
|
|
378
|
+
if (bad !== void 0) throw new Error(`${path} isn't a valid skilllib.json: ${name}'s folder "${bad}" isn't a skill folder (${ALL_PROJECT_DIRS.join(", ")})`);
|
|
366
379
|
return [
|
|
367
380
|
[
|
|
368
381
|
name,
|
|
@@ -375,7 +388,7 @@ function readManifest(root) {
|
|
|
375
388
|
]
|
|
376
389
|
];
|
|
377
390
|
}
|
|
378
|
-
|
|
391
|
+
throw new Error(`${path} isn't a valid skilllib.json: ${name}'s pin has no hash`);
|
|
379
392
|
})
|
|
380
393
|
);
|
|
381
394
|
return { ...data, skills };
|
|
@@ -406,7 +419,9 @@ function onePerFolder(paths) {
|
|
|
406
419
|
return [...new Set(byReal.values())];
|
|
407
420
|
}
|
|
408
421
|
function isProjectCandidate(root) {
|
|
409
|
-
|
|
422
|
+
const real = realPath(root);
|
|
423
|
+
const own = [userHome(), skilllibHome(), claudeDir(), codexHome(), grokDir(), join5(userHome(), ".agents"), join5(userHome(), ".cursor")];
|
|
424
|
+
return !own.some((d) => realPath(d) === real);
|
|
410
425
|
}
|
|
411
426
|
function rememberProjects(roots) {
|
|
412
427
|
const incoming = roots.map((r) => resolve3(r)).filter((r) => isProjectCandidate(r) && !isAgentWorktree(r));
|
|
@@ -446,7 +461,7 @@ function writeConfig(config) {
|
|
|
446
461
|
writeState(configFile(), config);
|
|
447
462
|
}
|
|
448
463
|
function expandHome(path) {
|
|
449
|
-
return resolve4(path.replace(/^~(
|
|
464
|
+
return resolve4(path.replace(/^~(?=$|[\\/])/, userHome()));
|
|
450
465
|
}
|
|
451
466
|
function addRoot(path) {
|
|
452
467
|
const root = expandHome(path);
|
|
@@ -470,7 +485,7 @@ function setHidden(project, hidden) {
|
|
|
470
485
|
writeConfig({ ...config, hidden: hidden ? [...rest, project].sort() : rest });
|
|
471
486
|
return true;
|
|
472
487
|
}
|
|
473
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
488
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([...BUILD_DIRS, "Library", "Applications", "Pictures", "Music", "Movies"]);
|
|
474
489
|
function findProjects(dir, depth = 4) {
|
|
475
490
|
if (depth < 0 || !existsSync5(dir)) return [];
|
|
476
491
|
if (isProjectCandidate(dir)) {
|
|
@@ -522,10 +537,14 @@ function setKeepGlobal(names, keep) {
|
|
|
522
537
|
writeConfig({ ...readConfig(), keepGlobal: [...kept].sort() });
|
|
523
538
|
}
|
|
524
539
|
|
|
540
|
+
// src/agentSkill.ts
|
|
541
|
+
import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
542
|
+
import { join as join9 } from "path";
|
|
543
|
+
|
|
525
544
|
// src/library.ts
|
|
526
545
|
import { execFileSync } from "child_process";
|
|
527
|
-
import { cpSync as cpSync2, existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readdirSync as readdirSync4, renameSync as renameSync2, rmSync as rmSync3, symlinkSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
528
|
-
import { basename as basename3, dirname as dirname3, join as join8, relative as relative2, sep as sep4 } from "path";
|
|
546
|
+
import { cpSync as cpSync2, existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync4, readlinkSync as readlinkSync2, renameSync as renameSync2, rmSync as rmSync3, symlinkSync, unlinkSync, writeFileSync as writeFileSync2, realpathSync as realpathSync3 } from "fs";
|
|
547
|
+
import { basename as basename3, dirname as dirname3, isAbsolute, join as join8, posix, relative as relative2, resolve as resolve6, sep as sep4 } from "path";
|
|
529
548
|
|
|
530
549
|
// src/sources.ts
|
|
531
550
|
import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readlinkSync } from "fs";
|
|
@@ -568,7 +587,7 @@ function globalFolderSkills(enabled) {
|
|
|
568
587
|
const lock = skillsShLock();
|
|
569
588
|
const agentsDir = join7(userHome(), ".agents", "skills");
|
|
570
589
|
const entries = globalSkillDirs().flatMap((dir) => {
|
|
571
|
-
if (!
|
|
590
|
+
if (!isDir(dir)) return [];
|
|
572
591
|
return readdirSync3(dir).filter((name) => !name.startsWith(".") && name !== "synced").flatMap((name) => {
|
|
573
592
|
const path = join7(dir, name);
|
|
574
593
|
const isLink2 = lstatSync(path).isSymbolicLink();
|
|
@@ -596,7 +615,7 @@ function globalFolderSkills(enabled) {
|
|
|
596
615
|
skills.push({
|
|
597
616
|
name: e.name,
|
|
598
617
|
kind: locked || e.isLink && inAgents ? "skills.sh" : "global",
|
|
599
|
-
origin: locked ?? (e.isLink && inAgents ? "skills.sh (not in its lock file)" : e.isLink ? `link \u2192 ${target_}` : `${
|
|
618
|
+
origin: locked ?? (e.isLink && inAgents ? "skills.sh (not in its lock file)" : e.isLink ? `link \u2192 ${target_}` : `${tildify(e.dir)} (origin unknown)`),
|
|
600
619
|
path: e.path,
|
|
601
620
|
description: e.broken ? "" : readSkillInfo(e.path).description,
|
|
602
621
|
movable: true,
|
|
@@ -613,13 +632,14 @@ function globalFolderSkills(enabled) {
|
|
|
613
632
|
}
|
|
614
633
|
return skills;
|
|
615
634
|
}
|
|
616
|
-
function tildify2(p) {
|
|
617
|
-
return tildify(p).replace(/\\/g, "/");
|
|
618
|
-
}
|
|
619
635
|
function grokBuiltInSkills(enabled) {
|
|
620
636
|
if (!enabled.includes("grok")) return [];
|
|
621
637
|
return skillDirsIn(join7(grokDir(), "bundled", "skills")).map((path) => vendorSkill(path, "built-in", "Grok", ["grok"]));
|
|
622
638
|
}
|
|
639
|
+
function codexBuiltInSkills(enabled) {
|
|
640
|
+
if (!enabled.includes("codex")) return [];
|
|
641
|
+
return skillDirsIn(join7(codexHome(), "skills", ".system")).map((path) => vendorSkill(path, "built-in", "Codex", ["codex"]));
|
|
642
|
+
}
|
|
623
643
|
function cursorBuiltInSkills(enabled) {
|
|
624
644
|
if (!enabled.includes("cursor")) return [];
|
|
625
645
|
return skillDirsIn(join7(userHome(), ".cursor", "skills-cursor")).map((path) => vendorSkill(path, "built-in", "Cursor", ["cursor"]));
|
|
@@ -636,7 +656,7 @@ function claudeAiSkills(enabled) {
|
|
|
636
656
|
byReal.set(real, entry);
|
|
637
657
|
}
|
|
638
658
|
return [...byReal.values()].flatMap(
|
|
639
|
-
({ dir, readers }) => readdirSync3(dir).filter((d) => !d.startsWith(".")).flatMap(
|
|
659
|
+
({ dir, readers }) => (isDir(dir) ? readdirSync3(dir) : []).filter((d) => !d.startsWith(".")).flatMap(
|
|
640
660
|
(bucket) => skillDirsIn(join7(dir, bucket)).map((path) => vendorSkill(path, "claude.ai", "claude.ai account", enabled.filter((id) => readers.includes(id))))
|
|
641
661
|
)
|
|
642
662
|
);
|
|
@@ -646,6 +666,8 @@ function installedPluginsJson() {
|
|
|
646
666
|
}
|
|
647
667
|
function pluginDirCandidates(id, installPaths) {
|
|
648
668
|
const [name = "", marketplace = ""] = id.split("@");
|
|
669
|
+
const oneName = (s) => s !== "" && s !== "." && s !== ".." && !/[\\/]/.test(s);
|
|
670
|
+
if (!oneName(name) || !oneName(marketplace)) return installPaths;
|
|
649
671
|
const plugins = join7(claudeDir(), "plugins");
|
|
650
672
|
return [
|
|
651
673
|
...installPaths,
|
|
@@ -678,7 +700,7 @@ function pluginSkills(harnesses, root) {
|
|
|
678
700
|
return loadedBy.length ? skillDirsIn(join7(dir, "skills")).map((path) => ({ path, origin: id, loadedBy })) : [];
|
|
679
701
|
});
|
|
680
702
|
const syncedDir = join7(plugins, "synced");
|
|
681
|
-
const fromSynced = claude2 && existsSync6(syncedDir) ? readdirSync3(syncedDir).filter((b) => !b.startsWith(".")).flatMap(
|
|
703
|
+
const fromSynced = claude2 && existsSync6(syncedDir) ? readdirSync3(syncedDir).filter((b) => !b.startsWith(".") && isDir(join7(syncedDir, b))).flatMap(
|
|
682
704
|
(bucket) => readdirSync3(join7(syncedDir, bucket), { withFileTypes: true }).filter((d) => d.isDirectory()).flatMap((d) => {
|
|
683
705
|
const root2 = join7(syncedDir, bucket, d.name);
|
|
684
706
|
const name = readJson(join7(root2, ".claude-plugin", "plugin.json"))?.name ?? d.name;
|
|
@@ -695,6 +717,7 @@ function machineSkills(enabled = enabledHarnesses()) {
|
|
|
695
717
|
...claudeAiSkills(enabled),
|
|
696
718
|
...pluginSkills(enabled),
|
|
697
719
|
...cursorBuiltInSkills(enabled),
|
|
720
|
+
...codexBuiltInSkills(enabled),
|
|
698
721
|
...grokBuiltInSkills(enabled),
|
|
699
722
|
...systemSkills(enabled)
|
|
700
723
|
].sort(byKindAndName);
|
|
@@ -719,7 +742,9 @@ function recordOrigin(name, origin) {
|
|
|
719
742
|
function originFor(dir) {
|
|
720
743
|
const under = (path, folder) => path.startsWith((realpathOrNull(folder) ?? folder) + sep3);
|
|
721
744
|
const real = realpathOrNull(dir) ?? dir;
|
|
722
|
-
|
|
745
|
+
const temps = [tmpdir(), "/tmp", "/var/tmp"];
|
|
746
|
+
const homeInTemp = temps.some((t) => under(realpathOrNull(userHome()) ?? userHome(), t));
|
|
747
|
+
if (temps.some((t) => under(real, t)) && !(homeInTemp && under(real, userHome()))) return "";
|
|
723
748
|
const match = machineSkills().find((s) => resolve5(s.path) === resolve5(dir));
|
|
724
749
|
if (match) return match.kind === "global" ? "global folder" : `${match.kind}: ${match.origin}`;
|
|
725
750
|
const project = dir.split(/[\\/]\.(?:claude|agents|grok|cursor|codex)[\\/]skills[\\/]/)[0];
|
|
@@ -746,6 +771,62 @@ function isLink(path) {
|
|
|
746
771
|
return false;
|
|
747
772
|
}
|
|
748
773
|
}
|
|
774
|
+
function notWritable(root, dir) {
|
|
775
|
+
const top = realpathOrNull(root) ?? root;
|
|
776
|
+
let p = root;
|
|
777
|
+
for (const part of dir.split("/")) {
|
|
778
|
+
p = join8(p, part);
|
|
779
|
+
if (!entryExists(p)) return null;
|
|
780
|
+
const real = realpathOrNull(p);
|
|
781
|
+
if (!isDir(p) || real === null || real !== top && !real.startsWith(top + sep4)) return relative2(root, p).split(sep4).join("/");
|
|
782
|
+
}
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
var linksOut = (root, path) => isLink(join8(root, path)) && isDir(join8(root, path));
|
|
786
|
+
function blockerSaid(root, path) {
|
|
787
|
+
const full = join8(root, path);
|
|
788
|
+
if (!isLink(full)) return `${path} is a file, not a folder`;
|
|
789
|
+
const real = realpathOrNull(full);
|
|
790
|
+
return real === null ? `${path} is a link to nothing` : linksOut(root, path) ? `${path} links out of the repo (to ${tildify(real)})` : `${path} links to a file`;
|
|
791
|
+
}
|
|
792
|
+
function inTheWaySaid(root, paths) {
|
|
793
|
+
return paths.map((p) => ALL_PROJECT_DIRS.includes(posix.dirname(p)) ? `${p} is taken (another copy, or a file)` : blockerSaid(root, p)).join("; ");
|
|
794
|
+
}
|
|
795
|
+
var writableDir = (root, dir) => notWritable(root, dir) === null;
|
|
796
|
+
function reposChoice(root, dir) {
|
|
797
|
+
if (writableDir(root, dir)) return true;
|
|
798
|
+
const real = isDir(join8(root, dir)) ? realNative(join8(root, dir)) : null;
|
|
799
|
+
return real !== null && !globalSkillDirs().some((g) => realNative(g) === real);
|
|
800
|
+
}
|
|
801
|
+
var firstStuck = (root, dirs) => dirs.map((d) => notWritable(root, d)).find((p) => p !== null) ?? null;
|
|
802
|
+
var fileAt = (path) => entryExists(path) && !isLink(path) && !isDir(path);
|
|
803
|
+
function syncDone(root, name, dep) {
|
|
804
|
+
const copy = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
|
|
805
|
+
return treeHash(copy) === dep.hash && (dep.links ?? []).every((d) => realpathOrNull(join8(root, d, name)) === realpathOrNull(copy));
|
|
806
|
+
}
|
|
807
|
+
function slotAt(link2, to) {
|
|
808
|
+
if (isLink(link2) && !isDir(link2)) return "free";
|
|
809
|
+
if (!entryExists(link2)) return "free";
|
|
810
|
+
return realpathOrNull(link2) === realpathOrNull(to) ? "ok" : "taken";
|
|
811
|
+
}
|
|
812
|
+
function syncBlocker(root, name, dep = readManifest(root).skills[name]) {
|
|
813
|
+
if (!dep) return null;
|
|
814
|
+
const dir = dep.dir ?? PROJECT_SKILLS_DIR;
|
|
815
|
+
const stuck = firstStuck(root, [dir, ...dep.links ?? []]);
|
|
816
|
+
if (stuck) return { said: blockerSaid(root, stuck), outside: linksOut(root, stuck) };
|
|
817
|
+
if (syncDone(root, name, dep)) return null;
|
|
818
|
+
const copy = join8(root, dir, name);
|
|
819
|
+
if (fileAt(copy)) return { said: `${dir}/${name} is a file, not a skill folder`, outside: false };
|
|
820
|
+
for (const d of dep.links ?? []) {
|
|
821
|
+
const link2 = join8(root, d, name);
|
|
822
|
+
if (!entryExists(link2)) continue;
|
|
823
|
+
if (!isLink(link2)) return { said: `${d}/${name} is in the way of its link`, outside: false };
|
|
824
|
+
if (!isDir(link2) || realpathOrNull(link2) === realpathOrNull(copy)) continue;
|
|
825
|
+
const there = treeHash(link2);
|
|
826
|
+
if (there !== dep.hash && versionForHash(name, there ?? "") === null) return { said: `${d}/${name} links to a different copy`, outside: false };
|
|
827
|
+
}
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
749
830
|
function linkDir(target, link2) {
|
|
750
831
|
if (process.platform === "win32") symlinkSync(target, link2, "junction");
|
|
751
832
|
else symlinkSync(relative2(dirname3(link2), target), link2);
|
|
@@ -759,11 +840,160 @@ function visibilityOf(root, name, enabled = enabledHarnesses()) {
|
|
|
759
840
|
paths: harness(id).projectDirs.filter((dir) => existsSync7(join8(root, dir, name, "SKILL.md"))).length
|
|
760
841
|
}));
|
|
761
842
|
}
|
|
843
|
+
function realNative(p) {
|
|
844
|
+
try {
|
|
845
|
+
return realpathSync3.native(p);
|
|
846
|
+
} catch {
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
function survives(path, gone) {
|
|
851
|
+
return lostAt(path, gone) === null;
|
|
852
|
+
}
|
|
853
|
+
function lostAt(path, gone) {
|
|
854
|
+
const key = (p) => process.platform === "win32" ? p.toLowerCase() : p;
|
|
855
|
+
const canon = (p) => {
|
|
856
|
+
const dir = realNative(dirname3(p)) ?? dirname3(p);
|
|
857
|
+
const leaf = basename3(p);
|
|
858
|
+
try {
|
|
859
|
+
lstatSync2(join8(dir, leaf));
|
|
860
|
+
const names = readdirSync4(dir);
|
|
861
|
+
return join8(dir, names.includes(leaf) ? leaf : names.find((n) => n.toLowerCase() === leaf.toLowerCase()) ?? leaf);
|
|
862
|
+
} catch {
|
|
863
|
+
return join8(dir, leaf);
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
const moved = gone.map((g) => key(canon(g)));
|
|
867
|
+
let at = path;
|
|
868
|
+
let last = null;
|
|
869
|
+
for (let hop = 0; hop < 40; hop++) {
|
|
870
|
+
const here = canon(at);
|
|
871
|
+
if (moved.some((m) => key(here) === m || key(here).startsWith(m + sep4))) return last ?? here;
|
|
872
|
+
if (!isLink(here)) return existsSync7(join8(here, "SKILL.md")) ? null : here;
|
|
873
|
+
last = here;
|
|
874
|
+
at = resolve6(dirname3(here), readlinkSync2(here).replace(/^\\\\\?\\(UNC\\)?/, (_m, unc) => unc ? "\\\\" : ""));
|
|
875
|
+
}
|
|
876
|
+
return last;
|
|
877
|
+
}
|
|
878
|
+
function blindWithoutGlobal(name, copies, roots, nestedOf = (root) => nestedEntries(root), packagesOf = (root) => packageDirs(root)) {
|
|
879
|
+
const gone = copies.flatMap((c) => [c.path, ...c.links ?? []]);
|
|
880
|
+
const readers = new Set(copies.flatMap((c) => c.harnesses));
|
|
881
|
+
const enabled = enabledHarnesses();
|
|
882
|
+
const holds = (root) => ALL_PROJECT_DIRS.some((d) => reposChoice(root, d) && entryExists(join8(root, d, name))) || pinsSkill(root, name);
|
|
883
|
+
const keepsTop = (root, id) => harness(id).projectDirs.some((d) => existsSync7(join8(root, d, name, "SKILL.md")) && survives(join8(root, d, name), gone));
|
|
884
|
+
const shown = (root, p) => {
|
|
885
|
+
const under = (dir) => ((d) => p.startsWith(d + sep4) ? relative2(d, p).split(sep4).join("/") : null)(realNative(dir) ?? dir);
|
|
886
|
+
return under(root) ?? ((h) => h === null ? p : `~/${h}`)(under(userHome()));
|
|
887
|
+
};
|
|
888
|
+
const blindIn = (root) => enabled.flatMap((id) => {
|
|
889
|
+
const reach = harness(id).projectDirs.some((d) => existsSync7(join8(root, d, name, "SKILL.md")));
|
|
890
|
+
if (keepsTop(root, id) || !readers.has(id) && !reach) return [];
|
|
891
|
+
const folder = harness(id).projectDirs.map((d) => reposChoice(root, d) ? null : notWritable(root, d)).find((f) => f !== null) ?? null;
|
|
892
|
+
const shared = harness(id).projectDirs.find((d) => !writableDir(root, d) && reposChoice(root, d));
|
|
893
|
+
const at = shared && existsSync7(join8(root, shared, name, "SKILL.md")) ? lostAt(join8(root, shared, name), gone) : null;
|
|
894
|
+
if (shared && !at) return [{ root, id, folder, shared: { dir: shared, to: shown(root, realNative(join8(root, shared)) ?? join8(root, shared)) } }];
|
|
895
|
+
const own = at ? null : harness(id).projectDirs.find((d) => writableDir(root, d) && isLink(join8(root, d, name)) && existsSync7(join8(root, d, name, "SKILL.md")));
|
|
896
|
+
const ownAt = own ? lostAt(join8(root, own, name), gone) : null;
|
|
897
|
+
return [{ root, id, folder, ...at ? { link: shown(root, at) } : ownAt ? { link: shown(root, ownAt) } : {} }];
|
|
898
|
+
});
|
|
899
|
+
const nestedBlind = (root) => nestedOf(root).filter((n) => n.name === name && !survives(n.path, gone)).flatMap(
|
|
900
|
+
(n) => n.readers.filter((id) => !keepsTop(root, id)).map((id) => ((pkg) => ({ root: pkg, id, folder: null, link: shown(pkg, lostAt(n.path, gone) ?? n.path) }))(join8(root, n.location, "..", "..")))
|
|
901
|
+
);
|
|
902
|
+
const unique = [...new Map(roots.map((r) => [realpathOrNull(r) ?? r, r])).values()];
|
|
903
|
+
const key = (p) => realpathOrNull(p) ?? p;
|
|
904
|
+
const asked = new Set(unique.map(key));
|
|
905
|
+
const pinnedPackages = (root) => packagesOf(root).filter((sub) => existsSync7(join8(sub, MANIFEST_FILE)) && pinsSkill(sub, name) && !asked.has(key(sub))).flatMap((sub) => blindIn(sub).filter((b) => !keepsTop(root, b.id)));
|
|
906
|
+
const all = unique.flatMap((root) => [...holds(root) ? blindIn(root) : [], ...nestedBlind(root), ...pinnedPackages(root)]);
|
|
907
|
+
const byWho = /* @__PURE__ */ new Map();
|
|
908
|
+
for (const b of all) {
|
|
909
|
+
const k = `${key(b.root)}\0${b.id}`;
|
|
910
|
+
const had = byWho.get(k);
|
|
911
|
+
if (!had || !(had.link || had.folder || had.shared) && (b.link || b.folder || b.shared)) byWho.set(k, b);
|
|
912
|
+
}
|
|
913
|
+
const within = (d) => unique.some((r) => d === r || d.startsWith(r + sep4));
|
|
914
|
+
const home2 = realNative(userHome()) ?? userHome();
|
|
915
|
+
const belowHome = (d) => (realNative(d) ?? d) !== home2 && !home2.startsWith((realNative(d) ?? d) + sep4);
|
|
916
|
+
const gitTop = (from) => {
|
|
917
|
+
for (let d = from; d !== dirname3(d) && belowHome(d); d = dirname3(d)) if (existsSync7(join8(d, ".git"))) return d;
|
|
918
|
+
return null;
|
|
919
|
+
};
|
|
920
|
+
const servedAbove = (b) => {
|
|
921
|
+
const top = gitTop(b.root);
|
|
922
|
+
const inside = (d) => belowHome(d) && (top ? d === top || d.startsWith(top + sep4) : within(d));
|
|
923
|
+
for (let d = dirname3(b.root); inside(d) && d !== dirname3(d); d = dirname3(d)) if (keepsTop(d, b.id)) return true;
|
|
924
|
+
return false;
|
|
925
|
+
};
|
|
926
|
+
return [...byWho.values()].filter((b) => !servedAbove(b));
|
|
927
|
+
}
|
|
928
|
+
function strandedLinks(name, copies, roots, label = basename3) {
|
|
929
|
+
const gone = copies.flatMap((c) => [c.path, ...c.links ?? []]);
|
|
930
|
+
const unique = [...new Map(roots.map((r) => [realpathOrNull(r) ?? r, r])).values()];
|
|
931
|
+
const seen = /* @__PURE__ */ new Set();
|
|
932
|
+
const once = (p) => ((k) => seen.has(k) ? false : (seen.add(k), true))(join8(realNative(dirname3(p)) ?? dirname3(p), basename3(p)));
|
|
933
|
+
return unique.flatMap((root) => [
|
|
934
|
+
...ALL_PROJECT_DIRS.filter(
|
|
935
|
+
(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
|
+
).map(
|
|
937
|
+
(d) => `${d}/${name} in ${label(root)}`
|
|
938
|
+
),
|
|
939
|
+
...nestedEntries(root).filter((n) => n.name === name && isLink(n.path) && !survives(n.path, gone) && once(n.path)).map((n) => `${relative2(root, n.path).split(sep4).join("/")} in ${label(root)}`)
|
|
940
|
+
]);
|
|
941
|
+
}
|
|
942
|
+
function copiesDiffer(paths) {
|
|
943
|
+
return new Set(paths.map((p) => treeHash(p)).filter((h) => h !== null)).size > 1;
|
|
944
|
+
}
|
|
945
|
+
function blindSaid(name, blind, label = basename3) {
|
|
946
|
+
const who = blind.map((b) => `${harness(b.id).name} in ${label(b.root)}`).join(", ");
|
|
947
|
+
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("~") || isAbsolute(b.link) ? b.link : `${b.link} in ${label(b.root)}`] : []))];
|
|
949
|
+
const sharedBy = /* @__PURE__ */ new Map();
|
|
950
|
+
for (const b of blind.filter((x) => x.shared)) {
|
|
951
|
+
if (!sharedBy.has(b.shared.to)) sharedBy.set(b.shared.to, { to: b.shared.to, where: /* @__PURE__ */ new Set(), dirs: /* @__PURE__ */ new Set() });
|
|
952
|
+
sharedBy.get(b.shared.to).where.add(`${b.shared.dir} in ${label(b.root)}`);
|
|
953
|
+
sharedBy.get(b.shared.to).dirs.add(b.shared.dir);
|
|
954
|
+
}
|
|
955
|
+
const shared = [...sharedBy.values()].map(
|
|
956
|
+
(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
|
+
);
|
|
958
|
+
const repos = blind.filter((b) => !b.folder && !b.link && !b.shared);
|
|
959
|
+
const fixes = [
|
|
960
|
+
...folders2.length ? [`point ${folders2.join(", ")} back into the repo`] : [],
|
|
961
|
+
// A package's link to the global copy: add or link at the repo's top doesn't touch it.
|
|
962
|
+
...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
|
+
...shared,
|
|
964
|
+
// Named, so with other advice beside it, it's clear which repo (or package) this is about.
|
|
965
|
+
...repos.length ? ((names) => [`give ${names.join(", ")} ${names.length === 1 ? "a copy of its own" : "their own copies"} (skilllib add, sync or link there)`])([...new Set(repos.map((b) => label(b.root)))]) : []
|
|
966
|
+
];
|
|
967
|
+
return `${who} only ${blind.length === 1 ? "sees" : "see"} the global copy: ${fixes.join("; ")}, or keep ${name} global (skilllib global keep)`;
|
|
968
|
+
}
|
|
762
969
|
function dependencyDirs(root, name, dep) {
|
|
763
970
|
const untracked = ALL_PROJECT_DIRS.find((d) => d !== GROK_SKILLS_DIR && existsSync7(join8(root, d, name, "SKILL.md")) && !isLink(join8(root, d, name)));
|
|
764
971
|
const primary = dep ? dep.dir ?? PROJECT_SKILLS_DIR : untracked ?? installDirs(enabledHarnesses())[0];
|
|
765
972
|
const wanted = installDirs(enabledHarnesses(), primary);
|
|
766
|
-
|
|
973
|
+
const real = (d) => realpathOrNull(join8(root, d));
|
|
974
|
+
const same = (d) => d === primary || real(d) !== null && real(d) === real(primary);
|
|
975
|
+
return [primary, .../* @__PURE__ */ new Set([...dep?.links ?? [], ...wanted.filter((d) => !same(d))])];
|
|
976
|
+
}
|
|
977
|
+
function trackBlocker(root, name) {
|
|
978
|
+
let recorded;
|
|
979
|
+
try {
|
|
980
|
+
recorded = readManifest(root).skills[name];
|
|
981
|
+
} catch {
|
|
982
|
+
return `${MANIFEST_FILE} can't be read`;
|
|
983
|
+
}
|
|
984
|
+
const stuck = firstStuck(root, dependencyDirs(root, name, recorded));
|
|
985
|
+
return stuck ? blockerSaid(root, stuck) : null;
|
|
986
|
+
}
|
|
987
|
+
function pinsSkill(root, name) {
|
|
988
|
+
try {
|
|
989
|
+
return name in readManifest(root).skills;
|
|
990
|
+
} catch {
|
|
991
|
+
try {
|
|
992
|
+
return readFileSync4(join8(root, MANIFEST_FILE), "utf-8").includes(JSON.stringify(name));
|
|
993
|
+
} catch {
|
|
994
|
+
return false;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
767
997
|
}
|
|
768
998
|
function isGitTracked(root, dir) {
|
|
769
999
|
try {
|
|
@@ -779,16 +1009,18 @@ function projectStatus(root) {
|
|
|
779
1009
|
const managed = Object.entries(skills).map(([name, dep]) => {
|
|
780
1010
|
const location = dep.dir ?? PROJECT_SKILLS_DIR;
|
|
781
1011
|
const path = join8(root, location, name);
|
|
782
|
-
const local = treeHash(path);
|
|
1012
|
+
const local = notWritable(root, location) ? null : treeHash(path);
|
|
783
1013
|
const latest = latestVersion(name);
|
|
784
|
-
const
|
|
785
|
-
const
|
|
786
|
-
const
|
|
1014
|
+
const known = local !== null && local !== dep.hash && versionForHash(name, local) !== null;
|
|
1015
|
+
const edited = local !== dep.hash && !known;
|
|
1016
|
+
const pinned = versionForHash(name, dep.hash)?.version ?? dep.version;
|
|
1017
|
+
const behind = latest !== null && latest.hash !== dep.hash && latest.version > pinned;
|
|
1018
|
+
const state = local === null ? "folder missing" : latest === null ? "not in library" : local === latest.hash && !known ? "ok" : known ? "out of sync" : edited && behind ? "edited locally, update available" : edited ? "edited locally" : behind ? "update available" : "ok";
|
|
787
1019
|
return {
|
|
788
1020
|
name,
|
|
789
1021
|
managed: true,
|
|
790
1022
|
state,
|
|
791
|
-
version:
|
|
1023
|
+
version: pinned || null,
|
|
792
1024
|
latest: latest?.version ?? null,
|
|
793
1025
|
location,
|
|
794
1026
|
path,
|
|
@@ -799,7 +1031,7 @@ function projectStatus(root) {
|
|
|
799
1031
|
const lock = projectSkillsLock(root);
|
|
800
1032
|
const unmanaged = [];
|
|
801
1033
|
const byName = /* @__PURE__ */ new Map();
|
|
802
|
-
for (const dir of ALL_PROJECT_DIRS) {
|
|
1034
|
+
for (const dir of ALL_PROJECT_DIRS.filter((d) => writableDir(root, d))) {
|
|
803
1035
|
for (const path of skillDirsIn(join8(root, dir))) {
|
|
804
1036
|
const name = basename3(path);
|
|
805
1037
|
if (seen.has(name)) continue;
|
|
@@ -840,40 +1072,57 @@ var NESTED_READERS = [
|
|
|
840
1072
|
// Cursor reads nested .grok/skills too, but skilllib doesn't count it: that folder stays Grok's.
|
|
841
1073
|
[".grok/skills", ["grok"]]
|
|
842
1074
|
];
|
|
843
|
-
|
|
844
|
-
|
|
1075
|
+
function nestedSkills(root, { depth = PACKAGE_DEPTH, enabled = enabledHarnesses() } = {}) {
|
|
1076
|
+
return nestedEntries(root, { depth, enabled }).map((n) => ({
|
|
1077
|
+
name: n.name,
|
|
1078
|
+
managed: false,
|
|
1079
|
+
state: unmanagedState(n.name, n.path, isRepoSkillLocation(n.skillsDir), void 0),
|
|
1080
|
+
version: null,
|
|
1081
|
+
latest: latestVersion(n.name)?.version ?? null,
|
|
1082
|
+
location: n.location,
|
|
1083
|
+
path: n.path,
|
|
1084
|
+
visibility: enabled.map((id) => ({ id, paths: n.readers.includes(id) ? 1 : 0 }))
|
|
1085
|
+
}));
|
|
1086
|
+
}
|
|
1087
|
+
function nestedEntries(root, { depth = PACKAGE_DEPTH, enabled = enabledHarnesses(), subs } = {}) {
|
|
1088
|
+
const top = realpathOrNull(root) ?? root;
|
|
1089
|
+
const found = packageSkillFolders(root, subs ?? packageDirs(root, depth)).flatMap(
|
|
1090
|
+
({ sub, skillsDir, location }) => skillDirsIn(join8(sub, skillsDir)).flatMap((path) => {
|
|
1091
|
+
if (isLink(path) && (realpathOrNull(path) ?? "").startsWith(top + sep4)) return [];
|
|
1092
|
+
const name = basename3(path);
|
|
1093
|
+
const readers = NESTED_READERS.filter(([d]) => realpathOrNull(join8(sub, d, name)) === realpathOrNull(path)).flatMap(([, r]) => r);
|
|
1094
|
+
return [{ name, path, location, skillsDir, readers: enabled.filter((id) => readers.includes(id)) }];
|
|
1095
|
+
})
|
|
1096
|
+
);
|
|
1097
|
+
return found.sort((a, b) => a.location.localeCompare(b.location) || a.name.localeCompare(b.name));
|
|
1098
|
+
}
|
|
1099
|
+
var PACKAGE_DEPTH = 5;
|
|
1100
|
+
function packageDirs(root, depth = PACKAGE_DEPTH) {
|
|
845
1101
|
const found = [];
|
|
846
1102
|
const walk = (dir, left) => {
|
|
847
1103
|
let subs = [];
|
|
848
1104
|
try {
|
|
849
|
-
subs = readdirSync4(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && !
|
|
1105
|
+
subs = readdirSync4(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && !BUILD_DIRS.has(d.name)).map((d) => join8(dir, d.name));
|
|
850
1106
|
} catch {
|
|
851
1107
|
return;
|
|
852
1108
|
}
|
|
853
1109
|
for (const sub of subs) {
|
|
854
1110
|
if (existsSync7(join8(sub, ".git"))) continue;
|
|
855
|
-
|
|
856
|
-
const location = relative2(root, join8(sub, skillsDir)).split(sep4).join("/");
|
|
857
|
-
for (const path of skillDirsIn(join8(sub, skillsDir))) {
|
|
858
|
-
if (isLink(path) && (realpathOrNull(path) ?? "").startsWith((realpathOrNull(root) ?? root) + sep4)) continue;
|
|
859
|
-
const name = basename3(path);
|
|
860
|
-
found.push({
|
|
861
|
-
name,
|
|
862
|
-
managed: false,
|
|
863
|
-
state: unmanagedState(name, path, isRepoSkillLocation(skillsDir), void 0),
|
|
864
|
-
version: null,
|
|
865
|
-
latest: latestVersion(name)?.version ?? null,
|
|
866
|
-
location,
|
|
867
|
-
path,
|
|
868
|
-
visibility: enabled.map((id) => ({ id, paths: readers.includes(id) ? 1 : 0 }))
|
|
869
|
-
});
|
|
870
|
-
}
|
|
871
|
-
}
|
|
1111
|
+
found.push(sub);
|
|
872
1112
|
if (left > 1) walk(sub, left - 1);
|
|
873
1113
|
}
|
|
874
1114
|
};
|
|
875
1115
|
walk(root, depth);
|
|
876
|
-
return found
|
|
1116
|
+
return found;
|
|
1117
|
+
}
|
|
1118
|
+
function packageSkillFolders(root, subs = packageDirs(root)) {
|
|
1119
|
+
return subs.flatMap(
|
|
1120
|
+
(sub) => NESTED_READERS.flatMap(([skillsDir]) => {
|
|
1121
|
+
if (!existsSync7(join8(sub, skillsDir))) return [];
|
|
1122
|
+
const location = relative2(root, join8(sub, skillsDir)).split(sep4).join("/");
|
|
1123
|
+
return reposChoice(root, location) ? [{ sub, skillsDir, location }] : [];
|
|
1124
|
+
})
|
|
1125
|
+
);
|
|
877
1126
|
}
|
|
878
1127
|
function sharedByGit(root, location, name) {
|
|
879
1128
|
const copy = `${location}/${name}`;
|
|
@@ -888,13 +1137,24 @@ function sharedByGit(root, location, name) {
|
|
|
888
1137
|
}
|
|
889
1138
|
function linkEverywhere(root, name, location, { allowTracked = false, dryRun = false } = {}) {
|
|
890
1139
|
const enabled = enabledHarnesses();
|
|
891
|
-
const result = { created: [], blocked: [], uncommitted: [] };
|
|
1140
|
+
const result = { created: [], blocked: [], uncommitted: [], inTheWay: [] };
|
|
892
1141
|
let shared;
|
|
1142
|
+
const stuck = (path) => void (result.inTheWay.includes(path) || result.inTheWay.push(path));
|
|
893
1143
|
for (const id of enabled) {
|
|
894
1144
|
if (visibilityOf(root, name, [id])[0].paths > 0) continue;
|
|
895
1145
|
const dir = harness(id).projectDirs[0];
|
|
896
1146
|
const link2 = join8(root, dir, name);
|
|
897
|
-
if (
|
|
1147
|
+
if (Object.values(result).some((dirs) => dirs.includes(dir))) continue;
|
|
1148
|
+
const blocker = notWritable(root, dir);
|
|
1149
|
+
if (blocker) {
|
|
1150
|
+
stuck(blocker);
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
const broken = isLink(link2) && !isDir(link2);
|
|
1154
|
+
if (entryExists(link2) && !broken) {
|
|
1155
|
+
stuck(`${dir}/${name}`);
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
898
1158
|
if (isGitTracked(root, dir)) {
|
|
899
1159
|
if (!(shared ??= sharedByGit(root, location, name))) {
|
|
900
1160
|
if (!result.uncommitted.includes(dir)) result.uncommitted.push(dir);
|
|
@@ -906,6 +1166,7 @@ function linkEverywhere(root, name, location, { allowTracked = false, dryRun = f
|
|
|
906
1166
|
}
|
|
907
1167
|
}
|
|
908
1168
|
if (!dryRun) {
|
|
1169
|
+
if (broken) unlinkSync(link2);
|
|
909
1170
|
mkdirSync4(join8(root, dir), { recursive: true });
|
|
910
1171
|
linkDir(join8(root, location, name), link2);
|
|
911
1172
|
}
|
|
@@ -913,15 +1174,10 @@ function linkEverywhere(root, name, location, { allowTracked = false, dryRun = f
|
|
|
913
1174
|
}
|
|
914
1175
|
return result;
|
|
915
1176
|
}
|
|
916
|
-
function unlinkEverywhere(root, name
|
|
917
|
-
|
|
918
|
-
return ALL_PROJECT_DIRS.filter((dir) => {
|
|
1177
|
+
function unlinkEverywhere(root, name) {
|
|
1178
|
+
return ALL_PROJECT_DIRS.filter((dir) => writableDir(root, dir)).filter((dir) => {
|
|
919
1179
|
const link2 = join8(root, dir, name);
|
|
920
|
-
if (!isLink(link2)) return false;
|
|
921
|
-
if (dir === GROK_SKILLS_DIR) {
|
|
922
|
-
const target = realpathOrNull(link2);
|
|
923
|
-
if (target !== null && target !== gone) return false;
|
|
924
|
-
}
|
|
1180
|
+
if (!isLink(link2) || realpathOrNull(link2) !== null) return false;
|
|
925
1181
|
unlinkSync(link2);
|
|
926
1182
|
return true;
|
|
927
1183
|
});
|
|
@@ -929,7 +1185,7 @@ function unlinkEverywhere(root, name, removing) {
|
|
|
929
1185
|
function relinkDependency(root, name, { allowTracked = false } = {}) {
|
|
930
1186
|
const manifest = readManifest(root);
|
|
931
1187
|
const dep = manifest.skills[name];
|
|
932
|
-
if (!dep) return { created: [], blocked: [], uncommitted: [] };
|
|
1188
|
+
if (!dep) return { created: [], blocked: [], uncommitted: [], inTheWay: [] };
|
|
933
1189
|
const result = linkEverywhere(root, name, dep.dir ?? PROJECT_SKILLS_DIR, { allowTracked });
|
|
934
1190
|
if (result.created.length) {
|
|
935
1191
|
manifest.skills[name] = { ...dep, links: [.../* @__PURE__ */ new Set([...dep.links ?? [], ...result.created])] };
|
|
@@ -937,39 +1193,57 @@ function relinkDependency(root, name, { allowTracked = false } = {}) {
|
|
|
937
1193
|
}
|
|
938
1194
|
return result;
|
|
939
1195
|
}
|
|
940
|
-
function linkAll(root, { allowTracked = false } = {}) {
|
|
1196
|
+
function linkAll(root, { allowTracked = false, names } = {}) {
|
|
941
1197
|
const linked = [];
|
|
942
1198
|
const blocked = /* @__PURE__ */ new Set();
|
|
943
1199
|
const uncommitted = [];
|
|
1200
|
+
const inTheWay = /* @__PURE__ */ new Set();
|
|
944
1201
|
for (const skill of projectStatus(root)) {
|
|
945
|
-
if (!skill.visibility.some((v) => v.paths === 0) || skill.state === "folder missing") continue;
|
|
1202
|
+
if (names && !names.includes(skill.name) || !skill.visibility.some((v) => v.paths === 0) || skill.state === "folder missing") continue;
|
|
946
1203
|
const res = skill.managed ? relinkDependency(root, skill.name, { allowTracked }) : linkEverywhere(root, skill.name, skill.location, { allowTracked });
|
|
947
1204
|
if (res.created.length) linked.push({ name: skill.name, into: res.created });
|
|
948
1205
|
for (const dir of res.blocked) blocked.add(dir);
|
|
949
1206
|
if (res.uncommitted.length) uncommitted.push(skill.name);
|
|
1207
|
+
for (const path of res.inTheWay) inTheWay.add(path);
|
|
950
1208
|
}
|
|
951
|
-
return { linked, blocked: [...blocked], uncommitted };
|
|
1209
|
+
return { linked, blocked: [...blocked], uncommitted, inTheWay: [...inTheWay] };
|
|
952
1210
|
}
|
|
953
|
-
function
|
|
954
|
-
const target = version === void 0 ? latestVersion(name) : getVersion(name, version);
|
|
955
|
-
if (!target) return { name, action: "skipped", reason: version === void 0 ? "not in the library" : `version ${version} doesn't exist` };
|
|
1211
|
+
function addPlan(root, name, hash, force) {
|
|
956
1212
|
const manifest = readManifest(root);
|
|
957
1213
|
const recorded = manifest.skills[name];
|
|
958
|
-
const [primary = PROJECT_SKILLS_DIR, ...
|
|
1214
|
+
const [primary = PROJECT_SKILLS_DIR, ...wantedLinks] = dependencyDirs(root, name, recorded);
|
|
1215
|
+
const stuck = firstStuck(root, [primary, ...wantedLinks]);
|
|
1216
|
+
if (stuck) return { reason: `${blockerSaid(root, stuck)}, so skilllib doesn't install there` };
|
|
959
1217
|
const to = join8(root, primary, name);
|
|
1218
|
+
if (fileAt(to)) return { reason: `${primary}/${name} is a file, not a skill folder: move it first` };
|
|
960
1219
|
const local = isLink(to) && !recorded ? null : treeHash(to);
|
|
961
1220
|
const knownContent = local !== null && (local === recorded?.hash || versionForHash(name, local) !== null);
|
|
962
|
-
if (
|
|
963
|
-
return {
|
|
964
|
-
|
|
1221
|
+
if (!recorded && entryExists(to) && isGitTracked(root, `${primary}/${name}`))
|
|
1222
|
+
return { reason: `${primary}/${name} is the repo's own (committed): skilllib doesn't replace it; remove it in the repo first if the library's should win` };
|
|
1223
|
+
const linked = local === null && !recorded && isLink(to) ? treeHash(to) : null;
|
|
1224
|
+
if (linked !== null && linked !== hash && versionForHash(name, linked) === null && !force)
|
|
1225
|
+
return { reason: `${primary}/${name} links to a different copy (use --force to replace the link)` };
|
|
1226
|
+
if (local !== null && local !== hash && !knownContent && !force) {
|
|
965
1227
|
return {
|
|
966
|
-
name,
|
|
967
|
-
action: "skipped",
|
|
968
1228
|
reason: recorded ? "has local edits (use --force to overwrite, or save them to the library first)" : `${primary}/${name} already exists with different content (use --force to replace it)`
|
|
969
1229
|
};
|
|
970
1230
|
}
|
|
1231
|
+
return { manifest, recorded, primary, wantedLinks, to, local };
|
|
1232
|
+
}
|
|
1233
|
+
function addRefusal(root, name, hash) {
|
|
1234
|
+
const plan = addPlan(root, name, hash, false);
|
|
1235
|
+
return "reason" in plan ? plan.reason : null;
|
|
1236
|
+
}
|
|
1237
|
+
function addSkill(root, name, { force = false, version, allowTracked = false } = {}) {
|
|
1238
|
+
if (!name || name === "." || name === ".." || /[\\/]/.test(name)) return { name, action: "skipped", reason: "not a skill name" };
|
|
1239
|
+
const target = version === void 0 ? latestVersion(name) : getVersion(name, version);
|
|
1240
|
+
if (!target) return { name, action: "skipped", reason: version === void 0 ? "not in the library" : `version ${version} doesn't exist` };
|
|
1241
|
+
const plan = addPlan(root, name, target.hash, force);
|
|
1242
|
+
if ("reason" in plan) return { name, action: "skipped", reason: plan.reason };
|
|
1243
|
+
const { manifest, recorded, primary, wantedLinks, to, local } = plan;
|
|
971
1244
|
let backedUp;
|
|
972
|
-
|
|
1245
|
+
const copied = local !== target.hash || isLink(to);
|
|
1246
|
+
if (copied) {
|
|
973
1247
|
mkdirSync4(join8(root, primary), { recursive: true });
|
|
974
1248
|
if (isLink(to)) unlinkSync(to);
|
|
975
1249
|
else if (local !== null && versionForHash(name, local) === null) backedUp = stash(to, "edit-backup");
|
|
@@ -978,14 +1252,21 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
978
1252
|
const links = [];
|
|
979
1253
|
const blocked = [];
|
|
980
1254
|
const uncommitted = [];
|
|
1255
|
+
const inTheWay = [];
|
|
981
1256
|
let shared;
|
|
982
|
-
|
|
1257
|
+
let relinked = false;
|
|
1258
|
+
for (const dir of wantedLinks) {
|
|
983
1259
|
const link2 = join8(root, dir, name);
|
|
984
|
-
|
|
1260
|
+
const slot = slotAt(link2, to);
|
|
1261
|
+
if (slot === "ok") {
|
|
985
1262
|
links.push(dir);
|
|
986
1263
|
continue;
|
|
987
1264
|
}
|
|
988
|
-
if (
|
|
1265
|
+
if (slot === "taken") {
|
|
1266
|
+
inTheWay.push(`${dir}/${name}`);
|
|
1267
|
+
if (recorded?.links?.includes(dir)) links.push(dir);
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
989
1270
|
if (!recorded?.links?.includes(dir) && isGitTracked(root, dir)) {
|
|
990
1271
|
if (!(shared ??= sharedByGit(root, primary, name))) {
|
|
991
1272
|
uncommitted.push(dir);
|
|
@@ -996,9 +1277,11 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
996
1277
|
continue;
|
|
997
1278
|
}
|
|
998
1279
|
}
|
|
1280
|
+
if (isLink(link2)) unlinkSync(link2);
|
|
999
1281
|
mkdirSync4(join8(root, dir), { recursive: true });
|
|
1000
1282
|
linkDir(to, link2);
|
|
1001
1283
|
links.push(dir);
|
|
1284
|
+
relinked = true;
|
|
1002
1285
|
}
|
|
1003
1286
|
manifest.skills[name] = {
|
|
1004
1287
|
version: target.version,
|
|
@@ -1007,13 +1290,21 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
1007
1290
|
...links.length ? { links } : {}
|
|
1008
1291
|
};
|
|
1009
1292
|
writeManifest(root, manifest);
|
|
1293
|
+
const from = recorded ? versionForHash(name, recorded.hash)?.version ?? recorded.version : void 0;
|
|
1010
1294
|
return {
|
|
1011
1295
|
name,
|
|
1012
|
-
action:
|
|
1013
|
-
|
|
1296
|
+
action: (
|
|
1297
|
+
// Nothing of it was there (or only an identical untracked copy, now tracked): installed, not updated.
|
|
1298
|
+
!recorded || local === null ? "installed" : backedUp && from === target.version ? "reset" : recorded && !copied && from === target.version ? (
|
|
1299
|
+
// The same version, already there: only links (if any) were written.
|
|
1300
|
+
relinked ? "relinked" : "unchanged"
|
|
1301
|
+
) : "updated"
|
|
1302
|
+
),
|
|
1303
|
+
...from !== void 0 ? { from } : {},
|
|
1014
1304
|
to: target.version,
|
|
1015
1305
|
...blocked.length ? { blocked } : {},
|
|
1016
1306
|
...uncommitted.length ? { uncommitted } : {},
|
|
1307
|
+
...inTheWay.length ? { inTheWay } : {},
|
|
1017
1308
|
...backedUp ? { backedUp } : {}
|
|
1018
1309
|
};
|
|
1019
1310
|
}
|
|
@@ -1021,37 +1312,79 @@ function removeSkill(root, name, { force = false } = {}) {
|
|
|
1021
1312
|
const manifest = readManifest(root);
|
|
1022
1313
|
const recorded = manifest.skills[name];
|
|
1023
1314
|
if (!recorded) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
|
|
1024
|
-
const
|
|
1315
|
+
const home2 = recorded.dir ?? PROJECT_SKILLS_DIR;
|
|
1316
|
+
const dir = join8(root, home2, name);
|
|
1317
|
+
const takenLinks = [];
|
|
1318
|
+
const unlinkOwn = () => {
|
|
1319
|
+
for (const linkDir2 of (recorded.links ?? []).filter((d) => writableDir(root, d))) {
|
|
1320
|
+
const link2 = join8(root, linkDir2, name);
|
|
1321
|
+
if (isLink(link2) && slotAt(link2, dir) !== "taken") unlinkSync(link2);
|
|
1322
|
+
else if (entryExists(link2) && slotAt(link2, dir) === "taken") takenLinks.push(`${linkDir2}/${name} is something else (another copy, or a file): left as it is`);
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
const leftLinks = (recorded.links ?? []).flatMap(
|
|
1326
|
+
(d) => ((b) => b && entryExists(join8(root, d, name)) ? [`${blockerSaid(root, b)}: its link there was left alone`] : [])(notWritable(root, d))
|
|
1327
|
+
);
|
|
1328
|
+
const ownStuck = notWritable(root, home2);
|
|
1329
|
+
if (ownStuck || fileAt(dir)) {
|
|
1330
|
+
delete manifest.skills[name];
|
|
1331
|
+
writeManifest(root, manifest);
|
|
1332
|
+
const why = ownStuck ? blockerSaid(root, ownStuck) : `${home2}/${name} is a file, not a skill folder`;
|
|
1333
|
+
const keptLinks = (recorded.links ?? []).flatMap((d) => {
|
|
1334
|
+
const link2 = join8(root, d, name);
|
|
1335
|
+
if (!writableDir(root, d) || !entryExists(link2)) return [];
|
|
1336
|
+
return [slotAt(link2, dir) === "taken" ? `${d}/${name} is something else (another copy, or a file): left as it is` : `its link ${d}/${name} was left too`];
|
|
1337
|
+
});
|
|
1338
|
+
return { name, action: "removed", reason: [`${why}: only its pin was removed, and that copy was left as it is`, ...keptLinks, ...leftLinks].join("; ") };
|
|
1339
|
+
}
|
|
1025
1340
|
const local = treeHash(dir);
|
|
1026
1341
|
const inLibrary = local !== null && versionForHash(name, local) !== null;
|
|
1027
1342
|
const edited = local !== null && local !== recorded.hash && !inLibrary;
|
|
1028
|
-
if (edited && !force)
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1343
|
+
if (edited && !force)
|
|
1344
|
+
return {
|
|
1345
|
+
name,
|
|
1346
|
+
action: "skipped",
|
|
1347
|
+
// A pin from another library isn't an edit of yours: say what it is. Either way --force backs the copy up first.
|
|
1348
|
+
reason: versionForHash(name, recorded.hash) === null ? "doesn't match what skilllib.json pins, and your library has neither (use --force to remove it; it's backed up first)" : "has local edits (use --force to delete anyway; they're backed up first)"
|
|
1349
|
+
};
|
|
1350
|
+
unlinkOwn();
|
|
1033
1351
|
const backedUp = local !== null && !inLibrary ? stash(dir, "edit-backup") : void 0;
|
|
1034
1352
|
if (!backedUp) rmSync3(dir, { recursive: true, force: true });
|
|
1035
1353
|
delete manifest.skills[name];
|
|
1036
1354
|
writeManifest(root, manifest);
|
|
1037
|
-
|
|
1355
|
+
const left = [...leftLinks, ...takenLinks];
|
|
1356
|
+
return { name, action: "removed", ...left.length ? { reason: left.join("; ") } : {}, ...backedUp ? { backedUp } : {} };
|
|
1038
1357
|
}
|
|
1039
|
-
function syncProject(root, { force = false } = {}) {
|
|
1040
|
-
return Object.keys(readManifest(root).skills).flatMap((name) => syncSkill(root, name, { force }));
|
|
1358
|
+
function syncProject(root, { force = false, names } = {}) {
|
|
1359
|
+
return Object.keys(readManifest(root).skills).filter((name) => !names || names.includes(name)).flatMap((name) => syncSkill(root, name, { force }));
|
|
1041
1360
|
}
|
|
1042
1361
|
function syncSkill(root, name, { force = false } = {}) {
|
|
1043
1362
|
const dep = readManifest(root).skills[name];
|
|
1044
1363
|
if (!dep) return [];
|
|
1045
1364
|
const copy = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
|
|
1046
1365
|
const local = treeHash(copy);
|
|
1047
|
-
const
|
|
1048
|
-
if (
|
|
1366
|
+
const blocker = syncBlocker(root, name, dep);
|
|
1367
|
+
if (blocker) return [{ name, action: "skipped", reason: `${blocker.said}: ${blocker.outside ? "skilllib only installs inside the repo" : "move it, then sync"}` }];
|
|
1368
|
+
if (syncDone(root, name, dep)) return [];
|
|
1049
1369
|
if (local === dep.hash) return [relinkPinned(root, name, dep)];
|
|
1050
|
-
if (local !== null && local !== dep.hash && !force) return [{ name, action: "skipped", reason: "has local edits (use --force to reset them)" }];
|
|
1051
1370
|
const version = pinnedVersion(name, dep);
|
|
1052
1371
|
if (typeof version === "string") return [{ name, action: "skipped", reason: version }];
|
|
1053
|
-
|
|
1054
|
-
|
|
1372
|
+
if (local !== null && local !== dep.hash && versionForHash(name, local) === null && !force) return [{ name, action: "skipped", reason: "has local edits (use --force to reset them)" }];
|
|
1373
|
+
const backedUp = local !== null && versionForHash(name, local) === null ? stash(copy, "edit-backup") : void 0;
|
|
1374
|
+
mkdirSync4(dirname3(copy), { recursive: true });
|
|
1375
|
+
copySkill(versionDir(name, version), copy);
|
|
1376
|
+
const links = relinkPinned(root, name, dep);
|
|
1377
|
+
const from = (local !== null ? versionForHash(name, local)?.version : void 0) ?? version;
|
|
1378
|
+
return [
|
|
1379
|
+
{
|
|
1380
|
+
name,
|
|
1381
|
+
action: local === null ? "installed" : backedUp ? "reset" : "updated",
|
|
1382
|
+
from,
|
|
1383
|
+
to: version,
|
|
1384
|
+
...backedUp ? { backedUp } : {},
|
|
1385
|
+
...links.action === "skipped" ? { reason: links.reason } : {}
|
|
1386
|
+
}
|
|
1387
|
+
];
|
|
1055
1388
|
}
|
|
1056
1389
|
function relinkPinned(root, name, dep) {
|
|
1057
1390
|
const to = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
|
|
@@ -1063,7 +1396,7 @@ function relinkPinned(root, name, dep) {
|
|
|
1063
1396
|
mkdirSync4(join8(root, dir), { recursive: true });
|
|
1064
1397
|
linkDir(to, link2);
|
|
1065
1398
|
}
|
|
1066
|
-
return { name, action: "relinked", to: dep.version };
|
|
1399
|
+
return { name, action: "relinked", to: versionForHash(name, dep.hash)?.version ?? dep.version };
|
|
1067
1400
|
}
|
|
1068
1401
|
function pinnedVersion(name, dep) {
|
|
1069
1402
|
if (!latestVersion(name)) return `not in your library (${tildify(libraryDir())}); import it, or set SKILLLIB_HOME to the library that has it`;
|
|
@@ -1074,20 +1407,18 @@ function discardEdits(root, name) {
|
|
|
1074
1407
|
if (!dep) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
|
|
1075
1408
|
const version = pinnedVersion(name, dep);
|
|
1076
1409
|
if (typeof version === "string") return { name, action: "skipped", reason: version };
|
|
1077
|
-
const dir = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
|
|
1078
|
-
if (existsSync7(dir) && !isLink(dir)) stash(dir, "edit-backup");
|
|
1079
1410
|
return addSkill(root, name, { force: true, version });
|
|
1080
1411
|
}
|
|
1081
|
-
function updateProject(root, names, { force = false } = {}) {
|
|
1412
|
+
function updateProject(root, names, { force = false, allowTracked = false } = {}) {
|
|
1082
1413
|
const { skills } = readManifest(root);
|
|
1083
1414
|
return Object.keys(skills).filter((name) => !names || names.includes(name)).flatMap((name) => {
|
|
1084
1415
|
const latest = latestVersion(name);
|
|
1085
|
-
if (!latest) return [{ name, action: "skipped", reason: "
|
|
1416
|
+
if (!latest) return [{ name, action: "skipped", reason: "not in your library" }];
|
|
1086
1417
|
const dep = skills[name];
|
|
1087
1418
|
const local = treeHash(join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name));
|
|
1088
1419
|
if (dep.hash === latest.hash && local === latest.hash) return [];
|
|
1089
1420
|
if (dep.hash === latest.hash && local !== null && !force) return [];
|
|
1090
|
-
const change = addSkill(root, name, { force });
|
|
1421
|
+
const change = addSkill(root, name, { force, allowTracked });
|
|
1091
1422
|
return change.action === "skipped" || change.from !== change.to || local !== latest.hash ? [change] : [];
|
|
1092
1423
|
});
|
|
1093
1424
|
}
|
|
@@ -1100,6 +1431,10 @@ function importSkill(dir, { force = false } = {}) {
|
|
|
1100
1431
|
if (existing !== null && !force) return { name, status: "exists" };
|
|
1101
1432
|
mkdirSync4(libraryDir(), { recursive: true });
|
|
1102
1433
|
const origin = existing !== null && libraryOrigins()[name] ? null : originFor(dir);
|
|
1434
|
+
if (existing !== null) {
|
|
1435
|
+
forgetLatest(name);
|
|
1436
|
+
latestVersion(name);
|
|
1437
|
+
}
|
|
1103
1438
|
copySkill(dir, to);
|
|
1104
1439
|
if (origin) recordOrigin(name, origin);
|
|
1105
1440
|
forgetLatest(name);
|
|
@@ -1134,11 +1469,14 @@ function stash(dir, bucket) {
|
|
|
1134
1469
|
return target;
|
|
1135
1470
|
}
|
|
1136
1471
|
function removeUntracked(root, name, path) {
|
|
1137
|
-
|
|
1138
|
-
|
|
1472
|
+
const stuck = notWritable(root, relative2(root, dirname3(path)));
|
|
1473
|
+
if (stuck) throw new Error(`${stuck} isn't a folder in the repo: skilllib leaves what's in it alone`);
|
|
1474
|
+
const to = stash(path, "trash");
|
|
1475
|
+
unlinkEverywhere(root, name);
|
|
1476
|
+
return to;
|
|
1139
1477
|
}
|
|
1140
1478
|
function projectsUsing(name) {
|
|
1141
|
-
return knownProjects().filter((p) =>
|
|
1479
|
+
return knownProjects().filter((p) => pinsSkill(p, name));
|
|
1142
1480
|
}
|
|
1143
1481
|
function deleteLibrarySkill(name) {
|
|
1144
1482
|
const using = projectsUsing(name);
|
|
@@ -1173,7 +1511,7 @@ function linkGlobal(path, agents) {
|
|
|
1173
1511
|
continue;
|
|
1174
1512
|
}
|
|
1175
1513
|
mkdirSync4(dir, { recursive: true });
|
|
1176
|
-
linkDir(path,
|
|
1514
|
+
linkDir(realpathOrNull(path) ?? path, join8(realpathOrNull(dir) ?? dir, name));
|
|
1177
1515
|
linked.push(link2);
|
|
1178
1516
|
}
|
|
1179
1517
|
return { linked, skipped };
|
|
@@ -1185,9 +1523,10 @@ function createSkill(name, description) {
|
|
|
1185
1523
|
mkdirSync4(dir, { recursive: true });
|
|
1186
1524
|
writeFileSync2(
|
|
1187
1525
|
join8(dir, "SKILL.md"),
|
|
1526
|
+
// JSON strings are valid YAML: a description with ": " or "#" stays one value.
|
|
1188
1527
|
`---
|
|
1189
1528
|
name: ${name}
|
|
1190
|
-
description: ${description || "Describe when an agent should use this skill."}
|
|
1529
|
+
description: ${JSON.stringify(description || "Describe when an agent should use this skill.")}
|
|
1191
1530
|
---
|
|
1192
1531
|
|
|
1193
1532
|
# ${name}
|
|
@@ -1212,6 +1551,11 @@ function listBackups() {
|
|
|
1212
1551
|
});
|
|
1213
1552
|
}).sort((a, b) => b.path.slice(-24).localeCompare(a.path.slice(-24)));
|
|
1214
1553
|
}
|
|
1554
|
+
function movedTogether(backups, backup) {
|
|
1555
|
+
if (backup.kind === "plugin") return [backup];
|
|
1556
|
+
const at = (b) => Date.parse(b.path.slice(-24).replace(/^(.{13})-(\d\d)-(\d\d)-(\d{3})Z$/, "$1:$2:$3.$4Z"));
|
|
1557
|
+
return backups.filter((b) => b.name === backup.name && b.kind === backup.kind && Math.abs(at(b) - at(backup)) <= 2e3);
|
|
1558
|
+
}
|
|
1215
1559
|
function backupFrom(backup) {
|
|
1216
1560
|
if (backup.kind === "plugin") return "Claude Code plugin";
|
|
1217
1561
|
if (!backup.from) return "unknown";
|
|
@@ -1222,12 +1566,7 @@ function restoreBackup(backup) {
|
|
|
1222
1566
|
if (!to) return { ok: false, reason: `skilllib doesn't know where it came from; its files are in ${tildify(backup.path)}` };
|
|
1223
1567
|
if (backup.kind === "tidy-backup" && isLink(to)) unlinkSync(to);
|
|
1224
1568
|
if (backup.kind === "edit-backup" && !isLink(to) && versionForHash(backup.name, treeHash(to) ?? "") !== null) rmSync3(to, { recursive: true, force: true });
|
|
1225
|
-
|
|
1226
|
-
try {
|
|
1227
|
-
taken = taken || lstatSync2(to) !== null;
|
|
1228
|
-
} catch {
|
|
1229
|
-
}
|
|
1230
|
-
if (taken) return { ok: false, reason: `${backup.name} already exists there` };
|
|
1569
|
+
if (entryExists(to)) return { ok: false, reason: `${backup.name} already exists there` };
|
|
1231
1570
|
mkdirSync4(join8(to, ".."), { recursive: true });
|
|
1232
1571
|
move(backup.path, to);
|
|
1233
1572
|
forgetLatest(backup.name);
|
|
@@ -1237,45 +1576,39 @@ function restoreBackup(backup) {
|
|
|
1237
1576
|
// src/agentSkill.ts
|
|
1238
1577
|
var AGENT_SKILL_NAME = "skilllib";
|
|
1239
1578
|
function shippedSkill() {
|
|
1240
|
-
return
|
|
1579
|
+
return readFileSync5(new URL("../skills/skilllib/SKILL.md", import.meta.url), "utf-8");
|
|
1241
1580
|
}
|
|
1242
1581
|
function agentSkillDirs(enabled = enabledHarnesses()) {
|
|
1243
1582
|
const claudeSkills = join9(claudeDir(), "skills");
|
|
1244
1583
|
const agentsSkills = join9(userHome(), ".agents", "skills");
|
|
1245
1584
|
const needsClaude = enabled.some((id) => harness(id).globalDirs().includes(claudeSkills));
|
|
1246
1585
|
const needsAgents = enabled.some((id) => !harness(id).globalDirs().includes(claudeSkills));
|
|
1247
|
-
|
|
1586
|
+
const dirs = [...needsClaude ? [claudeSkills] : [], ...needsAgents ? [agentsSkills] : []];
|
|
1587
|
+
return dirs.filter((d, i) => dirs.findIndex((o) => realPath(o) === realPath(d)) === i).map((d) => join9(d, AGENT_SKILL_NAME));
|
|
1248
1588
|
}
|
|
1249
1589
|
function isOurs(dir) {
|
|
1250
1590
|
return existsSync8(join9(dir, OWN_SKILL_MARKER));
|
|
1251
1591
|
}
|
|
1252
1592
|
function isDangling(path) {
|
|
1253
|
-
return
|
|
1254
|
-
}
|
|
1255
|
-
function entryExists2(path) {
|
|
1256
|
-
try {
|
|
1257
|
-
lstatSync3(path);
|
|
1258
|
-
return true;
|
|
1259
|
-
} catch {
|
|
1260
|
-
return false;
|
|
1261
|
-
}
|
|
1593
|
+
return entryExists(path) && lstatSync3(path).isSymbolicLink() && !existsSync8(path);
|
|
1262
1594
|
}
|
|
1263
1595
|
function agentSkillState(enabled = enabledHarnesses()) {
|
|
1264
1596
|
const [primary, ...links] = agentSkillDirs(enabled);
|
|
1265
1597
|
if (!primary || !isOurs(primary) || links.some((l) => !existsSync8(join9(l, "SKILL.md")))) return "missing";
|
|
1266
|
-
return
|
|
1598
|
+
return [primary, ...links].every((d) => readFileSync5(join9(d, "SKILL.md"), "utf-8") === shippedSkill()) ? "installed" : "outdated";
|
|
1267
1599
|
}
|
|
1268
1600
|
function installAgentSkill(enabled = enabledHarnesses()) {
|
|
1269
1601
|
const [primary, ...links] = agentSkillDirs(enabled);
|
|
1270
1602
|
if (!primary) return { ok: false, reason: "no agents chosen yet (skilllib harnesses <id>...)" };
|
|
1271
|
-
const foreign = [primary, ...links].find((d) =>
|
|
1603
|
+
const foreign = [primary, ...links].find((d) => entryExists(d) && !isOurs(d) && !isDangling(d));
|
|
1272
1604
|
if (foreign) return { ok: false, reason: `${tildify(foreign)} already exists and isn't skilllib's` };
|
|
1273
1605
|
for (const d of [primary, ...links]) if (isDangling(d)) unlinkSync2(d);
|
|
1274
1606
|
mkdirSync5(primary, { recursive: true });
|
|
1275
1607
|
writeFileSync3(join9(primary, "SKILL.md"), shippedSkill());
|
|
1276
1608
|
writeFileSync3(join9(primary, OWN_SKILL_MARKER), "Installed by skilllib. `skilllib agent-skill remove` removes it.\n");
|
|
1277
1609
|
for (const link2 of links) {
|
|
1278
|
-
if (
|
|
1610
|
+
if (entryExists(link2) && !isLink(link2) && isOurs(link2)) rmSync4(link2, { recursive: true, force: true });
|
|
1611
|
+
if (entryExists(link2)) continue;
|
|
1279
1612
|
mkdirSync5(join9(link2, ".."), { recursive: true });
|
|
1280
1613
|
linkDir(primary, link2);
|
|
1281
1614
|
}
|
|
@@ -1283,9 +1616,9 @@ function installAgentSkill(enabled = enabledHarnesses()) {
|
|
|
1283
1616
|
}
|
|
1284
1617
|
function removeAgentSkill() {
|
|
1285
1618
|
const all = agentSkillDirs(["claude-code", "codex"]);
|
|
1286
|
-
const removed = all.filter((d) =>
|
|
1619
|
+
const removed = all.filter((d) => entryExists(d) && (isOurs(d) || isDangling(d)));
|
|
1287
1620
|
for (const d of removed) {
|
|
1288
|
-
if (!
|
|
1621
|
+
if (!entryExists(d)) continue;
|
|
1289
1622
|
if (lstatSync3(d).isSymbolicLink()) unlinkSync2(d);
|
|
1290
1623
|
else rmSync4(d, { recursive: true, force: true });
|
|
1291
1624
|
}
|
|
@@ -1299,14 +1632,14 @@ function refreshAgentSkill() {
|
|
|
1299
1632
|
}
|
|
1300
1633
|
|
|
1301
1634
|
// src/commands.ts
|
|
1302
|
-
import { existsSync as existsSync14, readFileSync as
|
|
1635
|
+
import { existsSync as existsSync14, readFileSync as readFileSync7 } from "fs";
|
|
1303
1636
|
import { parseArgs as parseFlags } from "util";
|
|
1304
|
-
import { basename as basename9, join as join15, resolve as
|
|
1637
|
+
import { basename as basename9, join as join15, resolve as resolve8, sep as sep8 } from "path";
|
|
1305
1638
|
|
|
1306
1639
|
// src/git.ts
|
|
1307
1640
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
1308
|
-
import { existsSync as existsSync9 } from "fs";
|
|
1309
|
-
import { join as join10, relative as relative3 } from "path";
|
|
1641
|
+
import { existsSync as existsSync9, realpathSync as realpathSync4 } from "fs";
|
|
1642
|
+
import { isAbsolute as isAbsolute2, join as join10, relative as relative3, sep as sep5 } from "path";
|
|
1310
1643
|
function git(root, args, input) {
|
|
1311
1644
|
try {
|
|
1312
1645
|
return execFileSync2("git", args, { cwd: root, encoding: "utf-8", input, stdio: ["pipe", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 });
|
|
@@ -1316,11 +1649,32 @@ function git(root, args, input) {
|
|
|
1316
1649
|
}
|
|
1317
1650
|
}
|
|
1318
1651
|
function gitInfo(root, extraDirs = []) {
|
|
1319
|
-
|
|
1320
|
-
|
|
1652
|
+
let inRepo;
|
|
1653
|
+
try {
|
|
1654
|
+
inRepo = execFileSync2("git", ["rev-parse", "--show-prefix"], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1655
|
+
} catch {
|
|
1656
|
+
if (!existsSync9(join10(root, ".git"))) return null;
|
|
1657
|
+
inRepo = "";
|
|
1658
|
+
}
|
|
1659
|
+
const real = (p) => {
|
|
1660
|
+
try {
|
|
1661
|
+
return realpathSync4.native(p);
|
|
1662
|
+
} catch {
|
|
1663
|
+
return p;
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
const realRoot = real(root);
|
|
1667
|
+
const places = new Map(
|
|
1668
|
+
[.../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS, MANIFEST_FILE, ...extraDirs])].filter((d) => existsSync9(join10(root, d))).map((d) => [d, relative3(realRoot, real(join10(root, d))).split(sep5).join("/")]).filter(([, place]) => place !== "" && place !== ".." && !place.startsWith("../") && !isAbsolute2(place))
|
|
1669
|
+
);
|
|
1670
|
+
const dirs = [...new Set(places.values())];
|
|
1321
1671
|
if (dirs.length === 0) return { of: () => "new" };
|
|
1672
|
+
const placeOf = (rel) => {
|
|
1673
|
+
const dir = [...places.keys()].find((d) => rel === d || rel.startsWith(d + "/"));
|
|
1674
|
+
return dir === void 0 ? rel : places.get(dir) + rel.slice(dir.length);
|
|
1675
|
+
};
|
|
1322
1676
|
const tracked = (git(root, ["ls-files", "-z", "--", ...dirs]) ?? "").split("\0").filter(Boolean);
|
|
1323
|
-
const dirty = (git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...dirs]) ?? "").split("\0").filter(Boolean).map((entry) => entry.slice(3));
|
|
1677
|
+
const dirty = (git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...dirs]) ?? "").split("\0").filter(Boolean).map((entry) => entry.slice(3)).filter((path) => path.startsWith(inRepo)).map((path) => path.slice(inRepo.length));
|
|
1324
1678
|
const ignoredCache = /* @__PURE__ */ new Map();
|
|
1325
1679
|
const isIgnored = (rel) => {
|
|
1326
1680
|
if (!ignoredCache.has(rel)) {
|
|
@@ -1332,7 +1686,7 @@ function gitInfo(root, extraDirs = []) {
|
|
|
1332
1686
|
const under = (list2, rel) => list2.some((p) => p === rel || p.startsWith(rel + "/"));
|
|
1333
1687
|
return {
|
|
1334
1688
|
of: (relPath) => {
|
|
1335
|
-
const rel = relPath.replace(/\/+$/, "");
|
|
1689
|
+
const rel = placeOf(relPath.replace(/\/+$/, ""));
|
|
1336
1690
|
if (under(tracked, rel)) return under(dirty, rel) ? "changed" : "committed";
|
|
1337
1691
|
return isIgnored(rel) ? "ignored" : "new";
|
|
1338
1692
|
}
|
|
@@ -1347,13 +1701,13 @@ import { readdirSync as readdirSync8 } from "fs";
|
|
|
1347
1701
|
import { basename as basename7 } from "path";
|
|
1348
1702
|
|
|
1349
1703
|
// src/health.ts
|
|
1350
|
-
import { existsSync as existsSync12, lstatSync as lstatSync4, readdirSync as readdirSync7
|
|
1351
|
-
import { basename as basename6, dirname as dirname6, join as join13 } from "path";
|
|
1704
|
+
import { existsSync as existsSync12, lstatSync as lstatSync4, readdirSync as readdirSync7 } from "fs";
|
|
1705
|
+
import { basename as basename6, dirname as dirname6, isAbsolute as isAbsolute3, join as join13, relative as relative4, sep as sep7 } from "path";
|
|
1352
1706
|
|
|
1353
1707
|
// src/plugins.ts
|
|
1354
1708
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1355
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync5, readFileSync as
|
|
1356
|
-
import { basename as basename4, dirname as dirname4, join as join11, resolve as
|
|
1709
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
1710
|
+
import { basename as basename4, dirname as dirname4, join as join11, resolve as resolve7 } from "path";
|
|
1357
1711
|
function nonEmptyDir(path) {
|
|
1358
1712
|
try {
|
|
1359
1713
|
return readdirSync5(path).some((f) => !f.startsWith("."));
|
|
@@ -1383,6 +1737,8 @@ function claudePlugins(machine, root) {
|
|
|
1383
1737
|
scope: synced ? null : entry?.scope ?? "user",
|
|
1384
1738
|
...entry?.projectPath ? { projectPath: entry.projectPath } : {},
|
|
1385
1739
|
synced,
|
|
1740
|
+
// Turned on in your settings with no install record (as installedPlugins finds it).
|
|
1741
|
+
...!synced && !entries.length ? { settingsOnly: true } : {},
|
|
1386
1742
|
skills,
|
|
1387
1743
|
extras: extrasOf(dirname4(dirname4(skills[0])))
|
|
1388
1744
|
};
|
|
@@ -1522,8 +1878,8 @@ function ignoreLocalSettings(root) {
|
|
|
1522
1878
|
} catch {
|
|
1523
1879
|
}
|
|
1524
1880
|
try {
|
|
1525
|
-
const exclude =
|
|
1526
|
-
const before = existsSync10(exclude) ?
|
|
1881
|
+
const exclude = resolve7(root, git2("rev-parse", "--git-path", "info/exclude"));
|
|
1882
|
+
const before = existsSync10(exclude) ? readFileSync6(exclude, "utf-8") : "";
|
|
1527
1883
|
mkdirSync6(dirname4(exclude), { recursive: true });
|
|
1528
1884
|
writeFileSync4(exclude, `${before}${before && !before.endsWith("\n") ? "\n" : ""}/${git2("rev-parse", "--show-prefix")}${file}
|
|
1529
1885
|
`);
|
|
@@ -1534,7 +1890,7 @@ function ignoreLocalSettings(root) {
|
|
|
1534
1890
|
}
|
|
1535
1891
|
function turnOffIn(plugin, root) {
|
|
1536
1892
|
const done = `${plugin.id} turned off in ${basename4(root)}; other repos keep it`;
|
|
1537
|
-
const bin = plugin.synced ? null : claudeBinary();
|
|
1893
|
+
const bin = plugin.synced || plugin.settingsOnly ? null : claudeBinary();
|
|
1538
1894
|
if (!bin) {
|
|
1539
1895
|
const r2 = setPluginIn(join11(root, ".claude", "settings.local.json"), plugin.id, false);
|
|
1540
1896
|
return r2.ok ? { ok: true, message: `${done} (in .claude/settings.local.json${ignoreLocalSettings(root)})` } : { ok: false, message: `${plugin.id}: ${r2.reason}` };
|
|
@@ -1551,7 +1907,8 @@ function whereOf(p) {
|
|
|
1551
1907
|
return p.projectPath ? ` in ${basename4(p.projectPath)} (${p.scope})` : "";
|
|
1552
1908
|
}
|
|
1553
1909
|
function setPluginOn(p, on) {
|
|
1554
|
-
const
|
|
1910
|
+
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" : "";
|
|
1911
|
+
const done = { ok: true, message: `${p.id} is ${on ? "on" : "off"}${whereOf(p)}${p.synced ? "" : "; Claude Code picks it up in its next session"}${shared}` };
|
|
1555
1912
|
if (p.scope && p.scope !== "user" && !p.projectPath) return { ok: false, message: `${p.id}: skilllib doesn't know which project it's installed in; run \`claude plugin ${on ? "enable" : "disable"} ${p.id} --scope ${p.scope}\` there` };
|
|
1556
1913
|
const bin = p.synced || p.settingsOnly ? null : claudeBinary();
|
|
1557
1914
|
if (bin) {
|
|
@@ -1560,7 +1917,8 @@ function setPluginOn(p, on) {
|
|
|
1560
1917
|
}
|
|
1561
1918
|
const r = setPluginIn(settingsFileOf(p), p.id, on);
|
|
1562
1919
|
if (!r.ok) return { ok: false, message: `${p.id}: ${r.reason}` };
|
|
1563
|
-
|
|
1920
|
+
if (p.scope === "local" && p.projectPath) return { ...done, message: done.message + ignoreLocalSettings(p.projectPath) };
|
|
1921
|
+
return done;
|
|
1564
1922
|
}
|
|
1565
1923
|
function uninstallPlugin(p) {
|
|
1566
1924
|
if (p.synced) return { ok: false, message: `${p.id} is synced from your claude.ai account: remove it there, or turn it off here` };
|
|
@@ -1590,10 +1948,11 @@ function removedFile() {
|
|
|
1590
1948
|
return join11(skilllibHome(), "plugin-backup.json");
|
|
1591
1949
|
}
|
|
1592
1950
|
function removePlugin(plugin, how) {
|
|
1951
|
+
if (plugin.settingsOnly && how === "delete") return { ok: false, message: `${plugin.id} isn't installed, only turned on in your settings: turn it off instead` };
|
|
1593
1952
|
if (plugin.scope && plugin.scope !== "user" && !plugin.projectPath) {
|
|
1594
1953
|
return { ok: false, message: `${plugin.id}: installed for one project; run \`claude plugin ${how === "delete" ? "uninstall" : "disable"} ${plugin.id} --scope ${plugin.scope}\` there` };
|
|
1595
1954
|
}
|
|
1596
|
-
const viaSettings = plugin.synced || !claudeBinary() && how === "off" && !plugin.projectPath;
|
|
1955
|
+
const viaSettings = plugin.synced || plugin.settingsOnly || !claudeBinary() && how === "off" && !plugin.projectPath;
|
|
1597
1956
|
if (!viaSettings && !claudeBinary()) {
|
|
1598
1957
|
return { ok: false, message: `${plugin.id}: nothing changed; Claude Code's CLI didn't run (tried \`claude\` on your PATH and ~/.local/bin/claude)` };
|
|
1599
1958
|
}
|
|
@@ -1602,7 +1961,7 @@ function removePlugin(plugin, how) {
|
|
|
1602
1961
|
const imported = plugin.skills.filter((dir) => !yours.has(basename4(dir)) && latestVersion(basename4(dir)) === null).map((dir) => importSkill(dir).name);
|
|
1603
1962
|
const scope = plugin.scope ? ["--scope", plugin.scope] : [];
|
|
1604
1963
|
const r = viaSettings ? (
|
|
1605
|
-
// Synced plugins aren't installed, so `claude plugin` can't touch them: their switch is enabledPlugins.
|
|
1964
|
+
// Synced and settings-only plugins aren't installed, so `claude plugin` can't touch them: their switch is enabledPlugins.
|
|
1606
1965
|
setPluginIn(join11(claudeDir(), "settings.json"), plugin.id, false)
|
|
1607
1966
|
) : (
|
|
1608
1967
|
// --keep-data, as uninstallPlugin: its saved data stays, so a restore brings it back as it was.
|
|
@@ -1611,7 +1970,9 @@ function removePlugin(plugin, how) {
|
|
|
1611
1970
|
const saved = imported.length ? `; ${imported.join(", ")} copied into Your skills` : "";
|
|
1612
1971
|
if (!r.ok) return { ok: false, message: `${plugin.id}: ${r.reason}${saved}` };
|
|
1613
1972
|
if (how === "delete" && !plugin.synced) recordRemovedPlugin(plugin.id, plugin.scope ?? "user", plugin.projectPath, wasOn);
|
|
1614
|
-
|
|
1973
|
+
const others = how === "delete" ? (installedPluginsJson()[plugin.id] ?? []).filter((e) => (e.scope ?? "user") !== (plugin.scope ?? "user") || e.projectPath !== plugin.projectPath) : [];
|
|
1974
|
+
const left = others.length ? `; its ${others.map((e) => `${e.scope ?? "user"} install${e.projectPath ? ` in ${basename4(e.projectPath)}` : ""}`).join(", ")} stays` : "";
|
|
1975
|
+
return { ok: true, message: `${plugin.id} ${how === "delete" && !plugin.synced ? `removed (${plugin.scope ?? "user"} install)` : "turned off"}${saved}${left}` };
|
|
1615
1976
|
}
|
|
1616
1977
|
function recordRemovedPlugin(id, scope, projectPath, on = true) {
|
|
1617
1978
|
const list2 = readState(removedFile()) ?? [];
|
|
@@ -1647,7 +2008,7 @@ function cursorPluginSkills(plugins = cursorPlugins()) {
|
|
|
1647
2008
|
|
|
1648
2009
|
// src/tidy.ts
|
|
1649
2010
|
import { existsSync as existsSync11, mkdirSync as mkdirSync7, readdirSync as readdirSync6, unlinkSync as unlinkSync3 } from "fs";
|
|
1650
|
-
import { basename as basename5, dirname as dirname5, join as join12, sep as
|
|
2011
|
+
import { basename as basename5, dirname as dirname5, join as join12, sep as sep6 } from "path";
|
|
1651
2012
|
function entriesOf(dirs, name) {
|
|
1652
2013
|
return dirs.flatMap((dir) => {
|
|
1653
2014
|
const path = join12(dir, name);
|
|
@@ -1665,12 +2026,13 @@ function hashReals(reals) {
|
|
|
1665
2026
|
function namesIn(dirs) {
|
|
1666
2027
|
const names = /* @__PURE__ */ new Set();
|
|
1667
2028
|
for (const dir of dirs) {
|
|
1668
|
-
if (!
|
|
2029
|
+
if (!isDir(dir)) continue;
|
|
1669
2030
|
for (const name of readdirSync6(dir)) if (!name.startsWith(".") && name !== "synced") names.add(name);
|
|
1670
2031
|
}
|
|
1671
2032
|
return [...names].sort();
|
|
1672
2033
|
}
|
|
1673
2034
|
var PROJECT_TIDY_DIRS = ALL_PROJECT_DIRS.filter((d) => d !== GROK_SKILLS_DIR);
|
|
2035
|
+
var repoTidyDirs = (root) => PROJECT_TIDY_DIRS.filter((d) => notWritable(root, d) === null);
|
|
1674
2036
|
function globalTidyDirs() {
|
|
1675
2037
|
const own = realPath(join12(grokDir(), "skills"));
|
|
1676
2038
|
return globalSkillDirs().filter((d) => realPath(d) !== own);
|
|
@@ -1697,11 +2059,13 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1697
2059
|
const readsOf = (id) => harness(id).projectDirs.map(abs);
|
|
1698
2060
|
let git2;
|
|
1699
2061
|
const skillsLock = projectSkillsLock(root);
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
const
|
|
2062
|
+
const dirs = repoTidyDirs(root).map(abs);
|
|
2063
|
+
for (const name of namesIn(dirs)) {
|
|
2064
|
+
const entries = entriesOf(dirs, name);
|
|
2065
|
+
const external = entries.find((e) => e.link && e.real && !(e.real + sep6).startsWith(realpathOrNull(root) + sep6));
|
|
1703
2066
|
if (external) {
|
|
1704
|
-
|
|
2067
|
+
const differs = entries.some((e) => e !== external && e.real && existsSync11(join12(e.real, "SKILL.md")) && treeHash(e.real) !== treeHash(external.real));
|
|
2068
|
+
report.skipped.push({ name, reason: `${rel(external.path)} links outside the repo`, ...differs ? { differs } : {} });
|
|
1705
2069
|
continue;
|
|
1706
2070
|
}
|
|
1707
2071
|
const reals = entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link && realpathOrNull(o.path) === realpathOrNull(e.path)) === i);
|
|
@@ -1727,6 +2091,11 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1727
2091
|
report.conflicts.push({ ...conflictOf(name, root, reals, enabled, readsOf), ...dep ? { managed: primary.dir } : {} });
|
|
1728
2092
|
continue;
|
|
1729
2093
|
}
|
|
2094
|
+
const elsewhere = entries.find((e) => e.link && e.real && e.real !== primary.real && existsSync11(join12(e.real, "SKILL.md")) && treeHash(e.real) !== treeHash(primary.path));
|
|
2095
|
+
if (elsewhere) {
|
|
2096
|
+
report.skipped.push({ name, reason: `${rel(elsewhere.path)} links to a different copy`, differs: true });
|
|
2097
|
+
continue;
|
|
2098
|
+
}
|
|
1730
2099
|
const wanted = new Set([...installDirs(enabled, rel(primary.dir)), ...dep?.links ?? []].map(abs));
|
|
1731
2100
|
const steps = [];
|
|
1732
2101
|
for (const e of entries) {
|
|
@@ -1739,6 +2108,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1739
2108
|
} else steps.push(needed ? { kind: "link", path: e.path, target: primary.path } : { kind: "unlink", path: e.path });
|
|
1740
2109
|
} else {
|
|
1741
2110
|
const differs = e.hash !== primary.hash ? { differs: true } : {};
|
|
2111
|
+
if (!needed && ["committed", "changed"].includes((git2 ??= gitInfo(root))?.of(rel(e.path)) ?? "")) continue;
|
|
1742
2112
|
steps.push(needed ? { kind: "replace", path: e.path, target: primary.path, ...differs } : { kind: "remove", path: e.path, ...differs });
|
|
1743
2113
|
}
|
|
1744
2114
|
}
|
|
@@ -1778,10 +2148,10 @@ function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
|
|
|
1778
2148
|
}
|
|
1779
2149
|
function gitVisibleSteps(plan, git2 = plan.root ? gitInfo(plan.root) : null) {
|
|
1780
2150
|
if (!plan.root || !git2) return [];
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
2151
|
+
const shared = (rel) => ["committed", "changed"].includes(git2.of(rel));
|
|
2152
|
+
const dep = readManifest(plan.root).skills[plan.name];
|
|
2153
|
+
const recordsLink = (s) => !!dep && (s.kind === "link" || s.kind === "replace") && !dep.links?.includes(relativeTo(plan.root, dirname5(s.path)));
|
|
2154
|
+
return plan.steps.filter((s) => shared(relativeTo(plan.root, s.path)) || recordsLink(s) && shared(MANIFEST_FILE));
|
|
1785
2155
|
}
|
|
1786
2156
|
function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
|
|
1787
2157
|
const held = git2 === "keep" ? gitVisibleSteps(plan, info2) : [];
|
|
@@ -1822,18 +2192,19 @@ function describeStep(step, root) {
|
|
|
1822
2192
|
return `remove ${step.differs ? "differing" : "duplicate"} copy ${shown}`;
|
|
1823
2193
|
}
|
|
1824
2194
|
}
|
|
1825
|
-
function copyLabel(conflict, dir) {
|
|
1826
|
-
return folderLabel(conflict.root, dir);
|
|
1827
|
-
}
|
|
1828
2195
|
function folderLabel(root, dir) {
|
|
1829
2196
|
return root ? relativeTo(root, dir) : tildify(dir);
|
|
1830
2197
|
}
|
|
1831
2198
|
function foldersOf(root, name) {
|
|
1832
|
-
const entries = entriesOf(root ?
|
|
2199
|
+
const entries = entriesOf(root ? repoTidyDirs(root).map((d) => join12(root, d)) : globalTidyDirs(), name);
|
|
1833
2200
|
return { all: entries.map((e) => e.dir), real: entries.filter((e) => !e.link).map((e) => e.dir) };
|
|
1834
2201
|
}
|
|
1835
2202
|
|
|
1836
2203
|
// src/health.ts
|
|
2204
|
+
function detailWithGit(issue) {
|
|
2205
|
+
const notes = [...issue.fix ? [issue.fix] : [], ...issue.choices ?? []].filter((c) => c.hint && /\bgit\b|committed/.test(c.hint)).map((c) => `"${c.label}": ${c.hint}.`);
|
|
2206
|
+
return notes.length ? `${issue.detail} ${notes.join(" ")}` : issue.detail;
|
|
2207
|
+
}
|
|
1837
2208
|
function runFix(choice) {
|
|
1838
2209
|
try {
|
|
1839
2210
|
return { ok: true, message: choice.run() };
|
|
@@ -1865,6 +2236,16 @@ function summarize(where, verb, changes) {
|
|
|
1865
2236
|
}
|
|
1866
2237
|
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1867
2238
|
var rows = (root, skills) => skills.map((skill) => ({ root, skill }));
|
|
2239
|
+
var removesClean = (root, s) => s.managed && !s.state.startsWith("edited") && s.state !== "not in library" && !syncBlocker(root, s.name);
|
|
2240
|
+
var gitMemo = /* @__PURE__ */ new Map();
|
|
2241
|
+
var gitOf = (root) => {
|
|
2242
|
+
if (!gitMemo.has(root)) gitMemo.set(root, gitInfo(root));
|
|
2243
|
+
return gitMemo.get(root);
|
|
2244
|
+
};
|
|
2245
|
+
var gitNote = (root, paths) => {
|
|
2246
|
+
const git2 = gitOf(root);
|
|
2247
|
+
return paths.some((p) => ["committed", "changed"].includes(git2?.of(relativeTo(root, p)) ?? "")) ? "; git will see it" : "";
|
|
2248
|
+
};
|
|
1868
2249
|
var agentNames = (ids) => ids.map((id) => harness(id).name).join(", ");
|
|
1869
2250
|
function conflictIssue(c) {
|
|
1870
2251
|
const where = c.root ? basename6(c.root) : "your global folders";
|
|
@@ -1874,21 +2255,22 @@ function conflictIssue(c) {
|
|
|
1874
2255
|
const plan = planFor(dir);
|
|
1875
2256
|
const r = plan ? applyTidy(plan, { git: git2 }) : null;
|
|
1876
2257
|
if (!r?.applied.length) throw new Error(`${c.name}: nothing changed${r?.held.length ? " (git would see every change)" : ""}`);
|
|
1877
|
-
return `${c.name}: kept the ${
|
|
2258
|
+
return `${c.name}: kept the ${folderLabel(c.root, dir)} copy${r.held.length ? `; held back ${plural(r.held.length, "change")} git would see` : ""}`;
|
|
1878
2259
|
};
|
|
1879
2260
|
return {
|
|
1880
2261
|
id: `conflict:${c.root ?? "global"}:${c.name}`,
|
|
1881
2262
|
severity: "problem",
|
|
1882
2263
|
title: `${c.name} in ${where}: the copies differ`,
|
|
1883
|
-
detail: `Your agents run different versions: ${c.copies.map((x) => `${
|
|
2264
|
+
detail: `Your agents run different versions: ${c.copies.map((x) => `${folderLabel(c.root, x.dir)}${x.runs.length ? ` (${agentNames(x.runs)})` : ""}`).join(" vs ")}. Keep one; the others become links to it and are restorable with skilllib restore (Settings \u203A Backups in the app).`,
|
|
1884
2265
|
at: rows(c.root, [c.name]),
|
|
1885
2266
|
choices: winners.flatMap((x) => {
|
|
1886
2267
|
const plan = planFor(x.dir);
|
|
1887
2268
|
const visible2 = plan ? gitVisibleSteps(plan).map((s) => describeStep(s, c.root)) : [];
|
|
1888
|
-
const label = `Keep the ${
|
|
2269
|
+
const label = `Keep the ${folderLabel(c.root, x.dir)} copy`;
|
|
1889
2270
|
if (!visible2.length) return [{ label, hint: x.dir === c.managed ? "skilllib's version" : "the others become links to it", run: keep(x.dir, "go") }];
|
|
1890
2271
|
return [
|
|
1891
|
-
|
|
2272
|
+
// "Keep git as it is" only when it leaves something to do.
|
|
2273
|
+
...visible2.length < (plan?.steps.length ?? 0) ? [{ label: `${label}, but keep git as it is`, hint: `skips what git would see: ${visible2.join(", ")}`, run: keep(x.dir, "keep") }] : [],
|
|
1892
2274
|
{ label: `${label}, and change what git tracks`, hint: `git will see: ${visible2.join(", ")}`, run: keep(x.dir, "go") }
|
|
1893
2275
|
];
|
|
1894
2276
|
})
|
|
@@ -1898,34 +2280,68 @@ function applyAll(plans, git2) {
|
|
|
1898
2280
|
const results = plans.map((p) => applyTidy(p, { git: git2 }));
|
|
1899
2281
|
return { skills: results.filter((r) => r.applied.length).length, held: results.reduce((n, r) => n + r.held.length, 0) };
|
|
1900
2282
|
}
|
|
1901
|
-
function brokenLinksIn(root) {
|
|
2283
|
+
function brokenLinksIn(root, opened) {
|
|
1902
2284
|
const where = basename6(root);
|
|
1903
|
-
const
|
|
1904
|
-
const hasCopy = (name) =>
|
|
2285
|
+
const tidyDirs = repoTidyDirs(root);
|
|
2286
|
+
const hasCopy = (name) => tidyDirs.some((d) => !isLink(join13(root, d, name)) && existsSync12(join13(root, d, name, "SKILL.md")));
|
|
2287
|
+
const mends = (project, dir, name) => {
|
|
2288
|
+
const dep = pinsIn(project)[name];
|
|
2289
|
+
return !!dep && (dep.links ?? []).includes(dir) && !syncBlocker(project, name, dep) && versionForHash(name, dep.hash) !== null;
|
|
2290
|
+
};
|
|
2291
|
+
const syncMends = (dir, name) => mends(root, dir, name);
|
|
1905
2292
|
let git2;
|
|
1906
|
-
|
|
2293
|
+
const realRoot = realNative(root) ?? root;
|
|
2294
|
+
const ownerOf = (real) => {
|
|
2295
|
+
let nearest = null;
|
|
2296
|
+
for (let d = dirname6(dirname6(real)); d.startsWith(realRoot + sep7); d = dirname6(d)) {
|
|
2297
|
+
if (!existsSync12(join13(d, MANIFEST_FILE))) continue;
|
|
2298
|
+
if (opened.has(d)) return { opened: true, own: d };
|
|
2299
|
+
nearest ??= d;
|
|
2300
|
+
}
|
|
2301
|
+
return { opened: false, own: nearest };
|
|
2302
|
+
};
|
|
2303
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2304
|
+
const aliased = (dir) => (realNative(join13(root, dir)) ?? "") !== join13(realRoot, dir);
|
|
2305
|
+
const packageFolders = packageSkillFolders(root).map((f) => ({ dir: f.location, top: false })).sort((a, b) => Number(aliased(a.dir)) - Number(aliased(b.dir)));
|
|
2306
|
+
const folders2 = [...ALL_PROJECT_DIRS.map((dir) => ({ dir, top: true })), ...packageFolders].flatMap(({ dir, top }) => {
|
|
2307
|
+
const real = realNative(join13(root, dir)) ?? join13(root, dir);
|
|
2308
|
+
if (seen.has(real)) return [];
|
|
2309
|
+
seen.add(real);
|
|
2310
|
+
const owner = ownerOf(real);
|
|
2311
|
+
return owner.opened ? [] : [{ dir, top: top && !owner.own, own: owner.own, real }];
|
|
2312
|
+
});
|
|
2313
|
+
return folders2.flatMap(({ dir, top, own, real }) => {
|
|
1907
2314
|
const folder = join13(root, dir);
|
|
1908
|
-
if (!
|
|
2315
|
+
if (!isDir(folder) || notWritable(root, dir)) return [];
|
|
1909
2316
|
return readdirSync7(folder).flatMap((name) => {
|
|
1910
2317
|
const path = join13(folder, name);
|
|
1911
|
-
if (!isLink(path) ||
|
|
2318
|
+
if (!isLink(path) || isDir(path) || top && (dir !== GROK_SKILLS_DIR && hasCopy(name) || syncMends(dir, name))) return [];
|
|
1912
2319
|
const remove2 = {
|
|
1913
2320
|
label: "Remove the link",
|
|
2321
|
+
hint: "restorable with skilllib restore",
|
|
1914
2322
|
run: () => {
|
|
1915
|
-
|
|
1916
|
-
|
|
2323
|
+
if (!isLink(path)) return `${dir}/${name} in ${where} is already gone`;
|
|
2324
|
+
stash(path, "tidy-backup");
|
|
2325
|
+
return `Removed the broken link ${dir}/${name} in ${where} (restorable with skilllib restore)`;
|
|
1917
2326
|
}
|
|
1918
2327
|
};
|
|
1919
|
-
|
|
1920
|
-
|
|
2328
|
+
const placeOf = (p) => relative4(realRoot, p).split(sep7).join("/");
|
|
2329
|
+
git2 ??= gitInfo(
|
|
2330
|
+
root,
|
|
2331
|
+
folders2.map((f) => placeOf(f.real)).filter((p) => p !== "" && p !== ".." && !p.startsWith("../") && !isAbsolute3(p))
|
|
2332
|
+
);
|
|
2333
|
+
const tracked = ["committed", "changed"].includes(git2?.of(placeOf(join13(real, name))) ?? "");
|
|
2334
|
+
const recorded = !!own && mends(own, relative4(own, real).split(sep7).join("/"), name);
|
|
2335
|
+
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` : ""}.` : "";
|
|
1921
2336
|
return [
|
|
1922
2337
|
{
|
|
1923
2338
|
id: `broken:${root}:${dir}/${name}`,
|
|
1924
2339
|
severity: "problem",
|
|
1925
2340
|
title: `${name}: broken link in ${where}/${dir}`,
|
|
1926
|
-
detail: `It points at a folder that no longer exists, so it loads nothing.${tracked ? " Git tracks the link, so removing it shows in git status." : ""}`,
|
|
2341
|
+
detail: `It points at ${existsSync12(path) ? "a file, not a skill folder" : "a folder that no longer exists"}, so it loads nothing.${tracked ? " Git tracks the link, so removing it shows in git status." : ""}${pinnedThere}`,
|
|
1927
2342
|
at: rows(root, [name]),
|
|
1928
|
-
|
|
2343
|
+
// Removed by itself only at the repo's top, uncommitted: a package's link (or a committed one) is asked first.
|
|
2344
|
+
...top && !tracked ? { fix: remove2 } : { choices: [remove2] }
|
|
1929
2345
|
}
|
|
1930
2346
|
];
|
|
1931
2347
|
});
|
|
@@ -1956,32 +2372,37 @@ function grokDiffers(projects2, machine, statusOf) {
|
|
|
1956
2372
|
return [...global, ...repos];
|
|
1957
2373
|
}
|
|
1958
2374
|
function findIssues(roots, machine, libraryNames, statusOf = projectStatus, loadedIn = (root) => skillsLoadedIn(root, machine), nestedOf = nestedSkills) {
|
|
2375
|
+
gitMemo = /* @__PURE__ */ new Map();
|
|
1959
2376
|
const { readable: projects2, unreadable } = readableRoots(roots);
|
|
1960
2377
|
const issues = unreadable.map(({ root, reason }) => ({
|
|
1961
2378
|
id: `manifest:${root}`,
|
|
1962
2379
|
severity: "problem",
|
|
1963
2380
|
title: `${basename6(root)}: skilllib.json can't be read`,
|
|
1964
2381
|
detail: `${reason}. Until it's fixed, skilllib leaves ${basename6(root)} out.`,
|
|
1965
|
-
at: []
|
|
2382
|
+
at: [{ root, skill: "" }]
|
|
1966
2383
|
}));
|
|
1967
2384
|
const statuses = new Map(projects2.map((root) => [root, statusOf(root)]));
|
|
2385
|
+
const openedProjects = new Set(projects2.map((p) => realNative(p) ?? p));
|
|
1968
2386
|
issues.push(...grokDiffers(projects2, machine, (root) => statuses.get(root)));
|
|
1969
2387
|
const nested = new Map(projects2.map((root) => [root, nestedOf(root)]));
|
|
1970
2388
|
for (const skill of machine.filter((m) => m.broken)) {
|
|
2389
|
+
const reaches = isDir(skill.path) ? "folder" : existsSync12(skill.path) ? "file" : "nothing";
|
|
2390
|
+
const remove2 = {
|
|
2391
|
+
label: "Remove the link",
|
|
2392
|
+
hint: "restorable with skilllib restore",
|
|
2393
|
+
run: () => {
|
|
2394
|
+
const r = unloadGlobal(skill.path);
|
|
2395
|
+
if (!r.ok) throw new Error(`${skill.name}: ${r.reason}`);
|
|
2396
|
+
return `Removed broken link ${skill.name}`;
|
|
2397
|
+
}
|
|
2398
|
+
};
|
|
1971
2399
|
issues.push({
|
|
1972
2400
|
id: `broken:${skill.path}`,
|
|
1973
2401
|
severity: "problem",
|
|
1974
2402
|
title: `${skill.name}: broken link in ${tildify(dirname6(skill.path))}`,
|
|
1975
|
-
detail: "It points at a folder that no longer exists, so it loads nothing.",
|
|
2403
|
+
detail: reaches === "nothing" ? "It points at a folder that no longer exists, so it loads nothing." : reaches === "file" ? "It points at a file, not a skill folder, so it loads nothing." : "It points at a folder with no SKILL.md: most agents load nothing from it (Cursor may find skills deeper inside).",
|
|
1976
2404
|
at: rows(null, [skill.name]),
|
|
1977
|
-
fix:
|
|
1978
|
-
label: "Remove the link",
|
|
1979
|
-
run: () => {
|
|
1980
|
-
const r = unloadGlobal(skill.path);
|
|
1981
|
-
if (!r.ok) throw new Error(`${skill.name}: ${r.reason}`);
|
|
1982
|
-
return `Removed broken link ${skill.name}`;
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
2405
|
+
...reaches === "folder" ? { choices: [remove2] } : { fix: remove2 }
|
|
1985
2406
|
});
|
|
1986
2407
|
}
|
|
1987
2408
|
const loaded = machine.filter((m) => !m.broken);
|
|
@@ -2006,18 +2427,21 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2006
2427
|
];
|
|
2007
2428
|
const pluginAgentsIn = (plugin, root) => new Set(loadedHere.get(root).filter((m) => plugin.skills.includes(m.path)).flatMap((m) => m.harnesses));
|
|
2008
2429
|
const globalYours = new Set(loaded.filter((m) => m.movable).map((m) => m.name));
|
|
2009
|
-
const installedYours = /* @__PURE__ */ new Set([...globalYours, ...[...statuses.values()].flat().map((s) => s.name)]);
|
|
2430
|
+
const installedYours = /* @__PURE__ */ new Set([...globalYours, ...[...statuses.values()].flat().map((s) => s.name), ...knownProjects().flatMap((p) => Object.keys(pinsIn(p)))]);
|
|
2010
2431
|
const repoChoices = (plugin, root, names) => {
|
|
2011
2432
|
const where = basename6(root);
|
|
2012
2433
|
const copies = statuses.get(root).filter((s) => names.includes(s.name));
|
|
2013
|
-
const allManaged = names.every((n) => copies.some((c) => c.name === n && c
|
|
2434
|
+
const allManaged = names.every((n) => copies.some((c) => c.name === n && removesClean(root, c)));
|
|
2435
|
+
const committed = gitNote(root, copies.map((s) => s.path)) !== "";
|
|
2436
|
+
const pluginAgents = pluginAgentsIn(plugin, root);
|
|
2437
|
+
const leftOut = copies.flatMap((s) => s.visibility.filter((v) => v.paths > 0 && !pluginAgents.has(v.id)).map((v) => v.id));
|
|
2014
2438
|
return [
|
|
2015
2439
|
{ label: `Turn the plugin ${plugin.id} off in ${where} only`, hint: "other repos keep it; in .claude/settings.local.json, a local-only file", run: () => orThrow(turnOffIn(plugin, root)) },
|
|
2016
2440
|
// Only copies skilllib installed: Your skills keeps them. A repo's own skill is the team's, never removed from here.
|
|
2017
|
-
...names.length && allManaged ? [
|
|
2441
|
+
...names.length && allManaged && !committed && !leftOut.length ? [
|
|
2018
2442
|
{
|
|
2019
2443
|
label: `Remove your ${copies.map((s) => s.name).join(", ")} from ${where}`,
|
|
2020
|
-
hint:
|
|
2444
|
+
hint: `the plugin's copy stays; Your skills keeps yours${gitNote(root, [join13(root, MANIFEST_FILE)])}`,
|
|
2021
2445
|
run: () => {
|
|
2022
2446
|
for (const s of copies) {
|
|
2023
2447
|
const r = removeSkill(root, s.name);
|
|
@@ -2058,12 +2482,13 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2058
2482
|
claudeBoth.length ? `Claude Code loads both (the plugin's as /${pluginName}:${claudeBoth[0]}). Your skill wins.` : "",
|
|
2059
2483
|
cursorWheres.length ? `${cursorWheres.join(", ")} ${cursorWheres.length === 1 ? "turns" : "turn"} it off for Claude Code, but Cursor ignores repo settings and lists both there: only turning the plugin off or removing it changes that.` : "",
|
|
2060
2484
|
notInstalled.length ? `Your ${notInstalled.join(", ")} ${notInstalled.length === 1 ? "is" : "are"} only in Your skills, not installed anywhere: once the plugin is gone, add ${notInstalled.length === 1 ? "it" : "them"} where you need ${notInstalled.length === 1 ? "it" : "them"}.` : "",
|
|
2061
|
-
others ? `
|
|
2062
|
-
plugin.extras.length ? `It also brings ${plugin.extras.join(", ")}: ${plugin.synced ? "turning it off pauses those" : "removing it drops those, turning it off pauses them"}.` : "",
|
|
2485
|
+
others ? `Turning it off or removing it copies its other ${others === 1 ? "skill" : `${others} skills`} into Your skills first, so nothing is lost.` : "",
|
|
2486
|
+
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"}.` : "",
|
|
2063
2487
|
plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : "",
|
|
2488
|
+
plugin.settingsOnly ? "It's only turned on in your settings, not installed, so it can only be turned off here." : "",
|
|
2064
2489
|
inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) => basename6(root)).join(", ")}.` : ""
|
|
2065
2490
|
].filter(Boolean).join(" "),
|
|
2066
|
-
choices: [...plugin.synced ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off], ...inRepos.flatMap(([root, d]) => repoChoices(plugin, root, d))],
|
|
2491
|
+
choices: [...plugin.synced || plugin.settingsOnly ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off], ...inRepos.flatMap(([root, d]) => repoChoices(plugin, root, d))],
|
|
2067
2492
|
// Global's rows (yours and the plugin's), and the repos where both load.
|
|
2068
2493
|
at: [...rows(null, dupes), ...[...inRepos, ...cursorRepos].flatMap(([root, d]) => rows(root, d))],
|
|
2069
2494
|
plugin: plugin.id
|
|
@@ -2086,7 +2511,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2086
2511
|
for (const root of projects2) {
|
|
2087
2512
|
const where = basename6(root);
|
|
2088
2513
|
const repoOnly = loadedHere.get(root).filter((m) => m.kind === "plugin" && !machinePluginSkills.has(m.path));
|
|
2089
|
-
for (const plugin of claudePlugins(repoOnly, root)) {
|
|
2514
|
+
for (const plugin of enabledHarnesses().includes("claude-code") ? claudePlugins(repoOnly, root) : []) {
|
|
2090
2515
|
const names = plugin.skills.map((p) => basename6(p));
|
|
2091
2516
|
const inRepo = namesIn2(root).filter((n) => names.includes(n));
|
|
2092
2517
|
const dupes = names.filter((n) => inRepo.includes(n) || globalYours.has(n));
|
|
@@ -2180,19 +2605,37 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2180
2605
|
for (const root of projects2) {
|
|
2181
2606
|
const where = basename6(root);
|
|
2182
2607
|
const status2 = statuses.get(root);
|
|
2183
|
-
issues.push(...brokenLinksIn(root));
|
|
2608
|
+
issues.push(...brokenLinksIn(root, openedProjects));
|
|
2184
2609
|
const pins = readManifest(root).skills;
|
|
2185
|
-
const
|
|
2186
|
-
const
|
|
2187
|
-
|
|
2610
|
+
const blockedFor = /* @__PURE__ */ new Map();
|
|
2611
|
+
const untrackable = (s) => {
|
|
2612
|
+
if (!blockedFor.has(s.name)) blockedFor.set(s.name, trackBlocker(root, s.name));
|
|
2613
|
+
return blockedFor.get(s.name);
|
|
2614
|
+
};
|
|
2615
|
+
const blockers = new Map(status2.filter((s) => s.managed).flatMap((s) => ((b) => b ? [[s.name, b]] : [])(syncBlocker(root, s.name, pins[s.name]))));
|
|
2616
|
+
for (const [name, blocker] of blockers)
|
|
2617
|
+
issues.push({
|
|
2618
|
+
id: `stuck:${root}:${name}`,
|
|
2619
|
+
severity: "problem",
|
|
2620
|
+
title: `${name} in ${where}: sync can't put it back as skilllib.json pins it`,
|
|
2621
|
+
// What to do depends on what's there: a folder that links out of the repo is a setup of yours, and a pin
|
|
2622
|
+
// your library has no version of can't come back by sync anyway.
|
|
2623
|
+
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."}`,
|
|
2624
|
+
at: rows(root, [name])
|
|
2625
|
+
});
|
|
2626
|
+
const unstuck = status2.filter((s) => !blockers.has(s.name));
|
|
2627
|
+
const missing = unstuck.filter((s) => s.state === "folder missing" && s.latest !== null);
|
|
2628
|
+
const knownPin = (s) => versionForHash(s.name, pins[s.name]?.hash ?? "") !== null;
|
|
2629
|
+
const restorable = missing.filter(knownPin);
|
|
2630
|
+
for (const s of unstuck.filter((m) => (m.state === "folder missing" || m.state === "out of sync") && m.latest !== null && !knownPin(m)))
|
|
2188
2631
|
issues.push({
|
|
2189
2632
|
id: `pin:${root}:${s.name}`,
|
|
2190
2633
|
severity: "problem",
|
|
2191
2634
|
title: `${s.name}: the version ${where}'s skilllib.json pins isn't in your library`,
|
|
2192
|
-
detail:
|
|
2635
|
+
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.`}`,
|
|
2193
2636
|
at: rows(root, [s.name]),
|
|
2194
2637
|
// It changes what the shared skilllib.json pins: a choice, never run by `doctor --fix`.
|
|
2195
|
-
choices: [{ label: `Install your newest (v${s.latest})`, hint: `${where}'s skilllib.json pins it`, run: () => `${s.name}: installed v${track(root, s.name).to}` }]
|
|
2638
|
+
...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}` }] }
|
|
2196
2639
|
});
|
|
2197
2640
|
if (restorable.length > 0) {
|
|
2198
2641
|
issues.push({
|
|
@@ -2203,16 +2646,35 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2203
2646
|
at: rows(root, restorable.map((s) => s.name)),
|
|
2204
2647
|
fix: {
|
|
2205
2648
|
label: "Sync the project",
|
|
2206
|
-
|
|
2649
|
+
// Only the missing folders: an out-of-sync copy (maybe committed) is its own issue's choice.
|
|
2650
|
+
run: () => summarize(where, "restored", syncProject(root, { names: restorable.map((s) => s.name) }))
|
|
2207
2651
|
}
|
|
2208
2652
|
});
|
|
2209
2653
|
}
|
|
2210
|
-
|
|
2654
|
+
const drifted = unstuck.filter((s) => s.state === "out of sync" && knownPin(s));
|
|
2655
|
+
if (drifted.length) {
|
|
2656
|
+
const seen = drifted.some((s) => ["committed", "changed"].includes(gitOf(root)?.of(relativeTo(root, s.path)) ?? ""));
|
|
2657
|
+
const yours = drifted.some((s) => untrackable(s));
|
|
2658
|
+
const install = {
|
|
2659
|
+
label: "Install the pinned versions",
|
|
2660
|
+
hint: seen ? "git will see the copies change" : "your library keeps every version",
|
|
2661
|
+
run: () => summarize(where, "synced", syncProject(root, { names: drifted.map((s) => s.name) }))
|
|
2662
|
+
};
|
|
2663
|
+
issues.push({
|
|
2664
|
+
id: `out-of-sync:${root}`,
|
|
2665
|
+
severity: "suggestion",
|
|
2666
|
+
title: `${where}: ${plural(drifted.length, "skill")} not at the version skilllib.json pins`,
|
|
2667
|
+
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.` : ""}`,
|
|
2668
|
+
at: rows(root, drifted.map((s) => s.name)),
|
|
2669
|
+
...seen || yours ? { choices: [install] } : { fix: install }
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
for (const s of unstuck.filter((s2) => s2.state === "folder missing" && s2.latest === null))
|
|
2211
2673
|
issues.push({
|
|
2212
2674
|
id: `unavailable:${root}:${s.name}`,
|
|
2213
2675
|
severity: "problem",
|
|
2214
2676
|
title: `${s.name}: pinned in ${where}'s skilllib.json, but not in your library`,
|
|
2215
|
-
detail: `Its folder is missing, and your library (${tildify(libraryDir())}) has no copy to restore: import it, or set SKILLLIB_HOME to the library
|
|
2677
|
+
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.`,
|
|
2216
2678
|
at: rows(root, [s.name]),
|
|
2217
2679
|
// Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
|
|
2218
2680
|
choices: [
|
|
@@ -2227,7 +2689,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2227
2689
|
}
|
|
2228
2690
|
]
|
|
2229
2691
|
});
|
|
2230
|
-
const behind =
|
|
2692
|
+
const behind = unstuck.filter((s) => s.state === "update available" && !untrackable(s));
|
|
2231
2693
|
if (behind.length > 0)
|
|
2232
2694
|
issues.push({
|
|
2233
2695
|
id: `update:${root}`,
|
|
@@ -2235,14 +2697,18 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2235
2697
|
title: `${where}: ${plural(behind.length, "skill")} out of date`,
|
|
2236
2698
|
detail: behind.map((s) => `${s.name} (v${s.version} \u2192 v${s.latest})`).join(", "),
|
|
2237
2699
|
at: rows(root, behind.map((s) => s.name)),
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2700
|
+
// It changes what the repo pins and the copies its agents load (often committed): a choice, never `doctor --fix`.
|
|
2701
|
+
choices: [
|
|
2702
|
+
{
|
|
2703
|
+
label: "Update to the newest versions",
|
|
2704
|
+
hint: ["committed", "changed"].includes(gitOf(root)?.of(MANIFEST_FILE) ?? "") ? "git will see the new copies and skilllib.json" : "replaces each copy with your library's newest",
|
|
2705
|
+
run: () => summarize(where, "updated", updateProject(root, behind.map((s) => s.name)))
|
|
2706
|
+
}
|
|
2707
|
+
]
|
|
2242
2708
|
});
|
|
2243
2709
|
const tidy2 = planProjectTidy(root);
|
|
2244
2710
|
if (tidy2.plans.length) {
|
|
2245
|
-
const git3 =
|
|
2711
|
+
const git3 = gitOf(root);
|
|
2246
2712
|
const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p, git3).map((s) => describeStep(s, root)));
|
|
2247
2713
|
const run = (mode) => () => {
|
|
2248
2714
|
const r = applyAll(tidy2.plans, mode);
|
|
@@ -2253,7 +2719,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2253
2719
|
id: `tidy:${root}`,
|
|
2254
2720
|
severity: "suggestion",
|
|
2255
2721
|
title: `${where}: ${plural(tidy2.plans.length, "skill")} with extra copies or links`,
|
|
2256
|
-
detail: tidy2.plans.map((p) => `${p.name}: ${p.steps.map((s) => describeStep(s, root)).join(", ")}`).join(" \xB7 ") + (visible2.length ? `. Git would see ${plural(visible2.length, "of these change")}: ${visible2.join(", ")}.` : ". Replaced copies are restorable with skilllib restore."),
|
|
2722
|
+
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." : "."),
|
|
2257
2723
|
at: rows(root, tidy2.plans.map((p) => p.name)),
|
|
2258
2724
|
...visible2.length ? {
|
|
2259
2725
|
choices: [
|
|
@@ -2265,8 +2731,47 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2265
2731
|
});
|
|
2266
2732
|
}
|
|
2267
2733
|
issues.push(...tidy2.conflicts.map(conflictIssue));
|
|
2268
|
-
|
|
2734
|
+
for (const s of tidy2.skipped.filter((x) => x.differs))
|
|
2735
|
+
issues.push({
|
|
2736
|
+
id: `linkdiffers:${root}:${s.name}`,
|
|
2737
|
+
severity: "problem",
|
|
2738
|
+
title: `${s.name} in ${where}: the copies differ`,
|
|
2739
|
+
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.`,
|
|
2740
|
+
at: rows(root, [s.name])
|
|
2741
|
+
});
|
|
2742
|
+
const loadsGlobally = (name, id) => {
|
|
2743
|
+
const here = status2.find((s) => s.name === name);
|
|
2744
|
+
return globalCopies.some((m) => m.name === name && m.harnesses.includes(id) && (!here || treeHash(m.path) === treeHash(here.path)));
|
|
2745
|
+
};
|
|
2269
2746
|
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 }) }));
|
|
2747
|
+
const enabledIds = enabledHarnesses();
|
|
2748
|
+
const notFolders = /* @__PURE__ */ new Set();
|
|
2749
|
+
for (const dir of new Set(enabledIds.map((id) => harness(id).projectDirs[0]))) {
|
|
2750
|
+
const blocker = notWritable(root, dir);
|
|
2751
|
+
if (!blocker || notFolders.has(blocker)) continue;
|
|
2752
|
+
notFolders.add(blocker);
|
|
2753
|
+
const who = agentNames(enabledIds.filter((id) => notWritable(root, harness(id).projectDirs[0]) === blocker));
|
|
2754
|
+
const out2 = linksOut(root, blocker);
|
|
2755
|
+
issues.push({
|
|
2756
|
+
id: `notdir:${root}:${blocker}`,
|
|
2757
|
+
severity: out2 ? "suggestion" : "problem",
|
|
2758
|
+
title: out2 ? `${where}: ${blocker} links out of the repo` : `${where}: ${blocker} isn't a folder`,
|
|
2759
|
+
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.`,
|
|
2760
|
+
at: [{ root, skill: "" }]
|
|
2761
|
+
// the repo itself: its row in Places
|
|
2762
|
+
});
|
|
2763
|
+
}
|
|
2764
|
+
for (const { s, would } of blindHere) {
|
|
2765
|
+
const way = would.inTheWay.filter((p) => !notFolders.has(p));
|
|
2766
|
+
if (!way.length || blockers.has(s.name)) continue;
|
|
2767
|
+
issues.push({
|
|
2768
|
+
id: `intheway:${root}:${s.name}`,
|
|
2769
|
+
severity: "problem",
|
|
2770
|
+
title: `${s.name} in ${where}: ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))} can't use it here`,
|
|
2771
|
+
detail: `${inTheWaySaid(root, way)}. skilllib won't link past what's there: clear the way, then run skilllib link.`,
|
|
2772
|
+
at: rows(root, [s.name])
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2270
2775
|
for (const { s, would } of blindHere.filter(({ would: w }) => !w.created.length && !w.blocked.length && w.uncommitted.length))
|
|
2271
2776
|
issues.push({
|
|
2272
2777
|
id: `uncommitted:${root}:${s.name}`,
|
|
@@ -2279,20 +2784,25 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2279
2784
|
if (partial.length) {
|
|
2280
2785
|
const missing2 = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
|
|
2281
2786
|
const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
|
|
2787
|
+
const manifestShared = partial.some((s) => s.managed) && ["committed", "changed"].includes(gitOf(root)?.of(MANIFEST_FILE) ?? "");
|
|
2282
2788
|
const run = (allowTracked) => () => {
|
|
2283
2789
|
if (allowTracked) allowTrackedLinks(root);
|
|
2284
|
-
const r = linkAll(root, { allowTracked });
|
|
2790
|
+
const r = linkAll(root, { allowTracked, names: partial.map((s) => s.name) });
|
|
2285
2791
|
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)` : ""}`;
|
|
2286
|
-
if (!r.linked.length)
|
|
2792
|
+
if (!r.linked.length) {
|
|
2793
|
+
const still = partial.filter((s) => visibilityOf(root, s.name).some((v) => v.paths === 0 && !loadsGlobally(s.name, v.id)));
|
|
2794
|
+
if (!still.length) return `${where}: already usable by ${agentNames(missing2)}`;
|
|
2795
|
+
throw new Error(`${where}: nothing linked${notes}`);
|
|
2796
|
+
}
|
|
2287
2797
|
return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing2)}${notes}`;
|
|
2288
2798
|
};
|
|
2289
2799
|
issues.push({
|
|
2290
2800
|
id: `usable:${root}`,
|
|
2291
2801
|
severity: "suggestion",
|
|
2292
2802
|
title: `${where}: ${plural(partial.length, "skill")} not usable by ${agentNames(missing2)}`,
|
|
2293
|
-
detail: `${partial.map((s) => `${s.name} (not ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))})`).join(", ")}. Adding links makes the one real copy reach every agent you use; nothing is copied or moved.${tracked.length ? ` Git tracks ${tracked.join(", ")}, so links there show in git status.` : ""}`,
|
|
2803
|
+
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." : ""}`,
|
|
2294
2804
|
at: rows(root, partial.map((s) => s.name)),
|
|
2295
|
-
...tracked.length ? {
|
|
2805
|
+
...manifestShared && !tracked.length ? { choices: [{ label: "Add the links", hint: "git will see skilllib.json change", run: run(false) }] } : tracked.length ? {
|
|
2296
2806
|
choices: [
|
|
2297
2807
|
// Only when some link goes where git doesn't track: otherwise this one would link nothing.
|
|
2298
2808
|
...blindHere.some(({ would }) => would.created.length) ? [{ label: "Add links, but not where git tracks the folder", hint: `skips ${tracked.join(", ")}`, run: run(false) }] : [],
|
|
@@ -2302,16 +2812,17 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2302
2812
|
});
|
|
2303
2813
|
}
|
|
2304
2814
|
const differing = new Set(tidy2.conflicts.map((c) => c.name));
|
|
2305
|
-
const git2 =
|
|
2815
|
+
const git2 = gitOf(root);
|
|
2306
2816
|
const committed = (s) => ["committed", "changed"].includes(git2?.of(relativeTo(root, s.path)) ?? "");
|
|
2307
|
-
for (const s of
|
|
2817
|
+
for (const s of unstuck) {
|
|
2308
2818
|
if (isLink(s.path) || differing.has(s.name)) continue;
|
|
2309
|
-
if (s.state === "local only" && committed(s)) {
|
|
2819
|
+
if (s.state === "local only" && (committed(s) || untrackable(s))) {
|
|
2820
|
+
const why = committed(s) ? "it's the team's (committed)" : `${untrackable(s)}, so skilllib can't track it here`;
|
|
2310
2821
|
issues.push({
|
|
2311
2822
|
id: `local:${root}:${s.name}`,
|
|
2312
2823
|
severity: "suggestion",
|
|
2313
2824
|
title: `${s.name} in ${where} isn't in your library`,
|
|
2314
|
-
detail:
|
|
2825
|
+
detail: `Copy it into the library to reuse it in other projects. The repo's copy stays as it is: ${why}.`,
|
|
2315
2826
|
at: rows(root, [s.name]),
|
|
2316
2827
|
choices: [
|
|
2317
2828
|
{
|
|
@@ -2334,7 +2845,8 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2334
2845
|
choices: [
|
|
2335
2846
|
{
|
|
2336
2847
|
label: "Import into the library",
|
|
2337
|
-
|
|
2848
|
+
// Tracking it writes skilllib.json.
|
|
2849
|
+
hint: `copies it into Your skills${gitNote(root, [join13(root, MANIFEST_FILE)])}`,
|
|
2338
2850
|
run: () => {
|
|
2339
2851
|
intoLibrary(s.path);
|
|
2340
2852
|
track(root, s.name);
|
|
@@ -2343,7 +2855,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2343
2855
|
}
|
|
2344
2856
|
]
|
|
2345
2857
|
});
|
|
2346
|
-
} else if (s.state === "untracked copy of library skill") {
|
|
2858
|
+
} else if (s.state === "untracked copy of library skill" && !committed(s) && !untrackable(s)) {
|
|
2347
2859
|
issues.push({
|
|
2348
2860
|
id: `adopt:${root}:${s.name}`,
|
|
2349
2861
|
severity: "suggestion",
|
|
@@ -2353,12 +2865,13 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2353
2865
|
choices: [{ label: "Track it", hint: `adds it to ${where}'s skilllib.json`, run: () => `${track(root, s.name).name} tracked in ${where}` }]
|
|
2354
2866
|
});
|
|
2355
2867
|
} else if (s.state === "untracked, differs from library" || s.state === "repo skill, differs from library") {
|
|
2356
|
-
const teams = s.state.startsWith("repo skill") || committed(s);
|
|
2868
|
+
const teams = s.state.startsWith("repo skill") || committed(s) || !!untrackable(s);
|
|
2869
|
+
const whose = s.state.startsWith("repo skill") || committed(s) ? "It's the team's copy (committed, or in the repo's own .agents/skills)" : `${untrackable(s)}, so skilllib can't track it here`;
|
|
2357
2870
|
issues.push({
|
|
2358
2871
|
id: `differs:${root}:${s.name}`,
|
|
2359
2872
|
severity: "suggestion",
|
|
2360
2873
|
title: `${s.name} in ${where} differs from your library`,
|
|
2361
|
-
detail: teams ?
|
|
2874
|
+
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.`,
|
|
2362
2875
|
at: rows(root, [s.name]),
|
|
2363
2876
|
choices: [
|
|
2364
2877
|
{
|
|
@@ -2384,13 +2897,13 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2384
2897
|
]
|
|
2385
2898
|
]
|
|
2386
2899
|
});
|
|
2387
|
-
} else if (s.state === "from npx skills") {
|
|
2388
|
-
const teams = committed(s);
|
|
2900
|
+
} else if (s.state === "from npx skills" && !((committed(s) || untrackable(s)) && versionForHash(s.name, treeHash(s.path) ?? "") !== null)) {
|
|
2901
|
+
const teams = committed(s) || !!untrackable(s);
|
|
2389
2902
|
issues.push({
|
|
2390
2903
|
id: `npx:${root}:${s.name}`,
|
|
2391
2904
|
severity: "suggestion",
|
|
2392
2905
|
title: `${s.name} in ${where} came from npx skills, not your library`,
|
|
2393
|
-
detail: `Installed with \`npx skills add\`${s.source ? ` (${s.source})` : ""}. Import it into your library to reuse it in other projects and keep it backed up${teams ? `; ${where}'s copy stays as it is (committed)` : `; ${where} then tracks it in skilllib.json`}.`,
|
|
2906
|
+
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`}.`,
|
|
2394
2907
|
at: rows(root, [s.name]),
|
|
2395
2908
|
choices: [
|
|
2396
2909
|
{
|
|
@@ -2431,12 +2944,21 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2431
2944
|
id: `edited:${root}:${s.name}`,
|
|
2432
2945
|
severity: "suggestion",
|
|
2433
2946
|
title: `${s.name} in ${where} has local edits`,
|
|
2434
|
-
detail: `It no longer matches ${pinned} from Your skills${newer2 ? `, and v${s.latest} is out` : ""}. Save the edits as a new version so your other repos can get them, or discard them. Keeping them is fine too: skilllib never overwrites edits
|
|
2947
|
+
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."}`,
|
|
2435
2948
|
at: rows(root, [s.name]),
|
|
2436
|
-
choices: [
|
|
2949
|
+
choices: untrackable(s) ? [
|
|
2950
|
+
{
|
|
2951
|
+
label: "Copy the edits into Your skills",
|
|
2952
|
+
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)`,
|
|
2953
|
+
run: () => {
|
|
2954
|
+
intoLibrary(s.path, true);
|
|
2955
|
+
return `${s.name}: the edits are in Your skills as v${latestVersion(s.name)?.version}; ${where} stays as it is`;
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
] : [
|
|
2437
2959
|
{
|
|
2438
2960
|
label: "Save as a new version in Your skills",
|
|
2439
|
-
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`,
|
|
2961
|
+
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)])}`,
|
|
2440
2962
|
run: () => {
|
|
2441
2963
|
importSkill(s.path, { force: true });
|
|
2442
2964
|
const c = addSkill(root, s.name);
|
|
@@ -2446,7 +2968,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2446
2968
|
},
|
|
2447
2969
|
{
|
|
2448
2970
|
label: "Discard the edits",
|
|
2449
|
-
hint: `back to ${pinned}; the edited copy is restorable with skilllib restore`,
|
|
2971
|
+
hint: `back to ${pinned}; the edited copy is restorable with skilllib restore${gitNote(root, [s.path, join13(root, MANIFEST_FILE)])}`,
|
|
2450
2972
|
run: () => {
|
|
2451
2973
|
const c = discardEdits(root, s.name);
|
|
2452
2974
|
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
@@ -2465,26 +2987,42 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2465
2987
|
if (alsoLoadedGlobally(n) && !copies.some((c) => c.root === root)) alsoGlobal.set(n.name, [...copies, { root, managed: false, nested: true }]);
|
|
2466
2988
|
}
|
|
2467
2989
|
}
|
|
2990
|
+
const everyRepo = [...projects2, ...knownProjects()];
|
|
2991
|
+
const nestedMemo = /* @__PURE__ */ new Map();
|
|
2992
|
+
const subsMemo = /* @__PURE__ */ new Map();
|
|
2993
|
+
const subsOf = (root) => subsMemo.get(root) ?? (subsMemo.set(root, packageDirs(root)), subsMemo.get(root));
|
|
2994
|
+
const entriesOf2 = (root) => nestedMemo.get(root) ?? (nestedMemo.set(root, nestedEntries(root, { subs: subsOf(root) })), nestedMemo.get(root));
|
|
2468
2995
|
for (const [name, copies] of alsoGlobal) {
|
|
2469
2996
|
const wheres = copies.map((c) => basename6(c.root)).join(", ");
|
|
2470
|
-
const removable = copies.filter((c) => c.managed).map((c) => c.root);
|
|
2997
|
+
const removable = copies.filter((c) => c.managed && statuses.get(c.root)?.some((s) => s.name === name && removesClean(c.root, s))).map((c) => c.root);
|
|
2471
2998
|
const repoOwn = copies.filter((c) => !c.managed).map((c) => basename6(c.root));
|
|
2472
2999
|
const at = [...copies.flatMap((c) => rows(c.root, [name])), ...rows(null, [name])];
|
|
2473
3000
|
if (kept.has(name)) {
|
|
2474
|
-
const
|
|
3001
|
+
const globalLoaders = new Set(globalCopies.filter((m) => m.name === name).flatMap((m) => m.harnesses));
|
|
3002
|
+
const onlyHere = (root) => visibilityOf(root, name).filter((v) => v.paths > 0 && !globalLoaders.has(v.id)).map((v) => harness(v.id).name);
|
|
3003
|
+
const needed = removable.filter((root) => onlyHere(root).length);
|
|
3004
|
+
const extra = removable.filter((root) => !onlyHere(root).length);
|
|
3005
|
+
const removeWheres = extra.map((r) => basename6(r)).join(", ");
|
|
2475
3006
|
issues.push({
|
|
2476
3007
|
id: `twice:${name}`,
|
|
2477
3008
|
severity: "suggestion",
|
|
2478
3009
|
title: `${name}: in ${wheres}, and you keep it global`,
|
|
2479
|
-
detail:
|
|
3010
|
+
detail: [
|
|
3011
|
+
extra.length ? `The global copy already loads in ${removeWheres} for every agent that reads the project copy, so those are extra.` : "",
|
|
3012
|
+
needed.length ? `In ${needed.map((r) => `${basename6(r)} (${onlyHere(r).join(", ")})`).join(", ")}, some agents read only the repo's copy: keep it there, or link the global one for them.` : "",
|
|
3013
|
+
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.` : ""
|
|
3014
|
+
].filter(Boolean).join(" "),
|
|
2480
3015
|
at,
|
|
2481
|
-
choices:
|
|
3016
|
+
choices: extra.length ? [
|
|
2482
3017
|
{
|
|
2483
3018
|
label: `Remove the copies in ${removeWheres}`,
|
|
2484
|
-
|
|
3019
|
+
// Removing a managed copy edits its skilllib.json too.
|
|
3020
|
+
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" : ""}`,
|
|
2485
3021
|
run: () => {
|
|
2486
|
-
const
|
|
2487
|
-
|
|
3022
|
+
const results = extra.map((root) => [root, removeSkill(root, name)]);
|
|
3023
|
+
const removed = results.filter(([, c]) => c.action === "removed").map(([r]) => basename6(r));
|
|
3024
|
+
const kept2 = results.filter(([, c]) => c.action !== "removed").map(([r, c]) => `${basename6(r)} (${c.reason})`);
|
|
3025
|
+
if (kept2.length) throw new Error(`${name}: ${removed.length ? `removed from ${removed.join(", ")}; ` : ""}kept in ${kept2.join(", ")}`);
|
|
2488
3026
|
return `${name}: removed from ${removed.join(", ")}; the global copy stays`;
|
|
2489
3027
|
}
|
|
2490
3028
|
}
|
|
@@ -2492,26 +3030,31 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2492
3030
|
});
|
|
2493
3031
|
continue;
|
|
2494
3032
|
}
|
|
2495
|
-
const movable = machine.find((m) => m.name === name && m.movable);
|
|
3033
|
+
const movable = machine.find((m) => m.name === name && m.movable && !m.broken);
|
|
3034
|
+
const doomed = machine.filter((x) => x.name === name && x.movable);
|
|
3035
|
+
const differ = copiesDiffer(doomed.map((m) => m.path));
|
|
3036
|
+
const libraryDiffers = !!movable && libraryNames.has(name) && versionForHash(name, treeHash(movable.path) ?? "") === null;
|
|
3037
|
+
const blind = movable && !differ && !libraryDiffers ? blindWithoutGlobal(name, doomed, everyRepo, entriesOf2, subsOf) : [];
|
|
2496
3038
|
issues.push({
|
|
2497
3039
|
at,
|
|
2498
3040
|
id: `twice:${name}`,
|
|
2499
3041
|
severity: "suggestion",
|
|
2500
3042
|
title: `${name}: in ${wheres} and also loaded globally`,
|
|
2501
|
-
detail: `${repoOwn.length === copies.length ? `Agents there load both the repo's copy and the global one.` : "The project copies are enough; the global one loads it in every other project too."} ${movable ?
|
|
2502
|
-
choices: movable ? [
|
|
3043
|
+
detail: `${repoOwn.length === copies.length ? `Agents there load both the repo's copy and the global one.` : "The project copies are enough; the global one loads it in every other project too."} ${!movable ? `The global copy comes from ${machine.find((m) => m.name === name && !m.movable)?.origin ?? "a vendor"}: turn it off there.` : differ ? `Your global copies of ${name} differ: keep one first (skilllib tidy --global), then stop loading it globally.` : libraryDiffers ? `The global copy isn't a version in Your skills (agents loading it globally run different content than the repo's): update your library from it first (skilllib import ${tildify(movable.path)} --force), or keep it global (skilllib global keep).` : blind.length ? `${blindSaid(name, blind)}.` : "Stop loading it globally, or keep it global on purpose (skilllib global keep)."}`,
|
|
3044
|
+
choices: movable && !differ && !libraryDiffers && !blind.length ? [
|
|
2503
3045
|
{
|
|
2504
3046
|
label: "Stop loading it globally",
|
|
2505
3047
|
hint: "restorable with skilllib restore",
|
|
2506
3048
|
run: () => {
|
|
2507
|
-
const
|
|
2508
|
-
if (
|
|
3049
|
+
const now = blindWithoutGlobal(name, doomed, everyRepo);
|
|
3050
|
+
if (now.length) throw new Error(`${name}: ${blindSaid(name, now)}`);
|
|
2509
3051
|
if (!libraryNames.has(name)) importSkill(movable.path);
|
|
3052
|
+
const left = strandedLinks(name, doomed, everyRepo);
|
|
2510
3053
|
for (const m of machine.filter((x) => x.name === name && x.movable)) {
|
|
2511
3054
|
const r = unloadGlobal(m.path, m.links);
|
|
2512
3055
|
if (!r.ok) throw new Error(`${name}: ${r.reason}`);
|
|
2513
3056
|
}
|
|
2514
|
-
return `${name} no longer loads globally`;
|
|
3057
|
+
return `${name} no longer loads globally${left.length ? `; ${left.join(", ")} now point${left.length === 1 ? "s" : ""} at nothing: remove ${left.length === 1 ? "it" : "them"}` : ""}`;
|
|
2515
3058
|
}
|
|
2516
3059
|
}
|
|
2517
3060
|
] : void 0
|
|
@@ -2548,6 +3091,7 @@ function addedAt(path) {
|
|
|
2548
3091
|
}
|
|
2549
3092
|
}
|
|
2550
3093
|
function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectStatus) {
|
|
3094
|
+
gitMemo = /* @__PURE__ */ new Map();
|
|
2551
3095
|
if (!enabledHarnesses().includes("claude-code")) return [];
|
|
2552
3096
|
const issues = [];
|
|
2553
3097
|
const since = Date.now() - uses.days * 24 * 60 * 60 * 1e3;
|
|
@@ -2555,23 +3099,23 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
|
|
|
2555
3099
|
const where = basename6(root);
|
|
2556
3100
|
const unused = statusOf(root).filter((s) => s.state !== "folder missing" && uses.inProject(root, s.name) === 0 && addedAt(s.path) <= since);
|
|
2557
3101
|
if (!unused.length) continue;
|
|
2558
|
-
const removable = unused.filter((s) => s
|
|
3102
|
+
const removable = unused.filter((s) => removesClean(root, s));
|
|
2559
3103
|
issues.push({
|
|
2560
3104
|
id: `unused:${root}`,
|
|
2561
3105
|
severity: "suggestion",
|
|
2562
|
-
title: `${where}: ${plural(unused.length, "skill")} unused in ${uses.days}
|
|
2563
|
-
detail: `${unused.map((s) => s.name).join(", ")}: no Claude Code session in ${where} used ${unused.length === 1 ? "it" : "them"}. Each one's name and description still takes context in every session there.${unused.
|
|
3106
|
+
title: `${where}: ${plural(unused.length, "skill")} unused in ${plural(uses.days, "day")}`,
|
|
3107
|
+
detail: `${unused.map((s) => s.name).join(", ")}: no Claude Code session in ${where} used ${unused.length === 1 ? "it" : "them"}. Each one's name and description still takes context in every session there.${unused.some((s) => !s.managed) ? " The repo's own skills are the team's: remove those in the repo if nobody needs them." : ""}${unused.some((s) => s.managed && !removable.includes(s)) ? ` Not offered: ${unused.filter((s) => s.managed && !removable.includes(s)).map((s) => `${s.name} (${s.state.startsWith("edited") ? "edited" : s.state === "not in library" ? "not in your library" : "something is in sync's way"})`).join(", ")}; their own issues say what to do first.` : ""}`,
|
|
2564
3108
|
at: rows(root, unused.map((s) => s.name)),
|
|
2565
3109
|
choices: removable.length ? [
|
|
2566
3110
|
{
|
|
2567
3111
|
label: `Remove ${removable.length === unused.length ? "them" : `the ${plural(removable.length, "skill")} skilllib installed`} from ${where}`,
|
|
2568
|
-
hint:
|
|
3112
|
+
hint: `Your skills keeps them; edited ones stay${gitNote(root, [...removable.map((s) => s.path), join13(root, MANIFEST_FILE)])}`,
|
|
2569
3113
|
run: () => {
|
|
2570
3114
|
const changes = removable.map((s) => removeSkill(root, s.name));
|
|
2571
3115
|
const removed = changes.filter((c) => c.action === "removed").map((c) => c.name);
|
|
2572
3116
|
const kept = changes.filter((c) => c.action !== "removed").map((c) => c.name);
|
|
2573
|
-
if (
|
|
2574
|
-
return `${where}: removed ${removed.join(", ")}
|
|
3117
|
+
if (kept.length) throw new Error(`${where}: ${removed.length ? `removed ${removed.join(", ")}; ` : "removed nothing; "}kept ${kept.join(", ")} (${changes.find((c) => c.action !== "removed")?.reason ?? "skipped"})`);
|
|
3118
|
+
return `${where}: removed ${removed.join(", ")}`;
|
|
2575
3119
|
}
|
|
2576
3120
|
}
|
|
2577
3121
|
] : void 0
|
|
@@ -2584,11 +3128,11 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
|
|
|
2584
3128
|
issues.push({
|
|
2585
3129
|
id: `plugin-unused:${installId(p)}`,
|
|
2586
3130
|
severity: "suggestion",
|
|
2587
|
-
title: `${p.id}${installedIn(p)}: its skills weren't used in ${uses.days}
|
|
3131
|
+
title: `${p.id}${installedIn(p)}: its skills weren't used in ${plural(uses.days, "day")}`,
|
|
2588
3132
|
detail: `No Claude Code session used ${p.skills.map((path) => `/${name}:${basename6(path)}`).join(", ")}. Each one's name and description still takes context in every session.${p.extras.length ? ` It also brings ${p.extras.join(", ")}: turning it off pauses those too.` : ""}`,
|
|
2589
3133
|
choices: [
|
|
2590
|
-
{ label: `Turn the plugin ${p.id} off`, hint: "stays installed", run: () => orThrow(setPluginOn(p, false)) },
|
|
2591
|
-
...p.synced || p.settingsOnly ? [] : [{ label: `Uninstall the plugin ${p.id}`, hint:
|
|
3134
|
+
{ label: `Turn the plugin ${p.id} off`, hint: p.scope === "project" ? `stays installed; edits ${basename6(p.projectPath ?? "the repo")}'s committed .claude/settings.json` : "stays installed", run: () => orThrow(setPluginOn(p, false)) },
|
|
3135
|
+
...p.synced || p.settingsOnly ? [] : [{ label: `Uninstall the plugin ${p.id}`, hint: `its data stays; restorable with skilllib restore${p.scope === "project" ? `; edits ${basename6(p.projectPath ?? "the repo")}'s committed .claude/settings.json` : ""}`, run: () => orThrow(uninstallPlugin(p)) }]
|
|
2592
3136
|
],
|
|
2593
3137
|
plugin: p.id,
|
|
2594
3138
|
pluginAt: p.projectPath ?? null
|
|
@@ -2603,7 +3147,7 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
|
|
|
2603
3147
|
issues.push({
|
|
2604
3148
|
id: "unused:library",
|
|
2605
3149
|
severity: "suggestion",
|
|
2606
|
-
title: `${plural(idle.length, "skill")} in Your skills: in no repo, unused in ${uses.days}
|
|
3150
|
+
title: `${plural(idle.length, "skill")} in Your skills: in no repo, unused in ${plural(uses.days, "day")}`,
|
|
2607
3151
|
detail: `${idle.sort().join(", ")}. They load nowhere, so they take no context: add the ones you want to a repo, or delete the ones you've outgrown.`,
|
|
2608
3152
|
// Your skills' rows (none loads globally, so none is in Global).
|
|
2609
3153
|
at: rows(null, idle),
|
|
@@ -2636,13 +3180,12 @@ function groupBy(items) {
|
|
|
2636
3180
|
}
|
|
2637
3181
|
var sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
|
|
2638
3182
|
function globalFrom(kind, origin, path) {
|
|
2639
|
-
if (kind === "global" || kind === "skilllib") return tildify(path.replace(/[\\/][^\\/]+$/, ""))
|
|
3183
|
+
if (kind === "global" || kind === "skilllib") return tildify(path.replace(/[\\/][^\\/]+$/, ""));
|
|
2640
3184
|
return origin;
|
|
2641
3185
|
}
|
|
2642
|
-
var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "target", "vendor", "coverage"]);
|
|
2643
3186
|
function topLevel(root) {
|
|
2644
3187
|
try {
|
|
2645
|
-
return readdirSync8(root, { withFileTypes: true }).filter((d) => !d.name.startsWith(".") && !
|
|
3188
|
+
return readdirSync8(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);
|
|
2646
3189
|
} catch {
|
|
2647
3190
|
return [];
|
|
2648
3191
|
}
|
|
@@ -2664,7 +3207,7 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2664
3207
|
const usual = s.state === "ok" || s.state === "local only" || repo && s.state !== "from npx skills";
|
|
2665
3208
|
const attrs = {
|
|
2666
3209
|
source: s.managed ? "library" : repo ? "repo" : "local",
|
|
2667
|
-
dir: s.location !==
|
|
3210
|
+
dir: s.location !== PROJECT_SKILLS_DIR ? s.location : void 0,
|
|
2668
3211
|
version: s.version ?? void 0,
|
|
2669
3212
|
state: usual ? void 0 : s.state,
|
|
2670
3213
|
latest: behind ? s.latest : void 0,
|
|
@@ -2674,7 +3217,14 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2674
3217
|
return [JSON.stringify(attrs), s.name, () => ({ ...attrs, skills: [] })];
|
|
2675
3218
|
})
|
|
2676
3219
|
);
|
|
2677
|
-
const
|
|
3220
|
+
const own = (() => {
|
|
3221
|
+
try {
|
|
3222
|
+
return inRepo ? projectStatus(root) : [];
|
|
3223
|
+
} catch {
|
|
3224
|
+
return [];
|
|
3225
|
+
}
|
|
3226
|
+
})();
|
|
3227
|
+
const skills = group(own);
|
|
2678
3228
|
const nested = group(nestedHere);
|
|
2679
3229
|
const machine = machineSkills(agents);
|
|
2680
3230
|
const loaded = inRepo ? skillsLoadedIn(root, machine, agents) : machine;
|
|
@@ -2688,7 +3238,7 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2688
3238
|
for (const i of statusIssues(root, machine)) {
|
|
2689
3239
|
const choices = i.choices?.map((c) => c.label);
|
|
2690
3240
|
const key = `${i.detail}\0${i.fix?.label ?? ""}\0${choices?.join() ?? ""}`;
|
|
2691
|
-
const issue = issues.get(key) ?? { problems: [], ids: [], detail: i
|
|
3241
|
+
const issue = issues.get(key) ?? { problems: [], ids: [], detail: detailWithGit(i), fix: i.fix?.label, choices };
|
|
2692
3242
|
issue.problems.push(i.title);
|
|
2693
3243
|
issue.ids.push(i.id);
|
|
2694
3244
|
issues.set(key, issue);
|
|
@@ -2807,29 +3357,41 @@ function usesFromTranscriptLine(line) {
|
|
|
2807
3357
|
function transcriptFiles(sinceMs) {
|
|
2808
3358
|
const base = join14(claudeDir(), "projects");
|
|
2809
3359
|
if (!existsSync13(base)) return [];
|
|
2810
|
-
|
|
3360
|
+
const list2 = (dir) => {
|
|
3361
|
+
try {
|
|
3362
|
+
return readdirSync9(dir);
|
|
3363
|
+
} catch {
|
|
3364
|
+
return [];
|
|
3365
|
+
}
|
|
3366
|
+
};
|
|
3367
|
+
return list2(base).flatMap((project) => {
|
|
2811
3368
|
const dir = join14(base, project);
|
|
2812
|
-
if (!statSync5(dir)
|
|
2813
|
-
return
|
|
3369
|
+
if (!statSync5(dir, { throwIfNoEntry: false })?.isDirectory()) return [];
|
|
3370
|
+
return list2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join14(dir, f)).filter((f) => (statSync5(f, { throwIfNoEntry: false })?.mtimeMs ?? 0) >= sinceMs);
|
|
2814
3371
|
});
|
|
2815
3372
|
}
|
|
2816
3373
|
async function scanUsage(days) {
|
|
2817
3374
|
const sinceMs = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
2818
3375
|
const seen = /* @__PURE__ */ new Map();
|
|
2819
3376
|
for (const file of transcriptFiles(sinceMs)) {
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
for (const
|
|
2823
|
-
|
|
3377
|
+
try {
|
|
3378
|
+
const lines = createInterface({ input: createReadStream(file), crlfDelay: Infinity });
|
|
3379
|
+
for await (const line of lines) {
|
|
3380
|
+
for (const use of usesFromTranscriptLine(line)) {
|
|
3381
|
+
if (Date.parse(use.at) >= sinceMs) seen.set(use.key, use);
|
|
3382
|
+
}
|
|
2824
3383
|
}
|
|
3384
|
+
} catch {
|
|
3385
|
+
continue;
|
|
2825
3386
|
}
|
|
2826
3387
|
}
|
|
2827
3388
|
return [...seen.values()];
|
|
2828
3389
|
}
|
|
3390
|
+
var skillOf = (key) => key.split(":").pop();
|
|
2829
3391
|
function summarize2(uses, projectOf = () => null) {
|
|
2830
3392
|
const bySkill = /* @__PURE__ */ new Map();
|
|
2831
3393
|
for (const use of uses) {
|
|
2832
|
-
const skill = use.skill
|
|
3394
|
+
const skill = skillOf(use.skill);
|
|
2833
3395
|
const summary = bySkill.get(skill) ?? { skill, uses: 0, lastUsed: "", projects: /* @__PURE__ */ new Set() };
|
|
2834
3396
|
summary.uses += 1;
|
|
2835
3397
|
if (use.at > summary.lastUsed) summary.lastUsed = use.at;
|
|
@@ -2845,7 +3407,8 @@ function usesByProject(uses, projectOf) {
|
|
|
2845
3407
|
const root = projectOf(use.cwd);
|
|
2846
3408
|
if (!root) continue;
|
|
2847
3409
|
const counts = byProject.get(root) ?? /* @__PURE__ */ new Map();
|
|
2848
|
-
|
|
3410
|
+
const skill = skillOf(use.skill);
|
|
3411
|
+
counts.set(skill, (counts.get(skill) ?? 0) + 1);
|
|
2849
3412
|
byProject.set(root, counts);
|
|
2850
3413
|
}
|
|
2851
3414
|
return byProject;
|
|
@@ -2857,28 +3420,39 @@ function moveGlobalSkills(skills, roots, {
|
|
|
2857
3420
|
group,
|
|
2858
3421
|
allowTracked = false,
|
|
2859
3422
|
label = basename8,
|
|
2860
|
-
|
|
3423
|
+
allowTrackedHint = "re-run with --allow-tracked"
|
|
2861
3424
|
} = {}) {
|
|
2862
3425
|
const byName = /* @__PURE__ */ new Map();
|
|
2863
3426
|
for (const m of skills) byName.set(m.name, [...byName.get(m.name) ?? [], m]);
|
|
2864
3427
|
return [...byName].map(([name, copies]) => {
|
|
2865
|
-
if (
|
|
3428
|
+
if (copiesDiffer(copies.map((m) => m.path)))
|
|
2866
3429
|
return { name, moved: false, reason: `your global copies of ${name} differ; nothing moved (keep one first: skilllib tidy --global)`, changes: [] };
|
|
2867
|
-
|
|
3430
|
+
const elsewhere = blindWithoutGlobal(name, copies, knownProjects().filter((p) => !roots.some((r) => realPath(r) === realPath(p))));
|
|
3431
|
+
if (elsewhere.length) return { name, moved: false, reason: `nothing moved: ${blindSaid(name, elsewhere, label)}`, changes: [] };
|
|
3432
|
+
const hash = treeHash(copies[0].path) ?? "";
|
|
3433
|
+
const cant = roots.flatMap((root) => ((b) => b ? [`${label(root)} (${b.replace(/ \(use --force to replace (it|the link)\)$/, "")})`] : [])(trackBlocker(root, name) ?? addRefusal(root, name, hash)));
|
|
3434
|
+
if (cant.length) return { name, moved: false, reason: `nothing moved: it can't be added to ${cant.join(", ")}`, changes: [] };
|
|
3435
|
+
const imported = importSkill(copies[0].path).status;
|
|
3436
|
+
if (imported === "exists")
|
|
2868
3437
|
return { name, moved: false, reason: `your library has a different ${name}; nothing moved (update your library from this copy first)`, changes: [] };
|
|
2869
3438
|
if (group) recordOrigin(name, `group: ${group}`);
|
|
2870
3439
|
const changes = roots.map((root) => [root, addSkill(root, name, { allowTracked })]);
|
|
2871
3440
|
const skipped = changes.filter(([, c]) => c.action === "skipped");
|
|
2872
3441
|
const why = (reason = "") => reason.replace(/ \(use --force to replace it\)$/, ": move or remove it there first");
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
const
|
|
2876
|
-
if (
|
|
2877
|
-
const
|
|
2878
|
-
|
|
3442
|
+
const kept = imported === "added" ? `; it's in Your skills now` : "";
|
|
3443
|
+
if (skipped.length) return { name, moved: false, reason: `not added to ${skipped.map(([r, c]) => `${label(r)} (${why(c.reason)})`).join(", ")}, so it still loads globally${kept}`, changes };
|
|
3444
|
+
const blindIn = changes.flatMap(([root, c]) => ((b) => b.length ? [{ root, c, b }] : [])(blindWithoutGlobal(name, copies, [root])));
|
|
3445
|
+
if (blindIn.length) {
|
|
3446
|
+
const per = blindIn.map(({ root, c, b }) => {
|
|
3447
|
+
const who = b.map((x) => `${harness(x.id).name}${realPath(x.root) === realPath(root) ? "" : ` in ${label(x.root)}`}`).join(", ");
|
|
3448
|
+
const next = c.inTheWay?.length ? `${inTheWaySaid(root, c.inTheWay)}: clear the way, then run skilllib link there` : c.uncommitted?.length ? `git tracks ${c.uncommitted.join(", ")} but not the skill's copy: commit it first, then allow the link there` : c.blocked?.length ? `git tracks ${c.blocked.join(", ")}: ${allowTrackedHint}` : blindSaid(name, b, label);
|
|
3449
|
+
return `${who} ${b.every((x) => realPath(x.root) === realPath(root)) ? `in ${label(root)} ` : `(${label(root)}) `}couldn't see it (${next})`;
|
|
3450
|
+
});
|
|
3451
|
+
return { name, moved: false, reason: `added, but ${per.join("; ")}, so it still loads globally`, changes };
|
|
2879
3452
|
}
|
|
3453
|
+
const left = strandedLinks(name, copies, [...roots, ...knownProjects()], label);
|
|
2880
3454
|
const failed = copies.map((m) => unloadGlobal(m.path, m.links)).find((r) => !r.ok);
|
|
2881
|
-
return failed && !failed.ok ? { name, moved: false, reason: failed.reason, changes } : { name, moved: true, changes };
|
|
3455
|
+
return failed && !failed.ok ? { name, moved: false, reason: failed.reason, changes } : { name, moved: true, changes, ...left.length ? { left } : {} };
|
|
2882
3456
|
});
|
|
2883
3457
|
}
|
|
2884
3458
|
|
|
@@ -2925,26 +3499,28 @@ function colorState(state) {
|
|
|
2925
3499
|
function withDirs(root, changes) {
|
|
2926
3500
|
const { skills } = readManifest(root);
|
|
2927
3501
|
return changes.map((c) => {
|
|
2928
|
-
const dep = c.action
|
|
2929
|
-
return dep ? { ...c, dirs: [dep.dir ??
|
|
3502
|
+
const dep = c.action !== "removed" && c.action !== "skipped" ? skills[c.name] : void 0;
|
|
3503
|
+
return dep ? { ...c, dirs: [dep.dir ?? PROJECT_SKILLS_DIR, ...(dep.links ?? []).filter((d) => !c.inTheWay?.includes(`${d}/${c.name}`))] } : c;
|
|
2930
3504
|
});
|
|
2931
3505
|
}
|
|
2932
|
-
var incomplete = (c) => c.action === "skipped" || !!c.blocked?.length || !!c.uncommitted?.length;
|
|
2933
|
-
function warnHeldLinks(c, where = "here") {
|
|
3506
|
+
var incomplete = (c) => c.action === "skipped" || !!c.blocked?.length || !!c.uncommitted?.length || !!c.inTheWay?.length;
|
|
3507
|
+
function warnHeldLinks(root, c, where = "here") {
|
|
2934
3508
|
if (c.blocked?.length) warn(`${c.name}: git tracks ${c.blocked.join(", ")} ${where}, so no link was added there. Re-run with --allow-tracked to add it.`);
|
|
2935
3509
|
if (c.uncommitted?.length)
|
|
2936
3510
|
warn(`${c.name}: git tracks ${c.uncommitted.join(", ")} ${where} but not ${c.name}'s real copy, so no link was added there (teammates would get a broken link). Commit ${c.name} first, then run skilllib link --allow-tracked.`);
|
|
3511
|
+
if (c.inTheWay?.length) warn(`${c.name}: not linked everywhere ${where}: ${inTheWaySaid(root, c.inTheWay)}. Clear the way, then run skilllib link.`);
|
|
2937
3512
|
}
|
|
2938
3513
|
function printChanges(root, changes, args) {
|
|
2939
3514
|
if (changes.some(incomplete)) process.exitCode = 1;
|
|
2940
3515
|
if (args.json) return json("changes", withDirs(root, changes));
|
|
2941
3516
|
for (const c of changes) {
|
|
2942
|
-
warnHeldLinks(c);
|
|
3517
|
+
warnHeldLinks(root, c);
|
|
2943
3518
|
if (c.action === "skipped") error(`${c.name}: ${c.reason}`);
|
|
2944
3519
|
else {
|
|
2945
|
-
const icon = c.action === "removed" ? red("-") : c.action === "reset" ? yellow("\u21BA") : green(c.action === "installed" ? "+" : c.action === "relinked" ? "\u21E2" : "\u2191");
|
|
3520
|
+
const icon = c.action === "removed" ? red("-") : c.action === "reset" ? yellow("\u21BA") : c.action === "unchanged" ? dim("=") : green(c.action === "installed" ? "+" : c.action === "relinked" ? "\u21E2" : "\u2191");
|
|
2946
3521
|
const versions = c.action === "removed" ? "" : c.from && c.from !== c.to ? ` v${c.from} \u2192 v${c.to}` : c.to ? ` v${c.to}` : "";
|
|
2947
3522
|
info(` ${icon} ${c.name}${versions} ${dim(c.action)}`);
|
|
3523
|
+
if (c.reason) info(dim(` ${c.reason}`));
|
|
2948
3524
|
}
|
|
2949
3525
|
if (c.backedUp) info(dim(` local edits saved to ${tildify(c.backedUp)}; \`skilllib restore ${c.name}\` brings them back`));
|
|
2950
3526
|
}
|
|
@@ -2957,9 +3533,9 @@ function projectOfFactory(projects2) {
|
|
|
2957
3533
|
const sorted = [...projects2].sort((a, b) => b.length - a.length);
|
|
2958
3534
|
return (cwd) => {
|
|
2959
3535
|
if (!cwd) return null;
|
|
2960
|
-
const known = sorted.find((p) => cwd === p || cwd.startsWith(p +
|
|
3536
|
+
const known = sorted.find((p) => cwd === p || cwd.startsWith(p + sep8));
|
|
2961
3537
|
if (known) return known;
|
|
2962
|
-
const repo = cwd.split(`${
|
|
3538
|
+
const repo = cwd.split(`${sep8}.claude${sep8}worktrees${sep8}`)[0];
|
|
2963
3539
|
return isProjectCandidate(repo) ? repo : null;
|
|
2964
3540
|
};
|
|
2965
3541
|
}
|
|
@@ -2983,6 +3559,8 @@ function requireProject() {
|
|
|
2983
3559
|
const root = projectHere();
|
|
2984
3560
|
if (root) return root;
|
|
2985
3561
|
if (!isProjectCandidate(findProjectRoot())) {
|
|
3562
|
+
const top = findProjectRoot();
|
|
3563
|
+
if (realPath(top) !== realPath(userHome())) throw new Error(`${tildify(top)} is skilllib's or an agent's own folder, not a project. Run this inside a project.`);
|
|
2986
3564
|
const stray = existsSync14(join15(userHome(), MANIFEST_FILE)) ? `, though ${tildify(join15(userHome(), MANIFEST_FILE))} makes it look like one` : "";
|
|
2987
3565
|
throw new Error(`Your home folder isn't a project${stray}: skills added here would load in every repo. Run this inside a project.`);
|
|
2988
3566
|
}
|
|
@@ -2996,7 +3574,10 @@ async function status(args) {
|
|
|
2996
3574
|
const root2 = findProjectRoot();
|
|
2997
3575
|
const usage3 = await usageBySkill(args.days, root2);
|
|
2998
3576
|
json("status", usableHere(root2, new Map([...usage3].map(([name, u]) => [name, u.uses]))));
|
|
2999
|
-
|
|
3577
|
+
try {
|
|
3578
|
+
if (projectHere()) rememberProjects([root2]);
|
|
3579
|
+
} catch {
|
|
3580
|
+
}
|
|
3000
3581
|
return;
|
|
3001
3582
|
}
|
|
3002
3583
|
const root = requireProject();
|
|
@@ -3035,17 +3616,19 @@ async function status(args) {
|
|
|
3035
3616
|
}))
|
|
3036
3617
|
);
|
|
3037
3618
|
}
|
|
3038
|
-
const states = new Set(skills.map((s) => s
|
|
3619
|
+
const states = new Set(skills.map((s) => teamState(s)));
|
|
3039
3620
|
const hints = [];
|
|
3040
3621
|
if (states.has("update available")) hints.push("`skilllib update` to install the newest versions");
|
|
3041
3622
|
const missing = skills.filter((s) => s.state === "folder missing");
|
|
3042
|
-
if (missing.some((s) => s.latest !== null)) hints.push("`skilllib sync` to restore missing skills");
|
|
3623
|
+
if (missing.some((s) => s.latest !== null && !syncBlocker(root, s.name))) hints.push("`skilllib sync` to restore missing skills");
|
|
3624
|
+
if (missing.some((s) => syncBlocker(root, s.name))) hints.push("`skilllib doctor` says what's in sync's way");
|
|
3043
3625
|
const notInLibrary = missing.filter((s) => s.latest === null).map((s) => s.name);
|
|
3044
3626
|
if (notInLibrary.length)
|
|
3045
|
-
hints.push(`${notInLibrary.join(", ")} ${notInLibrary.length === 1 ? "isn't" : "aren't"} in your library (${tildify(libraryDir())}): \`skilllib import <dir>\` to add, or set SKILLLIB_HOME to the library
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3627
|
+
hints.push(`${notInLibrary.join(", ")} ${notInLibrary.length === 1 ? "isn't" : "aren't"} in your library (${tildify(libraryDir())}): \`skilllib import <dir>\` to add, or set SKILLLIB_HOME to the skilllib folder whose library has ${notInLibrary.length === 1 ? "it" : "them"}`);
|
|
3628
|
+
const at = (s) => relativeTo(root, s.path);
|
|
3629
|
+
for (const s of skills.filter((x) => x.state === "edited locally" || x.state === "edited locally, update available"))
|
|
3630
|
+
hints.push(`\`skilllib import ${at(s)} --force\` to save ${s.name}'s local edits to the library`);
|
|
3631
|
+
for (const s of skills.filter((x) => x.state === "local only")) hints.push(`\`skilllib import ${at(s)}\` to add ${s.name} to the library`);
|
|
3049
3632
|
if (states.has("untracked copy of library skill") || states.has("untracked, differs from library"))
|
|
3050
3633
|
hints.push("`skilllib add <name>` to manage an untracked skill (--force if it differs)");
|
|
3051
3634
|
if (hints.length > 0) info("\n" + hints.map((h) => dim(`\u2192 ${h}`)).join("\n"));
|
|
@@ -3084,14 +3667,14 @@ async function list(args) {
|
|
|
3084
3667
|
skills.map((s) => ({
|
|
3085
3668
|
Skill: s.name,
|
|
3086
3669
|
Description: truncate(s.description, 50),
|
|
3087
|
-
From: dim(truncate(origins[s.name] ?? "", 30)),
|
|
3670
|
+
From: dim(truncate(tildify(origins[s.name] ?? ""), 30)),
|
|
3088
3671
|
Projects: (installs.get(s.name) ?? []).join(", ") || dim("none"),
|
|
3089
3672
|
[`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0)
|
|
3090
3673
|
}))
|
|
3091
3674
|
);
|
|
3092
3675
|
}
|
|
3093
3676
|
const libraryNames = new Set(skills.map((s) => s.name));
|
|
3094
|
-
const globals = [...new Set(yourGlobals().map((m) => m.name))];
|
|
3677
|
+
const globals = [...new Set(yourGlobals().filter((m) => m.harnesses.length).map((m) => m.name))];
|
|
3095
3678
|
if (globals.length > 0) {
|
|
3096
3679
|
info("");
|
|
3097
3680
|
warn(`${globals.length} of your global ${globals.length === 1 ? "skill still loads" : "skills still load"} in every project`);
|
|
@@ -3139,13 +3722,14 @@ function projects(args) {
|
|
|
3139
3722
|
table(
|
|
3140
3723
|
roots.map((root) => {
|
|
3141
3724
|
const skills = projectStatus(root);
|
|
3142
|
-
const
|
|
3143
|
-
const
|
|
3725
|
+
const teamState = teamStateIn(root);
|
|
3726
|
+
const usual = /* @__PURE__ */ new Set(["ok", "local only", "repo skill", "repo skill, in library", "from npx skills"]);
|
|
3727
|
+
const attention = skills.filter((s) => !usual.has(teamState(s))).length;
|
|
3144
3728
|
return {
|
|
3145
3729
|
Project: basename9(root),
|
|
3146
3730
|
Path: dim(tildify(root)),
|
|
3147
|
-
Skills:
|
|
3148
|
-
"Local only": String(skills.filter((s) => s
|
|
3731
|
+
Skills: skills.map((s) => s.name).join(", ") || dim("none"),
|
|
3732
|
+
"Local only": String(skills.filter((s) => teamState(s) === "local only").length),
|
|
3149
3733
|
Status: attention === 0 ? green("ok") : yellow(`${attention} need attention`)
|
|
3150
3734
|
};
|
|
3151
3735
|
})
|
|
@@ -3156,8 +3740,7 @@ function add(args) {
|
|
|
3156
3740
|
throw new Error("Usage: skilllib add <skill-name>... [--force]");
|
|
3157
3741
|
}
|
|
3158
3742
|
const root = requireProject();
|
|
3159
|
-
|
|
3160
|
-
printChanges(root, args.positional.map((name) => addSkill(root, name, { force: args.force })), args);
|
|
3743
|
+
printChanges(root, args.positional.map((name) => addSkill(root, name, { force: args.force, allowTracked: args.allowTracked })), args);
|
|
3161
3744
|
rememberProjects([root]);
|
|
3162
3745
|
}
|
|
3163
3746
|
function remove(args) {
|
|
@@ -3170,7 +3753,6 @@ function remove(args) {
|
|
|
3170
3753
|
function changeEach(args, change, nothing) {
|
|
3171
3754
|
const roots = projectsFor(args);
|
|
3172
3755
|
const results = roots.map((root) => {
|
|
3173
|
-
if (args.allowTracked) allowTrackedLinks(root);
|
|
3174
3756
|
const changes = change(root);
|
|
3175
3757
|
if (changes.some(incomplete)) process.exitCode = 1;
|
|
3176
3758
|
if (!args.json) {
|
|
@@ -3197,7 +3779,7 @@ function update(args) {
|
|
|
3197
3779
|
changeEach(
|
|
3198
3780
|
args,
|
|
3199
3781
|
(root) => {
|
|
3200
|
-
const changes = updateProject(root, names, { force: args.force });
|
|
3782
|
+
const changes = updateProject(root, names, { force: args.force, allowTracked: args.allowTracked });
|
|
3201
3783
|
if (!args.all) {
|
|
3202
3784
|
for (const name of names ?? []) if (!(name in readManifest(root).skills)) changes.push({ name, action: "skipped", reason: "not managed by skilllib in this project" });
|
|
3203
3785
|
}
|
|
@@ -3220,7 +3802,7 @@ function outdated(args) {
|
|
|
3220
3802
|
}
|
|
3221
3803
|
function importSkills(args) {
|
|
3222
3804
|
if (args.global && args.positional.length) throw new Error("skilllib import --global takes no folders: it imports all your global skills");
|
|
3223
|
-
const dirs = args.global ? yourGlobals().map((m) => m.path) : args.positional.map((p) =>
|
|
3805
|
+
const dirs = args.global ? yourGlobals().map((m) => m.path) : args.positional.map((p) => resolve8(p));
|
|
3224
3806
|
if (dirs.length === 0) {
|
|
3225
3807
|
throw new Error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
|
|
3226
3808
|
}
|
|
@@ -3240,7 +3822,7 @@ function importSkills(args) {
|
|
|
3240
3822
|
continue;
|
|
3241
3823
|
}
|
|
3242
3824
|
success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
|
|
3243
|
-
const root =
|
|
3825
|
+
const root = resolve8(dir, "..", "..", "..");
|
|
3244
3826
|
const dep = projectHere(root) === root ? readManifest(root).skills[name] : void 0;
|
|
3245
3827
|
if (dep && realpathOrNull(join15(root, dep.dir ?? PROJECT_SKILLS_DIR, name)) === realpathOrNull(dir) && dep.hash !== treeHash(dir)) {
|
|
3246
3828
|
const change = addSkill(root, name);
|
|
@@ -3259,7 +3841,8 @@ function scan(args) {
|
|
|
3259
3841
|
const found = discoverProjects();
|
|
3260
3842
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
3261
3843
|
const skillNames = (root) => [
|
|
3262
|
-
|
|
3844
|
+
// Not a folder that links out of the repo: what's in it isn't the repo's (as in status).
|
|
3845
|
+
.../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.filter((d) => !notWritable(root, d)).flatMap((d) => skillDirsIn(join15(root, d))).map((d) => basename9(d)), ...nestedSkills(root).map((s) => s.name)])
|
|
3263
3846
|
];
|
|
3264
3847
|
const withSkills = found.filter((root) => skillNames(root).length > 0);
|
|
3265
3848
|
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
|
|
@@ -3277,47 +3860,42 @@ ${yellow("Yellow")} skills aren't in your library yet (\`skilllib import <skill-
|
|
|
3277
3860
|
function folders(args) {
|
|
3278
3861
|
const [action, ...paths] = args.positional;
|
|
3279
3862
|
if (action && !["add", "remove", "rm"].includes(action)) throw new Error("Usage: skilllib folders [add|remove <folder>...]");
|
|
3280
|
-
|
|
3863
|
+
const failures = [];
|
|
3281
3864
|
if (action === "add") {
|
|
3282
3865
|
for (const p of paths) {
|
|
3283
3866
|
try {
|
|
3284
3867
|
success(`Added ${tildify(addRoot(p))}`);
|
|
3285
3868
|
} catch (e) {
|
|
3286
|
-
|
|
3287
|
-
failed = true;
|
|
3869
|
+
failures.push(e.message);
|
|
3288
3870
|
}
|
|
3289
3871
|
}
|
|
3290
3872
|
discoverProjects();
|
|
3291
3873
|
} else if (action === "remove" || action === "rm") {
|
|
3292
3874
|
for (const p of paths) {
|
|
3293
3875
|
if (removeRoot(p)) success(`Removed ${tildify(expandHome(p))}`);
|
|
3294
|
-
else {
|
|
3295
|
-
warn(`${tildify(expandHome(p))} wasn't a project folder`);
|
|
3296
|
-
failed = true;
|
|
3297
|
-
}
|
|
3876
|
+
else failures.push(`${tildify(expandHome(p))} wasn't a project folder`);
|
|
3298
3877
|
}
|
|
3299
3878
|
}
|
|
3300
3879
|
const { roots, hidden } = readConfig();
|
|
3301
3880
|
info(roots.length ? `Project folders:
|
|
3302
3881
|
${roots.map((r) => ` ${tildify(r)}`).join("\n")}` : "No project folders. Add one: skilllib folders add ~/Projects");
|
|
3303
3882
|
if (hidden.length) info(dim(`Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")} (skilllib unhide <path>)`));
|
|
3304
|
-
if (
|
|
3883
|
+
if (failures.length) throw new Error(failures.join("; "));
|
|
3305
3884
|
}
|
|
3306
3885
|
function unhide(args) {
|
|
3307
3886
|
if (args.positional.length === 0) {
|
|
3308
3887
|
throw new Error("Usage: skilllib unhide <path>...");
|
|
3309
3888
|
}
|
|
3310
|
-
|
|
3889
|
+
const failures = [];
|
|
3311
3890
|
for (const p of args.positional) {
|
|
3312
3891
|
const path = expandHome(p);
|
|
3313
3892
|
if (setHidden(path, false)) success(`${tildify(path)} is visible again`);
|
|
3314
3893
|
else {
|
|
3315
3894
|
const { hidden } = readConfig();
|
|
3316
|
-
|
|
3317
|
-
failed = true;
|
|
3895
|
+
failures.push(`${tildify(path)} wasn't hidden. ${hidden.length ? `Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")}` : "No projects are hidden."}`);
|
|
3318
3896
|
}
|
|
3319
3897
|
}
|
|
3320
|
-
if (
|
|
3898
|
+
if (failures.length) throw new Error(failures.join("; "));
|
|
3321
3899
|
}
|
|
3322
3900
|
function newSkill(args) {
|
|
3323
3901
|
const [name, ...rest] = args.positional;
|
|
@@ -3341,16 +3919,13 @@ function restore(args) {
|
|
|
3341
3919
|
}
|
|
3342
3920
|
const newest = backups.find((b) => b.name === name);
|
|
3343
3921
|
if (!newest) throw new Error(`No backup named ${name} (skilllib restore lists them; a skill removed from a repo that your library has comes back with skilllib add ${name})`);
|
|
3344
|
-
const
|
|
3345
|
-
|
|
3346
|
-
for (const backup of together) {
|
|
3922
|
+
const failures = [];
|
|
3923
|
+
for (const backup of movedTogether(backups, newest)) {
|
|
3347
3924
|
const r = backup.kind === "plugin" ? restorePlugin(backup) : restoreBackup(backup);
|
|
3348
3925
|
if (r.ok) success(`Restored ${name} to ${tildify(r.to)}`);
|
|
3349
|
-
else
|
|
3350
|
-
error(`Can't restore ${name}: ${r.reason}`);
|
|
3351
|
-
process.exitCode = 1;
|
|
3352
|
-
}
|
|
3926
|
+
else failures.push(r.reason);
|
|
3353
3927
|
}
|
|
3928
|
+
if (failures.length) throw new Error(`Can't restore ${name}: ${failures.join("; ")}`);
|
|
3354
3929
|
}
|
|
3355
3930
|
async function allIssues(args, withUsage = true) {
|
|
3356
3931
|
const projects2 = visibleProjects();
|
|
@@ -3390,7 +3965,7 @@ async function doctor(args) {
|
|
|
3390
3965
|
id,
|
|
3391
3966
|
severity,
|
|
3392
3967
|
title,
|
|
3393
|
-
detail,
|
|
3968
|
+
detail: detailWithGit({ detail, fix, choices }),
|
|
3394
3969
|
fix: fix?.label,
|
|
3395
3970
|
choices: choices?.map((c) => c.label),
|
|
3396
3971
|
fixed: args.fix && fix ? fixed(fix) : void 0
|
|
@@ -3402,7 +3977,8 @@ async function doctor(args) {
|
|
|
3402
3977
|
for (const issue of issues) {
|
|
3403
3978
|
info(`${issue.severity === "problem" ? red("\u25CF") : yellow("\u25C6")} ${issue.title}`);
|
|
3404
3979
|
info(dim(` ${issue.detail}${issue.fix ? ` \u2192 ${issue.fix.label}` : ""}`));
|
|
3405
|
-
|
|
3980
|
+
const spans = new Set(issue.at?.map((a) => a.root)).size > 1;
|
|
3981
|
+
if (issue.choices) info(dim(` your call: ${issue.choices.map((c, i) => `${i + 1}) ${c.label}`).join(" ")} \u2192 skilllib apply ${issue.id} ${spans ? '"<choice>"' : "<n>"}`));
|
|
3406
3982
|
if (args.fix && issue.fix) {
|
|
3407
3983
|
const r = fixed(issue.fix);
|
|
3408
3984
|
info(` ${r.ok ? green("\u2713") : red("\u2717")} ${r.message}`);
|
|
@@ -3420,7 +3996,7 @@ async function apply(args) {
|
|
|
3420
3996
|
const byLabel = pick === void 0 ? void 0 : found.find((i) => optionsOf(i).some((c) => c.label.toLowerCase() === pick.toLowerCase()));
|
|
3421
3997
|
if (!byLabel && found.some((i) => labels(i) !== labels(found[0]))) {
|
|
3422
3998
|
const all = [...new Set(found.flatMap((i) => optionsOf(i).map((c) => c.label)))];
|
|
3423
|
-
throw new Error(`${found[0].title}: its choices differ
|
|
3999
|
+
throw new Error(`${found[0].title}: its choices here differ from doctor's (all your repos); pick one by label: ${all.map((l) => `"${l}"`).join(" ")}`);
|
|
3424
4000
|
}
|
|
3425
4001
|
const issue = byLabel ?? found[0];
|
|
3426
4002
|
const options = optionsOf(issue);
|
|
@@ -3481,14 +4057,14 @@ function show(args) {
|
|
|
3481
4057
|
path: dir,
|
|
3482
4058
|
latest: latestVersion(name)?.version ?? null,
|
|
3483
4059
|
origin: libraryOrigins()[name] ?? "",
|
|
3484
|
-
projects:
|
|
3485
|
-
skillMd:
|
|
4060
|
+
projects: projectsUsing(name).map((p) => basename9(p)),
|
|
4061
|
+
skillMd: readFileSync7(join15(dir, "SKILL.md"), "utf-8")
|
|
3486
4062
|
});
|
|
3487
4063
|
}
|
|
3488
4064
|
info(`${name} ${dim(tildify(dir))}`);
|
|
3489
4065
|
if (description) info(`
|
|
3490
4066
|
${description}`);
|
|
3491
|
-
const using =
|
|
4067
|
+
const using = projectsUsing(name);
|
|
3492
4068
|
info(`
|
|
3493
4069
|
Installed in: ${using.map((p) => tildify(p)).join(", ") || "no projects"}`);
|
|
3494
4070
|
}
|
|
@@ -3512,25 +4088,33 @@ New installs go to ${installDirs(enabled).join(" (real copy) + ")}${installDirs(
|
|
|
3512
4088
|
function link(args) {
|
|
3513
4089
|
const roots = projectsFor(args);
|
|
3514
4090
|
for (const root of roots) {
|
|
3515
|
-
|
|
3516
|
-
const res = linkAll(root);
|
|
4091
|
+
const res = linkAll(root, { allowTracked: args.allowTracked });
|
|
3517
4092
|
const label = args.all ? `${basename9(root)}: ` : "";
|
|
3518
4093
|
if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
|
|
3519
|
-
else if (!args.all && !res.blocked.length && !res.uncommitted.length
|
|
4094
|
+
else if (!args.all && !res.blocked.length && !res.uncommitted.length && !res.inTheWay.length) {
|
|
4095
|
+
const missing = projectStatus(root).filter((s) => s.state === "folder missing").map((s) => s.name);
|
|
4096
|
+
if (missing.length) warn(`Nothing to link; ${missing.join(", ")} ${missing.length === 1 ? "isn't" : "aren't"} installed here (skilllib sync, or skilllib doctor says what's in the way)`);
|
|
4097
|
+
else success("Every skill here is already usable by all your agents");
|
|
4098
|
+
}
|
|
4099
|
+
if (res.inTheWay.length) {
|
|
4100
|
+
process.exitCode = 1;
|
|
4101
|
+
error(`${label}some skills weren't linked everywhere: ${inTheWaySaid(root, res.inTheWay)}. Clear the way, then link again`);
|
|
4102
|
+
}
|
|
3520
4103
|
if (res.blocked.length) {
|
|
3521
4104
|
process.exitCode = 1;
|
|
3522
4105
|
error(`${label}git tracks ${res.blocked.join(", ")}, so ${res.linked.length ? "some skills weren't" : "nothing was"} linked there; re-run with --allow-tracked to link there too`);
|
|
3523
4106
|
}
|
|
3524
|
-
if (res.uncommitted.length)
|
|
3525
|
-
|
|
4107
|
+
if (res.uncommitted.length) {
|
|
4108
|
+
process.exitCode = 1;
|
|
3526
4109
|
warn(
|
|
3527
4110
|
`${label}${res.uncommitted.join(", ")} ${res.uncommitted.length === 1 ? "isn't" : "aren't"} committed, so not linked into folders git tracks (teammates would get broken links); commit ${res.uncommitted.length === 1 ? "it" : "them"} first`
|
|
3528
4111
|
);
|
|
4112
|
+
}
|
|
3529
4113
|
}
|
|
3530
4114
|
}
|
|
3531
4115
|
function globalCommand(args) {
|
|
3532
4116
|
const [action, ...names] = args.positional;
|
|
3533
|
-
const yours = yourGlobals();
|
|
4117
|
+
const yours = yourGlobals().filter((m) => m.harnesses.length);
|
|
3534
4118
|
if (action === "move") return moveGlobal(args, names, yours);
|
|
3535
4119
|
if (action === "keep" || action === "unkeep") {
|
|
3536
4120
|
const unknown = names.filter((n) => !yours.some((m) => m.name === n));
|
|
@@ -3572,10 +4156,11 @@ function moveGlobal(args, names, yours) {
|
|
|
3572
4156
|
if (args.json) {
|
|
3573
4157
|
return json(
|
|
3574
4158
|
"global move",
|
|
3575
|
-
moves.map(({ name, moved, reason, changes }) => ({
|
|
4159
|
+
moves.map(({ name, moved, reason, changes, left }) => ({
|
|
3576
4160
|
skill: name,
|
|
3577
4161
|
moved,
|
|
3578
4162
|
...reason ? { reason } : {},
|
|
4163
|
+
...left ? { left } : {},
|
|
3579
4164
|
projects: changes.map(([root, c]) => ({ project: basename9(root), path: root, change: withDirs(root, [c])[0] }))
|
|
3580
4165
|
}))
|
|
3581
4166
|
);
|
|
@@ -3583,13 +4168,14 @@ function moveGlobal(args, names, yours) {
|
|
|
3583
4168
|
for (const m of moves) {
|
|
3584
4169
|
if (m.moved) success(`${m.name} now loads only in ${m.changes.map(([r]) => basename9(r)).join(", ")} (the global copy is in skilllib restore)`);
|
|
3585
4170
|
else error(`${m.name}: ${m.reason}`);
|
|
3586
|
-
for (const [root, c] of m.changes) warnHeldLinks(c, `in ${basename9(root)}`);
|
|
4171
|
+
for (const [root, c] of m.changes) warnHeldLinks(root, c, `in ${basename9(root)}`);
|
|
4172
|
+
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"}`);
|
|
3587
4173
|
}
|
|
3588
4174
|
if (moves.some((m) => m.moved)) info(dim("Commit skilllib.json and the skill folders in each repo."));
|
|
3589
4175
|
}
|
|
3590
4176
|
function tidy(args) {
|
|
3591
4177
|
const names = args.positional;
|
|
3592
|
-
const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ?
|
|
4178
|
+
const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve8(base, args.keep) : expandHome(args.keep)])) : {};
|
|
3593
4179
|
if (args.keep && !names.length) {
|
|
3594
4180
|
throw new Error("Usage: skilllib tidy <name>... --keep <folder>");
|
|
3595
4181
|
}
|
|
@@ -3621,7 +4207,9 @@ function tidy(args) {
|
|
|
3621
4207
|
for (const { label, report } of reports) {
|
|
3622
4208
|
const plans = report.plans.filter((p) => !names.length || names.includes(p.name));
|
|
3623
4209
|
const conflicts = report.conflicts.filter((c) => !names.length || names.includes(c.name));
|
|
3624
|
-
|
|
4210
|
+
const skippedNamed = names.length ? report.skipped.filter((x) => names.includes(x.name) && x.differs) : [];
|
|
4211
|
+
if (skippedNamed.length) process.exitCode = 1;
|
|
4212
|
+
if (!plans.length && !conflicts.length && !skippedNamed.length && !args.all) success(`${label}: nothing to tidy (no extra copies or links)`);
|
|
3625
4213
|
for (const plan of plans) {
|
|
3626
4214
|
const wouldHold = args.allowGit ? [] : gitVisibleSteps(plan);
|
|
3627
4215
|
const result = args.dryRun ? { applied: plan.steps.filter((s) => !wouldHold.includes(s)), held: wouldHold } : applyTidy(plan, { git: args.allowGit ? "go" : "keep" });
|
|
@@ -3630,21 +4218,24 @@ function tidy(args) {
|
|
|
3630
4218
|
held += result.held.length;
|
|
3631
4219
|
changed += result.applied.length;
|
|
3632
4220
|
}
|
|
3633
|
-
if (args.keep
|
|
4221
|
+
if (conflicts.length && (args.keep || names.length)) process.exitCode = 1;
|
|
3634
4222
|
for (const c of conflicts) {
|
|
3635
4223
|
warn(`${label} \xB7 ${c.name}: the copies differ`);
|
|
3636
4224
|
for (const copy of c.copies) {
|
|
3637
4225
|
const runs = copy.runs.map((id) => harness(id).name);
|
|
3638
|
-
info(` ${
|
|
4226
|
+
info(` ${folderLabel(c.root, copy.dir)}${runs.length ? dim(` run by ${runs.join(", ")}`) : ""}${c.managed === copy.dir ? dim(" (skilllib's copy)") : ""}`);
|
|
3639
4227
|
}
|
|
3640
|
-
const pick =
|
|
4228
|
+
const pick = folderLabel(c.root, c.managed ?? c.copies[0].dir);
|
|
3641
4229
|
info(dim(` keep one: skilllib tidy ${c.name}${args.global ? " --global" : ""} --keep ${pick}`));
|
|
3642
4230
|
}
|
|
3643
4231
|
for (const s of report.skipped.filter((x) => !names.length || names.includes(x.name))) info(dim(`- ${label} \xB7 ${s.name}: skipped, ${s.reason}`));
|
|
3644
4232
|
}
|
|
3645
4233
|
if (args.dryRun) info(dim("\nNothing changed (--dry-run)."));
|
|
3646
4234
|
else if (changed) info(dim("\nReplaced copies are in ~/.skilllib/tidy-backup; `skilllib restore` puts them back."));
|
|
3647
|
-
if (held)
|
|
4235
|
+
if (held) {
|
|
4236
|
+
process.exitCode = 1;
|
|
4237
|
+
info(dim(`${held} change${held === 1 ? "" : "s"} held back because git would see them. Re-run with --allow-git to make them.`));
|
|
4238
|
+
}
|
|
3648
4239
|
}
|
|
3649
4240
|
function agentSkill(args) {
|
|
3650
4241
|
const action = args.positional[0];
|
|
@@ -3673,17 +4264,22 @@ function agentSkill(args) {
|
|
|
3673
4264
|
}
|
|
3674
4265
|
|
|
3675
4266
|
export {
|
|
4267
|
+
realPath,
|
|
3676
4268
|
skilllibHome,
|
|
3677
4269
|
PROJECT_SKILLS_DIR,
|
|
3678
4270
|
HARNESSES,
|
|
3679
4271
|
harness,
|
|
4272
|
+
installDirs,
|
|
3680
4273
|
readSkillInfo,
|
|
3681
4274
|
treeHash,
|
|
4275
|
+
isDir,
|
|
3682
4276
|
versionHistory,
|
|
3683
4277
|
forgetLatest,
|
|
3684
4278
|
latestVersion,
|
|
3685
4279
|
projectHere,
|
|
3686
4280
|
readManifest,
|
|
4281
|
+
knownProjects,
|
|
4282
|
+
rememberProjects,
|
|
3687
4283
|
readableRoots,
|
|
3688
4284
|
setJsonMode,
|
|
3689
4285
|
isJsonMode,
|
|
@@ -3709,10 +4305,18 @@ export {
|
|
|
3709
4305
|
recordOrigin,
|
|
3710
4306
|
librarySkills,
|
|
3711
4307
|
librarySkillDir,
|
|
3712
|
-
|
|
4308
|
+
notWritable,
|
|
4309
|
+
inTheWaySaid,
|
|
4310
|
+
entryExists,
|
|
4311
|
+
survives,
|
|
4312
|
+
blindWithoutGlobal,
|
|
4313
|
+
strandedLinks,
|
|
4314
|
+
blindSaid,
|
|
4315
|
+
trackBlocker,
|
|
3713
4316
|
projectStatus,
|
|
3714
4317
|
linkEverywhere,
|
|
3715
4318
|
relinkDependency,
|
|
4319
|
+
addRefusal,
|
|
3716
4320
|
addSkill,
|
|
3717
4321
|
removeSkill,
|
|
3718
4322
|
updateProject,
|
|
@@ -3723,6 +4327,7 @@ export {
|
|
|
3723
4327
|
deleteGlobal,
|
|
3724
4328
|
createSkill,
|
|
3725
4329
|
listBackups,
|
|
4330
|
+
movedTogether,
|
|
3726
4331
|
restoreBackup,
|
|
3727
4332
|
gitInfo,
|
|
3728
4333
|
relativeTo,
|
|
@@ -3744,6 +4349,7 @@ export {
|
|
|
3744
4349
|
findIssues,
|
|
3745
4350
|
usageIssues,
|
|
3746
4351
|
scanUsage,
|
|
4352
|
+
skillOf,
|
|
3747
4353
|
moveGlobalSkills,
|
|
3748
4354
|
parseArgs,
|
|
3749
4355
|
projectOfFactory,
|