skilllib 1.6.0 → 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 +20 -15
- package/dist/{chunk-PHRUPZGX.js → chunk-AYAN7Q2X.js} +546 -218
- package/dist/index.js +7 -6
- package/dist/{tui-WYVSDGHD.js → tui-RU255WYX.js} +445 -168
- package/package.json +1 -1
- package/skills/skilllib/SKILL.md +5 -4
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
HARNESSES,
|
|
3
|
+
PROJECT_SKILLS_DIR,
|
|
3
4
|
addRoot,
|
|
4
5
|
addSkill,
|
|
6
|
+
addedAt,
|
|
7
|
+
agentSkillState,
|
|
5
8
|
allowTrackedLinks,
|
|
6
9
|
applyTidy,
|
|
7
10
|
claudeBinary,
|
|
@@ -14,14 +17,13 @@ import {
|
|
|
14
17
|
describeStep,
|
|
15
18
|
discoverProjects,
|
|
16
19
|
enabledHarnesses,
|
|
17
|
-
findProjectRoot,
|
|
18
20
|
forgetLatest,
|
|
19
21
|
gitInfo,
|
|
20
22
|
gitVisibleSteps,
|
|
21
23
|
harness,
|
|
22
24
|
harnessesChosen,
|
|
23
25
|
importSkill,
|
|
24
|
-
|
|
26
|
+
installAgentSkill,
|
|
25
27
|
keptGlobal,
|
|
26
28
|
latestVersion,
|
|
27
29
|
libraryOrigins,
|
|
@@ -34,15 +36,18 @@ import {
|
|
|
34
36
|
planGlobalTidy,
|
|
35
37
|
planProjectTidy,
|
|
36
38
|
pluginBackups,
|
|
39
|
+
projectHere,
|
|
37
40
|
projectOfFactory,
|
|
38
41
|
projectStatus,
|
|
39
42
|
projectsUsing,
|
|
40
43
|
readConfig,
|
|
44
|
+
readManifest,
|
|
41
45
|
readSkillInfo,
|
|
42
46
|
recordOrigin,
|
|
43
47
|
recordRemovedPlugin,
|
|
44
48
|
relativeTo,
|
|
45
49
|
relinkDependency,
|
|
50
|
+
removeAgentSkill,
|
|
46
51
|
removeRoot,
|
|
47
52
|
removeSkill,
|
|
48
53
|
removeUntracked,
|
|
@@ -56,13 +61,14 @@ import {
|
|
|
56
61
|
skilllibHome,
|
|
57
62
|
skillsLoadedIn,
|
|
58
63
|
syncProject,
|
|
64
|
+
tildify,
|
|
59
65
|
turnOffIn,
|
|
60
66
|
unloadGlobal,
|
|
61
67
|
updateProject,
|
|
62
|
-
userHome,
|
|
63
68
|
versionHistory,
|
|
69
|
+
visibilityOf,
|
|
64
70
|
visibleProjects
|
|
65
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-AYAN7Q2X.js";
|
|
66
72
|
|
|
67
73
|
// src/tui/index.tsx
|
|
68
74
|
import { render } from "ink";
|
|
@@ -188,10 +194,7 @@ import { spawnSync } from "child_process";
|
|
|
188
194
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
189
195
|
import { join } from "path";
|
|
190
196
|
var EXCERPT_CHARS = 1200;
|
|
191
|
-
|
|
192
|
-
const home = userHome();
|
|
193
|
-
return p.startsWith(home) ? "~" + p.slice(home.length) : p;
|
|
194
|
-
}
|
|
197
|
+
var tildify2 = (p) => tildify(p);
|
|
195
198
|
function skillExcerpt(dir, chars) {
|
|
196
199
|
const file = join(dir, "SKILL.md");
|
|
197
200
|
if (!existsSync(file)) return { text: "(SKILL.md not found)", files: 0, size: 0 };
|
|
@@ -215,7 +218,7 @@ function buildReviewPrompt(skills, ctx) {
|
|
|
215
218
|
`- Description: ${s.description || "(none)"}`,
|
|
216
219
|
`- Source: ${s.source}`,
|
|
217
220
|
`- Installed via: ${s.installedHow}`,
|
|
218
|
-
`- Location: ${
|
|
221
|
+
`- Location: ${tildify2(s.path)}${s.links.length ? ` (also linked from ${s.links.map(tildify2).join(", ")})` : ""}`,
|
|
219
222
|
`- Loaded by: ${s.loadedBy}`,
|
|
220
223
|
...s.vendor ? [`- Vendor-managed: ${s.vendor}`] : [],
|
|
221
224
|
...s.keptGlobal ? ["- I marked this as global on purpose: recommend changing that only for a strong reason (unused, duplicated, low quality)"] : [],
|
|
@@ -241,7 +244,7 @@ function buildReviewPrompt(skills, ctx) {
|
|
|
241
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.
|
|
242
245
|
|
|
243
246
|
## My projects
|
|
244
|
-
${ctx.projects.map((p) => `- ${p.name} \u2014 ${
|
|
247
|
+
${ctx.projects.map((p) => `- ${p.name} \u2014 ${tildify2(p.path)}`).join("\n") || "(none)"}
|
|
245
248
|
|
|
246
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).
|
|
247
250
|
|
|
@@ -292,9 +295,17 @@ function matchScore(text, query) {
|
|
|
292
295
|
}
|
|
293
296
|
|
|
294
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
|
+
};
|
|
295
306
|
var SEVERITY_RANK = { problem: 0, warning: 1, hint: 2 };
|
|
296
307
|
function filterOf(source) {
|
|
297
|
-
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";
|
|
298
309
|
}
|
|
299
310
|
function project(w, name) {
|
|
300
311
|
return w.projects.find((p) => p.name === name);
|
|
@@ -325,6 +336,10 @@ function lib(w, name) {
|
|
|
325
336
|
return w.library.find((l) => l.name === name);
|
|
326
337
|
}
|
|
327
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
|
+
}
|
|
328
343
|
function dupeIssues(repo, name, d) {
|
|
329
344
|
const g = repo === null ? "-g" : "";
|
|
330
345
|
if (d.differ)
|
|
@@ -381,7 +396,7 @@ function globalFixes(m) {
|
|
|
381
396
|
label: "Move it to the repos that need it\u2026",
|
|
382
397
|
preview: `Pick repos. ${m.name} goes into your library and those repos, and stops loading globally (original backed up).`,
|
|
383
398
|
run: () => "",
|
|
384
|
-
pickRepos: (w, repos) => w.ops.moveGlobal(m, repos)
|
|
399
|
+
pickRepos: (w, repos) => w.ops.moveGlobal([m], repos)
|
|
385
400
|
},
|
|
386
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) },
|
|
387
402
|
{ label: "Delete it", preview: `Remove ${m.path}. A backup is kept in Settings \u203A Backups.`, run: (w) => w.ops.deleteGlobal(m) }
|
|
@@ -389,9 +404,10 @@ function globalFixes(m) {
|
|
|
389
404
|
}
|
|
390
405
|
function machineActions(m) {
|
|
391
406
|
if (m.broken) return [];
|
|
392
|
-
return m.source === "global" ? globalFixes(m) : [];
|
|
407
|
+
return m.source === "global" ? globalFixes(m) : m.source === "skilllib" ? [removeAgentSkillFix()] : [];
|
|
393
408
|
}
|
|
394
409
|
function machineIssues(w, m) {
|
|
410
|
+
if (m.source === "skilllib") return [agentSkillIssue(w)].flatMap((i) => i ? [i] : []);
|
|
395
411
|
if (m.broken)
|
|
396
412
|
return [
|
|
397
413
|
{
|
|
@@ -411,11 +427,12 @@ function machineIssues(w, m) {
|
|
|
411
427
|
severity: "problem",
|
|
412
428
|
title: `Loaded twice: also in ${other.source} ${other.where}`,
|
|
413
429
|
short: "Loaded twice",
|
|
414
|
-
|
|
430
|
+
// Every repo then runs the other copy instead of yours: a decision, never "fix all".
|
|
431
|
+
decision: true,
|
|
415
432
|
fixes: [
|
|
416
433
|
{
|
|
417
434
|
label: `Keep the ${other.source}'s copy, stop loading yours globally`,
|
|
418
|
-
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.`,
|
|
419
436
|
run: (w2) => w2.ops.unloadGlobal(m)
|
|
420
437
|
}
|
|
421
438
|
]
|
|
@@ -458,7 +475,16 @@ function issuesOf(w, projectName, u) {
|
|
|
458
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.`,
|
|
459
476
|
run: (w2) => w2.ops.remove(projectName, s.name)
|
|
460
477
|
};
|
|
461
|
-
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)
|
|
462
488
|
issues.push({
|
|
463
489
|
id: `missing:${s.name}`,
|
|
464
490
|
severity: "problem",
|
|
@@ -467,7 +493,7 @@ function issuesOf(w, projectName, u) {
|
|
|
467
493
|
decision: false,
|
|
468
494
|
fixes: [{ label: "Restore it (sync)", preview: `Reinstall ${s.name} v${s.version} into ${s.dir}.`, run: (w2) => w2.ops.restore(projectName, s.name) }]
|
|
469
495
|
});
|
|
470
|
-
if (s.source === "lib" && s.library === "missing")
|
|
496
|
+
else if (s.source === "lib" && s.library === "missing")
|
|
471
497
|
issues.push({
|
|
472
498
|
id: `not-in-lib:${s.name}`,
|
|
473
499
|
severity: "problem",
|
|
@@ -487,7 +513,7 @@ function issuesOf(w, projectName, u) {
|
|
|
487
513
|
...s.source !== "repo" ? [{ ...remove, label: "Remove this repo's copy, keep it global" }] : [],
|
|
488
514
|
{
|
|
489
515
|
label: "Stop loading it globally after all",
|
|
490
|
-
preview: `Copy ${s.name} into your library if needed, then move ${g.path} to the backups.
|
|
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)}.`,
|
|
491
517
|
run: (w2) => w2.ops.unloadGlobal(g)
|
|
492
518
|
}
|
|
493
519
|
]
|
|
@@ -498,17 +524,28 @@ function issuesOf(w, projectName, u) {
|
|
|
498
524
|
severity: "problem",
|
|
499
525
|
title: `Loaded twice: also global in ${g.where}`,
|
|
500
526
|
short: "Loaded twice",
|
|
501
|
-
|
|
527
|
+
// Unloading the global copy changes every repo: a decision, never "fix all".
|
|
528
|
+
decision: true,
|
|
502
529
|
fixes: [
|
|
503
530
|
{
|
|
504
531
|
label: "Keep this repo's copy, stop loading it globally",
|
|
505
|
-
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)}.`,
|
|
506
533
|
run: (w2) => w2.ops.unloadGlobal(g)
|
|
507
534
|
},
|
|
508
535
|
...s.source !== "repo" ? [{ ...remove, label: "Keep it global, remove it from this repo" }] : []
|
|
509
536
|
]
|
|
510
537
|
});
|
|
511
|
-
|
|
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)
|
|
512
549
|
issues.push({
|
|
513
550
|
id: `twice-p:${s.name}`,
|
|
514
551
|
severity: "problem",
|
|
@@ -519,7 +556,7 @@ function issuesOf(w, projectName, u) {
|
|
|
519
556
|
...s.source !== "repo" ? [{ ...remove, label: `Remove this repo's copy, use the ${vendor.source}'s` }] : [],
|
|
520
557
|
vendor.source === "plugin" ? {
|
|
521
558
|
label: `Turn the plugin off in ${projectName} only`,
|
|
522
|
-
preview: `Sets "${vendor.where}": false in ${projectName}'s .claude/settings.local.json (claude plugin disable --scope local). Other repos keep it
|
|
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." : ""}`,
|
|
523
560
|
run: (w2) => w2.ops.pluginOffHere(projectName, vendor.where)
|
|
524
561
|
} : {
|
|
525
562
|
label: `Turn it off in ${vendor.where}`,
|
|
@@ -552,21 +589,34 @@ function issuesOf(w, projectName, u) {
|
|
|
552
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) }]
|
|
553
590
|
});
|
|
554
591
|
const blind = w.agents.filter((a) => !u.agents.includes(a));
|
|
555
|
-
|
|
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)
|
|
556
596
|
issues.push({
|
|
557
597
|
id: `blind:${s.name}`,
|
|
558
598
|
severity: "warning",
|
|
559
|
-
title: `${blind
|
|
560
|
-
short: `${blind
|
|
599
|
+
title: `${agentNames(blind)} can't see it`,
|
|
600
|
+
short: `${agentNames(blind)} can't see it`,
|
|
561
601
|
decision: false,
|
|
562
602
|
fixes: [
|
|
563
603
|
{
|
|
564
604
|
label: "Link it for every agent",
|
|
565
|
-
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}`,
|
|
566
606
|
run: (w2) => w2.ops.link(projectName, s.name)
|
|
567
607
|
}
|
|
568
608
|
]
|
|
569
609
|
});
|
|
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
|
+
});
|
|
570
620
|
const differ = !!s.dupes?.differ;
|
|
571
621
|
if (s.source !== "lib" && s.library === "differs" && !differ)
|
|
572
622
|
issues.push({
|
|
@@ -586,7 +636,8 @@ function issuesOf(w, projectName, u) {
|
|
|
586
636
|
severity: "hint",
|
|
587
637
|
title: "Same as your library skill, but not tracked",
|
|
588
638
|
short: "Not tracked",
|
|
589
|
-
|
|
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,
|
|
590
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) }]
|
|
591
642
|
});
|
|
592
643
|
if (s.source === "untracked" && !l && !vendor && !differ)
|
|
@@ -595,7 +646,8 @@ function issuesOf(w, projectName, u) {
|
|
|
595
646
|
severity: "hint",
|
|
596
647
|
title: "Only exists in this repo",
|
|
597
648
|
short: "Only in this repo",
|
|
598
|
-
|
|
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,
|
|
599
651
|
fixes: [
|
|
600
652
|
{
|
|
601
653
|
label: "Import it into your library",
|
|
@@ -604,7 +656,7 @@ function issuesOf(w, projectName, u) {
|
|
|
604
656
|
}
|
|
605
657
|
]
|
|
606
658
|
});
|
|
607
|
-
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)
|
|
608
660
|
issues.push({
|
|
609
661
|
id: `unused:${s.name}`,
|
|
610
662
|
severity: "hint",
|
|
@@ -615,6 +667,53 @@ function issuesOf(w, projectName, u) {
|
|
|
615
667
|
});
|
|
616
668
|
return issues;
|
|
617
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
|
+
}
|
|
618
717
|
function worst(issues) {
|
|
619
718
|
return [...issues].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity])[0];
|
|
620
719
|
}
|
|
@@ -643,7 +742,7 @@ function deleteLibraryFix(w, names) {
|
|
|
643
742
|
"Every copy goes to Settings \u203A Backups.",
|
|
644
743
|
others.length ? `Untracked or the team's own copies in ${others.join(", ")} stay.` : ""
|
|
645
744
|
].filter(Boolean).join(" "),
|
|
646
|
-
run: (w2) => names.map((n) => w2.ops.deleteLibrary(n))
|
|
745
|
+
run: (w2) => joined(names.map((n) => w2.ops.deleteLibrary(n)))
|
|
647
746
|
};
|
|
648
747
|
}
|
|
649
748
|
function replacePluginFix(w, id) {
|
|
@@ -690,6 +789,7 @@ function groupCandidates(w, u) {
|
|
|
690
789
|
if (m?.source === "claude.ai") return [{ key: "claude.ai", label: "claude.ai skills" }];
|
|
691
790
|
if (m?.source === "cursor") return [{ key: "cursor", label: "Cursor built-ins" }];
|
|
692
791
|
if (m?.source === "system") return [{ key: `system:${m.where}`, label: m.where }];
|
|
792
|
+
if (m?.source === "skilllib") return [];
|
|
693
793
|
if (m) {
|
|
694
794
|
const origin = originGroup(m.origin);
|
|
695
795
|
return [...origin ? [origin] : [], ...byTime("global", m.installed), ...byWord("global", m.name)];
|
|
@@ -733,10 +833,8 @@ function addRows(w, target, q) {
|
|
|
733
833
|
name: q ? `+ Create "${q}"${where}` : "+ Create a new skill (type its name)",
|
|
734
834
|
note: "",
|
|
735
835
|
description: "",
|
|
736
|
-
run: (w2) =>
|
|
737
|
-
|
|
738
|
-
return target ? w2.ops.add(target, q) : made;
|
|
739
|
-
}
|
|
836
|
+
run: (w2) => w2.ops.createSkill(q),
|
|
837
|
+
...target ? { afterEdit: (w2) => w2.ops.add(target, q) } : {}
|
|
740
838
|
},
|
|
741
839
|
{ key: "agent", agent: true, name: q ? `\u2726 Write "${q}" with an agent` : "\u2726 Write a new skill with an agent", note: "", description: "" }
|
|
742
840
|
];
|
|
@@ -745,9 +843,32 @@ function addRows(w, target, q) {
|
|
|
745
843
|
}
|
|
746
844
|
|
|
747
845
|
// src/tui/prompts.ts
|
|
748
|
-
|
|
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) {
|
|
749
850
|
const skills = names.map((name) => {
|
|
750
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
|
+
}
|
|
751
872
|
const m = copies.find((c) => c.source === "global") ?? copies[0];
|
|
752
873
|
const lib2 = w.library.find((l) => l.name === name);
|
|
753
874
|
const repos = w.projects.filter((p) => p.skills.some((s) => s.name === name));
|
|
@@ -755,11 +876,11 @@ function reviewPrompt(w, names, scope) {
|
|
|
755
876
|
return {
|
|
756
877
|
name,
|
|
757
878
|
description: w.descriptions[name] ?? "",
|
|
758
|
-
source: m ? m.source === "global" ? "my global skills" : `vendor: ${m.source} (${m.where})` : lib2 ? "Your skills (skilllib library)" : "a repo's own skill",
|
|
759
|
-
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"),
|
|
760
881
|
path: m?.path ?? (lib2 ? w.ops.libraryFile(name).replace(/\/SKILL\.md$/, "") : local?.path ?? ""),
|
|
761
882
|
links: m?.links ?? [],
|
|
762
|
-
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",
|
|
763
884
|
vendor: m && m.source !== "global" ? "turn it off at its source (/plugin in Claude Code, claude.ai settings, or Cursor)" : null,
|
|
764
885
|
usesTotal: w.days ? totalUses(w, name) : null,
|
|
765
886
|
usesByProject: Object.entries(w.usage).flatMap(([p, u]) => u[name] ? [[p, u[name]]] : []),
|
|
@@ -792,9 +913,7 @@ Keep it under 200 lines. ${p ? "skilllib will offer to import it into my library
|
|
|
792
913
|
|
|
793
914
|
// src/tui/App.tsx
|
|
794
915
|
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
795
|
-
|
|
796
|
-
return p.startsWith(userHome()) ? "~" + p.slice(userHome().length) : p;
|
|
797
|
-
}
|
|
916
|
+
var tildify3 = (p) => tildify(p);
|
|
798
917
|
var PLACES = [
|
|
799
918
|
["projects", "Projects"],
|
|
800
919
|
["library", "Your skills"],
|
|
@@ -826,6 +945,7 @@ function tag(u, w) {
|
|
|
826
945
|
if (s) return [s.source, color.blue];
|
|
827
946
|
if (u.source === "global") return [u.machine?.kept ? "global \u2713" : "global", color.yellow];
|
|
828
947
|
if (u.source === "plugin") return [`\u29C9 ${u.where.split("@")[0]}`, color.magenta];
|
|
948
|
+
if (u.source === "skilllib") return ["\u25C6 skilllib", color.accent];
|
|
829
949
|
return [u.source, color.muted];
|
|
830
950
|
}
|
|
831
951
|
function isNameChar(input, key) {
|
|
@@ -876,7 +996,7 @@ function TitleRow({ cells, width }) {
|
|
|
876
996
|
}
|
|
877
997
|
var fixOption = (fix) => ({ label: fix.label, fix });
|
|
878
998
|
var GIT_LABEL = { committed: "\u2713 committed", changed: "\xB1 uncommitted changes", new: "+ not committed", ignored: "\u2205 gitignored" };
|
|
879
|
-
var COPY_RANK = { lib: 0, repo: 0, untracked: 0, global: 1, plugin: 2, "claude.ai": 3, cursor: 3, system: 3 };
|
|
999
|
+
var COPY_RANK = { lib: 0, repo: 0, untracked: 0, global: 1, skilllib: 1, plugin: 2, "claude.ai": 3, cursor: 3, system: 3 };
|
|
880
1000
|
function byName(copies) {
|
|
881
1001
|
const groups = /* @__PURE__ */ new Map();
|
|
882
1002
|
for (const c of copies) groups.set(c.name, [...groups.get(c.name) ?? [], c]);
|
|
@@ -944,7 +1064,7 @@ function skillItem(w, copies, issues, nameW, actions, repo) {
|
|
|
944
1064
|
const b = builder(u.name);
|
|
945
1065
|
for (const l of wrap(w.descriptions[u.name] || "(no description)", width - 4, 3)) b.line([l, color.muted]);
|
|
946
1066
|
b.line();
|
|
947
|
-
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]);
|
|
948
1068
|
else b.issues(issues);
|
|
949
1069
|
b.actions(actions, width);
|
|
950
1070
|
b.line();
|
|
@@ -953,7 +1073,7 @@ function skillItem(w, copies, issues, nameW, actions, repo) {
|
|
|
953
1073
|
b.line(["Loaded from", color.accent]);
|
|
954
1074
|
for (const c of copies) {
|
|
955
1075
|
const s = c.local;
|
|
956
|
-
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;
|
|
957
1077
|
const extra = s ? s.missing ? "missing" : s.git ? GIT_LABEL[s.git] : "" : "";
|
|
958
1078
|
const [ct, cc] = c === u ? ["", void 0] : tag(c, w);
|
|
959
1079
|
b.line([" ", void 0], [path + " ", color.text], [ct ? ct + " " : "", cc], [extra, color.faint]);
|
|
@@ -967,7 +1087,10 @@ function projectSkillItems(w, p, nameW, ui) {
|
|
|
967
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)
|
|
968
1088
|
);
|
|
969
1089
|
}
|
|
970
|
-
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
|
+
});
|
|
971
1094
|
function versionsOption(w, repo, name, installed, ui) {
|
|
972
1095
|
return {
|
|
973
1096
|
label: "Other versions\u2026",
|
|
@@ -980,27 +1103,35 @@ function versionsOption(w, repo, name, installed, ui) {
|
|
|
980
1103
|
function skillActions(w, p, u, ui) {
|
|
981
1104
|
const s = u.local;
|
|
982
1105
|
const inLibrary = w.library.find((l) => l.name === u.name);
|
|
983
|
-
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
|
+
};
|
|
984
1112
|
if (s?.source === "lib")
|
|
985
1113
|
return [
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
+
] : [],
|
|
989
1120
|
fixOption(remove),
|
|
990
|
-
reviewOption(w, u.name, ui)
|
|
1121
|
+
reviewOption(w, u.name, ui, p)
|
|
991
1122
|
];
|
|
992
1123
|
if (s?.source === "repo")
|
|
993
1124
|
return [
|
|
994
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) })],
|
|
995
1126
|
{ label: "Edit SKILL.md", action: () => ui.edit(`${s.path}/SKILL.md`) },
|
|
996
|
-
reviewOption(w, u.name, ui)
|
|
1127
|
+
reviewOption(w, u.name, ui, p)
|
|
997
1128
|
];
|
|
998
1129
|
if (s)
|
|
999
1130
|
return [
|
|
1000
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) }),
|
|
1001
1132
|
{ label: "Edit SKILL.md", action: () => ui.edit(`${s.path}/SKILL.md`) },
|
|
1002
1133
|
fixOption(remove),
|
|
1003
|
-
reviewOption(w, u.name, ui)
|
|
1134
|
+
reviewOption(w, u.name, ui, p)
|
|
1004
1135
|
];
|
|
1005
1136
|
return [{ label: "Open in Global", action: () => ui.openInGlobal(u.name) }, reviewOption(w, u.name, ui)];
|
|
1006
1137
|
}
|
|
@@ -1009,7 +1140,7 @@ function overview(w, p) {
|
|
|
1009
1140
|
const info = w.ops.repoInfo(p);
|
|
1010
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];
|
|
1011
1142
|
return [
|
|
1012
|
-
["Folder", [[
|
|
1143
|
+
["Folder", [[tildify3(project2.path), color.text]]],
|
|
1013
1144
|
["Remote", [[info.remote ?? "none", info.remote ? color.blue : color.faint]]],
|
|
1014
1145
|
["Branch", [[info.branch ?? "\u2013", color.text], [info.dirty ? ` ${info.dirty} uncommitted` : "", color.yellow]]],
|
|
1015
1146
|
["Manifest", [manifest]]
|
|
@@ -1060,7 +1191,7 @@ function globalActions(w, u, ui) {
|
|
|
1060
1191
|
return [
|
|
1061
1192
|
...machineActions(m).map(fixOption),
|
|
1062
1193
|
...m.source === "plugin" ? [fixOption(replacePluginFix(w, m.where))] : [],
|
|
1063
|
-
...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`) }],
|
|
1064
1195
|
...m.source === "global" && !m.broken ? [{ label: "Edit SKILL.md", action: () => ui.edit(`${m.path}/SKILL.md`) }] : [],
|
|
1065
1196
|
reviewOption(w, m.name, ui)
|
|
1066
1197
|
];
|
|
@@ -1068,8 +1199,9 @@ function globalActions(w, u, ui) {
|
|
|
1068
1199
|
function libraryItems(w, nameW, ui) {
|
|
1069
1200
|
return w.library.map((l) => {
|
|
1070
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"));
|
|
1071
1203
|
const uses = totalUses(w, l.name);
|
|
1072
|
-
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]) };
|
|
1073
1205
|
const delFix = deleteLibraryFix(w, [l.name]);
|
|
1074
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] }];
|
|
1075
1207
|
const behind = repos.filter((p) => p.skills.some((s) => s.name === l.name && s.source === "lib" && s.version < l.latest)).length;
|
|
@@ -1101,13 +1233,13 @@ function libraryItems(w, nameW, ui) {
|
|
|
1101
1233
|
b.issues(issues);
|
|
1102
1234
|
b.actions([
|
|
1103
1235
|
fixOption(addFix),
|
|
1104
|
-
...
|
|
1236
|
+
...removable.length ? [
|
|
1105
1237
|
fixOption({
|
|
1106
1238
|
label: "Remove from repos\u2026",
|
|
1107
1239
|
preview: `Pick repos to remove ${l.name} from. Your library keeps it.`,
|
|
1108
1240
|
run: () => "",
|
|
1109
|
-
candidates: () =>
|
|
1110
|
-
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)))
|
|
1111
1243
|
})
|
|
1112
1244
|
] : [],
|
|
1113
1245
|
fixOption(delFix),
|
|
@@ -1123,15 +1255,39 @@ function libraryItems(w, nameW, ui) {
|
|
|
1123
1255
|
}
|
|
1124
1256
|
function healthItems(w, nameW, ui) {
|
|
1125
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]] }));
|
|
1126
|
-
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
|
+
];
|
|
1127
1285
|
}
|
|
1128
1286
|
function addCursor(rows, cursor) {
|
|
1129
1287
|
const i = Math.min(cursor, rows.length - 1);
|
|
1130
1288
|
return rows[i] && !rows[i].header ? i : Math.max(0, rows.findIndex((r) => !r.header));
|
|
1131
1289
|
}
|
|
1132
|
-
|
|
1133
|
-
return typeof r === "string" ? r : r.message;
|
|
1134
|
-
}
|
|
1290
|
+
var said2 = said;
|
|
1135
1291
|
function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
1136
1292
|
const { exit, suspendTerminal } = useApp();
|
|
1137
1293
|
const { columns: termCols, rows: termRows } = useWindowSize();
|
|
@@ -1169,12 +1325,14 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1169
1325
|
notify: setToast,
|
|
1170
1326
|
openInGlobal,
|
|
1171
1327
|
edit,
|
|
1172
|
-
copy: (text, what) => setToast(
|
|
1328
|
+
copy: (text, what) => setToast(said2(world.ops.copy(text, what))),
|
|
1173
1329
|
menu: (title, options) => setModal({ kind: "menu", title, options, cursor: 0 })
|
|
1174
1330
|
};
|
|
1175
1331
|
const flagged = [
|
|
1176
1332
|
...world.projects.flatMap((p) => projectIssues(world, p.name).map((x) => ({ key: `${p.name}:${x.skill.name}`, issue: x.issue }))),
|
|
1177
|
-
...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 }] : [])
|
|
1178
1336
|
];
|
|
1179
1337
|
const healthCount = new Set(flagged.map((f) => f.key)).size;
|
|
1180
1338
|
const healthWorst = worst(flagged.map((f) => f.issue));
|
|
@@ -1203,16 +1361,18 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1203
1361
|
if (items[cursor]?.header) cursor = firstSelectable;
|
|
1204
1362
|
const current = items[cursor];
|
|
1205
1363
|
const detail = current?.detail?.(columns);
|
|
1206
|
-
function apply(run) {
|
|
1364
|
+
function apply(run, w = world) {
|
|
1207
1365
|
let result;
|
|
1208
1366
|
try {
|
|
1209
|
-
result = run(
|
|
1367
|
+
result = run(w);
|
|
1210
1368
|
} catch (e) {
|
|
1211
|
-
result = `Couldn't do it: ${e.message}
|
|
1369
|
+
result = failed(`Couldn't do it: ${e.message}`);
|
|
1212
1370
|
}
|
|
1213
1371
|
setBase(reload());
|
|
1214
|
-
|
|
1215
|
-
|
|
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 });
|
|
1216
1376
|
}
|
|
1217
1377
|
function choose(o) {
|
|
1218
1378
|
if (o.action) return o.action();
|
|
@@ -1232,6 +1392,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1232
1392
|
function fixSelected() {
|
|
1233
1393
|
const issue = current && worst(current.issues);
|
|
1234
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}`);
|
|
1235
1396
|
if (issue.decision) {
|
|
1236
1397
|
setFocus("detail");
|
|
1237
1398
|
setDetailCursor(Math.max(0, detail?.options.findIndex((o) => o.issue === issue) ?? 0));
|
|
@@ -1308,7 +1469,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1308
1469
|
preview: `Pick repos. These ${n(mine.length)} go into your library and those repos, and stop loading globally (originals backed up).`,
|
|
1309
1470
|
run: () => "",
|
|
1310
1471
|
preticked: usedIn,
|
|
1311
|
-
pickRepos: (w, repos) =>
|
|
1472
|
+
pickRepos: (w, repos) => w.ops.moveGlobal(mine, repos, label)
|
|
1312
1473
|
}),
|
|
1313
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`) })] : [],
|
|
1314
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)`) })
|
|
@@ -1319,7 +1480,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1319
1480
|
preview: `Install these ${n(libs.length)} into the repos you pick.`,
|
|
1320
1481
|
run: () => "",
|
|
1321
1482
|
preticked: () => [],
|
|
1322
|
-
pickRepos: (w, repos) => (repos
|
|
1483
|
+
pickRepos: (w, repos) => w.ops.addTo(repos, libs.map((l) => l.name))
|
|
1323
1484
|
})
|
|
1324
1485
|
] : [],
|
|
1325
1486
|
...libs.length ? [fixOption(deleteLibraryFix(world, libs.map((l) => l.name)))] : [],
|
|
@@ -1359,9 +1520,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1359
1520
|
`\u22EF ${repo}\u2026`,
|
|
1360
1521
|
["Open its folder, hide it from the list, or copy a review prompt for its skills."],
|
|
1361
1522
|
() => ui.menu(repo, [
|
|
1362
|
-
{ label: "Open its folder", action: () => setToast(
|
|
1523
|
+
{ label: "Open its folder", action: () => setToast(said2(world.ops.openFolder(project(world, repo).path))) },
|
|
1363
1524
|
{ label: "Hide it from the list", action: () => (apply((w) => w.ops.hide(repo)), setOpen(null), setFocus(sidebar ? "sidebar" : "list")) },
|
|
1364
|
-
...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}`) }] : []
|
|
1365
1526
|
])
|
|
1366
1527
|
)
|
|
1367
1528
|
];
|
|
@@ -1393,19 +1554,21 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1393
1554
|
setQuery("");
|
|
1394
1555
|
setChip("all");
|
|
1395
1556
|
}
|
|
1396
|
-
function edit(file) {
|
|
1397
|
-
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`));
|
|
1398
1559
|
const editor = process.env.VISUAL || process.env.EDITOR || (process.platform === "win32" ? "notepad" : "vi");
|
|
1399
1560
|
void suspendTerminal(() => {
|
|
1400
1561
|
if (process.platform === "win32") spawnSync2(`${editor} "${file}"`, { shell: true, stdio: "inherit" });
|
|
1401
1562
|
else spawnSync2("/bin/sh", ["-c", `${editor} "$1"`, "sh", file], { stdio: "inherit" });
|
|
1402
1563
|
}).then(() => {
|
|
1403
|
-
|
|
1404
|
-
|
|
1564
|
+
const fresh = reload();
|
|
1565
|
+
if (then) return apply(then, fresh);
|
|
1566
|
+
setBase(fresh);
|
|
1567
|
+
setToast(`Saved ${tildify3(file)}`);
|
|
1405
1568
|
});
|
|
1406
1569
|
}
|
|
1407
1570
|
function askForFolder(title) {
|
|
1408
|
-
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)) });
|
|
1409
1572
|
}
|
|
1410
1573
|
useEffect(() => {
|
|
1411
1574
|
const folder = () => !initial.roots.length && askForFolder("Where are your projects? skilllib finds the git repos in there");
|
|
@@ -1434,20 +1597,27 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1434
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"}`));
|
|
1435
1598
|
})
|
|
1436
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
|
+
),
|
|
1437
1607
|
header("#roots", "Project folders"),
|
|
1438
1608
|
...world.roots.map(
|
|
1439
1609
|
(r) => row(
|
|
1440
1610
|
`root:${r}`,
|
|
1441
|
-
|
|
1611
|
+
tildify3(r),
|
|
1442
1612
|
"scanned for git repos",
|
|
1443
|
-
() => ui.menu(
|
|
1613
|
+
() => ui.menu(tildify3(r), [
|
|
1444
1614
|
{ label: "Look for repos again", action: () => apply((w) => w.ops.rescan()) },
|
|
1445
|
-
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) })
|
|
1446
1616
|
])
|
|
1447
1617
|
)
|
|
1448
1618
|
),
|
|
1449
1619
|
row("root:add", "+ Add a folder\u2026", "", () => askForFolder("Folder that contains your projects")),
|
|
1450
|
-
...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))))] : [],
|
|
1451
1621
|
header("#backups", "Backups"),
|
|
1452
1622
|
...world.backups.length ? world.backups.map(
|
|
1453
1623
|
(b, i) => row(`backup:${i}`, `\u21BA ${b.name}`, `${b.from} \xB7 ${b.at}`, () => apply((w) => w.ops.restoreBackup(i)))
|
|
@@ -1551,7 +1721,11 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1551
1721
|
}
|
|
1552
1722
|
if (m.kind === "input") {
|
|
1553
1723
|
if (key.escape) return setModal(null);
|
|
1554
|
-
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
|
+
}
|
|
1555
1729
|
if (key.backspace || key.delete) return setModal({ ...m, value: m.value.slice(0, -1) });
|
|
1556
1730
|
if (key.ctrl && input === "u") return setModal({ ...m, value: "" });
|
|
1557
1731
|
if (input && !key.ctrl && !key.meta) return setModal({ ...m, value: m.value + input });
|
|
@@ -1564,7 +1738,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1564
1738
|
if (key.return && m.options[m.cursor]) return setModal(null), choose(m.options[m.cursor]);
|
|
1565
1739
|
return;
|
|
1566
1740
|
}
|
|
1567
|
-
if (key.escape) return setModal(null);
|
|
1741
|
+
if (key.escape) return setModal(null), m.kind === "confirm" && m.toast && setToast(m.toast);
|
|
1568
1742
|
if (m.kind === "confirm") {
|
|
1569
1743
|
if (key.return || input === "y") {
|
|
1570
1744
|
setModal(null);
|
|
@@ -1601,17 +1775,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1601
1775
|
if (key.return) {
|
|
1602
1776
|
setModal(null);
|
|
1603
1777
|
const chosen = m.items.filter((it) => it.on);
|
|
1604
|
-
apply((w) =>
|
|
1605
|
-
let ok = 0;
|
|
1606
|
-
for (const it of chosen) {
|
|
1607
|
-
try {
|
|
1608
|
-
it.fix.run(w);
|
|
1609
|
-
ok++;
|
|
1610
|
-
} catch {
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
return `Applied ${ok} fix${ok === 1 ? "" : "es"} (anything removed is in Settings \u203A Backups)`;
|
|
1614
|
-
});
|
|
1778
|
+
apply((w) => runAll(w, chosen.map((it) => it.fix)));
|
|
1615
1779
|
}
|
|
1616
1780
|
return;
|
|
1617
1781
|
}
|
|
@@ -1634,9 +1798,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1634
1798
|
const row = rows[cursor2];
|
|
1635
1799
|
if (row.create && !/^[a-z0-9]/.test(m.query)) return setToast("Type the new skill's name first");
|
|
1636
1800
|
setModal(null);
|
|
1637
|
-
apply(row.run);
|
|
1638
|
-
|
|
1639
|
-
return;
|
|
1801
|
+
if (!row.create) return apply(row.run);
|
|
1802
|
+
row.run(world);
|
|
1803
|
+
return edit(world.ops.libraryFile(m.query), row.afterEdit);
|
|
1640
1804
|
}
|
|
1641
1805
|
if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
|
|
1642
1806
|
}
|
|
@@ -1749,8 +1913,11 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1749
1913
|
);
|
|
1750
1914
|
else if (modal?.kind === "input")
|
|
1751
1915
|
body = /* @__PURE__ */ jsxs2(Panel, { title: modal.title, focused: true, width: columns, height: 5, children: [
|
|
1752
|
-
/* @__PURE__ */ jsx2(Text2, { color: color.text, children: modal.value + "\u258F" }),
|
|
1753
|
-
|
|
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` })
|
|
1754
1921
|
] });
|
|
1755
1922
|
else if (modal?.kind === "agents")
|
|
1756
1923
|
body = /* @__PURE__ */ jsx2(
|
|
@@ -1877,7 +2044,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1877
2044
|
] })
|
|
1878
2045
|
] }),
|
|
1879
2046
|
/* @__PURE__ */ jsxs2(Box2, { height: 1, width: termCols, justifyContent: "space-between", children: [
|
|
1880
|
-
/* @__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 }),
|
|
1881
2048
|
toast ? null : /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: hints + " " })
|
|
1882
2049
|
] })
|
|
1883
2050
|
] });
|
|
@@ -1936,9 +2103,8 @@ function Help({ width, height }) {
|
|
|
1936
2103
|
import { execFileSync, spawn, spawnSync as spawnSync3 } from "child_process";
|
|
1937
2104
|
import { existsSync as existsSync3, lstatSync, realpathSync } from "fs";
|
|
1938
2105
|
import { basename, dirname, join as join2 } from "path";
|
|
1939
|
-
function
|
|
1940
|
-
|
|
1941
|
-
return (p.startsWith(home) ? "~" + p.slice(home.length) : p).replace(/\\/g, "/");
|
|
2106
|
+
function tildify4(p) {
|
|
2107
|
+
return tildify(p).replace(/\\/g, "/");
|
|
1942
2108
|
}
|
|
1943
2109
|
function uniqueNames(roots) {
|
|
1944
2110
|
const tail = (r, n) => r.split(/[\\/]/).filter(Boolean).slice(-n).join("/");
|
|
@@ -1956,25 +2122,37 @@ function localSkill(root, s, git2) {
|
|
|
1956
2122
|
"update available": {},
|
|
1957
2123
|
"edited locally": { edited: true },
|
|
1958
2124
|
"edited locally, update available": { edited: true },
|
|
1959
|
-
|
|
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" } },
|
|
1960
2127
|
"not in library": { library: "missing" }
|
|
1961
2128
|
};
|
|
1962
2129
|
const other = {
|
|
1963
2130
|
"untracked copy of library skill": { source: "untracked", library: "same" },
|
|
1964
2131
|
"untracked, differs from library": { source: "untracked", library: "differs" },
|
|
1965
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" },
|
|
1966
2135
|
"repo skill": { source: "repo" },
|
|
1967
2136
|
"repo skill, in library": { source: "repo", library: "same" },
|
|
1968
2137
|
"repo skill, differs from library": { source: "repo", library: "differs" }
|
|
1969
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]));
|
|
1970
2144
|
return {
|
|
1971
2145
|
name: s.name,
|
|
1972
2146
|
source: "lib",
|
|
1973
2147
|
dir: s.location,
|
|
1974
2148
|
path: s.path,
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
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
|
|
1978
2156
|
};
|
|
1979
2157
|
}
|
|
1980
2158
|
var partsOf = /* @__PURE__ */ new Map();
|
|
@@ -2004,8 +2182,8 @@ function machineSkill(s, kept) {
|
|
|
2004
2182
|
const global = s.kind === "global" || s.kind === "skills.sh";
|
|
2005
2183
|
return {
|
|
2006
2184
|
name: s.name,
|
|
2007
|
-
source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : s.kind === "system" ? "system" : "cursor",
|
|
2008
|
-
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,
|
|
2009
2187
|
...s.kind === "skills.sh" ? { origin: `npx skills: ${s.origin}` } : {},
|
|
2010
2188
|
path: s.path,
|
|
2011
2189
|
links: s.links,
|
|
@@ -2020,14 +2198,13 @@ function dupesOf(r, root, git2 = null) {
|
|
|
2020
2198
|
const out = /* @__PURE__ */ new Map();
|
|
2021
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)) });
|
|
2022
2200
|
for (const c of r.conflicts)
|
|
2023
|
-
out.set(c.name, { steps: [], git: [], differ: c.copies.map((x) => ({ dir: x.dir, label: root ? copyLabel(c, x.dir) :
|
|
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 })) });
|
|
2024
2202
|
return out;
|
|
2025
2203
|
}
|
|
2026
2204
|
function loadWorld() {
|
|
2027
2205
|
forgetLatest();
|
|
2028
2206
|
const agents = enabledHarnesses();
|
|
2029
|
-
const
|
|
2030
|
-
const here = isProjectCandidate(cwdRoot) && (existsSync3(join2(cwdRoot, ".git")) || existsSync3(join2(cwdRoot, "skilllib.json"))) ? cwdRoot : null;
|
|
2207
|
+
const here = projectHere();
|
|
2031
2208
|
const real = (p) => {
|
|
2032
2209
|
try {
|
|
2033
2210
|
return realpathSync(p);
|
|
@@ -2090,10 +2267,11 @@ function loadWorld() {
|
|
|
2090
2267
|
library,
|
|
2091
2268
|
usage: {},
|
|
2092
2269
|
descriptions,
|
|
2093
|
-
backups: backups.map((b) => ({ name: b.name, from:
|
|
2270
|
+
backups: backups.map((b) => ({ name: b.name, from: tildify4(b.from), at: b.movedAt })),
|
|
2094
2271
|
roots: readConfig().roots,
|
|
2095
2272
|
hidden: readConfig().hidden,
|
|
2096
2273
|
agentsChosen: harnessesChosen(),
|
|
2274
|
+
agentSkill: agentSkillState(agents),
|
|
2097
2275
|
ops: realOps(new Map(projects.map((p) => [p.name, p.path])), backups)
|
|
2098
2276
|
};
|
|
2099
2277
|
}
|
|
@@ -2125,44 +2303,99 @@ function remoteLabel(url) {
|
|
|
2125
2303
|
if (!url) return void 0;
|
|
2126
2304
|
return url.replace(/^[a-z+]+:\/\//, "").replace(/^[^@/]+@/, "").replace(/:(?!\d)/, "/").replace(/\.git$/, "");
|
|
2127
2305
|
}
|
|
2128
|
-
function
|
|
2306
|
+
function said3(c, repo) {
|
|
2129
2307
|
if (c.action === "skipped") return `${c.name}: ${c.reason ?? "skipped"}`;
|
|
2130
2308
|
if (c.action === "removed") return `Removed ${c.name} from ${repo}`;
|
|
2131
|
-
|
|
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;
|
|
2132
2312
|
}
|
|
2133
2313
|
function realOps(roots, backups) {
|
|
2134
2314
|
const rootOf = (repo) => roots.get(repo);
|
|
2135
2315
|
const find = (repo, name) => projectStatus(rootOf(repo)).find((s) => s.name === name);
|
|
2136
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;
|
|
2137
2319
|
const withLinks = (c, repo, name) => {
|
|
2138
|
-
|
|
2139
|
-
|
|
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 }]) };
|
|
2140
2325
|
};
|
|
2141
|
-
const
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
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);
|
|
2150
2381
|
const toLibrary = (path) => {
|
|
2151
2382
|
const r = importSkill(path, { force: true });
|
|
2152
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`;
|
|
2153
2384
|
};
|
|
2154
2385
|
return {
|
|
2155
|
-
add: (repo, name) =>
|
|
2386
|
+
add: (repo, name) => addTo([repo], [name]),
|
|
2387
|
+
addTo,
|
|
2156
2388
|
remove: (repo, name) => {
|
|
2157
2389
|
const s = find(repo, name);
|
|
2158
|
-
if (!s) return `${name} isn't in ${repo}
|
|
2159
|
-
|
|
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`);
|
|
2160
2393
|
if (!s.managed) return removeUntracked(rootOf(repo), name, s.path), `Removed ${name} from ${repo} (in Settings \u203A Backups)`;
|
|
2161
2394
|
return orForce(removeSkill(rootOf(repo), name), repo, "Remove it anyway", () => removeSkill(rootOf(repo), name, { force: true }));
|
|
2162
2395
|
},
|
|
2163
2396
|
restore: (repo, name) => {
|
|
2164
2397
|
const c = syncProject(rootOf(repo)).find((x) => x.name === name);
|
|
2165
|
-
return c ?
|
|
2398
|
+
return c ? result(c, repo) : `${name} is already in place`;
|
|
2166
2399
|
},
|
|
2167
2400
|
update: (repo, name) => {
|
|
2168
2401
|
const c = updateProject(rootOf(repo), [name])[0];
|
|
@@ -2180,10 +2413,10 @@ function realOps(roots, backups) {
|
|
|
2180
2413
|
return withLinks(addSkill(rootOf(repo), name, s.managed && s.version ? { version: s.version } : {}), repo, name);
|
|
2181
2414
|
},
|
|
2182
2415
|
link: (repo, name, allow = false) => {
|
|
2183
|
-
const
|
|
2184
|
-
const
|
|
2185
|
-
|
|
2186
|
-
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 }]) };
|
|
2187
2420
|
},
|
|
2188
2421
|
track: (repo, name) => withLinks(addSkill(rootOf(repo), name), repo, name),
|
|
2189
2422
|
importLocal: (repo, name) => {
|
|
@@ -2199,10 +2432,9 @@ function realOps(roots, backups) {
|
|
|
2199
2432
|
if (!plan) return `${name}: nothing to tidy`;
|
|
2200
2433
|
const r = applyTidy(plan, { git: allowGit ? "go" : "keep" });
|
|
2201
2434
|
const steps = (list) => list.map((s) => describeStep(s, root)).join(", ");
|
|
2202
|
-
|
|
2203
|
-
if (!r.held.length) return message;
|
|
2435
|
+
if (!r.held.length) return r.applied.length ? `${name}: ${steps(r.applied)}` : `${name}: nothing changed`;
|
|
2204
2436
|
return {
|
|
2205
|
-
message,
|
|
2437
|
+
message: `${name}: ${r.applied.length ? `${steps(r.applied)}; ` : ""}held back ${steps(r.held)}, because git tracks it`,
|
|
2206
2438
|
then: {
|
|
2207
2439
|
label: "Also change what git tracks",
|
|
2208
2440
|
preview: `${steps(r.held)}: these changes will show in git status in ${repo}. Replaced copies go to Settings \u203A Backups.`,
|
|
@@ -2213,52 +2445,89 @@ function realOps(roots, backups) {
|
|
|
2213
2445
|
unloadGlobal: (m) => {
|
|
2214
2446
|
if (!m.broken && !latestVersion(m.name)) importSkill(m.path);
|
|
2215
2447
|
const r = unloadGlobal(m.path, m.links);
|
|
2216
|
-
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}`);
|
|
2217
2449
|
},
|
|
2218
2450
|
deleteGlobal: (m) => {
|
|
2219
2451
|
const r = deleteGlobal(m.path, m.links);
|
|
2220
|
-
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}`);
|
|
2221
2453
|
},
|
|
2222
2454
|
keepGlobal: (name, keep) => (setKeepGlobal([name], keep), keep ? `${name} marked as global on purpose` : `${name} is no longer marked as global on purpose`),
|
|
2223
2455
|
linkGlobal: (m, agents) => {
|
|
2224
2456
|
const r = linkGlobal(m.path, agents);
|
|
2225
|
-
const linked = r.linked.map(
|
|
2226
|
-
const skipped = r.skipped.length ? `; ${r.skipped.map(
|
|
2227
|
-
|
|
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;
|
|
2228
2461
|
},
|
|
2229
|
-
moveGlobal: (
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
const
|
|
2233
|
-
const
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
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;
|
|
2240
2510
|
},
|
|
2241
2511
|
createSkill: (name) => {
|
|
2242
2512
|
const r = createSkill(name, "");
|
|
2243
|
-
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}`);
|
|
2244
2514
|
},
|
|
2245
2515
|
pluginOffHere: (repo, id) => {
|
|
2246
2516
|
const root = rootOf(repo);
|
|
2247
2517
|
const plugin = claudePlugins(skillsLoadedIn(root, machineSkills()), root).find((p) => p.id === id);
|
|
2248
|
-
if (!plugin) return `${id} isn't on in ${repo}
|
|
2518
|
+
if (!plugin) return failed(`${id} isn't on in ${repo}`);
|
|
2249
2519
|
const r = turnOffIn(plugin, root);
|
|
2250
|
-
|
|
2251
|
-
return r.message;
|
|
2520
|
+
return r.ok ? r.message : failed(r.message);
|
|
2252
2521
|
},
|
|
2253
2522
|
replacePlugin: (id, repos) => {
|
|
2254
2523
|
const skills = machineSkills().filter((s) => s.kind === "plugin" && s.origin === id && !s.broken);
|
|
2255
2524
|
const clashes = skills.filter((s) => importSkill(s.path).status === "exists").map((s) => s.name);
|
|
2256
|
-
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`);
|
|
2257
2526
|
const skipped = repos.flatMap((r) => skills.flatMap((s) => {
|
|
2258
2527
|
const c = addSkill(rootOf(r), s.name);
|
|
2259
2528
|
return c.action === "skipped" ? [`${s.name} in ${r} (${c.reason})`] : [];
|
|
2260
2529
|
}));
|
|
2261
|
-
if (skipped.length) return `${id} kept: not added ${skipped.join(", ")}
|
|
2530
|
+
if (skipped.length) return failed(`${id} kept: not added ${skipped.join(", ")}`);
|
|
2262
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(", ")}` : ""}`;
|
|
2263
2532
|
const bin = claudeBinary();
|
|
2264
2533
|
const claude = (...args) => spawnSync3(bin, ["plugin", ...args], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 2e4 });
|
|
@@ -2274,7 +2543,7 @@ function realOps(roots, backups) {
|
|
|
2274
2543
|
try {
|
|
2275
2544
|
setPluginEnabled(id, false);
|
|
2276
2545
|
} catch (e) {
|
|
2277
|
-
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`);
|
|
2278
2547
|
}
|
|
2279
2548
|
return `${copied}; turned ${id} off in ~/.claude/settings.json (${why})${synced ? "" : `: finish with /plugin uninstall ${id}`}`;
|
|
2280
2549
|
},
|
|
@@ -2287,13 +2556,13 @@ function realOps(roots, backups) {
|
|
|
2287
2556
|
from.push([...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root));
|
|
2288
2557
|
}
|
|
2289
2558
|
const r = deleteLibrarySkill(name);
|
|
2290
|
-
if (!r.ok) return `${name}: ${r.reason}
|
|
2559
|
+
if (!r.ok) return failed(`${name}: ${r.reason}`);
|
|
2291
2560
|
return `Deleted ${name} from your library${from.length ? ` and from ${from.join(", ")}` : ""} (in Settings \u203A Backups)`;
|
|
2292
2561
|
},
|
|
2293
2562
|
restoreBackup: (i) => {
|
|
2294
2563
|
const b = backups[i];
|
|
2295
2564
|
const r = b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b);
|
|
2296
|
-
return r.ok ? `Restored ${b.name} to ${
|
|
2565
|
+
return r.ok ? `Restored ${b.name} to ${tildify4(r.to)}` : failed(`${b.name}: ${r.reason}`);
|
|
2297
2566
|
},
|
|
2298
2567
|
setAgents: (ids) => (setHarnesses(ids), "Saved your agents"),
|
|
2299
2568
|
installVersion: (repo, name, version) => withLinks(addSkill(rootOf(repo), name, { version, force: true }), repo, name),
|
|
@@ -2302,20 +2571,28 @@ function realOps(roots, backups) {
|
|
|
2302
2571
|
addRoot: (path) => {
|
|
2303
2572
|
const root = addRoot(path);
|
|
2304
2573
|
discoverProjects();
|
|
2305
|
-
return `Scanning ${
|
|
2574
|
+
return `Scanning ${tildify4(root)} for repos`;
|
|
2306
2575
|
},
|
|
2307
|
-
removeRoot: (path) => (removeRoot(path), `Stopped scanning ${
|
|
2576
|
+
removeRoot: (path) => (removeRoot(path), `Stopped scanning ${tildify4(path)}; repos already found stay until you hide them`),
|
|
2308
2577
|
rescan: () => (discoverProjects(), "Looked for repos in your project folders again"),
|
|
2309
2578
|
hide: (repo) => (setHidden(rootOf(repo), true), `Hid ${repo}: Settings \u203A Hidden repos brings it back`),
|
|
2310
|
-
unhide: (path) => (setHidden(path, false), `${
|
|
2579
|
+
unhide: (path) => (setHidden(path, false), `${tildify4(path)} is back in the list`),
|
|
2311
2580
|
openFolder: (path) => {
|
|
2312
2581
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
2313
2582
|
spawn(cmd, [path], { detached: true, stdio: "ignore" }).unref();
|
|
2314
|
-
return `Opened ${
|
|
2583
|
+
return `Opened ${tildify4(path)}`;
|
|
2315
2584
|
},
|
|
2316
2585
|
copy: (text, what) => {
|
|
2317
2586
|
const r = copyText(text);
|
|
2318
|
-
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");
|
|
2319
2596
|
},
|
|
2320
2597
|
repoInfo: (repo) => {
|
|
2321
2598
|
if (!info.has(repo)) {
|