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.
- package/package.json +1 -1
- package/src/data/predefined-profiles.js +191 -0
- package/src/data/predefined-profiles.ts +205 -0
- package/src/data/skill-repos.js +11 -0
- package/src/data/skill-repos.ts +12 -0
- package/src/services/skills-manager.js +40 -13
- package/src/services/skills-manager.ts +38 -16
- package/src/ui/adapters/skillsAdapter.js +5 -4
- package/src/ui/adapters/skillsAdapter.ts +5 -4
- package/src/ui/renderers/skillRenderers.js +12 -19
- package/src/ui/renderers/skillRenderers.tsx +46 -35
- package/src/ui/screens/ProfilesScreen.js +121 -20
- package/src/ui/screens/ProfilesScreen.tsx +248 -52
- package/src/ui/screens/SkillsScreen.js +5 -5
- package/src/ui/screens/SkillsScreen.tsx +4 -4
|
@@ -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
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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:
|
|
93
|
+
count: popularWithStars.length,
|
|
93
94
|
tone: "teal",
|
|
94
95
|
});
|
|
95
|
-
for (const skill of
|
|
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
|
-
|
|
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:
|
|
146
|
+
count: popularWithStars.length,
|
|
146
147
|
tone: "teal",
|
|
147
148
|
});
|
|
148
|
-
for (const skill of
|
|
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,
|
|
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
|
-
|
|
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.
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
<
|
|
158
|
-
|
|
159
|
-
skill.installed
|
|
160
|
-
?
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
72
|
+
if (selectedItem?.kind === "saved")
|
|
73
|
+
void handleRename();
|
|
53
74
|
}
|
|
54
75
|
else if (event.name === "d") {
|
|
55
|
-
|
|
76
|
+
if (selectedItem?.kind === "saved")
|
|
77
|
+
void handleDelete();
|
|
56
78
|
}
|
|
57
79
|
else if (event.name === "c") {
|
|
58
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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,
|
|
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 (
|
|
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 = (
|
|
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 (
|
|
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: "
|
|
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:
|
|
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;
|