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.
@@ -0,0 +1,2184 @@
1
+ import {
2
+ HARNESSES,
3
+ addRoot,
4
+ addSkill,
5
+ allowTrackedLinks,
6
+ claudeBinary,
7
+ createSkill,
8
+ deleteGlobal,
9
+ deleteLibrarySkill,
10
+ discoverProjects,
11
+ enabledHarnesses,
12
+ findProjectRoot,
13
+ forgetLatest,
14
+ gitInfo,
15
+ harness,
16
+ harnessesChosen,
17
+ importSkill,
18
+ isProjectCandidate,
19
+ keptGlobal,
20
+ latestVersion,
21
+ libraryOrigins,
22
+ librarySkillDir,
23
+ librarySkills,
24
+ linkEverywhere,
25
+ listBackups,
26
+ machineSkills,
27
+ pluginBackups,
28
+ projectOfFactory,
29
+ projectStatus,
30
+ projectsUsing,
31
+ readConfig,
32
+ readSkillInfo,
33
+ recordOrigin,
34
+ recordRemovedPlugin,
35
+ relativeTo,
36
+ relinkDependency,
37
+ removeRoot,
38
+ removeSkill,
39
+ removeUntracked,
40
+ restoreBackup,
41
+ restorePlugin,
42
+ scanUsage,
43
+ setHarnesses,
44
+ setHidden,
45
+ setKeepGlobal,
46
+ setPluginEnabled,
47
+ skilllibHome,
48
+ syncProject,
49
+ unloadGlobal,
50
+ updateProject,
51
+ userHome,
52
+ versionHistory,
53
+ visibleProjects
54
+ } from "./chunk-TWQQHXVG.js";
55
+
56
+ // src/tui/index.tsx
57
+ import { render } from "ink";
58
+
59
+ // src/tui/App.tsx
60
+ import { useEffect, useRef, useState } from "react";
61
+ import { Box as Box2, Text as Text2, useApp, useInput, useWindowSize } from "ink";
62
+ import { spawnSync as spawnSync2 } from "child_process";
63
+ import { existsSync as existsSync2 } from "fs";
64
+
65
+ // src/tui/components.tsx
66
+ import { Box, Text } from "ink";
67
+
68
+ // src/tui/theme.ts
69
+ var color = {
70
+ accent: "#8B9DFF",
71
+ accentDim: "#4B5578",
72
+ text: "#E5E7EB",
73
+ muted: "#8A93A6",
74
+ faint: "#5A6275",
75
+ green: "#6BCB77",
76
+ yellow: "#F2C94C",
77
+ red: "#EF6B6B",
78
+ magenta: "#C792EA",
79
+ blue: "#5EB8F7",
80
+ selection: "#2A3350"
81
+ };
82
+
83
+ // src/tui/components.tsx
84
+ import { jsx, jsxs } from "react/jsx-runtime";
85
+ function fit(text, width) {
86
+ if (width <= 0) return "";
87
+ return text.length > width ? text.slice(0, Math.max(0, width - 1)) + "\u2026" : text.padEnd(width);
88
+ }
89
+ function Panel({
90
+ title,
91
+ hint,
92
+ focused,
93
+ width,
94
+ height,
95
+ children
96
+ }) {
97
+ const border = focused ? color.accent : color.accentDim;
98
+ const label = ` ${title} `;
99
+ const extra = hint ? ` ${hint} ` : "";
100
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, height, borderStyle: "round", borderColor: border, paddingX: 1, children: [
101
+ /* @__PURE__ */ jsxs(Box, { position: "absolute", marginTop: -1, marginLeft: 1, children: [
102
+ /* @__PURE__ */ jsx(Text, { color: focused ? color.accent : color.muted, bold: true, children: fit(label, Math.max(0, width - 6 - extra.length)).trimEnd() + " " }),
103
+ extra ? /* @__PURE__ */ jsx(Text, { color: color.faint, children: extra }) : null
104
+ ] }),
105
+ children
106
+ ] });
107
+ }
108
+ function ListPanel({
109
+ title,
110
+ focused,
111
+ width,
112
+ height,
113
+ rows,
114
+ selected,
115
+ empty,
116
+ filter,
117
+ header,
118
+ headerHeight = 0
119
+ }) {
120
+ const visible = Math.max(1, height - 2 - headerHeight);
121
+ const offset = Math.min(Math.max(0, selected - Math.floor(visible / 2)), Math.max(0, rows.length - visible));
122
+ const shown = rows.slice(offset, offset + visible);
123
+ const inner = width - 4;
124
+ const hint = [filter ?? "", rows.length > visible ? `${selected + 1}/${rows.length}` : ""].filter(Boolean).join(" ");
125
+ return /* @__PURE__ */ jsxs(Panel, { title, hint, focused, width, height, children: [
126
+ header,
127
+ 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) => {
128
+ if (row.header) {
129
+ const label = row.cells.map((c) => c.text).join("");
130
+ return /* @__PURE__ */ jsxs(Box, { height: 1, children: [
131
+ /* @__PURE__ */ jsx(Text, { color: color.accent, bold: true, children: " " + label + " " }),
132
+ /* @__PURE__ */ jsx(Text, { color: color.accentDim, children: "\u2500".repeat(Math.max(0, inner - label.length - 3)) })
133
+ ] }, row.key);
134
+ }
135
+ const active = offset + i === selected;
136
+ const fixed = row.cells.filter((c) => !c.grow).reduce((n, c) => n + (c.width ?? c.text.length), 0);
137
+ const growWidth = Math.max(3, inner - 2 - fixed);
138
+ const bg = active ? focused ? color.selection : void 0 : void 0;
139
+ return /* @__PURE__ */ jsxs(Box, { height: 1, children: [
140
+ /* @__PURE__ */ jsx(Text, { backgroundColor: bg, color: active && focused ? color.accent : color.faint, children: active ? "\u258C " : " " }),
141
+ row.cells.map((c, j) => /* @__PURE__ */ jsx(
142
+ Text,
143
+ {
144
+ backgroundColor: bg,
145
+ color: c.color ?? (active && focused ? color.text : void 0),
146
+ bold: c.bold || active && focused && c.grow,
147
+ dimColor: c.dim,
148
+ wrap: "truncate-end",
149
+ children: fit(c.text, c.grow ? growWidth : c.width ?? c.text.length)
150
+ },
151
+ j
152
+ ))
153
+ ] }, row.key);
154
+ })
155
+ ] });
156
+ }
157
+ function wrap(text, width, max) {
158
+ const words = text.replace(/\s+/g, " ").trim().split(" ");
159
+ const lines = [];
160
+ let line = "";
161
+ for (const word2 of words) {
162
+ if ((line + " " + word2).trim().length > width) {
163
+ lines.push(line);
164
+ line = word2;
165
+ if (lines.length === max) break;
166
+ } else line = (line + " " + word2).trim();
167
+ }
168
+ if (lines.length < max && line) lines.push(line);
169
+ if (lines.length === max && words.join(" ").length > lines.join(" ").length) {
170
+ lines[max - 1] = fit(lines[max - 1], width - 1).trimEnd() + "\u2026";
171
+ }
172
+ return lines;
173
+ }
174
+
175
+ // src/review.ts
176
+ import { spawnSync } from "child_process";
177
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
178
+ import { join } from "path";
179
+ var EXCERPT_CHARS = 1200;
180
+ function tildify(p) {
181
+ const home = userHome();
182
+ return p.startsWith(home) ? "~" + p.slice(home.length) : p;
183
+ }
184
+ function skillExcerpt(dir, chars) {
185
+ const file = join(dir, "SKILL.md");
186
+ if (!existsSync(file)) return { text: "(SKILL.md not found)", files: 0, size: 0 };
187
+ const content = readFileSync(file, "utf-8").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
188
+ let files = 0;
189
+ try {
190
+ files = readdirSync(dir, { recursive: true, withFileTypes: true }).filter((d) => d.isFile()).length;
191
+ } catch {
192
+ files = 1;
193
+ }
194
+ return { text: content.length > chars ? content.slice(0, chars) + "\n\u2026(truncated)" : content, files, size: content.length };
195
+ }
196
+ function buildReviewPrompt(skills, ctx) {
197
+ const many = skills.length > 1;
198
+ const excerptChars = many ? Math.max(300, Math.floor(12e3 / skills.length)) : EXCERPT_CHARS * 3;
199
+ const sections = skills.map((s, i) => {
200
+ const ex = skillExcerpt(s.path, excerptChars);
201
+ 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(", ")})` : ""}`;
202
+ return [
203
+ `### ${many ? `${i + 1}. ` : ""}${s.name}`,
204
+ `- Description: ${s.description || "(none)"}`,
205
+ `- Source: ${s.source}`,
206
+ `- Installed via: ${s.installedHow}`,
207
+ `- Location: ${tildify(s.path)}${s.links.length ? ` (also linked from ${s.links.map(tildify).join(", ")})` : ""}`,
208
+ `- Loaded by: ${s.loadedBy}`,
209
+ ...s.vendor ? [`- Vendor-managed: ${s.vendor}`] : [],
210
+ ...s.keptGlobal ? ["- I marked this as global on purpose: recommend changing that only for a strong reason (unused, duplicated, low quality)"] : [],
211
+ `- Usage (Claude Code transcripts only): ${usage}`,
212
+ `- Installed in projects: ${s.installedIn.length ? s.installedIn.join(", ") : "none"}`,
213
+ `- Other copies agents can see: ${s.otherCopies.length ? s.otherCopies.join("; ") : "none"}`,
214
+ `- In Your skills (skilllib library): ${s.inYourSkills ?? "no"}`,
215
+ `- Size: ${ex.files} file${ex.files === 1 ? "" : "s"}, SKILL.md ${ex.size} chars`,
216
+ "",
217
+ "SKILL.md body (excerpt):",
218
+ "```markdown",
219
+ ex.text.trim(),
220
+ "```"
221
+ ].join("\n");
222
+ });
223
+ return `You are reviewing Claude Code / agent skills installed on my machine to decide which ones to keep. ${ctx.scope}
224
+
225
+ ## Background
226
+ - 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.
227
+ - "Global" skills load in EVERY repo for the listed agents. Project skills only load inside that repo.
228
+ - I use these agents: ${ctx.harnesses.join(", ") || "(none configured)"}.
229
+ - 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.
230
+ - 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.
231
+
232
+ ## My projects
233
+ ${ctx.projects.map((p) => `- ${p.name} \u2014 ${tildify(p.path)}`).join("\n") || "(none)"}
234
+
235
+ 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).
236
+
237
+ ## Skills to review
238
+ ${sections.join("\n\n")}
239
+
240
+ ## What I want back
241
+ For each skill, recommend exactly one of:
242
+ - **keep global**: genuinely useful in almost every repo
243
+ - **move to projects**: only relevant to some; name them from my project list
244
+ - **delete**: unused, redundant, low quality, or superseded by another copy
245
+ - **vendor: turn off at source**: say how (e.g. /plugin in Claude Code)
246
+
247
+ 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.
248
+
249
+ Reply with a short table (skill \xB7 decision \xB7 projects \xB7 one-line reason), then this JSON so I can apply it:
250
+
251
+ \`\`\`json
252
+ [{ "skill": "name", "decision": "keep-global | move | delete | vendor-off", "projects": ["project-name"], "reason": "\u2026" }]
253
+ \`\`\`
254
+ `;
255
+ }
256
+ function copyText(text) {
257
+ const savedTo = join(skilllibHome(), "review-prompt.md");
258
+ mkdirSync(skilllibHome(), { recursive: true });
259
+ writeFileSync(savedTo, text);
260
+ const tools = process.platform === "darwin" ? [["pbcopy", []]] : process.platform === "win32" ? [["clip", []]] : [
261
+ ["wl-copy", []],
262
+ ["xclip", ["-selection", "clipboard"]],
263
+ ["xsel", ["--clipboard", "--input"]]
264
+ ];
265
+ for (const [cmd, args] of tools) {
266
+ const r = spawnSync(cmd, args, { input: text, stdio: ["pipe", "ignore", "ignore"] });
267
+ if (r.status === 0) return { copied: true, savedTo };
268
+ }
269
+ return { copied: false, savedTo };
270
+ }
271
+
272
+ // src/search.ts
273
+ function matchScore(text, query) {
274
+ if (!query) return 0;
275
+ const t = text.toLowerCase();
276
+ const q = query.toLowerCase();
277
+ if (t.startsWith(q)) return 0;
278
+ if (t.includes(q)) return 1;
279
+ const initials = t.split(/[-_\s./]+/).filter(Boolean).map((w) => w[0]).join("");
280
+ return q.length >= 2 && initials.startsWith(q) ? 2 : null;
281
+ }
282
+
283
+ // src/tui/world.ts
284
+ var SEVERITY_RANK = { problem: 0, warning: 1, hint: 2 };
285
+ function filterOf(source) {
286
+ return source === "lib" || source === "repo" || source === "untracked" ? "local" : source === "global" ? "global" : source === "plugin" ? "plugins" : "vendor";
287
+ }
288
+ function project(w, name) {
289
+ return w.projects.find((p) => p.name === name);
290
+ }
291
+ function totalUses(w, name) {
292
+ return Object.values(w.usage).reduce((n, u) => n + (u[name] ?? 0), 0);
293
+ }
294
+ function dailyUses(w, name, onlyRepo) {
295
+ const days = new Array(30).fill(0);
296
+ for (const [repo, bySkill] of Object.entries(w.days ?? {})) {
297
+ if (onlyRepo && repo !== onlyRepo) continue;
298
+ bySkill[name]?.forEach((n, i) => days[i] += n);
299
+ }
300
+ return days;
301
+ }
302
+ function usable(w, projectName) {
303
+ const p = project(w, projectName);
304
+ const uses = (n) => w.usage[projectName]?.[n] ?? 0;
305
+ return [
306
+ ...p.skills.map((s) => ({ name: s.name, source: s.source, where: s.dir, agents: s.agents, uses: uses(s.name), local: s })),
307
+ ...w.machine.map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: uses(m.name), machine: m }))
308
+ ];
309
+ }
310
+ function lib(w, name) {
311
+ return w.library.find((l) => l.name === name);
312
+ }
313
+ function globalFixes(m) {
314
+ return [
315
+ {
316
+ label: "Move it to the repos that need it\u2026",
317
+ preview: `Pick repos. ${m.name} goes into your library and those repos, and stops loading globally (original backed up).`,
318
+ run: () => "",
319
+ pickRepos: (w, repos) => w.ops.moveGlobal(m, repos)
320
+ },
321
+ m.kept ? { label: "Stop marking it as global on purpose", preview: `skilllib warns about ${m.name} again, like any global skill.`, run: (w) => w.ops.keepGlobal(m.name, false) } : { label: "Keep it global on purpose", preview: `Mark ${m.name} as global on purpose. skilllib stops warning about it.`, run: (w) => w.ops.keepGlobal(m.name, true) },
322
+ { label: "Delete it", preview: `Remove ${m.path}. A backup is kept in Settings \u203A Backups.`, run: (w) => w.ops.deleteGlobal(m) }
323
+ ];
324
+ }
325
+ function machineActions(m) {
326
+ if (m.broken) return [];
327
+ return m.source === "global" ? globalFixes(m) : [];
328
+ }
329
+ function machineIssues(w, m) {
330
+ if (m.broken)
331
+ return [
332
+ {
333
+ id: `broken:${m.path}`,
334
+ severity: "problem",
335
+ title: "Broken link: points at a folder that no longer exists",
336
+ short: "Broken link",
337
+ decision: false,
338
+ fixes: [{ label: "Remove the link", preview: `Remove ${m.path}.`, run: (w2) => w2.ops.deleteGlobal(m) }]
339
+ }
340
+ ];
341
+ const issues = [];
342
+ const other = w.machine.find((x) => x !== m && x.name === m.name && !x.broken && x.agents.some((a) => m.agents.includes(a)));
343
+ if (m.source === "global" && other && other.source !== "global")
344
+ issues.push({
345
+ id: `dup-plugin:${m.name}`,
346
+ severity: "problem",
347
+ title: `Loaded twice: also in ${other.source} ${other.where}`,
348
+ short: "Loaded twice",
349
+ decision: false,
350
+ fixes: [
351
+ {
352
+ label: `Keep the ${other.source}'s copy, stop loading yours globally`,
353
+ preview: `Copy ${m.name} into your library if needed, then move ${m.path} to the backups.`,
354
+ run: (w2) => w2.ops.unloadGlobal(m)
355
+ }
356
+ ]
357
+ });
358
+ const twins = w.machine.filter((x) => x.source === "global" && x.name === m.name && !x.broken && x.agents.some((a) => m.agents.includes(a)));
359
+ if (m.source === "global" && twins.length > 1)
360
+ issues.push({
361
+ id: `dup-global:${m.name}`,
362
+ severity: "problem",
363
+ title: `Loaded twice: in ${twins.map((t) => t.where).join(" and ")}`,
364
+ short: "Loaded twice",
365
+ decision: true,
366
+ fixes: twins.map((t) => ({ label: `Delete the copy in ${t.where}`, preview: `Move ${t.path} to Settings \u203A Backups; the other copy stays.`, run: (w2) => w2.ops.deleteGlobal(t) }))
367
+ });
368
+ if (m.source === "global" && !m.kept)
369
+ issues.push({ id: `global:${m.name}`, severity: "warning", title: "Global, not reviewed: loads in every repo", short: "Not reviewed", decision: true, fixes: globalFixes(m) });
370
+ return issues;
371
+ }
372
+ function issuesOf(w, projectName, u) {
373
+ const s = u.local;
374
+ const issues = [];
375
+ const shares = (m) => m.name === s.name && !m.broken && m.agents.some((a) => s.agents.includes(a));
376
+ const g = w.machine.find((m) => m.source === "global" && shares(m));
377
+ const vendor = w.machine.find((m) => m.source !== "global" && shares(m));
378
+ const l = lib(w, s.name);
379
+ const remove = {
380
+ label: "Remove this repo's copy",
381
+ preview: s.source === "lib" ? `Delete ${s.dir}/${s.name} in ${projectName}. Your library keeps it.` : `Move ${s.dir}/${s.name} in ${projectName} to Settings \u203A Backups.`,
382
+ run: (w2) => w2.ops.remove(projectName, s.name)
383
+ };
384
+ if (s.missing)
385
+ issues.push({
386
+ id: `missing:${s.name}`,
387
+ severity: "problem",
388
+ title: "Listed in skilllib.json but the folder is missing",
389
+ short: "Folder missing",
390
+ decision: false,
391
+ fixes: [{ label: "Restore it (sync)", preview: `Reinstall ${s.name} v${s.version} into ${s.dir}.`, run: (w2) => w2.ops.restore(projectName, s.name) }]
392
+ });
393
+ if (s.source === "lib" && s.library === "missing")
394
+ issues.push({
395
+ id: `not-in-lib:${s.name}`,
396
+ severity: "problem",
397
+ title: "Tracked here, but no longer in your library",
398
+ short: "Not in your library",
399
+ decision: false,
400
+ fixes: [{ label: "Copy it back into your library", preview: `Copy this repo's ${s.name} into your library, so it can be restored and shared again.`, run: (w2) => w2.ops.copyToLibrary(projectName, s.name) }]
401
+ });
402
+ if (g)
403
+ issues.push({
404
+ id: `twice-g:${s.name}`,
405
+ severity: "problem",
406
+ title: `Loaded twice: also global in ${g.where}`,
407
+ short: "Loaded twice",
408
+ decision: false,
409
+ fixes: [
410
+ {
411
+ label: "Keep this repo's copy, stop loading it globally",
412
+ preview: `Copy ${s.name} into your library if needed, then move ${g.path} to the backups. Other repos stop seeing it.`,
413
+ run: (w2) => w2.ops.unloadGlobal(g)
414
+ },
415
+ ...s.source !== "repo" ? [{ ...remove, label: "Keep it global, remove it from this repo" }] : []
416
+ ]
417
+ });
418
+ if (vendor)
419
+ issues.push({
420
+ id: `twice-p:${s.name}`,
421
+ severity: "problem",
422
+ title: `Same name as a skill in ${vendor.source} ${vendor.where}`,
423
+ short: `Same name as a ${vendor.source} skill`,
424
+ decision: true,
425
+ fixes: [
426
+ ...s.source !== "repo" ? [{ ...remove, label: `Remove this repo's copy, use the ${vendor.source}'s` }] : [],
427
+ {
428
+ label: vendor.source === "plugin" ? "Turn the plugin off with /plugin in Claude Code" : `Turn it off in ${vendor.where}`,
429
+ preview: "skilllib can't turn it off for you yet.",
430
+ run: () => `Turn ${vendor.where} off at its source; skilllib picks it up next time`
431
+ }
432
+ ]
433
+ });
434
+ if (s.source === "lib" && s.edited)
435
+ issues.push({
436
+ id: `edited:${s.name}`,
437
+ severity: "warning",
438
+ title: `Edited here: differs from library v${s.version}`,
439
+ short: "Edited here",
440
+ decision: true,
441
+ fixes: [
442
+ { label: `Save as v${(l?.latest ?? 0) + 1} in your library`, preview: `Your library gets v${(l?.latest ?? 0) + 1} with these edits. Other repos can update to it.`, run: (w2) => w2.ops.saveEdits(projectName, s.name) },
443
+ { label: "Discard the edits", preview: `Replace ${s.dir}/${s.name} with library v${s.version} (edits backed up).`, run: (w2) => w2.ops.discardEdits(projectName, s.name) }
444
+ ]
445
+ });
446
+ if (s.source === "lib" && l && s.version < l.latest && !s.edited && !s.missing)
447
+ issues.push({
448
+ id: `outdated:${s.name}`,
449
+ severity: "warning",
450
+ title: `Update available: v${s.version} \u2192 v${l.latest}`,
451
+ short: `Update to v${l.latest}`,
452
+ decision: false,
453
+ fixes: [{ label: `Update to v${l.latest}`, preview: `Replace ${s.dir}/${s.name} with library v${l.latest}.`, run: (w2) => w2.ops.update(projectName, s.name) }]
454
+ });
455
+ const blind = w.agents.filter((a) => !u.agents.includes(a));
456
+ if (!s.missing && blind.length)
457
+ issues.push({
458
+ id: `blind:${s.name}`,
459
+ severity: "warning",
460
+ title: `${blind.map((a) => harness(a).name).join(", ")} can't see it`,
461
+ short: `${blind.map((a) => harness(a).name).join(", ")} can't see it`,
462
+ decision: false,
463
+ fixes: [
464
+ {
465
+ label: "Link it for every agent",
466
+ preview: `Add links (nothing is copied or moved), so ${blind.map((a) => harness(a).name).join(", ")} load it too.`,
467
+ run: (w2) => w2.ops.link(projectName, s.name)
468
+ }
469
+ ]
470
+ });
471
+ if (s.source !== "lib" && s.library === "differs")
472
+ issues.push({
473
+ id: `differs:${s.name}`,
474
+ severity: "hint",
475
+ title: "Differs from the copy in your library",
476
+ short: "Differs from library",
477
+ decision: true,
478
+ fixes: [
479
+ { label: "Update your library from this copy", preview: `Your library gets a new version of ${s.name} with this repo's content.`, run: (w2) => w2.ops.copyToLibrary(projectName, s.name) },
480
+ ...s.source === "untracked" ? [{ label: "Replace it with the library version", preview: `Replace ${s.dir}/${s.name} with your library's newest version and track it (this copy backed up).`, run: (w2) => w2.ops.discardEdits(projectName, s.name) }] : []
481
+ ]
482
+ });
483
+ if (s.source === "untracked" && s.library === "same")
484
+ issues.push({
485
+ id: `adopt:${s.name}`,
486
+ severity: "hint",
487
+ title: "Same as your library skill, but not tracked",
488
+ short: "Not tracked",
489
+ decision: false,
490
+ fixes: [{ label: "Track it", preview: `Record ${s.name} in skilllib.json so library updates reach it.`, run: (w2) => w2.ops.track(projectName, s.name) }]
491
+ });
492
+ if (s.source === "untracked" && !l && !vendor)
493
+ issues.push({
494
+ id: `local:${s.name}`,
495
+ severity: "hint",
496
+ title: "Only exists in this repo",
497
+ short: "Only in this repo",
498
+ decision: false,
499
+ fixes: [
500
+ {
501
+ label: "Import it into your library",
502
+ preview: `Copy ${s.name} into your library and track it here, so other repos can use it.`,
503
+ run: (w2) => w2.ops.importLocal(projectName, s.name)
504
+ }
505
+ ]
506
+ });
507
+ if (s.source === "lib" && w.days && u.uses === 0 && !s.missing)
508
+ issues.push({
509
+ id: `unused:${s.name}`,
510
+ severity: "hint",
511
+ title: "Not used here in 30 days (Claude Code)",
512
+ short: "Unused 30 days",
513
+ decision: true,
514
+ fixes: [{ ...remove, label: "Remove it from this repo", preview: `Delete ${s.dir}/${s.name} in ${projectName}. Your library keeps it.` }]
515
+ });
516
+ return issues;
517
+ }
518
+ function worst(issues) {
519
+ return [...issues].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity])[0];
520
+ }
521
+ function tally(issues) {
522
+ return {
523
+ problem: issues.filter((i) => i.severity === "problem").length,
524
+ warning: issues.filter((i) => i.severity === "warning").length,
525
+ hint: issues.filter((i) => i.severity === "hint").length
526
+ };
527
+ }
528
+ function projectIssues(w, projectName) {
529
+ return usable(w, projectName).filter((u) => u.local).flatMap((u) => issuesOf(w, projectName, u).map((issue) => ({ skill: u, issue })));
530
+ }
531
+ function deleteLibraryFix(w, names) {
532
+ const tracked = w.projects.flatMap((p) => p.skills.filter((s) => names.includes(s.name) && s.source === "lib").map((s) => ({ repo: p.name, edited: s.edited })));
533
+ const repos = [...new Set(tracked.map((t) => t.repo))];
534
+ const edited = [...new Set(tracked.filter((t) => t.edited).map((t) => t.repo))];
535
+ const others = [...new Set(w.projects.filter((p) => p.skills.some((s) => names.includes(s.name) && s.source !== "lib")).map((p) => p.name))];
536
+ const what = names.length === 1 ? names[0] : `these ${names.length} skills`;
537
+ return {
538
+ label: `Delete ${names.length === 1 ? "" : `all ${names.length} `}from your library${repos.length ? ` and ${repos.length} repo${repos.length === 1 ? "" : "s"}\u2026` : ""}`,
539
+ preview: [
540
+ `Delete ${what} and ${names.length === 1 ? "its" : "their"} versions from your library.`,
541
+ repos.length ? `Removes ${names.length === 1 ? "it" : "them"} from ${repos.length === 1 ? "this repo" : `these ${repos.length} repos`}: ${repos.join(", ")}.` : "No repo uses it.",
542
+ edited.length ? `${edited.join(", ")} ${edited.length === 1 ? "has" : "have"} local edits: backed up too.` : "",
543
+ "Every copy goes to Settings \u203A Backups.",
544
+ others.length ? `Untracked or the team's own copies in ${others.join(", ")} stay.` : ""
545
+ ].filter(Boolean).join(" "),
546
+ run: (w2) => names.map((n) => w2.ops.deleteLibrary(n)).map((r) => typeof r === "string" ? r : r.message).join(" \xB7 ")
547
+ };
548
+ }
549
+ function replacePluginFix(w, id) {
550
+ const skills = w.machine.filter((m) => m.source === "plugin" && m.where === id);
551
+ const names = skills.map((m) => m.name);
552
+ const parts = skills[0]?.pluginParts ?? [];
553
+ const synced = id.endsWith("@synced");
554
+ return {
555
+ label: `Replace ${id.split("@")[0]} with library skills\u2026`,
556
+ preview: [
557
+ `Copies its ${skills.length} skill${skills.length === 1 ? "" : "s"} into your library and adds them to the repos you tick (or none),`,
558
+ synced ? `then turns ${id} off in Claude Code. It's synced from your claude.ai account: to delete it for good, remove it there.` : `then uninstalls ${id} from Claude Code (its saved data is kept).`,
559
+ parts.length ? `It also brings ${parts.join(", ")}: those ${synced ? "stop" : "go"} too.` : "",
560
+ synced ? "Turn it back on with /plugin." : "Reinstall it anytime with /plugin."
561
+ ].filter(Boolean).join(" "),
562
+ run: () => "",
563
+ allowNone: true,
564
+ preticked: (w2) => w2.projects.filter((p) => names.some((n) => (w2.usage[p.name]?.[n] ?? 0) > 0)).map((p) => p.name),
565
+ pickRepos: (w2, repos) => w2.ops.replacePlugin(id, repos)
566
+ };
567
+ }
568
+ function originGroup(origin) {
569
+ if (!origin) return null;
570
+ const plugin = origin.match(/^plugin: (.+)$/);
571
+ if (plugin) return { key: `plugin:${plugin[1]}`, label: `\u29C9 ${plugin[1].split("@")[0]} plugin` };
572
+ const source = origin.match(/^(?:skills\.sh|npx skills): (.+)$/);
573
+ if (source) return { key: `src:${source[1]}`, label: source[1] };
574
+ const group = origin.match(/^group: (.+)$/);
575
+ if (group) return { key: `group:${group[1]}`, label: group[1] };
576
+ const repo = origin.match(/^project: (.+)$/);
577
+ if (repo) return { key: `repo:${repo[1]}`, label: `from ${repo[1]}` };
578
+ return null;
579
+ }
580
+ var word = (name) => name.split(/[-_.]/)[0];
581
+ function groupCandidates(w, u) {
582
+ const byTime = (family, installed) => installed ? [{ key: `time:${family}:${installed}`, label: `installed together ${installed.slice(0, 10)}` }] : [];
583
+ const byWord = (family, name) => word(name).length >= 3 ? [{ key: `word:${family}:${word(name)}`, label: `${word(name)}-*` }] : [];
584
+ if (!("source" in u)) {
585
+ const origin = originGroup(u.origin);
586
+ return [...origin ? [origin] : [], ...byWord("library", u.name)];
587
+ }
588
+ const m = u.machine;
589
+ if (m?.source === "plugin") return [{ key: `plugin:${m.where}`, label: `\u29C9 ${m.where.split("@")[0]} plugin` }];
590
+ if (m?.source === "claude.ai") return [{ key: "claude.ai", label: "claude.ai skills" }];
591
+ if (m?.source === "cursor") return [{ key: "cursor", label: "Cursor built-ins" }];
592
+ if (m) {
593
+ const origin = originGroup(m.origin);
594
+ return [...origin ? [origin] : [], ...byTime("global", m.installed), ...byWord("global", m.name)];
595
+ }
596
+ if (u.local?.source === "lib") {
597
+ const l = w.library.find((x) => x.name === u.name);
598
+ const origin = originGroup(l?.origin);
599
+ return [...origin ? [origin] : [], ...byWord("local", u.name)];
600
+ }
601
+ return byWord("local", u.name);
602
+ }
603
+ function assignGroups(candidates) {
604
+ const count = /* @__PURE__ */ new Map();
605
+ for (const c of candidates) for (const k of c) count.set(k.key, (count.get(k.key) ?? 0) + 1);
606
+ const picked = candidates.map((c) => c.find((k) => count.get(k.key) >= 2));
607
+ const size = /* @__PURE__ */ new Map();
608
+ for (const p of picked) if (p) size.set(p.key, (size.get(p.key) ?? 0) + 1);
609
+ return picked.map((p) => p && size.get(p.key) >= 2 ? p : void 0);
610
+ }
611
+ function groupLabel(g, names) {
612
+ if (!g.key.startsWith("time:")) return g.label;
613
+ const counts = /* @__PURE__ */ new Map();
614
+ for (const n of names) counts.set(word(n), (counts.get(word(n)) ?? 0) + 1);
615
+ const top = [...counts].sort((a, b) => b[1] - a[1])[0][0];
616
+ return `${top} + ${names.length - 1} more \xB7 ${g.label}`;
617
+ }
618
+ function matches(name, query) {
619
+ return matchScore(name, query) !== null;
620
+ }
621
+ function addRows(w, target, q) {
622
+ const taken = new Set(target ? project(w, target).skills.map((s) => s.name) : []);
623
+ const loaded = new Set(target ? usable(w, target).map((u) => u.name) : []);
624
+ const hit = (name, description) => !q || matches(name, q) || q.length >= 3 && description.toLowerCase().includes(q);
625
+ const library = target ? w.library.filter((l) => !taken.has(l.name) && hit(l.name, w.descriptions[l.name] ?? "")).map((l) => ({ key: `lib:${l.name}`, name: l.name, note: `v${l.latest}`, description: w.descriptions[l.name] ?? "", run: (w2) => w2.ops.add(target, l.name) })) : [];
626
+ const exists = q !== "" && (loaded.has(q) || w.library.some((l) => l.name === q));
627
+ const where = target ? ` for ${target}` : "";
628
+ const fresh = exists ? [] : [
629
+ {
630
+ key: "create",
631
+ create: true,
632
+ name: q ? `+ Create "${q}"${where}` : "+ Create a new skill (type its name)",
633
+ note: "",
634
+ description: "",
635
+ run: (w2) => {
636
+ const made = w2.ops.createSkill(q);
637
+ return target ? w2.ops.add(target, q) : made;
638
+ }
639
+ },
640
+ { key: "agent", agent: true, name: q ? `\u2726 Write "${q}" with an agent` : "\u2726 Write a new skill with an agent", note: "", description: "" }
641
+ ];
642
+ const group = (title, rows) => rows.length ? [{ key: `#${title}`, header: title, name: "", note: "", description: "" }, ...rows] : [];
643
+ return [...group("Your library", library), ...group("New", fresh)];
644
+ }
645
+
646
+ // src/tui/prompts.ts
647
+ function reviewPrompt(w, names, scope) {
648
+ const skills = names.map((name) => {
649
+ const copies = w.machine.filter((m2) => m2.name === name);
650
+ const m = copies.find((c) => c.source === "global") ?? copies[0];
651
+ const lib2 = w.library.find((l) => l.name === name);
652
+ const repos = w.projects.filter((p) => p.skills.some((s) => s.name === name));
653
+ const local = repos[0]?.skills.find((s) => s.name === name);
654
+ return {
655
+ name,
656
+ description: w.descriptions[name] ?? "",
657
+ source: m ? m.source === "global" ? "my global skills" : `vendor: ${m.source} (${m.where})` : lib2 ? "Your skills (skilllib library)" : "a repo's own skill",
658
+ installedHow: m?.origin ?? (m ? m.where : lib2 ? "created or imported into skilllib" : "committed in the repo"),
659
+ path: m?.path ?? (lib2 ? w.ops.libraryFile(name).replace(/\/SKILL\.md$/, "") : local?.path ?? ""),
660
+ links: m?.links ?? [],
661
+ loadedBy: m ? `${m.agents.map((a) => harness(a).name).join(", ") || "none of my agents"} \u2014 in every repo` : `repos: ${repos.map((p) => p.name).join(", ") || "none"}`,
662
+ vendor: m && m.source !== "global" ? "turn it off at its source (/plugin in Claude Code, claude.ai settings, or Cursor)" : null,
663
+ usesTotal: w.days ? totalUses(w, name) : null,
664
+ usesByProject: Object.entries(w.usage).flatMap(([p, u]) => u[name] ? [[p, u[name]]] : []),
665
+ installedIn: repos.map((p) => p.name),
666
+ otherCopies: copies.filter((c) => c !== m).map((c) => `${c.source} ${c.where}`),
667
+ inYourSkills: lib2 ? `yes, v${lib2.latest}` : null,
668
+ keptGlobal: m?.kept
669
+ };
670
+ });
671
+ return buildReviewPrompt(skills, {
672
+ scope,
673
+ usageDays: 30,
674
+ harnesses: w.agents.map((a) => harness(a).name),
675
+ projects: w.projects.map((p) => ({ name: p.name, path: p.path }))
676
+ });
677
+ }
678
+ function writeSkillPrompt(w, name, repo) {
679
+ const p = repo ? w.projects.find((x) => x.name === repo) : void 0;
680
+ const file = p ? `${p.path}/.claude/skills/${name || "<name>"}/SKILL.md` : w.ops.libraryFile(name || "<name>");
681
+ return `Write a new agent skill${name ? ` named "${name}"` : ""}${p ? ` for the repo at ${p.path}` : " for my skill library"}.
682
+
683
+ Create ${file} with:
684
+ - frontmatter: \`name\` (lowercase-with-dashes) and \`description\` saying exactly when an agent should use it
685
+ - then short, concrete instructions an agent can follow
686
+
687
+ ${p ? `Look at the repo first (package.json or equivalent, README, and the skills already in .claude/skills and .agents/skills) so the skill fits how it's built, and doesn't repeat an existing one.` : "Keep it general enough to reuse across repos."}
688
+ Keep it under 200 lines. ${p ? "skilllib will offer to import it into my library afterwards." : ""}
689
+ `;
690
+ }
691
+
692
+ // src/tui/App.tsx
693
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
694
+ function tildify2(p) {
695
+ return p.startsWith(userHome()) ? "~" + p.slice(userHome().length) : p;
696
+ }
697
+ var PLACES = [
698
+ ["projects", "Projects"],
699
+ ["library", "Your skills"],
700
+ ["global", "Global"],
701
+ ["health", "Health"],
702
+ ["settings", "Settings"]
703
+ ];
704
+ var CHIPS = [
705
+ ["all", "All"],
706
+ ["issues", "Issues"],
707
+ ["local", "Local"],
708
+ ["global", "Global"],
709
+ ["plugins", "Plugins"],
710
+ ["vendor", "Vendor"]
711
+ ];
712
+ var SEV = {
713
+ problem: { icon: "\u2715", color: color.red },
714
+ warning: { icon: "\u26A0", color: color.yellow },
715
+ hint: { icon: "\xB7", color: color.blue }
716
+ };
717
+ var SIDEBAR_AT = 80;
718
+ var SIDEBAR_W = 28;
719
+ var bySeverity = (a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
720
+ function tag(u, w) {
721
+ const s = u.local;
722
+ if (s?.source === "lib") {
723
+ return [`lib v${s.version}`, color.accent];
724
+ }
725
+ if (s) return [s.source, color.blue];
726
+ if (u.source === "global") return [u.machine?.kept ? "global \u2713" : "global", color.yellow];
727
+ if (u.source === "plugin") return [`\u29C9 ${u.where.split("@")[0]}`, color.magenta];
728
+ return [u.source, color.muted];
729
+ }
730
+ function isNameChar(input, key) {
731
+ return !key.ctrl && !key.meta && /^[a-zA-Z0-9._-]$/.test(input);
732
+ }
733
+ function clip(text, width) {
734
+ return text.length > width - 1 ? text.slice(0, width - 2) + "\u2026" : text;
735
+ }
736
+ function usesText(n) {
737
+ return (n ? String(n) : "\u2013").padStart(5) + " ";
738
+ }
739
+ function builder(title) {
740
+ const d = { title, body: [], options: [] };
741
+ return {
742
+ d,
743
+ line: (...segs) => d.body.push({ segs }),
744
+ issues(issues) {
745
+ if (!issues.length) return d.body.push({ segs: [["\u2713 No issues", color.green]] });
746
+ for (const issue of [...issues].sort(bySeverity)) {
747
+ d.body.push({ segs: [[`${SEV[issue.severity].icon} `, SEV[issue.severity].color], [issue.title, color.text], [issue.decision ? " \xB7 your call" : "", color.faint]] });
748
+ issue.fixes.forEach((fix, i) => d.body.push({ segs: [], option: d.options.push({ label: fix.label, fix, issue, recommended: !issue.decision && i === 0 }) - 1 }));
749
+ }
750
+ return 0;
751
+ },
752
+ /** Everything else you can do, side by side, wrapping at `width`. Skips what an issue above already offers. */
753
+ actions(options, width) {
754
+ const offered = new Set(d.options.map((o) => o.label));
755
+ let line = { segs: [["Actions ", color.accent]], row: [] };
756
+ let used = 9;
757
+ d.body.push({ segs: [] }, line);
758
+ for (const o of options.filter((o2) => !offered.has(o2.label))) {
759
+ if (line.row.length && used + 3 + o.label.length > width - 6) {
760
+ d.body.push(line = { segs: [[" "]], row: [] });
761
+ used = 9;
762
+ }
763
+ used += (line.row.length ? 3 : 0) + o.label.length;
764
+ line.row.push(d.options.push(o) - 1);
765
+ }
766
+ }
767
+ };
768
+ }
769
+ function titles(cols) {
770
+ return cols.map(([t, width], i) => i === cols.length - 1 ? { text: t, grow: true, color: color.faint } : { text: t.slice(0, width).padEnd(width), width, color: color.faint });
771
+ }
772
+ function TitleRow({ cells, width }) {
773
+ const fixed = cells.filter((c) => !c.grow).reduce((n, c) => n + (c.width ?? 0), 0);
774
+ return /* @__PURE__ */ jsx2(Box2, { height: 1, overflow: "hidden", children: /* @__PURE__ */ jsx2(Text2, { color: color.faint, wrap: "truncate-end", children: " " + cells.map((c) => c.grow ? c.text.padEnd(Math.max(0, width - 6 - fixed)) : c.text).join("") }) });
775
+ }
776
+ var fixOption = (fix) => ({ label: fix.label, fix });
777
+ var GIT_LABEL = { committed: "\u2713 committed", changed: "\xB1 uncommitted changes", new: "+ not committed", ignored: "\u2205 gitignored" };
778
+ var COPY_RANK = { lib: 0, repo: 0, untracked: 0, global: 1, plugin: 2, "claude.ai": 3, cursor: 3 };
779
+ function byName(copies) {
780
+ const groups = /* @__PURE__ */ new Map();
781
+ for (const c of copies) groups.set(c.name, [...groups.get(c.name) ?? [], c]);
782
+ return [...groups.values()].map((g) => g.sort((a, b) => COPY_RANK[a.source] - COPY_RANK[b.source]));
783
+ }
784
+ function mergeIssues(lists) {
785
+ const seen = /* @__PURE__ */ new Set();
786
+ return lists.flat().filter((i) => !seen.has(i.id) && seen.add(i.id));
787
+ }
788
+ var BLOCKS = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
789
+ function barChart(values, width, height = 3) {
790
+ const max = Math.max(1, ...values);
791
+ const gap = width >= values.length * 2;
792
+ return Array.from(
793
+ { length: height },
794
+ (_, r) => values.map((v) => {
795
+ const level = v ? Math.max(1, Math.round(v / max * height * 8)) : 0;
796
+ return BLOCKS[Math.max(0, Math.min(8, level - (height - 1 - r) * 8))] + (gap ? " " : "");
797
+ }).join("")
798
+ );
799
+ }
800
+ function usageLines(w, name, width, repo) {
801
+ const days = dailyUses(w, name, repo ?? void 0);
802
+ const total = days.reduce((a, b) => a + b, 0);
803
+ let last = -1;
804
+ days.forEach((v, i) => v && (last = i));
805
+ const ago = last < 0 ? "" : last === 29 ? "today" : last === 28 ? "yesterday" : `${29 - last} days ago`;
806
+ const head = [[repo ? "Usage here" : "Usage", color.accent], [total ? ` last used ${ago}` : " not used in 30 days", color.muted]];
807
+ if (!total) return [head];
808
+ const chart = barChart(days, width - 8);
809
+ const repos = Object.entries(w.usage).filter(() => !repo).map(([r, u]) => [r, u[name] ?? 0]).filter(([, n]) => n).sort((a, b) => b[1] - a[1]);
810
+ const barW = Math.max(6, Math.min(30, width - 30));
811
+ return [
812
+ head,
813
+ ...chart.map((line) => [[" " + line, color.accent]]),
814
+ [[" 30 days ago".padEnd(Math.max(14, chart[0].trimEnd().length - 3)) + "today", color.faint]],
815
+ ...repos.length ? [[]] : [],
816
+ ...repos.map(([r, n]) => [
817
+ [" " + clip(r, 16).padEnd(16), color.muted],
818
+ ["\u2588".repeat(Math.max(1, Math.round(n / repos[0][1] * barW))), color.accentDim],
819
+ [` ${n}`, color.muted]
820
+ ])
821
+ ];
822
+ }
823
+ function skillItem(w, copies, issues, nameW, actions, repo) {
824
+ const u = copies[0];
825
+ const top = worst(issues);
826
+ const [t, tc] = tag(u, w);
827
+ return {
828
+ key: u.name,
829
+ name: u.name,
830
+ group: filterOf(u.source),
831
+ cands: groupCandidates(w, u),
832
+ skill: u,
833
+ issues,
834
+ uses: u.uses,
835
+ cells: [
836
+ { text: top ? SEV[top.severity].icon : " ", width: 2, color: top ? SEV[top.severity].color : void 0 },
837
+ { text: clip(u.name, nameW), width: nameW },
838
+ { text: clip(t, 14), width: 14, color: tc },
839
+ { text: usesText(u.uses), width: 6, color: color.muted },
840
+ { text: " " + (top?.short ?? ""), grow: true, color: top ? SEV[top.severity].color : color.faint }
841
+ ],
842
+ detail: (width) => {
843
+ const b = builder(u.name);
844
+ for (const l of wrap(w.descriptions[u.name] || "(no description)", width - 4, 3)) b.line([l, color.muted]);
845
+ b.line();
846
+ if (repo && !u.local) b.line(["Loads in every repo", color.text], [u.source === "global" ? " issues and cleanup live in Global" : " managed at its source", color.faint]);
847
+ else b.issues(issues);
848
+ b.actions(actions, width);
849
+ b.line();
850
+ for (const segs of usageLines(w, u.name, width, repo)) b.line(...segs);
851
+ b.line();
852
+ b.line(["Loaded from", color.accent]);
853
+ for (const c of copies) {
854
+ const s = c.local;
855
+ const path = s ? `${s.dir}/${c.name}` : c.source === "global" ? `${c.where}/${c.name}` : c.where;
856
+ const extra = s ? s.missing ? "missing" : s.git ? GIT_LABEL[s.git] : "" : "";
857
+ const [ct, cc] = c === u ? ["", void 0] : tag(c, w);
858
+ b.line([" ", void 0], [path + " ", color.text], [ct ? ct + " " : "", cc], [extra, color.faint]);
859
+ }
860
+ return b.d;
861
+ }
862
+ };
863
+ }
864
+ function projectSkillItems(w, p, nameW, ui) {
865
+ return byName(usable(w, p)).map(
866
+ (copies) => skillItem(w, copies, mergeIssues(copies.filter((c) => c.local).map((c) => issuesOf(w, p, c))), nameW, skillActions(w, p, copies[0], ui), p)
867
+ );
868
+ }
869
+ var reviewOption = (w, name, ui) => ({ label: "Review prompt", action: () => ui.copy(reviewPrompt(w, [name], `Review the skill ${name}.`), `a review prompt for ${name}`) });
870
+ function versionsOption(w, repo, name, installed, ui) {
871
+ return {
872
+ label: "Other versions\u2026",
873
+ action: () => ui.menu(
874
+ `${name}: versions`,
875
+ w.ops.versions(name).reverse().map((v) => fixOption({ label: `v${v.version} ${v.date}${v.version === installed ? " (installed)" : ""}`, preview: `Install ${name} v${v.version} in ${repo}.`, run: (w2) => w2.ops.installVersion(repo, name, v.version) }))
876
+ )
877
+ };
878
+ }
879
+ function skillActions(w, p, u, ui) {
880
+ const s = u.local;
881
+ const inLibrary = w.library.find((l) => l.name === u.name);
882
+ const remove = { label: "Remove from repo", preview: s?.source === "lib" ? `Delete ${s.dir}/${u.name} in ${p}. Your library keeps it.` : `Move ${s?.dir}/${u.name} in ${p} to Settings \u203A Backups.`, run: (w2) => w2.ops.remove(p, u.name) };
883
+ if (s?.source === "lib")
884
+ return [
885
+ ...inLibrary && s.version < inLibrary.latest ? [fixOption({ label: `Update to v${inLibrary.latest}`, preview: `Replace ${s.dir}/${u.name} with library v${inLibrary.latest}.`, run: (w2) => w2.ops.update(p, u.name) })] : [],
886
+ { label: "Edit in library", action: () => ui.edit(w.ops.libraryFile(u.name)) },
887
+ versionsOption(w, p, u.name, s.version, ui),
888
+ fixOption(remove),
889
+ reviewOption(w, u.name, ui)
890
+ ];
891
+ if (s?.source === "repo")
892
+ return [
893
+ ...inLibrary ? [] : [fixOption({ label: "Copy into library", preview: `Copy ${u.name} into your library, so other repos can add it. The repo's copy stays as it is.`, run: (w2) => w2.ops.copyToLibrary(p, u.name) })],
894
+ { label: "Edit SKILL.md", action: () => ui.edit(`${s.path}/SKILL.md`) },
895
+ reviewOption(w, u.name, ui)
896
+ ];
897
+ if (s)
898
+ return [
899
+ inLibrary ? fixOption({ label: "Track it", preview: `Record ${u.name} in skilllib.json so library updates reach it.`, run: (w2) => w2.ops.track(p, u.name) }) : fixOption({ label: "Import it into your library", preview: `Copy ${u.name} into your library and track it here.`, run: (w2) => w2.ops.importLocal(p, u.name) }),
900
+ { label: "Edit SKILL.md", action: () => ui.edit(`${s.path}/SKILL.md`) },
901
+ fixOption(remove),
902
+ reviewOption(w, u.name, ui)
903
+ ];
904
+ return [{ label: "Open in Global", action: () => ui.openInGlobal(u.name) }, reviewOption(w, u.name, ui)];
905
+ }
906
+ function overview(w, p) {
907
+ const project2 = project(w, p);
908
+ const info = w.ops.repoInfo(p);
909
+ const manifest = project2.manifest === "none" ? ["no skilllib.json", color.faint] : project2.manifest === "no git" ? ["skilllib.json (not a git repo)", color.muted] : [`skilllib.json ${GIT_LABEL[project2.manifest]}`, project2.manifest === "committed" ? color.text : color.yellow];
910
+ return [
911
+ ["Folder", [[tildify2(project2.path), color.text]]],
912
+ ["Remote", [[info.remote ?? "none", info.remote ? color.blue : color.faint]]],
913
+ ["Branch", [[info.branch ?? "\u2013", color.text], [info.dirty ? ` ${info.dirty} uncommitted` : "", color.yellow]]],
914
+ ["Manifest", [manifest]]
915
+ ];
916
+ }
917
+ function InfoGrid({ rows, columns, width }) {
918
+ const cell = Math.floor((width - 4) / columns);
919
+ const lines = [];
920
+ for (let i = 0; i < rows.length; i += columns) lines.push(rows.slice(i, i + columns));
921
+ return /* @__PURE__ */ jsx2(Fragment, { children: lines.map((line, i) => /* @__PURE__ */ jsx2(
922
+ SegLine,
923
+ {
924
+ segs: line.flatMap(([label, value], j) => {
925
+ const used = value.reduce((n, [t]) => n + t.length, 0) + 10;
926
+ return [[label.padEnd(10), color.muted], ...value, [j < line.length - 1 ? " ".repeat(Math.max(1, cell - used)) : "", void 0]];
927
+ })
928
+ },
929
+ i
930
+ )) });
931
+ }
932
+ function projectItems(w) {
933
+ return w.projects.map((p) => {
934
+ const issues = projectIssues(w, p.name).map((x) => x.issue);
935
+ const t = tally(issues);
936
+ return {
937
+ key: p.name,
938
+ name: p.name,
939
+ issues,
940
+ uses: 0,
941
+ cells: [
942
+ { text: p.name === w.cwd ? "\u25C6 " : " ", width: 2, color: color.accent },
943
+ { text: p.name, width: 18 },
944
+ { text: p.skills.length ? `${p.skills.length} skill${p.skills.length === 1 ? "" : "s"}` : "\u2013", width: 10, color: color.muted },
945
+ { text: t.problem ? `\u2715 ${t.problem}` : "", width: 5, color: color.red },
946
+ { text: t.warning ? `\u26A0 ${t.warning}` : "", width: 5, color: color.yellow },
947
+ { text: t.hint ? `\xB7 ${t.hint}` : "", grow: true, color: color.blue }
948
+ ]
949
+ };
950
+ });
951
+ }
952
+ function globalItems(w, nameW, ui) {
953
+ const copies = w.machine.map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: totalUses(w, m.name), machine: m }));
954
+ return byName(copies).map((g) => skillItem(w, g, mergeIssues(g.map((c) => machineIssues(w, c.machine))), nameW, globalActions(w, g[0], ui), null));
955
+ }
956
+ function globalActions(w, u, ui) {
957
+ const m = u.machine;
958
+ const atSource = m.source === "plugin" ? "Turn it off with /plugin in Claude Code" : m.source === "claude.ai" ? "Turn it off in claude.ai \u203A Settings" : "Cursor manages it";
959
+ return [
960
+ ...machineActions(m).map(fixOption),
961
+ ...m.source === "plugin" ? [fixOption(replacePluginFix(w, m.where))] : [],
962
+ ...m.source === "global" ? [] : [{ label: atSource, action: () => ui.notify(`${atSource}; skilllib picks up the change next time`) }],
963
+ ...m.source === "global" && !m.broken ? [{ label: "Edit SKILL.md", action: () => ui.edit(`${m.path}/SKILL.md`) }] : [],
964
+ reviewOption(w, m.name, ui)
965
+ ];
966
+ }
967
+ function libraryItems(w, nameW, ui) {
968
+ return w.library.map((l) => {
969
+ const repos = w.projects.filter((p) => p.skills.some((s) => s.name === l.name));
970
+ const uses = totalUses(w, l.name);
971
+ const addFix = { label: "Add to repos\u2026", preview: `Install ${l.name} v${l.latest} into the repos you pick.`, run: () => "", candidates: (w2) => w2.projects.filter((p) => !p.skills.some((s) => s.name === l.name)).map((p) => p.name), pickRepos: (w2, rs) => rs.map((r) => said(w2.ops.add(r, l.name))).join(" \xB7 ") };
972
+ const delFix = deleteLibraryFix(w, [l.name]);
973
+ const issues = repos.length ? [] : [{ id: `nowhere:${l.name}`, severity: "hint", title: "Used in no repo", short: "Used in no repo", decision: true, fixes: [addFix, delFix] }];
974
+ const behind = repos.filter((p) => p.skills.some((s) => s.name === l.name && s.source === "lib" && s.version < l.latest)).length;
975
+ return {
976
+ key: l.name,
977
+ name: l.name,
978
+ issues,
979
+ uses,
980
+ cands: groupCandidates(w, l),
981
+ lib: l,
982
+ cells: [
983
+ { text: issues[0] ? SEV.hint.icon : " ", width: 2, color: color.blue },
984
+ { text: clip(l.name, nameW), width: nameW },
985
+ { text: `v${l.latest}`, width: 5, color: color.accent },
986
+ { text: `${repos.length} repo${repos.length === 1 ? "" : "s"}${behind ? ` (${behind} behind)` : ""}`, width: 18, color: behind ? color.yellow : color.muted },
987
+ { text: usesText(uses), width: 6, color: color.muted },
988
+ { text: " " + (issues[0]?.short ?? w.descriptions[l.name] ?? ""), grow: true, color: issues[0] ? color.blue : color.faint }
989
+ ],
990
+ detail: (width) => {
991
+ const b = builder(l.name);
992
+ for (const line of wrap(w.descriptions[l.name] ?? "", width - 4, 3)) b.line([line, color.muted]);
993
+ b.line();
994
+ b.line(["Versions ", color.muted], [Array.from({ length: l.latest }, (_, i) => `v${i + 1}`).join(" \xB7 ") + " (newest)", color.text]);
995
+ b.line(["In repos ", color.muted], [repos.map((p) => {
996
+ const s = p.skills.find((x) => x.name === l.name);
997
+ return `${p.name} ${s.source === "lib" ? `v${s.version}` : s.source}`;
998
+ }).join(" \xB7 ") || "none", color.text]);
999
+ b.line();
1000
+ b.issues(issues);
1001
+ b.actions([
1002
+ fixOption(addFix),
1003
+ ...repos.length ? [
1004
+ fixOption({
1005
+ label: "Remove from repos\u2026",
1006
+ preview: `Pick repos to remove ${l.name} from. Your library keeps it.`,
1007
+ run: () => "",
1008
+ candidates: () => repos.map((p) => p.name),
1009
+ pickRepos: (w2, rs) => rs.map((r) => said(w2.ops.remove(r, l.name))).join(" \xB7 ")
1010
+ })
1011
+ ] : [],
1012
+ fixOption(delFix),
1013
+ { label: "Edit SKILL.md", action: () => ui.edit(w.ops.libraryFile(l.name)) },
1014
+ reviewOption(w, l.name, ui)
1015
+ ], width);
1016
+ b.line();
1017
+ for (const segs of usageLines(w, l.name, width, null)) b.line(...segs);
1018
+ return b.d;
1019
+ }
1020
+ };
1021
+ });
1022
+ }
1023
+ function healthItems(w, nameW, ui) {
1024
+ const flagged = (where, items) => items.filter((i) => i.issues.length).map((i) => ({ ...i, key: `${where}:${i.key}`, cells: [i.cells[0], i.cells[1], { text: clip(where, 16), width: 16, color: color.muted }, i.cells[4]] }));
1025
+ return [...w.projects.flatMap((p) => flagged(p.name, projectSkillItems(w, p.name, nameW, ui))), ...flagged("Global", globalItems(w, nameW, ui))];
1026
+ }
1027
+ function addCursor(rows, cursor) {
1028
+ const i = Math.min(cursor, rows.length - 1);
1029
+ return rows[i] && !rows[i].header ? i : Math.max(0, rows.findIndex((r) => !r.header));
1030
+ }
1031
+ function said(r) {
1032
+ return typeof r === "string" ? r : r.message;
1033
+ }
1034
+ function App({ initial, reload, loadUsage: loadUsage2 }) {
1035
+ const { exit, suspendTerminal } = useApp();
1036
+ const { columns: termCols, rows: termRows } = useWindowSize();
1037
+ const sidebar = termCols >= SIDEBAR_AT;
1038
+ const columns = sidebar ? termCols - SIDEBAR_W : termCols;
1039
+ const [base, setBase] = useState(initial);
1040
+ const [used, setUsed] = useState();
1041
+ const world = used ? { ...base, ...used } : base;
1042
+ useEffect(() => {
1043
+ let live = true;
1044
+ void loadUsage2?.(base).then((u) => live && setUsed(u));
1045
+ return () => {
1046
+ live = false;
1047
+ };
1048
+ }, [base.projects.map((p) => p.path).join("\n")]);
1049
+ const [place, setPlace] = useState("projects");
1050
+ const [openName, setOpen] = useState(initial.cwd ?? initial.projects[0]?.name ?? null);
1051
+ const open = openName && world.projects.some((p) => p.name === openName) ? openName : null;
1052
+ const [chip, setChip] = useState("all");
1053
+ const [folds, setFolds] = useState(/* @__PURE__ */ new Set());
1054
+ const [query, setQuery] = useState("");
1055
+ const [cursors, setCursors] = useState({});
1056
+ const [focus, setFocus] = useState(sidebar ? "sidebar" : "list");
1057
+ const [sideQuery, setSideQuery] = useState("");
1058
+ const [onHelp, setOnHelp] = useState(false);
1059
+ const [detailCursor, setDetailCursor] = useState(0);
1060
+ const [modal, setModal] = useState(null);
1061
+ const [toast, setToast] = useState("");
1062
+ const order = useRef({ key: "", keys: [] });
1063
+ const split = termRows >= 30;
1064
+ const nameW = Math.min(22, Math.max(17, Math.floor(columns * 0.22)));
1065
+ const dashboard = place === "projects" && open !== null;
1066
+ const chips = dashboard ? CHIPS : place === "global" ? CHIPS.filter(([c]) => c !== "local") : place === "settings" ? [] : place === "health" ? CHIPS.slice(0, 1) : CHIPS.slice(0, 2);
1067
+ const ui = {
1068
+ notify: setToast,
1069
+ openInGlobal,
1070
+ edit,
1071
+ copy: (text, what) => setToast(said(world.ops.copy(text, what))),
1072
+ menu: (title, options) => setModal({ kind: "menu", title, options, cursor: 0 })
1073
+ };
1074
+ const flagged = [
1075
+ ...world.projects.flatMap((p) => projectIssues(world, p.name).map((x) => ({ key: `${p.name}:${x.skill.name}`, issue: x.issue }))),
1076
+ ...world.machine.flatMap((m) => machineIssues(world, m).map((issue) => ({ key: `Global:${m.name}`, issue })))
1077
+ ];
1078
+ const healthCount = new Set(flagged.map((f) => f.key)).size;
1079
+ const healthWorst = worst(flagged.map((f) => f.issue));
1080
+ const all = place === "projects" ? open ? projectSkillItems(world, open, nameW, ui) : projectItems(world) : place === "global" ? globalItems(world, nameW, ui) : place === "library" ? libraryItems(world, nameW, ui) : place === "health" ? healthItems(world, nameW, ui) : settingsItems();
1081
+ const inChip = (i, c) => c === "all" || (c === "issues" ? i.issues.length > 0 : i.group === c);
1082
+ let items = all.filter((i) => i.header || (!query || matches(i.name, query)) && inChip(i, chip));
1083
+ const viewKey = [place, open, chip, query, used ? "usage" : ""].join("|");
1084
+ const sortable = place !== "settings" && !(place === "projects" && !open);
1085
+ if (sortable) {
1086
+ if (order.current.key !== viewKey) {
1087
+ const rank = (i) => i.issues.length ? SEVERITY_RANK[[...i.issues].sort(bySeverity)[0].severity] : 3;
1088
+ order.current = { key: viewKey, keys: [...items].sort((a, b) => rank(a) - rank(b) || b.uses - a.uses || a.name.localeCompare(b.name)).map((i) => i.key) };
1089
+ }
1090
+ const pos = new Map(order.current.keys.map((k, i) => [k, i]));
1091
+ items = [...items].sort((a, b) => (pos.get(a.key) ?? 1e9) - (pos.get(b.key) ?? 1e9));
1092
+ } else if (place === "projects") {
1093
+ const rank = (i) => i.key === world.cwd ? -1 : i.issues.some((x) => x.severity === "problem") ? 0 : i.issues.length ? 1 : 2;
1094
+ const skills = (i) => project(world, i.name).skills.length;
1095
+ items = [...items].sort((a, b) => rank(a) - rank(b) || skills(b) - skills(a) || a.name.localeCompare(b.name));
1096
+ }
1097
+ if (!query && (dashboard || place === "global" || place === "library")) items = folded(items);
1098
+ if (!query) items = [...actionRows(), ...items];
1099
+ const cursorKey = `${place}|${open}`;
1100
+ const firstSelectable = Math.max(0, items.findIndex((i) => !i.header && !i.action));
1101
+ let cursor = Math.min(cursors[cursorKey] ?? firstSelectable, items.length - 1);
1102
+ if (items[cursor]?.header) cursor = firstSelectable;
1103
+ const current = items[cursor];
1104
+ const detail = current?.detail?.(columns);
1105
+ function apply(run) {
1106
+ let result;
1107
+ try {
1108
+ result = run(world);
1109
+ } catch (e) {
1110
+ result = `Couldn't do it: ${e.message}`;
1111
+ }
1112
+ setBase(reload());
1113
+ setToast(`\u2713 ${said(result)}`);
1114
+ if (typeof result !== "string") setModal({ kind: "confirm", fix: result.then });
1115
+ }
1116
+ function choose(o) {
1117
+ if (o.action) return o.action();
1118
+ if (!o.fix) return setToast("Not in the prototype");
1119
+ if (o.fix.pickRepos) {
1120
+ const name = current?.name ?? "";
1121
+ const only = o.fix.candidates?.(world);
1122
+ const picked = new Set(
1123
+ (o.fix.preticked?.(world) ?? world.projects.filter((p) => (world.usage[p.name]?.[name] ?? 0) > 0 && !p.skills.some((s) => s.name === name)).map((p) => p.name)).filter(
1124
+ (p) => !only || only.includes(p)
1125
+ )
1126
+ );
1127
+ return setModal({ kind: "repos", fix: o.fix, picked, cursor: 0, only, query: "" });
1128
+ }
1129
+ setModal({ kind: "confirm", fix: o.fix });
1130
+ }
1131
+ function fixSelected() {
1132
+ const issue = current && worst(current.issues);
1133
+ if (!issue) return setToast("Nothing to fix \xB7 enter shows what you can do");
1134
+ if (issue.decision) {
1135
+ setFocus("detail");
1136
+ setDetailCursor(Math.max(0, detail?.options.findIndex((o) => o.issue === issue) ?? 0));
1137
+ return setToast("Your call: pick one");
1138
+ }
1139
+ choose({ label: issue.fixes[0].label, fix: issue.fixes[0] });
1140
+ }
1141
+ function fixAllList(from) {
1142
+ const seen = /* @__PURE__ */ new Set();
1143
+ return from.flatMap(
1144
+ (i) => i.issues.filter((x) => !x.decision && !seen.has(`${i.key}|${x.id}`) && seen.add(`${i.key}|${x.id}`)).map((x) => ({ label: `${i.name}: ${x.title} \u2192 ${x.fixes[0].label}`, fix: x.fixes[0], on: true }))
1145
+ );
1146
+ }
1147
+ function toggleFold(key) {
1148
+ const next = new Set(folds);
1149
+ if (!next.delete(key)) next.add(key);
1150
+ setFolds(next);
1151
+ }
1152
+ function folded(list2) {
1153
+ const groups = assignGroups(list2.map((i) => i.cands ?? []));
1154
+ const out = [];
1155
+ const seen = /* @__PURE__ */ new Set();
1156
+ list2.forEach((it, i) => {
1157
+ const g = groups[i];
1158
+ if (!g) return void out.push(it);
1159
+ if (seen.has(g.key)) return;
1160
+ seen.add(g.key);
1161
+ const members = list2.filter((_, j) => groups[j]?.key === g.key);
1162
+ const isOpen = folds.has(g.key);
1163
+ out.push(groupItem(g, members, isOpen));
1164
+ if (isOpen) out.push(...members.map((m) => ({ ...m, inFold: g.key, cells: m.cells.map((c, k) => k === 1 ? { ...c, text: clip(" " + m.name, c.width ?? nameW) } : c) })));
1165
+ });
1166
+ return out;
1167
+ }
1168
+ function groupItem(g, members, isOpen) {
1169
+ const label = groupLabel(g, members.map((m) => m.name));
1170
+ const issues = members.flatMap((m) => m.issues);
1171
+ const top = worst(issues);
1172
+ const flagged2 = members.filter((m) => m.issues.length).length;
1173
+ return {
1174
+ key: `fold:${g.key}`,
1175
+ name: label,
1176
+ group: members[0].group,
1177
+ fold: { key: g.key, open: isOpen },
1178
+ issues,
1179
+ uses: members.reduce((n, m) => n + m.uses, 0),
1180
+ cells: [
1181
+ { text: top ? SEV[top.severity].icon : " ", width: 2, color: top ? SEV[top.severity].color : void 0 },
1182
+ { text: `${isOpen ? "\u25BE" : "\u25B8"} ${label} (${members.length})`, grow: true, color: color.accent },
1183
+ { text: flagged2 ? `${flagged2} to fix` : "", width: 10, color: top ? SEV[top.severity].color : color.faint }
1184
+ ],
1185
+ detail: (width) => {
1186
+ const b = builder(label);
1187
+ for (const l of wrap(members.map((m) => m.name).join(", "), width - 4, 4)) b.line([l, color.muted]);
1188
+ b.line();
1189
+ if (flagged2) b.line([`${flagged2} of them ${flagged2 === 1 ? "has" : "have"} issues`, top ? SEV[top.severity].color : color.text], [" open the group to fix them one by one", color.faint]);
1190
+ b.actions(groupActions(label, members), width);
1191
+ return b.d;
1192
+ }
1193
+ };
1194
+ }
1195
+ function groupActions(label, members) {
1196
+ const names = members.map((m) => m.name);
1197
+ const machines = members.flatMap((m) => m.skill?.machine ? [m.skill.machine] : []);
1198
+ const mine = machines.filter((m) => m.source === "global" && !m.broken);
1199
+ const locals = dashboard ? members.flatMap((m) => m.skill?.local && m.skill.local.source !== "repo" ? [m.skill.local] : []) : [];
1200
+ const libs = members.flatMap((m) => m.lib ? [m.lib] : []);
1201
+ const usedIn = (w) => w.projects.filter((p) => names.some((n2) => (w.usage[p.name]?.[n2] ?? 0) > 0)).map((p) => p.name);
1202
+ const n = (k) => `${k} skill${k === 1 ? "" : "s"}`;
1203
+ return [
1204
+ ...mine.length ? [
1205
+ fixOption({
1206
+ label: `Move all ${mine.length} to repos\u2026`,
1207
+ preview: `Pick repos. These ${n(mine.length)} go into your library and those repos, and stop loading globally (originals backed up).`,
1208
+ run: () => "",
1209
+ preticked: usedIn,
1210
+ pickRepos: (w, repos) => (mine.forEach((m) => w.ops.moveGlobal(m, repos, label)), `${n(mine.length)} now load only in ${repos.join(", ")}`)
1211
+ }),
1212
+ ...mine.some((m) => !m.kept) ? [fixOption({ label: `Keep all ${mine.length} global on purpose`, preview: `skilllib stops warning about these ${n(mine.length)}.`, run: (w) => (mine.forEach((m) => w.ops.keepGlobal(m.name, true)), `${n(mine.length)} marked as global on purpose`) })] : [],
1213
+ fixOption({ label: `Delete all ${mine.length}`, preview: `Move these ${n(mine.length)} to Settings \u203A Backups.`, run: (w) => (mine.forEach((m) => w.ops.deleteGlobal(m)), `Deleted ${n(mine.length)} (in Settings \u203A Backups)`) })
1214
+ ] : [],
1215
+ ...libs.length ? [
1216
+ fixOption({
1217
+ label: `Add all ${libs.length} to repos\u2026`,
1218
+ preview: `Install these ${n(libs.length)} into the repos you pick.`,
1219
+ run: () => "",
1220
+ preticked: () => [],
1221
+ pickRepos: (w, repos) => (repos.forEach((r) => libs.forEach((l) => w.ops.add(r, l.name))), `Added ${n(libs.length)} to ${repos.join(", ")}`)
1222
+ })
1223
+ ] : [],
1224
+ ...libs.length ? [fixOption(deleteLibraryFix(world, libs.map((l) => l.name)))] : [],
1225
+ ...locals.length ? [fixOption({ label: `Remove all ${locals.length} from this repo`, preview: `Remove these ${n(locals.length)} from ${open}; your library keeps the tracked ones.`, run: (w) => (locals.forEach((s) => w.ops.remove(open, s.name)), `Removed ${n(locals.length)} from ${open}`) })] : [],
1226
+ ...[...new Set(machines.filter((m) => m.source === "plugin").map((m) => m.where))].map((id) => fixOption(replacePluginFix(world, id))),
1227
+ ...machines.some((m) => m.source === "plugin") ? [{ label: "Turn the plugin off with /plugin in Claude Code", action: () => setToast("Turn it off with /plugin in Claude Code; skilllib picks it up next time") }] : [],
1228
+ { label: "Review prompt", action: () => ui.copy(reviewPrompt(world, names, `Review these related skills (${label}).`), `a review prompt for ${label}`) }
1229
+ ];
1230
+ }
1231
+ function actionRows() {
1232
+ const row = (key, text, lines, onEnter) => ({
1233
+ key,
1234
+ name: "",
1235
+ action: true,
1236
+ issues: [],
1237
+ uses: 0,
1238
+ cells: [{ text, grow: true, color: color.accent }],
1239
+ onEnter,
1240
+ detail: (width) => {
1241
+ const b = builder(text.replace(/^[^ ]+ /, ""));
1242
+ for (const l of lines.flatMap((l2) => l2 ? wrap(l2, width - 4, 4) : [""])) b.line([l, color.muted]);
1243
+ b.line();
1244
+ b.line(["enter to start", color.faint]);
1245
+ return b.d;
1246
+ }
1247
+ });
1248
+ const autoFixes = fixAllList(all);
1249
+ const fix = autoFixes.length ? [row("#fixall", `\u2726 Fix ${autoFixes.length} issue${autoFixes.length === 1 ? "" : "s"} automatically\u2026`, ["You'll see the list first and can untick any of them.", "", 'Decisions (marked "your call") are never applied automatically: open the skill to choose.'], () => setModal({ kind: "fixall", items: autoFixes, cursor: 0 }))] : [];
1250
+ if (dashboard) {
1251
+ const repo = open;
1252
+ const skills = project(world, repo).skills.map((s) => s.name);
1253
+ return [
1254
+ row("#add", "+ Add a skill\u2026", ["Search your library, or create a new skill."], () => setModal({ kind: "add", cursor: 0, query: "", target: repo })),
1255
+ ...fix,
1256
+ row(
1257
+ "#repo",
1258
+ `\u22EF ${repo}\u2026`,
1259
+ ["Open its folder, hide it from the list, or copy a review prompt for its skills."],
1260
+ () => ui.menu(repo, [
1261
+ { label: "Open its folder", action: () => setToast(said(world.ops.openFolder(project(world, repo).path))) },
1262
+ { label: "Hide it from the list", action: () => (apply((w) => w.ops.hide(repo)), setOpen(null), setFocus(sidebar ? "sidebar" : "list")) },
1263
+ ...skills.length ? [{ label: `Review prompt for its ${skills.length} skills`, action: () => ui.copy(reviewPrompt(world, skills, `Review the skills in ${repo}.`), `a review prompt for ${repo}`) }] : []
1264
+ ])
1265
+ )
1266
+ ];
1267
+ }
1268
+ if (place === "global") {
1269
+ const globals = [...new Set(world.machine.filter((m) => m.source === "global" && !m.broken).map((m) => m.name))];
1270
+ return [
1271
+ ...fix,
1272
+ ...globals.length ? [row("#review", `\u29C9 Review prompt for your ${globals.length} global skills\u2026`, ["An agent recommends, for each: keep global, move to repos, or delete."], () => ui.copy(reviewPrompt(world, globals, "Review my global skills: they load in every repo."), "a review prompt for your global skills"))] : []
1273
+ ];
1274
+ }
1275
+ if (place === "health") return fix;
1276
+ if (place === "library")
1277
+ return [row("#add", "+ New skill\u2026", ["Create a skill in your library, or have an agent write it. Skills in your repos can be copied in from their own actions."], () => setModal({ kind: "add", cursor: 0, query: "", target: null }))];
1278
+ return [];
1279
+ }
1280
+ function move(delta) {
1281
+ let i = cursor;
1282
+ do
1283
+ i = Math.max(0, Math.min(items.length - 1, i + delta));
1284
+ while (items[i]?.header && i > 0 && i < items.length - 1);
1285
+ if (items[i]?.header) return;
1286
+ setCursors({ ...cursors, [cursorKey]: i });
1287
+ setDetailCursor(0);
1288
+ }
1289
+ function goPlace(p) {
1290
+ setPlace(p);
1291
+ setFocus("list");
1292
+ setQuery("");
1293
+ setChip("all");
1294
+ }
1295
+ function edit(file) {
1296
+ if (!existsSync2(file)) return setToast(`${tildify2(file)} doesn't exist`);
1297
+ const editor = process.env.VISUAL || process.env.EDITOR || (process.platform === "win32" ? "notepad" : "vi");
1298
+ void suspendTerminal(() => {
1299
+ if (process.platform === "win32") spawnSync2(`${editor} "${file}"`, { shell: true, stdio: "inherit" });
1300
+ else spawnSync2("/bin/sh", ["-c", `${editor} "$1"`, "sh", file], { stdio: "inherit" });
1301
+ }).then(() => {
1302
+ setBase(reload());
1303
+ setToast(`Saved ${tildify2(file)}`);
1304
+ });
1305
+ }
1306
+ function askForFolder(title) {
1307
+ setModal({ kind: "input", title, value: "~/Projects", submit: (v) => apply((w) => w.ops.addRoot(v)) });
1308
+ }
1309
+ useEffect(() => {
1310
+ const folder = () => !initial.roots.length && askForFolder("Where are your projects? skilllib finds the git repos in there");
1311
+ if (!initial.agentsChosen) setModal({ kind: "agents", selected: initial.agents, cursor: 0, then: folder });
1312
+ else folder();
1313
+ }, []);
1314
+ function openInGlobal(name) {
1315
+ goPlace("global");
1316
+ setQuery(name);
1317
+ }
1318
+ function settingsItems() {
1319
+ const header = (key, text) => ({ key, name: "", header: true, issues: [], uses: 0, cells: [{ text }] });
1320
+ const row = (key, text, sub, onEnter, c) => ({
1321
+ key,
1322
+ name: text,
1323
+ issues: [],
1324
+ uses: 0,
1325
+ cells: [{ text, width: 34, color: c }, { text: sub, grow: true, color: color.faint }],
1326
+ onEnter
1327
+ });
1328
+ return [
1329
+ header("#agents", "Agents"),
1330
+ ...HARNESSES.map(
1331
+ (h) => row(`agent:${h.id}`, `${world.agents.includes(h.id) ? "[x]" : "[ ]"} ${h.icon} ${h.name}`, h.projectDirs.join(", "), () => {
1332
+ const wasOn = world.agents.includes(h.id);
1333
+ apply((w) => (w.ops.setAgents(wasOn ? w.agents.filter((a) => a !== h.id) : HARNESSES.map((x) => x.id).filter((id) => id === h.id || w.agents.includes(id))), `${h.name} ${wasOn ? "off" : "on"}`));
1334
+ })
1335
+ ),
1336
+ header("#roots", "Project folders"),
1337
+ ...world.roots.map(
1338
+ (r) => row(
1339
+ `root:${r}`,
1340
+ tildify2(r),
1341
+ "scanned for git repos",
1342
+ () => ui.menu(tildify2(r), [
1343
+ { label: "Look for repos again", action: () => apply((w) => w.ops.rescan()) },
1344
+ fixOption({ label: "Stop scanning this folder", preview: `Repos already found in ${tildify2(r)} stay listed until you hide them.`, run: (w) => w.ops.removeRoot(r) })
1345
+ ])
1346
+ )
1347
+ ),
1348
+ row("root:add", "+ Add a folder\u2026", "", () => askForFolder("Folder that contains your projects")),
1349
+ ...world.hidden.length ? [header("#hidden", "Hidden repos"), ...world.hidden.map((h) => row(`hidden:${h}`, tildify2(h), "enter shows it again", () => apply((w) => w.ops.unhide(h))))] : [],
1350
+ header("#backups", "Backups"),
1351
+ ...world.backups.length ? world.backups.map(
1352
+ (b, i) => row(`backup:${i}`, `\u21BA ${b.name}`, `${b.from} \xB7 ${b.at}`, () => apply((w) => w.ops.restoreBackup(i)))
1353
+ ) : [row("none", "No backups yet", "anything skilllib removes lands here")]
1354
+ ];
1355
+ }
1356
+ useInput((input, key) => {
1357
+ if (key.ctrl && input === "c") return exit();
1358
+ setToast("");
1359
+ if (modal) return modalKeys(input, key);
1360
+ if (key.ctrl && input === "r") return setBase(reload()), setToast("Read everything again");
1361
+ if (focus === "sidebar" && (isNameChar(input, key) || key.backspace || key.delete)) {
1362
+ const q = isNameChar(input, key) ? sideQuery + input.toLowerCase() : sideQuery.slice(0, -1);
1363
+ setSideQuery(q);
1364
+ const first = q && world.projects.find((p) => matches(p.name, q));
1365
+ if (first) toRepo(first.name)();
1366
+ return;
1367
+ }
1368
+ if (isNameChar(input, key)) return setFocus("list"), setQuery(query + input.toLowerCase());
1369
+ if (key.backspace || key.delete) return setQuery(query.slice(0, -1));
1370
+ if (input === "?") return setModal({ kind: "help" });
1371
+ if (focus === "detail") {
1372
+ const lines = (detail?.body ?? []).flatMap((l) => l.row ? [l.row] : l.option !== void 0 ? [[l.option]] : []);
1373
+ const at = lines.findIndex((r) => r.includes(detailCursor));
1374
+ const col = at < 0 ? 0 : lines[at].indexOf(detailCursor);
1375
+ const goLine = (i) => lines[i] && setDetailCursor(lines[i][Math.min(col, lines[i].length - 1)]);
1376
+ if (key.escape) return setFocus("list");
1377
+ if (key.upArrow) return goLine(Math.max(0, at - 1));
1378
+ if (key.downArrow) return goLine(at + 1);
1379
+ if (key.leftArrow) return col > 0 ? setDetailCursor(lines[at][col - 1]) : setFocus("list");
1380
+ if (key.rightArrow) return lines[at]?.[col + 1] !== void 0 ? setDetailCursor(lines[at][col + 1]) : void 0;
1381
+ if ((key.return || input === " ") && detail?.options[detailCursor]) return choose(detail.options[detailCursor]);
1382
+ return;
1383
+ }
1384
+ if (focus === "sidebar") {
1385
+ if (key.upArrow || key.downArrow) {
1386
+ const d = key.upArrow ? -1 : 1;
1387
+ let i = sideAt + d;
1388
+ while (sideRows[i]?.header) i += d;
1389
+ const next = sideRows[i];
1390
+ if (!next) return;
1391
+ if (next.key === "help") return setOnHelp(true);
1392
+ return next.go(), setOnHelp(false), setFocus("sidebar");
1393
+ }
1394
+ if (key.return && onHelp) return setModal({ kind: "help" });
1395
+ if (key.rightArrow || key.return) return setOnHelp(false), setFocus("list");
1396
+ if (key.escape) return setSideQuery("");
1397
+ return;
1398
+ }
1399
+ const fold = current?.fold;
1400
+ if (fold && !fold.open && (key.rightArrow || key.return || input === " ")) return toggleFold(fold.key);
1401
+ if (fold?.open && (key.leftArrow || input === " ")) return toggleFold(fold.key);
1402
+ if (current?.inFold && key.leftArrow) {
1403
+ toggleFold(current.inFold);
1404
+ return setCursors({ ...cursors, [cursorKey]: items.findIndex((i) => i.fold?.key === current.inFold) });
1405
+ }
1406
+ if (sidebar && key.leftArrow) return setFocus("sidebar");
1407
+ if (sidebar && key.rightArrow && detail) return setFocus("detail"), setDetailCursor(0);
1408
+ if (key.upArrow) return move(-1);
1409
+ if (key.downArrow) return move(1);
1410
+ if (!sidebar && (key.leftArrow || key.rightArrow)) {
1411
+ const i = PLACES.findIndex(([p]) => p === place) + (key.leftArrow ? -1 : 1);
1412
+ if (PLACES[i]) goPlace(PLACES[i][0]);
1413
+ return;
1414
+ }
1415
+ if (key.escape) {
1416
+ if (query) return setQuery("");
1417
+ if (sidebar) return setFocus("sidebar");
1418
+ if (chip !== "all") return setChip("all");
1419
+ if (dashboard) return setOpen(null);
1420
+ return;
1421
+ }
1422
+ if (key.tab && chips.length) {
1423
+ const i = chips.findIndex(([c]) => c === chip);
1424
+ return setChip(chips[(i + (key.shift ? chips.length - 1 : 1)) % chips.length][0]);
1425
+ }
1426
+ if (!current) return;
1427
+ if (key.return && current.onEnter) return current.onEnter();
1428
+ if (place === "settings") return input === " " && current.onEnter ? current.onEnter() : void 0;
1429
+ if (place === "projects" && !open) return key.return ? (setOpen(current.name), setQuery(""), setChip("all")) : void 0;
1430
+ if (key.return && detail) return setFocus("detail"), setDetailCursor(0);
1431
+ if (input === " ") return fixSelected();
1432
+ });
1433
+ function modalKeys(input, key) {
1434
+ const m = modal;
1435
+ if (m.kind === "help") return setModal(null);
1436
+ if (m.kind === "agents") {
1437
+ if (key.escape && !m.then) return setModal(null);
1438
+ if (key.upArrow) return setModal({ ...m, cursor: Math.max(0, m.cursor - 1) });
1439
+ if (key.downArrow) return setModal({ ...m, cursor: Math.min(HARNESSES.length - 1, m.cursor + 1) });
1440
+ if (input === " ") {
1441
+ const id = HARNESSES[m.cursor].id;
1442
+ return setModal({ ...m, selected: m.selected.includes(id) ? m.selected.filter((x) => x !== id) : [...m.selected, id] });
1443
+ }
1444
+ if (key.return) {
1445
+ setModal(null);
1446
+ apply((w) => w.ops.setAgents(m.selected));
1447
+ m.then?.();
1448
+ }
1449
+ return;
1450
+ }
1451
+ if (m.kind === "input") {
1452
+ if (key.escape) return setModal(null);
1453
+ if (key.return) return setModal(null), m.submit(m.value.trim());
1454
+ if (key.backspace || key.delete) return setModal({ ...m, value: m.value.slice(0, -1) });
1455
+ if (key.ctrl && input === "u") return setModal({ ...m, value: "" });
1456
+ if (input && !key.ctrl && !key.meta) return setModal({ ...m, value: m.value + input });
1457
+ return;
1458
+ }
1459
+ if (m.kind === "menu") {
1460
+ if (key.escape) return setModal(null);
1461
+ if (key.upArrow) return setModal({ ...m, cursor: Math.max(0, m.cursor - 1) });
1462
+ if (key.downArrow) return setModal({ ...m, cursor: Math.min(m.options.length - 1, m.cursor + 1) });
1463
+ if (key.return && m.options[m.cursor]) return setModal(null), choose(m.options[m.cursor]);
1464
+ return;
1465
+ }
1466
+ if (key.escape) return setModal(null);
1467
+ if (m.kind === "confirm") {
1468
+ if (key.return || input === "y") {
1469
+ setModal(null);
1470
+ setFocus("list");
1471
+ apply(m.fix.run);
1472
+ }
1473
+ return;
1474
+ }
1475
+ if (m.kind === "repos") {
1476
+ const repos = (m.only ?? world.projects.map((p) => p.name)).filter((r) => !m.query || matches(r, m.query));
1477
+ if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
1478
+ if (key.backspace || key.delete) return setModal({ ...m, query: m.query.slice(0, -1), cursor: 0 });
1479
+ if (key.upArrow) return setModal({ ...m, cursor: Math.max(0, m.cursor - 1) });
1480
+ if (key.downArrow) return setModal({ ...m, cursor: Math.min(repos.length - 1, m.cursor + 1) });
1481
+ if (input === " ") {
1482
+ const picked = new Set(m.picked);
1483
+ const r = repos[m.cursor];
1484
+ if (!r) return;
1485
+ if (!picked.delete(r)) picked.add(r);
1486
+ return setModal({ ...m, picked });
1487
+ }
1488
+ if (key.return) {
1489
+ if (!m.picked.size && !m.fix.allowNone) return setToast("Tick at least one repo with space, or esc to cancel");
1490
+ setModal(null);
1491
+ setFocus("list");
1492
+ apply((w) => m.fix.pickRepos(w, [...m.picked]));
1493
+ }
1494
+ return;
1495
+ }
1496
+ if (m.kind === "fixall") {
1497
+ if (key.upArrow) return setModal({ ...m, cursor: Math.max(0, m.cursor - 1) });
1498
+ if (key.downArrow) return setModal({ ...m, cursor: Math.min(m.items.length - 1, m.cursor + 1) });
1499
+ if (input === " ") return setModal({ ...m, items: m.items.map((it, i) => i === m.cursor ? { ...it, on: !it.on } : it) });
1500
+ if (key.return) {
1501
+ setModal(null);
1502
+ const chosen = m.items.filter((it) => it.on);
1503
+ apply((w) => {
1504
+ let ok = 0;
1505
+ for (const it of chosen) {
1506
+ try {
1507
+ it.fix.run(w);
1508
+ ok++;
1509
+ } catch {
1510
+ }
1511
+ }
1512
+ return `Applied ${ok} fix${ok === 1 ? "" : "es"} (anything removed is in Settings \u203A Backups)`;
1513
+ });
1514
+ }
1515
+ return;
1516
+ }
1517
+ if (m.kind === "add") {
1518
+ const rows = addRows(world, m.target, m.query);
1519
+ const cursor2 = addCursor(rows, m.cursor);
1520
+ const step = (d) => {
1521
+ let i = cursor2 + d;
1522
+ while (rows[i]?.header) i += d;
1523
+ return rows[i] ? setModal({ ...m, cursor: i }) : void 0;
1524
+ };
1525
+ if (key.upArrow) return step(-1);
1526
+ if (key.downArrow) return step(1);
1527
+ if (key.backspace || key.delete) return setModal({ ...m, query: m.query.slice(0, -1), cursor: 0 });
1528
+ if (key.return && rows[cursor2]?.agent) {
1529
+ setModal(null);
1530
+ return ui.copy(writeSkillPrompt(world, m.query, m.target), "a prompt for an agent to write the skill");
1531
+ }
1532
+ if (key.return && rows[cursor2]?.run) {
1533
+ const row = rows[cursor2];
1534
+ if (row.create && !/^[a-z0-9]/.test(m.query)) return setToast("Type the new skill's name first");
1535
+ setModal(null);
1536
+ apply(row.run);
1537
+ if (row.create) edit(world.ops.libraryFile(m.query));
1538
+ return;
1539
+ }
1540
+ if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
1541
+ }
1542
+ }
1543
+ const bottomH = 1;
1544
+ const confirmLines = modal?.kind === "confirm" ? wrap(modal.fix.preview, columns - 6, 6) : [];
1545
+ const confirmH = modal?.kind === "confirm" ? confirmLines.length + 2 : 0;
1546
+ const bodyH = Math.max(8, termRows - (sidebar ? 0 : 1) - bottomH - confirmH);
1547
+ const globalFlagged = world.machine.map((m) => [m.name, machineIssues(world, m)]).filter(([, issues]) => issues.length);
1548
+ const globalIssues = new Set(globalFlagged.map(([name]) => name)).size;
1549
+ const globalProblem = globalFlagged.some(([, issues]) => issues.some((x) => x.severity === "problem"));
1550
+ const badge = (p) => {
1551
+ const found = projectIssues(world, p);
1552
+ const top = worst(found.map((x) => x.issue));
1553
+ return { text: top ? SEV[top.severity].icon : "", width: 3, color: top ? SEV[top.severity].color : void 0 };
1554
+ };
1555
+ const sideRow = (key, label, here, go, count = []) => ({
1556
+ key,
1557
+ header: false,
1558
+ here: here && !onHelp,
1559
+ go,
1560
+ cells: [{ text: label, grow: true, bold: !key.startsWith("repo:") }, ...count]
1561
+ });
1562
+ const sideHeader = (key, text) => ({ key, header: true, here: false, go: () => {
1563
+ }, cells: [{ text }] });
1564
+ const toRepo = (name) => () => (setPlace("projects"), setOpen(name), setQuery(""), setChip("all"));
1565
+ const sideRows = [
1566
+ sideRow("library", "\u25A4 Your skills", place === "library", () => goPlace("library"), [{ text: String(world.library.length), width: 4, color: color.muted }]),
1567
+ // The number turns red or yellow when any of them has an issue.
1568
+ sideRow("global", "\u25C8 Global", place === "global", () => goPlace("global"), [
1569
+ { text: String(new Set(world.machine.map((m) => m.name)).size), width: 4, color: globalProblem ? color.red : globalIssues ? color.yellow : color.muted }
1570
+ ]),
1571
+ sideRow("health", "\u2713 Health", place === "health", () => goPlace("health"), [
1572
+ { text: String(healthCount), width: 4, color: healthWorst ? SEV[healthWorst.severity].color : color.green }
1573
+ ]),
1574
+ sideHeader("#projects", "Projects"),
1575
+ ...world.projects.filter((p) => !sideQuery || matches(p.name, sideQuery)).map((p) => sideRow(`repo:${p.name}`, `${p.name === world.cwd ? "\u25C6" : " "} ${p.name}`, place === "projects" && open === p.name, toRepo(p.name), [badge(p.name)])),
1576
+ sideHeader("#end", ""),
1577
+ sideRow("settings", "\u2699 Settings", place === "settings", () => goPlace("settings")),
1578
+ { ...sideRow("help", "? Help", false, () => {
1579
+ }), here: onHelp }
1580
+ ];
1581
+ const sideAt = sideRows.findIndex((r) => r.here);
1582
+ const topBar = /* @__PURE__ */ jsx2(Box2, { height: 1, width: columns, justifyContent: "space-between", children: /* @__PURE__ */ jsxs2(Box2, { children: [
1583
+ /* @__PURE__ */ jsx2(Text2, { color: color.accent, bold: true, children: " \u25C6 skilllib " }),
1584
+ PLACES.map(([p, label]) => /* @__PURE__ */ jsxs2(Text2, { backgroundColor: place === p ? color.accent : void 0, color: place === p ? "#0B1020" : color.muted, bold: place === p, children: [
1585
+ ` ${label}`,
1586
+ /* @__PURE__ */ jsx2(Text2, { color: place === p ? "#0B1020" : healthWorst ? SEV[healthWorst.severity].color : color.green, children: p === "health" && healthCount ? ` ${healthCount}` : "" }),
1587
+ " "
1588
+ ] }, p))
1589
+ ] }) });
1590
+ const chipRow = /* @__PURE__ */ jsx2(Box2, { height: 1, overflow: "hidden", children: /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate-end", children: [
1591
+ chips.map(([c, label], i) => {
1592
+ const n = all.filter((x) => !x.header && !x.action && inChip(x, c)).length;
1593
+ return /* @__PURE__ */ jsxs2(Text2, { children: [
1594
+ i ? " " : "",
1595
+ /* @__PURE__ */ jsx2(
1596
+ Text2,
1597
+ {
1598
+ backgroundColor: chip === c ? color.accent : void 0,
1599
+ color: chip === c ? "#0B1020" : c === "issues" && n ? color.yellow : color.muted,
1600
+ bold: chip === c,
1601
+ children: ` ${label} ${n} `
1602
+ }
1603
+ )
1604
+ ] }, c);
1605
+ }),
1606
+ /* @__PURE__ */ jsx2(Text2, { color: color.accent, children: query ? ` \u2315 ${query}\u258F` : "" })
1607
+ ] }) });
1608
+ const columnTitles = dashboard || place === "global" ? titles([["", 2], ["Skill", nameW], ["Source", 14], [" Uses", 6], [" Issue", 0]]) : place === "projects" ? titles([["", 2], ["Project", 18], ["Skills", 10], ["Issues", 0]]) : place === "library" ? titles([["", 2], ["Skill", nameW], ["Ver", 5], ["Used in", 18], [" Uses", 6], [" Issue / description", 0]]) : place === "health" ? titles([["", 2], ["Skill", nameW], ["Where", 16], [" Issue", 0]]) : null;
1609
+ const showChips = chips.length > 0;
1610
+ const listHeader = /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
1611
+ showChips ? chipRow : null,
1612
+ columnTitles ? /* @__PURE__ */ jsx2(TitleRow, { cells: columnTitles, width: columns }) : null
1613
+ ] });
1614
+ const overviewH = dashboard && !sidebar ? 4 : 0;
1615
+ const listH = split ? Math.floor((bodyH - overviewH) / 2) : bodyH - overviewH;
1616
+ const detailH = split ? bodyH - overviewH - listH : bodyH;
1617
+ const listRows = items.map((i) => ({ key: i.key, cells: i.cells, header: i.header }));
1618
+ const listTitle = dashboard ? "Skills" : place === "projects" ? "Projects" : place === "library" ? "Your skills" : place === "global" ? "Loads everywhere" : place === "health" ? "To fix" : "Settings";
1619
+ const list = /* @__PURE__ */ jsx2(
1620
+ ListPanel,
1621
+ {
1622
+ title: listTitle,
1623
+ focused: focus === "list",
1624
+ width: columns,
1625
+ height: listH,
1626
+ rows: listRows,
1627
+ selected: Math.max(0, cursor),
1628
+ empty: query ? `Nothing matches "${query}" \xB7 esc clears` : chip === "issues" || place === "health" ? "\u2713 Nothing to fix" : "Nothing here",
1629
+ header: listHeader,
1630
+ headerHeight: (showChips ? 1 : 0) + (columnTitles ? 1 : 0)
1631
+ }
1632
+ );
1633
+ const side = sidebar && focus === "sidebar" && dashboard ? /* @__PURE__ */ jsx2(Panel, { title: open, focused: false, width: columns, height: detailH, children: /* @__PURE__ */ jsx2(InfoGrid, { rows: overview(world, open), columns: 2, width: columns }) }) : place === "projects" && !open && current && !current.header ? /* @__PURE__ */ jsx2(Panel, { title: current.name, focused: false, width: columns, height: detailH, children: /* @__PURE__ */ jsx2(InfoGrid, { rows: overview(world, current.name), columns: 2, width: columns }) }) : detail ? /* @__PURE__ */ jsx2(DetailPanel, { detail, width: columns, height: detailH, focused: focus === "detail", cursor: detailCursor }) : null;
1634
+ let body;
1635
+ if (modal?.kind === "help") body = /* @__PURE__ */ jsx2(Help, { width: columns, height: bodyH });
1636
+ else if (modal?.kind === "menu")
1637
+ body = /* @__PURE__ */ jsx2(
1638
+ ListPanel,
1639
+ {
1640
+ title: modal.title,
1641
+ focused: true,
1642
+ width: columns,
1643
+ height: bodyH,
1644
+ selected: modal.cursor,
1645
+ empty: "Nothing to do here",
1646
+ rows: modal.options.map((o, i) => ({ key: String(i), cells: [{ text: o.label, grow: true }] }))
1647
+ }
1648
+ );
1649
+ else if (modal?.kind === "input")
1650
+ body = /* @__PURE__ */ jsxs2(Panel, { title: modal.title, focused: true, width: columns, height: 5, children: [
1651
+ /* @__PURE__ */ jsx2(Text2, { color: color.text, children: modal.value + "\u258F" }),
1652
+ /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: "enter to confirm \xB7 ctrl+u clears \xB7 esc cancels" })
1653
+ ] });
1654
+ else if (modal?.kind === "agents")
1655
+ body = /* @__PURE__ */ jsx2(
1656
+ ListPanel,
1657
+ {
1658
+ title: "Which agents do you use?",
1659
+ focused: true,
1660
+ width: columns,
1661
+ height: bodyH,
1662
+ selected: modal.cursor,
1663
+ empty: "",
1664
+ header: /* @__PURE__ */ jsx2(Text2, { color: color.muted, children: "skilllib makes your skills visible to each one. Detected ones are ticked." }),
1665
+ headerHeight: 1,
1666
+ rows: HARNESSES.map((h) => ({
1667
+ key: h.id,
1668
+ cells: [
1669
+ { text: modal.selected.includes(h.id) ? "[x] " : "[ ] ", width: 4, color: modal.selected.includes(h.id) ? color.green : color.faint },
1670
+ { text: `${h.icon} ${h.name}`, width: 18, color: h.color },
1671
+ { text: `reads ${h.projectDirs.join(", ")}`, grow: true, color: color.faint }
1672
+ ]
1673
+ }))
1674
+ }
1675
+ );
1676
+ else if (modal?.kind === "repos") {
1677
+ const repos = (modal.only ?? world.projects.map((p) => p.name)).filter((r) => !modal.query || matches(r, modal.query));
1678
+ const name = current?.name ?? "";
1679
+ const pickerLines = wrap(modal.fix.preview, columns - 6, 4);
1680
+ body = /* @__PURE__ */ jsx2(
1681
+ ListPanel,
1682
+ {
1683
+ title: `${modal.fix.label.replace("\u2026", "")}: ${name}`,
1684
+ focused: true,
1685
+ width: columns,
1686
+ height: bodyH,
1687
+ selected: modal.cursor,
1688
+ empty: "No repos",
1689
+ header: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
1690
+ pickerLines.map((l, i) => /* @__PURE__ */ jsx2(Text2, { color: color.text, children: l }, i)),
1691
+ /* @__PURE__ */ jsx2(Text2, { color: modal.query ? color.accent : color.muted, children: modal.query ? `\u2315 ${modal.query}\u258F` : `ticked: repos where it was used${modal.fix.allowNone ? " (or none)" : ""} \xB7 type to filter` })
1692
+ ] }),
1693
+ headerHeight: pickerLines.length + 1,
1694
+ rows: repos.map((r) => ({
1695
+ key: r,
1696
+ cells: [
1697
+ { text: modal.picked.has(r) ? "[x] " : "[ ] ", width: 4, color: modal.picked.has(r) ? color.green : color.faint },
1698
+ { text: r, width: 20 },
1699
+ { text: world.usage[r]?.[name] ? `used ${world.usage[r][name]}\xD7 here` : "", grow: true, color: color.faint }
1700
+ ]
1701
+ }))
1702
+ }
1703
+ );
1704
+ } else if (modal?.kind === "fixall") {
1705
+ body = /* @__PURE__ */ jsx2(
1706
+ ListPanel,
1707
+ {
1708
+ title: `Fix automatically (${modal.items.filter((i) => i.on).length} of ${modal.items.length})`,
1709
+ focused: true,
1710
+ width: columns,
1711
+ height: bodyH,
1712
+ selected: modal.cursor,
1713
+ empty: "",
1714
+ header: /* @__PURE__ */ jsx2(Text2, { color: color.muted, children: "Decisions are never listed here: they stay with you." }),
1715
+ headerHeight: 1,
1716
+ rows: modal.items.map((it, i) => ({
1717
+ key: String(i),
1718
+ cells: [
1719
+ { text: it.on ? "[x] " : "[ ] ", width: 4, color: it.on ? color.green : color.faint },
1720
+ { text: it.label, grow: true }
1721
+ ]
1722
+ }))
1723
+ }
1724
+ );
1725
+ } else if (modal?.kind === "add") {
1726
+ const rows = addRows(world, modal.target, modal.query);
1727
+ body = /* @__PURE__ */ jsx2(
1728
+ ListPanel,
1729
+ {
1730
+ title: modal.target ? `Add a skill to ${modal.target}` : "New skill",
1731
+ focused: true,
1732
+ width: columns,
1733
+ height: bodyH,
1734
+ selected: addCursor(rows, modal.cursor),
1735
+ empty: "",
1736
+ header: /* @__PURE__ */ jsx2(Text2, { color: modal.query ? color.accent : color.muted, children: modal.query ? `\u2315 ${modal.query}\u258F` : modal.target ? "type to search your library, or to name a new skill" : "type the new skill's name" }),
1737
+ headerHeight: 1,
1738
+ rows: rows.map(
1739
+ (r) => r.header ? { key: r.key, header: true, cells: [{ text: r.header }] } : r.create || r.agent ? { key: r.key, cells: [{ text: r.name, grow: true, color: color.accent }] } : {
1740
+ key: r.key,
1741
+ cells: [
1742
+ { text: clip(r.name, 22), width: 22 },
1743
+ { text: r.note, width: 6, color: color.accent },
1744
+ { text: r.description, grow: true, color: color.faint }
1745
+ ]
1746
+ }
1747
+ )
1748
+ }
1749
+ );
1750
+ } else if (!split && focus === "detail" && side) body = side;
1751
+ else
1752
+ body = /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
1753
+ overviewH ? /* @__PURE__ */ jsx2(Panel, { title: open, focused: false, width: columns, height: overviewH, children: /* @__PURE__ */ jsx2(InfoGrid, { rows: overview(world, open), columns: 2, width: columns }) }) : null,
1754
+ list,
1755
+ split ? side : null
1756
+ ] });
1757
+ const hints = modal?.kind === "confirm" ? "enter apply \xB7 esc cancel" : modal?.kind === "repos" || modal?.kind === "fixall" ? "space tick \xB7 enter apply \xB7 esc cancel" : modal?.kind === "agents" ? "space tick \xB7 enter save" : modal?.kind === "menu" ? "\u2191\u2193 choose \xB7 enter do it \xB7 esc cancel" : modal?.kind === "add" ? "enter add \xB7 esc cancel" : modal ? "any key closes" : focus === "detail" ? "\u2191\u2193 \u2190\u2192 choose \xB7 enter apply \xB7 esc back" : focus === "sidebar" ? "\u2191\u2193 choose \xB7 enter open \xB7 type to filter repos" : place === "projects" && !open ? `type to filter \xB7 enter open \xB7 ${sidebar ? "\u2190 sidebar" : "\u2190\u2192 places"} \xB7 ? keys` : place === "settings" ? `enter toggle \xB7 ${sidebar ? "\u2190 sidebar" : "\u2190\u2192 places"} \xB7 ? keys` : `type to filter \xB7 space \u2605 fix \xB7 enter actions \xB7 tab tabs${sidebar ? " \xB7 esc back" : `${dashboard ? " \xB7 esc all projects" : ""} \xB7 ? keys`}`;
1758
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: termCols, height: termRows, children: [
1759
+ sidebar ? null : topBar,
1760
+ /* @__PURE__ */ jsxs2(Box2, { children: [
1761
+ sidebar ? /* @__PURE__ */ jsx2(
1762
+ ListPanel,
1763
+ {
1764
+ title: sideQuery ? `\u25C6 skilllib \u2315 ${sideQuery}\u258F` : "\u25C6 skilllib",
1765
+ focused: focus === "sidebar",
1766
+ width: SIDEBAR_W,
1767
+ height: bodyH + confirmH,
1768
+ rows: sideRows,
1769
+ selected: sideAt,
1770
+ empty: ""
1771
+ }
1772
+ ) : null,
1773
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: columns, children: [
1774
+ body,
1775
+ modal?.kind === "confirm" ? /* @__PURE__ */ jsx2(Panel, { title: modal.fix.label, focused: true, width: columns, height: confirmH, children: confirmLines.map((l, i) => /* @__PURE__ */ jsx2(Text2, { color: color.text, children: l }, i)) }) : null
1776
+ ] })
1777
+ ] }),
1778
+ /* @__PURE__ */ jsxs2(Box2, { height: 1, width: termCols, justifyContent: "space-between", children: [
1779
+ /* @__PURE__ */ jsx2(Text2, { color: toast.startsWith("\u2713") || toast.startsWith("\u21BA") ? color.green : color.muted, wrap: "truncate-end", children: " " + toast }),
1780
+ toast ? null : /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: hints + " " })
1781
+ ] })
1782
+ ] });
1783
+ }
1784
+ function SegLine({ segs }) {
1785
+ return /* @__PURE__ */ jsx2(Box2, { height: 1, overflow: "hidden", children: /* @__PURE__ */ jsx2(Text2, { wrap: "truncate-end", children: segs.map(([t, c], i) => /* @__PURE__ */ jsx2(Text2, { color: c, children: t }, i)) }) });
1786
+ }
1787
+ function DetailPanel({ detail, width, height, focused, cursor }) {
1788
+ const visible = height - 2;
1789
+ const at = detail.body.findIndex((l) => l.option === cursor || l.row?.includes(cursor));
1790
+ const offset = focused && at >= visible ? at - visible + 2 : 0;
1791
+ return /* @__PURE__ */ jsx2(Panel, { title: detail.title, focused, width, height, children: detail.body.slice(offset, offset + visible).map((l, i) => {
1792
+ if (l.row)
1793
+ return /* @__PURE__ */ jsx2(Box2, { height: 1, overflow: "hidden", children: /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate-end", children: [
1794
+ l.segs.map(([t, c], j) => /* @__PURE__ */ jsx2(Text2, { color: c, children: t }, j)),
1795
+ l.row.map((o2, j) => /* @__PURE__ */ jsxs2(Text2, { children: [
1796
+ /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: j ? " \xB7 " : "" }),
1797
+ /* @__PURE__ */ jsx2(Text2, { color: focused && o2 === cursor ? color.text : color.muted, bold: focused && o2 === cursor, backgroundColor: focused && o2 === cursor ? color.selection : void 0, children: detail.options[o2].label })
1798
+ ] }, o2))
1799
+ ] }) }, i);
1800
+ if (l.option === void 0) return /* @__PURE__ */ jsx2(SegLine, { segs: l.segs.length ? l.segs : [[" "]] }, i);
1801
+ const o = detail.options[l.option];
1802
+ const on = focused && l.option === cursor;
1803
+ return /* @__PURE__ */ jsx2(Box2, { height: 1, overflow: "hidden", children: /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate-end", children: [
1804
+ /* @__PURE__ */ jsx2(Text2, { color: on ? color.accent : color.faint, children: on ? " \u25B8 " : " " }),
1805
+ /* @__PURE__ */ jsx2(Text2, { color: color.green, children: o.recommended ? "\u2605 " : " " }),
1806
+ /* @__PURE__ */ jsx2(Text2, { color: on ? color.text : color.muted, bold: on, backgroundColor: on ? color.selection : void 0, children: o.label })
1807
+ ] }) }, i);
1808
+ }) });
1809
+ }
1810
+ function Help({ width, height }) {
1811
+ const keys = [
1812
+ ["a-z 0-9 -", "filter the pane you're in (initials work: cfw finds cloudflare-workers)"],
1813
+ ["\u2191 \u2193", "move"],
1814
+ ["enter \u2192", "one pane deeper: Places \u2192 list \u2192 details (and applies the chosen fix)"],
1815
+ ["space", "apply the \u2605 recommended fix (or show the choices when it's your call)"],
1816
+ ["tab \u21E7tab", "next / previous tab: All \xB7 Issues \xB7 Local \xB7 Global \xB7 Plugins \xB7 Vendor"],
1817
+ ["", "under 80 columns there's no Places pane: \u2190 \u2192 switch places instead"],
1818
+ ["esc \u2190", "one pane back (esc clears a filter first)"],
1819
+ ["ctrl+r", "read everything again from disk"],
1820
+ ["ctrl+c", "quit"]
1821
+ ];
1822
+ return /* @__PURE__ */ jsxs2(Panel, { title: "Keys", hint: "any key closes", focused: true, width, height, children: [
1823
+ keys.map(([k, d]) => /* @__PURE__ */ jsxs2(Text2, { children: [
1824
+ /* @__PURE__ */ jsx2(Text2, { color: color.accent, children: k.padEnd(12) }),
1825
+ /* @__PURE__ */ jsx2(Text2, { color: color.text, children: d })
1826
+ ] }, k)),
1827
+ /* @__PURE__ */ jsx2(Text2, { children: " " }),
1828
+ /* @__PURE__ */ jsx2(Text2, { color: color.muted, children: '\u2715 problem \xB7 \u26A0 warning \xB7 \xB7 hint \xB7 \u2605 recommended fix \xB7 "your call" = a decision, never auto-fixed' }),
1829
+ /* @__PURE__ */ jsx2(Text2, { color: color.muted, children: "Source: lib = from your library \xB7 repo = committed by the team \xB7 untracked = only on this machine" }),
1830
+ /* @__PURE__ */ jsx2(Text2, { color: color.muted, children: "Usage counts Claude Code sessions only: other agents don't record skill use." })
1831
+ ] });
1832
+ }
1833
+
1834
+ // src/tui/load.ts
1835
+ import { execFileSync, spawn, spawnSync as spawnSync3 } from "child_process";
1836
+ import { existsSync as existsSync3, lstatSync, realpathSync } from "fs";
1837
+ import { basename, dirname, join as join2 } from "path";
1838
+ function tildify3(p) {
1839
+ const home = userHome();
1840
+ return (p.startsWith(home) ? "~" + p.slice(home.length) : p).replace(/\\/g, "/");
1841
+ }
1842
+ function uniqueNames(roots) {
1843
+ const tail = (r, n) => r.split(/[\\/]/).filter(Boolean).slice(-n).join("/");
1844
+ return new Map(
1845
+ roots.map((r) => {
1846
+ let n = 1;
1847
+ while (roots.some((o) => o !== r && tail(o, n) === tail(r, n)) && tail(r, n) !== tail(r, n + 1)) n++;
1848
+ return [r, tail(r, n)];
1849
+ })
1850
+ );
1851
+ }
1852
+ function localSkill(root, s, git2) {
1853
+ const managed = {
1854
+ ok: {},
1855
+ "update available": {},
1856
+ "edited locally": { edited: true },
1857
+ "edited locally, update available": { edited: true },
1858
+ "folder missing": { missing: true },
1859
+ "not in library": { library: "missing" }
1860
+ };
1861
+ const other = {
1862
+ "untracked copy of library skill": { source: "untracked", library: "same" },
1863
+ "untracked, differs from library": { source: "untracked", library: "differs" },
1864
+ "local only": { source: "untracked" },
1865
+ "repo skill": { source: "repo" },
1866
+ "repo skill, in library": { source: "repo", library: "same" },
1867
+ "repo skill, differs from library": { source: "repo", library: "differs" }
1868
+ };
1869
+ return {
1870
+ name: s.name,
1871
+ source: "lib",
1872
+ dir: s.location,
1873
+ path: s.path,
1874
+ agents: s.visibility.map((v) => v.id),
1875
+ ...s.managed ? { library: "same", version: s.version ?? void 0, ...managed[s.state] } : other[s.state],
1876
+ git: git2 ? git2.of(relativeTo(root, s.path)) : null
1877
+ };
1878
+ }
1879
+ var partsOf = /* @__PURE__ */ new Map();
1880
+ function pluginParts(root) {
1881
+ if (!partsOf.has(root))
1882
+ partsOf.set(
1883
+ root,
1884
+ [
1885
+ ["commands", "commands"],
1886
+ ["agents", "agents"],
1887
+ ["hooks", "hooks"],
1888
+ [".mcp.json", "MCP servers"]
1889
+ ].flatMap(([file, what]) => existsSync3(join2(root, file)) ? [what] : [])
1890
+ );
1891
+ return partsOf.get(root);
1892
+ }
1893
+ function installedAt(path) {
1894
+ try {
1895
+ const st = lstatSync(path);
1896
+ const at = st.birthtimeMs > 0 ? st.birthtime : st.mtime;
1897
+ return new Date(at.getTime() - at.getTimezoneOffset() * 6e4).toISOString().slice(0, 16).replace("T", " ");
1898
+ } catch {
1899
+ return void 0;
1900
+ }
1901
+ }
1902
+ function machineSkill(s, kept) {
1903
+ const global = s.kind === "global" || s.kind === "skills.sh";
1904
+ return {
1905
+ name: s.name,
1906
+ source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : "cursor",
1907
+ where: global ? tildify3(dirname(s.path)) : s.kind === "claude.ai" ? "claude.ai account" : s.kind === "built-in" ? `${s.origin} built-in` : s.origin,
1908
+ ...s.kind === "skills.sh" ? { origin: `npx skills: ${s.origin}` } : {},
1909
+ path: s.path,
1910
+ links: s.links,
1911
+ agents: s.harnesses,
1912
+ ...global && kept.has(s.name) ? { kept: true } : {},
1913
+ ...s.broken ? { broken: true } : {},
1914
+ ...global && s.kind !== "skills.sh" ? { installed: installedAt(s.path) } : {},
1915
+ ...s.kind === "plugin" ? { pluginParts: pluginParts(dirname(dirname(s.path))) } : {}
1916
+ };
1917
+ }
1918
+ function loadWorld() {
1919
+ forgetLatest();
1920
+ const agents = enabledHarnesses();
1921
+ const cwdRoot = findProjectRoot();
1922
+ const here = isProjectCandidate(cwdRoot) && (existsSync3(join2(cwdRoot, ".git")) || existsSync3(join2(cwdRoot, "skilllib.json"))) ? cwdRoot : null;
1923
+ const real = (p) => {
1924
+ try {
1925
+ return realpathSync(p);
1926
+ } catch {
1927
+ return p;
1928
+ }
1929
+ };
1930
+ const visible = visibleProjects();
1931
+ const cwd = here ? visible.find((p) => real(p) === real(here)) ?? here : null;
1932
+ const roots = [.../* @__PURE__ */ new Set([...cwd ? [cwd] : [], ...visible])];
1933
+ const names = uniqueNames(roots);
1934
+ const descriptions = {};
1935
+ const origins = libraryOrigins();
1936
+ const library = librarySkills().map((s) => {
1937
+ descriptions[s.name] = s.description;
1938
+ return { name: s.name, latest: latestVersion(s.name)?.version ?? 1, origin: origins[s.name] };
1939
+ });
1940
+ const kept = keptGlobal();
1941
+ const machine = machineSkills(agents).map((s) => {
1942
+ descriptions[s.name] ??= s.description;
1943
+ return machineSkill(s, kept);
1944
+ });
1945
+ const projects = roots.map((root) => {
1946
+ const status = projectStatus(root);
1947
+ const hasManifest = existsSync3(join2(root, "skilllib.json"));
1948
+ const git2 = status.length || hasManifest ? gitInfo(root) : null;
1949
+ for (const s of status) if (!descriptions[s.name] && !s.state.includes("missing")) descriptions[s.name] = readSkillInfo(s.path).description;
1950
+ return {
1951
+ name: names.get(root),
1952
+ path: root,
1953
+ manifest: hasManifest ? git2 ? git2.of("skilllib.json") : "no git" : "none",
1954
+ skills: status.map((s) => localSkill(root, s, git2))
1955
+ };
1956
+ });
1957
+ const backups = [...listBackups(), ...pluginBackups()].sort((a, b) => b.movedAt.localeCompare(a.movedAt));
1958
+ return {
1959
+ agents,
1960
+ cwd: cwd ? names.get(cwd) : null,
1961
+ projects,
1962
+ machine,
1963
+ library,
1964
+ usage: {},
1965
+ descriptions,
1966
+ backups: backups.map((b) => ({ name: b.name, from: tildify3(b.from), at: b.movedAt })),
1967
+ roots: readConfig().roots,
1968
+ hidden: readConfig().hidden,
1969
+ agentsChosen: harnessesChosen(),
1970
+ ops: realOps(new Map(projects.map((p) => [p.name, p.path])), backups)
1971
+ };
1972
+ }
1973
+ async function loadUsage(w) {
1974
+ const repoOf = new Map(w.projects.map((p) => [p.path, p.name]));
1975
+ const projectOf = projectOfFactory(w.projects.map((p) => p.path));
1976
+ const usage = {};
1977
+ const days = {};
1978
+ const midnight = (t) => new Date(t).setHours(0, 0, 0, 0);
1979
+ const today = midnight(Date.now());
1980
+ for (const use of await scanUsage(30)) {
1981
+ const root = projectOf(use.cwd);
1982
+ const repo = root ? repoOf.get(root) : void 0;
1983
+ const day = 29 - Math.round((today - midnight(Date.parse(use.at))) / 864e5);
1984
+ if (!repo || day < 0 || day > 29) continue;
1985
+ (usage[repo] ??= {})[use.skill] = (usage[repo][use.skill] ?? 0) + 1;
1986
+ ((days[repo] ??= {})[use.skill] ??= new Array(30).fill(0))[day] += 1;
1987
+ }
1988
+ return { usage, days };
1989
+ }
1990
+ function git(root, args) {
1991
+ try {
1992
+ return execFileSync("git", args, { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1993
+ } catch {
1994
+ return "";
1995
+ }
1996
+ }
1997
+ function remoteLabel(url) {
1998
+ if (!url) return void 0;
1999
+ return url.replace(/^[a-z+]+:\/\//, "").replace(/^[^@/]+@/, "").replace(/:(?!\d)/, "/").replace(/\.git$/, "");
2000
+ }
2001
+ function said2(c, repo) {
2002
+ if (c.action === "skipped") return `${c.name}: ${c.reason ?? "skipped"}`;
2003
+ if (c.action === "removed") return `Removed ${c.name} from ${repo}`;
2004
+ return `${c.action === "installed" ? "Added" : "Updated"} ${c.name}${c.to ? ` v${c.to}` : ""} in ${repo}`;
2005
+ }
2006
+ function realOps(roots, backups) {
2007
+ const rootOf = (repo) => roots.get(repo);
2008
+ const find = (repo, name) => projectStatus(rootOf(repo)).find((s) => s.name === name);
2009
+ const info = /* @__PURE__ */ new Map();
2010
+ const withLinks = (c, repo, name) => {
2011
+ if (!c.blocked?.length) return said2(c, repo);
2012
+ return { message: said2(c, repo), then: allowTracked(repo, name, c.blocked) };
2013
+ };
2014
+ const allowTracked = (repo, name, blocked) => ({
2015
+ label: `Also link it in ${blocked.join(", ")}`,
2016
+ preview: `${blocked.join(", ")} is committed in ${repo}: the links will show in git status. skilllib remembers this for ${repo}.`,
2017
+ run: (w) => {
2018
+ allowTrackedLinks(rootOf(repo));
2019
+ return w.ops.link(repo, name, true);
2020
+ }
2021
+ });
2022
+ const orForce = (c, repo, label, force) => c.action === "skipped" && c.reason?.includes("edits") ? { message: said2(c, repo), then: { label, preview: `Your edits to ${c.name} in ${repo} are lost.`, run: () => said2(force(), repo) } } : said2(c, repo);
2023
+ const toLibrary = (path) => {
2024
+ const r = importSkill(path, { force: true });
2025
+ return r.status === "unchanged" ? `${r.name} is already the same in your library` : `${r.name} v${latestVersion(r.name)?.version} is in your library`;
2026
+ };
2027
+ return {
2028
+ add: (repo, name) => withLinks(addSkill(rootOf(repo), name), repo, name),
2029
+ remove: (repo, name) => {
2030
+ const s = find(repo, name);
2031
+ if (!s) return `${name} isn't in ${repo}`;
2032
+ if (s.state.startsWith("repo skill")) return `${name} is the repo's own (committed by your team): skilllib doesn't delete it`;
2033
+ if (!s.managed) return removeUntracked(rootOf(repo), name, s.path), `Removed ${name} from ${repo} (in Settings \u203A Backups)`;
2034
+ return orForce(removeSkill(rootOf(repo), name), repo, "Remove it anyway", () => removeSkill(rootOf(repo), name, { force: true }));
2035
+ },
2036
+ restore: (repo, name) => {
2037
+ const c = syncProject(rootOf(repo)).find((x) => x.name === name);
2038
+ return c ? said2(c, repo) : `${name} is already in place`;
2039
+ },
2040
+ update: (repo, name) => {
2041
+ const c = updateProject(rootOf(repo), [name])[0];
2042
+ return c ? orForce(c, repo, "Update anyway", () => updateProject(rootOf(repo), [name], { force: true })[0]) : `${name} is up to date`;
2043
+ },
2044
+ saveEdits: (repo, name) => {
2045
+ const s = find(repo, name);
2046
+ const saved = toLibrary(s.path);
2047
+ addSkill(rootOf(repo), name);
2048
+ return saved;
2049
+ },
2050
+ discardEdits: (repo, name) => {
2051
+ const s = find(repo, name);
2052
+ removeUntracked(rootOf(repo), name, s.path);
2053
+ return withLinks(addSkill(rootOf(repo), name, s.managed && s.version ? { version: s.version } : {}), repo, name);
2054
+ },
2055
+ link: (repo, name, allow = false) => {
2056
+ const s = find(repo, name);
2057
+ const r = s.managed ? relinkDependency(rootOf(repo), name, { allowTracked: allow }) : linkEverywhere(rootOf(repo), name, s.location, { allowTracked: allow });
2058
+ const message = r.created.length ? `${name}: linked in ${r.created.join(", ")}` : `${name}: nothing to link`;
2059
+ return r.blocked.length && !allow ? { message, then: allowTracked(repo, name, r.blocked) } : message;
2060
+ },
2061
+ track: (repo, name) => withLinks(addSkill(rootOf(repo), name), repo, name),
2062
+ importLocal: (repo, name) => {
2063
+ const s = find(repo, name);
2064
+ importSkill(s.path);
2065
+ return withLinks(addSkill(rootOf(repo), name), repo, name);
2066
+ },
2067
+ copyToLibrary: (repo, name) => toLibrary(find(repo, name).path),
2068
+ unloadGlobal: (m) => {
2069
+ if (!m.broken && !latestVersion(m.name)) importSkill(m.path);
2070
+ const r = unloadGlobal(m.path, m.links);
2071
+ return r.ok ? `${m.name} no longer loads globally (in Settings \u203A Backups)` : `${m.name}: ${r.reason}`;
2072
+ },
2073
+ deleteGlobal: (m) => {
2074
+ const r = deleteGlobal(m.path, m.links);
2075
+ return r.ok ? m.broken ? `Removed the broken link ${m.name}` : `Deleted ${m.name} (in Settings \u203A Backups)` : `${m.name}: ${r.reason}`;
2076
+ },
2077
+ keepGlobal: (name, keep) => (setKeepGlobal([name], keep), keep ? `${name} marked as global on purpose` : `${name} is no longer marked as global on purpose`),
2078
+ moveGlobal: (m, repos, group) => {
2079
+ if (importSkill(m.path).status === "exists") return `${m.name}: your library has a different ${m.name}; nothing moved (update your library from this copy first)`;
2080
+ if (group) recordOrigin(m.name, `group: ${group}`);
2081
+ const results = repos.map((r2) => [r2, addSkill(rootOf(r2), m.name)]);
2082
+ const skipped = results.filter(([, c]) => c.action === "skipped");
2083
+ if (skipped.length)
2084
+ return `${m.name}: not added to ${skipped.map(([r2, c]) => `${r2} (${c.reason})`).join(", ")}, so it still loads globally`;
2085
+ const blocked = results.filter(([, c]) => c.blocked?.length).map(([r2]) => r2);
2086
+ const r = unloadGlobal(m.path, m.links);
2087
+ if (!r.ok) return `${m.name}: ${r.reason}`;
2088
+ return `${m.name} now loads only in ${repos.join(", ")}${blocked.length ? ` (some agents can't see it in ${blocked.join(", ")}: git tracks the folder)` : ""}`;
2089
+ },
2090
+ createSkill: (name) => {
2091
+ const r = createSkill(name, "");
2092
+ return r.ok ? `Created ${name} in your library: edit its SKILL.md from Your skills` : `${name}: ${r.reason}`;
2093
+ },
2094
+ replacePlugin: (id, repos) => {
2095
+ const skills = machineSkills().filter((s) => s.kind === "plugin" && s.origin === id && !s.broken);
2096
+ const clashes = skills.filter((s) => importSkill(s.path).status === "exists").map((s) => s.name);
2097
+ if (clashes.length) return `${id} kept: your library has different ${clashes.join(", ")}; update or delete ${clashes.length === 1 ? "it" : "them"} first`;
2098
+ const skipped = repos.flatMap((r) => skills.flatMap((s) => {
2099
+ const c = addSkill(rootOf(r), s.name);
2100
+ return c.action === "skipped" ? [`${s.name} in ${r} (${c.reason})`] : [];
2101
+ }));
2102
+ if (skipped.length) return `${id} kept: not added ${skipped.join(", ")}`;
2103
+ const copied = `${skills.length} skill${skills.length === 1 ? " from " + id + " is" : "s from " + id + " are"} in your library${repos.length ? ` and in ${repos.join(", ")}` : ""}`;
2104
+ const bin = claudeBinary();
2105
+ const claude = (...args) => spawnSync3(bin, ["plugin", ...args], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 2e4 });
2106
+ const synced = id.endsWith("@synced");
2107
+ const off = bin && !synced && claude("uninstall", id, "--keep-data").status === 0 ? "uninstalled" : bin ? claude("disable", id) : null;
2108
+ if (off === "uninstalled") {
2109
+ recordRemovedPlugin(id, "user");
2110
+ return `${copied}; ${id} is uninstalled (restore it from Settings \u203A Backups, or reinstall with /plugin)`;
2111
+ }
2112
+ if (off?.status === 0)
2113
+ return synced ? `${copied}; ${id} is off (it's synced from claude.ai: remove it there to delete it for good)` : `${copied}; ${id} is off: finish with /plugin uninstall ${id}`;
2114
+ const why = !off ? "the claude command isn't on your PATH" : off.error ? `claude didn't answer: ${off.error.message}` : (off.stderr || off.stdout).trim().split("\n")[0] || `exit ${off.status}`;
2115
+ try {
2116
+ setPluginEnabled(id, false);
2117
+ } catch (e) {
2118
+ return `${copied}; couldn't turn ${id} off (${why}; ${e.message}): turn it off with /plugin`;
2119
+ }
2120
+ return `${copied}; turned ${id} off in ~/.claude/settings.json (${why})${synced ? "" : `: finish with /plugin uninstall ${id}`}`;
2121
+ },
2122
+ deleteLibrary: (name) => {
2123
+ const from = [];
2124
+ for (const root of projectsUsing(name)) {
2125
+ const s = projectStatus(root).find((x) => x.name === name && x.managed);
2126
+ if (s && existsSync3(s.path)) removeUntracked(root, name, s.path);
2127
+ removeSkill(root, name, { force: true });
2128
+ from.push([...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root));
2129
+ }
2130
+ const r = deleteLibrarySkill(name);
2131
+ if (!r.ok) return `${name}: ${r.reason}`;
2132
+ return `Deleted ${name} from your library${from.length ? ` and from ${from.join(", ")}` : ""} (in Settings \u203A Backups)`;
2133
+ },
2134
+ restoreBackup: (i) => {
2135
+ const b = backups[i];
2136
+ const r = b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b);
2137
+ return r.ok ? `Restored ${b.name} to ${tildify3(r.to)}` : `${b.name}: ${r.reason}`;
2138
+ },
2139
+ setAgents: (ids) => (setHarnesses(ids), "Saved your agents"),
2140
+ installVersion: (repo, name, version) => withLinks(addSkill(rootOf(repo), name, { version, force: true }), repo, name),
2141
+ versions: (name) => versionHistory(name).map((v) => ({ version: v.version, date: v.date.slice(0, 10) })),
2142
+ libraryFile: (name) => join2(librarySkillDir(name), "SKILL.md"),
2143
+ addRoot: (path) => {
2144
+ const root = addRoot(path);
2145
+ discoverProjects();
2146
+ return `Scanning ${tildify3(root)} for repos`;
2147
+ },
2148
+ removeRoot: (path) => (removeRoot(path), `Stopped scanning ${tildify3(path)}; repos already found stay until you hide them`),
2149
+ rescan: () => (discoverProjects(), "Looked for repos in your project folders again"),
2150
+ hide: (repo) => (setHidden(rootOf(repo), true), `Hid ${repo}: Settings \u203A Hidden repos brings it back`),
2151
+ unhide: (path) => (setHidden(path, false), `${tildify3(path)} is back in the list`),
2152
+ openFolder: (path) => {
2153
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
2154
+ spawn(cmd, [path], { detached: true, stdio: "ignore" }).unref();
2155
+ return `Opened ${tildify3(path)}`;
2156
+ },
2157
+ copy: (text, what) => {
2158
+ const r = copyText(text);
2159
+ return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor or Codex` : `Couldn't reach the clipboard; ${what} is in ${tildify3(r.savedTo)}`;
2160
+ },
2161
+ repoInfo: (repo) => {
2162
+ if (!info.has(repo)) {
2163
+ const root = rootOf(repo);
2164
+ const status = git(root, ["status", "--porcelain"]);
2165
+ info.set(repo, {
2166
+ remote: remoteLabel(git(root, ["remote", "get-url", "origin"])),
2167
+ branch: git(root, ["rev-parse", "--abbrev-ref", "HEAD"]) || void 0,
2168
+ dirty: status ? status.split("\n").length : 0
2169
+ });
2170
+ }
2171
+ return info.get(repo);
2172
+ }
2173
+ };
2174
+ }
2175
+
2176
+ // src/tui/index.tsx
2177
+ import { jsx as jsx3 } from "react/jsx-runtime";
2178
+ async function tui() {
2179
+ const instance = render(/* @__PURE__ */ jsx3(App, { initial: loadWorld(), reload: loadWorld, loadUsage }), { alternateScreen: true, exitOnCtrlC: true });
2180
+ await instance.waitUntilExit();
2181
+ }
2182
+ export {
2183
+ tui
2184
+ };