skilllib 1.11.2 → 1.11.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -40
- package/dist/{chunk-OFWHWIER.js → chunk-CS6T66EE.js} +1564 -677
- package/dist/index.js +45 -13
- package/dist/json.d.cts +20 -7
- package/dist/json.d.ts +20 -7
- package/dist/{tui-NGTOOVAQ.js → tui-BDLJOMSC.js} +469 -801
- package/package.json +2 -2
- package/skills/skilllib/SKILL.md +10 -7
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import { resolve as resolve2, sep } from "path";
|
|
3
3
|
|
|
4
4
|
// src/paths.ts
|
|
5
|
-
import { realpathSync } from "fs";
|
|
5
|
+
import { mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
|
|
6
6
|
import { homedir } from "os";
|
|
7
|
-
import { join, resolve } from "path";
|
|
7
|
+
import { dirname, join, resolve } from "path";
|
|
8
8
|
function userHome() {
|
|
9
9
|
return process.env.HOME || homedir();
|
|
10
10
|
}
|
|
@@ -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");
|
|
@@ -41,6 +41,47 @@ function isRepoSkillLocation(location) {
|
|
|
41
41
|
return location === AGENTS_SKILLS_DIR || location === GROK_SKILLS_DIR;
|
|
42
42
|
}
|
|
43
43
|
var OWN_SKILL_MARKER = ".managed-by-skilllib";
|
|
44
|
+
function readJson(path) {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function realpathOrNull(path) {
|
|
52
|
+
try {
|
|
53
|
+
return realpathSync(path);
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function readState(path) {
|
|
59
|
+
let text;
|
|
60
|
+
try {
|
|
61
|
+
text = readFileSync(path, "utf-8");
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (err.code === "ENOENT") return null;
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(text);
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error(`${path} isn't valid JSON: fix it, or move it away to start that file over`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function writeState(path, data) {
|
|
73
|
+
const target = realpathOrNull(path) ?? path;
|
|
74
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
75
|
+
const temp = `${target}.${process.pid}.tmp`;
|
|
76
|
+
try {
|
|
77
|
+
writeFileSync(temp, JSON.stringify(data, null, 2) + "\n", { mode: statSync(target, { throwIfNoEntry: false })?.mode ?? 420 });
|
|
78
|
+
renameSync(temp, target);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
rmSync(temp, { force: true });
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
var BUILD_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "vendor", "target", "coverage"]);
|
|
44
85
|
|
|
45
86
|
// src/output.ts
|
|
46
87
|
var jsonMode = false;
|
|
@@ -63,30 +104,29 @@ var warn = (msg) => out(`${yellow("!")} ${msg}`);
|
|
|
63
104
|
var error = (msg) => console.error(`${red("\u2717")} ${msg}`);
|
|
64
105
|
function tildify(path) {
|
|
65
106
|
for (const home2 of /* @__PURE__ */ new Set([resolve2(userHome()), realPath(userHome())]))
|
|
66
|
-
if (path === home2 || path.startsWith(home2 + sep))
|
|
67
|
-
return path;
|
|
107
|
+
if (path === home2 || path.startsWith(home2 + sep)) path = "~" + path.slice(home2.length);
|
|
108
|
+
return path.replace(/\\/g, "/");
|
|
68
109
|
}
|
|
69
110
|
function truncate(s, max) {
|
|
70
111
|
const line = s.replace(/\s+/g, " ");
|
|
71
112
|
return line.length > max ? line.slice(0, max - 1) + "\u2026" : line;
|
|
72
113
|
}
|
|
73
114
|
var visible = (s) => s.replace(/\x1b\[\d+m/g, "");
|
|
74
|
-
function table(
|
|
75
|
-
if (
|
|
76
|
-
const keys = Object.keys(
|
|
77
|
-
const widths = keys.map((k) => Math.max(k.length, ...
|
|
115
|
+
function table(rows2) {
|
|
116
|
+
if (rows2.length === 0) return;
|
|
117
|
+
const keys = Object.keys(rows2[0]);
|
|
118
|
+
const widths = keys.map((k) => Math.max(k.length, ...rows2.map((r) => visible(r[k] ?? "").length)));
|
|
78
119
|
const pad = (s, w) => s + " ".repeat(Math.max(0, w - visible(s).length));
|
|
79
120
|
info(dim(keys.map((k, i) => pad(k, widths[i])).join(" ")));
|
|
80
|
-
for (const row of
|
|
121
|
+
for (const row of rows2) info(keys.map((k, i) => pad(row[k] ?? "", widths[i])).join(" ").trimEnd());
|
|
122
|
+
}
|
|
123
|
+
function localStamp(ms) {
|
|
124
|
+
const at = new Date(ms);
|
|
125
|
+
return new Date(ms - at.getTimezoneOffset() * 6e4).toISOString().slice(0, 16).replace("T", " ");
|
|
81
126
|
}
|
|
82
|
-
|
|
83
|
-
// src/agentSkill.ts
|
|
84
|
-
import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirSync6, readFileSync as readFileSync7, rmSync as rmSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
85
|
-
import { join as join9 } from "path";
|
|
86
127
|
|
|
87
128
|
// src/config.ts
|
|
88
|
-
import { existsSync as existsSync5,
|
|
89
|
-
import { homedir as homedir2 } from "os";
|
|
129
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
90
130
|
import { join as join6, resolve as resolve4 } from "path";
|
|
91
131
|
|
|
92
132
|
// src/harnesses.ts
|
|
@@ -185,16 +225,16 @@ function installDirs(enabled, keep) {
|
|
|
185
225
|
}
|
|
186
226
|
|
|
187
227
|
// src/project.ts
|
|
188
|
-
import { existsSync as existsSync4,
|
|
189
|
-
import { dirname, join as join5, resolve as resolve3 } from "path";
|
|
228
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
229
|
+
import { dirname as dirname2, join as join5, resolve as resolve3 } from "path";
|
|
190
230
|
|
|
191
231
|
// src/versions.ts
|
|
192
|
-
import { existsSync as existsSync3, mkdirSync
|
|
232
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
193
233
|
import { join as join4 } from "path";
|
|
194
234
|
|
|
195
235
|
// src/skills.ts
|
|
196
236
|
import { createHash } from "crypto";
|
|
197
|
-
import { cpSync, existsSync as existsSync2, readdirSync, readFileSync, realpathSync as realpathSync2, rmSync, statSync } from "fs";
|
|
237
|
+
import { cpSync, existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, realpathSync as realpathSync2, rmSync as rmSync2, statSync as statSync2 } from "fs";
|
|
198
238
|
import { basename, join as join3, relative, sep as sep2 } from "path";
|
|
199
239
|
import { parse as parseYaml } from "yaml";
|
|
200
240
|
var IGNORED = /* @__PURE__ */ new Set([".DS_Store", ".git", "node_modules"]);
|
|
@@ -202,7 +242,7 @@ function readSkillInfo(dir) {
|
|
|
202
242
|
const skillMd = join3(dir, "SKILL.md");
|
|
203
243
|
const fallback = { name: basename(dir), description: "" };
|
|
204
244
|
if (!existsSync2(skillMd)) return fallback;
|
|
205
|
-
const match =
|
|
245
|
+
const match = readFileSync2(skillMd, "utf-8").replace(/^\uFEFF/, "").match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
206
246
|
if (!match) return fallback;
|
|
207
247
|
try {
|
|
208
248
|
const fm = parseYaml(match[1]);
|
|
@@ -211,35 +251,47 @@ function readSkillInfo(dir) {
|
|
|
211
251
|
description: typeof fm?.description === "string" ? fm.description.trim() : ""
|
|
212
252
|
};
|
|
213
253
|
} catch {
|
|
214
|
-
|
|
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") };
|
|
215
256
|
}
|
|
216
257
|
}
|
|
217
258
|
function isSkillDir(dir) {
|
|
218
259
|
return existsSync2(join3(dir, "SKILL.md"));
|
|
219
260
|
}
|
|
220
|
-
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);
|
|
221
265
|
return readdirSync(dir).filter((entry) => !IGNORED.has(entry)).flatMap((entry) => {
|
|
222
266
|
const full = join3(dir, entry);
|
|
223
|
-
const st =
|
|
224
|
-
return !st ? [] : st.isDirectory() ? listFiles(full) : [full];
|
|
267
|
+
const st = statSync2(full, { throwIfNoEntry: false });
|
|
268
|
+
return !st ? [] : st.isDirectory() ? listFiles(full, seen) : [full];
|
|
225
269
|
});
|
|
226
270
|
}
|
|
227
271
|
function treeHash(dir) {
|
|
228
|
-
if (!
|
|
272
|
+
if (!isDir(dir)) return null;
|
|
229
273
|
const hash = createHash("sha256");
|
|
230
|
-
const files = listFiles(dir).map((f) => ({ path: relative(dir, f).split(sep2).join("/"), full: f })).sort((a, b) => a.path.localeCompare(b.path));
|
|
274
|
+
const files = listFiles(dir).map((f) => ({ path: relative(dir, f).split(sep2).join("/"), full: f })).sort((a, b) => a.path.localeCompare(b.path, "en"));
|
|
231
275
|
for (const file of files) {
|
|
232
|
-
hash.update(file.path).update("\0").update(createHash("sha256").update(
|
|
276
|
+
hash.update(file.path).update("\0").update(createHash("sha256").update(readFileSync2(file.full)).digest("hex")).update("\n");
|
|
233
277
|
}
|
|
234
278
|
return hash.digest("hex").slice(0, 16);
|
|
235
279
|
}
|
|
236
280
|
function copySkill(from, to) {
|
|
237
281
|
const source = realpathSync2(from);
|
|
238
|
-
|
|
239
|
-
|
|
282
|
+
rmSync2(to, { recursive: true, force: true });
|
|
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
|
+
}
|
|
240
292
|
}
|
|
241
293
|
function skillDirsIn(dir) {
|
|
242
|
-
if (!
|
|
294
|
+
if (!isDir(dir)) return [];
|
|
243
295
|
return readdirSync(dir, { withFileTypes: true }).filter((d) => (d.isDirectory() || d.isSymbolicLink()) && isSkillDir(join3(dir, d.name))).map((d) => join3(dir, d.name)).sort();
|
|
244
296
|
}
|
|
245
297
|
|
|
@@ -251,11 +303,7 @@ function indexFile(name) {
|
|
|
251
303
|
return join4(storeDir(name), "versions.json");
|
|
252
304
|
}
|
|
253
305
|
function versionHistory(name) {
|
|
254
|
-
|
|
255
|
-
return JSON.parse(readFileSync2(indexFile(name), "utf-8"));
|
|
256
|
-
} catch {
|
|
257
|
-
return [];
|
|
258
|
-
}
|
|
306
|
+
return readState(indexFile(name)) ?? [];
|
|
259
307
|
}
|
|
260
308
|
function versionDir(name, version) {
|
|
261
309
|
return join4(storeDir(name), String(version));
|
|
@@ -277,9 +325,10 @@ function latestVersion(name) {
|
|
|
277
325
|
result = last;
|
|
278
326
|
} else {
|
|
279
327
|
const next = { version: (last?.version ?? 0) + 1, hash, date: (/* @__PURE__ */ new Date()).toISOString() };
|
|
280
|
-
|
|
328
|
+
if (existsSync3(versionDir(name, next.version))) throw new Error(`${versionDir(name, next.version)} exists but ${indexFile(name)} doesn't list it: fix the index before adding versions`);
|
|
329
|
+
mkdirSync2(storeDir(name), { recursive: true });
|
|
281
330
|
copySkill(source, versionDir(name, next.version));
|
|
282
|
-
|
|
331
|
+
writeState(indexFile(name), [...history, next]);
|
|
283
332
|
result = next;
|
|
284
333
|
}
|
|
285
334
|
}
|
|
@@ -298,7 +347,7 @@ function findProjectRoot(from = process.cwd()) {
|
|
|
298
347
|
let dir = resolve3(from);
|
|
299
348
|
while (true) {
|
|
300
349
|
if (existsSync4(join5(dir, MANIFEST_FILE)) || existsSync4(join5(dir, ".git"))) return dir;
|
|
301
|
-
const parent =
|
|
350
|
+
const parent = dirname2(dir);
|
|
302
351
|
if (parent === dir) return resolve3(from);
|
|
303
352
|
dir = parent;
|
|
304
353
|
}
|
|
@@ -310,13 +359,23 @@ function projectHere(from = process.cwd()) {
|
|
|
310
359
|
function readManifest(root) {
|
|
311
360
|
const path = join5(root, MANIFEST_FILE);
|
|
312
361
|
if (!existsSync4(path)) return { skills: {} };
|
|
313
|
-
|
|
362
|
+
let data;
|
|
363
|
+
try {
|
|
364
|
+
data = JSON.parse(readFileSync3(path, "utf-8"));
|
|
365
|
+
} catch {
|
|
366
|
+
throw new Error(`${path} isn't valid JSON (a merge conflict?): fix it, then run skilllib again`);
|
|
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 })`);
|
|
314
370
|
const skills = Object.fromEntries(
|
|
315
371
|
Object.entries(data.skills ?? {}).flatMap(([name, value]) => {
|
|
316
|
-
if (name.startsWith("@")) return [];
|
|
372
|
+
if (name.startsWith("@") || !name || name === "." || name === ".." || /[\\/]/.test(name)) return [];
|
|
317
373
|
if (typeof value === "string") return [[name, { version: versionForHash(name, value)?.version ?? 0, hash: value }]];
|
|
318
374
|
if (value && typeof value === "object" && "hash" in value) {
|
|
319
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(", ")})`);
|
|
320
379
|
return [
|
|
321
380
|
[
|
|
322
381
|
name,
|
|
@@ -329,14 +388,14 @@ function readManifest(root) {
|
|
|
329
388
|
]
|
|
330
389
|
];
|
|
331
390
|
}
|
|
332
|
-
|
|
391
|
+
throw new Error(`${path} isn't a valid skilllib.json: ${name}'s pin has no hash`);
|
|
333
392
|
})
|
|
334
393
|
);
|
|
335
394
|
return { ...data, skills };
|
|
336
395
|
}
|
|
337
396
|
function writeManifest(root, manifest) {
|
|
338
|
-
const skills = Object.fromEntries(Object.entries(manifest.skills).sort(([a], [b]) => a.localeCompare(b)));
|
|
339
|
-
|
|
397
|
+
const skills = Object.fromEntries(Object.entries(manifest.skills).sort(([a], [b]) => a.localeCompare(b, "en")));
|
|
398
|
+
writeState(join5(root, MANIFEST_FILE), { ...manifest, skills });
|
|
340
399
|
}
|
|
341
400
|
function projectsFile() {
|
|
342
401
|
return join5(skilllibHome(), "projects.json");
|
|
@@ -344,7 +403,7 @@ function projectsFile() {
|
|
|
344
403
|
function knownProjects() {
|
|
345
404
|
const path = projectsFile();
|
|
346
405
|
if (!existsSync4(path)) return [];
|
|
347
|
-
const list2 =
|
|
406
|
+
const list2 = readState(path) ?? [];
|
|
348
407
|
return onePerFolder(list2.filter((p) => existsSync4(p) && isProjectCandidate(p) && !isAgentWorktree(p)));
|
|
349
408
|
}
|
|
350
409
|
function isAgentWorktree(path) {
|
|
@@ -360,13 +419,34 @@ function onePerFolder(paths) {
|
|
|
360
419
|
return [...new Set(byReal.values())];
|
|
361
420
|
}
|
|
362
421
|
function isProjectCandidate(root) {
|
|
363
|
-
|
|
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);
|
|
364
425
|
}
|
|
365
426
|
function rememberProjects(roots) {
|
|
366
427
|
const incoming = roots.map((r) => resolve3(r)).filter((r) => isProjectCandidate(r) && !isAgentWorktree(r));
|
|
367
428
|
const all = onePerFolder([...knownProjects(), ...incoming]).sort();
|
|
368
|
-
|
|
369
|
-
|
|
429
|
+
writeState(projectsFile(), all);
|
|
430
|
+
}
|
|
431
|
+
function readableRoots(roots) {
|
|
432
|
+
const unreadable = [];
|
|
433
|
+
const readable = roots.filter((root) => {
|
|
434
|
+
try {
|
|
435
|
+
readManifest(root);
|
|
436
|
+
return true;
|
|
437
|
+
} catch (err) {
|
|
438
|
+
unreadable.push({ root, reason: err.message });
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
return { readable, unreadable };
|
|
443
|
+
}
|
|
444
|
+
function pinsIn(root) {
|
|
445
|
+
try {
|
|
446
|
+
return readManifest(root).skills;
|
|
447
|
+
} catch {
|
|
448
|
+
return {};
|
|
449
|
+
}
|
|
370
450
|
}
|
|
371
451
|
|
|
372
452
|
// src/config.ts
|
|
@@ -374,24 +454,19 @@ function configFile() {
|
|
|
374
454
|
return join6(skilllibHome(), "config.json");
|
|
375
455
|
}
|
|
376
456
|
function readConfig() {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
return { ...data, roots: data.roots ?? [], hidden: data.hidden ?? [] };
|
|
380
|
-
} catch {
|
|
381
|
-
return { roots: [], hidden: [] };
|
|
382
|
-
}
|
|
457
|
+
const data = readState(configFile()) ?? {};
|
|
458
|
+
return { ...data, roots: data.roots ?? [], hidden: data.hidden ?? [] };
|
|
383
459
|
}
|
|
384
460
|
function writeConfig(config) {
|
|
385
|
-
|
|
386
|
-
writeFileSync3(configFile(), JSON.stringify(config, null, 2) + "\n");
|
|
461
|
+
writeState(configFile(), config);
|
|
387
462
|
}
|
|
388
463
|
function expandHome(path) {
|
|
389
|
-
return resolve4(path.replace(/^~(
|
|
464
|
+
return resolve4(path.replace(/^~(?=$|[\\/])/, userHome()));
|
|
390
465
|
}
|
|
391
466
|
function addRoot(path) {
|
|
392
467
|
const root = expandHome(path);
|
|
393
468
|
if (!existsSync5(root)) throw new Error(`${tildify(root)} doesn't exist`);
|
|
394
|
-
if (!
|
|
469
|
+
if (!statSync3(root).isDirectory()) throw new Error(`${tildify(root)} isn't a folder`);
|
|
395
470
|
const config = readConfig();
|
|
396
471
|
if (!config.roots.includes(root)) writeConfig({ ...config, roots: [...config.roots, root].sort() });
|
|
397
472
|
return root;
|
|
@@ -410,7 +485,7 @@ function setHidden(project, hidden) {
|
|
|
410
485
|
writeConfig({ ...config, hidden: hidden ? [...rest, project].sort() : rest });
|
|
411
486
|
return true;
|
|
412
487
|
}
|
|
413
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
488
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([...BUILD_DIRS, "Library", "Applications", "Pictures", "Music", "Movies"]);
|
|
414
489
|
function findProjects(dir, depth = 4) {
|
|
415
490
|
if (depth < 0 || !existsSync5(dir)) return [];
|
|
416
491
|
if (isProjectCandidate(dir)) {
|
|
@@ -462,37 +537,31 @@ function setKeepGlobal(names, keep) {
|
|
|
462
537
|
writeConfig({ ...readConfig(), keepGlobal: [...kept].sort() });
|
|
463
538
|
}
|
|
464
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
|
+
|
|
465
544
|
// src/library.ts
|
|
466
545
|
import { execFileSync } from "child_process";
|
|
467
|
-
import { cpSync as cpSync2, existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as
|
|
468
|
-
import { basename as basename3, dirname as
|
|
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";
|
|
469
548
|
|
|
470
549
|
// src/sources.ts
|
|
471
|
-
import { existsSync as existsSync6, lstatSync, mkdirSync as
|
|
472
|
-
import { basename as basename2, join as join7, resolve as resolve5 } from "path";
|
|
473
|
-
|
|
474
|
-
try {
|
|
475
|
-
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
476
|
-
} catch {
|
|
477
|
-
return null;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
550
|
+
import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readlinkSync } from "fs";
|
|
551
|
+
import { basename as basename2, join as join7, resolve as resolve5, sep as sep3 } from "path";
|
|
552
|
+
import { tmpdir } from "os";
|
|
480
553
|
function skillsShLock() {
|
|
481
554
|
return readJson(join7(userHome(), ".agents", ".skill-lock.json"))?.skills ?? {};
|
|
482
555
|
}
|
|
483
556
|
function projectSkillsLock(root) {
|
|
484
557
|
return readJson(join7(root, "skills-lock.json"))?.skills ?? {};
|
|
485
558
|
}
|
|
486
|
-
function
|
|
487
|
-
|
|
488
|
-
return realpathSync3(path);
|
|
489
|
-
} catch {
|
|
490
|
-
return null;
|
|
491
|
-
}
|
|
559
|
+
function vendorSkill(path, kind, origin, harnesses) {
|
|
560
|
+
return { name: basename2(path), kind, origin, path, description: readSkillInfo(path).description, movable: false, broken: false, harnesses, links: [] };
|
|
492
561
|
}
|
|
493
562
|
function harnessesReading(dir, enabled) {
|
|
494
563
|
const real = realPath(dir);
|
|
495
|
-
return enabled.filter((id) =>
|
|
564
|
+
return enabled.filter((id) => harness(id).globalDirs().some((d) => realPath(d) === real));
|
|
496
565
|
}
|
|
497
566
|
var systemDirs = () => [codexSystemDir()];
|
|
498
567
|
function globalSkillDirs() {
|
|
@@ -511,24 +580,14 @@ function globalDirAs(dir) {
|
|
|
511
580
|
}
|
|
512
581
|
function systemSkills(enabled) {
|
|
513
582
|
return systemDirs().flatMap(
|
|
514
|
-
(dir) => skillDirsIn(dir).map((path) => (
|
|
515
|
-
name: basename2(path),
|
|
516
|
-
kind: "system",
|
|
517
|
-
origin: dir.replace(/\\/g, "/"),
|
|
518
|
-
path,
|
|
519
|
-
description: readSkillInfo(path).description,
|
|
520
|
-
movable: false,
|
|
521
|
-
broken: false,
|
|
522
|
-
harnesses: harnessesReading(dir, enabled),
|
|
523
|
-
links: []
|
|
524
|
-
}))
|
|
583
|
+
(dir) => skillDirsIn(dir).map((path) => vendorSkill(path, "system", dir.replace(/\\/g, "/"), harnessesReading(dir, enabled)))
|
|
525
584
|
).filter((s) => s.harnesses.length > 0);
|
|
526
585
|
}
|
|
527
586
|
function globalFolderSkills(enabled) {
|
|
528
587
|
const lock = skillsShLock();
|
|
529
588
|
const agentsDir = join7(userHome(), ".agents", "skills");
|
|
530
589
|
const entries = globalSkillDirs().flatMap((dir) => {
|
|
531
|
-
if (!
|
|
590
|
+
if (!isDir(dir)) return [];
|
|
532
591
|
return readdirSync3(dir).filter((name) => !name.startsWith(".") && name !== "synced").flatMap((name) => {
|
|
533
592
|
const path = join7(dir, name);
|
|
534
593
|
const isLink2 = lstatSync(path).isSymbolicLink();
|
|
@@ -550,13 +609,13 @@ function globalFolderSkills(enabled) {
|
|
|
550
609
|
const inAgents = e.dir === agentsDir || target_.includes(".agents/skills");
|
|
551
610
|
const locked = lock[e.name]?.source;
|
|
552
611
|
if (existsSync6(join7(e.path, OWN_SKILL_MARKER))) {
|
|
553
|
-
skills.push(
|
|
612
|
+
skills.push(vendorSkill(e.path, "skilllib", "skilllib", harnessesReading(e.dir, enabled)));
|
|
554
613
|
continue;
|
|
555
614
|
}
|
|
556
615
|
skills.push({
|
|
557
616
|
name: e.name,
|
|
558
617
|
kind: locked || e.isLink && inAgents ? "skills.sh" : "global",
|
|
559
|
-
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)`),
|
|
560
619
|
path: e.path,
|
|
561
620
|
description: e.broken ? "" : readSkillInfo(e.path).description,
|
|
562
621
|
movable: true,
|
|
@@ -573,36 +632,17 @@ function globalFolderSkills(enabled) {
|
|
|
573
632
|
}
|
|
574
633
|
return skills;
|
|
575
634
|
}
|
|
576
|
-
function tildify2(p) {
|
|
577
|
-
return tildify(p).replace(/\\/g, "/");
|
|
578
|
-
}
|
|
579
635
|
function grokBuiltInSkills(enabled) {
|
|
580
636
|
if (!enabled.includes("grok")) return [];
|
|
581
|
-
return skillDirsIn(join7(grokDir(), "bundled", "skills")).map((path) => (
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
description: readSkillInfo(path).description,
|
|
587
|
-
movable: false,
|
|
588
|
-
broken: false,
|
|
589
|
-
harnesses: ["grok"],
|
|
590
|
-
links: []
|
|
591
|
-
}));
|
|
637
|
+
return skillDirsIn(join7(grokDir(), "bundled", "skills")).map((path) => vendorSkill(path, "built-in", "Grok", ["grok"]));
|
|
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"]));
|
|
592
642
|
}
|
|
593
643
|
function cursorBuiltInSkills(enabled) {
|
|
594
644
|
if (!enabled.includes("cursor")) return [];
|
|
595
|
-
return skillDirsIn(join7(userHome(), ".cursor", "skills-cursor")).map((path) => (
|
|
596
|
-
name: basename2(path),
|
|
597
|
-
kind: "built-in",
|
|
598
|
-
origin: "Cursor",
|
|
599
|
-
path,
|
|
600
|
-
description: readSkillInfo(path).description,
|
|
601
|
-
movable: false,
|
|
602
|
-
broken: false,
|
|
603
|
-
harnesses: ["cursor"],
|
|
604
|
-
links: []
|
|
605
|
-
}));
|
|
645
|
+
return skillDirsIn(join7(userHome(), ".cursor", "skills-cursor")).map((path) => vendorSkill(path, "built-in", "Cursor", ["cursor"]));
|
|
606
646
|
}
|
|
607
647
|
function claudeAiSkills(enabled) {
|
|
608
648
|
const byReal = /* @__PURE__ */ new Map();
|
|
@@ -616,23 +656,18 @@ function claudeAiSkills(enabled) {
|
|
|
616
656
|
byReal.set(real, entry);
|
|
617
657
|
}
|
|
618
658
|
return [...byReal.values()].flatMap(
|
|
619
|
-
({ dir, readers }) => readdirSync3(dir).filter((d) => !d.startsWith(".")).flatMap(
|
|
620
|
-
(bucket) => skillDirsIn(join7(dir, bucket)).map((path) => (
|
|
621
|
-
name: basename2(path),
|
|
622
|
-
kind: "claude.ai",
|
|
623
|
-
origin: "claude.ai account",
|
|
624
|
-
path,
|
|
625
|
-
description: readSkillInfo(path).description,
|
|
626
|
-
movable: false,
|
|
627
|
-
broken: false,
|
|
628
|
-
harnesses: enabled.filter((id) => readers.includes(id)),
|
|
629
|
-
links: []
|
|
630
|
-
}))
|
|
659
|
+
({ dir, readers }) => (isDir(dir) ? readdirSync3(dir) : []).filter((d) => !d.startsWith(".")).flatMap(
|
|
660
|
+
(bucket) => skillDirsIn(join7(dir, bucket)).map((path) => vendorSkill(path, "claude.ai", "claude.ai account", enabled.filter((id) => readers.includes(id))))
|
|
631
661
|
)
|
|
632
662
|
);
|
|
633
663
|
}
|
|
664
|
+
function installedPluginsJson() {
|
|
665
|
+
return readJson(join7(claudeDir(), "plugins", "installed_plugins.json"))?.plugins ?? {};
|
|
666
|
+
}
|
|
634
667
|
function pluginDirCandidates(id, installPaths) {
|
|
635
668
|
const [name = "", marketplace = ""] = id.split("@");
|
|
669
|
+
const oneName = (s) => s !== "" && s !== "." && s !== ".." && !/[\\/]/.test(s);
|
|
670
|
+
if (!oneName(name) || !oneName(marketplace)) return installPaths;
|
|
636
671
|
const plugins = join7(claudeDir(), "plugins");
|
|
637
672
|
return [
|
|
638
673
|
...installPaths,
|
|
@@ -653,7 +688,7 @@ function pluginSkills(harnesses, root) {
|
|
|
653
688
|
const cursor = harnesses.includes("cursor");
|
|
654
689
|
if (!claude2 && !cursor) return [];
|
|
655
690
|
const plugins = join7(claudeDir(), "plugins");
|
|
656
|
-
const installed =
|
|
691
|
+
const installed = installedPluginsJson();
|
|
657
692
|
const user = enabledPlugins();
|
|
658
693
|
const enabled = root ? enabledPlugins(root) : user;
|
|
659
694
|
const fromMarketplaces = [.../* @__PURE__ */ new Set([...Object.keys(enabled), ...Object.keys(user)])].filter((id) => claude2 && enabled[id] || cursor && user[id]).flatMap((id) => {
|
|
@@ -665,7 +700,7 @@ function pluginSkills(harnesses, root) {
|
|
|
665
700
|
return loadedBy.length ? skillDirsIn(join7(dir, "skills")).map((path) => ({ path, origin: id, loadedBy })) : [];
|
|
666
701
|
});
|
|
667
702
|
const syncedDir = join7(plugins, "synced");
|
|
668
|
-
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(
|
|
669
704
|
(bucket) => readdirSync3(join7(syncedDir, bucket), { withFileTypes: true }).filter((d) => d.isDirectory()).flatMap((d) => {
|
|
670
705
|
const root2 = join7(syncedDir, bucket, d.name);
|
|
671
706
|
const name = readJson(join7(root2, ".claude-plugin", "plugin.json"))?.name ?? d.name;
|
|
@@ -673,17 +708,7 @@ function pluginSkills(harnesses, root) {
|
|
|
673
708
|
return skillDirsIn(join7(root2, "skills")).map((path) => ({ path, origin: `${name}@synced`, loadedBy: ["claude-code"] }));
|
|
674
709
|
})
|
|
675
710
|
) : [];
|
|
676
|
-
return [...fromMarketplaces, ...fromSynced].map(({ path, origin, loadedBy }) => (
|
|
677
|
-
name: basename2(path),
|
|
678
|
-
kind: "plugin",
|
|
679
|
-
origin,
|
|
680
|
-
path,
|
|
681
|
-
description: readSkillInfo(path).description,
|
|
682
|
-
movable: false,
|
|
683
|
-
broken: false,
|
|
684
|
-
harnesses: loadedBy,
|
|
685
|
-
links: []
|
|
686
|
-
}));
|
|
711
|
+
return [...fromMarketplaces, ...fromSynced].map(({ path, origin, loadedBy }) => vendorSkill(path, "plugin", origin, loadedBy));
|
|
687
712
|
}
|
|
688
713
|
var byKindAndName = (a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name);
|
|
689
714
|
function machineSkills(enabled = enabledHarnesses()) {
|
|
@@ -692,6 +717,7 @@ function machineSkills(enabled = enabledHarnesses()) {
|
|
|
692
717
|
...claudeAiSkills(enabled),
|
|
693
718
|
...pluginSkills(enabled),
|
|
694
719
|
...cursorBuiltInSkills(enabled),
|
|
720
|
+
...codexBuiltInSkills(enabled),
|
|
695
721
|
...grokBuiltInSkills(enabled),
|
|
696
722
|
...systemSkills(enabled)
|
|
697
723
|
].sort(byKindAndName);
|
|
@@ -705,15 +731,20 @@ function originsFile() {
|
|
|
705
731
|
return join7(skilllibHome(), "origins.json");
|
|
706
732
|
}
|
|
707
733
|
function libraryOrigins() {
|
|
708
|
-
return
|
|
734
|
+
return readState(originsFile()) ?? {};
|
|
709
735
|
}
|
|
710
736
|
function recordOrigin(name, origin) {
|
|
711
737
|
const origins = libraryOrigins();
|
|
712
738
|
if (origins[name] === origin) return;
|
|
713
|
-
|
|
714
|
-
|
|
739
|
+
mkdirSync3(skilllibHome(), { recursive: true });
|
|
740
|
+
writeState(originsFile(), { ...origins, [name]: origin });
|
|
715
741
|
}
|
|
716
742
|
function originFor(dir) {
|
|
743
|
+
const under = (path, folder) => path.startsWith((realpathOrNull(folder) ?? folder) + sep3);
|
|
744
|
+
const real = realpathOrNull(dir) ?? dir;
|
|
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 "";
|
|
717
748
|
const match = machineSkills().find((s) => resolve5(s.path) === resolve5(dir));
|
|
718
749
|
if (match) return match.kind === "global" ? "global folder" : `${match.kind}: ${match.origin}`;
|
|
719
750
|
const project = dir.split(/[\\/]\.(?:claude|agents|grok|cursor|codex)[\\/]skills[\\/]/)[0];
|
|
@@ -733,13 +764,6 @@ function librarySkills() {
|
|
|
733
764
|
function librarySkillDir(name) {
|
|
734
765
|
return join8(libraryDir(), name);
|
|
735
766
|
}
|
|
736
|
-
function realpathOrNull2(path) {
|
|
737
|
-
try {
|
|
738
|
-
return realpathSync4(path);
|
|
739
|
-
} catch {
|
|
740
|
-
return null;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
767
|
function isLink(path) {
|
|
744
768
|
try {
|
|
745
769
|
return lstatSync2(path).isSymbolicLink();
|
|
@@ -747,9 +771,65 @@ function isLink(path) {
|
|
|
747
771
|
return false;
|
|
748
772
|
}
|
|
749
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
|
+
}
|
|
750
830
|
function linkDir(target, link2) {
|
|
751
831
|
if (process.platform === "win32") symlinkSync(target, link2, "junction");
|
|
752
|
-
else symlinkSync(relative2(
|
|
832
|
+
else symlinkSync(relative2(dirname3(link2), target), link2);
|
|
753
833
|
}
|
|
754
834
|
function entryExists(path) {
|
|
755
835
|
return isLink(path) || existsSync7(path);
|
|
@@ -760,11 +840,160 @@ function visibilityOf(root, name, enabled = enabledHarnesses()) {
|
|
|
760
840
|
paths: harness(id).projectDirs.filter((dir) => existsSync7(join8(root, dir, name, "SKILL.md"))).length
|
|
761
841
|
}));
|
|
762
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
|
+
}
|
|
763
969
|
function dependencyDirs(root, name, dep) {
|
|
764
|
-
const wanted = installDirs(enabledHarnesses());
|
|
765
970
|
const untracked = ALL_PROJECT_DIRS.find((d) => d !== GROK_SKILLS_DIR && existsSync7(join8(root, d, name, "SKILL.md")) && !isLink(join8(root, d, name)));
|
|
766
|
-
const primary = dep ? dep.dir ?? PROJECT_SKILLS_DIR : untracked ??
|
|
767
|
-
|
|
971
|
+
const primary = dep ? dep.dir ?? PROJECT_SKILLS_DIR : untracked ?? installDirs(enabledHarnesses())[0];
|
|
972
|
+
const wanted = installDirs(enabledHarnesses(), primary);
|
|
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
|
+
}
|
|
768
997
|
}
|
|
769
998
|
function isGitTracked(root, dir) {
|
|
770
999
|
try {
|
|
@@ -780,16 +1009,18 @@ function projectStatus(root) {
|
|
|
780
1009
|
const managed = Object.entries(skills).map(([name, dep]) => {
|
|
781
1010
|
const location = dep.dir ?? PROJECT_SKILLS_DIR;
|
|
782
1011
|
const path = join8(root, location, name);
|
|
783
|
-
const local = treeHash(path);
|
|
1012
|
+
const local = notWritable(root, location) ? null : treeHash(path);
|
|
784
1013
|
const latest = latestVersion(name);
|
|
785
|
-
const
|
|
786
|
-
const
|
|
787
|
-
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";
|
|
788
1019
|
return {
|
|
789
1020
|
name,
|
|
790
1021
|
managed: true,
|
|
791
1022
|
state,
|
|
792
|
-
version:
|
|
1023
|
+
version: pinned || null,
|
|
793
1024
|
latest: latest?.version ?? null,
|
|
794
1025
|
location,
|
|
795
1026
|
path,
|
|
@@ -800,7 +1031,7 @@ function projectStatus(root) {
|
|
|
800
1031
|
const lock = projectSkillsLock(root);
|
|
801
1032
|
const unmanaged = [];
|
|
802
1033
|
const byName = /* @__PURE__ */ new Map();
|
|
803
|
-
for (const dir of ALL_PROJECT_DIRS) {
|
|
1034
|
+
for (const dir of ALL_PROJECT_DIRS.filter((d) => writableDir(root, d))) {
|
|
804
1035
|
for (const path of skillDirsIn(join8(root, dir))) {
|
|
805
1036
|
const name = basename3(path);
|
|
806
1037
|
if (seen.has(name)) continue;
|
|
@@ -809,7 +1040,7 @@ function projectStatus(root) {
|
|
|
809
1040
|
}
|
|
810
1041
|
for (const [name, paths] of byName) {
|
|
811
1042
|
const real = paths.find((p) => !isLink(p)) ?? paths[0];
|
|
812
|
-
const location = relative2(root, join8(real, "..")).split(
|
|
1043
|
+
const location = relative2(root, join8(real, "..")).split(sep4).join("/");
|
|
813
1044
|
const source = lock[name]?.source;
|
|
814
1045
|
unmanaged.push({
|
|
815
1046
|
name,
|
|
@@ -841,40 +1072,57 @@ var NESTED_READERS = [
|
|
|
841
1072
|
// Cursor reads nested .grok/skills too, but skilllib doesn't count it: that folder stays Grok's.
|
|
842
1073
|
[".grok/skills", ["grok"]]
|
|
843
1074
|
];
|
|
844
|
-
|
|
845
|
-
|
|
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) {
|
|
846
1101
|
const found = [];
|
|
847
1102
|
const walk = (dir, left) => {
|
|
848
1103
|
let subs = [];
|
|
849
1104
|
try {
|
|
850
|
-
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));
|
|
851
1106
|
} catch {
|
|
852
1107
|
return;
|
|
853
1108
|
}
|
|
854
1109
|
for (const sub of subs) {
|
|
855
1110
|
if (existsSync7(join8(sub, ".git"))) continue;
|
|
856
|
-
|
|
857
|
-
const location = relative2(root, join8(sub, skillsDir)).split(sep3).join("/");
|
|
858
|
-
for (const path of skillDirsIn(join8(sub, skillsDir))) {
|
|
859
|
-
if (isLink(path) && (realpathOrNull2(path) ?? "").startsWith((realpathOrNull2(root) ?? root) + sep3)) continue;
|
|
860
|
-
const name = basename3(path);
|
|
861
|
-
found.push({
|
|
862
|
-
name,
|
|
863
|
-
managed: false,
|
|
864
|
-
state: unmanagedState(name, path, isRepoSkillLocation(skillsDir), void 0),
|
|
865
|
-
version: null,
|
|
866
|
-
latest: latestVersion(name)?.version ?? null,
|
|
867
|
-
location,
|
|
868
|
-
path,
|
|
869
|
-
visibility: enabled.map((id) => ({ id, paths: readers.includes(id) ? 1 : 0 }))
|
|
870
|
-
});
|
|
871
|
-
}
|
|
872
|
-
}
|
|
1111
|
+
found.push(sub);
|
|
873
1112
|
if (left > 1) walk(sub, left - 1);
|
|
874
1113
|
}
|
|
875
1114
|
};
|
|
876
1115
|
walk(root, depth);
|
|
877
|
-
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
|
+
);
|
|
878
1126
|
}
|
|
879
1127
|
function sharedByGit(root, location, name) {
|
|
880
1128
|
const copy = `${location}/${name}`;
|
|
@@ -889,13 +1137,24 @@ function sharedByGit(root, location, name) {
|
|
|
889
1137
|
}
|
|
890
1138
|
function linkEverywhere(root, name, location, { allowTracked = false, dryRun = false } = {}) {
|
|
891
1139
|
const enabled = enabledHarnesses();
|
|
892
|
-
const result = { created: [], blocked: [], uncommitted: [] };
|
|
1140
|
+
const result = { created: [], blocked: [], uncommitted: [], inTheWay: [] };
|
|
893
1141
|
let shared;
|
|
1142
|
+
const stuck = (path) => void (result.inTheWay.includes(path) || result.inTheWay.push(path));
|
|
894
1143
|
for (const id of enabled) {
|
|
895
1144
|
if (visibilityOf(root, name, [id])[0].paths > 0) continue;
|
|
896
1145
|
const dir = harness(id).projectDirs[0];
|
|
897
1146
|
const link2 = join8(root, dir, name);
|
|
898
|
-
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
|
+
}
|
|
899
1158
|
if (isGitTracked(root, dir)) {
|
|
900
1159
|
if (!(shared ??= sharedByGit(root, location, name))) {
|
|
901
1160
|
if (!result.uncommitted.includes(dir)) result.uncommitted.push(dir);
|
|
@@ -907,22 +1166,18 @@ function linkEverywhere(root, name, location, { allowTracked = false, dryRun = f
|
|
|
907
1166
|
}
|
|
908
1167
|
}
|
|
909
1168
|
if (!dryRun) {
|
|
910
|
-
|
|
1169
|
+
if (broken) unlinkSync(link2);
|
|
1170
|
+
mkdirSync4(join8(root, dir), { recursive: true });
|
|
911
1171
|
linkDir(join8(root, location, name), link2);
|
|
912
1172
|
}
|
|
913
1173
|
result.created.push(dir);
|
|
914
1174
|
}
|
|
915
1175
|
return result;
|
|
916
1176
|
}
|
|
917
|
-
function unlinkEverywhere(root, name
|
|
918
|
-
|
|
919
|
-
return ALL_PROJECT_DIRS.filter((dir) => {
|
|
1177
|
+
function unlinkEverywhere(root, name) {
|
|
1178
|
+
return ALL_PROJECT_DIRS.filter((dir) => writableDir(root, dir)).filter((dir) => {
|
|
920
1179
|
const link2 = join8(root, dir, name);
|
|
921
|
-
if (!isLink(link2)) return false;
|
|
922
|
-
if (dir === GROK_SKILLS_DIR) {
|
|
923
|
-
const target = realpathOrNull2(link2);
|
|
924
|
-
if (target !== null && target !== gone) return false;
|
|
925
|
-
}
|
|
1180
|
+
if (!isLink(link2) || realpathOrNull(link2) !== null) return false;
|
|
926
1181
|
unlinkSync(link2);
|
|
927
1182
|
return true;
|
|
928
1183
|
});
|
|
@@ -930,7 +1185,7 @@ function unlinkEverywhere(root, name, removing) {
|
|
|
930
1185
|
function relinkDependency(root, name, { allowTracked = false } = {}) {
|
|
931
1186
|
const manifest = readManifest(root);
|
|
932
1187
|
const dep = manifest.skills[name];
|
|
933
|
-
if (!dep) return { created: [], blocked: [], uncommitted: [] };
|
|
1188
|
+
if (!dep) return { created: [], blocked: [], uncommitted: [], inTheWay: [] };
|
|
934
1189
|
const result = linkEverywhere(root, name, dep.dir ?? PROJECT_SKILLS_DIR, { allowTracked });
|
|
935
1190
|
if (result.created.length) {
|
|
936
1191
|
manifest.skills[name] = { ...dep, links: [.../* @__PURE__ */ new Set([...dep.links ?? [], ...result.created])] };
|
|
@@ -938,53 +1193,80 @@ function relinkDependency(root, name, { allowTracked = false } = {}) {
|
|
|
938
1193
|
}
|
|
939
1194
|
return result;
|
|
940
1195
|
}
|
|
941
|
-
function linkAll(root, { allowTracked = false } = {}) {
|
|
1196
|
+
function linkAll(root, { allowTracked = false, names } = {}) {
|
|
942
1197
|
const linked = [];
|
|
943
1198
|
const blocked = /* @__PURE__ */ new Set();
|
|
944
1199
|
const uncommitted = [];
|
|
1200
|
+
const inTheWay = /* @__PURE__ */ new Set();
|
|
945
1201
|
for (const skill of projectStatus(root)) {
|
|
946
|
-
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;
|
|
947
1203
|
const res = skill.managed ? relinkDependency(root, skill.name, { allowTracked }) : linkEverywhere(root, skill.name, skill.location, { allowTracked });
|
|
948
1204
|
if (res.created.length) linked.push({ name: skill.name, into: res.created });
|
|
949
1205
|
for (const dir of res.blocked) blocked.add(dir);
|
|
950
1206
|
if (res.uncommitted.length) uncommitted.push(skill.name);
|
|
1207
|
+
for (const path of res.inTheWay) inTheWay.add(path);
|
|
951
1208
|
}
|
|
952
|
-
return { linked, blocked: [...blocked], uncommitted };
|
|
1209
|
+
return { linked, blocked: [...blocked], uncommitted, inTheWay: [...inTheWay] };
|
|
953
1210
|
}
|
|
954
|
-
function
|
|
955
|
-
const target = version === void 0 ? latestVersion(name) : getVersion(name, version);
|
|
956
|
-
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) {
|
|
957
1212
|
const manifest = readManifest(root);
|
|
958
1213
|
const recorded = manifest.skills[name];
|
|
959
|
-
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` };
|
|
960
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` };
|
|
961
1219
|
const local = isLink(to) && !recorded ? null : treeHash(to);
|
|
962
1220
|
const knownContent = local !== null && (local === recorded?.hash || versionForHash(name, local) !== null);
|
|
963
|
-
if (
|
|
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) {
|
|
964
1227
|
return {
|
|
965
|
-
name,
|
|
966
|
-
action: "skipped",
|
|
967
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)`
|
|
968
1229
|
};
|
|
969
1230
|
}
|
|
1231
|
+
return { manifest, recorded, primary, wantedLinks, to, local };
|
|
1232
|
+
}
|
|
1233
|
+
function addRefusal(root, name, hash) {
|
|
1234
|
+
const plan = addPlan(root, name, hash, false);
|
|
1235
|
+
return "reason" in plan ? plan.reason : null;
|
|
1236
|
+
}
|
|
1237
|
+
function addSkill(root, name, { force = false, version, allowTracked = false } = {}) {
|
|
1238
|
+
if (!name || name === "." || name === ".." || /[\\/]/.test(name)) return { name, action: "skipped", reason: "not a skill name" };
|
|
1239
|
+
const target = version === void 0 ? latestVersion(name) : getVersion(name, version);
|
|
1240
|
+
if (!target) return { name, action: "skipped", reason: version === void 0 ? "not in the library" : `version ${version} doesn't exist` };
|
|
1241
|
+
const plan = addPlan(root, name, target.hash, force);
|
|
1242
|
+
if ("reason" in plan) return { name, action: "skipped", reason: plan.reason };
|
|
1243
|
+
const { manifest, recorded, primary, wantedLinks, to, local } = plan;
|
|
970
1244
|
let backedUp;
|
|
971
|
-
|
|
972
|
-
|
|
1245
|
+
const copied = local !== target.hash || isLink(to);
|
|
1246
|
+
if (copied) {
|
|
1247
|
+
mkdirSync4(join8(root, primary), { recursive: true });
|
|
973
1248
|
if (isLink(to)) unlinkSync(to);
|
|
974
|
-
else if (local !== null &&
|
|
1249
|
+
else if (local !== null && versionForHash(name, local) === null) backedUp = stash(to, "edit-backup");
|
|
975
1250
|
copySkill(versionDir(name, target.version), to);
|
|
976
1251
|
}
|
|
977
1252
|
const links = [];
|
|
978
1253
|
const blocked = [];
|
|
979
1254
|
const uncommitted = [];
|
|
1255
|
+
const inTheWay = [];
|
|
980
1256
|
let shared;
|
|
981
|
-
|
|
1257
|
+
let relinked = false;
|
|
1258
|
+
for (const dir of wantedLinks) {
|
|
982
1259
|
const link2 = join8(root, dir, name);
|
|
983
|
-
|
|
1260
|
+
const slot = slotAt(link2, to);
|
|
1261
|
+
if (slot === "ok") {
|
|
984
1262
|
links.push(dir);
|
|
985
1263
|
continue;
|
|
986
1264
|
}
|
|
987
|
-
if (
|
|
1265
|
+
if (slot === "taken") {
|
|
1266
|
+
inTheWay.push(`${dir}/${name}`);
|
|
1267
|
+
if (recorded?.links?.includes(dir)) links.push(dir);
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
988
1270
|
if (!recorded?.links?.includes(dir) && isGitTracked(root, dir)) {
|
|
989
1271
|
if (!(shared ??= sharedByGit(root, primary, name))) {
|
|
990
1272
|
uncommitted.push(dir);
|
|
@@ -995,9 +1277,11 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
995
1277
|
continue;
|
|
996
1278
|
}
|
|
997
1279
|
}
|
|
998
|
-
|
|
1280
|
+
if (isLink(link2)) unlinkSync(link2);
|
|
1281
|
+
mkdirSync4(join8(root, dir), { recursive: true });
|
|
999
1282
|
linkDir(to, link2);
|
|
1000
1283
|
links.push(dir);
|
|
1284
|
+
relinked = true;
|
|
1001
1285
|
}
|
|
1002
1286
|
manifest.skills[name] = {
|
|
1003
1287
|
version: target.version,
|
|
@@ -1006,13 +1290,21 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
1006
1290
|
...links.length ? { links } : {}
|
|
1007
1291
|
};
|
|
1008
1292
|
writeManifest(root, manifest);
|
|
1293
|
+
const from = recorded ? versionForHash(name, recorded.hash)?.version ?? recorded.version : void 0;
|
|
1009
1294
|
return {
|
|
1010
1295
|
name,
|
|
1011
|
-
action:
|
|
1012
|
-
|
|
1296
|
+
action: (
|
|
1297
|
+
// Nothing of it was there (or only an identical untracked copy, now tracked): installed, not updated.
|
|
1298
|
+
!recorded || local === null ? "installed" : backedUp && from === target.version ? "reset" : recorded && !copied && from === target.version ? (
|
|
1299
|
+
// The same version, already there: only links (if any) were written.
|
|
1300
|
+
relinked ? "relinked" : "unchanged"
|
|
1301
|
+
) : "updated"
|
|
1302
|
+
),
|
|
1303
|
+
...from !== void 0 ? { from } : {},
|
|
1013
1304
|
to: target.version,
|
|
1014
1305
|
...blocked.length ? { blocked } : {},
|
|
1015
1306
|
...uncommitted.length ? { uncommitted } : {},
|
|
1307
|
+
...inTheWay.length ? { inTheWay } : {},
|
|
1016
1308
|
...backedUp ? { backedUp } : {}
|
|
1017
1309
|
};
|
|
1018
1310
|
}
|
|
@@ -1020,54 +1312,113 @@ function removeSkill(root, name, { force = false } = {}) {
|
|
|
1020
1312
|
const manifest = readManifest(root);
|
|
1021
1313
|
const recorded = manifest.skills[name];
|
|
1022
1314
|
if (!recorded) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
|
|
1023
|
-
const
|
|
1024
|
-
const
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1315
|
+
const home2 = recorded.dir ?? PROJECT_SKILLS_DIR;
|
|
1316
|
+
const dir = join8(root, home2, name);
|
|
1317
|
+
const takenLinks = [];
|
|
1318
|
+
const unlinkOwn = () => {
|
|
1319
|
+
for (const linkDir2 of (recorded.links ?? []).filter((d) => writableDir(root, d))) {
|
|
1320
|
+
const link2 = join8(root, linkDir2, name);
|
|
1321
|
+
if (isLink(link2) && slotAt(link2, dir) !== "taken") unlinkSync(link2);
|
|
1322
|
+
else if (entryExists(link2) && slotAt(link2, dir) === "taken") takenLinks.push(`${linkDir2}/${name} is something else (another copy, or a file): left as it is`);
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
const leftLinks = (recorded.links ?? []).flatMap(
|
|
1326
|
+
(d) => ((b) => b && entryExists(join8(root, d, name)) ? [`${blockerSaid(root, b)}: its link there was left alone`] : [])(notWritable(root, d))
|
|
1327
|
+
);
|
|
1328
|
+
const ownStuck = notWritable(root, home2);
|
|
1329
|
+
if (ownStuck || fileAt(dir)) {
|
|
1330
|
+
delete manifest.skills[name];
|
|
1331
|
+
writeManifest(root, manifest);
|
|
1332
|
+
const why = ownStuck ? blockerSaid(root, ownStuck) : `${home2}/${name} is a file, not a skill folder`;
|
|
1333
|
+
const keptLinks = (recorded.links ?? []).flatMap((d) => {
|
|
1334
|
+
const link2 = join8(root, d, name);
|
|
1335
|
+
if (!writableDir(root, d) || !entryExists(link2)) return [];
|
|
1336
|
+
return [slotAt(link2, dir) === "taken" ? `${d}/${name} is something else (another copy, or a file): left as it is` : `its link ${d}/${name} was left too`];
|
|
1337
|
+
});
|
|
1338
|
+
return { name, action: "removed", reason: [`${why}: only its pin was removed, and that copy was left as it is`, ...keptLinks, ...leftLinks].join("; ") };
|
|
1030
1339
|
}
|
|
1031
|
-
const
|
|
1032
|
-
|
|
1340
|
+
const local = treeHash(dir);
|
|
1341
|
+
const inLibrary = local !== null && versionForHash(name, local) !== null;
|
|
1342
|
+
const edited = local !== null && local !== recorded.hash && !inLibrary;
|
|
1343
|
+
if (edited && !force)
|
|
1344
|
+
return {
|
|
1345
|
+
name,
|
|
1346
|
+
action: "skipped",
|
|
1347
|
+
// A pin from another library isn't an edit of yours: say what it is. Either way --force backs the copy up first.
|
|
1348
|
+
reason: versionForHash(name, recorded.hash) === null ? "doesn't match what skilllib.json pins, and your library has neither (use --force to remove it; it's backed up first)" : "has local edits (use --force to delete anyway; they're backed up first)"
|
|
1349
|
+
};
|
|
1350
|
+
unlinkOwn();
|
|
1351
|
+
const backedUp = local !== null && !inLibrary ? stash(dir, "edit-backup") : void 0;
|
|
1352
|
+
if (!backedUp) rmSync3(dir, { recursive: true, force: true });
|
|
1033
1353
|
delete manifest.skills[name];
|
|
1034
1354
|
writeManifest(root, manifest);
|
|
1035
|
-
|
|
1355
|
+
const left = [...leftLinks, ...takenLinks];
|
|
1356
|
+
return { name, action: "removed", ...left.length ? { reason: left.join("; ") } : {}, ...backedUp ? { backedUp } : {} };
|
|
1036
1357
|
}
|
|
1037
|
-
function syncProject(root, { force = false } = {}) {
|
|
1038
|
-
return Object.keys(readManifest(root).skills).flatMap((name) => syncSkill(root, name, { force }));
|
|
1358
|
+
function syncProject(root, { force = false, names } = {}) {
|
|
1359
|
+
return Object.keys(readManifest(root).skills).filter((name) => !names || names.includes(name)).flatMap((name) => syncSkill(root, name, { force }));
|
|
1039
1360
|
}
|
|
1040
1361
|
function syncSkill(root, name, { force = false } = {}) {
|
|
1041
1362
|
const dep = readManifest(root).skills[name];
|
|
1042
1363
|
if (!dep) return [];
|
|
1043
|
-
const
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
if (
|
|
1047
|
-
|
|
1048
|
-
if (
|
|
1049
|
-
|
|
1364
|
+
const copy = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
|
|
1365
|
+
const local = treeHash(copy);
|
|
1366
|
+
const blocker = syncBlocker(root, name, dep);
|
|
1367
|
+
if (blocker) return [{ name, action: "skipped", reason: `${blocker.said}: ${blocker.outside ? "skilllib only installs inside the repo" : "move it, then sync"}` }];
|
|
1368
|
+
if (syncDone(root, name, dep)) return [];
|
|
1369
|
+
if (local === dep.hash) return [relinkPinned(root, name, dep)];
|
|
1370
|
+
const version = pinnedVersion(name, dep);
|
|
1371
|
+
if (typeof version === "string") return [{ name, action: "skipped", reason: version }];
|
|
1372
|
+
if (local !== null && local !== dep.hash && versionForHash(name, local) === null && !force) return [{ name, action: "skipped", reason: "has local edits (use --force to reset them)" }];
|
|
1373
|
+
const backedUp = local !== null && versionForHash(name, local) === null ? stash(copy, "edit-backup") : void 0;
|
|
1374
|
+
mkdirSync4(dirname3(copy), { recursive: true });
|
|
1375
|
+
copySkill(versionDir(name, version), copy);
|
|
1376
|
+
const links = relinkPinned(root, name, dep);
|
|
1377
|
+
const from = (local !== null ? versionForHash(name, local)?.version : void 0) ?? version;
|
|
1378
|
+
return [
|
|
1379
|
+
{
|
|
1380
|
+
name,
|
|
1381
|
+
action: local === null ? "installed" : backedUp ? "reset" : "updated",
|
|
1382
|
+
from,
|
|
1383
|
+
to: version,
|
|
1384
|
+
...backedUp ? { backedUp } : {},
|
|
1385
|
+
...links.action === "skipped" ? { reason: links.reason } : {}
|
|
1386
|
+
}
|
|
1387
|
+
];
|
|
1388
|
+
}
|
|
1389
|
+
function relinkPinned(root, name, dep) {
|
|
1390
|
+
const to = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
|
|
1391
|
+
for (const dir of dep.links ?? []) {
|
|
1392
|
+
const link2 = join8(root, dir, name);
|
|
1393
|
+
if (realpathOrNull(link2) === realpathOrNull(to)) continue;
|
|
1394
|
+
if (isLink(link2)) unlinkSync(link2);
|
|
1395
|
+
else if (entryExists(link2)) return { name, action: "skipped", reason: `${dir}/${name} is in the way of its link: move it, then sync` };
|
|
1396
|
+
mkdirSync4(join8(root, dir), { recursive: true });
|
|
1397
|
+
linkDir(to, link2);
|
|
1398
|
+
}
|
|
1399
|
+
return { name, action: "relinked", to: versionForHash(name, dep.hash)?.version ?? dep.version };
|
|
1050
1400
|
}
|
|
1051
1401
|
function pinnedVersion(name, dep) {
|
|
1052
|
-
|
|
1402
|
+
if (!latestVersion(name)) return `not in your library (${tildify(libraryDir())}); import it, or set SKILLLIB_HOME to the library that has it`;
|
|
1403
|
+
return versionForHash(name, dep.hash)?.version ?? `your library has no copy of the version skilllib.json pins (pinned from another library?); skilllib update ${name} installs your newest`;
|
|
1053
1404
|
}
|
|
1054
1405
|
function discardEdits(root, name) {
|
|
1055
1406
|
const dep = readManifest(root).skills[name];
|
|
1056
1407
|
if (!dep) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
|
|
1057
|
-
const
|
|
1058
|
-
if (
|
|
1059
|
-
return addSkill(root, name, { force: true, version
|
|
1408
|
+
const version = pinnedVersion(name, dep);
|
|
1409
|
+
if (typeof version === "string") return { name, action: "skipped", reason: version };
|
|
1410
|
+
return addSkill(root, name, { force: true, version });
|
|
1060
1411
|
}
|
|
1061
|
-
function updateProject(root, names, { force = false } = {}) {
|
|
1412
|
+
function updateProject(root, names, { force = false, allowTracked = false } = {}) {
|
|
1062
1413
|
const { skills } = readManifest(root);
|
|
1063
1414
|
return Object.keys(skills).filter((name) => !names || names.includes(name)).flatMap((name) => {
|
|
1064
1415
|
const latest = latestVersion(name);
|
|
1065
|
-
if (!latest) return [{ name, action: "skipped", reason: "
|
|
1416
|
+
if (!latest) return [{ name, action: "skipped", reason: "not in your library" }];
|
|
1066
1417
|
const dep = skills[name];
|
|
1067
1418
|
const local = treeHash(join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name));
|
|
1068
1419
|
if (dep.hash === latest.hash && local === latest.hash) return [];
|
|
1069
1420
|
if (dep.hash === latest.hash && local !== null && !force) return [];
|
|
1070
|
-
const change = addSkill(root, name, { force });
|
|
1421
|
+
const change = addSkill(root, name, { force, allowTracked });
|
|
1071
1422
|
return change.action === "skipped" || change.from !== change.to || local !== latest.hash ? [change] : [];
|
|
1072
1423
|
});
|
|
1073
1424
|
}
|
|
@@ -1078,8 +1429,12 @@ function importSkill(dir, { force = false } = {}) {
|
|
|
1078
1429
|
const incoming = treeHash(dir);
|
|
1079
1430
|
if (existing === incoming) return { name, status: "unchanged" };
|
|
1080
1431
|
if (existing !== null && !force) return { name, status: "exists" };
|
|
1081
|
-
|
|
1432
|
+
mkdirSync4(libraryDir(), { recursive: true });
|
|
1082
1433
|
const origin = existing !== null && libraryOrigins()[name] ? null : originFor(dir);
|
|
1434
|
+
if (existing !== null) {
|
|
1435
|
+
forgetLatest(name);
|
|
1436
|
+
latestVersion(name);
|
|
1437
|
+
}
|
|
1083
1438
|
copySkill(dir, to);
|
|
1084
1439
|
if (origin) recordOrigin(name, origin);
|
|
1085
1440
|
forgetLatest(name);
|
|
@@ -1090,19 +1445,15 @@ function backupIndexFile(bucket) {
|
|
|
1090
1445
|
return join8(skilllibHome(), bucket, "index.json");
|
|
1091
1446
|
}
|
|
1092
1447
|
function readBackupIndex(bucket) {
|
|
1093
|
-
|
|
1094
|
-
return JSON.parse(readFileSync6(backupIndexFile(bucket), "utf-8"));
|
|
1095
|
-
} catch {
|
|
1096
|
-
return {};
|
|
1097
|
-
}
|
|
1448
|
+
return readState(backupIndexFile(bucket)) ?? {};
|
|
1098
1449
|
}
|
|
1099
1450
|
function move(from, to) {
|
|
1100
1451
|
try {
|
|
1101
|
-
|
|
1452
|
+
renameSync2(from, to);
|
|
1102
1453
|
} catch (err) {
|
|
1103
1454
|
if (err.code !== "EXDEV") throw err;
|
|
1104
1455
|
cpSync2(from, to, { recursive: true, verbatimSymlinks: true });
|
|
1105
|
-
|
|
1456
|
+
rmSync3(from, { recursive: true, force: true });
|
|
1106
1457
|
}
|
|
1107
1458
|
}
|
|
1108
1459
|
function stash(dir, bucket) {
|
|
@@ -1111,17 +1462,21 @@ function stash(dir, bucket) {
|
|
|
1111
1462
|
while (entryExists(join8(skilllibHome(), bucket, entryAt(at)))) at++;
|
|
1112
1463
|
const entry = entryAt(at);
|
|
1113
1464
|
const target = join8(skilllibHome(), bucket, entry);
|
|
1114
|
-
|
|
1465
|
+
const index = readBackupIndex(bucket);
|
|
1466
|
+
mkdirSync4(join8(skilllibHome(), bucket), { recursive: true });
|
|
1115
1467
|
move(dir, target);
|
|
1116
|
-
|
|
1468
|
+
writeState(backupIndexFile(bucket), { ...index, [entry]: dir });
|
|
1117
1469
|
return target;
|
|
1118
1470
|
}
|
|
1119
1471
|
function removeUntracked(root, name, path) {
|
|
1120
|
-
|
|
1121
|
-
|
|
1472
|
+
const stuck = notWritable(root, relative2(root, dirname3(path)));
|
|
1473
|
+
if (stuck) throw new Error(`${stuck} isn't a folder in the repo: skilllib leaves what's in it alone`);
|
|
1474
|
+
const to = stash(path, "trash");
|
|
1475
|
+
unlinkEverywhere(root, name);
|
|
1476
|
+
return to;
|
|
1122
1477
|
}
|
|
1123
1478
|
function projectsUsing(name) {
|
|
1124
|
-
return knownProjects().filter((p) =>
|
|
1479
|
+
return knownProjects().filter((p) => pinsSkill(p, name));
|
|
1125
1480
|
}
|
|
1126
1481
|
function deleteLibrarySkill(name) {
|
|
1127
1482
|
const using = projectsUsing(name);
|
|
@@ -1150,13 +1505,13 @@ function linkGlobal(path, agents) {
|
|
|
1150
1505
|
const skipped = [];
|
|
1151
1506
|
for (const dir of new Set(agents.map((id) => harness(id).globalDirs()[0]))) {
|
|
1152
1507
|
const link2 = join8(dir, name);
|
|
1153
|
-
if (isLink(link2) &&
|
|
1508
|
+
if (isLink(link2) && realpathOrNull(link2) === null) unlinkSync(link2);
|
|
1154
1509
|
else if (entryExists(link2)) {
|
|
1155
|
-
if (
|
|
1510
|
+
if (realpathOrNull(link2) !== realpathOrNull(path)) skipped.push(link2);
|
|
1156
1511
|
continue;
|
|
1157
1512
|
}
|
|
1158
|
-
|
|
1159
|
-
linkDir(path,
|
|
1513
|
+
mkdirSync4(dir, { recursive: true });
|
|
1514
|
+
linkDir(realpathOrNull(path) ?? path, join8(realpathOrNull(dir) ?? dir, name));
|
|
1160
1515
|
linked.push(link2);
|
|
1161
1516
|
}
|
|
1162
1517
|
return { linked, skipped };
|
|
@@ -1165,12 +1520,13 @@ function createSkill(name, description) {
|
|
|
1165
1520
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) return { ok: false, reason: "use lowercase letters, digits and dashes" };
|
|
1166
1521
|
const dir = librarySkillDir(name);
|
|
1167
1522
|
if (existsSync7(dir)) return { ok: false, reason: `${name} already exists` };
|
|
1168
|
-
|
|
1169
|
-
|
|
1523
|
+
mkdirSync4(dir, { recursive: true });
|
|
1524
|
+
writeFileSync2(
|
|
1170
1525
|
join8(dir, "SKILL.md"),
|
|
1526
|
+
// JSON strings are valid YAML: a description with ": " or "#" stays one value.
|
|
1171
1527
|
`---
|
|
1172
1528
|
name: ${name}
|
|
1173
|
-
description: ${description || "Describe when an agent should use this skill."}
|
|
1529
|
+
description: ${JSON.stringify(description || "Describe when an agent should use this skill.")}
|
|
1174
1530
|
---
|
|
1175
1531
|
|
|
1176
1532
|
# ${name}
|
|
@@ -1190,26 +1546,28 @@ function listBackups() {
|
|
|
1190
1546
|
const match = entry.match(/^(.*)-(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-\d{3}Z$/);
|
|
1191
1547
|
if (!match) return [];
|
|
1192
1548
|
const name = match[1];
|
|
1193
|
-
const from = index[entry] ?? (kind === "trash" ? librarySkillDir(name) : join8(claudeDir(), "skills", name));
|
|
1194
|
-
return [{ name, kind, path: join8(dir, entry), movedAt: `${match[2]}
|
|
1549
|
+
const from = index[entry] ?? (kind === "trash" ? librarySkillDir(name) : kind === "global-backup" ? join8(claudeDir(), "skills", name) : "");
|
|
1550
|
+
return [{ name, kind, path: join8(dir, entry), movedAt: localStamp(Date.parse(`${match[2]}T${match[3]}:${match[4]}:${match[5]}Z`)), from }];
|
|
1195
1551
|
});
|
|
1196
1552
|
}).sort((a, b) => b.path.slice(-24).localeCompare(a.path.slice(-24)));
|
|
1197
1553
|
}
|
|
1554
|
+
function movedTogether(backups, backup) {
|
|
1555
|
+
if (backup.kind === "plugin") return [backup];
|
|
1556
|
+
const at = (b) => Date.parse(b.path.slice(-24).replace(/^(.{13})-(\d\d)-(\d\d)-(\d{3})Z$/, "$1:$2:$3.$4Z"));
|
|
1557
|
+
return backups.filter((b) => b.name === backup.name && b.kind === backup.kind && Math.abs(at(b) - at(backup)) <= 2e3);
|
|
1558
|
+
}
|
|
1198
1559
|
function backupFrom(backup) {
|
|
1199
1560
|
if (backup.kind === "plugin") return "Claude Code plugin";
|
|
1200
|
-
|
|
1561
|
+
if (!backup.from) return "unknown";
|
|
1562
|
+
return backup.from === librarySkillDir(backup.name) ? "library (deleted)" : tildify(dirname3(backup.from));
|
|
1201
1563
|
}
|
|
1202
1564
|
function restoreBackup(backup) {
|
|
1203
1565
|
const to = backup.from;
|
|
1566
|
+
if (!to) return { ok: false, reason: `skilllib doesn't know where it came from; its files are in ${tildify(backup.path)}` };
|
|
1204
1567
|
if (backup.kind === "tidy-backup" && isLink(to)) unlinkSync(to);
|
|
1205
|
-
if (backup.kind === "edit-backup" && !isLink(to) && versionForHash(backup.name, treeHash(to) ?? "") !== null)
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
taken = taken || lstatSync2(to) !== null;
|
|
1209
|
-
} catch {
|
|
1210
|
-
}
|
|
1211
|
-
if (taken) return { ok: false, reason: `${backup.name} already exists there` };
|
|
1212
|
-
mkdirSync5(join8(to, ".."), { recursive: true });
|
|
1568
|
+
if (backup.kind === "edit-backup" && !isLink(to) && versionForHash(backup.name, treeHash(to) ?? "") !== null) rmSync3(to, { recursive: true, force: true });
|
|
1569
|
+
if (entryExists(to)) return { ok: false, reason: `${backup.name} already exists there` };
|
|
1570
|
+
mkdirSync4(join8(to, ".."), { recursive: true });
|
|
1213
1571
|
move(backup.path, to);
|
|
1214
1572
|
forgetLatest(backup.name);
|
|
1215
1573
|
return { ok: true, to };
|
|
@@ -1218,56 +1576,51 @@ function restoreBackup(backup) {
|
|
|
1218
1576
|
// src/agentSkill.ts
|
|
1219
1577
|
var AGENT_SKILL_NAME = "skilllib";
|
|
1220
1578
|
function shippedSkill() {
|
|
1221
|
-
return
|
|
1579
|
+
return readFileSync5(new URL("../skills/skilllib/SKILL.md", import.meta.url), "utf-8");
|
|
1222
1580
|
}
|
|
1223
1581
|
function agentSkillDirs(enabled = enabledHarnesses()) {
|
|
1224
1582
|
const claudeSkills = join9(claudeDir(), "skills");
|
|
1225
1583
|
const agentsSkills = join9(userHome(), ".agents", "skills");
|
|
1226
1584
|
const needsClaude = enabled.some((id) => harness(id).globalDirs().includes(claudeSkills));
|
|
1227
1585
|
const needsAgents = enabled.some((id) => !harness(id).globalDirs().includes(claudeSkills));
|
|
1228
|
-
|
|
1586
|
+
const dirs = [...needsClaude ? [claudeSkills] : [], ...needsAgents ? [agentsSkills] : []];
|
|
1587
|
+
return dirs.filter((d, i) => dirs.findIndex((o) => realPath(o) === realPath(d)) === i).map((d) => join9(d, AGENT_SKILL_NAME));
|
|
1229
1588
|
}
|
|
1230
1589
|
function isOurs(dir) {
|
|
1231
1590
|
return existsSync8(join9(dir, OWN_SKILL_MARKER));
|
|
1232
1591
|
}
|
|
1233
1592
|
function isDangling(path) {
|
|
1234
|
-
return
|
|
1235
|
-
}
|
|
1236
|
-
function entryExists2(path) {
|
|
1237
|
-
try {
|
|
1238
|
-
lstatSync3(path);
|
|
1239
|
-
return true;
|
|
1240
|
-
} catch {
|
|
1241
|
-
return false;
|
|
1242
|
-
}
|
|
1593
|
+
return entryExists(path) && lstatSync3(path).isSymbolicLink() && !existsSync8(path);
|
|
1243
1594
|
}
|
|
1244
1595
|
function agentSkillState(enabled = enabledHarnesses()) {
|
|
1245
1596
|
const [primary, ...links] = agentSkillDirs(enabled);
|
|
1246
1597
|
if (!primary || !isOurs(primary) || links.some((l) => !existsSync8(join9(l, "SKILL.md")))) return "missing";
|
|
1247
|
-
return
|
|
1598
|
+
return [primary, ...links].every((d) => readFileSync5(join9(d, "SKILL.md"), "utf-8") === shippedSkill()) ? "installed" : "outdated";
|
|
1248
1599
|
}
|
|
1249
1600
|
function installAgentSkill(enabled = enabledHarnesses()) {
|
|
1250
1601
|
const [primary, ...links] = agentSkillDirs(enabled);
|
|
1251
1602
|
if (!primary) return { ok: false, reason: "no agents chosen yet (skilllib harnesses <id>...)" };
|
|
1252
|
-
const foreign = [primary, ...links].find((d) =>
|
|
1603
|
+
const foreign = [primary, ...links].find((d) => entryExists(d) && !isOurs(d) && !isDangling(d));
|
|
1253
1604
|
if (foreign) return { ok: false, reason: `${tildify(foreign)} already exists and isn't skilllib's` };
|
|
1254
1605
|
for (const d of [primary, ...links]) if (isDangling(d)) unlinkSync2(d);
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1606
|
+
mkdirSync5(primary, { recursive: true });
|
|
1607
|
+
writeFileSync3(join9(primary, "SKILL.md"), shippedSkill());
|
|
1608
|
+
writeFileSync3(join9(primary, OWN_SKILL_MARKER), "Installed by skilllib. `skilllib agent-skill remove` removes it.\n");
|
|
1258
1609
|
for (const link2 of links) {
|
|
1259
|
-
if (
|
|
1260
|
-
|
|
1610
|
+
if (entryExists(link2) && !isLink(link2) && isOurs(link2)) rmSync4(link2, { recursive: true, force: true });
|
|
1611
|
+
if (entryExists(link2)) continue;
|
|
1612
|
+
mkdirSync5(join9(link2, ".."), { recursive: true });
|
|
1261
1613
|
linkDir(primary, link2);
|
|
1262
1614
|
}
|
|
1263
1615
|
return { ok: true, dirs: [primary, ...links] };
|
|
1264
1616
|
}
|
|
1265
1617
|
function removeAgentSkill() {
|
|
1266
1618
|
const all = agentSkillDirs(["claude-code", "codex"]);
|
|
1267
|
-
const removed = all.filter((d) =>
|
|
1619
|
+
const removed = all.filter((d) => entryExists(d) && (isOurs(d) || isDangling(d)));
|
|
1268
1620
|
for (const d of removed) {
|
|
1621
|
+
if (!entryExists(d)) continue;
|
|
1269
1622
|
if (lstatSync3(d).isSymbolicLink()) unlinkSync2(d);
|
|
1270
|
-
else
|
|
1623
|
+
else rmSync4(d, { recursive: true, force: true });
|
|
1271
1624
|
}
|
|
1272
1625
|
return removed;
|
|
1273
1626
|
}
|
|
@@ -1279,14 +1632,14 @@ function refreshAgentSkill() {
|
|
|
1279
1632
|
}
|
|
1280
1633
|
|
|
1281
1634
|
// src/commands.ts
|
|
1282
|
-
import { existsSync as
|
|
1635
|
+
import { existsSync as existsSync14, readFileSync as readFileSync7 } from "fs";
|
|
1283
1636
|
import { parseArgs as parseFlags } from "util";
|
|
1284
|
-
import { basename as basename9, join as
|
|
1637
|
+
import { basename as basename9, join as join15, resolve as resolve8, sep as sep8 } from "path";
|
|
1285
1638
|
|
|
1286
1639
|
// src/git.ts
|
|
1287
1640
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
1288
|
-
import { existsSync as existsSync9 } from "fs";
|
|
1289
|
-
import { join as join10, relative as relative3 } from "path";
|
|
1641
|
+
import { existsSync as existsSync9, realpathSync as realpathSync4 } from "fs";
|
|
1642
|
+
import { isAbsolute as isAbsolute2, join as join10, relative as relative3, sep as sep5 } from "path";
|
|
1290
1643
|
function git(root, args, input) {
|
|
1291
1644
|
try {
|
|
1292
1645
|
return execFileSync2("git", args, { cwd: root, encoding: "utf-8", input, stdio: ["pipe", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 });
|
|
@@ -1296,11 +1649,32 @@ function git(root, args, input) {
|
|
|
1296
1649
|
}
|
|
1297
1650
|
}
|
|
1298
1651
|
function gitInfo(root, extraDirs = []) {
|
|
1299
|
-
|
|
1300
|
-
|
|
1652
|
+
let inRepo;
|
|
1653
|
+
try {
|
|
1654
|
+
inRepo = execFileSync2("git", ["rev-parse", "--show-prefix"], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1655
|
+
} catch {
|
|
1656
|
+
if (!existsSync9(join10(root, ".git"))) return null;
|
|
1657
|
+
inRepo = "";
|
|
1658
|
+
}
|
|
1659
|
+
const real = (p) => {
|
|
1660
|
+
try {
|
|
1661
|
+
return realpathSync4.native(p);
|
|
1662
|
+
} catch {
|
|
1663
|
+
return p;
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
const realRoot = real(root);
|
|
1667
|
+
const places = new Map(
|
|
1668
|
+
[.../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS, MANIFEST_FILE, ...extraDirs])].filter((d) => existsSync9(join10(root, d))).map((d) => [d, relative3(realRoot, real(join10(root, d))).split(sep5).join("/")]).filter(([, place]) => place !== "" && place !== ".." && !place.startsWith("../") && !isAbsolute2(place))
|
|
1669
|
+
);
|
|
1670
|
+
const dirs = [...new Set(places.values())];
|
|
1301
1671
|
if (dirs.length === 0) return { of: () => "new" };
|
|
1672
|
+
const placeOf = (rel) => {
|
|
1673
|
+
const dir = [...places.keys()].find((d) => rel === d || rel.startsWith(d + "/"));
|
|
1674
|
+
return dir === void 0 ? rel : places.get(dir) + rel.slice(dir.length);
|
|
1675
|
+
};
|
|
1302
1676
|
const tracked = (git(root, ["ls-files", "-z", "--", ...dirs]) ?? "").split("\0").filter(Boolean);
|
|
1303
|
-
const dirty = (git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...dirs]) ?? "").split("\0").filter(Boolean).map((entry) => entry.slice(3));
|
|
1677
|
+
const dirty = (git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...dirs]) ?? "").split("\0").filter(Boolean).map((entry) => entry.slice(3)).filter((path) => path.startsWith(inRepo)).map((path) => path.slice(inRepo.length));
|
|
1304
1678
|
const ignoredCache = /* @__PURE__ */ new Map();
|
|
1305
1679
|
const isIgnored = (rel) => {
|
|
1306
1680
|
if (!ignoredCache.has(rel)) {
|
|
@@ -1312,7 +1686,7 @@ function gitInfo(root, extraDirs = []) {
|
|
|
1312
1686
|
const under = (list2, rel) => list2.some((p) => p === rel || p.startsWith(rel + "/"));
|
|
1313
1687
|
return {
|
|
1314
1688
|
of: (relPath) => {
|
|
1315
|
-
const rel = relPath.replace(/\/+$/, "");
|
|
1689
|
+
const rel = placeOf(relPath.replace(/\/+$/, ""));
|
|
1316
1690
|
if (under(tracked, rel)) return under(dirty, rel) ? "changed" : "committed";
|
|
1317
1691
|
return isIgnored(rel) ? "ignored" : "new";
|
|
1318
1692
|
}
|
|
@@ -1323,24 +1697,17 @@ function relativeTo(root, path) {
|
|
|
1323
1697
|
}
|
|
1324
1698
|
|
|
1325
1699
|
// src/here.ts
|
|
1326
|
-
import { readdirSync as
|
|
1700
|
+
import { readdirSync as readdirSync8 } from "fs";
|
|
1327
1701
|
import { basename as basename7 } from "path";
|
|
1328
1702
|
|
|
1329
1703
|
// src/health.ts
|
|
1330
|
-
import { lstatSync as lstatSync4 } from "fs";
|
|
1331
|
-
import { basename as basename6, dirname as
|
|
1704
|
+
import { existsSync as existsSync12, lstatSync as lstatSync4, readdirSync as readdirSync7 } from "fs";
|
|
1705
|
+
import { basename as basename6, dirname as dirname6, isAbsolute as isAbsolute3, join as join13, relative as relative4, sep as sep7 } from "path";
|
|
1332
1706
|
|
|
1333
1707
|
// src/plugins.ts
|
|
1334
1708
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1335
|
-
import { existsSync as existsSync10, mkdirSync as
|
|
1336
|
-
import { basename as basename4, dirname as
|
|
1337
|
-
function readJson2(path) {
|
|
1338
|
-
try {
|
|
1339
|
-
return JSON.parse(readFileSync8(path, "utf-8"));
|
|
1340
|
-
} catch {
|
|
1341
|
-
return null;
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1709
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
1710
|
+
import { basename as basename4, dirname as dirname4, join as join11, resolve as resolve7 } from "path";
|
|
1344
1711
|
function nonEmptyDir(path) {
|
|
1345
1712
|
try {
|
|
1346
1713
|
return readdirSync5(path).some((f) => !f.startsWith("."));
|
|
@@ -1349,7 +1716,7 @@ function nonEmptyDir(path) {
|
|
|
1349
1716
|
}
|
|
1350
1717
|
}
|
|
1351
1718
|
function extrasOf(root) {
|
|
1352
|
-
const manifest =
|
|
1719
|
+
const manifest = readJson(join11(root, ".claude-plugin", "plugin.json")) ?? {};
|
|
1353
1720
|
return [
|
|
1354
1721
|
...existsSync10(join11(root, ".mcp.json")) || manifest.mcpServers ? ["MCP servers"] : [],
|
|
1355
1722
|
...existsSync10(join11(root, "hooks", "hooks.json")) || manifest.hooks ? ["hooks"] : [],
|
|
@@ -1358,21 +1725,22 @@ function extrasOf(root) {
|
|
|
1358
1725
|
];
|
|
1359
1726
|
}
|
|
1360
1727
|
function claudePlugins(machine, root) {
|
|
1361
|
-
const installed =
|
|
1728
|
+
const installed = installedPluginsJson();
|
|
1362
1729
|
const byOrigin = /* @__PURE__ */ new Map();
|
|
1363
1730
|
for (const m of machine) if (m.kind === "plugin") byOrigin.set(m.origin, [...byOrigin.get(m.origin) ?? [], m.path]);
|
|
1364
1731
|
return [...byOrigin].map(([origin, skills]) => {
|
|
1365
1732
|
const synced = origin.endsWith("@synced");
|
|
1366
|
-
const id = origin;
|
|
1367
1733
|
const entries = installed[origin] ?? [];
|
|
1368
1734
|
const entry = entries.find((e) => root && e.projectPath === root) ?? entries.find((e) => (e.scope ?? "user") === "user") ?? entries[0];
|
|
1369
1735
|
return {
|
|
1370
|
-
id,
|
|
1736
|
+
id: origin,
|
|
1371
1737
|
scope: synced ? null : entry?.scope ?? "user",
|
|
1372
1738
|
...entry?.projectPath ? { projectPath: entry.projectPath } : {},
|
|
1373
1739
|
synced,
|
|
1740
|
+
// Turned on in your settings with no install record (as installedPlugins finds it).
|
|
1741
|
+
...!synced && !entries.length ? { settingsOnly: true } : {},
|
|
1374
1742
|
skills,
|
|
1375
|
-
extras: extrasOf(
|
|
1743
|
+
extras: extrasOf(dirname4(dirname4(skills[0])))
|
|
1376
1744
|
};
|
|
1377
1745
|
});
|
|
1378
1746
|
}
|
|
@@ -1385,10 +1753,10 @@ function newer(a, b) {
|
|
|
1385
1753
|
}
|
|
1386
1754
|
function updateOf(name, marketplace, entry, catalogs) {
|
|
1387
1755
|
const dir = join11(claudeDir(), "plugins", "marketplaces", marketplace);
|
|
1388
|
-
if (!catalogs.has(marketplace)) catalogs.set(marketplace,
|
|
1756
|
+
if (!catalogs.has(marketplace)) catalogs.set(marketplace, readJson(join11(dir, ".claude-plugin", "marketplace.json"))?.plugins ?? []);
|
|
1389
1757
|
const listed = catalogs.get(marketplace).find((p) => p.name === name);
|
|
1390
1758
|
if (!listed) return void 0;
|
|
1391
|
-
const version = listed.version ?? (typeof listed.source === "string" ?
|
|
1759
|
+
const version = listed.version ?? (typeof listed.source === "string" ? readJson(join11(dir, listed.source, ".claude-plugin", "plugin.json"))?.version : void 0);
|
|
1392
1760
|
if (version && entry.version) return newer(version, entry.version) ? version : void 0;
|
|
1393
1761
|
const sha = typeof listed.source === "object" ? listed.source?.sha : void 0;
|
|
1394
1762
|
return sha && entry.gitCommitSha && sha !== entry.gitCommitSha ? "newer" : void 0;
|
|
@@ -1398,12 +1766,12 @@ function pluginRoot(id, entry) {
|
|
|
1398
1766
|
return candidates.find((c) => existsSync10(join11(c, "skills"))) ?? candidates.find((c) => existsSync10(c)) ?? candidates[0];
|
|
1399
1767
|
}
|
|
1400
1768
|
function describe(root) {
|
|
1401
|
-
const manifest =
|
|
1769
|
+
const manifest = readJson(join11(root, ".claude-plugin", "plugin.json")) ?? {};
|
|
1402
1770
|
return { description: manifest.description ?? "", version: manifest.version, skills: skillDirsIn(join11(root, "skills")), extras: extrasOf(root) };
|
|
1403
1771
|
}
|
|
1404
1772
|
function installedPlugins() {
|
|
1405
1773
|
const plugins = join11(claudeDir(), "plugins");
|
|
1406
|
-
const installed =
|
|
1774
|
+
const installed = installedPluginsJson();
|
|
1407
1775
|
const catalogs = /* @__PURE__ */ new Map();
|
|
1408
1776
|
const fromMarketplaces = Object.entries(installed).flatMap(
|
|
1409
1777
|
([id, entries]) => entries.map((entry) => {
|
|
@@ -1435,18 +1803,18 @@ function installedPlugins() {
|
|
|
1435
1803
|
const syncedDir = join11(plugins, "synced");
|
|
1436
1804
|
const dirs = (p) => existsSync10(p) ? readdirSync5(p, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => join11(p, d.name)) : [];
|
|
1437
1805
|
const fromSynced = dirs(syncedDir).flatMap(dirs).map((root) => {
|
|
1438
|
-
const id = `${
|
|
1806
|
+
const id = `${readJson(join11(root, ".claude-plugin", "plugin.json"))?.name ?? basename4(root)}@synced`;
|
|
1439
1807
|
return { id, scope: null, synced: true, agent: "claude-code", on: user[id] !== false, root, ...describe(root) };
|
|
1440
1808
|
});
|
|
1441
1809
|
return [...fromMarketplaces, ...fromSettings, ...fromSynced];
|
|
1442
1810
|
}
|
|
1443
1811
|
function cursorPlugins() {
|
|
1444
1812
|
const cache = join11(userHome(), ".cursor", "plugins", "cache");
|
|
1445
|
-
const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") &&
|
|
1813
|
+
const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") && statSync4(join11(p, d)).isDirectory()) : [];
|
|
1446
1814
|
return dirs(cache).flatMap(
|
|
1447
1815
|
(marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
|
|
1448
1816
|
const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
|
|
1449
|
-
const root = versions.sort((a, b) =>
|
|
1817
|
+
const root = versions.sort((a, b) => statSync4(b).mtimeMs - statSync4(a).mtimeMs)[0];
|
|
1450
1818
|
if (!root) return [];
|
|
1451
1819
|
const about = describe(root);
|
|
1452
1820
|
return [{ id: `${plugin}@${marketplace}`, scope: null, synced: false, agent: "cursor", on: null, root, ...about, version: about.version ?? basename4(root) }];
|
|
@@ -1485,11 +1853,10 @@ function claude(args, cwd, bin = claudeBinary()) {
|
|
|
1485
1853
|
}
|
|
1486
1854
|
}
|
|
1487
1855
|
function setPluginIn(file, id, on) {
|
|
1488
|
-
const settings = existsSync10(file) ?
|
|
1856
|
+
const settings = existsSync10(file) ? readJson(file) : {};
|
|
1489
1857
|
if (!settings) return { ok: false, reason: `can't read ${file}` };
|
|
1490
1858
|
const enabled = settings.enabledPlugins ?? {};
|
|
1491
|
-
|
|
1492
|
-
writeFileSync7(file, JSON.stringify({ ...settings, enabledPlugins: { ...enabled, [id]: on } }, null, 2) + "\n");
|
|
1859
|
+
writeState(file, { ...settings, enabledPlugins: { ...enabled, [id]: on } });
|
|
1493
1860
|
return { ok: true };
|
|
1494
1861
|
}
|
|
1495
1862
|
function ignoreLocalSettings(root) {
|
|
@@ -1511,10 +1878,10 @@ function ignoreLocalSettings(root) {
|
|
|
1511
1878
|
} catch {
|
|
1512
1879
|
}
|
|
1513
1880
|
try {
|
|
1514
|
-
const exclude =
|
|
1515
|
-
const before = existsSync10(exclude) ?
|
|
1516
|
-
|
|
1517
|
-
|
|
1881
|
+
const exclude = resolve7(root, git2("rev-parse", "--git-path", "info/exclude"));
|
|
1882
|
+
const before = existsSync10(exclude) ? readFileSync6(exclude, "utf-8") : "";
|
|
1883
|
+
mkdirSync6(dirname4(exclude), { recursive: true });
|
|
1884
|
+
writeFileSync4(exclude, `${before}${before && !before.endsWith("\n") ? "\n" : ""}/${git2("rev-parse", "--show-prefix")}${file}
|
|
1518
1885
|
`);
|
|
1519
1886
|
return ", which git now ignores via .git/info/exclude";
|
|
1520
1887
|
} catch {
|
|
@@ -1523,7 +1890,7 @@ function ignoreLocalSettings(root) {
|
|
|
1523
1890
|
}
|
|
1524
1891
|
function turnOffIn(plugin, root) {
|
|
1525
1892
|
const done = `${plugin.id} turned off in ${basename4(root)}; other repos keep it`;
|
|
1526
|
-
const bin = plugin.synced ? null : claudeBinary();
|
|
1893
|
+
const bin = plugin.synced || plugin.settingsOnly ? null : claudeBinary();
|
|
1527
1894
|
if (!bin) {
|
|
1528
1895
|
const r2 = setPluginIn(join11(root, ".claude", "settings.local.json"), plugin.id, false);
|
|
1529
1896
|
return r2.ok ? { ok: true, message: `${done} (in .claude/settings.local.json${ignoreLocalSettings(root)})` } : { ok: false, message: `${plugin.id}: ${r2.reason}` };
|
|
@@ -1540,7 +1907,8 @@ function whereOf(p) {
|
|
|
1540
1907
|
return p.projectPath ? ` in ${basename4(p.projectPath)} (${p.scope})` : "";
|
|
1541
1908
|
}
|
|
1542
1909
|
function setPluginOn(p, on) {
|
|
1543
|
-
const
|
|
1910
|
+
const shared = p.scope === "project" && p.projectPath ? "; it's in .claude/settings.json, which git tracks, so commit it if the team should get it" : "";
|
|
1911
|
+
const done = { ok: true, message: `${p.id} is ${on ? "on" : "off"}${whereOf(p)}${p.synced ? "" : "; Claude Code picks it up in its next session"}${shared}` };
|
|
1544
1912
|
if (p.scope && p.scope !== "user" && !p.projectPath) return { ok: false, message: `${p.id}: skilllib doesn't know which project it's installed in; run \`claude plugin ${on ? "enable" : "disable"} ${p.id} --scope ${p.scope}\` there` };
|
|
1545
1913
|
const bin = p.synced || p.settingsOnly ? null : claudeBinary();
|
|
1546
1914
|
if (bin) {
|
|
@@ -1549,7 +1917,8 @@ function setPluginOn(p, on) {
|
|
|
1549
1917
|
}
|
|
1550
1918
|
const r = setPluginIn(settingsFileOf(p), p.id, on);
|
|
1551
1919
|
if (!r.ok) return { ok: false, message: `${p.id}: ${r.reason}` };
|
|
1552
|
-
|
|
1920
|
+
if (p.scope === "local" && p.projectPath) return { ...done, message: done.message + ignoreLocalSettings(p.projectPath) };
|
|
1921
|
+
return done;
|
|
1553
1922
|
}
|
|
1554
1923
|
function uninstallPlugin(p) {
|
|
1555
1924
|
if (p.synced) return { ok: false, message: `${p.id} is synced from your claude.ai account: remove it there, or turn it off here` };
|
|
@@ -1579,35 +1948,44 @@ function removedFile() {
|
|
|
1579
1948
|
return join11(skilllibHome(), "plugin-backup.json");
|
|
1580
1949
|
}
|
|
1581
1950
|
function removePlugin(plugin, how) {
|
|
1951
|
+
if (plugin.settingsOnly && how === "delete") return { ok: false, message: `${plugin.id} isn't installed, only turned on in your settings: turn it off instead` };
|
|
1582
1952
|
if (plugin.scope && plugin.scope !== "user" && !plugin.projectPath) {
|
|
1583
1953
|
return { ok: false, message: `${plugin.id}: installed for one project; run \`claude plugin ${how === "delete" ? "uninstall" : "disable"} ${plugin.id} --scope ${plugin.scope}\` there` };
|
|
1584
1954
|
}
|
|
1585
|
-
|
|
1955
|
+
const viaSettings = plugin.synced || plugin.settingsOnly || !claudeBinary() && how === "off" && !plugin.projectPath;
|
|
1956
|
+
if (!viaSettings && !claudeBinary()) {
|
|
1586
1957
|
return { ok: false, message: `${plugin.id}: nothing changed; Claude Code's CLI didn't run (tried \`claude\` on your PATH and ~/.local/bin/claude)` };
|
|
1587
1958
|
}
|
|
1588
|
-
const
|
|
1959
|
+
const wasOn = enabledPlugins(plugin.projectPath)[plugin.id] === true;
|
|
1960
|
+
const yours = new Set(machineSkills().filter((m) => m.movable).map((m) => m.name));
|
|
1961
|
+
const imported = plugin.skills.filter((dir) => !yours.has(basename4(dir)) && latestVersion(basename4(dir)) === null).map((dir) => importSkill(dir).name);
|
|
1589
1962
|
const scope = plugin.scope ? ["--scope", plugin.scope] : [];
|
|
1590
|
-
const r =
|
|
1591
|
-
// Synced plugins aren't installed, so `claude plugin` can't touch them: their switch is enabledPlugins.
|
|
1963
|
+
const r = viaSettings ? (
|
|
1964
|
+
// Synced and settings-only plugins aren't installed, so `claude plugin` can't touch them: their switch is enabledPlugins.
|
|
1592
1965
|
setPluginIn(join11(claudeDir(), "settings.json"), plugin.id, false)
|
|
1593
|
-
) :
|
|
1966
|
+
) : (
|
|
1967
|
+
// --keep-data, as uninstallPlugin: its saved data stays, so a restore brings it back as it was.
|
|
1968
|
+
claude(["plugin", how === "delete" ? "uninstall" : "disable", plugin.id, ...how === "delete" ? ["--keep-data"] : [], ...scope], plugin.projectPath)
|
|
1969
|
+
);
|
|
1594
1970
|
const saved = imported.length ? `; ${imported.join(", ")} copied into Your skills` : "";
|
|
1595
1971
|
if (!r.ok) return { ok: false, message: `${plugin.id}: ${r.reason}${saved}` };
|
|
1596
|
-
if (how === "delete" && !plugin.synced) recordRemovedPlugin(plugin.id, plugin.scope ?? "user", plugin.projectPath,
|
|
1597
|
-
|
|
1972
|
+
if (how === "delete" && !plugin.synced) recordRemovedPlugin(plugin.id, plugin.scope ?? "user", plugin.projectPath, wasOn);
|
|
1973
|
+
const others = how === "delete" ? (installedPluginsJson()[plugin.id] ?? []).filter((e) => (e.scope ?? "user") !== (plugin.scope ?? "user") || e.projectPath !== plugin.projectPath) : [];
|
|
1974
|
+
const left = others.length ? `; its ${others.map((e) => `${e.scope ?? "user"} install${e.projectPath ? ` in ${basename4(e.projectPath)}` : ""}`).join(", ")} stays` : "";
|
|
1975
|
+
return { ok: true, message: `${plugin.id} ${how === "delete" && !plugin.synced ? `removed (${plugin.scope ?? "user"} install)` : "turned off"}${saved}${left}` };
|
|
1598
1976
|
}
|
|
1599
1977
|
function recordRemovedPlugin(id, scope, projectPath, on = true) {
|
|
1600
|
-
const list2 =
|
|
1601
|
-
|
|
1602
|
-
|
|
1978
|
+
const list2 = readState(removedFile()) ?? [];
|
|
1979
|
+
mkdirSync6(skilllibHome(), { recursive: true });
|
|
1980
|
+
writeState(removedFile(), [...list2, { id, scope, ...projectPath ? { projectPath } : {}, on, at: (/* @__PURE__ */ new Date()).toISOString() }]);
|
|
1603
1981
|
}
|
|
1604
1982
|
function pluginBackups() {
|
|
1605
|
-
return (
|
|
1983
|
+
return (readState(removedFile()) ?? []).map((r) => ({
|
|
1606
1984
|
name: r.id,
|
|
1607
1985
|
kind: "plugin",
|
|
1608
1986
|
// Project and local installs go back into their project.
|
|
1609
1987
|
path: r.projectPath ? `${r.scope}:${r.projectPath}` : r.scope,
|
|
1610
|
-
movedAt: r.at
|
|
1988
|
+
movedAt: localStamp(Date.parse(r.at)),
|
|
1611
1989
|
from: r.id
|
|
1612
1990
|
}));
|
|
1613
1991
|
}
|
|
@@ -1616,11 +1994,11 @@ function restorePlugin(backup) {
|
|
|
1616
1994
|
const projectPath = rest.join(":") || void 0;
|
|
1617
1995
|
const r = claude(["plugin", "install", backup.from, "--scope", scope], projectPath);
|
|
1618
1996
|
if (!r.ok) return r;
|
|
1619
|
-
const records =
|
|
1997
|
+
const records = readState(removedFile()) ?? [];
|
|
1620
1998
|
const same = (x) => x.id === backup.from && (x.projectPath ?? void 0) === projectPath && x.scope === scope;
|
|
1621
1999
|
const wasOff = records.filter(same).pop()?.on === false;
|
|
1622
2000
|
const off = wasOff ? claude(["plugin", "disable", backup.from, "--scope", scope], projectPath) : null;
|
|
1623
|
-
|
|
2001
|
+
writeState(removedFile(), records.filter((x) => !same(x)));
|
|
1624
2002
|
const where = `Claude Code plugins (${scope}${projectPath ? ` in ${basename4(projectPath)}` : ""}${wasOff ? off?.ok ? ", off as it was" : ", but it's on: turn it off from Plugins" : ""})`;
|
|
1625
2003
|
return { ok: true, to: where };
|
|
1626
2004
|
}
|
|
@@ -1629,14 +2007,14 @@ function cursorPluginSkills(plugins = cursorPlugins()) {
|
|
|
1629
2007
|
}
|
|
1630
2008
|
|
|
1631
2009
|
// src/tidy.ts
|
|
1632
|
-
import { existsSync as existsSync11, mkdirSync as
|
|
1633
|
-
import { basename as basename5, dirname as
|
|
2010
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync7, readdirSync as readdirSync6, unlinkSync as unlinkSync3 } from "fs";
|
|
2011
|
+
import { basename as basename5, dirname as dirname5, join as join12, sep as sep6 } from "path";
|
|
1634
2012
|
function entriesOf(dirs, name) {
|
|
1635
2013
|
return dirs.flatMap((dir) => {
|
|
1636
2014
|
const path = join12(dir, name);
|
|
1637
2015
|
if (!entryExists(path)) return [];
|
|
1638
2016
|
const link2 = isLink(path);
|
|
1639
|
-
const real =
|
|
2017
|
+
const real = realpathOrNull(path);
|
|
1640
2018
|
const skill = existsSync11(join12(path, "SKILL.md"));
|
|
1641
2019
|
if (!link2 && !skill) return [];
|
|
1642
2020
|
return [{ dir, path, link: link2, real, hash: null }];
|
|
@@ -1648,12 +2026,13 @@ function hashReals(reals) {
|
|
|
1648
2026
|
function namesIn(dirs) {
|
|
1649
2027
|
const names = /* @__PURE__ */ new Set();
|
|
1650
2028
|
for (const dir of dirs) {
|
|
1651
|
-
if (!
|
|
2029
|
+
if (!isDir(dir)) continue;
|
|
1652
2030
|
for (const name of readdirSync6(dir)) if (!name.startsWith(".") && name !== "synced") names.add(name);
|
|
1653
2031
|
}
|
|
1654
2032
|
return [...names].sort();
|
|
1655
2033
|
}
|
|
1656
2034
|
var PROJECT_TIDY_DIRS = ALL_PROJECT_DIRS.filter((d) => d !== GROK_SKILLS_DIR);
|
|
2035
|
+
var repoTidyDirs = (root) => PROJECT_TIDY_DIRS.filter((d) => notWritable(root, d) === null);
|
|
1657
2036
|
function globalTidyDirs() {
|
|
1658
2037
|
const own = realPath(join12(grokDir(), "skills"));
|
|
1659
2038
|
return globalSkillDirs().filter((d) => realPath(d) !== own);
|
|
@@ -1661,7 +2040,7 @@ function globalTidyDirs() {
|
|
|
1661
2040
|
function runsFrom(id, dirsWithCopy, readsOf) {
|
|
1662
2041
|
const reads = readsOf(id).filter((d) => dirsWithCopy.includes(d));
|
|
1663
2042
|
if (id === "codex") return reads;
|
|
1664
|
-
const pick = (d) => CURSOR_PICKS.indexOf(basename5(
|
|
2043
|
+
const pick = (d) => CURSOR_PICKS.indexOf(basename5(dirname5(d)));
|
|
1665
2044
|
return id === "cursor" ? [...reads].sort((a, b) => pick(a) - pick(b)).slice(0, 1) : reads.slice(0, 1);
|
|
1666
2045
|
}
|
|
1667
2046
|
function conflictOf(name, root, reals, enabled, readsOf) {
|
|
@@ -1677,17 +2056,19 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1677
2056
|
const { skills } = readManifest(root);
|
|
1678
2057
|
const abs = (dir) => join12(root, dir);
|
|
1679
2058
|
const rel = (dir) => relativeTo(root, dir);
|
|
1680
|
-
const readsOf = (id) =>
|
|
2059
|
+
const readsOf = (id) => harness(id).projectDirs.map(abs);
|
|
1681
2060
|
let git2;
|
|
1682
2061
|
const skillsLock = projectSkillsLock(root);
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
const
|
|
2062
|
+
const dirs = repoTidyDirs(root).map(abs);
|
|
2063
|
+
for (const name of namesIn(dirs)) {
|
|
2064
|
+
const entries = entriesOf(dirs, name);
|
|
2065
|
+
const external = entries.find((e) => e.link && e.real && !(e.real + sep6).startsWith(realpathOrNull(root) + sep6));
|
|
1686
2066
|
if (external) {
|
|
1687
|
-
|
|
2067
|
+
const differs = entries.some((e) => e !== external && e.real && existsSync11(join12(e.real, "SKILL.md")) && treeHash(e.real) !== treeHash(external.real));
|
|
2068
|
+
report.skipped.push({ name, reason: `${rel(external.path)} links outside the repo`, ...differs ? { differs } : {} });
|
|
1688
2069
|
continue;
|
|
1689
2070
|
}
|
|
1690
|
-
const reals = entries.filter((e) => !e.link);
|
|
2071
|
+
const reals = entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link && realpathOrNull(o.path) === realpathOrNull(e.path)) === i);
|
|
1691
2072
|
if (reals.length === 0) continue;
|
|
1692
2073
|
hashReals(reals);
|
|
1693
2074
|
const dep = skills[name];
|
|
@@ -1710,17 +2091,24 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1710
2091
|
report.conflicts.push({ ...conflictOf(name, root, reals, enabled, readsOf), ...dep ? { managed: primary.dir } : {} });
|
|
1711
2092
|
continue;
|
|
1712
2093
|
}
|
|
2094
|
+
const elsewhere = entries.find((e) => e.link && e.real && e.real !== primary.real && existsSync11(join12(e.real, "SKILL.md")) && treeHash(e.real) !== treeHash(primary.path));
|
|
2095
|
+
if (elsewhere) {
|
|
2096
|
+
report.skipped.push({ name, reason: `${rel(elsewhere.path)} links to a different copy`, differs: true });
|
|
2097
|
+
continue;
|
|
2098
|
+
}
|
|
1713
2099
|
const wanted = new Set([...installDirs(enabled, rel(primary.dir)), ...dep?.links ?? []].map(abs));
|
|
1714
2100
|
const steps = [];
|
|
1715
2101
|
for (const e of entries) {
|
|
1716
|
-
if (e === primary) continue;
|
|
2102
|
+
if (e === primary || !e.link && e.real === primary.real) continue;
|
|
1717
2103
|
const needed = wanted.has(e.dir);
|
|
1718
2104
|
if (e.link) {
|
|
1719
2105
|
if (e.real === primary.real) {
|
|
2106
|
+
if (!needed && !dep && ["committed", "changed"].includes((git2 ??= gitInfo(root))?.of(rel(e.path)) ?? "")) continue;
|
|
1720
2107
|
if (!needed) steps.push({ kind: "unlink", path: e.path });
|
|
1721
2108
|
} else steps.push(needed ? { kind: "link", path: e.path, target: primary.path } : { kind: "unlink", path: e.path });
|
|
1722
2109
|
} else {
|
|
1723
2110
|
const differs = e.hash !== primary.hash ? { differs: true } : {};
|
|
2111
|
+
if (!needed && ["committed", "changed"].includes((git2 ??= gitInfo(root))?.of(rel(e.path)) ?? "")) continue;
|
|
1724
2112
|
steps.push(needed ? { kind: "replace", path: e.path, target: primary.path, ...differs } : { kind: "remove", path: e.path, ...differs });
|
|
1725
2113
|
}
|
|
1726
2114
|
}
|
|
@@ -1729,7 +2117,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1729
2117
|
return report;
|
|
1730
2118
|
}
|
|
1731
2119
|
function globalReadsOf(id) {
|
|
1732
|
-
return
|
|
2120
|
+
return harness(id).globalDirs().map(globalDirAs);
|
|
1733
2121
|
}
|
|
1734
2122
|
function isSkillsDirPlugin(path) {
|
|
1735
2123
|
return existsSync11(join12(path, ".claude-plugin", "plugin.json"));
|
|
@@ -1744,7 +2132,7 @@ function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
|
|
|
1744
2132
|
report.skipped.push({ name, reason: "a Claude Code plugin kept in a skills folder" });
|
|
1745
2133
|
continue;
|
|
1746
2134
|
}
|
|
1747
|
-
const reals = entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link &&
|
|
2135
|
+
const reals = entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link && realpathOrNull(o.path) === realpathOrNull(e.path)) === i);
|
|
1748
2136
|
if (reals.length < 2) continue;
|
|
1749
2137
|
hashReals(reals);
|
|
1750
2138
|
const kept = reals.find((r) => r.dir === keep[name]);
|
|
@@ -1760,21 +2148,22 @@ function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
|
|
|
1760
2148
|
}
|
|
1761
2149
|
function gitVisibleSteps(plan, git2 = plan.root ? gitInfo(plan.root) : null) {
|
|
1762
2150
|
if (!plan.root || !git2) return [];
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
2151
|
+
const shared = (rel) => ["committed", "changed"].includes(git2.of(rel));
|
|
2152
|
+
const dep = readManifest(plan.root).skills[plan.name];
|
|
2153
|
+
const recordsLink = (s) => !!dep && (s.kind === "link" || s.kind === "replace") && !dep.links?.includes(relativeTo(plan.root, dirname5(s.path)));
|
|
2154
|
+
return plan.steps.filter((s) => shared(relativeTo(plan.root, s.path)) || recordsLink(s) && shared(MANIFEST_FILE));
|
|
1767
2155
|
}
|
|
1768
2156
|
function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
|
|
1769
2157
|
const held = git2 === "keep" ? gitVisibleSteps(plan, info2) : [];
|
|
1770
2158
|
const applied = [];
|
|
1771
2159
|
for (const step of plan.steps) {
|
|
1772
2160
|
if (held.includes(step) || !entryExists(step.path) || "target" in step && !entryExists(step.target)) continue;
|
|
2161
|
+
if ("target" in step && realpathOrNull(step.path) === realpathOrNull(step.target)) continue;
|
|
1773
2162
|
if (step.kind === "link" || step.kind === "unlink") {
|
|
1774
2163
|
if (isLink(step.path)) unlinkSync3(step.path);
|
|
1775
2164
|
} else stash(step.path, "tidy-backup");
|
|
1776
2165
|
if (step.kind === "link" || step.kind === "replace") {
|
|
1777
|
-
|
|
2166
|
+
mkdirSync7(dirname5(step.path), { recursive: true });
|
|
1778
2167
|
linkDir(step.target, step.path);
|
|
1779
2168
|
}
|
|
1780
2169
|
applied.push(step);
|
|
@@ -1782,7 +2171,7 @@ function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
|
|
|
1782
2171
|
if (plan.root) {
|
|
1783
2172
|
const manifest = readManifest(plan.root);
|
|
1784
2173
|
const dep = manifest.skills[plan.name];
|
|
1785
|
-
const added = applied.filter((s) => s.kind === "link" || s.kind === "replace").map((s) => relativeTo(plan.root,
|
|
2174
|
+
const added = applied.filter((s) => s.kind === "link" || s.kind === "replace").map((s) => relativeTo(plan.root, dirname5(s.path)));
|
|
1786
2175
|
if (dep && added.length) {
|
|
1787
2176
|
manifest.skills[plan.name] = { ...dep, links: [.../* @__PURE__ */ new Set([...dep.links ?? [], ...added])] };
|
|
1788
2177
|
writeManifest(plan.root, manifest);
|
|
@@ -1803,18 +2192,19 @@ function describeStep(step, root) {
|
|
|
1803
2192
|
return `remove ${step.differs ? "differing" : "duplicate"} copy ${shown}`;
|
|
1804
2193
|
}
|
|
1805
2194
|
}
|
|
1806
|
-
function copyLabel(conflict, dir) {
|
|
1807
|
-
return folderLabel(conflict.root, dir);
|
|
1808
|
-
}
|
|
1809
2195
|
function folderLabel(root, dir) {
|
|
1810
2196
|
return root ? relativeTo(root, dir) : tildify(dir);
|
|
1811
2197
|
}
|
|
1812
2198
|
function foldersOf(root, name) {
|
|
1813
|
-
const entries = entriesOf(root ?
|
|
2199
|
+
const entries = entriesOf(root ? repoTidyDirs(root).map((d) => join12(root, d)) : globalTidyDirs(), name);
|
|
1814
2200
|
return { all: entries.map((e) => e.dir), real: entries.filter((e) => !e.link).map((e) => e.dir) };
|
|
1815
2201
|
}
|
|
1816
2202
|
|
|
1817
2203
|
// src/health.ts
|
|
2204
|
+
function detailWithGit(issue) {
|
|
2205
|
+
const notes = [...issue.fix ? [issue.fix] : [], ...issue.choices ?? []].filter((c) => c.hint && /\bgit\b|committed/.test(c.hint)).map((c) => `"${c.label}": ${c.hint}.`);
|
|
2206
|
+
return notes.length ? `${issue.detail} ${notes.join(" ")}` : issue.detail;
|
|
2207
|
+
}
|
|
1818
2208
|
function runFix(choice) {
|
|
1819
2209
|
try {
|
|
1820
2210
|
return { ok: true, message: choice.run() };
|
|
@@ -1826,50 +2216,193 @@ function orThrow(r) {
|
|
|
1826
2216
|
if (!r.ok) throw new Error(r.message);
|
|
1827
2217
|
return r.message;
|
|
1828
2218
|
}
|
|
2219
|
+
function intoLibrary(path, force = false) {
|
|
2220
|
+
const r = importSkill(path, { force });
|
|
2221
|
+
if (r.status === "exists") throw new Error(`${r.name}: your library has a different ${r.name}; update or delete it first`);
|
|
2222
|
+
return r.name;
|
|
2223
|
+
}
|
|
2224
|
+
function track(root, name) {
|
|
2225
|
+
const c = addSkill(root, name);
|
|
2226
|
+
if (c.action === "skipped") throw new Error(`${name}: ${c.reason}`);
|
|
2227
|
+
return c;
|
|
2228
|
+
}
|
|
2229
|
+
var installedIn = (p) => p.projectPath ? ` in ${basename6(p.projectPath)}` : "";
|
|
2230
|
+
var installId = (p) => `${p.projectPath ? `${p.projectPath}:` : ""}${p.id}`;
|
|
2231
|
+
function summarize(where, verb, changes) {
|
|
2232
|
+
const done = changes.filter((c) => c.action !== "skipped");
|
|
2233
|
+
const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
|
|
2234
|
+
if (!done.length) throw new Error(`${where}: nothing ${verb}${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
|
|
2235
|
+
return `${where}: ${verb} ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
|
|
2236
|
+
}
|
|
1829
2237
|
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1830
|
-
var
|
|
2238
|
+
var rows = (root, skills) => skills.map((skill) => ({ root, skill }));
|
|
2239
|
+
var removesClean = (root, s) => s.managed && !s.state.startsWith("edited") && s.state !== "not in library" && !syncBlocker(root, s.name);
|
|
2240
|
+
var gitMemo = /* @__PURE__ */ new Map();
|
|
2241
|
+
var gitOf = (root) => {
|
|
2242
|
+
if (!gitMemo.has(root)) gitMemo.set(root, gitInfo(root));
|
|
2243
|
+
return gitMemo.get(root);
|
|
2244
|
+
};
|
|
2245
|
+
var gitNote = (root, paths) => {
|
|
2246
|
+
const git2 = gitOf(root);
|
|
2247
|
+
return paths.some((p) => ["committed", "changed"].includes(git2?.of(relativeTo(root, p)) ?? "")) ? "; git will see it" : "";
|
|
2248
|
+
};
|
|
2249
|
+
var agentNames = (ids) => ids.map((id) => harness(id).name).join(", ");
|
|
1831
2250
|
function conflictIssue(c) {
|
|
1832
2251
|
const where = c.root ? basename6(c.root) : "your global folders";
|
|
1833
2252
|
const winners = c.managed ? c.copies.filter((x) => x.dir === c.managed) : c.copies;
|
|
2253
|
+
const planFor = (dir) => (c.root ? planProjectTidy(c.root, { keep: { [c.name]: dir } }) : planGlobalTidy({ keep: { [c.name]: dir } })).plans.find((p) => p.name === c.name);
|
|
2254
|
+
const keep = (dir, git2) => () => {
|
|
2255
|
+
const plan = planFor(dir);
|
|
2256
|
+
const r = plan ? applyTidy(plan, { git: git2 }) : null;
|
|
2257
|
+
if (!r?.applied.length) throw new Error(`${c.name}: nothing changed${r?.held.length ? " (git would see every change)" : ""}`);
|
|
2258
|
+
return `${c.name}: kept the ${folderLabel(c.root, dir)} copy${r.held.length ? `; held back ${plural(r.held.length, "change")} git would see` : ""}`;
|
|
2259
|
+
};
|
|
1834
2260
|
return {
|
|
1835
2261
|
id: `conflict:${c.root ?? "global"}:${c.name}`,
|
|
1836
2262
|
severity: "problem",
|
|
1837
2263
|
title: `${c.name} in ${where}: the copies differ`,
|
|
1838
|
-
detail: `Your agents run different versions: ${c.copies.map((x) => `${
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
2264
|
+
detail: `Your agents run different versions: ${c.copies.map((x) => `${folderLabel(c.root, x.dir)}${x.runs.length ? ` (${agentNames(x.runs)})` : ""}`).join(" vs ")}. Keep one; the others become links to it and are restorable with skilllib restore (Settings \u203A Backups in the app).`,
|
|
2265
|
+
at: rows(c.root, [c.name]),
|
|
2266
|
+
choices: winners.flatMap((x) => {
|
|
2267
|
+
const plan = planFor(x.dir);
|
|
2268
|
+
const visible2 = plan ? gitVisibleSteps(plan).map((s) => describeStep(s, c.root)) : [];
|
|
2269
|
+
const label = `Keep the ${folderLabel(c.root, x.dir)} copy`;
|
|
2270
|
+
if (!visible2.length) return [{ label, hint: x.dir === c.managed ? "skilllib's version" : "the others become links to it", run: keep(x.dir, "go") }];
|
|
2271
|
+
return [
|
|
2272
|
+
// "Keep git as it is" only when it leaves something to do.
|
|
2273
|
+
...visible2.length < (plan?.steps.length ?? 0) ? [{ label: `${label}, but keep git as it is`, hint: `skips what git would see: ${visible2.join(", ")}`, run: keep(x.dir, "keep") }] : [],
|
|
2274
|
+
{ label: `${label}, and change what git tracks`, hint: `git will see: ${visible2.join(", ")}`, run: keep(x.dir, "go") }
|
|
2275
|
+
];
|
|
2276
|
+
})
|
|
1849
2277
|
};
|
|
1850
2278
|
}
|
|
1851
2279
|
function applyAll(plans, git2) {
|
|
1852
2280
|
const results = plans.map((p) => applyTidy(p, { git: git2 }));
|
|
1853
2281
|
return { skills: results.filter((r) => r.applied.length).length, held: results.reduce((n, r) => n + r.held.length, 0) };
|
|
1854
2282
|
}
|
|
1855
|
-
function
|
|
1856
|
-
const
|
|
2283
|
+
function brokenLinksIn(root, opened) {
|
|
2284
|
+
const where = basename6(root);
|
|
2285
|
+
const tidyDirs = repoTidyDirs(root);
|
|
2286
|
+
const hasCopy = (name) => tidyDirs.some((d) => !isLink(join13(root, d, name)) && existsSync12(join13(root, d, name, "SKILL.md")));
|
|
2287
|
+
const mends = (project, dir, name) => {
|
|
2288
|
+
const dep = pinsIn(project)[name];
|
|
2289
|
+
return !!dep && (dep.links ?? []).includes(dir) && !syncBlocker(project, name, dep) && versionForHash(name, dep.hash) !== null;
|
|
2290
|
+
};
|
|
2291
|
+
const syncMends = (dir, name) => mends(root, dir, name);
|
|
2292
|
+
let git2;
|
|
2293
|
+
const realRoot = realNative(root) ?? root;
|
|
2294
|
+
const ownerOf = (real) => {
|
|
2295
|
+
let nearest = null;
|
|
2296
|
+
for (let d = dirname6(dirname6(real)); d.startsWith(realRoot + sep7); d = dirname6(d)) {
|
|
2297
|
+
if (!existsSync12(join13(d, MANIFEST_FILE))) continue;
|
|
2298
|
+
if (opened.has(d)) return { opened: true, own: d };
|
|
2299
|
+
nearest ??= d;
|
|
2300
|
+
}
|
|
2301
|
+
return { opened: false, own: nearest };
|
|
2302
|
+
};
|
|
2303
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2304
|
+
const aliased = (dir) => (realNative(join13(root, dir)) ?? "") !== join13(realRoot, dir);
|
|
2305
|
+
const packageFolders = packageSkillFolders(root).map((f) => ({ dir: f.location, top: false })).sort((a, b) => Number(aliased(a.dir)) - Number(aliased(b.dir)));
|
|
2306
|
+
const folders2 = [...ALL_PROJECT_DIRS.map((dir) => ({ dir, top: true })), ...packageFolders].flatMap(({ dir, top }) => {
|
|
2307
|
+
const real = realNative(join13(root, dir)) ?? join13(root, dir);
|
|
2308
|
+
if (seen.has(real)) return [];
|
|
2309
|
+
seen.add(real);
|
|
2310
|
+
const owner = ownerOf(real);
|
|
2311
|
+
return owner.opened ? [] : [{ dir, top: top && !owner.own, own: owner.own, real }];
|
|
2312
|
+
});
|
|
2313
|
+
return folders2.flatMap(({ dir, top, own, real }) => {
|
|
2314
|
+
const folder = join13(root, dir);
|
|
2315
|
+
if (!isDir(folder) || notWritable(root, dir)) return [];
|
|
2316
|
+
return readdirSync7(folder).flatMap((name) => {
|
|
2317
|
+
const path = join13(folder, name);
|
|
2318
|
+
if (!isLink(path) || isDir(path) || top && (dir !== GROK_SKILLS_DIR && hasCopy(name) || syncMends(dir, name))) return [];
|
|
2319
|
+
const remove2 = {
|
|
2320
|
+
label: "Remove the link",
|
|
2321
|
+
hint: "restorable with skilllib restore",
|
|
2322
|
+
run: () => {
|
|
2323
|
+
if (!isLink(path)) return `${dir}/${name} in ${where} is already gone`;
|
|
2324
|
+
stash(path, "tidy-backup");
|
|
2325
|
+
return `Removed the broken link ${dir}/${name} in ${where} (restorable with skilllib restore)`;
|
|
2326
|
+
}
|
|
2327
|
+
};
|
|
2328
|
+
const placeOf = (p) => relative4(realRoot, p).split(sep7).join("/");
|
|
2329
|
+
git2 ??= gitInfo(
|
|
2330
|
+
root,
|
|
2331
|
+
folders2.map((f) => placeOf(f.real)).filter((p) => p !== "" && p !== ".." && !p.startsWith("../") && !isAbsolute3(p))
|
|
2332
|
+
);
|
|
2333
|
+
const tracked = ["committed", "changed"].includes(git2?.of(placeOf(join13(real, name))) ?? "");
|
|
2334
|
+
const recorded = !!own && mends(own, relative4(own, real).split(sep7).join("/"), name);
|
|
2335
|
+
const pinnedThere = own ? ` ${placeOf(own)} has its own skilllib.json${recorded ? `, which records this link: skilllib sync in ${placeOf(own)} brings back what it points to` : ""}.` : "";
|
|
2336
|
+
return [
|
|
2337
|
+
{
|
|
2338
|
+
id: `broken:${root}:${dir}/${name}`,
|
|
2339
|
+
severity: "problem",
|
|
2340
|
+
title: `${name}: broken link in ${where}/${dir}`,
|
|
2341
|
+
detail: `It points at ${existsSync12(path) ? "a file, not a skill folder" : "a folder that no longer exists"}, so it loads nothing.${tracked ? " Git tracks the link, so removing it shows in git status." : ""}${pinnedThere}`,
|
|
2342
|
+
at: rows(root, [name]),
|
|
2343
|
+
// Removed by itself only at the repo's top, uncommitted: a package's link (or a committed one) is asked first.
|
|
2344
|
+
...top && !tracked ? { fix: remove2 } : { choices: [remove2] }
|
|
2345
|
+
}
|
|
2346
|
+
];
|
|
2347
|
+
});
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
function grokDiffers(projects2, machine, statusOf) {
|
|
2351
|
+
if (!enabledHarnesses().includes("grok")) return [];
|
|
2352
|
+
const issue = (root, name, grokCopy, other) => ({
|
|
2353
|
+
id: `grok-differs:${root ?? "global"}:${name}`,
|
|
2354
|
+
severity: "suggestion",
|
|
2355
|
+
title: `${name}${root ? ` in ${basename6(root)}` : ""}: Grok runs a different copy`,
|
|
2356
|
+
detail: `Grok reads ${grokCopy} first; your other agents run ${other}, and the two differ. skilllib doesn't change Grok's folder: update or remove that copy there to make them match.`,
|
|
2357
|
+
at: rows(root, [name])
|
|
2358
|
+
});
|
|
2359
|
+
const grokGlobal = join13(grokDir(), "skills");
|
|
2360
|
+
const global = skillDirsIn(grokGlobal).filter((path) => !isLink(path)).flatMap((path) => {
|
|
2361
|
+
const name = basename6(path);
|
|
2362
|
+
const other = machine.find((m) => m.name === name && m.movable && !m.broken && m.harnesses.some((id) => id !== "grok") && realPath(dirname6(m.path)) !== realPath(grokGlobal));
|
|
2363
|
+
return other && treeHash(other.path) !== treeHash(path) ? [issue(null, name, tildify(path), tildify(other.path))] : [];
|
|
2364
|
+
});
|
|
2365
|
+
const repos = projects2.flatMap(
|
|
2366
|
+
(root) => skillDirsIn(join13(root, GROK_SKILLS_DIR)).filter((path) => !isLink(path)).flatMap((path) => {
|
|
2367
|
+
const name = basename6(path);
|
|
2368
|
+
const other = statusOf(root).find((s) => s.name === name && !s.path.startsWith(join13(root, GROK_SKILLS_DIR)));
|
|
2369
|
+
return other && treeHash(other.path) !== treeHash(path) ? [issue(root, name, `${GROK_SKILLS_DIR}/${name}`, relativeTo(root, other.path))] : [];
|
|
2370
|
+
})
|
|
2371
|
+
);
|
|
2372
|
+
return [...global, ...repos];
|
|
2373
|
+
}
|
|
2374
|
+
function findIssues(roots, machine, libraryNames, statusOf = projectStatus, loadedIn = (root) => skillsLoadedIn(root, machine), nestedOf = nestedSkills) {
|
|
2375
|
+
gitMemo = /* @__PURE__ */ new Map();
|
|
2376
|
+
const { readable: projects2, unreadable } = readableRoots(roots);
|
|
2377
|
+
const issues = unreadable.map(({ root, reason }) => ({
|
|
2378
|
+
id: `manifest:${root}`,
|
|
2379
|
+
severity: "problem",
|
|
2380
|
+
title: `${basename6(root)}: skilllib.json can't be read`,
|
|
2381
|
+
detail: `${reason}. Until it's fixed, skilllib leaves ${basename6(root)} out.`,
|
|
2382
|
+
at: [{ root, skill: "" }]
|
|
2383
|
+
}));
|
|
1857
2384
|
const statuses = new Map(projects2.map((root) => [root, statusOf(root)]));
|
|
2385
|
+
const openedProjects = new Set(projects2.map((p) => realNative(p) ?? p));
|
|
2386
|
+
issues.push(...grokDiffers(projects2, machine, (root) => statuses.get(root)));
|
|
1858
2387
|
const nested = new Map(projects2.map((root) => [root, nestedOf(root)]));
|
|
1859
2388
|
for (const skill of machine.filter((m) => m.broken)) {
|
|
2389
|
+
const reaches = isDir(skill.path) ? "folder" : existsSync12(skill.path) ? "file" : "nothing";
|
|
2390
|
+
const remove2 = {
|
|
2391
|
+
label: "Remove the link",
|
|
2392
|
+
hint: "restorable with skilllib restore",
|
|
2393
|
+
run: () => {
|
|
2394
|
+
const r = unloadGlobal(skill.path);
|
|
2395
|
+
if (!r.ok) throw new Error(`${skill.name}: ${r.reason}`);
|
|
2396
|
+
return `Removed broken link ${skill.name}`;
|
|
2397
|
+
}
|
|
2398
|
+
};
|
|
1860
2399
|
issues.push({
|
|
1861
|
-
id: `broken:${skill.
|
|
2400
|
+
id: `broken:${skill.path}`,
|
|
1862
2401
|
severity: "problem",
|
|
1863
|
-
title: `${skill.name}: broken link in ${tildify(
|
|
1864
|
-
detail: "It points at a folder that no longer exists, so it loads nothing.",
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
run: () => {
|
|
1868
|
-
const r = unloadGlobal(skill.path);
|
|
1869
|
-
if (!r.ok) throw new Error(`${skill.name}: ${r.reason}`);
|
|
1870
|
-
return `Removed broken link ${skill.name}`;
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
2402
|
+
title: `${skill.name}: broken link in ${tildify(dirname6(skill.path))}`,
|
|
2403
|
+
detail: reaches === "nothing" ? "It points at a folder that no longer exists, so it loads nothing." : reaches === "file" ? "It points at a file, not a skill folder, so it loads nothing." : "It points at a folder with no SKILL.md: most agents load nothing from it (Cursor may find skills deeper inside).",
|
|
2404
|
+
at: rows(null, [skill.name]),
|
|
2405
|
+
...reaches === "folder" ? { choices: [remove2] } : { fix: remove2 }
|
|
1873
2406
|
});
|
|
1874
2407
|
}
|
|
1875
2408
|
const loaded = machine.filter((m) => !m.broken);
|
|
@@ -1884,7 +2417,8 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1884
2417
|
id: `dup:${name}`,
|
|
1885
2418
|
severity: "problem",
|
|
1886
2419
|
title: `${name}: loaded twice in ${harnesses.join(" and ")}`,
|
|
1887
|
-
detail: `${copies.map((c) => `${c.kind} (${c.origin})`).join(" and ")}. Your skill wins: turn the ${vendor.map((c) => c.kind).join(" and ")} copy off at its source${vendor.some((c) => c.kind === "claude.ai") ? " (claude.ai \u2192 Settings \u2192 Skills)" : ""}${vendor.some((c) => c.kind === "system") ? ` (whoever manages ${vendor.find((c) => c.kind === "system").origin})` : ""}
|
|
2420
|
+
detail: `${copies.map((c) => `${c.kind} (${c.origin})`).join(" and ")}. Your skill wins: turn the ${vendor.map((c) => c.kind).join(" and ")} copy off at its source${vendor.some((c) => c.kind === "claude.ai") ? " (claude.ai \u2192 Settings \u2192 Skills)" : ""}${vendor.some((c) => c.kind === "system") ? ` (whoever manages ${vendor.find((c) => c.kind === "system").origin})` : ""}.`,
|
|
2421
|
+
at: rows(null, [name])
|
|
1888
2422
|
});
|
|
1889
2423
|
}
|
|
1890
2424
|
const loadedHere = new Map(projects2.map((root) => [root, loadedIn(root)]));
|
|
@@ -1893,18 +2427,21 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1893
2427
|
];
|
|
1894
2428
|
const pluginAgentsIn = (plugin, root) => new Set(loadedHere.get(root).filter((m) => plugin.skills.includes(m.path)).flatMap((m) => m.harnesses));
|
|
1895
2429
|
const globalYours = new Set(loaded.filter((m) => m.movable).map((m) => m.name));
|
|
1896
|
-
const installedYours = /* @__PURE__ */ new Set([...globalYours, ...[...statuses.values()].flat().map((s) => s.name)]);
|
|
2430
|
+
const installedYours = /* @__PURE__ */ new Set([...globalYours, ...[...statuses.values()].flat().map((s) => s.name), ...knownProjects().flatMap((p) => Object.keys(pinsIn(p)))]);
|
|
1897
2431
|
const repoChoices = (plugin, root, names) => {
|
|
1898
2432
|
const where = basename6(root);
|
|
1899
2433
|
const copies = statuses.get(root).filter((s) => names.includes(s.name));
|
|
1900
|
-
const allManaged = names.every((n) => copies.some((c) => c.name === n && c
|
|
2434
|
+
const allManaged = names.every((n) => copies.some((c) => c.name === n && removesClean(root, c)));
|
|
2435
|
+
const committed = gitNote(root, copies.map((s) => s.path)) !== "";
|
|
2436
|
+
const pluginAgents = pluginAgentsIn(plugin, root);
|
|
2437
|
+
const leftOut = copies.flatMap((s) => s.visibility.filter((v) => v.paths > 0 && !pluginAgents.has(v.id)).map((v) => v.id));
|
|
1901
2438
|
return [
|
|
1902
2439
|
{ label: `Turn the plugin ${plugin.id} off in ${where} only`, hint: "other repos keep it; in .claude/settings.local.json, a local-only file", run: () => orThrow(turnOffIn(plugin, root)) },
|
|
1903
2440
|
// Only copies skilllib installed: Your skills keeps them. A repo's own skill is the team's, never removed from here.
|
|
1904
|
-
...names.length && allManaged ? [
|
|
2441
|
+
...names.length && allManaged && !committed && !leftOut.length ? [
|
|
1905
2442
|
{
|
|
1906
2443
|
label: `Remove your ${copies.map((s) => s.name).join(", ")} from ${where}`,
|
|
1907
|
-
hint:
|
|
2444
|
+
hint: `the plugin's copy stays; Your skills keeps yours${gitNote(root, [join13(root, MANIFEST_FILE)])}`,
|
|
1908
2445
|
run: () => {
|
|
1909
2446
|
for (const s of copies) {
|
|
1910
2447
|
const r = removeSkill(root, s.name);
|
|
@@ -1929,7 +2466,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1929
2466
|
const pluginName = plugin.id.split("@")[0];
|
|
1930
2467
|
const remove2 = {
|
|
1931
2468
|
label: `Remove the plugin ${plugin.id}`,
|
|
1932
|
-
hint: plugin.extras.length ? `also removes its ${plugin.extras.join(", ")}` : "restorable
|
|
2469
|
+
hint: plugin.extras.length ? `also removes its ${plugin.extras.join(", ")}` : "restorable with skilllib restore",
|
|
1933
2470
|
run: () => orThrow(removePlugin(plugin, "delete"))
|
|
1934
2471
|
};
|
|
1935
2472
|
const off = { label: `Turn the plugin ${plugin.id} off`, hint: "stays installed", run: () => orThrow(removePlugin(plugin, "off")) };
|
|
@@ -1945,19 +2482,36 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1945
2482
|
claudeBoth.length ? `Claude Code loads both (the plugin's as /${pluginName}:${claudeBoth[0]}). Your skill wins.` : "",
|
|
1946
2483
|
cursorWheres.length ? `${cursorWheres.join(", ")} ${cursorWheres.length === 1 ? "turns" : "turn"} it off for Claude Code, but Cursor ignores repo settings and lists both there: only turning the plugin off or removing it changes that.` : "",
|
|
1947
2484
|
notInstalled.length ? `Your ${notInstalled.join(", ")} ${notInstalled.length === 1 ? "is" : "are"} only in Your skills, not installed anywhere: once the plugin is gone, add ${notInstalled.length === 1 ? "it" : "them"} where you need ${notInstalled.length === 1 ? "it" : "them"}.` : "",
|
|
1948
|
-
others ? `
|
|
1949
|
-
plugin.extras.length ? `It also brings ${plugin.extras.join(", ")}: ${plugin.synced ? "turning it off pauses those" : "removing it drops those, turning it off pauses them"}.` : "",
|
|
2485
|
+
others ? `Turning it off or removing it copies its other ${others === 1 ? "skill" : `${others} skills`} into Your skills first, so nothing is lost.` : "",
|
|
2486
|
+
plugin.extras.length ? `It also brings ${plugin.extras.join(", ")}: ${plugin.synced || plugin.settingsOnly ? "turning it off pauses those" : "removing it drops those, turning it off pauses them"}.` : "",
|
|
1950
2487
|
plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : "",
|
|
2488
|
+
plugin.settingsOnly ? "It's only turned on in your settings, not installed, so it can only be turned off here." : "",
|
|
1951
2489
|
inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) => basename6(root)).join(", ")}.` : ""
|
|
1952
2490
|
].filter(Boolean).join(" "),
|
|
1953
|
-
choices: [...plugin.synced ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off], ...inRepos.flatMap(([root, d]) => repoChoices(plugin, root, d))]
|
|
2491
|
+
choices: [...plugin.synced || plugin.settingsOnly ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off], ...inRepos.flatMap(([root, d]) => repoChoices(plugin, root, d))],
|
|
2492
|
+
// Global's rows (yours and the plugin's), and the repos where both load.
|
|
2493
|
+
at: [...rows(null, dupes), ...[...inRepos, ...cursorRepos].flatMap(([root, d]) => rows(root, d))],
|
|
2494
|
+
plugin: plugin.id
|
|
1954
2495
|
});
|
|
1955
2496
|
}
|
|
2497
|
+
if (enabledHarnesses().includes("claude-code"))
|
|
2498
|
+
for (const p of installedPlugins().filter((x) => x.update && !x.synced && !x.settingsOnly)) {
|
|
2499
|
+
const update2 = { label: p.update === "newer" ? "Update it" : `Update to ${p.update}`, hint: `runs claude plugin update ${p.id}`, run: () => orThrow(updatePlugin(p)) };
|
|
2500
|
+
issues.push({
|
|
2501
|
+
id: `plugin-update:${installId(p)}`,
|
|
2502
|
+
severity: "suggestion",
|
|
2503
|
+
title: `${p.id}${installedIn(p)}: ${p.update === "newer" ? "its marketplace has a newer commit" : `update available (${p.version ?? "?"} \u2192 ${p.update})`}`,
|
|
2504
|
+
detail: "Updates can bring new commands, hooks or MCP servers. If the marketplace asks to run a command, skilllib stops and you update it yourself. Claude Code uses the new version after a restart.",
|
|
2505
|
+
choices: [update2],
|
|
2506
|
+
plugin: p.id,
|
|
2507
|
+
pluginAt: p.projectPath ?? null
|
|
2508
|
+
});
|
|
2509
|
+
}
|
|
1956
2510
|
const machinePluginSkills = new Set(machinePlugins.flatMap((p) => p.skills));
|
|
1957
2511
|
for (const root of projects2) {
|
|
1958
2512
|
const where = basename6(root);
|
|
1959
2513
|
const repoOnly = loadedHere.get(root).filter((m) => m.kind === "plugin" && !machinePluginSkills.has(m.path));
|
|
1960
|
-
for (const plugin of claudePlugins(repoOnly, root)) {
|
|
2514
|
+
for (const plugin of enabledHarnesses().includes("claude-code") ? claudePlugins(repoOnly, root) : []) {
|
|
1961
2515
|
const names = plugin.skills.map((p) => basename6(p));
|
|
1962
2516
|
const inRepo = namesIn2(root).filter((n) => names.includes(n));
|
|
1963
2517
|
const dupes = names.filter((n) => inRepo.includes(n) || globalYours.has(n));
|
|
@@ -1967,7 +2521,9 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1967
2521
|
severity: "problem",
|
|
1968
2522
|
title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}, on in ${where}`,
|
|
1969
2523
|
detail: `${where}'s Claude Code settings turn the plugin on, so Claude Code loads both there (the plugin's as /${plugin.id.split("@")[0]}:${dupes[0]}). Your skill wins.`,
|
|
1970
|
-
choices: repoChoices(plugin, root, inRepo)
|
|
2524
|
+
choices: repoChoices(plugin, root, inRepo),
|
|
2525
|
+
at: rows(root, dupes),
|
|
2526
|
+
plugin: plugin.id
|
|
1971
2527
|
});
|
|
1972
2528
|
}
|
|
1973
2529
|
}
|
|
@@ -1979,7 +2535,9 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1979
2535
|
id: `cursor-plugin:${plugin}`,
|
|
1980
2536
|
severity: "suggestion",
|
|
1981
2537
|
title: `${names.join(", ")}: also in the Cursor plugin ${plugin}`,
|
|
1982
|
-
detail: `Cursor lists both copies when that plugin is on. Your skill wins: turn the plugin off in Cursor (Settings \u2192 Plugins)
|
|
2538
|
+
detail: `Cursor lists both copies when that plugin is on. Your skill wins: turn the plugin off in Cursor (Settings \u2192 Plugins).`,
|
|
2539
|
+
at: [...rows(null, names.filter((n) => globalYours.has(n))), ...projects2.flatMap((root) => rows(root, names.filter((n) => statuses.get(root).some((s) => s.name === n))))],
|
|
2540
|
+
plugin
|
|
1983
2541
|
});
|
|
1984
2542
|
}
|
|
1985
2543
|
}
|
|
@@ -1989,7 +2547,8 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1989
2547
|
id: "tidy:global",
|
|
1990
2548
|
severity: "suggestion",
|
|
1991
2549
|
title: `${plural(global.plans.length, "global skill")} with duplicate copies`,
|
|
1992
|
-
detail: `${global.plans.map((p) => p.name).join(", ")}: each keeps one copy (in ~/.agents/skills when there's one) and the others become links. Restorable
|
|
2550
|
+
detail: `${global.plans.map((p) => p.name).join(", ")}: each keeps one copy (in ~/.agents/skills when there's one) and the others become links. Restorable with skilllib restore.`,
|
|
2551
|
+
at: rows(null, global.plans.map((p) => p.name)),
|
|
1993
2552
|
fix: {
|
|
1994
2553
|
label: "Keep one copy of each",
|
|
1995
2554
|
run: () => {
|
|
@@ -2002,13 +2561,14 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2002
2561
|
}
|
|
2003
2562
|
issues.push(...global.conflicts.map(conflictIssue));
|
|
2004
2563
|
const kept = keptGlobal();
|
|
2005
|
-
const unreviewed = [...new Set(loaded.filter((m) => m.movable && !kept.has(m.name)).map((m) => m.name))];
|
|
2564
|
+
const unreviewed = [...new Set(loaded.filter((m) => m.movable && m.harnesses.length && !kept.has(m.name)).map((m) => m.name))];
|
|
2006
2565
|
if (unreviewed.length) {
|
|
2007
2566
|
issues.push({
|
|
2008
2567
|
id: "review:global",
|
|
2009
2568
|
severity: "suggestion",
|
|
2010
2569
|
title: `${plural(unreviewed.length, "global skill")} of yours ${unreviewed.length === 1 ? "loads" : "load"} in every repo`,
|
|
2011
|
-
detail: `${unreviewed.join(", ")}. Move each to the repos that need it (skilllib global move <name> --to <repo>...), or mark the ones you want everywhere as global on purpose (skilllib global keep <name>)
|
|
2570
|
+
detail: `${unreviewed.join(", ")}. Move each to the repos that need it (skilllib global move <name> --to <repo>...), or mark the ones you want everywhere as global on purpose (skilllib global keep <name>).`,
|
|
2571
|
+
at: rows(null, unreviewed)
|
|
2012
2572
|
});
|
|
2013
2573
|
}
|
|
2014
2574
|
const enabled = enabledHarnesses();
|
|
@@ -2035,6 +2595,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2035
2595
|
severity: "suggestion",
|
|
2036
2596
|
title: `${plural(skills.length, "global skill")}${onPurpose ? " you keep global" : ""} not usable by ${agentNames(missing)}`,
|
|
2037
2597
|
detail: `${skills.map((m) => `${m.name} (only ${agentNames(m.harnesses)})`).join(", ")}. ${onPurpose ? "A link in each agent's global folder makes the one real copy reach every agent you use." : `Link ${skills.length === 1 ? "it" : "them"} for every agent, or move ${skills.length === 1 ? "it" : "them"} to the repos that need ${skills.length === 1 ? "it" : "them"} (skilllib global move), which works for every agent too.`}`,
|
|
2598
|
+
at: rows(null, [...new Set(skills.map((m) => m.name))]),
|
|
2038
2599
|
...onPurpose ? { fix: link2 } : { choices: [link2] }
|
|
2039
2600
|
});
|
|
2040
2601
|
}
|
|
@@ -2044,31 +2605,77 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2044
2605
|
for (const root of projects2) {
|
|
2045
2606
|
const where = basename6(root);
|
|
2046
2607
|
const status2 = statuses.get(root);
|
|
2047
|
-
|
|
2608
|
+
issues.push(...brokenLinksIn(root, openedProjects));
|
|
2609
|
+
const pins = readManifest(root).skills;
|
|
2610
|
+
const blockedFor = /* @__PURE__ */ new Map();
|
|
2611
|
+
const untrackable = (s) => {
|
|
2612
|
+
if (!blockedFor.has(s.name)) blockedFor.set(s.name, trackBlocker(root, s.name));
|
|
2613
|
+
return blockedFor.get(s.name);
|
|
2614
|
+
};
|
|
2615
|
+
const blockers = new Map(status2.filter((s) => s.managed).flatMap((s) => ((b) => b ? [[s.name, b]] : [])(syncBlocker(root, s.name, pins[s.name]))));
|
|
2616
|
+
for (const [name, blocker] of blockers)
|
|
2617
|
+
issues.push({
|
|
2618
|
+
id: `stuck:${root}:${name}`,
|
|
2619
|
+
severity: "problem",
|
|
2620
|
+
title: `${name} in ${where}: sync can't put it back as skilllib.json pins it`,
|
|
2621
|
+
// What to do depends on what's there: a folder that links out of the repo is a setup of yours, and a pin
|
|
2622
|
+
// your library has no version of can't come back by sync anyway.
|
|
2623
|
+
detail: `${blocker.said}, so sync can't put it back there.${blocker.outside ? " skilllib only installs inside the repo: point that folder back into the repo, or remove the skill here (skilllib remove)." : versionForHash(name, pins[name]?.hash ?? "") !== null ? " Move it out of the way, then run skilllib sync." : " Move it out of the way; your library has no copy of the pinned version either."}`,
|
|
2624
|
+
at: rows(root, [name])
|
|
2625
|
+
});
|
|
2626
|
+
const unstuck = status2.filter((s) => !blockers.has(s.name));
|
|
2627
|
+
const missing = unstuck.filter((s) => s.state === "folder missing" && s.latest !== null);
|
|
2628
|
+
const knownPin = (s) => versionForHash(s.name, pins[s.name]?.hash ?? "") !== null;
|
|
2629
|
+
const restorable = missing.filter(knownPin);
|
|
2630
|
+
for (const s of unstuck.filter((m) => (m.state === "folder missing" || m.state === "out of sync") && m.latest !== null && !knownPin(m)))
|
|
2631
|
+
issues.push({
|
|
2632
|
+
id: `pin:${root}:${s.name}`,
|
|
2633
|
+
severity: "problem",
|
|
2634
|
+
title: `${s.name}: the version ${where}'s skilllib.json pins isn't in your library`,
|
|
2635
|
+
detail: `${s.state === "folder missing" ? "The folder is missing, and no" : "The copy is another version, and no"} version in Your skills has the content it pins (pinned from another library?). ${untrackable(s) ? `${untrackable(s)}, so skilllib can't install it here.` : `Install your newest v${s.latest}; ${where}'s skilllib.json then pins it.`}`,
|
|
2636
|
+
at: rows(root, [s.name]),
|
|
2637
|
+
// It changes what the shared skilllib.json pins: a choice, never run by `doctor --fix`.
|
|
2638
|
+
...untrackable(s) ? {} : { choices: [{ label: `Install your newest (v${s.latest})`, hint: `${where}'s skilllib.json pins it`, run: () => `${s.name}: installed v${track(root, s.name).to}` }] }
|
|
2639
|
+
});
|
|
2048
2640
|
if (restorable.length > 0) {
|
|
2049
2641
|
issues.push({
|
|
2050
2642
|
id: `sync:${root}`,
|
|
2051
2643
|
severity: "problem",
|
|
2052
2644
|
title: `${where}: ${plural(restorable.length, "skill")} in skilllib.json not installed`,
|
|
2053
2645
|
detail: `${restorable.map((s) => s.name).join(", ")}: the folder is missing`,
|
|
2646
|
+
at: rows(root, restorable.map((s) => s.name)),
|
|
2054
2647
|
fix: {
|
|
2055
2648
|
label: "Sync the project",
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
const done = changes.filter((c) => c.action !== "skipped");
|
|
2059
|
-
const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
|
|
2060
|
-
if (!done.length) throw new Error(`${where}: nothing restored${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
|
|
2061
|
-
return `${where}: restored ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
|
|
2062
|
-
}
|
|
2649
|
+
// Only the missing folders: an out-of-sync copy (maybe committed) is its own issue's choice.
|
|
2650
|
+
run: () => summarize(where, "restored", syncProject(root, { names: restorable.map((s) => s.name) }))
|
|
2063
2651
|
}
|
|
2064
2652
|
});
|
|
2065
2653
|
}
|
|
2066
|
-
|
|
2654
|
+
const drifted = unstuck.filter((s) => s.state === "out of sync" && knownPin(s));
|
|
2655
|
+
if (drifted.length) {
|
|
2656
|
+
const seen = drifted.some((s) => ["committed", "changed"].includes(gitOf(root)?.of(relativeTo(root, s.path)) ?? ""));
|
|
2657
|
+
const yours = drifted.some((s) => untrackable(s));
|
|
2658
|
+
const install = {
|
|
2659
|
+
label: "Install the pinned versions",
|
|
2660
|
+
hint: seen ? "git will see the copies change" : "your library keeps every version",
|
|
2661
|
+
run: () => summarize(where, "synced", syncProject(root, { names: drifted.map((s) => s.name) }))
|
|
2662
|
+
};
|
|
2663
|
+
issues.push({
|
|
2664
|
+
id: `out-of-sync:${root}`,
|
|
2665
|
+
severity: "suggestion",
|
|
2666
|
+
title: `${where}: ${plural(drifted.length, "skill")} not at the version skilllib.json pins`,
|
|
2667
|
+
detail: `${drifted.map((s) => s.name).join(", ")}: each copy is another version from your library. sync installs the pinned one.${yours ? ` ${[...new Set(drifted.flatMap((s) => untrackable(s) ? [untrackable(s)] : []))].map((why) => `${drifted.filter((s) => untrackable(s) === why).map((s) => s.name).join(", ")}: ${why}`).join("; ")}, so skilllib can't pin the version that's there; ${drifted.filter((s) => untrackable(s)).length === 1 ? "it may be edits you copied into Your skills on purpose, and stays" : "they may be edits you copied into Your skills on purpose, and stay"} unless you choose this.` : ""}`,
|
|
2668
|
+
at: rows(root, drifted.map((s) => s.name)),
|
|
2669
|
+
...seen || yours ? { choices: [install] } : { fix: install }
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
for (const s of unstuck.filter((s2) => s2.state === "folder missing" && s2.latest === null))
|
|
2067
2673
|
issues.push({
|
|
2068
2674
|
id: `unavailable:${root}:${s.name}`,
|
|
2069
2675
|
severity: "problem",
|
|
2070
2676
|
title: `${s.name}: pinned in ${where}'s skilllib.json, but not in your library`,
|
|
2071
|
-
detail: `Its folder is missing, and your library (${tildify(libraryDir())}) has no copy to restore: import it, or set SKILLLIB_HOME to the library
|
|
2677
|
+
detail: `Its folder is missing, and your library (${tildify(libraryDir())}) has no copy to restore: import it, or set SKILLLIB_HOME to the skilllib folder whose library has it.`,
|
|
2678
|
+
at: rows(root, [s.name]),
|
|
2072
2679
|
// Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
|
|
2073
2680
|
choices: [
|
|
2074
2681
|
{
|
|
@@ -2082,27 +2689,26 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2082
2689
|
}
|
|
2083
2690
|
]
|
|
2084
2691
|
});
|
|
2085
|
-
const behind =
|
|
2692
|
+
const behind = unstuck.filter((s) => s.state === "update available" && !untrackable(s));
|
|
2086
2693
|
if (behind.length > 0)
|
|
2087
2694
|
issues.push({
|
|
2088
2695
|
id: `update:${root}`,
|
|
2089
2696
|
severity: "suggestion",
|
|
2090
2697
|
title: `${where}: ${plural(behind.length, "skill")} out of date`,
|
|
2091
2698
|
detail: behind.map((s) => `${s.name} (v${s.version} \u2192 v${s.latest})`).join(", "),
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
return `${where}: updated ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
|
|
2699
|
+
at: rows(root, behind.map((s) => s.name)),
|
|
2700
|
+
// It changes what the repo pins and the copies its agents load (often committed): a choice, never `doctor --fix`.
|
|
2701
|
+
choices: [
|
|
2702
|
+
{
|
|
2703
|
+
label: "Update to the newest versions",
|
|
2704
|
+
hint: ["committed", "changed"].includes(gitOf(root)?.of(MANIFEST_FILE) ?? "") ? "git will see the new copies and skilllib.json" : "replaces each copy with your library's newest",
|
|
2705
|
+
run: () => summarize(where, "updated", updateProject(root, behind.map((s) => s.name)))
|
|
2100
2706
|
}
|
|
2101
|
-
|
|
2707
|
+
]
|
|
2102
2708
|
});
|
|
2103
2709
|
const tidy2 = planProjectTidy(root);
|
|
2104
2710
|
if (tidy2.plans.length) {
|
|
2105
|
-
const git3 =
|
|
2711
|
+
const git3 = gitOf(root);
|
|
2106
2712
|
const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p, git3).map((s) => describeStep(s, root)));
|
|
2107
2713
|
const run = (mode) => () => {
|
|
2108
2714
|
const r = applyAll(tidy2.plans, mode);
|
|
@@ -2113,41 +2719,90 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2113
2719
|
id: `tidy:${root}`,
|
|
2114
2720
|
severity: "suggestion",
|
|
2115
2721
|
title: `${where}: ${plural(tidy2.plans.length, "skill")} with extra copies or links`,
|
|
2116
|
-
detail: tidy2.plans.map((p) => `${p.name}: ${p.steps.map((s) => describeStep(s, root)).join(", ")}`).join(" \xB7 ") + (visible2.length ? `. Git would see ${plural(visible2.length, "of these change")}: ${visible2.join(", ")}.` : ". Replaced copies are restorable
|
|
2722
|
+
detail: tidy2.plans.map((p) => `${p.name}: ${p.steps.map((s) => describeStep(s, root)).join(", ")}`).join(" \xB7 ") + (visible2.length ? `. Git would see ${plural(visible2.length, "of these change")}: ${visible2.join(", ")}.` : tidy2.plans.some((p) => p.steps.some((s) => s.kind === "replace" || s.kind === "remove")) ? ". Replaced copies are restorable with skilllib restore." : "."),
|
|
2723
|
+
at: rows(root, tidy2.plans.map((p) => p.name)),
|
|
2117
2724
|
...visible2.length ? {
|
|
2118
2725
|
choices: [
|
|
2119
|
-
|
|
2726
|
+
// "Keep git as it is" only when it leaves something to do.
|
|
2727
|
+
...visible2.length < tidy2.plans.reduce((n, p) => n + p.steps.length, 0) ? [{ label: "Tidy, but keep git as it is", hint: `skips the ${plural(visible2.length, "change")} git would see`, run: run("keep") }] : [],
|
|
2120
2728
|
{ label: "Tidy everything", hint: `${plural(visible2.length, "change")} will show in git status`, run: run("go") }
|
|
2121
2729
|
]
|
|
2122
2730
|
} : { fix: { label: "Tidy", run: run("go") } }
|
|
2123
2731
|
});
|
|
2124
2732
|
}
|
|
2125
2733
|
issues.push(...tidy2.conflicts.map(conflictIssue));
|
|
2126
|
-
|
|
2734
|
+
for (const s of tidy2.skipped.filter((x) => x.differs))
|
|
2735
|
+
issues.push({
|
|
2736
|
+
id: `linkdiffers:${root}:${s.name}`,
|
|
2737
|
+
severity: "problem",
|
|
2738
|
+
title: `${s.name} in ${where}: the copies differ`,
|
|
2739
|
+
detail: `${s.reason}, with other content than the copy beside it: your agents run different versions. Make them the same, or remove the link; skilllib doesn't change a link to another copy.`,
|
|
2740
|
+
at: rows(root, [s.name])
|
|
2741
|
+
});
|
|
2742
|
+
const loadsGlobally = (name, id) => {
|
|
2743
|
+
const here = status2.find((s) => s.name === name);
|
|
2744
|
+
return globalCopies.some((m) => m.name === name && m.harnesses.includes(id) && (!here || treeHash(m.path) === treeHash(here.path)));
|
|
2745
|
+
};
|
|
2746
|
+
const blindHere = status2.filter((s) => s.state !== "folder missing" && s.visibility.some((v) => v.paths === 0 && !loadsGlobally(s.name, v.id))).map((s) => ({ s, would: linkEverywhere(root, s.name, s.location, { dryRun: true }) }));
|
|
2747
|
+
const enabledIds = enabledHarnesses();
|
|
2748
|
+
const notFolders = /* @__PURE__ */ new Set();
|
|
2749
|
+
for (const dir of new Set(enabledIds.map((id) => harness(id).projectDirs[0]))) {
|
|
2750
|
+
const blocker = notWritable(root, dir);
|
|
2751
|
+
if (!blocker || notFolders.has(blocker)) continue;
|
|
2752
|
+
notFolders.add(blocker);
|
|
2753
|
+
const who = agentNames(enabledIds.filter((id) => notWritable(root, harness(id).projectDirs[0]) === blocker));
|
|
2754
|
+
const out2 = linksOut(root, blocker);
|
|
2755
|
+
issues.push({
|
|
2756
|
+
id: `notdir:${root}:${blocker}`,
|
|
2757
|
+
severity: out2 ? "suggestion" : "problem",
|
|
2758
|
+
title: out2 ? `${where}: ${blocker} links out of the repo` : `${where}: ${blocker} isn't a folder`,
|
|
2759
|
+
detail: out2 ? `${blockerSaid(root, blocker)}. ${who} ${who.includes(",") || who.includes(" and ") ? "load" : "loads"} what's there, but skilllib treats it as outside ${where}: it adds, links and removes nothing there.` : `${blockerSaid(root, blocker)}: ${who} can't load skills from ${where}, and skilllib adds none there. Move it out of the way, then run skilllib link.`,
|
|
2760
|
+
at: [{ root, skill: "" }]
|
|
2761
|
+
// the repo itself: its row in Places
|
|
2762
|
+
});
|
|
2763
|
+
}
|
|
2764
|
+
for (const { s, would } of blindHere) {
|
|
2765
|
+
const way = would.inTheWay.filter((p) => !notFolders.has(p));
|
|
2766
|
+
if (!way.length || blockers.has(s.name)) continue;
|
|
2767
|
+
issues.push({
|
|
2768
|
+
id: `intheway:${root}:${s.name}`,
|
|
2769
|
+
severity: "problem",
|
|
2770
|
+
title: `${s.name} in ${where}: ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))} can't use it here`,
|
|
2771
|
+
detail: `${inTheWaySaid(root, way)}. skilllib won't link past what's there: clear the way, then run skilllib link.`,
|
|
2772
|
+
at: rows(root, [s.name])
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2127
2775
|
for (const { s, would } of blindHere.filter(({ would: w }) => !w.created.length && !w.blocked.length && w.uncommitted.length))
|
|
2128
2776
|
issues.push({
|
|
2129
2777
|
id: `uncommitted:${root}:${s.name}`,
|
|
2130
2778
|
severity: "suggestion",
|
|
2131
2779
|
title: `${s.name} in ${where}: ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))} can't use it until it's committed`,
|
|
2132
|
-
detail: `Git tracks ${would.uncommitted.join(", ")}, but not ${s.location}/${s.name}: a link there would be broken for teammates. Commit it, then link it (\`skilllib link --allow-tracked
|
|
2780
|
+
detail: `Git tracks ${would.uncommitted.join(", ")}, but not ${s.location}/${s.name}: a link there would be broken for teammates. Commit it, then link it (\`skilllib link --allow-tracked\`).`,
|
|
2781
|
+
at: rows(root, [s.name])
|
|
2133
2782
|
});
|
|
2134
2783
|
const partial = blindHere.filter(({ would }) => would.created.length || would.blocked.length).map(({ s }) => s);
|
|
2135
2784
|
if (partial.length) {
|
|
2136
|
-
const
|
|
2785
|
+
const missing2 = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
|
|
2137
2786
|
const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
|
|
2787
|
+
const manifestShared = partial.some((s) => s.managed) && ["committed", "changed"].includes(gitOf(root)?.of(MANIFEST_FILE) ?? "");
|
|
2138
2788
|
const run = (allowTracked) => () => {
|
|
2139
2789
|
if (allowTracked) allowTrackedLinks(root);
|
|
2140
|
-
const r = linkAll(root, { allowTracked });
|
|
2790
|
+
const r = linkAll(root, { allowTracked, names: partial.map((s) => s.name) });
|
|
2141
2791
|
const notes = `${r.blocked.length ? `; skipped ${r.blocked.join(", ")} (git tracks ${r.blocked.length === 1 ? "it" : "them"})` : ""}${r.uncommitted.length ? `; ${r.uncommitted.join(", ")} not linked into folders git tracks (not committed, so teammates would get broken links)` : ""}`;
|
|
2142
|
-
if (!r.linked.length)
|
|
2143
|
-
|
|
2792
|
+
if (!r.linked.length) {
|
|
2793
|
+
const still = partial.filter((s) => visibilityOf(root, s.name).some((v) => v.paths === 0 && !loadsGlobally(s.name, v.id)));
|
|
2794
|
+
if (!still.length) return `${where}: already usable by ${agentNames(missing2)}`;
|
|
2795
|
+
throw new Error(`${where}: nothing linked${notes}`);
|
|
2796
|
+
}
|
|
2797
|
+
return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing2)}${notes}`;
|
|
2144
2798
|
};
|
|
2145
2799
|
issues.push({
|
|
2146
2800
|
id: `usable:${root}`,
|
|
2147
2801
|
severity: "suggestion",
|
|
2148
|
-
title: `${where}: ${plural(partial.length, "skill")} not usable by ${agentNames(
|
|
2149
|
-
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.` : ""}`,
|
|
2150
|
-
|
|
2802
|
+
title: `${where}: ${plural(partial.length, "skill")} not usable by ${agentNames(missing2)}`,
|
|
2803
|
+
detail: `${partial.map((s) => `${s.name} (not ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))})`).join(", ")}. Adding links makes the one real copy reach every agent you use; nothing is copied or moved.${tracked.length ? ` Git tracks ${tracked.join(", ")}, so links there show in git status.` : ""}${manifestShared ? " The new links are recorded in skilllib.json, which git tracks." : ""}`,
|
|
2804
|
+
at: rows(root, partial.map((s) => s.name)),
|
|
2805
|
+
...manifestShared && !tracked.length ? { choices: [{ label: "Add the links", hint: "git will see skilllib.json change", run: run(false) }] } : tracked.length ? {
|
|
2151
2806
|
choices: [
|
|
2152
2807
|
// Only when some link goes where git doesn't track: otherwise this one would link nothing.
|
|
2153
2808
|
...blindHere.some(({ would }) => would.created.length) ? [{ label: "Add links, but not where git tracks the folder", hint: `skips ${tracked.join(", ")}`, run: run(false) }] : [],
|
|
@@ -2157,22 +2812,24 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2157
2812
|
});
|
|
2158
2813
|
}
|
|
2159
2814
|
const differing = new Set(tidy2.conflicts.map((c) => c.name));
|
|
2160
|
-
const git2 =
|
|
2815
|
+
const git2 = gitOf(root);
|
|
2161
2816
|
const committed = (s) => ["committed", "changed"].includes(git2?.of(relativeTo(root, s.path)) ?? "");
|
|
2162
|
-
for (const s of
|
|
2817
|
+
for (const s of unstuck) {
|
|
2163
2818
|
if (isLink(s.path) || differing.has(s.name)) continue;
|
|
2164
|
-
if (s.state === "local only" && committed(s)) {
|
|
2819
|
+
if (s.state === "local only" && (committed(s) || untrackable(s))) {
|
|
2820
|
+
const why = committed(s) ? "it's the team's (committed)" : `${untrackable(s)}, so skilllib can't track it here`;
|
|
2165
2821
|
issues.push({
|
|
2166
2822
|
id: `local:${root}:${s.name}`,
|
|
2167
2823
|
severity: "suggestion",
|
|
2168
2824
|
title: `${s.name} in ${where} isn't in your library`,
|
|
2169
|
-
detail:
|
|
2825
|
+
detail: `Copy it into the library to reuse it in other projects. The repo's copy stays as it is: ${why}.`,
|
|
2826
|
+
at: rows(root, [s.name]),
|
|
2170
2827
|
choices: [
|
|
2171
2828
|
{
|
|
2172
2829
|
label: "Copy into the library",
|
|
2173
2830
|
hint: `${where}'s copy stays as it is`,
|
|
2174
2831
|
run: () => {
|
|
2175
|
-
|
|
2832
|
+
intoLibrary(s.path);
|
|
2176
2833
|
return `${s.name} copied into your library; ${where}'s copy stays as it is`;
|
|
2177
2834
|
}
|
|
2178
2835
|
}
|
|
@@ -2184,27 +2841,102 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2184
2841
|
severity: "suggestion",
|
|
2185
2842
|
title: `${s.name} in ${where} isn't in your library`,
|
|
2186
2843
|
detail: "Import it into the library to reuse it in other projects and keep it backed up. The repo then tracks it in skilllib.json.",
|
|
2844
|
+
at: rows(root, [s.name]),
|
|
2187
2845
|
choices: [
|
|
2188
2846
|
{
|
|
2189
2847
|
label: "Import into the library",
|
|
2190
|
-
|
|
2848
|
+
// Tracking it writes skilllib.json.
|
|
2849
|
+
hint: `copies it into Your skills${gitNote(root, [join13(root, MANIFEST_FILE)])}`,
|
|
2191
2850
|
run: () => {
|
|
2192
|
-
|
|
2193
|
-
|
|
2851
|
+
intoLibrary(s.path);
|
|
2852
|
+
track(root, s.name);
|
|
2194
2853
|
return `${s.name} imported and tracked in ${where}`;
|
|
2195
2854
|
}
|
|
2196
2855
|
}
|
|
2197
2856
|
]
|
|
2198
2857
|
});
|
|
2199
|
-
} else if (s.state === "untracked copy of library skill") {
|
|
2858
|
+
} else if (s.state === "untracked copy of library skill" && !committed(s) && !untrackable(s)) {
|
|
2200
2859
|
issues.push({
|
|
2201
2860
|
id: `adopt:${root}:${s.name}`,
|
|
2202
2861
|
severity: "suggestion",
|
|
2203
2862
|
title: `${s.name} in ${where} isn't tracked`,
|
|
2204
2863
|
detail: "It's identical to the library copy; track it (in skilllib.json) so future library changes reach it.",
|
|
2205
|
-
|
|
2864
|
+
at: rows(root, [s.name]),
|
|
2865
|
+
choices: [{ label: "Track it", hint: `adds it to ${where}'s skilllib.json`, run: () => `${track(root, s.name).name} tracked in ${where}` }]
|
|
2866
|
+
});
|
|
2867
|
+
} else if (s.state === "untracked, differs from library" || s.state === "repo skill, differs from library") {
|
|
2868
|
+
const teams = s.state.startsWith("repo skill") || committed(s) || !!untrackable(s);
|
|
2869
|
+
const whose = s.state.startsWith("repo skill") || committed(s) ? "It's the team's copy (committed, or in the repo's own .agents/skills)" : `${untrackable(s)}, so skilllib can't track it here`;
|
|
2870
|
+
issues.push({
|
|
2871
|
+
id: `differs:${root}:${s.name}`,
|
|
2872
|
+
severity: "suggestion",
|
|
2873
|
+
title: `${s.name} in ${where} differs from your library`,
|
|
2874
|
+
detail: teams ? `${whose}, and no version in Your skills matches it. Update your library from it if it's the one to keep; ${where}'s copy stays as it is.` : `No version in Your skills matches it. Update your library from it (${where} then tracks it), or replace it with your library's.`,
|
|
2875
|
+
at: rows(root, [s.name]),
|
|
2876
|
+
choices: [
|
|
2877
|
+
{
|
|
2878
|
+
label: "Update your library from it",
|
|
2879
|
+
hint: teams ? `a new version in Your skills; ${where}'s copy stays as it is` : `a new version in Your skills; ${where} then tracks it`,
|
|
2880
|
+
run: () => {
|
|
2881
|
+
intoLibrary(s.path, true);
|
|
2882
|
+
if (teams) return `${s.name}: your library has ${where}'s copy; ${where}'s copy stays as it is`;
|
|
2883
|
+
const c = track(root, s.name);
|
|
2884
|
+
return `${s.name}: your library has ${where}'s copy as v${c.to}; ${where} tracks it`;
|
|
2885
|
+
}
|
|
2886
|
+
},
|
|
2887
|
+
...teams ? [] : [
|
|
2888
|
+
{
|
|
2889
|
+
label: "Replace it with your library's",
|
|
2890
|
+
hint: "this copy is restorable with skilllib restore",
|
|
2891
|
+
run: () => {
|
|
2892
|
+
const c = addSkill(root, s.name, { force: true });
|
|
2893
|
+
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
2894
|
+
return `${s.name} in ${where} is your library's v${c.to} now; the old copy is restorable with skilllib restore`;
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
]
|
|
2898
|
+
]
|
|
2899
|
+
});
|
|
2900
|
+
} else if (s.state === "from npx skills" && !((committed(s) || untrackable(s)) && versionForHash(s.name, treeHash(s.path) ?? "") !== null)) {
|
|
2901
|
+
const teams = committed(s) || !!untrackable(s);
|
|
2902
|
+
issues.push({
|
|
2903
|
+
id: `npx:${root}:${s.name}`,
|
|
2904
|
+
severity: "suggestion",
|
|
2905
|
+
title: `${s.name} in ${where} came from npx skills, not your library`,
|
|
2906
|
+
detail: `Installed with \`npx skills add\`${s.source ? ` (${s.source})` : ""}. Import it into your library to reuse it in other projects and keep it backed up${teams ? `; ${where}'s copy stays as it is (${committed(s) ? "committed" : `${untrackable(s)}, so skilllib can't track it here`})` : `; ${where} then tracks it in skilllib.json`}.`,
|
|
2907
|
+
at: rows(root, [s.name]),
|
|
2908
|
+
choices: [
|
|
2909
|
+
{
|
|
2910
|
+
label: "Import it into your library",
|
|
2911
|
+
hint: teams ? `${where}'s copy stays as it is` : `${where} then tracks it`,
|
|
2912
|
+
run: () => {
|
|
2913
|
+
intoLibrary(s.path);
|
|
2914
|
+
if (teams) return `${s.name} copied into your library; ${where}'s copy stays as it is`;
|
|
2915
|
+
track(root, s.name);
|
|
2916
|
+
return `${s.name} imported and tracked in ${where}`;
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
]
|
|
2206
2920
|
});
|
|
2207
2921
|
}
|
|
2922
|
+
if (s.state === "not in library")
|
|
2923
|
+
issues.push({
|
|
2924
|
+
id: `not-in-lib:${root}:${s.name}`,
|
|
2925
|
+
severity: "suggestion",
|
|
2926
|
+
title: `${s.name} in ${where} is tracked, but no longer in your library`,
|
|
2927
|
+
detail: `${where}'s skilllib.json pins it, but your library has no copy: if the folder goes, sync can't restore it, and your other repos can't add it. Copy ${where}'s copy back into the library.`,
|
|
2928
|
+
at: rows(root, [s.name]),
|
|
2929
|
+
choices: [
|
|
2930
|
+
{
|
|
2931
|
+
label: "Copy it back into your library",
|
|
2932
|
+
hint: `${where}'s copy stays as it is`,
|
|
2933
|
+
run: () => {
|
|
2934
|
+
intoLibrary(s.path);
|
|
2935
|
+
return `${s.name} copied back into your library`;
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
]
|
|
2939
|
+
});
|
|
2208
2940
|
if (s.state === "edited locally" || s.state === "edited locally, update available") {
|
|
2209
2941
|
const newer2 = s.state !== "edited locally";
|
|
2210
2942
|
const pinned = s.version ? `v${s.version}` : "the library version";
|
|
@@ -2212,11 +2944,21 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2212
2944
|
id: `edited:${root}:${s.name}`,
|
|
2213
2945
|
severity: "suggestion",
|
|
2214
2946
|
title: `${s.name} in ${where} has local edits`,
|
|
2215
|
-
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
|
|
2216
|
-
|
|
2947
|
+
detail: `It no longer matches ${pinned} from Your skills${newer2 ? `, and v${s.latest} is out` : ""}. ${untrackable(s) ? `${untrackable(s)}, so skilllib can't re-pin or discard them here; copy them into Your skills to keep them for your other repos.` : "Save the edits as a new version so your other repos can get them, or discard them. Keeping them is fine too: skilllib never overwrites edits."}`,
|
|
2948
|
+
at: rows(root, [s.name]),
|
|
2949
|
+
choices: untrackable(s) ? [
|
|
2950
|
+
{
|
|
2951
|
+
label: "Copy the edits into Your skills",
|
|
2952
|
+
hint: `${newer2 ? `becomes v${s.latest + 1}; v${s.latest}'s changes aren't merged in` : "a new version there"}; ${where}'s copy and pin stay as they are (skilllib sync there would put the pinned version back)`,
|
|
2953
|
+
run: () => {
|
|
2954
|
+
intoLibrary(s.path, true);
|
|
2955
|
+
return `${s.name}: the edits are in Your skills as v${latestVersion(s.name)?.version}; ${where} stays as it is`;
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
] : [
|
|
2217
2959
|
{
|
|
2218
2960
|
label: "Save as a new version in Your skills",
|
|
2219
|
-
hint: newer2 ? `becomes v${s.latest + 1}; v${s.latest}'s changes aren't merged in` : `becomes v${(s.latest ?? 0) + 1}; ${where} uses it`,
|
|
2961
|
+
hint: `${newer2 ? `becomes v${s.latest + 1}; v${s.latest}'s changes aren't merged in` : `becomes v${(s.latest ?? 0) + 1}; ${where} uses it`}${gitNote(root, [join13(root, MANIFEST_FILE)])}`,
|
|
2220
2962
|
run: () => {
|
|
2221
2963
|
importSkill(s.path, { force: true });
|
|
2222
2964
|
const c = addSkill(root, s.name);
|
|
@@ -2226,7 +2968,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2226
2968
|
},
|
|
2227
2969
|
{
|
|
2228
2970
|
label: "Discard the edits",
|
|
2229
|
-
hint: `back to ${pinned}; the edited copy is restorable
|
|
2971
|
+
hint: `back to ${pinned}; the edited copy is restorable with skilllib restore${gitNote(root, [s.path, join13(root, MANIFEST_FILE)])}`,
|
|
2230
2972
|
run: () => {
|
|
2231
2973
|
const c = discardEdits(root, s.name);
|
|
2232
2974
|
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
@@ -2242,48 +2984,77 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2242
2984
|
for (const root of projects2) {
|
|
2243
2985
|
for (const n of nested.get(root)) {
|
|
2244
2986
|
const copies = alsoGlobal.get(n.name) ?? [];
|
|
2245
|
-
if (alsoLoadedGlobally(n) && !copies.some((c) => c.root === root)) alsoGlobal.set(n.name, [...copies, { root, managed: false }]);
|
|
2987
|
+
if (alsoLoadedGlobally(n) && !copies.some((c) => c.root === root)) alsoGlobal.set(n.name, [...copies, { root, managed: false, nested: true }]);
|
|
2246
2988
|
}
|
|
2247
2989
|
}
|
|
2990
|
+
const everyRepo = [...projects2, ...knownProjects()];
|
|
2991
|
+
const nestedMemo = /* @__PURE__ */ new Map();
|
|
2992
|
+
const subsMemo = /* @__PURE__ */ new Map();
|
|
2993
|
+
const subsOf = (root) => subsMemo.get(root) ?? (subsMemo.set(root, packageDirs(root)), subsMemo.get(root));
|
|
2994
|
+
const entriesOf2 = (root) => nestedMemo.get(root) ?? (nestedMemo.set(root, nestedEntries(root, { subs: subsOf(root) })), nestedMemo.get(root));
|
|
2248
2995
|
for (const [name, copies] of alsoGlobal) {
|
|
2249
2996
|
const wheres = copies.map((c) => basename6(c.root)).join(", ");
|
|
2250
|
-
const removable = copies.filter((c) => c.managed).map((c) => c.root);
|
|
2997
|
+
const removable = copies.filter((c) => c.managed && statuses.get(c.root)?.some((s) => s.name === name && removesClean(c.root, s))).map((c) => c.root);
|
|
2251
2998
|
const repoOwn = copies.filter((c) => !c.managed).map((c) => basename6(c.root));
|
|
2999
|
+
const at = [...copies.flatMap((c) => rows(c.root, [name])), ...rows(null, [name])];
|
|
2252
3000
|
if (kept.has(name)) {
|
|
2253
|
-
const
|
|
3001
|
+
const globalLoaders = new Set(globalCopies.filter((m) => m.name === name).flatMap((m) => m.harnesses));
|
|
3002
|
+
const onlyHere = (root) => visibilityOf(root, name).filter((v) => v.paths > 0 && !globalLoaders.has(v.id)).map((v) => harness(v.id).name);
|
|
3003
|
+
const needed = removable.filter((root) => onlyHere(root).length);
|
|
3004
|
+
const extra = removable.filter((root) => !onlyHere(root).length);
|
|
3005
|
+
const removeWheres = extra.map((r) => basename6(r)).join(", ");
|
|
2254
3006
|
issues.push({
|
|
2255
3007
|
id: `twice:${name}`,
|
|
2256
3008
|
severity: "suggestion",
|
|
2257
3009
|
title: `${name}: in ${wheres}, and you keep it global`,
|
|
2258
|
-
detail:
|
|
2259
|
-
|
|
3010
|
+
detail: [
|
|
3011
|
+
extra.length ? `The global copy already loads in ${removeWheres} for every agent that reads the project copy, so those are extra.` : "",
|
|
3012
|
+
needed.length ? `In ${needed.map((r) => `${basename6(r)} (${onlyHere(r).join(", ")})`).join(", ")}, some agents read only the repo's copy: keep it there, or link the global one for them.` : "",
|
|
3013
|
+
repoOwn.length ? `${repoOwn.join(", ")} ${repoOwn.length === 1 ? "has its own copy" : "have their own copies"}: remove ${repoOwn.length === 1 ? "it" : "them"} in the repo, or stop keeping ${name} global.` : ""
|
|
3014
|
+
].filter(Boolean).join(" "),
|
|
3015
|
+
at,
|
|
3016
|
+
choices: extra.length ? [
|
|
2260
3017
|
{
|
|
2261
3018
|
label: `Remove the copies in ${removeWheres}`,
|
|
2262
|
-
|
|
3019
|
+
// Removing a managed copy edits its skilllib.json too.
|
|
3020
|
+
hint: `the global copy stays${extra.some((r) => gitNote(r, [...statuses.get(r).filter((s) => s.name === name).map((s) => s.path), join13(r, MANIFEST_FILE)])) ? "; git will see it" : ""}`,
|
|
2263
3021
|
run: () => {
|
|
2264
|
-
const
|
|
2265
|
-
|
|
3022
|
+
const results = extra.map((root) => [root, removeSkill(root, name)]);
|
|
3023
|
+
const removed = results.filter(([, c]) => c.action === "removed").map(([r]) => basename6(r));
|
|
3024
|
+
const kept2 = results.filter(([, c]) => c.action !== "removed").map(([r, c]) => `${basename6(r)} (${c.reason})`);
|
|
3025
|
+
if (kept2.length) throw new Error(`${name}: ${removed.length ? `removed from ${removed.join(", ")}; ` : ""}kept in ${kept2.join(", ")}`);
|
|
3026
|
+
return `${name}: removed from ${removed.join(", ")}; the global copy stays`;
|
|
2266
3027
|
}
|
|
2267
3028
|
}
|
|
2268
3029
|
] : void 0
|
|
2269
3030
|
});
|
|
2270
3031
|
continue;
|
|
2271
3032
|
}
|
|
2272
|
-
const movable = machine.find((m) => m.name === name && m.movable);
|
|
3033
|
+
const movable = machine.find((m) => m.name === name && m.movable && !m.broken);
|
|
3034
|
+
const doomed = machine.filter((x) => x.name === name && x.movable);
|
|
3035
|
+
const differ = copiesDiffer(doomed.map((m) => m.path));
|
|
3036
|
+
const libraryDiffers = !!movable && libraryNames.has(name) && versionForHash(name, treeHash(movable.path) ?? "") === null;
|
|
3037
|
+
const blind = movable && !differ && !libraryDiffers ? blindWithoutGlobal(name, doomed, everyRepo, entriesOf2, subsOf) : [];
|
|
2273
3038
|
issues.push({
|
|
3039
|
+
at,
|
|
2274
3040
|
id: `twice:${name}`,
|
|
2275
3041
|
severity: "suggestion",
|
|
2276
3042
|
title: `${name}: in ${wheres} and also loaded globally`,
|
|
2277
|
-
detail: `${repoOwn.length === copies.length ? `Agents there load both the repo's copy and the global one.` : "The project copies are enough; the global one loads it in every other project too."} ${movable ?
|
|
2278
|
-
choices: movable ? [
|
|
3043
|
+
detail: `${repoOwn.length === copies.length ? `Agents there load both the repo's copy and the global one.` : "The project copies are enough; the global one loads it in every other project too."} ${!movable ? `The global copy comes from ${machine.find((m) => m.name === name && !m.movable)?.origin ?? "a vendor"}: turn it off there.` : differ ? `Your global copies of ${name} differ: keep one first (skilllib tidy --global), then stop loading it globally.` : libraryDiffers ? `The global copy isn't a version in Your skills (agents loading it globally run different content than the repo's): update your library from it first (skilllib import ${tildify(movable.path)} --force), or keep it global (skilllib global keep).` : blind.length ? `${blindSaid(name, blind)}.` : "Stop loading it globally, or keep it global on purpose (skilllib global keep)."}`,
|
|
3044
|
+
choices: movable && !differ && !libraryDiffers && !blind.length ? [
|
|
2279
3045
|
{
|
|
2280
3046
|
label: "Stop loading it globally",
|
|
2281
|
-
hint: "restorable
|
|
3047
|
+
hint: "restorable with skilllib restore",
|
|
2282
3048
|
run: () => {
|
|
3049
|
+
const now = blindWithoutGlobal(name, doomed, everyRepo);
|
|
3050
|
+
if (now.length) throw new Error(`${name}: ${blindSaid(name, now)}`);
|
|
2283
3051
|
if (!libraryNames.has(name)) importSkill(movable.path);
|
|
2284
|
-
const
|
|
2285
|
-
|
|
2286
|
-
|
|
3052
|
+
const left = strandedLinks(name, doomed, everyRepo);
|
|
3053
|
+
for (const m of machine.filter((x) => x.name === name && x.movable)) {
|
|
3054
|
+
const r = unloadGlobal(m.path, m.links);
|
|
3055
|
+
if (!r.ok) throw new Error(`${name}: ${r.reason}`);
|
|
3056
|
+
}
|
|
3057
|
+
return `${name} no longer loads globally${left.length ? `; ${left.join(", ")} now point${left.length === 1 ? "s" : ""} at nothing: remove ${left.length === 1 ? "it" : "them"}` : ""}`;
|
|
2287
3058
|
}
|
|
2288
3059
|
}
|
|
2289
3060
|
] : void 0
|
|
@@ -2305,6 +3076,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2305
3076
|
severity: "suggestion",
|
|
2306
3077
|
title: agentSkill2 === "missing" ? "Your agents don't know about skilllib" : "The skilllib skill for your agents is out of date",
|
|
2307
3078
|
detail: "The skilllib skill lets you ask your agents which skills they can use here, where each comes from, and which of your skills a repo should add.",
|
|
3079
|
+
at: rows(null, ["skilllib"]),
|
|
2308
3080
|
...agentSkill2 === "missing" ? { choices: [install] } : { fix: install }
|
|
2309
3081
|
});
|
|
2310
3082
|
}
|
|
@@ -2319,6 +3091,7 @@ function addedAt(path) {
|
|
|
2319
3091
|
}
|
|
2320
3092
|
}
|
|
2321
3093
|
function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectStatus) {
|
|
3094
|
+
gitMemo = /* @__PURE__ */ new Map();
|
|
2322
3095
|
if (!enabledHarnesses().includes("claude-code")) return [];
|
|
2323
3096
|
const issues = [];
|
|
2324
3097
|
const since = Date.now() - uses.days * 24 * 60 * 60 * 1e3;
|
|
@@ -2326,26 +3099,45 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
|
|
|
2326
3099
|
const where = basename6(root);
|
|
2327
3100
|
const unused = statusOf(root).filter((s) => s.state !== "folder missing" && uses.inProject(root, s.name) === 0 && addedAt(s.path) <= since);
|
|
2328
3101
|
if (!unused.length) continue;
|
|
2329
|
-
const removable = unused.filter((s) => s
|
|
3102
|
+
const removable = unused.filter((s) => removesClean(root, s));
|
|
2330
3103
|
issues.push({
|
|
2331
3104
|
id: `unused:${root}`,
|
|
2332
3105
|
severity: "suggestion",
|
|
2333
|
-
title: `${where}: ${plural(unused.length, "skill")} unused in ${uses.days}
|
|
2334
|
-
detail: `${unused.map((s) => s.name).join(", ")}: no Claude Code session in ${where} used ${unused.length === 1 ? "it" : "them"}. Each one's name and description still takes context in every session there.${unused.
|
|
3106
|
+
title: `${where}: ${plural(unused.length, "skill")} unused in ${plural(uses.days, "day")}`,
|
|
3107
|
+
detail: `${unused.map((s) => s.name).join(", ")}: no Claude Code session in ${where} used ${unused.length === 1 ? "it" : "them"}. Each one's name and description still takes context in every session there.${unused.some((s) => !s.managed) ? " The repo's own skills are the team's: remove those in the repo if nobody needs them." : ""}${unused.some((s) => s.managed && !removable.includes(s)) ? ` Not offered: ${unused.filter((s) => s.managed && !removable.includes(s)).map((s) => `${s.name} (${s.state.startsWith("edited") ? "edited" : s.state === "not in library" ? "not in your library" : "something is in sync's way"})`).join(", ")}; their own issues say what to do first.` : ""}`,
|
|
3108
|
+
at: rows(root, unused.map((s) => s.name)),
|
|
2335
3109
|
choices: removable.length ? [
|
|
2336
3110
|
{
|
|
2337
3111
|
label: `Remove ${removable.length === unused.length ? "them" : `the ${plural(removable.length, "skill")} skilllib installed`} from ${where}`,
|
|
2338
|
-
hint:
|
|
3112
|
+
hint: `Your skills keeps them; edited ones stay${gitNote(root, [...removable.map((s) => s.path), join13(root, MANIFEST_FILE)])}`,
|
|
2339
3113
|
run: () => {
|
|
2340
3114
|
const changes = removable.map((s) => removeSkill(root, s.name));
|
|
2341
3115
|
const removed = changes.filter((c) => c.action === "removed").map((c) => c.name);
|
|
2342
3116
|
const kept = changes.filter((c) => c.action !== "removed").map((c) => c.name);
|
|
2343
|
-
|
|
3117
|
+
if (kept.length) throw new Error(`${where}: ${removed.length ? `removed ${removed.join(", ")}; ` : "removed nothing; "}kept ${kept.join(", ")} (${changes.find((c) => c.action !== "removed")?.reason ?? "skipped"})`);
|
|
3118
|
+
return `${where}: removed ${removed.join(", ")}`;
|
|
2344
3119
|
}
|
|
2345
3120
|
}
|
|
2346
3121
|
] : void 0
|
|
2347
3122
|
});
|
|
2348
3123
|
}
|
|
3124
|
+
if (uses.asUsed)
|
|
3125
|
+
for (const p of installedPlugins().filter((x) => x.agent === "claude-code" && x.on && x.skills.length)) {
|
|
3126
|
+
const name = p.id.split("@")[0];
|
|
3127
|
+
if (p.skills.some((path) => uses.asUsed(`${name}:${basename6(path)}`) > 0)) continue;
|
|
3128
|
+
issues.push({
|
|
3129
|
+
id: `plugin-unused:${installId(p)}`,
|
|
3130
|
+
severity: "suggestion",
|
|
3131
|
+
title: `${p.id}${installedIn(p)}: its skills weren't used in ${plural(uses.days, "day")}`,
|
|
3132
|
+
detail: `No Claude Code session used ${p.skills.map((path) => `/${name}:${basename6(path)}`).join(", ")}. Each one's name and description still takes context in every session.${p.extras.length ? ` It also brings ${p.extras.join(", ")}: turning it off pauses those too.` : ""}`,
|
|
3133
|
+
choices: [
|
|
3134
|
+
{ label: `Turn the plugin ${p.id} off`, hint: p.scope === "project" ? `stays installed; edits ${basename6(p.projectPath ?? "the repo")}'s committed .claude/settings.json` : "stays installed", run: () => orThrow(setPluginOn(p, false)) },
|
|
3135
|
+
...p.synced || p.settingsOnly ? [] : [{ label: `Uninstall the plugin ${p.id}`, hint: `its data stays; restorable with skilllib restore${p.scope === "project" ? `; edits ${basename6(p.projectPath ?? "the repo")}'s committed .claude/settings.json` : ""}`, run: () => orThrow(uninstallPlugin(p)) }]
|
|
3136
|
+
],
|
|
3137
|
+
plugin: p.id,
|
|
3138
|
+
pluginAt: p.projectPath ?? null
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
2349
3141
|
const installed = /* @__PURE__ */ new Set([...projects2.flatMap((root) => statusOf(root).map((s) => s.name)), ...machine.filter((m) => !m.broken).map((m) => m.name)]);
|
|
2350
3142
|
const idle = [...libraryNames].filter((name) => {
|
|
2351
3143
|
const first = versionHistory(name)[0];
|
|
@@ -2355,15 +3147,18 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
|
|
|
2355
3147
|
issues.push({
|
|
2356
3148
|
id: "unused:library",
|
|
2357
3149
|
severity: "suggestion",
|
|
2358
|
-
title: `${plural(idle.length, "skill")} in Your skills: in no repo, unused in ${uses.days}
|
|
3150
|
+
title: `${plural(idle.length, "skill")} in Your skills: in no repo, unused in ${plural(uses.days, "day")}`,
|
|
2359
3151
|
detail: `${idle.sort().join(", ")}. They load nowhere, so they take no context: add the ones you want to a repo, or delete the ones you've outgrown.`,
|
|
3152
|
+
// Your skills' rows (none loads globally, so none is in Global).
|
|
3153
|
+
at: rows(null, idle),
|
|
2360
3154
|
choices: [
|
|
2361
3155
|
{
|
|
2362
3156
|
label: `Delete ${idle.length === 1 ? "it" : `all ${idle.length}`} from Your skills`,
|
|
2363
|
-
hint: "restorable
|
|
3157
|
+
hint: "restorable with skilllib restore",
|
|
2364
3158
|
run: () => {
|
|
2365
3159
|
const done = idle.map((name) => [name, deleteLibrarySkill(name)]);
|
|
2366
3160
|
const failed = done.filter(([, r]) => !r.ok).map(([name]) => name);
|
|
3161
|
+
if (failed.length === done.length) throw new Error(`Deleted nothing; kept ${failed.join(", ")}`);
|
|
2367
3162
|
return `Deleted ${plural(done.length - failed.length, "skill")} from Your skills${failed.length ? `; kept ${failed.join(", ")}` : ""}`;
|
|
2368
3163
|
}
|
|
2369
3164
|
}
|
|
@@ -2385,13 +3180,12 @@ function groupBy(items) {
|
|
|
2385
3180
|
}
|
|
2386
3181
|
var sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
|
|
2387
3182
|
function globalFrom(kind, origin, path) {
|
|
2388
|
-
if (kind === "global" || kind === "skilllib") return tildify(path.replace(/[\\/][^\\/]+$/, ""))
|
|
3183
|
+
if (kind === "global" || kind === "skilllib") return tildify(path.replace(/[\\/][^\\/]+$/, ""));
|
|
2389
3184
|
return origin;
|
|
2390
3185
|
}
|
|
2391
|
-
var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "target", "vendor", "coverage"]);
|
|
2392
3186
|
function topLevel(root) {
|
|
2393
3187
|
try {
|
|
2394
|
-
return
|
|
3188
|
+
return readdirSync8(root, { withFileTypes: true }).filter((d) => !d.name.startsWith(".") && !BUILD_DIRS.has(d.name)).map((d) => d.isDirectory() ? `${d.name}/` : d.name).sort().slice(0, 60);
|
|
2395
3189
|
} catch {
|
|
2396
3190
|
return [];
|
|
2397
3191
|
}
|
|
@@ -2407,13 +3201,13 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2407
3201
|
const group = (list2) => groupBy(
|
|
2408
3202
|
list2.map((s) => {
|
|
2409
3203
|
const loadedBy = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
|
|
2410
|
-
const behind = s.
|
|
3204
|
+
const behind = s.state.endsWith("update available");
|
|
2411
3205
|
const gitState = git2?.of(relativeTo(root, s.path));
|
|
2412
3206
|
const repo = !s.managed && (s.state.startsWith("repo skill") || gitState === "committed" || gitState === "changed");
|
|
2413
3207
|
const usual = s.state === "ok" || s.state === "local only" || repo && s.state !== "from npx skills";
|
|
2414
3208
|
const attrs = {
|
|
2415
3209
|
source: s.managed ? "library" : repo ? "repo" : "local",
|
|
2416
|
-
dir: s.location !==
|
|
3210
|
+
dir: s.location !== PROJECT_SKILLS_DIR ? s.location : void 0,
|
|
2417
3211
|
version: s.version ?? void 0,
|
|
2418
3212
|
state: usual ? void 0 : s.state,
|
|
2419
3213
|
latest: behind ? s.latest : void 0,
|
|
@@ -2423,7 +3217,14 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2423
3217
|
return [JSON.stringify(attrs), s.name, () => ({ ...attrs, skills: [] })];
|
|
2424
3218
|
})
|
|
2425
3219
|
);
|
|
2426
|
-
const
|
|
3220
|
+
const own = (() => {
|
|
3221
|
+
try {
|
|
3222
|
+
return inRepo ? projectStatus(root) : [];
|
|
3223
|
+
} catch {
|
|
3224
|
+
return [];
|
|
3225
|
+
}
|
|
3226
|
+
})();
|
|
3227
|
+
const skills = group(own);
|
|
2427
3228
|
const nested = group(nestedHere);
|
|
2428
3229
|
const machine = machineSkills(agents);
|
|
2429
3230
|
const loaded = inRepo ? skillsLoadedIn(root, machine, agents) : machine;
|
|
@@ -2437,7 +3238,7 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2437
3238
|
for (const i of statusIssues(root, machine)) {
|
|
2438
3239
|
const choices = i.choices?.map((c) => c.label);
|
|
2439
3240
|
const key = `${i.detail}\0${i.fix?.label ?? ""}\0${choices?.join() ?? ""}`;
|
|
2440
|
-
const issue = issues.get(key) ?? { problems: [], ids: [], detail: i
|
|
3241
|
+
const issue = issues.get(key) ?? { problems: [], ids: [], detail: detailWithGit(i), fix: i.fix?.label, choices };
|
|
2441
3242
|
issue.problems.push(i.title);
|
|
2442
3243
|
issue.ids.push(i.id);
|
|
2443
3244
|
issues.set(key, issue);
|
|
@@ -2445,7 +3246,8 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2445
3246
|
const usesHere = Object.fromEntries([...uses].filter(([, n]) => n > 0));
|
|
2446
3247
|
return {
|
|
2447
3248
|
project: tildify(root),
|
|
2448
|
-
files
|
|
3249
|
+
// Outside a repo there are no project files to read (and the folder may be your home).
|
|
3250
|
+
files: inRepo ? topLevel(root) : [],
|
|
2449
3251
|
agents,
|
|
2450
3252
|
skills,
|
|
2451
3253
|
nested: nested.length > 0 ? nested : void 0,
|
|
@@ -2460,7 +3262,7 @@ function firstSentence(description) {
|
|
|
2460
3262
|
return sentence.length > 200 ? sentence.slice(0, 199).trimEnd() + "\u2026" : sentence;
|
|
2461
3263
|
}
|
|
2462
3264
|
function libraryFor(currentProject) {
|
|
2463
|
-
const manifests = visibleProjects().map((p) => ({ name: basename7(p), skills:
|
|
3265
|
+
const manifests = visibleProjects().map((p) => ({ name: basename7(p), skills: pinsIn(p) }));
|
|
2464
3266
|
const here = currentProject ? new Set(projectStatus(currentProject).map((s) => s.name)) : null;
|
|
2465
3267
|
const all = librarySkills();
|
|
2466
3268
|
const describe2 = (s) => {
|
|
@@ -2472,8 +3274,8 @@ function libraryFor(currentProject) {
|
|
|
2472
3274
|
}
|
|
2473
3275
|
|
|
2474
3276
|
// src/usage.ts
|
|
2475
|
-
import { createReadStream, existsSync as
|
|
2476
|
-
import { join as
|
|
3277
|
+
import { createReadStream, existsSync as existsSync13, readdirSync as readdirSync9, statSync as statSync5 } from "fs";
|
|
3278
|
+
import { join as join14 } from "path";
|
|
2477
3279
|
import { createInterface } from "readline";
|
|
2478
3280
|
var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
|
|
2479
3281
|
"add-dir",
|
|
@@ -2553,31 +3355,43 @@ function usesFromTranscriptLine(line) {
|
|
|
2553
3355
|
return [];
|
|
2554
3356
|
}
|
|
2555
3357
|
function transcriptFiles(sinceMs) {
|
|
2556
|
-
const base =
|
|
2557
|
-
if (!
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
3358
|
+
const base = join14(claudeDir(), "projects");
|
|
3359
|
+
if (!existsSync13(base)) return [];
|
|
3360
|
+
const list2 = (dir) => {
|
|
3361
|
+
try {
|
|
3362
|
+
return readdirSync9(dir);
|
|
3363
|
+
} catch {
|
|
3364
|
+
return [];
|
|
3365
|
+
}
|
|
3366
|
+
};
|
|
3367
|
+
return list2(base).flatMap((project) => {
|
|
3368
|
+
const dir = join14(base, project);
|
|
3369
|
+
if (!statSync5(dir, { throwIfNoEntry: false })?.isDirectory()) return [];
|
|
3370
|
+
return list2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join14(dir, f)).filter((f) => (statSync5(f, { throwIfNoEntry: false })?.mtimeMs ?? 0) >= sinceMs);
|
|
2562
3371
|
});
|
|
2563
3372
|
}
|
|
2564
3373
|
async function scanUsage(days) {
|
|
2565
3374
|
const sinceMs = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
2566
3375
|
const seen = /* @__PURE__ */ new Map();
|
|
2567
3376
|
for (const file of transcriptFiles(sinceMs)) {
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
for (const
|
|
2571
|
-
|
|
3377
|
+
try {
|
|
3378
|
+
const lines = createInterface({ input: createReadStream(file), crlfDelay: Infinity });
|
|
3379
|
+
for await (const line of lines) {
|
|
3380
|
+
for (const use of usesFromTranscriptLine(line)) {
|
|
3381
|
+
if (Date.parse(use.at) >= sinceMs) seen.set(use.key, use);
|
|
3382
|
+
}
|
|
2572
3383
|
}
|
|
3384
|
+
} catch {
|
|
3385
|
+
continue;
|
|
2573
3386
|
}
|
|
2574
3387
|
}
|
|
2575
3388
|
return [...seen.values()];
|
|
2576
3389
|
}
|
|
2577
|
-
|
|
3390
|
+
var skillOf = (key) => key.split(":").pop();
|
|
3391
|
+
function summarize2(uses, projectOf = () => null) {
|
|
2578
3392
|
const bySkill = /* @__PURE__ */ new Map();
|
|
2579
3393
|
for (const use of uses) {
|
|
2580
|
-
const skill = use.skill
|
|
3394
|
+
const skill = skillOf(use.skill);
|
|
2581
3395
|
const summary = bySkill.get(skill) ?? { skill, uses: 0, lastUsed: "", projects: /* @__PURE__ */ new Set() };
|
|
2582
3396
|
summary.uses += 1;
|
|
2583
3397
|
if (use.at > summary.lastUsed) summary.lastUsed = use.at;
|
|
@@ -2593,7 +3407,8 @@ function usesByProject(uses, projectOf) {
|
|
|
2593
3407
|
const root = projectOf(use.cwd);
|
|
2594
3408
|
if (!root) continue;
|
|
2595
3409
|
const counts = byProject.get(root) ?? /* @__PURE__ */ new Map();
|
|
2596
|
-
|
|
3410
|
+
const skill = skillOf(use.skill);
|
|
3411
|
+
counts.set(skill, (counts.get(skill) ?? 0) + 1);
|
|
2597
3412
|
byProject.set(root, counts);
|
|
2598
3413
|
}
|
|
2599
3414
|
return byProject;
|
|
@@ -2601,20 +3416,43 @@ function usesByProject(uses, projectOf) {
|
|
|
2601
3416
|
|
|
2602
3417
|
// src/move.ts
|
|
2603
3418
|
import { basename as basename8 } from "path";
|
|
2604
|
-
function moveGlobalSkills(skills, roots, {
|
|
3419
|
+
function moveGlobalSkills(skills, roots, {
|
|
3420
|
+
group,
|
|
3421
|
+
allowTracked = false,
|
|
3422
|
+
label = basename8,
|
|
3423
|
+
allowTrackedHint = "re-run with --allow-tracked"
|
|
3424
|
+
} = {}) {
|
|
2605
3425
|
const byName = /* @__PURE__ */ new Map();
|
|
2606
3426
|
for (const m of skills) byName.set(m.name, [...byName.get(m.name) ?? [], m]);
|
|
2607
3427
|
return [...byName].map(([name, copies]) => {
|
|
2608
|
-
if (
|
|
3428
|
+
if (copiesDiffer(copies.map((m) => m.path)))
|
|
2609
3429
|
return { name, moved: false, reason: `your global copies of ${name} differ; nothing moved (keep one first: skilllib tidy --global)`, changes: [] };
|
|
2610
|
-
|
|
3430
|
+
const elsewhere = blindWithoutGlobal(name, copies, knownProjects().filter((p) => !roots.some((r) => realPath(r) === realPath(p))));
|
|
3431
|
+
if (elsewhere.length) return { name, moved: false, reason: `nothing moved: ${blindSaid(name, elsewhere, label)}`, changes: [] };
|
|
3432
|
+
const hash = treeHash(copies[0].path) ?? "";
|
|
3433
|
+
const cant = roots.flatMap((root) => ((b) => b ? [`${label(root)} (${b.replace(/ \(use --force to replace (it|the link)\)$/, "")})`] : [])(trackBlocker(root, name) ?? addRefusal(root, name, hash)));
|
|
3434
|
+
if (cant.length) return { name, moved: false, reason: `nothing moved: it can't be added to ${cant.join(", ")}`, changes: [] };
|
|
3435
|
+
const imported = importSkill(copies[0].path).status;
|
|
3436
|
+
if (imported === "exists")
|
|
2611
3437
|
return { name, moved: false, reason: `your library has a different ${name}; nothing moved (update your library from this copy first)`, changes: [] };
|
|
2612
3438
|
if (group) recordOrigin(name, `group: ${group}`);
|
|
2613
3439
|
const changes = roots.map((root) => [root, addSkill(root, name, { allowTracked })]);
|
|
2614
3440
|
const skipped = changes.filter(([, c]) => c.action === "skipped");
|
|
2615
|
-
|
|
3441
|
+
const why = (reason = "") => reason.replace(/ \(use --force to replace it\)$/, ": move or remove it there first");
|
|
3442
|
+
const kept = imported === "added" ? `; it's in Your skills now` : "";
|
|
3443
|
+
if (skipped.length) return { name, moved: false, reason: `not added to ${skipped.map(([r, c]) => `${label(r)} (${why(c.reason)})`).join(", ")}, so it still loads globally${kept}`, changes };
|
|
3444
|
+
const blindIn = changes.flatMap(([root, c]) => ((b) => b.length ? [{ root, c, b }] : [])(blindWithoutGlobal(name, copies, [root])));
|
|
3445
|
+
if (blindIn.length) {
|
|
3446
|
+
const per = blindIn.map(({ root, c, b }) => {
|
|
3447
|
+
const who = b.map((x) => `${harness(x.id).name}${realPath(x.root) === realPath(root) ? "" : ` in ${label(x.root)}`}`).join(", ");
|
|
3448
|
+
const next = c.inTheWay?.length ? `${inTheWaySaid(root, c.inTheWay)}: clear the way, then run skilllib link there` : c.uncommitted?.length ? `git tracks ${c.uncommitted.join(", ")} but not the skill's copy: commit it first, then allow the link there` : c.blocked?.length ? `git tracks ${c.blocked.join(", ")}: ${allowTrackedHint}` : blindSaid(name, b, label);
|
|
3449
|
+
return `${who} ${b.every((x) => realPath(x.root) === realPath(root)) ? `in ${label(root)} ` : `(${label(root)}) `}couldn't see it (${next})`;
|
|
3450
|
+
});
|
|
3451
|
+
return { name, moved: false, reason: `added, but ${per.join("; ")}, so it still loads globally`, changes };
|
|
3452
|
+
}
|
|
3453
|
+
const left = strandedLinks(name, copies, [...roots, ...knownProjects()], label);
|
|
2616
3454
|
const failed = copies.map((m) => unloadGlobal(m.path, m.links)).find((r) => !r.ok);
|
|
2617
|
-
return failed && !failed.ok ? { name, moved: false, reason: failed.reason, changes } : { name, moved: true, changes };
|
|
3455
|
+
return failed && !failed.ok ? { name, moved: false, reason: failed.reason, changes } : { name, moved: true, changes, ...left.length ? { left } : {} };
|
|
2618
3456
|
});
|
|
2619
3457
|
}
|
|
2620
3458
|
|
|
@@ -2636,6 +3474,7 @@ var FLAGS = {
|
|
|
2636
3474
|
function parseArgs(argv) {
|
|
2637
3475
|
const { values: v, positionals } = parseFlags({ args: argv, options: FLAGS, allowPositionals: true, strict: true });
|
|
2638
3476
|
const days = Number(v.days ?? USAGE_DAYS);
|
|
3477
|
+
if (!Number.isInteger(days) || days <= 0) throw new Error(`--days takes a whole number of days, like --days 30 (got ${v.days})`);
|
|
2639
3478
|
return {
|
|
2640
3479
|
positional: positionals,
|
|
2641
3480
|
force: !!v.force,
|
|
@@ -2644,7 +3483,7 @@ function parseArgs(argv) {
|
|
|
2644
3483
|
fix: !!v.fix,
|
|
2645
3484
|
allowTracked: !!v["allow-tracked"],
|
|
2646
3485
|
json: !!v.json,
|
|
2647
|
-
days
|
|
3486
|
+
days,
|
|
2648
3487
|
dryRun: !!v["dry-run"],
|
|
2649
3488
|
allowGit: !!v["allow-git"],
|
|
2650
3489
|
...v.keep ? { keep: v.keep } : {},
|
|
@@ -2660,27 +3499,28 @@ function colorState(state) {
|
|
|
2660
3499
|
function withDirs(root, changes) {
|
|
2661
3500
|
const { skills } = readManifest(root);
|
|
2662
3501
|
return changes.map((c) => {
|
|
2663
|
-
const dep = c.action
|
|
2664
|
-
return dep ? { ...c, dirs: [dep.dir ??
|
|
3502
|
+
const dep = c.action !== "removed" && c.action !== "skipped" ? skills[c.name] : void 0;
|
|
3503
|
+
return dep ? { ...c, dirs: [dep.dir ?? PROJECT_SKILLS_DIR, ...(dep.links ?? []).filter((d) => !c.inTheWay?.includes(`${d}/${c.name}`))] } : c;
|
|
2665
3504
|
});
|
|
2666
3505
|
}
|
|
3506
|
+
var incomplete = (c) => c.action === "skipped" || !!c.blocked?.length || !!c.uncommitted?.length || !!c.inTheWay?.length;
|
|
3507
|
+
function warnHeldLinks(root, c, where = "here") {
|
|
3508
|
+
if (c.blocked?.length) warn(`${c.name}: git tracks ${c.blocked.join(", ")} ${where}, so no link was added there. Re-run with --allow-tracked to add it.`);
|
|
3509
|
+
if (c.uncommitted?.length)
|
|
3510
|
+
warn(`${c.name}: git tracks ${c.uncommitted.join(", ")} ${where} but not ${c.name}'s real copy, so no link was added there (teammates would get a broken link). Commit ${c.name} first, then run skilllib link --allow-tracked.`);
|
|
3511
|
+
if (c.inTheWay?.length) warn(`${c.name}: not linked everywhere ${where}: ${inTheWaySaid(root, c.inTheWay)}. Clear the way, then run skilllib link.`);
|
|
3512
|
+
}
|
|
2667
3513
|
function printChanges(root, changes, args) {
|
|
2668
|
-
if (changes.some(
|
|
3514
|
+
if (changes.some(incomplete)) process.exitCode = 1;
|
|
2669
3515
|
if (args.json) return json("changes", withDirs(root, changes));
|
|
2670
3516
|
for (const c of changes) {
|
|
2671
|
-
|
|
2672
|
-
warn(`${c.name}: git tracks ${c.blocked.join(", ")} here, so no link was added there. Re-run with --allow-tracked to add it.`);
|
|
2673
|
-
}
|
|
2674
|
-
if (c.uncommitted?.length) {
|
|
2675
|
-
warn(
|
|
2676
|
-
`${c.name}: git tracks ${c.uncommitted.join(", ")} here but not ${c.name}'s real copy, so no link was added there (teammates would get a broken link). Commit ${c.name} first, then run skilllib link --allow-tracked.`
|
|
2677
|
-
);
|
|
2678
|
-
}
|
|
3517
|
+
warnHeldLinks(root, c);
|
|
2679
3518
|
if (c.action === "skipped") error(`${c.name}: ${c.reason}`);
|
|
2680
3519
|
else {
|
|
2681
|
-
const icon = c.action === "removed" ? red("-") : c.action === "reset" ? yellow("\u21BA") : green(c.action === "installed" ? "+" : "\u2191");
|
|
3520
|
+
const icon = c.action === "removed" ? red("-") : c.action === "reset" ? yellow("\u21BA") : c.action === "unchanged" ? dim("=") : green(c.action === "installed" ? "+" : c.action === "relinked" ? "\u21E2" : "\u2191");
|
|
2682
3521
|
const versions = c.action === "removed" ? "" : c.from && c.from !== c.to ? ` v${c.from} \u2192 v${c.to}` : c.to ? ` v${c.to}` : "";
|
|
2683
3522
|
info(` ${icon} ${c.name}${versions} ${dim(c.action)}`);
|
|
3523
|
+
if (c.reason) info(dim(` ${c.reason}`));
|
|
2684
3524
|
}
|
|
2685
3525
|
if (c.backedUp) info(dim(` local edits saved to ${tildify(c.backedUp)}; \`skilllib restore ${c.name}\` brings them back`));
|
|
2686
3526
|
}
|
|
@@ -2693,9 +3533,9 @@ function projectOfFactory(projects2) {
|
|
|
2693
3533
|
const sorted = [...projects2].sort((a, b) => b.length - a.length);
|
|
2694
3534
|
return (cwd) => {
|
|
2695
3535
|
if (!cwd) return null;
|
|
2696
|
-
const known = sorted.find((p) => cwd === p || cwd.startsWith(p +
|
|
3536
|
+
const known = sorted.find((p) => cwd === p || cwd.startsWith(p + sep8));
|
|
2697
3537
|
if (known) return known;
|
|
2698
|
-
const repo = cwd.split(`${
|
|
3538
|
+
const repo = cwd.split(`${sep8}.claude${sep8}worktrees${sep8}`)[0];
|
|
2699
3539
|
return isProjectCandidate(repo) ? repo : null;
|
|
2700
3540
|
};
|
|
2701
3541
|
}
|
|
@@ -2703,13 +3543,25 @@ async function usageBySkill(days, onlyProject) {
|
|
|
2703
3543
|
const projectOf = projectOfFactory(onlyProject ? [onlyProject, ...knownProjects()] : knownProjects());
|
|
2704
3544
|
const uses = await scanUsage(days);
|
|
2705
3545
|
const scoped = onlyProject ? uses.filter((u) => projectOf(u.cwd) === onlyProject) : uses;
|
|
2706
|
-
return new Map(
|
|
3546
|
+
return new Map(summarize2(scoped, projectOf).map((s) => [s.skill, s]));
|
|
2707
3547
|
}
|
|
3548
|
+
var projectsFor = (args) => {
|
|
3549
|
+
if (!args.all) return [requireProject()];
|
|
3550
|
+
const { readable, unreadable } = readableRoots(visibleProjects());
|
|
3551
|
+
for (const u of unreadable) {
|
|
3552
|
+
error(`${basename9(u.root)}: ${u.reason}`);
|
|
3553
|
+
process.exitCode = 1;
|
|
3554
|
+
}
|
|
3555
|
+
return readable;
|
|
3556
|
+
};
|
|
3557
|
+
var yourGlobals = () => machineSkills().filter((m) => m.movable && !m.broken);
|
|
2708
3558
|
function requireProject() {
|
|
2709
3559
|
const root = projectHere();
|
|
2710
3560
|
if (root) return root;
|
|
2711
3561
|
if (!isProjectCandidate(findProjectRoot())) {
|
|
2712
|
-
const
|
|
3562
|
+
const top = findProjectRoot();
|
|
3563
|
+
if (realPath(top) !== realPath(userHome())) throw new Error(`${tildify(top)} is skilllib's or an agent's own folder, not a project. Run this inside a project.`);
|
|
3564
|
+
const stray = existsSync14(join15(userHome(), MANIFEST_FILE)) ? `, though ${tildify(join15(userHome(), MANIFEST_FILE))} makes it look like one` : "";
|
|
2713
3565
|
throw new Error(`Your home folder isn't a project${stray}: skills added here would load in every repo. Run this inside a project.`);
|
|
2714
3566
|
}
|
|
2715
3567
|
throw new Error("Not in a git repo. Run this inside a project (or `git init` first).");
|
|
@@ -2722,7 +3574,10 @@ async function status(args) {
|
|
|
2722
3574
|
const root2 = findProjectRoot();
|
|
2723
3575
|
const usage3 = await usageBySkill(args.days, root2);
|
|
2724
3576
|
json("status", usableHere(root2, new Map([...usage3].map(([name, u]) => [name, u.uses]))));
|
|
2725
|
-
|
|
3577
|
+
try {
|
|
3578
|
+
if (projectHere()) rememberProjects([root2]);
|
|
3579
|
+
} catch {
|
|
3580
|
+
}
|
|
2726
3581
|
return;
|
|
2727
3582
|
}
|
|
2728
3583
|
const root = requireProject();
|
|
@@ -2732,14 +3587,8 @@ async function status(args) {
|
|
|
2732
3587
|
`);
|
|
2733
3588
|
if (skills.length === 0 && nested.length === 0) info("No skills in this project yet. Add one from your library with: skilllib add <name>");
|
|
2734
3589
|
const usage2 = await usageBySkill(args.days, root);
|
|
2735
|
-
const
|
|
2736
|
-
const
|
|
2737
|
-
const asRepo = {
|
|
2738
|
-
"local only": "repo skill",
|
|
2739
|
-
"untracked copy of library skill": "repo skill, in library",
|
|
2740
|
-
"untracked, differs from library": "repo skill, differs from library"
|
|
2741
|
-
};
|
|
2742
|
-
const stateOf = (s) => colorState(committed(s.path) && asRepo[s.state] || s.state);
|
|
3590
|
+
const teamState = teamStateIn(root, nested.map((s) => s.location));
|
|
3591
|
+
const stateOf = (s) => colorState(teamState(s));
|
|
2743
3592
|
const agentsOf = (s) => {
|
|
2744
3593
|
const ids = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
|
|
2745
3594
|
return ids.length === s.visibility.length ? dim("all") : ids.length ? ids.map((id) => harness(id).name).join(", ") : red("none");
|
|
@@ -2767,17 +3616,19 @@ async function status(args) {
|
|
|
2767
3616
|
}))
|
|
2768
3617
|
);
|
|
2769
3618
|
}
|
|
2770
|
-
const states = new Set(skills.map((s) => s
|
|
3619
|
+
const states = new Set(skills.map((s) => teamState(s)));
|
|
2771
3620
|
const hints = [];
|
|
2772
3621
|
if (states.has("update available")) hints.push("`skilllib update` to install the newest versions");
|
|
2773
3622
|
const missing = skills.filter((s) => s.state === "folder missing");
|
|
2774
|
-
if (missing.some((s) => s.latest !== null)) hints.push("`skilllib sync` to restore missing skills");
|
|
3623
|
+
if (missing.some((s) => s.latest !== null && !syncBlocker(root, s.name))) hints.push("`skilllib sync` to restore missing skills");
|
|
3624
|
+
if (missing.some((s) => syncBlocker(root, s.name))) hints.push("`skilllib doctor` says what's in sync's way");
|
|
2775
3625
|
const notInLibrary = missing.filter((s) => s.latest === null).map((s) => s.name);
|
|
2776
3626
|
if (notInLibrary.length)
|
|
2777
|
-
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
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
3627
|
+
hints.push(`${notInLibrary.join(", ")} ${notInLibrary.length === 1 ? "isn't" : "aren't"} in your library (${tildify(libraryDir())}): \`skilllib import <dir>\` to add, or set SKILLLIB_HOME to the skilllib folder whose library has ${notInLibrary.length === 1 ? "it" : "them"}`);
|
|
3628
|
+
const at = (s) => relativeTo(root, s.path);
|
|
3629
|
+
for (const s of skills.filter((x) => x.state === "edited locally" || x.state === "edited locally, update available"))
|
|
3630
|
+
hints.push(`\`skilllib import ${at(s)} --force\` to save ${s.name}'s local edits to the library`);
|
|
3631
|
+
for (const s of skills.filter((x) => x.state === "local only")) hints.push(`\`skilllib import ${at(s)}\` to add ${s.name} to the library`);
|
|
2781
3632
|
if (states.has("untracked copy of library skill") || states.has("untracked, differs from library"))
|
|
2782
3633
|
hints.push("`skilllib add <name>` to manage an untracked skill (--force if it differs)");
|
|
2783
3634
|
if (hints.length > 0) info("\n" + hints.map((h) => dim(`\u2192 ${h}`)).join("\n"));
|
|
@@ -2804,7 +3655,7 @@ async function list(args) {
|
|
|
2804
3655
|
} else {
|
|
2805
3656
|
const installs = /* @__PURE__ */ new Map();
|
|
2806
3657
|
for (const project of visibleProjects()) {
|
|
2807
|
-
for (const name of Object.keys(
|
|
3658
|
+
for (const name of Object.keys(pinsIn(project))) {
|
|
2808
3659
|
installs.set(name, [...installs.get(name) ?? [], basename9(project)]);
|
|
2809
3660
|
}
|
|
2810
3661
|
}
|
|
@@ -2816,14 +3667,14 @@ async function list(args) {
|
|
|
2816
3667
|
skills.map((s) => ({
|
|
2817
3668
|
Skill: s.name,
|
|
2818
3669
|
Description: truncate(s.description, 50),
|
|
2819
|
-
From: dim(truncate(origins[s.name] ?? "", 30)),
|
|
3670
|
+
From: dim(truncate(tildify(origins[s.name] ?? ""), 30)),
|
|
2820
3671
|
Projects: (installs.get(s.name) ?? []).join(", ") || dim("none"),
|
|
2821
3672
|
[`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0)
|
|
2822
3673
|
}))
|
|
2823
3674
|
);
|
|
2824
3675
|
}
|
|
2825
3676
|
const libraryNames = new Set(skills.map((s) => s.name));
|
|
2826
|
-
const globals = [...new Set(
|
|
3677
|
+
const globals = [...new Set(yourGlobals().filter((m) => m.harnesses.length).map((m) => m.name))];
|
|
2827
3678
|
if (globals.length > 0) {
|
|
2828
3679
|
info("");
|
|
2829
3680
|
warn(`${globals.length} of your global ${globals.length === 1 ? "skill still loads" : "skills still load"} in every project`);
|
|
@@ -2835,20 +3686,33 @@ async function list(args) {
|
|
|
2835
3686
|
);
|
|
2836
3687
|
}
|
|
2837
3688
|
}
|
|
3689
|
+
function teamStateIn(root, extraDirs = []) {
|
|
3690
|
+
const git2 = gitInfo(root, [...new Set(extraDirs)]);
|
|
3691
|
+
const committed = (path) => ["committed", "changed"].includes(git2?.of(relativeTo(root, path)) ?? "");
|
|
3692
|
+
const asRepo = {
|
|
3693
|
+
"local only": "repo skill",
|
|
3694
|
+
"untracked copy of library skill": "repo skill, in library",
|
|
3695
|
+
"untracked, differs from library": "repo skill, differs from library"
|
|
3696
|
+
};
|
|
3697
|
+
return (s) => committed(s.path) && asRepo[s.state] || s.state;
|
|
3698
|
+
}
|
|
2838
3699
|
function projects(args) {
|
|
2839
|
-
const roots = visibleProjects();
|
|
3700
|
+
const { readable: roots, unreadable } = readableRoots(visibleProjects());
|
|
3701
|
+
for (const u of unreadable) {
|
|
3702
|
+
error(`${basename9(u.root)}: ${u.reason}`);
|
|
3703
|
+
process.exitCode = 1;
|
|
3704
|
+
}
|
|
2840
3705
|
if (args.json) {
|
|
2841
3706
|
return json(
|
|
2842
3707
|
"projects",
|
|
2843
|
-
roots.map(
|
|
2844
|
-
(root)
|
|
3708
|
+
roots.map((root) => {
|
|
3709
|
+
const teamState = teamStateIn(root);
|
|
3710
|
+
return {
|
|
2845
3711
|
name: basename9(root),
|
|
2846
3712
|
path: root,
|
|
2847
|
-
skills: projectStatus(root).map(
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
})
|
|
2851
|
-
)
|
|
3713
|
+
skills: projectStatus(root).map((s) => ({ name: s.name, managed: s.managed, state: teamState(s), version: s.version, latest: s.latest }))
|
|
3714
|
+
};
|
|
3715
|
+
})
|
|
2852
3716
|
);
|
|
2853
3717
|
}
|
|
2854
3718
|
if (roots.length === 0) {
|
|
@@ -2858,13 +3722,14 @@ function projects(args) {
|
|
|
2858
3722
|
table(
|
|
2859
3723
|
roots.map((root) => {
|
|
2860
3724
|
const skills = projectStatus(root);
|
|
2861
|
-
const
|
|
2862
|
-
const
|
|
3725
|
+
const teamState = teamStateIn(root);
|
|
3726
|
+
const usual = /* @__PURE__ */ new Set(["ok", "local only", "repo skill", "repo skill, in library", "from npx skills"]);
|
|
3727
|
+
const attention = skills.filter((s) => !usual.has(teamState(s))).length;
|
|
2863
3728
|
return {
|
|
2864
3729
|
Project: basename9(root),
|
|
2865
3730
|
Path: dim(tildify(root)),
|
|
2866
|
-
Skills:
|
|
2867
|
-
"Local only": String(skills.filter((s) => s
|
|
3731
|
+
Skills: skills.map((s) => s.name).join(", ") || dim("none"),
|
|
3732
|
+
"Local only": String(skills.filter((s) => teamState(s) === "local only").length),
|
|
2868
3733
|
Status: attention === 0 ? green("ok") : yellow(`${attention} need attention`)
|
|
2869
3734
|
};
|
|
2870
3735
|
})
|
|
@@ -2875,8 +3740,7 @@ function add(args) {
|
|
|
2875
3740
|
throw new Error("Usage: skilllib add <skill-name>... [--force]");
|
|
2876
3741
|
}
|
|
2877
3742
|
const root = requireProject();
|
|
2878
|
-
|
|
2879
|
-
printChanges(root, args.positional.map((name) => addSkill(root, name, { force: args.force })), args);
|
|
3743
|
+
printChanges(root, args.positional.map((name) => addSkill(root, name, { force: args.force, allowTracked: args.allowTracked })), args);
|
|
2880
3744
|
rememberProjects([root]);
|
|
2881
3745
|
}
|
|
2882
3746
|
function remove(args) {
|
|
@@ -2886,59 +3750,59 @@ function remove(args) {
|
|
|
2886
3750
|
const root = requireProject();
|
|
2887
3751
|
printChanges(root, args.positional.map((name) => removeSkill(root, name, { force: args.force })), args);
|
|
2888
3752
|
}
|
|
2889
|
-
function
|
|
2890
|
-
|
|
2891
|
-
const roots = args.all ? visibleProjects() : [requireProject()];
|
|
3753
|
+
function changeEach(args, change, nothing) {
|
|
3754
|
+
const roots = projectsFor(args);
|
|
2892
3755
|
const results = roots.map((root) => {
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
|
|
3756
|
+
const changes = change(root);
|
|
3757
|
+
if (changes.some(incomplete)) process.exitCode = 1;
|
|
2896
3758
|
if (!args.json) {
|
|
2897
3759
|
if (args.all) info(`${basename9(root)} ${dim(tildify(root))}`);
|
|
2898
|
-
if (changes.length === 0)
|
|
2899
|
-
|
|
2900
|
-
else info(dim("No skilllib.json: nothing to sync"));
|
|
2901
|
-
} else printChanges(root, changes, args);
|
|
3760
|
+
if (changes.length === 0) nothing(root);
|
|
3761
|
+
else printChanges(root, changes, args);
|
|
2902
3762
|
}
|
|
2903
3763
|
return { project: basename9(root), path: root, changes: withDirs(root, changes) };
|
|
2904
3764
|
});
|
|
2905
3765
|
if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
|
|
3766
|
+
return roots;
|
|
3767
|
+
}
|
|
3768
|
+
function sync(args) {
|
|
3769
|
+
if (args.positional.length) throw new Error(`sync takes no skill names: it installs exactly what skilllib.json pins. To add a skill: skilllib add ${args.positional.join(" ")}`);
|
|
3770
|
+
const roots = changeEach(
|
|
3771
|
+
args,
|
|
3772
|
+
(root) => syncProject(root, { force: args.force }),
|
|
3773
|
+
(root) => existsSync14(join15(root, MANIFEST_FILE)) ? success("Installed versions match skilllib.json") : info(dim("No skilllib.json: nothing to sync"))
|
|
3774
|
+
);
|
|
2906
3775
|
if (!args.all) rememberProjects(roots);
|
|
2907
3776
|
}
|
|
2908
3777
|
function update(args) {
|
|
2909
|
-
const
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
if (changes.length === 0) success("Everything is on the latest version");
|
|
2922
|
-
else printChanges(root, changes, args);
|
|
2923
|
-
}
|
|
2924
|
-
return { project: basename9(root), path: root, changes: withDirs(root, changes) };
|
|
2925
|
-
});
|
|
2926
|
-
if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
|
|
3778
|
+
const names = args.positional.length ? args.positional : void 0;
|
|
3779
|
+
changeEach(
|
|
3780
|
+
args,
|
|
3781
|
+
(root) => {
|
|
3782
|
+
const changes = updateProject(root, names, { force: args.force, allowTracked: args.allowTracked });
|
|
3783
|
+
if (!args.all) {
|
|
3784
|
+
for (const name of names ?? []) if (!(name in readManifest(root).skills)) changes.push({ name, action: "skipped", reason: "not managed by skilllib in this project" });
|
|
3785
|
+
}
|
|
3786
|
+
return changes;
|
|
3787
|
+
},
|
|
3788
|
+
() => success("Everything is on the latest version")
|
|
3789
|
+
);
|
|
2927
3790
|
}
|
|
2928
3791
|
function outdated(args) {
|
|
2929
|
-
const roots = args
|
|
3792
|
+
const roots = projectsFor(args);
|
|
2930
3793
|
const behind = roots.flatMap(
|
|
2931
|
-
(root) => projectStatus(root).filter((s) => s.
|
|
3794
|
+
(root) => projectStatus(root).filter((s) => s.state.endsWith("update available") && s.latest !== null && s.version !== null).map((s) => ({ project: basename9(root), path: root, skill: s.name, installed: s.version, latest: s.latest, state: s.state }))
|
|
2932
3795
|
);
|
|
2933
3796
|
if (args.json) return json("outdated", behind);
|
|
2934
|
-
const
|
|
2935
|
-
if (
|
|
2936
|
-
table(
|
|
3797
|
+
const rows2 = behind.map((b) => ({ Project: b.project, Skill: b.skill, Installed: `v${b.installed}`, Latest: `v${b.latest}`, Status: colorState(b.state) }));
|
|
3798
|
+
if (rows2.length === 0) return success("Everything is on the latest version");
|
|
3799
|
+
table(rows2);
|
|
2937
3800
|
info(dim(`
|
|
2938
3801
|
\u2192 skilllib update${args.all ? " --all" : ""}`));
|
|
2939
3802
|
}
|
|
2940
3803
|
function importSkills(args) {
|
|
2941
|
-
|
|
3804
|
+
if (args.global && args.positional.length) throw new Error("skilllib import --global takes no folders: it imports all your global skills");
|
|
3805
|
+
const dirs = args.global ? yourGlobals().map((m) => m.path) : args.positional.map((p) => resolve8(p));
|
|
2942
3806
|
if (dirs.length === 0) {
|
|
2943
3807
|
throw new Error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
|
|
2944
3808
|
}
|
|
@@ -2958,14 +3822,11 @@ function importSkills(args) {
|
|
|
2958
3822
|
continue;
|
|
2959
3823
|
}
|
|
2960
3824
|
success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
|
|
2961
|
-
const
|
|
2962
|
-
const
|
|
2963
|
-
if (
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
if (change.action !== "skipped") info(dim(` ${basename9(root)} now depends on ${name} v${change.to}`));
|
|
2967
|
-
}
|
|
2968
|
-
rememberProjects([root]);
|
|
3825
|
+
const root = resolve8(dir, "..", "..", "..");
|
|
3826
|
+
const dep = projectHere(root) === root ? readManifest(root).skills[name] : void 0;
|
|
3827
|
+
if (dep && realpathOrNull(join15(root, dep.dir ?? PROJECT_SKILLS_DIR, name)) === realpathOrNull(dir) && dep.hash !== treeHash(dir)) {
|
|
3828
|
+
const change = addSkill(root, name);
|
|
3829
|
+
if (change.action !== "skipped") info(dim(` ${basename9(root)} now depends on ${name} v${change.to}`));
|
|
2969
3830
|
}
|
|
2970
3831
|
}
|
|
2971
3832
|
if (args.json) json("import", results);
|
|
@@ -2980,7 +3841,8 @@ function scan(args) {
|
|
|
2980
3841
|
const found = discoverProjects();
|
|
2981
3842
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
2982
3843
|
const skillNames = (root) => [
|
|
2983
|
-
|
|
3844
|
+
// Not a folder that links out of the repo: what's in it isn't the repo's (as in status).
|
|
3845
|
+
.../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.filter((d) => !notWritable(root, d)).flatMap((d) => skillDirsIn(join15(root, d))).map((d) => basename9(d)), ...nestedSkills(root).map((s) => s.name)])
|
|
2984
3846
|
];
|
|
2985
3847
|
const withSkills = found.filter((root) => skillNames(root).length > 0);
|
|
2986
3848
|
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
|
|
@@ -2997,44 +3859,43 @@ ${yellow("Yellow")} skills aren't in your library yet (\`skilllib import <skill-
|
|
|
2997
3859
|
}
|
|
2998
3860
|
function folders(args) {
|
|
2999
3861
|
const [action, ...paths] = args.positional;
|
|
3000
|
-
|
|
3862
|
+
if (action && !["add", "remove", "rm"].includes(action)) throw new Error("Usage: skilllib folders [add|remove <folder>...]");
|
|
3863
|
+
const failures = [];
|
|
3001
3864
|
if (action === "add") {
|
|
3002
3865
|
for (const p of paths) {
|
|
3003
3866
|
try {
|
|
3004
3867
|
success(`Added ${tildify(addRoot(p))}`);
|
|
3005
3868
|
} catch (e) {
|
|
3006
|
-
|
|
3007
|
-
failed = true;
|
|
3869
|
+
failures.push(e.message);
|
|
3008
3870
|
}
|
|
3009
3871
|
}
|
|
3010
3872
|
discoverProjects();
|
|
3011
3873
|
} else if (action === "remove" || action === "rm") {
|
|
3012
3874
|
for (const p of paths) {
|
|
3013
3875
|
if (removeRoot(p)) success(`Removed ${tildify(expandHome(p))}`);
|
|
3014
|
-
else
|
|
3876
|
+
else failures.push(`${tildify(expandHome(p))} wasn't a project folder`);
|
|
3015
3877
|
}
|
|
3016
3878
|
}
|
|
3017
3879
|
const { roots, hidden } = readConfig();
|
|
3018
3880
|
info(roots.length ? `Project folders:
|
|
3019
3881
|
${roots.map((r) => ` ${tildify(r)}`).join("\n")}` : "No project folders. Add one: skilllib folders add ~/Projects");
|
|
3020
3882
|
if (hidden.length) info(dim(`Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")} (skilllib unhide <path>)`));
|
|
3021
|
-
if (
|
|
3883
|
+
if (failures.length) throw new Error(failures.join("; "));
|
|
3022
3884
|
}
|
|
3023
3885
|
function unhide(args) {
|
|
3024
3886
|
if (args.positional.length === 0) {
|
|
3025
3887
|
throw new Error("Usage: skilllib unhide <path>...");
|
|
3026
3888
|
}
|
|
3027
|
-
|
|
3889
|
+
const failures = [];
|
|
3028
3890
|
for (const p of args.positional) {
|
|
3029
3891
|
const path = expandHome(p);
|
|
3030
3892
|
if (setHidden(path, false)) success(`${tildify(path)} is visible again`);
|
|
3031
3893
|
else {
|
|
3032
3894
|
const { hidden } = readConfig();
|
|
3033
|
-
|
|
3034
|
-
failed = true;
|
|
3895
|
+
failures.push(`${tildify(path)} wasn't hidden. ${hidden.length ? `Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")}` : "No projects are hidden."}`);
|
|
3035
3896
|
}
|
|
3036
3897
|
}
|
|
3037
|
-
if (
|
|
3898
|
+
if (failures.length) throw new Error(failures.join("; "));
|
|
3038
3899
|
}
|
|
3039
3900
|
function newSkill(args) {
|
|
3040
3901
|
const [name, ...rest] = args.positional;
|
|
@@ -3045,7 +3906,7 @@ function newSkill(args) {
|
|
|
3045
3906
|
if (!r.ok) {
|
|
3046
3907
|
throw new Error(`Can't create ${name}: ${r.reason}`);
|
|
3047
3908
|
}
|
|
3048
|
-
success(`Created ${tildify(
|
|
3909
|
+
success(`Created ${tildify(join15(r.dir, "SKILL.md"))}`);
|
|
3049
3910
|
}
|
|
3050
3911
|
function restore(args) {
|
|
3051
3912
|
const backups = [...listBackups(), ...pluginBackups()];
|
|
@@ -3056,16 +3917,15 @@ function restore(args) {
|
|
|
3056
3917
|
info(dim("\n\u2192 skilllib restore <name>"));
|
|
3057
3918
|
return;
|
|
3058
3919
|
}
|
|
3059
|
-
const
|
|
3060
|
-
if (!
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
error(`Can't restore ${name}: ${r.reason}`);
|
|
3067
|
-
process.exitCode = 1;
|
|
3920
|
+
const newest = backups.find((b) => b.name === name);
|
|
3921
|
+
if (!newest) throw new Error(`No backup named ${name} (skilllib restore lists them; a skill removed from a repo that your library has comes back with skilllib add ${name})`);
|
|
3922
|
+
const failures = [];
|
|
3923
|
+
for (const backup of movedTogether(backups, newest)) {
|
|
3924
|
+
const r = backup.kind === "plugin" ? restorePlugin(backup) : restoreBackup(backup);
|
|
3925
|
+
if (r.ok) success(`Restored ${name} to ${tildify(r.to)}`);
|
|
3926
|
+
else failures.push(r.reason);
|
|
3068
3927
|
}
|
|
3928
|
+
if (failures.length) throw new Error(`Can't restore ${name}: ${failures.join("; ")}`);
|
|
3069
3929
|
}
|
|
3070
3930
|
async function allIssues(args, withUsage = true) {
|
|
3071
3931
|
const projects2 = visibleProjects();
|
|
@@ -3076,11 +3936,14 @@ async function allIssues(args, withUsage = true) {
|
|
|
3076
3936
|
const projectOf = projectOfFactory(projects2);
|
|
3077
3937
|
const uses = await scanUsage(args.days);
|
|
3078
3938
|
const inProject = usesByProject(uses, projectOf);
|
|
3079
|
-
const total = new Map(
|
|
3939
|
+
const total = new Map(summarize2(uses, projectOf).map((s) => [s.skill, s.uses]));
|
|
3940
|
+
const asUsed = /* @__PURE__ */ new Map();
|
|
3941
|
+
for (const u of uses) asUsed.set(u.skill, (asUsed.get(u.skill) ?? 0) + 1);
|
|
3080
3942
|
issues.push(
|
|
3081
|
-
...usageIssues(projects2, machine, libraryNames, {
|
|
3943
|
+
...usageIssues(readableRoots(projects2).readable, machine, libraryNames, {
|
|
3082
3944
|
inProject: (root, skill) => inProject.get(root)?.get(skill) ?? 0,
|
|
3083
3945
|
total: (skill) => total.get(skill) ?? 0,
|
|
3946
|
+
asUsed: (key) => asUsed.get(key) ?? 0,
|
|
3084
3947
|
days: args.days
|
|
3085
3948
|
})
|
|
3086
3949
|
);
|
|
@@ -3102,7 +3965,7 @@ async function doctor(args) {
|
|
|
3102
3965
|
id,
|
|
3103
3966
|
severity,
|
|
3104
3967
|
title,
|
|
3105
|
-
detail,
|
|
3968
|
+
detail: detailWithGit({ detail, fix, choices }),
|
|
3106
3969
|
fix: fix?.label,
|
|
3107
3970
|
choices: choices?.map((c) => c.label),
|
|
3108
3971
|
fixed: args.fix && fix ? fixed(fix) : void 0
|
|
@@ -3114,7 +3977,8 @@ async function doctor(args) {
|
|
|
3114
3977
|
for (const issue of issues) {
|
|
3115
3978
|
info(`${issue.severity === "problem" ? red("\u25CF") : yellow("\u25C6")} ${issue.title}`);
|
|
3116
3979
|
info(dim(` ${issue.detail}${issue.fix ? ` \u2192 ${issue.fix.label}` : ""}`));
|
|
3117
|
-
|
|
3980
|
+
const spans = new Set(issue.at?.map((a) => a.root)).size > 1;
|
|
3981
|
+
if (issue.choices) info(dim(` your call: ${issue.choices.map((c, i) => `${i + 1}) ${c.label}`).join(" ")} \u2192 skilllib apply ${issue.id} ${spans ? '"<choice>"' : "<n>"}`));
|
|
3118
3982
|
if (args.fix && issue.fix) {
|
|
3119
3983
|
const r = fixed(issue.fix);
|
|
3120
3984
|
info(` ${r.ok ? green("\u2713") : red("\u2717")} ${r.message}`);
|
|
@@ -3125,14 +3989,14 @@ async function doctor(args) {
|
|
|
3125
3989
|
async function apply(args) {
|
|
3126
3990
|
const [id, pick] = args.positional;
|
|
3127
3991
|
if (!id) throw new Error("Usage: skilllib apply <issue-id> [choice] (ids and choices: skilllib status --json or doctor --json)");
|
|
3128
|
-
const found = [...statusIssues(findProjectRoot()), ...await allIssues(args,
|
|
3992
|
+
const found = [...statusIssues(findProjectRoot()), ...await allIssues(args, /^(plugin-)?unused:/.test(id))].filter((i) => i.id === id);
|
|
3129
3993
|
if (!found.length) throw new Error(`No issue ${id} right now (it may be fixed already; for an unused:\u2026 issue, pass doctor's --days). See skilllib status --json or doctor --json`);
|
|
3130
3994
|
const optionsOf = (i) => i.choices ?? (i.fix ? [i.fix] : []);
|
|
3131
3995
|
const labels = (i) => optionsOf(i).map((c) => c.label).join("\0");
|
|
3132
3996
|
const byLabel = pick === void 0 ? void 0 : found.find((i) => optionsOf(i).some((c) => c.label.toLowerCase() === pick.toLowerCase()));
|
|
3133
3997
|
if (!byLabel && found.some((i) => labels(i) !== labels(found[0]))) {
|
|
3134
3998
|
const all = [...new Set(found.flatMap((i) => optionsOf(i).map((c) => c.label)))];
|
|
3135
|
-
throw new Error(`${found[0].title}: its choices differ
|
|
3999
|
+
throw new Error(`${found[0].title}: its choices here differ from doctor's (all your repos); pick one by label: ${all.map((l) => `"${l}"`).join(" ")}`);
|
|
3136
4000
|
}
|
|
3137
4001
|
const issue = byLabel ?? found[0];
|
|
3138
4002
|
const options = optionsOf(issue);
|
|
@@ -3148,22 +4012,22 @@ async function apply(args) {
|
|
|
3148
4012
|
else error(r.message);
|
|
3149
4013
|
}
|
|
3150
4014
|
async function usage(args) {
|
|
3151
|
-
const summaries =
|
|
4015
|
+
const summaries = summarize2(await scanUsage(args.days), projectOfFactory(knownProjects()));
|
|
4016
|
+
const library = new Set(librarySkills().map((s) => s.name));
|
|
4017
|
+
const unused = [...library].filter((name) => !summaries.some((u) => u.skill === name));
|
|
3152
4018
|
if (args.json) {
|
|
3153
|
-
const library2 = new Set(librarySkills().map((s) => s.name));
|
|
3154
4019
|
return json("usage", {
|
|
3155
4020
|
usageDays: args.days,
|
|
3156
4021
|
source: "Claude Code transcripts",
|
|
3157
|
-
skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) => basename9(p)), inLibrary:
|
|
3158
|
-
unusedLibrarySkills:
|
|
4022
|
+
skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) => basename9(p)), inLibrary: library.has(s.skill) })),
|
|
4023
|
+
unusedLibrarySkills: unused
|
|
3159
4024
|
});
|
|
3160
4025
|
}
|
|
3161
4026
|
if (summaries.length === 0) {
|
|
3162
4027
|
info(`No skill usage in Claude Code transcripts from the last ${args.days} days.`);
|
|
3163
4028
|
return;
|
|
3164
4029
|
}
|
|
3165
|
-
|
|
3166
|
-
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join14(claudeDir(), "projects"))})`)}
|
|
4030
|
+
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join15(claudeDir(), "projects"))})`)}
|
|
3167
4031
|
`);
|
|
3168
4032
|
table(
|
|
3169
4033
|
summaries.map((s) => ({
|
|
@@ -3174,15 +4038,14 @@ async function usage(args) {
|
|
|
3174
4038
|
Library: library.has(s.skill) ? green("yes") : dim("no")
|
|
3175
4039
|
}))
|
|
3176
4040
|
);
|
|
3177
|
-
const unused = librarySkills().filter((s) => !summaries.some((u) => u.skill === s.name));
|
|
3178
4041
|
if (unused.length > 0) {
|
|
3179
4042
|
info("");
|
|
3180
|
-
warn(`Library skills unused in the last ${args.days} days: ${unused.
|
|
4043
|
+
warn(`Library skills unused in the last ${args.days} days: ${unused.join(", ")}`);
|
|
3181
4044
|
}
|
|
3182
4045
|
}
|
|
3183
4046
|
function show(args) {
|
|
3184
4047
|
const name = args.positional[0];
|
|
3185
|
-
const dir = name ?
|
|
4048
|
+
const dir = name ? join15(libraryDir(), name) : "";
|
|
3186
4049
|
if (!name || !isSkillDir(dir)) {
|
|
3187
4050
|
throw new Error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
|
|
3188
4051
|
}
|
|
@@ -3194,14 +4057,14 @@ function show(args) {
|
|
|
3194
4057
|
path: dir,
|
|
3195
4058
|
latest: latestVersion(name)?.version ?? null,
|
|
3196
4059
|
origin: libraryOrigins()[name] ?? "",
|
|
3197
|
-
projects:
|
|
3198
|
-
skillMd:
|
|
4060
|
+
projects: projectsUsing(name).map((p) => basename9(p)),
|
|
4061
|
+
skillMd: readFileSync7(join15(dir, "SKILL.md"), "utf-8")
|
|
3199
4062
|
});
|
|
3200
4063
|
}
|
|
3201
4064
|
info(`${name} ${dim(tildify(dir))}`);
|
|
3202
4065
|
if (description) info(`
|
|
3203
4066
|
${description}`);
|
|
3204
|
-
const using =
|
|
4067
|
+
const using = projectsUsing(name);
|
|
3205
4068
|
info(`
|
|
3206
4069
|
Installed in: ${using.map((p) => tildify(p)).join(", ") || "no projects"}`);
|
|
3207
4070
|
}
|
|
@@ -3223,26 +4086,35 @@ function harnessesCommand(args) {
|
|
|
3223
4086
|
New installs go to ${installDirs(enabled).join(" (real copy) + ")}${installDirs(enabled).length > 1 ? " (link)" : ""}.`));
|
|
3224
4087
|
}
|
|
3225
4088
|
function link(args) {
|
|
3226
|
-
const roots = args
|
|
4089
|
+
const roots = projectsFor(args);
|
|
3227
4090
|
for (const root of roots) {
|
|
3228
|
-
|
|
3229
|
-
const res = linkAll(root);
|
|
4091
|
+
const res = linkAll(root, { allowTracked: args.allowTracked });
|
|
3230
4092
|
const label = args.all ? `${basename9(root)}: ` : "";
|
|
3231
4093
|
if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
|
|
3232
|
-
else if (!args.all && !res.blocked.length && !res.uncommitted.length
|
|
4094
|
+
else if (!args.all && !res.blocked.length && !res.uncommitted.length && !res.inTheWay.length) {
|
|
4095
|
+
const missing = projectStatus(root).filter((s) => s.state === "folder missing").map((s) => s.name);
|
|
4096
|
+
if (missing.length) warn(`Nothing to link; ${missing.join(", ")} ${missing.length === 1 ? "isn't" : "aren't"} installed here (skilllib sync, or skilllib doctor says what's in the way)`);
|
|
4097
|
+
else success("Every skill here is already usable by all your agents");
|
|
4098
|
+
}
|
|
4099
|
+
if (res.inTheWay.length) {
|
|
4100
|
+
process.exitCode = 1;
|
|
4101
|
+
error(`${label}some skills weren't linked everywhere: ${inTheWaySaid(root, res.inTheWay)}. Clear the way, then link again`);
|
|
4102
|
+
}
|
|
3233
4103
|
if (res.blocked.length) {
|
|
3234
4104
|
process.exitCode = 1;
|
|
3235
4105
|
error(`${label}git tracks ${res.blocked.join(", ")}, so ${res.linked.length ? "some skills weren't" : "nothing was"} linked there; re-run with --allow-tracked to link there too`);
|
|
3236
4106
|
}
|
|
3237
|
-
if (res.uncommitted.length)
|
|
4107
|
+
if (res.uncommitted.length) {
|
|
4108
|
+
process.exitCode = 1;
|
|
3238
4109
|
warn(
|
|
3239
4110
|
`${label}${res.uncommitted.join(", ")} ${res.uncommitted.length === 1 ? "isn't" : "aren't"} committed, so not linked into folders git tracks (teammates would get broken links); commit ${res.uncommitted.length === 1 ? "it" : "them"} first`
|
|
3240
4111
|
);
|
|
4112
|
+
}
|
|
3241
4113
|
}
|
|
3242
4114
|
}
|
|
3243
4115
|
function globalCommand(args) {
|
|
3244
4116
|
const [action, ...names] = args.positional;
|
|
3245
|
-
const yours =
|
|
4117
|
+
const yours = yourGlobals().filter((m) => m.harnesses.length);
|
|
3246
4118
|
if (action === "move") return moveGlobal(args, names, yours);
|
|
3247
4119
|
if (action === "keep" || action === "unkeep") {
|
|
3248
4120
|
const unknown = names.filter((n) => !yours.some((m) => m.name === n));
|
|
@@ -3267,7 +4139,7 @@ function repoRoot(repo) {
|
|
|
3267
4139
|
if (named.length === 1) return named[0];
|
|
3268
4140
|
if (named.length > 1) throw new Error(`Several projects are named ${repo}: ${named.map(tildify).join(", ")}. Pass its path.`);
|
|
3269
4141
|
const path = expandHome(repo);
|
|
3270
|
-
if (!
|
|
4142
|
+
if (!existsSync14(path)) throw new Error(`No project ${repo} (pass its path, or see skilllib projects)`);
|
|
3271
4143
|
const root = projectHere(path);
|
|
3272
4144
|
if (root !== path) throw new Error(root ? `${tildify(path)} is inside ${tildify(root)}: pass the repo's top folder` : `${tildify(path)} isn't in a git repo`);
|
|
3273
4145
|
return root;
|
|
@@ -3279,14 +4151,16 @@ function moveGlobal(args, names, yours) {
|
|
|
3279
4151
|
const roots = [...new Set(args.to.map(repoRoot))];
|
|
3280
4152
|
const skills = yours.filter((m) => names.includes(m.name));
|
|
3281
4153
|
const moves = moveGlobalSkills(skills, roots, { allowTracked: args.allowTracked });
|
|
4154
|
+
rememberProjects(roots);
|
|
3282
4155
|
if (moves.some((m) => !m.moved)) process.exitCode = 1;
|
|
3283
4156
|
if (args.json) {
|
|
3284
4157
|
return json(
|
|
3285
4158
|
"global move",
|
|
3286
|
-
moves.map(({ name, moved, reason, changes }) => ({
|
|
4159
|
+
moves.map(({ name, moved, reason, changes, left }) => ({
|
|
3287
4160
|
skill: name,
|
|
3288
4161
|
moved,
|
|
3289
4162
|
...reason ? { reason } : {},
|
|
4163
|
+
...left ? { left } : {},
|
|
3290
4164
|
projects: changes.map(([root, c]) => ({ project: basename9(root), path: root, change: withDirs(root, [c])[0] }))
|
|
3291
4165
|
}))
|
|
3292
4166
|
);
|
|
@@ -3294,20 +4168,18 @@ function moveGlobal(args, names, yours) {
|
|
|
3294
4168
|
for (const m of moves) {
|
|
3295
4169
|
if (m.moved) success(`${m.name} now loads only in ${m.changes.map(([r]) => basename9(r)).join(", ")} (the global copy is in skilllib restore)`);
|
|
3296
4170
|
else error(`${m.name}: ${m.reason}`);
|
|
3297
|
-
for (const [root, c] of m.changes) {
|
|
3298
|
-
|
|
3299
|
-
if (c.uncommitted?.length) warn(` ${basename9(root)}: no link in ${c.uncommitted.join(", ")}: git tracks it but not the skill's copy, so the link would break for teammates. Commit the copy, then run skilllib link --allow-tracked.`);
|
|
3300
|
-
}
|
|
4171
|
+
for (const [root, c] of m.changes) warnHeldLinks(root, c, `in ${basename9(root)}`);
|
|
4172
|
+
if (m.left) warn(`${m.name}: ${m.left.join(", ")} now point${m.left.length === 1 ? "s" : ""} at nothing (the repo's own copy serves that package): remove ${m.left.length === 1 ? "it" : "them"}`);
|
|
3301
4173
|
}
|
|
3302
4174
|
if (moves.some((m) => m.moved)) info(dim("Commit skilllib.json and the skill folders in each repo."));
|
|
3303
4175
|
}
|
|
3304
4176
|
function tidy(args) {
|
|
3305
4177
|
const names = args.positional;
|
|
3306
|
-
const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ?
|
|
4178
|
+
const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve8(base, args.keep) : expandHome(args.keep)])) : {};
|
|
3307
4179
|
if (args.keep && !names.length) {
|
|
3308
4180
|
throw new Error("Usage: skilllib tidy <name>... --keep <folder>");
|
|
3309
4181
|
}
|
|
3310
|
-
const targets = args.global ? [null] : args
|
|
4182
|
+
const targets = args.global ? [null] : projectsFor(args);
|
|
3311
4183
|
const problems = [];
|
|
3312
4184
|
for (const name of names) {
|
|
3313
4185
|
const where = targets.map((root) => ({ root, ...foldersOf(root, name) })).filter((w) => w.all.length);
|
|
@@ -3335,30 +4207,35 @@ function tidy(args) {
|
|
|
3335
4207
|
for (const { label, report } of reports) {
|
|
3336
4208
|
const plans = report.plans.filter((p) => !names.length || names.includes(p.name));
|
|
3337
4209
|
const conflicts = report.conflicts.filter((c) => !names.length || names.includes(c.name));
|
|
3338
|
-
|
|
4210
|
+
const skippedNamed = names.length ? report.skipped.filter((x) => names.includes(x.name) && x.differs) : [];
|
|
4211
|
+
if (skippedNamed.length) process.exitCode = 1;
|
|
4212
|
+
if (!plans.length && !conflicts.length && !skippedNamed.length && !args.all) success(`${label}: nothing to tidy (no extra copies or links)`);
|
|
3339
4213
|
for (const plan of plans) {
|
|
3340
|
-
const
|
|
3341
|
-
const
|
|
3342
|
-
if (
|
|
3343
|
-
|
|
3344
|
-
held +=
|
|
4214
|
+
const wouldHold = args.allowGit ? [] : gitVisibleSteps(plan);
|
|
4215
|
+
const result = args.dryRun ? { applied: plan.steps.filter((s) => !wouldHold.includes(s)), held: wouldHold } : applyTidy(plan, { git: args.allowGit ? "go" : "keep" });
|
|
4216
|
+
if (result.applied.length) info(`${args.dryRun ? dim("would") : green("\u2713")} ${label} \xB7 ${plan.name}: ${result.applied.map((s) => describeStep(s, plan.root)).join(", ")}`);
|
|
4217
|
+
for (const step of result.held) info(`${yellow("!")} ${label} \xB7 ${plan.name}: ${args.dryRun ? "would be held" : "held"} back, git would see it: ${describeStep(step, plan.root)}`);
|
|
4218
|
+
held += result.held.length;
|
|
3345
4219
|
changed += result.applied.length;
|
|
3346
4220
|
}
|
|
3347
|
-
if (args.keep
|
|
4221
|
+
if (conflicts.length && (args.keep || names.length)) process.exitCode = 1;
|
|
3348
4222
|
for (const c of conflicts) {
|
|
3349
4223
|
warn(`${label} \xB7 ${c.name}: the copies differ`);
|
|
3350
4224
|
for (const copy of c.copies) {
|
|
3351
|
-
const runs = copy.runs.map((id) =>
|
|
3352
|
-
info(` ${
|
|
4225
|
+
const runs = copy.runs.map((id) => harness(id).name);
|
|
4226
|
+
info(` ${folderLabel(c.root, copy.dir)}${runs.length ? dim(` run by ${runs.join(", ")}`) : ""}${c.managed === copy.dir ? dim(" (skilllib's copy)") : ""}`);
|
|
3353
4227
|
}
|
|
3354
|
-
const pick =
|
|
4228
|
+
const pick = folderLabel(c.root, c.managed ?? c.copies[0].dir);
|
|
3355
4229
|
info(dim(` keep one: skilllib tidy ${c.name}${args.global ? " --global" : ""} --keep ${pick}`));
|
|
3356
4230
|
}
|
|
3357
4231
|
for (const s of report.skipped.filter((x) => !names.length || names.includes(x.name))) info(dim(`- ${label} \xB7 ${s.name}: skipped, ${s.reason}`));
|
|
3358
4232
|
}
|
|
3359
4233
|
if (args.dryRun) info(dim("\nNothing changed (--dry-run)."));
|
|
3360
4234
|
else if (changed) info(dim("\nReplaced copies are in ~/.skilllib/tidy-backup; `skilllib restore` puts them back."));
|
|
3361
|
-
if (held)
|
|
4235
|
+
if (held) {
|
|
4236
|
+
process.exitCode = 1;
|
|
4237
|
+
info(dim(`${held} change${held === 1 ? "" : "s"} held back because git would see them. Re-run with --allow-git to make them.`));
|
|
4238
|
+
}
|
|
3362
4239
|
}
|
|
3363
4240
|
function agentSkill(args) {
|
|
3364
4241
|
const action = args.positional[0];
|
|
@@ -3378,6 +4255,7 @@ function agentSkill(args) {
|
|
|
3378
4255
|
if (args.json) return json("agent-skill", { state: agentSkillState(), removed });
|
|
3379
4256
|
return removed.length ? success(`Removed from ${removed.map((d) => tildify(d)).join(" and ")}`) : info("The skilllib skill isn't installed.");
|
|
3380
4257
|
}
|
|
4258
|
+
if (action) throw new Error("Usage: skilllib agent-skill [install|remove]");
|
|
3381
4259
|
const state = agentSkillState();
|
|
3382
4260
|
if (args.json) return json("agent-skill", { state, dirs: agentSkillDirs() });
|
|
3383
4261
|
if (state === "installed") success(`Your agents can use skilllib ${dim(`(${agentSkillDirs().map((d) => tildify(d)).join(", ")})`)}`);
|
|
@@ -3386,21 +4264,29 @@ function agentSkill(args) {
|
|
|
3386
4264
|
}
|
|
3387
4265
|
|
|
3388
4266
|
export {
|
|
4267
|
+
realPath,
|
|
3389
4268
|
skilllibHome,
|
|
3390
4269
|
PROJECT_SKILLS_DIR,
|
|
3391
4270
|
HARNESSES,
|
|
3392
4271
|
harness,
|
|
4272
|
+
installDirs,
|
|
3393
4273
|
readSkillInfo,
|
|
4274
|
+
treeHash,
|
|
4275
|
+
isDir,
|
|
3394
4276
|
versionHistory,
|
|
3395
4277
|
forgetLatest,
|
|
3396
4278
|
latestVersion,
|
|
3397
4279
|
projectHere,
|
|
3398
4280
|
readManifest,
|
|
4281
|
+
knownProjects,
|
|
4282
|
+
rememberProjects,
|
|
4283
|
+
readableRoots,
|
|
3399
4284
|
setJsonMode,
|
|
3400
4285
|
isJsonMode,
|
|
3401
4286
|
json,
|
|
3402
4287
|
error,
|
|
3403
4288
|
tildify,
|
|
4289
|
+
localStamp,
|
|
3404
4290
|
readConfig,
|
|
3405
4291
|
addRoot,
|
|
3406
4292
|
removeRoot,
|
|
@@ -3419,50 +4305,51 @@ export {
|
|
|
3419
4305
|
recordOrigin,
|
|
3420
4306
|
librarySkills,
|
|
3421
4307
|
librarySkillDir,
|
|
3422
|
-
|
|
4308
|
+
notWritable,
|
|
4309
|
+
inTheWaySaid,
|
|
4310
|
+
entryExists,
|
|
4311
|
+
survives,
|
|
4312
|
+
blindWithoutGlobal,
|
|
4313
|
+
strandedLinks,
|
|
4314
|
+
blindSaid,
|
|
4315
|
+
trackBlocker,
|
|
3423
4316
|
projectStatus,
|
|
3424
4317
|
linkEverywhere,
|
|
3425
4318
|
relinkDependency,
|
|
4319
|
+
addRefusal,
|
|
3426
4320
|
addSkill,
|
|
3427
4321
|
removeSkill,
|
|
3428
|
-
syncSkill,
|
|
3429
4322
|
updateProject,
|
|
3430
4323
|
importSkill,
|
|
3431
4324
|
removeUntracked,
|
|
3432
4325
|
projectsUsing,
|
|
3433
4326
|
deleteLibrarySkill,
|
|
3434
4327
|
deleteGlobal,
|
|
3435
|
-
unloadGlobal,
|
|
3436
|
-
linkGlobal,
|
|
3437
4328
|
createSkill,
|
|
3438
4329
|
listBackups,
|
|
4330
|
+
movedTogether,
|
|
3439
4331
|
restoreBackup,
|
|
3440
4332
|
gitInfo,
|
|
3441
4333
|
relativeTo,
|
|
3442
|
-
claudePlugins,
|
|
3443
4334
|
installedPlugins,
|
|
3444
4335
|
cursorPlugins,
|
|
3445
4336
|
forgetClaudeBinary,
|
|
3446
4337
|
claudeBinary,
|
|
3447
|
-
turnOffIn,
|
|
3448
4338
|
setPluginOn,
|
|
3449
4339
|
uninstallPlugin,
|
|
3450
4340
|
updatePlugin,
|
|
3451
4341
|
pluginBackups,
|
|
3452
4342
|
restorePlugin,
|
|
3453
|
-
cursorPluginSkills,
|
|
3454
4343
|
agentSkillState,
|
|
3455
4344
|
installAgentSkill,
|
|
3456
4345
|
removeAgentSkill,
|
|
3457
4346
|
refreshAgentSkill,
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
describeStep,
|
|
3463
|
-
copyLabel,
|
|
3464
|
-
addedAt,
|
|
4347
|
+
foldersOf,
|
|
4348
|
+
runFix,
|
|
4349
|
+
findIssues,
|
|
4350
|
+
usageIssues,
|
|
3465
4351
|
scanUsage,
|
|
4352
|
+
skillOf,
|
|
3466
4353
|
moveGlobalSkills,
|
|
3467
4354
|
parseArgs,
|
|
3468
4355
|
projectOfFactory,
|