claudeup 3.15.0 → 3.17.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.
@@ -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
  },
@@ -6,7 +6,21 @@ import { useKeyboard } from "../hooks/useKeyboard.js";
6
6
  import { ScreenLayout } from "../components/layout/index.js";
7
7
  import { ScrollableList } from "../components/ScrollableList.js";
8
8
  import { listProfiles, applyProfile, renameProfile, deleteProfile, exportProfileToJson, importProfileFromJson, } from "../../services/profiles.js";
9
+ import { readSettings, writeSettings, } from "../../services/claude-settings.js";
9
10
  import { writeClipboard, readClipboard, ClipboardUnavailableError, } from "../../utils/clipboard.js";
11
+ import { PREDEFINED_PROFILES, } from "../../data/predefined-profiles.js";
12
+ function buildListItems(profileList) {
13
+ const predefined = PREDEFINED_PROFILES.map((p) => ({
14
+ kind: "predefined",
15
+ profile: p,
16
+ }));
17
+ const saved = profileList.map((e) => ({
18
+ kind: "saved",
19
+ entry: e,
20
+ }));
21
+ return [...predefined, ...saved];
22
+ }
23
+ // ─── Component ────────────────────────────────────────────────────────────────
10
24
  export function ProfilesScreen() {
11
25
  const { state, dispatch } = useApp();
12
26
  const { profiles: profilesState } = state;
@@ -32,7 +46,8 @@ export function ProfilesScreen() {
32
46
  const profileList = profilesState.profiles.status === "success"
33
47
  ? profilesState.profiles.data
34
48
  : [];
35
- const selectedProfile = profileList[profilesState.selectedIndex];
49
+ const allItems = buildListItems(profileList);
50
+ const selectedItem = allItems[profilesState.selectedIndex];
36
51
  // Keyboard handling
37
52
  useKeyboard((event) => {
38
53
  if (state.isSearching || state.modal)
@@ -42,28 +57,88 @@ export function ProfilesScreen() {
42
57
  dispatch({ type: "PROFILES_SELECT", index: newIndex });
43
58
  }
44
59
  else if (event.name === "down" || event.name === "j") {
45
- const newIndex = Math.min(Math.max(0, profileList.length - 1), profilesState.selectedIndex + 1);
60
+ const newIndex = Math.min(Math.max(0, allItems.length - 1), profilesState.selectedIndex + 1);
46
61
  dispatch({ type: "PROFILES_SELECT", index: newIndex });
47
62
  }
48
63
  else if (event.name === "enter" || event.name === "a") {
49
- handleApply();
64
+ if (selectedItem?.kind === "predefined") {
65
+ void handleApplyPredefined(selectedItem.profile);
66
+ }
67
+ else {
68
+ void handleApply();
69
+ }
50
70
  }
51
71
  else if (event.name === "r") {
52
- handleRename();
72
+ if (selectedItem?.kind === "saved")
73
+ void handleRename();
53
74
  }
54
75
  else if (event.name === "d") {
55
- handleDelete();
76
+ if (selectedItem?.kind === "saved")
77
+ void handleDelete();
56
78
  }
57
79
  else if (event.name === "c") {
58
- handleCopy();
80
+ if (selectedItem?.kind === "saved")
81
+ void handleCopy();
59
82
  }
60
83
  else if (event.name === "i") {
61
- handleImport();
84
+ void handleImport();
62
85
  }
63
86
  });
87
+ // ─── Predefined profile apply ─────────────────────────────────────────────
88
+ const handleApplyPredefined = async (profile) => {
89
+ const allPlugins = [
90
+ ...profile.magusPlugins.map((p) => `${p}@magus`),
91
+ ...profile.anthropicPlugins.map((p) => `${p}@claude-plugins-official`),
92
+ ];
93
+ const settingsCount = Object.keys(profile.settings).length;
94
+ const confirmed = await modal.confirm(`Apply ${profile.name}?`, `This will add ${allPlugins.length} plugins, ${profile.skills.length} skills, and update ${settingsCount} settings.\n\nSettings are merged additively — existing values are kept.`);
95
+ if (!confirmed)
96
+ return;
97
+ modal.loading(`Applying "${profile.name}"...`);
98
+ try {
99
+ const settings = await readSettings(state.projectPath);
100
+ // Merge plugins (additive only)
101
+ settings.enabledPlugins = settings.enabledPlugins ?? {};
102
+ for (const plugin of allPlugins) {
103
+ if (!settings.enabledPlugins[plugin]) {
104
+ settings.enabledPlugins[plugin] = true;
105
+ }
106
+ }
107
+ // Merge top-level settings (additive — only set if not already set)
108
+ for (const [key, value] of Object.entries(profile.settings)) {
109
+ if (key === "env") {
110
+ const envMap = value;
111
+ const existing = settings;
112
+ const existingEnv = existing["env"] ?? {};
113
+ for (const [envKey, envVal] of Object.entries(envMap)) {
114
+ if (!existingEnv[envKey]) {
115
+ existingEnv[envKey] = envVal;
116
+ }
117
+ }
118
+ settings["env"] = existingEnv;
119
+ }
120
+ else {
121
+ const settingsMap = settings;
122
+ if (settingsMap[key] === undefined) {
123
+ settingsMap[key] = value;
124
+ }
125
+ }
126
+ }
127
+ await writeSettings(settings, state.projectPath);
128
+ modal.hideModal();
129
+ dispatch({ type: "DATA_REFRESH_COMPLETE" });
130
+ await modal.message("Applied", `Profile "${profile.name}" merged into project settings.`, "success");
131
+ }
132
+ catch (error) {
133
+ modal.hideModal();
134
+ await modal.message("Error", `Failed to apply profile: ${error}`, "error");
135
+ }
136
+ };
137
+ // ─── Saved profile actions ────────────────────────────────────────────────
64
138
  const handleApply = async () => {
65
- if (!selectedProfile)
139
+ if (selectedItem?.kind !== "saved")
66
140
  return;
141
+ const selectedProfile = selectedItem.entry;
67
142
  const scopeChoice = await modal.select("Apply Profile", `Apply "${selectedProfile.name}" to which scope?`, [
68
143
  { label: "User — ~/.claude/settings.json (global)", value: "user" },
69
144
  {
@@ -98,8 +173,9 @@ export function ProfilesScreen() {
98
173
  }
99
174
  };
100
175
  const handleRename = async () => {
101
- if (!selectedProfile)
176
+ if (selectedItem?.kind !== "saved")
102
177
  return;
178
+ const selectedProfile = selectedItem.entry;
103
179
  const newName = await modal.input("Rename Profile", "New name:", selectedProfile.name);
104
180
  if (newName === null || !newName.trim())
105
181
  return;
@@ -116,8 +192,9 @@ export function ProfilesScreen() {
116
192
  }
117
193
  };
118
194
  const handleDelete = async () => {
119
- if (!selectedProfile)
195
+ if (selectedItem?.kind !== "saved")
120
196
  return;
197
+ const selectedProfile = selectedItem.entry;
121
198
  const confirmed = await modal.confirm(`Delete "${selectedProfile.name}"?`, "This will permanently remove the profile.");
122
199
  if (!confirmed)
123
200
  return;
@@ -126,7 +203,7 @@ export function ProfilesScreen() {
126
203
  await deleteProfile(selectedProfile.id, selectedProfile.scope, state.projectPath);
127
204
  modal.hideModal();
128
205
  // Adjust selection if we deleted the last item
129
- const newIndex = Math.max(0, Math.min(profilesState.selectedIndex, profileList.length - 2));
206
+ const newIndex = Math.max(0, Math.min(profilesState.selectedIndex, allItems.length - 2));
130
207
  dispatch({ type: "PROFILES_SELECT", index: newIndex });
131
208
  await fetchData();
132
209
  await modal.message("Deleted", "Profile deleted.", "success");
@@ -137,8 +214,9 @@ export function ProfilesScreen() {
137
214
  }
138
215
  };
139
216
  const handleCopy = async () => {
140
- if (!selectedProfile)
217
+ if (selectedItem?.kind !== "saved")
141
218
  return;
219
+ const selectedProfile = selectedItem.entry;
142
220
  modal.loading("Exporting...");
143
221
  try {
144
222
  const json = await exportProfileToJson(selectedProfile.id, selectedProfile.scope, state.projectPath);
@@ -202,6 +280,7 @@ export function ProfilesScreen() {
202
280
  await modal.message("Error", `Failed to import: ${error}`, "error");
203
281
  }
204
282
  };
283
+ // ─── Rendering helpers ────────────────────────────────────────────────────
205
284
  const formatDate = (iso) => {
206
285
  try {
207
286
  const d = new Date(iso);
@@ -215,7 +294,18 @@ export function ProfilesScreen() {
215
294
  return iso;
216
295
  }
217
296
  };
218
- const renderListItem = (entry, _idx, isSelected) => {
297
+ const renderListItem = (item, _idx, isSelected) => {
298
+ if (item.kind === "predefined") {
299
+ const { profile } = item;
300
+ const pluginCount = profile.magusPlugins.length + profile.anthropicPlugins.length;
301
+ const skillCount = profile.skills.length;
302
+ if (isSelected) {
303
+ return (_jsxs("text", { bg: "blue", fg: "white", children: [" ", profile.icon, " ", profile.name, " \u2014 ", pluginCount, " plugins \u00B7 ", skillCount, " ", "skill", skillCount !== 1 ? "s" : "", " "] }));
304
+ }
305
+ return (_jsxs("text", { children: [_jsx("span", { fg: "blue", children: "[preset]" }), _jsx("span", { children: " " }), _jsxs("span", { fg: "white", children: [profile.icon, " ", profile.name] }), _jsxs("span", { fg: "gray", children: [" ", "\u2014 ", pluginCount, " plugins \u00B7 ", skillCount, " skill", skillCount !== 1 ? "s" : ""] })] }));
306
+ }
307
+ // Saved profile
308
+ const { entry } = item;
219
309
  const pluginCount = Object.keys(entry.plugins).length;
220
310
  const dateStr = formatDate(entry.updatedAt);
221
311
  const scopeColor = entry.scope === "user" ? "cyan" : "green";
@@ -232,12 +322,24 @@ export function ProfilesScreen() {
232
322
  if (profilesState.profiles.status === "error") {
233
323
  return (_jsxs("text", { fg: "red", children: ["Error: ", profilesState.profiles.error.message] }));
234
324
  }
235
- if (profileList.length === 0) {
236
- return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "gray", children: "No profiles yet." }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "green", children: "Press 's' in the Plugins screen to save the current selection as a profile." }) })] }));
237
- }
238
- if (!selectedProfile) {
325
+ if (!selectedItem) {
239
326
  return _jsx("text", { fg: "gray", children: "Select a profile to see details" });
240
327
  }
328
+ if (selectedItem.kind === "predefined") {
329
+ return renderPredefinedDetail(selectedItem.profile);
330
+ }
331
+ return renderSavedDetail(selectedItem.entry);
332
+ };
333
+ const renderPredefinedDetail = (profile) => {
334
+ const allPlugins = [
335
+ ...profile.magusPlugins.map((p) => `${p}@magus`),
336
+ ...profile.anthropicPlugins.map((p) => `${p}@claude-plugins-official`),
337
+ ];
338
+ return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "blue", children: _jsxs("strong", { children: [profile.icon, " ", profile.name] }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: profile.description }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { fg: "gray", children: ["Magus plugins (", profile.magusPlugins.length, "):"] }), profile.magusPlugins.map((p) => (_jsx("box", { children: _jsxs("text", { fg: "cyan", children: [" ", p, "@magus"] }) }, p)))] }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { fg: "gray", children: ["Anthropic plugins (", profile.anthropicPlugins.length, "):"] }), profile.anthropicPlugins.map((p) => (_jsx("box", { children: _jsxs("text", { fg: "yellow", children: [" ", p, "@claude-plugins-official"] }) }, p)))] }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { fg: "gray", children: ["Skills (", profile.skills.length, "):"] }), profile.skills.map((s) => (_jsx("box", { children: _jsxs("text", { fg: "white", children: [" ", s] }) }, s)))] }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { fg: "gray", children: ["Settings (", Object.keys(profile.settings).length, "):"] }), Object.entries(profile.settings)
339
+ .filter(([k]) => k !== "env")
340
+ .map(([k, v]) => (_jsx("box", { children: _jsxs("text", { fg: "white", children: [" ", k, ": ", String(v)] }) }, k)))] }), _jsx("box", { marginTop: 2, flexDirection: "column", children: _jsxs("box", { children: [_jsxs("text", { bg: "blue", fg: "white", children: [" ", "Enter/a", " "] }), _jsxs("text", { fg: "gray", children: [" ", "Apply (merges ", allPlugins.length, " plugins into project settings)"] })] }) })] }));
341
+ };
342
+ const renderSavedDetail = (selectedProfile) => {
241
343
  const plugins = Object.keys(selectedProfile.plugins);
242
344
  const scopeColor = selectedProfile.scope === "user" ? "cyan" : "green";
243
345
  const scopeLabel = selectedProfile.scope === "user"
@@ -248,8 +350,7 @@ export function ProfilesScreen() {
248
350
  const profileCount = profileList.length;
249
351
  const userCount = profileList.filter((p) => p.scope === "user").length;
250
352
  const projCount = profileList.filter((p) => p.scope === "project").length;
251
- const statusContent = (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Profiles: " }), _jsxs("span", { fg: "cyan", children: [userCount, " user"] }), _jsx("span", { fg: "gray", children: " + " }), _jsxs("span", { fg: "green", children: [projCount, " project"] }), _jsx("span", { fg: "gray", children: " = " }), _jsxs("span", { fg: "white", children: [profileCount, " total"] })] }));
252
- return (_jsx(ScreenLayout, { title: "claudeup Plugin Profiles", currentScreen: "profiles", statusLine: statusContent, footerHints: "\u2191\u2193:nav \u2502 Enter/a:apply \u2502 r:rename \u2502 d:delete \u2502 c:copy \u2502 i:import", listPanel: profileList.length === 0 &&
253
- profilesState.profiles.status !== "loading" ? (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "gray", children: "No profiles yet." }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "green", children: "Go to Plugins (1) and press 's' to save a profile." }) })] })) : (_jsx(ScrollableList, { items: profileList, selectedIndex: profilesState.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight })), detailPanel: renderDetail() }));
353
+ const statusContent = (_jsxs("text", { children: [_jsxs("span", { fg: "blue", children: [PREDEFINED_PROFILES.length, " presets"] }), _jsx("span", { fg: "gray", children: " + " }), _jsxs("span", { fg: "cyan", children: [userCount, " user"] }), _jsx("span", { fg: "gray", children: " + " }), _jsxs("span", { fg: "green", children: [projCount, " project"] }), _jsx("span", { fg: "gray", children: " = " }), _jsxs("span", { fg: "white", children: [PREDEFINED_PROFILES.length + profileCount, " total"] })] }));
354
+ return (_jsx(ScreenLayout, { title: "claudeup Plugin Profiles", currentScreen: "profiles", statusLine: statusContent, footerHints: "\u2191\u2193:nav \u2502 Enter/a:apply \u2502 r:rename \u2502 d:delete \u2502 c:copy \u2502 i:import", listPanel: _jsx(ScrollableList, { items: allItems, selectedIndex: profilesState.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight }), detailPanel: renderDetail() }));
254
355
  }
255
356
  export default ProfilesScreen;