claudeup 3.7.2 → 3.9.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.
Files changed (52) hide show
  1. package/package.json +1 -1
  2. package/src/data/settings-catalog.js +612 -0
  3. package/src/data/settings-catalog.ts +689 -0
  4. package/src/data/skill-repos.js +86 -0
  5. package/src/data/skill-repos.ts +97 -0
  6. package/src/services/plugin-manager.js +2 -0
  7. package/src/services/plugin-manager.ts +3 -0
  8. package/src/services/profiles.js +161 -0
  9. package/src/services/profiles.ts +225 -0
  10. package/src/services/settings-manager.js +108 -0
  11. package/src/services/settings-manager.ts +140 -0
  12. package/src/services/skills-manager.js +239 -0
  13. package/src/services/skills-manager.ts +328 -0
  14. package/src/services/skillsmp-client.js +67 -0
  15. package/src/services/skillsmp-client.ts +89 -0
  16. package/src/types/index.ts +101 -1
  17. package/src/ui/App.js +23 -18
  18. package/src/ui/App.tsx +27 -23
  19. package/src/ui/components/TabBar.js +9 -8
  20. package/src/ui/components/TabBar.tsx +15 -19
  21. package/src/ui/components/layout/ScreenLayout.js +8 -14
  22. package/src/ui/components/layout/ScreenLayout.tsx +51 -58
  23. package/src/ui/components/modals/ModalContainer.js +43 -11
  24. package/src/ui/components/modals/ModalContainer.tsx +44 -12
  25. package/src/ui/components/modals/SelectModal.js +4 -18
  26. package/src/ui/components/modals/SelectModal.tsx +10 -21
  27. package/src/ui/screens/CliToolsScreen.js +2 -2
  28. package/src/ui/screens/CliToolsScreen.tsx +8 -8
  29. package/src/ui/screens/EnvVarsScreen.js +248 -116
  30. package/src/ui/screens/EnvVarsScreen.tsx +419 -184
  31. package/src/ui/screens/McpRegistryScreen.tsx +18 -6
  32. package/src/ui/screens/McpScreen.js +1 -1
  33. package/src/ui/screens/McpScreen.tsx +15 -5
  34. package/src/ui/screens/ModelSelectorScreen.js +3 -5
  35. package/src/ui/screens/ModelSelectorScreen.tsx +12 -16
  36. package/src/ui/screens/PluginsScreen.js +154 -66
  37. package/src/ui/screens/PluginsScreen.tsx +280 -97
  38. package/src/ui/screens/ProfilesScreen.js +255 -0
  39. package/src/ui/screens/ProfilesScreen.tsx +487 -0
  40. package/src/ui/screens/SkillsScreen.js +325 -0
  41. package/src/ui/screens/SkillsScreen.tsx +574 -0
  42. package/src/ui/screens/StatusLineScreen.js +2 -2
  43. package/src/ui/screens/StatusLineScreen.tsx +10 -12
  44. package/src/ui/screens/index.js +3 -2
  45. package/src/ui/screens/index.ts +3 -2
  46. package/src/ui/state/AppContext.js +2 -1
  47. package/src/ui/state/AppContext.tsx +2 -0
  48. package/src/ui/state/reducer.js +151 -19
  49. package/src/ui/state/reducer.ts +167 -19
  50. package/src/ui/state/types.ts +58 -14
  51. package/src/utils/clipboard.js +56 -0
  52. package/src/utils/clipboard.ts +58 -0
@@ -0,0 +1,325 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
+ import { useEffect, useCallback, useMemo } from "react";
3
+ import { useApp, useModal } from "../state/AppContext.js";
4
+ import { useDimensions } from "../state/DimensionsContext.js";
5
+ import { useKeyboard } from "../hooks/useKeyboard.js";
6
+ import { ScreenLayout } from "../components/layout/index.js";
7
+ import { ScrollableList } from "../components/ScrollableList.js";
8
+ import { fetchAvailableSkills, fetchSkillFrontmatter, installSkill, uninstallSkill, } from "../../services/skills-manager.js";
9
+ import { DEFAULT_SKILL_REPOS, RECOMMENDED_SKILLS } from "../../data/skill-repos.js";
10
+ const CATEGORY_COLORS = {
11
+ recommended: "#2e7d32",
12
+ frontend: "#1565c0",
13
+ design: "#6a1b9a",
14
+ media: "#e65100",
15
+ security: "#b71c1c",
16
+ debugging: "#00838f",
17
+ database: "#4527a0",
18
+ search: "#4e342e",
19
+ general: "#333333",
20
+ };
21
+ const RECOMMENDED_NAMES = new Set(RECOMMENDED_SKILLS.map((r) => r.name));
22
+ export function SkillsScreen() {
23
+ const { state, dispatch } = useApp();
24
+ const { skills: skillsState } = state;
25
+ const modal = useModal();
26
+ const dimensions = useDimensions();
27
+ const isSearchActive = state.isSearching &&
28
+ state.currentRoute.screen === "skills" &&
29
+ !state.modal;
30
+ // Fetch data
31
+ const fetchData = useCallback(async () => {
32
+ dispatch({ type: "SKILLS_DATA_LOADING" });
33
+ try {
34
+ const skills = await fetchAvailableSkills(DEFAULT_SKILL_REPOS, state.projectPath);
35
+ dispatch({ type: "SKILLS_DATA_SUCCESS", skills });
36
+ }
37
+ catch (error) {
38
+ dispatch({
39
+ type: "SKILLS_DATA_ERROR",
40
+ error: error instanceof Error ? error : new Error(String(error)),
41
+ });
42
+ }
43
+ }, [dispatch, state.projectPath]);
44
+ useEffect(() => {
45
+ fetchData();
46
+ }, [fetchData, state.dataRefreshVersion]);
47
+ // Build flat list: recommended first (as their own category), then by repo
48
+ const allItems = useMemo(() => {
49
+ if (skillsState.skills.status !== "success")
50
+ return [];
51
+ const skills = skillsState.skills.data;
52
+ const query = skillsState.searchQuery.toLowerCase();
53
+ const filtered = query
54
+ ? skills.filter((s) => s.name.toLowerCase().includes(query) ||
55
+ s.source.repo.toLowerCase().includes(query) ||
56
+ s.frontmatter?.description?.toLowerCase().includes(query))
57
+ : skills;
58
+ const items = [];
59
+ // Recommended section
60
+ const recommendedSkills = filtered.filter((s) => RECOMMENDED_NAMES.has(s.name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())) || RECOMMENDED_SKILLS.some((r) => r.skillPath === s.name));
61
+ if (recommendedSkills.length > 0) {
62
+ items.push({
63
+ id: "cat:recommended",
64
+ type: "category",
65
+ label: "Recommended",
66
+ categoryKey: "recommended",
67
+ });
68
+ for (const skill of recommendedSkills) {
69
+ items.push({
70
+ id: `skill:${skill.id}`,
71
+ type: "skill",
72
+ label: skill.name,
73
+ skill,
74
+ });
75
+ }
76
+ }
77
+ // Group remaining skills by repo
78
+ const repoMap = new Map();
79
+ for (const skill of filtered) {
80
+ const isRec = recommendedSkills.includes(skill);
81
+ if (isRec)
82
+ continue;
83
+ const existing = repoMap.get(skill.source.repo) || [];
84
+ existing.push(skill);
85
+ repoMap.set(skill.source.repo, existing);
86
+ }
87
+ for (const [repo, repoSkills] of repoMap) {
88
+ items.push({
89
+ id: `cat:${repo}`,
90
+ type: "category",
91
+ label: `${repo} (${repoSkills.length})`,
92
+ categoryKey: repo,
93
+ });
94
+ for (const skill of repoSkills) {
95
+ items.push({
96
+ id: `skill:${skill.id}`,
97
+ type: "skill",
98
+ label: skill.name,
99
+ skill,
100
+ });
101
+ }
102
+ }
103
+ return items;
104
+ }, [skillsState.skills, skillsState.searchQuery]);
105
+ const selectableItems = useMemo(() => allItems.filter((item) => item.type === "skill" || item.type === "category"), [allItems]);
106
+ const selectedItem = selectableItems[skillsState.selectedIndex];
107
+ const selectedSkill = selectedItem?.type === "skill" ? selectedItem.skill : undefined;
108
+ // Lazy-load frontmatter for selected skill
109
+ useEffect(() => {
110
+ if (!selectedSkill || selectedSkill.frontmatter)
111
+ return;
112
+ fetchSkillFrontmatter(selectedSkill).then((fm) => {
113
+ dispatch({
114
+ type: "SKILLS_UPDATE_ITEM",
115
+ name: selectedSkill.name,
116
+ updates: { frontmatter: fm },
117
+ });
118
+ }).catch(() => { });
119
+ }, [selectedSkill?.id, dispatch]);
120
+ // Install handler
121
+ const handleInstall = useCallback(async (scope) => {
122
+ if (!selectedSkill)
123
+ return;
124
+ modal.loading(`Installing ${selectedSkill.name}...`);
125
+ try {
126
+ await installSkill(selectedSkill, scope, state.projectPath);
127
+ modal.hideModal();
128
+ dispatch({
129
+ type: "SKILLS_UPDATE_ITEM",
130
+ name: selectedSkill.name,
131
+ updates: {
132
+ installed: true,
133
+ installedScope: scope,
134
+ },
135
+ });
136
+ await modal.message("Installed", `${selectedSkill.name} installed to ${scope === "user" ? "~/.claude/skills/" : ".claude/skills/"}`, "success");
137
+ }
138
+ catch (error) {
139
+ modal.hideModal();
140
+ await modal.message("Error", `Failed to install: ${error}`, "error");
141
+ }
142
+ }, [selectedSkill, state.projectPath, dispatch, modal]);
143
+ // Uninstall handler
144
+ const handleUninstall = useCallback(async () => {
145
+ if (!selectedSkill || !selectedSkill.installed)
146
+ return;
147
+ const scope = selectedSkill.installedScope;
148
+ if (!scope)
149
+ return;
150
+ const confirmed = await modal.confirm(`Uninstall "${selectedSkill.name}"?`, `This will remove it from the ${scope} scope.`);
151
+ if (!confirmed)
152
+ return;
153
+ modal.loading(`Uninstalling ${selectedSkill.name}...`);
154
+ try {
155
+ await uninstallSkill(selectedSkill.name, scope, state.projectPath);
156
+ modal.hideModal();
157
+ dispatch({
158
+ type: "SKILLS_UPDATE_ITEM",
159
+ name: selectedSkill.name,
160
+ updates: {
161
+ installed: false,
162
+ installedScope: null,
163
+ },
164
+ });
165
+ await modal.message("Uninstalled", `${selectedSkill.name} removed.`, "success");
166
+ }
167
+ catch (error) {
168
+ modal.hideModal();
169
+ await modal.message("Error", `Failed to uninstall: ${error}`, "error");
170
+ }
171
+ }, [selectedSkill, state.projectPath, dispatch, modal]);
172
+ // Keyboard handling
173
+ useKeyboard((event) => {
174
+ if (state.modal)
175
+ return;
176
+ if (event.name === "up" || event.name === "k") {
177
+ if (state.isSearching)
178
+ return;
179
+ const newIndex = Math.max(0, skillsState.selectedIndex - 1);
180
+ dispatch({ type: "SKILLS_SELECT", index: newIndex });
181
+ }
182
+ else if (event.name === "down" || event.name === "j") {
183
+ if (state.isSearching)
184
+ return;
185
+ const newIndex = Math.min(Math.max(0, selectableItems.length - 1), skillsState.selectedIndex + 1);
186
+ dispatch({ type: "SKILLS_SELECT", index: newIndex });
187
+ }
188
+ else if (event.name === "u") {
189
+ if (state.isSearching)
190
+ return;
191
+ if (selectedSkill) {
192
+ if (selectedSkill.installed && selectedSkill.installedScope === "user") {
193
+ handleUninstall();
194
+ }
195
+ else {
196
+ handleInstall("user");
197
+ }
198
+ }
199
+ }
200
+ else if (event.name === "p") {
201
+ if (state.isSearching)
202
+ return;
203
+ if (selectedSkill) {
204
+ if (selectedSkill.installed && selectedSkill.installedScope === "project") {
205
+ handleUninstall();
206
+ }
207
+ else {
208
+ handleInstall("project");
209
+ }
210
+ }
211
+ }
212
+ else if (event.name === "return" || event.name === "enter") {
213
+ if (state.isSearching)
214
+ return;
215
+ if (selectedSkill && !selectedSkill.installed) {
216
+ handleInstall("project");
217
+ }
218
+ }
219
+ else if (event.name === "d") {
220
+ if (state.isSearching)
221
+ return;
222
+ if (selectedSkill?.installed) {
223
+ handleUninstall();
224
+ }
225
+ }
226
+ else if (event.name === "r") {
227
+ if (state.isSearching)
228
+ return;
229
+ fetchData();
230
+ }
231
+ else if (event.name === "escape") {
232
+ if (skillsState.searchQuery) {
233
+ dispatch({ type: "SKILLS_SET_SEARCH", query: "" });
234
+ dispatch({ type: "SET_SEARCHING", isSearching: false });
235
+ }
236
+ }
237
+ else if (event.name === "backspace") {
238
+ if (skillsState.searchQuery) {
239
+ const newQuery = skillsState.searchQuery.slice(0, -1);
240
+ dispatch({ type: "SKILLS_SET_SEARCH", query: newQuery });
241
+ if (!newQuery) {
242
+ dispatch({ type: "SET_SEARCHING", isSearching: false });
243
+ }
244
+ }
245
+ }
246
+ else if (event.name &&
247
+ event.name.length === 1 &&
248
+ !/[0-9]/.test(event.name) &&
249
+ !event.ctrl &&
250
+ !event.meta) {
251
+ // Inline search: type to filter
252
+ const newQuery = skillsState.searchQuery + event.name;
253
+ dispatch({ type: "SKILLS_SET_SEARCH", query: newQuery });
254
+ dispatch({ type: "SET_SEARCHING", isSearching: true });
255
+ }
256
+ });
257
+ const renderListItem = (item, _idx, isSelected) => {
258
+ if (item.type === "category") {
259
+ const catKey = item.categoryKey || "general";
260
+ const isRec = catKey === "recommended";
261
+ const bgColor = CATEGORY_COLORS[catKey] || CATEGORY_COLORS.general;
262
+ const star = isRec ? "★ " : "";
263
+ if (isSelected) {
264
+ return (_jsx("text", { bg: "magenta", fg: "white", children: _jsxs("strong", { children: [" ", star, item.label, " "] }) }));
265
+ }
266
+ return (_jsx("text", { bg: bgColor, fg: "white", children: _jsxs("strong", { children: [" ", star, item.label, " "] }) }));
267
+ }
268
+ if (item.type === "skill" && item.skill) {
269
+ const skill = item.skill;
270
+ const indicator = skill.installed ? "●" : "○";
271
+ const indicatorColor = skill.installed ? "cyan" : "gray";
272
+ const scopeLabel = skill.installedScope
273
+ ? skill.installedScope === "user"
274
+ ? "[u]"
275
+ : "[p]"
276
+ : " ";
277
+ const updateBadge = skill.hasUpdate ? "[UPDT] " : "";
278
+ if (isSelected) {
279
+ return (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", indicator, " ", scopeLabel, " ", updateBadge, skill.name.padEnd(30), skill.source.repo, " "] }));
280
+ }
281
+ return (_jsxs("text", { children: [_jsxs("span", { fg: indicatorColor, children: [" ", indicator, " "] }), _jsx("span", { fg: skill.installedScope === "user" ? "cyan" : skill.installedScope === "project" ? "green" : "gray", children: scopeLabel }), _jsx("span", { children: " " }), skill.hasUpdate && (_jsx("span", { bg: "yellow", fg: "black", children: updateBadge })), _jsx("span", { fg: "white", children: skill.name.padEnd(30) }), _jsx("span", { fg: "gray", children: skill.source.repo })] }));
282
+ }
283
+ return _jsx("text", { fg: "gray", children: item.label });
284
+ };
285
+ const renderDetail = () => {
286
+ if (skillsState.skills.status === "loading") {
287
+ return _jsx("text", { fg: "gray", children: "Loading skills..." });
288
+ }
289
+ if (skillsState.skills.status === "error") {
290
+ return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "red", children: "Failed to load skills" }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: skillsState.skills.error.message }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: "Set GITHUB_TOKEN to increase rate limits." }) })] }));
291
+ }
292
+ if (!selectedItem) {
293
+ return _jsx("text", { fg: "gray", children: "Select a skill to see details" });
294
+ }
295
+ if (selectedItem.type === "category") {
296
+ const catKey = selectedItem.categoryKey || "general";
297
+ const color = CATEGORY_COLORS[catKey] ? "green" : "cyan";
298
+ return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: color, children: _jsx("strong", { children: selectedItem.label }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: "Skills in this category" }) })] }));
299
+ }
300
+ if (!selectedSkill)
301
+ return null;
302
+ const fm = selectedSkill.frontmatter;
303
+ const scopeColor = selectedSkill.installedScope === "user" ? "cyan" : "green";
304
+ return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "cyan", children: _jsx("strong", { children: selectedSkill.name }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "white", children: fm ? fm.description : "Loading..." }) }), fm?.category && (_jsx("box", { marginTop: 1, children: _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Category " }), _jsx("span", { fg: "cyan", children: fm.category })] }) })), fm?.author && (_jsx("box", { children: _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Author " }), _jsx("span", { children: fm.author })] }) })), fm?.version && (_jsx("box", { children: _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Version " }), _jsx("span", { children: fm.version })] }) })), fm?.tags && fm.tags.length > 0 && (_jsx("box", { children: _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Tags " }), _jsx("span", { children: fm.tags.join(", ") })] }) })), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Source " }), _jsx("span", { fg: "#5c9aff", children: selectedSkill.source.repo })] }), _jsxs("text", { children: [_jsx("span", { fg: "gray", children: " " }), _jsx("span", { fg: "gray", children: selectedSkill.repoPath })] })] }), selectedSkill.installed && selectedSkill.installedScope && (_jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("text", { children: "─".repeat(24) }), _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Installed " }), _jsxs("span", { fg: scopeColor, children: [selectedSkill.installedScope === "user"
305
+ ? "~/.claude/skills/"
306
+ : ".claude/skills/", selectedSkill.name, "/"] })] })] })), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("text", { children: "─".repeat(24) }), _jsx("text", { children: _jsx("strong", { children: "Install scope:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { bg: "cyan", fg: "black", children: " u " }), _jsx("span", { fg: selectedSkill.installedScope === "user" ? "cyan" : "gray", children: selectedSkill.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: selectedSkill.installedScope === "project" ? "green" : "gray", children: selectedSkill.installedScope === "project" ? " ● " : " ○ " }), _jsx("span", { fg: "green", children: "Project" }), _jsx("span", { fg: "gray", children: " .claude/skills/" })] })] })] }), selectedSkill.hasUpdate && (_jsx("box", { marginTop: 1, children: _jsx("text", { bg: "yellow", fg: "black", children: " UPDATE AVAILABLE " }) })), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [!selectedSkill.installed && (_jsx("text", { fg: "gray", children: "Press u/p to install in scope" })), selectedSkill.installed && (_jsx("text", { fg: "gray", children: "Press d to uninstall" }))] })] }));
307
+ };
308
+ const skills = skillsState.skills.status === "success" ? skillsState.skills.data : [];
309
+ const installedCount = skills.filter((s) => s.installed).length;
310
+ const totalCount = skills.length;
311
+ const updateCount = skills.filter((s) => s.hasUpdate).length;
312
+ const statusContent = (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Skills: " }), _jsxs("span", { fg: "cyan", children: [installedCount, " installed"] }), _jsxs("span", { fg: "gray", children: [" \u2502 ", totalCount, " available"] }), updateCount > 0 && (_jsxs("span", { fg: "yellow", children: [" \u2502 ", updateCount, " updates"] }))] }));
313
+ return (_jsx(ScreenLayout, { title: "claudeup Skills", currentScreen: "skills", statusLine: statusContent, search: skillsState.searchQuery || isSearchActive
314
+ ? {
315
+ isActive: isSearchActive,
316
+ query: skillsState.searchQuery,
317
+ placeholder: "type to search",
318
+ }
319
+ : undefined, footerHints: "\u2191\u2193:nav \u2502 u:user scope \u2502 p:project scope \u2502 Enter:install \u2502 d:uninstall \u2502 type to search", listPanel: skillsState.skills.status !== "success" ? (_jsx("text", { fg: "gray", children: skillsState.skills.status === "loading"
320
+ ? "Loading skills..."
321
+ : skillsState.skills.status === "error"
322
+ ? "Error loading skills"
323
+ : "Press r to load skills" })) : (_jsx(ScrollableList, { items: selectableItems, selectedIndex: skillsState.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight })), detailPanel: renderDetail() }));
324
+ }
325
+ export default SkillsScreen;