skilllib 1.3.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -34
- package/dist/{chunk-TWQQHXVG.js → chunk-AYAN7Q2X.js} +1391 -407
- package/dist/index.js +23 -7
- package/dist/{tui-H5IQAGAL.js → tui-RU255WYX.js} +615 -179
- package/package.json +2 -1
- package/skills/skilllib/SKILL.md +43 -0
|
@@ -1,39 +1,53 @@
|
|
|
1
1
|
import {
|
|
2
2
|
HARNESSES,
|
|
3
|
+
PROJECT_SKILLS_DIR,
|
|
3
4
|
addRoot,
|
|
4
5
|
addSkill,
|
|
6
|
+
addedAt,
|
|
7
|
+
agentSkillState,
|
|
5
8
|
allowTrackedLinks,
|
|
9
|
+
applyTidy,
|
|
6
10
|
claudeBinary,
|
|
11
|
+
claudePlugins,
|
|
12
|
+
copyLabel,
|
|
7
13
|
createSkill,
|
|
14
|
+
cursorPluginSkills,
|
|
8
15
|
deleteGlobal,
|
|
9
16
|
deleteLibrarySkill,
|
|
17
|
+
describeStep,
|
|
10
18
|
discoverProjects,
|
|
11
19
|
enabledHarnesses,
|
|
12
|
-
findProjectRoot,
|
|
13
20
|
forgetLatest,
|
|
14
21
|
gitInfo,
|
|
22
|
+
gitVisibleSteps,
|
|
15
23
|
harness,
|
|
16
24
|
harnessesChosen,
|
|
17
25
|
importSkill,
|
|
18
|
-
|
|
26
|
+
installAgentSkill,
|
|
19
27
|
keptGlobal,
|
|
20
28
|
latestVersion,
|
|
21
29
|
libraryOrigins,
|
|
22
30
|
librarySkillDir,
|
|
23
31
|
librarySkills,
|
|
24
32
|
linkEverywhere,
|
|
33
|
+
linkGlobal,
|
|
25
34
|
listBackups,
|
|
26
35
|
machineSkills,
|
|
36
|
+
planGlobalTidy,
|
|
37
|
+
planProjectTidy,
|
|
27
38
|
pluginBackups,
|
|
39
|
+
projectHere,
|
|
28
40
|
projectOfFactory,
|
|
29
41
|
projectStatus,
|
|
30
42
|
projectsUsing,
|
|
31
43
|
readConfig,
|
|
44
|
+
readManifest,
|
|
32
45
|
readSkillInfo,
|
|
33
46
|
recordOrigin,
|
|
34
47
|
recordRemovedPlugin,
|
|
35
48
|
relativeTo,
|
|
36
49
|
relinkDependency,
|
|
50
|
+
removeAgentSkill,
|
|
37
51
|
removeRoot,
|
|
38
52
|
removeSkill,
|
|
39
53
|
removeUntracked,
|
|
@@ -45,13 +59,16 @@ import {
|
|
|
45
59
|
setKeepGlobal,
|
|
46
60
|
setPluginEnabled,
|
|
47
61
|
skilllibHome,
|
|
62
|
+
skillsLoadedIn,
|
|
48
63
|
syncProject,
|
|
64
|
+
tildify,
|
|
65
|
+
turnOffIn,
|
|
49
66
|
unloadGlobal,
|
|
50
67
|
updateProject,
|
|
51
|
-
userHome,
|
|
52
68
|
versionHistory,
|
|
69
|
+
visibilityOf,
|
|
53
70
|
visibleProjects
|
|
54
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-AYAN7Q2X.js";
|
|
55
72
|
|
|
56
73
|
// src/tui/index.tsx
|
|
57
74
|
import { render } from "ink";
|
|
@@ -177,10 +194,7 @@ import { spawnSync } from "child_process";
|
|
|
177
194
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
178
195
|
import { join } from "path";
|
|
179
196
|
var EXCERPT_CHARS = 1200;
|
|
180
|
-
|
|
181
|
-
const home = userHome();
|
|
182
|
-
return p.startsWith(home) ? "~" + p.slice(home.length) : p;
|
|
183
|
-
}
|
|
197
|
+
var tildify2 = (p) => tildify(p);
|
|
184
198
|
function skillExcerpt(dir, chars) {
|
|
185
199
|
const file = join(dir, "SKILL.md");
|
|
186
200
|
if (!existsSync(file)) return { text: "(SKILL.md not found)", files: 0, size: 0 };
|
|
@@ -204,7 +218,7 @@ function buildReviewPrompt(skills, ctx) {
|
|
|
204
218
|
`- Description: ${s.description || "(none)"}`,
|
|
205
219
|
`- Source: ${s.source}`,
|
|
206
220
|
`- Installed via: ${s.installedHow}`,
|
|
207
|
-
`- Location: ${
|
|
221
|
+
`- Location: ${tildify2(s.path)}${s.links.length ? ` (also linked from ${s.links.map(tildify2).join(", ")})` : ""}`,
|
|
208
222
|
`- Loaded by: ${s.loadedBy}`,
|
|
209
223
|
...s.vendor ? [`- Vendor-managed: ${s.vendor}`] : [],
|
|
210
224
|
...s.keptGlobal ? ["- I marked this as global on purpose: recommend changing that only for a strong reason (unused, duplicated, low quality)"] : [],
|
|
@@ -230,7 +244,7 @@ function buildReviewPrompt(skills, ctx) {
|
|
|
230
244
|
- 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
245
|
|
|
232
246
|
## My projects
|
|
233
|
-
${ctx.projects.map((p) => `- ${p.name} \u2014 ${
|
|
247
|
+
${ctx.projects.map((p) => `- ${p.name} \u2014 ${tildify2(p.path)}`).join("\n") || "(none)"}
|
|
234
248
|
|
|
235
249
|
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
250
|
|
|
@@ -281,9 +295,17 @@ function matchScore(text, query) {
|
|
|
281
295
|
}
|
|
282
296
|
|
|
283
297
|
// src/tui/world.ts
|
|
298
|
+
var failed = (message) => ({ message, failed: true });
|
|
299
|
+
var isFailure = (r) => typeof r !== "string" && "failed" in r;
|
|
300
|
+
var followUp = (r) => typeof r !== "string" && "then" in r ? r.then : void 0;
|
|
301
|
+
var said = (r) => typeof r === "string" ? r : r.message;
|
|
302
|
+
var joined = (results) => {
|
|
303
|
+
const message = results.map(said).join(" \xB7 ");
|
|
304
|
+
return results.some(isFailure) ? failed(message) : message;
|
|
305
|
+
};
|
|
284
306
|
var SEVERITY_RANK = { problem: 0, warning: 1, hint: 2 };
|
|
285
307
|
function filterOf(source) {
|
|
286
|
-
return source === "lib" || source === "repo" || source === "untracked" ? "local" : source === "global" ? "global" : source === "plugin" ? "plugins" : "vendor";
|
|
308
|
+
return source === "lib" || source === "repo" || source === "untracked" ? "local" : source === "global" || source === "skilllib" ? "global" : source === "plugin" ? "plugins" : "vendor";
|
|
287
309
|
}
|
|
288
310
|
function project(w, name) {
|
|
289
311
|
return w.projects.find((p) => p.name === name);
|
|
@@ -299,24 +321,82 @@ function dailyUses(w, name, onlyRepo) {
|
|
|
299
321
|
}
|
|
300
322
|
return days;
|
|
301
323
|
}
|
|
324
|
+
function machineIn(w, p) {
|
|
325
|
+
return p.machine ?? w.machine;
|
|
326
|
+
}
|
|
302
327
|
function usable(w, projectName) {
|
|
303
328
|
const p = project(w, projectName);
|
|
304
329
|
const uses = (n) => w.usage[projectName]?.[n] ?? 0;
|
|
305
330
|
return [
|
|
306
331
|
...p.skills.map((s) => ({ name: s.name, source: s.source, where: s.dir, agents: s.agents, uses: uses(s.name), local: s })),
|
|
307
|
-
...w.
|
|
332
|
+
...machineIn(w, p).map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: uses(m.name), machine: m }))
|
|
308
333
|
];
|
|
309
334
|
}
|
|
310
335
|
function lib(w, name) {
|
|
311
336
|
return w.library.find((l) => l.name === name);
|
|
312
337
|
}
|
|
338
|
+
var agentNames = (ids) => ids.map((a) => harness(a).name).join(", ");
|
|
339
|
+
function usedElsewhere(w, except, name) {
|
|
340
|
+
const repos = w.projects.filter((p) => p.name !== except && (w.usage[p.name]?.[name] ?? 0) > 0 && !p.skills.some((s) => s.name === name)).map((p) => p.name);
|
|
341
|
+
return repos.length ? `, and ${repos.join(", ")} used it in the last 30 days` : "";
|
|
342
|
+
}
|
|
343
|
+
function dupeIssues(repo, name, d) {
|
|
344
|
+
const g = repo === null ? "-g" : "";
|
|
345
|
+
if (d.differ)
|
|
346
|
+
return [
|
|
347
|
+
{
|
|
348
|
+
id: `conflict${g}:${name}`,
|
|
349
|
+
severity: "problem",
|
|
350
|
+
title: `Copies differ: ${d.differ.map((c) => c.label + (c.runs.length ? ` (${agentNames(c.runs)})` : "")).join(" vs ")}`,
|
|
351
|
+
short: "Copies differ",
|
|
352
|
+
decision: true,
|
|
353
|
+
fixes: d.differ.filter((c) => c.canWin).map((c) => ({
|
|
354
|
+
label: `Keep the ${c.label} copy`,
|
|
355
|
+
preview: `The other ${d.differ.length === 2 ? "copy becomes a link" : "copies become links"} to it, so every agent runs this one. Replaced copies go to Settings \u203A Backups.${repo ? " If git tracks a copy that changes, skilllib asks first." : ""}`,
|
|
356
|
+
run: (w) => w.ops.tidy(repo, name, { keep: c.dir })
|
|
357
|
+
}))
|
|
358
|
+
}
|
|
359
|
+
];
|
|
360
|
+
return [
|
|
361
|
+
{
|
|
362
|
+
id: `copies${g}:${name}`,
|
|
363
|
+
severity: "warning",
|
|
364
|
+
title: repo ? "More copies or links in this repo than your agents need" : "Identical copies in several global folders",
|
|
365
|
+
short: "Extra copies",
|
|
366
|
+
decision: false,
|
|
367
|
+
fixes: [
|
|
368
|
+
{
|
|
369
|
+
label: "Keep one copy, plus the links your agents need",
|
|
370
|
+
preview: `${d.steps.join("; ")}. Replaced copies go to Settings \u203A Backups.${d.git.length ? ` Git would see ${d.git.join(", ")}: skilllib asks before ${d.git.length === 1 ? "that one" : "those"}.` : ""}`,
|
|
371
|
+
run: (w) => w.ops.tidy(repo, name)
|
|
372
|
+
}
|
|
373
|
+
]
|
|
374
|
+
}
|
|
375
|
+
];
|
|
376
|
+
}
|
|
377
|
+
function cursorPluginIssue(name, plugin) {
|
|
378
|
+
return {
|
|
379
|
+
id: `cursor-plugin:${name}`,
|
|
380
|
+
severity: "warning",
|
|
381
|
+
title: `Also in the Cursor plugin ${plugin}: Cursor lists both while it's on`,
|
|
382
|
+
short: "Also in a Cursor plugin",
|
|
383
|
+
decision: true,
|
|
384
|
+
fixes: [
|
|
385
|
+
{
|
|
386
|
+
label: "Turn the plugin off in Cursor (Settings \u203A Plugins)",
|
|
387
|
+
preview: "skilllib can't read or change Cursor's plugins. Your skill wins once the plugin is off.",
|
|
388
|
+
run: () => `Turn ${plugin} off in Cursor; skilllib can't tell whether it's on`
|
|
389
|
+
}
|
|
390
|
+
]
|
|
391
|
+
};
|
|
392
|
+
}
|
|
313
393
|
function globalFixes(m) {
|
|
314
394
|
return [
|
|
315
395
|
{
|
|
316
396
|
label: "Move it to the repos that need it\u2026",
|
|
317
397
|
preview: `Pick repos. ${m.name} goes into your library and those repos, and stops loading globally (original backed up).`,
|
|
318
398
|
run: () => "",
|
|
319
|
-
pickRepos: (w, repos) => w.ops.moveGlobal(m, repos)
|
|
399
|
+
pickRepos: (w, repos) => w.ops.moveGlobal([m], repos)
|
|
320
400
|
},
|
|
321
401
|
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
402
|
{ label: "Delete it", preview: `Remove ${m.path}. A backup is kept in Settings \u203A Backups.`, run: (w) => w.ops.deleteGlobal(m) }
|
|
@@ -324,9 +404,10 @@ function globalFixes(m) {
|
|
|
324
404
|
}
|
|
325
405
|
function machineActions(m) {
|
|
326
406
|
if (m.broken) return [];
|
|
327
|
-
return m.source === "global" ? globalFixes(m) : [];
|
|
407
|
+
return m.source === "global" ? globalFixes(m) : m.source === "skilllib" ? [removeAgentSkillFix()] : [];
|
|
328
408
|
}
|
|
329
409
|
function machineIssues(w, m) {
|
|
410
|
+
if (m.source === "skilllib") return [agentSkillIssue(w)].flatMap((i) => i ? [i] : []);
|
|
330
411
|
if (m.broken)
|
|
331
412
|
return [
|
|
332
413
|
{
|
|
@@ -346,42 +427,64 @@ function machineIssues(w, m) {
|
|
|
346
427
|
severity: "problem",
|
|
347
428
|
title: `Loaded twice: also in ${other.source} ${other.where}`,
|
|
348
429
|
short: "Loaded twice",
|
|
349
|
-
|
|
430
|
+
// Every repo then runs the other copy instead of yours: a decision, never "fix all".
|
|
431
|
+
decision: true,
|
|
350
432
|
fixes: [
|
|
351
433
|
{
|
|
352
434
|
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.`,
|
|
435
|
+
preview: `Copy ${m.name} into your library if needed, then move ${m.path} to the backups. Every repo then runs the ${other.source}'s copy instead of yours.`,
|
|
354
436
|
run: (w2) => w2.ops.unloadGlobal(m)
|
|
355
437
|
}
|
|
356
438
|
]
|
|
357
439
|
});
|
|
358
|
-
|
|
359
|
-
if (m.source === "global" &&
|
|
440
|
+
if (m.source === "global" && m.dupes) issues.push(...dupeIssues(null, m.name, m.dupes));
|
|
441
|
+
if (m.source === "global" && m.cursorPlugin && m.agents.includes("cursor")) issues.push(cursorPluginIssue(m.name, m.cursorPlugin));
|
|
442
|
+
const seen = new Set(w.machine.filter((x) => x.name === m.name && !x.broken).flatMap((x) => x.agents));
|
|
443
|
+
const blind = m.source === "global" && m.agents.length && !m.dupes ? w.agents.filter((a) => !seen.has(a)) : [];
|
|
444
|
+
const names = blind.map((a) => harness(a).name).join(", ");
|
|
445
|
+
const link = {
|
|
446
|
+
label: `Link it for ${names}`,
|
|
447
|
+
preview: `Add a link in the global skills folder ${names} read${blind.length === 1 ? "s" : ""} (nothing is copied or moved), so ${names} load${blind.length === 1 ? "s" : ""} it in every repo too.`,
|
|
448
|
+
run: (w2) => w2.ops.linkGlobal(m, blind)
|
|
449
|
+
};
|
|
450
|
+
if (m.kept && blind.length)
|
|
451
|
+
issues.push({ id: `blind-global:${m.name}`, severity: "warning", title: `${names} can't see it: only ${m.agents.map((a) => harness(a).name).join(", ")} load${m.agents.length === 1 ? "s" : ""} it`, short: `${names} can't see it`, decision: false, fixes: [link] });
|
|
452
|
+
if (m.source === "global" && !m.kept) {
|
|
453
|
+
const [move, ...rest] = globalFixes(m);
|
|
360
454
|
issues.push({
|
|
361
|
-
id: `
|
|
362
|
-
severity: "
|
|
363
|
-
title: `
|
|
364
|
-
short: "
|
|
455
|
+
id: `global:${m.name}`,
|
|
456
|
+
severity: "warning",
|
|
457
|
+
title: blind.length ? `Global, not reviewed: loads in every repo, but ${names} can't see it` : "Global, not reviewed: loads in every repo",
|
|
458
|
+
short: blind.length ? `Not reviewed \xB7 ${names} can't see it` : "Not reviewed",
|
|
365
459
|
decision: true,
|
|
366
|
-
fixes:
|
|
460
|
+
fixes: blind.length ? [move, link, ...rest] : [move, ...rest]
|
|
367
461
|
});
|
|
368
|
-
|
|
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) });
|
|
462
|
+
}
|
|
370
463
|
return issues;
|
|
371
464
|
}
|
|
372
465
|
function issuesOf(w, projectName, u) {
|
|
373
466
|
const s = u.local;
|
|
374
467
|
const issues = [];
|
|
375
468
|
const shares = (m) => m.name === s.name && !m.broken && m.agents.some((a) => s.agents.includes(a));
|
|
376
|
-
const
|
|
377
|
-
const
|
|
469
|
+
const loaded = machineIn(w, project(w, projectName));
|
|
470
|
+
const g = loaded.find((m) => m.source === "global" && shares(m));
|
|
471
|
+
const vendor = loaded.find((m) => m.source !== "global" && shares(m));
|
|
378
472
|
const l = lib(w, s.name);
|
|
379
473
|
const remove = {
|
|
380
474
|
label: "Remove this repo's copy",
|
|
381
475
|
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
476
|
run: (w2) => w2.ops.remove(projectName, s.name)
|
|
383
477
|
};
|
|
384
|
-
if (s.missing)
|
|
478
|
+
if (s.missing && s.library === "missing")
|
|
479
|
+
issues.push({
|
|
480
|
+
id: `unavailable:${s.name}`,
|
|
481
|
+
severity: "problem",
|
|
482
|
+
title: "Pinned in skilllib.json, but not in your library: import it, or set SKILLLIB_HOME to the library that has it",
|
|
483
|
+
short: "Pinned, not in your library",
|
|
484
|
+
decision: true,
|
|
485
|
+
fixes: [{ label: "Remove it from skilllib.json", preview: `${projectName}'s skilllib.json stops pinning ${s.name} v${s.version}. There's no folder to delete.`, run: (w2) => w2.ops.remove(projectName, s.name) }]
|
|
486
|
+
});
|
|
487
|
+
else if (s.missing)
|
|
385
488
|
issues.push({
|
|
386
489
|
id: `missing:${s.name}`,
|
|
387
490
|
severity: "problem",
|
|
@@ -390,7 +493,7 @@ function issuesOf(w, projectName, u) {
|
|
|
390
493
|
decision: false,
|
|
391
494
|
fixes: [{ label: "Restore it (sync)", preview: `Reinstall ${s.name} v${s.version} into ${s.dir}.`, run: (w2) => w2.ops.restore(projectName, s.name) }]
|
|
392
495
|
});
|
|
393
|
-
if (s.source === "lib" && s.library === "missing")
|
|
496
|
+
else if (s.source === "lib" && s.library === "missing")
|
|
394
497
|
issues.push({
|
|
395
498
|
id: `not-in-lib:${s.name}`,
|
|
396
499
|
severity: "problem",
|
|
@@ -399,23 +502,50 @@ function issuesOf(w, projectName, u) {
|
|
|
399
502
|
decision: false,
|
|
400
503
|
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
504
|
});
|
|
402
|
-
if (g)
|
|
505
|
+
if (g?.kept)
|
|
506
|
+
issues.push({
|
|
507
|
+
id: `kept-g:${s.name}`,
|
|
508
|
+
severity: "warning",
|
|
509
|
+
title: `Also global in ${g.where}, and you keep it global: this repo's copy is extra`,
|
|
510
|
+
short: "Extra: you keep it global",
|
|
511
|
+
decision: true,
|
|
512
|
+
fixes: [
|
|
513
|
+
...s.source !== "repo" ? [{ ...remove, label: "Remove this repo's copy, keep it global" }] : [],
|
|
514
|
+
{
|
|
515
|
+
label: "Stop loading it globally after all",
|
|
516
|
+
preview: `Copy ${s.name} into your library if needed, then move ${g.path} to the backups. Every other repo stops seeing it${usedElsewhere(w, projectName, s.name)}.`,
|
|
517
|
+
run: (w2) => w2.ops.unloadGlobal(g)
|
|
518
|
+
}
|
|
519
|
+
]
|
|
520
|
+
});
|
|
521
|
+
else if (g)
|
|
403
522
|
issues.push({
|
|
404
523
|
id: `twice-g:${s.name}`,
|
|
405
524
|
severity: "problem",
|
|
406
525
|
title: `Loaded twice: also global in ${g.where}`,
|
|
407
526
|
short: "Loaded twice",
|
|
408
|
-
|
|
527
|
+
// Unloading the global copy changes every repo: a decision, never "fix all".
|
|
528
|
+
decision: true,
|
|
409
529
|
fixes: [
|
|
410
530
|
{
|
|
411
531
|
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.
|
|
532
|
+
preview: `Copy ${s.name} into your library if needed, then move ${g.path} to the backups. Every other repo stops seeing it${usedElsewhere(w, projectName, s.name)}.`,
|
|
413
533
|
run: (w2) => w2.ops.unloadGlobal(g)
|
|
414
534
|
},
|
|
415
535
|
...s.source !== "repo" ? [{ ...remove, label: "Keep it global, remove it from this repo" }] : []
|
|
416
536
|
]
|
|
417
537
|
});
|
|
418
|
-
|
|
538
|
+
const cursorOnly = vendor?.source === "plugin" && w.agents.includes("claude-code") && !vendor.agents.includes("claude-code");
|
|
539
|
+
if (vendor && cursorOnly)
|
|
540
|
+
issues.push({
|
|
541
|
+
id: `twice-p:${s.name}`,
|
|
542
|
+
severity: "warning",
|
|
543
|
+
title: `Same name as a skill in plugin ${vendor.where}: Claude Code has it off in ${projectName}, but Cursor ignores repo settings and lists both`,
|
|
544
|
+
short: "Cursor also lists a plugin's copy",
|
|
545
|
+
decision: true,
|
|
546
|
+
fixes: [replacePluginFix(w, vendor.where)]
|
|
547
|
+
});
|
|
548
|
+
else if (vendor)
|
|
419
549
|
issues.push({
|
|
420
550
|
id: `twice-p:${s.name}`,
|
|
421
551
|
severity: "problem",
|
|
@@ -424,13 +554,19 @@ function issuesOf(w, projectName, u) {
|
|
|
424
554
|
decision: true,
|
|
425
555
|
fixes: [
|
|
426
556
|
...s.source !== "repo" ? [{ ...remove, label: `Remove this repo's copy, use the ${vendor.source}'s` }] : [],
|
|
427
|
-
{
|
|
428
|
-
label:
|
|
429
|
-
preview: "
|
|
557
|
+
vendor.source === "plugin" ? {
|
|
558
|
+
label: `Turn the plugin off in ${projectName} only`,
|
|
559
|
+
preview: `Sets "${vendor.where}": false in ${projectName}'s .claude/settings.local.json (claude plugin disable --scope local), a local-only file git ignores. Other repos keep it.${vendor.agents.includes("cursor") ? " Cursor ignores repo settings, so it still lists both." : ""}`,
|
|
560
|
+
run: (w2) => w2.ops.pluginOffHere(projectName, vendor.where)
|
|
561
|
+
} : {
|
|
562
|
+
label: `Turn it off in ${vendor.where}`,
|
|
563
|
+
preview: "skilllib can't turn it off for you.",
|
|
430
564
|
run: () => `Turn ${vendor.where} off at its source; skilllib picks it up next time`
|
|
431
565
|
}
|
|
432
566
|
]
|
|
433
567
|
});
|
|
568
|
+
if (s.dupes) issues.push(...dupeIssues(projectName, s.name, s.dupes));
|
|
569
|
+
if (s.cursorPlugin && s.agents.includes("cursor")) issues.push(cursorPluginIssue(s.name, s.cursorPlugin));
|
|
434
570
|
if (s.source === "lib" && s.edited)
|
|
435
571
|
issues.push({
|
|
436
572
|
id: `edited:${s.name}`,
|
|
@@ -453,22 +589,36 @@ function issuesOf(w, projectName, u) {
|
|
|
453
589
|
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
590
|
});
|
|
455
591
|
const blind = w.agents.filter((a) => !u.agents.includes(a));
|
|
456
|
-
|
|
592
|
+
const unshared = blind.filter((a) => s.unshared?.includes(a));
|
|
593
|
+
const linkable = blind.filter((a) => !unshared.includes(a));
|
|
594
|
+
const commitFirst = unshared.length ? ` ${agentNames(unshared)} ${unshared.length === 1 ? "needs" : "need"} it committed first (git tracks the folder they read).` : "";
|
|
595
|
+
if (!s.missing && linkable.length)
|
|
457
596
|
issues.push({
|
|
458
597
|
id: `blind:${s.name}`,
|
|
459
598
|
severity: "warning",
|
|
460
|
-
title: `${blind
|
|
461
|
-
short: `${blind
|
|
599
|
+
title: `${agentNames(blind)} can't see it`,
|
|
600
|
+
short: `${agentNames(blind)} can't see it`,
|
|
462
601
|
decision: false,
|
|
463
602
|
fixes: [
|
|
464
603
|
{
|
|
465
604
|
label: "Link it for every agent",
|
|
466
|
-
preview: `Add links (nothing is copied or moved), so ${
|
|
605
|
+
preview: `Add links (nothing is copied or moved), so ${agentNames(linkable)} ${linkable.length === 1 ? "loads" : "load"} it too.${commitFirst}`,
|
|
467
606
|
run: (w2) => w2.ops.link(projectName, s.name)
|
|
468
607
|
}
|
|
469
608
|
]
|
|
470
609
|
});
|
|
471
|
-
if (s.
|
|
610
|
+
else if (!s.missing && unshared.length)
|
|
611
|
+
issues.push({
|
|
612
|
+
id: `blind:${s.name}`,
|
|
613
|
+
severity: "warning",
|
|
614
|
+
title: `${agentNames(unshared)} can't see it: commit ${s.dir}/${s.name} first, then link it (git tracks the folder they read, and a link to an uncommitted copy would break for teammates)`,
|
|
615
|
+
short: `${agentNames(unshared)} can't see it: commit it first`,
|
|
616
|
+
// Nothing skilllib can do: committing is yours.
|
|
617
|
+
decision: true,
|
|
618
|
+
fixes: []
|
|
619
|
+
});
|
|
620
|
+
const differ = !!s.dupes?.differ;
|
|
621
|
+
if (s.source !== "lib" && s.library === "differs" && !differ)
|
|
472
622
|
issues.push({
|
|
473
623
|
id: `differs:${s.name}`,
|
|
474
624
|
severity: "hint",
|
|
@@ -480,22 +630,24 @@ function issuesOf(w, projectName, u) {
|
|
|
480
630
|
...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
631
|
]
|
|
482
632
|
});
|
|
483
|
-
if (s.source === "untracked" && s.library === "same")
|
|
633
|
+
if (s.source === "untracked" && s.library === "same" && !differ)
|
|
484
634
|
issues.push({
|
|
485
635
|
id: `adopt:${s.name}`,
|
|
486
636
|
severity: "hint",
|
|
487
637
|
title: "Same as your library skill, but not tracked",
|
|
488
638
|
short: "Not tracked",
|
|
489
|
-
|
|
639
|
+
// It writes to the repo's skilllib.json (and your library): your call, never in Fix all (as in doctor, #11).
|
|
640
|
+
decision: true,
|
|
490
641
|
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
642
|
});
|
|
492
|
-
if (s.source === "untracked" && !l && !vendor)
|
|
643
|
+
if (s.source === "untracked" && !l && !vendor && !differ)
|
|
493
644
|
issues.push({
|
|
494
645
|
id: `local:${s.name}`,
|
|
495
646
|
severity: "hint",
|
|
496
647
|
title: "Only exists in this repo",
|
|
497
648
|
short: "Only in this repo",
|
|
498
|
-
|
|
649
|
+
// It writes to the repo's skilllib.json (and your library): your call, never in Fix all (as in doctor, #11).
|
|
650
|
+
decision: true,
|
|
499
651
|
fixes: [
|
|
500
652
|
{
|
|
501
653
|
label: "Import it into your library",
|
|
@@ -504,7 +656,7 @@ function issuesOf(w, projectName, u) {
|
|
|
504
656
|
}
|
|
505
657
|
]
|
|
506
658
|
});
|
|
507
|
-
if (s.source === "lib" && w.days && u.uses === 0 && !s.missing)
|
|
659
|
+
if (s.source === "lib" && w.days && u.uses === 0 && !s.missing && (s.added ?? 0) <= Date.now() - 30 * 24 * 60 * 60 * 1e3)
|
|
508
660
|
issues.push({
|
|
509
661
|
id: `unused:${s.name}`,
|
|
510
662
|
severity: "hint",
|
|
@@ -515,6 +667,53 @@ function issuesOf(w, projectName, u) {
|
|
|
515
667
|
});
|
|
516
668
|
return issues;
|
|
517
669
|
}
|
|
670
|
+
function agentSkillFix(w) {
|
|
671
|
+
return {
|
|
672
|
+
label: w.agentSkill === "outdated" ? "Update the skilllib skill" : "Install the skilllib skill for your agents",
|
|
673
|
+
preview: "One global skill, skilllib, where your agents look in every repo (~/.claude/skills, and ~/.agents/skills for Codex and Zed). Then ask your agents which skills they can use here, where each comes from, and which of your skills a repo should add. `skilllib agent-skill remove` takes it out.",
|
|
674
|
+
run: (w2) => w2.ops.installAgentSkill()
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
function removeAgentSkillFix() {
|
|
678
|
+
return {
|
|
679
|
+
label: "Remove the skilllib skill",
|
|
680
|
+
preview: "Removes it from ~/.claude/skills and ~/.agents/skills, where skilllib put it. Your agents then stop using skilllib to answer which skills they can use. Settings \u203A skilllib skill puts it back.",
|
|
681
|
+
run: (w) => w.ops.removeAgentSkill()
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
function agentSkillIssue(w) {
|
|
685
|
+
if (w.agentSkill === "installed" || !w.agents.length) return null;
|
|
686
|
+
const missing = w.agentSkill === "missing";
|
|
687
|
+
return {
|
|
688
|
+
id: "agent-skill",
|
|
689
|
+
severity: missing ? "hint" : "warning",
|
|
690
|
+
title: missing ? "Your agents don't know about skilllib" : "The skilllib skill for your agents is out of date",
|
|
691
|
+
short: missing ? "Your agents don't know about skilllib" : "Out of date",
|
|
692
|
+
decision: missing,
|
|
693
|
+
fixes: [agentSkillFix(w)]
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
function runAll(w, fixes) {
|
|
697
|
+
const held = [];
|
|
698
|
+
const bad = [];
|
|
699
|
+
for (const fix of fixes) {
|
|
700
|
+
let r;
|
|
701
|
+
try {
|
|
702
|
+
r = fix.run(w);
|
|
703
|
+
} catch (e) {
|
|
704
|
+
r = failed(e.message);
|
|
705
|
+
}
|
|
706
|
+
if (isFailure(r)) bad.push(said(r));
|
|
707
|
+
else if (followUp(r)) held.push(said(r));
|
|
708
|
+
}
|
|
709
|
+
const ok = fixes.length - held.length - bad.length;
|
|
710
|
+
const message = [
|
|
711
|
+
`Applied ${ok}${ok === fixes.length ? "" : ` of ${fixes.length}`} fix${fixes.length === 1 ? "" : "es"}${ok ? " (anything removed is in Settings \u203A Backups)" : ""}`,
|
|
712
|
+
held.length ? `${held.join("; ")} (fix ${held.length === 1 ? "it" : "them"} on ${held.length === 1 ? "its" : "their"} own to go ahead)` : "",
|
|
713
|
+
bad.length ? `couldn't: ${bad.join("; ")}` : ""
|
|
714
|
+
].filter(Boolean).join(" \xB7 ");
|
|
715
|
+
return bad.length ? failed(message) : message;
|
|
716
|
+
}
|
|
518
717
|
function worst(issues) {
|
|
519
718
|
return [...issues].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity])[0];
|
|
520
719
|
}
|
|
@@ -543,7 +742,7 @@ function deleteLibraryFix(w, names) {
|
|
|
543
742
|
"Every copy goes to Settings \u203A Backups.",
|
|
544
743
|
others.length ? `Untracked or the team's own copies in ${others.join(", ")} stay.` : ""
|
|
545
744
|
].filter(Boolean).join(" "),
|
|
546
|
-
run: (w2) => names.map((n) => w2.ops.deleteLibrary(n))
|
|
745
|
+
run: (w2) => joined(names.map((n) => w2.ops.deleteLibrary(n)))
|
|
547
746
|
};
|
|
548
747
|
}
|
|
549
748
|
function replacePluginFix(w, id) {
|
|
@@ -589,6 +788,8 @@ function groupCandidates(w, u) {
|
|
|
589
788
|
if (m?.source === "plugin") return [{ key: `plugin:${m.where}`, label: `\u29C9 ${m.where.split("@")[0]} plugin` }];
|
|
590
789
|
if (m?.source === "claude.ai") return [{ key: "claude.ai", label: "claude.ai skills" }];
|
|
591
790
|
if (m?.source === "cursor") return [{ key: "cursor", label: "Cursor built-ins" }];
|
|
791
|
+
if (m?.source === "system") return [{ key: `system:${m.where}`, label: m.where }];
|
|
792
|
+
if (m?.source === "skilllib") return [];
|
|
592
793
|
if (m) {
|
|
593
794
|
const origin = originGroup(m.origin);
|
|
594
795
|
return [...origin ? [origin] : [], ...byTime("global", m.installed), ...byWord("global", m.name)];
|
|
@@ -632,10 +833,8 @@ function addRows(w, target, q) {
|
|
|
632
833
|
name: q ? `+ Create "${q}"${where}` : "+ Create a new skill (type its name)",
|
|
633
834
|
note: "",
|
|
634
835
|
description: "",
|
|
635
|
-
run: (w2) =>
|
|
636
|
-
|
|
637
|
-
return target ? w2.ops.add(target, q) : made;
|
|
638
|
-
}
|
|
836
|
+
run: (w2) => w2.ops.createSkill(q),
|
|
837
|
+
...target ? { afterEdit: (w2) => w2.ops.add(target, q) } : {}
|
|
639
838
|
},
|
|
640
839
|
{ key: "agent", agent: true, name: q ? `\u2726 Write "${q}" with an agent` : "\u2726 Write a new skill with an agent", note: "", description: "" }
|
|
641
840
|
];
|
|
@@ -644,9 +843,32 @@ function addRows(w, target, q) {
|
|
|
644
843
|
}
|
|
645
844
|
|
|
646
845
|
// src/tui/prompts.ts
|
|
647
|
-
|
|
846
|
+
var agentNames2 = (agents) => agents.map((a) => harness(a).name).join(", ") || "none of my agents";
|
|
847
|
+
var localSource = (s) => s.source === "lib" ? "Your skills (skilllib library), installed in the repo" : s.source === "repo" ? "a repo's own skill (committed by the team)" : "a copy only on this machine (not committed)";
|
|
848
|
+
var localInstalled = (s) => s.origin ?? (s.source === "lib" ? "added from my library with skilllib" : s.source === "repo" ? "committed in the repo" : "copied into the repo by hand (not committed)");
|
|
849
|
+
function reviewPrompt(w, names, scope, repo) {
|
|
648
850
|
const skills = names.map((name) => {
|
|
649
851
|
const copies = w.machine.filter((m2) => m2.name === name);
|
|
852
|
+
const here = repo ? w.projects.find((p) => p.name === repo)?.skills.find((s) => s.name === name) : void 0;
|
|
853
|
+
if (here) {
|
|
854
|
+
const lib3 = w.library.find((l) => l.name === name);
|
|
855
|
+
return {
|
|
856
|
+
name,
|
|
857
|
+
description: w.descriptions[name] ?? "",
|
|
858
|
+
source: localSource(here),
|
|
859
|
+
installedHow: localInstalled(here),
|
|
860
|
+
path: here.path,
|
|
861
|
+
links: [],
|
|
862
|
+
loadedBy: `${agentNames2(here.agents)} \u2014 in ${repo}`,
|
|
863
|
+
vendor: null,
|
|
864
|
+
usesTotal: w.days ? totalUses(w, name) : null,
|
|
865
|
+
usesByProject: Object.entries(w.usage).flatMap(([p, u]) => u[name] ? [[p, u[name]]] : []),
|
|
866
|
+
installedIn: w.projects.filter((p) => p.skills.some((s) => s.name === name)).map((p) => p.name),
|
|
867
|
+
otherCopies: copies.map((c) => `${c.source} ${c.where}`),
|
|
868
|
+
inYourSkills: lib3 ? `yes, v${lib3.latest}` : null,
|
|
869
|
+
keptGlobal: void 0
|
|
870
|
+
};
|
|
871
|
+
}
|
|
650
872
|
const m = copies.find((c) => c.source === "global") ?? copies[0];
|
|
651
873
|
const lib2 = w.library.find((l) => l.name === name);
|
|
652
874
|
const repos = w.projects.filter((p) => p.skills.some((s) => s.name === name));
|
|
@@ -654,11 +876,11 @@ function reviewPrompt(w, names, scope) {
|
|
|
654
876
|
return {
|
|
655
877
|
name,
|
|
656
878
|
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"),
|
|
879
|
+
source: m ? m.source === "global" ? "my global skills" : `vendor: ${m.source} (${m.where})` : lib2 ? "Your skills (skilllib library)" : local ? localSource(local) : "a repo's own skill",
|
|
880
|
+
installedHow: m?.origin ?? (m ? m.where : lib2 ? "created or imported into skilllib" : local ? localInstalled(local) : "committed in the repo"),
|
|
659
881
|
path: m?.path ?? (lib2 ? w.ops.libraryFile(name).replace(/\/SKILL\.md$/, "") : local?.path ?? ""),
|
|
660
882
|
links: m?.links ?? [],
|
|
661
|
-
loadedBy: m ? `${m.agents.map((
|
|
883
|
+
loadedBy: m ? `${agentNames2(m.agents)} \u2014 in every repo` : repos.map((p) => `${agentNames2(p.skills.find((s) => s.name === name).agents)} \u2014 in ${p.name}`).join("; ") || "none of my agents",
|
|
662
884
|
vendor: m && m.source !== "global" ? "turn it off at its source (/plugin in Claude Code, claude.ai settings, or Cursor)" : null,
|
|
663
885
|
usesTotal: w.days ? totalUses(w, name) : null,
|
|
664
886
|
usesByProject: Object.entries(w.usage).flatMap(([p, u]) => u[name] ? [[p, u[name]]] : []),
|
|
@@ -691,9 +913,7 @@ Keep it under 200 lines. ${p ? "skilllib will offer to import it into my library
|
|
|
691
913
|
|
|
692
914
|
// src/tui/App.tsx
|
|
693
915
|
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
694
|
-
|
|
695
|
-
return p.startsWith(userHome()) ? "~" + p.slice(userHome().length) : p;
|
|
696
|
-
}
|
|
916
|
+
var tildify3 = (p) => tildify(p);
|
|
697
917
|
var PLACES = [
|
|
698
918
|
["projects", "Projects"],
|
|
699
919
|
["library", "Your skills"],
|
|
@@ -725,6 +945,7 @@ function tag(u, w) {
|
|
|
725
945
|
if (s) return [s.source, color.blue];
|
|
726
946
|
if (u.source === "global") return [u.machine?.kept ? "global \u2713" : "global", color.yellow];
|
|
727
947
|
if (u.source === "plugin") return [`\u29C9 ${u.where.split("@")[0]}`, color.magenta];
|
|
948
|
+
if (u.source === "skilllib") return ["\u25C6 skilllib", color.accent];
|
|
728
949
|
return [u.source, color.muted];
|
|
729
950
|
}
|
|
730
951
|
function isNameChar(input, key) {
|
|
@@ -775,7 +996,7 @@ function TitleRow({ cells, width }) {
|
|
|
775
996
|
}
|
|
776
997
|
var fixOption = (fix) => ({ label: fix.label, fix });
|
|
777
998
|
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 };
|
|
999
|
+
var COPY_RANK = { lib: 0, repo: 0, untracked: 0, global: 1, skilllib: 1, plugin: 2, "claude.ai": 3, cursor: 3, system: 3 };
|
|
779
1000
|
function byName(copies) {
|
|
780
1001
|
const groups = /* @__PURE__ */ new Map();
|
|
781
1002
|
for (const c of copies) groups.set(c.name, [...groups.get(c.name) ?? [], c]);
|
|
@@ -843,7 +1064,7 @@ function skillItem(w, copies, issues, nameW, actions, repo) {
|
|
|
843
1064
|
const b = builder(u.name);
|
|
844
1065
|
for (const l of wrap(w.descriptions[u.name] || "(no description)", width - 4, 3)) b.line([l, color.muted]);
|
|
845
1066
|
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]);
|
|
1067
|
+
if (repo && !u.local) b.line(["Loads in every repo", color.text], [u.source === "global" || u.source === "skilllib" ? " issues and cleanup live in Global" : " managed at its source", color.faint]);
|
|
847
1068
|
else b.issues(issues);
|
|
848
1069
|
b.actions(actions, width);
|
|
849
1070
|
b.line();
|
|
@@ -852,7 +1073,7 @@ function skillItem(w, copies, issues, nameW, actions, repo) {
|
|
|
852
1073
|
b.line(["Loaded from", color.accent]);
|
|
853
1074
|
for (const c of copies) {
|
|
854
1075
|
const s = c.local;
|
|
855
|
-
const path = s ? `${s.dir}/${c.name}` : c.source === "global" ? `${c.where}/${c.name}` : c.where;
|
|
1076
|
+
const path = s ? `${s.dir}/${c.name}` : c.source === "global" || c.source === "skilllib" ? `${c.where}/${c.name}` : c.where;
|
|
856
1077
|
const extra = s ? s.missing ? "missing" : s.git ? GIT_LABEL[s.git] : "" : "";
|
|
857
1078
|
const [ct, cc] = c === u ? ["", void 0] : tag(c, w);
|
|
858
1079
|
b.line([" ", void 0], [path + " ", color.text], [ct ? ct + " " : "", cc], [extra, color.faint]);
|
|
@@ -866,7 +1087,10 @@ function projectSkillItems(w, p, nameW, ui) {
|
|
|
866
1087
|
(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
1088
|
);
|
|
868
1089
|
}
|
|
869
|
-
var reviewOption = (w, name, ui
|
|
1090
|
+
var reviewOption = (w, name, ui, repo) => ({
|
|
1091
|
+
label: "Review prompt",
|
|
1092
|
+
action: () => ui.copy(reviewPrompt(w, [name], `Review the skill ${name}${repo ? ` in ${repo}` : ""}.`, repo), `a review prompt for ${name}`)
|
|
1093
|
+
});
|
|
870
1094
|
function versionsOption(w, repo, name, installed, ui) {
|
|
871
1095
|
return {
|
|
872
1096
|
label: "Other versions\u2026",
|
|
@@ -879,27 +1103,35 @@ function versionsOption(w, repo, name, installed, ui) {
|
|
|
879
1103
|
function skillActions(w, p, u, ui) {
|
|
880
1104
|
const s = u.local;
|
|
881
1105
|
const inLibrary = w.library.find((l) => l.name === u.name);
|
|
882
|
-
const remove = {
|
|
1106
|
+
const remove = {
|
|
1107
|
+
// Its folder gone: all that's left to remove is the skilllib.json entry (the label Health offers too).
|
|
1108
|
+
label: s?.missing ? "Remove it from skilllib.json" : "Remove from repo",
|
|
1109
|
+
preview: s?.missing ? `${p}'s skilllib.json stops pinning ${u.name}. There's no folder to delete.` : s?.source === "lib" ? `Delete ${s.dir}/${u.name} in ${p}.${inLibrary ? " Your library keeps it." : ""}` : `Move ${s?.dir}/${u.name} in ${p} to Settings \u203A Backups.`,
|
|
1110
|
+
run: (w2) => w2.ops.remove(p, u.name)
|
|
1111
|
+
};
|
|
883
1112
|
if (s?.source === "lib")
|
|
884
1113
|
return [
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1114
|
+
// A tracked skill your library doesn't have: nothing to edit there, no versions to pick.
|
|
1115
|
+
...inLibrary ? [
|
|
1116
|
+
...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) })] : [],
|
|
1117
|
+
{ label: "Edit in library", action: () => ui.edit(w.ops.libraryFile(u.name)) },
|
|
1118
|
+
versionsOption(w, p, u.name, s.version, ui)
|
|
1119
|
+
] : [],
|
|
888
1120
|
fixOption(remove),
|
|
889
|
-
reviewOption(w, u.name, ui)
|
|
1121
|
+
reviewOption(w, u.name, ui, p)
|
|
890
1122
|
];
|
|
891
1123
|
if (s?.source === "repo")
|
|
892
1124
|
return [
|
|
893
1125
|
...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
1126
|
{ label: "Edit SKILL.md", action: () => ui.edit(`${s.path}/SKILL.md`) },
|
|
895
|
-
reviewOption(w, u.name, ui)
|
|
1127
|
+
reviewOption(w, u.name, ui, p)
|
|
896
1128
|
];
|
|
897
1129
|
if (s)
|
|
898
1130
|
return [
|
|
899
1131
|
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
1132
|
{ label: "Edit SKILL.md", action: () => ui.edit(`${s.path}/SKILL.md`) },
|
|
901
1133
|
fixOption(remove),
|
|
902
|
-
reviewOption(w, u.name, ui)
|
|
1134
|
+
reviewOption(w, u.name, ui, p)
|
|
903
1135
|
];
|
|
904
1136
|
return [{ label: "Open in Global", action: () => ui.openInGlobal(u.name) }, reviewOption(w, u.name, ui)];
|
|
905
1137
|
}
|
|
@@ -908,7 +1140,7 @@ function overview(w, p) {
|
|
|
908
1140
|
const info = w.ops.repoInfo(p);
|
|
909
1141
|
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
1142
|
return [
|
|
911
|
-
["Folder", [[
|
|
1143
|
+
["Folder", [[tildify3(project2.path), color.text]]],
|
|
912
1144
|
["Remote", [[info.remote ?? "none", info.remote ? color.blue : color.faint]]],
|
|
913
1145
|
["Branch", [[info.branch ?? "\u2013", color.text], [info.dirty ? ` ${info.dirty} uncommitted` : "", color.yellow]]],
|
|
914
1146
|
["Manifest", [manifest]]
|
|
@@ -955,11 +1187,11 @@ function globalItems(w, nameW, ui) {
|
|
|
955
1187
|
}
|
|
956
1188
|
function globalActions(w, u, ui) {
|
|
957
1189
|
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";
|
|
1190
|
+
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" : m.source === "system" ? `An admin manages ${m.where}` : "Cursor manages it";
|
|
959
1191
|
return [
|
|
960
1192
|
...machineActions(m).map(fixOption),
|
|
961
1193
|
...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`) }],
|
|
1194
|
+
...m.source === "global" || m.source === "skilllib" ? [] : [{ label: atSource, action: () => ui.notify(`${atSource}; skilllib picks up the change next time`) }],
|
|
963
1195
|
...m.source === "global" && !m.broken ? [{ label: "Edit SKILL.md", action: () => ui.edit(`${m.path}/SKILL.md`) }] : [],
|
|
964
1196
|
reviewOption(w, m.name, ui)
|
|
965
1197
|
];
|
|
@@ -967,8 +1199,9 @@ function globalActions(w, u, ui) {
|
|
|
967
1199
|
function libraryItems(w, nameW, ui) {
|
|
968
1200
|
return w.library.map((l) => {
|
|
969
1201
|
const repos = w.projects.filter((p) => p.skills.some((s) => s.name === l.name));
|
|
1202
|
+
const removable = repos.filter((p) => p.skills.some((s) => s.name === l.name && s.source !== "repo"));
|
|
970
1203
|
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) =>
|
|
1204
|
+
const addFix = { label: "Add to repos\u2026", preview: `Install ${l.name} v${l.latest} into the repos you pick.`, run: () => "", candidates: (w2) => w2.projects.filter((p) => !p.skills.some((s) => s.name === l.name)).map((p) => p.name), pickRepos: (w2, rs) => w2.ops.addTo(rs, [l.name]) };
|
|
972
1205
|
const delFix = deleteLibraryFix(w, [l.name]);
|
|
973
1206
|
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
1207
|
const behind = repos.filter((p) => p.skills.some((s) => s.name === l.name && s.source === "lib" && s.version < l.latest)).length;
|
|
@@ -1000,13 +1233,13 @@ function libraryItems(w, nameW, ui) {
|
|
|
1000
1233
|
b.issues(issues);
|
|
1001
1234
|
b.actions([
|
|
1002
1235
|
fixOption(addFix),
|
|
1003
|
-
...
|
|
1236
|
+
...removable.length ? [
|
|
1004
1237
|
fixOption({
|
|
1005
1238
|
label: "Remove from repos\u2026",
|
|
1006
1239
|
preview: `Pick repos to remove ${l.name} from. Your library keeps it.`,
|
|
1007
1240
|
run: () => "",
|
|
1008
|
-
candidates: () =>
|
|
1009
|
-
pickRepos: (w2, rs) => rs.map((r) =>
|
|
1241
|
+
candidates: () => removable.map((p) => p.name),
|
|
1242
|
+
pickRepos: (w2, rs) => joined(rs.map((r) => w2.ops.remove(r, l.name)))
|
|
1010
1243
|
})
|
|
1011
1244
|
] : [],
|
|
1012
1245
|
fixOption(delFix),
|
|
@@ -1022,15 +1255,39 @@ function libraryItems(w, nameW, ui) {
|
|
|
1022
1255
|
}
|
|
1023
1256
|
function healthItems(w, nameW, ui) {
|
|
1024
1257
|
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))];
|
|
1258
|
+
return [...w.projects.flatMap((p) => flagged(p.name, projectSkillItems(w, p.name, nameW, ui))), ...flagged("Global", globalItems(w, nameW, ui)), ...agentSkillItems(w, nameW)];
|
|
1259
|
+
}
|
|
1260
|
+
function agentSkillItems(w, nameW) {
|
|
1261
|
+
const issue = agentSkillIssue(w);
|
|
1262
|
+
if (!issue || w.machine.some((m) => m.source === "skilllib")) return [];
|
|
1263
|
+
const sev = SEV[issue.severity];
|
|
1264
|
+
return [
|
|
1265
|
+
{
|
|
1266
|
+
key: "agent-skill",
|
|
1267
|
+
name: "skilllib",
|
|
1268
|
+
issues: [issue],
|
|
1269
|
+
uses: 0,
|
|
1270
|
+
cells: [
|
|
1271
|
+
{ text: sev.icon, width: 2, color: sev.color },
|
|
1272
|
+
{ text: clip("skilllib", nameW), width: nameW },
|
|
1273
|
+
{ text: clip("Your agents", 16), width: 16, color: color.muted },
|
|
1274
|
+
{ text: " " + issue.short, grow: true, color: sev.color }
|
|
1275
|
+
],
|
|
1276
|
+
detail: (width) => {
|
|
1277
|
+
const b = builder("skilllib");
|
|
1278
|
+
for (const l of wrap("The skilllib skill lets you ask your agents which skills they can use here, where each comes from, and which of your skills a repo should add.", width - 4, 3)) b.line([l, color.muted]);
|
|
1279
|
+
b.line();
|
|
1280
|
+
b.issues([issue]);
|
|
1281
|
+
return b.d;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
];
|
|
1026
1285
|
}
|
|
1027
1286
|
function addCursor(rows, cursor) {
|
|
1028
1287
|
const i = Math.min(cursor, rows.length - 1);
|
|
1029
1288
|
return rows[i] && !rows[i].header ? i : Math.max(0, rows.findIndex((r) => !r.header));
|
|
1030
1289
|
}
|
|
1031
|
-
|
|
1032
|
-
return typeof r === "string" ? r : r.message;
|
|
1033
|
-
}
|
|
1290
|
+
var said2 = said;
|
|
1034
1291
|
function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
1035
1292
|
const { exit, suspendTerminal } = useApp();
|
|
1036
1293
|
const { columns: termCols, rows: termRows } = useWindowSize();
|
|
@@ -1068,12 +1325,14 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1068
1325
|
notify: setToast,
|
|
1069
1326
|
openInGlobal,
|
|
1070
1327
|
edit,
|
|
1071
|
-
copy: (text, what) => setToast(
|
|
1328
|
+
copy: (text, what) => setToast(said2(world.ops.copy(text, what))),
|
|
1072
1329
|
menu: (title, options) => setModal({ kind: "menu", title, options, cursor: 0 })
|
|
1073
1330
|
};
|
|
1074
1331
|
const flagged = [
|
|
1075
1332
|
...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 })))
|
|
1333
|
+
...world.machine.flatMap((m) => machineIssues(world, m).map((issue) => ({ key: `Global:${m.name}`, issue }))),
|
|
1334
|
+
// The skilllib skill's issue is on its Global row when it has one (see agentSkillItems).
|
|
1335
|
+
...[agentSkillIssue(world)].flatMap((issue) => issue ? [{ key: "Global:skilllib", issue }] : [])
|
|
1077
1336
|
];
|
|
1078
1337
|
const healthCount = new Set(flagged.map((f) => f.key)).size;
|
|
1079
1338
|
const healthWorst = worst(flagged.map((f) => f.issue));
|
|
@@ -1102,16 +1361,18 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1102
1361
|
if (items[cursor]?.header) cursor = firstSelectable;
|
|
1103
1362
|
const current = items[cursor];
|
|
1104
1363
|
const detail = current?.detail?.(columns);
|
|
1105
|
-
function apply(run) {
|
|
1364
|
+
function apply(run, w = world) {
|
|
1106
1365
|
let result;
|
|
1107
1366
|
try {
|
|
1108
|
-
result = run(
|
|
1367
|
+
result = run(w);
|
|
1109
1368
|
} catch (e) {
|
|
1110
|
-
result = `Couldn't do it: ${e.message}
|
|
1369
|
+
result = failed(`Couldn't do it: ${e.message}`);
|
|
1111
1370
|
}
|
|
1112
1371
|
setBase(reload());
|
|
1113
|
-
|
|
1114
|
-
|
|
1372
|
+
const then = followUp(result);
|
|
1373
|
+
const toast2 = `${isFailure(result) ? "\u2717" : then ? "!" : "\u2713"} ${said2(result)}`;
|
|
1374
|
+
setToast(toast2);
|
|
1375
|
+
if (then) setModal({ kind: "confirm", fix: then, toast: toast2 });
|
|
1115
1376
|
}
|
|
1116
1377
|
function choose(o) {
|
|
1117
1378
|
if (o.action) return o.action();
|
|
@@ -1131,6 +1392,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1131
1392
|
function fixSelected() {
|
|
1132
1393
|
const issue = current && worst(current.issues);
|
|
1133
1394
|
if (!issue) return setToast("Nothing to fix \xB7 enter shows what you can do");
|
|
1395
|
+
if (!issue.fixes.length) return setToast(`Nothing skilllib can do here: ${issue.short}`);
|
|
1134
1396
|
if (issue.decision) {
|
|
1135
1397
|
setFocus("detail");
|
|
1136
1398
|
setDetailCursor(Math.max(0, detail?.options.findIndex((o) => o.issue === issue) ?? 0));
|
|
@@ -1207,7 +1469,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1207
1469
|
preview: `Pick repos. These ${n(mine.length)} go into your library and those repos, and stop loading globally (originals backed up).`,
|
|
1208
1470
|
run: () => "",
|
|
1209
1471
|
preticked: usedIn,
|
|
1210
|
-
pickRepos: (w, repos) =>
|
|
1472
|
+
pickRepos: (w, repos) => w.ops.moveGlobal(mine, repos, label)
|
|
1211
1473
|
}),
|
|
1212
1474
|
...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
1475
|
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)`) })
|
|
@@ -1218,7 +1480,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1218
1480
|
preview: `Install these ${n(libs.length)} into the repos you pick.`,
|
|
1219
1481
|
run: () => "",
|
|
1220
1482
|
preticked: () => [],
|
|
1221
|
-
pickRepos: (w, repos) => (repos
|
|
1483
|
+
pickRepos: (w, repos) => w.ops.addTo(repos, libs.map((l) => l.name))
|
|
1222
1484
|
})
|
|
1223
1485
|
] : [],
|
|
1224
1486
|
...libs.length ? [fixOption(deleteLibraryFix(world, libs.map((l) => l.name)))] : [],
|
|
@@ -1258,9 +1520,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1258
1520
|
`\u22EF ${repo}\u2026`,
|
|
1259
1521
|
["Open its folder, hide it from the list, or copy a review prompt for its skills."],
|
|
1260
1522
|
() => ui.menu(repo, [
|
|
1261
|
-
{ label: "Open its folder", action: () => setToast(
|
|
1523
|
+
{ label: "Open its folder", action: () => setToast(said2(world.ops.openFolder(project(world, repo).path))) },
|
|
1262
1524
|
{ 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}
|
|
1525
|
+
...skills.length ? [{ label: `Review prompt for its ${skills.length} skills`, action: () => ui.copy(reviewPrompt(world, skills, `Review the skills in ${repo}.`, repo), `a review prompt for ${repo}`) }] : []
|
|
1264
1526
|
])
|
|
1265
1527
|
)
|
|
1266
1528
|
];
|
|
@@ -1292,19 +1554,21 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1292
1554
|
setQuery("");
|
|
1293
1555
|
setChip("all");
|
|
1294
1556
|
}
|
|
1295
|
-
function edit(file) {
|
|
1296
|
-
if (!existsSync2(file)) return setToast(`${
|
|
1557
|
+
function edit(file, then) {
|
|
1558
|
+
if (!existsSync2(file)) return then ? apply(then) : (setBase(reload()), setToast(`${tildify3(file)} doesn't exist`));
|
|
1297
1559
|
const editor = process.env.VISUAL || process.env.EDITOR || (process.platform === "win32" ? "notepad" : "vi");
|
|
1298
1560
|
void suspendTerminal(() => {
|
|
1299
1561
|
if (process.platform === "win32") spawnSync2(`${editor} "${file}"`, { shell: true, stdio: "inherit" });
|
|
1300
1562
|
else spawnSync2("/bin/sh", ["-c", `${editor} "$1"`, "sh", file], { stdio: "inherit" });
|
|
1301
1563
|
}).then(() => {
|
|
1302
|
-
|
|
1303
|
-
|
|
1564
|
+
const fresh = reload();
|
|
1565
|
+
if (then) return apply(then, fresh);
|
|
1566
|
+
setBase(fresh);
|
|
1567
|
+
setToast(`Saved ${tildify3(file)}`);
|
|
1304
1568
|
});
|
|
1305
1569
|
}
|
|
1306
1570
|
function askForFolder(title) {
|
|
1307
|
-
setModal({ kind: "input", title, value: "~/Projects", submit: (v) => apply((w) => w.ops.addRoot(v)) });
|
|
1571
|
+
setModal({ kind: "input", title, value: "", placeholder: "~/Projects", submit: (v) => apply((w) => w.ops.addRoot(v)) });
|
|
1308
1572
|
}
|
|
1309
1573
|
useEffect(() => {
|
|
1310
1574
|
const folder = () => !initial.roots.length && askForFolder("Where are your projects? skilllib finds the git repos in there");
|
|
@@ -1333,20 +1597,27 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1333
1597
|
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
1598
|
})
|
|
1335
1599
|
),
|
|
1600
|
+
// The skill that lets your agents use skilllib: installing adds a global skill, so it asks first.
|
|
1601
|
+
row(
|
|
1602
|
+
"agent-skill",
|
|
1603
|
+
`${world.agentSkill === "installed" ? "[x]" : "[ ]"} \u25C6 skilllib skill`,
|
|
1604
|
+
world.agentSkill === "installed" ? "your agents can use skilllib" : world.agentSkill === "outdated" ? "out of date \xB7 enter updates it" : "lets your agents use skilllib \xB7 enter installs it",
|
|
1605
|
+
() => world.agentSkill === "installed" ? setToast("Installed \xB7 `skilllib agent-skill remove` takes it out") : choose(fixOption(agentSkillFix(world)))
|
|
1606
|
+
),
|
|
1336
1607
|
header("#roots", "Project folders"),
|
|
1337
1608
|
...world.roots.map(
|
|
1338
1609
|
(r) => row(
|
|
1339
1610
|
`root:${r}`,
|
|
1340
|
-
|
|
1611
|
+
tildify3(r),
|
|
1341
1612
|
"scanned for git repos",
|
|
1342
|
-
() => ui.menu(
|
|
1613
|
+
() => ui.menu(tildify3(r), [
|
|
1343
1614
|
{ label: "Look for repos again", action: () => apply((w) => w.ops.rescan()) },
|
|
1344
|
-
fixOption({ label: "Stop scanning this folder", preview: `Repos already found in ${
|
|
1615
|
+
fixOption({ label: "Stop scanning this folder", preview: `Repos already found in ${tildify3(r)} stay listed until you hide them.`, run: (w) => w.ops.removeRoot(r) })
|
|
1345
1616
|
])
|
|
1346
1617
|
)
|
|
1347
1618
|
),
|
|
1348
1619
|
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}`,
|
|
1620
|
+
...world.hidden.length ? [header("#hidden", "Hidden repos"), ...world.hidden.map((h) => row(`hidden:${h}`, tildify3(h), "enter shows it again", () => apply((w) => w.ops.unhide(h))))] : [],
|
|
1350
1621
|
header("#backups", "Backups"),
|
|
1351
1622
|
...world.backups.length ? world.backups.map(
|
|
1352
1623
|
(b, i) => row(`backup:${i}`, `\u21BA ${b.name}`, `${b.from} \xB7 ${b.at}`, () => apply((w) => w.ops.restoreBackup(i)))
|
|
@@ -1450,7 +1721,11 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1450
1721
|
}
|
|
1451
1722
|
if (m.kind === "input") {
|
|
1452
1723
|
if (key.escape) return setModal(null);
|
|
1453
|
-
if (key.return)
|
|
1724
|
+
if (key.return) {
|
|
1725
|
+
const value = m.value.trim() || m.placeholder;
|
|
1726
|
+
setModal(null);
|
|
1727
|
+
return value ? m.submit(value) : void 0;
|
|
1728
|
+
}
|
|
1454
1729
|
if (key.backspace || key.delete) return setModal({ ...m, value: m.value.slice(0, -1) });
|
|
1455
1730
|
if (key.ctrl && input === "u") return setModal({ ...m, value: "" });
|
|
1456
1731
|
if (input && !key.ctrl && !key.meta) return setModal({ ...m, value: m.value + input });
|
|
@@ -1463,7 +1738,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1463
1738
|
if (key.return && m.options[m.cursor]) return setModal(null), choose(m.options[m.cursor]);
|
|
1464
1739
|
return;
|
|
1465
1740
|
}
|
|
1466
|
-
if (key.escape) return setModal(null);
|
|
1741
|
+
if (key.escape) return setModal(null), m.kind === "confirm" && m.toast && setToast(m.toast);
|
|
1467
1742
|
if (m.kind === "confirm") {
|
|
1468
1743
|
if (key.return || input === "y") {
|
|
1469
1744
|
setModal(null);
|
|
@@ -1500,17 +1775,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1500
1775
|
if (key.return) {
|
|
1501
1776
|
setModal(null);
|
|
1502
1777
|
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
|
-
});
|
|
1778
|
+
apply((w) => runAll(w, chosen.map((it) => it.fix)));
|
|
1514
1779
|
}
|
|
1515
1780
|
return;
|
|
1516
1781
|
}
|
|
@@ -1533,9 +1798,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1533
1798
|
const row = rows[cursor2];
|
|
1534
1799
|
if (row.create && !/^[a-z0-9]/.test(m.query)) return setToast("Type the new skill's name first");
|
|
1535
1800
|
setModal(null);
|
|
1536
|
-
apply(row.run);
|
|
1537
|
-
|
|
1538
|
-
return;
|
|
1801
|
+
if (!row.create) return apply(row.run);
|
|
1802
|
+
row.run(world);
|
|
1803
|
+
return edit(world.ops.libraryFile(m.query), row.afterEdit);
|
|
1539
1804
|
}
|
|
1540
1805
|
if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
|
|
1541
1806
|
}
|
|
@@ -1648,8 +1913,11 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1648
1913
|
);
|
|
1649
1914
|
else if (modal?.kind === "input")
|
|
1650
1915
|
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
|
-
|
|
1916
|
+
modal.value || !modal.placeholder ? /* @__PURE__ */ jsx2(Text2, { color: color.text, children: modal.value + "\u258F" }) : /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1917
|
+
"\u258F",
|
|
1918
|
+
/* @__PURE__ */ jsx2(Text2, { color: color.faint, children: modal.placeholder })
|
|
1919
|
+
] }),
|
|
1920
|
+
/* @__PURE__ */ jsx2(Text2, { color: color.faint, children: modal.value || !modal.placeholder ? "enter to confirm \xB7 ctrl+u clears \xB7 esc cancels" : `type a path, or enter for ${modal.placeholder} \xB7 esc cancels` })
|
|
1653
1921
|
] });
|
|
1654
1922
|
else if (modal?.kind === "agents")
|
|
1655
1923
|
body = /* @__PURE__ */ jsx2(
|
|
@@ -1776,7 +2044,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1776
2044
|
] })
|
|
1777
2045
|
] }),
|
|
1778
2046
|
/* @__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 }),
|
|
2047
|
+
/* @__PURE__ */ jsx2(Text2, { color: toast.startsWith("\u2713") || toast.startsWith("\u21BA") ? color.green : toast.startsWith("\u2717") ? color.red : toast.startsWith("!") ? color.yellow : color.muted, wrap: "truncate-end", children: " " + toast }),
|
|
1780
2048
|
toast ? null : /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: hints + " " })
|
|
1781
2049
|
] })
|
|
1782
2050
|
] });
|
|
@@ -1835,9 +2103,8 @@ function Help({ width, height }) {
|
|
|
1835
2103
|
import { execFileSync, spawn, spawnSync as spawnSync3 } from "child_process";
|
|
1836
2104
|
import { existsSync as existsSync3, lstatSync, realpathSync } from "fs";
|
|
1837
2105
|
import { basename, dirname, join as join2 } from "path";
|
|
1838
|
-
function
|
|
1839
|
-
|
|
1840
|
-
return (p.startsWith(home) ? "~" + p.slice(home.length) : p).replace(/\\/g, "/");
|
|
2106
|
+
function tildify4(p) {
|
|
2107
|
+
return tildify(p).replace(/\\/g, "/");
|
|
1841
2108
|
}
|
|
1842
2109
|
function uniqueNames(roots) {
|
|
1843
2110
|
const tail = (r, n) => r.split(/[\\/]/).filter(Boolean).slice(-n).join("/");
|
|
@@ -1855,25 +2122,37 @@ function localSkill(root, s, git2) {
|
|
|
1855
2122
|
"update available": {},
|
|
1856
2123
|
"edited locally": { edited: true },
|
|
1857
2124
|
"edited locally, update available": { edited: true },
|
|
1858
|
-
|
|
2125
|
+
// sync restores it from your library, unless the library doesn't have it (a teammate's skill, another library).
|
|
2126
|
+
"folder missing": { missing: true, ...s.latest === null && { library: "missing" } },
|
|
1859
2127
|
"not in library": { library: "missing" }
|
|
1860
2128
|
};
|
|
1861
2129
|
const other = {
|
|
1862
2130
|
"untracked copy of library skill": { source: "untracked", library: "same" },
|
|
1863
2131
|
"untracked, differs from library": { source: "untracked", library: "differs" },
|
|
1864
2132
|
"local only": { source: "untracked" },
|
|
2133
|
+
// Installed with `npx skills add` (the repo's skills-lock.json): not skilllib's, so not a library skill.
|
|
2134
|
+
"from npx skills": { source: "untracked" },
|
|
1865
2135
|
"repo skill": { source: "repo" },
|
|
1866
2136
|
"repo skill, in library": { source: "repo", library: "same" },
|
|
1867
2137
|
"repo skill, differs from library": { source: "repo", library: "differs" }
|
|
1868
2138
|
};
|
|
2139
|
+
const state = git2 ? git2.of(relativeTo(root, s.path)) : null;
|
|
2140
|
+
const own = other[s.state]?.source === "untracked" && (state === "committed" || state === "changed") ? { source: "repo" } : {};
|
|
2141
|
+
const blind = s.state === "folder missing" ? [] : s.visibility.filter((v) => v.paths === 0).map((v) => v.id);
|
|
2142
|
+
const uncommitted = blind.length ? linkEverywhere(root, s.name, s.location, { dryRun: true }).uncommitted : [];
|
|
2143
|
+
const unshared = blind.filter((id) => uncommitted.includes(harness(id).projectDirs[0]));
|
|
1869
2144
|
return {
|
|
1870
2145
|
name: s.name,
|
|
1871
2146
|
source: "lib",
|
|
1872
2147
|
dir: s.location,
|
|
1873
2148
|
path: s.path,
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
2149
|
+
// Agents that actually load it here: visibility lists every agent you use, with `paths: 0` for the blind ones.
|
|
2150
|
+
agents: s.visibility.filter((v) => v.paths > 0).map((v) => v.id),
|
|
2151
|
+
...s.managed ? { library: "same", version: s.version ?? void 0, ...managed[s.state] } : { ...other[s.state], ...own },
|
|
2152
|
+
...unshared.length && { unshared },
|
|
2153
|
+
...s.source ? { origin: `npx skills: ${s.source}` } : {},
|
|
2154
|
+
added: addedAt(s.path),
|
|
2155
|
+
git: state
|
|
1877
2156
|
};
|
|
1878
2157
|
}
|
|
1879
2158
|
var partsOf = /* @__PURE__ */ new Map();
|
|
@@ -1903,8 +2182,8 @@ function machineSkill(s, kept) {
|
|
|
1903
2182
|
const global = s.kind === "global" || s.kind === "skills.sh";
|
|
1904
2183
|
return {
|
|
1905
2184
|
name: s.name,
|
|
1906
|
-
source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : "cursor",
|
|
1907
|
-
where: global ?
|
|
2185
|
+
source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : s.kind === "system" ? "system" : s.kind === "skilllib" ? "skilllib" : "cursor",
|
|
2186
|
+
where: global || s.kind === "skilllib" ? tildify4(dirname(s.path)) : s.kind === "claude.ai" ? "claude.ai account" : s.kind === "built-in" ? `${s.origin} built-in` : s.origin,
|
|
1908
2187
|
...s.kind === "skills.sh" ? { origin: `npx skills: ${s.origin}` } : {},
|
|
1909
2188
|
path: s.path,
|
|
1910
2189
|
links: s.links,
|
|
@@ -1915,11 +2194,17 @@ function machineSkill(s, kept) {
|
|
|
1915
2194
|
...s.kind === "plugin" ? { pluginParts: pluginParts(dirname(dirname(s.path))) } : {}
|
|
1916
2195
|
};
|
|
1917
2196
|
}
|
|
2197
|
+
function dupesOf(r, root, git2 = null) {
|
|
2198
|
+
const out = /* @__PURE__ */ new Map();
|
|
2199
|
+
for (const p of r.plans) out.set(p.name, { steps: p.steps.map((s) => describeStep(s, root)), git: gitVisibleSteps(p, git2).map((s) => describeStep(s, root)) });
|
|
2200
|
+
for (const c of r.conflicts)
|
|
2201
|
+
out.set(c.name, { steps: [], git: [], differ: c.copies.map((x) => ({ dir: x.dir, label: root ? copyLabel(c, x.dir) : tildify4(x.dir), runs: x.runs, canWin: !c.managed || x.dir === c.managed })) });
|
|
2202
|
+
return out;
|
|
2203
|
+
}
|
|
1918
2204
|
function loadWorld() {
|
|
1919
2205
|
forgetLatest();
|
|
1920
2206
|
const agents = enabledHarnesses();
|
|
1921
|
-
const
|
|
1922
|
-
const here = isProjectCandidate(cwdRoot) && (existsSync3(join2(cwdRoot, ".git")) || existsSync3(join2(cwdRoot, "skilllib.json"))) ? cwdRoot : null;
|
|
2207
|
+
const here = projectHere();
|
|
1923
2208
|
const real = (p) => {
|
|
1924
2209
|
try {
|
|
1925
2210
|
return realpathSync(p);
|
|
@@ -1938,20 +2223,39 @@ function loadWorld() {
|
|
|
1938
2223
|
return { name: s.name, latest: latestVersion(s.name)?.version ?? 1, origin: origins[s.name] };
|
|
1939
2224
|
});
|
|
1940
2225
|
const kept = keptGlobal();
|
|
1941
|
-
const
|
|
2226
|
+
const cursorPlugin = new Map(agents.includes("cursor") ? cursorPluginSkills().map((s) => [s.name, s.plugin]) : []);
|
|
2227
|
+
const globalDupes = dupesOf(planGlobalTidy({ enabled: agents }), null);
|
|
2228
|
+
const sourced = machineSkills(agents);
|
|
2229
|
+
const machine = sourced.map((s) => {
|
|
1942
2230
|
descriptions[s.name] ??= s.description;
|
|
1943
|
-
|
|
2231
|
+
const m = machineSkill(s, kept);
|
|
2232
|
+
if (m.source !== "global" || m.broken) return m;
|
|
2233
|
+
const dupes = globalDupes.get(m.name);
|
|
2234
|
+
const plugin = cursorPlugin.get(m.name);
|
|
2235
|
+
return { ...m, ...dupes ? { dupes } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
|
|
1944
2236
|
});
|
|
2237
|
+
const plugins = (root) => {
|
|
2238
|
+
const loaded = skillsLoadedIn(root, sourced, agents);
|
|
2239
|
+
if (loaded === sourced) return {};
|
|
2240
|
+
for (const s of loaded) descriptions[s.name] ??= s.description;
|
|
2241
|
+
return { machine: loaded.map((s) => machine[sourced.indexOf(s)] ?? machineSkill(s, kept)) };
|
|
2242
|
+
};
|
|
1945
2243
|
const projects = roots.map((root) => {
|
|
1946
2244
|
const status = projectStatus(root);
|
|
1947
2245
|
const hasManifest = existsSync3(join2(root, "skilllib.json"));
|
|
1948
2246
|
const git2 = status.length || hasManifest ? gitInfo(root) : null;
|
|
1949
2247
|
for (const s of status) if (!descriptions[s.name] && !s.state.includes("missing")) descriptions[s.name] = readSkillInfo(s.path).description;
|
|
2248
|
+
const dupes = status.length ? dupesOf(planProjectTidy(root, { enabled: agents }), root, git2) : /* @__PURE__ */ new Map();
|
|
1950
2249
|
return {
|
|
1951
2250
|
name: names.get(root),
|
|
1952
2251
|
path: root,
|
|
1953
2252
|
manifest: hasManifest ? git2 ? git2.of("skilllib.json") : "no git" : "none",
|
|
1954
|
-
skills: status.map((s) =>
|
|
2253
|
+
skills: status.map((s) => {
|
|
2254
|
+
const d = dupes.get(s.name);
|
|
2255
|
+
const plugin = cursorPlugin.get(s.name);
|
|
2256
|
+
return { ...localSkill(root, s, git2), ...d ? { dupes: d } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
|
|
2257
|
+
}),
|
|
2258
|
+
...plugins(root)
|
|
1955
2259
|
};
|
|
1956
2260
|
});
|
|
1957
2261
|
const backups = [...listBackups(), ...pluginBackups()].sort((a, b) => b.movedAt.localeCompare(a.movedAt));
|
|
@@ -1963,10 +2267,11 @@ function loadWorld() {
|
|
|
1963
2267
|
library,
|
|
1964
2268
|
usage: {},
|
|
1965
2269
|
descriptions,
|
|
1966
|
-
backups: backups.map((b) => ({ name: b.name, from:
|
|
2270
|
+
backups: backups.map((b) => ({ name: b.name, from: tildify4(b.from), at: b.movedAt })),
|
|
1967
2271
|
roots: readConfig().roots,
|
|
1968
2272
|
hidden: readConfig().hidden,
|
|
1969
2273
|
agentsChosen: harnessesChosen(),
|
|
2274
|
+
agentSkill: agentSkillState(agents),
|
|
1970
2275
|
ops: realOps(new Map(projects.map((p) => [p.name, p.path])), backups)
|
|
1971
2276
|
};
|
|
1972
2277
|
}
|
|
@@ -1998,44 +2303,99 @@ function remoteLabel(url) {
|
|
|
1998
2303
|
if (!url) return void 0;
|
|
1999
2304
|
return url.replace(/^[a-z+]+:\/\//, "").replace(/^[^@/]+@/, "").replace(/:(?!\d)/, "/").replace(/\.git$/, "");
|
|
2000
2305
|
}
|
|
2001
|
-
function
|
|
2306
|
+
function said3(c, repo) {
|
|
2002
2307
|
if (c.action === "skipped") return `${c.name}: ${c.reason ?? "skipped"}`;
|
|
2003
2308
|
if (c.action === "removed") return `Removed ${c.name} from ${repo}`;
|
|
2004
|
-
|
|
2309
|
+
const back = c.action === "updated" && c.from !== void 0 && c.to !== void 0 && c.to < c.from;
|
|
2310
|
+
const done = back ? `Pinned ${c.name} to v${c.to} in ${repo} (was v${c.from})` : `${c.action === "installed" ? "Added" : c.action === "reset" ? "Reset" : "Updated"} ${c.name}${c.to ? ` v${c.to}` : ""} in ${repo}`;
|
|
2311
|
+
return c.backedUp ? `${done} (your edits are in Settings \u203A Backups)` : done;
|
|
2005
2312
|
}
|
|
2006
2313
|
function realOps(roots, backups) {
|
|
2007
2314
|
const rootOf = (repo) => roots.get(repo);
|
|
2008
2315
|
const find = (repo, name) => projectStatus(rootOf(repo)).find((s) => s.name === name);
|
|
2009
2316
|
const info = /* @__PURE__ */ new Map();
|
|
2317
|
+
const result = (c, repo) => c.action === "skipped" ? failed(said3(c, repo)) : said3(c, repo);
|
|
2318
|
+
const copyDir = (repo, name) => readManifest(rootOf(repo)).skills[name]?.dir ?? PROJECT_SKILLS_DIR;
|
|
2010
2319
|
const withLinks = (c, repo, name) => {
|
|
2011
|
-
|
|
2012
|
-
|
|
2320
|
+
const it = (dirs) => dirs.length === 1 ? "it" : "them";
|
|
2321
|
+
if (c.uncommitted?.length)
|
|
2322
|
+
return `${said3(c, repo)}; not linked in ${c.uncommitted.join(", ")}: git tracks ${it(c.uncommitted)} and ${copyDir(repo, name)}/${name} isn't committed, so teammates would get broken links`;
|
|
2323
|
+
if (!c.blocked?.length) return result(c, repo);
|
|
2324
|
+
return { message: `${said3(c, repo)}; not linked in ${c.blocked.join(", ")}: git tracks ${it(c.blocked)}`, then: allowTracked([{ repo, name, blocked: c.blocked }]) };
|
|
2013
2325
|
};
|
|
2014
|
-
const
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2326
|
+
const addTo = (repos, names) => {
|
|
2327
|
+
if (repos.length === 1 && names.length === 1) return withLinks(addSkill(rootOf(repos[0]), names[0]), repos[0], names[0]);
|
|
2328
|
+
const changes = repos.flatMap((repo) => names.map((name) => ({ repo, name, c: addSkill(rootOf(repo), name) })));
|
|
2329
|
+
const done = changes.filter((x) => x.c.action !== "skipped");
|
|
2330
|
+
const added = [...new Set(done.map((x) => x.name))];
|
|
2331
|
+
const summary = done.length ? `Added ${added.length === 1 ? `${added[0]} v${done[0].c.to}` : `${added.length} skills`} to ${[...new Set(done.map((x) => x.repo))].join(", ")}` : "";
|
|
2332
|
+
const notes = [];
|
|
2333
|
+
for (const repo of repos)
|
|
2334
|
+
for (const kind of ["blocked", "uncommitted"]) {
|
|
2335
|
+
const here = done.filter((x) => x.repo === repo && x.c[kind]?.length);
|
|
2336
|
+
if (!here.length) continue;
|
|
2337
|
+
const dirs = [...new Set(here.flatMap((x) => x.c[kind]))];
|
|
2338
|
+
const who = here.length === names.length ? "" : `${here.map((x) => x.name).join(", ")}: `;
|
|
2339
|
+
const copies = [...new Set(here.map((x) => copyDir(repo, x.name)))];
|
|
2340
|
+
const why = kind === "blocked" ? `git tracks ${dirs.length === 1 ? "it" : "them"}` : `${copies.join(", ")} ${copies.length === 1 ? "isn't" : "aren't"} committed, so a link would break for teammates`;
|
|
2341
|
+
notes.push(`${who}not linked in ${dirs.join(", ")} in ${repo}: ${why}`);
|
|
2342
|
+
}
|
|
2343
|
+
const skipped = changes.filter((x) => x.c.action === "skipped").map((x) => `${x.name} not added to ${x.repo}: ${x.c.reason}`);
|
|
2344
|
+
const text = [summary, ...notes, ...skipped].filter(Boolean).join("; ");
|
|
2345
|
+
const held = done.flatMap((x) => x.c.blocked?.length ? [{ repo: x.repo, name: x.name, blocked: x.c.blocked }] : []);
|
|
2346
|
+
if (!done.length) return failed(text);
|
|
2347
|
+
if (held.length) return { message: text, then: allowTracked(held) };
|
|
2348
|
+
return skipped.length ? failed(text) : text;
|
|
2349
|
+
};
|
|
2350
|
+
const allowTracked = (held) => {
|
|
2351
|
+
const repos = [...new Set(held.map((h) => h.repo))];
|
|
2352
|
+
const dirs = [...new Set(held.flatMap((h) => h.blocked))];
|
|
2353
|
+
return {
|
|
2354
|
+
label: `Also link ${new Set(held.map((h) => h.name)).size === 1 ? "it" : "them"} in ${dirs.join(", ")}`,
|
|
2355
|
+
preview: `${dirs.join(", ")} ${dirs.length === 1 ? "is" : "are"} committed in ${repos.join(", ")}: the links will show in git status. skilllib remembers this for ${repos.join(", ")}.`,
|
|
2356
|
+
run: (w) => {
|
|
2357
|
+
for (const repo of repos) allowTrackedLinks(rootOf(repo));
|
|
2358
|
+
if (held.length === 1) return w.ops.link(held[0].repo, held[0].name, true);
|
|
2359
|
+
const results = held.map((h) => ({ ...h, r: linkIn(h.repo, h.name, true) }));
|
|
2360
|
+
const linked = results.filter((x) => x.r.created.length);
|
|
2361
|
+
const names = [...new Set(linked.map((x) => x.name))];
|
|
2362
|
+
const summary = linked.length ? `Linked ${names.length === 1 ? names[0] : `${names.length} skills`} in ${[...new Set(linked.flatMap((x) => x.r.created))].join(", ")} in ${[...new Set(linked.map((x) => x.repo))].join(", ")}` : "";
|
|
2363
|
+
const others = results.filter((x) => !x.r.created.length || x.r.uncommitted.length).map((x) => `${x.repo}: ${linkSaid(x.name, x.r)}`);
|
|
2364
|
+
return [summary, ...others].join("; ");
|
|
2365
|
+
}
|
|
2366
|
+
};
|
|
2367
|
+
};
|
|
2368
|
+
const linkIn = (repo, name, allow) => {
|
|
2369
|
+
const s = find(repo, name);
|
|
2370
|
+
return s.managed ? relinkDependency(rootOf(repo), name, { allowTracked: allow }) : linkEverywhere(rootOf(repo), name, s.location, { allowTracked: allow });
|
|
2371
|
+
};
|
|
2372
|
+
const linkSaid = (name, r) => {
|
|
2373
|
+
const linked = r.created.length ? `linked in ${r.created.join(", ")}` : "";
|
|
2374
|
+
const notLinked = r.uncommitted.length ? `not linked in ${r.uncommitted.join(", ")}: git tracks ${r.uncommitted.length === 1 ? "it" : "them"} and ${name} isn't committed, so teammates would get broken links` : "";
|
|
2375
|
+
return `${name}: ${[linked, notLinked].filter(Boolean).join("; ") || "nothing to link"}`;
|
|
2376
|
+
};
|
|
2377
|
+
const orForce = (c, repo, label, force) => c.action === "skipped" && c.reason?.includes("edits") ? {
|
|
2378
|
+
message: `${c.name} in ${repo} has local edits`,
|
|
2379
|
+
then: { label, preview: `Your edits to ${c.name} in ${repo} go to Settings \u203A Backups.`, run: () => result(force(), repo) }
|
|
2380
|
+
} : result(c, repo);
|
|
2023
2381
|
const toLibrary = (path) => {
|
|
2024
2382
|
const r = importSkill(path, { force: true });
|
|
2025
2383
|
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
2384
|
};
|
|
2027
2385
|
return {
|
|
2028
|
-
add: (repo, name) =>
|
|
2386
|
+
add: (repo, name) => addTo([repo], [name]),
|
|
2387
|
+
addTo,
|
|
2029
2388
|
remove: (repo, name) => {
|
|
2030
2389
|
const s = find(repo, name);
|
|
2031
|
-
if (!s) return `${name} isn't in ${repo}
|
|
2032
|
-
|
|
2390
|
+
if (!s) return failed(`${name} isn't in ${repo}`);
|
|
2391
|
+
const committed = !s.managed && ["committed", "changed"].includes(gitInfo(rootOf(repo))?.of(relativeTo(rootOf(repo), s.path)) ?? "");
|
|
2392
|
+
if (s.state.startsWith("repo skill") || committed) return failed(`${name} is the repo's own (committed by your team): skilllib doesn't delete it`);
|
|
2033
2393
|
if (!s.managed) return removeUntracked(rootOf(repo), name, s.path), `Removed ${name} from ${repo} (in Settings \u203A Backups)`;
|
|
2034
2394
|
return orForce(removeSkill(rootOf(repo), name), repo, "Remove it anyway", () => removeSkill(rootOf(repo), name, { force: true }));
|
|
2035
2395
|
},
|
|
2036
2396
|
restore: (repo, name) => {
|
|
2037
2397
|
const c = syncProject(rootOf(repo)).find((x) => x.name === name);
|
|
2038
|
-
return c ?
|
|
2398
|
+
return c ? result(c, repo) : `${name} is already in place`;
|
|
2039
2399
|
},
|
|
2040
2400
|
update: (repo, name) => {
|
|
2041
2401
|
const c = updateProject(rootOf(repo), [name])[0];
|
|
@@ -2053,10 +2413,10 @@ function realOps(roots, backups) {
|
|
|
2053
2413
|
return withLinks(addSkill(rootOf(repo), name, s.managed && s.version ? { version: s.version } : {}), repo, name);
|
|
2054
2414
|
},
|
|
2055
2415
|
link: (repo, name, allow = false) => {
|
|
2056
|
-
const
|
|
2057
|
-
const
|
|
2058
|
-
|
|
2059
|
-
return r.blocked.length
|
|
2416
|
+
const r = linkIn(repo, name, allow);
|
|
2417
|
+
const message = linkSaid(name, r);
|
|
2418
|
+
if (!r.blocked.length || allow) return r.created.length ? message : failed(message);
|
|
2419
|
+
return { message: `${message}; not linked in ${r.blocked.join(", ")}: git tracks ${r.blocked.length === 1 ? "it" : "them"}`, then: allowTracked([{ repo, name, blocked: r.blocked }]) };
|
|
2060
2420
|
},
|
|
2061
2421
|
track: (repo, name) => withLinks(addSkill(rootOf(repo), name), repo, name),
|
|
2062
2422
|
importLocal: (repo, name) => {
|
|
@@ -2065,41 +2425,109 @@ function realOps(roots, backups) {
|
|
|
2065
2425
|
return withLinks(addSkill(rootOf(repo), name), repo, name);
|
|
2066
2426
|
},
|
|
2067
2427
|
copyToLibrary: (repo, name) => toLibrary(find(repo, name).path),
|
|
2428
|
+
tidy: (repo, name, { keep, allowGit = false } = {}) => {
|
|
2429
|
+
const root = repo === null ? null : rootOf(repo);
|
|
2430
|
+
const opts = keep ? { keep: { [name]: keep } } : {};
|
|
2431
|
+
const plan = (root ? planProjectTidy(root, opts) : planGlobalTidy(opts)).plans.find((p) => p.name === name);
|
|
2432
|
+
if (!plan) return `${name}: nothing to tidy`;
|
|
2433
|
+
const r = applyTidy(plan, { git: allowGit ? "go" : "keep" });
|
|
2434
|
+
const steps = (list) => list.map((s) => describeStep(s, root)).join(", ");
|
|
2435
|
+
if (!r.held.length) return r.applied.length ? `${name}: ${steps(r.applied)}` : `${name}: nothing changed`;
|
|
2436
|
+
return {
|
|
2437
|
+
message: `${name}: ${r.applied.length ? `${steps(r.applied)}; ` : ""}held back ${steps(r.held)}, because git tracks it`,
|
|
2438
|
+
then: {
|
|
2439
|
+
label: "Also change what git tracks",
|
|
2440
|
+
preview: `${steps(r.held)}: these changes will show in git status in ${repo}. Replaced copies go to Settings \u203A Backups.`,
|
|
2441
|
+
run: (w) => w.ops.tidy(repo, name, { keep, allowGit: true })
|
|
2442
|
+
}
|
|
2443
|
+
};
|
|
2444
|
+
},
|
|
2068
2445
|
unloadGlobal: (m) => {
|
|
2069
2446
|
if (!m.broken && !latestVersion(m.name)) importSkill(m.path);
|
|
2070
2447
|
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}
|
|
2448
|
+
return r.ok ? `${m.name} no longer loads globally (in Settings \u203A Backups)` : failed(`${m.name}: ${r.reason}`);
|
|
2072
2449
|
},
|
|
2073
2450
|
deleteGlobal: (m) => {
|
|
2074
2451
|
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}
|
|
2452
|
+
return r.ok ? m.broken ? `Removed the broken link ${m.name}` : `Deleted ${m.name} (in Settings \u203A Backups)` : failed(`${m.name}: ${r.reason}`);
|
|
2076
2453
|
},
|
|
2077
2454
|
keepGlobal: (name, keep) => (setKeepGlobal([name], keep), keep ? `${name} marked as global on purpose` : `${name} is no longer marked as global on purpose`),
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
const
|
|
2082
|
-
const
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
const
|
|
2087
|
-
|
|
2088
|
-
|
|
2455
|
+
linkGlobal: (m, agents) => {
|
|
2456
|
+
const r = linkGlobal(m.path, agents);
|
|
2457
|
+
const linked = r.linked.map(tildify4).join(", ");
|
|
2458
|
+
const skipped = r.skipped.length ? `; ${r.skipped.map(tildify4).join(", ")} already holds a different skill` : "";
|
|
2459
|
+
const message = `${m.name}: ${linked ? `linked ${linked}` : "nothing linked"}${skipped}`;
|
|
2460
|
+
return !linked && skipped ? failed(message) : message;
|
|
2461
|
+
},
|
|
2462
|
+
moveGlobal: (skills, repos, group) => {
|
|
2463
|
+
const moved = [];
|
|
2464
|
+
const notes = [];
|
|
2465
|
+
const held = [];
|
|
2466
|
+
for (const m of skills) {
|
|
2467
|
+
if (importSkill(m.path).status === "exists") {
|
|
2468
|
+
notes.push(`${m.name}: your library has a different ${m.name}; nothing moved (update your library from this copy first)`);
|
|
2469
|
+
continue;
|
|
2470
|
+
}
|
|
2471
|
+
if (group) recordOrigin(m.name, `group: ${group}`);
|
|
2472
|
+
const results = repos.map((r2) => [r2, addSkill(rootOf(r2), m.name)]);
|
|
2473
|
+
const skipped = results.filter(([, c]) => c.action === "skipped");
|
|
2474
|
+
if (skipped.length) {
|
|
2475
|
+
notes.push(`${m.name}: not added to ${skipped.map(([r2, c]) => `${r2} (${c.reason})`).join(", ")}, so it still loads globally`);
|
|
2476
|
+
continue;
|
|
2477
|
+
}
|
|
2478
|
+
const r = unloadGlobal(m.path, m.links);
|
|
2479
|
+
if (!r.ok) {
|
|
2480
|
+
notes.push(`${m.name}: ${r.reason}`);
|
|
2481
|
+
continue;
|
|
2482
|
+
}
|
|
2483
|
+
moved.push(m.name);
|
|
2484
|
+
for (const [repo, c] of results) {
|
|
2485
|
+
const blind = visibilityOf(rootOf(repo), m.name).flatMap((v) => v.paths === 0 ? [v.id] : []);
|
|
2486
|
+
const blocked = [...c.blocked ?? [], ...c.uncommitted ?? []];
|
|
2487
|
+
if (!blocked.length || !blind.length) continue;
|
|
2488
|
+
held.push({ repo, name: m.name, blocked, blind, copy: copyDir(repo, m.name), shared: !c.uncommitted?.length });
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
const gaps = /* @__PURE__ */ new Map();
|
|
2492
|
+
for (const repo of new Set(held.map((h) => h.repo)))
|
|
2493
|
+
for (const shared of [true, false]) {
|
|
2494
|
+
const here = held.filter((h) => h.repo === repo && h.shared === shared);
|
|
2495
|
+
if (!here.length) continue;
|
|
2496
|
+
const agents = [...new Set(here.flatMap((h) => h.blind))].map((id) => harness(id).name).join(", ");
|
|
2497
|
+
const what = skills.length === 1 ? "it" : here.length === moved.length ? "them" : here.map((h) => h.name).join(", ");
|
|
2498
|
+
const dirs = [...new Set(here.flatMap((h) => h.blocked))].join(", ");
|
|
2499
|
+
const why = shared ? `git tracks ${dirs}` : `${[...new Set(here.map((h) => h.copy))].join(", ")} isn't committed, so a link in ${dirs} would break for teammates`;
|
|
2500
|
+
const key = `${agents}|${what}|${why}`;
|
|
2501
|
+
if (!gaps.has(key)) gaps.set(key, { who: `${agents} can't see ${what}`, why, repos: [] });
|
|
2502
|
+
gaps.get(key).repos.push(repo);
|
|
2503
|
+
}
|
|
2504
|
+
const blindNotes = [...gaps.values()].map((g) => `${g.who} in ${g.repos.join(", ")}: ${g.why}`);
|
|
2505
|
+
const summary = moved.length ? `${moved.length === 1 ? moved[0] : `${moved.length} skills`} now load${moved.length === 1 ? "s" : ""} only in ${repos.join(", ")}` : "";
|
|
2506
|
+
const text = [summary, ...blindNotes, ...notes].filter(Boolean).join("; ") || "Nothing moved";
|
|
2507
|
+
const askable = held.filter((h) => h.shared);
|
|
2508
|
+
if (!moved.length && notes.length) return failed(text);
|
|
2509
|
+
return askable.length ? { message: text, then: allowTracked(askable) } : text;
|
|
2089
2510
|
},
|
|
2090
2511
|
createSkill: (name) => {
|
|
2091
2512
|
const r = createSkill(name, "");
|
|
2092
|
-
return r.ok ? `Created ${name} in your library: edit its SKILL.md from Your skills` : `${name}: ${r.reason}
|
|
2513
|
+
return r.ok ? `Created ${name} in your library: edit its SKILL.md from Your skills` : failed(`${name}: ${r.reason}`);
|
|
2514
|
+
},
|
|
2515
|
+
pluginOffHere: (repo, id) => {
|
|
2516
|
+
const root = rootOf(repo);
|
|
2517
|
+
const plugin = claudePlugins(skillsLoadedIn(root, machineSkills()), root).find((p) => p.id === id);
|
|
2518
|
+
if (!plugin) return failed(`${id} isn't on in ${repo}`);
|
|
2519
|
+
const r = turnOffIn(plugin, root);
|
|
2520
|
+
return r.ok ? r.message : failed(r.message);
|
|
2093
2521
|
},
|
|
2094
2522
|
replacePlugin: (id, repos) => {
|
|
2095
2523
|
const skills = machineSkills().filter((s) => s.kind === "plugin" && s.origin === id && !s.broken);
|
|
2096
2524
|
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
|
|
2525
|
+
if (clashes.length) return failed(`${id} kept: your library has different ${clashes.join(", ")}; update or delete ${clashes.length === 1 ? "it" : "them"} first`);
|
|
2098
2526
|
const skipped = repos.flatMap((r) => skills.flatMap((s) => {
|
|
2099
2527
|
const c = addSkill(rootOf(r), s.name);
|
|
2100
2528
|
return c.action === "skipped" ? [`${s.name} in ${r} (${c.reason})`] : [];
|
|
2101
2529
|
}));
|
|
2102
|
-
if (skipped.length) return `${id} kept: not added ${skipped.join(", ")}
|
|
2530
|
+
if (skipped.length) return failed(`${id} kept: not added ${skipped.join(", ")}`);
|
|
2103
2531
|
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
2532
|
const bin = claudeBinary();
|
|
2105
2533
|
const claude = (...args) => spawnSync3(bin, ["plugin", ...args], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 2e4 });
|
|
@@ -2115,7 +2543,7 @@ function realOps(roots, backups) {
|
|
|
2115
2543
|
try {
|
|
2116
2544
|
setPluginEnabled(id, false);
|
|
2117
2545
|
} catch (e) {
|
|
2118
|
-
return `${copied}; couldn't turn ${id} off (${why}; ${e.message}): turn it off with /plugin
|
|
2546
|
+
return failed(`${copied}; couldn't turn ${id} off (${why}; ${e.message}): turn it off with /plugin`);
|
|
2119
2547
|
}
|
|
2120
2548
|
return `${copied}; turned ${id} off in ~/.claude/settings.json (${why})${synced ? "" : `: finish with /plugin uninstall ${id}`}`;
|
|
2121
2549
|
},
|
|
@@ -2128,13 +2556,13 @@ function realOps(roots, backups) {
|
|
|
2128
2556
|
from.push([...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root));
|
|
2129
2557
|
}
|
|
2130
2558
|
const r = deleteLibrarySkill(name);
|
|
2131
|
-
if (!r.ok) return `${name}: ${r.reason}
|
|
2559
|
+
if (!r.ok) return failed(`${name}: ${r.reason}`);
|
|
2132
2560
|
return `Deleted ${name} from your library${from.length ? ` and from ${from.join(", ")}` : ""} (in Settings \u203A Backups)`;
|
|
2133
2561
|
},
|
|
2134
2562
|
restoreBackup: (i) => {
|
|
2135
2563
|
const b = backups[i];
|
|
2136
2564
|
const r = b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b);
|
|
2137
|
-
return r.ok ? `Restored ${b.name} to ${
|
|
2565
|
+
return r.ok ? `Restored ${b.name} to ${tildify4(r.to)}` : failed(`${b.name}: ${r.reason}`);
|
|
2138
2566
|
},
|
|
2139
2567
|
setAgents: (ids) => (setHarnesses(ids), "Saved your agents"),
|
|
2140
2568
|
installVersion: (repo, name, version) => withLinks(addSkill(rootOf(repo), name, { version, force: true }), repo, name),
|
|
@@ -2143,20 +2571,28 @@ function realOps(roots, backups) {
|
|
|
2143
2571
|
addRoot: (path) => {
|
|
2144
2572
|
const root = addRoot(path);
|
|
2145
2573
|
discoverProjects();
|
|
2146
|
-
return `Scanning ${
|
|
2574
|
+
return `Scanning ${tildify4(root)} for repos`;
|
|
2147
2575
|
},
|
|
2148
|
-
removeRoot: (path) => (removeRoot(path), `Stopped scanning ${
|
|
2576
|
+
removeRoot: (path) => (removeRoot(path), `Stopped scanning ${tildify4(path)}; repos already found stay until you hide them`),
|
|
2149
2577
|
rescan: () => (discoverProjects(), "Looked for repos in your project folders again"),
|
|
2150
2578
|
hide: (repo) => (setHidden(rootOf(repo), true), `Hid ${repo}: Settings \u203A Hidden repos brings it back`),
|
|
2151
|
-
unhide: (path) => (setHidden(path, false), `${
|
|
2579
|
+
unhide: (path) => (setHidden(path, false), `${tildify4(path)} is back in the list`),
|
|
2152
2580
|
openFolder: (path) => {
|
|
2153
2581
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
2154
2582
|
spawn(cmd, [path], { detached: true, stdio: "ignore" }).unref();
|
|
2155
|
-
return `Opened ${
|
|
2583
|
+
return `Opened ${tildify4(path)}`;
|
|
2156
2584
|
},
|
|
2157
2585
|
copy: (text, what) => {
|
|
2158
2586
|
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 ${
|
|
2587
|
+
return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor or Codex` : `Couldn't reach the clipboard; ${what} is in ${tildify4(r.savedTo)}`;
|
|
2588
|
+
},
|
|
2589
|
+
installAgentSkill: () => {
|
|
2590
|
+
const r = installAgentSkill();
|
|
2591
|
+
return r.ok ? `Your agents can now use skilllib (${r.dirs.map(tildify4).join(", ")})` : failed(`skilllib skill: ${r.reason}`);
|
|
2592
|
+
},
|
|
2593
|
+
removeAgentSkill: () => {
|
|
2594
|
+
const removed = removeAgentSkill();
|
|
2595
|
+
return removed.length ? `Removed the skilllib skill (${removed.map(tildify4).join(", ")})` : failed("The skilllib skill isn't installed");
|
|
2160
2596
|
},
|
|
2161
2597
|
repoInfo: (repo) => {
|
|
2162
2598
|
if (!info.has(repo)) {
|