skilllib 1.11.3 → 1.11.5
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 +23 -20
- package/dist/{chunk-3XDWEHI4.js → chunk-TE2EDZ7L.js} +1006 -373
- package/dist/index.js +8 -5
- package/dist/json.d.cts +20 -8
- package/dist/json.d.ts +20 -8
- package/dist/{tui-WSHQRBPF.js → tui-GFQUHPEC.js} +219 -136
- 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,60 @@ 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 withoutForceHint(reason) {
|
|
1234
|
+
return reason.replace(/ \(use --force[^)]*\)$/, "");
|
|
1235
|
+
}
|
|
1236
|
+
function addRefusal(root, name, hash) {
|
|
1237
|
+
const plan = addPlan(root, name, hash, false);
|
|
1238
|
+
return "reason" in plan ? plan.reason : null;
|
|
1239
|
+
}
|
|
1240
|
+
function addSkill(root, name, { force = false, version, allowTracked = false } = {}) {
|
|
1241
|
+
if (!name || name === "." || name === ".." || /[\\/]/.test(name)) return { name, action: "skipped", reason: "not a skill name" };
|
|
1242
|
+
const target = version === void 0 ? latestVersion(name) : getVersion(name, version);
|
|
1243
|
+
if (!target) return { name, action: "skipped", reason: version === void 0 ? "not in the library" : `version ${version} doesn't exist` };
|
|
1244
|
+
const plan = addPlan(root, name, target.hash, force);
|
|
1245
|
+
if ("reason" in plan) return { name, action: "skipped", reason: plan.reason };
|
|
1246
|
+
const { manifest, recorded, primary, wantedLinks, to, local } = plan;
|
|
971
1247
|
let backedUp;
|
|
972
|
-
|
|
1248
|
+
const copied = local !== target.hash || isLink(to);
|
|
1249
|
+
if (copied) {
|
|
973
1250
|
mkdirSync4(join8(root, primary), { recursive: true });
|
|
974
1251
|
if (isLink(to)) unlinkSync(to);
|
|
975
1252
|
else if (local !== null && versionForHash(name, local) === null) backedUp = stash(to, "edit-backup");
|
|
@@ -978,14 +1255,21 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
978
1255
|
const links = [];
|
|
979
1256
|
const blocked = [];
|
|
980
1257
|
const uncommitted = [];
|
|
1258
|
+
const inTheWay = [];
|
|
981
1259
|
let shared;
|
|
982
|
-
|
|
1260
|
+
let relinked = false;
|
|
1261
|
+
for (const dir of wantedLinks) {
|
|
983
1262
|
const link2 = join8(root, dir, name);
|
|
984
|
-
|
|
1263
|
+
const slot = slotAt(link2, to);
|
|
1264
|
+
if (slot === "ok") {
|
|
985
1265
|
links.push(dir);
|
|
986
1266
|
continue;
|
|
987
1267
|
}
|
|
988
|
-
if (
|
|
1268
|
+
if (slot === "taken") {
|
|
1269
|
+
inTheWay.push(`${dir}/${name}`);
|
|
1270
|
+
if (recorded?.links?.includes(dir)) links.push(dir);
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
989
1273
|
if (!recorded?.links?.includes(dir) && isGitTracked(root, dir)) {
|
|
990
1274
|
if (!(shared ??= sharedByGit(root, primary, name))) {
|
|
991
1275
|
uncommitted.push(dir);
|
|
@@ -996,9 +1280,11 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
996
1280
|
continue;
|
|
997
1281
|
}
|
|
998
1282
|
}
|
|
1283
|
+
if (isLink(link2)) unlinkSync(link2);
|
|
999
1284
|
mkdirSync4(join8(root, dir), { recursive: true });
|
|
1000
1285
|
linkDir(to, link2);
|
|
1001
1286
|
links.push(dir);
|
|
1287
|
+
relinked = true;
|
|
1002
1288
|
}
|
|
1003
1289
|
manifest.skills[name] = {
|
|
1004
1290
|
version: target.version,
|
|
@@ -1007,13 +1293,21 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
1007
1293
|
...links.length ? { links } : {}
|
|
1008
1294
|
};
|
|
1009
1295
|
writeManifest(root, manifest);
|
|
1296
|
+
const from = recorded ? versionForHash(name, recorded.hash)?.version ?? recorded.version : void 0;
|
|
1010
1297
|
return {
|
|
1011
1298
|
name,
|
|
1012
|
-
action:
|
|
1013
|
-
|
|
1299
|
+
action: (
|
|
1300
|
+
// Nothing of it was there (or only an identical untracked copy, now tracked): installed, not updated.
|
|
1301
|
+
!recorded || local === null ? "installed" : backedUp && from === target.version ? "reset" : recorded && !copied && from === target.version ? (
|
|
1302
|
+
// The same version, already there: only links (if any) were written.
|
|
1303
|
+
relinked ? "relinked" : "unchanged"
|
|
1304
|
+
) : "updated"
|
|
1305
|
+
),
|
|
1306
|
+
...from !== void 0 ? { from } : {},
|
|
1014
1307
|
to: target.version,
|
|
1015
1308
|
...blocked.length ? { blocked } : {},
|
|
1016
1309
|
...uncommitted.length ? { uncommitted } : {},
|
|
1310
|
+
...inTheWay.length ? { inTheWay } : {},
|
|
1017
1311
|
...backedUp ? { backedUp } : {}
|
|
1018
1312
|
};
|
|
1019
1313
|
}
|
|
@@ -1021,37 +1315,79 @@ function removeSkill(root, name, { force = false } = {}) {
|
|
|
1021
1315
|
const manifest = readManifest(root);
|
|
1022
1316
|
const recorded = manifest.skills[name];
|
|
1023
1317
|
if (!recorded) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
|
|
1024
|
-
const
|
|
1318
|
+
const home2 = recorded.dir ?? PROJECT_SKILLS_DIR;
|
|
1319
|
+
const dir = join8(root, home2, name);
|
|
1320
|
+
const takenLinks = [];
|
|
1321
|
+
const unlinkOwn = () => {
|
|
1322
|
+
for (const linkDir2 of (recorded.links ?? []).filter((d) => writableDir(root, d))) {
|
|
1323
|
+
const link2 = join8(root, linkDir2, name);
|
|
1324
|
+
if (isLink(link2) && slotAt(link2, dir) !== "taken") unlinkSync(link2);
|
|
1325
|
+
else if (entryExists(link2) && slotAt(link2, dir) === "taken") takenLinks.push(`${linkDir2}/${name} is something else (another copy, or a file): left as it is`);
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
const leftLinks = (recorded.links ?? []).flatMap(
|
|
1329
|
+
(d) => ((b) => b && entryExists(join8(root, d, name)) ? [`${blockerSaid(root, b)}: its link there was left alone`] : [])(notWritable(root, d))
|
|
1330
|
+
);
|
|
1331
|
+
const ownStuck = notWritable(root, home2);
|
|
1332
|
+
if (ownStuck || fileAt(dir)) {
|
|
1333
|
+
delete manifest.skills[name];
|
|
1334
|
+
writeManifest(root, manifest);
|
|
1335
|
+
const why = ownStuck ? blockerSaid(root, ownStuck) : `${home2}/${name} is a file, not a skill folder`;
|
|
1336
|
+
const keptLinks = (recorded.links ?? []).flatMap((d) => {
|
|
1337
|
+
const link2 = join8(root, d, name);
|
|
1338
|
+
if (!writableDir(root, d) || !entryExists(link2)) return [];
|
|
1339
|
+
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`];
|
|
1340
|
+
});
|
|
1341
|
+
return { name, action: "removed", reason: [`${why}: only its pin was removed, and that copy was left as it is`, ...keptLinks, ...leftLinks].join("; ") };
|
|
1342
|
+
}
|
|
1025
1343
|
const local = treeHash(dir);
|
|
1026
1344
|
const inLibrary = local !== null && versionForHash(name, local) !== null;
|
|
1027
1345
|
const edited = local !== null && local !== recorded.hash && !inLibrary;
|
|
1028
|
-
if (edited && !force)
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1346
|
+
if (edited && !force)
|
|
1347
|
+
return {
|
|
1348
|
+
name,
|
|
1349
|
+
action: "skipped",
|
|
1350
|
+
// A pin from another library isn't an edit of yours: say what it is. Either way --force backs the copy up first.
|
|
1351
|
+
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)"
|
|
1352
|
+
};
|
|
1353
|
+
unlinkOwn();
|
|
1033
1354
|
const backedUp = local !== null && !inLibrary ? stash(dir, "edit-backup") : void 0;
|
|
1034
1355
|
if (!backedUp) rmSync3(dir, { recursive: true, force: true });
|
|
1035
1356
|
delete manifest.skills[name];
|
|
1036
1357
|
writeManifest(root, manifest);
|
|
1037
|
-
|
|
1358
|
+
const left = [...leftLinks, ...takenLinks];
|
|
1359
|
+
return { name, action: "removed", ...left.length ? { reason: left.join("; ") } : {}, ...backedUp ? { backedUp } : {} };
|
|
1038
1360
|
}
|
|
1039
|
-
function syncProject(root, { force = false } = {}) {
|
|
1040
|
-
return Object.keys(readManifest(root).skills).flatMap((name) => syncSkill(root, name, { force }));
|
|
1361
|
+
function syncProject(root, { force = false, names } = {}) {
|
|
1362
|
+
return Object.keys(readManifest(root).skills).filter((name) => !names || names.includes(name)).flatMap((name) => syncSkill(root, name, { force }));
|
|
1041
1363
|
}
|
|
1042
1364
|
function syncSkill(root, name, { force = false } = {}) {
|
|
1043
1365
|
const dep = readManifest(root).skills[name];
|
|
1044
1366
|
if (!dep) return [];
|
|
1045
1367
|
const copy = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
|
|
1046
1368
|
const local = treeHash(copy);
|
|
1047
|
-
const
|
|
1048
|
-
if (
|
|
1369
|
+
const blocker = syncBlocker(root, name, dep);
|
|
1370
|
+
if (blocker) return [{ name, action: "skipped", reason: `${blocker.said}: ${blocker.outside ? "skilllib only installs inside the repo" : "move it, then sync"}` }];
|
|
1371
|
+
if (syncDone(root, name, dep)) return [];
|
|
1049
1372
|
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
1373
|
const version = pinnedVersion(name, dep);
|
|
1052
1374
|
if (typeof version === "string") return [{ name, action: "skipped", reason: version }];
|
|
1053
|
-
|
|
1054
|
-
|
|
1375
|
+
if (local !== null && local !== dep.hash && versionForHash(name, local) === null && !force) return [{ name, action: "skipped", reason: "has local edits (use --force to reset them)" }];
|
|
1376
|
+
const backedUp = local !== null && versionForHash(name, local) === null ? stash(copy, "edit-backup") : void 0;
|
|
1377
|
+
mkdirSync4(dirname3(copy), { recursive: true });
|
|
1378
|
+
copySkill(versionDir(name, version), copy);
|
|
1379
|
+
const links = relinkPinned(root, name, dep);
|
|
1380
|
+
const from = (local !== null ? versionForHash(name, local)?.version : void 0) ?? version;
|
|
1381
|
+
return [
|
|
1382
|
+
{
|
|
1383
|
+
name,
|
|
1384
|
+
action: local === null ? "installed" : backedUp ? "reset" : "updated",
|
|
1385
|
+
from,
|
|
1386
|
+
to: version,
|
|
1387
|
+
...backedUp ? { backedUp } : {},
|
|
1388
|
+
...links.action === "skipped" ? { reason: links.reason } : {}
|
|
1389
|
+
}
|
|
1390
|
+
];
|
|
1055
1391
|
}
|
|
1056
1392
|
function relinkPinned(root, name, dep) {
|
|
1057
1393
|
const to = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
|
|
@@ -1063,7 +1399,7 @@ function relinkPinned(root, name, dep) {
|
|
|
1063
1399
|
mkdirSync4(join8(root, dir), { recursive: true });
|
|
1064
1400
|
linkDir(to, link2);
|
|
1065
1401
|
}
|
|
1066
|
-
return { name, action: "relinked", to: dep.version };
|
|
1402
|
+
return { name, action: "relinked", to: versionForHash(name, dep.hash)?.version ?? dep.version };
|
|
1067
1403
|
}
|
|
1068
1404
|
function pinnedVersion(name, dep) {
|
|
1069
1405
|
if (!latestVersion(name)) return `not in your library (${tildify(libraryDir())}); import it, or set SKILLLIB_HOME to the library that has it`;
|
|
@@ -1074,20 +1410,18 @@ function discardEdits(root, name) {
|
|
|
1074
1410
|
if (!dep) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
|
|
1075
1411
|
const version = pinnedVersion(name, dep);
|
|
1076
1412
|
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
1413
|
return addSkill(root, name, { force: true, version });
|
|
1080
1414
|
}
|
|
1081
|
-
function updateProject(root, names, { force = false } = {}) {
|
|
1415
|
+
function updateProject(root, names, { force = false, allowTracked = false } = {}) {
|
|
1082
1416
|
const { skills } = readManifest(root);
|
|
1083
1417
|
return Object.keys(skills).filter((name) => !names || names.includes(name)).flatMap((name) => {
|
|
1084
1418
|
const latest = latestVersion(name);
|
|
1085
|
-
if (!latest) return [{ name, action: "skipped", reason: "
|
|
1419
|
+
if (!latest) return [{ name, action: "skipped", reason: "not in your library" }];
|
|
1086
1420
|
const dep = skills[name];
|
|
1087
1421
|
const local = treeHash(join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name));
|
|
1088
1422
|
if (dep.hash === latest.hash && local === latest.hash) return [];
|
|
1089
1423
|
if (dep.hash === latest.hash && local !== null && !force) return [];
|
|
1090
|
-
const change = addSkill(root, name, { force });
|
|
1424
|
+
const change = addSkill(root, name, { force, allowTracked });
|
|
1091
1425
|
return change.action === "skipped" || change.from !== change.to || local !== latest.hash ? [change] : [];
|
|
1092
1426
|
});
|
|
1093
1427
|
}
|
|
@@ -1100,6 +1434,10 @@ function importSkill(dir, { force = false } = {}) {
|
|
|
1100
1434
|
if (existing !== null && !force) return { name, status: "exists" };
|
|
1101
1435
|
mkdirSync4(libraryDir(), { recursive: true });
|
|
1102
1436
|
const origin = existing !== null && libraryOrigins()[name] ? null : originFor(dir);
|
|
1437
|
+
if (existing !== null) {
|
|
1438
|
+
forgetLatest(name);
|
|
1439
|
+
latestVersion(name);
|
|
1440
|
+
}
|
|
1103
1441
|
copySkill(dir, to);
|
|
1104
1442
|
if (origin) recordOrigin(name, origin);
|
|
1105
1443
|
forgetLatest(name);
|
|
@@ -1134,11 +1472,14 @@ function stash(dir, bucket) {
|
|
|
1134
1472
|
return target;
|
|
1135
1473
|
}
|
|
1136
1474
|
function removeUntracked(root, name, path) {
|
|
1137
|
-
|
|
1138
|
-
|
|
1475
|
+
const stuck = notWritable(root, relative2(root, dirname3(path)));
|
|
1476
|
+
if (stuck) throw new Error(`${stuck} isn't a folder in the repo: skilllib leaves what's in it alone`);
|
|
1477
|
+
const to = stash(path, "trash");
|
|
1478
|
+
unlinkEverywhere(root, name);
|
|
1479
|
+
return to;
|
|
1139
1480
|
}
|
|
1140
1481
|
function projectsUsing(name) {
|
|
1141
|
-
return knownProjects().filter((p) =>
|
|
1482
|
+
return knownProjects().filter((p) => pinsSkill(p, name));
|
|
1142
1483
|
}
|
|
1143
1484
|
function deleteLibrarySkill(name) {
|
|
1144
1485
|
const using = projectsUsing(name);
|
|
@@ -1173,7 +1514,7 @@ function linkGlobal(path, agents) {
|
|
|
1173
1514
|
continue;
|
|
1174
1515
|
}
|
|
1175
1516
|
mkdirSync4(dir, { recursive: true });
|
|
1176
|
-
linkDir(path,
|
|
1517
|
+
linkDir(realpathOrNull(path) ?? path, join8(realpathOrNull(dir) ?? dir, name));
|
|
1177
1518
|
linked.push(link2);
|
|
1178
1519
|
}
|
|
1179
1520
|
return { linked, skipped };
|
|
@@ -1185,9 +1526,10 @@ function createSkill(name, description) {
|
|
|
1185
1526
|
mkdirSync4(dir, { recursive: true });
|
|
1186
1527
|
writeFileSync2(
|
|
1187
1528
|
join8(dir, "SKILL.md"),
|
|
1529
|
+
// JSON strings are valid YAML: a description with ": " or "#" stays one value.
|
|
1188
1530
|
`---
|
|
1189
1531
|
name: ${name}
|
|
1190
|
-
description: ${description || "Describe when an agent should use this skill."}
|
|
1532
|
+
description: ${JSON.stringify(description || "Describe when an agent should use this skill.")}
|
|
1191
1533
|
---
|
|
1192
1534
|
|
|
1193
1535
|
# ${name}
|
|
@@ -1212,6 +1554,11 @@ function listBackups() {
|
|
|
1212
1554
|
});
|
|
1213
1555
|
}).sort((a, b) => b.path.slice(-24).localeCompare(a.path.slice(-24)));
|
|
1214
1556
|
}
|
|
1557
|
+
function movedTogether(backups, backup) {
|
|
1558
|
+
if (backup.kind === "plugin") return [backup];
|
|
1559
|
+
const at = (b) => Date.parse(b.path.slice(-24).replace(/^(.{13})-(\d\d)-(\d\d)-(\d{3})Z$/, "$1:$2:$3.$4Z"));
|
|
1560
|
+
return backups.filter((b) => b.name === backup.name && b.kind === backup.kind && Math.abs(at(b) - at(backup)) <= 2e3);
|
|
1561
|
+
}
|
|
1215
1562
|
function backupFrom(backup) {
|
|
1216
1563
|
if (backup.kind === "plugin") return "Claude Code plugin";
|
|
1217
1564
|
if (!backup.from) return "unknown";
|
|
@@ -1222,12 +1569,7 @@ function restoreBackup(backup) {
|
|
|
1222
1569
|
if (!to) return { ok: false, reason: `skilllib doesn't know where it came from; its files are in ${tildify(backup.path)}` };
|
|
1223
1570
|
if (backup.kind === "tidy-backup" && isLink(to)) unlinkSync(to);
|
|
1224
1571
|
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` };
|
|
1572
|
+
if (entryExists(to)) return { ok: false, reason: `${backup.name} already exists there` };
|
|
1231
1573
|
mkdirSync4(join8(to, ".."), { recursive: true });
|
|
1232
1574
|
move(backup.path, to);
|
|
1233
1575
|
forgetLatest(backup.name);
|
|
@@ -1237,45 +1579,39 @@ function restoreBackup(backup) {
|
|
|
1237
1579
|
// src/agentSkill.ts
|
|
1238
1580
|
var AGENT_SKILL_NAME = "skilllib";
|
|
1239
1581
|
function shippedSkill() {
|
|
1240
|
-
return
|
|
1582
|
+
return readFileSync5(new URL("../skills/skilllib/SKILL.md", import.meta.url), "utf-8");
|
|
1241
1583
|
}
|
|
1242
1584
|
function agentSkillDirs(enabled = enabledHarnesses()) {
|
|
1243
1585
|
const claudeSkills = join9(claudeDir(), "skills");
|
|
1244
1586
|
const agentsSkills = join9(userHome(), ".agents", "skills");
|
|
1245
1587
|
const needsClaude = enabled.some((id) => harness(id).globalDirs().includes(claudeSkills));
|
|
1246
1588
|
const needsAgents = enabled.some((id) => !harness(id).globalDirs().includes(claudeSkills));
|
|
1247
|
-
|
|
1589
|
+
const dirs = [...needsClaude ? [claudeSkills] : [], ...needsAgents ? [agentsSkills] : []];
|
|
1590
|
+
return dirs.filter((d, i) => dirs.findIndex((o) => realPath(o) === realPath(d)) === i).map((d) => join9(d, AGENT_SKILL_NAME));
|
|
1248
1591
|
}
|
|
1249
1592
|
function isOurs(dir) {
|
|
1250
1593
|
return existsSync8(join9(dir, OWN_SKILL_MARKER));
|
|
1251
1594
|
}
|
|
1252
1595
|
function isDangling(path) {
|
|
1253
|
-
return
|
|
1254
|
-
}
|
|
1255
|
-
function entryExists2(path) {
|
|
1256
|
-
try {
|
|
1257
|
-
lstatSync3(path);
|
|
1258
|
-
return true;
|
|
1259
|
-
} catch {
|
|
1260
|
-
return false;
|
|
1261
|
-
}
|
|
1596
|
+
return entryExists(path) && lstatSync3(path).isSymbolicLink() && !existsSync8(path);
|
|
1262
1597
|
}
|
|
1263
1598
|
function agentSkillState(enabled = enabledHarnesses()) {
|
|
1264
1599
|
const [primary, ...links] = agentSkillDirs(enabled);
|
|
1265
1600
|
if (!primary || !isOurs(primary) || links.some((l) => !existsSync8(join9(l, "SKILL.md")))) return "missing";
|
|
1266
|
-
return
|
|
1601
|
+
return [primary, ...links].every((d) => readFileSync5(join9(d, "SKILL.md"), "utf-8") === shippedSkill()) ? "installed" : "outdated";
|
|
1267
1602
|
}
|
|
1268
1603
|
function installAgentSkill(enabled = enabledHarnesses()) {
|
|
1269
1604
|
const [primary, ...links] = agentSkillDirs(enabled);
|
|
1270
1605
|
if (!primary) return { ok: false, reason: "no agents chosen yet (skilllib harnesses <id>...)" };
|
|
1271
|
-
const foreign = [primary, ...links].find((d) =>
|
|
1606
|
+
const foreign = [primary, ...links].find((d) => entryExists(d) && !isOurs(d) && !isDangling(d));
|
|
1272
1607
|
if (foreign) return { ok: false, reason: `${tildify(foreign)} already exists and isn't skilllib's` };
|
|
1273
1608
|
for (const d of [primary, ...links]) if (isDangling(d)) unlinkSync2(d);
|
|
1274
1609
|
mkdirSync5(primary, { recursive: true });
|
|
1275
1610
|
writeFileSync3(join9(primary, "SKILL.md"), shippedSkill());
|
|
1276
1611
|
writeFileSync3(join9(primary, OWN_SKILL_MARKER), "Installed by skilllib. `skilllib agent-skill remove` removes it.\n");
|
|
1277
1612
|
for (const link2 of links) {
|
|
1278
|
-
if (
|
|
1613
|
+
if (entryExists(link2) && !isLink(link2) && isOurs(link2)) rmSync4(link2, { recursive: true, force: true });
|
|
1614
|
+
if (entryExists(link2)) continue;
|
|
1279
1615
|
mkdirSync5(join9(link2, ".."), { recursive: true });
|
|
1280
1616
|
linkDir(primary, link2);
|
|
1281
1617
|
}
|
|
@@ -1283,9 +1619,9 @@ function installAgentSkill(enabled = enabledHarnesses()) {
|
|
|
1283
1619
|
}
|
|
1284
1620
|
function removeAgentSkill() {
|
|
1285
1621
|
const all = agentSkillDirs(["claude-code", "codex"]);
|
|
1286
|
-
const removed = all.filter((d) =>
|
|
1622
|
+
const removed = all.filter((d) => entryExists(d) && (isOurs(d) || isDangling(d)));
|
|
1287
1623
|
for (const d of removed) {
|
|
1288
|
-
if (!
|
|
1624
|
+
if (!entryExists(d)) continue;
|
|
1289
1625
|
if (lstatSync3(d).isSymbolicLink()) unlinkSync2(d);
|
|
1290
1626
|
else rmSync4(d, { recursive: true, force: true });
|
|
1291
1627
|
}
|
|
@@ -1299,14 +1635,17 @@ function refreshAgentSkill() {
|
|
|
1299
1635
|
}
|
|
1300
1636
|
|
|
1301
1637
|
// src/commands.ts
|
|
1302
|
-
import { existsSync as existsSync14, readFileSync as
|
|
1638
|
+
import { existsSync as existsSync14, readFileSync as readFileSync7 } from "fs";
|
|
1303
1639
|
import { parseArgs as parseFlags } from "util";
|
|
1304
|
-
import { basename as basename9, join as join15, resolve as
|
|
1640
|
+
import { basename as basename9, join as join15, resolve as resolve8, sep as sep8 } from "path";
|
|
1305
1641
|
|
|
1306
1642
|
// src/git.ts
|
|
1307
1643
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
1308
|
-
import { existsSync as existsSync9 } from "fs";
|
|
1309
|
-
import { join as join10, relative as relative3 } from "path";
|
|
1644
|
+
import { existsSync as existsSync9, realpathSync as realpathSync4 } from "fs";
|
|
1645
|
+
import { isAbsolute as isAbsolute2, join as join10, relative as relative3, sep as sep5 } from "path";
|
|
1646
|
+
function isShared(state) {
|
|
1647
|
+
return state === "committed" || state === "changed";
|
|
1648
|
+
}
|
|
1310
1649
|
function git(root, args, input) {
|
|
1311
1650
|
try {
|
|
1312
1651
|
return execFileSync2("git", args, { cwd: root, encoding: "utf-8", input, stdio: ["pipe", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 });
|
|
@@ -1316,11 +1655,32 @@ function git(root, args, input) {
|
|
|
1316
1655
|
}
|
|
1317
1656
|
}
|
|
1318
1657
|
function gitInfo(root, extraDirs = []) {
|
|
1319
|
-
|
|
1320
|
-
|
|
1658
|
+
let inRepo;
|
|
1659
|
+
try {
|
|
1660
|
+
inRepo = execFileSync2("git", ["rev-parse", "--show-prefix"], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1661
|
+
} catch {
|
|
1662
|
+
if (!existsSync9(join10(root, ".git"))) return null;
|
|
1663
|
+
inRepo = "";
|
|
1664
|
+
}
|
|
1665
|
+
const real = (p) => {
|
|
1666
|
+
try {
|
|
1667
|
+
return realpathSync4.native(p);
|
|
1668
|
+
} catch {
|
|
1669
|
+
return p;
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
const realRoot = real(root);
|
|
1673
|
+
const places = new Map(
|
|
1674
|
+
[.../* @__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))
|
|
1675
|
+
);
|
|
1676
|
+
const dirs = [...new Set(places.values())];
|
|
1321
1677
|
if (dirs.length === 0) return { of: () => "new" };
|
|
1678
|
+
const placeOf = (rel) => {
|
|
1679
|
+
const dir = [...places.keys()].find((d) => rel === d || rel.startsWith(d + "/"));
|
|
1680
|
+
return dir === void 0 ? rel : places.get(dir) + rel.slice(dir.length);
|
|
1681
|
+
};
|
|
1322
1682
|
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));
|
|
1683
|
+
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
1684
|
const ignoredCache = /* @__PURE__ */ new Map();
|
|
1325
1685
|
const isIgnored = (rel) => {
|
|
1326
1686
|
if (!ignoredCache.has(rel)) {
|
|
@@ -1332,7 +1692,7 @@ function gitInfo(root, extraDirs = []) {
|
|
|
1332
1692
|
const under = (list2, rel) => list2.some((p) => p === rel || p.startsWith(rel + "/"));
|
|
1333
1693
|
return {
|
|
1334
1694
|
of: (relPath) => {
|
|
1335
|
-
const rel = relPath.replace(/\/+$/, "");
|
|
1695
|
+
const rel = placeOf(relPath.replace(/\/+$/, ""));
|
|
1336
1696
|
if (under(tracked, rel)) return under(dirty, rel) ? "changed" : "committed";
|
|
1337
1697
|
return isIgnored(rel) ? "ignored" : "new";
|
|
1338
1698
|
}
|
|
@@ -1347,13 +1707,16 @@ import { readdirSync as readdirSync8 } from "fs";
|
|
|
1347
1707
|
import { basename as basename7 } from "path";
|
|
1348
1708
|
|
|
1349
1709
|
// 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";
|
|
1710
|
+
import { existsSync as existsSync12, lstatSync as lstatSync4, readdirSync as readdirSync7 } from "fs";
|
|
1711
|
+
import { basename as basename6, dirname as dirname6, isAbsolute as isAbsolute3, join as join13, relative as relative4, sep as sep7 } from "path";
|
|
1352
1712
|
|
|
1353
1713
|
// src/plugins.ts
|
|
1354
1714
|
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
|
|
1715
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
1716
|
+
import { basename as basename4, dirname as dirname4, join as join11, resolve as resolve7 } from "path";
|
|
1717
|
+
function pluginNameOf(id) {
|
|
1718
|
+
return id.split("@")[0];
|
|
1719
|
+
}
|
|
1357
1720
|
function nonEmptyDir(path) {
|
|
1358
1721
|
try {
|
|
1359
1722
|
return readdirSync5(path).some((f) => !f.startsWith("."));
|
|
@@ -1383,6 +1746,8 @@ function claudePlugins(machine, root) {
|
|
|
1383
1746
|
scope: synced ? null : entry?.scope ?? "user",
|
|
1384
1747
|
...entry?.projectPath ? { projectPath: entry.projectPath } : {},
|
|
1385
1748
|
synced,
|
|
1749
|
+
// Turned on in your settings with no install record (as installedPlugins finds it).
|
|
1750
|
+
...!synced && !entries.length ? { settingsOnly: true } : {},
|
|
1386
1751
|
skills,
|
|
1387
1752
|
extras: extrasOf(dirname4(dirname4(skills[0])))
|
|
1388
1753
|
};
|
|
@@ -1522,8 +1887,8 @@ function ignoreLocalSettings(root) {
|
|
|
1522
1887
|
} catch {
|
|
1523
1888
|
}
|
|
1524
1889
|
try {
|
|
1525
|
-
const exclude =
|
|
1526
|
-
const before = existsSync10(exclude) ?
|
|
1890
|
+
const exclude = resolve7(root, git2("rev-parse", "--git-path", "info/exclude"));
|
|
1891
|
+
const before = existsSync10(exclude) ? readFileSync6(exclude, "utf-8") : "";
|
|
1527
1892
|
mkdirSync6(dirname4(exclude), { recursive: true });
|
|
1528
1893
|
writeFileSync4(exclude, `${before}${before && !before.endsWith("\n") ? "\n" : ""}/${git2("rev-parse", "--show-prefix")}${file}
|
|
1529
1894
|
`);
|
|
@@ -1534,7 +1899,7 @@ function ignoreLocalSettings(root) {
|
|
|
1534
1899
|
}
|
|
1535
1900
|
function turnOffIn(plugin, root) {
|
|
1536
1901
|
const done = `${plugin.id} turned off in ${basename4(root)}; other repos keep it`;
|
|
1537
|
-
const bin = plugin.synced ? null : claudeBinary();
|
|
1902
|
+
const bin = plugin.synced || plugin.settingsOnly ? null : claudeBinary();
|
|
1538
1903
|
if (!bin) {
|
|
1539
1904
|
const r2 = setPluginIn(join11(root, ".claude", "settings.local.json"), plugin.id, false);
|
|
1540
1905
|
return r2.ok ? { ok: true, message: `${done} (in .claude/settings.local.json${ignoreLocalSettings(root)})` } : { ok: false, message: `${plugin.id}: ${r2.reason}` };
|
|
@@ -1542,6 +1907,10 @@ function turnOffIn(plugin, root) {
|
|
|
1542
1907
|
const r = claude(["plugin", "disable", plugin.id, "--scope", "local"], root, bin);
|
|
1543
1908
|
return r.ok ? { ok: true, message: done } : { ok: false, message: `${plugin.id}: ${r.reason}` };
|
|
1544
1909
|
}
|
|
1910
|
+
function unplaced(p, verb) {
|
|
1911
|
+
if (!p.scope || p.scope === "user" || p.projectPath) return null;
|
|
1912
|
+
return { ok: false, message: `${p.id}: skilllib doesn't know which project it's installed in; run \`claude plugin ${verb} ${p.id} --scope ${p.scope}\` there` };
|
|
1913
|
+
}
|
|
1545
1914
|
function settingsFileOf(p) {
|
|
1546
1915
|
if (p.synced || !p.scope || p.scope === "user") return join11(claudeDir(), "settings.json");
|
|
1547
1916
|
if (!p.projectPath) return null;
|
|
@@ -1551,8 +1920,10 @@ function whereOf(p) {
|
|
|
1551
1920
|
return p.projectPath ? ` in ${basename4(p.projectPath)} (${p.scope})` : "";
|
|
1552
1921
|
}
|
|
1553
1922
|
function setPluginOn(p, on) {
|
|
1554
|
-
const
|
|
1555
|
-
|
|
1923
|
+
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" : "";
|
|
1924
|
+
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}` };
|
|
1925
|
+
const lost = unplaced(p, on ? "enable" : "disable");
|
|
1926
|
+
if (lost) return lost;
|
|
1556
1927
|
const bin = p.synced || p.settingsOnly ? null : claudeBinary();
|
|
1557
1928
|
if (bin) {
|
|
1558
1929
|
const r2 = claude(["plugin", on ? "enable" : "disable", p.id, ...p.scope ? ["--scope", p.scope] : []], p.projectPath, bin);
|
|
@@ -1560,12 +1931,14 @@ function setPluginOn(p, on) {
|
|
|
1560
1931
|
}
|
|
1561
1932
|
const r = setPluginIn(settingsFileOf(p), p.id, on);
|
|
1562
1933
|
if (!r.ok) return { ok: false, message: `${p.id}: ${r.reason}` };
|
|
1563
|
-
|
|
1934
|
+
if (p.scope === "local" && p.projectPath) return { ...done, message: done.message + ignoreLocalSettings(p.projectPath) };
|
|
1935
|
+
return done;
|
|
1564
1936
|
}
|
|
1565
1937
|
function uninstallPlugin(p) {
|
|
1566
1938
|
if (p.synced) return { ok: false, message: `${p.id} is synced from your claude.ai account: remove it there, or turn it off here` };
|
|
1567
1939
|
if (p.settingsOnly) return { ok: false, message: `${p.id} isn't installed, only turned on in your settings: turn it off instead` };
|
|
1568
|
-
|
|
1940
|
+
const lost = unplaced(p, "uninstall");
|
|
1941
|
+
if (lost) return lost;
|
|
1569
1942
|
const wasOn = enabledPlugins(p.projectPath)[p.id] === true;
|
|
1570
1943
|
const r = claude(["plugin", "uninstall", p.id, "--keep-data", "--scope", p.scope ?? "user"], p.projectPath);
|
|
1571
1944
|
if (!r.ok) return { ok: false, message: `${p.id}: ${r.reason}` };
|
|
@@ -1590,10 +1963,10 @@ function removedFile() {
|
|
|
1590
1963
|
return join11(skilllibHome(), "plugin-backup.json");
|
|
1591
1964
|
}
|
|
1592
1965
|
function removePlugin(plugin, how) {
|
|
1593
|
-
if (plugin.
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
const viaSettings = plugin.synced || !claudeBinary() && how === "off" && !plugin.projectPath;
|
|
1966
|
+
if (plugin.settingsOnly && how === "delete") return { ok: false, message: `${plugin.id} isn't installed, only turned on in your settings: turn it off instead` };
|
|
1967
|
+
const lost = unplaced(plugin, how === "delete" ? "uninstall" : "disable");
|
|
1968
|
+
if (lost) return lost;
|
|
1969
|
+
const viaSettings = plugin.synced || plugin.settingsOnly || !claudeBinary() && how === "off" && !plugin.projectPath;
|
|
1597
1970
|
if (!viaSettings && !claudeBinary()) {
|
|
1598
1971
|
return { ok: false, message: `${plugin.id}: nothing changed; Claude Code's CLI didn't run (tried \`claude\` on your PATH and ~/.local/bin/claude)` };
|
|
1599
1972
|
}
|
|
@@ -1602,7 +1975,7 @@ function removePlugin(plugin, how) {
|
|
|
1602
1975
|
const imported = plugin.skills.filter((dir) => !yours.has(basename4(dir)) && latestVersion(basename4(dir)) === null).map((dir) => importSkill(dir).name);
|
|
1603
1976
|
const scope = plugin.scope ? ["--scope", plugin.scope] : [];
|
|
1604
1977
|
const r = viaSettings ? (
|
|
1605
|
-
// Synced plugins aren't installed, so `claude plugin` can't touch them: their switch is enabledPlugins.
|
|
1978
|
+
// Synced and settings-only plugins aren't installed, so `claude plugin` can't touch them: their switch is enabledPlugins.
|
|
1606
1979
|
setPluginIn(join11(claudeDir(), "settings.json"), plugin.id, false)
|
|
1607
1980
|
) : (
|
|
1608
1981
|
// --keep-data, as uninstallPlugin: its saved data stays, so a restore brings it back as it was.
|
|
@@ -1611,7 +1984,9 @@ function removePlugin(plugin, how) {
|
|
|
1611
1984
|
const saved = imported.length ? `; ${imported.join(", ")} copied into Your skills` : "";
|
|
1612
1985
|
if (!r.ok) return { ok: false, message: `${plugin.id}: ${r.reason}${saved}` };
|
|
1613
1986
|
if (how === "delete" && !plugin.synced) recordRemovedPlugin(plugin.id, plugin.scope ?? "user", plugin.projectPath, wasOn);
|
|
1614
|
-
|
|
1987
|
+
const others = how === "delete" ? (installedPluginsJson()[plugin.id] ?? []).filter((e) => (e.scope ?? "user") !== (plugin.scope ?? "user") || e.projectPath !== plugin.projectPath) : [];
|
|
1988
|
+
const left = others.length ? `; its ${others.map((e) => `${e.scope ?? "user"} install${e.projectPath ? ` in ${basename4(e.projectPath)}` : ""}`).join(", ")} stays` : "";
|
|
1989
|
+
return { ok: true, message: `${plugin.id} ${how === "delete" && !plugin.synced ? `removed (${plugin.scope ?? "user"} install)` : "turned off"}${saved}${left}` };
|
|
1615
1990
|
}
|
|
1616
1991
|
function recordRemovedPlugin(id, scope, projectPath, on = true) {
|
|
1617
1992
|
const list2 = readState(removedFile()) ?? [];
|
|
@@ -1642,12 +2017,12 @@ function restorePlugin(backup) {
|
|
|
1642
2017
|
return { ok: true, to: where };
|
|
1643
2018
|
}
|
|
1644
2019
|
function cursorPluginSkills(plugins = cursorPlugins()) {
|
|
1645
|
-
return plugins.flatMap((p) => p.skills.map((path) => ({ name: basename4(path), plugin: p.id
|
|
2020
|
+
return plugins.flatMap((p) => p.skills.map((path) => ({ name: basename4(path), plugin: pluginNameOf(p.id), path })));
|
|
1646
2021
|
}
|
|
1647
2022
|
|
|
1648
2023
|
// src/tidy.ts
|
|
1649
2024
|
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
|
|
2025
|
+
import { basename as basename5, dirname as dirname5, join as join12, sep as sep6 } from "path";
|
|
1651
2026
|
function entriesOf(dirs, name) {
|
|
1652
2027
|
return dirs.flatMap((dir) => {
|
|
1653
2028
|
const path = join12(dir, name);
|
|
@@ -1665,12 +2040,13 @@ function hashReals(reals) {
|
|
|
1665
2040
|
function namesIn(dirs) {
|
|
1666
2041
|
const names = /* @__PURE__ */ new Set();
|
|
1667
2042
|
for (const dir of dirs) {
|
|
1668
|
-
if (!
|
|
2043
|
+
if (!isDir(dir)) continue;
|
|
1669
2044
|
for (const name of readdirSync6(dir)) if (!name.startsWith(".") && name !== "synced") names.add(name);
|
|
1670
2045
|
}
|
|
1671
2046
|
return [...names].sort();
|
|
1672
2047
|
}
|
|
1673
2048
|
var PROJECT_TIDY_DIRS = ALL_PROJECT_DIRS.filter((d) => d !== GROK_SKILLS_DIR);
|
|
2049
|
+
var repoTidyDirs = (root) => PROJECT_TIDY_DIRS.filter((d) => notWritable(root, d) === null);
|
|
1674
2050
|
function globalTidyDirs() {
|
|
1675
2051
|
const own = realPath(join12(grokDir(), "skills"));
|
|
1676
2052
|
return globalSkillDirs().filter((d) => realPath(d) !== own);
|
|
@@ -1697,11 +2073,13 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1697
2073
|
const readsOf = (id) => harness(id).projectDirs.map(abs);
|
|
1698
2074
|
let git2;
|
|
1699
2075
|
const skillsLock = projectSkillsLock(root);
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
const
|
|
2076
|
+
const dirs = repoTidyDirs(root).map(abs);
|
|
2077
|
+
for (const name of namesIn(dirs)) {
|
|
2078
|
+
const entries = entriesOf(dirs, name);
|
|
2079
|
+
const external = entries.find((e) => e.link && e.real && !(e.real + sep6).startsWith(realpathOrNull(root) + sep6));
|
|
1703
2080
|
if (external) {
|
|
1704
|
-
|
|
2081
|
+
const differs = entries.some((e) => e !== external && e.real && existsSync11(join12(e.real, "SKILL.md")) && treeHash(e.real) !== treeHash(external.real));
|
|
2082
|
+
report.skipped.push({ name, reason: `${rel(external.path)} links outside the repo`, ...differs ? { differs } : {} });
|
|
1705
2083
|
continue;
|
|
1706
2084
|
}
|
|
1707
2085
|
const reals = entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link && realpathOrNull(o.path) === realpathOrNull(e.path)) === i);
|
|
@@ -1719,7 +2097,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1719
2097
|
if (git2 === void 0) git2 = gitInfo(root);
|
|
1720
2098
|
const planned = installDirs(enabled).map(abs);
|
|
1721
2099
|
const fromSkillsSh = name in skillsLock;
|
|
1722
|
-
const rank = (e) => (
|
|
2100
|
+
const rank = (e) => (isShared(git2?.of(rel(e.path))) ? 0 : 1e3) + (fromSkillsSh && e.dir === abs(AGENTS_SKILLS_DIR) ? 0 : 100) + (planned.includes(e.dir) ? planned.indexOf(e.dir) : 50);
|
|
1723
2101
|
primary = [...reals].sort((a, b) => rank(a) - rank(b))[0];
|
|
1724
2102
|
}
|
|
1725
2103
|
if (!primary) continue;
|
|
@@ -1727,6 +2105,11 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1727
2105
|
report.conflicts.push({ ...conflictOf(name, root, reals, enabled, readsOf), ...dep ? { managed: primary.dir } : {} });
|
|
1728
2106
|
continue;
|
|
1729
2107
|
}
|
|
2108
|
+
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));
|
|
2109
|
+
if (elsewhere) {
|
|
2110
|
+
report.skipped.push({ name, reason: `${rel(elsewhere.path)} links to a different copy`, differs: true });
|
|
2111
|
+
continue;
|
|
2112
|
+
}
|
|
1730
2113
|
const wanted = new Set([...installDirs(enabled, rel(primary.dir)), ...dep?.links ?? []].map(abs));
|
|
1731
2114
|
const steps = [];
|
|
1732
2115
|
for (const e of entries) {
|
|
@@ -1734,11 +2117,12 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1734
2117
|
const needed = wanted.has(e.dir);
|
|
1735
2118
|
if (e.link) {
|
|
1736
2119
|
if (e.real === primary.real) {
|
|
1737
|
-
if (!needed && !dep &&
|
|
2120
|
+
if (!needed && !dep && isShared((git2 ??= gitInfo(root))?.of(rel(e.path)))) continue;
|
|
1738
2121
|
if (!needed) steps.push({ kind: "unlink", path: e.path });
|
|
1739
2122
|
} else steps.push(needed ? { kind: "link", path: e.path, target: primary.path } : { kind: "unlink", path: e.path });
|
|
1740
2123
|
} else {
|
|
1741
2124
|
const differs = e.hash !== primary.hash ? { differs: true } : {};
|
|
2125
|
+
if (!needed && !chosen && isShared((git2 ??= gitInfo(root))?.of(rel(e.path)))) continue;
|
|
1742
2126
|
steps.push(needed ? { kind: "replace", path: e.path, target: primary.path, ...differs } : { kind: "remove", path: e.path, ...differs });
|
|
1743
2127
|
}
|
|
1744
2128
|
}
|
|
@@ -1778,13 +2162,13 @@ function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
|
|
|
1778
2162
|
}
|
|
1779
2163
|
function gitVisibleSteps(plan, git2 = plan.root ? gitInfo(plan.root) : null) {
|
|
1780
2164
|
if (!plan.root || !git2) return [];
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
2165
|
+
const shared = (rel) => isShared(git2.of(rel));
|
|
2166
|
+
const dep = readManifest(plan.root).skills[plan.name];
|
|
2167
|
+
const recordsLink = (s) => !!dep && (s.kind === "link" || s.kind === "replace") && !dep.links?.includes(relativeTo(plan.root, dirname5(s.path)));
|
|
2168
|
+
return plan.steps.filter((s) => shared(relativeTo(plan.root, s.path)) || recordsLink(s) && shared(MANIFEST_FILE));
|
|
1785
2169
|
}
|
|
1786
|
-
function applyTidy(plan, { git: git2 = "keep"
|
|
1787
|
-
const held = git2 === "keep" ? gitVisibleSteps(plan
|
|
2170
|
+
function applyTidy(plan, { git: git2 = "keep" } = {}) {
|
|
2171
|
+
const held = git2 === "keep" ? gitVisibleSteps(plan) : [];
|
|
1788
2172
|
const applied = [];
|
|
1789
2173
|
for (const step of plan.steps) {
|
|
1790
2174
|
if (held.includes(step) || !entryExists(step.path) || "target" in step && !entryExists(step.target)) continue;
|
|
@@ -1822,18 +2206,19 @@ function describeStep(step, root) {
|
|
|
1822
2206
|
return `remove ${step.differs ? "differing" : "duplicate"} copy ${shown}`;
|
|
1823
2207
|
}
|
|
1824
2208
|
}
|
|
1825
|
-
function copyLabel(conflict, dir) {
|
|
1826
|
-
return folderLabel(conflict.root, dir);
|
|
1827
|
-
}
|
|
1828
2209
|
function folderLabel(root, dir) {
|
|
1829
2210
|
return root ? relativeTo(root, dir) : tildify(dir);
|
|
1830
2211
|
}
|
|
1831
2212
|
function foldersOf(root, name) {
|
|
1832
|
-
const entries = entriesOf(root ?
|
|
2213
|
+
const entries = entriesOf(root ? repoTidyDirs(root).map((d) => join12(root, d)) : globalTidyDirs(), name);
|
|
1833
2214
|
return { all: entries.map((e) => e.dir), real: entries.filter((e) => !e.link).map((e) => e.dir) };
|
|
1834
2215
|
}
|
|
1835
2216
|
|
|
1836
2217
|
// src/health.ts
|
|
2218
|
+
function detailWithGit(issue) {
|
|
2219
|
+
const notes = [...issue.fix ? [issue.fix] : [], ...issue.choices ?? []].filter((c) => c.hint && /\bgit\b|committed/.test(c.hint)).map((c) => `"${c.label}": ${c.hint}.`);
|
|
2220
|
+
return notes.length ? `${issue.detail} ${notes.join(" ")}` : issue.detail;
|
|
2221
|
+
}
|
|
1837
2222
|
function runFix(choice) {
|
|
1838
2223
|
try {
|
|
1839
2224
|
return { ok: true, message: choice.run() };
|
|
@@ -1865,6 +2250,16 @@ function summarize(where, verb, changes) {
|
|
|
1865
2250
|
}
|
|
1866
2251
|
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1867
2252
|
var rows = (root, skills) => skills.map((skill) => ({ root, skill }));
|
|
2253
|
+
var removesClean = (root, s) => s.managed && !s.state.startsWith("edited") && s.state !== "not in library" && !syncBlocker(root, s.name);
|
|
2254
|
+
var gitMemo = /* @__PURE__ */ new Map();
|
|
2255
|
+
var gitOf = (root) => {
|
|
2256
|
+
if (!gitMemo.has(root)) gitMemo.set(root, gitInfo(root));
|
|
2257
|
+
return gitMemo.get(root);
|
|
2258
|
+
};
|
|
2259
|
+
var gitNote = (root, paths) => {
|
|
2260
|
+
const git2 = gitOf(root);
|
|
2261
|
+
return paths.some((p) => isShared(git2?.of(relativeTo(root, p)))) ? "; git will see it" : "";
|
|
2262
|
+
};
|
|
1868
2263
|
var agentNames = (ids) => ids.map((id) => harness(id).name).join(", ");
|
|
1869
2264
|
function conflictIssue(c) {
|
|
1870
2265
|
const where = c.root ? basename6(c.root) : "your global folders";
|
|
@@ -1874,21 +2269,23 @@ function conflictIssue(c) {
|
|
|
1874
2269
|
const plan = planFor(dir);
|
|
1875
2270
|
const r = plan ? applyTidy(plan, { git: git2 }) : null;
|
|
1876
2271
|
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 ${
|
|
2272
|
+
return `${c.name}: kept the ${folderLabel(c.root, dir)} copy${r.held.length ? `; held back ${plural(r.held.length, "change")} git would see` : ""}`;
|
|
1878
2273
|
};
|
|
1879
2274
|
return {
|
|
1880
2275
|
id: `conflict:${c.root ?? "global"}:${c.name}`,
|
|
1881
2276
|
severity: "problem",
|
|
1882
2277
|
title: `${c.name} in ${where}: the copies differ`,
|
|
1883
|
-
detail: `Your agents run different versions: ${c.copies.map((x) => `${
|
|
2278
|
+
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
2279
|
at: rows(c.root, [c.name]),
|
|
1885
2280
|
choices: winners.flatMap((x) => {
|
|
1886
2281
|
const plan = planFor(x.dir);
|
|
2282
|
+
if (!plan?.steps.length) return [];
|
|
1887
2283
|
const visible2 = plan ? gitVisibleSteps(plan).map((s) => describeStep(s, c.root)) : [];
|
|
1888
|
-
const label = `Keep the ${
|
|
2284
|
+
const label = `Keep the ${folderLabel(c.root, x.dir)} copy`;
|
|
1889
2285
|
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
2286
|
return [
|
|
1891
|
-
|
|
2287
|
+
// "Keep git as it is" only when it leaves something to do.
|
|
2288
|
+
...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
2289
|
{ label: `${label}, and change what git tracks`, hint: `git will see: ${visible2.join(", ")}`, run: keep(x.dir, "go") }
|
|
1893
2290
|
];
|
|
1894
2291
|
})
|
|
@@ -1898,34 +2295,68 @@ function applyAll(plans, git2) {
|
|
|
1898
2295
|
const results = plans.map((p) => applyTidy(p, { git: git2 }));
|
|
1899
2296
|
return { skills: results.filter((r) => r.applied.length).length, held: results.reduce((n, r) => n + r.held.length, 0) };
|
|
1900
2297
|
}
|
|
1901
|
-
function brokenLinksIn(root) {
|
|
2298
|
+
function brokenLinksIn(root, opened) {
|
|
1902
2299
|
const where = basename6(root);
|
|
1903
|
-
const
|
|
1904
|
-
const hasCopy = (name) =>
|
|
2300
|
+
const tidyDirs = repoTidyDirs(root);
|
|
2301
|
+
const hasCopy = (name) => tidyDirs.some((d) => !isLink(join13(root, d, name)) && existsSync12(join13(root, d, name, "SKILL.md")));
|
|
2302
|
+
const mends = (project, dir, name) => {
|
|
2303
|
+
const dep = pinsIn(project)[name];
|
|
2304
|
+
return !!dep && (dep.links ?? []).includes(dir) && !syncBlocker(project, name, dep) && versionForHash(name, dep.hash) !== null;
|
|
2305
|
+
};
|
|
2306
|
+
const syncMends = (dir, name) => mends(root, dir, name);
|
|
1905
2307
|
let git2;
|
|
1906
|
-
|
|
2308
|
+
const realRoot = realNative(root) ?? root;
|
|
2309
|
+
const ownerOf = (real) => {
|
|
2310
|
+
let nearest = null;
|
|
2311
|
+
for (let d = dirname6(dirname6(real)); d.startsWith(realRoot + sep7); d = dirname6(d)) {
|
|
2312
|
+
if (!existsSync12(join13(d, MANIFEST_FILE))) continue;
|
|
2313
|
+
if (opened.has(d)) return { opened: true, own: d };
|
|
2314
|
+
nearest ??= d;
|
|
2315
|
+
}
|
|
2316
|
+
return { opened: false, own: nearest };
|
|
2317
|
+
};
|
|
2318
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2319
|
+
const aliased = (dir) => (realNative(join13(root, dir)) ?? "") !== join13(realRoot, dir);
|
|
2320
|
+
const packageFolders = packageSkillFolders(root).map((f) => ({ dir: f.location, top: false })).sort((a, b) => Number(aliased(a.dir)) - Number(aliased(b.dir)));
|
|
2321
|
+
const folders2 = [...ALL_PROJECT_DIRS.map((dir) => ({ dir, top: true })), ...packageFolders].flatMap(({ dir, top }) => {
|
|
2322
|
+
const real = realNative(join13(root, dir)) ?? join13(root, dir);
|
|
2323
|
+
if (seen.has(real)) return [];
|
|
2324
|
+
seen.add(real);
|
|
2325
|
+
const owner = ownerOf(real);
|
|
2326
|
+
return owner.opened ? [] : [{ dir, top: top && !owner.own, own: owner.own, real }];
|
|
2327
|
+
});
|
|
2328
|
+
return folders2.flatMap(({ dir, top, own, real }) => {
|
|
1907
2329
|
const folder = join13(root, dir);
|
|
1908
|
-
if (!
|
|
2330
|
+
if (!isDir(folder) || notWritable(root, dir)) return [];
|
|
1909
2331
|
return readdirSync7(folder).flatMap((name) => {
|
|
1910
2332
|
const path = join13(folder, name);
|
|
1911
|
-
if (!isLink(path) ||
|
|
2333
|
+
if (!isLink(path) || isDir(path) || top && (dir !== GROK_SKILLS_DIR && hasCopy(name) || syncMends(dir, name))) return [];
|
|
1912
2334
|
const remove2 = {
|
|
1913
2335
|
label: "Remove the link",
|
|
2336
|
+
hint: "restorable with skilllib restore",
|
|
1914
2337
|
run: () => {
|
|
1915
|
-
|
|
1916
|
-
|
|
2338
|
+
if (!isLink(path)) return `${dir}/${name} in ${where} is already gone`;
|
|
2339
|
+
stash(path, "tidy-backup");
|
|
2340
|
+
return `Removed the broken link ${dir}/${name} in ${where} (restorable with skilllib restore)`;
|
|
1917
2341
|
}
|
|
1918
2342
|
};
|
|
1919
|
-
|
|
1920
|
-
|
|
2343
|
+
const placeOf = (p) => relative4(realRoot, p).split(sep7).join("/");
|
|
2344
|
+
git2 ??= gitInfo(
|
|
2345
|
+
root,
|
|
2346
|
+
folders2.map((f) => placeOf(f.real)).filter((p) => p !== "" && p !== ".." && !p.startsWith("../") && !isAbsolute3(p))
|
|
2347
|
+
);
|
|
2348
|
+
const tracked = isShared(git2?.of(placeOf(join13(real, name))));
|
|
2349
|
+
const recorded = !!own && mends(own, relative4(own, real).split(sep7).join("/"), name);
|
|
2350
|
+
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
2351
|
return [
|
|
1922
2352
|
{
|
|
1923
2353
|
id: `broken:${root}:${dir}/${name}`,
|
|
1924
2354
|
severity: "problem",
|
|
1925
2355
|
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." : ""}`,
|
|
2356
|
+
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
2357
|
at: rows(root, [name]),
|
|
1928
|
-
|
|
2358
|
+
// Removed by itself only at the repo's top, uncommitted: a package's link (or a committed one) is asked first.
|
|
2359
|
+
...top && !tracked ? { fix: remove2 } : { choices: [remove2] }
|
|
1929
2360
|
}
|
|
1930
2361
|
];
|
|
1931
2362
|
});
|
|
@@ -1956,32 +2387,37 @@ function grokDiffers(projects2, machine, statusOf) {
|
|
|
1956
2387
|
return [...global, ...repos];
|
|
1957
2388
|
}
|
|
1958
2389
|
function findIssues(roots, machine, libraryNames, statusOf = projectStatus, loadedIn = (root) => skillsLoadedIn(root, machine), nestedOf = nestedSkills) {
|
|
2390
|
+
gitMemo = /* @__PURE__ */ new Map();
|
|
1959
2391
|
const { readable: projects2, unreadable } = readableRoots(roots);
|
|
1960
2392
|
const issues = unreadable.map(({ root, reason }) => ({
|
|
1961
2393
|
id: `manifest:${root}`,
|
|
1962
2394
|
severity: "problem",
|
|
1963
2395
|
title: `${basename6(root)}: skilllib.json can't be read`,
|
|
1964
2396
|
detail: `${reason}. Until it's fixed, skilllib leaves ${basename6(root)} out.`,
|
|
1965
|
-
at: []
|
|
2397
|
+
at: [{ root, skill: "" }]
|
|
1966
2398
|
}));
|
|
1967
2399
|
const statuses = new Map(projects2.map((root) => [root, statusOf(root)]));
|
|
2400
|
+
const openedProjects = new Set(projects2.map((p) => realNative(p) ?? p));
|
|
1968
2401
|
issues.push(...grokDiffers(projects2, machine, (root) => statuses.get(root)));
|
|
1969
2402
|
const nested = new Map(projects2.map((root) => [root, nestedOf(root)]));
|
|
1970
2403
|
for (const skill of machine.filter((m) => m.broken)) {
|
|
2404
|
+
const reaches = isDir(skill.path) ? "folder" : existsSync12(skill.path) ? "file" : "nothing";
|
|
2405
|
+
const remove2 = {
|
|
2406
|
+
label: "Remove the link",
|
|
2407
|
+
hint: "restorable with skilllib restore",
|
|
2408
|
+
run: () => {
|
|
2409
|
+
const r = unloadGlobal(skill.path);
|
|
2410
|
+
if (!r.ok) throw new Error(`${skill.name}: ${r.reason}`);
|
|
2411
|
+
return `Removed broken link ${skill.name}`;
|
|
2412
|
+
}
|
|
2413
|
+
};
|
|
1971
2414
|
issues.push({
|
|
1972
2415
|
id: `broken:${skill.path}`,
|
|
1973
2416
|
severity: "problem",
|
|
1974
2417
|
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.",
|
|
2418
|
+
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
2419
|
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
|
-
}
|
|
2420
|
+
...reaches === "folder" ? { choices: [remove2] } : { fix: remove2 }
|
|
1985
2421
|
});
|
|
1986
2422
|
}
|
|
1987
2423
|
const loaded = machine.filter((m) => !m.broken);
|
|
@@ -2006,18 +2442,21 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2006
2442
|
];
|
|
2007
2443
|
const pluginAgentsIn = (plugin, root) => new Set(loadedHere.get(root).filter((m) => plugin.skills.includes(m.path)).flatMap((m) => m.harnesses));
|
|
2008
2444
|
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)]);
|
|
2445
|
+
const installedYours = /* @__PURE__ */ new Set([...globalYours, ...[...statuses.values()].flat().map((s) => s.name), ...knownProjects().flatMap((p) => Object.keys(pinsIn(p)))]);
|
|
2010
2446
|
const repoChoices = (plugin, root, names) => {
|
|
2011
2447
|
const where = basename6(root);
|
|
2012
2448
|
const copies = statuses.get(root).filter((s) => names.includes(s.name));
|
|
2013
|
-
const allManaged = names.every((n) => copies.some((c) => c.name === n && c
|
|
2449
|
+
const allManaged = names.every((n) => copies.some((c) => c.name === n && removesClean(root, c)));
|
|
2450
|
+
const committed = gitNote(root, copies.map((s) => s.path)) !== "";
|
|
2451
|
+
const pluginAgents = pluginAgentsIn(plugin, root);
|
|
2452
|
+
const leftOut = copies.flatMap((s) => s.visibility.filter((v) => v.paths > 0 && !pluginAgents.has(v.id)).map((v) => v.id));
|
|
2014
2453
|
return [
|
|
2015
2454
|
{ 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
2455
|
// 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 ? [
|
|
2456
|
+
...names.length && allManaged && !committed && !leftOut.length ? [
|
|
2018
2457
|
{
|
|
2019
2458
|
label: `Remove your ${copies.map((s) => s.name).join(", ")} from ${where}`,
|
|
2020
|
-
hint:
|
|
2459
|
+
hint: `the plugin's copy stays; Your skills keeps yours${gitNote(root, [join13(root, MANIFEST_FILE)])}`,
|
|
2021
2460
|
run: () => {
|
|
2022
2461
|
for (const s of copies) {
|
|
2023
2462
|
const r = removeSkill(root, s.name);
|
|
@@ -2039,7 +2478,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2039
2478
|
const dupes = names.filter((n) => libraryNames.has(n) || loadedYours.has(n));
|
|
2040
2479
|
if (!dupes.length) continue;
|
|
2041
2480
|
const others = plugin.skills.length - dupes.length;
|
|
2042
|
-
const pluginName = plugin.id
|
|
2481
|
+
const pluginName = pluginNameOf(plugin.id);
|
|
2043
2482
|
const remove2 = {
|
|
2044
2483
|
label: `Remove the plugin ${plugin.id}`,
|
|
2045
2484
|
hint: plugin.extras.length ? `also removes its ${plugin.extras.join(", ")}` : "restorable with skilllib restore",
|
|
@@ -2058,12 +2497,13 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2058
2497
|
claudeBoth.length ? `Claude Code loads both (the plugin's as /${pluginName}:${claudeBoth[0]}). Your skill wins.` : "",
|
|
2059
2498
|
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
2499
|
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"}.` : "",
|
|
2500
|
+
others ? `Turning it off or removing it copies its other ${others === 1 ? "skill" : `${others} skills`} into Your skills first, so nothing is lost.` : "",
|
|
2501
|
+
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
2502
|
plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : "",
|
|
2503
|
+
plugin.settingsOnly ? "It's only turned on in your settings, not installed, so it can only be turned off here." : "",
|
|
2064
2504
|
inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) => basename6(root)).join(", ")}.` : ""
|
|
2065
2505
|
].filter(Boolean).join(" "),
|
|
2066
|
-
choices: [...plugin.synced ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off], ...inRepos.flatMap(([root, d]) => repoChoices(plugin, root, d))],
|
|
2506
|
+
choices: [...plugin.synced || plugin.settingsOnly ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off], ...inRepos.flatMap(([root, d]) => repoChoices(plugin, root, d))],
|
|
2067
2507
|
// Global's rows (yours and the plugin's), and the repos where both load.
|
|
2068
2508
|
at: [...rows(null, dupes), ...[...inRepos, ...cursorRepos].flatMap(([root, d]) => rows(root, d))],
|
|
2069
2509
|
plugin: plugin.id
|
|
@@ -2086,7 +2526,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2086
2526
|
for (const root of projects2) {
|
|
2087
2527
|
const where = basename6(root);
|
|
2088
2528
|
const repoOnly = loadedHere.get(root).filter((m) => m.kind === "plugin" && !machinePluginSkills.has(m.path));
|
|
2089
|
-
for (const plugin of claudePlugins(repoOnly, root)) {
|
|
2529
|
+
for (const plugin of enabledHarnesses().includes("claude-code") ? claudePlugins(repoOnly, root) : []) {
|
|
2090
2530
|
const names = plugin.skills.map((p) => basename6(p));
|
|
2091
2531
|
const inRepo = namesIn2(root).filter((n) => names.includes(n));
|
|
2092
2532
|
const dupes = names.filter((n) => inRepo.includes(n) || globalYours.has(n));
|
|
@@ -2095,7 +2535,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2095
2535
|
id: `plugin:${root}:${plugin.id}`,
|
|
2096
2536
|
severity: "problem",
|
|
2097
2537
|
title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}, on in ${where}`,
|
|
2098
|
-
detail: `${where}'s Claude Code settings turn the plugin on, so Claude Code loads both there (the plugin's as /${plugin.id
|
|
2538
|
+
detail: `${where}'s Claude Code settings turn the plugin on, so Claude Code loads both there (the plugin's as /${pluginNameOf(plugin.id)}:${dupes[0]}). Your skill wins.`,
|
|
2099
2539
|
choices: repoChoices(plugin, root, inRepo),
|
|
2100
2540
|
at: rows(root, dupes),
|
|
2101
2541
|
plugin: plugin.id
|
|
@@ -2180,19 +2620,37 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2180
2620
|
for (const root of projects2) {
|
|
2181
2621
|
const where = basename6(root);
|
|
2182
2622
|
const status2 = statuses.get(root);
|
|
2183
|
-
issues.push(...brokenLinksIn(root));
|
|
2623
|
+
issues.push(...brokenLinksIn(root, openedProjects));
|
|
2184
2624
|
const pins = readManifest(root).skills;
|
|
2185
|
-
const
|
|
2186
|
-
const
|
|
2187
|
-
|
|
2625
|
+
const blockedFor = /* @__PURE__ */ new Map();
|
|
2626
|
+
const untrackable = (s) => {
|
|
2627
|
+
if (!blockedFor.has(s.name)) blockedFor.set(s.name, trackBlocker(root, s.name));
|
|
2628
|
+
return blockedFor.get(s.name);
|
|
2629
|
+
};
|
|
2630
|
+
const blockers = new Map(status2.filter((s) => s.managed).flatMap((s) => ((b) => b ? [[s.name, b]] : [])(syncBlocker(root, s.name, pins[s.name]))));
|
|
2631
|
+
for (const [name, blocker] of blockers)
|
|
2632
|
+
issues.push({
|
|
2633
|
+
id: `stuck:${root}:${name}`,
|
|
2634
|
+
severity: "problem",
|
|
2635
|
+
title: `${name} in ${where}: sync can't put it back as skilllib.json pins it`,
|
|
2636
|
+
// What to do depends on what's there: a folder that links out of the repo is a setup of yours, and a pin
|
|
2637
|
+
// your library has no version of can't come back by sync anyway.
|
|
2638
|
+
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."}`,
|
|
2639
|
+
at: rows(root, [name])
|
|
2640
|
+
});
|
|
2641
|
+
const unstuck = status2.filter((s) => !blockers.has(s.name));
|
|
2642
|
+
const missing = unstuck.filter((s) => s.state === "folder missing" && s.latest !== null);
|
|
2643
|
+
const knownPin = (s) => versionForHash(s.name, pins[s.name]?.hash ?? "") !== null;
|
|
2644
|
+
const restorable = missing.filter(knownPin);
|
|
2645
|
+
for (const s of unstuck.filter((m) => (m.state === "folder missing" || m.state === "out of sync") && m.latest !== null && !knownPin(m)))
|
|
2188
2646
|
issues.push({
|
|
2189
2647
|
id: `pin:${root}:${s.name}`,
|
|
2190
2648
|
severity: "problem",
|
|
2191
2649
|
title: `${s.name}: the version ${where}'s skilllib.json pins isn't in your library`,
|
|
2192
|
-
detail:
|
|
2650
|
+
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
2651
|
at: rows(root, [s.name]),
|
|
2194
2652
|
// 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}` }]
|
|
2653
|
+
...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
2654
|
});
|
|
2197
2655
|
if (restorable.length > 0) {
|
|
2198
2656
|
issues.push({
|
|
@@ -2203,16 +2661,35 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2203
2661
|
at: rows(root, restorable.map((s) => s.name)),
|
|
2204
2662
|
fix: {
|
|
2205
2663
|
label: "Sync the project",
|
|
2206
|
-
|
|
2664
|
+
// Only the missing folders: an out-of-sync copy (maybe committed) is its own issue's choice.
|
|
2665
|
+
run: () => summarize(where, "restored", syncProject(root, { names: restorable.map((s) => s.name) }))
|
|
2207
2666
|
}
|
|
2208
2667
|
});
|
|
2209
2668
|
}
|
|
2210
|
-
|
|
2669
|
+
const drifted = unstuck.filter((s) => s.state === "out of sync" && knownPin(s));
|
|
2670
|
+
if (drifted.length) {
|
|
2671
|
+
const seen = drifted.some((s) => isShared(gitOf(root)?.of(relativeTo(root, s.path))));
|
|
2672
|
+
const yours = drifted.some((s) => untrackable(s));
|
|
2673
|
+
const install = {
|
|
2674
|
+
label: "Install the pinned versions",
|
|
2675
|
+
hint: seen ? "git will see the copies change" : "your library keeps every version",
|
|
2676
|
+
run: () => summarize(where, "synced", syncProject(root, { names: drifted.map((s) => s.name) }))
|
|
2677
|
+
};
|
|
2678
|
+
issues.push({
|
|
2679
|
+
id: `out-of-sync:${root}`,
|
|
2680
|
+
severity: "suggestion",
|
|
2681
|
+
title: `${where}: ${plural(drifted.length, "skill")} not at the version skilllib.json pins`,
|
|
2682
|
+
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.` : ""}`,
|
|
2683
|
+
at: rows(root, drifted.map((s) => s.name)),
|
|
2684
|
+
...seen || yours ? { choices: [install] } : { fix: install }
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
for (const s of unstuck.filter((s2) => s2.state === "folder missing" && s2.latest === null))
|
|
2211
2688
|
issues.push({
|
|
2212
2689
|
id: `unavailable:${root}:${s.name}`,
|
|
2213
2690
|
severity: "problem",
|
|
2214
2691
|
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
|
|
2692
|
+
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
2693
|
at: rows(root, [s.name]),
|
|
2217
2694
|
// Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
|
|
2218
2695
|
choices: [
|
|
@@ -2227,7 +2704,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2227
2704
|
}
|
|
2228
2705
|
]
|
|
2229
2706
|
});
|
|
2230
|
-
const behind =
|
|
2707
|
+
const behind = unstuck.filter((s) => s.state === "update available" && !untrackable(s));
|
|
2231
2708
|
if (behind.length > 0)
|
|
2232
2709
|
issues.push({
|
|
2233
2710
|
id: `update:${root}`,
|
|
@@ -2235,14 +2712,18 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2235
2712
|
title: `${where}: ${plural(behind.length, "skill")} out of date`,
|
|
2236
2713
|
detail: behind.map((s) => `${s.name} (v${s.version} \u2192 v${s.latest})`).join(", "),
|
|
2237
2714
|
at: rows(root, behind.map((s) => s.name)),
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2715
|
+
// It changes what the repo pins and the copies its agents load (often committed): a choice, never `doctor --fix`.
|
|
2716
|
+
choices: [
|
|
2717
|
+
{
|
|
2718
|
+
label: "Update to the newest versions",
|
|
2719
|
+
hint: isShared(gitOf(root)?.of(MANIFEST_FILE)) ? "git will see the new copies and skilllib.json" : "replaces each copy with your library's newest",
|
|
2720
|
+
run: () => summarize(where, "updated", updateProject(root, behind.map((s) => s.name)))
|
|
2721
|
+
}
|
|
2722
|
+
]
|
|
2242
2723
|
});
|
|
2243
2724
|
const tidy2 = planProjectTidy(root);
|
|
2244
2725
|
if (tidy2.plans.length) {
|
|
2245
|
-
const git3 =
|
|
2726
|
+
const git3 = gitOf(root);
|
|
2246
2727
|
const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p, git3).map((s) => describeStep(s, root)));
|
|
2247
2728
|
const run = (mode) => () => {
|
|
2248
2729
|
const r = applyAll(tidy2.plans, mode);
|
|
@@ -2253,7 +2734,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2253
2734
|
id: `tidy:${root}`,
|
|
2254
2735
|
severity: "suggestion",
|
|
2255
2736
|
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."),
|
|
2737
|
+
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
2738
|
at: rows(root, tidy2.plans.map((p) => p.name)),
|
|
2258
2739
|
...visible2.length ? {
|
|
2259
2740
|
choices: [
|
|
@@ -2265,8 +2746,47 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2265
2746
|
});
|
|
2266
2747
|
}
|
|
2267
2748
|
issues.push(...tidy2.conflicts.map(conflictIssue));
|
|
2268
|
-
|
|
2749
|
+
for (const s of tidy2.skipped.filter((x) => x.differs))
|
|
2750
|
+
issues.push({
|
|
2751
|
+
id: `linkdiffers:${root}:${s.name}`,
|
|
2752
|
+
severity: "problem",
|
|
2753
|
+
title: `${s.name} in ${where}: the copies differ`,
|
|
2754
|
+
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.`,
|
|
2755
|
+
at: rows(root, [s.name])
|
|
2756
|
+
});
|
|
2757
|
+
const loadsGlobally = (name, id) => {
|
|
2758
|
+
const here = status2.find((s) => s.name === name);
|
|
2759
|
+
return globalCopies.some((m) => m.name === name && m.harnesses.includes(id) && (!here || treeHash(m.path) === treeHash(here.path)));
|
|
2760
|
+
};
|
|
2269
2761
|
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 }) }));
|
|
2762
|
+
const enabledIds = enabledHarnesses();
|
|
2763
|
+
const notFolders = /* @__PURE__ */ new Set();
|
|
2764
|
+
for (const dir of new Set(enabledIds.map((id) => harness(id).projectDirs[0]))) {
|
|
2765
|
+
const blocker = notWritable(root, dir);
|
|
2766
|
+
if (!blocker || notFolders.has(blocker)) continue;
|
|
2767
|
+
notFolders.add(blocker);
|
|
2768
|
+
const who = agentNames(enabledIds.filter((id) => notWritable(root, harness(id).projectDirs[0]) === blocker));
|
|
2769
|
+
const out2 = linksOut(root, blocker);
|
|
2770
|
+
issues.push({
|
|
2771
|
+
id: `notdir:${root}:${blocker}`,
|
|
2772
|
+
severity: out2 ? "suggestion" : "problem",
|
|
2773
|
+
title: out2 ? `${where}: ${blocker} links out of the repo` : `${where}: ${blocker} isn't a folder`,
|
|
2774
|
+
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.`,
|
|
2775
|
+
at: [{ root, skill: "" }]
|
|
2776
|
+
// the repo itself: its row in Places
|
|
2777
|
+
});
|
|
2778
|
+
}
|
|
2779
|
+
for (const { s, would } of blindHere) {
|
|
2780
|
+
const way = would.inTheWay.filter((p) => !notFolders.has(p));
|
|
2781
|
+
if (!way.length || blockers.has(s.name)) continue;
|
|
2782
|
+
issues.push({
|
|
2783
|
+
id: `intheway:${root}:${s.name}`,
|
|
2784
|
+
severity: "problem",
|
|
2785
|
+
title: `${s.name} in ${where}: ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))} can't use it here`,
|
|
2786
|
+
detail: `${inTheWaySaid(root, way)}. skilllib won't link past what's there: clear the way, then run skilllib link.`,
|
|
2787
|
+
at: rows(root, [s.name])
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2270
2790
|
for (const { s, would } of blindHere.filter(({ would: w }) => !w.created.length && !w.blocked.length && w.uncommitted.length))
|
|
2271
2791
|
issues.push({
|
|
2272
2792
|
id: `uncommitted:${root}:${s.name}`,
|
|
@@ -2279,20 +2799,25 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2279
2799
|
if (partial.length) {
|
|
2280
2800
|
const missing2 = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
|
|
2281
2801
|
const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
|
|
2802
|
+
const manifestShared = partial.some((s) => s.managed) && isShared(gitOf(root)?.of(MANIFEST_FILE));
|
|
2282
2803
|
const run = (allowTracked) => () => {
|
|
2283
2804
|
if (allowTracked) allowTrackedLinks(root);
|
|
2284
|
-
const r = linkAll(root, { allowTracked });
|
|
2805
|
+
const r = linkAll(root, { allowTracked, names: partial.map((s) => s.name) });
|
|
2285
2806
|
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)
|
|
2807
|
+
if (!r.linked.length) {
|
|
2808
|
+
const still = partial.filter((s) => visibilityOf(root, s.name).some((v) => v.paths === 0 && !loadsGlobally(s.name, v.id)));
|
|
2809
|
+
if (!still.length) return `${where}: already usable by ${agentNames(missing2)}`;
|
|
2810
|
+
throw new Error(`${where}: nothing linked${notes}`);
|
|
2811
|
+
}
|
|
2287
2812
|
return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing2)}${notes}`;
|
|
2288
2813
|
};
|
|
2289
2814
|
issues.push({
|
|
2290
2815
|
id: `usable:${root}`,
|
|
2291
2816
|
severity: "suggestion",
|
|
2292
2817
|
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.` : ""}`,
|
|
2818
|
+
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
2819
|
at: rows(root, partial.map((s) => s.name)),
|
|
2295
|
-
...tracked.length ? {
|
|
2820
|
+
...manifestShared && !tracked.length ? { choices: [{ label: "Add the links", hint: "git will see skilllib.json change", run: run(false) }] } : tracked.length ? {
|
|
2296
2821
|
choices: [
|
|
2297
2822
|
// Only when some link goes where git doesn't track: otherwise this one would link nothing.
|
|
2298
2823
|
...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 +2827,17 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2302
2827
|
});
|
|
2303
2828
|
}
|
|
2304
2829
|
const differing = new Set(tidy2.conflicts.map((c) => c.name));
|
|
2305
|
-
const git2 =
|
|
2306
|
-
const committed = (s) =>
|
|
2307
|
-
for (const s of
|
|
2830
|
+
const git2 = gitOf(root);
|
|
2831
|
+
const committed = (s) => isShared(git2?.of(relativeTo(root, s.path)));
|
|
2832
|
+
for (const s of unstuck) {
|
|
2308
2833
|
if (isLink(s.path) || differing.has(s.name)) continue;
|
|
2309
|
-
if (s.state === "local only" && committed(s)) {
|
|
2834
|
+
if (s.state === "local only" && (committed(s) || untrackable(s))) {
|
|
2835
|
+
const why = committed(s) ? "it's the team's (committed)" : `${untrackable(s)}, so skilllib can't track it here`;
|
|
2310
2836
|
issues.push({
|
|
2311
2837
|
id: `local:${root}:${s.name}`,
|
|
2312
2838
|
severity: "suggestion",
|
|
2313
2839
|
title: `${s.name} in ${where} isn't in your library`,
|
|
2314
|
-
detail:
|
|
2840
|
+
detail: `Copy it into the library to reuse it in other projects. The repo's copy stays as it is: ${why}.`,
|
|
2315
2841
|
at: rows(root, [s.name]),
|
|
2316
2842
|
choices: [
|
|
2317
2843
|
{
|
|
@@ -2334,7 +2860,8 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2334
2860
|
choices: [
|
|
2335
2861
|
{
|
|
2336
2862
|
label: "Import into the library",
|
|
2337
|
-
|
|
2863
|
+
// Tracking it writes skilllib.json.
|
|
2864
|
+
hint: `copies it into Your skills${gitNote(root, [join13(root, MANIFEST_FILE)])}`,
|
|
2338
2865
|
run: () => {
|
|
2339
2866
|
intoLibrary(s.path);
|
|
2340
2867
|
track(root, s.name);
|
|
@@ -2343,7 +2870,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2343
2870
|
}
|
|
2344
2871
|
]
|
|
2345
2872
|
});
|
|
2346
|
-
} else if (s.state === "untracked copy of library skill") {
|
|
2873
|
+
} else if (s.state === "untracked copy of library skill" && !committed(s) && !untrackable(s)) {
|
|
2347
2874
|
issues.push({
|
|
2348
2875
|
id: `adopt:${root}:${s.name}`,
|
|
2349
2876
|
severity: "suggestion",
|
|
@@ -2353,12 +2880,13 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2353
2880
|
choices: [{ label: "Track it", hint: `adds it to ${where}'s skilllib.json`, run: () => `${track(root, s.name).name} tracked in ${where}` }]
|
|
2354
2881
|
});
|
|
2355
2882
|
} 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);
|
|
2883
|
+
const teams = s.state.startsWith("repo skill") || committed(s) || !!untrackable(s);
|
|
2884
|
+
const whose = s.state.startsWith("repo skill") || committed(s) ? "It's the team's copy (committed, or in the repo's own .agents/skills or .grok/skills)" : `${untrackable(s)}, so skilllib can't track it here`;
|
|
2357
2885
|
issues.push({
|
|
2358
2886
|
id: `differs:${root}:${s.name}`,
|
|
2359
2887
|
severity: "suggestion",
|
|
2360
2888
|
title: `${s.name} in ${where} differs from your library`,
|
|
2361
|
-
detail: teams ?
|
|
2889
|
+
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
2890
|
at: rows(root, [s.name]),
|
|
2363
2891
|
choices: [
|
|
2364
2892
|
{
|
|
@@ -2384,13 +2912,13 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2384
2912
|
]
|
|
2385
2913
|
]
|
|
2386
2914
|
});
|
|
2387
|
-
} else if (s.state === "from npx skills") {
|
|
2388
|
-
const teams = committed(s);
|
|
2915
|
+
} else if (s.state === "from npx skills" && !((committed(s) || untrackable(s)) && versionForHash(s.name, treeHash(s.path) ?? "") !== null)) {
|
|
2916
|
+
const teams = committed(s) || !!untrackable(s);
|
|
2389
2917
|
issues.push({
|
|
2390
2918
|
id: `npx:${root}:${s.name}`,
|
|
2391
2919
|
severity: "suggestion",
|
|
2392
2920
|
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`}.`,
|
|
2921
|
+
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
2922
|
at: rows(root, [s.name]),
|
|
2395
2923
|
choices: [
|
|
2396
2924
|
{
|
|
@@ -2431,12 +2959,21 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2431
2959
|
id: `edited:${root}:${s.name}`,
|
|
2432
2960
|
severity: "suggestion",
|
|
2433
2961
|
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
|
|
2962
|
+
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
2963
|
at: rows(root, [s.name]),
|
|
2436
|
-
choices: [
|
|
2964
|
+
choices: untrackable(s) ? [
|
|
2965
|
+
{
|
|
2966
|
+
label: "Copy the edits into Your skills",
|
|
2967
|
+
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)`,
|
|
2968
|
+
run: () => {
|
|
2969
|
+
intoLibrary(s.path, true);
|
|
2970
|
+
return `${s.name}: the edits are in Your skills as v${latestVersion(s.name)?.version}; ${where} stays as it is`;
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
] : [
|
|
2437
2974
|
{
|
|
2438
2975
|
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`,
|
|
2976
|
+
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
2977
|
run: () => {
|
|
2441
2978
|
importSkill(s.path, { force: true });
|
|
2442
2979
|
const c = addSkill(root, s.name);
|
|
@@ -2446,7 +2983,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2446
2983
|
},
|
|
2447
2984
|
{
|
|
2448
2985
|
label: "Discard the edits",
|
|
2449
|
-
hint: `back to ${pinned}; the edited copy is restorable with skilllib restore`,
|
|
2986
|
+
hint: `back to ${pinned}; the edited copy is restorable with skilllib restore${gitNote(root, [s.path, join13(root, MANIFEST_FILE)])}`,
|
|
2450
2987
|
run: () => {
|
|
2451
2988
|
const c = discardEdits(root, s.name);
|
|
2452
2989
|
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
@@ -2465,26 +3002,42 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2465
3002
|
if (alsoLoadedGlobally(n) && !copies.some((c) => c.root === root)) alsoGlobal.set(n.name, [...copies, { root, managed: false, nested: true }]);
|
|
2466
3003
|
}
|
|
2467
3004
|
}
|
|
3005
|
+
const everyRepo = [...projects2, ...knownProjects()];
|
|
3006
|
+
const nestedMemo = /* @__PURE__ */ new Map();
|
|
3007
|
+
const subsMemo = /* @__PURE__ */ new Map();
|
|
3008
|
+
const subsOf = (root) => subsMemo.get(root) ?? (subsMemo.set(root, packageDirs(root)), subsMemo.get(root));
|
|
3009
|
+
const entriesOf2 = (root) => nestedMemo.get(root) ?? (nestedMemo.set(root, nestedEntries(root, { subs: subsOf(root) })), nestedMemo.get(root));
|
|
2468
3010
|
for (const [name, copies] of alsoGlobal) {
|
|
2469
3011
|
const wheres = copies.map((c) => basename6(c.root)).join(", ");
|
|
2470
|
-
const removable = copies.filter((c) => c.managed).map((c) => c.root);
|
|
3012
|
+
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
3013
|
const repoOwn = copies.filter((c) => !c.managed).map((c) => basename6(c.root));
|
|
2472
3014
|
const at = [...copies.flatMap((c) => rows(c.root, [name])), ...rows(null, [name])];
|
|
2473
3015
|
if (kept.has(name)) {
|
|
2474
|
-
const
|
|
3016
|
+
const globalLoaders = new Set(globalCopies.filter((m) => m.name === name).flatMap((m) => m.harnesses));
|
|
3017
|
+
const onlyHere = (root) => visibilityOf(root, name).filter((v) => v.paths > 0 && !globalLoaders.has(v.id)).map((v) => harness(v.id).name);
|
|
3018
|
+
const needed = removable.filter((root) => onlyHere(root).length);
|
|
3019
|
+
const extra = removable.filter((root) => !onlyHere(root).length);
|
|
3020
|
+
const removeWheres = extra.map((r) => basename6(r)).join(", ");
|
|
2475
3021
|
issues.push({
|
|
2476
3022
|
id: `twice:${name}`,
|
|
2477
3023
|
severity: "suggestion",
|
|
2478
3024
|
title: `${name}: in ${wheres}, and you keep it global`,
|
|
2479
|
-
detail:
|
|
3025
|
+
detail: [
|
|
3026
|
+
extra.length ? `The global copy already loads in ${removeWheres} for every agent that reads the project copy, so those are extra.` : "",
|
|
3027
|
+
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.` : "",
|
|
3028
|
+
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.` : ""
|
|
3029
|
+
].filter(Boolean).join(" "),
|
|
2480
3030
|
at,
|
|
2481
|
-
choices:
|
|
3031
|
+
choices: extra.length ? [
|
|
2482
3032
|
{
|
|
2483
3033
|
label: `Remove the copies in ${removeWheres}`,
|
|
2484
|
-
|
|
3034
|
+
// Removing a managed copy edits its skilllib.json too.
|
|
3035
|
+
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
3036
|
run: () => {
|
|
2486
|
-
const
|
|
2487
|
-
|
|
3037
|
+
const results = extra.map((root) => [root, removeSkill(root, name)]);
|
|
3038
|
+
const removed = results.filter(([, c]) => c.action === "removed").map(([r]) => basename6(r));
|
|
3039
|
+
const kept2 = results.filter(([, c]) => c.action !== "removed").map(([r, c]) => `${basename6(r)} (${c.reason})`);
|
|
3040
|
+
if (kept2.length) throw new Error(`${name}: ${removed.length ? `removed from ${removed.join(", ")}; ` : ""}kept in ${kept2.join(", ")}`);
|
|
2488
3041
|
return `${name}: removed from ${removed.join(", ")}; the global copy stays`;
|
|
2489
3042
|
}
|
|
2490
3043
|
}
|
|
@@ -2492,26 +3045,31 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2492
3045
|
});
|
|
2493
3046
|
continue;
|
|
2494
3047
|
}
|
|
2495
|
-
const movable = machine.find((m) => m.name === name && m.movable);
|
|
3048
|
+
const movable = machine.find((m) => m.name === name && m.movable && !m.broken);
|
|
3049
|
+
const doomed = machine.filter((x) => x.name === name && x.movable);
|
|
3050
|
+
const differ = copiesDiffer(doomed.map((m) => m.path));
|
|
3051
|
+
const libraryDiffers = !!movable && libraryNames.has(name) && versionForHash(name, treeHash(movable.path) ?? "") === null;
|
|
3052
|
+
const blind = movable && !differ && !libraryDiffers ? blindWithoutGlobal(name, doomed, everyRepo, entriesOf2, subsOf) : [];
|
|
2496
3053
|
issues.push({
|
|
2497
3054
|
at,
|
|
2498
3055
|
id: `twice:${name}`,
|
|
2499
3056
|
severity: "suggestion",
|
|
2500
3057
|
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 ? [
|
|
3058
|
+
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)."}`,
|
|
3059
|
+
choices: movable && !differ && !libraryDiffers && !blind.length ? [
|
|
2503
3060
|
{
|
|
2504
3061
|
label: "Stop loading it globally",
|
|
2505
3062
|
hint: "restorable with skilllib restore",
|
|
2506
3063
|
run: () => {
|
|
2507
|
-
const
|
|
2508
|
-
if (
|
|
3064
|
+
const now = blindWithoutGlobal(name, doomed, everyRepo);
|
|
3065
|
+
if (now.length) throw new Error(`${name}: ${blindSaid(name, now)}`);
|
|
2509
3066
|
if (!libraryNames.has(name)) importSkill(movable.path);
|
|
3067
|
+
const left = strandedLinks(name, doomed, everyRepo);
|
|
2510
3068
|
for (const m of machine.filter((x) => x.name === name && x.movable)) {
|
|
2511
3069
|
const r = unloadGlobal(m.path, m.links);
|
|
2512
3070
|
if (!r.ok) throw new Error(`${name}: ${r.reason}`);
|
|
2513
3071
|
}
|
|
2514
|
-
return `${name} no longer loads globally`;
|
|
3072
|
+
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
3073
|
}
|
|
2516
3074
|
}
|
|
2517
3075
|
] : void 0
|
|
@@ -2548,6 +3106,7 @@ function addedAt(path) {
|
|
|
2548
3106
|
}
|
|
2549
3107
|
}
|
|
2550
3108
|
function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectStatus) {
|
|
3109
|
+
gitMemo = /* @__PURE__ */ new Map();
|
|
2551
3110
|
if (!enabledHarnesses().includes("claude-code")) return [];
|
|
2552
3111
|
const issues = [];
|
|
2553
3112
|
const since = Date.now() - uses.days * 24 * 60 * 60 * 1e3;
|
|
@@ -2555,23 +3114,23 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
|
|
|
2555
3114
|
const where = basename6(root);
|
|
2556
3115
|
const unused = statusOf(root).filter((s) => s.state !== "folder missing" && uses.inProject(root, s.name) === 0 && addedAt(s.path) <= since);
|
|
2557
3116
|
if (!unused.length) continue;
|
|
2558
|
-
const removable = unused.filter((s) => s
|
|
3117
|
+
const removable = unused.filter((s) => removesClean(root, s));
|
|
2559
3118
|
issues.push({
|
|
2560
3119
|
id: `unused:${root}`,
|
|
2561
3120
|
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.
|
|
3121
|
+
title: `${where}: ${plural(unused.length, "skill")} unused in ${plural(uses.days, "day")}`,
|
|
3122
|
+
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
3123
|
at: rows(root, unused.map((s) => s.name)),
|
|
2565
3124
|
choices: removable.length ? [
|
|
2566
3125
|
{
|
|
2567
3126
|
label: `Remove ${removable.length === unused.length ? "them" : `the ${plural(removable.length, "skill")} skilllib installed`} from ${where}`,
|
|
2568
|
-
hint:
|
|
3127
|
+
hint: `Your skills keeps them; edited ones stay${gitNote(root, [...removable.map((s) => s.path), join13(root, MANIFEST_FILE)])}`,
|
|
2569
3128
|
run: () => {
|
|
2570
3129
|
const changes = removable.map((s) => removeSkill(root, s.name));
|
|
2571
3130
|
const removed = changes.filter((c) => c.action === "removed").map((c) => c.name);
|
|
2572
3131
|
const kept = changes.filter((c) => c.action !== "removed").map((c) => c.name);
|
|
2573
|
-
if (
|
|
2574
|
-
return `${where}: removed ${removed.join(", ")}
|
|
3132
|
+
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"})`);
|
|
3133
|
+
return `${where}: removed ${removed.join(", ")}`;
|
|
2575
3134
|
}
|
|
2576
3135
|
}
|
|
2577
3136
|
] : void 0
|
|
@@ -2579,16 +3138,16 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
|
|
|
2579
3138
|
}
|
|
2580
3139
|
if (uses.asUsed)
|
|
2581
3140
|
for (const p of installedPlugins().filter((x) => x.agent === "claude-code" && x.on && x.skills.length)) {
|
|
2582
|
-
const name = p.id
|
|
3141
|
+
const name = pluginNameOf(p.id);
|
|
2583
3142
|
if (p.skills.some((path) => uses.asUsed(`${name}:${basename6(path)}`) > 0)) continue;
|
|
2584
3143
|
issues.push({
|
|
2585
3144
|
id: `plugin-unused:${installId(p)}`,
|
|
2586
3145
|
severity: "suggestion",
|
|
2587
|
-
title: `${p.id}${installedIn(p)}: its skills weren't used in ${uses.days}
|
|
3146
|
+
title: `${p.id}${installedIn(p)}: its skills weren't used in ${plural(uses.days, "day")}`,
|
|
2588
3147
|
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
3148
|
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:
|
|
3149
|
+
{ 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)) },
|
|
3150
|
+
...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
3151
|
],
|
|
2593
3152
|
plugin: p.id,
|
|
2594
3153
|
pluginAt: p.projectPath ?? null
|
|
@@ -2603,7 +3162,7 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
|
|
|
2603
3162
|
issues.push({
|
|
2604
3163
|
id: "unused:library",
|
|
2605
3164
|
severity: "suggestion",
|
|
2606
|
-
title: `${plural(idle.length, "skill")} in Your skills: in no repo, unused in ${uses.days}
|
|
3165
|
+
title: `${plural(idle.length, "skill")} in Your skills: in no repo, unused in ${plural(uses.days, "day")}`,
|
|
2607
3166
|
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
3167
|
// Your skills' rows (none loads globally, so none is in Global).
|
|
2609
3168
|
at: rows(null, idle),
|
|
@@ -2636,13 +3195,12 @@ function groupBy(items) {
|
|
|
2636
3195
|
}
|
|
2637
3196
|
var sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
|
|
2638
3197
|
function globalFrom(kind, origin, path) {
|
|
2639
|
-
if (kind === "global" || kind === "skilllib") return tildify(path.replace(/[\\/][^\\/]+$/, ""))
|
|
3198
|
+
if (kind === "global" || kind === "skilllib") return tildify(path.replace(/[\\/][^\\/]+$/, ""));
|
|
2640
3199
|
return origin;
|
|
2641
3200
|
}
|
|
2642
|
-
var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "target", "vendor", "coverage"]);
|
|
2643
3201
|
function topLevel(root) {
|
|
2644
3202
|
try {
|
|
2645
|
-
return readdirSync8(root, { withFileTypes: true }).filter((d) => !d.name.startsWith(".") && !
|
|
3203
|
+
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
3204
|
} catch {
|
|
2647
3205
|
return [];
|
|
2648
3206
|
}
|
|
@@ -2660,11 +3218,11 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2660
3218
|
const loadedBy = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
|
|
2661
3219
|
const behind = s.state.endsWith("update available");
|
|
2662
3220
|
const gitState = git2?.of(relativeTo(root, s.path));
|
|
2663
|
-
const repo = !s.managed && (s.state.startsWith("repo skill") || gitState
|
|
3221
|
+
const repo = !s.managed && (s.state.startsWith("repo skill") || isShared(gitState));
|
|
2664
3222
|
const usual = s.state === "ok" || s.state === "local only" || repo && s.state !== "from npx skills";
|
|
2665
3223
|
const attrs = {
|
|
2666
3224
|
source: s.managed ? "library" : repo ? "repo" : "local",
|
|
2667
|
-
dir: s.location !==
|
|
3225
|
+
dir: s.location !== PROJECT_SKILLS_DIR ? s.location : void 0,
|
|
2668
3226
|
version: s.version ?? void 0,
|
|
2669
3227
|
state: usual ? void 0 : s.state,
|
|
2670
3228
|
latest: behind ? s.latest : void 0,
|
|
@@ -2674,7 +3232,14 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2674
3232
|
return [JSON.stringify(attrs), s.name, () => ({ ...attrs, skills: [] })];
|
|
2675
3233
|
})
|
|
2676
3234
|
);
|
|
2677
|
-
const
|
|
3235
|
+
const own = (() => {
|
|
3236
|
+
try {
|
|
3237
|
+
return inRepo ? projectStatus(root) : [];
|
|
3238
|
+
} catch {
|
|
3239
|
+
return [];
|
|
3240
|
+
}
|
|
3241
|
+
})();
|
|
3242
|
+
const skills = group(own);
|
|
2678
3243
|
const nested = group(nestedHere);
|
|
2679
3244
|
const machine = machineSkills(agents);
|
|
2680
3245
|
const loaded = inRepo ? skillsLoadedIn(root, machine, agents) : machine;
|
|
@@ -2688,7 +3253,7 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2688
3253
|
for (const i of statusIssues(root, machine)) {
|
|
2689
3254
|
const choices = i.choices?.map((c) => c.label);
|
|
2690
3255
|
const key = `${i.detail}\0${i.fix?.label ?? ""}\0${choices?.join() ?? ""}`;
|
|
2691
|
-
const issue = issues.get(key) ?? { problems: [], ids: [], detail: i
|
|
3256
|
+
const issue = issues.get(key) ?? { problems: [], ids: [], detail: detailWithGit(i), fix: i.fix?.label, choices };
|
|
2692
3257
|
issue.problems.push(i.title);
|
|
2693
3258
|
issue.ids.push(i.id);
|
|
2694
3259
|
issues.set(key, issue);
|
|
@@ -2807,29 +3372,48 @@ function usesFromTranscriptLine(line) {
|
|
|
2807
3372
|
function transcriptFiles(sinceMs) {
|
|
2808
3373
|
const base = join14(claudeDir(), "projects");
|
|
2809
3374
|
if (!existsSync13(base)) return [];
|
|
2810
|
-
|
|
3375
|
+
const list2 = (dir) => {
|
|
3376
|
+
try {
|
|
3377
|
+
return readdirSync9(dir);
|
|
3378
|
+
} catch {
|
|
3379
|
+
return [];
|
|
3380
|
+
}
|
|
3381
|
+
};
|
|
3382
|
+
return list2(base).flatMap((project) => {
|
|
2811
3383
|
const dir = join14(base, project);
|
|
2812
|
-
if (!statSync5(dir)
|
|
2813
|
-
return
|
|
3384
|
+
if (!statSync5(dir, { throwIfNoEntry: false })?.isDirectory()) return [];
|
|
3385
|
+
return list2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join14(dir, f)).filter((f) => {
|
|
3386
|
+
try {
|
|
3387
|
+
const st = statSync5(f);
|
|
3388
|
+
return st.isFile() && st.mtimeMs >= sinceMs;
|
|
3389
|
+
} catch {
|
|
3390
|
+
return false;
|
|
3391
|
+
}
|
|
3392
|
+
});
|
|
2814
3393
|
});
|
|
2815
3394
|
}
|
|
2816
3395
|
async function scanUsage(days) {
|
|
2817
3396
|
const sinceMs = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
2818
3397
|
const seen = /* @__PURE__ */ new Map();
|
|
2819
3398
|
for (const file of transcriptFiles(sinceMs)) {
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
for (const
|
|
2823
|
-
|
|
3399
|
+
try {
|
|
3400
|
+
const lines = createInterface({ input: createReadStream(file), crlfDelay: Infinity });
|
|
3401
|
+
for await (const line of lines) {
|
|
3402
|
+
for (const use of usesFromTranscriptLine(line)) {
|
|
3403
|
+
if (Date.parse(use.at) >= sinceMs) seen.set(use.key, use);
|
|
3404
|
+
}
|
|
2824
3405
|
}
|
|
3406
|
+
} catch {
|
|
3407
|
+
continue;
|
|
2825
3408
|
}
|
|
2826
3409
|
}
|
|
2827
3410
|
return [...seen.values()];
|
|
2828
3411
|
}
|
|
3412
|
+
var skillOf = (key) => key.split(":").pop();
|
|
2829
3413
|
function summarize2(uses, projectOf = () => null) {
|
|
2830
3414
|
const bySkill = /* @__PURE__ */ new Map();
|
|
2831
3415
|
for (const use of uses) {
|
|
2832
|
-
const skill = use.skill
|
|
3416
|
+
const skill = skillOf(use.skill);
|
|
2833
3417
|
const summary = bySkill.get(skill) ?? { skill, uses: 0, lastUsed: "", projects: /* @__PURE__ */ new Set() };
|
|
2834
3418
|
summary.uses += 1;
|
|
2835
3419
|
if (use.at > summary.lastUsed) summary.lastUsed = use.at;
|
|
@@ -2845,7 +3429,8 @@ function usesByProject(uses, projectOf) {
|
|
|
2845
3429
|
const root = projectOf(use.cwd);
|
|
2846
3430
|
if (!root) continue;
|
|
2847
3431
|
const counts = byProject.get(root) ?? /* @__PURE__ */ new Map();
|
|
2848
|
-
|
|
3432
|
+
const skill = skillOf(use.skill);
|
|
3433
|
+
counts.set(skill, (counts.get(skill) ?? 0) + 1);
|
|
2849
3434
|
byProject.set(root, counts);
|
|
2850
3435
|
}
|
|
2851
3436
|
return byProject;
|
|
@@ -2857,28 +3442,39 @@ function moveGlobalSkills(skills, roots, {
|
|
|
2857
3442
|
group,
|
|
2858
3443
|
allowTracked = false,
|
|
2859
3444
|
label = basename8,
|
|
2860
|
-
|
|
3445
|
+
allowTrackedHint = "re-run with --allow-tracked"
|
|
2861
3446
|
} = {}) {
|
|
2862
3447
|
const byName = /* @__PURE__ */ new Map();
|
|
2863
3448
|
for (const m of skills) byName.set(m.name, [...byName.get(m.name) ?? [], m]);
|
|
2864
3449
|
return [...byName].map(([name, copies]) => {
|
|
2865
|
-
if (
|
|
3450
|
+
if (copiesDiffer(copies.map((m) => m.path)))
|
|
2866
3451
|
return { name, moved: false, reason: `your global copies of ${name} differ; nothing moved (keep one first: skilllib tidy --global)`, changes: [] };
|
|
2867
|
-
|
|
3452
|
+
const elsewhere = blindWithoutGlobal(name, copies, knownProjects().filter((p) => !roots.some((r) => realPath(r) === realPath(p))));
|
|
3453
|
+
if (elsewhere.length) return { name, moved: false, reason: `nothing moved: ${blindSaid(name, elsewhere, label)}`, changes: [] };
|
|
3454
|
+
const hash = treeHash(copies[0].path) ?? "";
|
|
3455
|
+
const cant = roots.flatMap((root) => ((b) => b ? [`${label(root)} (${withoutForceHint(b)})`] : [])(trackBlocker(root, name) ?? addRefusal(root, name, hash)));
|
|
3456
|
+
if (cant.length) return { name, moved: false, reason: `nothing moved: it can't be added to ${cant.join(", ")}`, changes: [] };
|
|
3457
|
+
const imported = importSkill(copies[0].path).status;
|
|
3458
|
+
if (imported === "exists")
|
|
2868
3459
|
return { name, moved: false, reason: `your library has a different ${name}; nothing moved (update your library from this copy first)`, changes: [] };
|
|
2869
3460
|
if (group) recordOrigin(name, `group: ${group}`);
|
|
2870
3461
|
const changes = roots.map((root) => [root, addSkill(root, name, { allowTracked })]);
|
|
2871
3462
|
const skipped = changes.filter(([, c]) => c.action === "skipped");
|
|
2872
|
-
const why = (reason = "") => reason
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
const
|
|
2876
|
-
if (
|
|
2877
|
-
const
|
|
2878
|
-
|
|
3463
|
+
const why = (reason = "") => withoutForceHint(reason) === reason ? reason : `${withoutForceHint(reason)}: move or remove it there first`;
|
|
3464
|
+
const kept = imported === "added" ? `; it's in Your skills now` : "";
|
|
3465
|
+
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 };
|
|
3466
|
+
const blindIn = changes.flatMap(([root, c]) => ((b) => b.length ? [{ root, c, b }] : [])(blindWithoutGlobal(name, copies, [root])));
|
|
3467
|
+
if (blindIn.length) {
|
|
3468
|
+
const per = blindIn.map(({ root, c, b }) => {
|
|
3469
|
+
const who = b.map((x) => `${harness(x.id).name}${realPath(x.root) === realPath(root) ? "" : ` in ${label(x.root)}`}`).join(", ");
|
|
3470
|
+
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);
|
|
3471
|
+
return `${who} ${b.every((x) => realPath(x.root) === realPath(root)) ? `in ${label(root)} ` : `(${label(root)}) `}couldn't see it (${next})`;
|
|
3472
|
+
});
|
|
3473
|
+
return { name, moved: false, reason: `added, but ${per.join("; ")}, so it still loads globally`, changes };
|
|
2879
3474
|
}
|
|
3475
|
+
const left = strandedLinks(name, copies, [...roots, ...knownProjects()], label);
|
|
2880
3476
|
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 };
|
|
3477
|
+
return failed && !failed.ok ? { name, moved: false, reason: failed.reason, changes } : { name, moved: true, changes, ...left.length ? { left } : {} };
|
|
2882
3478
|
});
|
|
2883
3479
|
}
|
|
2884
3480
|
|
|
@@ -2897,6 +3493,7 @@ var FLAGS = {
|
|
|
2897
3493
|
keep: { type: "string" },
|
|
2898
3494
|
to: { type: "string", multiple: true }
|
|
2899
3495
|
};
|
|
3496
|
+
var VALUE_FLAGS = Object.entries(FLAGS).flatMap(([name, f]) => f.type === "string" ? [`--${name}`] : []);
|
|
2900
3497
|
function parseArgs(argv) {
|
|
2901
3498
|
const { values: v, positionals } = parseFlags({ args: argv, options: FLAGS, allowPositionals: true, strict: true });
|
|
2902
3499
|
const days = Number(v.days ?? USAGE_DAYS);
|
|
@@ -2925,26 +3522,28 @@ function colorState(state) {
|
|
|
2925
3522
|
function withDirs(root, changes) {
|
|
2926
3523
|
const { skills } = readManifest(root);
|
|
2927
3524
|
return changes.map((c) => {
|
|
2928
|
-
const dep = c.action
|
|
2929
|
-
return dep ? { ...c, dirs: [dep.dir ??
|
|
3525
|
+
const dep = c.action !== "removed" && c.action !== "skipped" ? skills[c.name] : void 0;
|
|
3526
|
+
return dep ? { ...c, dirs: [dep.dir ?? PROJECT_SKILLS_DIR, ...(dep.links ?? []).filter((d) => !c.inTheWay?.includes(`${d}/${c.name}`))] } : c;
|
|
2930
3527
|
});
|
|
2931
3528
|
}
|
|
2932
|
-
var incomplete = (c) => c.action === "skipped" || !!c.blocked?.length || !!c.uncommitted?.length;
|
|
2933
|
-
function warnHeldLinks(c, where = "here") {
|
|
3529
|
+
var incomplete = (c) => c.action === "skipped" || !!c.blocked?.length || !!c.uncommitted?.length || !!c.inTheWay?.length;
|
|
3530
|
+
function warnHeldLinks(root, c, where = "here") {
|
|
2934
3531
|
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
3532
|
if (c.uncommitted?.length)
|
|
2936
3533
|
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.`);
|
|
3534
|
+
if (c.inTheWay?.length) warn(`${c.name}: not linked everywhere ${where}: ${inTheWaySaid(root, c.inTheWay)}. Clear the way, then run skilllib link.`);
|
|
2937
3535
|
}
|
|
2938
3536
|
function printChanges(root, changes, args) {
|
|
2939
3537
|
if (changes.some(incomplete)) process.exitCode = 1;
|
|
2940
3538
|
if (args.json) return json("changes", withDirs(root, changes));
|
|
2941
3539
|
for (const c of changes) {
|
|
2942
|
-
warnHeldLinks(c);
|
|
3540
|
+
warnHeldLinks(root, c);
|
|
2943
3541
|
if (c.action === "skipped") error(`${c.name}: ${c.reason}`);
|
|
2944
3542
|
else {
|
|
2945
|
-
const icon = c.action === "removed" ? red("-") : c.action === "reset" ? yellow("\u21BA") : green(c.action === "installed" ? "+" : c.action === "relinked" ? "\u21E2" : "\u2191");
|
|
3543
|
+
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
3544
|
const versions = c.action === "removed" ? "" : c.from && c.from !== c.to ? ` v${c.from} \u2192 v${c.to}` : c.to ? ` v${c.to}` : "";
|
|
2947
3545
|
info(` ${icon} ${c.name}${versions} ${dim(c.action)}`);
|
|
3546
|
+
if (c.reason) info(dim(` ${c.reason}`));
|
|
2948
3547
|
}
|
|
2949
3548
|
if (c.backedUp) info(dim(` local edits saved to ${tildify(c.backedUp)}; \`skilllib restore ${c.name}\` brings them back`));
|
|
2950
3549
|
}
|
|
@@ -2957,9 +3556,9 @@ function projectOfFactory(projects2) {
|
|
|
2957
3556
|
const sorted = [...projects2].sort((a, b) => b.length - a.length);
|
|
2958
3557
|
return (cwd) => {
|
|
2959
3558
|
if (!cwd) return null;
|
|
2960
|
-
const known = sorted.find((p) => cwd === p || cwd.startsWith(p +
|
|
3559
|
+
const known = sorted.find((p) => cwd === p || cwd.startsWith(p + sep8));
|
|
2961
3560
|
if (known) return known;
|
|
2962
|
-
const repo = cwd.split(`${
|
|
3561
|
+
const repo = cwd.split(`${sep8}.claude${sep8}worktrees${sep8}`)[0];
|
|
2963
3562
|
return isProjectCandidate(repo) ? repo : null;
|
|
2964
3563
|
};
|
|
2965
3564
|
}
|
|
@@ -2983,6 +3582,8 @@ function requireProject() {
|
|
|
2983
3582
|
const root = projectHere();
|
|
2984
3583
|
if (root) return root;
|
|
2985
3584
|
if (!isProjectCandidate(findProjectRoot())) {
|
|
3585
|
+
const top = findProjectRoot();
|
|
3586
|
+
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
3587
|
const stray = existsSync14(join15(userHome(), MANIFEST_FILE)) ? `, though ${tildify(join15(userHome(), MANIFEST_FILE))} makes it look like one` : "";
|
|
2987
3588
|
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
3589
|
}
|
|
@@ -2996,7 +3597,10 @@ async function status(args) {
|
|
|
2996
3597
|
const root2 = findProjectRoot();
|
|
2997
3598
|
const usage3 = await usageBySkill(args.days, root2);
|
|
2998
3599
|
json("status", usableHere(root2, new Map([...usage3].map(([name, u]) => [name, u.uses]))));
|
|
2999
|
-
|
|
3600
|
+
try {
|
|
3601
|
+
if (projectHere()) rememberProjects([root2]);
|
|
3602
|
+
} catch {
|
|
3603
|
+
}
|
|
3000
3604
|
return;
|
|
3001
3605
|
}
|
|
3002
3606
|
const root = requireProject();
|
|
@@ -3035,17 +3639,19 @@ async function status(args) {
|
|
|
3035
3639
|
}))
|
|
3036
3640
|
);
|
|
3037
3641
|
}
|
|
3038
|
-
const states = new Set(skills.map((s) => s
|
|
3642
|
+
const states = new Set(skills.map((s) => teamState(s)));
|
|
3039
3643
|
const hints = [];
|
|
3040
3644
|
if (states.has("update available")) hints.push("`skilllib update` to install the newest versions");
|
|
3041
3645
|
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");
|
|
3646
|
+
if (missing.some((s) => s.latest !== null && !syncBlocker(root, s.name))) hints.push("`skilllib sync` to restore missing skills");
|
|
3647
|
+
if (missing.some((s) => syncBlocker(root, s.name))) hints.push("`skilllib doctor` says what's in sync's way");
|
|
3043
3648
|
const notInLibrary = missing.filter((s) => s.latest === null).map((s) => s.name);
|
|
3044
3649
|
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
|
-
|
|
3650
|
+
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"}`);
|
|
3651
|
+
const at = (s) => relativeTo(root, s.path);
|
|
3652
|
+
for (const s of skills.filter((x) => x.state === "edited locally" || x.state === "edited locally, update available"))
|
|
3653
|
+
hints.push(`\`skilllib import ${at(s)} --force\` to save ${s.name}'s local edits to the library`);
|
|
3654
|
+
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
3655
|
if (states.has("untracked copy of library skill") || states.has("untracked, differs from library"))
|
|
3050
3656
|
hints.push("`skilllib add <name>` to manage an untracked skill (--force if it differs)");
|
|
3051
3657
|
if (hints.length > 0) info("\n" + hints.map((h) => dim(`\u2192 ${h}`)).join("\n"));
|
|
@@ -3084,14 +3690,14 @@ async function list(args) {
|
|
|
3084
3690
|
skills.map((s) => ({
|
|
3085
3691
|
Skill: s.name,
|
|
3086
3692
|
Description: truncate(s.description, 50),
|
|
3087
|
-
From: dim(truncate(origins[s.name] ?? "", 30)),
|
|
3693
|
+
From: dim(truncate(tildify(origins[s.name] ?? ""), 30)),
|
|
3088
3694
|
Projects: (installs.get(s.name) ?? []).join(", ") || dim("none"),
|
|
3089
3695
|
[`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0)
|
|
3090
3696
|
}))
|
|
3091
3697
|
);
|
|
3092
3698
|
}
|
|
3093
3699
|
const libraryNames = new Set(skills.map((s) => s.name));
|
|
3094
|
-
const globals = [...new Set(yourGlobals().map((m) => m.name))];
|
|
3700
|
+
const globals = [...new Set(yourGlobals().filter((m) => m.harnesses.length).map((m) => m.name))];
|
|
3095
3701
|
if (globals.length > 0) {
|
|
3096
3702
|
info("");
|
|
3097
3703
|
warn(`${globals.length} of your global ${globals.length === 1 ? "skill still loads" : "skills still load"} in every project`);
|
|
@@ -3105,7 +3711,7 @@ async function list(args) {
|
|
|
3105
3711
|
}
|
|
3106
3712
|
function teamStateIn(root, extraDirs = []) {
|
|
3107
3713
|
const git2 = gitInfo(root, [...new Set(extraDirs)]);
|
|
3108
|
-
const committed = (path) =>
|
|
3714
|
+
const committed = (path) => isShared(git2?.of(relativeTo(root, path)));
|
|
3109
3715
|
const asRepo = {
|
|
3110
3716
|
"local only": "repo skill",
|
|
3111
3717
|
"untracked copy of library skill": "repo skill, in library",
|
|
@@ -3139,13 +3745,14 @@ function projects(args) {
|
|
|
3139
3745
|
table(
|
|
3140
3746
|
roots.map((root) => {
|
|
3141
3747
|
const skills = projectStatus(root);
|
|
3142
|
-
const
|
|
3143
|
-
const
|
|
3748
|
+
const teamState = teamStateIn(root);
|
|
3749
|
+
const usual = /* @__PURE__ */ new Set(["ok", "local only", "repo skill", "repo skill, in library", "from npx skills"]);
|
|
3750
|
+
const attention = skills.filter((s) => !usual.has(teamState(s))).length;
|
|
3144
3751
|
return {
|
|
3145
3752
|
Project: basename9(root),
|
|
3146
3753
|
Path: dim(tildify(root)),
|
|
3147
|
-
Skills:
|
|
3148
|
-
"Local only": String(skills.filter((s) => s
|
|
3754
|
+
Skills: skills.map((s) => s.name).join(", ") || dim("none"),
|
|
3755
|
+
"Local only": String(skills.filter((s) => teamState(s) === "local only").length),
|
|
3149
3756
|
Status: attention === 0 ? green("ok") : yellow(`${attention} need attention`)
|
|
3150
3757
|
};
|
|
3151
3758
|
})
|
|
@@ -3156,8 +3763,7 @@ function add(args) {
|
|
|
3156
3763
|
throw new Error("Usage: skilllib add <skill-name>... [--force]");
|
|
3157
3764
|
}
|
|
3158
3765
|
const root = requireProject();
|
|
3159
|
-
|
|
3160
|
-
printChanges(root, args.positional.map((name) => addSkill(root, name, { force: args.force })), args);
|
|
3766
|
+
printChanges(root, args.positional.map((name) => addSkill(root, name, { force: args.force, allowTracked: args.allowTracked })), args);
|
|
3161
3767
|
rememberProjects([root]);
|
|
3162
3768
|
}
|
|
3163
3769
|
function remove(args) {
|
|
@@ -3170,7 +3776,6 @@ function remove(args) {
|
|
|
3170
3776
|
function changeEach(args, change, nothing) {
|
|
3171
3777
|
const roots = projectsFor(args);
|
|
3172
3778
|
const results = roots.map((root) => {
|
|
3173
|
-
if (args.allowTracked) allowTrackedLinks(root);
|
|
3174
3779
|
const changes = change(root);
|
|
3175
3780
|
if (changes.some(incomplete)) process.exitCode = 1;
|
|
3176
3781
|
if (!args.json) {
|
|
@@ -3197,7 +3802,7 @@ function update(args) {
|
|
|
3197
3802
|
changeEach(
|
|
3198
3803
|
args,
|
|
3199
3804
|
(root) => {
|
|
3200
|
-
const changes = updateProject(root, names, { force: args.force });
|
|
3805
|
+
const changes = updateProject(root, names, { force: args.force, allowTracked: args.allowTracked });
|
|
3201
3806
|
if (!args.all) {
|
|
3202
3807
|
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
3808
|
}
|
|
@@ -3220,7 +3825,7 @@ function outdated(args) {
|
|
|
3220
3825
|
}
|
|
3221
3826
|
function importSkills(args) {
|
|
3222
3827
|
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) =>
|
|
3828
|
+
const dirs = args.global ? yourGlobals().map((m) => m.path) : args.positional.map((p) => resolve8(p));
|
|
3224
3829
|
if (dirs.length === 0) {
|
|
3225
3830
|
throw new Error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
|
|
3226
3831
|
}
|
|
@@ -3240,7 +3845,7 @@ function importSkills(args) {
|
|
|
3240
3845
|
continue;
|
|
3241
3846
|
}
|
|
3242
3847
|
success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
|
|
3243
|
-
const root =
|
|
3848
|
+
const root = resolve8(dir, "..", "..", "..");
|
|
3244
3849
|
const dep = projectHere(root) === root ? readManifest(root).skills[name] : void 0;
|
|
3245
3850
|
if (dep && realpathOrNull(join15(root, dep.dir ?? PROJECT_SKILLS_DIR, name)) === realpathOrNull(dir) && dep.hash !== treeHash(dir)) {
|
|
3246
3851
|
const change = addSkill(root, name);
|
|
@@ -3259,7 +3864,8 @@ function scan(args) {
|
|
|
3259
3864
|
const found = discoverProjects();
|
|
3260
3865
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
3261
3866
|
const skillNames = (root) => [
|
|
3262
|
-
|
|
3867
|
+
// Not a folder that links out of the repo: what's in it isn't the repo's (as in status).
|
|
3868
|
+
.../* @__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
3869
|
];
|
|
3264
3870
|
const withSkills = found.filter((root) => skillNames(root).length > 0);
|
|
3265
3871
|
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
|
|
@@ -3277,47 +3883,42 @@ ${yellow("Yellow")} skills aren't in your library yet (\`skilllib import <skill-
|
|
|
3277
3883
|
function folders(args) {
|
|
3278
3884
|
const [action, ...paths] = args.positional;
|
|
3279
3885
|
if (action && !["add", "remove", "rm"].includes(action)) throw new Error("Usage: skilllib folders [add|remove <folder>...]");
|
|
3280
|
-
|
|
3886
|
+
const failures = [];
|
|
3281
3887
|
if (action === "add") {
|
|
3282
3888
|
for (const p of paths) {
|
|
3283
3889
|
try {
|
|
3284
3890
|
success(`Added ${tildify(addRoot(p))}`);
|
|
3285
3891
|
} catch (e) {
|
|
3286
|
-
|
|
3287
|
-
failed = true;
|
|
3892
|
+
failures.push(e.message);
|
|
3288
3893
|
}
|
|
3289
3894
|
}
|
|
3290
3895
|
discoverProjects();
|
|
3291
3896
|
} else if (action === "remove" || action === "rm") {
|
|
3292
3897
|
for (const p of paths) {
|
|
3293
3898
|
if (removeRoot(p)) success(`Removed ${tildify(expandHome(p))}`);
|
|
3294
|
-
else {
|
|
3295
|
-
warn(`${tildify(expandHome(p))} wasn't a project folder`);
|
|
3296
|
-
failed = true;
|
|
3297
|
-
}
|
|
3899
|
+
else failures.push(`${tildify(expandHome(p))} wasn't a project folder`);
|
|
3298
3900
|
}
|
|
3299
3901
|
}
|
|
3300
3902
|
const { roots, hidden } = readConfig();
|
|
3301
3903
|
info(roots.length ? `Project folders:
|
|
3302
3904
|
${roots.map((r) => ` ${tildify(r)}`).join("\n")}` : "No project folders. Add one: skilllib folders add ~/Projects");
|
|
3303
3905
|
if (hidden.length) info(dim(`Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")} (skilllib unhide <path>)`));
|
|
3304
|
-
if (
|
|
3906
|
+
if (failures.length) throw new Error(failures.join("; "));
|
|
3305
3907
|
}
|
|
3306
3908
|
function unhide(args) {
|
|
3307
3909
|
if (args.positional.length === 0) {
|
|
3308
3910
|
throw new Error("Usage: skilllib unhide <path>...");
|
|
3309
3911
|
}
|
|
3310
|
-
|
|
3912
|
+
const failures = [];
|
|
3311
3913
|
for (const p of args.positional) {
|
|
3312
3914
|
const path = expandHome(p);
|
|
3313
3915
|
if (setHidden(path, false)) success(`${tildify(path)} is visible again`);
|
|
3314
3916
|
else {
|
|
3315
3917
|
const { hidden } = readConfig();
|
|
3316
|
-
|
|
3317
|
-
failed = true;
|
|
3918
|
+
failures.push(`${tildify(path)} wasn't hidden. ${hidden.length ? `Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")}` : "No projects are hidden."}`);
|
|
3318
3919
|
}
|
|
3319
3920
|
}
|
|
3320
|
-
if (
|
|
3921
|
+
if (failures.length) throw new Error(failures.join("; "));
|
|
3321
3922
|
}
|
|
3322
3923
|
function newSkill(args) {
|
|
3323
3924
|
const [name, ...rest] = args.positional;
|
|
@@ -3341,16 +3942,13 @@ function restore(args) {
|
|
|
3341
3942
|
}
|
|
3342
3943
|
const newest = backups.find((b) => b.name === name);
|
|
3343
3944
|
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) {
|
|
3945
|
+
const failures = [];
|
|
3946
|
+
for (const backup of movedTogether(backups, newest)) {
|
|
3347
3947
|
const r = backup.kind === "plugin" ? restorePlugin(backup) : restoreBackup(backup);
|
|
3348
3948
|
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
|
-
}
|
|
3949
|
+
else failures.push(r.reason);
|
|
3353
3950
|
}
|
|
3951
|
+
if (failures.length) throw new Error(`Can't restore ${name}: ${failures.join("; ")}`);
|
|
3354
3952
|
}
|
|
3355
3953
|
async function allIssues(args, withUsage = true) {
|
|
3356
3954
|
const projects2 = visibleProjects();
|
|
@@ -3390,7 +3988,7 @@ async function doctor(args) {
|
|
|
3390
3988
|
id,
|
|
3391
3989
|
severity,
|
|
3392
3990
|
title,
|
|
3393
|
-
detail,
|
|
3991
|
+
detail: detailWithGit({ detail, fix, choices }),
|
|
3394
3992
|
fix: fix?.label,
|
|
3395
3993
|
choices: choices?.map((c) => c.label),
|
|
3396
3994
|
fixed: args.fix && fix ? fixed(fix) : void 0
|
|
@@ -3402,7 +4000,8 @@ async function doctor(args) {
|
|
|
3402
4000
|
for (const issue of issues) {
|
|
3403
4001
|
info(`${issue.severity === "problem" ? red("\u25CF") : yellow("\u25C6")} ${issue.title}`);
|
|
3404
4002
|
info(dim(` ${issue.detail}${issue.fix ? ` \u2192 ${issue.fix.label}` : ""}`));
|
|
3405
|
-
|
|
4003
|
+
const spans = new Set(issue.at?.map((a) => a.root)).size > 1;
|
|
4004
|
+
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
4005
|
if (args.fix && issue.fix) {
|
|
3407
4006
|
const r = fixed(issue.fix);
|
|
3408
4007
|
info(` ${r.ok ? green("\u2713") : red("\u2717")} ${r.message}`);
|
|
@@ -3420,7 +4019,7 @@ async function apply(args) {
|
|
|
3420
4019
|
const byLabel = pick === void 0 ? void 0 : found.find((i) => optionsOf(i).some((c) => c.label.toLowerCase() === pick.toLowerCase()));
|
|
3421
4020
|
if (!byLabel && found.some((i) => labels(i) !== labels(found[0]))) {
|
|
3422
4021
|
const all = [...new Set(found.flatMap((i) => optionsOf(i).map((c) => c.label)))];
|
|
3423
|
-
throw new Error(`${found[0].title}: its choices differ
|
|
4022
|
+
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
4023
|
}
|
|
3425
4024
|
const issue = byLabel ?? found[0];
|
|
3426
4025
|
const options = optionsOf(issue);
|
|
@@ -3481,14 +4080,14 @@ function show(args) {
|
|
|
3481
4080
|
path: dir,
|
|
3482
4081
|
latest: latestVersion(name)?.version ?? null,
|
|
3483
4082
|
origin: libraryOrigins()[name] ?? "",
|
|
3484
|
-
projects:
|
|
3485
|
-
skillMd:
|
|
4083
|
+
projects: projectsUsing(name).map((p) => basename9(p)),
|
|
4084
|
+
skillMd: readFileSync7(join15(dir, "SKILL.md"), "utf-8")
|
|
3486
4085
|
});
|
|
3487
4086
|
}
|
|
3488
4087
|
info(`${name} ${dim(tildify(dir))}`);
|
|
3489
4088
|
if (description) info(`
|
|
3490
4089
|
${description}`);
|
|
3491
|
-
const using =
|
|
4090
|
+
const using = projectsUsing(name);
|
|
3492
4091
|
info(`
|
|
3493
4092
|
Installed in: ${using.map((p) => tildify(p)).join(", ") || "no projects"}`);
|
|
3494
4093
|
}
|
|
@@ -3512,25 +4111,33 @@ New installs go to ${installDirs(enabled).join(" (real copy) + ")}${installDirs(
|
|
|
3512
4111
|
function link(args) {
|
|
3513
4112
|
const roots = projectsFor(args);
|
|
3514
4113
|
for (const root of roots) {
|
|
3515
|
-
|
|
3516
|
-
const res = linkAll(root);
|
|
4114
|
+
const res = linkAll(root, { allowTracked: args.allowTracked });
|
|
3517
4115
|
const label = args.all ? `${basename9(root)}: ` : "";
|
|
3518
4116
|
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
|
|
4117
|
+
else if (!args.all && !res.blocked.length && !res.uncommitted.length && !res.inTheWay.length) {
|
|
4118
|
+
const missing = projectStatus(root).filter((s) => s.state === "folder missing").map((s) => s.name);
|
|
4119
|
+
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)`);
|
|
4120
|
+
else success("Every skill here is already usable by all your agents");
|
|
4121
|
+
}
|
|
4122
|
+
if (res.inTheWay.length) {
|
|
4123
|
+
process.exitCode = 1;
|
|
4124
|
+
error(`${label}some skills weren't linked everywhere: ${inTheWaySaid(root, res.inTheWay)}. Clear the way, then link again`);
|
|
4125
|
+
}
|
|
3520
4126
|
if (res.blocked.length) {
|
|
3521
4127
|
process.exitCode = 1;
|
|
3522
4128
|
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
4129
|
}
|
|
3524
|
-
if (res.uncommitted.length)
|
|
3525
|
-
|
|
4130
|
+
if (res.uncommitted.length) {
|
|
4131
|
+
process.exitCode = 1;
|
|
3526
4132
|
warn(
|
|
3527
4133
|
`${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
4134
|
);
|
|
4135
|
+
}
|
|
3529
4136
|
}
|
|
3530
4137
|
}
|
|
3531
4138
|
function globalCommand(args) {
|
|
3532
4139
|
const [action, ...names] = args.positional;
|
|
3533
|
-
const yours = yourGlobals();
|
|
4140
|
+
const yours = yourGlobals().filter((m) => m.harnesses.length);
|
|
3534
4141
|
if (action === "move") return moveGlobal(args, names, yours);
|
|
3535
4142
|
if (action === "keep" || action === "unkeep") {
|
|
3536
4143
|
const unknown = names.filter((n) => !yours.some((m) => m.name === n));
|
|
@@ -3572,10 +4179,11 @@ function moveGlobal(args, names, yours) {
|
|
|
3572
4179
|
if (args.json) {
|
|
3573
4180
|
return json(
|
|
3574
4181
|
"global move",
|
|
3575
|
-
moves.map(({ name, moved, reason, changes }) => ({
|
|
4182
|
+
moves.map(({ name, moved, reason, changes, left }) => ({
|
|
3576
4183
|
skill: name,
|
|
3577
4184
|
moved,
|
|
3578
4185
|
...reason ? { reason } : {},
|
|
4186
|
+
...left ? { left } : {},
|
|
3579
4187
|
projects: changes.map(([root, c]) => ({ project: basename9(root), path: root, change: withDirs(root, [c])[0] }))
|
|
3580
4188
|
}))
|
|
3581
4189
|
);
|
|
@@ -3583,13 +4191,14 @@ function moveGlobal(args, names, yours) {
|
|
|
3583
4191
|
for (const m of moves) {
|
|
3584
4192
|
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
4193
|
else error(`${m.name}: ${m.reason}`);
|
|
3586
|
-
for (const [root, c] of m.changes) warnHeldLinks(c, `in ${basename9(root)}`);
|
|
4194
|
+
for (const [root, c] of m.changes) warnHeldLinks(root, c, `in ${basename9(root)}`);
|
|
4195
|
+
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
4196
|
}
|
|
3588
4197
|
if (moves.some((m) => m.moved)) info(dim("Commit skilllib.json and the skill folders in each repo."));
|
|
3589
4198
|
}
|
|
3590
4199
|
function tidy(args) {
|
|
3591
4200
|
const names = args.positional;
|
|
3592
|
-
const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ?
|
|
4201
|
+
const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve8(base, args.keep) : expandHome(args.keep)])) : {};
|
|
3593
4202
|
if (args.keep && !names.length) {
|
|
3594
4203
|
throw new Error("Usage: skilllib tidy <name>... --keep <folder>");
|
|
3595
4204
|
}
|
|
@@ -3621,7 +4230,9 @@ function tidy(args) {
|
|
|
3621
4230
|
for (const { label, report } of reports) {
|
|
3622
4231
|
const plans = report.plans.filter((p) => !names.length || names.includes(p.name));
|
|
3623
4232
|
const conflicts = report.conflicts.filter((c) => !names.length || names.includes(c.name));
|
|
3624
|
-
|
|
4233
|
+
const skippedNamed = names.length ? report.skipped.filter((x) => names.includes(x.name) && x.differs) : [];
|
|
4234
|
+
if (skippedNamed.length) process.exitCode = 1;
|
|
4235
|
+
if (!plans.length && !conflicts.length && !skippedNamed.length && !args.all) success(`${label}: nothing to tidy (no extra copies or links)`);
|
|
3625
4236
|
for (const plan of plans) {
|
|
3626
4237
|
const wouldHold = args.allowGit ? [] : gitVisibleSteps(plan);
|
|
3627
4238
|
const result = args.dryRun ? { applied: plan.steps.filter((s) => !wouldHold.includes(s)), held: wouldHold } : applyTidy(plan, { git: args.allowGit ? "go" : "keep" });
|
|
@@ -3630,21 +4241,25 @@ function tidy(args) {
|
|
|
3630
4241
|
held += result.held.length;
|
|
3631
4242
|
changed += result.applied.length;
|
|
3632
4243
|
}
|
|
3633
|
-
if (args.keep
|
|
4244
|
+
if (conflicts.length && (args.keep || names.length)) process.exitCode = 1;
|
|
3634
4245
|
for (const c of conflicts) {
|
|
3635
4246
|
warn(`${label} \xB7 ${c.name}: the copies differ`);
|
|
3636
4247
|
for (const copy of c.copies) {
|
|
3637
4248
|
const runs = copy.runs.map((id) => harness(id).name);
|
|
3638
|
-
info(` ${
|
|
4249
|
+
info(` ${folderLabel(c.root, copy.dir)}${runs.length ? dim(` run by ${runs.join(", ")}`) : ""}${c.managed === copy.dir ? dim(" (skilllib's copy)") : ""}`);
|
|
3639
4250
|
}
|
|
3640
|
-
const pick =
|
|
4251
|
+
const pick = folderLabel(c.root, c.managed ?? c.copies[0].dir);
|
|
3641
4252
|
info(dim(` keep one: skilllib tidy ${c.name}${args.global ? " --global" : ""} --keep ${pick}`));
|
|
3642
4253
|
}
|
|
3643
4254
|
for (const s of report.skipped.filter((x) => !names.length || names.includes(x.name))) info(dim(`- ${label} \xB7 ${s.name}: skipped, ${s.reason}`));
|
|
3644
4255
|
}
|
|
3645
4256
|
if (args.dryRun) info(dim("\nNothing changed (--dry-run)."));
|
|
3646
|
-
else if (changed) info(dim(
|
|
3647
|
-
|
|
4257
|
+
else if (changed) info(dim(`
|
|
4258
|
+
Replaced copies are in ${tildify(join15(skilllibHome(), "tidy-backup"))}; \`skilllib restore\` puts them back.`));
|
|
4259
|
+
if (held) {
|
|
4260
|
+
process.exitCode = 1;
|
|
4261
|
+
info(dim(`${held} change${held === 1 ? "" : "s"} held back because git would see them. Re-run with --allow-git to make them.`));
|
|
4262
|
+
}
|
|
3648
4263
|
}
|
|
3649
4264
|
function agentSkill(args) {
|
|
3650
4265
|
const action = args.positional[0];
|
|
@@ -3673,17 +4288,22 @@ function agentSkill(args) {
|
|
|
3673
4288
|
}
|
|
3674
4289
|
|
|
3675
4290
|
export {
|
|
4291
|
+
realPath,
|
|
3676
4292
|
skilllibHome,
|
|
3677
4293
|
PROJECT_SKILLS_DIR,
|
|
3678
4294
|
HARNESSES,
|
|
3679
4295
|
harness,
|
|
4296
|
+
installDirs,
|
|
3680
4297
|
readSkillInfo,
|
|
3681
4298
|
treeHash,
|
|
4299
|
+
isDir,
|
|
3682
4300
|
versionHistory,
|
|
3683
4301
|
forgetLatest,
|
|
3684
4302
|
latestVersion,
|
|
3685
4303
|
projectHere,
|
|
3686
4304
|
readManifest,
|
|
4305
|
+
knownProjects,
|
|
4306
|
+
rememberProjects,
|
|
3687
4307
|
readableRoots,
|
|
3688
4308
|
setJsonMode,
|
|
3689
4309
|
isJsonMode,
|
|
@@ -3709,10 +4329,19 @@ export {
|
|
|
3709
4329
|
recordOrigin,
|
|
3710
4330
|
librarySkills,
|
|
3711
4331
|
librarySkillDir,
|
|
3712
|
-
|
|
4332
|
+
notWritable,
|
|
4333
|
+
inTheWaySaid,
|
|
4334
|
+
entryExists,
|
|
4335
|
+
survives,
|
|
4336
|
+
blindWithoutGlobal,
|
|
4337
|
+
strandedLinks,
|
|
4338
|
+
blindSaid,
|
|
4339
|
+
trackBlocker,
|
|
3713
4340
|
projectStatus,
|
|
3714
4341
|
linkEverywhere,
|
|
3715
4342
|
relinkDependency,
|
|
4343
|
+
withoutForceHint,
|
|
4344
|
+
addRefusal,
|
|
3716
4345
|
addSkill,
|
|
3717
4346
|
removeSkill,
|
|
3718
4347
|
updateProject,
|
|
@@ -3723,7 +4352,9 @@ export {
|
|
|
3723
4352
|
deleteGlobal,
|
|
3724
4353
|
createSkill,
|
|
3725
4354
|
listBackups,
|
|
4355
|
+
movedTogether,
|
|
3726
4356
|
restoreBackup,
|
|
4357
|
+
isShared,
|
|
3727
4358
|
gitInfo,
|
|
3728
4359
|
relativeTo,
|
|
3729
4360
|
installedPlugins,
|
|
@@ -3744,7 +4375,9 @@ export {
|
|
|
3744
4375
|
findIssues,
|
|
3745
4376
|
usageIssues,
|
|
3746
4377
|
scanUsage,
|
|
4378
|
+
skillOf,
|
|
3747
4379
|
moveGlobalSkills,
|
|
4380
|
+
VALUE_FLAGS,
|
|
3748
4381
|
parseArgs,
|
|
3749
4382
|
projectOfFactory,
|
|
3750
4383
|
status,
|