skilllib 1.2.1 → 1.3.0
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 +59 -77
- package/dist/{chunk-O5TY4NWZ.js → chunk-TWQQHXVG.js} +29 -16
- package/dist/index.js +3 -3
- package/dist/tui-H5IQAGAL.js +2184 -0
- package/package.json +1 -1
- package/dist/tui-6BOQIKAU.js +0 -2742
package/dist/tui-6BOQIKAU.js
DELETED
|
@@ -1,2742 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ALL_PROJECT_DIRS,
|
|
3
|
-
HARNESSES,
|
|
4
|
-
addRoot,
|
|
5
|
-
addSkill,
|
|
6
|
-
allowTrackedLinks,
|
|
7
|
-
createSkill,
|
|
8
|
-
deleteGlobal,
|
|
9
|
-
deleteLibrarySkill,
|
|
10
|
-
discoverProjects,
|
|
11
|
-
enabledHarnesses,
|
|
12
|
-
findIssues,
|
|
13
|
-
findProjectRoot,
|
|
14
|
-
forgetLatest,
|
|
15
|
-
gitInfo,
|
|
16
|
-
harnessesChosen,
|
|
17
|
-
importSkill,
|
|
18
|
-
installDirs,
|
|
19
|
-
isProjectCandidate,
|
|
20
|
-
keptGlobal,
|
|
21
|
-
latestVersion,
|
|
22
|
-
libraryOrigins,
|
|
23
|
-
librarySkillDir,
|
|
24
|
-
librarySkills,
|
|
25
|
-
linkAll,
|
|
26
|
-
linkEverywhere,
|
|
27
|
-
listBackups,
|
|
28
|
-
machineSkills,
|
|
29
|
-
pluginBackups,
|
|
30
|
-
projectOfFactory,
|
|
31
|
-
projectStatus,
|
|
32
|
-
readConfig,
|
|
33
|
-
readSkillInfo,
|
|
34
|
-
relativeTo,
|
|
35
|
-
relinkDependency,
|
|
36
|
-
removeRoot,
|
|
37
|
-
removeSkill,
|
|
38
|
-
restoreBackup,
|
|
39
|
-
restorePlugin,
|
|
40
|
-
runFix,
|
|
41
|
-
scanUsage,
|
|
42
|
-
setHarnesses,
|
|
43
|
-
setHidden,
|
|
44
|
-
setKeepGlobal,
|
|
45
|
-
skilllibHome,
|
|
46
|
-
summarize,
|
|
47
|
-
syncProject,
|
|
48
|
-
unlinkEverywhere,
|
|
49
|
-
unloadGlobal,
|
|
50
|
-
updateProject,
|
|
51
|
-
userHome,
|
|
52
|
-
versionHistory,
|
|
53
|
-
visibilityOf,
|
|
54
|
-
visibleProjects
|
|
55
|
-
} from "./chunk-O5TY4NWZ.js";
|
|
56
|
-
|
|
57
|
-
// src/tui/index.tsx
|
|
58
|
-
import { render } from "ink";
|
|
59
|
-
|
|
60
|
-
// src/tui/App.tsx
|
|
61
|
-
import { spawn, spawnSync as spawnSync2 } from "child_process";
|
|
62
|
-
import { homedir } from "os";
|
|
63
|
-
import { basename as basename3, dirname, join as join3, sep } from "path";
|
|
64
|
-
import { useEffect, useRef, useState as useState2 } from "react";
|
|
65
|
-
import { Box as Box3, Text as Text3, useApp, useInput, useWindowSize } from "ink";
|
|
66
|
-
import { lstatSync } from "fs";
|
|
67
|
-
|
|
68
|
-
// src/tui/components.tsx
|
|
69
|
-
import { Box, Text } from "ink";
|
|
70
|
-
|
|
71
|
-
// src/tui/theme.ts
|
|
72
|
-
var color = {
|
|
73
|
-
accent: "#8B9DFF",
|
|
74
|
-
accentDim: "#4B5578",
|
|
75
|
-
text: "#E5E7EB",
|
|
76
|
-
muted: "#8A93A6",
|
|
77
|
-
faint: "#5A6275",
|
|
78
|
-
green: "#6BCB77",
|
|
79
|
-
yellow: "#F2C94C",
|
|
80
|
-
red: "#EF6B6B",
|
|
81
|
-
magenta: "#C792EA",
|
|
82
|
-
blue: "#5EB8F7",
|
|
83
|
-
selection: "#2A3350"
|
|
84
|
-
};
|
|
85
|
-
function stateBadge(state) {
|
|
86
|
-
switch (state) {
|
|
87
|
-
case "ok":
|
|
88
|
-
return { icon: "\u25CF", label: "in sync", color: color.green };
|
|
89
|
-
case "update available":
|
|
90
|
-
return { icon: "\u2191", label: "update", color: color.yellow };
|
|
91
|
-
case "edited locally":
|
|
92
|
-
return { icon: "\u270E", label: "edited", color: color.magenta };
|
|
93
|
-
case "edited locally, update available":
|
|
94
|
-
return { icon: "\u270E", label: "edited + update", color: color.magenta };
|
|
95
|
-
case "folder missing":
|
|
96
|
-
return { icon: "\u2717", label: "missing", color: color.red };
|
|
97
|
-
case "not in library":
|
|
98
|
-
return { icon: "\u2717", label: "not in library", color: color.red };
|
|
99
|
-
case "untracked copy of library skill":
|
|
100
|
-
return { icon: "\u25CB", label: "untracked", color: color.blue };
|
|
101
|
-
case "untracked, differs from library":
|
|
102
|
-
return { icon: "\u2260", label: "differs", color: color.yellow };
|
|
103
|
-
case "local only":
|
|
104
|
-
return { icon: "\u25CB", label: "only here", color: color.faint };
|
|
105
|
-
case "repo skill":
|
|
106
|
-
return { icon: "\u25CB", label: "only here", color: color.faint };
|
|
107
|
-
case "repo skill, in library":
|
|
108
|
-
return { icon: "\u2713", label: "in your skills too", color: color.green };
|
|
109
|
-
case "repo skill, differs from library":
|
|
110
|
-
return { icon: "\u2260", label: "differs from yours", color: color.yellow };
|
|
111
|
-
case "from npx skills":
|
|
112
|
-
return { icon: "\u2193", label: "npx skills", color: color.magenta };
|
|
113
|
-
case "available":
|
|
114
|
-
return { icon: " ", label: "", color: color.faint };
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
var kindColor = {
|
|
118
|
-
global: color.yellow,
|
|
119
|
-
"skills.sh": color.magenta,
|
|
120
|
-
"claude.ai": color.blue,
|
|
121
|
-
plugin: color.accent,
|
|
122
|
-
"built-in": color.muted
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
// src/tui/components.tsx
|
|
126
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
127
|
-
function fit(text, width) {
|
|
128
|
-
if (width <= 0) return "";
|
|
129
|
-
return text.length > width ? text.slice(0, Math.max(0, width - 1)) + "\u2026" : text.padEnd(width);
|
|
130
|
-
}
|
|
131
|
-
function Panel({
|
|
132
|
-
title,
|
|
133
|
-
hint,
|
|
134
|
-
focused,
|
|
135
|
-
width,
|
|
136
|
-
height,
|
|
137
|
-
children
|
|
138
|
-
}) {
|
|
139
|
-
const border = focused ? color.accent : color.accentDim;
|
|
140
|
-
const label = ` ${title} `;
|
|
141
|
-
const extra = hint ? ` ${hint} ` : "";
|
|
142
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, height, borderStyle: "round", borderColor: border, paddingX: 1, children: [
|
|
143
|
-
/* @__PURE__ */ jsxs(Box, { position: "absolute", marginTop: -1, marginLeft: 1, children: [
|
|
144
|
-
/* @__PURE__ */ jsx(Text, { color: focused ? color.accent : color.muted, bold: true, children: fit(label, Math.max(0, width - 6 - extra.length)).trimEnd() + " " }),
|
|
145
|
-
extra ? /* @__PURE__ */ jsx(Text, { color: color.faint, children: extra }) : null
|
|
146
|
-
] }),
|
|
147
|
-
children
|
|
148
|
-
] });
|
|
149
|
-
}
|
|
150
|
-
function ListPanel({
|
|
151
|
-
title,
|
|
152
|
-
focused,
|
|
153
|
-
width,
|
|
154
|
-
height,
|
|
155
|
-
rows,
|
|
156
|
-
selected,
|
|
157
|
-
empty,
|
|
158
|
-
filter,
|
|
159
|
-
header,
|
|
160
|
-
headerHeight = 0
|
|
161
|
-
}) {
|
|
162
|
-
const visible = Math.max(1, height - 2 - headerHeight);
|
|
163
|
-
const offset = Math.min(Math.max(0, selected - Math.floor(visible / 2)), Math.max(0, rows.length - visible));
|
|
164
|
-
const shown = rows.slice(offset, offset + visible);
|
|
165
|
-
const inner = width - 4;
|
|
166
|
-
const hint = [filter ?? "", rows.length > visible ? `${selected + 1}/${rows.length}` : ""].filter(Boolean).join(" ");
|
|
167
|
-
return /* @__PURE__ */ jsxs(Panel, { title, hint, focused, width, height, children: [
|
|
168
|
-
header,
|
|
169
|
-
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) => {
|
|
170
|
-
if (row.header) {
|
|
171
|
-
const label = row.cells.map((c) => c.text).join("");
|
|
172
|
-
return /* @__PURE__ */ jsxs(Box, { height: 1, children: [
|
|
173
|
-
/* @__PURE__ */ jsx(Text, { color: color.accent, bold: true, children: " " + label + " " }),
|
|
174
|
-
/* @__PURE__ */ jsx(Text, { color: color.accentDim, children: "\u2500".repeat(Math.max(0, inner - label.length - 3)) })
|
|
175
|
-
] }, row.key);
|
|
176
|
-
}
|
|
177
|
-
const active = offset + i === selected;
|
|
178
|
-
const fixed = row.cells.filter((c) => !c.grow).reduce((n, c) => n + (c.width ?? c.text.length), 0);
|
|
179
|
-
const growWidth = Math.max(3, inner - 2 - fixed);
|
|
180
|
-
const bg = active ? focused ? color.selection : void 0 : void 0;
|
|
181
|
-
return /* @__PURE__ */ jsxs(Box, { height: 1, children: [
|
|
182
|
-
/* @__PURE__ */ jsx(Text, { backgroundColor: bg, color: active && focused ? color.accent : color.faint, children: active ? "\u258C " : " " }),
|
|
183
|
-
row.cells.map((c, j) => /* @__PURE__ */ jsx(
|
|
184
|
-
Text,
|
|
185
|
-
{
|
|
186
|
-
backgroundColor: bg,
|
|
187
|
-
color: c.color ?? (active && focused ? color.text : void 0),
|
|
188
|
-
bold: c.bold || active && focused && c.grow,
|
|
189
|
-
dimColor: c.dim,
|
|
190
|
-
wrap: "truncate-end",
|
|
191
|
-
children: fit(c.text, c.grow ? growWidth : c.width ?? c.text.length)
|
|
192
|
-
},
|
|
193
|
-
j
|
|
194
|
-
))
|
|
195
|
-
] }, row.key);
|
|
196
|
-
})
|
|
197
|
-
] });
|
|
198
|
-
}
|
|
199
|
-
function KeyBar({ hints, width }) {
|
|
200
|
-
let used = 0;
|
|
201
|
-
const fitting = hints.filter(([k, l]) => {
|
|
202
|
-
used += k.length + l.length + 4;
|
|
203
|
-
return used < width;
|
|
204
|
-
});
|
|
205
|
-
return /* @__PURE__ */ jsx(Box, { height: 1, width, children: fitting.map(([k, l]) => /* @__PURE__ */ jsxs(Box, { marginRight: 2, children: [
|
|
206
|
-
/* @__PURE__ */ jsx(Text, { backgroundColor: color.accentDim, color: color.text, children: ` ${k} ` }),
|
|
207
|
-
/* @__PURE__ */ jsx(Text, { color: color.muted, children: ` ${l}` })
|
|
208
|
-
] }, k + l)) });
|
|
209
|
-
}
|
|
210
|
-
function wrap(text, width, max) {
|
|
211
|
-
const words = text.replace(/\s+/g, " ").trim().split(" ");
|
|
212
|
-
const lines = [];
|
|
213
|
-
let line = "";
|
|
214
|
-
for (const word of words) {
|
|
215
|
-
if ((line + " " + word).trim().length > width) {
|
|
216
|
-
lines.push(line);
|
|
217
|
-
line = word;
|
|
218
|
-
if (lines.length === max) break;
|
|
219
|
-
} else line = (line + " " + word).trim();
|
|
220
|
-
}
|
|
221
|
-
if (lines.length < max && line) lines.push(line);
|
|
222
|
-
if (lines.length === max && words.join(" ").length > lines.join(" ").length) {
|
|
223
|
-
lines[max - 1] = fit(lines[max - 1], width - 1).trimEnd() + "\u2026";
|
|
224
|
-
}
|
|
225
|
-
return lines;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// src/tui/state.ts
|
|
229
|
-
import { basename, join } from "path";
|
|
230
|
-
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
231
|
-
var USAGE_DAYS = 30;
|
|
232
|
-
function loadSnapshot() {
|
|
233
|
-
forgetLatest();
|
|
234
|
-
const cwdRoot = findProjectRoot();
|
|
235
|
-
const cwdProject = isProjectCandidate(cwdRoot) && (existsSync(join(cwdRoot, ".git")) || existsSync(join(cwdRoot, "skilllib.json"))) ? cwdRoot : null;
|
|
236
|
-
const statusCache = /* @__PURE__ */ new Map();
|
|
237
|
-
const statusOf = (p) => {
|
|
238
|
-
let status = statusCache.get(p);
|
|
239
|
-
if (!status) statusCache.set(p, status = projectStatus(p));
|
|
240
|
-
return status;
|
|
241
|
-
};
|
|
242
|
-
const hasSkills = (p) => statusOf(p).length > 0;
|
|
243
|
-
const byName = (a, b) => basename(a).localeCompare(basename(b));
|
|
244
|
-
const others = visibleProjects().filter((p) => p !== cwdProject);
|
|
245
|
-
const projects = [
|
|
246
|
-
...cwdProject ? [cwdProject] : [],
|
|
247
|
-
...others.filter(hasSkills).sort(byName),
|
|
248
|
-
...others.filter((p) => !hasSkills(p)).sort(byName)
|
|
249
|
-
];
|
|
250
|
-
const origins = libraryOrigins();
|
|
251
|
-
const machine = machineSkills();
|
|
252
|
-
const inferred = (name) => {
|
|
253
|
-
const m = machine.find((s) => s.name === name && !s.broken && s.movable) ?? machine.find((s) => s.name === name && !s.broken);
|
|
254
|
-
return m ? m.kind === "global" ? "global folder" : `${m.kind}: ${m.origin}` : "";
|
|
255
|
-
};
|
|
256
|
-
const library = librarySkills().map((s) => ({
|
|
257
|
-
name: s.name,
|
|
258
|
-
description: s.description,
|
|
259
|
-
origin: origins[s.name] ?? inferred(s.name),
|
|
260
|
-
// Managed status entries are the manifest, already read above; no need to re-read every manifest per skill.
|
|
261
|
-
projects: projects.filter((p) => statusOf(p).some((x) => x.managed && x.name === s.name)).sort(),
|
|
262
|
-
version: latestVersion(s.name)?.version ?? null
|
|
263
|
-
}));
|
|
264
|
-
return {
|
|
265
|
-
cwdProject,
|
|
266
|
-
projects,
|
|
267
|
-
rows: new Map(projects.map((p) => [p, projectRows(p, library, statusOf(p))])),
|
|
268
|
-
library,
|
|
269
|
-
machine,
|
|
270
|
-
issues: findIssues(projects, machine, new Set(library.map((l) => l.name)), statusOf),
|
|
271
|
-
backups: [...listBackups(), ...pluginBackups()].sort((a, b) => b.movedAt.localeCompare(a.movedAt)),
|
|
272
|
-
roots: readConfig().roots,
|
|
273
|
-
harnesses: enabledHarnesses(),
|
|
274
|
-
harnessesChosen: harnessesChosen(),
|
|
275
|
-
keptGlobal: keptGlobal()
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
function projectRows(root, library, status = projectStatus(root)) {
|
|
279
|
-
const byName = new Map(status.map((s) => [s.name, s]));
|
|
280
|
-
const libraryNames = new Set(library.map((l) => l.name));
|
|
281
|
-
const installed = status.map(
|
|
282
|
-
(s) => ({
|
|
283
|
-
name: s.name,
|
|
284
|
-
installed: true,
|
|
285
|
-
inLibrary: libraryNames.has(s.name),
|
|
286
|
-
state: s.state,
|
|
287
|
-
managed: s.managed,
|
|
288
|
-
version: s.version,
|
|
289
|
-
latest: s.latest,
|
|
290
|
-
location: s.location,
|
|
291
|
-
path: s.path,
|
|
292
|
-
visibility: s.visibility,
|
|
293
|
-
...s.source ? { source: s.source } : {},
|
|
294
|
-
description: library.find((l) => l.name === s.name)?.description ?? readSkillInfo(s.path).description
|
|
295
|
-
})
|
|
296
|
-
);
|
|
297
|
-
const available = library.filter((l) => !byName.has(l.name)).map(
|
|
298
|
-
(l) => ({
|
|
299
|
-
name: l.name,
|
|
300
|
-
installed: false,
|
|
301
|
-
inLibrary: true,
|
|
302
|
-
state: "available",
|
|
303
|
-
description: l.description,
|
|
304
|
-
managed: false,
|
|
305
|
-
version: null,
|
|
306
|
-
latest: l.version,
|
|
307
|
-
location: "",
|
|
308
|
-
path: librarySkillDir(l.name),
|
|
309
|
-
visibility: []
|
|
310
|
-
})
|
|
311
|
-
);
|
|
312
|
-
return [...installed, ...available].sort((a, b) => a.name.localeCompare(b.name));
|
|
313
|
-
}
|
|
314
|
-
async function loadUsage(projects) {
|
|
315
|
-
const projectOf = projectOfFactory(projects);
|
|
316
|
-
const uses = await scanUsage(USAGE_DAYS);
|
|
317
|
-
const summaries = summarize(uses, projectOf);
|
|
318
|
-
const projectSkill = new Set(
|
|
319
|
-
uses.filter((u) => u.cwd && (existsSync(join(u.cwd, ".claude", "skills", u.skill)) || existsSync(join(u.cwd, ".agents", "skills", u.skill)))).map((u) => u.skill)
|
|
320
|
-
);
|
|
321
|
-
const byProject = /* @__PURE__ */ new Map();
|
|
322
|
-
for (const use of uses) {
|
|
323
|
-
const root = projectOf(use.cwd);
|
|
324
|
-
if (!root) continue;
|
|
325
|
-
const counts = byProject.get(root) ?? /* @__PURE__ */ new Map();
|
|
326
|
-
counts.set(use.skill, (counts.get(use.skill) ?? 0) + 1);
|
|
327
|
-
byProject.set(root, counts);
|
|
328
|
-
}
|
|
329
|
-
return {
|
|
330
|
-
total: new Map(summaries.map((s) => [s.skill, s.uses])),
|
|
331
|
-
byProject,
|
|
332
|
-
rows: summaries.map((s) => ({
|
|
333
|
-
skill: s.skill,
|
|
334
|
-
uses: s.uses,
|
|
335
|
-
lastUsed: s.lastUsed.slice(0, 10),
|
|
336
|
-
projects: [...s.projects].map((p) => basename(p)),
|
|
337
|
-
inProject: projectSkill.has(s.skill)
|
|
338
|
-
}))
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
function readPreview(dir) {
|
|
342
|
-
const path = join(dir, "SKILL.md");
|
|
343
|
-
if (!existsSync(path)) return null;
|
|
344
|
-
const content = readFileSync(path, "utf-8");
|
|
345
|
-
const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").split("\n").map((l) => l.trimEnd()).filter((l) => l.trim() !== "");
|
|
346
|
-
let files = 0;
|
|
347
|
-
try {
|
|
348
|
-
files = readdirSync(dir, { recursive: true, withFileTypes: true }).filter((d) => d.isFile()).length;
|
|
349
|
-
} catch {
|
|
350
|
-
files = 0;
|
|
351
|
-
}
|
|
352
|
-
return { description: readSkillInfo(dir).description, body, files };
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// src/tui/CleanupWizard.tsx
|
|
356
|
-
import { basename as basename2 } from "path";
|
|
357
|
-
import { useState } from "react";
|
|
358
|
-
import { Box as Box2, Text as Text2 } from "ink";
|
|
359
|
-
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
360
|
-
var NEXT_MODE = { move: "keep", keep: "delete", delete: "move" };
|
|
361
|
-
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
362
|
-
function CleanupWizard({
|
|
363
|
-
skills,
|
|
364
|
-
kept,
|
|
365
|
-
projects,
|
|
366
|
-
usesIn,
|
|
367
|
-
usesTotal,
|
|
368
|
-
onReviewPrompt,
|
|
369
|
-
usageDays,
|
|
370
|
-
installedIn,
|
|
371
|
-
initialSkill,
|
|
372
|
-
width,
|
|
373
|
-
height,
|
|
374
|
-
onDone,
|
|
375
|
-
onCancel,
|
|
376
|
-
inputRef
|
|
377
|
-
}) {
|
|
378
|
-
const [step, setStep] = useState("choose");
|
|
379
|
-
const [focus, setFocus] = useState("skills");
|
|
380
|
-
const [modes, setModes] = useState(
|
|
381
|
-
() => new Map(skills.map((s) => [s.path, s.name === initialSkill || !initialSkill && !kept.has(s.name) ? "move" : "keep"]))
|
|
382
|
-
);
|
|
383
|
-
const modeOf = (s) => modes.get(s.path) ?? "keep";
|
|
384
|
-
const [touched, setTouched] = useState(() => new Set(initialSkill ? [] : skills.map((s) => s.path)));
|
|
385
|
-
const setMode = (s, mode) => {
|
|
386
|
-
setModes((prev) => new Map(prev).set(s.path, mode));
|
|
387
|
-
setTouched((prev) => new Set(prev).add(s.path));
|
|
388
|
-
};
|
|
389
|
-
const [showKept, setShowKept] = useState(false);
|
|
390
|
-
const shown = (on) => skills.filter((s) => on || !kept.has(s.name) || s.name === initialSkill);
|
|
391
|
-
const list = shown(showKept);
|
|
392
|
-
const hiddenKept = skills.length - shown(false).length;
|
|
393
|
-
const [skillIndex, setSkillIndex] = useState(() => Math.max(0, list.findIndex((s) => s.name === initialSkill)));
|
|
394
|
-
const [projectIndex, setProjectIndex] = useState(0);
|
|
395
|
-
const [status, setStatus] = useState("");
|
|
396
|
-
const [targets, setTargets] = useState(
|
|
397
|
-
() => new Map(
|
|
398
|
-
skills.map((s) => [s.path, new Set(projects.filter((p) => installedIn(s.name).includes(p) || (usesIn(s.name, p) ?? 0) > 0))])
|
|
399
|
-
)
|
|
400
|
-
);
|
|
401
|
-
const skill = list[skillIndex];
|
|
402
|
-
const chosen = skills.filter((s) => modeOf(s) === "move");
|
|
403
|
-
const toDelete = skills.filter((s) => modeOf(s) === "delete");
|
|
404
|
-
const toKeep = skills.filter((s) => modeOf(s) === "keep" && touched.has(s.path) && !kept.has(s.name));
|
|
405
|
-
const totalInstalls = chosen.reduce((n, s) => n + (targets.get(s.path)?.size ?? 0), 0);
|
|
406
|
-
const orphans = chosen.filter((s) => (targets.get(s.path)?.size ?? 0) === 0);
|
|
407
|
-
const toggleTarget = (s, project) => setTargets((prev) => {
|
|
408
|
-
const next = new Map(prev);
|
|
409
|
-
const set = new Set(next.get(s.path));
|
|
410
|
-
if (set.has(project)) set.delete(project);
|
|
411
|
-
else set.add(project);
|
|
412
|
-
next.set(s.path, set);
|
|
413
|
-
return next;
|
|
414
|
-
});
|
|
415
|
-
const apply = () => {
|
|
416
|
-
const problems = [];
|
|
417
|
-
let installs = 0;
|
|
418
|
-
let unloaded = 0;
|
|
419
|
-
let deleted = 0;
|
|
420
|
-
for (const s of toDelete) {
|
|
421
|
-
const r = deleteGlobal(s.path, s.links);
|
|
422
|
-
if (r.ok) deleted++;
|
|
423
|
-
else problems.push(`${s.name}: ${r.reason}`);
|
|
424
|
-
}
|
|
425
|
-
for (const s of chosen) {
|
|
426
|
-
importSkill(s.path);
|
|
427
|
-
let failed = false;
|
|
428
|
-
for (const project of targets.get(s.path) ?? []) {
|
|
429
|
-
const change = addSkill(project, s.name);
|
|
430
|
-
if (change.action === "skipped") {
|
|
431
|
-
problems.push(`${s.name} in ${basename2(project)}: ${change.reason}`);
|
|
432
|
-
failed = true;
|
|
433
|
-
} else installs++;
|
|
434
|
-
}
|
|
435
|
-
if (failed) continue;
|
|
436
|
-
const r = unloadGlobal(s.path, s.links);
|
|
437
|
-
if (r.ok) unloaded++;
|
|
438
|
-
else problems.push(`${s.name}: ${r.reason}`);
|
|
439
|
-
}
|
|
440
|
-
setKeepGlobal(toKeep.map((s) => s.name), true);
|
|
441
|
-
setKeepGlobal([...chosen, ...toDelete].map((s) => s.name).filter((n) => kept.has(n)), false);
|
|
442
|
-
onDone({ unloaded, installs, deleted, kept: toKeep.length, problems });
|
|
443
|
-
};
|
|
444
|
-
inputRef.current = (input, key) => {
|
|
445
|
-
if (key.ctrl && input === "c") return onCancel();
|
|
446
|
-
if (step === "review") {
|
|
447
|
-
if (key.return) return apply();
|
|
448
|
-
if (key.escape || key.leftArrow) return setStep("choose");
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
if (key.escape) return onCancel();
|
|
452
|
-
if (input === "p") return setStatus(onReviewPrompt(list));
|
|
453
|
-
if (input === "k" && hiddenKept) {
|
|
454
|
-
const next = shown(!showKept);
|
|
455
|
-
setShowKept(!showKept);
|
|
456
|
-
return setSkillIndex(Math.max(0, next.findIndex((s) => s.path === skill?.path)));
|
|
457
|
-
}
|
|
458
|
-
if (key.return) return chosen.length || toDelete.length || toKeep.length ? setStep("review") : void 0;
|
|
459
|
-
if (key.tab || key.rightArrow || key.leftArrow) {
|
|
460
|
-
return setFocus(key.leftArrow ? "skills" : key.rightArrow ? "projects" : focus === "skills" ? "projects" : "skills");
|
|
461
|
-
}
|
|
462
|
-
if (focus === "skills") {
|
|
463
|
-
if (key.upArrow) return setSkillIndex((i) => Math.max(0, i - 1));
|
|
464
|
-
if (key.downArrow) return setSkillIndex((i) => Math.min(list.length - 1, i + 1));
|
|
465
|
-
if (input === " " && skill) return setMode(skill, NEXT_MODE[modeOf(skill)]);
|
|
466
|
-
if (input === "d" && skill) return setMode(skill, modeOf(skill) === "delete" ? "keep" : "delete");
|
|
467
|
-
} else {
|
|
468
|
-
if (key.upArrow) return setProjectIndex((i) => Math.max(0, i - 1));
|
|
469
|
-
if (key.downArrow) return setProjectIndex((i) => Math.min(projects.length - 1, i + 1));
|
|
470
|
-
if (input === " " && skill && projects[projectIndex]) {
|
|
471
|
-
if (modeOf(skill) !== "move") setMode(skill, "move");
|
|
472
|
-
return toggleTarget(skill, projects[projectIndex]);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
};
|
|
476
|
-
const bodyHeight = height - 5;
|
|
477
|
-
if (step === "review") {
|
|
478
|
-
const lines = chosen.map((s) => {
|
|
479
|
-
const where = [...targets.get(s.path) ?? []].map((p) => basename2(p));
|
|
480
|
-
return { s, where };
|
|
481
|
-
});
|
|
482
|
-
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width, height, children: [
|
|
483
|
-
/* @__PURE__ */ jsxs2(Box2, { height: 1, paddingX: 1, children: [
|
|
484
|
-
/* @__PURE__ */ jsx2(Text2, { color: color.accent, bold: true, children: "Clean up global skills" }),
|
|
485
|
-
/* @__PURE__ */ jsx2(Text2, { color: color.muted, children: " \xB7 2 of 2 \xB7 review" })
|
|
486
|
-
] }),
|
|
487
|
-
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: color.accent, paddingX: 2, paddingY: 1, height: bodyHeight + 2, children: [
|
|
488
|
-
/* @__PURE__ */ jsx2(Text2, { color: color.text, children: [
|
|
489
|
-
chosen.length ? `${plural(chosen.length, "skill")} ${chosen.length === 1 ? "moves" : "move"} into your projects (${plural(totalInstalls, "install")})` : "",
|
|
490
|
-
toDelete.length ? `${plural(toDelete.length, "skill")} ${toDelete.length === 1 ? "is" : "are"} deleted` : "",
|
|
491
|
-
toKeep.length ? `${plural(toKeep.length, "skill")} ${toKeep.length === 1 ? "stays" : "stay"} global on purpose` : ""
|
|
492
|
-
].filter(Boolean).join(" \xB7 ") + "." }),
|
|
493
|
-
/* @__PURE__ */ jsx2(Text2, { color: color.faint, children: "Moved skills are copied into Your skills first. Every original goes to a backup you can restore from Health." }),
|
|
494
|
-
/* @__PURE__ */ jsx2(Text2, { children: " " }),
|
|
495
|
-
lines.slice(0, Math.max(1, bodyHeight - 7)).map(({ s, where }) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate-end", children: [
|
|
496
|
-
/* @__PURE__ */ jsx2(Text2, { color: color.text, children: s.name.padEnd(32) }),
|
|
497
|
-
/* @__PURE__ */ jsx2(Text2, { color: color.faint, children: `${usesTotal(s.name) ?? 0 ? `${usesTotal(s.name)} uses` : "unused"}`.padEnd(10) }),
|
|
498
|
-
/* @__PURE__ */ jsx2(Text2, { color: where.length ? color.green : color.yellow, children: where.length ? `\u2192 ${where.join(", ")}` : "\u2192 only in Your skills (no project)" })
|
|
499
|
-
] }, s.path)),
|
|
500
|
-
lines.length > bodyHeight - 7 ? /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: `\u2026 and ${lines.length - (bodyHeight - 7)} more` }) : null,
|
|
501
|
-
toDelete.length ? /* @__PURE__ */ jsx2(Text2, { color: color.red, wrap: "truncate-end", children: `\u2715 delete (not kept in Your skills): ${toDelete.map((s) => s.name).join(", ")}` }) : null,
|
|
502
|
-
toKeep.length ? /* @__PURE__ */ jsx2(Text2, { color: color.muted, wrap: "truncate-end", children: `\u2713 keep global, and skip next cleanup: ${toKeep.map((s) => s.name).join(", ")}` }) : null,
|
|
503
|
-
/* @__PURE__ */ jsx2(Box2, { flexGrow: 1 }),
|
|
504
|
-
orphans.length ? /* @__PURE__ */ jsx2(Text2, { color: color.yellow, children: `${plural(orphans.length, "skill")} won't be in any project \u2014 add them later from "Add skills".` }) : null
|
|
505
|
-
] }),
|
|
506
|
-
/* @__PURE__ */ jsx2(Box2, { height: 1, paddingX: 1, children: /* @__PURE__ */ jsx2(
|
|
507
|
-
KeyBar,
|
|
508
|
-
{
|
|
509
|
-
width: width - 2,
|
|
510
|
-
hints: [
|
|
511
|
-
["enter", "apply"],
|
|
512
|
-
["esc", "back"]
|
|
513
|
-
]
|
|
514
|
-
}
|
|
515
|
-
) })
|
|
516
|
-
] });
|
|
517
|
-
}
|
|
518
|
-
const leftWidth = Math.max(40, Math.floor(width * 0.55));
|
|
519
|
-
const detailHeight = height >= 26 ? 7 : height >= 20 ? 5 : 0;
|
|
520
|
-
const listHeight = bodyHeight - detailHeight;
|
|
521
|
-
const usesText = (n) => n === null ? "\u2026" : n === 0 ? "unused" : `${n} use${n === 1 ? "" : "s"}`;
|
|
522
|
-
const skillRows = list.map((s) => {
|
|
523
|
-
const mode = modeOf(s);
|
|
524
|
-
const n = targets.get(s.path)?.size ?? 0;
|
|
525
|
-
return {
|
|
526
|
-
key: s.path,
|
|
527
|
-
cells: [
|
|
528
|
-
{ text: mode === "move" ? "\u25C9 " : mode === "delete" ? "\u2715 " : "\u25CB ", color: mode === "move" ? color.accent : mode === "delete" ? color.red : color.faint },
|
|
529
|
-
{ text: s.name, grow: true, color: mode === "keep" ? color.muted : mode === "delete" ? color.red : void 0 },
|
|
530
|
-
{ text: usesText(usesTotal(s.name)).padStart(9), width: 9, color: usesTotal(s.name) ? color.text : color.faint },
|
|
531
|
-
{
|
|
532
|
-
text: mode === "keep" ? kept.has(s.name) ? " \u2713 kept global" : " keep global" : mode === "delete" ? " delete" : n ? ` \u2192 ${plural(n, "project")}` : " \u2192 no project",
|
|
533
|
-
width: 15,
|
|
534
|
-
color: mode === "keep" ? color.faint : mode === "delete" ? color.red : n ? color.green : color.yellow
|
|
535
|
-
}
|
|
536
|
-
]
|
|
537
|
-
};
|
|
538
|
-
});
|
|
539
|
-
const projectRows2 = skill ? projects.map((p) => {
|
|
540
|
-
const on = targets.get(skill.path)?.has(p) ?? false;
|
|
541
|
-
const uses = usesIn(skill.name, p);
|
|
542
|
-
return {
|
|
543
|
-
key: p,
|
|
544
|
-
cells: [
|
|
545
|
-
{ text: on ? "\u25C9 " : "\u25CB ", color: on ? color.green : color.faint },
|
|
546
|
-
{ text: basename2(p), grow: true, color: on ? void 0 : color.muted },
|
|
547
|
-
{ text: uses ? `${uses} use${uses === 1 ? "" : "s"}` : "", color: color.muted }
|
|
548
|
-
]
|
|
549
|
-
};
|
|
550
|
-
}) : [];
|
|
551
|
-
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width, height, children: [
|
|
552
|
-
/* @__PURE__ */ jsxs2(Box2, { height: 1, paddingX: 1, children: [
|
|
553
|
-
/* @__PURE__ */ jsx2(Text2, { color: color.accent, bold: true, children: "Clean up global skills" }),
|
|
554
|
-
/* @__PURE__ */ jsx2(Text2, { color: color.muted, children: " \xB7 1 of 2 \xB7 choose where each skill should live" })
|
|
555
|
-
] }),
|
|
556
|
-
/* @__PURE__ */ jsxs2(Box2, { height: 2, paddingX: 1, flexDirection: "column", children: [
|
|
557
|
-
/* @__PURE__ */ jsx2(Text2, { color: color.text, wrap: "truncate-end", children: "These load in every repo, so every agent reads them all the time. Pick the projects that should keep each one." }),
|
|
558
|
-
/* @__PURE__ */ jsx2(Text2, { color: color.faint, wrap: "truncate-end", children: status ? `\u2713 ${status}` : "\u25C9 move into the ticked projects \xB7 \u25CB keep global (remembered) \xB7 \u2715 delete. Pre-ticked: projects that have it or used it. p: ask an agent." })
|
|
559
|
-
] }),
|
|
560
|
-
/* @__PURE__ */ jsxs2(Box2, { height: listHeight, children: [
|
|
561
|
-
/* @__PURE__ */ jsx2(
|
|
562
|
-
ListPanel,
|
|
563
|
-
{
|
|
564
|
-
title: `Global skills \xB7 ${chosen.length} move \xB7 ${toDelete.length} delete${hiddenKept && !showKept ? ` \xB7 ${hiddenKept} kept hidden` : ""} \xB7 uses ${usageDays}d`,
|
|
565
|
-
focused: focus === "skills",
|
|
566
|
-
width: leftWidth,
|
|
567
|
-
height: listHeight,
|
|
568
|
-
rows: skillRows,
|
|
569
|
-
selected: skillIndex,
|
|
570
|
-
empty: hiddenKept ? `All reviewed: you keep ${plural(hiddenKept, "skill")} global on purpose. k shows them.` : "None of your skills load globally. \u{1F389}"
|
|
571
|
-
}
|
|
572
|
-
),
|
|
573
|
-
/* @__PURE__ */ jsx2(
|
|
574
|
-
ListPanel,
|
|
575
|
-
{
|
|
576
|
-
title: skill ? `Keep ${skill.name} in\u2026` : "Projects",
|
|
577
|
-
focused: focus === "projects",
|
|
578
|
-
width: width - leftWidth,
|
|
579
|
-
height: listHeight,
|
|
580
|
-
rows: projectRows2,
|
|
581
|
-
selected: projectIndex,
|
|
582
|
-
empty: "No projects yet. Add a folder in Settings."
|
|
583
|
-
}
|
|
584
|
-
)
|
|
585
|
-
] }),
|
|
586
|
-
detailHeight && skill ? /* @__PURE__ */ jsxs2(Panel, { title: skill.name, focused: false, width, height: detailHeight, children: [
|
|
587
|
-
/* @__PURE__ */ jsx2(Text2, { color: color.muted, wrap: "truncate-end", children: (() => {
|
|
588
|
-
const total = usesTotal(skill.name);
|
|
589
|
-
const where = projects.map((p) => ({ p, n: usesIn(skill.name, p) ?? 0 })).filter((x) => x.n > 0).sort((a, b) => b.n - a.n).map((x) => `${basename2(x.p)} ${x.n}`);
|
|
590
|
-
const usage = total === null ? "reading usage\u2026" : total === 0 ? `not used by Claude Code in the last ${usageDays} days` : `used ${total} time${total === 1 ? "" : "s"} in ${usageDays} days${where.length ? ` \u2014 ${where.join(", ")}` : " (outside your projects)"}`;
|
|
591
|
-
return `${usage} \xB7 ${skill.origin}`;
|
|
592
|
-
})() }),
|
|
593
|
-
wrap(skill.description || "(no description)", width - 6, detailHeight - 3).map((l, i) => /* @__PURE__ */ jsx2(Text2, { color: color.text, wrap: "truncate-end", children: l }, i))
|
|
594
|
-
] }) : null,
|
|
595
|
-
/* @__PURE__ */ jsx2(Box2, { height: 1, paddingX: 1, children: /* @__PURE__ */ jsx2(
|
|
596
|
-
KeyBar,
|
|
597
|
-
{
|
|
598
|
-
width: width - 2,
|
|
599
|
-
hints: [
|
|
600
|
-
["space", focus === "skills" ? "move / keep / delete" : "tick project"],
|
|
601
|
-
...focus === "skills" ? [["d", "delete"]] : [],
|
|
602
|
-
["tab", "switch side"],
|
|
603
|
-
...hiddenKept ? [["k", showKept ? "hide kept" : `show kept (${hiddenKept})`]] : [],
|
|
604
|
-
["enter", `review (${plural(totalInstalls, "install")})`],
|
|
605
|
-
["p", "ask an agent"],
|
|
606
|
-
["esc", "cancel"]
|
|
607
|
-
]
|
|
608
|
-
}
|
|
609
|
-
) })
|
|
610
|
-
] });
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// src/help.ts
|
|
614
|
-
var HELP_TOPICS = [
|
|
615
|
-
{
|
|
616
|
-
id: "basics",
|
|
617
|
-
title: "Using skilllib",
|
|
618
|
-
blocks: [
|
|
619
|
-
{
|
|
620
|
-
lines: [
|
|
621
|
-
"Pick a place on the left: a project, Your skills, Global, Health or Settings. Its skills show on the right.",
|
|
622
|
-
"Inside a project, tab switches between Usable here and Add skills."
|
|
623
|
-
]
|
|
624
|
-
},
|
|
625
|
-
{
|
|
626
|
-
heading: "Keys",
|
|
627
|
-
lines: [
|
|
628
|
-
"type search the pane you're in",
|
|
629
|
-
"\u2191 \u2193 move \u2192 \u2190 open / close a group",
|
|
630
|
-
"space the main action: add, remove, copy, clean up, restore",
|
|
631
|
-
"enter every action for the selected row (or open a place)",
|
|
632
|
-
"tab switch lists inside a project",
|
|
633
|
-
"esc clear the search, then go back to the left pane",
|
|
634
|
-
"ctrl+r reload ctrl+c quit",
|
|
635
|
-
"p in the cleanup wizard: copy a prompt asking an agent to review the skills",
|
|
636
|
-
"k in the cleanup wizard: show / hide the skills you keep global on purpose"
|
|
637
|
-
]
|
|
638
|
-
},
|
|
639
|
-
{
|
|
640
|
-
heading: "Badges",
|
|
641
|
-
lines: [
|
|
642
|
-
"\u273B \u2B21 \u25CE \u2124 Claude Code, Cursor, Codex, Zed \u2014 lit when that agent loads the skill here",
|
|
643
|
-
"\u26A0 loaded globally loads in every repo on this machine, and you haven't decided about it yet",
|
|
644
|
-
"\u2713 global loads in every repo, and you keep it global on purpose (Enter \u2192 Keep global on purpose)",
|
|
645
|
-
"\u29C9 N copies the same skill reaches agents from N places \u2014 Health has the fix",
|
|
646
|
-
"\u25CF in sync \xB7 \u2191 update \xB7 \u270E edited \xB7 \u25CB only here \xB7 \u2193 owner/repo installed here by npx skills",
|
|
647
|
-
"\u2713 committed in git, so teammates get it \xB1 changed committed, with local edits",
|
|
648
|
-
"+ not added not committed yet \u2205 ignored gitignored \u2014 only on your machine"
|
|
649
|
-
]
|
|
650
|
-
}
|
|
651
|
-
]
|
|
652
|
-
},
|
|
653
|
-
{
|
|
654
|
-
id: "sources",
|
|
655
|
-
title: "Where skills come from",
|
|
656
|
-
blocks: [
|
|
657
|
-
{
|
|
658
|
-
lines: ["A skill is a folder with a SKILL.md. Agents only ever read folders \u2014 these are all the places they can be:"]
|
|
659
|
-
},
|
|
660
|
-
{
|
|
661
|
-
heading: "In a repo (only agents working in that repo see them)",
|
|
662
|
-
lines: [
|
|
663
|
-
".agents/skills the shared folder: Codex, Cursor, Zed and most other agents. Repos often commit their team skills here.",
|
|
664
|
-
" skilllib (like `npx skills`) keeps the real copy here whenever one of your agents needs it.",
|
|
665
|
-
".claude/skills Claude Code (Cursor reads it too). A link to .agents/skills, or the copy itself if only Claude Code needs it.",
|
|
666
|
-
".cursor/skills Cursor only. .codex/skills Cursor only (compatibility)."
|
|
667
|
-
]
|
|
668
|
-
},
|
|
669
|
-
{
|
|
670
|
-
heading: "Global (every repo on this machine \u2014 every agent reads them all the time)",
|
|
671
|
-
lines: [
|
|
672
|
-
"~/.claude/skills Claude Code and Cursor. Things you or tools copied in.",
|
|
673
|
-
"~/.agents/skills Codex, Cursor, Zed. `npx skills add` installs here (and links into ~/.claude/skills).",
|
|
674
|
-
"~/.cursor/skills Cursor only."
|
|
675
|
-
]
|
|
676
|
-
},
|
|
677
|
-
{
|
|
678
|
-
heading: "From vendors (skilllib can't remove these \u2014 manage them at the source)",
|
|
679
|
-
lines: [
|
|
680
|
-
"Claude Code plugins /plugin; skills are namespaced, e.g. /ponytail:ponytail",
|
|
681
|
-
"claude.ai skills synced from your account to ~/.claude/skills/synced (/anthropic-skills:name)",
|
|
682
|
-
"Claude Code bundled built-in commands like /code-review",
|
|
683
|
-
"Cursor built-in ~/.cursor/skills-cursor, updates with Cursor",
|
|
684
|
-
"Cursor plugins installed from Cursor's marketplace (skilllib doesn't list these yet)",
|
|
685
|
-
"Codex system skills bundled by OpenAI; /etc/codex/skills for admin-installed ones"
|
|
686
|
-
]
|
|
687
|
-
},
|
|
688
|
-
{
|
|
689
|
-
heading: "Your skills (skilllib)",
|
|
690
|
-
lines: [
|
|
691
|
-
"~/.skilllib/library holds one master copy of each skill you saved. Nothing there loads by itself \u2014",
|
|
692
|
-
"a project gets a copy (pinned to a version in skilllib.json) when you add it."
|
|
693
|
-
]
|
|
694
|
-
}
|
|
695
|
-
]
|
|
696
|
-
},
|
|
697
|
-
{
|
|
698
|
-
id: "claude-code",
|
|
699
|
-
title: "Claude Code",
|
|
700
|
-
icon: "\u273B",
|
|
701
|
-
blocks: [
|
|
702
|
-
{
|
|
703
|
-
heading: "Where it looks (winner first when names collide)",
|
|
704
|
-
lines: [
|
|
705
|
-
"1 Enterprise managed settings folder (org-deployed)",
|
|
706
|
-
"2 Personal ~/.claude/skills \u2014 every project",
|
|
707
|
-
"3 Project .claude/skills in the repo",
|
|
708
|
-
"4 Nested <subfolder>/.claude/skills \u2014 sessions working in that folder",
|
|
709
|
-
"5 Plugin <plugin>/skills \u2014 namespaced as /plugin:skill",
|
|
710
|
-
"6 Bundled built-ins like /code-review",
|
|
711
|
-
"Also: skills synced from claude.ai (~/.claude/skills/synced), and folders added with --add-dir.",
|
|
712
|
-
"It does NOT read .agents/skills \u2014 link a skill into .claude/skills for Claude to see it."
|
|
713
|
-
]
|
|
714
|
-
},
|
|
715
|
-
{
|
|
716
|
-
heading: "How it uses them",
|
|
717
|
-
lines: [
|
|
718
|
-
"Every skill's description is always in context; the full SKILL.md loads when you type /name",
|
|
719
|
-
"or when Claude decides it matches the task.",
|
|
720
|
-
"disable-model-invocation: true \u2192 only you can run it. user-invocable: false \u2192 only Claude can."
|
|
721
|
-
]
|
|
722
|
-
},
|
|
723
|
-
{
|
|
724
|
-
heading: "Per-repo control",
|
|
725
|
-
lines: [
|
|
726
|
-
'skillOverrides in .claude/settings(.local).json: "off", "name-only", "user-invocable-only".',
|
|
727
|
-
"It's Claude-only, so skilllib moves skills out of global instead of blocking them."
|
|
728
|
-
]
|
|
729
|
-
}
|
|
730
|
-
]
|
|
731
|
-
},
|
|
732
|
-
{
|
|
733
|
-
id: "cursor",
|
|
734
|
-
title: "Cursor",
|
|
735
|
-
icon: "\u2B21",
|
|
736
|
-
blocks: [
|
|
737
|
-
{
|
|
738
|
-
heading: "Where it looks",
|
|
739
|
-
lines: [
|
|
740
|
-
"Project .agents/skills, .cursor/skills \u2014 nested folders too (scoped to files inside them)",
|
|
741
|
-
"User ~/.agents/skills, ~/.cursor/skills",
|
|
742
|
-
"Compat .claude/skills, .codex/skills, ~/.claude/skills, ~/.codex/skills",
|
|
743
|
-
"Plus plugins from its marketplace, and Cursor's built-in skills",
|
|
744
|
-
"Cloud Agents only see ~/.cursor/skills.",
|
|
745
|
-
"A skill in two folders is listed once (tested). A plugin's copy and yours are listed separately."
|
|
746
|
-
]
|
|
747
|
-
},
|
|
748
|
-
{
|
|
749
|
-
heading: "How it uses them",
|
|
750
|
-
lines: [
|
|
751
|
-
"Descriptions decide relevance; full content loads on use. Type / to run one.",
|
|
752
|
-
"paths: [globs] in frontmatter limits a skill to matching files.",
|
|
753
|
-
"disable-model-invocation: true makes it a manual slash command.",
|
|
754
|
-
"No per-project switch for global skills."
|
|
755
|
-
]
|
|
756
|
-
}
|
|
757
|
-
]
|
|
758
|
-
},
|
|
759
|
-
{
|
|
760
|
-
id: "codex",
|
|
761
|
-
title: "Codex",
|
|
762
|
-
icon: "\u25CE",
|
|
763
|
-
blocks: [
|
|
764
|
-
{
|
|
765
|
-
heading: "Where it looks",
|
|
766
|
-
lines: [
|
|
767
|
-
"Repo .agents/skills in the working folder, its parents, and the repo root",
|
|
768
|
-
"User ~/.agents/skills",
|
|
769
|
-
"Admin /etc/codex/skills",
|
|
770
|
-
"System skills bundled by OpenAI",
|
|
771
|
-
"It does NOT read .claude/skills \u2014 skilllib keeps your skills' real copies in .agents/skills."
|
|
772
|
-
]
|
|
773
|
-
},
|
|
774
|
-
{
|
|
775
|
-
heading: "How it uses them",
|
|
776
|
-
lines: [
|
|
777
|
-
"Starts with names and descriptions (capped at ~2% of context), loads SKILL.md when used.",
|
|
778
|
-
"Type $ to pick one; it also chooses them itself when the description matches.",
|
|
779
|
-
"Same-name skills aren't merged \u2014 both can show up.",
|
|
780
|
-
"agents/openai.yaml \u2192 policy.allow_implicit_invocation: false keeps it manual.",
|
|
781
|
-
"Disable one everywhere: [[skills.config]] path = \u2026, enabled = false in ~/.codex/config.toml."
|
|
782
|
-
]
|
|
783
|
-
}
|
|
784
|
-
]
|
|
785
|
-
},
|
|
786
|
-
{
|
|
787
|
-
id: "zed",
|
|
788
|
-
title: "Zed",
|
|
789
|
-
icon: "\u2124",
|
|
790
|
-
blocks: [
|
|
791
|
-
{
|
|
792
|
-
heading: "Where it looks",
|
|
793
|
-
lines: [
|
|
794
|
-
"Project <worktree>/.agents/skills \u2014 only in trusted worktrees",
|
|
795
|
-
"Global ~/.agents/skills",
|
|
796
|
-
"Only direct children count (no nested skill folders).",
|
|
797
|
-
"When names collide, the project's skill wins."
|
|
798
|
-
]
|
|
799
|
-
},
|
|
800
|
-
{
|
|
801
|
-
heading: "How it uses them",
|
|
802
|
-
lines: [
|
|
803
|
-
"The agent sees a catalog (name + description) and loads a skill when it matches.",
|
|
804
|
-
"Type / or @skill to run one. Changes apply immediately \u2014 no restart.",
|
|
805
|
-
"disable-model-invocation: true keeps it manual."
|
|
806
|
-
]
|
|
807
|
-
}
|
|
808
|
-
]
|
|
809
|
-
}
|
|
810
|
-
];
|
|
811
|
-
|
|
812
|
-
// src/review.ts
|
|
813
|
-
import { spawnSync } from "child_process";
|
|
814
|
-
import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
815
|
-
import { join as join2 } from "path";
|
|
816
|
-
var EXCERPT_CHARS = 1200;
|
|
817
|
-
function tildify(p) {
|
|
818
|
-
const home = userHome();
|
|
819
|
-
return p.startsWith(home) ? "~" + p.slice(home.length) : p;
|
|
820
|
-
}
|
|
821
|
-
function skillExcerpt(dir, chars) {
|
|
822
|
-
const file = join2(dir, "SKILL.md");
|
|
823
|
-
if (!existsSync2(file)) return { text: "(SKILL.md not found)", files: 0, size: 0 };
|
|
824
|
-
const content = readFileSync2(file, "utf-8").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
|
|
825
|
-
let files = 0;
|
|
826
|
-
try {
|
|
827
|
-
files = readdirSync2(dir, { recursive: true, withFileTypes: true }).filter((d) => d.isFile()).length;
|
|
828
|
-
} catch {
|
|
829
|
-
files = 1;
|
|
830
|
-
}
|
|
831
|
-
return { text: content.length > chars ? content.slice(0, chars) + "\n\u2026(truncated)" : content, files, size: content.length };
|
|
832
|
-
}
|
|
833
|
-
function buildReviewPrompt(skills, ctx) {
|
|
834
|
-
const many = skills.length > 1;
|
|
835
|
-
const excerptChars = many ? Math.max(300, Math.floor(12e3 / skills.length)) : EXCERPT_CHARS * 3;
|
|
836
|
-
const sections = skills.map((s, i) => {
|
|
837
|
-
const ex = skillExcerpt(s.path, excerptChars);
|
|
838
|
-
const usage = s.usesTotal === null ? "unknown (usage not loaded)" : s.usesTotal === 0 ? `not used in the last ${ctx.usageDays} days` : `${s.usesTotal} use${s.usesTotal === 1 ? "" : "s"} in the last ${ctx.usageDays} days${s.usesByProject.length ? ` (${s.usesByProject.map(([p, n]) => `${p}: ${n}`).join(", ")})` : ""}`;
|
|
839
|
-
return [
|
|
840
|
-
`### ${many ? `${i + 1}. ` : ""}${s.name}`,
|
|
841
|
-
`- Description: ${s.description || "(none)"}`,
|
|
842
|
-
`- Source: ${s.source}`,
|
|
843
|
-
`- Installed via: ${s.installedHow}`,
|
|
844
|
-
`- Location: ${tildify(s.path)}${s.links.length ? ` (also linked from ${s.links.map(tildify).join(", ")})` : ""}`,
|
|
845
|
-
`- Loaded by: ${s.loadedBy}`,
|
|
846
|
-
...s.vendor ? [`- Vendor-managed: ${s.vendor}`] : [],
|
|
847
|
-
...s.keptGlobal ? ["- I marked this as global on purpose: recommend changing that only for a strong reason (unused, duplicated, low quality)"] : [],
|
|
848
|
-
`- Usage (Claude Code transcripts only): ${usage}`,
|
|
849
|
-
`- Installed in projects: ${s.installedIn.length ? s.installedIn.join(", ") : "none"}`,
|
|
850
|
-
`- Other copies agents can see: ${s.otherCopies.length ? s.otherCopies.join("; ") : "none"}`,
|
|
851
|
-
`- In Your skills (skilllib library): ${s.inYourSkills ?? "no"}`,
|
|
852
|
-
`- Size: ${ex.files} file${ex.files === 1 ? "" : "s"}, SKILL.md ${ex.size} chars`,
|
|
853
|
-
"",
|
|
854
|
-
"SKILL.md body (excerpt):",
|
|
855
|
-
"```markdown",
|
|
856
|
-
ex.text.trim(),
|
|
857
|
-
"```"
|
|
858
|
-
].join("\n");
|
|
859
|
-
});
|
|
860
|
-
return `You are reviewing Claude Code / agent skills installed on my machine to decide which ones to keep. ${ctx.scope}
|
|
861
|
-
|
|
862
|
-
## Background
|
|
863
|
-
- A skill is a folder with a SKILL.md. Every loaded skill's name and description sit in the agent's context in every session, so each one costs context and can trigger when it shouldn't.
|
|
864
|
-
- "Global" skills load in EVERY repo for the listed agents. Project skills only load inside that repo.
|
|
865
|
-
- I use these agents: ${ctx.harnesses.join(", ") || "(none configured)"}.
|
|
866
|
-
- I manage skills with skilllib, which can: keep a skill global; move it into specific projects (and out of global); delete it (a backup is kept); or leave vendor skills alone. Vendor skills (plugins, claude.ai, Cursor built-ins) can only be turned off at their source.
|
|
867
|
-
- 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.
|
|
868
|
-
|
|
869
|
-
## My projects
|
|
870
|
-
${ctx.projects.map((p) => `- ${p.name} \u2014 ${tildify(p.path)}`).join("\n") || "(none)"}
|
|
871
|
-
|
|
872
|
-
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).
|
|
873
|
-
|
|
874
|
-
## Skills to review
|
|
875
|
-
${sections.join("\n\n")}
|
|
876
|
-
|
|
877
|
-
## What I want back
|
|
878
|
-
For each skill, recommend exactly one of:
|
|
879
|
-
- **keep global**: genuinely useful in almost every repo
|
|
880
|
-
- **move to projects**: only relevant to some; name them from my project list
|
|
881
|
-
- **delete**: unused, redundant, low quality, or superseded by another copy
|
|
882
|
-
- **vendor: turn off at source**: say how (e.g. /plugin in Claude Code)
|
|
883
|
-
|
|
884
|
-
Weigh usage, relevance to my projects' stacks, duplicates (prefer one copy), overlap with other skills, and quality/staleness of the SKILL.md. Be decisive; say when you're unsure.
|
|
885
|
-
|
|
886
|
-
Reply with a short table (skill \xB7 decision \xB7 projects \xB7 one-line reason), then this JSON so I can apply it:
|
|
887
|
-
|
|
888
|
-
\`\`\`json
|
|
889
|
-
[{ "skill": "name", "decision": "keep-global | move | delete | vendor-off", "projects": ["project-name"], "reason": "\u2026" }]
|
|
890
|
-
\`\`\`
|
|
891
|
-
`;
|
|
892
|
-
}
|
|
893
|
-
function copyText(text) {
|
|
894
|
-
const savedTo = join2(skilllibHome(), "review-prompt.md");
|
|
895
|
-
mkdirSync(skilllibHome(), { recursive: true });
|
|
896
|
-
writeFileSync(savedTo, text);
|
|
897
|
-
const tools = process.platform === "darwin" ? [["pbcopy", []]] : process.platform === "win32" ? [["clip", []]] : [
|
|
898
|
-
["wl-copy", []],
|
|
899
|
-
["xclip", ["-selection", "clipboard"]],
|
|
900
|
-
["xsel", ["--clipboard", "--input"]]
|
|
901
|
-
];
|
|
902
|
-
for (const [cmd, args] of tools) {
|
|
903
|
-
const r = spawnSync(cmd, args, { input: text, stdio: ["pipe", "ignore", "ignore"] });
|
|
904
|
-
if (r.status === 0) return { copied: true, savedTo };
|
|
905
|
-
}
|
|
906
|
-
return { copied: false, savedTo };
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
// src/tui/App.tsx
|
|
910
|
-
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
911
|
-
function sourceGroup(m) {
|
|
912
|
-
if (m.kind === "plugin") {
|
|
913
|
-
const synced = m.origin.includes("(claude.ai)");
|
|
914
|
-
const name = m.origin.split("@")[0].replace(" (claude.ai)", "");
|
|
915
|
-
return {
|
|
916
|
-
key: `plugin:${m.origin}`,
|
|
917
|
-
label: `${name} plugin`,
|
|
918
|
-
about: synced ? `A Claude Code plugin your claude.ai account syncs to this machine. Only Claude Code loads it, in every repo. Manage it in claude.ai or with /plugin.` : `A Claude Code plugin you installed (${m.origin}), enabled in ~/.claude/settings.json. Only Claude Code loads it, in every repo. Turn it off with /plugin.`
|
|
919
|
-
};
|
|
920
|
-
}
|
|
921
|
-
if (m.kind === "claude.ai")
|
|
922
|
-
return {
|
|
923
|
-
key: "claude.ai",
|
|
924
|
-
label: "claude.ai skills",
|
|
925
|
-
about: "Skills from your claude.ai account (Settings \u2192 Capabilities), synced to ~/.claude/skills/synced. Only Claude Code loads them, in every repo. Manage them on claude.ai."
|
|
926
|
-
};
|
|
927
|
-
if (m.kind === "built-in")
|
|
928
|
-
return {
|
|
929
|
-
key: `builtin:${m.origin}`,
|
|
930
|
-
label: `${m.origin} built-in`,
|
|
931
|
-
about: "Skills that ship with Cursor (~/.cursor/skills-cursor). Only Cursor loads them, in every repo. They update with Cursor and can't be removed."
|
|
932
|
-
};
|
|
933
|
-
if (m.kind === "skills.sh" && m.origin.includes("/"))
|
|
934
|
-
return {
|
|
935
|
-
key: `repo:${m.origin}`,
|
|
936
|
-
label: m.origin,
|
|
937
|
-
about: `Installed with \`npx skills add ${m.origin}\` into ~/.agents/skills, so they load in every repo.`
|
|
938
|
-
};
|
|
939
|
-
return prefixGroup(m.name);
|
|
940
|
-
}
|
|
941
|
-
function prefixGroup(name) {
|
|
942
|
-
const first = name.split("-")[0];
|
|
943
|
-
return first ? { key: `prefix:${first}`, label: `${first}*` } : void 0;
|
|
944
|
-
}
|
|
945
|
-
function commonPrefixLabel(names, fallback) {
|
|
946
|
-
const parts = names.map((n) => n.split("-"));
|
|
947
|
-
const shared = [];
|
|
948
|
-
for (let i = 0; parts.every((p) => p.length > i + 1 && p[i] === parts[0][i]); i++) shared.push(parts[0][i]);
|
|
949
|
-
return shared.length ? `${shared.join("-")}-*` : fallback;
|
|
950
|
-
}
|
|
951
|
-
function groupSection(section, expanded, toggle, usesOf) {
|
|
952
|
-
const counts = /* @__PURE__ */ new Map();
|
|
953
|
-
for (const i of section.items) if (i.group) counts.set(i.group.key, (counts.get(i.group.key) ?? 0) + 1);
|
|
954
|
-
const emitted = /* @__PURE__ */ new Set();
|
|
955
|
-
const items = [];
|
|
956
|
-
for (const item of section.items) {
|
|
957
|
-
const g = item.group;
|
|
958
|
-
if (!g || (counts.get(g.key) ?? 0) < 2) {
|
|
959
|
-
items.push(item);
|
|
960
|
-
continue;
|
|
961
|
-
}
|
|
962
|
-
if (emitted.has(g.key)) continue;
|
|
963
|
-
emitted.add(g.key);
|
|
964
|
-
const members = section.items.filter((i) => i.group?.key === g.key);
|
|
965
|
-
const key = `grp:${section.title}:${g.key}`;
|
|
966
|
-
const label = g.key.startsWith("prefix:") ? commonPrefixLabel(members.map((m) => m.skill?.name ?? ""), g.label) : g.label;
|
|
967
|
-
const open = expanded.has(key);
|
|
968
|
-
const run = () => toggle(key);
|
|
969
|
-
items.push({
|
|
970
|
-
key,
|
|
971
|
-
search: [label, ...members.map((m) => m.search)].join(" "),
|
|
972
|
-
tags: [],
|
|
973
|
-
row: {
|
|
974
|
-
key,
|
|
975
|
-
cells: [
|
|
976
|
-
{ text: open ? "\u25BE " : "\u25B8 ", color: color.accent },
|
|
977
|
-
{ text: label, grow: true, bold: true },
|
|
978
|
-
{ text: plural2(members.length, "skill").padStart(10), width: 10, color: color.muted },
|
|
979
|
-
{ text: usesOf(members.map((m) => m.skill?.name ?? "")).padStart(6), width: 6, color: color.muted }
|
|
980
|
-
]
|
|
981
|
-
},
|
|
982
|
-
primary: { label: open ? "collapse" : "expand", run },
|
|
983
|
-
actions: [{ label: open ? "Collapse" : `Show the ${members.length} skills`, run }, ...section.groupActions?.(members) ?? []],
|
|
984
|
-
preview: {
|
|
985
|
-
title: label,
|
|
986
|
-
meta: [plural2(members.length, "skill"), open ? "space collapses" : "space expands \xB7 enter for actions on all of them"],
|
|
987
|
-
description: g.about ? `${g.about} Skills: ${members.map((m) => m.skill?.name ?? m.key).join(", ")}` : members.map((m) => m.skill?.name ?? m.key).join(", ")
|
|
988
|
-
}
|
|
989
|
-
});
|
|
990
|
-
if (open) {
|
|
991
|
-
for (const m of members) items.push({ ...m, row: { ...m.row, cells: [{ text: " " }, ...m.row.cells] } });
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
return { ...section, items };
|
|
995
|
-
}
|
|
996
|
-
var PROJECT_TABS = [
|
|
997
|
-
{ id: "usable", label: "Usable here" },
|
|
998
|
-
{ id: "add", label: "Add skills" }
|
|
999
|
-
];
|
|
1000
|
-
var tildify2 = (p) => p === homedir() || p.startsWith(homedir() + sep) ? "~" + p.slice(homedir().length) : p;
|
|
1001
|
-
var plural2 = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1002
|
-
var ok = (text) => ({ text, ok: true });
|
|
1003
|
-
var warn = (text) => ({ text, ok: false });
|
|
1004
|
-
function describe(change, where) {
|
|
1005
|
-
if (change.action === "skipped") return warn(`${change.name}: ${change.reason}`);
|
|
1006
|
-
const versions = change.from && change.from !== change.to ? ` v${change.from} \u2192 v${change.to}` : change.to ? ` v${change.to}` : "";
|
|
1007
|
-
const verb = { installed: "added to", updated: "updated in", removed: "removed from" }[change.action];
|
|
1008
|
-
return ok(where ? `${change.name}${change.action === "removed" ? "" : versions} ${verb} ${where}` : `${change.name} ${change.action}`);
|
|
1009
|
-
}
|
|
1010
|
-
function openFolder(dir) {
|
|
1011
|
-
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
1012
|
-
spawn(cmd, [dir], { detached: true, stdio: "ignore" }).unref();
|
|
1013
|
-
}
|
|
1014
|
-
function matchScore(text, query) {
|
|
1015
|
-
if (!query) return 0;
|
|
1016
|
-
const t = text.toLowerCase();
|
|
1017
|
-
const q = query.toLowerCase();
|
|
1018
|
-
if (t.startsWith(q)) return 0;
|
|
1019
|
-
if (t.includes(q)) return 1;
|
|
1020
|
-
const initials = t.split(/[-_\s./]+/).filter(Boolean).map((w) => w[0]).join("");
|
|
1021
|
-
return q.length >= 2 && initials.startsWith(q) ? 2 : null;
|
|
1022
|
-
}
|
|
1023
|
-
function placeKey(place) {
|
|
1024
|
-
return place.kind === "project" ? `project:${place.root}` : place.kind;
|
|
1025
|
-
}
|
|
1026
|
-
function versionText(r) {
|
|
1027
|
-
if (r.state === "available") return r.latest ? `v${r.latest}` : "";
|
|
1028
|
-
if (!r.managed) return r.latest ? `lib v${r.latest}` : "";
|
|
1029
|
-
if (r.version && r.latest && r.latest > r.version) return `v${r.version} \u2192 v${r.latest}`;
|
|
1030
|
-
return r.version ? `v${r.version}` : "";
|
|
1031
|
-
}
|
|
1032
|
-
var harnessName = (id) => HARNESSES.find((h) => h.id === id)?.name ?? id;
|
|
1033
|
-
function harnessChips(visibility, installed) {
|
|
1034
|
-
return visibility.map((v) => {
|
|
1035
|
-
const h = HARNESSES.find((x) => x.id === v.id);
|
|
1036
|
-
const on = installed && v.paths > 0;
|
|
1037
|
-
return { text: ` ${h?.icon ?? "?"} `, color: on ? h?.color : color.faint, dim: !on };
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1040
|
-
var BACKUP_LABEL = {
|
|
1041
|
-
trash: "deleted from library",
|
|
1042
|
-
"global-backup": "unloaded from global",
|
|
1043
|
-
"tidy-backup": "duplicate replaced by a link",
|
|
1044
|
-
plugin: "plugin removed"
|
|
1045
|
-
};
|
|
1046
|
-
var GIT_LABEL = {
|
|
1047
|
-
committed: { text: "\u2713 committed", color: color.green },
|
|
1048
|
-
changed: { text: "\xB1 changed", color: color.yellow },
|
|
1049
|
-
new: { text: "+ not added", color: color.blue },
|
|
1050
|
-
ignored: { text: "\u2205 ignored", color: color.faint }
|
|
1051
|
-
};
|
|
1052
|
-
var GIT_ABOUT = {
|
|
1053
|
-
committed: "committed to git \u2014 teammates get it",
|
|
1054
|
-
changed: "committed, with uncommitted changes",
|
|
1055
|
-
new: "not committed yet (shows in git status)",
|
|
1056
|
-
ignored: "gitignored \u2014 only on this machine"
|
|
1057
|
-
};
|
|
1058
|
-
function gitCell(state, isRepo) {
|
|
1059
|
-
if (!isRepo) return [];
|
|
1060
|
-
if (!state) return [{ text: "", width: 12 }];
|
|
1061
|
-
const l = GIT_LABEL[state];
|
|
1062
|
-
return [{ text: ` ${l.text}`, width: 12, color: l.color }];
|
|
1063
|
-
}
|
|
1064
|
-
function HarnessLegend({ ids }) {
|
|
1065
|
-
return /* @__PURE__ */ jsx3(Text3, { children: ids.map((id) => {
|
|
1066
|
-
const h = HARNESSES.find((x) => x.id === id);
|
|
1067
|
-
return /* @__PURE__ */ jsxs3(Text3, { children: [
|
|
1068
|
-
/* @__PURE__ */ jsx3(Text3, { color: h.color, children: `${h.icon} ` }),
|
|
1069
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.muted, children: `${h.name} ` })
|
|
1070
|
-
] }, id);
|
|
1071
|
-
}) });
|
|
1072
|
-
}
|
|
1073
|
-
function wrapAligned(line, width) {
|
|
1074
|
-
if (line.length <= width) return [line];
|
|
1075
|
-
const gap = line.match(/^(\S.*?\s{2,})\S/);
|
|
1076
|
-
const indent = gap && gap[1].length < width / 2 ? gap[1].length : 0;
|
|
1077
|
-
const out = [];
|
|
1078
|
-
let rest = line;
|
|
1079
|
-
while (rest.length > width) {
|
|
1080
|
-
const cut = rest.lastIndexOf(" ", width);
|
|
1081
|
-
const at = cut > indent ? cut : width;
|
|
1082
|
-
out.push(rest.slice(0, at));
|
|
1083
|
-
rest = " ".repeat(indent) + rest.slice(at).trimStart();
|
|
1084
|
-
}
|
|
1085
|
-
out.push(rest);
|
|
1086
|
-
return out;
|
|
1087
|
-
}
|
|
1088
|
-
function hasLinks(root, name) {
|
|
1089
|
-
return ALL_PROJECT_DIRS.some((d) => {
|
|
1090
|
-
try {
|
|
1091
|
-
return lstatSync(join3(root, d, name)).isSymbolicLink();
|
|
1092
|
-
} catch {
|
|
1093
|
-
return false;
|
|
1094
|
-
}
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
function App() {
|
|
1098
|
-
const { exit, suspendTerminal } = useApp();
|
|
1099
|
-
const { columns, rows: termRows } = useWindowSize();
|
|
1100
|
-
const [snapshot, setSnapshot] = useState2(() => loadSnapshot());
|
|
1101
|
-
const [usage, setUsage] = useState2(null);
|
|
1102
|
-
const [setup, setSetup] = useState2(
|
|
1103
|
-
() => !snapshot.harnessesChosen || snapshot.roots.length === 0 ? { step: snapshot.harnessesChosen ? "folder" : "harnesses", selected: snapshot.harnesses, index: 0, value: "~/Projects" } : null
|
|
1104
|
-
);
|
|
1105
|
-
const [focus, setFocus] = useState2("left");
|
|
1106
|
-
const [selected, setSelected] = useState2(() => {
|
|
1107
|
-
const initial = {};
|
|
1108
|
-
if (snapshot.cwdProject) initial.left = `project:${snapshot.cwdProject}`;
|
|
1109
|
-
return initial;
|
|
1110
|
-
});
|
|
1111
|
-
const [search, setSearch] = useState2({ left: "", right: "" });
|
|
1112
|
-
const [projectTab, setProjectTab] = useState2("usable");
|
|
1113
|
-
const [expanded, setExpanded] = useState2(() => /* @__PURE__ */ new Set());
|
|
1114
|
-
const [wizard, setWizard] = useState2(null);
|
|
1115
|
-
const wizardInput = useRef(null);
|
|
1116
|
-
const gitCache = useRef(/* @__PURE__ */ new Map());
|
|
1117
|
-
const gitFor = (root) => {
|
|
1118
|
-
const hit = gitCache.current.get(root);
|
|
1119
|
-
if (hit && hit.snapshot === snapshot) return hit.info;
|
|
1120
|
-
const info = gitInfo(root);
|
|
1121
|
-
gitCache.current.set(root, { snapshot, info });
|
|
1122
|
-
return info;
|
|
1123
|
-
};
|
|
1124
|
-
const toggleGroup = (key) => setExpanded((prev) => {
|
|
1125
|
-
const next = new Set(prev);
|
|
1126
|
-
if (next.has(key)) next.delete(key);
|
|
1127
|
-
else next.add(key);
|
|
1128
|
-
return next;
|
|
1129
|
-
});
|
|
1130
|
-
const [overlay, setOverlay] = useState2(null);
|
|
1131
|
-
const [toast, setToast] = useState2(null);
|
|
1132
|
-
const [scanning, setScanning] = useState2(false);
|
|
1133
|
-
const reload = () => setSnapshot(loadSnapshot());
|
|
1134
|
-
const say = (text, good = true) => setToast({ text, ok: good });
|
|
1135
|
-
const act = (fn) => {
|
|
1136
|
-
const result = fn();
|
|
1137
|
-
reload();
|
|
1138
|
-
setToast(result);
|
|
1139
|
-
};
|
|
1140
|
-
const confirm = (message, onYes) => setOverlay({ type: "confirm", message, onYes });
|
|
1141
|
-
const rescan = (then) => {
|
|
1142
|
-
setScanning(true);
|
|
1143
|
-
setTimeout(() => {
|
|
1144
|
-
discoverProjects();
|
|
1145
|
-
reload();
|
|
1146
|
-
setScanning(false);
|
|
1147
|
-
then?.();
|
|
1148
|
-
}, 30);
|
|
1149
|
-
};
|
|
1150
|
-
useEffect(() => {
|
|
1151
|
-
if (snapshot.roots.length > 0) rescan();
|
|
1152
|
-
}, []);
|
|
1153
|
-
useEffect(() => {
|
|
1154
|
-
let live = true;
|
|
1155
|
-
void loadUsage(snapshot.projects).then((u) => live && setUsage(u));
|
|
1156
|
-
return () => {
|
|
1157
|
-
live = false;
|
|
1158
|
-
};
|
|
1159
|
-
}, [snapshot.projects.join("\n")]);
|
|
1160
|
-
const edit = async (file) => {
|
|
1161
|
-
const editor = process.env.VISUAL || process.env.EDITOR || (process.platform === "win32" ? "notepad" : "vi");
|
|
1162
|
-
await suspendTerminal(() => {
|
|
1163
|
-
if (process.platform === "win32") spawnSync2(`${editor} "${file}"`, { shell: true, stdio: "inherit" });
|
|
1164
|
-
else spawnSync2("/bin/sh", ["-c", `${editor} "$1"`, "sh", file], { stdio: "inherit" });
|
|
1165
|
-
});
|
|
1166
|
-
reload();
|
|
1167
|
-
say(`Saved ${tildify2(file)}`);
|
|
1168
|
-
};
|
|
1169
|
-
const usesOf = (skill, root) => usage ? (root ? usage.byProject.get(root)?.get(skill) : usage.total.get(skill)) ?? 0 : null;
|
|
1170
|
-
const usesText = (skill, root) => {
|
|
1171
|
-
const n = usesOf(skill, root);
|
|
1172
|
-
return n === null ? "\u2026" : n === 0 ? "\u2013" : String(n);
|
|
1173
|
-
};
|
|
1174
|
-
const libraryNames = new Set(snapshot.library.map((l) => l.name));
|
|
1175
|
-
const enabledIds = snapshot.harnesses;
|
|
1176
|
-
const issueCount = snapshot.issues.length;
|
|
1177
|
-
const problems = snapshot.issues.filter((i) => i.severity === "problem").length;
|
|
1178
|
-
const placeRow = (key, icon, label, hint, hintColor = color.muted) => ({
|
|
1179
|
-
key,
|
|
1180
|
-
cells: [
|
|
1181
|
-
{ text: `${icon} `, color: color.accent },
|
|
1182
|
-
{ text: label, grow: true, bold: true },
|
|
1183
|
-
{ text: hint, color: hintColor }
|
|
1184
|
-
]
|
|
1185
|
-
});
|
|
1186
|
-
const leftEntries = [
|
|
1187
|
-
{ key: "library", place: { kind: "library" }, row: placeRow("library", "\u25A4", "Your skills", `${snapshot.library.length}`), search: "your skills library" },
|
|
1188
|
-
{
|
|
1189
|
-
key: "global",
|
|
1190
|
-
place: { kind: "global" },
|
|
1191
|
-
row: placeRow("global", "\u25C8", "Global", `${snapshot.machine.filter((m) => !m.broken).length}`),
|
|
1192
|
-
search: "global"
|
|
1193
|
-
},
|
|
1194
|
-
{
|
|
1195
|
-
key: "health",
|
|
1196
|
-
place: { kind: "health" },
|
|
1197
|
-
row: placeRow("health", problems ? "\u25CF" : "\u2713", "Health", issueCount ? `${issueCount}` : "ok", problems ? color.red : color.green),
|
|
1198
|
-
search: "health"
|
|
1199
|
-
},
|
|
1200
|
-
...snapshot.projects.map((root) => {
|
|
1201
|
-
const rows = snapshot.rows.get(root) ?? [];
|
|
1202
|
-
const installed = rows.filter((r) => r.installed).length;
|
|
1203
|
-
const behind = rows.some((r) => r.state.includes("update") || r.state === "folder missing");
|
|
1204
|
-
return {
|
|
1205
|
-
key: `project:${root}`,
|
|
1206
|
-
place: { kind: "project", root },
|
|
1207
|
-
search: basename3(root),
|
|
1208
|
-
row: {
|
|
1209
|
-
key: `project:${root}`,
|
|
1210
|
-
cells: [
|
|
1211
|
-
{ text: root === snapshot.cwdProject ? "\u25C6 " : " ", color: color.accent },
|
|
1212
|
-
{ text: basename3(root), grow: true },
|
|
1213
|
-
{ text: installed ? String(installed).padStart(3) : " \u2013", color: installed ? color.muted : color.faint },
|
|
1214
|
-
{ text: behind ? " \u2191" : " ", color: color.yellow }
|
|
1215
|
-
]
|
|
1216
|
-
}
|
|
1217
|
-
};
|
|
1218
|
-
}),
|
|
1219
|
-
{
|
|
1220
|
-
key: "settings",
|
|
1221
|
-
place: { kind: "settings" },
|
|
1222
|
-
row: {
|
|
1223
|
-
key: "settings",
|
|
1224
|
-
cells: [
|
|
1225
|
-
{ text: "\u2699 ", color: color.accent },
|
|
1226
|
-
{ text: "Settings", grow: true, bold: true },
|
|
1227
|
-
...snapshot.harnesses.map((id) => {
|
|
1228
|
-
const h = HARNESSES.find((x) => x.id === id);
|
|
1229
|
-
return { text: ` ${h.icon}`, color: h.color };
|
|
1230
|
-
})
|
|
1231
|
-
]
|
|
1232
|
-
},
|
|
1233
|
-
search: "settings harnesses folders"
|
|
1234
|
-
},
|
|
1235
|
-
{
|
|
1236
|
-
key: "help",
|
|
1237
|
-
place: { kind: "help" },
|
|
1238
|
-
row: placeRow("help", "?", "Help", "where skills come from", color.faint),
|
|
1239
|
-
search: "help docs where skills come from"
|
|
1240
|
-
}
|
|
1241
|
-
];
|
|
1242
|
-
const leftVisible = leftEntries.filter((e) => matchScore(e.search, search.left) !== null);
|
|
1243
|
-
const leftRows = [];
|
|
1244
|
-
let lastGroup = "";
|
|
1245
|
-
for (const e of leftVisible) {
|
|
1246
|
-
const group = e.place.kind === "project" ? "Projects" : e.place.kind === "settings" || e.place.kind === "help" ? " " : "";
|
|
1247
|
-
if (group && group !== lastGroup) leftRows.push({ key: `h:${group}`, header: true, cells: [{ text: group.trim() }] });
|
|
1248
|
-
lastGroup = group;
|
|
1249
|
-
leftRows.push(e.row);
|
|
1250
|
-
}
|
|
1251
|
-
const leftKey = leftVisible.some((e) => e.key === selected.left) ? selected.left : leftVisible[0]?.key ?? "";
|
|
1252
|
-
const place = leftVisible.find((e) => e.key === leftKey)?.place ?? { kind: "library" };
|
|
1253
|
-
const pKey = placeKey(place);
|
|
1254
|
-
function projectSections(root) {
|
|
1255
|
-
const where = basename3(root);
|
|
1256
|
-
const git = gitFor(root);
|
|
1257
|
-
const gitOf = (path) => git ? git.of(relativeTo(root, path)) : null;
|
|
1258
|
-
const rows = snapshot.rows.get(root) ?? [];
|
|
1259
|
-
const projectActions = [
|
|
1260
|
-
{
|
|
1261
|
-
label: `Update every skill in ${where}`,
|
|
1262
|
-
run: () => act(() => ok(updateProject(root).map((c) => describe(c).text).join(" \xB7 ") || `${where} is up to date`))
|
|
1263
|
-
},
|
|
1264
|
-
{
|
|
1265
|
-
label: "Restore missing skills (sync)",
|
|
1266
|
-
run: () => act(() => ok(syncProject(root).map((c) => describe(c).text).join(" \xB7 ") || "Nothing missing"))
|
|
1267
|
-
},
|
|
1268
|
-
{ label: `Open ${where} folder`, run: () => openFolder(root) },
|
|
1269
|
-
{
|
|
1270
|
-
label: `Hide ${where} from the list`,
|
|
1271
|
-
run: () => confirm(`Hide ${where}? Bring it back with: skilllib unhide ${tildify2(root)}`, () => {
|
|
1272
|
-
setHidden(root, true);
|
|
1273
|
-
return ok(`${where} hidden`);
|
|
1274
|
-
})
|
|
1275
|
-
}
|
|
1276
|
-
];
|
|
1277
|
-
const install = (name, opts = {}) => {
|
|
1278
|
-
const change = addSkill(root, name, opts);
|
|
1279
|
-
reload();
|
|
1280
|
-
const result = describe(change, where);
|
|
1281
|
-
if (change.blocked?.length) {
|
|
1282
|
-
const who = visibilityOf(root, name).filter((v) => v.paths === 0).map((v) => harnessName(v.id));
|
|
1283
|
-
return confirm(
|
|
1284
|
-
`${result.text}. ${where} tracks ${change.blocked.join(", ")} in git \u2014 add a link there too so ${who.join(", ") || "other agents"} can use it? (remembered for ${where})`,
|
|
1285
|
-
() => {
|
|
1286
|
-
allowTrackedLinks(root);
|
|
1287
|
-
return describe(addSkill(root, name, { ...opts, allowTracked: true }), where);
|
|
1288
|
-
}
|
|
1289
|
-
);
|
|
1290
|
-
}
|
|
1291
|
-
setToast(result);
|
|
1292
|
-
};
|
|
1293
|
-
const versionsMenu = (name, current2) => ({
|
|
1294
|
-
label: "Install a specific version\u2026",
|
|
1295
|
-
run: () => setOverlay({
|
|
1296
|
-
type: "menu",
|
|
1297
|
-
title: `${name} versions`,
|
|
1298
|
-
index: 0,
|
|
1299
|
-
build: () => [...versionHistory(name)].reverse().map((v) => ({
|
|
1300
|
-
label: `v${v.version}${v.version === current2 ? " (installed)" : ""}`,
|
|
1301
|
-
hint: v.date.slice(0, 16).replace("T", " "),
|
|
1302
|
-
run: () => install(name, { version: v.version, force: true })
|
|
1303
|
-
}))
|
|
1304
|
-
})
|
|
1305
|
-
});
|
|
1306
|
-
const item = (r) => {
|
|
1307
|
-
const dir = r.path;
|
|
1308
|
-
const badge = stateBadge(r.state);
|
|
1309
|
-
const edited = r.state.startsWith("edited");
|
|
1310
|
-
const npx = r.state === "from npx skills";
|
|
1311
|
-
const inRepo = r.location === ".agents/skills" && !r.managed && !npx;
|
|
1312
|
-
const missing = r.visibility.filter((v) => v.paths === 0);
|
|
1313
|
-
const seenBy = r.visibility.filter((v) => v.paths > 0);
|
|
1314
|
-
const notInLibrary = r.state === "local only" || r.state === "untracked, differs from library" || r.state === "repo skill" || r.state === "repo skill, differs from library";
|
|
1315
|
-
const behind = r.state.includes("update");
|
|
1316
|
-
const key = `${r.location}:${r.name}`;
|
|
1317
|
-
const globalCopy = snapshot.machine.find((m) => m.name === r.name && !m.broken && m.harnesses.length > 0);
|
|
1318
|
-
const saveToLibrary = () => act(() => {
|
|
1319
|
-
const imported = importSkill(dir, { force: true });
|
|
1320
|
-
const change = addSkill(root, r.name);
|
|
1321
|
-
return ok(`${r.name} ${imported.status === "added" ? "added to" : "saved to"} the library as v${change.to}; ${where} now tracks it`);
|
|
1322
|
-
});
|
|
1323
|
-
const copyRepoSkillToLibrary = () => act(() => {
|
|
1324
|
-
const imported = importSkill(dir, { force: true });
|
|
1325
|
-
return ok(
|
|
1326
|
-
imported.status === "unchanged" ? `${r.name} is already in the library` : `${r.name} ${imported.status === "added" ? "copied into" : "updated in"} the library; this repo keeps its copy`
|
|
1327
|
-
);
|
|
1328
|
-
});
|
|
1329
|
-
const makeVisible = (allowTracked = false) => {
|
|
1330
|
-
const res = r.managed ? relinkDependency(root, r.name, { allowTracked }) : linkEverywhere(root, r.name, r.location, { allowTracked });
|
|
1331
|
-
if (res.blocked.length && !allowTracked) {
|
|
1332
|
-
return confirm(`${where} tracks ${res.blocked.join(", ")} in git. Add links there anyway? (remembered for ${where})`, () => {
|
|
1333
|
-
allowTrackedLinks(root);
|
|
1334
|
-
const again = r.managed ? relinkDependency(root, r.name, { allowTracked: true }) : linkEverywhere(root, r.name, r.location, { allowTracked: true });
|
|
1335
|
-
return ok(`${r.name}: linked into ${again.created.join(", ") || "nothing new"}`);
|
|
1336
|
-
});
|
|
1337
|
-
}
|
|
1338
|
-
reload();
|
|
1339
|
-
say(res.created.length ? `${r.name}: linked into ${res.created.join(", ")}` : `${r.name} is already visible to all your harnesses`);
|
|
1340
|
-
};
|
|
1341
|
-
const removeLinks = () => act(() => {
|
|
1342
|
-
const removed = unlinkEverywhere(root, r.name);
|
|
1343
|
-
return ok(removed.length ? `${r.name}: removed links in ${removed.join(", ")}` : `${r.name} has no links`);
|
|
1344
|
-
});
|
|
1345
|
-
const remove = () => edited ? confirm(`${r.name} has local edits in ${where}. Remove it and lose them?`, () => describe(removeSkill(root, r.name, { force: true }), where)) : act(() => describe(removeSkill(root, r.name), where));
|
|
1346
|
-
let primary;
|
|
1347
|
-
const actions = [];
|
|
1348
|
-
const visibilityActions = [
|
|
1349
|
-
...r.installed && missing.length ? [{ label: `Make visible to ${missing.map((v) => harnessName(v.id)).join(", ")}`, hint: "adds links; one real copy", run: () => makeVisible() }] : [],
|
|
1350
|
-
...r.installed && !r.managed && r.visibility.some((v) => v.paths > 0) && hasLinks(root, r.name) ? [{ label: "Remove links", hint: "keeps the real folder", run: removeLinks }] : []
|
|
1351
|
-
];
|
|
1352
|
-
if (npx) {
|
|
1353
|
-
primary = { label: "copy to your skills", run: copyRepoSkillToLibrary };
|
|
1354
|
-
actions.push({ label: "Copy to Your skills", hint: "so other repos can use it; npx skills keeps this copy", run: copyRepoSkillToLibrary });
|
|
1355
|
-
} else if (inRepo) {
|
|
1356
|
-
const libraryAction = r.state === "repo skill" ? { label: "Copy to Your skills", hint: "so other repos can use it; this repo keeps its copy", run: copyRepoSkillToLibrary } : r.state === "repo skill, differs from library" ? { label: "Update Your skills from this repo's copy", hint: "becomes a new version", run: copyRepoSkillToLibrary } : { label: "Already in Your skills", hint: `v${r.latest}`, run: () => say(`${r.name} matches library v${r.latest}`) };
|
|
1357
|
-
primary = r.state === "repo skill, in library" ? missing.length ? { label: "make visible to all", run: () => makeVisible() } : void 0 : { label: "copy to your skills", run: copyRepoSkillToLibrary };
|
|
1358
|
-
actions.push(libraryAction, ...visibilityActions);
|
|
1359
|
-
} else if (notInLibrary) {
|
|
1360
|
-
primary = { label: "copy to your skills", run: saveToLibrary };
|
|
1361
|
-
actions.push({ label: "Copy to Your skills", hint: "so other repos can use it; this repo then tracks it", run: saveToLibrary });
|
|
1362
|
-
if (r.state === "untracked, differs from library") {
|
|
1363
|
-
actions.push({
|
|
1364
|
-
label: "Replace with the library version",
|
|
1365
|
-
run: () => confirm(`Replace ${r.name} with library v${r.latest}? This project's copy is lost.`, () => describe(addSkill(root, r.name, { force: true }), where))
|
|
1366
|
-
});
|
|
1367
|
-
}
|
|
1368
|
-
} else if (!r.installed && globalCopy) {
|
|
1369
|
-
const scope = () => confirm(
|
|
1370
|
-
`Use ${r.name} only where you add it? It's added to ${where} and stops loading globally (other projects lose it until you add it there). Restorable from Health.`,
|
|
1371
|
-
() => {
|
|
1372
|
-
const added = addSkill(root, r.name);
|
|
1373
|
-
if (added.action === "skipped") return warn(`${r.name}: ${added.reason}`);
|
|
1374
|
-
const res = unloadGlobal(globalCopy.path, globalCopy.links);
|
|
1375
|
-
return res.ok ? ok(`${r.name} now lives in ${where} (v${added.to}) and no longer loads everywhere`) : warn(`${r.name}: ${res.reason}`);
|
|
1376
|
-
}
|
|
1377
|
-
);
|
|
1378
|
-
const addAnyway = () => confirm(`${r.name} already loads in every project. Add a project copy too? Agents will see it twice.`, () => {
|
|
1379
|
-
const added = addSkill(root, r.name);
|
|
1380
|
-
return describe(added, where);
|
|
1381
|
-
});
|
|
1382
|
-
primary = { label: "add (already global)", run: addAnyway };
|
|
1383
|
-
actions.push(
|
|
1384
|
-
...globalCopy.movable ? [{ label: `Use only in ${where}`, hint: "add here, stop loading everywhere", run: scope }] : [],
|
|
1385
|
-
{ label: `Add to ${where} anyway`, hint: "it'll load twice", run: addAnyway }
|
|
1386
|
-
);
|
|
1387
|
-
} else if (!r.installed) {
|
|
1388
|
-
primary = { label: "add", run: () => install(r.name) };
|
|
1389
|
-
actions.push({ label: `Add to ${where}`, hint: r.latest ? `v${r.latest}` : "", run: primary.run }, versionsMenu(r.name, null));
|
|
1390
|
-
} else if (!r.managed) {
|
|
1391
|
-
primary = { label: "track", run: () => install(r.name) };
|
|
1392
|
-
actions.push({ label: "Track it as a dependency", hint: "same as the library copy", run: primary.run });
|
|
1393
|
-
} else {
|
|
1394
|
-
primary = { label: "remove", run: remove };
|
|
1395
|
-
if (behind) {
|
|
1396
|
-
actions.push({ label: `Update to v${r.latest}`, run: () => install(r.name, { force: edited }) });
|
|
1397
|
-
}
|
|
1398
|
-
if (r.state === "folder missing") {
|
|
1399
|
-
actions.push({
|
|
1400
|
-
label: "Restore it",
|
|
1401
|
-
run: () => install(r.name, { version: r.version ?? void 0, force: true })
|
|
1402
|
-
});
|
|
1403
|
-
}
|
|
1404
|
-
if (edited) {
|
|
1405
|
-
actions.push({ label: "Save local edits to Your skills", hint: "becomes a new version", run: saveToLibrary });
|
|
1406
|
-
actions.push({
|
|
1407
|
-
label: `Revert to v${r.version}`,
|
|
1408
|
-
run: () => confirm(`Discard local edits to ${r.name}?`, () => describe(addSkill(root, r.name, { version: r.version ?? void 0, force: true }), where))
|
|
1409
|
-
});
|
|
1410
|
-
}
|
|
1411
|
-
actions.push(versionsMenu(r.name, r.version), { label: `Remove from ${where}`, run: remove });
|
|
1412
|
-
}
|
|
1413
|
-
if (!inRepo) actions.push(...visibilityActions);
|
|
1414
|
-
actions.push(
|
|
1415
|
-
{ label: "Edit SKILL.md", run: () => void edit(join3(dir, "SKILL.md")) },
|
|
1416
|
-
{ label: "Open folder", run: () => openFolder(dir) },
|
|
1417
|
-
...projectActions
|
|
1418
|
-
);
|
|
1419
|
-
const origin = snapshot.library.find((l) => l.name === r.name)?.origin;
|
|
1420
|
-
const whereLabel = r.installed ? r.location.replace("/skills", "") : "";
|
|
1421
|
-
return {
|
|
1422
|
-
key,
|
|
1423
|
-
search: r.name,
|
|
1424
|
-
group: npx ? { key: `repo:${r.source}`, label: r.source, about: `Installed with \`npx skills add ${r.source}\` into this repo (recorded in skills-lock.json).` } : prefixGroup(r.name),
|
|
1425
|
-
skill: { name: r.name, path: dir },
|
|
1426
|
-
tags: [
|
|
1427
|
-
r.installed ? "in this project" : "installable",
|
|
1428
|
-
...r.installed && r.state !== "ok" && r.state !== "repo skill, in library" && !npx ? ["needs attention"] : []
|
|
1429
|
-
],
|
|
1430
|
-
row: {
|
|
1431
|
-
key,
|
|
1432
|
-
cells: [
|
|
1433
|
-
{
|
|
1434
|
-
text: !r.installed ? "\u25CB " : missing.length === 0 ? "\u25C9 " : "\u25CC ",
|
|
1435
|
-
color: !r.installed ? color.faint : missing.length === 0 ? color.green : color.blue
|
|
1436
|
-
},
|
|
1437
|
-
{ text: r.name, grow: true, color: r.installed ? void 0 : color.muted },
|
|
1438
|
-
{ text: whereLabel.padStart(8), width: 8, color: color.faint },
|
|
1439
|
-
...gitCell(r.installed ? gitOf(r.path) : null, !!git),
|
|
1440
|
-
...harnessChips(r.visibility.length ? r.visibility : enabledIds.map((id) => ({ id, paths: 0 })), r.installed),
|
|
1441
|
-
{ text: versionText(r).padStart(8), width: 8, color: behind ? color.yellow : color.muted },
|
|
1442
|
-
{
|
|
1443
|
-
text: r.installed && dupes(r.name) > 1 ? ` \u29C9 ${dupes(r.name)} copies` : !r.installed && globalCopy ? " \u2197 already global" : ` ${badge.icon} ${npx ? r.source : badge.label}`,
|
|
1444
|
-
width: 19,
|
|
1445
|
-
color: r.installed && dupes(r.name) > 1 ? color.red : !r.installed && globalCopy ? color.yellow : badge.color
|
|
1446
|
-
},
|
|
1447
|
-
{ text: usesText(r.name, root).padStart(4), color: color.muted }
|
|
1448
|
-
]
|
|
1449
|
-
},
|
|
1450
|
-
primary,
|
|
1451
|
-
actions,
|
|
1452
|
-
preview: {
|
|
1453
|
-
title: r.name,
|
|
1454
|
-
dir,
|
|
1455
|
-
meta: [
|
|
1456
|
-
r.installed ? `${seenBy.length ? `loaded by ${seenBy.map((v) => `${harnessName(v.id)}${v.paths > 1 ? ` (via ${v.paths} folders)` : ""}`).join(", ")}` : "not loaded by your harnesses"}${missing.length ? ` \xB7 not ${missing.map((v) => harnessName(v.id)).join(", ")}` : ""}` : "not installed",
|
|
1457
|
-
npx ? `installed by npx skills from ${r.source} into ${r.location}` : inRepo ? `committed in ${r.location}` : r.installed ? r.managed ? `v${r.version} installed in ${r.location}` : notInLibrary ? "only in this project" : "untracked copy" : "not installed",
|
|
1458
|
-
...r.installed && git ? [GIT_ABOUT[gitOf(r.path)]] : [],
|
|
1459
|
-
r.inLibrary ? `in Your skills (v${r.latest}${origin ? `, from ${origin}` : ""})` : "not in Your skills",
|
|
1460
|
-
`${usesText(r.name, root)} Claude Code uses here (${USAGE_DAYS}d)`
|
|
1461
|
-
],
|
|
1462
|
-
...r.installed && dupes(r.name) > 1 ? { description: `Reaches your agents from ${dupes(r.name)} places: ${copiesOf(r.name).join(" \xB7 ")}. Health has the fix (your skill wins over plugins).` } : {}
|
|
1463
|
-
}
|
|
1464
|
-
};
|
|
1465
|
-
};
|
|
1466
|
-
const usableGlobals = snapshot.machine.filter((m) => !m.broken && m.harnesses.length > 0);
|
|
1467
|
-
const copiesOf = (name) => [
|
|
1468
|
-
...rows.filter((r) => r.installed && r.name === name).map((r) => `this repo (${r.location})`),
|
|
1469
|
-
...usableGlobals.filter((m) => m.name === name).map((m) => m.kind === "plugin" ? `plugin ${m.origin}` : m.kind === "claude.ai" ? "claude.ai" : `global ${tildify2(m.path)}`)
|
|
1470
|
-
];
|
|
1471
|
-
const dupes = (name) => copiesOf(name).length;
|
|
1472
|
-
const globalItem = (m) => {
|
|
1473
|
-
const inLib = libraryNames.has(m.name);
|
|
1474
|
-
const kept = m.movable && snapshot.keptGlobal.has(m.name);
|
|
1475
|
-
const alsoLocal = rows.some((r) => r.installed && r.name === m.name);
|
|
1476
|
-
const moveHere = () => confirm(
|
|
1477
|
-
`Use ${m.name} only where you add it? It's ${inLib ? "" : "imported into the library, "}added to ${where}, and stops loading globally \u2014 other projects lose it until you add it there. Restorable from Health.`,
|
|
1478
|
-
() => {
|
|
1479
|
-
if (!inLib) importSkill(m.path);
|
|
1480
|
-
const added = addSkill(root, m.name);
|
|
1481
|
-
if (added.action === "skipped") return warn(`${m.name}: ${added.reason}`);
|
|
1482
|
-
const r = unloadGlobal(m.path, m.links);
|
|
1483
|
-
return r.ok ? ok(`${m.name} now lives in ${where} (v${added.to}) and no longer loads everywhere`) : warn(`${m.name}: ${r.reason}`);
|
|
1484
|
-
}
|
|
1485
|
-
);
|
|
1486
|
-
const actions = m.movable ? [
|
|
1487
|
-
{ label: "Choose which repos keep it\u2026", hint: "cleanup wizard", run: () => setWizard({ initialSkill: m.name }) },
|
|
1488
|
-
keepAction(m),
|
|
1489
|
-
{ label: `Use only in ${where}`, hint: "add here, stop loading everywhere", run: moveHere },
|
|
1490
|
-
{ label: "Delete", hint: "not kept in Your skills \xB7 restorable from Health", run: () => confirmDeleteGlobal([m]) },
|
|
1491
|
-
...inLib ? [] : [
|
|
1492
|
-
{
|
|
1493
|
-
label: "Import into the library",
|
|
1494
|
-
hint: "keeps it global too",
|
|
1495
|
-
run: () => act(() => ok(`${m.name} ${importSkill(m.path).status === "added" ? "imported into the library" : "already in the library"}`))
|
|
1496
|
-
}
|
|
1497
|
-
],
|
|
1498
|
-
{ label: "Open folder", run: () => openFolder(m.path) }
|
|
1499
|
-
] : [{ label: `Comes from ${m.origin} \u2014 manage it there`, run: () => say(`${m.name} is managed by ${m.origin}`, false) }];
|
|
1500
|
-
actions.push({ label: "Copy review prompt", hint: "an agent decides keep / move / delete", run: () => copyReviewPrompt([reviewOfGlobal(m)], "This is one global skill.") });
|
|
1501
|
-
return {
|
|
1502
|
-
key: `global:${m.path}`,
|
|
1503
|
-
search: m.name,
|
|
1504
|
-
group: sourceGroup(m),
|
|
1505
|
-
skill: { name: m.name, path: m.path },
|
|
1506
|
-
tags: ["global", ...alsoLocal ? ["needs attention"] : []],
|
|
1507
|
-
row: {
|
|
1508
|
-
key: `global:${m.path}`,
|
|
1509
|
-
cells: [
|
|
1510
|
-
{ text: "\u25C8 ", color: color.accentDim },
|
|
1511
|
-
{ text: m.name, grow: true },
|
|
1512
|
-
{ text: (m.kind === "global" || m.kind === "skills.sh" ? "global" : m.kind).padStart(8), width: 8, color: color.faint },
|
|
1513
|
-
...gitCell(null, !!git),
|
|
1514
|
-
...harnessChips(enabledIds.map((id) => ({ id, paths: m.harnesses.includes(id) ? 1 : 0 })), true),
|
|
1515
|
-
{ text: "", width: 8 },
|
|
1516
|
-
{
|
|
1517
|
-
text: ` ${dupes(m.name) > 1 ? `\u29C9 ${dupes(m.name)} copies` : kept ? "\u2713 global" : m.movable ? "\u26A0 loaded globally" : "vendor"}`,
|
|
1518
|
-
width: 19,
|
|
1519
|
-
color: dupes(m.name) > 1 ? color.red : kept ? color.green : m.movable ? color.yellow : color.faint
|
|
1520
|
-
},
|
|
1521
|
-
{ text: usesText(m.name, root).padStart(4), color: color.muted }
|
|
1522
|
-
]
|
|
1523
|
-
},
|
|
1524
|
-
primary: m.movable ? { label: "choose repos\u2026", run: () => setWizard({ initialSkill: m.name }) } : void 0,
|
|
1525
|
-
actions,
|
|
1526
|
-
preview: {
|
|
1527
|
-
title: m.name,
|
|
1528
|
-
dir: m.path,
|
|
1529
|
-
meta: [
|
|
1530
|
-
`loaded in every project by ${m.harnesses.map(harnessName).join(", ")}`,
|
|
1531
|
-
`${m.kind}: ${m.origin}`,
|
|
1532
|
-
kept ? "yours: kept global on purpose" : m.movable ? "yours: the cleanup wizard can scope it to projects" : "managed by the vendor"
|
|
1533
|
-
],
|
|
1534
|
-
description: dupes(m.name) > 1 ? `Reaches your agents from ${dupes(m.name)} places: ${copiesOf(m.name).join(" \xB7 ")}. Health has the fix (your skill wins over plugins).` : sourceGroup(m)?.about
|
|
1535
|
-
}
|
|
1536
|
-
};
|
|
1537
|
-
};
|
|
1538
|
-
const local = rows.filter((r) => r.installed);
|
|
1539
|
-
const installable = rows.filter((r) => !r.installed);
|
|
1540
|
-
const globals = snapshot.machine.filter((m) => !m.broken && m.harnesses.length > 0);
|
|
1541
|
-
const partial = local.filter((r) => r.state !== "folder missing" && r.visibility.some((v) => v.paths === 0));
|
|
1542
|
-
const missingAgents = [...new Set(partial.flatMap((r) => r.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
|
|
1543
|
-
const runLinkAll = (allowTracked = false) => {
|
|
1544
|
-
const res = linkAll(root, { allowTracked });
|
|
1545
|
-
if (res.blocked.length && !allowTracked) {
|
|
1546
|
-
return confirm(
|
|
1547
|
-
`${where} tracks ${res.blocked.join(", ")} in git. Add links there too? (remembered for ${where})${res.linked.length ? ` Already linked ${plural2(res.linked.length, "skill")}.` : ""}`,
|
|
1548
|
-
() => {
|
|
1549
|
-
allowTrackedLinks(root);
|
|
1550
|
-
const again = linkAll(root, { allowTracked: true });
|
|
1551
|
-
return ok(`Linked ${plural2(res.linked.length + again.linked.length, "skill")}; every skill here is usable by all your agents`);
|
|
1552
|
-
}
|
|
1553
|
-
);
|
|
1554
|
-
}
|
|
1555
|
-
reload();
|
|
1556
|
-
say(res.linked.length ? `Linked ${plural2(res.linked.length, "skill")} for ${missingAgents.map(harnessName).join(", ")}` : "Every skill here is already usable by all your agents");
|
|
1557
|
-
};
|
|
1558
|
-
const linkAllItem = partial.length ? [
|
|
1559
|
-
{
|
|
1560
|
-
key: "__link-all",
|
|
1561
|
-
search: "make all usable link",
|
|
1562
|
-
tags: ["in this project", "needs attention"],
|
|
1563
|
-
row: {
|
|
1564
|
-
key: "__link-all",
|
|
1565
|
-
cells: [
|
|
1566
|
-
{ text: "\u21C4 ", color: color.accent },
|
|
1567
|
-
{
|
|
1568
|
-
text: `Make all ${partial.length} skills here usable by ${missingAgents.map(harnessName).join(", ")}`,
|
|
1569
|
-
grow: true,
|
|
1570
|
-
color: color.accent
|
|
1571
|
-
}
|
|
1572
|
-
]
|
|
1573
|
-
},
|
|
1574
|
-
primary: { label: "link them all", run: () => runLinkAll() },
|
|
1575
|
-
actions: [{ label: "Link every skill for every agent you use", hint: "adds links only; copies and moves nothing", run: () => runLinkAll() }],
|
|
1576
|
-
preview: {
|
|
1577
|
-
title: "Make every skill usable by all your agents",
|
|
1578
|
-
meta: [`${plural2(partial.length, "skill")} missing for ${missingAgents.map(harnessName).join(", ")}`],
|
|
1579
|
-
description: `Adds relative symlinks so each agent's folder reaches the one real copy (e.g. .claude/skills/<name> \u2192 .agents/skills/<name>). The repo's files aren't changed; folders git tracks need your OK first. Cursor lists a skill it reaches through several folders once.`
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
] : [];
|
|
1583
|
-
const fromLibrary = local.filter((r) => r.managed || r.state === "untracked copy of library skill");
|
|
1584
|
-
const fromNpx = local.filter((r) => r.state === "from npx skills");
|
|
1585
|
-
const repoOwn = local.filter((r) => !fromLibrary.includes(r) && !fromNpx.includes(r));
|
|
1586
|
-
const copyAllToLibrary = (members) => [
|
|
1587
|
-
{
|
|
1588
|
-
label: `Copy all ${members.length} into your library`,
|
|
1589
|
-
hint: "the repo keeps its copies",
|
|
1590
|
-
run: () => act(() => {
|
|
1591
|
-
const added = members.map((m) => importSkill(m.skill.path, { force: true })).filter((r) => r.status !== "unchanged").length;
|
|
1592
|
-
return ok(`${plural2(added, "skill")} copied into your library`);
|
|
1593
|
-
})
|
|
1594
|
-
}
|
|
1595
|
-
];
|
|
1596
|
-
const yours = globals.filter((m) => m.movable);
|
|
1597
|
-
const keptYours = yours.filter((m) => snapshot.keptGlobal.has(m.name));
|
|
1598
|
-
const unreviewed = yours.filter((m) => !snapshot.keptGlobal.has(m.name));
|
|
1599
|
-
const vendor = globals.filter((m) => !m.movable);
|
|
1600
|
-
const cleanupItem = unreviewed.length ? [
|
|
1601
|
-
{
|
|
1602
|
-
key: "__cleanup",
|
|
1603
|
-
search: "clean up global",
|
|
1604
|
-
tags: [],
|
|
1605
|
-
row: {
|
|
1606
|
-
key: "__cleanup",
|
|
1607
|
-
cells: [
|
|
1608
|
-
{ text: "\u26A0 ", color: color.yellow },
|
|
1609
|
-
{ text: `${plural2(unreviewed.length, "skill")} of yours load in every repo \u2014 choose where each should live\u2026`, grow: true, color: color.yellow }
|
|
1610
|
-
]
|
|
1611
|
-
},
|
|
1612
|
-
primary: { label: "clean up", run: () => setWizard({}) },
|
|
1613
|
-
actions: [{ label: "Open the cleanup wizard", run: () => setWizard({}) }],
|
|
1614
|
-
preview: {
|
|
1615
|
-
title: "Clean up global skills",
|
|
1616
|
-
meta: ["every agent reads global skills in every repo"],
|
|
1617
|
-
description: "Pick which projects keep each of your global skills. The wizard installs them there and stops loading them everywhere else (originals kept in backup)."
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
] : [];
|
|
1621
|
-
return {
|
|
1622
|
-
usable: [
|
|
1623
|
-
{ title: "", items: linkAllItem },
|
|
1624
|
-
{
|
|
1625
|
-
title: `From your skills (${fromLibrary.length})`,
|
|
1626
|
-
items: fromLibrary.map(item),
|
|
1627
|
-
groupActions: (members) => [
|
|
1628
|
-
{
|
|
1629
|
-
label: `Remove all ${members.length} from ${where}`,
|
|
1630
|
-
hint: "edited ones are kept",
|
|
1631
|
-
run: () => confirm(`Remove ${plural2(members.length, "skill")} from ${where}? Your library keeps them.`, () => {
|
|
1632
|
-
const done = members.map((m) => removeSkill(root, m.skill.name)).filter((c) => c.action === "removed").length;
|
|
1633
|
-
return ok(`Removed ${plural2(done, "skill")} from ${where}`);
|
|
1634
|
-
})
|
|
1635
|
-
}
|
|
1636
|
-
]
|
|
1637
|
-
},
|
|
1638
|
-
{ title: `From npx skills (${fromNpx.length})`, items: fromNpx.map(item), groupActions: copyAllToLibrary },
|
|
1639
|
-
{ title: `The repo's own (${repoOwn.length})`, items: repoOwn.map(item), groupActions: copyAllToLibrary },
|
|
1640
|
-
{
|
|
1641
|
-
title: `\u26A0 Global \xB7 yours, not reviewed, loaded in every repo (${unreviewed.length})`,
|
|
1642
|
-
items: [...cleanupItem, ...unreviewed.map(globalItem)],
|
|
1643
|
-
groupActions: (members) => [...keepGroupActions(members), ...yourGlobalActions(members)]
|
|
1644
|
-
},
|
|
1645
|
-
{
|
|
1646
|
-
title: `\u2713 Global \xB7 yours, on purpose (${keptYours.length})`,
|
|
1647
|
-
items: keptYours.map(globalItem),
|
|
1648
|
-
groupActions: (members) => [...keepGroupActions(members), ...yourGlobalActions(members)]
|
|
1649
|
-
},
|
|
1650
|
-
{
|
|
1651
|
-
title: `Global \xB7 from vendors: plugins, claude.ai, built-in (${vendor.length})`,
|
|
1652
|
-
items: vendor.map(globalItem),
|
|
1653
|
-
groupActions: (members) => [{
|
|
1654
|
-
label: `Copy review prompt for all ${members.length}`,
|
|
1655
|
-
hint: "for an agent to decide keep / move / delete",
|
|
1656
|
-
run: () => copyReviewPrompt(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)).map(reviewOfGlobal), `These are global skills (group ${members[0]?.group?.label ?? ""}).`)
|
|
1657
|
-
}]
|
|
1658
|
-
}
|
|
1659
|
-
],
|
|
1660
|
-
add: [
|
|
1661
|
-
{
|
|
1662
|
-
title: "",
|
|
1663
|
-
items: installable.map(item),
|
|
1664
|
-
groupActions: (members) => [
|
|
1665
|
-
{
|
|
1666
|
-
label: `Add all ${members.length} to ${where}`,
|
|
1667
|
-
hint: members.some((m) => snapshot.machine.some((g) => g.name === m.skill?.name && !g.broken)) ? "some already load globally" : "",
|
|
1668
|
-
run: () => act(() => {
|
|
1669
|
-
const added = members.map((m) => addSkill(root, m.skill.name)).filter((c) => c.action !== "skipped").length;
|
|
1670
|
-
return ok(`Added ${plural2(added, "skill")} to ${where}`);
|
|
1671
|
-
})
|
|
1672
|
-
}
|
|
1673
|
-
]
|
|
1674
|
-
}
|
|
1675
|
-
]
|
|
1676
|
-
};
|
|
1677
|
-
}
|
|
1678
|
-
function librarySections() {
|
|
1679
|
-
const newSkill = {
|
|
1680
|
-
key: "__new",
|
|
1681
|
-
search: "new skill create",
|
|
1682
|
-
tags: [],
|
|
1683
|
-
row: { key: "__new", cells: [{ text: "+ ", color: color.accent }, { text: "New skill\u2026", grow: true, color: color.accent }] },
|
|
1684
|
-
primary: { label: "create", run: () => promptNewSkill() },
|
|
1685
|
-
actions: [{ label: "Create a new skill", run: () => promptNewSkill() }],
|
|
1686
|
-
preview: { title: "New skill", meta: ["creates ~/.skilllib/library/<name>/SKILL.md and opens it in your editor"] }
|
|
1687
|
-
};
|
|
1688
|
-
const items = snapshot.library.map((l) => {
|
|
1689
|
-
const toggleProjects = {
|
|
1690
|
-
label: "Add to / remove from projects\u2026",
|
|
1691
|
-
run: () => setOverlay({
|
|
1692
|
-
type: "menu",
|
|
1693
|
-
title: `${l.name} \xB7 space toggles a project`,
|
|
1694
|
-
index: 0,
|
|
1695
|
-
build: () => {
|
|
1696
|
-
const fresh = loadSnapshot();
|
|
1697
|
-
const now = fresh.library.find((x) => x.name === l.name)?.projects ?? [];
|
|
1698
|
-
return fresh.projects.map((root) => {
|
|
1699
|
-
const on = now.includes(root);
|
|
1700
|
-
return {
|
|
1701
|
-
label: `${on ? "\u25C9" : "\u25CB"} ${basename3(root)}`,
|
|
1702
|
-
hint: tildify2(root),
|
|
1703
|
-
keepOpen: true,
|
|
1704
|
-
run: () => act(() => describe(on ? removeSkill(root, l.name) : addSkill(root, l.name), basename3(root)))
|
|
1705
|
-
};
|
|
1706
|
-
});
|
|
1707
|
-
}
|
|
1708
|
-
})
|
|
1709
|
-
};
|
|
1710
|
-
return {
|
|
1711
|
-
key: l.name,
|
|
1712
|
-
search: `${l.name} ${l.description}`,
|
|
1713
|
-
group: prefixGroup(l.name),
|
|
1714
|
-
skill: { name: l.name, path: librarySkillDir(l.name) },
|
|
1715
|
-
tags: [l.projects.length ? "in use" : "unused"],
|
|
1716
|
-
row: {
|
|
1717
|
-
key: l.name,
|
|
1718
|
-
cells: [
|
|
1719
|
-
{ text: l.name, grow: true },
|
|
1720
|
-
{ text: l.version ? `v${l.version}`.padStart(6) : " ", color: color.muted },
|
|
1721
|
-
{
|
|
1722
|
-
text: (l.projects.length ? plural2(l.projects.length, "project") : "unused").padStart(13),
|
|
1723
|
-
color: l.projects.length ? color.green : color.faint
|
|
1724
|
-
},
|
|
1725
|
-
{ text: usesText(l.name).padStart(5), color: color.muted }
|
|
1726
|
-
]
|
|
1727
|
-
},
|
|
1728
|
-
primary: { label: "choose projects", run: toggleProjects.run },
|
|
1729
|
-
actions: [
|
|
1730
|
-
toggleProjects,
|
|
1731
|
-
{ label: "Edit SKILL.md", hint: "saving creates a new version", run: () => void edit(join3(librarySkillDir(l.name), "SKILL.md")) },
|
|
1732
|
-
{ label: "Open folder", run: () => openFolder(librarySkillDir(l.name)) },
|
|
1733
|
-
{
|
|
1734
|
-
label: "Copy review prompt",
|
|
1735
|
-
hint: "an agent decides keep or delete",
|
|
1736
|
-
run: () => copyReviewPrompt([reviewOfLibrary(l)], "This skill is in my skilllib library (Your skills).")
|
|
1737
|
-
},
|
|
1738
|
-
{
|
|
1739
|
-
label: "Version history\u2026",
|
|
1740
|
-
run: () => setOverlay({
|
|
1741
|
-
type: "menu",
|
|
1742
|
-
title: `${l.name} versions`,
|
|
1743
|
-
index: 0,
|
|
1744
|
-
build: () => [...versionHistory(l.name)].reverse().map((v) => ({ label: `v${v.version}`, hint: v.date.slice(0, 16).replace("T", " "), run: () => {
|
|
1745
|
-
} }))
|
|
1746
|
-
})
|
|
1747
|
-
},
|
|
1748
|
-
{
|
|
1749
|
-
label: "Delete from library",
|
|
1750
|
-
hint: "moves to trash",
|
|
1751
|
-
run: () => confirm(`Delete ${l.name} from the library? Restore it from Health.`, () => {
|
|
1752
|
-
const r = deleteLibrarySkill(l.name);
|
|
1753
|
-
return r.ok ? ok(`${l.name} moved to the trash`) : warn(`Can't delete ${l.name}: ${r.reason}`);
|
|
1754
|
-
})
|
|
1755
|
-
}
|
|
1756
|
-
],
|
|
1757
|
-
preview: {
|
|
1758
|
-
title: l.name,
|
|
1759
|
-
dir: librarySkillDir(l.name),
|
|
1760
|
-
meta: [
|
|
1761
|
-
`v${l.version ?? "?"}`,
|
|
1762
|
-
l.origin ? `from ${l.origin}` : "library",
|
|
1763
|
-
l.projects.length ? `in ${l.projects.map((p) => basename3(p)).join(", ")}` : "in no projects",
|
|
1764
|
-
`${usesText(l.name)} Claude Code uses (${USAGE_DAYS}d)`
|
|
1765
|
-
]
|
|
1766
|
-
}
|
|
1767
|
-
};
|
|
1768
|
-
});
|
|
1769
|
-
return [
|
|
1770
|
-
{
|
|
1771
|
-
title: "",
|
|
1772
|
-
items: [newSkill, ...items],
|
|
1773
|
-
groupActions: (members) => [
|
|
1774
|
-
{
|
|
1775
|
-
label: `Copy review prompt for all ${members.length}`,
|
|
1776
|
-
hint: "an agent decides keep or delete",
|
|
1777
|
-
run: () => copyReviewPrompt(
|
|
1778
|
-
snapshot.library.filter((l) => members.some((m) => m.skill?.name === l.name)).map(reviewOfLibrary),
|
|
1779
|
-
"These skills are in my skilllib library (Your skills)."
|
|
1780
|
-
)
|
|
1781
|
-
}
|
|
1782
|
-
]
|
|
1783
|
-
}
|
|
1784
|
-
];
|
|
1785
|
-
}
|
|
1786
|
-
function globalSections() {
|
|
1787
|
-
const yours = snapshot.machine.filter((m) => m.movable);
|
|
1788
|
-
const unreviewed = yours.filter((m) => !m.broken && !snapshot.keptGlobal.has(m.name));
|
|
1789
|
-
const keptCount = yours.filter((m) => !m.broken && snapshot.keptGlobal.has(m.name)).length;
|
|
1790
|
-
const importAll = {
|
|
1791
|
-
label: "Copy all of yours to Your skills",
|
|
1792
|
-
run: () => {
|
|
1793
|
-
const pending = yours.filter((m) => !m.broken && !libraryNames.has(m.name));
|
|
1794
|
-
act(() => {
|
|
1795
|
-
for (const m of pending) importSkill(m.path);
|
|
1796
|
-
return ok(pending.length ? `Imported ${plural2(pending.length, "skill")}` : "All of yours are already in the library");
|
|
1797
|
-
});
|
|
1798
|
-
}
|
|
1799
|
-
};
|
|
1800
|
-
const unloadAll = {
|
|
1801
|
-
label: "Stop loading all of yours globally",
|
|
1802
|
-
hint: "add them per project instead",
|
|
1803
|
-
run: () => {
|
|
1804
|
-
const ready = yours.filter((m) => !m.broken && libraryNames.has(m.name));
|
|
1805
|
-
if (ready.length === 0) return say("Import them into the library first", false);
|
|
1806
|
-
confirm(
|
|
1807
|
-
`Stop loading ${plural2(ready.length, "skill")} in every project? Restorable from Health.`,
|
|
1808
|
-
() => ok(`${plural2(ready.filter((m) => unloadGlobal(m.path, m.links).ok).length, "skill")} no longer load globally`)
|
|
1809
|
-
);
|
|
1810
|
-
}
|
|
1811
|
-
};
|
|
1812
|
-
const folder = (m) => dirname(m.path);
|
|
1813
|
-
const yourFolders = [...new Set(snapshot.machine.filter((m) => m.movable).map(folder))].sort();
|
|
1814
|
-
const groups = [
|
|
1815
|
-
...yourFolders.map((f) => [`Yours \xB7 ${tildify2(f)}`, (m) => m.movable && folder(m) === f]),
|
|
1816
|
-
["claude.ai \xB7 synced to your account", (m) => m.kind === "claude.ai"],
|
|
1817
|
-
["Claude Code plugins", (m) => m.kind === "plugin"],
|
|
1818
|
-
["Cursor built-in", (m) => m.kind === "built-in"]
|
|
1819
|
-
];
|
|
1820
|
-
const launcher = {
|
|
1821
|
-
key: "__cleanup",
|
|
1822
|
-
search: "clean up",
|
|
1823
|
-
tags: [],
|
|
1824
|
-
row: {
|
|
1825
|
-
key: "__cleanup",
|
|
1826
|
-
cells: [
|
|
1827
|
-
{ text: "\u26A0 ", color: color.yellow },
|
|
1828
|
-
{ text: `Clean up: choose which repos keep each of your ${unreviewed.length} unreviewed global skills\u2026`, grow: true, color: color.yellow }
|
|
1829
|
-
]
|
|
1830
|
-
},
|
|
1831
|
-
primary: { label: "clean up", run: () => setWizard({}) },
|
|
1832
|
-
actions: [{ label: "Open the cleanup wizard", run: () => setWizard({}) }],
|
|
1833
|
-
preview: {
|
|
1834
|
-
title: "Clean up global skills",
|
|
1835
|
-
meta: ["every agent reads global skills in every repo", ...keptCount ? [`skips the ${keptCount} you keep global on purpose (k shows them)`] : []],
|
|
1836
|
-
description: "Install each skill only where it's needed, keep it global on purpose, or delete it. Originals are kept in backup."
|
|
1837
|
-
}
|
|
1838
|
-
};
|
|
1839
|
-
const grouped = groups.map(([title, test]) => ({
|
|
1840
|
-
title,
|
|
1841
|
-
groupActions: (members) => [
|
|
1842
|
-
{
|
|
1843
|
-
label: `Copy review prompt for all ${members.length}`,
|
|
1844
|
-
hint: "for an agent to decide keep / move / delete",
|
|
1845
|
-
run: () => copyReviewPrompt(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)).map(reviewOfGlobal), `These are global skills (group ${members[0]?.group?.label ?? ""}).`)
|
|
1846
|
-
},
|
|
1847
|
-
...title.startsWith("Yours") ? [
|
|
1848
|
-
...keepGroupActions(members),
|
|
1849
|
-
{
|
|
1850
|
-
label: `Delete all ${members.length}`,
|
|
1851
|
-
hint: "not kept in Your skills \xB7 restorable from Health",
|
|
1852
|
-
run: () => confirmDeleteGlobal(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)))
|
|
1853
|
-
}
|
|
1854
|
-
] : []
|
|
1855
|
-
],
|
|
1856
|
-
items: snapshot.machine.filter((m) => test(m)).map((m) => {
|
|
1857
|
-
const inLib = libraryNames.has(m.name);
|
|
1858
|
-
const importIt = () => act(() => {
|
|
1859
|
-
const r = importSkill(m.path, { force: true });
|
|
1860
|
-
return ok(
|
|
1861
|
-
`${m.name} ${r.status === "unchanged" ? "is already in the library" : r.status === "added" ? "imported into the library" : "updated in the library"}`
|
|
1862
|
-
);
|
|
1863
|
-
});
|
|
1864
|
-
const unload = () => confirm(
|
|
1865
|
-
m.broken ? `Remove the broken link ${m.name}? Restorable from Health.` : `Stop loading ${m.name} in every project?${inLib ? "" : " It's imported into the library first."} Restorable from Health.`,
|
|
1866
|
-
() => {
|
|
1867
|
-
if (!m.broken && !inLib) importSkill(m.path);
|
|
1868
|
-
const r = unloadGlobal(m.path, m.links);
|
|
1869
|
-
return r.ok ? ok(`${m.name} no longer loads globally`) : warn(`${m.name}: ${r.reason}`);
|
|
1870
|
-
}
|
|
1871
|
-
);
|
|
1872
|
-
const actions = [];
|
|
1873
|
-
let primary;
|
|
1874
|
-
if (m.broken) {
|
|
1875
|
-
primary = { label: "remove link", run: unload };
|
|
1876
|
-
actions.push({ label: "Remove the broken link", run: unload });
|
|
1877
|
-
} else {
|
|
1878
|
-
if (!inLib) actions.push({ label: "Copy to Your skills", hint: "keeps loading globally", run: importIt });
|
|
1879
|
-
if (m.movable) actions.push(keepAction(m), { label: "Stop loading it globally", hint: "use it per project instead", run: unload });
|
|
1880
|
-
primary = m.movable ? { label: "choose repos\u2026", run: () => setWizard({ initialSkill: m.name }) } : !inLib ? { label: "copy to your skills", run: importIt } : void 0;
|
|
1881
|
-
if (m.movable) {
|
|
1882
|
-
actions.unshift({ label: "Choose which repos keep it\u2026", hint: "cleanup wizard", run: () => setWizard({ initialSkill: m.name }) });
|
|
1883
|
-
actions.push({ label: "Delete", hint: "not kept in Your skills \xB7 restorable from Health", run: () => confirmDeleteGlobal([m]) });
|
|
1884
|
-
}
|
|
1885
|
-
actions.push({ label: "Open folder", run: () => openFolder(m.path) });
|
|
1886
|
-
actions.push({ label: "Copy review prompt", hint: "an agent decides keep / move / delete", run: () => copyReviewPrompt([reviewOfGlobal(m)], "This is one global skill.") });
|
|
1887
|
-
}
|
|
1888
|
-
if (m.movable) actions.push(importAll, unloadAll);
|
|
1889
|
-
return {
|
|
1890
|
-
key: `${m.kind}:${m.path}`,
|
|
1891
|
-
search: `${m.name} ${m.origin}`,
|
|
1892
|
-
group: sourceGroup(m),
|
|
1893
|
-
skill: { name: m.name, path: m.path },
|
|
1894
|
-
tags: [m.movable ? "yours" : "vendor"],
|
|
1895
|
-
row: {
|
|
1896
|
-
key: `${m.kind}:${m.path}`,
|
|
1897
|
-
cells: [
|
|
1898
|
-
{ text: m.name.padEnd(30).slice(0, 30), color: m.broken ? color.red : void 0 },
|
|
1899
|
-
{ text: m.broken ? "broken link" : m.origin, grow: true, color: m.broken ? color.red : kindColor[m.kind] },
|
|
1900
|
-
...harnessChips(enabledIds.map((id) => ({ id, paths: m.harnesses.includes(id) ? 1 : 0 })), !m.broken),
|
|
1901
|
-
{ text: m.movable && snapshot.keptGlobal.has(m.name) ? " \u2713 kept" : " ", color: color.green },
|
|
1902
|
-
{ text: inLib ? " \u2713 library" : " ", color: color.green },
|
|
1903
|
-
{ text: usesText(m.name).padStart(5), color: color.muted }
|
|
1904
|
-
]
|
|
1905
|
-
},
|
|
1906
|
-
primary,
|
|
1907
|
-
actions,
|
|
1908
|
-
preview: {
|
|
1909
|
-
title: m.name,
|
|
1910
|
-
dir: m.broken ? void 0 : m.path,
|
|
1911
|
-
meta: [
|
|
1912
|
-
m.harnesses.length ? `loaded everywhere by ${m.harnesses.map(harnessName).join(", ")}` : "not loaded by your harnesses",
|
|
1913
|
-
`${m.kind}: ${m.origin}`,
|
|
1914
|
-
m.movable ? snapshot.keptGlobal.has(m.name) ? "yours \xB7 kept global on purpose" : "yours to manage" : "managed by the vendor",
|
|
1915
|
-
tildify2(m.path) + (m.links.length ? ` (+ ${m.links.length} link${m.links.length > 1 ? "s" : ""})` : "")
|
|
1916
|
-
],
|
|
1917
|
-
description: m.broken ? "Broken link: it points at a folder that no longer exists, so it loads nothing." : void 0
|
|
1918
|
-
}
|
|
1919
|
-
};
|
|
1920
|
-
})
|
|
1921
|
-
}));
|
|
1922
|
-
const reviewAll = {
|
|
1923
|
-
key: "__review-all",
|
|
1924
|
-
search: "review agent prompt",
|
|
1925
|
-
tags: [],
|
|
1926
|
-
row: {
|
|
1927
|
-
key: "__review-all",
|
|
1928
|
-
cells: [
|
|
1929
|
-
{ text: "\u2726 ", color: color.accent },
|
|
1930
|
-
{ text: `Ask an agent to review all ${snapshot.machine.filter((m) => !m.broken).length} global skills (copies a prompt)`, grow: true, color: color.accent }
|
|
1931
|
-
]
|
|
1932
|
-
},
|
|
1933
|
-
primary: {
|
|
1934
|
-
label: "copy prompt",
|
|
1935
|
-
run: () => copyReviewPrompt(snapshot.machine.filter((m) => !m.broken).map(reviewOfGlobal), "These are ALL the skills that load globally on my machine.")
|
|
1936
|
-
},
|
|
1937
|
-
actions: [
|
|
1938
|
-
{
|
|
1939
|
-
label: "Copy review prompt \u2014 all global skills",
|
|
1940
|
-
run: () => copyReviewPrompt(snapshot.machine.filter((m) => !m.broken).map(reviewOfGlobal), "These are ALL the skills that load globally on my machine.")
|
|
1941
|
-
},
|
|
1942
|
-
{
|
|
1943
|
-
label: "Copy review prompt \u2014 only mine (not vendor)",
|
|
1944
|
-
run: () => copyReviewPrompt(yours.filter((m) => !m.broken).map(reviewOfGlobal), "These are my own global skills (vendor skills excluded).")
|
|
1945
|
-
}
|
|
1946
|
-
],
|
|
1947
|
-
preview: {
|
|
1948
|
-
title: "Ask an agent to review your skills",
|
|
1949
|
-
meta: ["copies a prompt to your clipboard", "also saved to ~/.skilllib/review-prompt.md"],
|
|
1950
|
-
description: "The prompt includes each skill's description, a SKILL.md excerpt, how it was installed (vendor, skills.sh, plugin\u2026), which agents load it, usage per project, duplicates and your project list. Paste it into Claude Code, Cursor or Codex; it replies with keep / move / delete per skill."
|
|
1951
|
-
}
|
|
1952
|
-
};
|
|
1953
|
-
return [{ title: "", items: [...unreviewed.length ? [launcher] : [], reviewAll] }, ...grouped];
|
|
1954
|
-
}
|
|
1955
|
-
function markKept(names, keep) {
|
|
1956
|
-
act(() => {
|
|
1957
|
-
setKeepGlobal(names, keep);
|
|
1958
|
-
const label = names.length === 1 ? names[0] : plural2(names.length, "skill");
|
|
1959
|
-
return ok(keep ? `${label} kept global on purpose \u2014 cleanup skips ${names.length === 1 ? "it" : "them"}` : `${label} back in cleanup`);
|
|
1960
|
-
});
|
|
1961
|
-
}
|
|
1962
|
-
function keepAction(m) {
|
|
1963
|
-
return snapshot.keptGlobal.has(m.name) ? { label: "Unmark: not global on purpose", hint: "cleanup asks about it again", run: () => markKept([m.name], false) } : { label: "\u2713 Keep global on purpose", hint: "stops the \u26A0 and skips it in cleanup", run: () => markKept([m.name], true) };
|
|
1964
|
-
}
|
|
1965
|
-
function keepGroupActions(members) {
|
|
1966
|
-
const names = [...new Set(members.flatMap((m) => m.skill ? [m.skill.name] : []))];
|
|
1967
|
-
const unkept = names.filter((n) => !snapshot.keptGlobal.has(n));
|
|
1968
|
-
const kept = names.filter((n) => snapshot.keptGlobal.has(n));
|
|
1969
|
-
return [
|
|
1970
|
-
...unkept.length ? [{ label: `\u2713 Keep all ${unkept.length} global on purpose`, hint: "cleanup skips them", run: () => markKept(unkept, true) }] : [],
|
|
1971
|
-
...kept.length ? [{ label: `Unmark all ${kept.length}`, hint: "cleanup asks about them again", run: () => markKept(kept, false) }] : []
|
|
1972
|
-
];
|
|
1973
|
-
}
|
|
1974
|
-
function yourGlobalActions(members) {
|
|
1975
|
-
return [
|
|
1976
|
-
{
|
|
1977
|
-
label: `Copy review prompt for all ${members.length}`,
|
|
1978
|
-
hint: "for an agent to decide keep / move / delete",
|
|
1979
|
-
run: () => copyReviewPrompt(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)).map(reviewOfGlobal), `These are global skills (group ${members[0]?.group?.label ?? ""}).`)
|
|
1980
|
-
},
|
|
1981
|
-
{
|
|
1982
|
-
label: `Copy all ${members.length} to Your skills`,
|
|
1983
|
-
hint: "they keep loading globally",
|
|
1984
|
-
run: () => act(() => {
|
|
1985
|
-
const added = members.map((m) => importSkill(m.skill.path)).filter((r) => r.status === "added").length;
|
|
1986
|
-
return ok(`${plural2(added, "skill")} imported into your library`);
|
|
1987
|
-
})
|
|
1988
|
-
},
|
|
1989
|
-
{
|
|
1990
|
-
label: `Delete all ${members.length}`,
|
|
1991
|
-
hint: "not kept in Your skills \xB7 restorable from Health",
|
|
1992
|
-
run: () => confirmDeleteGlobal(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)))
|
|
1993
|
-
}
|
|
1994
|
-
];
|
|
1995
|
-
}
|
|
1996
|
-
function confirmDeleteGlobal(skills) {
|
|
1997
|
-
if (skills.length === 0) return;
|
|
1998
|
-
const label = skills.length === 1 ? skills[0].name : plural2(skills.length, "global skill");
|
|
1999
|
-
confirm(`Delete ${label}? It stops loading everywhere and is NOT kept in Your skills. A backup stays restorable from Health.`, () => {
|
|
2000
|
-
const done = skills.filter((m) => deleteGlobal(m.path, m.links).ok).length;
|
|
2001
|
-
return ok(`Deleted ${plural2(done, "global skill")} (restorable from Health)`);
|
|
2002
|
-
});
|
|
2003
|
-
}
|
|
2004
|
-
function usageFor(name) {
|
|
2005
|
-
const total = usage ? usage.total.get(name) ?? 0 : null;
|
|
2006
|
-
const byProject = usage ? snapshot.projects.flatMap((p) => {
|
|
2007
|
-
const n = usage.byProject.get(p)?.get(name) ?? 0;
|
|
2008
|
-
return n ? [[basename3(p), n]] : [];
|
|
2009
|
-
}) : [];
|
|
2010
|
-
return { total, byProject };
|
|
2011
|
-
}
|
|
2012
|
-
function projectsWith(name) {
|
|
2013
|
-
return snapshot.projects.filter((p) => (snapshot.rows.get(p) ?? []).some((r) => r.installed && r.name === name)).map((p) => basename3(p));
|
|
2014
|
-
}
|
|
2015
|
-
function reviewOfGlobal(m) {
|
|
2016
|
-
const u = usageFor(m.name);
|
|
2017
|
-
const lib = snapshot.library.find((l) => l.name === m.name);
|
|
2018
|
-
const how = {
|
|
2019
|
-
global: `copied into ${tildify2(dirname(m.path))} by hand or by a tool (no record of where from)`,
|
|
2020
|
-
"skills.sh": m.origin.includes("/") ? `\`npx skills add ${m.origin}\` (skills.sh)` : "the `npx skills` CLI (not in its lock file)",
|
|
2021
|
-
plugin: m.origin.includes("(claude.ai)") ? `a Claude Code plugin synced from my claude.ai account (${m.origin})` : `the Claude Code plugin ${m.origin} (/plugin install)`,
|
|
2022
|
-
"claude.ai": "synced from my claude.ai account (Settings \u2192 Capabilities)",
|
|
2023
|
-
"built-in": "ships with Cursor"
|
|
2024
|
-
};
|
|
2025
|
-
const vendor = {
|
|
2026
|
-
plugin: `turn it off with /plugin in Claude Code (disable ${m.origin.replace(" (claude.ai)", "")}), or in claude.ai if it's synced`,
|
|
2027
|
-
"claude.ai": "remove it from my claude.ai account's skills",
|
|
2028
|
-
"built-in": "bundled with Cursor; it can't be removed"
|
|
2029
|
-
};
|
|
2030
|
-
return {
|
|
2031
|
-
name: m.name,
|
|
2032
|
-
description: m.description,
|
|
2033
|
-
source: m.movable ? `my global skills (${m.kind === "skills.sh" ? "skills.sh package" : "global folder"})` : `vendor: ${m.kind} (${m.origin})`,
|
|
2034
|
-
installedHow: how[m.kind] ?? m.origin,
|
|
2035
|
-
path: m.path,
|
|
2036
|
-
links: m.links,
|
|
2037
|
-
loadedBy: `${m.harnesses.map(harnessName).join(", ") || "none of my agents"} \u2014 in every repo`,
|
|
2038
|
-
vendor: m.movable ? null : vendor[m.kind] ?? "managed by its vendor",
|
|
2039
|
-
usesTotal: u.total,
|
|
2040
|
-
usesByProject: u.byProject,
|
|
2041
|
-
installedIn: projectsWith(m.name),
|
|
2042
|
-
otherCopies: snapshot.machine.filter((x) => x.name === m.name && x.path !== m.path).map((x) => `${x.kind} ${x.origin} (${tildify2(x.path)})`),
|
|
2043
|
-
inYourSkills: lib ? `yes, v${lib.version}` : null,
|
|
2044
|
-
keptGlobal: m.movable && snapshot.keptGlobal.has(m.name)
|
|
2045
|
-
};
|
|
2046
|
-
}
|
|
2047
|
-
function reviewOfLibrary(l) {
|
|
2048
|
-
const u = usageFor(l.name);
|
|
2049
|
-
return {
|
|
2050
|
-
name: l.name,
|
|
2051
|
-
description: l.description,
|
|
2052
|
-
source: "Your skills (skilllib library) \u2014 loads nowhere by itself",
|
|
2053
|
-
installedHow: l.origin ? `imported into skilllib from ${l.origin}` : "created or imported into skilllib",
|
|
2054
|
-
path: librarySkillDir(l.name),
|
|
2055
|
-
links: [],
|
|
2056
|
-
loadedBy: l.projects.length ? `projects that added it: ${l.projects.map((p) => basename3(p)).join(", ")}` : "nowhere (no project has added it)",
|
|
2057
|
-
vendor: null,
|
|
2058
|
-
usesTotal: u.total,
|
|
2059
|
-
usesByProject: u.byProject,
|
|
2060
|
-
installedIn: l.projects.map((p) => basename3(p)),
|
|
2061
|
-
otherCopies: snapshot.machine.filter((x) => x.name === l.name && !x.broken).map((x) => `global ${x.kind} (${tildify2(x.path)})`),
|
|
2062
|
-
inYourSkills: `yes, v${l.version}`
|
|
2063
|
-
};
|
|
2064
|
-
}
|
|
2065
|
-
function copyReviewPrompt(skills, scope) {
|
|
2066
|
-
if (skills.length === 0) return say("Nothing to review", false);
|
|
2067
|
-
const prompt = buildReviewPrompt(skills, {
|
|
2068
|
-
scope,
|
|
2069
|
-
usageDays: USAGE_DAYS,
|
|
2070
|
-
harnesses: snapshot.harnesses.map(harnessName),
|
|
2071
|
-
projects: snapshot.projects.map((p) => ({ name: basename3(p), path: p }))
|
|
2072
|
-
});
|
|
2073
|
-
const r = copyText(prompt);
|
|
2074
|
-
say(
|
|
2075
|
-
r.copied ? `Copied a review prompt for ${plural2(skills.length, "skill")} (${Math.round(prompt.length / 1e3)}k chars) \u2014 paste it into Claude Code, Cursor or Codex` : `Couldn't reach the clipboard; the prompt is in ${tildify2(r.savedTo)}`,
|
|
2076
|
-
r.copied
|
|
2077
|
-
);
|
|
2078
|
-
}
|
|
2079
|
-
function settingsSections() {
|
|
2080
|
-
const harnessItems = HARNESSES.map((h) => {
|
|
2081
|
-
const on = snapshot.harnesses.includes(h.id);
|
|
2082
|
-
const toggle = () => act(() => {
|
|
2083
|
-
const next = on ? snapshot.harnesses.filter((id) => id !== h.id) : [...snapshot.harnesses, h.id];
|
|
2084
|
-
setHarnesses(next);
|
|
2085
|
-
return ok(
|
|
2086
|
-
`${h.name} ${on ? "off" : "on"}. New installs go to ${installDirs(next).join(" + ")}; use "Make visible" on existing skills to add links.`
|
|
2087
|
-
);
|
|
2088
|
-
});
|
|
2089
|
-
return {
|
|
2090
|
-
key: `harness:${h.id}`,
|
|
2091
|
-
search: h.name,
|
|
2092
|
-
tags: [],
|
|
2093
|
-
row: {
|
|
2094
|
-
key: `harness:${h.id}`,
|
|
2095
|
-
cells: [
|
|
2096
|
-
{ text: on ? "\u25C9 " : "\u25CB ", color: on ? color.green : color.faint },
|
|
2097
|
-
{ text: `${h.icon} `, color: on ? h.color : color.faint },
|
|
2098
|
-
{ text: h.name, grow: true, bold: on },
|
|
2099
|
-
{ text: `reads ${h.projectDirs.map((d) => d.replace("/skills", "")).join(" ")}`, color: color.faint },
|
|
2100
|
-
{ text: h.installed() ? " detected" : " ", color: color.muted }
|
|
2101
|
-
]
|
|
2102
|
-
},
|
|
2103
|
-
primary: { label: on ? "turn off" : "turn on", run: toggle },
|
|
2104
|
-
actions: [{ label: on ? `Stop using ${h.name}` : `Use ${h.name}`, run: toggle }],
|
|
2105
|
-
preview: {
|
|
2106
|
-
title: h.name,
|
|
2107
|
-
meta: [`project: ${h.projectDirs.join(", ")}`, `global: ${h.globalDirs().map(tildify2).join(", ")}`],
|
|
2108
|
-
description: `skilllib installs library skills where every harness you use can see them. With your current choice: ${installDirs(snapshot.harnesses).join(" (real copy) + ")}${installDirs(snapshot.harnesses).length > 1 ? " (link)" : ""}.`
|
|
2109
|
-
}
|
|
2110
|
-
};
|
|
2111
|
-
});
|
|
2112
|
-
const folderItems = snapshot.roots.map((root) => {
|
|
2113
|
-
const remove = () => confirm(`Stop scanning ${tildify2(root)}? Projects already found stay listed until you hide them.`, () => {
|
|
2114
|
-
removeRoot(root);
|
|
2115
|
-
return ok(`${tildify2(root)} removed from your project folders`);
|
|
2116
|
-
});
|
|
2117
|
-
return {
|
|
2118
|
-
key: `root:${root}`,
|
|
2119
|
-
search: root,
|
|
2120
|
-
tags: [],
|
|
2121
|
-
row: { key: `root:${root}`, cells: [{ text: "\u25B8 ", color: color.accent }, { text: tildify2(root), grow: true }] },
|
|
2122
|
-
primary: { label: "remove folder", run: remove },
|
|
2123
|
-
actions: [
|
|
2124
|
-
{ label: "Rescan now", run: () => rescan(() => say("Rescanned your project folders")) },
|
|
2125
|
-
{ label: "Remove this folder", run: remove }
|
|
2126
|
-
],
|
|
2127
|
-
preview: { title: tildify2(root), meta: ["scanned for git repos every time skilllib opens"] }
|
|
2128
|
-
};
|
|
2129
|
-
});
|
|
2130
|
-
const addFolder = {
|
|
2131
|
-
key: "__add-folder",
|
|
2132
|
-
search: "add folder",
|
|
2133
|
-
tags: [],
|
|
2134
|
-
row: { key: "__add-folder", cells: [{ text: "+ ", color: color.accent }, { text: "Add a folder\u2026", grow: true, color: color.accent }] },
|
|
2135
|
-
primary: { label: "add", run: () => promptAddFolder() },
|
|
2136
|
-
actions: [{ label: "Add a folder", run: () => promptAddFolder() }],
|
|
2137
|
-
preview: { title: "Add a folder", meta: ["skilllib scans it for git repos now and every time it opens"] }
|
|
2138
|
-
};
|
|
2139
|
-
const rescanItem = {
|
|
2140
|
-
key: "__rescan",
|
|
2141
|
-
search: "rescan",
|
|
2142
|
-
tags: [],
|
|
2143
|
-
row: { key: "__rescan", cells: [{ text: "\u27F3 ", color: color.accent }, { text: scanning ? "Scanning\u2026" : "Rescan now", grow: true, color: color.accent }] },
|
|
2144
|
-
primary: { label: "rescan", run: () => rescan(() => say("Rescanned your project folders")) },
|
|
2145
|
-
actions: [{ label: "Rescan now", run: () => rescan(() => say("Rescanned your project folders")) }],
|
|
2146
|
-
preview: { title: "Rescan", meta: [`${plural2(snapshot.projects.length, "project")} found`] }
|
|
2147
|
-
};
|
|
2148
|
-
return [
|
|
2149
|
-
{ title: "Harnesses you use \xB7 space toggles", items: harnessItems },
|
|
2150
|
-
{ title: "Project folders", items: [...folderItems, addFolder, rescanItem] }
|
|
2151
|
-
];
|
|
2152
|
-
}
|
|
2153
|
-
function healthSections() {
|
|
2154
|
-
return [
|
|
2155
|
-
{
|
|
2156
|
-
title: "Issues",
|
|
2157
|
-
items: snapshot.issues.map((issue) => {
|
|
2158
|
-
const fix = issue.fix;
|
|
2159
|
-
const result = (c) => {
|
|
2160
|
-
const r = runFix(c);
|
|
2161
|
-
return r.ok ? ok(r.message) : warn(r.message);
|
|
2162
|
-
};
|
|
2163
|
-
const choices = (issue.choices ?? []).map((c) => ({ label: c.label, hint: c.hint, run: () => act(() => result(c)) }));
|
|
2164
|
-
const run = fix ? () => confirm(`${fix.label}?`, () => result(fix)) : choices.length ? () => setOverlay({ type: "menu", title: issue.title, index: 0, build: () => choices }) : void 0;
|
|
2165
|
-
return {
|
|
2166
|
-
key: issue.id,
|
|
2167
|
-
search: issue.title,
|
|
2168
|
-
tags: [],
|
|
2169
|
-
row: {
|
|
2170
|
-
key: issue.id,
|
|
2171
|
-
cells: [
|
|
2172
|
-
{ text: issue.severity === "problem" ? "\u25CF " : "\u25C6 ", color: issue.severity === "problem" ? color.red : color.yellow },
|
|
2173
|
-
{ text: issue.title, grow: true },
|
|
2174
|
-
{ text: run ? " space: fix" : "", color: color.accent }
|
|
2175
|
-
]
|
|
2176
|
-
},
|
|
2177
|
-
primary: run ? { label: "fix", run } : void 0,
|
|
2178
|
-
actions: fix && run ? [{ label: fix.label, run }] : choices,
|
|
2179
|
-
preview: {
|
|
2180
|
-
title: issue.title,
|
|
2181
|
-
meta: [issue.severity, fix ? `fix: ${fix.label.toLowerCase()}` : choices.length ? `${choices.length} ways to fix it: pick one` : "no automatic fix"],
|
|
2182
|
-
description: issue.detail
|
|
2183
|
-
}
|
|
2184
|
-
};
|
|
2185
|
-
})
|
|
2186
|
-
},
|
|
2187
|
-
{
|
|
2188
|
-
title: "Moved out \xB7 restorable",
|
|
2189
|
-
items: snapshot.backups.map((b) => {
|
|
2190
|
-
const restore = () => act(() => {
|
|
2191
|
-
const r = b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b);
|
|
2192
|
-
return r.ok ? ok(`${b.name} restored to ${tildify2(r.to)}`) : warn(`Can't restore ${b.name}: ${r.reason}`);
|
|
2193
|
-
});
|
|
2194
|
-
return {
|
|
2195
|
-
key: b.path,
|
|
2196
|
-
search: b.name,
|
|
2197
|
-
tags: [],
|
|
2198
|
-
row: {
|
|
2199
|
-
key: b.path,
|
|
2200
|
-
cells: [
|
|
2201
|
-
{ text: b.name, grow: true },
|
|
2202
|
-
{ text: ` ${BACKUP_LABEL[b.kind]}`, color: color.muted },
|
|
2203
|
-
{ text: ` ${b.movedAt}`, color: color.faint }
|
|
2204
|
-
]
|
|
2205
|
-
},
|
|
2206
|
-
primary: { label: "restore", run: restore },
|
|
2207
|
-
actions: [{ label: "Restore", run: restore }],
|
|
2208
|
-
preview: {
|
|
2209
|
-
title: b.name,
|
|
2210
|
-
dir: b.path,
|
|
2211
|
-
meta: [b.kind === "plugin" ? "Claude Code plugin; restoring reinstalls it" : `from ${tildify2(b.from)}`, `moved ${b.movedAt}`]
|
|
2212
|
-
}
|
|
2213
|
-
};
|
|
2214
|
-
})
|
|
2215
|
-
}
|
|
2216
|
-
];
|
|
2217
|
-
}
|
|
2218
|
-
const projectTabs = place.kind === "project" ? projectSections(place.root) : null;
|
|
2219
|
-
const sections = projectTabs ? projectTabs[projectTab] : place.kind === "library" ? librarySections() : place.kind === "global" ? globalSections() : place.kind === "health" ? healthSections() : place.kind === "help" ? [] : settingsSections();
|
|
2220
|
-
const filtered = sections;
|
|
2221
|
-
const visibleSections = search.right ? [
|
|
2222
|
-
{
|
|
2223
|
-
title: "",
|
|
2224
|
-
items: filtered.flatMap((s) => s.items).map((i, order) => ({ i, order, score: matchScore(i.search.split(" ")[0] ?? i.search, search.right) ?? matchScore(i.search, search.right) })).filter((x) => x.score !== null).sort((a, b) => (a.score ?? 0) - (b.score ?? 0) || a.order - b.order).map((x) => x.i)
|
|
2225
|
-
}
|
|
2226
|
-
].filter((s) => s.items.length > 0) : filtered.map(
|
|
2227
|
-
(sec) => groupSection(sec, expanded, toggleGroup, (names) => {
|
|
2228
|
-
if (!usage) return "\u2026";
|
|
2229
|
-
const n = names.reduce((sum, name) => sum + (usesOf(name, projectTabs ? pKey.slice("project:".length) : void 0) ?? 0), 0);
|
|
2230
|
-
return n ? String(n) : "\u2013";
|
|
2231
|
-
})
|
|
2232
|
-
).filter((s) => s.items.length > 0);
|
|
2233
|
-
const rightItems = visibleSections.flatMap((s) => s.items);
|
|
2234
|
-
const rightRows = visibleSections.flatMap((s) => [
|
|
2235
|
-
...s.title ? [{ key: `h:${s.title}`, header: true, cells: [{ text: s.title }] }] : [],
|
|
2236
|
-
...s.items.map((i) => i.row)
|
|
2237
|
-
]);
|
|
2238
|
-
const rightSelKey = `right:${pKey}${projectTabs ? `:${projectTab}` : ""}`;
|
|
2239
|
-
const rightKey = rightItems.some((i) => i.key === selected[rightSelKey]) ? selected[rightSelKey] : rightItems[0]?.key ?? "";
|
|
2240
|
-
const current = rightItems.find((i) => i.key === rightKey);
|
|
2241
|
-
function promptAddFolder() {
|
|
2242
|
-
setOverlay({
|
|
2243
|
-
type: "prompt",
|
|
2244
|
-
label: "Folder that contains your projects",
|
|
2245
|
-
value: "~/Projects",
|
|
2246
|
-
onSubmit: (value) => {
|
|
2247
|
-
const root = addRoot(value);
|
|
2248
|
-
rescan(() => say(`Added ${tildify2(root)}. It's rescanned every time skilllib opens.`));
|
|
2249
|
-
}
|
|
2250
|
-
});
|
|
2251
|
-
}
|
|
2252
|
-
function promptNewSkill() {
|
|
2253
|
-
setOverlay({
|
|
2254
|
-
type: "prompt",
|
|
2255
|
-
label: "New skill name (lowercase-with-dashes)",
|
|
2256
|
-
value: "",
|
|
2257
|
-
onSubmit: (value) => {
|
|
2258
|
-
const r = createSkill(value.trim(), "");
|
|
2259
|
-
if (!r.ok) return say(`Can't create ${value}: ${r.reason}`, false);
|
|
2260
|
-
setSelected((s) => ({ ...s, "right:library": value.trim() }));
|
|
2261
|
-
reload();
|
|
2262
|
-
void edit(join3(r.dir, "SKILL.md"));
|
|
2263
|
-
}
|
|
2264
|
-
});
|
|
2265
|
-
}
|
|
2266
|
-
function enterPlace() {
|
|
2267
|
-
if (place.kind === "help") return setOverlay({ type: "help", topic: 1, scroll: 0 });
|
|
2268
|
-
setSelected((s) => ({ ...s, left: leftKey }));
|
|
2269
|
-
setFocus("right");
|
|
2270
|
-
}
|
|
2271
|
-
function openMenu(item) {
|
|
2272
|
-
if (item.actions.length === 0) return say("Nothing to do here", false);
|
|
2273
|
-
setOverlay({ type: "menu", title: item.preview?.title ?? item.key, index: 0, build: () => item.actions });
|
|
2274
|
-
}
|
|
2275
|
-
function move(delta, absolute) {
|
|
2276
|
-
const keys = focus === "left" ? leftVisible.map((e) => e.key) : rightItems.map((i2) => i2.key);
|
|
2277
|
-
const currentKey = focus === "left" ? leftKey : rightKey;
|
|
2278
|
-
const i = keys.indexOf(currentKey);
|
|
2279
|
-
const next = absolute === "start" ? 0 : absolute === "end" ? keys.length - 1 : Math.max(0, Math.min(keys.length - 1, i + delta));
|
|
2280
|
-
const nextKey = keys[next] ?? "";
|
|
2281
|
-
if (focus === "left") {
|
|
2282
|
-
setSelected((s) => ({ ...s, left: nextKey }));
|
|
2283
|
-
setSearch((s) => ({ ...s, right: "" }));
|
|
2284
|
-
} else {
|
|
2285
|
-
setSelected((s) => ({ ...s, [rightSelKey]: nextKey }));
|
|
2286
|
-
}
|
|
2287
|
-
}
|
|
2288
|
-
useInput((input, key) => {
|
|
2289
|
-
if (wizard) return wizardInput.current?.(input, key);
|
|
2290
|
-
if (key.ctrl && input === "c") return exit();
|
|
2291
|
-
if (setup?.step === "harnesses") {
|
|
2292
|
-
if (key.upArrow) return setSetup({ ...setup, index: Math.max(0, setup.index - 1) });
|
|
2293
|
-
if (key.downArrow) return setSetup({ ...setup, index: Math.min(HARNESSES.length - 1, setup.index + 1) });
|
|
2294
|
-
if (input === " ") {
|
|
2295
|
-
const id = HARNESSES[setup.index].id;
|
|
2296
|
-
const selected2 = setup.selected.includes(id) ? setup.selected.filter((x) => x !== id) : [...setup.selected, id];
|
|
2297
|
-
return setSetup({ ...setup, selected: selected2 });
|
|
2298
|
-
}
|
|
2299
|
-
if (key.return || key.escape) {
|
|
2300
|
-
setHarnesses(setup.selected);
|
|
2301
|
-
if (snapshot.roots.length === 0) return setSetup({ ...setup, step: "folder" });
|
|
2302
|
-
setSetup(null);
|
|
2303
|
-
reload();
|
|
2304
|
-
return say(`Using ${setup.selected.map(harnessName).join(", ") || "no harnesses"}. Change it anytime in Settings.`);
|
|
2305
|
-
}
|
|
2306
|
-
return;
|
|
2307
|
-
}
|
|
2308
|
-
if (setup?.step === "folder") {
|
|
2309
|
-
if (key.escape) {
|
|
2310
|
-
setSetup(null);
|
|
2311
|
-
return reload();
|
|
2312
|
-
}
|
|
2313
|
-
if (key.return) {
|
|
2314
|
-
const root = addRoot(setup.value);
|
|
2315
|
-
setSetup(null);
|
|
2316
|
-
return rescan(() => say(`Found your projects in ${tildify2(root)}. Type a name to find one, enter to open it.`));
|
|
2317
|
-
}
|
|
2318
|
-
if (key.backspace || key.delete) return setSetup({ ...setup, value: setup.value.slice(0, -1) });
|
|
2319
|
-
if (key.ctrl && input === "u") return setSetup({ ...setup, value: "" });
|
|
2320
|
-
if (input && !key.ctrl && !key.meta) return setSetup({ ...setup, value: setup.value + input });
|
|
2321
|
-
return;
|
|
2322
|
-
}
|
|
2323
|
-
if (overlay?.type === "confirm") {
|
|
2324
|
-
if (input === "y" || key.return) {
|
|
2325
|
-
const run = overlay.onYes;
|
|
2326
|
-
setOverlay(null);
|
|
2327
|
-
act(run);
|
|
2328
|
-
} else if (input === "n" || key.escape) {
|
|
2329
|
-
setOverlay(null);
|
|
2330
|
-
say("Cancelled", false);
|
|
2331
|
-
}
|
|
2332
|
-
return;
|
|
2333
|
-
}
|
|
2334
|
-
if (overlay?.type === "prompt") {
|
|
2335
|
-
if (key.escape) {
|
|
2336
|
-
setOverlay(null);
|
|
2337
|
-
} else if (key.return) {
|
|
2338
|
-
const { value, onSubmit } = overlay;
|
|
2339
|
-
setOverlay(null);
|
|
2340
|
-
onSubmit(value);
|
|
2341
|
-
} else if (key.backspace || key.delete) setOverlay({ ...overlay, value: overlay.value.slice(0, -1) });
|
|
2342
|
-
else if (key.ctrl && input === "u") setOverlay({ ...overlay, value: "" });
|
|
2343
|
-
else if (input && !key.ctrl && !key.meta && !key.tab) setOverlay({ ...overlay, value: overlay.value + input });
|
|
2344
|
-
return;
|
|
2345
|
-
}
|
|
2346
|
-
if (overlay?.type === "menu") {
|
|
2347
|
-
const options = overlay.build();
|
|
2348
|
-
if (key.escape || key.leftArrow) return setOverlay(null);
|
|
2349
|
-
if (key.upArrow) return setOverlay({ ...overlay, index: Math.max(0, overlay.index - 1) });
|
|
2350
|
-
if (key.downArrow) return setOverlay({ ...overlay, index: Math.min(options.length - 1, overlay.index + 1) });
|
|
2351
|
-
if (key.return || input === " ") {
|
|
2352
|
-
const option = options[overlay.index];
|
|
2353
|
-
if (!option) return setOverlay(null);
|
|
2354
|
-
if (!option.keepOpen) setOverlay(null);
|
|
2355
|
-
option.run();
|
|
2356
|
-
}
|
|
2357
|
-
return;
|
|
2358
|
-
}
|
|
2359
|
-
if (overlay?.type === "help") {
|
|
2360
|
-
const last = HELP_TOPICS.length - 1;
|
|
2361
|
-
if (key.upArrow) return setOverlay({ ...overlay, topic: Math.max(0, overlay.topic - 1), scroll: 0 });
|
|
2362
|
-
if (key.downArrow || key.tab) return setOverlay({ ...overlay, topic: Math.min(last, overlay.topic + 1), scroll: 0 });
|
|
2363
|
-
if (key.pageDown || input === " ") return setOverlay({ ...overlay, scroll: overlay.scroll + 5 });
|
|
2364
|
-
if (key.pageUp) return setOverlay({ ...overlay, scroll: Math.max(0, overlay.scroll - 5) });
|
|
2365
|
-
if (key.escape || input === "?" || input === "q" || key.return) return setOverlay(null);
|
|
2366
|
-
return;
|
|
2367
|
-
}
|
|
2368
|
-
if (key.upArrow) return move(-1);
|
|
2369
|
-
if (key.downArrow) return move(1);
|
|
2370
|
-
if (key.pageUp) return move(-(termRows - 14));
|
|
2371
|
-
if (key.pageDown) return move(termRows - 14);
|
|
2372
|
-
if (key.home) return move(0, "start");
|
|
2373
|
-
if (key.end) return move(0, "end");
|
|
2374
|
-
if (key.ctrl && input === "r") {
|
|
2375
|
-
reload();
|
|
2376
|
-
return say("Reloaded");
|
|
2377
|
-
}
|
|
2378
|
-
const pane = focus;
|
|
2379
|
-
if (key.escape) {
|
|
2380
|
-
setToast(null);
|
|
2381
|
-
if (search[pane]) return setSearch((s) => ({ ...s, [pane]: "" }));
|
|
2382
|
-
if (pane === "right") return setFocus("left");
|
|
2383
|
-
return say("Press ctrl+c to quit", false);
|
|
2384
|
-
}
|
|
2385
|
-
if (key.backspace || key.delete) return setSearch((s) => ({ ...s, [pane]: s[pane].slice(0, -1) }));
|
|
2386
|
-
if (key.tab) {
|
|
2387
|
-
if (pane === "left") return enterPlace();
|
|
2388
|
-
if (!projectTabs) return;
|
|
2389
|
-
const i = PROJECT_TABS.findIndex((t) => t.id === projectTab);
|
|
2390
|
-
return setProjectTab(PROJECT_TABS[(i + (key.shift ? PROJECT_TABS.length - 1 : 1)) % PROJECT_TABS.length].id);
|
|
2391
|
-
}
|
|
2392
|
-
const onGroup = pane === "right" && current?.key.startsWith("grp:");
|
|
2393
|
-
if (key.rightArrow) {
|
|
2394
|
-
if (pane === "left") return enterPlace();
|
|
2395
|
-
if (onGroup && !expanded.has(current.key)) toggleGroup(current.key);
|
|
2396
|
-
return;
|
|
2397
|
-
}
|
|
2398
|
-
if (key.leftArrow) {
|
|
2399
|
-
if (onGroup && expanded.has(current.key)) return toggleGroup(current.key);
|
|
2400
|
-
return setFocus("left");
|
|
2401
|
-
}
|
|
2402
|
-
if (key.return) {
|
|
2403
|
-
setToast(null);
|
|
2404
|
-
if (pane === "left") return enterPlace();
|
|
2405
|
-
if (current) setSelected((s) => ({ ...s, [rightSelKey]: current.key }));
|
|
2406
|
-
if (current) openMenu(current);
|
|
2407
|
-
return;
|
|
2408
|
-
}
|
|
2409
|
-
if (input === " ") {
|
|
2410
|
-
setToast(null);
|
|
2411
|
-
if (pane === "left") return enterPlace();
|
|
2412
|
-
if (current) setSelected((s) => ({ ...s, [rightSelKey]: current.key }));
|
|
2413
|
-
if (current?.primary) current.primary.run();
|
|
2414
|
-
else if (current) openMenu(current);
|
|
2415
|
-
return;
|
|
2416
|
-
}
|
|
2417
|
-
if (input === "?" && !search[pane]) return setOverlay({ type: "help", topic: 0, scroll: 0 });
|
|
2418
|
-
if (input && !key.ctrl && !key.meta && input.length === 1 && input >= "!") {
|
|
2419
|
-
setToast(null);
|
|
2420
|
-
setSearch((s) => pane === "left" ? { left: s.left + input, right: "" } : { ...s, right: s.right + input });
|
|
2421
|
-
}
|
|
2422
|
-
});
|
|
2423
|
-
const previewHeight = termRows >= 30 ? 9 : termRows >= 22 ? 7 : 0;
|
|
2424
|
-
const bodyHeight = Math.max(6, termRows - 4 - previewHeight);
|
|
2425
|
-
const leftWidth = Math.max(28, Math.min(40, Math.floor(columns * 0.3)));
|
|
2426
|
-
const rightWidth = columns - leftWidth;
|
|
2427
|
-
const placeTitle = place.kind === "project" ? basename3(place.root) : place.kind === "library" ? "Your skills \xB7 not loaded until you add them to a repo" : place.kind === "global" ? "Global \xB7 loaded in every project" : place.kind === "health" ? "Health" : place.kind === "help" ? "Help" : "Settings";
|
|
2428
|
-
const leftIndex = leftRows.findIndex((r) => r.key === leftKey);
|
|
2429
|
-
const rightIndex = rightRows.findIndex((r) => r.key === rightKey);
|
|
2430
|
-
const leftPreview = (() => {
|
|
2431
|
-
if (place.kind === "project") {
|
|
2432
|
-
const rows = snapshot.rows.get(place.root) ?? [];
|
|
2433
|
-
const installed = rows.filter((r) => r.installed);
|
|
2434
|
-
return {
|
|
2435
|
-
title: basename3(place.root),
|
|
2436
|
-
meta: [
|
|
2437
|
-
tildify2(place.root),
|
|
2438
|
-
`${plural2(installed.length, "skill")} installed`,
|
|
2439
|
-
`+ ${plural2(snapshot.machine.filter((m) => !m.broken).length, "global skill")}`
|
|
2440
|
-
],
|
|
2441
|
-
description: installed.length ? installed.map((r) => `${stateBadge(r.state).icon} ${r.name}`).join(" ") : "No skills yet. Press enter, then space on a library skill to add it."
|
|
2442
|
-
};
|
|
2443
|
-
}
|
|
2444
|
-
const about = {
|
|
2445
|
-
library: [
|
|
2446
|
-
"Library",
|
|
2447
|
-
"Your shelf of skills. Nothing here loads anywhere until you add it to a project. Every change becomes a new version projects can update to."
|
|
2448
|
-
],
|
|
2449
|
-
global: [
|
|
2450
|
-
"Global",
|
|
2451
|
-
"Skills your harnesses load in EVERY project (~/.claude/skills, ~/.agents/skills, plugins\u2026). Move yours into the library and add them only where needed."
|
|
2452
|
-
],
|
|
2453
|
-
help: ["Help", "Where skills come from, and how Claude Code, Cursor, Codex and Zed find and use them. Press enter (or ? anywhere)."],
|
|
2454
|
-
health: ["Health", "Broken links, duplicates, copies that differ, plugins that repeat your skills, skills some agents can't reach, out-of-date projects \u2014 and everything skilllib moved out, ready to restore."],
|
|
2455
|
-
settings: ["Settings", `Harnesses: ${snapshot.harnesses.map(harnessName).join(", ") || "none"} \xB7 Folders: ${snapshot.roots.map(tildify2).join(", ") || "none yet"}`]
|
|
2456
|
-
};
|
|
2457
|
-
const [title, description] = about[place.kind] ?? ["", ""];
|
|
2458
|
-
return { title, meta: [], description };
|
|
2459
|
-
})();
|
|
2460
|
-
const preview = focus === "right" && current ? current.preview : leftPreview;
|
|
2461
|
-
const hints = focus === "left" ? [
|
|
2462
|
-
["type", "search"],
|
|
2463
|
-
["\u2191\u2193", "move"],
|
|
2464
|
-
["enter", "open"],
|
|
2465
|
-
["ctrl+c", "quit"]
|
|
2466
|
-
] : [
|
|
2467
|
-
["type", "search"],
|
|
2468
|
-
...current?.primary ? [["space", current.primary.label]] : [],
|
|
2469
|
-
["enter", "actions"],
|
|
2470
|
-
...projectTabs ? [["tab", "next list"]] : [],
|
|
2471
|
-
["esc", "back"]
|
|
2472
|
-
];
|
|
2473
|
-
const header = /* @__PURE__ */ jsxs3(Box3, { height: 1, width: columns, children: [
|
|
2474
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.accent, bold: true, children: " \u25C6 skilllib " }),
|
|
2475
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.faint, children: " " }),
|
|
2476
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.muted, children: place.kind === "project" ? "Projects \u203A " : "" }),
|
|
2477
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.text, bold: true, children: placeTitle }),
|
|
2478
|
-
/* @__PURE__ */ jsx3(Box3, { flexGrow: 1 }),
|
|
2479
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.faint, children: scanning ? "scanning\u2026 " : usage ? "" : "reading usage\u2026 " }),
|
|
2480
|
-
columns >= 110 ? /* @__PURE__ */ jsx3(HarnessLegend, { ids: enabledIds }) : null,
|
|
2481
|
-
problems > 0 ? /* @__PURE__ */ jsx3(Text3, { color: color.red, children: `\u25CF ${plural2(problems, "problem")} ` }) : /* @__PURE__ */ jsx3(Text3, { color: color.green, children: "\u2713 healthy " })
|
|
2482
|
-
] });
|
|
2483
|
-
const renderPreview = () => {
|
|
2484
|
-
if (previewHeight === 0) return null;
|
|
2485
|
-
if (overlay?.type === "menu") {
|
|
2486
|
-
const options = overlay.build();
|
|
2487
|
-
const lines2 = previewHeight - 2;
|
|
2488
|
-
const offset = Math.max(0, Math.min(overlay.index - Math.floor(lines2 / 2), options.length - lines2));
|
|
2489
|
-
return /* @__PURE__ */ jsx3(Panel, { title: overlay.title, hint: "\u2191\u2193 \xB7 enter \xB7 esc", focused: true, width: columns, height: previewHeight, children: options.slice(offset, offset + lines2).map((o, i) => {
|
|
2490
|
-
const active = offset + i === overlay.index;
|
|
2491
|
-
return /* @__PURE__ */ jsxs3(Box3, { height: 1, children: [
|
|
2492
|
-
/* @__PURE__ */ jsx3(Text3, { backgroundColor: active ? color.selection : void 0, color: active ? color.accent : color.faint, children: active ? "\u258C " : " " }),
|
|
2493
|
-
/* @__PURE__ */ jsx3(Text3, { backgroundColor: active ? color.selection : void 0, color: active ? color.text : color.muted, bold: active, children: o.label }),
|
|
2494
|
-
o.hint ? /* @__PURE__ */ jsx3(Text3, { color: color.faint, children: ` ${o.hint}` }) : null
|
|
2495
|
-
] }, o.label + i);
|
|
2496
|
-
}) });
|
|
2497
|
-
}
|
|
2498
|
-
const inner = columns - 6;
|
|
2499
|
-
const lines = previewHeight - 3;
|
|
2500
|
-
if (!preview) {
|
|
2501
|
-
return /* @__PURE__ */ jsx3(Panel, { title: "Details", focused: false, width: columns, height: previewHeight, children: /* @__PURE__ */ jsx3(Text3, { color: color.faint, children: "Nothing selected" }) });
|
|
2502
|
-
}
|
|
2503
|
-
const info = preview.dir ? readPreview(preview.dir) : null;
|
|
2504
|
-
const description = preview.description ?? info?.description ?? "";
|
|
2505
|
-
const room = lines + (preview.meta.length ? 0 : 1);
|
|
2506
|
-
const descLines = description ? wrap(description, inner, Math.min(2, room)) : [];
|
|
2507
|
-
const bodyLines = (info?.body ?? []).filter((l) => !l.startsWith("# ")).slice(0, Math.max(0, room - descLines.length));
|
|
2508
|
-
return /* @__PURE__ */ jsxs3(Panel, { title: preview.title, hint: info ? plural2(info.files, "file") : void 0, focused: false, width: columns, height: previewHeight, children: [
|
|
2509
|
-
preview.meta.length > 0 ? /* @__PURE__ */ jsx3(Text3, { color: color.muted, wrap: "truncate-end", children: preview.meta.join(" \xB7 ") }) : null,
|
|
2510
|
-
descLines.map((l, i) => /* @__PURE__ */ jsx3(Text3, { color: color.text, wrap: "truncate-end", children: l }, `d${i}`)),
|
|
2511
|
-
bodyLines.map((l, i) => /* @__PURE__ */ jsx3(Text3, { color: color.faint, wrap: "truncate-end", children: l }, `b${i}`))
|
|
2512
|
-
] });
|
|
2513
|
-
};
|
|
2514
|
-
const statusLine = (() => {
|
|
2515
|
-
if (overlay?.type === "confirm") {
|
|
2516
|
-
return /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate-end", children: [
|
|
2517
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.yellow, bold: true, children: "? " }),
|
|
2518
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.text, children: overlay.message }),
|
|
2519
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.muted, children: " y / n" })
|
|
2520
|
-
] });
|
|
2521
|
-
}
|
|
2522
|
-
if (overlay?.type === "prompt") {
|
|
2523
|
-
return /* @__PURE__ */ jsxs3(Text3, { wrap: "truncate-end", children: [
|
|
2524
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.accent, bold: true, children: "\u203A " }),
|
|
2525
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.muted, children: `${overlay.label}: ` }),
|
|
2526
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.text, children: overlay.value }),
|
|
2527
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.accent, children: "\u2588" }),
|
|
2528
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.faint, children: " enter ok \xB7 esc cancel" })
|
|
2529
|
-
] });
|
|
2530
|
-
}
|
|
2531
|
-
if (toast) {
|
|
2532
|
-
return /* @__PURE__ */ jsx3(Text3, { wrap: "truncate-end", color: toast.ok ? color.green : color.yellow, children: `${toast.ok ? "\u2713" : "!"} ${toast.text}` });
|
|
2533
|
-
}
|
|
2534
|
-
return /* @__PURE__ */ jsx3(Text3, { children: " " });
|
|
2535
|
-
})();
|
|
2536
|
-
if (wizard) {
|
|
2537
|
-
const yours = snapshot.machine.filter((m) => m.movable && !m.broken);
|
|
2538
|
-
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width: columns, height: termRows, children: [
|
|
2539
|
-
header,
|
|
2540
|
-
/* @__PURE__ */ jsx3(
|
|
2541
|
-
CleanupWizard,
|
|
2542
|
-
{
|
|
2543
|
-
skills: yours,
|
|
2544
|
-
kept: snapshot.keptGlobal,
|
|
2545
|
-
projects: snapshot.projects,
|
|
2546
|
-
usesIn: (skill, project) => usage ? usage.byProject.get(project)?.get(skill) ?? 0 : null,
|
|
2547
|
-
usesTotal: (skill) => usage ? usage.total.get(skill) ?? 0 : null,
|
|
2548
|
-
onReviewPrompt: (skills) => {
|
|
2549
|
-
const prompt = buildReviewPrompt(skills.map(reviewOfGlobal), {
|
|
2550
|
-
scope: "These are my global skills; I'm deciding which to move into specific projects, keep global, or delete.",
|
|
2551
|
-
usageDays: USAGE_DAYS,
|
|
2552
|
-
harnesses: snapshot.harnesses.map(harnessName),
|
|
2553
|
-
projects: snapshot.projects.map((p) => ({ name: basename3(p), path: p }))
|
|
2554
|
-
});
|
|
2555
|
-
const r = copyText(prompt);
|
|
2556
|
-
return r.copied ? `Copied a review prompt for ${plural2(skills.length, "skill")} \u2014 paste it into your agent, then set each skill here` : `Saved the prompt to ${tildify2(r.savedTo)} (clipboard unavailable)`;
|
|
2557
|
-
},
|
|
2558
|
-
usageDays: USAGE_DAYS,
|
|
2559
|
-
installedIn: (skill) => snapshot.library.find((l) => l.name === skill)?.projects ?? [],
|
|
2560
|
-
initialSkill: wizard.initialSkill,
|
|
2561
|
-
inputRef: wizardInput,
|
|
2562
|
-
width: columns,
|
|
2563
|
-
height: termRows - 2,
|
|
2564
|
-
onCancel: () => {
|
|
2565
|
-
setWizard(null);
|
|
2566
|
-
say("Cleanup cancelled \u2014 nothing changed", false);
|
|
2567
|
-
},
|
|
2568
|
-
onDone: (r) => {
|
|
2569
|
-
setWizard(null);
|
|
2570
|
-
reload();
|
|
2571
|
-
setToast({
|
|
2572
|
-
ok: r.problems.length === 0,
|
|
2573
|
-
text: [
|
|
2574
|
-
r.unloaded || r.installs ? `${plural2(r.unloaded, "skill")} moved into projects (${plural2(r.installs, "install")})` : "",
|
|
2575
|
-
r.deleted ? `${r.deleted} deleted` : "",
|
|
2576
|
-
r.kept ? `${plural2(r.kept, "skill")} kept global on purpose` : "",
|
|
2577
|
-
r.problems.length ? `${r.problems.length} left global: ${r.problems[0]}` : ""
|
|
2578
|
-
].filter(Boolean).join(" \xB7 ")
|
|
2579
|
-
});
|
|
2580
|
-
}
|
|
2581
|
-
}
|
|
2582
|
-
)
|
|
2583
|
-
] });
|
|
2584
|
-
}
|
|
2585
|
-
if (setup) {
|
|
2586
|
-
const box = Math.min(84, columns - 4);
|
|
2587
|
-
return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", width: columns, height: termRows, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", borderStyle: "round", borderColor: color.accent, paddingX: 3, paddingY: 1, width: box, children: [
|
|
2588
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.accent, bold: true, children: "\u25C6 Welcome to skilllib" }),
|
|
2589
|
-
/* @__PURE__ */ jsx3(Text3, { children: " " }),
|
|
2590
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.text, children: "Manage agent skills like dependencies: one versioned library, and each project picks what it needs." }),
|
|
2591
|
-
/* @__PURE__ */ jsx3(Text3, { children: " " }),
|
|
2592
|
-
setup.step === "harnesses" ? /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
2593
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.text, bold: true, children: "1 \xB7 Which coding agents do you use?" }),
|
|
2594
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.faint, children: "skilllib installs skills where each of them looks." }),
|
|
2595
|
-
/* @__PURE__ */ jsx3(Text3, { children: " " }),
|
|
2596
|
-
HARNESSES.map((h, i) => {
|
|
2597
|
-
const on = setup.selected.includes(h.id);
|
|
2598
|
-
const active = i === setup.index;
|
|
2599
|
-
return /* @__PURE__ */ jsxs3(Box3, { height: 1, children: [
|
|
2600
|
-
/* @__PURE__ */ jsx3(Text3, { color: active ? color.accent : color.faint, children: active ? "\u258C " : " " }),
|
|
2601
|
-
/* @__PURE__ */ jsx3(Text3, { color: on ? color.green : color.faint, children: on ? "\u25C9 " : "\u25CB " }),
|
|
2602
|
-
/* @__PURE__ */ jsx3(Text3, { color: h.color, children: `${h.icon} ` }),
|
|
2603
|
-
/* @__PURE__ */ jsx3(Text3, { color: active ? color.text : color.muted, bold: active, children: h.name.padEnd(13) }),
|
|
2604
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.faint, wrap: "truncate-end", children: `${h.installed() ? "detected \xB7 " : ""}reads ${h.projectDirs.map((d) => d.replace("/skills", "")).join(" ")}` })
|
|
2605
|
-
] }, h.id);
|
|
2606
|
-
}),
|
|
2607
|
-
/* @__PURE__ */ jsx3(Text3, { children: " " }),
|
|
2608
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.faint, children: "\u2191\u2193 move \xB7 space toggle \xB7 enter continue" })
|
|
2609
|
-
] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
2610
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.text, bold: true, children: "2 \xB7 Where do your projects live?" }),
|
|
2611
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.faint, children: "skilllib finds every git repo inside and rescans it each time it opens." }),
|
|
2612
|
-
/* @__PURE__ */ jsxs3(Box3, { borderStyle: "round", borderColor: color.accentDim, paddingX: 1, children: [
|
|
2613
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.text, children: setup.value }),
|
|
2614
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.accent, children: "\u2588" })
|
|
2615
|
-
] }),
|
|
2616
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.faint, children: "enter to scan \xB7 esc to skip \xB7 change both later in Settings" })
|
|
2617
|
-
] })
|
|
2618
|
-
] }) });
|
|
2619
|
-
}
|
|
2620
|
-
if (overlay?.type === "help") {
|
|
2621
|
-
const topicWidth = 30;
|
|
2622
|
-
const contentWidth = columns - topicWidth - 4;
|
|
2623
|
-
const topic = HELP_TOPICS[overlay.topic];
|
|
2624
|
-
const lines = [];
|
|
2625
|
-
for (const block of topic.blocks) {
|
|
2626
|
-
if (lines.length) lines.push({ text: "", kind: "gap" });
|
|
2627
|
-
if (block.heading) lines.push({ text: block.heading, kind: "heading" });
|
|
2628
|
-
for (const line of block.lines) {
|
|
2629
|
-
for (const w of wrapAligned(line, contentWidth - 2)) lines.push({ text: w, kind: "body" });
|
|
2630
|
-
}
|
|
2631
|
-
}
|
|
2632
|
-
const room = termRows - 5;
|
|
2633
|
-
const scroll = Math.min(overlay.scroll, Math.max(0, lines.length - room));
|
|
2634
|
-
const topicRows = HELP_TOPICS.map((t) => {
|
|
2635
|
-
const h = HARNESSES.find((x) => x.id === t.id);
|
|
2636
|
-
return {
|
|
2637
|
-
key: t.id,
|
|
2638
|
-
cells: [
|
|
2639
|
-
{ text: `${t.icon ?? "\xB7"} `, color: h?.color ?? color.accent },
|
|
2640
|
-
{ text: t.title, grow: true, bold: true }
|
|
2641
|
-
]
|
|
2642
|
-
};
|
|
2643
|
-
});
|
|
2644
|
-
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width: columns, height: termRows, children: [
|
|
2645
|
-
header,
|
|
2646
|
-
/* @__PURE__ */ jsxs3(Box3, { height: termRows - 2, children: [
|
|
2647
|
-
/* @__PURE__ */ jsx3(ListPanel, { title: "Help", focused: true, width: topicWidth, height: termRows - 2, rows: topicRows, selected: overlay.topic, empty: "" }),
|
|
2648
|
-
/* @__PURE__ */ jsx3(
|
|
2649
|
-
Panel,
|
|
2650
|
-
{
|
|
2651
|
-
title: topic.title,
|
|
2652
|
-
hint: lines.length > room ? `${scroll + 1}\u2013${Math.min(lines.length, scroll + room)} of ${lines.length} \xB7 pgdn` : void 0,
|
|
2653
|
-
focused: false,
|
|
2654
|
-
width: columns - topicWidth,
|
|
2655
|
-
height: termRows - 2,
|
|
2656
|
-
children: lines.slice(scroll, scroll + room).map(
|
|
2657
|
-
(l, i) => l.kind === "heading" ? /* @__PURE__ */ jsx3(Text3, { color: color.accent, bold: true, wrap: "truncate-end", children: l.text }, i) : /* @__PURE__ */ jsx3(Text3, { color: l.kind === "gap" ? void 0 : color.text, wrap: "truncate-end", children: l.text || " " }, i)
|
|
2658
|
-
)
|
|
2659
|
-
}
|
|
2660
|
-
)
|
|
2661
|
-
] }),
|
|
2662
|
-
/* @__PURE__ */ jsx3(Box3, { height: 1, paddingX: 1, children: /* @__PURE__ */ jsx3(
|
|
2663
|
-
KeyBar,
|
|
2664
|
-
{
|
|
2665
|
-
hints: [
|
|
2666
|
-
["\u2191\u2193", "topic"],
|
|
2667
|
-
["pgdn", "scroll"],
|
|
2668
|
-
["esc", "close"]
|
|
2669
|
-
],
|
|
2670
|
-
width: columns - 2
|
|
2671
|
-
}
|
|
2672
|
-
) })
|
|
2673
|
-
] });
|
|
2674
|
-
}
|
|
2675
|
-
const TAB_ABOUT = {
|
|
2676
|
-
usable: "Everything agents see in this repo, grouped by where it comes from. \u26A0 global ones load in every repo.",
|
|
2677
|
-
add: "Your skills that this repo doesn't have yet. Space adds one."
|
|
2678
|
-
};
|
|
2679
|
-
const tabCount = (tab) => (projectTabs?.[tab] ?? []).flatMap((sec) => sec.items).filter((i) => !i.key.startsWith("__")).length;
|
|
2680
|
-
const tabMatches = (tab) => (projectTabs?.[tab] ?? []).flatMap((sec) => sec.items).filter((i) => !i.key.startsWith("__") && matchScore(i.search.split(" ")[0] ?? "", search.right) !== null).length;
|
|
2681
|
-
const elsewhere = projectTabs && search.right ? PROJECT_TABS.filter((t) => t.id !== projectTab && tabMatches(t.id) > 0).map((t) => `${tabMatches(t.id)} in ${t.label} (tab)`).join(", ") : "";
|
|
2682
|
-
const tabBar = projectTabs ? /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", height: 3, children: [
|
|
2683
|
-
/* @__PURE__ */ jsxs3(Box3, { height: 1, children: [
|
|
2684
|
-
PROJECT_TABS.map((t) => {
|
|
2685
|
-
const active = t.id === projectTab;
|
|
2686
|
-
const count = search.right ? tabMatches(t.id) : tabCount(t.id);
|
|
2687
|
-
return /* @__PURE__ */ jsx3(Box3, { marginRight: 1, children: /* @__PURE__ */ jsx3(Text3, { backgroundColor: active ? color.accent : void 0, color: active ? "#0B1020" : color.muted, bold: active, children: ` ${t.label} ${count} ` }) }, t.id);
|
|
2688
|
-
}),
|
|
2689
|
-
/* @__PURE__ */ jsx3(Box3, { flexGrow: 1 }),
|
|
2690
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.faint, children: focus === "right" ? "tab \u21E5 next list" : "" })
|
|
2691
|
-
] }),
|
|
2692
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.muted, wrap: "truncate-end", children: TAB_ABOUT[projectTab] }),
|
|
2693
|
-
/* @__PURE__ */ jsx3(Text3, { color: color.faint, wrap: "truncate-end", children: search.right && elsewhere ? `also matching: ${elsewhere}` : " " })
|
|
2694
|
-
] }) : null;
|
|
2695
|
-
const paneHint = (pane) => search[pane] ? `\u2315 ${search[pane]}` : "";
|
|
2696
|
-
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width: columns, height: termRows, children: [
|
|
2697
|
-
header,
|
|
2698
|
-
/* @__PURE__ */ jsxs3(Box3, { height: bodyHeight, children: [
|
|
2699
|
-
/* @__PURE__ */ jsx3(
|
|
2700
|
-
ListPanel,
|
|
2701
|
-
{
|
|
2702
|
-
title: "Places",
|
|
2703
|
-
focused: focus === "left",
|
|
2704
|
-
width: leftWidth,
|
|
2705
|
-
height: bodyHeight,
|
|
2706
|
-
rows: leftRows,
|
|
2707
|
-
selected: Math.max(0, leftIndex),
|
|
2708
|
-
filter: paneHint("left"),
|
|
2709
|
-
empty: `Nothing matches "${search.left}"`
|
|
2710
|
-
}
|
|
2711
|
-
),
|
|
2712
|
-
/* @__PURE__ */ jsx3(
|
|
2713
|
-
ListPanel,
|
|
2714
|
-
{
|
|
2715
|
-
title: placeTitle,
|
|
2716
|
-
header: tabBar,
|
|
2717
|
-
headerHeight: tabBar ? 3 : 0,
|
|
2718
|
-
focused: focus === "right",
|
|
2719
|
-
width: rightWidth,
|
|
2720
|
-
height: bodyHeight,
|
|
2721
|
-
rows: rightRows,
|
|
2722
|
-
selected: Math.max(0, rightIndex),
|
|
2723
|
-
filter: paneHint("right"),
|
|
2724
|
-
empty: search.right ? `Nothing matches "${search.right}" here.${elsewhere ? ` ${elsewhere}` : ""}` : place.kind === "project" ? projectTab === "usable" ? "No skills here yet. Press tab to add some from Your skills." : snapshot.library.length ? "Every one of your skills is already in this repo." : "You have no saved skills yet. Copy one from a project (space on it) or clean up your globals." : place.kind === "health" ? "\u2713 Everything looks good." : place.kind === "help" ? "Press enter to open help." : "Nothing here yet."
|
|
2725
|
-
}
|
|
2726
|
-
)
|
|
2727
|
-
] }),
|
|
2728
|
-
renderPreview(),
|
|
2729
|
-
/* @__PURE__ */ jsx3(Box3, { height: 1, paddingX: 1, children: statusLine }),
|
|
2730
|
-
/* @__PURE__ */ jsx3(Box3, { height: 1, paddingX: 1, children: /* @__PURE__ */ jsx3(KeyBar, { hints: [...hints, ["?", "help"]], width: columns - 2 }) })
|
|
2731
|
-
] });
|
|
2732
|
-
}
|
|
2733
|
-
|
|
2734
|
-
// src/tui/index.tsx
|
|
2735
|
-
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
2736
|
-
async function tui() {
|
|
2737
|
-
const instance = render(/* @__PURE__ */ jsx4(App, {}), { alternateScreen: true, exitOnCtrlC: true });
|
|
2738
|
-
await instance.waitUntilExit();
|
|
2739
|
-
}
|
|
2740
|
-
export {
|
|
2741
|
-
tui
|
|
2742
|
-
};
|