skilllib 1.11.2 → 1.11.3
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 +31 -25
- package/dist/{chunk-OFWHWIER.js → chunk-3XDWEHI4.js} +718 -437
- package/dist/index.js +43 -13
- package/dist/json.d.cts +2 -1
- package/dist/json.d.ts +2 -1
- package/dist/{tui-NGTOOVAQ.js → tui-WSHQRBPF.js} +291 -700
- package/package.json +1 -1
- package/skills/skilllib/SKILL.md +4 -4
|
@@ -3,25 +3,20 @@ import {
|
|
|
3
3
|
PROJECT_SKILLS_DIR,
|
|
4
4
|
addRoot,
|
|
5
5
|
addSkill,
|
|
6
|
-
addedAt,
|
|
7
6
|
agentSkillState,
|
|
8
7
|
allowTrackedLinks,
|
|
9
|
-
applyTidy,
|
|
10
8
|
claudeBinary,
|
|
11
|
-
claudePlugins,
|
|
12
|
-
copyLabel,
|
|
13
9
|
createSkill,
|
|
14
|
-
cursorPluginSkills,
|
|
15
10
|
cursorPlugins,
|
|
16
11
|
deleteGlobal,
|
|
17
12
|
deleteLibrarySkill,
|
|
18
|
-
describeStep,
|
|
19
13
|
discoverProjects,
|
|
20
14
|
enabledHarnesses,
|
|
15
|
+
findIssues,
|
|
16
|
+
foldersOf,
|
|
21
17
|
forgetClaudeBinary,
|
|
22
18
|
forgetLatest,
|
|
23
19
|
gitInfo,
|
|
24
|
-
gitVisibleSteps,
|
|
25
20
|
harness,
|
|
26
21
|
harnessesChosen,
|
|
27
22
|
importSkill,
|
|
@@ -33,12 +28,10 @@ import {
|
|
|
33
28
|
librarySkillDir,
|
|
34
29
|
librarySkills,
|
|
35
30
|
linkEverywhere,
|
|
36
|
-
linkGlobal,
|
|
37
31
|
listBackups,
|
|
32
|
+
localStamp,
|
|
38
33
|
machineSkills,
|
|
39
34
|
moveGlobalSkills,
|
|
40
|
-
planGlobalTidy,
|
|
41
|
-
planProjectTidy,
|
|
42
35
|
pluginBackups,
|
|
43
36
|
projectHere,
|
|
44
37
|
projectOfFactory,
|
|
@@ -47,6 +40,7 @@ import {
|
|
|
47
40
|
readConfig,
|
|
48
41
|
readManifest,
|
|
49
42
|
readSkillInfo,
|
|
43
|
+
readableRoots,
|
|
50
44
|
recordOrigin,
|
|
51
45
|
relativeTo,
|
|
52
46
|
relinkDependency,
|
|
@@ -56,6 +50,7 @@ import {
|
|
|
56
50
|
removeUntracked,
|
|
57
51
|
restoreBackup,
|
|
58
52
|
restorePlugin,
|
|
53
|
+
runFix,
|
|
59
54
|
scanUsage,
|
|
60
55
|
setHarnesses,
|
|
61
56
|
setHidden,
|
|
@@ -63,23 +58,22 @@ import {
|
|
|
63
58
|
setPluginOn,
|
|
64
59
|
skilllibHome,
|
|
65
60
|
skillsLoadedIn,
|
|
66
|
-
syncSkill,
|
|
67
61
|
tildify,
|
|
68
|
-
|
|
62
|
+
treeHash,
|
|
69
63
|
uninstallPlugin,
|
|
70
|
-
unloadGlobal,
|
|
71
64
|
updatePlugin,
|
|
72
65
|
updateProject,
|
|
66
|
+
usageIssues,
|
|
73
67
|
versionHistory,
|
|
74
68
|
visibilityOf,
|
|
75
69
|
visibleProjects
|
|
76
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-3XDWEHI4.js";
|
|
77
71
|
|
|
78
72
|
// src/tui/index.tsx
|
|
79
73
|
import { render } from "ink";
|
|
80
74
|
|
|
81
75
|
// src/tui/App.tsx
|
|
82
|
-
import { useEffect, useRef, useState } from "react";
|
|
76
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
83
77
|
import { Box as Box2, Text as Text2, useApp, useInput, useWindowSize } from "ink";
|
|
84
78
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
85
79
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -135,7 +129,6 @@ function ListPanel({
|
|
|
135
129
|
rows,
|
|
136
130
|
selected,
|
|
137
131
|
empty,
|
|
138
|
-
filter,
|
|
139
132
|
header,
|
|
140
133
|
headerHeight = 0
|
|
141
134
|
}) {
|
|
@@ -143,7 +136,7 @@ function ListPanel({
|
|
|
143
136
|
const offset = Math.min(Math.max(0, selected - Math.floor(visible / 2)), Math.max(0, rows.length - visible));
|
|
144
137
|
const shown = rows.slice(offset, offset + visible);
|
|
145
138
|
const inner = width - 4;
|
|
146
|
-
const hint =
|
|
139
|
+
const hint = rows.length > visible ? `${selected + 1}/${rows.length}` : "";
|
|
147
140
|
return /* @__PURE__ */ jsxs(Panel, { title, hint, focused, width, height, children: [
|
|
148
141
|
header,
|
|
149
142
|
rows.length === 0 ? /* @__PURE__ */ jsx(Box, { paddingTop: 1, paddingX: 1, children: typeof empty === "string" ? /* @__PURE__ */ jsx(Text, { color: color.muted, children: empty }) : empty }) : shown.map((row, i) => {
|
|
@@ -166,7 +159,6 @@ function ListPanel({
|
|
|
166
159
|
backgroundColor: bg,
|
|
167
160
|
color: c.color ?? (active && focused ? color.text : void 0),
|
|
168
161
|
bold: c.bold || active && focused && c.grow,
|
|
169
|
-
dimColor: c.dim,
|
|
170
162
|
wrap: "truncate-end",
|
|
171
163
|
children: fit(c.text, c.grow ? growWidth : c.width ?? c.text.length)
|
|
172
164
|
},
|
|
@@ -199,7 +191,6 @@ import { spawnSync } from "child_process";
|
|
|
199
191
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
200
192
|
import { join } from "path";
|
|
201
193
|
var EXCERPT_CHARS = 1200;
|
|
202
|
-
var tildify2 = (p) => tildify(p);
|
|
203
194
|
function skillExcerpt(dir, chars) {
|
|
204
195
|
const file = join(dir, "SKILL.md");
|
|
205
196
|
if (!existsSync(file)) return { text: "(SKILL.md not found)", files: 0, size: 0 };
|
|
@@ -223,7 +214,7 @@ function buildReviewPrompt(skills, ctx) {
|
|
|
223
214
|
`- Description: ${s.description || "(none)"}`,
|
|
224
215
|
`- Source: ${s.source}`,
|
|
225
216
|
`- Installed via: ${s.installedHow}`,
|
|
226
|
-
`- Location: ${
|
|
217
|
+
`- Location: ${tildify(s.path)}${s.links.length ? ` (also linked from ${s.links.map(tildify).join(", ")})` : ""}`,
|
|
227
218
|
`- Loaded by: ${s.loadedBy}`,
|
|
228
219
|
...s.vendor ? [`- Vendor-managed: ${s.vendor}`] : [],
|
|
229
220
|
...s.keptGlobal ? ["- I marked this as global on purpose: recommend changing that only for a strong reason (unused, duplicated, low quality)"] : [],
|
|
@@ -249,7 +240,7 @@ function buildReviewPrompt(skills, ctx) {
|
|
|
249
240
|
- Usage counts only cover Claude Code sessions over the last ${ctx.usageDays} days; other agents' usage isn't tracked, so treat "unused" as a strong hint, not proof.
|
|
250
241
|
|
|
251
242
|
## My projects
|
|
252
|
-
${ctx.projects.map((p) => `- ${p.name} \u2014 ${
|
|
243
|
+
${ctx.projects.map((p) => `- ${p.name} \u2014 ${tildify(p.path)}`).join("\n") || "(none)"}
|
|
253
244
|
|
|
254
245
|
You can open the paths above (skills and projects) to check what a skill does and what each project uses (package.json, README, tech stack).
|
|
255
246
|
|
|
@@ -308,7 +299,65 @@ var joined = (results) => {
|
|
|
308
299
|
const message = results.map(said).join(" \xB7 ");
|
|
309
300
|
return results.some(isFailure) ? failed(message) : message;
|
|
310
301
|
};
|
|
311
|
-
var SEVERITY_RANK = { problem: 0,
|
|
302
|
+
var SEVERITY_RANK = { problem: 0, suggestion: 1 };
|
|
303
|
+
function fixOf(c, found) {
|
|
304
|
+
return {
|
|
305
|
+
label: c.label,
|
|
306
|
+
preview: c.hint ? `${c.hint[0].toUpperCase()}${c.hint.slice(1)}. ${found.detail}` : found.detail,
|
|
307
|
+
run: () => {
|
|
308
|
+
const r = runFix(c);
|
|
309
|
+
return r.ok ? r.message : failed(r.message);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function shortOf(found) {
|
|
314
|
+
const kind = found.id.split(":")[0];
|
|
315
|
+
if (found.id === "unused:library") return "In no repo, unused 30 days";
|
|
316
|
+
if (kind === "plugin") return `Also in \u29C9 ${found.plugin.split("@")[0]}`;
|
|
317
|
+
if (kind === "cursor-plugin") return `Also in Cursor's ${found.plugin}`;
|
|
318
|
+
const short = {
|
|
319
|
+
broken: "Broken link",
|
|
320
|
+
dup: "Loaded twice",
|
|
321
|
+
tidy: "Extra copies",
|
|
322
|
+
conflict: "Copies differ",
|
|
323
|
+
review: "Not reviewed",
|
|
324
|
+
usable: "Not every agent can use it",
|
|
325
|
+
sync: "Folder missing",
|
|
326
|
+
unavailable: "Pinned, not in your library",
|
|
327
|
+
pin: "Pinned version not in your library",
|
|
328
|
+
update: "Out of date",
|
|
329
|
+
uncommitted: "Commit it first",
|
|
330
|
+
local: "Not in your library",
|
|
331
|
+
adopt: "Not tracked",
|
|
332
|
+
differs: "Differs from your library",
|
|
333
|
+
npx: "From npx skills",
|
|
334
|
+
"plugin-update": "Update available",
|
|
335
|
+
"plugin-unused": "Unused 30 days",
|
|
336
|
+
edited: "Edited here",
|
|
337
|
+
"not-in-lib": "No longer in your library",
|
|
338
|
+
twice: "Repo and global copies",
|
|
339
|
+
unused: "Unused 30 days"
|
|
340
|
+
};
|
|
341
|
+
return short[kind] ?? found.title;
|
|
342
|
+
}
|
|
343
|
+
var views = /* @__PURE__ */ new WeakMap();
|
|
344
|
+
function issueOf(found) {
|
|
345
|
+
let view = views.get(found);
|
|
346
|
+
if (!view) views.set(found, view = viewOf(found));
|
|
347
|
+
return view;
|
|
348
|
+
}
|
|
349
|
+
function viewOf(found) {
|
|
350
|
+
return {
|
|
351
|
+
id: found.id,
|
|
352
|
+
severity: found.severity,
|
|
353
|
+
title: found.title,
|
|
354
|
+
short: shortOf(found),
|
|
355
|
+
detail: found.detail,
|
|
356
|
+
skills: [...new Set(found.at?.map((a) => a.skill))],
|
|
357
|
+
decision: !found.fix,
|
|
358
|
+
fixes: (found.fix ? [found.fix] : found.choices ?? []).map((c) => fixOf(c, found))
|
|
359
|
+
};
|
|
360
|
+
}
|
|
312
361
|
function filterOf(source) {
|
|
313
362
|
return source === "lib" || source === "repo" || source === "untracked" ? "local" : source === "global" || source === "skilllib" ? "global" : source === "plugin" ? "plugins" : "vendor";
|
|
314
363
|
}
|
|
@@ -337,63 +386,25 @@ function usable(w, projectName) {
|
|
|
337
386
|
...machineIn(w, p).map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: uses(m.name), machine: m }))
|
|
338
387
|
];
|
|
339
388
|
}
|
|
340
|
-
function
|
|
341
|
-
|
|
389
|
+
function issuesAt(w, repo, skill) {
|
|
390
|
+
const root = repo === null ? null : project(w, repo).path;
|
|
391
|
+
return w.issues.filter((i) => i.at?.some((a) => a.root === root && a.skill === skill)).map(issueOf);
|
|
342
392
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
return repos.length ? `, and ${repos.join(", ")} used it in the last 30 days` : "";
|
|
393
|
+
function repoIssues(w, repo) {
|
|
394
|
+
const root = project(w, repo).path;
|
|
395
|
+
return w.issues.filter((i) => i.at?.some((a) => a.root === root)).map(issueOf);
|
|
347
396
|
}
|
|
348
|
-
function
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
{
|
|
353
|
-
id: `conflict${g}:${name}`,
|
|
354
|
-
severity: "problem",
|
|
355
|
-
title: `Copies differ: ${d.differ.map((c) => c.label + (c.runs.length ? ` (${agentNames(c.runs)})` : "")).join(" vs ")}`,
|
|
356
|
-
short: "Copies differ",
|
|
357
|
-
decision: true,
|
|
358
|
-
fixes: d.differ.filter((c) => c.canWin).map((c) => ({
|
|
359
|
-
label: `Keep the ${c.label} copy`,
|
|
360
|
-
preview: `The other ${d.differ.length === 2 ? "copy becomes a link" : "copies become links"} to it, so every agent runs this one. Replaced copies go to Settings \u203A Backups.${repo ? " If git tracks a copy that changes, skilllib asks first." : ""}`,
|
|
361
|
-
run: (w) => w.ops.tidy(repo, name, { keep: c.dir })
|
|
362
|
-
}))
|
|
363
|
-
}
|
|
364
|
-
];
|
|
365
|
-
return [
|
|
366
|
-
{
|
|
367
|
-
id: `copies${g}:${name}`,
|
|
368
|
-
severity: "warning",
|
|
369
|
-
title: repo ? "More copies or links in this repo than your agents need" : "Identical copies in several global folders",
|
|
370
|
-
short: "Extra copies",
|
|
371
|
-
decision: false,
|
|
372
|
-
fixes: [
|
|
373
|
-
{
|
|
374
|
-
label: "Keep one copy, plus the links your agents need",
|
|
375
|
-
preview: `${d.steps.join("; ")}. Replaced copies go to Settings \u203A Backups.${d.git.length ? ` Git would see ${d.git.join(", ")}: skilllib asks before ${d.git.length === 1 ? "that one" : "those"}.` : ""}`,
|
|
376
|
-
run: (w) => w.ops.tidy(repo, name)
|
|
377
|
-
}
|
|
378
|
-
]
|
|
379
|
-
}
|
|
380
|
-
];
|
|
397
|
+
function unlisted(w, repo) {
|
|
398
|
+
const root = project(w, repo).path;
|
|
399
|
+
const listed = new Set(usable(w, repo).map((u) => u.name));
|
|
400
|
+
return [...new Set(w.issues.flatMap((i) => (i.at ?? []).filter((a) => a.root === root && !listed.has(a.skill)).map((a) => a.skill)))];
|
|
381
401
|
}
|
|
382
|
-
function
|
|
383
|
-
return
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
decision: true,
|
|
389
|
-
fixes: [
|
|
390
|
-
{
|
|
391
|
-
label: "Turn the plugin off in Cursor (Settings \u203A Plugins)",
|
|
392
|
-
preview: "skilllib can't read or change Cursor's plugins. Your skill wins once the plugin is off.",
|
|
393
|
-
run: () => `Turn ${plugin} off in Cursor; skilllib can't tell whether it's on`
|
|
394
|
-
}
|
|
395
|
-
]
|
|
396
|
-
};
|
|
402
|
+
function libraryIssues(w, name) {
|
|
403
|
+
return w.issues.filter((i) => i.id === "unused:library" && i.at?.some((a) => a.skill === name)).map(issueOf);
|
|
404
|
+
}
|
|
405
|
+
function pluginIssues(w, p) {
|
|
406
|
+
const id = p.agent === "cursor" ? pluginName(p) : p.id;
|
|
407
|
+
return w.issues.filter((i) => i.plugin === id && (i.pluginAt === void 0 || i.pluginAt === (p.projectPath ?? null))).map(issueOf);
|
|
397
408
|
}
|
|
398
409
|
function globalFixes(m) {
|
|
399
410
|
return [
|
|
@@ -403,7 +414,7 @@ function globalFixes(m) {
|
|
|
403
414
|
run: () => "",
|
|
404
415
|
pickRepos: (w, repos) => w.ops.moveGlobal([m], repos)
|
|
405
416
|
},
|
|
406
|
-
m.kept ? { label: "Stop marking it as global on purpose", preview:
|
|
417
|
+
m.kept ? { label: "Stop marking it as global on purpose", preview: `${m.name} shows as Not reviewed again, like any global skill.`, run: (w) => w.ops.keepGlobal(m.name, false) } : { label: "Keep it global on purpose", preview: `Mark ${m.name} as global on purpose: it no longer shows as Not reviewed. Its other issues stay.`, run: (w) => w.ops.keepGlobal(m.name, true) },
|
|
407
418
|
{ label: "Delete it", preview: `Remove ${m.path}. A backup is kept in Settings \u203A Backups.`, run: (w) => w.ops.deleteGlobal(m) }
|
|
408
419
|
];
|
|
409
420
|
}
|
|
@@ -411,267 +422,6 @@ function machineActions(m) {
|
|
|
411
422
|
if (m.broken) return [];
|
|
412
423
|
return m.source === "global" ? globalFixes(m) : m.source === "skilllib" ? [removeAgentSkillFix()] : [];
|
|
413
424
|
}
|
|
414
|
-
function machineIssues(w, m) {
|
|
415
|
-
if (m.source === "skilllib") return [agentSkillIssue(w)].flatMap((i) => i ? [i] : []);
|
|
416
|
-
if (m.broken)
|
|
417
|
-
return [
|
|
418
|
-
{
|
|
419
|
-
id: `broken:${m.path}`,
|
|
420
|
-
severity: "problem",
|
|
421
|
-
title: "Broken link: points at a folder that no longer exists",
|
|
422
|
-
short: "Broken link",
|
|
423
|
-
decision: false,
|
|
424
|
-
fixes: [{ label: "Remove the link", preview: `Remove ${m.path}.`, run: (w2) => w2.ops.deleteGlobal(m) }]
|
|
425
|
-
}
|
|
426
|
-
];
|
|
427
|
-
const issues = [];
|
|
428
|
-
const other = w.machine.find((x) => x !== m && x.name === m.name && !x.broken && x.agents.some((a) => m.agents.includes(a)));
|
|
429
|
-
if (m.source === "global" && other && other.source !== "global")
|
|
430
|
-
issues.push({
|
|
431
|
-
id: `dup-plugin:${m.name}`,
|
|
432
|
-
severity: "problem",
|
|
433
|
-
title: `Loaded twice: also in ${other.source} ${other.where}`,
|
|
434
|
-
short: "Loaded twice",
|
|
435
|
-
// Every repo then runs the other copy instead of yours: a decision, never "fix all".
|
|
436
|
-
decision: true,
|
|
437
|
-
fixes: [
|
|
438
|
-
{
|
|
439
|
-
label: `Keep the ${other.source}'s copy, stop loading yours globally`,
|
|
440
|
-
preview: `Copy ${m.name} into your library if needed, then move ${m.path} to the backups. Every repo then runs the ${other.source}'s copy instead of yours.`,
|
|
441
|
-
run: (w2) => w2.ops.unloadGlobal(m)
|
|
442
|
-
}
|
|
443
|
-
]
|
|
444
|
-
});
|
|
445
|
-
if (m.source === "global" && m.dupes) issues.push(...dupeIssues(null, m.name, m.dupes));
|
|
446
|
-
if (m.source === "global" && m.cursorPlugin && m.agents.includes("cursor")) issues.push(cursorPluginIssue(m.name, m.cursorPlugin));
|
|
447
|
-
const seen = new Set(w.machine.filter((x) => x.name === m.name && !x.broken).flatMap((x) => x.agents));
|
|
448
|
-
const blind = m.source === "global" && m.agents.length && !m.dupes ? w.agents.filter((a) => !seen.has(a)) : [];
|
|
449
|
-
const names = blind.map((a) => harness(a).name).join(", ");
|
|
450
|
-
const link = {
|
|
451
|
-
label: `Link it for ${names}`,
|
|
452
|
-
preview: `Add a link in the global skills folder ${names} read${blind.length === 1 ? "s" : ""} (nothing is copied or moved), so ${names} load${blind.length === 1 ? "s" : ""} it in every repo too.`,
|
|
453
|
-
run: (w2) => w2.ops.linkGlobal(m, blind)
|
|
454
|
-
};
|
|
455
|
-
if (m.kept && blind.length)
|
|
456
|
-
issues.push({ id: `blind-global:${m.name}`, severity: "warning", title: `${names} can't see it: only ${m.agents.map((a) => harness(a).name).join(", ")} load${m.agents.length === 1 ? "s" : ""} it`, short: `${names} can't see it`, decision: false, fixes: [link] });
|
|
457
|
-
if (m.source === "global" && !m.kept) {
|
|
458
|
-
const [move, ...rest] = globalFixes(m);
|
|
459
|
-
issues.push({
|
|
460
|
-
id: `global:${m.name}`,
|
|
461
|
-
severity: "warning",
|
|
462
|
-
title: blind.length ? `Global, not reviewed: loads in every repo, but ${names} can't see it` : "Global, not reviewed: loads in every repo",
|
|
463
|
-
short: blind.length ? `Not reviewed \xB7 ${names} can't see it` : "Not reviewed",
|
|
464
|
-
decision: true,
|
|
465
|
-
fixes: blind.length ? [move, link, ...rest] : [move, ...rest]
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
return issues;
|
|
469
|
-
}
|
|
470
|
-
function issuesOf(w, projectName, u) {
|
|
471
|
-
const s = u.local;
|
|
472
|
-
const issues = [];
|
|
473
|
-
const shares = (m) => m.name === s.name && !m.broken && m.agents.some((a) => s.agents.includes(a));
|
|
474
|
-
const loaded = machineIn(w, project(w, projectName));
|
|
475
|
-
const g = loaded.find((m) => m.source === "global" && shares(m));
|
|
476
|
-
const vendor = loaded.find((m) => m.source !== "global" && shares(m));
|
|
477
|
-
const l = lib(w, s.name);
|
|
478
|
-
const remove = {
|
|
479
|
-
label: "Remove this repo's copy",
|
|
480
|
-
preview: s.source === "lib" ? `Delete ${s.dir}/${s.name} in ${projectName}. Your library keeps it.` : `Move ${s.dir}/${s.name} in ${projectName} to Settings \u203A Backups.`,
|
|
481
|
-
run: (w2) => w2.ops.remove(projectName, s.name)
|
|
482
|
-
};
|
|
483
|
-
if (s.missing && s.library === "missing")
|
|
484
|
-
issues.push({
|
|
485
|
-
id: `unavailable:${s.name}`,
|
|
486
|
-
severity: "problem",
|
|
487
|
-
title: "Pinned in skilllib.json, but not in your library: import it, or set SKILLLIB_HOME to the library that has it",
|
|
488
|
-
short: "Pinned, not in your library",
|
|
489
|
-
decision: true,
|
|
490
|
-
fixes: [{ label: "Remove it from skilllib.json", preview: `${projectName}'s skilllib.json stops pinning ${s.name} v${s.version}. There's no folder to delete.`, run: (w2) => w2.ops.remove(projectName, s.name) }]
|
|
491
|
-
});
|
|
492
|
-
else if (s.missing)
|
|
493
|
-
issues.push({
|
|
494
|
-
id: `missing:${s.name}`,
|
|
495
|
-
severity: "problem",
|
|
496
|
-
title: "Listed in skilllib.json but the folder is missing",
|
|
497
|
-
short: "Folder missing",
|
|
498
|
-
decision: false,
|
|
499
|
-
fixes: [{ label: "Restore it (sync)", preview: `Reinstall ${s.name} v${s.version} into ${s.dir}.`, run: (w2) => w2.ops.restore(projectName, s.name) }]
|
|
500
|
-
});
|
|
501
|
-
else if (s.source === "lib" && s.library === "missing")
|
|
502
|
-
issues.push({
|
|
503
|
-
id: `not-in-lib:${s.name}`,
|
|
504
|
-
severity: "problem",
|
|
505
|
-
title: "Tracked here, but no longer in your library",
|
|
506
|
-
short: "Not in your library",
|
|
507
|
-
decision: false,
|
|
508
|
-
fixes: [{ label: "Copy it back into your library", preview: `Copy this repo's ${s.name} into your library, so it can be restored and shared again.`, run: (w2) => w2.ops.copyToLibrary(projectName, s.name) }]
|
|
509
|
-
});
|
|
510
|
-
if (g?.kept)
|
|
511
|
-
issues.push({
|
|
512
|
-
id: `kept-g:${s.name}`,
|
|
513
|
-
severity: "warning",
|
|
514
|
-
title: `Also global in ${g.where}, and you keep it global: this repo's copy is extra`,
|
|
515
|
-
short: "Extra: you keep it global",
|
|
516
|
-
decision: true,
|
|
517
|
-
fixes: [
|
|
518
|
-
...s.source !== "repo" ? [{ ...remove, label: "Remove this repo's copy, keep it global" }] : [],
|
|
519
|
-
{
|
|
520
|
-
label: "Stop loading it globally after all",
|
|
521
|
-
preview: `Copy ${s.name} into your library if needed, then move ${g.path} to the backups. Every other repo stops seeing it${usedElsewhere(w, projectName, s.name)}.`,
|
|
522
|
-
run: (w2) => w2.ops.unloadGlobal(g)
|
|
523
|
-
}
|
|
524
|
-
]
|
|
525
|
-
});
|
|
526
|
-
else if (g)
|
|
527
|
-
issues.push({
|
|
528
|
-
id: `twice-g:${s.name}`,
|
|
529
|
-
severity: "problem",
|
|
530
|
-
title: `Loaded twice: also global in ${g.where}`,
|
|
531
|
-
short: "Loaded twice",
|
|
532
|
-
// Unloading the global copy changes every repo: a decision, never "fix all".
|
|
533
|
-
decision: true,
|
|
534
|
-
fixes: [
|
|
535
|
-
{
|
|
536
|
-
label: "Keep this repo's copy, stop loading it globally",
|
|
537
|
-
preview: `Copy ${s.name} into your library if needed, then move ${g.path} to the backups. Every other repo stops seeing it${usedElsewhere(w, projectName, s.name)}.`,
|
|
538
|
-
run: (w2) => w2.ops.unloadGlobal(g)
|
|
539
|
-
},
|
|
540
|
-
...s.source !== "repo" ? [{ ...remove, label: "Keep it global, remove it from this repo" }] : []
|
|
541
|
-
]
|
|
542
|
-
});
|
|
543
|
-
const cursorOnly = vendor?.source === "plugin" && w.agents.includes("claude-code") && !vendor.agents.includes("claude-code");
|
|
544
|
-
if (vendor && cursorOnly)
|
|
545
|
-
issues.push({
|
|
546
|
-
id: `twice-p:${s.name}`,
|
|
547
|
-
severity: "warning",
|
|
548
|
-
title: `Same name as a skill in plugin ${vendor.where}: Claude Code has it off in ${projectName}, but Cursor ignores repo settings and lists both`,
|
|
549
|
-
short: "Cursor also lists a plugin's copy",
|
|
550
|
-
decision: true,
|
|
551
|
-
fixes: [pluginFixFor(w, vendor.where)]
|
|
552
|
-
});
|
|
553
|
-
else if (vendor)
|
|
554
|
-
issues.push({
|
|
555
|
-
id: `twice-p:${s.name}`,
|
|
556
|
-
severity: "problem",
|
|
557
|
-
title: `Same name as a skill in ${vendor.source} ${vendor.where}`,
|
|
558
|
-
short: `Same name as a ${vendor.source} skill`,
|
|
559
|
-
decision: true,
|
|
560
|
-
fixes: [
|
|
561
|
-
...s.source !== "repo" ? [{ ...remove, label: `Remove this repo's copy, use the ${vendor.source}'s` }] : [],
|
|
562
|
-
vendor.source === "plugin" ? {
|
|
563
|
-
label: `Turn the plugin off in ${projectName} only`,
|
|
564
|
-
preview: `Sets "${vendor.where}": false in ${projectName}'s .claude/settings.local.json (claude plugin disable --scope local), a local-only file git ignores. Other repos keep it.${vendor.agents.includes("cursor") ? " Cursor ignores repo settings, so it still lists both." : ""}`,
|
|
565
|
-
run: (w2) => w2.ops.pluginOffHere(projectName, vendor.where)
|
|
566
|
-
} : {
|
|
567
|
-
label: `Turn it off in ${vendor.where}`,
|
|
568
|
-
preview: "skilllib can't turn it off for you.",
|
|
569
|
-
run: () => `Turn ${vendor.where} off at its source; skilllib picks it up next time`
|
|
570
|
-
}
|
|
571
|
-
]
|
|
572
|
-
});
|
|
573
|
-
if (s.dupes) issues.push(...dupeIssues(projectName, s.name, s.dupes));
|
|
574
|
-
if (s.cursorPlugin && s.agents.includes("cursor")) issues.push(cursorPluginIssue(s.name, s.cursorPlugin));
|
|
575
|
-
if (s.source === "lib" && s.edited)
|
|
576
|
-
issues.push({
|
|
577
|
-
id: `edited:${s.name}`,
|
|
578
|
-
severity: "warning",
|
|
579
|
-
title: `Edited here: differs from library v${s.version}`,
|
|
580
|
-
short: "Edited here",
|
|
581
|
-
decision: true,
|
|
582
|
-
fixes: [
|
|
583
|
-
{ label: `Save as v${(l?.latest ?? 0) + 1} in your library`, preview: `Your library gets v${(l?.latest ?? 0) + 1} with these edits. Other repos can update to it.`, run: (w2) => w2.ops.saveEdits(projectName, s.name) },
|
|
584
|
-
{ label: "Discard the edits", preview: `Replace ${s.dir}/${s.name} with library v${s.version} (edits backed up).`, run: (w2) => w2.ops.discardEdits(projectName, s.name) }
|
|
585
|
-
]
|
|
586
|
-
});
|
|
587
|
-
if (s.source === "lib" && l && s.version < l.latest && !s.edited && !s.missing)
|
|
588
|
-
issues.push({
|
|
589
|
-
id: `outdated:${s.name}`,
|
|
590
|
-
severity: "warning",
|
|
591
|
-
title: `Update available: v${s.version} \u2192 v${l.latest}`,
|
|
592
|
-
short: `Update to v${l.latest}`,
|
|
593
|
-
decision: false,
|
|
594
|
-
fixes: [{ label: `Update to v${l.latest}`, preview: `Replace ${s.dir}/${s.name} with library v${l.latest}.`, run: (w2) => w2.ops.update(projectName, s.name) }]
|
|
595
|
-
});
|
|
596
|
-
const blind = w.agents.filter((a) => !u.agents.includes(a));
|
|
597
|
-
const unshared = blind.filter((a) => s.unshared?.includes(a));
|
|
598
|
-
const linkable = blind.filter((a) => !unshared.includes(a));
|
|
599
|
-
const commitFirst = unshared.length ? ` ${agentNames(unshared)} ${unshared.length === 1 ? "needs" : "need"} it committed first (git tracks the folder they read).` : "";
|
|
600
|
-
if (!s.missing && linkable.length)
|
|
601
|
-
issues.push({
|
|
602
|
-
id: `blind:${s.name}`,
|
|
603
|
-
severity: "warning",
|
|
604
|
-
title: `${agentNames(blind)} can't see it`,
|
|
605
|
-
short: `${agentNames(blind)} can't see it`,
|
|
606
|
-
decision: false,
|
|
607
|
-
fixes: [
|
|
608
|
-
{
|
|
609
|
-
label: "Link it for every agent",
|
|
610
|
-
preview: `Add links (nothing is copied or moved), so ${agentNames(linkable)} ${linkable.length === 1 ? "loads" : "load"} it too.${commitFirst}`,
|
|
611
|
-
run: (w2) => w2.ops.link(projectName, s.name)
|
|
612
|
-
}
|
|
613
|
-
]
|
|
614
|
-
});
|
|
615
|
-
else if (!s.missing && unshared.length)
|
|
616
|
-
issues.push({
|
|
617
|
-
id: `blind:${s.name}`,
|
|
618
|
-
severity: "warning",
|
|
619
|
-
title: `${agentNames(unshared)} can't see it: commit ${s.dir}/${s.name} first, then link it (git tracks the folder they read, and a link to an uncommitted copy would break for teammates)`,
|
|
620
|
-
short: `${agentNames(unshared)} can't see it: commit it first`,
|
|
621
|
-
// Nothing skilllib can do: committing is yours.
|
|
622
|
-
decision: true,
|
|
623
|
-
fixes: []
|
|
624
|
-
});
|
|
625
|
-
const differ = !!s.dupes?.differ;
|
|
626
|
-
if (s.source !== "lib" && s.library === "differs" && !differ)
|
|
627
|
-
issues.push({
|
|
628
|
-
id: `differs:${s.name}`,
|
|
629
|
-
severity: "hint",
|
|
630
|
-
title: "Differs from the copy in your library",
|
|
631
|
-
short: "Differs from library",
|
|
632
|
-
decision: true,
|
|
633
|
-
fixes: [
|
|
634
|
-
{ label: "Update your library from this copy", preview: `Your library gets a new version of ${s.name} with this repo's content.`, run: (w2) => w2.ops.copyToLibrary(projectName, s.name) },
|
|
635
|
-
...s.source === "untracked" ? [{ label: "Replace it with the library version", preview: `Replace ${s.dir}/${s.name} with your library's newest version and track it (this copy backed up).`, run: (w2) => w2.ops.discardEdits(projectName, s.name) }] : []
|
|
636
|
-
]
|
|
637
|
-
});
|
|
638
|
-
if (s.source === "untracked" && s.library === "same" && !differ)
|
|
639
|
-
issues.push({
|
|
640
|
-
id: `adopt:${s.name}`,
|
|
641
|
-
severity: "hint",
|
|
642
|
-
title: "Same as your library skill, but not tracked",
|
|
643
|
-
short: "Not tracked",
|
|
644
|
-
// It writes to the repo's skilllib.json (and your library): your call, never in Fix all (as in doctor, #11).
|
|
645
|
-
decision: true,
|
|
646
|
-
fixes: [{ label: "Track it", preview: `Record ${s.name} in skilllib.json so library updates reach it.`, run: (w2) => w2.ops.track(projectName, s.name) }]
|
|
647
|
-
});
|
|
648
|
-
if (s.source === "untracked" && !l && !vendor && !differ)
|
|
649
|
-
issues.push({
|
|
650
|
-
id: `local:${s.name}`,
|
|
651
|
-
severity: "hint",
|
|
652
|
-
title: "Only exists in this repo",
|
|
653
|
-
short: "Only in this repo",
|
|
654
|
-
// It writes to the repo's skilllib.json (and your library): your call, never in Fix all (as in doctor, #11).
|
|
655
|
-
decision: true,
|
|
656
|
-
fixes: [
|
|
657
|
-
{
|
|
658
|
-
label: "Import it into your library",
|
|
659
|
-
preview: `Copy ${s.name} into your library and track it here, so other repos can use it.`,
|
|
660
|
-
run: (w2) => w2.ops.importLocal(projectName, s.name)
|
|
661
|
-
}
|
|
662
|
-
]
|
|
663
|
-
});
|
|
664
|
-
if (s.source === "lib" && w.days && u.uses === 0 && !s.missing && (s.added ?? 0) <= Date.now() - 30 * 24 * 60 * 60 * 1e3)
|
|
665
|
-
issues.push({
|
|
666
|
-
id: `unused:${s.name}`,
|
|
667
|
-
severity: "hint",
|
|
668
|
-
title: "Not used here in 30 days (Claude Code)",
|
|
669
|
-
short: "Unused 30 days",
|
|
670
|
-
decision: true,
|
|
671
|
-
fixes: [{ ...remove, label: "Remove it from this repo", preview: `Delete ${s.dir}/${s.name} in ${projectName}. Your library keeps it.` }]
|
|
672
|
-
});
|
|
673
|
-
return issues;
|
|
674
|
-
}
|
|
675
425
|
function agentSkillFix(w) {
|
|
676
426
|
return {
|
|
677
427
|
label: w.agentSkill === "outdated" ? "Update the skilllib skill" : "Install the skilllib skill for your agents",
|
|
@@ -686,35 +436,11 @@ function removeAgentSkillFix() {
|
|
|
686
436
|
run: (w) => w.ops.removeAgentSkill()
|
|
687
437
|
};
|
|
688
438
|
}
|
|
689
|
-
function agentSkillIssue(w) {
|
|
690
|
-
if (w.agentSkill === "installed" || !w.agents.length) return null;
|
|
691
|
-
const missing = w.agentSkill === "missing";
|
|
692
|
-
return {
|
|
693
|
-
id: "agent-skill",
|
|
694
|
-
severity: missing ? "hint" : "warning",
|
|
695
|
-
title: missing ? "Your agents don't know about skilllib" : "The skilllib skill for your agents is out of date",
|
|
696
|
-
short: missing ? "Your agents don't know about skilllib" : "Out of date",
|
|
697
|
-
decision: missing,
|
|
698
|
-
fixes: [agentSkillFix(w)]
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
439
|
function runAll(w, fixes) {
|
|
702
|
-
const
|
|
703
|
-
const
|
|
704
|
-
for (const fix of fixes) {
|
|
705
|
-
let r;
|
|
706
|
-
try {
|
|
707
|
-
r = fix.run(w);
|
|
708
|
-
} catch (e) {
|
|
709
|
-
r = failed(e.message);
|
|
710
|
-
}
|
|
711
|
-
if (isFailure(r)) bad.push(said(r));
|
|
712
|
-
else if (followUp(r)) held.push(said(r));
|
|
713
|
-
}
|
|
714
|
-
const ok = fixes.length - held.length - bad.length;
|
|
440
|
+
const bad = fixes.map((fix) => fix.run(w)).filter(isFailure).map(said);
|
|
441
|
+
const ok = fixes.length - bad.length;
|
|
715
442
|
const message = [
|
|
716
443
|
`Applied ${ok}${ok === fixes.length ? "" : ` of ${fixes.length}`} fix${fixes.length === 1 ? "" : "es"}${ok ? " (anything removed is in Settings \u203A Backups)" : ""}`,
|
|
717
|
-
held.length ? `${held.join("; ")} (fix ${held.length === 1 ? "it" : "them"} on ${held.length === 1 ? "its" : "their"} own to go ahead)` : "",
|
|
718
444
|
bad.length ? `couldn't: ${bad.join("; ")}` : ""
|
|
719
445
|
].filter(Boolean).join(" \xB7 ");
|
|
720
446
|
return bad.length ? failed(message) : message;
|
|
@@ -725,13 +451,9 @@ function worst(issues) {
|
|
|
725
451
|
function tally(issues) {
|
|
726
452
|
return {
|
|
727
453
|
problem: issues.filter((i) => i.severity === "problem").length,
|
|
728
|
-
|
|
729
|
-
hint: issues.filter((i) => i.severity === "hint").length
|
|
454
|
+
suggestion: issues.filter((i) => i.severity === "suggestion").length
|
|
730
455
|
};
|
|
731
456
|
}
|
|
732
|
-
function projectIssues(w, projectName) {
|
|
733
|
-
return usable(w, projectName).filter((u) => u.local).flatMap((u) => issuesOf(w, projectName, u).map((issue) => ({ skill: u, issue })));
|
|
734
|
-
}
|
|
735
457
|
function deleteLibraryFix(w, names) {
|
|
736
458
|
const tracked = w.projects.flatMap((p) => p.skills.filter((s) => names.includes(s.name) && s.source === "lib").map((s) => ({ repo: p.name, edited: s.edited })));
|
|
737
459
|
const repos = [...new Set(tracked.map((t) => t.repo))];
|
|
@@ -742,7 +464,7 @@ function deleteLibraryFix(w, names) {
|
|
|
742
464
|
label: `Delete ${names.length === 1 ? "" : `all ${names.length} `}from your library${repos.length ? ` and ${repos.length} repo${repos.length === 1 ? "" : "s"}\u2026` : ""}`,
|
|
743
465
|
preview: [
|
|
744
466
|
`Delete ${what} and ${names.length === 1 ? "its" : "their"} versions from your library.`,
|
|
745
|
-
repos.length ? `Removes ${names.length === 1 ? "it" : "them"} from ${repos.length === 1 ?
|
|
467
|
+
repos.length ? `Removes ${names.length === 1 ? "it" : "them"} from ${repos.length === 1 ? repos[0] : `these ${repos.length} repos: ${repos.join(", ")}`}.` : "No repo uses it.",
|
|
746
468
|
edited.length ? `${edited.join(", ")} ${edited.length === 1 ? "has" : "have"} local edits: backed up too.` : "",
|
|
747
469
|
"Every copy goes to Settings \u203A Backups.",
|
|
748
470
|
others.length ? `Untracked or the team's own copies in ${others.join(", ")} stay.` : ""
|
|
@@ -781,23 +503,18 @@ function replacePluginFix(p) {
|
|
|
781
503
|
pickRepos: (w, repos) => w.ops.replacePlugin(p, repos)
|
|
782
504
|
};
|
|
783
505
|
}
|
|
784
|
-
function pluginFixFor(w, id) {
|
|
785
|
-
const ofId = w.plugins.filter((p2) => p2.agent === "claude-code" && p2.id === id);
|
|
786
|
-
const p = ofId.find((x) => !x.repo) ?? ofId[0];
|
|
787
|
-
if (p) return replacePluginFix(p);
|
|
788
|
-
return { label: "Turn the plugin off with /plugin in Claude Code", preview: `skilllib can't tell where ${id} is installed.`, run: () => `Turn ${id} off with /plugin in Claude Code; skilllib picks it up next time` };
|
|
789
|
-
}
|
|
790
506
|
function pluginSwitchFix(p, on) {
|
|
791
507
|
const where = p.repo ? ` in ${p.repo} (${p.scope} install)` : p.scope === "claude.ai" ? " on this machine" : " in every repo";
|
|
792
508
|
return {
|
|
793
|
-
|
|
794
|
-
|
|
509
|
+
// doctor's wording, so the details list it once when an issue offers it too.
|
|
510
|
+
label: `Turn the plugin ${p.id} ${on ? "on" : "off"}`,
|
|
511
|
+
preview: `Turns ${p.id} ${on ? "on" : "off"}${where}${p.parts.length ? `, with its ${p.parts.join(", ")}` : ""}. ${p.scope === "claude.ai" || p.settingsOnly ? `Sets "${p.id}": ${on} in Claude Code's settings.` : `Runs claude plugin ${on ? "enable" : "disable"} ${p.id} --scope ${p.scope}${p.repo ? ` in ${p.repo}` : ""} (without the claude command, sets it in Claude Code's settings).`} Claude Code picks it up in its next session.`,
|
|
795
512
|
run: (w) => w.ops.setPlugin(p, on)
|
|
796
513
|
};
|
|
797
514
|
}
|
|
798
515
|
function uninstallPluginFix(p) {
|
|
799
516
|
return {
|
|
800
|
-
label:
|
|
517
|
+
label: `Uninstall the plugin ${p.id}`,
|
|
801
518
|
preview: `Runs claude plugin uninstall ${p.id} --keep-data --scope ${p.scope}${p.repo ? ` in ${p.repo}` : ""}: its ${[plural(p.skills.length, "skill"), ...p.parts].join(", ")} go, its saved data stays. Settings \u203A Backups reinstalls it.`,
|
|
802
519
|
run: (w) => w.ops.uninstallPlugin(p)
|
|
803
520
|
};
|
|
@@ -809,58 +526,6 @@ function updatePluginFix(p) {
|
|
|
809
526
|
run: (w) => w.ops.updatePlugin(p)
|
|
810
527
|
};
|
|
811
528
|
}
|
|
812
|
-
function pluginIssues(w, p) {
|
|
813
|
-
const library = new Set(w.library.map((l) => l.name));
|
|
814
|
-
const global = new Set(w.machine.filter((m) => m.source === "global" && !m.broken).map((m) => m.name));
|
|
815
|
-
if (p.agent === "cursor") {
|
|
816
|
-
const dupes = p.skills.filter((s) => library.has(s.name) || global.has(s.name));
|
|
817
|
-
return dupes.length ? [
|
|
818
|
-
{
|
|
819
|
-
id: `plugin-dupes:${p.key}`,
|
|
820
|
-
severity: "warning",
|
|
821
|
-
title: `Repeats your ${dupes.map((s) => s.name).join(", ")}: Cursor lists both while it's on`,
|
|
822
|
-
short: `Repeats ${dupes.length} of yours`,
|
|
823
|
-
decision: true,
|
|
824
|
-
fixes: [{ label: "Turn it off in Cursor (Settings \u203A Plugins)", preview: "skilllib can't read or change Cursor's plugins.", run: () => `Turn ${p.id} off in Cursor; skilllib can't tell whether it's on` }]
|
|
825
|
-
}
|
|
826
|
-
] : [];
|
|
827
|
-
}
|
|
828
|
-
const issues = [];
|
|
829
|
-
if (p.on) {
|
|
830
|
-
const repo = p.repo ? w.projects.find((x) => x.name === p.repo) : void 0;
|
|
831
|
-
const here = repo ? new Set(repo.skills.map((s) => s.name)) : void 0;
|
|
832
|
-
const yours = (n) => global.has(n) || (p.repo ? !!here?.has(n) : library.has(n));
|
|
833
|
-
const dupes = p.skills.filter((s) => yours(s.name));
|
|
834
|
-
if (dupes.length)
|
|
835
|
-
issues.push({
|
|
836
|
-
id: `plugin-dupes:${p.key}`,
|
|
837
|
-
severity: "warning",
|
|
838
|
-
title: `Repeats your ${dupes.map((s) => s.name).join(", ")}`,
|
|
839
|
-
short: `Repeats ${dupes.length} of yours`,
|
|
840
|
-
decision: true,
|
|
841
|
-
fixes: [replacePluginFix(p), pluginSwitchFix(p, false)]
|
|
842
|
-
});
|
|
843
|
-
if (w.days && p.skills.length && pluginUses(w, p) === 0)
|
|
844
|
-
issues.push({
|
|
845
|
-
id: `plugin-unused:${p.key}`,
|
|
846
|
-
severity: "hint",
|
|
847
|
-
title: `Its skills weren't used in 30 days (Claude Code)${p.parts.length ? `; it also brings ${p.parts.join(", ")}` : ""}`,
|
|
848
|
-
short: "Unused 30 days",
|
|
849
|
-
decision: true,
|
|
850
|
-
fixes: [pluginSwitchFix(p, false), ...p.scope === "claude.ai" || p.settingsOnly ? [] : [uninstallPluginFix(p)]]
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
|
-
if (p.update)
|
|
854
|
-
issues.push({
|
|
855
|
-
id: `plugin-update:${p.key}`,
|
|
856
|
-
severity: "hint",
|
|
857
|
-
title: p.update === "newer" ? "Its marketplace has a newer commit" : `Update available: ${p.version ?? "?"} \u2192 ${p.update}`,
|
|
858
|
-
short: p.update === "newer" ? "Update available" : `Update to ${p.update}`,
|
|
859
|
-
decision: true,
|
|
860
|
-
fixes: [updatePluginFix(p)]
|
|
861
|
-
});
|
|
862
|
-
return issues;
|
|
863
|
-
}
|
|
864
529
|
function originGroup(origin) {
|
|
865
530
|
if (!origin) return null;
|
|
866
531
|
const plugin = origin.match(/^plugin: (.+)$/);
|
|
@@ -923,8 +588,8 @@ function addRows(w, target, q) {
|
|
|
923
588
|
const taken = new Set(target ? project(w, target).skills.map((s) => s.name) : []);
|
|
924
589
|
const loaded = new Set(target ? usable(w, target).map((u) => u.name) : []);
|
|
925
590
|
const hit = (name, description) => !q || matches(name, q) || q.length >= 3 && description.toLowerCase().includes(q);
|
|
926
|
-
const groups = assignGroups(w.library.map((l) => groupCandidates(w, l)));
|
|
927
|
-
const shown = w.library.map((l, i) => ({ l, g: groups[i] })).filter(({ l }) => !taken.has(l.name) && hit(l.name, w.descriptions[l.name] ?? ""));
|
|
591
|
+
const groups = target ? assignGroups(w.library.map((l) => groupCandidates(w, l))) : [];
|
|
592
|
+
const shown = target ? w.library.map((l, i) => ({ l, g: groups[i] })).filter(({ l }) => !taken.has(l.name) && hit(l.name, w.descriptions[l.name] ?? "")) : [];
|
|
928
593
|
const skillRow = (l, inGroup) => ({
|
|
929
594
|
key: `lib:${l.name}`,
|
|
930
595
|
name: l.name,
|
|
@@ -936,7 +601,7 @@ function addRows(w, target, q) {
|
|
|
936
601
|
});
|
|
937
602
|
const library = [];
|
|
938
603
|
const done = /* @__PURE__ */ new Set();
|
|
939
|
-
for (const { l, g } of
|
|
604
|
+
for (const { l, g } of shown) {
|
|
940
605
|
if (done.has(l.name)) continue;
|
|
941
606
|
const members = g ? shown.filter((x) => x.g?.key === g.key).map((x) => x.l) : [l];
|
|
942
607
|
members.forEach((m) => done.add(m.name));
|
|
@@ -969,7 +634,7 @@ function addRows(w, target, q) {
|
|
|
969
634
|
}
|
|
970
635
|
|
|
971
636
|
// src/tui/prompts.ts
|
|
972
|
-
var
|
|
637
|
+
var agentNames = (agents) => agents.map((a) => harness(a).name).join(", ") || "none of my agents";
|
|
973
638
|
var localSource = (s) => s.source === "lib" ? "Your skills (skilllib library), installed in the repo" : s.source === "repo" ? "a repo's own skill (committed by the team)" : "a copy only on this machine (not committed)";
|
|
974
639
|
var localInstalled = (s) => s.origin ?? (s.source === "lib" ? "added from my library with skilllib" : s.source === "repo" ? "committed in the repo" : "copied into the repo by hand (not committed)");
|
|
975
640
|
function reviewPrompt(w, names, scope, repo) {
|
|
@@ -977,7 +642,7 @@ function reviewPrompt(w, names, scope, repo) {
|
|
|
977
642
|
const copies = w.machine.filter((m2) => m2.name === name);
|
|
978
643
|
const here = repo ? w.projects.find((p) => p.name === repo)?.skills.find((s) => s.name === name) : void 0;
|
|
979
644
|
if (here) {
|
|
980
|
-
const
|
|
645
|
+
const lib2 = w.library.find((l) => l.name === name);
|
|
981
646
|
return {
|
|
982
647
|
name,
|
|
983
648
|
description: w.descriptions[name] ?? "",
|
|
@@ -985,34 +650,34 @@ function reviewPrompt(w, names, scope, repo) {
|
|
|
985
650
|
installedHow: localInstalled(here),
|
|
986
651
|
path: here.path,
|
|
987
652
|
links: [],
|
|
988
|
-
loadedBy: `${
|
|
653
|
+
loadedBy: `${agentNames(here.agents)} \u2014 in ${repo}`,
|
|
989
654
|
vendor: null,
|
|
990
655
|
usesTotal: w.days ? totalUses(w, name) : null,
|
|
991
656
|
usesByProject: Object.entries(w.usage).flatMap(([p, u]) => u[name] ? [[p, u[name]]] : []),
|
|
992
657
|
installedIn: w.projects.filter((p) => p.skills.some((s) => s.name === name)).map((p) => p.name),
|
|
993
658
|
otherCopies: copies.map((c) => `${c.source} ${c.where}`),
|
|
994
|
-
inYourSkills:
|
|
659
|
+
inYourSkills: lib2 ? `yes, v${lib2.latest}` : null,
|
|
995
660
|
keptGlobal: void 0
|
|
996
661
|
};
|
|
997
662
|
}
|
|
998
663
|
const m = copies.find((c) => c.source === "global") ?? copies[0];
|
|
999
|
-
const
|
|
664
|
+
const lib = w.library.find((l) => l.name === name);
|
|
1000
665
|
const repos = w.projects.filter((p) => p.skills.some((s) => s.name === name));
|
|
1001
666
|
const local = repos[0]?.skills.find((s) => s.name === name);
|
|
1002
667
|
return {
|
|
1003
668
|
name,
|
|
1004
669
|
description: w.descriptions[name] ?? "",
|
|
1005
|
-
source: m ? m.source === "global" ? "my global skills" : `vendor: ${m.source} (${m.where})` :
|
|
1006
|
-
installedHow: m?.origin ?? (m ? m.where :
|
|
1007
|
-
path: m?.path ?? (
|
|
670
|
+
source: m ? m.source === "global" ? "my global skills" : `vendor: ${m.source} (${m.where})` : lib ? "Your skills (skilllib library)" : local ? localSource(local) : "a repo's own skill",
|
|
671
|
+
installedHow: m?.origin ?? (m ? m.where : lib ? "created or imported into skilllib" : local ? localInstalled(local) : "committed in the repo"),
|
|
672
|
+
path: m?.path ?? (lib ? w.ops.libraryFile(name).replace(/\/SKILL\.md$/, "") : local?.path ?? ""),
|
|
1008
673
|
links: m?.links ?? [],
|
|
1009
|
-
loadedBy: m ? `${
|
|
674
|
+
loadedBy: m ? `${agentNames(m.agents)} \u2014 in every repo` : repos.map((p) => `${agentNames(p.skills.find((s) => s.name === name).agents)} \u2014 in ${p.name}`).join("; ") || "none of my agents",
|
|
1010
675
|
vendor: m && m.source !== "global" ? "turn it off at its source (/plugin in Claude Code, claude.ai settings, or Cursor)" : null,
|
|
1011
676
|
usesTotal: w.days ? totalUses(w, name) : null,
|
|
1012
677
|
usesByProject: Object.entries(w.usage).flatMap(([p, u]) => u[name] ? [[p, u[name]]] : []),
|
|
1013
678
|
installedIn: repos.map((p) => p.name),
|
|
1014
679
|
otherCopies: copies.filter((c) => c !== m).map((c) => `${c.source} ${c.where}`),
|
|
1015
|
-
inYourSkills:
|
|
680
|
+
inYourSkills: lib ? `yes, v${lib.latest}` : null,
|
|
1016
681
|
keptGlobal: m?.kept
|
|
1017
682
|
};
|
|
1018
683
|
});
|
|
@@ -1039,7 +704,6 @@ Keep it under 200 lines. ${p ? "skilllib will offer to import it into my library
|
|
|
1039
704
|
|
|
1040
705
|
// src/tui/App.tsx
|
|
1041
706
|
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1042
|
-
var tildify3 = (p) => tildify(p);
|
|
1043
707
|
var PLACES = [
|
|
1044
708
|
["projects", "Projects"],
|
|
1045
709
|
["library", "Your skills"],
|
|
@@ -1058,13 +722,12 @@ var CHIPS = [
|
|
|
1058
722
|
];
|
|
1059
723
|
var SEV = {
|
|
1060
724
|
problem: { icon: "\u2715", color: color.red },
|
|
1061
|
-
|
|
1062
|
-
hint: { icon: "\xB7", color: color.blue }
|
|
725
|
+
suggestion: { icon: "\u26A0", color: color.yellow }
|
|
1063
726
|
};
|
|
1064
727
|
var SIDEBAR_AT = 80;
|
|
1065
728
|
var SIDEBAR_W = 28;
|
|
1066
729
|
var bySeverity = (a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
|
|
1067
|
-
function tag(u
|
|
730
|
+
function tag(u) {
|
|
1068
731
|
const s = u.local;
|
|
1069
732
|
if (s?.source === "lib") {
|
|
1070
733
|
return [`lib v${s.version}`, color.accent];
|
|
@@ -1129,10 +792,6 @@ function byName(copies) {
|
|
|
1129
792
|
for (const c of copies) groups.set(c.name, [...groups.get(c.name) ?? [], c]);
|
|
1130
793
|
return [...groups.values()].map((g) => g.sort((a, b) => COPY_RANK[a.source] - COPY_RANK[b.source]));
|
|
1131
794
|
}
|
|
1132
|
-
function mergeIssues(lists) {
|
|
1133
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1134
|
-
return lists.flat().filter((i) => !seen.has(i.id) && seen.add(i.id));
|
|
1135
|
-
}
|
|
1136
795
|
var BLOCKS = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
1137
796
|
function barChart(values, width, height = 3) {
|
|
1138
797
|
const max = Math.max(1, ...values);
|
|
@@ -1171,7 +830,7 @@ function usageLines(w, name, width, repo) {
|
|
|
1171
830
|
function skillItem(w, copies, issues, nameW, actions, repo) {
|
|
1172
831
|
const u = copies[0];
|
|
1173
832
|
const top = worst(issues);
|
|
1174
|
-
const [t, tc] = tag(u
|
|
833
|
+
const [t, tc] = tag(u);
|
|
1175
834
|
return {
|
|
1176
835
|
key: u.name,
|
|
1177
836
|
name: u.name,
|
|
@@ -1202,17 +861,44 @@ function skillItem(w, copies, issues, nameW, actions, repo) {
|
|
|
1202
861
|
const s = c.local;
|
|
1203
862
|
const path = s ? `${s.dir}/${c.name}` : c.source === "global" || c.source === "skilllib" ? `${c.where}/${c.name}` : c.where;
|
|
1204
863
|
const extra = s ? s.missing ? "missing" : s.git ? GIT_LABEL[s.git] : "" : "";
|
|
1205
|
-
const [ct, cc] = c === u ? ["", void 0] : tag(c
|
|
864
|
+
const [ct, cc] = c === u ? ["", void 0] : tag(c);
|
|
1206
865
|
b.line([" ", void 0], [path + " ", color.text], [ct ? ct + " " : "", cc], [extra, color.faint]);
|
|
866
|
+
for (const dir of s?.others ?? []) b.line([" ", void 0], [`${dir}/${c.name} `, color.text], ["another copy", color.faint]);
|
|
1207
867
|
}
|
|
1208
868
|
return b.d;
|
|
1209
869
|
}
|
|
1210
870
|
};
|
|
1211
871
|
}
|
|
1212
872
|
function projectSkillItems(w, p, nameW, ui) {
|
|
1213
|
-
return
|
|
1214
|
-
(
|
|
1215
|
-
|
|
873
|
+
return [
|
|
874
|
+
...byName(usable(w, p)).map(
|
|
875
|
+
(copies) => skillItem(w, copies, copies.some((c) => c.local) ? issuesAt(w, p, copies[0].name) : [], nameW, skillActions(w, p, copies[0], ui), p)
|
|
876
|
+
),
|
|
877
|
+
// A link to nothing loads nothing, but its issue still needs a row (and its fix, Fix all).
|
|
878
|
+
...unlisted(w, p).map((name) => {
|
|
879
|
+
const issues = issuesAt(w, p, name);
|
|
880
|
+
const top = worst(issues);
|
|
881
|
+
return {
|
|
882
|
+
key: name,
|
|
883
|
+
name,
|
|
884
|
+
group: "local",
|
|
885
|
+
issues,
|
|
886
|
+
uses: 0,
|
|
887
|
+
cells: [
|
|
888
|
+
{ text: SEV[top.severity].icon, width: 2, color: SEV[top.severity].color },
|
|
889
|
+
{ text: clip(name, nameW), width: nameW },
|
|
890
|
+
{ text: "\u2013", width: 14, color: color.faint },
|
|
891
|
+
{ text: usesText(0), width: 6, color: color.muted },
|
|
892
|
+
{ text: " " + top.short, grow: true, color: SEV[top.severity].color }
|
|
893
|
+
],
|
|
894
|
+
detail: () => {
|
|
895
|
+
const b = builder(name);
|
|
896
|
+
b.issues(issues);
|
|
897
|
+
return b.d;
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
})
|
|
901
|
+
];
|
|
1216
902
|
}
|
|
1217
903
|
var reviewOption = (w, name, ui, repo) => ({
|
|
1218
904
|
label: "Review prompt",
|
|
@@ -1233,7 +919,7 @@ function skillActions(w, p, u, ui) {
|
|
|
1233
919
|
const remove = {
|
|
1234
920
|
// Its folder gone: all that's left to remove is the skilllib.json entry (the label Health offers too).
|
|
1235
921
|
label: s?.missing ? "Remove it from skilllib.json" : "Remove from repo",
|
|
1236
|
-
preview: s?.missing ? `${p}'s skilllib.json stops pinning ${u.name}. There's no folder to delete.` : s?.source === "lib" ? `Delete ${s.dir}/${u.name} in ${p}.${inLibrary ? " Your library keeps it." : ""}` : `Move ${s?.dir}/${u.name} in ${p} to Settings \u203A Backups
|
|
922
|
+
preview: (s?.missing ? `${p}'s skilllib.json stops pinning ${u.name}. There's no folder to delete.` : s?.source === "lib" ? `Delete ${s.dir}/${u.name} in ${p}.${inLibrary ? " Your library keeps it." : ""}` : `Move ${s?.dir}/${u.name} in ${p} to Settings \u203A Backups.`) + (s?.others ? ` The copy in ${s.others.join(", ")} stays.` : ""),
|
|
1237
923
|
run: (w2) => w2.ops.remove(p, u.name)
|
|
1238
924
|
};
|
|
1239
925
|
if (s?.source === "lib")
|
|
@@ -1255,7 +941,6 @@ function skillActions(w, p, u, ui) {
|
|
|
1255
941
|
];
|
|
1256
942
|
if (s)
|
|
1257
943
|
return [
|
|
1258
|
-
inLibrary ? fixOption({ label: "Track it", preview: `Record ${u.name} in skilllib.json so library updates reach it.`, run: (w2) => w2.ops.track(p, u.name) }) : fixOption({ label: "Import it into your library", preview: `Copy ${u.name} into your library and track it here.`, run: (w2) => w2.ops.importLocal(p, u.name) }),
|
|
1259
944
|
{ label: "Edit SKILL.md", action: () => ui.edit(`${s.path}/SKILL.md`) },
|
|
1260
945
|
fixOption(remove),
|
|
1261
946
|
reviewOption(w, u.name, ui, p)
|
|
@@ -1264,13 +949,10 @@ function skillActions(w, p, u, ui) {
|
|
|
1264
949
|
}
|
|
1265
950
|
function overview(w, p) {
|
|
1266
951
|
const project2 = project(w, p);
|
|
1267
|
-
const info = w.ops.repoInfo(p);
|
|
1268
952
|
const manifest = project2.manifest === "none" ? ["no skilllib.json", color.faint] : project2.manifest === "no git" ? ["skilllib.json (not a git repo)", color.muted] : [`skilllib.json ${GIT_LABEL[project2.manifest]}`, project2.manifest === "committed" ? color.text : color.yellow];
|
|
1269
953
|
const plugins = [...new Set(machineIn(w, project2).filter((m) => m.source === "plugin").map((m) => m.where.split("@")[0]))];
|
|
1270
954
|
return [
|
|
1271
|
-
["Folder", [[
|
|
1272
|
-
["Remote", [[info.remote ?? "none", info.remote ? color.blue : color.faint]]],
|
|
1273
|
-
["Branch", [[info.branch ?? "\u2013", color.text], [info.dirty ? ` ${info.dirty} uncommitted` : "", color.yellow]]],
|
|
955
|
+
["Folder", [[tildify(project2.path), color.text]]],
|
|
1274
956
|
["Manifest", [manifest]],
|
|
1275
957
|
["Plugins", [[plugins.length ? plugins.join(", ") : "none", plugins.length ? color.magenta : color.faint]]]
|
|
1276
958
|
];
|
|
@@ -1292,7 +974,7 @@ function InfoGrid({ rows, columns, width }) {
|
|
|
1292
974
|
}
|
|
1293
975
|
function projectItems(w) {
|
|
1294
976
|
return w.projects.map((p) => {
|
|
1295
|
-
const issues =
|
|
977
|
+
const issues = repoIssues(w, p.name);
|
|
1296
978
|
const t = tally(issues);
|
|
1297
979
|
return {
|
|
1298
980
|
key: p.name,
|
|
@@ -1304,15 +986,14 @@ function projectItems(w) {
|
|
|
1304
986
|
{ text: p.name, width: 18 },
|
|
1305
987
|
{ text: p.skills.length ? `${p.skills.length} skill${p.skills.length === 1 ? "" : "s"}` : "\u2013", width: 10, color: color.muted },
|
|
1306
988
|
{ text: t.problem ? `\u2715 ${t.problem}` : "", width: 5, color: color.red },
|
|
1307
|
-
{ text: t.
|
|
1308
|
-
{ text: t.hint ? `\xB7 ${t.hint}` : "", grow: true, color: color.blue }
|
|
989
|
+
{ text: t.suggestion ? `\u26A0 ${t.suggestion}` : "", grow: true, color: color.yellow }
|
|
1309
990
|
]
|
|
1310
991
|
};
|
|
1311
992
|
});
|
|
1312
993
|
}
|
|
1313
994
|
function globalItems(w, nameW, ui) {
|
|
1314
995
|
const copies = w.machine.map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: totalUses(w, m.name), machine: m }));
|
|
1315
|
-
return byName(copies).map((g) => skillItem(w, g,
|
|
996
|
+
return byName(copies).map((g) => skillItem(w, g, issuesAt(w, null, g[0].name), nameW, globalActions(w, g[0], ui), null));
|
|
1316
997
|
}
|
|
1317
998
|
var AT_SOURCE = {
|
|
1318
999
|
global: null,
|
|
@@ -1340,7 +1021,8 @@ function libraryItems(w, nameW, ui) {
|
|
|
1340
1021
|
const uses = totalUses(w, l.name);
|
|
1341
1022
|
const addFix = { label: "Add to repos\u2026", preview: `Install ${l.name} v${l.latest} into the repos you pick.`, run: () => "", candidates: (w2) => w2.projects.filter((p) => !p.skills.some((s) => s.name === l.name)).map((p) => p.name), pickRepos: (w2, rs) => w2.ops.addTo(rs, [l.name]) };
|
|
1342
1023
|
const delFix = deleteLibraryFix(w, [l.name]);
|
|
1343
|
-
const issues =
|
|
1024
|
+
const issues = libraryIssues(w, l.name);
|
|
1025
|
+
const sev = issues[0] && SEV[issues[0].severity];
|
|
1344
1026
|
const behind = repos.filter((p) => p.skills.some((s) => s.name === l.name && s.source === "lib" && s.version < l.latest)).length;
|
|
1345
1027
|
return {
|
|
1346
1028
|
key: l.name,
|
|
@@ -1350,12 +1032,12 @@ function libraryItems(w, nameW, ui) {
|
|
|
1350
1032
|
cands: groupCandidates(w, l),
|
|
1351
1033
|
lib: l,
|
|
1352
1034
|
cells: [
|
|
1353
|
-
{ text:
|
|
1035
|
+
{ text: sev ? sev.icon : " ", width: 2, color: sev?.color },
|
|
1354
1036
|
{ text: clip(l.name, nameW), width: nameW },
|
|
1355
1037
|
{ text: `v${l.latest}`, width: 5, color: color.accent },
|
|
1356
|
-
{ text: `${repos.length} repo${repos.length === 1 ? "" : "s"}${behind ?
|
|
1038
|
+
{ text: clip(`${repos.length} repo${repos.length === 1 ? "" : "s"}${behind ? `, ${behind} behind` : ""}`, 16), width: 16, color: behind ? color.yellow : color.muted },
|
|
1357
1039
|
{ text: usesText(uses), width: 6, color: color.muted },
|
|
1358
|
-
{ text: " " + (issues[0]?.short ?? w.descriptions[l.name] ?? ""), grow: true, color:
|
|
1040
|
+
{ text: " " + (issues[0]?.short ?? w.descriptions[l.name] ?? ""), grow: true, color: sev ? sev.color : color.faint }
|
|
1359
1041
|
],
|
|
1360
1042
|
detail: (width) => {
|
|
1361
1043
|
const b = builder(l.name);
|
|
@@ -1390,43 +1072,37 @@ function libraryItems(w, nameW, ui) {
|
|
|
1390
1072
|
};
|
|
1391
1073
|
});
|
|
1392
1074
|
}
|
|
1393
|
-
function healthItems(w
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
const sev = SEV[issue.severity];
|
|
1401
|
-
return [
|
|
1402
|
-
{
|
|
1403
|
-
key: "agent-skill",
|
|
1404
|
-
name: "skilllib",
|
|
1075
|
+
function healthItems(w) {
|
|
1076
|
+
return w.issues.map((found) => {
|
|
1077
|
+
const issue = issueOf(found);
|
|
1078
|
+
const sev = SEV[issue.severity];
|
|
1079
|
+
return {
|
|
1080
|
+
key: issue.id,
|
|
1081
|
+
name: issue.title,
|
|
1405
1082
|
issues: [issue],
|
|
1406
1083
|
uses: 0,
|
|
1407
1084
|
cells: [
|
|
1408
1085
|
{ text: sev.icon, width: 2, color: sev.color },
|
|
1409
|
-
{ text:
|
|
1410
|
-
{ text: clip("Your agents", 16), width: 16, color: color.muted },
|
|
1411
|
-
{ text: " " + issue.short, grow: true, color: sev.color }
|
|
1086
|
+
{ text: issue.title, grow: true }
|
|
1412
1087
|
],
|
|
1413
1088
|
detail: (width) => {
|
|
1414
|
-
const b = builder(
|
|
1415
|
-
for (const l of wrap(
|
|
1089
|
+
const b = builder(issue.short);
|
|
1090
|
+
for (const l of wrap(found.detail, width - 4, Infinity)) b.line([l, color.muted]);
|
|
1416
1091
|
b.line();
|
|
1417
1092
|
b.issues([issue]);
|
|
1418
1093
|
return b.d;
|
|
1419
1094
|
}
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1095
|
+
};
|
|
1096
|
+
});
|
|
1422
1097
|
}
|
|
1423
|
-
var
|
|
1098
|
+
var pluginCols = (columns, nameW) => columns >= 76 ? { wide: true, name: Math.min(nameW, 17), scope: 19 } : { wide: false, name: 15, scope: 12 };
|
|
1424
1099
|
var STATE = { on: ["on", color.green], off: ["off", color.faint], unknown: ["?", color.faint] };
|
|
1425
|
-
function pluginItems(w, skillW, ui,
|
|
1426
|
-
const nameW =
|
|
1100
|
+
function pluginItems(w, columns, skillW, ui, issuesOf) {
|
|
1101
|
+
const { wide, name: nameW, scope: scopeW } = pluginCols(columns, skillW);
|
|
1427
1102
|
return w.plugins.map((p) => {
|
|
1428
|
-
const issues =
|
|
1103
|
+
const issues = issuesOf.get(p.key) ?? [];
|
|
1429
1104
|
const top = worst(issues);
|
|
1105
|
+
const repeats = new Set(issues.flatMap((i) => i.skills)).size;
|
|
1430
1106
|
const cursor = p.agent === "cursor";
|
|
1431
1107
|
const uses = cursor ? 0 : pluginUses(w, p);
|
|
1432
1108
|
const [state, stateColor] = STATE[p.on === null ? "unknown" : p.on ? "on" : "off"];
|
|
@@ -1448,11 +1124,12 @@ function pluginItems(w, skillW, ui, issuesOf2) {
|
|
|
1448
1124
|
{ text: top ? SEV[top.severity].icon : " ", width: 2, color: top ? SEV[top.severity].color : void 0 },
|
|
1449
1125
|
{ text: clip(name, nameW), width: nameW, color: p.on === false ? color.muted : void 0 },
|
|
1450
1126
|
{ text: harness(p.agent).icon, width: 3, color: harness(p.agent).color },
|
|
1451
|
-
{ text: clip(cursor ? "\u2013" : pluginScope(p),
|
|
1452
|
-
{ text: clip(`${n} skill${n === 1 ? "" : "s"}${p.parts.length ? ` +${p.parts.length}` : ""}`, 13), width: 13, color: color.muted },
|
|
1127
|
+
{ text: clip(cursor ? "\u2013" : pluginScope(p), scopeW), width: scopeW, color: p.repo ? color.blue : color.muted },
|
|
1128
|
+
...wide ? [{ text: clip(`${n} skill${n === 1 ? "" : "s"}${p.parts.length ? ` +${p.parts.length}` : ""}`, 13), width: 13, color: color.muted }] : [],
|
|
1453
1129
|
{ text: state.padEnd(4), width: 4, color: stateColor },
|
|
1454
|
-
{ text: cursor ? " \u2013 " : usesText(uses), width: 6, color: color.muted },
|
|
1455
|
-
|
|
1130
|
+
...wide ? [{ text: cursor ? " \u2013 " : usesText(uses), width: 6, color: color.muted }] : [],
|
|
1131
|
+
// Your skills it repeats, across its issues; else its worst issue.
|
|
1132
|
+
{ text: " " + (repeats ? `Repeats ${repeats} of yours` : top?.short ?? ""), grow: true, color: top ? SEV[top.severity].color : color.faint }
|
|
1456
1133
|
],
|
|
1457
1134
|
detail: (width) => {
|
|
1458
1135
|
const b = builder(p.id);
|
|
@@ -1474,7 +1151,7 @@ function pluginItems(w, skillW, ui, issuesOf2) {
|
|
|
1474
1151
|
b.line(label("Also brings"), [p.parts.join(", ") || "nothing else", p.parts.length ? color.text : color.faint]);
|
|
1475
1152
|
const installed = cursor ? "in Cursor (whether it's on is only in Cursor)" : p.scope === "claude.ai" ? "synced from your claude.ai account" : p.repo ? `${p.scope} scope, in ${p.repo} only` : `user scope, in every repo`;
|
|
1476
1153
|
b.line(label("Installed"), [installed, color.text], [p.version ? ` v${p.version.replace(/^v/, "")}` : "", color.muted], [p.id.includes("@") && p.scope !== "claude.ai" ? ` from ${p.id.split("@")[1]}` : "", color.faint]);
|
|
1477
|
-
b.line(label("Folder"), [
|
|
1154
|
+
b.line(label("Folder"), [tildify(p.path), color.faint]);
|
|
1478
1155
|
return b.d;
|
|
1479
1156
|
}
|
|
1480
1157
|
};
|
|
@@ -1492,7 +1169,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1492
1169
|
const columns = sidebar ? termCols - SIDEBAR_W : termCols;
|
|
1493
1170
|
const [base, setBase] = useState(initial);
|
|
1494
1171
|
const [used, setUsed] = useState();
|
|
1495
|
-
const
|
|
1172
|
+
const withUsage = used ? { ...base, ...used } : base;
|
|
1173
|
+
const usageIssues2 = useMemo(() => withUsage.days ? base.ops.usageIssues(withUsage) : [], [base, used]);
|
|
1174
|
+
const world = { ...withUsage, issues: [...base.issues, ...usageIssues2] };
|
|
1496
1175
|
useEffect(() => {
|
|
1497
1176
|
let live = true;
|
|
1498
1177
|
void loadUsage2?.(base).then((u) => live && setUsed(u));
|
|
@@ -1514,8 +1193,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1514
1193
|
const [modal, setModal] = useState(null);
|
|
1515
1194
|
const [toast, setToast] = useState("");
|
|
1516
1195
|
const order = useRef({ key: "", keys: [] });
|
|
1517
|
-
const split = termRows >= 30;
|
|
1196
|
+
const split = termRows >= 30 && place !== "settings";
|
|
1518
1197
|
const nameW = Math.min(22, Math.max(17, Math.floor(columns * 0.22)));
|
|
1198
|
+
const libNameW = columns < 90 ? nameW - 4 : nameW;
|
|
1519
1199
|
const dashboard = place === "projects" && open !== null;
|
|
1520
1200
|
const chips = dashboard ? CHIPS : place === "global" ? CHIPS.filter(([c]) => c !== "local") : place === "settings" ? [] : place === "health" ? CHIPS.slice(0, 1) : CHIPS.slice(0, 2);
|
|
1521
1201
|
const ui = {
|
|
@@ -1527,15 +1207,11 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1527
1207
|
menu: (title, options) => setModal({ kind: "menu", title, options, cursor: 0 })
|
|
1528
1208
|
};
|
|
1529
1209
|
const pluginIssues2 = new Map(world.plugins.map((p) => [p.key, pluginIssues(world, p)]));
|
|
1530
|
-
const
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
];
|
|
1536
|
-
const healthCount = new Set(flagged.map((f) => f.key)).size;
|
|
1537
|
-
const healthWorst = worst(flagged.map((f) => f.issue));
|
|
1538
|
-
const all = place === "projects" ? open ? projectSkillItems(world, open, nameW, ui) : projectItems(world) : place === "global" ? globalItems(world, nameW, ui) : place === "library" ? libraryItems(world, nameW, ui) : place === "plugins" ? pluginItems(world, nameW, ui, pluginIssues2) : place === "health" ? healthItems(world, nameW, ui) : settingsItems();
|
|
1210
|
+
const repoIssues2 = new Map(world.projects.map((p) => [p.name, repoIssues(world, p.name)]));
|
|
1211
|
+
const machineIssues = [...new Set(world.machine.map((m) => m.name))].map((name) => [name, issuesAt(world, null, name)]);
|
|
1212
|
+
const healthCount = world.issues.length;
|
|
1213
|
+
const healthWorst = worst(world.issues.map(issueOf));
|
|
1214
|
+
const all = place === "projects" ? open ? projectSkillItems(world, open, nameW, ui) : projectItems(world) : place === "global" ? globalItems(world, nameW, ui) : place === "library" ? libraryItems(world, libNameW, ui) : place === "plugins" ? pluginItems(world, columns, nameW, ui, pluginIssues2) : place === "health" ? healthItems(world) : settingsItems();
|
|
1539
1215
|
const inChip = (i, c) => c === "all" || (c === "issues" ? i.issues.length > 0 : i.group === c);
|
|
1540
1216
|
let items = all.filter((i) => i.header || (!query || matches(i.name, query)) && inChip(i, chip));
|
|
1541
1217
|
const viewKey = [place, open, chip, query, used ? "usage" : ""].join("|");
|
|
@@ -1556,7 +1232,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1556
1232
|
if (!query) items = [...actionRows(), ...items];
|
|
1557
1233
|
const cursorKey = `${place}|${open}`;
|
|
1558
1234
|
const firstSelectable = Math.max(0, items.findIndex((i) => !i.header && !i.action));
|
|
1559
|
-
|
|
1235
|
+
const saved = cursors[cursorKey];
|
|
1236
|
+
const kept = saved ? items.findIndex((i) => i.key === saved.key) : -1;
|
|
1237
|
+
let cursor = Math.min(kept >= 0 ? kept : saved?.at ?? firstSelectable, items.length - 1);
|
|
1560
1238
|
if (items[cursor]?.header) cursor = firstSelectable;
|
|
1561
1239
|
const current = items[cursor];
|
|
1562
1240
|
const detail = current?.detail?.(columns);
|
|
@@ -1602,7 +1280,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1602
1280
|
function fixAllList(from) {
|
|
1603
1281
|
const seen = /* @__PURE__ */ new Set();
|
|
1604
1282
|
return from.flatMap(
|
|
1605
|
-
(i) => i.issues.filter((x) => !x.decision && !seen.has(
|
|
1283
|
+
(i) => i.issues.filter((x) => !x.decision && !seen.has(x.id) && seen.add(x.id)).map((x) => ({ label: `${x.title} \u2192 ${x.fixes[0].label}`, fix: x.fixes[0], on: true }))
|
|
1606
1284
|
);
|
|
1607
1285
|
}
|
|
1608
1286
|
function toggleFold(key) {
|
|
@@ -1630,7 +1308,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1630
1308
|
const label = groupLabel(g, members.map((m) => m.name));
|
|
1631
1309
|
const issues = members.flatMap((m) => m.issues);
|
|
1632
1310
|
const top = worst(issues);
|
|
1633
|
-
const
|
|
1311
|
+
const flagged = members.filter((m) => m.issues.length).length;
|
|
1634
1312
|
return {
|
|
1635
1313
|
key: `fold:${g.key}`,
|
|
1636
1314
|
name: label,
|
|
@@ -1641,13 +1319,13 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1641
1319
|
cells: [
|
|
1642
1320
|
{ text: top ? SEV[top.severity].icon : " ", width: 2, color: top ? SEV[top.severity].color : void 0 },
|
|
1643
1321
|
{ text: `${isOpen ? "\u25BE" : "\u25B8"} ${label} (${members.length})`, grow: true, color: color.accent },
|
|
1644
|
-
{ text:
|
|
1322
|
+
{ text: flagged ? `${flagged} to fix` : "", width: 10, color: top ? SEV[top.severity].color : color.faint }
|
|
1645
1323
|
],
|
|
1646
1324
|
detail: (width) => {
|
|
1647
1325
|
const b = builder(label);
|
|
1648
1326
|
for (const l of wrap(members.map((m) => m.name).join(", "), width - 4, 4)) b.line([l, color.muted]);
|
|
1649
1327
|
b.line();
|
|
1650
|
-
if (
|
|
1328
|
+
if (flagged) b.line([`${flagged} of them ${flagged === 1 ? "has" : "have"} issues`, top ? SEV[top.severity].color : color.text], [" open the group to fix them one by one", color.faint]);
|
|
1651
1329
|
b.actions(groupActions(label, members), width);
|
|
1652
1330
|
return b.d;
|
|
1653
1331
|
}
|
|
@@ -1670,7 +1348,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1670
1348
|
preticked: usedIn,
|
|
1671
1349
|
pickRepos: (w, repos) => w.ops.moveGlobal(mine, repos, label)
|
|
1672
1350
|
}),
|
|
1673
|
-
...mine.some((m) => !m.kept) ? [fixOption({ label: `Keep all ${mine.length} global on purpose`, preview: `
|
|
1351
|
+
...mine.some((m) => !m.kept) ? [fixOption({ label: `Keep all ${mine.length} global on purpose`, preview: `Mark these ${n(mine.length)} as global on purpose: they no longer show as Not reviewed. Their other issues stay.`, run: (w) => (mine.forEach((m) => w.ops.keepGlobal(m.name, true)), `${n(mine.length)} marked as global on purpose`) })] : [],
|
|
1674
1352
|
fixOption({ label: `Delete all ${mine.length}`, preview: `Move these ${n(mine.length)} to Settings \u203A Backups.`, run: (w) => joined(mine.map((m) => w.ops.deleteGlobal(m))) })
|
|
1675
1353
|
] : [],
|
|
1676
1354
|
...libs.length ? [
|
|
@@ -1743,9 +1421,12 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1743
1421
|
i = Math.max(0, Math.min(items.length - 1, i + delta));
|
|
1744
1422
|
while (items[i]?.header && i > 0 && i < items.length - 1);
|
|
1745
1423
|
if (items[i]?.header) return;
|
|
1746
|
-
|
|
1424
|
+
setCursorAt(i);
|
|
1747
1425
|
setDetailCursor(0);
|
|
1748
1426
|
}
|
|
1427
|
+
function setCursorAt(i) {
|
|
1428
|
+
setCursors({ ...cursors, [cursorKey]: { key: items[i]?.key ?? "", at: i } });
|
|
1429
|
+
}
|
|
1749
1430
|
function goPlace(p) {
|
|
1750
1431
|
setPlace(p);
|
|
1751
1432
|
setFocus("list");
|
|
@@ -1753,7 +1434,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1753
1434
|
setChip("all");
|
|
1754
1435
|
}
|
|
1755
1436
|
function edit(file, then) {
|
|
1756
|
-
if (!existsSync2(file)) return then ? apply(then) : (setBase(reload()), setToast(`${
|
|
1437
|
+
if (!existsSync2(file)) return then ? apply(then) : (setBase(reload()), setToast(`${tildify(file)} doesn't exist`));
|
|
1757
1438
|
const editor = process.env.VISUAL || process.env.EDITOR || (process.platform === "win32" ? "notepad" : "vi");
|
|
1758
1439
|
void suspendTerminal(() => {
|
|
1759
1440
|
if (process.platform === "win32") spawnSync2(`${editor} "${file}"`, { shell: true, stdio: "inherit" });
|
|
@@ -1762,7 +1443,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1762
1443
|
const fresh = reload();
|
|
1763
1444
|
if (then) return apply(then, fresh);
|
|
1764
1445
|
setBase(fresh);
|
|
1765
|
-
setToast(`Saved ${
|
|
1446
|
+
setToast(`Saved ${tildify(file)}`);
|
|
1766
1447
|
});
|
|
1767
1448
|
}
|
|
1768
1449
|
function askForFolder(title) {
|
|
@@ -1810,16 +1491,16 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1810
1491
|
...world.roots.map(
|
|
1811
1492
|
(r) => row(
|
|
1812
1493
|
`root:${r}`,
|
|
1813
|
-
|
|
1494
|
+
tildify(r),
|
|
1814
1495
|
"scanned for git repos",
|
|
1815
|
-
() => ui.menu(
|
|
1496
|
+
() => ui.menu(tildify(r), [
|
|
1816
1497
|
{ label: "Look for repos again", action: () => apply((w) => w.ops.rescan()) },
|
|
1817
|
-
fixOption({ label: "Stop scanning this folder", preview: `Repos already found in ${
|
|
1498
|
+
fixOption({ label: "Stop scanning this folder", preview: `Repos already found in ${tildify(r)} stay listed until you hide them.`, run: (w) => w.ops.removeRoot(r) })
|
|
1818
1499
|
])
|
|
1819
1500
|
)
|
|
1820
1501
|
),
|
|
1821
1502
|
row("root:add", "+ Add a folder\u2026", "", () => askForFolder("Folder that contains your projects")),
|
|
1822
|
-
...world.hidden.length ? [header("#hidden", "Hidden repos"), ...world.hidden.map((h) => row(`hidden:${h}`,
|
|
1503
|
+
...world.hidden.length ? [header("#hidden", "Hidden repos"), ...world.hidden.map((h) => row(`hidden:${h}`, tildify(h), "enter shows it again", () => apply((w) => w.ops.unhide(h))))] : [],
|
|
1823
1504
|
header("#backups", "Backups"),
|
|
1824
1505
|
...world.backups.length ? world.backups.map(
|
|
1825
1506
|
(b, i) => row(`backup:${i}`, `\u21BA ${b.name}`, `${b.from} \xB7 ${b.at}`, () => apply((w) => w.ops.restoreBackup(i)))
|
|
@@ -1869,22 +1550,22 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1869
1550
|
if (key.escape) return setSideQuery("");
|
|
1870
1551
|
return;
|
|
1871
1552
|
}
|
|
1553
|
+
if (!sidebar && (key.leftArrow || key.rightArrow)) {
|
|
1554
|
+
const i = PLACES.findIndex(([p]) => p === place) + (key.leftArrow ? -1 : 1);
|
|
1555
|
+
if (PLACES[i]) goPlace(PLACES[i][0]);
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1872
1558
|
const fold = current?.fold;
|
|
1873
1559
|
if (fold && !fold.open && (key.rightArrow || key.return || input === " ")) return toggleFold(fold.key);
|
|
1874
1560
|
if (fold?.open && (key.leftArrow || input === " ")) return toggleFold(fold.key);
|
|
1875
1561
|
if (current?.inFold && key.leftArrow) {
|
|
1876
1562
|
toggleFold(current.inFold);
|
|
1877
|
-
return
|
|
1563
|
+
return setCursorAt(items.findIndex((i) => i.fold?.key === current.inFold));
|
|
1878
1564
|
}
|
|
1879
1565
|
if (sidebar && key.leftArrow) return setFocus("sidebar");
|
|
1880
1566
|
if (sidebar && key.rightArrow && detail) return setFocus("detail"), setDetailCursor(0);
|
|
1881
1567
|
if (key.upArrow) return move(-1);
|
|
1882
1568
|
if (key.downArrow) return move(1);
|
|
1883
|
-
if (!sidebar && (key.leftArrow || key.rightArrow)) {
|
|
1884
|
-
const i = PLACES.findIndex(([p]) => p === place) + (key.leftArrow ? -1 : 1);
|
|
1885
|
-
if (PLACES[i]) goPlace(PLACES[i][0]);
|
|
1886
|
-
return;
|
|
1887
|
-
}
|
|
1888
1569
|
if (key.escape) {
|
|
1889
1570
|
if (query) return setQuery("");
|
|
1890
1571
|
if (sidebar) return setFocus("sidebar");
|
|
@@ -2024,13 +1705,12 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
2024
1705
|
const confirmLines = modal?.kind === "confirm" ? wrap(modal.fix.preview, columns - 6, 6) : [];
|
|
2025
1706
|
const confirmH = modal?.kind === "confirm" ? confirmLines.length + 2 : 0;
|
|
2026
1707
|
const bodyH = Math.max(8, termRows - (sidebar ? 0 : 1) - bottomH - confirmH);
|
|
2027
|
-
const globalFlagged =
|
|
2028
|
-
const globalIssues =
|
|
1708
|
+
const globalFlagged = machineIssues.filter(([, issues]) => issues.length);
|
|
1709
|
+
const globalIssues = globalFlagged.length;
|
|
2029
1710
|
const globalProblem = globalFlagged.some(([, issues]) => issues.some((x) => x.severity === "problem"));
|
|
2030
|
-
const pluginsWorst = worst([...pluginIssues2.values()].flat()
|
|
1711
|
+
const pluginsWorst = worst([...pluginIssues2.values()].flat());
|
|
2031
1712
|
const badge = (p) => {
|
|
2032
|
-
const
|
|
2033
|
-
const top = worst(found.map((x) => x.issue));
|
|
1713
|
+
const top = worst(repoIssues2.get(p) ?? repoIssues(world, p));
|
|
2034
1714
|
return { text: top ? SEV[top.severity].icon : "", width: 3, color: top ? SEV[top.severity].color : void 0 };
|
|
2035
1715
|
};
|
|
2036
1716
|
const sideRow = (key, label, here, go, count = []) => ({
|
|
@@ -2089,7 +1769,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
2089
1769
|
}),
|
|
2090
1770
|
/* @__PURE__ */ jsx2(Text2, { color: color.accent, children: query ? ` \u2315 ${query}\u258F` : "" })
|
|
2091
1771
|
] }) });
|
|
2092
|
-
const columnTitles = dashboard || place === "global" ? titles([["", 2], ["Skill", nameW], ["Source", 14], [" Uses", 6], [" Issue", 0]]) : place === "projects" ? titles([["", 2], ["Project", 18], ["Skills", 10], ["Issues", 0]]) : place === "library" ? titles([["", 2], ["Skill",
|
|
1772
|
+
const columnTitles = dashboard || place === "global" ? titles([["", 2], ["Skill", nameW], ["Source", 14], [" Uses", 6], [" Issue", 0]]) : place === "projects" ? titles([["", 2], ["Project", 18], ["Skills", 10], ["Issues", 0]]) : place === "library" ? titles([["", 2], ["Skill", libNameW], ["Ver", 5], ["Used in", 16], [" Uses", 6], [" Issue / description", 0]]) : place === "plugins" ? ((c) => titles([["", 2], ["Plugin", c.name], ["", 3], ["Scope", c.scope], ...c.wide ? [["Brings", 13]] : [], ["On", 4], ...c.wide ? [[" Uses", 6]] : [], [" Issue", 0]]))(pluginCols(columns, nameW)) : place === "health" ? titles([["", 2], ["Issue", 0]]) : null;
|
|
2093
1773
|
const showChips = chips.length > 0;
|
|
2094
1774
|
const listHeader = /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
2095
1775
|
showChips ? chipRow : null,
|
|
@@ -2273,7 +1953,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
2273
1953
|
] })
|
|
2274
1954
|
] }),
|
|
2275
1955
|
/* @__PURE__ */ jsxs2(Box2, { height: 1, width: termCols, justifyContent: "space-between", children: [
|
|
2276
|
-
/* @__PURE__ */ jsx2(Text2, { color: toast.startsWith("\u2713")
|
|
1956
|
+
/* @__PURE__ */ jsx2(Text2, { color: toast.startsWith("\u2713") ? color.green : toast.startsWith("\u2717") ? color.red : toast.startsWith("!") ? color.yellow : color.muted, wrap: "truncate-end", children: " " + toast }),
|
|
2277
1957
|
toast ? null : /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: hints + " " })
|
|
2278
1958
|
] })
|
|
2279
1959
|
] });
|
|
@@ -2316,23 +1996,24 @@ function Help({ width, height }) {
|
|
|
2316
1996
|
["ctrl+r", "read everything again from disk"],
|
|
2317
1997
|
["ctrl+c", "quit"]
|
|
2318
1998
|
];
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
]
|
|
1999
|
+
const notes = [
|
|
2000
|
+
'\u2715 problem \xB7 \u26A0 suggestion \xB7 \u2605 recommended fix \xB7 "your call" = a decision, never auto-fixed',
|
|
2001
|
+
"Source: lib = from your library \xB7 repo = committed by the team \xB7 untracked = only on this machine",
|
|
2002
|
+
"Usage counts Claude Code sessions only: other agents don't record skill use."
|
|
2003
|
+
];
|
|
2004
|
+
const lines = [
|
|
2005
|
+
...keys.flatMap(([k, d]) => wrap(d, width - 16, 3).map((l, i) => [[(i ? "" : k).padEnd(12), color.accent], [l, color.text]])),
|
|
2006
|
+
[],
|
|
2007
|
+
...notes.flatMap((n) => wrap(n, width - 4, 3).map((l) => [[l, color.muted]]))
|
|
2008
|
+
];
|
|
2009
|
+
return /* @__PURE__ */ jsx2(Panel, { title: "Keys", hint: "any key closes", focused: true, width, height, children: lines.slice(0, height - 2).map((segs, i) => /* @__PURE__ */ jsx2(SegLine, { segs: segs.length ? segs : [[" "]] }, i)) });
|
|
2329
2010
|
}
|
|
2330
2011
|
|
|
2331
2012
|
// src/tui/load.ts
|
|
2332
|
-
import {
|
|
2013
|
+
import { spawn } from "child_process";
|
|
2333
2014
|
import { existsSync as existsSync3, lstatSync, realpathSync } from "fs";
|
|
2334
2015
|
import { basename, dirname, join as join2 } from "path";
|
|
2335
|
-
function
|
|
2016
|
+
function tildify2(p) {
|
|
2336
2017
|
return tildify(p).replace(/\\/g, "/");
|
|
2337
2018
|
}
|
|
2338
2019
|
function uniqueNames(roots) {
|
|
@@ -2345,31 +2026,25 @@ function uniqueNames(roots) {
|
|
|
2345
2026
|
})
|
|
2346
2027
|
);
|
|
2347
2028
|
}
|
|
2348
|
-
function localSkill(root, s,
|
|
2029
|
+
function localSkill(root, s, git) {
|
|
2349
2030
|
const managed = {
|
|
2350
|
-
ok: {},
|
|
2351
|
-
"update available": {},
|
|
2352
2031
|
"edited locally": { edited: true },
|
|
2353
2032
|
"edited locally, update available": { edited: true },
|
|
2354
|
-
|
|
2355
|
-
"folder missing": { missing: true, ...s.latest === null && { library: "missing" } },
|
|
2356
|
-
"not in library": { library: "missing" }
|
|
2033
|
+
"folder missing": { missing: true }
|
|
2357
2034
|
};
|
|
2358
2035
|
const other = {
|
|
2359
|
-
"untracked copy of library skill": { source: "untracked"
|
|
2360
|
-
"untracked, differs from library": { source: "untracked"
|
|
2036
|
+
"untracked copy of library skill": { source: "untracked" },
|
|
2037
|
+
"untracked, differs from library": { source: "untracked" },
|
|
2361
2038
|
"local only": { source: "untracked" },
|
|
2362
2039
|
// Installed with `npx skills add` (the repo's skills-lock.json): not skilllib's, so not a library skill.
|
|
2363
2040
|
"from npx skills": { source: "untracked" },
|
|
2364
2041
|
"repo skill": { source: "repo" },
|
|
2365
|
-
"repo skill, in library": { source: "repo"
|
|
2366
|
-
"repo skill, differs from library": { source: "repo"
|
|
2042
|
+
"repo skill, in library": { source: "repo" },
|
|
2043
|
+
"repo skill, differs from library": { source: "repo" }
|
|
2367
2044
|
};
|
|
2368
|
-
const state =
|
|
2045
|
+
const state = git ? git.of(relativeTo(root, s.path)) : null;
|
|
2046
|
+
const others = otherCopies(root, s.name, s.location);
|
|
2369
2047
|
const own = other[s.state]?.source === "untracked" && (state === "committed" || state === "changed") ? { source: "repo" } : {};
|
|
2370
|
-
const blind = s.state === "folder missing" ? [] : s.visibility.filter((v) => v.paths === 0).map((v) => v.id);
|
|
2371
|
-
const uncommitted = blind.length ? linkEverywhere(root, s.name, s.location, { dryRun: true }).uncommitted : [];
|
|
2372
|
-
const unshared = blind.filter((id) => uncommitted.includes(harness(id).projectDirs[0]));
|
|
2373
2048
|
return {
|
|
2374
2049
|
name: s.name,
|
|
2375
2050
|
source: "lib",
|
|
@@ -2377,18 +2052,19 @@ function localSkill(root, s, git2) {
|
|
|
2377
2052
|
path: s.path,
|
|
2378
2053
|
// Agents that actually load it here: visibility lists every agent you use, with `paths: 0` for the blind ones.
|
|
2379
2054
|
agents: s.visibility.filter((v) => v.paths > 0).map((v) => v.id),
|
|
2380
|
-
...s.managed ? {
|
|
2381
|
-
...unshared.length && { unshared },
|
|
2055
|
+
...s.managed ? { version: s.version ?? void 0, ...managed[s.state] } : { ...other[s.state], ...own },
|
|
2382
2056
|
...s.source ? { origin: `npx skills: ${s.source}` } : {},
|
|
2383
|
-
|
|
2057
|
+
...others.length ? { others } : {},
|
|
2384
2058
|
git: state
|
|
2385
2059
|
};
|
|
2386
2060
|
}
|
|
2061
|
+
function otherCopies(root, name, dir) {
|
|
2062
|
+
return foldersOf(root, name).real.map((d) => relativeTo(root, d)).filter((d) => d !== dir);
|
|
2063
|
+
}
|
|
2387
2064
|
function installedAt(path) {
|
|
2388
2065
|
try {
|
|
2389
2066
|
const st = lstatSync(path);
|
|
2390
|
-
|
|
2391
|
-
return new Date(at.getTime() - at.getTimezoneOffset() * 6e4).toISOString().slice(0, 16).replace("T", " ");
|
|
2067
|
+
return localStamp((st.birthtimeMs > 0 ? st.birthtime : st.mtime).getTime());
|
|
2392
2068
|
} catch {
|
|
2393
2069
|
return void 0;
|
|
2394
2070
|
}
|
|
@@ -2398,7 +2074,7 @@ function machineSkill(s, kept) {
|
|
|
2398
2074
|
return {
|
|
2399
2075
|
name: s.name,
|
|
2400
2076
|
source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : s.kind === "system" ? "system" : s.kind === "skilllib" ? "skilllib" : s.kind === "built-in" && s.harnesses[0] === "grok" ? "grok" : "cursor",
|
|
2401
|
-
where: global || s.kind === "skilllib" ?
|
|
2077
|
+
where: global || s.kind === "skilllib" ? tildify2(dirname(s.path)) : s.kind === "claude.ai" ? "claude.ai account" : s.kind === "built-in" ? `${s.origin} built-in` : s.origin,
|
|
2402
2078
|
...s.kind === "skills.sh" ? { origin: `npx skills: ${s.origin}` } : {},
|
|
2403
2079
|
path: s.path,
|
|
2404
2080
|
links: s.links,
|
|
@@ -2430,13 +2106,6 @@ function pluginOf(p, repoOf) {
|
|
|
2430
2106
|
function refOf(p) {
|
|
2431
2107
|
return { id: p.id, scope: p.scope === "claude.ai" ? null : p.scope, ...p.projectPath ? { projectPath: p.projectPath } : {}, synced: p.scope === "claude.ai", ...p.settingsOnly ? { settingsOnly: true } : {} };
|
|
2432
2108
|
}
|
|
2433
|
-
function dupesOf(r, root, git2 = null) {
|
|
2434
|
-
const out = /* @__PURE__ */ new Map();
|
|
2435
|
-
for (const p of r.plans) out.set(p.name, { steps: p.steps.map((s) => describeStep(s, root)), git: gitVisibleSteps(p, git2).map((s) => describeStep(s, root)) });
|
|
2436
|
-
for (const c of r.conflicts)
|
|
2437
|
-
out.set(c.name, { steps: [], git: [], differ: c.copies.map((x) => ({ dir: x.dir, label: root ? copyLabel(c, x.dir) : tildify4(x.dir), runs: x.runs, canWin: !c.managed || x.dir === c.managed })) });
|
|
2438
|
-
return out;
|
|
2439
|
-
}
|
|
2440
2109
|
function loadWorld() {
|
|
2441
2110
|
forgetLatest();
|
|
2442
2111
|
forgetClaudeBinary();
|
|
@@ -2451,7 +2120,8 @@ function loadWorld() {
|
|
|
2451
2120
|
};
|
|
2452
2121
|
const visible = visibleProjects();
|
|
2453
2122
|
const cwd = here ? visible.find((p) => real(p) === real(here)) ?? here : null;
|
|
2454
|
-
const
|
|
2123
|
+
const listed = [.../* @__PURE__ */ new Set([...cwd ? [cwd] : [], ...visible])];
|
|
2124
|
+
const roots = readableRoots(listed).readable;
|
|
2455
2125
|
const names = uniqueNames(roots);
|
|
2456
2126
|
const descriptions = {};
|
|
2457
2127
|
const origins = libraryOrigins();
|
|
@@ -2459,18 +2129,13 @@ function loadWorld() {
|
|
|
2459
2129
|
descriptions[s.name] = s.description;
|
|
2460
2130
|
return { name: s.name, latest: latestVersion(s.name)?.version ?? 1, origin: origins[s.name] };
|
|
2461
2131
|
});
|
|
2132
|
+
const libraryNames = new Set(library.map((l) => l.name));
|
|
2462
2133
|
const kept = keptGlobal();
|
|
2463
2134
|
const cursorList = agents.includes("cursor") ? cursorPlugins() : [];
|
|
2464
|
-
const cursorPlugin = new Map(cursorPluginSkills(cursorList).map((s) => [s.name, s.plugin]));
|
|
2465
|
-
const globalDupes = dupesOf(planGlobalTidy({ enabled: agents }), null);
|
|
2466
2135
|
const sourced = machineSkills(agents);
|
|
2467
2136
|
const machine = sourced.map((s) => {
|
|
2468
2137
|
descriptions[s.name] ??= s.description;
|
|
2469
|
-
|
|
2470
|
-
if (m.source !== "global" || m.broken) return m;
|
|
2471
|
-
const dupes = globalDupes.get(m.name);
|
|
2472
|
-
const plugin = cursorPlugin.get(m.name);
|
|
2473
|
-
return { ...m, ...dupes ? { dupes } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
|
|
2138
|
+
return machineSkill(s, kept);
|
|
2474
2139
|
});
|
|
2475
2140
|
const loadedHere = (root) => {
|
|
2476
2141
|
const loaded = skillsLoadedIn(root, sourced, agents);
|
|
@@ -2478,21 +2143,17 @@ function loadWorld() {
|
|
|
2478
2143
|
for (const s of loaded) descriptions[s.name] ??= s.description;
|
|
2479
2144
|
return { machine: loaded.map((s) => machine[sourced.indexOf(s)] ?? machineSkill(s, kept)) };
|
|
2480
2145
|
};
|
|
2146
|
+
const statuses = new Map(roots.map((root) => [root, projectStatus(root)]));
|
|
2481
2147
|
const projects = roots.map((root) => {
|
|
2482
|
-
const status =
|
|
2148
|
+
const status = statuses.get(root);
|
|
2483
2149
|
const hasManifest = existsSync3(join2(root, "skilllib.json"));
|
|
2484
|
-
const
|
|
2150
|
+
const git = status.length || hasManifest ? gitInfo(root) : null;
|
|
2485
2151
|
for (const s of status) if (!descriptions[s.name] && !s.state.includes("missing")) descriptions[s.name] = readSkillInfo(s.path).description;
|
|
2486
|
-
const dupes = status.length ? dupesOf(planProjectTidy(root, { enabled: agents }), root, git2) : /* @__PURE__ */ new Map();
|
|
2487
2152
|
return {
|
|
2488
2153
|
name: names.get(root),
|
|
2489
2154
|
path: root,
|
|
2490
|
-
manifest: hasManifest ?
|
|
2491
|
-
skills: status.map((s) =>
|
|
2492
|
-
const d = dupes.get(s.name);
|
|
2493
|
-
const plugin = cursorPlugin.get(s.name);
|
|
2494
|
-
return { ...localSkill(root, s, git2), ...d ? { dupes: d } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
|
|
2495
|
-
}),
|
|
2155
|
+
manifest: hasManifest ? git ? git.of("skilllib.json") : "no git" : "none",
|
|
2156
|
+
skills: status.map((s) => localSkill(root, s, git)),
|
|
2496
2157
|
...loadedHere(root)
|
|
2497
2158
|
};
|
|
2498
2159
|
});
|
|
@@ -2501,19 +2162,19 @@ function loadWorld() {
|
|
|
2501
2162
|
const plugins = [...installedPlugins(), ...cursorList].map((p) => pluginOf(p, repoOf));
|
|
2502
2163
|
return {
|
|
2503
2164
|
agents,
|
|
2504
|
-
cwd: cwd ? names.get(cwd) : null,
|
|
2165
|
+
cwd: cwd && names.has(cwd) ? names.get(cwd) : null,
|
|
2505
2166
|
projects,
|
|
2506
2167
|
machine,
|
|
2507
2168
|
plugins,
|
|
2508
2169
|
library,
|
|
2509
2170
|
usage: {},
|
|
2510
2171
|
descriptions,
|
|
2511
|
-
backups: backups.map((b) => ({ name: b.name, from:
|
|
2512
|
-
roots: readConfig()
|
|
2513
|
-
hidden: readConfig().hidden,
|
|
2172
|
+
backups: backups.map((b) => ({ name: b.name, from: tildify2(b.from), at: b.movedAt })),
|
|
2173
|
+
...(({ roots: roots2, hidden }) => ({ roots: roots2, hidden }))(readConfig()),
|
|
2514
2174
|
agentsChosen: harnessesChosen(),
|
|
2515
2175
|
agentSkill: agentSkillState(agents),
|
|
2516
|
-
|
|
2176
|
+
issues: findIssues(listed, sourced, libraryNames, (root) => statuses.get(root)),
|
|
2177
|
+
ops: realOps(new Map(projects.map((p) => [p.name, p.path])), backups, sourced, libraryNames)
|
|
2517
2178
|
};
|
|
2518
2179
|
}
|
|
2519
2180
|
async function loadUsage(w) {
|
|
@@ -2523,7 +2184,11 @@ async function loadUsage(w) {
|
|
|
2523
2184
|
const days = {};
|
|
2524
2185
|
const midnight = (t) => new Date(t).setHours(0, 0, 0, 0);
|
|
2525
2186
|
const today = midnight(Date.now());
|
|
2187
|
+
const totals = {};
|
|
2526
2188
|
for (const use of await scanUsage(30)) {
|
|
2189
|
+
const skill = use.skill.split(":").pop();
|
|
2190
|
+
totals[skill] = (totals[skill] ?? 0) + 1;
|
|
2191
|
+
if (skill !== use.skill) totals[use.skill] = (totals[use.skill] ?? 0) + 1;
|
|
2527
2192
|
const root = projectOf(use.cwd);
|
|
2528
2193
|
const repo = root ? repoOf.get(root) : void 0;
|
|
2529
2194
|
const day = 29 - Math.round((today - midnight(Date.parse(use.at))) / 864e5);
|
|
@@ -2531,18 +2196,7 @@ async function loadUsage(w) {
|
|
|
2531
2196
|
(usage[repo] ??= {})[use.skill] = (usage[repo][use.skill] ?? 0) + 1;
|
|
2532
2197
|
((days[repo] ??= {})[use.skill] ??= new Array(30).fill(0))[day] += 1;
|
|
2533
2198
|
}
|
|
2534
|
-
return { usage, days };
|
|
2535
|
-
}
|
|
2536
|
-
function git(root, args) {
|
|
2537
|
-
try {
|
|
2538
|
-
return execFileSync("git", args, { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
2539
|
-
} catch {
|
|
2540
|
-
return "";
|
|
2541
|
-
}
|
|
2542
|
-
}
|
|
2543
|
-
function remoteLabel(url) {
|
|
2544
|
-
if (!url) return void 0;
|
|
2545
|
-
return url.replace(/^[a-z+]+:\/\//, "").replace(/^[^@/]+@/, "").replace(/:(?!\d)/, "/").replace(/\.git$/, "");
|
|
2199
|
+
return { usage, days, totals };
|
|
2546
2200
|
}
|
|
2547
2201
|
function said3(c, repo) {
|
|
2548
2202
|
if (c.action === "skipped") return `${c.name}: ${c.reason ?? "skipped"}`;
|
|
@@ -2551,10 +2205,9 @@ function said3(c, repo) {
|
|
|
2551
2205
|
const done = back ? `Pinned ${c.name} to v${c.to} in ${repo} (was v${c.from})` : `${c.action === "installed" ? "Added" : c.action === "reset" ? "Reset" : "Updated"} ${c.name}${c.to ? ` v${c.to}` : ""} in ${repo}`;
|
|
2552
2206
|
return c.backedUp ? `${done} (your edits are in Settings \u203A Backups)` : done;
|
|
2553
2207
|
}
|
|
2554
|
-
function realOps(roots, backups) {
|
|
2208
|
+
function realOps(roots, backups, machine, libraryNames) {
|
|
2555
2209
|
const rootOf = (repo) => roots.get(repo);
|
|
2556
2210
|
const find = (repo, name) => projectStatus(rootOf(repo)).find((s) => s.name === name);
|
|
2557
|
-
const info = /* @__PURE__ */ new Map();
|
|
2558
2211
|
const result = (c, repo) => c.action === "skipped" ? failed(said3(c, repo)) : said3(c, repo);
|
|
2559
2212
|
const copyDir = (repo, name) => readManifest(rootOf(repo)).skills[name]?.dir ?? PROJECT_SKILLS_DIR;
|
|
2560
2213
|
const withLinks = (c, repo, name) => {
|
|
@@ -2596,9 +2249,8 @@ function realOps(roots, backups) {
|
|
|
2596
2249
|
return {
|
|
2597
2250
|
label: `Also link ${new Set(held.map((h) => h.name)).size === 1 ? "it" : "them"} in ${dirs.join(", ")}`,
|
|
2598
2251
|
preview: `${dirs.join(", ")} ${dirs.length === 1 ? "is" : "are"} committed in ${repos.join(", ")}: the links will show in git status. skilllib remembers this for ${repos.join(", ")}.`,
|
|
2599
|
-
run: (
|
|
2252
|
+
run: () => {
|
|
2600
2253
|
for (const repo of repos) allowTrackedLinks(rootOf(repo));
|
|
2601
|
-
if (held.length === 1) return w.ops.link(held[0].repo, held[0].name, true);
|
|
2602
2254
|
const results = held.map((h) => ({ ...h, r: linkIn(h.repo, h.name, true) }));
|
|
2603
2255
|
const linked = results.filter((x) => x.r.created.length);
|
|
2604
2256
|
const names = [...new Set(linked.map((x) => x.name))];
|
|
@@ -2633,81 +2285,27 @@ function realOps(roots, backups) {
|
|
|
2633
2285
|
if (!s) return failed(`${name} isn't in ${repo}`);
|
|
2634
2286
|
const committed = !s.managed && ["committed", "changed"].includes(gitInfo(rootOf(repo))?.of(relativeTo(rootOf(repo), s.path)) ?? "");
|
|
2635
2287
|
if (s.state.startsWith("repo skill") || committed) return failed(`${name} is the repo's own (committed by your team): skilllib doesn't delete it`);
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
const [c] = syncSkill(rootOf(repo), name);
|
|
2641
|
-
return c ? result(c, repo) : `${name} is already in place`;
|
|
2288
|
+
const others = otherCopies(rootOf(repo), name, s.location);
|
|
2289
|
+
const left = (r) => typeof r === "string" && others.length ? `${r}; the copy in ${others.join(", ")} stays` : r;
|
|
2290
|
+
if (!s.managed) return removeUntracked(rootOf(repo), name, s.path), left(`Removed ${others.length ? `${s.location}/${name}` : name} from ${repo} (in Settings \u203A Backups)`);
|
|
2291
|
+
return left(orForce(removeSkill(rootOf(repo), name), repo, "Remove it anyway", () => removeSkill(rootOf(repo), name, { force: true })));
|
|
2642
2292
|
},
|
|
2643
2293
|
update: (repo, name) => {
|
|
2644
2294
|
const c = updateProject(rootOf(repo), [name])[0];
|
|
2645
2295
|
return c ? orForce(c, repo, "Update anyway", () => updateProject(rootOf(repo), [name], { force: true })[0]) : `${name} is up to date`;
|
|
2646
2296
|
},
|
|
2647
|
-
saveEdits: (repo, name) => {
|
|
2648
|
-
const s = find(repo, name);
|
|
2649
|
-
const saved = toLibrary(s.path);
|
|
2650
|
-
addSkill(rootOf(repo), name);
|
|
2651
|
-
return saved;
|
|
2652
|
-
},
|
|
2653
|
-
discardEdits: (repo, name) => {
|
|
2654
|
-
const s = find(repo, name);
|
|
2655
|
-
removeUntracked(rootOf(repo), name, s.path);
|
|
2656
|
-
return withLinks(addSkill(rootOf(repo), name, s.managed && s.version ? { version: s.version } : {}), repo, name);
|
|
2657
|
-
},
|
|
2658
|
-
link: (repo, name, allow = false) => {
|
|
2659
|
-
const r = linkIn(repo, name, allow);
|
|
2660
|
-
const message = linkSaid(name, r);
|
|
2661
|
-
if (!r.blocked.length || allow) return r.created.length ? message : failed(message);
|
|
2662
|
-
return { message: `${message}; not linked in ${r.blocked.join(", ")}: git tracks ${r.blocked.length === 1 ? "it" : "them"}`, then: allowTracked([{ repo, name, blocked: r.blocked }]) };
|
|
2663
|
-
},
|
|
2664
|
-
track: (repo, name) => withLinks(addSkill(rootOf(repo), name), repo, name),
|
|
2665
|
-
importLocal: (repo, name) => {
|
|
2666
|
-
const s = find(repo, name);
|
|
2667
|
-
importSkill(s.path);
|
|
2668
|
-
return withLinks(addSkill(rootOf(repo), name), repo, name);
|
|
2669
|
-
},
|
|
2670
2297
|
copyToLibrary: (repo, name) => toLibrary(find(repo, name).path),
|
|
2671
|
-
tidy: (repo, name, { keep, allowGit = false } = {}) => {
|
|
2672
|
-
const root = repo === null ? null : rootOf(repo);
|
|
2673
|
-
const opts = keep ? { keep: { [name]: keep } } : {};
|
|
2674
|
-
const plan = (root ? planProjectTidy(root, opts) : planGlobalTidy(opts)).plans.find((p) => p.name === name);
|
|
2675
|
-
if (!plan) return `${name}: nothing to tidy`;
|
|
2676
|
-
const r = applyTidy(plan, { git: allowGit ? "go" : "keep" });
|
|
2677
|
-
const steps = (list) => list.map((s) => describeStep(s, root)).join(", ");
|
|
2678
|
-
if (!r.held.length) return r.applied.length ? `${name}: ${steps(r.applied)}` : `${name}: nothing changed`;
|
|
2679
|
-
return {
|
|
2680
|
-
message: `${name}: ${r.applied.length ? `${steps(r.applied)}; ` : ""}held back ${steps(r.held)}, because git tracks it`,
|
|
2681
|
-
then: {
|
|
2682
|
-
label: "Also change what git tracks",
|
|
2683
|
-
preview: `${steps(r.held)}: these changes will show in git status in ${repo}. Replaced copies go to Settings \u203A Backups.`,
|
|
2684
|
-
run: (w) => w.ops.tidy(repo, name, { keep, allowGit: true })
|
|
2685
|
-
}
|
|
2686
|
-
};
|
|
2687
|
-
},
|
|
2688
|
-
unloadGlobal: (m) => {
|
|
2689
|
-
if (!m.broken && !latestVersion(m.name)) importSkill(m.path);
|
|
2690
|
-
const r = unloadGlobal(m.path, m.links);
|
|
2691
|
-
return r.ok ? `${m.name} no longer loads globally (in Settings \u203A Backups)` : failed(`${m.name}: ${r.reason}`);
|
|
2692
|
-
},
|
|
2693
2298
|
deleteGlobal: (m) => {
|
|
2694
2299
|
const r = deleteGlobal(m.path, m.links);
|
|
2695
2300
|
return r.ok ? m.broken ? `Removed the broken link ${m.name}` : `Deleted ${m.name} (in Settings \u203A Backups)` : failed(`${m.name}: ${r.reason}`);
|
|
2696
2301
|
},
|
|
2697
2302
|
keepGlobal: (name, keep) => (setKeepGlobal([name], keep), keep ? `${name} marked as global on purpose` : `${name} is no longer marked as global on purpose`),
|
|
2698
|
-
linkGlobal: (m, agents) => {
|
|
2699
|
-
const r = linkGlobal(m.path, agents);
|
|
2700
|
-
const linked = r.linked.map(tildify4).join(", ");
|
|
2701
|
-
const skipped = r.skipped.length ? `; ${r.skipped.map(tildify4).join(", ")} already holds a different skill` : "";
|
|
2702
|
-
const message = `${m.name}: ${linked ? `linked ${linked}` : "nothing linked"}${skipped}`;
|
|
2703
|
-
return !linked && skipped ? failed(message) : message;
|
|
2704
|
-
},
|
|
2705
2303
|
moveGlobal: (skills, repos, group) => {
|
|
2706
2304
|
const moved = [];
|
|
2707
2305
|
const notes = [];
|
|
2708
2306
|
const held = [];
|
|
2709
2307
|
const repoRoots = repos.map(rootOf);
|
|
2710
|
-
for (const mv of moveGlobalSkills(skills, repoRoots, { group, label: (root) => repos[repoRoots.indexOf(root)] })) {
|
|
2308
|
+
for (const mv of moveGlobalSkills(skills, repoRoots, { group, label: (root) => repos[repoRoots.indexOf(root)], allowBlind: true })) {
|
|
2711
2309
|
if (!mv.moved) {
|
|
2712
2310
|
notes.push(`${mv.name}: ${mv.reason}`);
|
|
2713
2311
|
continue;
|
|
@@ -2745,18 +2343,14 @@ function realOps(roots, backups) {
|
|
|
2745
2343
|
const r = createSkill(name, "");
|
|
2746
2344
|
return r.ok ? `Created ${name} in your library: edit its SKILL.md from Your skills` : failed(`${name}: ${r.reason}`);
|
|
2747
2345
|
},
|
|
2748
|
-
pluginOffHere: (repo, id) => {
|
|
2749
|
-
const root = rootOf(repo);
|
|
2750
|
-
const plugin = claudePlugins(skillsLoadedIn(root, machineSkills()), root).find((p) => p.id === id);
|
|
2751
|
-
if (!plugin) return failed(`${id} isn't on in ${repo}`);
|
|
2752
|
-
const r = turnOffIn(plugin, root);
|
|
2753
|
-
return r.ok ? r.message : failed(r.message);
|
|
2754
|
-
},
|
|
2755
2346
|
replacePlugin: (p, repos) => {
|
|
2756
2347
|
const id = p.id;
|
|
2348
|
+
const clashes = p.skills.filter((s) => {
|
|
2349
|
+
const mine = treeHash(librarySkillDir(s.name));
|
|
2350
|
+
return mine !== null && mine !== treeHash(s.path);
|
|
2351
|
+
});
|
|
2352
|
+
if (clashes.length) return failed(`${id} kept: your library has different ${clashes.map((s) => s.name).join(", ")}; update or delete ${clashes.length === 1 ? "it" : "them"} first`);
|
|
2757
2353
|
const imported = p.skills.map((s) => ({ name: s.name, status: importSkill(s.path).status }));
|
|
2758
|
-
const clashes = imported.filter((r) => r.status === "exists").map((r) => r.name);
|
|
2759
|
-
if (clashes.length) return failed(`${id} kept: your library has different ${clashes.join(", ")}; update or delete ${clashes.length === 1 ? "it" : "them"} first`);
|
|
2760
2354
|
for (const r of imported) if (r.status !== "unchanged") recordOrigin(r.name, `plugin: ${id}`);
|
|
2761
2355
|
const added = repos.length ? addMany(repos, p.skills.map((s) => s.name)) : { result: "", skipped: [], notes: [] };
|
|
2762
2356
|
if (added.skipped.length) return failed(`${id} kept: ${added.skipped.join("; ")}`);
|
|
@@ -2806,7 +2400,7 @@ function realOps(roots, backups) {
|
|
|
2806
2400
|
restoreBackup: (i) => {
|
|
2807
2401
|
const b = backups[i];
|
|
2808
2402
|
const r = b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b);
|
|
2809
|
-
return r.ok ? `Restored ${b.name} to ${
|
|
2403
|
+
return r.ok ? `Restored ${b.name} to ${tildify2(r.to)}` : failed(`${b.name}: ${r.reason}`);
|
|
2810
2404
|
},
|
|
2811
2405
|
setAgents: (ids) => (setHarnesses(ids), "Saved your agents"),
|
|
2812
2406
|
installVersion: (repo, name, version) => withLinks(addSkill(rootOf(repo), name, { version, force: true }), repo, name),
|
|
@@ -2815,40 +2409,37 @@ function realOps(roots, backups) {
|
|
|
2815
2409
|
addRoot: (path) => {
|
|
2816
2410
|
const root = addRoot(path);
|
|
2817
2411
|
discoverProjects();
|
|
2818
|
-
return `Scanning ${
|
|
2412
|
+
return `Scanning ${tildify2(root)} for repos`;
|
|
2819
2413
|
},
|
|
2820
|
-
removeRoot: (path) => (removeRoot(path), `Stopped scanning ${
|
|
2414
|
+
removeRoot: (path) => (removeRoot(path), `Stopped scanning ${tildify2(path)}; repos already found stay until you hide them`),
|
|
2821
2415
|
rescan: () => (discoverProjects(), "Looked for repos in your project folders again"),
|
|
2822
2416
|
hide: (repo) => (setHidden(rootOf(repo), true), `Hid ${repo}: Settings \u203A Hidden repos brings it back`),
|
|
2823
|
-
unhide: (path) => (setHidden(path, false), `${
|
|
2417
|
+
unhide: (path) => (setHidden(path, false), `${tildify2(path)} is back in the list`),
|
|
2824
2418
|
openFolder: (path) => {
|
|
2825
2419
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
2826
2420
|
spawn(cmd, [path], { detached: true, stdio: "ignore" }).unref();
|
|
2827
|
-
return `Opened ${
|
|
2421
|
+
return `Opened ${tildify2(path)}`;
|
|
2828
2422
|
},
|
|
2829
2423
|
copy: (text, what) => {
|
|
2830
2424
|
const r = copyText(text);
|
|
2831
|
-
return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor, Codex or Grok` : `Couldn't reach the clipboard; ${what} is in ${
|
|
2425
|
+
return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor, Codex or Grok` : `Couldn't reach the clipboard; ${what} is in ${tildify2(r.savedTo)}`;
|
|
2832
2426
|
},
|
|
2833
2427
|
installAgentSkill: () => {
|
|
2834
2428
|
const r = installAgentSkill();
|
|
2835
|
-
return r.ok ? `Your agents can now use skilllib (${r.dirs.map(
|
|
2429
|
+
return r.ok ? `Your agents can now use skilllib (${r.dirs.map(tildify2).join(", ")})` : failed(`skilllib skill: ${r.reason}`);
|
|
2836
2430
|
},
|
|
2837
2431
|
removeAgentSkill: () => {
|
|
2838
2432
|
const removed = removeAgentSkill();
|
|
2839
|
-
return removed.length ? `Removed the skilllib skill (${removed.map(
|
|
2433
|
+
return removed.length ? `Removed the skilllib skill (${removed.map(tildify2).join(", ")})` : failed("The skilllib skill isn't installed");
|
|
2840
2434
|
},
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
});
|
|
2850
|
-
}
|
|
2851
|
-
return info.get(repo);
|
|
2435
|
+
usageIssues: ({ usage, totals = {} }) => {
|
|
2436
|
+
const repoOf = new Map([...roots].map(([name, root]) => [root, name]));
|
|
2437
|
+
return usageIssues([...roots.values()], machine, libraryNames, {
|
|
2438
|
+
inProject: (root, skill) => usage[repoOf.get(root)]?.[skill] ?? 0,
|
|
2439
|
+
total: (skill) => totals[skill] ?? 0,
|
|
2440
|
+
asUsed: (key) => totals[key] ?? 0,
|
|
2441
|
+
days: 30
|
|
2442
|
+
});
|
|
2852
2443
|
}
|
|
2853
2444
|
};
|
|
2854
2445
|
}
|