claudeup 3.15.0 → 3.16.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "3.15.0",
3
+ "version": "3.16.0",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -5,6 +5,7 @@ export const RECOMMENDED_SKILLS = [
5
5
  skillPath: "skills/find-skills",
6
6
  description: "Discover and install new skills from the ecosystem",
7
7
  category: "search",
8
+ stars: 12000,
8
9
  },
9
10
  {
10
11
  name: "React Best Practices",
@@ -12,6 +13,7 @@ export const RECOMMENDED_SKILLS = [
12
13
  skillPath: "skills/react-best-practices",
13
14
  description: "Modern React patterns and Vercel deployment guidelines",
14
15
  category: "frontend",
16
+ stars: 24000,
15
17
  },
16
18
  {
17
19
  name: "Web Design Guidelines",
@@ -19,6 +21,7 @@ export const RECOMMENDED_SKILLS = [
19
21
  skillPath: "skills/web-design-guidelines",
20
22
  description: "UI/UX design principles and web standards",
21
23
  category: "design",
24
+ stars: 24000,
22
25
  },
23
26
  {
24
27
  name: "Remotion Best Practices",
@@ -26,6 +29,7 @@ export const RECOMMENDED_SKILLS = [
26
29
  skillPath: "skills/remotion-best-practices",
27
30
  description: "Programmatic video creation with Remotion",
28
31
  category: "media",
32
+ stars: 2400,
29
33
  },
30
34
  {
31
35
  name: "UI/UX Pro Max",
@@ -33,6 +37,7 @@ export const RECOMMENDED_SKILLS = [
33
37
  skillPath: "skills_en/ui-ux-pro-max",
34
38
  description: "50 styles, 21 palettes, 50 font pairings, 9 stacks. Covers React, Next.js, Vue, Svelte, SwiftUI, Flutter, Tailwind, shadcn/ui",
35
39
  category: "design",
40
+ stars: 109,
36
41
  },
37
42
  {
38
43
  name: "ElevenLabs TTS",
@@ -40,6 +45,7 @@ export const RECOMMENDED_SKILLS = [
40
45
  skillPath: "skills/elevenlabs-tts",
41
46
  description: "Text-to-speech with ElevenLabs API integration",
42
47
  category: "media",
48
+ stars: 206,
43
49
  },
44
50
  {
45
51
  name: "Audit Website",
@@ -47,6 +53,7 @@ export const RECOMMENDED_SKILLS = [
47
53
  skillPath: "skills/audit-website",
48
54
  description: "Security and quality auditing for web applications",
49
55
  category: "security",
56
+ stars: 67,
50
57
  },
51
58
  {
52
59
  name: "Systematic Debugging",
@@ -54,6 +61,7 @@ export const RECOMMENDED_SKILLS = [
54
61
  skillPath: "skills/systematic-debugging",
55
62
  description: "Structured debugging methodology with root cause analysis",
56
63
  category: "debugging",
64
+ stars: 113000,
57
65
  },
58
66
  {
59
67
  name: "shadcn/ui",
@@ -61,6 +69,7 @@ export const RECOMMENDED_SKILLS = [
61
69
  skillPath: "packages/shadcn",
62
70
  description: "shadcn/ui component library patterns and usage",
63
71
  category: "frontend",
72
+ stars: 111000,
64
73
  },
65
74
  {
66
75
  name: "Neon Postgres",
@@ -68,6 +77,7 @@ export const RECOMMENDED_SKILLS = [
68
77
  skillPath: "skills/neon-postgres",
69
78
  description: "Neon serverless Postgres setup and best practices",
70
79
  category: "database",
80
+ stars: 42,
71
81
  },
72
82
  {
73
83
  name: "Neon Serverless",
@@ -75,6 +85,7 @@ export const RECOMMENDED_SKILLS = [
75
85
  skillPath: "skills/neon-serverless",
76
86
  description: "Serverless database patterns with Neon",
77
87
  category: "database",
88
+ stars: 81,
78
89
  },
79
90
  ];
80
91
  export const DEFAULT_SKILL_REPOS = [
@@ -6,6 +6,7 @@ export interface RecommendedSkill {
6
6
  skillPath: string;
7
7
  description: string;
8
8
  category: string;
9
+ stars?: number; // fallback when GitHub API is rate-limited
9
10
  }
10
11
 
11
12
  export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
@@ -15,6 +16,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
15
16
  skillPath: "skills/find-skills",
16
17
  description: "Discover and install new skills from the ecosystem",
17
18
  category: "search",
19
+ stars: 12000,
18
20
  },
19
21
  {
20
22
  name: "React Best Practices",
@@ -22,6 +24,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
22
24
  skillPath: "skills/react-best-practices",
23
25
  description: "Modern React patterns and Vercel deployment guidelines",
24
26
  category: "frontend",
27
+ stars: 24000,
25
28
  },
26
29
  {
27
30
  name: "Web Design Guidelines",
@@ -29,6 +32,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
29
32
  skillPath: "skills/web-design-guidelines",
30
33
  description: "UI/UX design principles and web standards",
31
34
  category: "design",
35
+ stars: 24000,
32
36
  },
33
37
  {
34
38
  name: "Remotion Best Practices",
@@ -36,6 +40,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
36
40
  skillPath: "skills/remotion-best-practices",
37
41
  description: "Programmatic video creation with Remotion",
38
42
  category: "media",
43
+ stars: 2400,
39
44
  },
40
45
  {
41
46
  name: "UI/UX Pro Max",
@@ -43,6 +48,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
43
48
  skillPath: "skills_en/ui-ux-pro-max",
44
49
  description: "50 styles, 21 palettes, 50 font pairings, 9 stacks. Covers React, Next.js, Vue, Svelte, SwiftUI, Flutter, Tailwind, shadcn/ui",
45
50
  category: "design",
51
+ stars: 109,
46
52
  },
47
53
  {
48
54
  name: "ElevenLabs TTS",
@@ -50,6 +56,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
50
56
  skillPath: "skills/elevenlabs-tts",
51
57
  description: "Text-to-speech with ElevenLabs API integration",
52
58
  category: "media",
59
+ stars: 206,
53
60
  },
54
61
  {
55
62
  name: "Audit Website",
@@ -57,6 +64,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
57
64
  skillPath: "skills/audit-website",
58
65
  description: "Security and quality auditing for web applications",
59
66
  category: "security",
67
+ stars: 67,
60
68
  },
61
69
  {
62
70
  name: "Systematic Debugging",
@@ -64,6 +72,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
64
72
  skillPath: "skills/systematic-debugging",
65
73
  description: "Structured debugging methodology with root cause analysis",
66
74
  category: "debugging",
75
+ stars: 113000,
67
76
  },
68
77
  {
69
78
  name: "shadcn/ui",
@@ -71,6 +80,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
71
80
  skillPath: "packages/shadcn",
72
81
  description: "shadcn/ui component library patterns and usage",
73
82
  category: "frontend",
83
+ stars: 111000,
74
84
  },
75
85
  {
76
86
  name: "Neon Postgres",
@@ -78,6 +88,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
78
88
  skillPath: "skills/neon-postgres",
79
89
  description: "Neon serverless Postgres setup and best practices",
80
90
  category: "database",
91
+ stars: 42,
81
92
  },
82
93
  {
83
94
  name: "Neon Serverless",
@@ -85,6 +96,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
85
96
  skillPath: "skills/neon-serverless",
86
97
  description: "Serverless database patterns with Neon",
87
98
  category: "database",
99
+ stars: 81,
88
100
  },
89
101
  ];
90
102
 
@@ -202,28 +202,55 @@ export async function fetchAvailableSkills(_repos, projectPath) {
202
202
  // 2. Fetch popular skills from Firebase API
203
203
  const popular = await fetchPopularSkills(30);
204
204
  const popularSkills = popular.map((s) => markInstalled({ ...s, isRecommended: false }));
205
- // 3. Enrich recommended skills with GitHub repo stars
206
- // Fetch stars for each unique repo (typically ~7 repos, parallel)
205
+ // 3. Enrich recommended skills with GitHub repo stars (cached to disk)
206
+ const starsCachePath = path.join(os.homedir(), ".claude", "skill-stars-cache.json");
207
+ let starsCache = {};
208
+ try {
209
+ starsCache = await fs.readJson(starsCachePath);
210
+ }
211
+ catch { /* no cache yet */ }
207
212
  const uniqueRepos = [...new Set(recommendedSkills.map((s) => s.source.repo))];
208
213
  const repoStars = new Map();
209
- try {
210
- const starResults = await Promise.allSettled(uniqueRepos.map(async (repo) => {
214
+ const cacheMaxAge = 24 * 60 * 60 * 1000; // 24 hours
215
+ let cacheUpdated = false;
216
+ for (const repo of uniqueRepos) {
217
+ const cached = starsCache[repo];
218
+ if (cached && Date.now() - new Date(cached.fetchedAt).getTime() < cacheMaxAge) {
219
+ repoStars.set(repo, cached.stars);
220
+ continue;
221
+ }
222
+ // Try fetching from GitHub (may be rate limited)
223
+ try {
211
224
  const res = await fetch(`https://api.github.com/repos/${repo}`, {
212
225
  headers: { Accept: "application/vnd.github+json" },
213
226
  signal: AbortSignal.timeout(5000),
214
227
  });
215
- if (!res.ok)
216
- return;
217
- const data = (await res.json());
218
- if (data.stargazers_count)
219
- repoStars.set(repo, data.stargazers_count);
220
- }));
228
+ if (res.ok) {
229
+ const data = (await res.json());
230
+ if (data.stargazers_count) {
231
+ repoStars.set(repo, data.stargazers_count);
232
+ starsCache[repo] = { stars: data.stargazers_count, fetchedAt: new Date().toISOString() };
233
+ cacheUpdated = true;
234
+ }
235
+ }
236
+ else if (cached) {
237
+ // Rate limited but have stale cache — use it
238
+ repoStars.set(repo, cached.stars);
239
+ }
240
+ }
241
+ catch {
242
+ if (cached)
243
+ repoStars.set(repo, cached.stars);
244
+ }
221
245
  }
222
- catch {
223
- // Non-fatal — stars are cosmetic
246
+ if (cacheUpdated) {
247
+ try {
248
+ await fs.writeJson(starsCachePath, starsCache);
249
+ }
250
+ catch { /* ignore */ }
224
251
  }
225
252
  for (const rec of recommendedSkills) {
226
- rec.stars = repoStars.get(rec.source.repo) || undefined;
253
+ rec.stars = repoStars.get(rec.source.repo) || rec.stars || undefined;
227
254
  }
228
255
  // 4. Combine: recommended first, then popular (dedup by name)
229
256
  const seen = new Set(recommendedSkills.map((s) => s.name));
@@ -271,27 +271,49 @@ export async function fetchAvailableSkills(
271
271
  const popular = await fetchPopularSkills(30);
272
272
  const popularSkills = popular.map((s) => markInstalled({ ...s, isRecommended: false }));
273
273
 
274
- // 3. Enrich recommended skills with GitHub repo stars
275
- // Fetch stars for each unique repo (typically ~7 repos, parallel)
274
+ // 3. Enrich recommended skills with GitHub repo stars (cached to disk)
275
+ const starsCachePath = path.join(os.homedir(), ".claude", "skill-stars-cache.json");
276
+ let starsCache: Record<string, { stars: number; fetchedAt: string }> = {};
277
+ try { starsCache = await fs.readJson(starsCachePath); } catch { /* no cache yet */ }
278
+
276
279
  const uniqueRepos = [...new Set(recommendedSkills.map((s) => s.source.repo))];
277
280
  const repoStars = new Map<string, number>();
278
- try {
279
- const starResults = await Promise.allSettled(
280
- uniqueRepos.map(async (repo) => {
281
- const res = await fetch(`https://api.github.com/repos/${repo}`, {
282
- headers: { Accept: "application/vnd.github+json" },
283
- signal: AbortSignal.timeout(5000),
284
- });
285
- if (!res.ok) return;
281
+ const cacheMaxAge = 24 * 60 * 60 * 1000; // 24 hours
282
+ let cacheUpdated = false;
283
+
284
+ for (const repo of uniqueRepos) {
285
+ const cached = starsCache[repo];
286
+ if (cached && Date.now() - new Date(cached.fetchedAt).getTime() < cacheMaxAge) {
287
+ repoStars.set(repo, cached.stars);
288
+ continue;
289
+ }
290
+ // Try fetching from GitHub (may be rate limited)
291
+ try {
292
+ const res = await fetch(`https://api.github.com/repos/${repo}`, {
293
+ headers: { Accept: "application/vnd.github+json" },
294
+ signal: AbortSignal.timeout(5000),
295
+ });
296
+ if (res.ok) {
286
297
  const data = (await res.json()) as { stargazers_count?: number };
287
- if (data.stargazers_count) repoStars.set(repo, data.stargazers_count);
288
- }),
289
- );
290
- } catch {
291
- // Non-fatal — stars are cosmetic
298
+ if (data.stargazers_count) {
299
+ repoStars.set(repo, data.stargazers_count);
300
+ starsCache[repo] = { stars: data.stargazers_count, fetchedAt: new Date().toISOString() };
301
+ cacheUpdated = true;
302
+ }
303
+ } else if (cached) {
304
+ // Rate limited but have stale cache — use it
305
+ repoStars.set(repo, cached.stars);
306
+ }
307
+ } catch {
308
+ if (cached) repoStars.set(repo, cached.stars);
309
+ }
310
+ }
311
+ if (cacheUpdated) {
312
+ try { await fs.writeJson(starsCachePath, starsCache); } catch { /* ignore */ }
292
313
  }
314
+
293
315
  for (const rec of recommendedSkills) {
294
- rec.stars = repoStars.get(rec.source.repo) || undefined;
316
+ rec.stars = repoStars.get(rec.source.repo) || rec.stars || undefined;
295
317
  }
296
318
 
297
319
  // 4. Combine: recommended first, then popular (dedup by name)
@@ -81,18 +81,19 @@ export function buildSkillBrowserItems({ recommended, popular, installed, search
81
81
  }
82
82
  return items;
83
83
  }
84
- // ── POPULAR (default, no search query) ──
85
- if (popular.length > 0) {
84
+ // ── POPULAR (default, no search query) — only skills with meaningful stars ──
85
+ const popularWithStars = popular.filter((s) => (s.stars ?? 0) >= 5);
86
+ if (popularWithStars.length > 0) {
86
87
  items.push({
87
88
  id: "cat:popular",
88
89
  kind: "category",
89
90
  label: "Popular",
90
91
  title: "Popular",
91
92
  categoryKey: "popular",
92
- count: popular.length,
93
+ count: popularWithStars.length,
93
94
  tone: "teal",
94
95
  });
95
- for (const skill of popular) {
96
+ for (const skill of popularWithStars) {
96
97
  items.push({
97
98
  id: `skill:${skill.id}`,
98
99
  kind: "skill",
@@ -134,18 +134,19 @@ export function buildSkillBrowserItems({
134
134
  return items;
135
135
  }
136
136
 
137
- // ── POPULAR (default, no search query) ──
138
- if (popular.length > 0) {
137
+ // ── POPULAR (default, no search query) — only skills with meaningful stars ──
138
+ const popularWithStars = popular.filter((s) => (s.stars ?? 0) >= 5);
139
+ if (popularWithStars.length > 0) {
139
140
  items.push({
140
141
  id: "cat:popular",
141
142
  kind: "category",
142
143
  label: "Popular",
143
144
  title: "Popular",
144
145
  categoryKey: "popular",
145
- count: popular.length,
146
+ count: popularWithStars.length,
146
147
  tone: "teal",
147
148
  });
148
- for (const skill of popular) {
149
+ for (const skill of popularWithStars) {
149
150
  items.push({
150
151
  id: `skill:${skill.id}`,
151
152
  kind: "skill",
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- import { SelectableRow, ListCategoryRow, ScopeSquares, ScopeDetail, ActionHints, MetaText, KeyValueLine, DetailSection, } from "../components/primitives/index.js";
2
+ import { SelectableRow, ListCategoryRow, ScopeSquares, MetaText, KeyValueLine, DetailSection, } from "../components/primitives/index.js";
3
3
  // ─── Helpers ──────────────────────────────────────────────────────────────────
4
4
  function formatStars(stars) {
5
5
  if (!stars)
@@ -26,36 +26,29 @@ const categoryRenderer = {
26
26
  },
27
27
  };
28
28
  // ─── Skill renderer ───────────────────────────────────────────────────────────
29
+ const MAX_SKILL_NAME_LEN = 35;
30
+ function truncateName(name) {
31
+ return name.length > MAX_SKILL_NAME_LEN
32
+ ? name.slice(0, MAX_SKILL_NAME_LEN - 1) + "\u2026"
33
+ : name;
34
+ }
29
35
  const skillRenderer = {
30
36
  renderRow: ({ item, isSelected }) => {
31
37
  const { skill } = item;
32
38
  const hasUser = skill.installedScope === "user";
33
39
  const hasProject = skill.installedScope === "project";
34
40
  const starsStr = formatStars(skill.stars);
35
- return (_jsxs(SelectableRow, { selected: isSelected, indent: 1, children: [_jsx(ScopeSquares, { user: hasUser, project: hasProject, selected: isSelected }), _jsx("span", { children: " " }), _jsx("span", { fg: isSelected ? "white" : skill.installed ? "white" : "gray", children: skill.name }), skill.hasUpdate ? _jsx(MetaText, { text: " \u2B06", tone: "warning" }) : null, starsStr ? _jsx(MetaText, { text: ` ${starsStr}`, tone: "warning" }) : null] }));
41
+ const displayName = truncateName(skill.name);
42
+ return (_jsxs(SelectableRow, { selected: isSelected, indent: 1, children: [_jsx(ScopeSquares, { user: hasUser, project: hasProject, selected: isSelected }), _jsx("span", { children: " " }), _jsx("span", { fg: isSelected ? "white" : skill.installed ? "white" : "gray", children: displayName }), skill.hasUpdate ? _jsx(MetaText, { text: " \u2B06", tone: "warning" }) : null, starsStr ? _jsx(MetaText, { text: ` ${starsStr}`, tone: "warning" }) : null] }));
36
43
  },
37
44
  renderDetail: ({ item }) => {
38
45
  const { skill } = item;
39
46
  const fm = skill.frontmatter;
40
47
  const description = fm?.description || skill.description || "Loading...";
41
48
  const starsStr = formatStars(skill.stars);
42
- return (_jsxs("box", { flexDirection: "column", children: [_jsxs("text", { fg: "cyan", children: [_jsx("strong", { children: skill.name }), starsStr ? _jsxs("span", { fg: "yellow", children: [" ", starsStr] }) : null] }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "white", children: description }) }), fm?.category ? (_jsx(KeyValueLine, { label: "Category", value: _jsx("span", { fg: "cyan", children: fm.category }) })) : null, fm?.author ? (_jsx(KeyValueLine, { label: "Author", value: _jsx("span", { children: fm.author }) })) : null, fm?.version ? (_jsx(KeyValueLine, { label: "Version", value: _jsx("span", { children: fm.version }) })) : null, fm?.tags && fm.tags.length > 0 ? (_jsx(KeyValueLine, { label: "Tags", value: _jsx("span", { children: fm.tags.join(", ") }) })) : null, _jsxs(DetailSection, { children: [_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Source " }), _jsx("span", { fg: "#5c9aff", children: skill.source.repo })] }), _jsxs("text", { children: [_jsx("span", { fg: "gray", children: " " }), _jsx("span", { fg: "gray", children: skill.repoPath })] })] }), skill.installed && skill.installedScope && (_jsxs(DetailSection, { children: [_jsx("text", { children: "".repeat(24) }), _jsx(ScopeDetail, { scopes: {
43
- user: skill.installedScope === "user",
44
- project: skill.installedScope === "project",
45
- }, paths: {
46
- user: "~/.claude/skills/",
47
- project: ".claude/skills/",
48
- } })] })), skill.hasUpdate && (_jsx("box", { marginTop: 1, children: _jsxs("text", { bg: "yellow", fg: "black", children: [" ", "UPDATE AVAILABLE", " "] }) })), _jsx(ActionHints, { hints: skill.installed
49
- ? [
50
- { key: "d", label: "Uninstall", tone: "danger" },
51
- { key: "u/p", label: "Reinstall in user/project scope" },
52
- { key: "o", label: "Open in browser" },
53
- ]
54
- : [
55
- { key: "u", label: "Install in user scope", tone: "primary" },
56
- { key: "p", label: "Install in project scope", tone: "primary" },
57
- { key: "o", label: "Open in browser" },
58
- ] })] }));
49
+ return (_jsxs("box", { flexDirection: "column", children: [_jsxs("text", { fg: "cyan", children: [_jsx("strong", { children: skill.name }), starsStr ? _jsxs("span", { fg: "yellow", children: [" ", starsStr] }) : null] }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "white", children: description }) }), fm?.category ? (_jsx(KeyValueLine, { label: "Category", value: _jsx("span", { fg: "cyan", children: fm.category }) })) : null, fm?.author ? (_jsx(KeyValueLine, { label: "Author", value: _jsx("span", { children: fm.author }) })) : null, fm?.version ? (_jsx(KeyValueLine, { label: "Version", value: _jsx("span", { children: fm.version }) })) : null, fm?.tags && fm.tags.length > 0 ? (_jsx(KeyValueLine, { label: "Tags", value: _jsx("span", { children: fm.tags.join(", ") }) })) : null, _jsxs(DetailSection, { children: [_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Source " }), _jsx("span", { fg: "#5c9aff", children: skill.source.repo })] }), _jsxs("text", { children: [_jsx("span", { fg: "gray", children: " " }), _jsx("span", { fg: "gray", children: skill.repoPath })] })] }), _jsxs(DetailSection, { children: [_jsx("text", { children: "─".repeat(24) }), _jsx("text", { children: _jsx("strong", { children: "Scopes:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { bg: "cyan", fg: "black", children: " u " }), _jsx("span", { fg: skill.installedScope === "user" ? "cyan" : "gray", children: skill.installedScope === "user" ? " ● " : " ○ " }), _jsx("span", { fg: "cyan", children: "User" }), _jsx("span", { fg: "gray", children: " ~/.claude/skills/" })] }), _jsxs("text", { children: [_jsx("span", { bg: "green", fg: "black", children: " p " }), _jsx("span", { fg: skill.installedScope === "project" ? "green" : "gray", children: skill.installedScope === "project" ? " ● " : " ○ " }), _jsx("span", { fg: "green", children: "Project" }), _jsx("span", { fg: "gray", children: " .claude/skills/" })] })] })] }), skill.hasUpdate && (_jsx("box", { marginTop: 1, children: _jsx("text", { bg: "yellow", fg: "black", children: " UPDATE AVAILABLE " }) })), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: skill.installed
50
+ ? "Press u/p to toggle scope"
51
+ : "Press u/p to install" }) }), _jsx("box", { children: _jsxs("text", { children: [_jsx("span", { bg: "#555555", fg: "white", children: " o " }), _jsx("span", { fg: "gray", children: " Open in browser" })] }) })] }));
59
52
  },
60
53
  };
61
54
  // ─── Registry ─────────────────────────────────────────────────────────────────
@@ -62,19 +62,28 @@ const categoryRenderer: ItemRenderer<SkillCategoryItem> = {
62
62
 
63
63
  // ─── Skill renderer ───────────────────────────────────────────────────────────
64
64
 
65
+ const MAX_SKILL_NAME_LEN = 35;
66
+
67
+ function truncateName(name: string): string {
68
+ return name.length > MAX_SKILL_NAME_LEN
69
+ ? name.slice(0, MAX_SKILL_NAME_LEN - 1) + "\u2026"
70
+ : name;
71
+ }
72
+
65
73
  const skillRenderer: ItemRenderer<SkillSkillItem> = {
66
74
  renderRow: ({ item, isSelected }) => {
67
75
  const { skill } = item;
68
76
  const hasUser = skill.installedScope === "user";
69
77
  const hasProject = skill.installedScope === "project";
70
78
  const starsStr = formatStars(skill.stars);
79
+ const displayName = truncateName(skill.name);
71
80
 
72
81
  return (
73
82
  <SelectableRow selected={isSelected} indent={1}>
74
83
  <ScopeSquares user={hasUser} project={hasProject} selected={isSelected} />
75
84
  <span> </span>
76
85
  <span fg={isSelected ? "white" : skill.installed ? "white" : "gray"}>
77
- {skill.name}
86
+ {displayName}
78
87
  </span>
79
88
  {skill.hasUpdate ? <MetaText text=" ⬆" tone="warning" /> : null}
80
89
  {starsStr ? <MetaText text={` ${starsStr}`} tone="warning" /> : null}
@@ -129,46 +138,48 @@ const skillRenderer: ItemRenderer<SkillSkillItem> = {
129
138
  </text>
130
139
  </DetailSection>
131
140
 
132
- {skill.installed && skill.installedScope && (
133
- <DetailSection>
134
- <text>{"─".repeat(24)}</text>
135
- <ScopeDetail
136
- scopes={{
137
- user: skill.installedScope === "user",
138
- project: skill.installedScope === "project",
139
- }}
140
- paths={{
141
- user: "~/.claude/skills/",
142
- project: ".claude/skills/",
143
- }}
144
- />
145
- </DetailSection>
146
- )}
141
+ <DetailSection>
142
+ <text>{"─".repeat(24)}</text>
143
+ <text><strong>Scopes:</strong></text>
144
+ <box marginTop={1} flexDirection="column">
145
+ <text>
146
+ <span bg="cyan" fg="black"> u </span>
147
+ <span fg={skill.installedScope === "user" ? "cyan" : "gray"}>
148
+ {skill.installedScope === "user" ? " ● " : " ○ "}
149
+ </span>
150
+ <span fg="cyan">User</span>
151
+ <span fg="gray"> ~/.claude/skills/</span>
152
+ </text>
153
+ <text>
154
+ <span bg="green" fg="black"> p </span>
155
+ <span fg={skill.installedScope === "project" ? "green" : "gray"}>
156
+ {skill.installedScope === "project" ? " ● " : " ○ "}
157
+ </span>
158
+ <span fg="green">Project</span>
159
+ <span fg="gray"> .claude/skills/</span>
160
+ </text>
161
+ </box>
162
+ </DetailSection>
147
163
 
148
164
  {skill.hasUpdate && (
149
165
  <box marginTop={1}>
150
- <text bg="yellow" fg="black">
151
- {" "}
152
- UPDATE AVAILABLE{" "}
153
- </text>
166
+ <text bg="yellow" fg="black"> UPDATE AVAILABLE </text>
154
167
  </box>
155
168
  )}
156
169
 
157
- <ActionHints
158
- hints={
159
- skill.installed
160
- ? [
161
- { key: "d", label: "Uninstall", tone: "danger" },
162
- { key: "u/p", label: "Reinstall in user/project scope" },
163
- { key: "o", label: "Open in browser" },
164
- ]
165
- : [
166
- { key: "u", label: "Install in user scope", tone: "primary" },
167
- { key: "p", label: "Install in project scope", tone: "primary" },
168
- { key: "o", label: "Open in browser" },
169
- ]
170
- }
171
- />
170
+ <box marginTop={1}>
171
+ <text fg="gray">
172
+ {skill.installed
173
+ ? "Press u/p to toggle scope"
174
+ : "Press u/p to install"}
175
+ </text>
176
+ </box>
177
+ <box>
178
+ <text>
179
+ <span bg="#555555" fg="white"> o </span>
180
+ <span fg="gray"> Open in browser</span>
181
+ </text>
182
+ </box>
172
183
  </box>
173
184
  );
174
185
  },
@@ -138,7 +138,7 @@ export function SkillsScreen() {
138
138
  installedScope: isProj ? "project" : isUser ? "user" : null,
139
139
  hasUpdate: false,
140
140
  isRecommended: true,
141
- stars: undefined,
141
+ stars: r.stars,
142
142
  };
143
143
  });
144
144
  }, [installedFromDisk]);
@@ -148,7 +148,10 @@ export function SkillsScreen() {
148
148
  const fetched = skillsState.skills.data.filter((s) => s.isRecommended);
149
149
  return staticRecommended.map((staticSkill) => {
150
150
  const match = fetched.find((f) => f.source.repo === staticSkill.source.repo && f.name === staticSkill.name);
151
- return match || staticSkill;
151
+ if (!match)
152
+ return staticSkill;
153
+ // Merge: prefer fetched data but keep static stars as fallback
154
+ return { ...staticSkill, ...match, stars: match.stars || staticSkill.stars };
152
155
  });
153
156
  }, [staticRecommended, skillsState.skills]);
154
157
  const installedSkills = useMemo(() => {
@@ -323,9 +326,6 @@ export function SkillsScreen() {
323
326
  else
324
327
  handleInstall("project");
325
328
  }
326
- else if (event.name === "d" && selectedSkill?.installed) {
327
- handleUninstall();
328
- }
329
329
  else if (event.name === "r") {
330
330
  fetchData();
331
331
  }
@@ -161,7 +161,7 @@ export function SkillsScreen() {
161
161
  installedScope: isProj ? "project" : isUser ? "user" : null,
162
162
  hasUpdate: false,
163
163
  isRecommended: true,
164
- stars: undefined,
164
+ stars: r.stars,
165
165
  };
166
166
  });
167
167
  }, [installedFromDisk]);
@@ -173,7 +173,9 @@ export function SkillsScreen() {
173
173
  const match = fetched.find(
174
174
  (f) => f.source.repo === staticSkill.source.repo && f.name === staticSkill.name,
175
175
  );
176
- return match || staticSkill;
176
+ if (!match) return staticSkill;
177
+ // Merge: prefer fetched data but keep static stars as fallback
178
+ return { ...staticSkill, ...match, stars: match.stars || staticSkill.stars };
177
179
  });
178
180
  }, [staticRecommended, skillsState.skills]);
179
181
 
@@ -365,8 +367,6 @@ export function SkillsScreen() {
365
367
  } else if (event.name === "p" && selectedSkill) {
366
368
  if (selectedSkill.installed && selectedSkill.installedScope === "project") handleUninstall();
367
369
  else handleInstall("project");
368
- } else if (event.name === "d" && selectedSkill?.installed) {
369
- handleUninstall();
370
370
  } else if (event.name === "r") {
371
371
  fetchData();
372
372
  } else if (event.name === "o" && selectedSkill) {