skilllib 1.11.3 → 1.11.5
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 +23 -20
- package/dist/{chunk-3XDWEHI4.js → chunk-TE2EDZ7L.js} +1006 -373
- package/dist/index.js +8 -5
- package/dist/json.d.cts +20 -8
- package/dist/json.d.ts +20 -8
- package/dist/{tui-WSHQRBPF.js → tui-GFQUHPEC.js} +219 -136
- package/package.json +2 -2
- package/skills/skilllib/SKILL.md +9 -6
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
HARNESSES,
|
|
3
3
|
PROJECT_SKILLS_DIR,
|
|
4
|
+
addRefusal,
|
|
4
5
|
addRoot,
|
|
5
6
|
addSkill,
|
|
6
7
|
agentSkillState,
|
|
7
8
|
allowTrackedLinks,
|
|
9
|
+
blindSaid,
|
|
10
|
+
blindWithoutGlobal,
|
|
8
11
|
claudeBinary,
|
|
9
12
|
createSkill,
|
|
10
13
|
cursorPlugins,
|
|
@@ -12,6 +15,7 @@ import {
|
|
|
12
15
|
deleteLibrarySkill,
|
|
13
16
|
discoverProjects,
|
|
14
17
|
enabledHarnesses,
|
|
18
|
+
entryExists,
|
|
15
19
|
findIssues,
|
|
16
20
|
foldersOf,
|
|
17
21
|
forgetClaudeBinary,
|
|
@@ -20,9 +24,14 @@ import {
|
|
|
20
24
|
harness,
|
|
21
25
|
harnessesChosen,
|
|
22
26
|
importSkill,
|
|
27
|
+
inTheWaySaid,
|
|
23
28
|
installAgentSkill,
|
|
29
|
+
installDirs,
|
|
24
30
|
installedPlugins,
|
|
31
|
+
isDir,
|
|
32
|
+
isShared,
|
|
25
33
|
keptGlobal,
|
|
34
|
+
knownProjects,
|
|
26
35
|
latestVersion,
|
|
27
36
|
libraryOrigins,
|
|
28
37
|
librarySkillDir,
|
|
@@ -32,6 +41,8 @@ import {
|
|
|
32
41
|
localStamp,
|
|
33
42
|
machineSkills,
|
|
34
43
|
moveGlobalSkills,
|
|
44
|
+
movedTogether,
|
|
45
|
+
notWritable,
|
|
35
46
|
pluginBackups,
|
|
36
47
|
projectHere,
|
|
37
48
|
projectOfFactory,
|
|
@@ -41,9 +52,11 @@ import {
|
|
|
41
52
|
readManifest,
|
|
42
53
|
readSkillInfo,
|
|
43
54
|
readableRoots,
|
|
55
|
+
realPath,
|
|
44
56
|
recordOrigin,
|
|
45
57
|
relativeTo,
|
|
46
58
|
relinkDependency,
|
|
59
|
+
rememberProjects,
|
|
47
60
|
removeAgentSkill,
|
|
48
61
|
removeRoot,
|
|
49
62
|
removeSkill,
|
|
@@ -56,18 +69,22 @@ import {
|
|
|
56
69
|
setHidden,
|
|
57
70
|
setKeepGlobal,
|
|
58
71
|
setPluginOn,
|
|
72
|
+
skillOf,
|
|
59
73
|
skilllibHome,
|
|
60
74
|
skillsLoadedIn,
|
|
75
|
+
strandedLinks,
|
|
76
|
+
survives,
|
|
61
77
|
tildify,
|
|
78
|
+
trackBlocker,
|
|
62
79
|
treeHash,
|
|
63
80
|
uninstallPlugin,
|
|
64
81
|
updatePlugin,
|
|
65
82
|
updateProject,
|
|
66
83
|
usageIssues,
|
|
67
84
|
versionHistory,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
} from "./chunk-
|
|
85
|
+
visibleProjects,
|
|
86
|
+
withoutForceHint
|
|
87
|
+
} from "./chunk-TE2EDZ7L.js";
|
|
71
88
|
|
|
72
89
|
// src/tui/index.tsx
|
|
73
90
|
import { render } from "ink";
|
|
@@ -186,6 +203,9 @@ function wrap(text, width, max) {
|
|
|
186
203
|
return lines;
|
|
187
204
|
}
|
|
188
205
|
|
|
206
|
+
// src/tui/prompts.ts
|
|
207
|
+
import { dirname } from "path";
|
|
208
|
+
|
|
189
209
|
// src/review.ts
|
|
190
210
|
import { spawnSync } from "child_process";
|
|
191
211
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -194,7 +214,7 @@ var EXCERPT_CHARS = 1200;
|
|
|
194
214
|
function skillExcerpt(dir, chars) {
|
|
195
215
|
const file = join(dir, "SKILL.md");
|
|
196
216
|
if (!existsSync(file)) return { text: "(SKILL.md not found)", files: 0, size: 0 };
|
|
197
|
-
const content = readFileSync(file, "utf-8").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
|
|
217
|
+
const content = readFileSync(file, "utf-8").replace(/^\uFEFF/, "").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
|
|
198
218
|
let files = 0;
|
|
199
219
|
try {
|
|
200
220
|
files = readdirSync(dir, { recursive: true, withFileTypes: true }).filter((d) => d.isFile()).length;
|
|
@@ -296,8 +316,9 @@ var isFailure = (r) => typeof r !== "string" && "failed" in r;
|
|
|
296
316
|
var followUp = (r) => typeof r !== "string" && "then" in r ? r.then : void 0;
|
|
297
317
|
var said = (r) => typeof r === "string" ? r : r.message;
|
|
298
318
|
var joined = (results) => {
|
|
299
|
-
const
|
|
300
|
-
|
|
319
|
+
const bad = results.filter(isFailure);
|
|
320
|
+
const message = [...bad.length && bad.length < results.length ? [`${results.length - bad.length} of ${results.length} done`] : [], ...results.map(said)].join(" \xB7 ");
|
|
321
|
+
return bad.length ? failed(message) : message;
|
|
301
322
|
};
|
|
302
323
|
var SEVERITY_RANK = { problem: 0, suggestion: 1 };
|
|
303
324
|
function fixOf(c, found) {
|
|
@@ -325,6 +346,7 @@ function shortOf(found) {
|
|
|
325
346
|
sync: "Folder missing",
|
|
326
347
|
unavailable: "Pinned, not in your library",
|
|
327
348
|
pin: "Pinned version not in your library",
|
|
349
|
+
"out-of-sync": "Not the pinned version",
|
|
328
350
|
update: "Out of date",
|
|
329
351
|
uncommitted: "Commit it first",
|
|
330
352
|
local: "Not in your library",
|
|
@@ -353,7 +375,7 @@ function viewOf(found) {
|
|
|
353
375
|
title: found.title,
|
|
354
376
|
short: shortOf(found),
|
|
355
377
|
detail: found.detail,
|
|
356
|
-
skills: [...new Set(found.at?.
|
|
378
|
+
skills: [...new Set(found.at?.flatMap((a) => a.skill ? [a.skill] : []))],
|
|
357
379
|
decision: !found.fix,
|
|
358
380
|
fixes: (found.fix ? [found.fix] : found.choices ?? []).map((c) => fixOf(c, found))
|
|
359
381
|
};
|
|
@@ -364,6 +386,9 @@ function filterOf(source) {
|
|
|
364
386
|
function project(w, name) {
|
|
365
387
|
return w.projects.find((p) => p.name === name);
|
|
366
388
|
}
|
|
389
|
+
function usesKey(m) {
|
|
390
|
+
return m.source === "plugin" ? `${m.where.split("@")[0]}:${m.name}` : m.name;
|
|
391
|
+
}
|
|
367
392
|
function totalUses(w, name) {
|
|
368
393
|
return Object.values(w.usage).reduce((n, u) => n + (u[name] ?? 0), 0);
|
|
369
394
|
}
|
|
@@ -383,7 +408,7 @@ function usable(w, projectName) {
|
|
|
383
408
|
const uses = (n) => w.usage[projectName]?.[n] ?? 0;
|
|
384
409
|
return [
|
|
385
410
|
...p.skills.map((s) => ({ name: s.name, source: s.source, where: s.dir, agents: s.agents, uses: uses(s.name), local: s })),
|
|
386
|
-
...machineIn(w, p).map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: uses(m
|
|
411
|
+
...machineIn(w, p).map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: uses(usesKey(m)), machine: m }))
|
|
387
412
|
];
|
|
388
413
|
}
|
|
389
414
|
function issuesAt(w, repo, skill) {
|
|
@@ -397,7 +422,7 @@ function repoIssues(w, repo) {
|
|
|
397
422
|
function unlisted(w, repo) {
|
|
398
423
|
const root = project(w, repo).path;
|
|
399
424
|
const listed = new Set(usable(w, repo).map((u) => u.name));
|
|
400
|
-
return [...new Set(w.issues.flatMap((i) => (i.at ?? []).filter((a) => a.root === root && !listed.has(a.skill)).map((a) => a.skill)))];
|
|
425
|
+
return [...new Set(w.issues.flatMap((i) => (i.at ?? []).filter((a) => a.root === root && a.skill && !listed.has(a.skill)).map((a) => a.skill)))];
|
|
401
426
|
}
|
|
402
427
|
function libraryIssues(w, name) {
|
|
403
428
|
return w.issues.filter((i) => i.id === "unused:library" && i.at?.some((a) => a.skill === name)).map(issueOf);
|
|
@@ -410,12 +435,12 @@ function globalFixes(m) {
|
|
|
410
435
|
return [
|
|
411
436
|
{
|
|
412
437
|
label: "Move it to the repos that need it\u2026",
|
|
413
|
-
preview: `Pick repos. ${m.name} goes into your library and those repos, and stops loading globally (
|
|
438
|
+
preview: `Pick repos. ${m.name} goes into your library and those repos, and stops loading globally (every global copy backed up).`,
|
|
414
439
|
run: () => "",
|
|
415
440
|
pickRepos: (w, repos) => w.ops.moveGlobal([m], repos)
|
|
416
441
|
},
|
|
417
442
|
m.kept ? { label: "Stop marking it as global on purpose", preview: `${m.name} shows as Not reviewed again, like any global skill.`, run: (w) => w.ops.keepGlobal(m.name, false) } : { label: "Keep it global on purpose", preview: `Mark ${m.name} as global on purpose: it no longer shows as Not reviewed. Its other issues stay.`, run: (w) => w.ops.keepGlobal(m.name, true) },
|
|
418
|
-
{ label: "Delete it", preview: `Remove ${m.
|
|
443
|
+
{ label: "Delete it", preview: `Remove ${m.where}/${m.name}. A backup is kept in Settings \u203A Backups.`, run: (w) => w.ops.deleteGlobal(m) }
|
|
419
444
|
];
|
|
420
445
|
}
|
|
421
446
|
function machineActions(m) {
|
|
@@ -466,7 +491,7 @@ function deleteLibraryFix(w, names) {
|
|
|
466
491
|
`Delete ${what} and ${names.length === 1 ? "its" : "their"} versions from your library.`,
|
|
467
492
|
repos.length ? `Removes ${names.length === 1 ? "it" : "them"} from ${repos.length === 1 ? repos[0] : `these ${repos.length} repos: ${repos.join(", ")}`}.` : "No repo uses it.",
|
|
468
493
|
edited.length ? `${edited.join(", ")} ${edited.length === 1 ? "has" : "have"} local edits: backed up too.` : "",
|
|
469
|
-
"Every copy goes to Settings \u203A Backups.",
|
|
494
|
+
"Every copy goes to Settings \u203A Backups; where a repo commits its copy, git will see it.",
|
|
470
495
|
others.length ? `Untracked or the team's own copies in ${others.join(", ")} stay.` : ""
|
|
471
496
|
].filter(Boolean).join(" "),
|
|
472
497
|
run: (w2) => joined(names.map((n) => w2.ops.deleteLibrary(n)))
|
|
@@ -495,7 +520,8 @@ function replacePluginFix(p) {
|
|
|
495
520
|
`Copies its ${plural(names.length, "skill")} into your library and adds them to the repos you tick (or none),`,
|
|
496
521
|
synced ? `then turns ${p.id} off in Claude Code. It's synced from your claude.ai account: to delete it for good, remove it there.` : p.settingsOnly ? `then turns ${p.id} off in ~/.claude/settings.json. It isn't installed, only turned on there, so there's nothing to uninstall.` : `then uninstalls ${p.id}${p.repo ? ` from ${p.repo}` : ""} (its saved data is kept).`,
|
|
497
522
|
p.parts.length ? `It also brings ${p.parts.join(", ")}: those ${offOnly ? "stop" : "go"} too.` : "",
|
|
498
|
-
|
|
523
|
+
// A plugin only turned on in settings isn't listed in Plugins once it's off: say where it comes back.
|
|
524
|
+
offOnly ? p.settingsOnly ? "Turn it back on with /plugin in Claude Code." : "Turn it back on from Plugins." : "Settings \u203A Backups reinstalls it."
|
|
499
525
|
].filter(Boolean).join(" "),
|
|
500
526
|
run: () => "",
|
|
501
527
|
allowNone: true,
|
|
@@ -638,14 +664,16 @@ var agentNames = (agents) => agents.map((a) => harness(a).name).join(", ") || "n
|
|
|
638
664
|
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)";
|
|
639
665
|
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)");
|
|
640
666
|
function reviewPrompt(w, names, scope, repo) {
|
|
667
|
+
const project2 = repo ? w.projects.find((p) => p.name === repo) : void 0;
|
|
668
|
+
const machine = project2 ? machineIn(w, project2) : w.machine;
|
|
641
669
|
const skills = names.map((name) => {
|
|
642
|
-
const copies =
|
|
643
|
-
const here =
|
|
670
|
+
const copies = machine.filter((m2) => m2.name === name);
|
|
671
|
+
const here = project2?.skills.find((s) => s.name === name);
|
|
644
672
|
if (here) {
|
|
645
673
|
const lib2 = w.library.find((l) => l.name === name);
|
|
646
674
|
return {
|
|
647
675
|
name,
|
|
648
|
-
description: w.descriptions[name] ?? "",
|
|
676
|
+
description: here.description ?? w.descriptions[name] ?? "",
|
|
649
677
|
source: localSource(here),
|
|
650
678
|
installedHow: localInstalled(here),
|
|
651
679
|
path: here.path,
|
|
@@ -669,12 +697,13 @@ function reviewPrompt(w, names, scope, repo) {
|
|
|
669
697
|
description: w.descriptions[name] ?? "",
|
|
670
698
|
source: m ? m.source === "global" ? "my global skills" : `vendor: ${m.source} (${m.where})` : lib ? "Your skills (skilllib library)" : local ? localSource(local) : "a repo's own skill",
|
|
671
699
|
installedHow: m?.origin ?? (m ? m.where : lib ? "created or imported into skilllib" : local ? localInstalled(local) : "committed in the repo"),
|
|
672
|
-
path: m?.path ?? (lib ? w.ops.libraryFile(name)
|
|
700
|
+
path: m?.path ?? (lib ? dirname(w.ops.libraryFile(name)) : local?.path ?? ""),
|
|
673
701
|
links: m?.links ?? [],
|
|
674
|
-
loadedBy: m ? `${agentNames(m.agents)} \u2014 in every repo` : repos.map((p) => `${agentNames(p.skills.find((s) => s.name === name).agents)} \u2014 in ${p.name}`).join("; ") || "none of my agents",
|
|
702
|
+
loadedBy: m ? `${agentNames(m.agents)} \u2014 ${w.machine.some((g) => g.path === m.path) ? "in every repo" : `in ${repo}`}` : repos.map((p) => `${agentNames(p.skills.find((s) => s.name === name).agents)} \u2014 in ${p.name}`).join("; ") || "none of my agents",
|
|
675
703
|
vendor: m && m.source !== "global" ? "turn it off at its source (/plugin in Claude Code, claude.ai settings, or Cursor)" : null,
|
|
676
|
-
|
|
677
|
-
|
|
704
|
+
// A plugin's copy is counted under plugin:skill.
|
|
705
|
+
usesTotal: w.days ? totalUses(w, m ? usesKey(m) : name) : null,
|
|
706
|
+
usesByProject: Object.entries(w.usage).flatMap(([p, u]) => /* @__PURE__ */ ((n) => n ? [[p, n]] : [])(u[m ? usesKey(m) : name])),
|
|
678
707
|
installedIn: repos.map((p) => p.name),
|
|
679
708
|
otherCopies: copies.filter((c) => c !== m).map((c) => `${c.source} ${c.where}`),
|
|
680
709
|
inYourSkills: lib ? `yes, v${lib.latest}` : null,
|
|
@@ -690,7 +719,7 @@ function reviewPrompt(w, names, scope, repo) {
|
|
|
690
719
|
}
|
|
691
720
|
function writeSkillPrompt(w, name, repo) {
|
|
692
721
|
const p = repo ? w.projects.find((x) => x.name === repo) : void 0;
|
|
693
|
-
const file = p ? `${p.path}
|
|
722
|
+
const file = p ? `${p.path}/${installDirs(w.agents)[0]}/${name || "<name>"}/SKILL.md` : w.ops.libraryFile(name || "<name>");
|
|
694
723
|
return `Write a new agent skill${name ? ` named "${name}"` : ""}${p ? ` for the repo at ${p.path}` : " for my skill library"}.
|
|
695
724
|
|
|
696
725
|
Create ${file} with:
|
|
@@ -739,7 +768,7 @@ function tag(u) {
|
|
|
739
768
|
return [u.source, color.muted];
|
|
740
769
|
}
|
|
741
770
|
function isNameChar(input, key) {
|
|
742
|
-
return !key.ctrl && !key.meta && /^[a-zA-Z0-9._-]
|
|
771
|
+
return !key.ctrl && !key.meta && /^[a-zA-Z0-9._-]+$/.test(input);
|
|
743
772
|
}
|
|
744
773
|
function clip(text, width) {
|
|
745
774
|
return text.length > width - 1 ? text.slice(0, width - 2) + "\u2026" : text;
|
|
@@ -755,7 +784,7 @@ function builder(title) {
|
|
|
755
784
|
issues(issues) {
|
|
756
785
|
if (!issues.length) return d.body.push({ segs: [["\u2713 No issues", color.green]] });
|
|
757
786
|
for (const issue of [...issues].sort(bySeverity)) {
|
|
758
|
-
d.body.push({ segs: [[`${SEV[issue.severity].icon} `, SEV[issue.severity].color], [issue.title, color.text], [issue.decision ? " \xB7 your call" : "", color.faint]] });
|
|
787
|
+
d.body.push({ segs: [[`${SEV[issue.severity].icon} `, SEV[issue.severity].color], [issue.title, color.text], [issue.decision && issue.fixes.length ? " \xB7 your call" : "", color.faint]] });
|
|
759
788
|
issue.fixes.forEach((fix, i) => d.body.push({ segs: [], option: d.options.push({ label: fix.label, fix, issue, recommended: !issue.decision && i === 0 }) - 1 }));
|
|
760
789
|
}
|
|
761
790
|
return 0;
|
|
@@ -848,13 +877,14 @@ function skillItem(w, copies, issues, nameW, actions, repo) {
|
|
|
848
877
|
],
|
|
849
878
|
detail: (width) => {
|
|
850
879
|
const b = builder(u.name);
|
|
851
|
-
for (const l of wrap(w.descriptions[u.name] || "(no description)", width - 4, 3)) b.line([l, color.muted]);
|
|
880
|
+
for (const l of wrap(u.local?.description || w.descriptions[u.name] || "(no description)", width - 4, 3)) b.line([l, color.muted]);
|
|
852
881
|
b.line();
|
|
853
|
-
|
|
882
|
+
const here = !!u.machine && !w.machine.some((m) => m.path === u.machine.path && m.where === u.machine.where);
|
|
883
|
+
if (repo && !u.local) b.line([here ? "Loads in this repo (its settings turn it on)" : "Loads in every repo", color.text], [u.source === "global" || u.source === "skilllib" ? " issues and cleanup live in Global" : " managed at its source", color.faint]);
|
|
854
884
|
else b.issues(issues);
|
|
855
885
|
b.actions(actions, width);
|
|
856
886
|
b.line();
|
|
857
|
-
for (const segs of usageLines(w, u.name, width, repo)) b.line(...segs);
|
|
887
|
+
for (const segs of usageLines(w, u.machine ? usesKey(u.machine) : u.name, width, repo)) b.line(...segs);
|
|
858
888
|
b.line();
|
|
859
889
|
b.line(["Loaded from", color.accent]);
|
|
860
890
|
for (const c of copies) {
|
|
@@ -904,12 +934,13 @@ var reviewOption = (w, name, ui, repo) => ({
|
|
|
904
934
|
label: "Review prompt",
|
|
905
935
|
action: () => ui.copy(reviewPrompt(w, [name], `Review the skill ${name}${repo ? ` in ${repo}` : ""}.`, repo), `a review prompt for ${name}`)
|
|
906
936
|
});
|
|
937
|
+
var gitSees = (s) => isShared(s?.git) ? " Git will see it." : "";
|
|
907
938
|
function versionsOption(w, repo, name, installed, ui) {
|
|
908
939
|
return {
|
|
909
940
|
label: "Other versions\u2026",
|
|
910
941
|
action: () => ui.menu(
|
|
911
942
|
`${name}: versions`,
|
|
912
|
-
w.ops.versions(name).reverse().map((v) => fixOption({ label: `v${v.version} ${v.date}${v.version === installed ? " (installed)" : ""}`, preview: `Install ${name} v${v.version} in ${repo}
|
|
943
|
+
w.ops.versions(name).reverse().map((v) => fixOption({ label: `v${v.version} ${v.date}${v.version === installed ? " (installed)" : ""}`, preview: `Install ${name} v${v.version} in ${repo}.${gitSees(project(w, repo).skills.find((x) => x.name === name))}`, run: (w2) => w2.ops.installVersion(repo, name, v.version) }))
|
|
913
944
|
)
|
|
914
945
|
};
|
|
915
946
|
}
|
|
@@ -919,14 +950,15 @@ function skillActions(w, p, u, ui) {
|
|
|
919
950
|
const remove = {
|
|
920
951
|
// Its folder gone: all that's left to remove is the skilllib.json entry (the label Health offers too).
|
|
921
952
|
label: s?.missing ? "Remove it from skilllib.json" : "Remove from repo",
|
|
922
|
-
preview: (s?.missing ? `${p}'s skilllib.json stops pinning ${u.name}. There's no folder to delete.` : s?.source === "lib" ? `Delete ${s.dir}/${u.name} in ${p}.${inLibrary ? " Your library keeps it." : ""}` : `Move ${s?.dir}/${u.name} in ${p} to Settings \u203A Backups.`) + (s?.others ? ` The copy in ${s.others.join(", ")} stays.` : "")
|
|
953
|
+
preview: (s?.missing ? `${p}'s skilllib.json stops pinning ${u.name}. There's no folder to delete.` : s?.source === "lib" ? `Delete ${s.dir}/${u.name} in ${p}.${inLibrary ? " Your library keeps it." : ""}` : `Move ${s?.dir}/${u.name} in ${p} to Settings \u203A Backups.`) + (s?.others ? ` The copy in ${s.others.join(", ")} stays.` : "") + // A managed skill's removal edits skilllib.json too.
|
|
954
|
+
(gitSees(s) || (s?.source === "lib" && project(w, p).manifest === "committed" ? " Git will see skilllib.json change." : "")),
|
|
923
955
|
run: (w2) => w2.ops.remove(p, u.name)
|
|
924
956
|
};
|
|
925
957
|
if (s?.source === "lib")
|
|
926
958
|
return [
|
|
927
959
|
// A tracked skill your library doesn't have: nothing to edit there, no versions to pick.
|
|
928
960
|
...inLibrary ? [
|
|
929
|
-
...s.version < inLibrary.latest ? [fixOption({ label: `Update to v${inLibrary.latest}`, preview: `Replace ${s.dir}/${u.name} with library v${inLibrary.latest}
|
|
961
|
+
...s.version < inLibrary.latest ? [fixOption({ label: `Update to v${inLibrary.latest}`, preview: `Replace ${s.dir}/${u.name} with library v${inLibrary.latest}.${gitSees(s)}`, run: (w2) => w2.ops.update(p, u.name) })] : [],
|
|
930
962
|
{ label: "Edit in library", action: () => ui.edit(w.ops.libraryFile(u.name)) },
|
|
931
963
|
versionsOption(w, p, u.name, s.version, ui)
|
|
932
964
|
] : [],
|
|
@@ -945,7 +977,8 @@ function skillActions(w, p, u, ui) {
|
|
|
945
977
|
fixOption(remove),
|
|
946
978
|
reviewOption(w, u.name, ui, p)
|
|
947
979
|
];
|
|
948
|
-
|
|
980
|
+
const everywhere = !!u.machine && w.machine.some((m) => m.path === u.machine.path && m.where === u.machine.where);
|
|
981
|
+
return everywhere ? [{ label: "Open in Global", action: () => ui.openInGlobal(u.name) }, reviewOption(w, u.name, ui)] : [reviewOption(w, u.name, ui, p)];
|
|
949
982
|
}
|
|
950
983
|
function overview(w, p) {
|
|
951
984
|
const project2 = project(w, p);
|
|
@@ -992,7 +1025,7 @@ function projectItems(w) {
|
|
|
992
1025
|
});
|
|
993
1026
|
}
|
|
994
1027
|
function globalItems(w, nameW, ui) {
|
|
995
|
-
const copies = w.machine.map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: totalUses(w, m
|
|
1028
|
+
const copies = w.machine.map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: totalUses(w, usesKey(m)), machine: m }));
|
|
996
1029
|
return byName(copies).map((g) => skillItem(w, g, issuesAt(w, null, g[0].name), nameW, globalActions(w, g[0], ui), null));
|
|
997
1030
|
}
|
|
998
1031
|
var AT_SOURCE = {
|
|
@@ -1055,7 +1088,7 @@ function libraryItems(w, nameW, ui) {
|
|
|
1055
1088
|
...removable.length ? [
|
|
1056
1089
|
fixOption({
|
|
1057
1090
|
label: "Remove from repos\u2026",
|
|
1058
|
-
preview: `Pick repos to remove ${l.name} from. Your library keeps it.`,
|
|
1091
|
+
preview: `Pick repos to remove ${l.name} from. Your library keeps it. Where a repo commits its copy, git will see it.`,
|
|
1059
1092
|
run: () => "",
|
|
1060
1093
|
candidates: () => removable.map((p) => p.name),
|
|
1061
1094
|
pickRepos: (w2, rs) => joined(rs.map((r) => w2.ops.remove(r, l.name)))
|
|
@@ -1149,7 +1182,7 @@ function pluginItems(w, columns, skillW, ui, issuesOf) {
|
|
|
1149
1182
|
const skills = p.skills.map((s) => cursor || !w.days ? s.name : `${s.name} (${uses2(totalUses(w, pluginSkillKey(p, s.name)))})`);
|
|
1150
1183
|
wrap(skills.join(" \xB7 ") || "none", width - 16, 4).forEach((l, i) => b.line(label(i ? "" : "Skills"), [l, color.text]));
|
|
1151
1184
|
b.line(label("Also brings"), [p.parts.join(", ") || "nothing else", p.parts.length ? color.text : color.faint]);
|
|
1152
|
-
const installed = cursor ? "in Cursor (whether it's on is only in Cursor)" : p.scope === "claude.ai" ? "synced from your claude.ai account" : p.repo ? `${p.scope} scope, in ${p.repo} only` : `user scope, in every repo`;
|
|
1185
|
+
const installed = cursor ? "in Cursor (whether it's on is only in Cursor)" : p.scope === "claude.ai" ? "synced from your claude.ai account" : p.settingsOnly ? "not installed: turned on in your settings, loaded from its marketplace's copy" : p.repo ? `${p.scope} scope, in ${p.repo} only` : `user scope, in every repo`;
|
|
1153
1186
|
b.line(label("Installed"), [installed, color.text], [p.version ? ` v${p.version.replace(/^v/, "")}` : "", color.muted], [p.id.includes("@") && p.scope !== "claude.ai" ? ` from ${p.id.split("@")[1]}` : "", color.faint]);
|
|
1154
1187
|
b.line(label("Folder"), [tildify(p.path), color.faint]);
|
|
1155
1188
|
return b.d;
|
|
@@ -1174,7 +1207,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1174
1207
|
const world = { ...withUsage, issues: [...base.issues, ...usageIssues2] };
|
|
1175
1208
|
useEffect(() => {
|
|
1176
1209
|
let live = true;
|
|
1177
|
-
void loadUsage2?.(base).then((u) => live && setUsed(u));
|
|
1210
|
+
void loadUsage2?.(base).then((u) => live && setUsed(u)).catch(() => live && setToast("\u2717 Couldn't read your Claude Code transcripts: usage isn't shown"));
|
|
1178
1211
|
return () => {
|
|
1179
1212
|
live = false;
|
|
1180
1213
|
};
|
|
@@ -1253,7 +1286,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1253
1286
|
}
|
|
1254
1287
|
function choose(o) {
|
|
1255
1288
|
if (o.action) return o.action();
|
|
1256
|
-
if (!o.fix) return
|
|
1289
|
+
if (!o.fix) return;
|
|
1257
1290
|
if (o.fix.pickRepos) {
|
|
1258
1291
|
const name = current?.name ?? "";
|
|
1259
1292
|
const only = o.fix.candidates?.(world);
|
|
@@ -1319,7 +1352,8 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1319
1352
|
cells: [
|
|
1320
1353
|
{ text: top ? SEV[top.severity].icon : " ", width: 2, color: top ? SEV[top.severity].color : void 0 },
|
|
1321
1354
|
{ text: `${isOpen ? "\u25BE" : "\u25B8"} ${label} (${members.length})`, grow: true, color: color.accent },
|
|
1322
|
-
|
|
1355
|
+
// A leading space keeps it apart from a name cut to fit.
|
|
1356
|
+
{ text: flagged ? ` ${flagged} to fix` : "", width: 11, color: top ? SEV[top.severity].color : color.faint }
|
|
1323
1357
|
],
|
|
1324
1358
|
detail: (width) => {
|
|
1325
1359
|
const b = builder(label);
|
|
@@ -1398,7 +1432,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1398
1432
|
() => ui.menu(repo, [
|
|
1399
1433
|
{ label: "Open its folder", action: () => setToast(said2(world.ops.openFolder(project(world, repo).path))) },
|
|
1400
1434
|
{ label: "Hide it from the list", action: () => (apply((w) => w.ops.hide(repo)), setOpen(null), setFocus(sidebar ? "sidebar" : "list")) },
|
|
1401
|
-
...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}`) }] : []
|
|
1435
|
+
...skills.length ? [{ label: `Review prompt for its ${skills.length === 1 ? "skill" : `${skills.length} skills`}`, action: () => ui.copy(reviewPrompt(world, skills, `Review the skills in ${repo}.`, repo), `a review prompt for ${repo}`) }] : []
|
|
1402
1436
|
])
|
|
1403
1437
|
)
|
|
1404
1438
|
];
|
|
@@ -1446,8 +1480,16 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1446
1480
|
setToast(`Saved ${tildify(file)}`);
|
|
1447
1481
|
});
|
|
1448
1482
|
}
|
|
1449
|
-
function askForFolder(title) {
|
|
1450
|
-
|
|
1483
|
+
function askForFolder(title, problem) {
|
|
1484
|
+
const submit = (v) => apply((w) => {
|
|
1485
|
+
try {
|
|
1486
|
+
return w.ops.addRoot(v);
|
|
1487
|
+
} catch (e) {
|
|
1488
|
+
askForFolder(title, e.message);
|
|
1489
|
+
throw e;
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
setModal({ kind: "input", title: problem ? `${problem}. ${title}` : title, value: "", placeholder: "~/Projects", submit });
|
|
1451
1493
|
}
|
|
1452
1494
|
useEffect(() => {
|
|
1453
1495
|
const folder = () => !initial.roots.length && askForFolder("Where are your projects? skilllib finds the git repos in there");
|
|
@@ -1701,7 +1743,8 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1701
1743
|
if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
|
|
1702
1744
|
}
|
|
1703
1745
|
}
|
|
1704
|
-
const
|
|
1746
|
+
const toastLines = toast && modal?.kind !== "confirm" ? wrap(toast, termCols - 2, 3).map((l) => " " + l) : [];
|
|
1747
|
+
const bottomH = Math.max(1, toastLines.length);
|
|
1705
1748
|
const confirmLines = modal?.kind === "confirm" ? wrap(modal.fix.preview, columns - 6, 6) : [];
|
|
1706
1749
|
const confirmH = modal?.kind === "confirm" ? confirmLines.length + 2 : 0;
|
|
1707
1750
|
const bodyH = Math.max(8, termRows - (sidebar ? 0 : 1) - bottomH - confirmH);
|
|
@@ -1952,9 +1995,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1952
1995
|
modal?.kind === "confirm" ? /* @__PURE__ */ jsx2(Panel, { title: modal.fix.label, focused: true, width: columns, height: confirmH, children: confirmLines.map((l, i) => /* @__PURE__ */ jsx2(Text2, { color: color.text, children: l }, i)) }) : null
|
|
1953
1996
|
] })
|
|
1954
1997
|
] }),
|
|
1955
|
-
/* @__PURE__ */ jsxs2(Box2, { height:
|
|
1956
|
-
/* @__PURE__ */ jsx2(Text2, { color: toast.startsWith("\u2713") ? color.green : toast.startsWith("\u2717") ? color.red : toast.startsWith("!") ? color.yellow : color.muted, wrap: "truncate-end", children: " " + toast }),
|
|
1957
|
-
toast ? null : /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: hints + " " })
|
|
1998
|
+
/* @__PURE__ */ jsxs2(Box2, { height: bottomH, width: termCols, justifyContent: "space-between", children: [
|
|
1999
|
+
toastLines.length ? /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: toastLines.map((l, i) => /* @__PURE__ */ jsx2(Text2, { wrap: "truncate-end", color: toast.startsWith("\u2713") ? color.green : toast.startsWith("\u2717") ? color.red : toast.startsWith("!") ? color.yellow : color.muted, children: l }, i)) }) : /* @__PURE__ */ jsx2(Text2, { color: toast.startsWith("\u2713") ? color.green : toast.startsWith("\u2717") ? color.red : toast.startsWith("!") ? color.yellow : color.muted, wrap: "truncate-end", children: " " + toast }),
|
|
2000
|
+
toast && modal?.kind !== "confirm" ? null : /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: " " + hints + " " })
|
|
1958
2001
|
] })
|
|
1959
2002
|
] });
|
|
1960
2003
|
}
|
|
@@ -1999,7 +2042,7 @@ function Help({ width, height }) {
|
|
|
1999
2042
|
const notes = [
|
|
2000
2043
|
'\u2715 problem \xB7 \u26A0 suggestion \xB7 \u2605 recommended fix \xB7 "your call" = a decision, never auto-fixed',
|
|
2001
2044
|
"Source: lib = from your library \xB7 repo = committed by the team \xB7 untracked = only on this machine",
|
|
2002
|
-
"Usage counts Claude Code sessions only:
|
|
2045
|
+
"Usage counts Claude Code sessions only: skilllib reads no other agent's transcripts."
|
|
2003
2046
|
];
|
|
2004
2047
|
const lines = [
|
|
2005
2048
|
...keys.flatMap(([k, d]) => wrap(d, width - 16, 3).map((l, i) => [[(i ? "" : k).padEnd(12), color.accent], [l, color.text]])),
|
|
@@ -2010,12 +2053,9 @@ function Help({ width, height }) {
|
|
|
2010
2053
|
}
|
|
2011
2054
|
|
|
2012
2055
|
// src/tui/load.ts
|
|
2013
|
-
import {
|
|
2014
|
-
import { existsSync as existsSync3, lstatSync
|
|
2015
|
-
import { basename, dirname, join as join2 } from "path";
|
|
2016
|
-
function tildify2(p) {
|
|
2017
|
-
return tildify(p).replace(/\\/g, "/");
|
|
2018
|
-
}
|
|
2056
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
2057
|
+
import { existsSync as existsSync3, lstatSync } from "fs";
|
|
2058
|
+
import { basename, dirname as dirname2, join as join2, relative, sep } from "path";
|
|
2019
2059
|
function uniqueNames(roots) {
|
|
2020
2060
|
const tail = (r, n) => r.split(/[\\/]/).filter(Boolean).slice(-n).join("/");
|
|
2021
2061
|
return new Map(
|
|
@@ -2044,7 +2084,7 @@ function localSkill(root, s, git) {
|
|
|
2044
2084
|
};
|
|
2045
2085
|
const state = git ? git.of(relativeTo(root, s.path)) : null;
|
|
2046
2086
|
const others = otherCopies(root, s.name, s.location);
|
|
2047
|
-
const own = other[s.state]?.source === "untracked" && (state
|
|
2087
|
+
const own = other[s.state]?.source === "untracked" && isShared(state) ? { source: "repo" } : {};
|
|
2048
2088
|
return {
|
|
2049
2089
|
name: s.name,
|
|
2050
2090
|
source: "lib",
|
|
@@ -2059,7 +2099,8 @@ function localSkill(root, s, git) {
|
|
|
2059
2099
|
};
|
|
2060
2100
|
}
|
|
2061
2101
|
function otherCopies(root, name, dir) {
|
|
2062
|
-
|
|
2102
|
+
const self = realPath(join2(root, dir, name));
|
|
2103
|
+
return foldersOf(root, name).real.filter((d) => realPath(join2(d, name)) !== self).map((d) => relativeTo(root, d));
|
|
2063
2104
|
}
|
|
2064
2105
|
function installedAt(path) {
|
|
2065
2106
|
try {
|
|
@@ -2073,8 +2114,8 @@ function machineSkill(s, kept) {
|
|
|
2073
2114
|
const global = s.kind === "global" || s.kind === "skills.sh";
|
|
2074
2115
|
return {
|
|
2075
2116
|
name: s.name,
|
|
2076
|
-
source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : s.kind === "system" ? "system" : s.kind === "skilllib" ? "skilllib" : s.kind === "built-in" && s.harnesses[0] === "grok" ? "grok" : "cursor",
|
|
2077
|
-
where: global || s.kind === "skilllib" ?
|
|
2117
|
+
source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : s.kind === "system" || s.kind === "built-in" && s.harnesses[0] === "codex" ? "system" : s.kind === "skilllib" ? "skilllib" : s.kind === "built-in" && s.harnesses[0] === "grok" ? "grok" : "cursor",
|
|
2118
|
+
where: global || s.kind === "skilllib" ? tildify(dirname2(s.path)) : s.kind === "claude.ai" ? "claude.ai account" : s.kind === "built-in" ? `${s.origin} built-in` : s.origin,
|
|
2078
2119
|
...s.kind === "skills.sh" ? { origin: `npx skills: ${s.origin}` } : {},
|
|
2079
2120
|
path: s.path,
|
|
2080
2121
|
links: s.links,
|
|
@@ -2111,13 +2152,8 @@ function loadWorld() {
|
|
|
2111
2152
|
forgetClaudeBinary();
|
|
2112
2153
|
const agents = enabledHarnesses();
|
|
2113
2154
|
const here = projectHere();
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
return realpathSync(p);
|
|
2117
|
-
} catch {
|
|
2118
|
-
return p;
|
|
2119
|
-
}
|
|
2120
|
-
};
|
|
2155
|
+
if (here) rememberProjects([here]);
|
|
2156
|
+
const real = realPath;
|
|
2121
2157
|
const visible = visibleProjects();
|
|
2122
2158
|
const cwd = here ? visible.find((p) => real(p) === real(here)) ?? here : null;
|
|
2123
2159
|
const listed = [.../* @__PURE__ */ new Set([...cwd ? [cwd] : [], ...visible])];
|
|
@@ -2132,7 +2168,7 @@ function loadWorld() {
|
|
|
2132
2168
|
const libraryNames = new Set(library.map((l) => l.name));
|
|
2133
2169
|
const kept = keptGlobal();
|
|
2134
2170
|
const cursorList = agents.includes("cursor") ? cursorPlugins() : [];
|
|
2135
|
-
const sourced = machineSkills(agents);
|
|
2171
|
+
const sourced = machineSkills(agents).filter((s) => s.harnesses.length || s.broken);
|
|
2136
2172
|
const machine = sourced.map((s) => {
|
|
2137
2173
|
descriptions[s.name] ??= s.description;
|
|
2138
2174
|
return machineSkill(s, kept);
|
|
@@ -2153,7 +2189,11 @@ function loadWorld() {
|
|
|
2153
2189
|
name: names.get(root),
|
|
2154
2190
|
path: root,
|
|
2155
2191
|
manifest: hasManifest ? git ? git.of("skilllib.json") : "no git" : "none",
|
|
2156
|
-
skills: status.map((s) =>
|
|
2192
|
+
skills: status.map((s) => {
|
|
2193
|
+
const local = localSkill(root, s, git);
|
|
2194
|
+
const own = s.state.includes("missing") ? "" : readSkillInfo(s.path).description;
|
|
2195
|
+
return own && own !== descriptions[s.name] ? { ...local, description: own } : local;
|
|
2196
|
+
}),
|
|
2157
2197
|
...loadedHere(root)
|
|
2158
2198
|
};
|
|
2159
2199
|
});
|
|
@@ -2169,7 +2209,7 @@ function loadWorld() {
|
|
|
2169
2209
|
library,
|
|
2170
2210
|
usage: {},
|
|
2171
2211
|
descriptions,
|
|
2172
|
-
backups: backups.map((b) => ({ name: b.name, from:
|
|
2212
|
+
backups: backups.map((b) => ({ name: b.name, from: tildify(b.from), at: b.movedAt })),
|
|
2173
2213
|
...(({ roots: roots2, hidden }) => ({ roots: roots2, hidden }))(readConfig()),
|
|
2174
2214
|
agentsChosen: harnessesChosen(),
|
|
2175
2215
|
agentSkill: agentSkillState(agents),
|
|
@@ -2186,7 +2226,7 @@ async function loadUsage(w) {
|
|
|
2186
2226
|
const today = midnight(Date.now());
|
|
2187
2227
|
const totals = {};
|
|
2188
2228
|
for (const use of await scanUsage(30)) {
|
|
2189
|
-
const skill = use.skill
|
|
2229
|
+
const skill = skillOf(use.skill);
|
|
2190
2230
|
totals[skill] = (totals[skill] ?? 0) + 1;
|
|
2191
2231
|
if (skill !== use.skill) totals[use.skill] = (totals[use.skill] ?? 0) + 1;
|
|
2192
2232
|
const root = projectOf(use.cwd);
|
|
@@ -2200,22 +2240,26 @@ async function loadUsage(w) {
|
|
|
2200
2240
|
}
|
|
2201
2241
|
function said3(c, repo) {
|
|
2202
2242
|
if (c.action === "skipped") return `${c.name}: ${c.reason ?? "skipped"}`;
|
|
2203
|
-
if (c.action === "removed") return `Removed ${c.name} from ${repo}`;
|
|
2243
|
+
if (c.action === "removed") return `Removed ${c.name} from ${repo}${c.reason ? ` (${c.reason})` : ""}`;
|
|
2244
|
+
if (c.action === "unchanged") return `${c.name}${c.to ? ` v${c.to}` : ""} is already in ${repo}`;
|
|
2245
|
+
if (c.action === "relinked") return `Linked ${c.name}${c.to ? ` v${c.to}` : ""} for your other agents in ${repo}`;
|
|
2204
2246
|
const back = c.action === "updated" && c.from !== void 0 && c.to !== void 0 && c.to < c.from;
|
|
2205
2247
|
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}`;
|
|
2206
2248
|
return c.backedUp ? `${done} (your edits are in Settings \u203A Backups)` : done;
|
|
2207
2249
|
}
|
|
2208
2250
|
function realOps(roots, backups, machine, libraryNames) {
|
|
2209
2251
|
const rootOf = (repo) => roots.get(repo);
|
|
2252
|
+
const nameOf = (root) => [...roots].find(([, r]) => r === root)?.[0] ?? basename(root);
|
|
2210
2253
|
const find = (repo, name) => projectStatus(rootOf(repo)).find((s) => s.name === name);
|
|
2211
2254
|
const result = (c, repo) => c.action === "skipped" ? failed(said3(c, repo)) : said3(c, repo);
|
|
2212
2255
|
const copyDir = (repo, name) => readManifest(rootOf(repo)).skills[name]?.dir ?? PROJECT_SKILLS_DIR;
|
|
2213
2256
|
const withLinks = (c, repo, name) => {
|
|
2214
2257
|
const it = (dirs) => dirs.length === 1 ? "it" : "them";
|
|
2258
|
+
const way = c.inTheWay?.length ? `; not linked everywhere: ${inTheWaySaid(rootOf(repo), c.inTheWay)} (clear the way, then link again)` : "";
|
|
2215
2259
|
if (c.uncommitted?.length)
|
|
2216
|
-
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`;
|
|
2217
|
-
if (!c.blocked?.length) return result(c, repo);
|
|
2218
|
-
return { message: `${said3(c, repo)}; not linked in ${c.blocked.join(", ")}: git tracks ${it(c.blocked)}`, then: allowTracked([{ repo, name, blocked: c.blocked }]) };
|
|
2260
|
+
return `${said3(c, repo)}${way}; not linked in ${c.uncommitted.join(", ")}: git tracks ${it(c.uncommitted)} and ${copyDir(repo, name)}/${name} isn't committed, so teammates would get broken links`;
|
|
2261
|
+
if (!c.blocked?.length) return way ? failed(`${said3(c, repo)}${way}`) : result(c, repo);
|
|
2262
|
+
return { message: `${said3(c, repo)}${way}; not linked in ${c.blocked.join(", ")}: git tracks ${it(c.blocked)}`, then: allowTracked([{ repo, name, blocked: c.blocked }]) };
|
|
2219
2263
|
};
|
|
2220
2264
|
const addTo = (repos, names) => {
|
|
2221
2265
|
if (repos.length === 1 && names.length === 1) return withLinks(addSkill(rootOf(repos[0]), names[0]), repos[0], names[0]);
|
|
@@ -2237,10 +2281,11 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2237
2281
|
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`;
|
|
2238
2282
|
notes.push(`${who}not linked in ${dirs.join(", ")} in ${repo}: ${why}`);
|
|
2239
2283
|
}
|
|
2284
|
+
for (const x of done.filter((d) => d.c.inTheWay?.length)) notes.push(`${x.name} not linked everywhere in ${x.repo}: ${inTheWaySaid(rootOf(x.repo), x.c.inTheWay)}`);
|
|
2240
2285
|
const skipped = changes.filter((x) => x.c.action === "skipped").map((x) => `${x.name} not added to ${x.repo}: ${x.c.reason}`);
|
|
2241
2286
|
const text = [summary, ...notes, ...skipped].filter(Boolean).join("; ");
|
|
2242
2287
|
const held = done.flatMap((x) => x.c.blocked?.length ? [{ repo: x.repo, name: x.name, blocked: x.c.blocked }] : []);
|
|
2243
|
-
const result2 = !done.length ? failed(text) : held.length ? { message: text, then: allowTracked(held) } : skipped.length ? failed(text) : text;
|
|
2288
|
+
const result2 = !done.length ? failed(text) : held.length ? { message: text, then: allowTracked(held) } : skipped.length || done.some((x) => x.c.inTheWay?.length) ? failed(text) : text;
|
|
2244
2289
|
return { result: result2, skipped, notes };
|
|
2245
2290
|
};
|
|
2246
2291
|
const allowTracked = (held) => {
|
|
@@ -2255,24 +2300,29 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2255
2300
|
const linked = results.filter((x) => x.r.created.length);
|
|
2256
2301
|
const names = [...new Set(linked.map((x) => x.name))];
|
|
2257
2302
|
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(", ")}` : "";
|
|
2258
|
-
const others = results.filter((x) => !x.r.created.length || x.r.uncommitted.length).map((x) => `${x.repo}: ${linkSaid(x.name, x.r)}`);
|
|
2303
|
+
const others = results.filter((x) => !x.r.created.length || x.r.uncommitted.length || x.r.inTheWay.length).map((x) => `${x.repo}: ${linkSaid(x.repo, x.name, x.r)}`);
|
|
2259
2304
|
return [summary, ...others].join("; ");
|
|
2260
2305
|
}
|
|
2261
2306
|
};
|
|
2262
2307
|
};
|
|
2263
2308
|
const linkIn = (repo, name, allow) => {
|
|
2264
2309
|
const s = find(repo, name);
|
|
2310
|
+
if (!s) throw new Error(`${name} isn't in ${repo} any more`);
|
|
2265
2311
|
return s.managed ? relinkDependency(rootOf(repo), name, { allowTracked: allow }) : linkEverywhere(rootOf(repo), name, s.location, { allowTracked: allow });
|
|
2266
2312
|
};
|
|
2267
|
-
const linkSaid = (name, r) => {
|
|
2313
|
+
const linkSaid = (repo, name, r) => {
|
|
2268
2314
|
const linked = r.created.length ? `linked in ${r.created.join(", ")}` : "";
|
|
2269
2315
|
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` : "";
|
|
2270
|
-
|
|
2316
|
+
const inTheWay = r.inTheWay.length ? inTheWaySaid(rootOf(repo), r.inTheWay) : "";
|
|
2317
|
+
return `${name}: ${[linked, notLinked, inTheWay].filter(Boolean).join("; ") || "nothing to link"}`;
|
|
2271
2318
|
};
|
|
2272
|
-
const orForce = (c, repo, label, force) =>
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2319
|
+
const orForce = (c, repo, label, force) => (
|
|
2320
|
+
// Whatever --force would get past (edits, a pin from another library, another copy in the way): offer it here.
|
|
2321
|
+
c.action === "skipped" && !!c.reason?.includes("--force") ? {
|
|
2322
|
+
message: `${c.name} in ${repo}: ${withoutForceHint(c.reason)}`,
|
|
2323
|
+
then: { label, preview: `What's there now in ${repo} goes to Settings \u203A Backups first.`, run: () => result(force(), repo) }
|
|
2324
|
+
} : result(c, repo)
|
|
2325
|
+
);
|
|
2276
2326
|
const toLibrary = (path) => {
|
|
2277
2327
|
const r = importSkill(path, { force: true });
|
|
2278
2328
|
return r.status === "unchanged" ? `${r.name} is already the same in your library` : `${r.name} v${latestVersion(r.name)?.version} is in your library`;
|
|
@@ -2283,7 +2333,7 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2283
2333
|
remove: (repo, name) => {
|
|
2284
2334
|
const s = find(repo, name);
|
|
2285
2335
|
if (!s) return failed(`${name} isn't in ${repo}`);
|
|
2286
|
-
const committed = !s.managed &&
|
|
2336
|
+
const committed = !s.managed && isShared(gitInfo(rootOf(repo))?.of(relativeTo(rootOf(repo), s.path)));
|
|
2287
2337
|
if (s.state.startsWith("repo skill") || committed) return failed(`${name} is the repo's own (committed by your team): skilllib doesn't delete it`);
|
|
2288
2338
|
const others = otherCopies(rootOf(repo), name, s.location);
|
|
2289
2339
|
const left = (r) => typeof r === "string" && others.length ? `${r}; the copy in ${others.join(", ")} stays` : r;
|
|
@@ -2294,50 +2344,66 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2294
2344
|
const c = updateProject(rootOf(repo), [name])[0];
|
|
2295
2345
|
return c ? orForce(c, repo, "Update anyway", () => updateProject(rootOf(repo), [name], { force: true })[0]) : `${name} is up to date`;
|
|
2296
2346
|
},
|
|
2297
|
-
copyToLibrary: (repo, name) => toLibrary(find(repo, name)
|
|
2347
|
+
copyToLibrary: (repo, name) => ((s) => s ? toLibrary(s.path) : failed(`${name} isn't in ${repo} any more`))(find(repo, name)),
|
|
2298
2348
|
deleteGlobal: (m) => {
|
|
2299
|
-
|
|
2300
|
-
|
|
2349
|
+
if (!entryExists(m.path)) return `${tildify(m.path)} is already gone`;
|
|
2350
|
+
const others = machine.filter((o) => o.name === m.name && o.path !== m.path && o.kind !== "plugin" && !o.broken);
|
|
2351
|
+
const dependents = others.filter((o) => !survives(o.path, [m.path, ...m.links]));
|
|
2352
|
+
const stays = others.filter((o) => !dependents.includes(o));
|
|
2353
|
+
if (!m.broken) {
|
|
2354
|
+
const readers = [.../* @__PURE__ */ new Set([...m.agents, ...dependents.flatMap((o) => o.harnesses)])].filter((id) => !stays.some((o) => o.harnesses.includes(id)));
|
|
2355
|
+
const blind = blindWithoutGlobal(m.name, [{ path: m.path, harnesses: readers, links: [...m.links, ...dependents.map((o) => o.path)] }], [...roots.values(), ...knownProjects()]);
|
|
2356
|
+
if (blind.length) return failed(`${m.name} kept: ${blindSaid(m.name, blind, nameOf)}`);
|
|
2357
|
+
}
|
|
2358
|
+
const left = m.broken ? [] : strandedLinks(m.name, [{ path: m.path, links: [...m.links, ...dependents.map((o) => o.path)] }], [...roots.values(), ...knownProjects()], nameOf);
|
|
2359
|
+
const r = deleteGlobal(m.path, [...m.links, ...dependents.map((o) => o.path)]);
|
|
2360
|
+
if (!r.ok) return failed(`${m.name}: ${r.reason}`);
|
|
2361
|
+
if (m.broken) return `Removed the broken link ${m.name}`;
|
|
2362
|
+
const still = stays.map((o) => tildify(o.path));
|
|
2363
|
+
return `Deleted ${tildify(m.path)} (in Settings \u203A Backups)${still.length ? `; ${m.name} still loads globally from ${still.join(", ")}` : ""}${left.length ? `; ${left.join(", ")} now point${left.length === 1 ? "s" : ""} at nothing: remove ${left.length === 1 ? "it" : "them"}` : ""}`;
|
|
2301
2364
|
},
|
|
2302
2365
|
keepGlobal: (name, keep) => (setKeepGlobal([name], keep), keep ? `${name} marked as global on purpose` : `${name} is no longer marked as global on purpose`),
|
|
2303
2366
|
moveGlobal: (skills, repos, group) => {
|
|
2304
|
-
const moved = [];
|
|
2305
|
-
const notes = [];
|
|
2306
|
-
const held = [];
|
|
2307
2367
|
const repoRoots = repos.map(rootOf);
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
moved.
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
}
|
|
2368
|
+
const label = (root) => repos[repoRoots.indexOf(root)] ?? nameOf(root);
|
|
2369
|
+
const picked = new Set(skills.map((m) => m.name));
|
|
2370
|
+
const sourced = machine.filter((o) => picked.has(o.name) && o.movable && !o.broken);
|
|
2371
|
+
const attempt = (names) => moveGlobalSkills(names ? sourced.filter((m) => names.includes(m.name)) : sourced, repoRoots, { group, label, allowTrackedHint: "allow links there, then move it again" });
|
|
2372
|
+
const said4 = (results2) => {
|
|
2373
|
+
const moved2 = results2.filter((mv) => mv.moved).map((mv) => mv.name);
|
|
2374
|
+
const which = moved2.length <= 4 ? moved2.join(", ") : `${moved2.length} skills`;
|
|
2375
|
+
const summary = moved2.length ? `${which} now load${moved2.length === 1 ? "s" : ""} only in ${repos.join(", ")}` : "";
|
|
2376
|
+
const left = results2.flatMap((mv) => mv.left ?? []);
|
|
2377
|
+
return {
|
|
2378
|
+
moved: moved2,
|
|
2379
|
+
text: [summary, ...left.length ? [`${left.join(", ")} now point${left.length === 1 ? "s" : ""} at nothing: remove ${left.length === 1 ? "it" : "them"}`] : [], ...results2.filter((mv) => !mv.moved).map((mv) => `${mv.name}: ${mv.reason}`)].filter(Boolean).join("; ") || "Nothing moved"
|
|
2380
|
+
};
|
|
2381
|
+
};
|
|
2382
|
+
const results = attempt();
|
|
2383
|
+
const { moved, text } = said4(results);
|
|
2384
|
+
const finishable = (mv) => !mv.moved && mv.changes.length > 0 && mv.changes.every(([, c]) => c.action !== "skipped" && !c.inTheWay?.length && !c.uncommitted?.length) && mv.changes.some(([, c]) => c.blocked?.length);
|
|
2385
|
+
const held = results.filter(finishable).flatMap((mv) => mv.changes.flatMap(([root, c]) => c.blocked?.length ? [{ repo: label(root), name: mv.name, blocked: c.blocked }] : []));
|
|
2386
|
+
if (held.length) {
|
|
2387
|
+
const ask = allowTracked(held);
|
|
2388
|
+
const names = [...new Set(held.map((h) => h.name))];
|
|
2389
|
+
return {
|
|
2390
|
+
message: text,
|
|
2391
|
+
then: {
|
|
2392
|
+
...ask,
|
|
2393
|
+
label: `${ask.label}, then stop loading ${names.length === 1 ? "it" : "them"} globally`,
|
|
2394
|
+
// The retry is what says whether it's done: a skill still global after it is a failure, said as one.
|
|
2395
|
+
run: (w) => {
|
|
2396
|
+
const linked = said(ask.run(w));
|
|
2397
|
+
const retry = said4(attempt(names));
|
|
2398
|
+
const before = said4(results.filter((mv) => mv.moved));
|
|
2399
|
+
const others = results.filter((mv) => !mv.moved && !names.includes(mv.name)).map((mv) => `${mv.name}: ${mv.reason}`);
|
|
2400
|
+
const text2 = [before.moved.length ? before.text : "", linked, retry.text, ...others].filter(Boolean).join("; ");
|
|
2401
|
+
return retry.moved.length === names.length ? text2 : failed(text2);
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
};
|
|
2321
2405
|
}
|
|
2322
|
-
|
|
2323
|
-
for (const repo of new Set(held.map((h) => h.repo)))
|
|
2324
|
-
for (const shared of [true, false]) {
|
|
2325
|
-
const here = held.filter((h) => h.repo === repo && h.shared === shared);
|
|
2326
|
-
if (!here.length) continue;
|
|
2327
|
-
const agents = [...new Set(here.flatMap((h) => h.blind))].map((id) => harness(id).name).join(", ");
|
|
2328
|
-
const what = skills.length === 1 ? "it" : here.length === moved.length ? "them" : here.map((h) => h.name).join(", ");
|
|
2329
|
-
const dirs = [...new Set(here.flatMap((h) => h.blocked))].join(", ");
|
|
2330
|
-
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`;
|
|
2331
|
-
const key = `${agents}|${what}|${why}`;
|
|
2332
|
-
if (!gaps.has(key)) gaps.set(key, { who: `${agents} can't see ${what}`, why, repos: [] });
|
|
2333
|
-
gaps.get(key).repos.push(repo);
|
|
2334
|
-
}
|
|
2335
|
-
const blindNotes = [...gaps.values()].map((g) => `${g.who} in ${g.repos.join(", ")}: ${g.why}`);
|
|
2336
|
-
const summary = moved.length ? `${moved.length === 1 ? moved[0] : `${moved.length} skills`} now load${moved.length === 1 ? "s" : ""} only in ${repos.join(", ")}` : "";
|
|
2337
|
-
const text = [summary, ...blindNotes, ...notes].filter(Boolean).join("; ") || "Nothing moved";
|
|
2338
|
-
const askable = held.filter((h) => h.shared);
|
|
2339
|
-
if (!moved.length && notes.length) return failed(text);
|
|
2340
|
-
return askable.length ? { message: text, then: allowTracked(askable) } : text;
|
|
2406
|
+
return moved.length ? text : failed(text);
|
|
2341
2407
|
},
|
|
2342
2408
|
createSkill: (name) => {
|
|
2343
2409
|
const r = createSkill(name, "");
|
|
@@ -2346,10 +2412,15 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2346
2412
|
replacePlugin: (p, repos) => {
|
|
2347
2413
|
const id = p.id;
|
|
2348
2414
|
const clashes = p.skills.filter((s) => {
|
|
2349
|
-
const
|
|
2350
|
-
|
|
2415
|
+
const theirs = treeHash(s.path);
|
|
2416
|
+
const mine = [librarySkillDir(s.name), ...machine.filter((m) => m.movable && m.name === s.name).map((m) => m.path)].map(treeHash);
|
|
2417
|
+
return mine.some((h) => h !== null && h !== theirs);
|
|
2351
2418
|
});
|
|
2352
|
-
if (clashes.length) return failed(`${id} kept:
|
|
2419
|
+
if (clashes.length) return failed(`${id} kept: you have different ${clashes.map((s) => s.name).join(", ")} (in your library or global skills); keep yours, or update or delete ${clashes.length === 1 ? "it" : "them"} first`);
|
|
2420
|
+
const cant = repos.flatMap(
|
|
2421
|
+
(repo) => p.skills.flatMap((s) => ((why) => why ? [`${s.name} in ${repo}: ${withoutForceHint(why)}`] : [])(trackBlocker(rootOf(repo), s.name) ?? addRefusal(rootOf(repo), s.name, treeHash(s.path) ?? "")))
|
|
2422
|
+
);
|
|
2423
|
+
if (cant.length) return failed(`${id} kept, nothing changed: ${cant.join("; ")}`);
|
|
2353
2424
|
const imported = p.skills.map((s) => ({ name: s.name, status: importSkill(s.path).status }));
|
|
2354
2425
|
for (const r of imported) if (r.status !== "unchanged") recordOrigin(r.name, `plugin: ${id}`);
|
|
2355
2426
|
const added = repos.length ? addMany(repos, p.skills.map((s) => s.name)) : { result: "", skipped: [], notes: [] };
|
|
@@ -2386,21 +2457,31 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2386
2457
|
return r.ok ? r.message : failed(r.message);
|
|
2387
2458
|
},
|
|
2388
2459
|
deleteLibrary: (name) => {
|
|
2460
|
+
const using = projectsUsing(name);
|
|
2461
|
+
const unreadable = readableRoots(using).unreadable.map((u) => u.root);
|
|
2462
|
+
if (unreadable.length) return failed(`${name} is pinned in ${unreadable.map((r2) => basename(r2)).join(", ")}, whose skilllib.json can't be read: fix it first, then delete`);
|
|
2463
|
+
const hidden = using.filter((root) => ![...roots.values()].includes(root));
|
|
2464
|
+
if (hidden.length) return failed(`${name} is also in ${hidden.map((r2) => basename(r2)).join(", ")}, which ${hidden.length === 1 ? "is" : "are"} hidden: unhide ${hidden.length === 1 ? "it" : "them"} (Settings) to delete it everywhere`);
|
|
2389
2465
|
const from = [];
|
|
2390
|
-
|
|
2466
|
+
const left = [];
|
|
2467
|
+
for (const root of using) {
|
|
2391
2468
|
const s = projectStatus(root).find((x) => x.name === name && x.managed);
|
|
2392
|
-
if (s &&
|
|
2393
|
-
removeSkill(root, name, { force: true });
|
|
2394
|
-
|
|
2469
|
+
if (s && isDir(s.path) && !notWritable(root, relative(root, dirname2(s.path)).split(sep).join("/"))) removeUntracked(root, name, s.path);
|
|
2470
|
+
const c = removeSkill(root, name, { force: true });
|
|
2471
|
+
if (c.action === "skipped") return failed(`${name}: ${c.reason}${from.length ? `; removed from ${from.join(", ")} so far` : ""}`);
|
|
2472
|
+
const repo = nameOf(root);
|
|
2473
|
+
from.push(repo);
|
|
2474
|
+
if (c.reason) left.push(`in ${repo}, ${c.reason}`);
|
|
2395
2475
|
}
|
|
2396
2476
|
const r = deleteLibrarySkill(name);
|
|
2397
2477
|
if (!r.ok) return failed(`${name}: ${r.reason}`);
|
|
2398
|
-
return `Deleted ${name} from your library${from.length ? ` and from ${from.join(", ")}` : ""} (in Settings \u203A Backups)`;
|
|
2478
|
+
return `Deleted ${name} from your library${from.length ? ` and from ${from.join(", ")}` : ""} (in Settings \u203A Backups)${left.length ? `; ${left.join("; ")}` : ""}`;
|
|
2399
2479
|
},
|
|
2400
2480
|
restoreBackup: (i) => {
|
|
2401
|
-
const
|
|
2402
|
-
const
|
|
2403
|
-
|
|
2481
|
+
const done = movedTogether(backups, backups[i]).map((b) => [b, b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b)]);
|
|
2482
|
+
const bad = done.filter(([, r]) => !r.ok);
|
|
2483
|
+
if (bad.length) return failed(bad.map(([b, r]) => `${b.name}: ${!r.ok ? r.reason : ""}`).join("; "));
|
|
2484
|
+
return done.map(([b, r]) => `Restored ${b.name} to ${r.ok ? tildify(r.to) : ""}`).join(" \xB7 ");
|
|
2404
2485
|
},
|
|
2405
2486
|
setAgents: (ids) => (setHarnesses(ids), "Saved your agents"),
|
|
2406
2487
|
installVersion: (repo, name, version) => withLinks(addSkill(rootOf(repo), name, { version, force: true }), repo, name),
|
|
@@ -2409,33 +2490,34 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2409
2490
|
addRoot: (path) => {
|
|
2410
2491
|
const root = addRoot(path);
|
|
2411
2492
|
discoverProjects();
|
|
2412
|
-
return `Scanning ${
|
|
2493
|
+
return `Scanning ${tildify(root)} for repos`;
|
|
2413
2494
|
},
|
|
2414
|
-
removeRoot: (path) => (removeRoot(path), `Stopped scanning ${
|
|
2495
|
+
removeRoot: (path) => (removeRoot(path), `Stopped scanning ${tildify(path)}; repos already found stay until you hide them`),
|
|
2415
2496
|
rescan: () => (discoverProjects(), "Looked for repos in your project folders again"),
|
|
2416
2497
|
hide: (repo) => (setHidden(rootOf(repo), true), `Hid ${repo}: Settings \u203A Hidden repos brings it back`),
|
|
2417
|
-
unhide: (path) => (setHidden(path, false), `${
|
|
2498
|
+
unhide: (path) => (setHidden(path, false), `${tildify(path)} is back in the list`),
|
|
2418
2499
|
openFolder: (path) => {
|
|
2419
2500
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
2420
|
-
|
|
2421
|
-
return `Opened ${
|
|
2501
|
+
const r = spawnSync3(cmd, [path], { stdio: "ignore", timeout: 1e4 });
|
|
2502
|
+
return r.error || r.status !== 0 && process.platform !== "win32" ? failed(`Couldn't open ${tildify(path)} (${cmd} ${r.error ? "isn't available" : "failed"})`) : `Opened ${tildify(path)}`;
|
|
2422
2503
|
},
|
|
2423
2504
|
copy: (text, what) => {
|
|
2424
2505
|
const r = copyText(text);
|
|
2425
|
-
return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor, Codex or Grok` : `Couldn't reach the clipboard; ${what} is in ${
|
|
2506
|
+
return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor, Codex or Grok` : `Couldn't reach the clipboard; ${what} is in ${tildify(r.savedTo)}`;
|
|
2426
2507
|
},
|
|
2427
2508
|
installAgentSkill: () => {
|
|
2428
2509
|
const r = installAgentSkill();
|
|
2429
|
-
return r.ok ? `Your agents can now use skilllib (${r.dirs.map(
|
|
2510
|
+
return r.ok ? `Your agents can now use skilllib (${r.dirs.map(tildify).join(", ")})` : failed(`skilllib skill: ${r.reason}`);
|
|
2430
2511
|
},
|
|
2431
2512
|
removeAgentSkill: () => {
|
|
2432
2513
|
const removed = removeAgentSkill();
|
|
2433
|
-
return removed.length ? `Removed the skilllib skill (${removed.map(
|
|
2514
|
+
return removed.length ? `Removed the skilllib skill (${removed.map(tildify).join(", ")})` : failed("The skilllib skill isn't installed");
|
|
2434
2515
|
},
|
|
2435
2516
|
usageIssues: ({ usage, totals = {} }) => {
|
|
2436
2517
|
const repoOf = new Map([...roots].map(([name, root]) => [root, name]));
|
|
2437
2518
|
return usageIssues([...roots.values()], machine, libraryNames, {
|
|
2438
|
-
|
|
2519
|
+
// As doctor counts them: a plugin's copy (plugin:name, kept apart for the Plugins rows) counts for the skill.
|
|
2520
|
+
inProject: (root, skill) => Object.entries(usage[repoOf.get(root)] ?? {}).reduce((n, [key, count]) => n + (skillOf(key) === skill ? count : 0), 0),
|
|
2439
2521
|
total: (skill) => totals[skill] ?? 0,
|
|
2440
2522
|
asUsed: (key) => totals[key] ?? 0,
|
|
2441
2523
|
days: 30
|
|
@@ -2447,6 +2529,7 @@ function realOps(roots, backups, machine, libraryNames) {
|
|
|
2447
2529
|
// src/tui/index.tsx
|
|
2448
2530
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
2449
2531
|
async function tui() {
|
|
2532
|
+
discoverProjects();
|
|
2450
2533
|
const instance = render(/* @__PURE__ */ jsx3(App, { initial: loadWorld(), reload: loadWorld, loadUsage }), { alternateScreen: true, exitOnCtrlC: true });
|
|
2451
2534
|
await instance.waitUntilExit();
|
|
2452
2535
|
}
|