skilllib 1.11.2 → 1.11.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -40
- package/dist/{chunk-OFWHWIER.js → chunk-CS6T66EE.js} +1564 -677
- package/dist/index.js +45 -13
- package/dist/json.d.cts +20 -7
- package/dist/json.d.ts +20 -7
- package/dist/{tui-NGTOOVAQ.js → tui-BDLJOMSC.js} +469 -801
- package/package.json +2 -2
- package/skills/skilllib/SKILL.md +10 -7
|
@@ -1,44 +1,47 @@
|
|
|
1
1
|
import {
|
|
2
2
|
HARNESSES,
|
|
3
3
|
PROJECT_SKILLS_DIR,
|
|
4
|
+
addRefusal,
|
|
4
5
|
addRoot,
|
|
5
6
|
addSkill,
|
|
6
|
-
addedAt,
|
|
7
7
|
agentSkillState,
|
|
8
8
|
allowTrackedLinks,
|
|
9
|
-
|
|
9
|
+
blindSaid,
|
|
10
|
+
blindWithoutGlobal,
|
|
10
11
|
claudeBinary,
|
|
11
|
-
claudePlugins,
|
|
12
|
-
copyLabel,
|
|
13
12
|
createSkill,
|
|
14
|
-
cursorPluginSkills,
|
|
15
13
|
cursorPlugins,
|
|
16
14
|
deleteGlobal,
|
|
17
15
|
deleteLibrarySkill,
|
|
18
|
-
describeStep,
|
|
19
16
|
discoverProjects,
|
|
20
17
|
enabledHarnesses,
|
|
18
|
+
entryExists,
|
|
19
|
+
findIssues,
|
|
20
|
+
foldersOf,
|
|
21
21
|
forgetClaudeBinary,
|
|
22
22
|
forgetLatest,
|
|
23
23
|
gitInfo,
|
|
24
|
-
gitVisibleSteps,
|
|
25
24
|
harness,
|
|
26
25
|
harnessesChosen,
|
|
27
26
|
importSkill,
|
|
27
|
+
inTheWaySaid,
|
|
28
28
|
installAgentSkill,
|
|
29
|
+
installDirs,
|
|
29
30
|
installedPlugins,
|
|
31
|
+
isDir,
|
|
30
32
|
keptGlobal,
|
|
33
|
+
knownProjects,
|
|
31
34
|
latestVersion,
|
|
32
35
|
libraryOrigins,
|
|
33
36
|
librarySkillDir,
|
|
34
37
|
librarySkills,
|
|
35
38
|
linkEverywhere,
|
|
36
|
-
linkGlobal,
|
|
37
39
|
listBackups,
|
|
40
|
+
localStamp,
|
|
38
41
|
machineSkills,
|
|
39
42
|
moveGlobalSkills,
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
movedTogether,
|
|
44
|
+
notWritable,
|
|
42
45
|
pluginBackups,
|
|
43
46
|
projectHere,
|
|
44
47
|
projectOfFactory,
|
|
@@ -47,39 +50,45 @@ import {
|
|
|
47
50
|
readConfig,
|
|
48
51
|
readManifest,
|
|
49
52
|
readSkillInfo,
|
|
53
|
+
readableRoots,
|
|
54
|
+
realPath,
|
|
50
55
|
recordOrigin,
|
|
51
56
|
relativeTo,
|
|
52
57
|
relinkDependency,
|
|
58
|
+
rememberProjects,
|
|
53
59
|
removeAgentSkill,
|
|
54
60
|
removeRoot,
|
|
55
61
|
removeSkill,
|
|
56
62
|
removeUntracked,
|
|
57
63
|
restoreBackup,
|
|
58
64
|
restorePlugin,
|
|
65
|
+
runFix,
|
|
59
66
|
scanUsage,
|
|
60
67
|
setHarnesses,
|
|
61
68
|
setHidden,
|
|
62
69
|
setKeepGlobal,
|
|
63
70
|
setPluginOn,
|
|
71
|
+
skillOf,
|
|
64
72
|
skilllibHome,
|
|
65
73
|
skillsLoadedIn,
|
|
66
|
-
|
|
74
|
+
strandedLinks,
|
|
75
|
+
survives,
|
|
67
76
|
tildify,
|
|
68
|
-
|
|
77
|
+
trackBlocker,
|
|
78
|
+
treeHash,
|
|
69
79
|
uninstallPlugin,
|
|
70
|
-
unloadGlobal,
|
|
71
80
|
updatePlugin,
|
|
72
81
|
updateProject,
|
|
82
|
+
usageIssues,
|
|
73
83
|
versionHistory,
|
|
74
|
-
visibilityOf,
|
|
75
84
|
visibleProjects
|
|
76
|
-
} from "./chunk-
|
|
85
|
+
} from "./chunk-CS6T66EE.js";
|
|
77
86
|
|
|
78
87
|
// src/tui/index.tsx
|
|
79
88
|
import { render } from "ink";
|
|
80
89
|
|
|
81
90
|
// src/tui/App.tsx
|
|
82
|
-
import { useEffect, useRef, useState } from "react";
|
|
91
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
83
92
|
import { Box as Box2, Text as Text2, useApp, useInput, useWindowSize } from "ink";
|
|
84
93
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
85
94
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -135,7 +144,6 @@ function ListPanel({
|
|
|
135
144
|
rows,
|
|
136
145
|
selected,
|
|
137
146
|
empty,
|
|
138
|
-
filter,
|
|
139
147
|
header,
|
|
140
148
|
headerHeight = 0
|
|
141
149
|
}) {
|
|
@@ -143,7 +151,7 @@ function ListPanel({
|
|
|
143
151
|
const offset = Math.min(Math.max(0, selected - Math.floor(visible / 2)), Math.max(0, rows.length - visible));
|
|
144
152
|
const shown = rows.slice(offset, offset + visible);
|
|
145
153
|
const inner = width - 4;
|
|
146
|
-
const hint =
|
|
154
|
+
const hint = rows.length > visible ? `${selected + 1}/${rows.length}` : "";
|
|
147
155
|
return /* @__PURE__ */ jsxs(Panel, { title, hint, focused, width, height, children: [
|
|
148
156
|
header,
|
|
149
157
|
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 +174,6 @@ function ListPanel({
|
|
|
166
174
|
backgroundColor: bg,
|
|
167
175
|
color: c.color ?? (active && focused ? color.text : void 0),
|
|
168
176
|
bold: c.bold || active && focused && c.grow,
|
|
169
|
-
dimColor: c.dim,
|
|
170
177
|
wrap: "truncate-end",
|
|
171
178
|
children: fit(c.text, c.grow ? growWidth : c.width ?? c.text.length)
|
|
172
179
|
},
|
|
@@ -194,16 +201,18 @@ function wrap(text, width, max) {
|
|
|
194
201
|
return lines;
|
|
195
202
|
}
|
|
196
203
|
|
|
204
|
+
// src/tui/prompts.ts
|
|
205
|
+
import { dirname } from "path";
|
|
206
|
+
|
|
197
207
|
// src/review.ts
|
|
198
208
|
import { spawnSync } from "child_process";
|
|
199
209
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
200
210
|
import { join } from "path";
|
|
201
211
|
var EXCERPT_CHARS = 1200;
|
|
202
|
-
var tildify2 = (p) => tildify(p);
|
|
203
212
|
function skillExcerpt(dir, chars) {
|
|
204
213
|
const file = join(dir, "SKILL.md");
|
|
205
214
|
if (!existsSync(file)) return { text: "(SKILL.md not found)", files: 0, size: 0 };
|
|
206
|
-
const content = readFileSync(file, "utf-8").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
|
|
215
|
+
const content = readFileSync(file, "utf-8").replace(/^\uFEFF/, "").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
|
|
207
216
|
let files = 0;
|
|
208
217
|
try {
|
|
209
218
|
files = readdirSync(dir, { recursive: true, withFileTypes: true }).filter((d) => d.isFile()).length;
|
|
@@ -223,7 +232,7 @@ function buildReviewPrompt(skills, ctx) {
|
|
|
223
232
|
`- Description: ${s.description || "(none)"}`,
|
|
224
233
|
`- Source: ${s.source}`,
|
|
225
234
|
`- Installed via: ${s.installedHow}`,
|
|
226
|
-
`- Location: ${
|
|
235
|
+
`- Location: ${tildify(s.path)}${s.links.length ? ` (also linked from ${s.links.map(tildify).join(", ")})` : ""}`,
|
|
227
236
|
`- Loaded by: ${s.loadedBy}`,
|
|
228
237
|
...s.vendor ? [`- Vendor-managed: ${s.vendor}`] : [],
|
|
229
238
|
...s.keptGlobal ? ["- I marked this as global on purpose: recommend changing that only for a strong reason (unused, duplicated, low quality)"] : [],
|
|
@@ -249,7 +258,7 @@ function buildReviewPrompt(skills, ctx) {
|
|
|
249
258
|
- 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
259
|
|
|
251
260
|
## My projects
|
|
252
|
-
${ctx.projects.map((p) => `- ${p.name} \u2014 ${
|
|
261
|
+
${ctx.projects.map((p) => `- ${p.name} \u2014 ${tildify(p.path)}`).join("\n") || "(none)"}
|
|
253
262
|
|
|
254
263
|
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
264
|
|
|
@@ -305,16 +314,79 @@ var isFailure = (r) => typeof r !== "string" && "failed" in r;
|
|
|
305
314
|
var followUp = (r) => typeof r !== "string" && "then" in r ? r.then : void 0;
|
|
306
315
|
var said = (r) => typeof r === "string" ? r : r.message;
|
|
307
316
|
var joined = (results) => {
|
|
308
|
-
const
|
|
309
|
-
|
|
317
|
+
const bad = results.filter(isFailure);
|
|
318
|
+
const message = [...bad.length && bad.length < results.length ? [`${results.length - bad.length} of ${results.length} done`] : [], ...results.map(said)].join(" \xB7 ");
|
|
319
|
+
return bad.length ? failed(message) : message;
|
|
310
320
|
};
|
|
311
|
-
var SEVERITY_RANK = { problem: 0,
|
|
321
|
+
var SEVERITY_RANK = { problem: 0, suggestion: 1 };
|
|
322
|
+
function fixOf(c, found) {
|
|
323
|
+
return {
|
|
324
|
+
label: c.label,
|
|
325
|
+
preview: c.hint ? `${c.hint[0].toUpperCase()}${c.hint.slice(1)}. ${found.detail}` : found.detail,
|
|
326
|
+
run: () => {
|
|
327
|
+
const r = runFix(c);
|
|
328
|
+
return r.ok ? r.message : failed(r.message);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function shortOf(found) {
|
|
333
|
+
const kind = found.id.split(":")[0];
|
|
334
|
+
if (found.id === "unused:library") return "In no repo, unused 30 days";
|
|
335
|
+
if (kind === "plugin") return `Also in \u29C9 ${found.plugin.split("@")[0]}`;
|
|
336
|
+
if (kind === "cursor-plugin") return `Also in Cursor's ${found.plugin}`;
|
|
337
|
+
const short = {
|
|
338
|
+
broken: "Broken link",
|
|
339
|
+
dup: "Loaded twice",
|
|
340
|
+
tidy: "Extra copies",
|
|
341
|
+
conflict: "Copies differ",
|
|
342
|
+
review: "Not reviewed",
|
|
343
|
+
usable: "Not every agent can use it",
|
|
344
|
+
sync: "Folder missing",
|
|
345
|
+
unavailable: "Pinned, not in your library",
|
|
346
|
+
pin: "Pinned version not in your library",
|
|
347
|
+
"out-of-sync": "Not the pinned version",
|
|
348
|
+
update: "Out of date",
|
|
349
|
+
uncommitted: "Commit it first",
|
|
350
|
+
local: "Not in your library",
|
|
351
|
+
adopt: "Not tracked",
|
|
352
|
+
differs: "Differs from your library",
|
|
353
|
+
npx: "From npx skills",
|
|
354
|
+
"plugin-update": "Update available",
|
|
355
|
+
"plugin-unused": "Unused 30 days",
|
|
356
|
+
edited: "Edited here",
|
|
357
|
+
"not-in-lib": "No longer in your library",
|
|
358
|
+
twice: "Repo and global copies",
|
|
359
|
+
unused: "Unused 30 days"
|
|
360
|
+
};
|
|
361
|
+
return short[kind] ?? found.title;
|
|
362
|
+
}
|
|
363
|
+
var views = /* @__PURE__ */ new WeakMap();
|
|
364
|
+
function issueOf(found) {
|
|
365
|
+
let view = views.get(found);
|
|
366
|
+
if (!view) views.set(found, view = viewOf(found));
|
|
367
|
+
return view;
|
|
368
|
+
}
|
|
369
|
+
function viewOf(found) {
|
|
370
|
+
return {
|
|
371
|
+
id: found.id,
|
|
372
|
+
severity: found.severity,
|
|
373
|
+
title: found.title,
|
|
374
|
+
short: shortOf(found),
|
|
375
|
+
detail: found.detail,
|
|
376
|
+
skills: [...new Set(found.at?.flatMap((a) => a.skill ? [a.skill] : []))],
|
|
377
|
+
decision: !found.fix,
|
|
378
|
+
fixes: (found.fix ? [found.fix] : found.choices ?? []).map((c) => fixOf(c, found))
|
|
379
|
+
};
|
|
380
|
+
}
|
|
312
381
|
function filterOf(source) {
|
|
313
382
|
return source === "lib" || source === "repo" || source === "untracked" ? "local" : source === "global" || source === "skilllib" ? "global" : source === "plugin" ? "plugins" : "vendor";
|
|
314
383
|
}
|
|
315
384
|
function project(w, name) {
|
|
316
385
|
return w.projects.find((p) => p.name === name);
|
|
317
386
|
}
|
|
387
|
+
function usesKey(m) {
|
|
388
|
+
return m.source === "plugin" ? `${m.where.split("@")[0]}:${m.name}` : m.name;
|
|
389
|
+
}
|
|
318
390
|
function totalUses(w, name) {
|
|
319
391
|
return Object.values(w.usage).reduce((n, u) => n + (u[name] ?? 0), 0);
|
|
320
392
|
}
|
|
@@ -334,344 +406,45 @@ function usable(w, projectName) {
|
|
|
334
406
|
const uses = (n) => w.usage[projectName]?.[n] ?? 0;
|
|
335
407
|
return [
|
|
336
408
|
...p.skills.map((s) => ({ name: s.name, source: s.source, where: s.dir, agents: s.agents, uses: uses(s.name), local: s })),
|
|
337
|
-
...machineIn(w, p).map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: uses(m
|
|
409
|
+
...machineIn(w, p).map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: uses(usesKey(m)), machine: m }))
|
|
338
410
|
];
|
|
339
411
|
}
|
|
340
|
-
function
|
|
341
|
-
|
|
412
|
+
function issuesAt(w, repo, skill) {
|
|
413
|
+
const root = repo === null ? null : project(w, repo).path;
|
|
414
|
+
return w.issues.filter((i) => i.at?.some((a) => a.root === root && a.skill === skill)).map(issueOf);
|
|
342
415
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
return repos.length ? `, and ${repos.join(", ")} used it in the last 30 days` : "";
|
|
416
|
+
function repoIssues(w, repo) {
|
|
417
|
+
const root = project(w, repo).path;
|
|
418
|
+
return w.issues.filter((i) => i.at?.some((a) => a.root === root)).map(issueOf);
|
|
347
419
|
}
|
|
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
|
-
];
|
|
420
|
+
function unlisted(w, repo) {
|
|
421
|
+
const root = project(w, repo).path;
|
|
422
|
+
const listed = new Set(usable(w, repo).map((u) => u.name));
|
|
423
|
+
return [...new Set(w.issues.flatMap((i) => (i.at ?? []).filter((a) => a.root === root && a.skill && !listed.has(a.skill)).map((a) => a.skill)))];
|
|
381
424
|
}
|
|
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
|
-
};
|
|
425
|
+
function libraryIssues(w, name) {
|
|
426
|
+
return w.issues.filter((i) => i.id === "unused:library" && i.at?.some((a) => a.skill === name)).map(issueOf);
|
|
427
|
+
}
|
|
428
|
+
function pluginIssues(w, p) {
|
|
429
|
+
const id = p.agent === "cursor" ? pluginName(p) : p.id;
|
|
430
|
+
return w.issues.filter((i) => i.plugin === id && (i.pluginAt === void 0 || i.pluginAt === (p.projectPath ?? null))).map(issueOf);
|
|
397
431
|
}
|
|
398
432
|
function globalFixes(m) {
|
|
399
433
|
return [
|
|
400
434
|
{
|
|
401
435
|
label: "Move it to the repos that need it\u2026",
|
|
402
|
-
preview: `Pick repos. ${m.name} goes into your library and those repos, and stops loading globally (
|
|
436
|
+
preview: `Pick repos. ${m.name} goes into your library and those repos, and stops loading globally (every global copy backed up).`,
|
|
403
437
|
run: () => "",
|
|
404
438
|
pickRepos: (w, repos) => w.ops.moveGlobal([m], repos)
|
|
405
439
|
},
|
|
406
|
-
m.kept ? { label: "Stop marking it as global on purpose", preview:
|
|
407
|
-
{ label: "Delete it", preview: `Remove ${m.
|
|
440
|
+
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) },
|
|
441
|
+
{ label: "Delete it", preview: `Remove ${m.where}/${m.name}. A backup is kept in Settings \u203A Backups.`, run: (w) => w.ops.deleteGlobal(m) }
|
|
408
442
|
];
|
|
409
443
|
}
|
|
410
444
|
function machineActions(m) {
|
|
411
445
|
if (m.broken) return [];
|
|
412
446
|
return m.source === "global" ? globalFixes(m) : m.source === "skilllib" ? [removeAgentSkillFix()] : [];
|
|
413
447
|
}
|
|
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
448
|
function agentSkillFix(w) {
|
|
676
449
|
return {
|
|
677
450
|
label: w.agentSkill === "outdated" ? "Update the skilllib skill" : "Install the skilllib skill for your agents",
|
|
@@ -686,35 +459,11 @@ function removeAgentSkillFix() {
|
|
|
686
459
|
run: (w) => w.ops.removeAgentSkill()
|
|
687
460
|
};
|
|
688
461
|
}
|
|
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
462
|
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;
|
|
463
|
+
const bad = fixes.map((fix) => fix.run(w)).filter(isFailure).map(said);
|
|
464
|
+
const ok = fixes.length - bad.length;
|
|
715
465
|
const message = [
|
|
716
466
|
`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
467
|
bad.length ? `couldn't: ${bad.join("; ")}` : ""
|
|
719
468
|
].filter(Boolean).join(" \xB7 ");
|
|
720
469
|
return bad.length ? failed(message) : message;
|
|
@@ -725,13 +474,9 @@ function worst(issues) {
|
|
|
725
474
|
function tally(issues) {
|
|
726
475
|
return {
|
|
727
476
|
problem: issues.filter((i) => i.severity === "problem").length,
|
|
728
|
-
|
|
729
|
-
hint: issues.filter((i) => i.severity === "hint").length
|
|
477
|
+
suggestion: issues.filter((i) => i.severity === "suggestion").length
|
|
730
478
|
};
|
|
731
479
|
}
|
|
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
480
|
function deleteLibraryFix(w, names) {
|
|
736
481
|
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
482
|
const repos = [...new Set(tracked.map((t) => t.repo))];
|
|
@@ -742,9 +487,9 @@ function deleteLibraryFix(w, names) {
|
|
|
742
487
|
label: `Delete ${names.length === 1 ? "" : `all ${names.length} `}from your library${repos.length ? ` and ${repos.length} repo${repos.length === 1 ? "" : "s"}\u2026` : ""}`,
|
|
743
488
|
preview: [
|
|
744
489
|
`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 ?
|
|
490
|
+
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
491
|
edited.length ? `${edited.join(", ")} ${edited.length === 1 ? "has" : "have"} local edits: backed up too.` : "",
|
|
747
|
-
"Every copy goes to Settings \u203A Backups.",
|
|
492
|
+
"Every copy goes to Settings \u203A Backups; where a repo commits its copy, git will see it.",
|
|
748
493
|
others.length ? `Untracked or the team's own copies in ${others.join(", ")} stay.` : ""
|
|
749
494
|
].filter(Boolean).join(" "),
|
|
750
495
|
run: (w2) => joined(names.map((n) => w2.ops.deleteLibrary(n)))
|
|
@@ -773,7 +518,8 @@ function replacePluginFix(p) {
|
|
|
773
518
|
`Copies its ${plural(names.length, "skill")} into your library and adds them to the repos you tick (or none),`,
|
|
774
519
|
synced ? `then turns ${p.id} off in Claude Code. It's synced from your claude.ai account: to delete it for good, remove it there.` : p.settingsOnly ? `then turns ${p.id} off in ~/.claude/settings.json. It isn't installed, only turned on there, so there's nothing to uninstall.` : `then uninstalls ${p.id}${p.repo ? ` from ${p.repo}` : ""} (its saved data is kept).`,
|
|
775
520
|
p.parts.length ? `It also brings ${p.parts.join(", ")}: those ${offOnly ? "stop" : "go"} too.` : "",
|
|
776
|
-
|
|
521
|
+
// A plugin only turned on in settings isn't listed in Plugins once it's off: say where it comes back.
|
|
522
|
+
offOnly ? p.settingsOnly ? "Turn it back on with /plugin in Claude Code." : "Turn it back on from Plugins." : "Settings \u203A Backups reinstalls it."
|
|
777
523
|
].filter(Boolean).join(" "),
|
|
778
524
|
run: () => "",
|
|
779
525
|
allowNone: true,
|
|
@@ -781,23 +527,18 @@ function replacePluginFix(p) {
|
|
|
781
527
|
pickRepos: (w, repos) => w.ops.replacePlugin(p, repos)
|
|
782
528
|
};
|
|
783
529
|
}
|
|
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
530
|
function pluginSwitchFix(p, on) {
|
|
791
531
|
const where = p.repo ? ` in ${p.repo} (${p.scope} install)` : p.scope === "claude.ai" ? " on this machine" : " in every repo";
|
|
792
532
|
return {
|
|
793
|
-
|
|
794
|
-
|
|
533
|
+
// doctor's wording, so the details list it once when an issue offers it too.
|
|
534
|
+
label: `Turn the plugin ${p.id} ${on ? "on" : "off"}`,
|
|
535
|
+
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
536
|
run: (w) => w.ops.setPlugin(p, on)
|
|
796
537
|
};
|
|
797
538
|
}
|
|
798
539
|
function uninstallPluginFix(p) {
|
|
799
540
|
return {
|
|
800
|
-
label:
|
|
541
|
+
label: `Uninstall the plugin ${p.id}`,
|
|
801
542
|
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
543
|
run: (w) => w.ops.uninstallPlugin(p)
|
|
803
544
|
};
|
|
@@ -809,58 +550,6 @@ function updatePluginFix(p) {
|
|
|
809
550
|
run: (w) => w.ops.updatePlugin(p)
|
|
810
551
|
};
|
|
811
552
|
}
|
|
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
553
|
function originGroup(origin) {
|
|
865
554
|
if (!origin) return null;
|
|
866
555
|
const plugin = origin.match(/^plugin: (.+)$/);
|
|
@@ -923,8 +612,8 @@ function addRows(w, target, q) {
|
|
|
923
612
|
const taken = new Set(target ? project(w, target).skills.map((s) => s.name) : []);
|
|
924
613
|
const loaded = new Set(target ? usable(w, target).map((u) => u.name) : []);
|
|
925
614
|
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] ?? ""));
|
|
615
|
+
const groups = target ? assignGroups(w.library.map((l) => groupCandidates(w, l))) : [];
|
|
616
|
+
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
617
|
const skillRow = (l, inGroup) => ({
|
|
929
618
|
key: `lib:${l.name}`,
|
|
930
619
|
name: l.name,
|
|
@@ -936,7 +625,7 @@ function addRows(w, target, q) {
|
|
|
936
625
|
});
|
|
937
626
|
const library = [];
|
|
938
627
|
const done = /* @__PURE__ */ new Set();
|
|
939
|
-
for (const { l, g } of
|
|
628
|
+
for (const { l, g } of shown) {
|
|
940
629
|
if (done.has(l.name)) continue;
|
|
941
630
|
const members = g ? shown.filter((x) => x.g?.key === g.key).map((x) => x.l) : [l];
|
|
942
631
|
members.forEach((m) => done.add(m.name));
|
|
@@ -969,50 +658,53 @@ function addRows(w, target, q) {
|
|
|
969
658
|
}
|
|
970
659
|
|
|
971
660
|
// src/tui/prompts.ts
|
|
972
|
-
var
|
|
661
|
+
var agentNames = (agents) => agents.map((a) => harness(a).name).join(", ") || "none of my agents";
|
|
973
662
|
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
663
|
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
664
|
function reviewPrompt(w, names, scope, repo) {
|
|
665
|
+
const project2 = repo ? w.projects.find((p) => p.name === repo) : void 0;
|
|
666
|
+
const machine = project2 ? machineIn(w, project2) : w.machine;
|
|
976
667
|
const skills = names.map((name) => {
|
|
977
|
-
const copies =
|
|
978
|
-
const here =
|
|
668
|
+
const copies = machine.filter((m2) => m2.name === name);
|
|
669
|
+
const here = project2?.skills.find((s) => s.name === name);
|
|
979
670
|
if (here) {
|
|
980
|
-
const
|
|
671
|
+
const lib2 = w.library.find((l) => l.name === name);
|
|
981
672
|
return {
|
|
982
673
|
name,
|
|
983
|
-
description: w.descriptions[name] ?? "",
|
|
674
|
+
description: here.description ?? w.descriptions[name] ?? "",
|
|
984
675
|
source: localSource(here),
|
|
985
676
|
installedHow: localInstalled(here),
|
|
986
677
|
path: here.path,
|
|
987
678
|
links: [],
|
|
988
|
-
loadedBy: `${
|
|
679
|
+
loadedBy: `${agentNames(here.agents)} \u2014 in ${repo}`,
|
|
989
680
|
vendor: null,
|
|
990
681
|
usesTotal: w.days ? totalUses(w, name) : null,
|
|
991
682
|
usesByProject: Object.entries(w.usage).flatMap(([p, u]) => u[name] ? [[p, u[name]]] : []),
|
|
992
683
|
installedIn: w.projects.filter((p) => p.skills.some((s) => s.name === name)).map((p) => p.name),
|
|
993
684
|
otherCopies: copies.map((c) => `${c.source} ${c.where}`),
|
|
994
|
-
inYourSkills:
|
|
685
|
+
inYourSkills: lib2 ? `yes, v${lib2.latest}` : null,
|
|
995
686
|
keptGlobal: void 0
|
|
996
687
|
};
|
|
997
688
|
}
|
|
998
689
|
const m = copies.find((c) => c.source === "global") ?? copies[0];
|
|
999
|
-
const
|
|
690
|
+
const lib = w.library.find((l) => l.name === name);
|
|
1000
691
|
const repos = w.projects.filter((p) => p.skills.some((s) => s.name === name));
|
|
1001
692
|
const local = repos[0]?.skills.find((s) => s.name === name);
|
|
1002
693
|
return {
|
|
1003
694
|
name,
|
|
1004
695
|
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 ?? (
|
|
696
|
+
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",
|
|
697
|
+
installedHow: m?.origin ?? (m ? m.where : lib ? "created or imported into skilllib" : local ? localInstalled(local) : "committed in the repo"),
|
|
698
|
+
path: m?.path ?? (lib ? dirname(w.ops.libraryFile(name)) : local?.path ?? ""),
|
|
1008
699
|
links: m?.links ?? [],
|
|
1009
|
-
loadedBy: m ? `${
|
|
700
|
+
loadedBy: m ? `${agentNames(m.agents)} \u2014 ${w.machine.some((g) => g.path === m.path) ? "in every repo" : `in ${repo}`}` : repos.map((p) => `${agentNames(p.skills.find((s) => s.name === name).agents)} \u2014 in ${p.name}`).join("; ") || "none of my agents",
|
|
1010
701
|
vendor: m && m.source !== "global" ? "turn it off at its source (/plugin in Claude Code, claude.ai settings, or Cursor)" : null,
|
|
1011
|
-
|
|
1012
|
-
|
|
702
|
+
// A plugin's copy is counted under plugin:skill.
|
|
703
|
+
usesTotal: w.days ? totalUses(w, m ? usesKey(m) : name) : null,
|
|
704
|
+
usesByProject: Object.entries(w.usage).flatMap(([p, u]) => /* @__PURE__ */ ((n) => n ? [[p, n]] : [])(u[m ? usesKey(m) : name])),
|
|
1013
705
|
installedIn: repos.map((p) => p.name),
|
|
1014
706
|
otherCopies: copies.filter((c) => c !== m).map((c) => `${c.source} ${c.where}`),
|
|
1015
|
-
inYourSkills:
|
|
707
|
+
inYourSkills: lib ? `yes, v${lib.latest}` : null,
|
|
1016
708
|
keptGlobal: m?.kept
|
|
1017
709
|
};
|
|
1018
710
|
});
|
|
@@ -1025,7 +717,7 @@ function reviewPrompt(w, names, scope, repo) {
|
|
|
1025
717
|
}
|
|
1026
718
|
function writeSkillPrompt(w, name, repo) {
|
|
1027
719
|
const p = repo ? w.projects.find((x) => x.name === repo) : void 0;
|
|
1028
|
-
const file = p ? `${p.path}
|
|
720
|
+
const file = p ? `${p.path}/${installDirs(w.agents)[0]}/${name || "<name>"}/SKILL.md` : w.ops.libraryFile(name || "<name>");
|
|
1029
721
|
return `Write a new agent skill${name ? ` named "${name}"` : ""}${p ? ` for the repo at ${p.path}` : " for my skill library"}.
|
|
1030
722
|
|
|
1031
723
|
Create ${file} with:
|
|
@@ -1039,7 +731,6 @@ Keep it under 200 lines. ${p ? "skilllib will offer to import it into my library
|
|
|
1039
731
|
|
|
1040
732
|
// src/tui/App.tsx
|
|
1041
733
|
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1042
|
-
var tildify3 = (p) => tildify(p);
|
|
1043
734
|
var PLACES = [
|
|
1044
735
|
["projects", "Projects"],
|
|
1045
736
|
["library", "Your skills"],
|
|
@@ -1058,13 +749,12 @@ var CHIPS = [
|
|
|
1058
749
|
];
|
|
1059
750
|
var SEV = {
|
|
1060
751
|
problem: { icon: "\u2715", color: color.red },
|
|
1061
|
-
|
|
1062
|
-
hint: { icon: "\xB7", color: color.blue }
|
|
752
|
+
suggestion: { icon: "\u26A0", color: color.yellow }
|
|
1063
753
|
};
|
|
1064
754
|
var SIDEBAR_AT = 80;
|
|
1065
755
|
var SIDEBAR_W = 28;
|
|
1066
756
|
var bySeverity = (a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
|
|
1067
|
-
function tag(u
|
|
757
|
+
function tag(u) {
|
|
1068
758
|
const s = u.local;
|
|
1069
759
|
if (s?.source === "lib") {
|
|
1070
760
|
return [`lib v${s.version}`, color.accent];
|
|
@@ -1076,7 +766,7 @@ function tag(u, w) {
|
|
|
1076
766
|
return [u.source, color.muted];
|
|
1077
767
|
}
|
|
1078
768
|
function isNameChar(input, key) {
|
|
1079
|
-
return !key.ctrl && !key.meta && /^[a-zA-Z0-9._-]
|
|
769
|
+
return !key.ctrl && !key.meta && /^[a-zA-Z0-9._-]+$/.test(input);
|
|
1080
770
|
}
|
|
1081
771
|
function clip(text, width) {
|
|
1082
772
|
return text.length > width - 1 ? text.slice(0, width - 2) + "\u2026" : text;
|
|
@@ -1092,7 +782,7 @@ function builder(title) {
|
|
|
1092
782
|
issues(issues) {
|
|
1093
783
|
if (!issues.length) return d.body.push({ segs: [["\u2713 No issues", color.green]] });
|
|
1094
784
|
for (const issue of [...issues].sort(bySeverity)) {
|
|
1095
|
-
d.body.push({ segs: [[`${SEV[issue.severity].icon} `, SEV[issue.severity].color], [issue.title, color.text], [issue.decision ? " \xB7 your call" : "", color.faint]] });
|
|
785
|
+
d.body.push({ segs: [[`${SEV[issue.severity].icon} `, SEV[issue.severity].color], [issue.title, color.text], [issue.decision && issue.fixes.length ? " \xB7 your call" : "", color.faint]] });
|
|
1096
786
|
issue.fixes.forEach((fix, i) => d.body.push({ segs: [], option: d.options.push({ label: fix.label, fix, issue, recommended: !issue.decision && i === 0 }) - 1 }));
|
|
1097
787
|
}
|
|
1098
788
|
return 0;
|
|
@@ -1129,10 +819,6 @@ function byName(copies) {
|
|
|
1129
819
|
for (const c of copies) groups.set(c.name, [...groups.get(c.name) ?? [], c]);
|
|
1130
820
|
return [...groups.values()].map((g) => g.sort((a, b) => COPY_RANK[a.source] - COPY_RANK[b.source]));
|
|
1131
821
|
}
|
|
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
822
|
var BLOCKS = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
1137
823
|
function barChart(values, width, height = 3) {
|
|
1138
824
|
const max = Math.max(1, ...values);
|
|
@@ -1171,7 +857,7 @@ function usageLines(w, name, width, repo) {
|
|
|
1171
857
|
function skillItem(w, copies, issues, nameW, actions, repo) {
|
|
1172
858
|
const u = copies[0];
|
|
1173
859
|
const top = worst(issues);
|
|
1174
|
-
const [t, tc] = tag(u
|
|
860
|
+
const [t, tc] = tag(u);
|
|
1175
861
|
return {
|
|
1176
862
|
key: u.name,
|
|
1177
863
|
name: u.name,
|
|
@@ -1189,41 +875,70 @@ function skillItem(w, copies, issues, nameW, actions, repo) {
|
|
|
1189
875
|
],
|
|
1190
876
|
detail: (width) => {
|
|
1191
877
|
const b = builder(u.name);
|
|
1192
|
-
for (const l of wrap(w.descriptions[u.name] || "(no description)", width - 4, 3)) b.line([l, color.muted]);
|
|
878
|
+
for (const l of wrap(u.local?.description || w.descriptions[u.name] || "(no description)", width - 4, 3)) b.line([l, color.muted]);
|
|
1193
879
|
b.line();
|
|
1194
|
-
|
|
880
|
+
const here = !!u.machine && !w.machine.some((m) => m.path === u.machine.path && m.where === u.machine.where);
|
|
881
|
+
if (repo && !u.local) b.line([here ? "Loads in this repo (its settings turn it on)" : "Loads in every repo", color.text], [u.source === "global" || u.source === "skilllib" ? " issues and cleanup live in Global" : " managed at its source", color.faint]);
|
|
1195
882
|
else b.issues(issues);
|
|
1196
883
|
b.actions(actions, width);
|
|
1197
884
|
b.line();
|
|
1198
|
-
for (const segs of usageLines(w, u.name, width, repo)) b.line(...segs);
|
|
885
|
+
for (const segs of usageLines(w, u.machine ? usesKey(u.machine) : u.name, width, repo)) b.line(...segs);
|
|
1199
886
|
b.line();
|
|
1200
887
|
b.line(["Loaded from", color.accent]);
|
|
1201
888
|
for (const c of copies) {
|
|
1202
889
|
const s = c.local;
|
|
1203
890
|
const path = s ? `${s.dir}/${c.name}` : c.source === "global" || c.source === "skilllib" ? `${c.where}/${c.name}` : c.where;
|
|
1204
891
|
const extra = s ? s.missing ? "missing" : s.git ? GIT_LABEL[s.git] : "" : "";
|
|
1205
|
-
const [ct, cc] = c === u ? ["", void 0] : tag(c
|
|
892
|
+
const [ct, cc] = c === u ? ["", void 0] : tag(c);
|
|
1206
893
|
b.line([" ", void 0], [path + " ", color.text], [ct ? ct + " " : "", cc], [extra, color.faint]);
|
|
894
|
+
for (const dir of s?.others ?? []) b.line([" ", void 0], [`${dir}/${c.name} `, color.text], ["another copy", color.faint]);
|
|
1207
895
|
}
|
|
1208
896
|
return b.d;
|
|
1209
897
|
}
|
|
1210
898
|
};
|
|
1211
899
|
}
|
|
1212
900
|
function projectSkillItems(w, p, nameW, ui) {
|
|
1213
|
-
return
|
|
1214
|
-
(
|
|
1215
|
-
|
|
901
|
+
return [
|
|
902
|
+
...byName(usable(w, p)).map(
|
|
903
|
+
(copies) => skillItem(w, copies, copies.some((c) => c.local) ? issuesAt(w, p, copies[0].name) : [], nameW, skillActions(w, p, copies[0], ui), p)
|
|
904
|
+
),
|
|
905
|
+
// A link to nothing loads nothing, but its issue still needs a row (and its fix, Fix all).
|
|
906
|
+
...unlisted(w, p).map((name) => {
|
|
907
|
+
const issues = issuesAt(w, p, name);
|
|
908
|
+
const top = worst(issues);
|
|
909
|
+
return {
|
|
910
|
+
key: name,
|
|
911
|
+
name,
|
|
912
|
+
group: "local",
|
|
913
|
+
issues,
|
|
914
|
+
uses: 0,
|
|
915
|
+
cells: [
|
|
916
|
+
{ text: SEV[top.severity].icon, width: 2, color: SEV[top.severity].color },
|
|
917
|
+
{ text: clip(name, nameW), width: nameW },
|
|
918
|
+
{ text: "\u2013", width: 14, color: color.faint },
|
|
919
|
+
{ text: usesText(0), width: 6, color: color.muted },
|
|
920
|
+
{ text: " " + top.short, grow: true, color: SEV[top.severity].color }
|
|
921
|
+
],
|
|
922
|
+
detail: () => {
|
|
923
|
+
const b = builder(name);
|
|
924
|
+
b.issues(issues);
|
|
925
|
+
return b.d;
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
})
|
|
929
|
+
];
|
|
1216
930
|
}
|
|
1217
931
|
var reviewOption = (w, name, ui, repo) => ({
|
|
1218
932
|
label: "Review prompt",
|
|
1219
933
|
action: () => ui.copy(reviewPrompt(w, [name], `Review the skill ${name}${repo ? ` in ${repo}` : ""}.`, repo), `a review prompt for ${name}`)
|
|
1220
934
|
});
|
|
935
|
+
var gitSees = (s) => s?.git === "committed" || s?.git === "changed" ? " Git will see it." : "";
|
|
1221
936
|
function versionsOption(w, repo, name, installed, ui) {
|
|
1222
937
|
return {
|
|
1223
938
|
label: "Other versions\u2026",
|
|
1224
939
|
action: () => ui.menu(
|
|
1225
940
|
`${name}: versions`,
|
|
1226
|
-
w.ops.versions(name).reverse().map((v) => fixOption({ label: `v${v.version} ${v.date}${v.version === installed ? " (installed)" : ""}`, preview: `Install ${name} v${v.version} in ${repo}
|
|
941
|
+
w.ops.versions(name).reverse().map((v) => fixOption({ label: `v${v.version} ${v.date}${v.version === installed ? " (installed)" : ""}`, preview: `Install ${name} v${v.version} in ${repo}.${gitSees(project(w, repo).skills.find((x) => x.name === name))}`, run: (w2) => w2.ops.installVersion(repo, name, v.version) }))
|
|
1227
942
|
)
|
|
1228
943
|
};
|
|
1229
944
|
}
|
|
@@ -1233,14 +948,15 @@ function skillActions(w, p, u, ui) {
|
|
|
1233
948
|
const remove = {
|
|
1234
949
|
// Its folder gone: all that's left to remove is the skilllib.json entry (the label Health offers too).
|
|
1235
950
|
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
|
|
951
|
+
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.` : "") + // A managed skill's removal edits skilllib.json too.
|
|
952
|
+
(gitSees(s) || (s?.source === "lib" && project(w, p).manifest === "committed" ? " Git will see skilllib.json change." : "")),
|
|
1237
953
|
run: (w2) => w2.ops.remove(p, u.name)
|
|
1238
954
|
};
|
|
1239
955
|
if (s?.source === "lib")
|
|
1240
956
|
return [
|
|
1241
957
|
// A tracked skill your library doesn't have: nothing to edit there, no versions to pick.
|
|
1242
958
|
...inLibrary ? [
|
|
1243
|
-
...s.version < inLibrary.latest ? [fixOption({ label: `Update to v${inLibrary.latest}`, preview: `Replace ${s.dir}/${u.name} with library v${inLibrary.latest}
|
|
959
|
+
...s.version < inLibrary.latest ? [fixOption({ label: `Update to v${inLibrary.latest}`, preview: `Replace ${s.dir}/${u.name} with library v${inLibrary.latest}.${gitSees(s)}`, run: (w2) => w2.ops.update(p, u.name) })] : [],
|
|
1244
960
|
{ label: "Edit in library", action: () => ui.edit(w.ops.libraryFile(u.name)) },
|
|
1245
961
|
versionsOption(w, p, u.name, s.version, ui)
|
|
1246
962
|
] : [],
|
|
@@ -1255,22 +971,19 @@ function skillActions(w, p, u, ui) {
|
|
|
1255
971
|
];
|
|
1256
972
|
if (s)
|
|
1257
973
|
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
974
|
{ label: "Edit SKILL.md", action: () => ui.edit(`${s.path}/SKILL.md`) },
|
|
1260
975
|
fixOption(remove),
|
|
1261
976
|
reviewOption(w, u.name, ui, p)
|
|
1262
977
|
];
|
|
1263
|
-
|
|
978
|
+
const everywhere = !!u.machine && w.machine.some((m) => m.path === u.machine.path && m.where === u.machine.where);
|
|
979
|
+
return everywhere ? [{ label: "Open in Global", action: () => ui.openInGlobal(u.name) }, reviewOption(w, u.name, ui)] : [reviewOption(w, u.name, ui, p)];
|
|
1264
980
|
}
|
|
1265
981
|
function overview(w, p) {
|
|
1266
982
|
const project2 = project(w, p);
|
|
1267
|
-
const info = w.ops.repoInfo(p);
|
|
1268
983
|
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
984
|
const plugins = [...new Set(machineIn(w, project2).filter((m) => m.source === "plugin").map((m) => m.where.split("@")[0]))];
|
|
1270
985
|
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]]],
|
|
986
|
+
["Folder", [[tildify(project2.path), color.text]]],
|
|
1274
987
|
["Manifest", [manifest]],
|
|
1275
988
|
["Plugins", [[plugins.length ? plugins.join(", ") : "none", plugins.length ? color.magenta : color.faint]]]
|
|
1276
989
|
];
|
|
@@ -1292,7 +1005,7 @@ function InfoGrid({ rows, columns, width }) {
|
|
|
1292
1005
|
}
|
|
1293
1006
|
function projectItems(w) {
|
|
1294
1007
|
return w.projects.map((p) => {
|
|
1295
|
-
const issues =
|
|
1008
|
+
const issues = repoIssues(w, p.name);
|
|
1296
1009
|
const t = tally(issues);
|
|
1297
1010
|
return {
|
|
1298
1011
|
key: p.name,
|
|
@@ -1304,15 +1017,14 @@ function projectItems(w) {
|
|
|
1304
1017
|
{ text: p.name, width: 18 },
|
|
1305
1018
|
{ text: p.skills.length ? `${p.skills.length} skill${p.skills.length === 1 ? "" : "s"}` : "\u2013", width: 10, color: color.muted },
|
|
1306
1019
|
{ 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 }
|
|
1020
|
+
{ text: t.suggestion ? `\u26A0 ${t.suggestion}` : "", grow: true, color: color.yellow }
|
|
1309
1021
|
]
|
|
1310
1022
|
};
|
|
1311
1023
|
});
|
|
1312
1024
|
}
|
|
1313
1025
|
function globalItems(w, nameW, ui) {
|
|
1314
|
-
const copies = w.machine.map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: totalUses(w, m
|
|
1315
|
-
return byName(copies).map((g) => skillItem(w, g,
|
|
1026
|
+
const copies = w.machine.map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: totalUses(w, usesKey(m)), machine: m }));
|
|
1027
|
+
return byName(copies).map((g) => skillItem(w, g, issuesAt(w, null, g[0].name), nameW, globalActions(w, g[0], ui), null));
|
|
1316
1028
|
}
|
|
1317
1029
|
var AT_SOURCE = {
|
|
1318
1030
|
global: null,
|
|
@@ -1340,7 +1052,8 @@ function libraryItems(w, nameW, ui) {
|
|
|
1340
1052
|
const uses = totalUses(w, l.name);
|
|
1341
1053
|
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
1054
|
const delFix = deleteLibraryFix(w, [l.name]);
|
|
1343
|
-
const issues =
|
|
1055
|
+
const issues = libraryIssues(w, l.name);
|
|
1056
|
+
const sev = issues[0] && SEV[issues[0].severity];
|
|
1344
1057
|
const behind = repos.filter((p) => p.skills.some((s) => s.name === l.name && s.source === "lib" && s.version < l.latest)).length;
|
|
1345
1058
|
return {
|
|
1346
1059
|
key: l.name,
|
|
@@ -1350,12 +1063,12 @@ function libraryItems(w, nameW, ui) {
|
|
|
1350
1063
|
cands: groupCandidates(w, l),
|
|
1351
1064
|
lib: l,
|
|
1352
1065
|
cells: [
|
|
1353
|
-
{ text:
|
|
1066
|
+
{ text: sev ? sev.icon : " ", width: 2, color: sev?.color },
|
|
1354
1067
|
{ text: clip(l.name, nameW), width: nameW },
|
|
1355
1068
|
{ text: `v${l.latest}`, width: 5, color: color.accent },
|
|
1356
|
-
{ text: `${repos.length} repo${repos.length === 1 ? "" : "s"}${behind ?
|
|
1069
|
+
{ text: clip(`${repos.length} repo${repos.length === 1 ? "" : "s"}${behind ? `, ${behind} behind` : ""}`, 16), width: 16, color: behind ? color.yellow : color.muted },
|
|
1357
1070
|
{ text: usesText(uses), width: 6, color: color.muted },
|
|
1358
|
-
{ text: " " + (issues[0]?.short ?? w.descriptions[l.name] ?? ""), grow: true, color:
|
|
1071
|
+
{ text: " " + (issues[0]?.short ?? w.descriptions[l.name] ?? ""), grow: true, color: sev ? sev.color : color.faint }
|
|
1359
1072
|
],
|
|
1360
1073
|
detail: (width) => {
|
|
1361
1074
|
const b = builder(l.name);
|
|
@@ -1373,7 +1086,7 @@ function libraryItems(w, nameW, ui) {
|
|
|
1373
1086
|
...removable.length ? [
|
|
1374
1087
|
fixOption({
|
|
1375
1088
|
label: "Remove from repos\u2026",
|
|
1376
|
-
preview: `Pick repos to remove ${l.name} from. Your library keeps it.`,
|
|
1089
|
+
preview: `Pick repos to remove ${l.name} from. Your library keeps it. Where a repo commits its copy, git will see it.`,
|
|
1377
1090
|
run: () => "",
|
|
1378
1091
|
candidates: () => removable.map((p) => p.name),
|
|
1379
1092
|
pickRepos: (w2, rs) => joined(rs.map((r) => w2.ops.remove(r, l.name)))
|
|
@@ -1390,43 +1103,37 @@ function libraryItems(w, nameW, ui) {
|
|
|
1390
1103
|
};
|
|
1391
1104
|
});
|
|
1392
1105
|
}
|
|
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",
|
|
1106
|
+
function healthItems(w) {
|
|
1107
|
+
return w.issues.map((found) => {
|
|
1108
|
+
const issue = issueOf(found);
|
|
1109
|
+
const sev = SEV[issue.severity];
|
|
1110
|
+
return {
|
|
1111
|
+
key: issue.id,
|
|
1112
|
+
name: issue.title,
|
|
1405
1113
|
issues: [issue],
|
|
1406
1114
|
uses: 0,
|
|
1407
1115
|
cells: [
|
|
1408
1116
|
{ 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 }
|
|
1117
|
+
{ text: issue.title, grow: true }
|
|
1412
1118
|
],
|
|
1413
1119
|
detail: (width) => {
|
|
1414
|
-
const b = builder(
|
|
1415
|
-
for (const l of wrap(
|
|
1120
|
+
const b = builder(issue.short);
|
|
1121
|
+
for (const l of wrap(found.detail, width - 4, Infinity)) b.line([l, color.muted]);
|
|
1416
1122
|
b.line();
|
|
1417
1123
|
b.issues([issue]);
|
|
1418
1124
|
return b.d;
|
|
1419
1125
|
}
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1126
|
+
};
|
|
1127
|
+
});
|
|
1422
1128
|
}
|
|
1423
|
-
var
|
|
1129
|
+
var pluginCols = (columns, nameW) => columns >= 76 ? { wide: true, name: Math.min(nameW, 17), scope: 19 } : { wide: false, name: 15, scope: 12 };
|
|
1424
1130
|
var STATE = { on: ["on", color.green], off: ["off", color.faint], unknown: ["?", color.faint] };
|
|
1425
|
-
function pluginItems(w, skillW, ui,
|
|
1426
|
-
const nameW =
|
|
1131
|
+
function pluginItems(w, columns, skillW, ui, issuesOf) {
|
|
1132
|
+
const { wide, name: nameW, scope: scopeW } = pluginCols(columns, skillW);
|
|
1427
1133
|
return w.plugins.map((p) => {
|
|
1428
|
-
const issues =
|
|
1134
|
+
const issues = issuesOf.get(p.key) ?? [];
|
|
1429
1135
|
const top = worst(issues);
|
|
1136
|
+
const repeats = new Set(issues.flatMap((i) => i.skills)).size;
|
|
1430
1137
|
const cursor = p.agent === "cursor";
|
|
1431
1138
|
const uses = cursor ? 0 : pluginUses(w, p);
|
|
1432
1139
|
const [state, stateColor] = STATE[p.on === null ? "unknown" : p.on ? "on" : "off"];
|
|
@@ -1448,11 +1155,12 @@ function pluginItems(w, skillW, ui, issuesOf2) {
|
|
|
1448
1155
|
{ text: top ? SEV[top.severity].icon : " ", width: 2, color: top ? SEV[top.severity].color : void 0 },
|
|
1449
1156
|
{ text: clip(name, nameW), width: nameW, color: p.on === false ? color.muted : void 0 },
|
|
1450
1157
|
{ 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 },
|
|
1158
|
+
{ text: clip(cursor ? "\u2013" : pluginScope(p), scopeW), width: scopeW, color: p.repo ? color.blue : color.muted },
|
|
1159
|
+
...wide ? [{ text: clip(`${n} skill${n === 1 ? "" : "s"}${p.parts.length ? ` +${p.parts.length}` : ""}`, 13), width: 13, color: color.muted }] : [],
|
|
1453
1160
|
{ text: state.padEnd(4), width: 4, color: stateColor },
|
|
1454
|
-
{ text: cursor ? " \u2013 " : usesText(uses), width: 6, color: color.muted },
|
|
1455
|
-
|
|
1161
|
+
...wide ? [{ text: cursor ? " \u2013 " : usesText(uses), width: 6, color: color.muted }] : [],
|
|
1162
|
+
// Your skills it repeats, across its issues; else its worst issue.
|
|
1163
|
+
{ text: " " + (repeats ? `Repeats ${repeats} of yours` : top?.short ?? ""), grow: true, color: top ? SEV[top.severity].color : color.faint }
|
|
1456
1164
|
],
|
|
1457
1165
|
detail: (width) => {
|
|
1458
1166
|
const b = builder(p.id);
|
|
@@ -1472,9 +1180,9 @@ function pluginItems(w, skillW, ui, issuesOf2) {
|
|
|
1472
1180
|
const skills = p.skills.map((s) => cursor || !w.days ? s.name : `${s.name} (${uses2(totalUses(w, pluginSkillKey(p, s.name)))})`);
|
|
1473
1181
|
wrap(skills.join(" \xB7 ") || "none", width - 16, 4).forEach((l, i) => b.line(label(i ? "" : "Skills"), [l, color.text]));
|
|
1474
1182
|
b.line(label("Also brings"), [p.parts.join(", ") || "nothing else", p.parts.length ? color.text : color.faint]);
|
|
1475
|
-
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`;
|
|
1183
|
+
const installed = cursor ? "in Cursor (whether it's on is only in Cursor)" : p.scope === "claude.ai" ? "synced from your claude.ai account" : p.settingsOnly ? "not installed: turned on in your settings, loaded from its marketplace's copy" : p.repo ? `${p.scope} scope, in ${p.repo} only` : `user scope, in every repo`;
|
|
1476
1184
|
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"), [
|
|
1185
|
+
b.line(label("Folder"), [tildify(p.path), color.faint]);
|
|
1478
1186
|
return b.d;
|
|
1479
1187
|
}
|
|
1480
1188
|
};
|
|
@@ -1492,10 +1200,12 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1492
1200
|
const columns = sidebar ? termCols - SIDEBAR_W : termCols;
|
|
1493
1201
|
const [base, setBase] = useState(initial);
|
|
1494
1202
|
const [used, setUsed] = useState();
|
|
1495
|
-
const
|
|
1203
|
+
const withUsage = used ? { ...base, ...used } : base;
|
|
1204
|
+
const usageIssues2 = useMemo(() => withUsage.days ? base.ops.usageIssues(withUsage) : [], [base, used]);
|
|
1205
|
+
const world = { ...withUsage, issues: [...base.issues, ...usageIssues2] };
|
|
1496
1206
|
useEffect(() => {
|
|
1497
1207
|
let live = true;
|
|
1498
|
-
void loadUsage2?.(base).then((u) => live && setUsed(u));
|
|
1208
|
+
void loadUsage2?.(base).then((u) => live && setUsed(u)).catch(() => live && setToast("\u2717 Couldn't read your Claude Code transcripts: usage isn't shown"));
|
|
1499
1209
|
return () => {
|
|
1500
1210
|
live = false;
|
|
1501
1211
|
};
|
|
@@ -1514,8 +1224,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1514
1224
|
const [modal, setModal] = useState(null);
|
|
1515
1225
|
const [toast, setToast] = useState("");
|
|
1516
1226
|
const order = useRef({ key: "", keys: [] });
|
|
1517
|
-
const split = termRows >= 30;
|
|
1227
|
+
const split = termRows >= 30 && place !== "settings";
|
|
1518
1228
|
const nameW = Math.min(22, Math.max(17, Math.floor(columns * 0.22)));
|
|
1229
|
+
const libNameW = columns < 90 ? nameW - 4 : nameW;
|
|
1519
1230
|
const dashboard = place === "projects" && open !== null;
|
|
1520
1231
|
const chips = dashboard ? CHIPS : place === "global" ? CHIPS.filter(([c]) => c !== "local") : place === "settings" ? [] : place === "health" ? CHIPS.slice(0, 1) : CHIPS.slice(0, 2);
|
|
1521
1232
|
const ui = {
|
|
@@ -1527,15 +1238,11 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1527
1238
|
menu: (title, options) => setModal({ kind: "menu", title, options, cursor: 0 })
|
|
1528
1239
|
};
|
|
1529
1240
|
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();
|
|
1241
|
+
const repoIssues2 = new Map(world.projects.map((p) => [p.name, repoIssues(world, p.name)]));
|
|
1242
|
+
const machineIssues = [...new Set(world.machine.map((m) => m.name))].map((name) => [name, issuesAt(world, null, name)]);
|
|
1243
|
+
const healthCount = world.issues.length;
|
|
1244
|
+
const healthWorst = worst(world.issues.map(issueOf));
|
|
1245
|
+
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
1246
|
const inChip = (i, c) => c === "all" || (c === "issues" ? i.issues.length > 0 : i.group === c);
|
|
1540
1247
|
let items = all.filter((i) => i.header || (!query || matches(i.name, query)) && inChip(i, chip));
|
|
1541
1248
|
const viewKey = [place, open, chip, query, used ? "usage" : ""].join("|");
|
|
@@ -1556,7 +1263,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1556
1263
|
if (!query) items = [...actionRows(), ...items];
|
|
1557
1264
|
const cursorKey = `${place}|${open}`;
|
|
1558
1265
|
const firstSelectable = Math.max(0, items.findIndex((i) => !i.header && !i.action));
|
|
1559
|
-
|
|
1266
|
+
const saved = cursors[cursorKey];
|
|
1267
|
+
const kept = saved ? items.findIndex((i) => i.key === saved.key) : -1;
|
|
1268
|
+
let cursor = Math.min(kept >= 0 ? kept : saved?.at ?? firstSelectable, items.length - 1);
|
|
1560
1269
|
if (items[cursor]?.header) cursor = firstSelectable;
|
|
1561
1270
|
const current = items[cursor];
|
|
1562
1271
|
const detail = current?.detail?.(columns);
|
|
@@ -1575,7 +1284,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1575
1284
|
}
|
|
1576
1285
|
function choose(o) {
|
|
1577
1286
|
if (o.action) return o.action();
|
|
1578
|
-
if (!o.fix) return
|
|
1287
|
+
if (!o.fix) return;
|
|
1579
1288
|
if (o.fix.pickRepos) {
|
|
1580
1289
|
const name = current?.name ?? "";
|
|
1581
1290
|
const only = o.fix.candidates?.(world);
|
|
@@ -1602,7 +1311,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1602
1311
|
function fixAllList(from) {
|
|
1603
1312
|
const seen = /* @__PURE__ */ new Set();
|
|
1604
1313
|
return from.flatMap(
|
|
1605
|
-
(i) => i.issues.filter((x) => !x.decision && !seen.has(
|
|
1314
|
+
(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
1315
|
);
|
|
1607
1316
|
}
|
|
1608
1317
|
function toggleFold(key) {
|
|
@@ -1630,7 +1339,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1630
1339
|
const label = groupLabel(g, members.map((m) => m.name));
|
|
1631
1340
|
const issues = members.flatMap((m) => m.issues);
|
|
1632
1341
|
const top = worst(issues);
|
|
1633
|
-
const
|
|
1342
|
+
const flagged = members.filter((m) => m.issues.length).length;
|
|
1634
1343
|
return {
|
|
1635
1344
|
key: `fold:${g.key}`,
|
|
1636
1345
|
name: label,
|
|
@@ -1641,13 +1350,14 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1641
1350
|
cells: [
|
|
1642
1351
|
{ text: top ? SEV[top.severity].icon : " ", width: 2, color: top ? SEV[top.severity].color : void 0 },
|
|
1643
1352
|
{ text: `${isOpen ? "\u25BE" : "\u25B8"} ${label} (${members.length})`, grow: true, color: color.accent },
|
|
1644
|
-
|
|
1353
|
+
// A leading space keeps it apart from a name cut to fit.
|
|
1354
|
+
{ text: flagged ? ` ${flagged} to fix` : "", width: 11, color: top ? SEV[top.severity].color : color.faint }
|
|
1645
1355
|
],
|
|
1646
1356
|
detail: (width) => {
|
|
1647
1357
|
const b = builder(label);
|
|
1648
1358
|
for (const l of wrap(members.map((m) => m.name).join(", "), width - 4, 4)) b.line([l, color.muted]);
|
|
1649
1359
|
b.line();
|
|
1650
|
-
if (
|
|
1360
|
+
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
1361
|
b.actions(groupActions(label, members), width);
|
|
1652
1362
|
return b.d;
|
|
1653
1363
|
}
|
|
@@ -1670,7 +1380,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1670
1380
|
preticked: usedIn,
|
|
1671
1381
|
pickRepos: (w, repos) => w.ops.moveGlobal(mine, repos, label)
|
|
1672
1382
|
}),
|
|
1673
|
-
...mine.some((m) => !m.kept) ? [fixOption({ label: `Keep all ${mine.length} global on purpose`, preview: `
|
|
1383
|
+
...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
1384
|
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
1385
|
] : [],
|
|
1676
1386
|
...libs.length ? [
|
|
@@ -1720,7 +1430,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1720
1430
|
() => ui.menu(repo, [
|
|
1721
1431
|
{ label: "Open its folder", action: () => setToast(said2(world.ops.openFolder(project(world, repo).path))) },
|
|
1722
1432
|
{ label: "Hide it from the list", action: () => (apply((w) => w.ops.hide(repo)), setOpen(null), setFocus(sidebar ? "sidebar" : "list")) },
|
|
1723
|
-
...skills.length ? [{ label: `Review prompt for its ${skills.length} skills`, action: () => ui.copy(reviewPrompt(world, skills, `Review the skills in ${repo}.`, repo), `a review prompt for ${repo}`) }] : []
|
|
1433
|
+
...skills.length ? [{ label: `Review prompt for its ${skills.length === 1 ? "skill" : `${skills.length} skills`}`, action: () => ui.copy(reviewPrompt(world, skills, `Review the skills in ${repo}.`, repo), `a review prompt for ${repo}`) }] : []
|
|
1724
1434
|
])
|
|
1725
1435
|
)
|
|
1726
1436
|
];
|
|
@@ -1743,9 +1453,12 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1743
1453
|
i = Math.max(0, Math.min(items.length - 1, i + delta));
|
|
1744
1454
|
while (items[i]?.header && i > 0 && i < items.length - 1);
|
|
1745
1455
|
if (items[i]?.header) return;
|
|
1746
|
-
|
|
1456
|
+
setCursorAt(i);
|
|
1747
1457
|
setDetailCursor(0);
|
|
1748
1458
|
}
|
|
1459
|
+
function setCursorAt(i) {
|
|
1460
|
+
setCursors({ ...cursors, [cursorKey]: { key: items[i]?.key ?? "", at: i } });
|
|
1461
|
+
}
|
|
1749
1462
|
function goPlace(p) {
|
|
1750
1463
|
setPlace(p);
|
|
1751
1464
|
setFocus("list");
|
|
@@ -1753,7 +1466,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1753
1466
|
setChip("all");
|
|
1754
1467
|
}
|
|
1755
1468
|
function edit(file, then) {
|
|
1756
|
-
if (!existsSync2(file)) return then ? apply(then) : (setBase(reload()), setToast(`${
|
|
1469
|
+
if (!existsSync2(file)) return then ? apply(then) : (setBase(reload()), setToast(`${tildify(file)} doesn't exist`));
|
|
1757
1470
|
const editor = process.env.VISUAL || process.env.EDITOR || (process.platform === "win32" ? "notepad" : "vi");
|
|
1758
1471
|
void suspendTerminal(() => {
|
|
1759
1472
|
if (process.platform === "win32") spawnSync2(`${editor} "${file}"`, { shell: true, stdio: "inherit" });
|
|
@@ -1762,11 +1475,19 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1762
1475
|
const fresh = reload();
|
|
1763
1476
|
if (then) return apply(then, fresh);
|
|
1764
1477
|
setBase(fresh);
|
|
1765
|
-
setToast(`Saved ${
|
|
1478
|
+
setToast(`Saved ${tildify(file)}`);
|
|
1766
1479
|
});
|
|
1767
1480
|
}
|
|
1768
|
-
function askForFolder(title) {
|
|
1769
|
-
|
|
1481
|
+
function askForFolder(title, problem) {
|
|
1482
|
+
const submit = (v) => apply((w) => {
|
|
1483
|
+
try {
|
|
1484
|
+
return w.ops.addRoot(v);
|
|
1485
|
+
} catch (e) {
|
|
1486
|
+
askForFolder(title, e.message);
|
|
1487
|
+
throw e;
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
setModal({ kind: "input", title: problem ? `${problem}. ${title}` : title, value: "", placeholder: "~/Projects", submit });
|
|
1770
1491
|
}
|
|
1771
1492
|
useEffect(() => {
|
|
1772
1493
|
const folder = () => !initial.roots.length && askForFolder("Where are your projects? skilllib finds the git repos in there");
|
|
@@ -1810,16 +1531,16 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1810
1531
|
...world.roots.map(
|
|
1811
1532
|
(r) => row(
|
|
1812
1533
|
`root:${r}`,
|
|
1813
|
-
|
|
1534
|
+
tildify(r),
|
|
1814
1535
|
"scanned for git repos",
|
|
1815
|
-
() => ui.menu(
|
|
1536
|
+
() => ui.menu(tildify(r), [
|
|
1816
1537
|
{ label: "Look for repos again", action: () => apply((w) => w.ops.rescan()) },
|
|
1817
|
-
fixOption({ label: "Stop scanning this folder", preview: `Repos already found in ${
|
|
1538
|
+
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
1539
|
])
|
|
1819
1540
|
)
|
|
1820
1541
|
),
|
|
1821
1542
|
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}`,
|
|
1543
|
+
...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
1544
|
header("#backups", "Backups"),
|
|
1824
1545
|
...world.backups.length ? world.backups.map(
|
|
1825
1546
|
(b, i) => row(`backup:${i}`, `\u21BA ${b.name}`, `${b.from} \xB7 ${b.at}`, () => apply((w) => w.ops.restoreBackup(i)))
|
|
@@ -1869,22 +1590,22 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1869
1590
|
if (key.escape) return setSideQuery("");
|
|
1870
1591
|
return;
|
|
1871
1592
|
}
|
|
1593
|
+
if (!sidebar && (key.leftArrow || key.rightArrow)) {
|
|
1594
|
+
const i = PLACES.findIndex(([p]) => p === place) + (key.leftArrow ? -1 : 1);
|
|
1595
|
+
if (PLACES[i]) goPlace(PLACES[i][0]);
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1872
1598
|
const fold = current?.fold;
|
|
1873
1599
|
if (fold && !fold.open && (key.rightArrow || key.return || input === " ")) return toggleFold(fold.key);
|
|
1874
1600
|
if (fold?.open && (key.leftArrow || input === " ")) return toggleFold(fold.key);
|
|
1875
1601
|
if (current?.inFold && key.leftArrow) {
|
|
1876
1602
|
toggleFold(current.inFold);
|
|
1877
|
-
return
|
|
1603
|
+
return setCursorAt(items.findIndex((i) => i.fold?.key === current.inFold));
|
|
1878
1604
|
}
|
|
1879
1605
|
if (sidebar && key.leftArrow) return setFocus("sidebar");
|
|
1880
1606
|
if (sidebar && key.rightArrow && detail) return setFocus("detail"), setDetailCursor(0);
|
|
1881
1607
|
if (key.upArrow) return move(-1);
|
|
1882
1608
|
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
1609
|
if (key.escape) {
|
|
1889
1610
|
if (query) return setQuery("");
|
|
1890
1611
|
if (sidebar) return setFocus("sidebar");
|
|
@@ -2020,17 +1741,17 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
2020
1741
|
if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
|
|
2021
1742
|
}
|
|
2022
1743
|
}
|
|
2023
|
-
const
|
|
1744
|
+
const toastLines = toast && modal?.kind !== "confirm" ? wrap(toast, termCols - 2, 3).map((l) => " " + l) : [];
|
|
1745
|
+
const bottomH = Math.max(1, toastLines.length);
|
|
2024
1746
|
const confirmLines = modal?.kind === "confirm" ? wrap(modal.fix.preview, columns - 6, 6) : [];
|
|
2025
1747
|
const confirmH = modal?.kind === "confirm" ? confirmLines.length + 2 : 0;
|
|
2026
1748
|
const bodyH = Math.max(8, termRows - (sidebar ? 0 : 1) - bottomH - confirmH);
|
|
2027
|
-
const globalFlagged =
|
|
2028
|
-
const globalIssues =
|
|
1749
|
+
const globalFlagged = machineIssues.filter(([, issues]) => issues.length);
|
|
1750
|
+
const globalIssues = globalFlagged.length;
|
|
2029
1751
|
const globalProblem = globalFlagged.some(([, issues]) => issues.some((x) => x.severity === "problem"));
|
|
2030
|
-
const pluginsWorst = worst([...pluginIssues2.values()].flat()
|
|
1752
|
+
const pluginsWorst = worst([...pluginIssues2.values()].flat());
|
|
2031
1753
|
const badge = (p) => {
|
|
2032
|
-
const
|
|
2033
|
-
const top = worst(found.map((x) => x.issue));
|
|
1754
|
+
const top = worst(repoIssues2.get(p) ?? repoIssues(world, p));
|
|
2034
1755
|
return { text: top ? SEV[top.severity].icon : "", width: 3, color: top ? SEV[top.severity].color : void 0 };
|
|
2035
1756
|
};
|
|
2036
1757
|
const sideRow = (key, label, here, go, count = []) => ({
|
|
@@ -2089,7 +1810,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
2089
1810
|
}),
|
|
2090
1811
|
/* @__PURE__ */ jsx2(Text2, { color: color.accent, children: query ? ` \u2315 ${query}\u258F` : "" })
|
|
2091
1812
|
] }) });
|
|
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",
|
|
1813
|
+
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
1814
|
const showChips = chips.length > 0;
|
|
2094
1815
|
const listHeader = /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
2095
1816
|
showChips ? chipRow : null,
|
|
@@ -2272,9 +1993,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
2272
1993
|
modal?.kind === "confirm" ? /* @__PURE__ */ jsx2(Panel, { title: modal.fix.label, focused: true, width: columns, height: confirmH, children: confirmLines.map((l, i) => /* @__PURE__ */ jsx2(Text2, { color: color.text, children: l }, i)) }) : null
|
|
2273
1994
|
] })
|
|
2274
1995
|
] }),
|
|
2275
|
-
/* @__PURE__ */ jsxs2(Box2, { height:
|
|
2276
|
-
/* @__PURE__ */ jsx2(Text2, { color: toast.startsWith("\u2713")
|
|
2277
|
-
toast ? null : /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: hints + " " })
|
|
1996
|
+
/* @__PURE__ */ jsxs2(Box2, { height: bottomH, width: termCols, justifyContent: "space-between", children: [
|
|
1997
|
+
toastLines.length ? /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: toastLines.map((l, i) => /* @__PURE__ */ jsx2(Text2, { wrap: "truncate-end", color: toast.startsWith("\u2713") ? color.green : toast.startsWith("\u2717") ? color.red : toast.startsWith("!") ? color.yellow : color.muted, children: l }, i)) }) : /* @__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 }),
|
|
1998
|
+
toast && modal?.kind !== "confirm" ? null : /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: " " + hints + " " })
|
|
2278
1999
|
] })
|
|
2279
2000
|
] });
|
|
2280
2001
|
}
|
|
@@ -2316,25 +2037,23 @@ function Help({ width, height }) {
|
|
|
2316
2037
|
["ctrl+r", "read everything again from disk"],
|
|
2317
2038
|
["ctrl+c", "quit"]
|
|
2318
2039
|
];
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
]
|
|
2040
|
+
const notes = [
|
|
2041
|
+
'\u2715 problem \xB7 \u26A0 suggestion \xB7 \u2605 recommended fix \xB7 "your call" = a decision, never auto-fixed',
|
|
2042
|
+
"Source: lib = from your library \xB7 repo = committed by the team \xB7 untracked = only on this machine",
|
|
2043
|
+
"Usage counts Claude Code sessions only: skilllib reads no other agent's transcripts."
|
|
2044
|
+
];
|
|
2045
|
+
const lines = [
|
|
2046
|
+
...keys.flatMap(([k, d]) => wrap(d, width - 16, 3).map((l, i) => [[(i ? "" : k).padEnd(12), color.accent], [l, color.text]])),
|
|
2047
|
+
[],
|
|
2048
|
+
...notes.flatMap((n) => wrap(n, width - 4, 3).map((l) => [[l, color.muted]]))
|
|
2049
|
+
];
|
|
2050
|
+
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
2051
|
}
|
|
2330
2052
|
|
|
2331
2053
|
// src/tui/load.ts
|
|
2332
|
-
import {
|
|
2333
|
-
import { existsSync as existsSync3, lstatSync
|
|
2334
|
-
import { basename, dirname, join as join2 } from "path";
|
|
2335
|
-
function tildify4(p) {
|
|
2336
|
-
return tildify(p).replace(/\\/g, "/");
|
|
2337
|
-
}
|
|
2054
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
2055
|
+
import { existsSync as existsSync3, lstatSync } from "fs";
|
|
2056
|
+
import { basename, dirname as dirname2, join as join2, relative, sep } from "path";
|
|
2338
2057
|
function uniqueNames(roots) {
|
|
2339
2058
|
const tail = (r, n) => r.split(/[\\/]/).filter(Boolean).slice(-n).join("/");
|
|
2340
2059
|
return new Map(
|
|
@@ -2345,31 +2064,25 @@ function uniqueNames(roots) {
|
|
|
2345
2064
|
})
|
|
2346
2065
|
);
|
|
2347
2066
|
}
|
|
2348
|
-
function localSkill(root, s,
|
|
2067
|
+
function localSkill(root, s, git) {
|
|
2349
2068
|
const managed = {
|
|
2350
|
-
ok: {},
|
|
2351
|
-
"update available": {},
|
|
2352
2069
|
"edited locally": { edited: true },
|
|
2353
2070
|
"edited locally, update available": { edited: true },
|
|
2354
|
-
|
|
2355
|
-
"folder missing": { missing: true, ...s.latest === null && { library: "missing" } },
|
|
2356
|
-
"not in library": { library: "missing" }
|
|
2071
|
+
"folder missing": { missing: true }
|
|
2357
2072
|
};
|
|
2358
2073
|
const other = {
|
|
2359
|
-
"untracked copy of library skill": { source: "untracked"
|
|
2360
|
-
"untracked, differs from library": { source: "untracked"
|
|
2074
|
+
"untracked copy of library skill": { source: "untracked" },
|
|
2075
|
+
"untracked, differs from library": { source: "untracked" },
|
|
2361
2076
|
"local only": { source: "untracked" },
|
|
2362
2077
|
// Installed with `npx skills add` (the repo's skills-lock.json): not skilllib's, so not a library skill.
|
|
2363
2078
|
"from npx skills": { source: "untracked" },
|
|
2364
2079
|
"repo skill": { source: "repo" },
|
|
2365
|
-
"repo skill, in library": { source: "repo"
|
|
2366
|
-
"repo skill, differs from library": { source: "repo"
|
|
2080
|
+
"repo skill, in library": { source: "repo" },
|
|
2081
|
+
"repo skill, differs from library": { source: "repo" }
|
|
2367
2082
|
};
|
|
2368
|
-
const state =
|
|
2083
|
+
const state = git ? git.of(relativeTo(root, s.path)) : null;
|
|
2084
|
+
const others = otherCopies(root, s.name, s.location);
|
|
2369
2085
|
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
2086
|
return {
|
|
2374
2087
|
name: s.name,
|
|
2375
2088
|
source: "lib",
|
|
@@ -2377,18 +2090,20 @@ function localSkill(root, s, git2) {
|
|
|
2377
2090
|
path: s.path,
|
|
2378
2091
|
// Agents that actually load it here: visibility lists every agent you use, with `paths: 0` for the blind ones.
|
|
2379
2092
|
agents: s.visibility.filter((v) => v.paths > 0).map((v) => v.id),
|
|
2380
|
-
...s.managed ? {
|
|
2381
|
-
...unshared.length && { unshared },
|
|
2093
|
+
...s.managed ? { version: s.version ?? void 0, ...managed[s.state] } : { ...other[s.state], ...own },
|
|
2382
2094
|
...s.source ? { origin: `npx skills: ${s.source}` } : {},
|
|
2383
|
-
|
|
2095
|
+
...others.length ? { others } : {},
|
|
2384
2096
|
git: state
|
|
2385
2097
|
};
|
|
2386
2098
|
}
|
|
2099
|
+
function otherCopies(root, name, dir) {
|
|
2100
|
+
const self = realPath(join2(root, dir, name));
|
|
2101
|
+
return foldersOf(root, name).real.filter((d) => realPath(join2(d, name)) !== self).map((d) => relativeTo(root, d));
|
|
2102
|
+
}
|
|
2387
2103
|
function installedAt(path) {
|
|
2388
2104
|
try {
|
|
2389
2105
|
const st = lstatSync(path);
|
|
2390
|
-
|
|
2391
|
-
return new Date(at.getTime() - at.getTimezoneOffset() * 6e4).toISOString().slice(0, 16).replace("T", " ");
|
|
2106
|
+
return localStamp((st.birthtimeMs > 0 ? st.birthtime : st.mtime).getTime());
|
|
2392
2107
|
} catch {
|
|
2393
2108
|
return void 0;
|
|
2394
2109
|
}
|
|
@@ -2397,8 +2112,8 @@ function machineSkill(s, kept) {
|
|
|
2397
2112
|
const global = s.kind === "global" || s.kind === "skills.sh";
|
|
2398
2113
|
return {
|
|
2399
2114
|
name: s.name,
|
|
2400
|
-
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" ?
|
|
2115
|
+
source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : s.kind === "system" || s.kind === "built-in" && s.harnesses[0] === "codex" ? "system" : s.kind === "skilllib" ? "skilllib" : s.kind === "built-in" && s.harnesses[0] === "grok" ? "grok" : "cursor",
|
|
2116
|
+
where: global || s.kind === "skilllib" ? tildify(dirname2(s.path)) : s.kind === "claude.ai" ? "claude.ai account" : s.kind === "built-in" ? `${s.origin} built-in` : s.origin,
|
|
2402
2117
|
...s.kind === "skills.sh" ? { origin: `npx skills: ${s.origin}` } : {},
|
|
2403
2118
|
path: s.path,
|
|
2404
2119
|
links: s.links,
|
|
@@ -2430,28 +2145,17 @@ function pluginOf(p, repoOf) {
|
|
|
2430
2145
|
function refOf(p) {
|
|
2431
2146
|
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
2147
|
}
|
|
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
2148
|
function loadWorld() {
|
|
2441
2149
|
forgetLatest();
|
|
2442
2150
|
forgetClaudeBinary();
|
|
2443
2151
|
const agents = enabledHarnesses();
|
|
2444
2152
|
const here = projectHere();
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
return realpathSync(p);
|
|
2448
|
-
} catch {
|
|
2449
|
-
return p;
|
|
2450
|
-
}
|
|
2451
|
-
};
|
|
2153
|
+
if (here) rememberProjects([here]);
|
|
2154
|
+
const real = realPath;
|
|
2452
2155
|
const visible = visibleProjects();
|
|
2453
2156
|
const cwd = here ? visible.find((p) => real(p) === real(here)) ?? here : null;
|
|
2454
|
-
const
|
|
2157
|
+
const listed = [.../* @__PURE__ */ new Set([...cwd ? [cwd] : [], ...visible])];
|
|
2158
|
+
const roots = readableRoots(listed).readable;
|
|
2455
2159
|
const names = uniqueNames(roots);
|
|
2456
2160
|
const descriptions = {};
|
|
2457
2161
|
const origins = libraryOrigins();
|
|
@@ -2459,18 +2163,13 @@ function loadWorld() {
|
|
|
2459
2163
|
descriptions[s.name] = s.description;
|
|
2460
2164
|
return { name: s.name, latest: latestVersion(s.name)?.version ?? 1, origin: origins[s.name] };
|
|
2461
2165
|
});
|
|
2166
|
+
const libraryNames = new Set(library.map((l) => l.name));
|
|
2462
2167
|
const kept = keptGlobal();
|
|
2463
2168
|
const cursorList = agents.includes("cursor") ? cursorPlugins() : [];
|
|
2464
|
-
const
|
|
2465
|
-
const globalDupes = dupesOf(planGlobalTidy({ enabled: agents }), null);
|
|
2466
|
-
const sourced = machineSkills(agents);
|
|
2169
|
+
const sourced = machineSkills(agents).filter((s) => s.harnesses.length || s.broken);
|
|
2467
2170
|
const machine = sourced.map((s) => {
|
|
2468
2171
|
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 } : {} };
|
|
2172
|
+
return machineSkill(s, kept);
|
|
2474
2173
|
});
|
|
2475
2174
|
const loadedHere = (root) => {
|
|
2476
2175
|
const loaded = skillsLoadedIn(root, sourced, agents);
|
|
@@ -2478,20 +2177,20 @@ function loadWorld() {
|
|
|
2478
2177
|
for (const s of loaded) descriptions[s.name] ??= s.description;
|
|
2479
2178
|
return { machine: loaded.map((s) => machine[sourced.indexOf(s)] ?? machineSkill(s, kept)) };
|
|
2480
2179
|
};
|
|
2180
|
+
const statuses = new Map(roots.map((root) => [root, projectStatus(root)]));
|
|
2481
2181
|
const projects = roots.map((root) => {
|
|
2482
|
-
const status =
|
|
2182
|
+
const status = statuses.get(root);
|
|
2483
2183
|
const hasManifest = existsSync3(join2(root, "skilllib.json"));
|
|
2484
|
-
const
|
|
2184
|
+
const git = status.length || hasManifest ? gitInfo(root) : null;
|
|
2485
2185
|
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
2186
|
return {
|
|
2488
2187
|
name: names.get(root),
|
|
2489
2188
|
path: root,
|
|
2490
|
-
manifest: hasManifest ?
|
|
2189
|
+
manifest: hasManifest ? git ? git.of("skilllib.json") : "no git" : "none",
|
|
2491
2190
|
skills: status.map((s) => {
|
|
2492
|
-
const
|
|
2493
|
-
const
|
|
2494
|
-
return
|
|
2191
|
+
const local = localSkill(root, s, git);
|
|
2192
|
+
const own = s.state.includes("missing") ? "" : readSkillInfo(s.path).description;
|
|
2193
|
+
return own && own !== descriptions[s.name] ? { ...local, description: own } : local;
|
|
2495
2194
|
}),
|
|
2496
2195
|
...loadedHere(root)
|
|
2497
2196
|
};
|
|
@@ -2501,19 +2200,19 @@ function loadWorld() {
|
|
|
2501
2200
|
const plugins = [...installedPlugins(), ...cursorList].map((p) => pluginOf(p, repoOf));
|
|
2502
2201
|
return {
|
|
2503
2202
|
agents,
|
|
2504
|
-
cwd: cwd ? names.get(cwd) : null,
|
|
2203
|
+
cwd: cwd && names.has(cwd) ? names.get(cwd) : null,
|
|
2505
2204
|
projects,
|
|
2506
2205
|
machine,
|
|
2507
2206
|
plugins,
|
|
2508
2207
|
library,
|
|
2509
2208
|
usage: {},
|
|
2510
2209
|
descriptions,
|
|
2511
|
-
backups: backups.map((b) => ({ name: b.name, from:
|
|
2512
|
-
roots: readConfig()
|
|
2513
|
-
hidden: readConfig().hidden,
|
|
2210
|
+
backups: backups.map((b) => ({ name: b.name, from: tildify(b.from), at: b.movedAt })),
|
|
2211
|
+
...(({ roots: roots2, hidden }) => ({ roots: roots2, hidden }))(readConfig()),
|
|
2514
2212
|
agentsChosen: harnessesChosen(),
|
|
2515
2213
|
agentSkill: agentSkillState(agents),
|
|
2516
|
-
|
|
2214
|
+
issues: findIssues(listed, sourced, libraryNames, (root) => statuses.get(root)),
|
|
2215
|
+
ops: realOps(new Map(projects.map((p) => [p.name, p.path])), backups, sourced, libraryNames)
|
|
2517
2216
|
};
|
|
2518
2217
|
}
|
|
2519
2218
|
async function loadUsage(w) {
|
|
@@ -2523,7 +2222,11 @@ async function loadUsage(w) {
|
|
|
2523
2222
|
const days = {};
|
|
2524
2223
|
const midnight = (t) => new Date(t).setHours(0, 0, 0, 0);
|
|
2525
2224
|
const today = midnight(Date.now());
|
|
2225
|
+
const totals = {};
|
|
2526
2226
|
for (const use of await scanUsage(30)) {
|
|
2227
|
+
const skill = skillOf(use.skill);
|
|
2228
|
+
totals[skill] = (totals[skill] ?? 0) + 1;
|
|
2229
|
+
if (skill !== use.skill) totals[use.skill] = (totals[use.skill] ?? 0) + 1;
|
|
2527
2230
|
const root = projectOf(use.cwd);
|
|
2528
2231
|
const repo = root ? repoOf.get(root) : void 0;
|
|
2529
2232
|
const day = 29 - Math.round((today - midnight(Date.parse(use.at))) / 864e5);
|
|
@@ -2531,38 +2234,29 @@ async function loadUsage(w) {
|
|
|
2531
2234
|
(usage[repo] ??= {})[use.skill] = (usage[repo][use.skill] ?? 0) + 1;
|
|
2532
2235
|
((days[repo] ??= {})[use.skill] ??= new Array(30).fill(0))[day] += 1;
|
|
2533
2236
|
}
|
|
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$/, "");
|
|
2237
|
+
return { usage, days, totals };
|
|
2546
2238
|
}
|
|
2547
2239
|
function said3(c, repo) {
|
|
2548
2240
|
if (c.action === "skipped") return `${c.name}: ${c.reason ?? "skipped"}`;
|
|
2549
|
-
if (c.action === "removed") return `Removed ${c.name} from ${repo}`;
|
|
2241
|
+
if (c.action === "removed") return `Removed ${c.name} from ${repo}${c.reason ? ` (${c.reason})` : ""}`;
|
|
2242
|
+
if (c.action === "unchanged") return `${c.name}${c.to ? ` v${c.to}` : ""} is already in ${repo}`;
|
|
2243
|
+
if (c.action === "relinked") return `Linked ${c.name}${c.to ? ` v${c.to}` : ""} for your other agents in ${repo}`;
|
|
2550
2244
|
const back = c.action === "updated" && c.from !== void 0 && c.to !== void 0 && c.to < c.from;
|
|
2551
2245
|
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
2246
|
return c.backedUp ? `${done} (your edits are in Settings \u203A Backups)` : done;
|
|
2553
2247
|
}
|
|
2554
|
-
function realOps(roots, backups) {
|
|
2248
|
+
function realOps(roots, backups, machine, libraryNames) {
|
|
2555
2249
|
const rootOf = (repo) => roots.get(repo);
|
|
2556
2250
|
const find = (repo, name) => projectStatus(rootOf(repo)).find((s) => s.name === name);
|
|
2557
|
-
const info = /* @__PURE__ */ new Map();
|
|
2558
2251
|
const result = (c, repo) => c.action === "skipped" ? failed(said3(c, repo)) : said3(c, repo);
|
|
2559
2252
|
const copyDir = (repo, name) => readManifest(rootOf(repo)).skills[name]?.dir ?? PROJECT_SKILLS_DIR;
|
|
2560
2253
|
const withLinks = (c, repo, name) => {
|
|
2561
2254
|
const it = (dirs) => dirs.length === 1 ? "it" : "them";
|
|
2255
|
+
const way = c.inTheWay?.length ? `; not linked everywhere: ${inTheWaySaid(rootOf(repo), c.inTheWay)} (clear the way, then link again)` : "";
|
|
2562
2256
|
if (c.uncommitted?.length)
|
|
2563
|
-
return `${said3(c, repo)}; not linked in ${c.uncommitted.join(", ")}: git tracks ${it(c.uncommitted)} and ${copyDir(repo, name)}/${name} isn't committed, so teammates would get broken links`;
|
|
2564
|
-
if (!c.blocked?.length) return result(c, repo);
|
|
2565
|
-
return { message: `${said3(c, repo)}; not linked in ${c.blocked.join(", ")}: git tracks ${it(c.blocked)}`, then: allowTracked([{ repo, name, blocked: c.blocked }]) };
|
|
2257
|
+
return `${said3(c, repo)}${way}; not linked in ${c.uncommitted.join(", ")}: git tracks ${it(c.uncommitted)} and ${copyDir(repo, name)}/${name} isn't committed, so teammates would get broken links`;
|
|
2258
|
+
if (!c.blocked?.length) return way ? failed(`${said3(c, repo)}${way}`) : result(c, repo);
|
|
2259
|
+
return { message: `${said3(c, repo)}${way}; not linked in ${c.blocked.join(", ")}: git tracks ${it(c.blocked)}`, then: allowTracked([{ repo, name, blocked: c.blocked }]) };
|
|
2566
2260
|
};
|
|
2567
2261
|
const addTo = (repos, names) => {
|
|
2568
2262
|
if (repos.length === 1 && names.length === 1) return withLinks(addSkill(rootOf(repos[0]), names[0]), repos[0], names[0]);
|
|
@@ -2584,10 +2278,11 @@ function realOps(roots, backups) {
|
|
|
2584
2278
|
const why = kind === "blocked" ? `git tracks ${dirs.length === 1 ? "it" : "them"}` : `${copies.join(", ")} ${copies.length === 1 ? "isn't" : "aren't"} committed, so a link would break for teammates`;
|
|
2585
2279
|
notes.push(`${who}not linked in ${dirs.join(", ")} in ${repo}: ${why}`);
|
|
2586
2280
|
}
|
|
2281
|
+
for (const x of done.filter((d) => d.c.inTheWay?.length)) notes.push(`${x.name} not linked everywhere in ${x.repo}: ${inTheWaySaid(rootOf(x.repo), x.c.inTheWay)}`);
|
|
2587
2282
|
const skipped = changes.filter((x) => x.c.action === "skipped").map((x) => `${x.name} not added to ${x.repo}: ${x.c.reason}`);
|
|
2588
2283
|
const text = [summary, ...notes, ...skipped].filter(Boolean).join("; ");
|
|
2589
2284
|
const held = done.flatMap((x) => x.c.blocked?.length ? [{ repo: x.repo, name: x.name, blocked: x.c.blocked }] : []);
|
|
2590
|
-
const result2 = !done.length ? failed(text) : held.length ? { message: text, then: allowTracked(held) } : skipped.length ? failed(text) : text;
|
|
2285
|
+
const result2 = !done.length ? failed(text) : held.length ? { message: text, then: allowTracked(held) } : skipped.length || done.some((x) => x.c.inTheWay?.length) ? failed(text) : text;
|
|
2591
2286
|
return { result: result2, skipped, notes };
|
|
2592
2287
|
};
|
|
2593
2288
|
const allowTracked = (held) => {
|
|
@@ -2596,14 +2291,13 @@ function realOps(roots, backups) {
|
|
|
2596
2291
|
return {
|
|
2597
2292
|
label: `Also link ${new Set(held.map((h) => h.name)).size === 1 ? "it" : "them"} in ${dirs.join(", ")}`,
|
|
2598
2293
|
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: (
|
|
2294
|
+
run: () => {
|
|
2600
2295
|
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
2296
|
const results = held.map((h) => ({ ...h, r: linkIn(h.repo, h.name, true) }));
|
|
2603
2297
|
const linked = results.filter((x) => x.r.created.length);
|
|
2604
2298
|
const names = [...new Set(linked.map((x) => x.name))];
|
|
2605
2299
|
const summary = linked.length ? `Linked ${names.length === 1 ? names[0] : `${names.length} skills`} in ${[...new Set(linked.flatMap((x) => x.r.created))].join(", ")} in ${[...new Set(linked.map((x) => x.repo))].join(", ")}` : "";
|
|
2606
|
-
const others = results.filter((x) => !x.r.created.length || x.r.uncommitted.length).map((x) => `${x.repo}: ${linkSaid(x.name, x.r)}`);
|
|
2300
|
+
const others = results.filter((x) => !x.r.created.length || x.r.uncommitted.length || x.r.inTheWay.length).map((x) => `${x.repo}: ${linkSaid(x.repo, x.name, x.r)}`);
|
|
2607
2301
|
return [summary, ...others].join("; ");
|
|
2608
2302
|
}
|
|
2609
2303
|
};
|
|
@@ -2612,15 +2306,19 @@ function realOps(roots, backups) {
|
|
|
2612
2306
|
const s = find(repo, name);
|
|
2613
2307
|
return s.managed ? relinkDependency(rootOf(repo), name, { allowTracked: allow }) : linkEverywhere(rootOf(repo), name, s.location, { allowTracked: allow });
|
|
2614
2308
|
};
|
|
2615
|
-
const linkSaid = (name, r) => {
|
|
2309
|
+
const linkSaid = (repo, name, r) => {
|
|
2616
2310
|
const linked = r.created.length ? `linked in ${r.created.join(", ")}` : "";
|
|
2617
2311
|
const notLinked = r.uncommitted.length ? `not linked in ${r.uncommitted.join(", ")}: git tracks ${r.uncommitted.length === 1 ? "it" : "them"} and ${name} isn't committed, so teammates would get broken links` : "";
|
|
2618
|
-
|
|
2312
|
+
const inTheWay = r.inTheWay.length ? inTheWaySaid(rootOf(repo), r.inTheWay) : "";
|
|
2313
|
+
return `${name}: ${[linked, notLinked, inTheWay].filter(Boolean).join("; ") || "nothing to link"}`;
|
|
2619
2314
|
};
|
|
2620
|
-
const orForce = (c, repo, label, force) =>
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2315
|
+
const orForce = (c, repo, label, force) => (
|
|
2316
|
+
// Whatever --force would get past (edits, a pin from another library, another copy in the way): offer it here.
|
|
2317
|
+
c.action === "skipped" && !!c.reason?.includes("--force") ? {
|
|
2318
|
+
message: `${c.name} in ${repo}: ${c.reason.replace(/ \(use --force[^)]*\)$/, "")}`,
|
|
2319
|
+
then: { label, preview: `What's there now in ${repo} goes to Settings \u203A Backups first.`, run: () => result(force(), repo) }
|
|
2320
|
+
} : result(c, repo)
|
|
2321
|
+
);
|
|
2624
2322
|
const toLibrary = (path) => {
|
|
2625
2323
|
const r = importSkill(path, { force: true });
|
|
2626
2324
|
return r.status === "unchanged" ? `${r.name} is already the same in your library` : `${r.name} v${latestVersion(r.name)?.version} is in your library`;
|
|
@@ -2633,130 +2331,92 @@ function realOps(roots, backups) {
|
|
|
2633
2331
|
if (!s) return failed(`${name} isn't in ${repo}`);
|
|
2634
2332
|
const committed = !s.managed && ["committed", "changed"].includes(gitInfo(rootOf(repo))?.of(relativeTo(rootOf(repo), s.path)) ?? "");
|
|
2635
2333
|
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`;
|
|
2334
|
+
const others = otherCopies(rootOf(repo), name, s.location);
|
|
2335
|
+
const left = (r) => typeof r === "string" && others.length ? `${r}; the copy in ${others.join(", ")} stays` : r;
|
|
2336
|
+
if (!s.managed) return removeUntracked(rootOf(repo), name, s.path), left(`Removed ${others.length ? `${s.location}/${name}` : name} from ${repo} (in Settings \u203A Backups)`);
|
|
2337
|
+
return left(orForce(removeSkill(rootOf(repo), name), repo, "Remove it anyway", () => removeSkill(rootOf(repo), name, { force: true })));
|
|
2642
2338
|
},
|
|
2643
2339
|
update: (repo, name) => {
|
|
2644
2340
|
const c = updateProject(rootOf(repo), [name])[0];
|
|
2645
2341
|
return c ? orForce(c, repo, "Update anyway", () => updateProject(rootOf(repo), [name], { force: true })[0]) : `${name} is up to date`;
|
|
2646
2342
|
},
|
|
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
2343
|
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
2344
|
deleteGlobal: (m) => {
|
|
2694
|
-
|
|
2695
|
-
|
|
2345
|
+
if (!entryExists(m.path)) return `${tildify(m.path)} is already gone`;
|
|
2346
|
+
const others = machine.filter((o) => o.name === m.name && o.path !== m.path && o.kind !== "plugin" && !o.broken);
|
|
2347
|
+
const dependents = others.filter((o) => !survives(o.path, [m.path, ...m.links]));
|
|
2348
|
+
const stays = others.filter((o) => !dependents.includes(o));
|
|
2349
|
+
if (!m.broken) {
|
|
2350
|
+
const readers = [.../* @__PURE__ */ new Set([...m.agents, ...dependents.flatMap((o) => o.harnesses)])].filter((id) => !stays.some((o) => o.harnesses.includes(id)));
|
|
2351
|
+
const blind = blindWithoutGlobal(m.name, [{ path: m.path, harnesses: readers, links: [...m.links, ...dependents.map((o) => o.path)] }], [...roots.values(), ...knownProjects()]);
|
|
2352
|
+
if (blind.length) return failed(`${m.name} kept: ${blindSaid(m.name, blind, (root) => [...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root))}`);
|
|
2353
|
+
}
|
|
2354
|
+
const left = m.broken ? [] : strandedLinks(m.name, [{ path: m.path, links: [...m.links, ...dependents.map((o) => o.path)] }], [...roots.values(), ...knownProjects()], (root) => [...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root));
|
|
2355
|
+
const r = deleteGlobal(m.path, [...m.links, ...dependents.map((o) => o.path)]);
|
|
2356
|
+
if (!r.ok) return failed(`${m.name}: ${r.reason}`);
|
|
2357
|
+
if (m.broken) return `Removed the broken link ${m.name}`;
|
|
2358
|
+
const still = stays.map((o) => tildify(o.path));
|
|
2359
|
+
return `Deleted ${tildify(m.path)} (in Settings \u203A Backups)${still.length ? `; ${m.name} still loads globally from ${still.join(", ")}` : ""}${left.length ? `; ${left.join(", ")} now point${left.length === 1 ? "s" : ""} at nothing: remove ${left.length === 1 ? "it" : "them"}` : ""}`;
|
|
2696
2360
|
},
|
|
2697
2361
|
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
2362
|
moveGlobal: (skills, repos, group) => {
|
|
2706
|
-
const moved = [];
|
|
2707
|
-
const notes = [];
|
|
2708
|
-
const held = [];
|
|
2709
2363
|
const repoRoots = repos.map(rootOf);
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
moved.
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
}
|
|
2364
|
+
const label = (root) => repos[repoRoots.indexOf(root)] ?? [...roots].find(([, r]) => r === root)?.[0] ?? basename(root);
|
|
2365
|
+
const picked = new Set(skills.map((m) => m.name));
|
|
2366
|
+
const sourced = machine.filter((o) => picked.has(o.name) && o.movable && !o.broken);
|
|
2367
|
+
const attempt = (names) => moveGlobalSkills(names ? sourced.filter((m) => names.includes(m.name)) : sourced, repoRoots, { group, label, allowTrackedHint: "allow links there, then move it again" });
|
|
2368
|
+
const said4 = (results2) => {
|
|
2369
|
+
const moved2 = results2.filter((mv) => mv.moved).map((mv) => mv.name);
|
|
2370
|
+
const which = moved2.length <= 4 ? moved2.join(", ") : `${moved2.length} skills`;
|
|
2371
|
+
const summary = moved2.length ? `${which} now load${moved2.length === 1 ? "s" : ""} only in ${repos.join(", ")}` : "";
|
|
2372
|
+
const left = results2.flatMap((mv) => mv.left ?? []);
|
|
2373
|
+
return {
|
|
2374
|
+
moved: moved2,
|
|
2375
|
+
text: [summary, ...left.length ? [`${left.join(", ")} now point${left.length === 1 ? "s" : ""} at nothing: remove ${left.length === 1 ? "it" : "them"}`] : [], ...results2.filter((mv) => !mv.moved).map((mv) => `${mv.name}: ${mv.reason}`)].filter(Boolean).join("; ") || "Nothing moved"
|
|
2376
|
+
};
|
|
2377
|
+
};
|
|
2378
|
+
const results = attempt();
|
|
2379
|
+
const { moved, text } = said4(results);
|
|
2380
|
+
const finishable = (mv) => !mv.moved && mv.changes.length > 0 && mv.changes.every(([, c]) => c.action !== "skipped" && !c.inTheWay?.length && !c.uncommitted?.length) && mv.changes.some(([, c]) => c.blocked?.length);
|
|
2381
|
+
const held = results.filter(finishable).flatMap((mv) => mv.changes.flatMap(([root, c]) => c.blocked?.length ? [{ repo: label(root), name: mv.name, blocked: c.blocked }] : []));
|
|
2382
|
+
if (held.length) {
|
|
2383
|
+
const ask = allowTracked(held);
|
|
2384
|
+
const names = [...new Set(held.map((h) => h.name))];
|
|
2385
|
+
return {
|
|
2386
|
+
message: text,
|
|
2387
|
+
then: {
|
|
2388
|
+
...ask,
|
|
2389
|
+
label: `${ask.label}, then stop loading ${names.length === 1 ? "it" : "them"} globally`,
|
|
2390
|
+
// The retry is what says whether it's done: a skill still global after it is a failure, said as one.
|
|
2391
|
+
run: (w) => {
|
|
2392
|
+
const linked = said(ask.run(w));
|
|
2393
|
+
const retry = said4(attempt(names));
|
|
2394
|
+
const before = said4(results.filter((mv) => mv.moved));
|
|
2395
|
+
const others = results.filter((mv) => !mv.moved && !names.includes(mv.name)).map((mv) => `${mv.name}: ${mv.reason}`);
|
|
2396
|
+
const text2 = [before.moved.length ? before.text : "", linked, retry.text, ...others].filter(Boolean).join("; ");
|
|
2397
|
+
return retry.moved.length === names.length ? text2 : failed(text2);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
};
|
|
2723
2401
|
}
|
|
2724
|
-
|
|
2725
|
-
for (const repo of new Set(held.map((h) => h.repo)))
|
|
2726
|
-
for (const shared of [true, false]) {
|
|
2727
|
-
const here = held.filter((h) => h.repo === repo && h.shared === shared);
|
|
2728
|
-
if (!here.length) continue;
|
|
2729
|
-
const agents = [...new Set(here.flatMap((h) => h.blind))].map((id) => harness(id).name).join(", ");
|
|
2730
|
-
const what = skills.length === 1 ? "it" : here.length === moved.length ? "them" : here.map((h) => h.name).join(", ");
|
|
2731
|
-
const dirs = [...new Set(here.flatMap((h) => h.blocked))].join(", ");
|
|
2732
|
-
const why = shared ? `git tracks ${dirs}` : `${[...new Set(here.map((h) => h.copy))].join(", ")} isn't committed, so a link in ${dirs} would break for teammates`;
|
|
2733
|
-
const key = `${agents}|${what}|${why}`;
|
|
2734
|
-
if (!gaps.has(key)) gaps.set(key, { who: `${agents} can't see ${what}`, why, repos: [] });
|
|
2735
|
-
gaps.get(key).repos.push(repo);
|
|
2736
|
-
}
|
|
2737
|
-
const blindNotes = [...gaps.values()].map((g) => `${g.who} in ${g.repos.join(", ")}: ${g.why}`);
|
|
2738
|
-
const summary = moved.length ? `${moved.length === 1 ? moved[0] : `${moved.length} skills`} now load${moved.length === 1 ? "s" : ""} only in ${repos.join(", ")}` : "";
|
|
2739
|
-
const text = [summary, ...blindNotes, ...notes].filter(Boolean).join("; ") || "Nothing moved";
|
|
2740
|
-
const askable = held.filter((h) => h.shared);
|
|
2741
|
-
if (!moved.length && notes.length) return failed(text);
|
|
2742
|
-
return askable.length ? { message: text, then: allowTracked(askable) } : text;
|
|
2402
|
+
return moved.length ? text : failed(text);
|
|
2743
2403
|
},
|
|
2744
2404
|
createSkill: (name) => {
|
|
2745
2405
|
const r = createSkill(name, "");
|
|
2746
2406
|
return r.ok ? `Created ${name} in your library: edit its SKILL.md from Your skills` : failed(`${name}: ${r.reason}`);
|
|
2747
2407
|
},
|
|
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
2408
|
replacePlugin: (p, repos) => {
|
|
2756
2409
|
const id = p.id;
|
|
2410
|
+
const clashes = p.skills.filter((s) => {
|
|
2411
|
+
const mine = treeHash(librarySkillDir(s.name));
|
|
2412
|
+
return mine !== null && mine !== treeHash(s.path);
|
|
2413
|
+
});
|
|
2414
|
+
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`);
|
|
2415
|
+
const cant = repos.flatMap(
|
|
2416
|
+
(repo) => p.skills.flatMap((s) => ((why) => why ? [`${s.name} in ${repo}: ${why.replace(/ \(use --force[^)]*\)$/, "")}`] : [])(trackBlocker(rootOf(repo), s.name) ?? addRefusal(rootOf(repo), s.name, treeHash(s.path) ?? "")))
|
|
2417
|
+
);
|
|
2418
|
+
if (cant.length) return failed(`${id} kept, nothing changed: ${cant.join("; ")}`);
|
|
2757
2419
|
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
2420
|
for (const r of imported) if (r.status !== "unchanged") recordOrigin(r.name, `plugin: ${id}`);
|
|
2761
2421
|
const added = repos.length ? addMany(repos, p.skills.map((s) => s.name)) : { result: "", skipped: [], notes: [] };
|
|
2762
2422
|
if (added.skipped.length) return failed(`${id} kept: ${added.skipped.join("; ")}`);
|
|
@@ -2792,21 +2452,31 @@ function realOps(roots, backups) {
|
|
|
2792
2452
|
return r.ok ? r.message : failed(r.message);
|
|
2793
2453
|
},
|
|
2794
2454
|
deleteLibrary: (name) => {
|
|
2455
|
+
const using = projectsUsing(name);
|
|
2456
|
+
const unreadable = readableRoots(using).unreadable.map((u) => u.root);
|
|
2457
|
+
if (unreadable.length) return failed(`${name} is pinned in ${unreadable.map((r2) => basename(r2)).join(", ")}, whose skilllib.json can't be read: fix it first, then delete`);
|
|
2458
|
+
const hidden = using.filter((root) => ![...roots.values()].includes(root));
|
|
2459
|
+
if (hidden.length) return failed(`${name} is also in ${hidden.map((r2) => basename(r2)).join(", ")}, which ${hidden.length === 1 ? "is" : "are"} hidden: unhide ${hidden.length === 1 ? "it" : "them"} (Settings) to delete it everywhere`);
|
|
2795
2460
|
const from = [];
|
|
2796
|
-
|
|
2461
|
+
const left = [];
|
|
2462
|
+
for (const root of using) {
|
|
2797
2463
|
const s = projectStatus(root).find((x) => x.name === name && x.managed);
|
|
2798
|
-
if (s &&
|
|
2799
|
-
removeSkill(root, name, { force: true });
|
|
2800
|
-
|
|
2464
|
+
if (s && isDir(s.path) && !notWritable(root, relative(root, dirname2(s.path)).split(sep).join("/"))) removeUntracked(root, name, s.path);
|
|
2465
|
+
const c = removeSkill(root, name, { force: true });
|
|
2466
|
+
if (c.action === "skipped") return failed(`${name}: ${c.reason}${from.length ? `; removed from ${from.join(", ")} so far` : ""}`);
|
|
2467
|
+
const repo = [...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root);
|
|
2468
|
+
from.push(repo);
|
|
2469
|
+
if (c.reason) left.push(`in ${repo}, ${c.reason}`);
|
|
2801
2470
|
}
|
|
2802
2471
|
const r = deleteLibrarySkill(name);
|
|
2803
2472
|
if (!r.ok) return failed(`${name}: ${r.reason}`);
|
|
2804
|
-
return `Deleted ${name} from your library${from.length ? ` and from ${from.join(", ")}` : ""} (in Settings \u203A Backups)`;
|
|
2473
|
+
return `Deleted ${name} from your library${from.length ? ` and from ${from.join(", ")}` : ""} (in Settings \u203A Backups)${left.length ? `; ${left.join("; ")}` : ""}`;
|
|
2805
2474
|
},
|
|
2806
2475
|
restoreBackup: (i) => {
|
|
2807
|
-
const
|
|
2808
|
-
const
|
|
2809
|
-
|
|
2476
|
+
const done = movedTogether(backups, backups[i]).map((b) => [b, b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b)]);
|
|
2477
|
+
const bad = done.filter(([, r]) => !r.ok);
|
|
2478
|
+
if (bad.length) return failed(bad.map(([b, r]) => `${b.name}: ${!r.ok ? r.reason : ""}`).join("; "));
|
|
2479
|
+
return done.map(([b, r]) => `Restored ${b.name} to ${r.ok ? tildify(r.to) : ""}`).join(" \xB7 ");
|
|
2810
2480
|
},
|
|
2811
2481
|
setAgents: (ids) => (setHarnesses(ids), "Saved your agents"),
|
|
2812
2482
|
installVersion: (repo, name, version) => withLinks(addSkill(rootOf(repo), name, { version, force: true }), repo, name),
|
|
@@ -2815,40 +2485,37 @@ function realOps(roots, backups) {
|
|
|
2815
2485
|
addRoot: (path) => {
|
|
2816
2486
|
const root = addRoot(path);
|
|
2817
2487
|
discoverProjects();
|
|
2818
|
-
return `Scanning ${
|
|
2488
|
+
return `Scanning ${tildify(root)} for repos`;
|
|
2819
2489
|
},
|
|
2820
|
-
removeRoot: (path) => (removeRoot(path), `Stopped scanning ${
|
|
2490
|
+
removeRoot: (path) => (removeRoot(path), `Stopped scanning ${tildify(path)}; repos already found stay until you hide them`),
|
|
2821
2491
|
rescan: () => (discoverProjects(), "Looked for repos in your project folders again"),
|
|
2822
2492
|
hide: (repo) => (setHidden(rootOf(repo), true), `Hid ${repo}: Settings \u203A Hidden repos brings it back`),
|
|
2823
|
-
unhide: (path) => (setHidden(path, false), `${
|
|
2493
|
+
unhide: (path) => (setHidden(path, false), `${tildify(path)} is back in the list`),
|
|
2824
2494
|
openFolder: (path) => {
|
|
2825
2495
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
2826
|
-
|
|
2827
|
-
return `Opened ${
|
|
2496
|
+
const r = spawnSync3(cmd, [path], { stdio: "ignore", timeout: 1e4 });
|
|
2497
|
+
return r.error || r.status !== 0 && process.platform !== "win32" ? failed(`Couldn't open ${tildify(path)} (${cmd} ${r.error ? "isn't available" : "failed"})`) : `Opened ${tildify(path)}`;
|
|
2828
2498
|
},
|
|
2829
2499
|
copy: (text, what) => {
|
|
2830
2500
|
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 ${
|
|
2501
|
+
return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor, Codex or Grok` : `Couldn't reach the clipboard; ${what} is in ${tildify(r.savedTo)}`;
|
|
2832
2502
|
},
|
|
2833
2503
|
installAgentSkill: () => {
|
|
2834
2504
|
const r = installAgentSkill();
|
|
2835
|
-
return r.ok ? `Your agents can now use skilllib (${r.dirs.map(
|
|
2505
|
+
return r.ok ? `Your agents can now use skilllib (${r.dirs.map(tildify).join(", ")})` : failed(`skilllib skill: ${r.reason}`);
|
|
2836
2506
|
},
|
|
2837
2507
|
removeAgentSkill: () => {
|
|
2838
2508
|
const removed = removeAgentSkill();
|
|
2839
|
-
return removed.length ? `Removed the skilllib skill (${removed.map(
|
|
2509
|
+
return removed.length ? `Removed the skilllib skill (${removed.map(tildify).join(", ")})` : failed("The skilllib skill isn't installed");
|
|
2840
2510
|
},
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
});
|
|
2850
|
-
}
|
|
2851
|
-
return info.get(repo);
|
|
2511
|
+
usageIssues: ({ usage, totals = {} }) => {
|
|
2512
|
+
const repoOf = new Map([...roots].map(([name, root]) => [root, name]));
|
|
2513
|
+
return usageIssues([...roots.values()], machine, libraryNames, {
|
|
2514
|
+
inProject: (root, skill) => usage[repoOf.get(root)]?.[skill] ?? 0,
|
|
2515
|
+
total: (skill) => totals[skill] ?? 0,
|
|
2516
|
+
asUsed: (key) => totals[key] ?? 0,
|
|
2517
|
+
days: 30
|
|
2518
|
+
});
|
|
2852
2519
|
}
|
|
2853
2520
|
};
|
|
2854
2521
|
}
|
|
@@ -2856,6 +2523,7 @@ function realOps(roots, backups) {
|
|
|
2856
2523
|
// src/tui/index.tsx
|
|
2857
2524
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
2858
2525
|
async function tui() {
|
|
2526
|
+
discoverProjects();
|
|
2859
2527
|
const instance = render(/* @__PURE__ */ jsx3(App, { initial: loadWorld(), reload: loadWorld, loadUsage }), { alternateScreen: true, exitOnCtrlC: true });
|
|
2860
2528
|
await instance.waitUntilExit();
|
|
2861
2529
|
}
|