claudeup 4.18.0 → 4.22.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 (191) hide show
  1. package/bin/claudeup.js +43 -19
  2. package/package.json +14 -19
  3. package/scripts/build-binaries.ts +77 -0
  4. package/scripts/check-optional-deps.ts +49 -0
  5. package/scripts/test-isolated.ts +43 -0
  6. package/src/__tests__/alias-adopt.test.ts +364 -0
  7. package/src/__tests__/alias-parser.test.ts +92 -0
  8. package/src/__tests__/alias-shell-writer.test.ts +7 -0
  9. package/src/__tests__/alias-store.test.ts +77 -0
  10. package/src/__tests__/cli-router.test.ts +65 -0
  11. package/src/__tests__/conventions-integration.test.ts +745 -0
  12. package/src/__tests__/conventions-manager.test.ts +1172 -0
  13. package/src/__tests__/doctor-bins.test.ts +35 -0
  14. package/src/__tests__/doctor.test.ts +450 -0
  15. package/src/__tests__/dual-write-prevention.test.ts +14 -4
  16. package/src/__tests__/file-locking.test.ts +208 -0
  17. package/src/__tests__/gitignore-prerun.test.ts +1 -0
  18. package/src/__tests__/install-command.test.ts +81 -0
  19. package/src/__tests__/install-plan.test.ts +98 -0
  20. package/src/__tests__/manifest.test.ts +136 -0
  21. package/src/__tests__/marketplace-refresh.test.ts +267 -0
  22. package/src/__tests__/plugin-requires.test.ts +133 -0
  23. package/src/__tests__/plugin-setup.test.ts +118 -0
  24. package/src/__tests__/profile-command.test.ts +116 -0
  25. package/src/__tests__/profile-materializer.test.ts +82 -0
  26. package/src/__tests__/profile-sync.test.ts +136 -0
  27. package/src/__tests__/registry-version-resolution.test.ts +74 -0
  28. package/src/__tests__/resolve-executable.test.ts +42 -0
  29. package/src/__tests__/resolver.test.ts +218 -0
  30. package/src/__tests__/symlink-manager.test.ts +99 -0
  31. package/src/__tests__/toolchain.test.ts +56 -0
  32. package/src/cli/claude.ts +21 -0
  33. package/src/cli/doctor.ts +132 -0
  34. package/src/cli/install.ts +360 -0
  35. package/src/cli/profile.ts +166 -0
  36. package/src/cli/router.ts +107 -0
  37. package/src/cli/update.ts +85 -0
  38. package/src/data/alias-flags.ts +11 -1
  39. package/src/data/cli-tools.ts +7 -7
  40. package/src/data/gitignore-defaults.ts +4 -0
  41. package/src/data/marketplaces.ts +12 -0
  42. package/src/data/predefined-profiles.ts +3 -4
  43. package/src/main.tsx +11 -154
  44. package/src/prerunner/index.ts +62 -27
  45. package/src/services/alias-shell-writer.ts +382 -8
  46. package/src/services/alias-store.ts +60 -0
  47. package/src/services/claude-cli.ts +19 -22
  48. package/src/services/claude-settings.ts +99 -26
  49. package/src/services/conventions-manager.ts +865 -0
  50. package/src/services/doctor-bins.ts +49 -0
  51. package/src/services/doctor.ts +509 -0
  52. package/src/services/install-plan.ts +107 -0
  53. package/src/services/manifest.ts +166 -0
  54. package/src/services/marketplace-refresh.ts +173 -0
  55. package/src/services/plugin-manager.ts +7 -1
  56. package/src/services/plugin-requires.ts +215 -0
  57. package/src/services/plugin-setup.ts +61 -4
  58. package/src/services/plugin-version-check.ts +9 -4
  59. package/src/services/profile-materializer.ts +61 -0
  60. package/src/services/profile-sync.ts +150 -0
  61. package/src/services/resolver.ts +293 -0
  62. package/src/services/symlink-manager.ts +146 -0
  63. package/src/services/toolchain.ts +97 -0
  64. package/src/services/version-check.ts +10 -11
  65. package/src/types/index.ts +138 -31
  66. package/src/ui/App.tsx +16 -11
  67. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  68. package/src/ui/components/modals/ConfirmModal.tsx +1 -1
  69. package/src/ui/screens/AliasScreen.tsx +491 -359
  70. package/src/ui/screens/CliToolsScreen.tsx +11 -3
  71. package/src/ui/screens/PluginsScreen.tsx +3 -1
  72. package/src/ui/screens/index.ts +0 -1
  73. package/src/ui/state/reducer.ts +6 -102
  74. package/src/ui/state/types.ts +8 -35
  75. package/src/utils/command-utils.ts +23 -0
  76. package/src/utils/file-locking.ts +144 -0
  77. package/src/data/alias-flags.js +0 -196
  78. package/src/data/cli-tools.js +0 -123
  79. package/src/data/gitignore-defaults.js +0 -24
  80. package/src/data/gitignore-reasons.js +0 -97
  81. package/src/data/gitignore-templates.js +0 -21
  82. package/src/data/marketplaces.js +0 -138
  83. package/src/data/mcp-servers.js +0 -509
  84. package/src/data/predefined-profiles.js +0 -248
  85. package/src/data/settings-catalog.js +0 -625
  86. package/src/data/skill-repos.js +0 -160
  87. package/src/data/statuslines.js +0 -159
  88. package/src/data/statuslines.ts +0 -188
  89. package/src/index.js +0 -4
  90. package/src/index.ts +0 -5
  91. package/src/main.js +0 -170
  92. package/src/prerunner/index.js +0 -252
  93. package/src/services/alias-settings.js +0 -51
  94. package/src/services/alias-shell-writer.js +0 -764
  95. package/src/services/alias-store.js +0 -77
  96. package/src/services/claude-cli.js +0 -189
  97. package/src/services/claude-runner.js +0 -28
  98. package/src/services/claude-settings.js +0 -1223
  99. package/src/services/gitignore-detector.js +0 -155
  100. package/src/services/gitignore-fixer.js +0 -231
  101. package/src/services/gitignore-prerun.js +0 -46
  102. package/src/services/gitignore-resolver.js +0 -143
  103. package/src/services/gitignore-service.js +0 -117
  104. package/src/services/local-marketplace.js +0 -339
  105. package/src/services/marketplace-fetcher.js +0 -96
  106. package/src/services/marketplace-sync.js +0 -94
  107. package/src/services/mcp-registry.js +0 -87
  108. package/src/services/plugin-manager.js +0 -473
  109. package/src/services/plugin-mcp-config.js +0 -177
  110. package/src/services/plugin-setup.js +0 -444
  111. package/src/services/plugin-version-check.js +0 -262
  112. package/src/services/profiles.js +0 -161
  113. package/src/services/settings-manager.js +0 -243
  114. package/src/services/skills-manager.js +0 -393
  115. package/src/services/skillsmp-client.js +0 -87
  116. package/src/services/update-cache.js +0 -52
  117. package/src/services/version-check.js +0 -99
  118. package/src/types/gitignore.js +0 -6
  119. package/src/types/index.js +0 -1
  120. package/src/ui/App.js +0 -346
  121. package/src/ui/adapters/pluginsAdapter.js +0 -139
  122. package/src/ui/adapters/settingsAdapter.js +0 -111
  123. package/src/ui/adapters/skillsAdapter.js +0 -154
  124. package/src/ui/components/CategoryHeader.js +0 -9
  125. package/src/ui/components/EmptyFilterState.js +0 -4
  126. package/src/ui/components/ScopeIndicator.js +0 -30
  127. package/src/ui/components/ScrollableList.js +0 -36
  128. package/src/ui/components/SearchInput.js +0 -19
  129. package/src/ui/components/StyledText.js +0 -39
  130. package/src/ui/components/TabBar.js +0 -19
  131. package/src/ui/components/layout/FooterHints.js +0 -29
  132. package/src/ui/components/layout/Panel.js +0 -6
  133. package/src/ui/components/layout/ProgressBar.js +0 -14
  134. package/src/ui/components/layout/ScopeTabs.js +0 -6
  135. package/src/ui/components/layout/ScreenLayout.js +0 -16
  136. package/src/ui/components/layout/index.js +0 -5
  137. package/src/ui/components/modals/ConfirmModal.js +0 -14
  138. package/src/ui/components/modals/InputModal.js +0 -5
  139. package/src/ui/components/modals/LoadingModal.js +0 -17
  140. package/src/ui/components/modals/MessageModal.js +0 -16
  141. package/src/ui/components/modals/ModalContainer.js +0 -137
  142. package/src/ui/components/modals/SelectModal.js +0 -18
  143. package/src/ui/components/modals/VersionMismatchModal.js +0 -36
  144. package/src/ui/components/modals/index.js +0 -6
  145. package/src/ui/components/primitives/ActionHints.js +0 -13
  146. package/src/ui/components/primitives/DetailSection.js +0 -7
  147. package/src/ui/components/primitives/KeyValueLine.js +0 -8
  148. package/src/ui/components/primitives/ListCategoryRow.js +0 -8
  149. package/src/ui/components/primitives/MetaText.js +0 -8
  150. package/src/ui/components/primitives/ScopeDetail.js +0 -32
  151. package/src/ui/components/primitives/ScopeSquares.js +0 -11
  152. package/src/ui/components/primitives/SelectableRow.js +0 -5
  153. package/src/ui/components/primitives/index.js +0 -8
  154. package/src/ui/hooks/index.js +0 -4
  155. package/src/ui/hooks/useAsyncData.js +0 -77
  156. package/src/ui/hooks/useGitignoreModal.js +0 -74
  157. package/src/ui/hooks/useKeyboard.js +0 -13
  158. package/src/ui/hooks/useKeyboardHandler.js +0 -39
  159. package/src/ui/hooks/useMismatchModal.js +0 -77
  160. package/src/ui/registry.js +0 -1
  161. package/src/ui/renderers/cliToolRenderers.js +0 -54
  162. package/src/ui/renderers/gitignoreRenderers.js +0 -46
  163. package/src/ui/renderers/mcpRenderers.js +0 -26
  164. package/src/ui/renderers/pluginRenderers.js +0 -124
  165. package/src/ui/renderers/profileRenderers.js +0 -177
  166. package/src/ui/renderers/settingsRenderers.js +0 -73
  167. package/src/ui/renderers/skillRenderers.js +0 -138
  168. package/src/ui/screens/AliasScreen.js +0 -1008
  169. package/src/ui/screens/CliToolsScreen.js +0 -338
  170. package/src/ui/screens/EnvVarsScreen.js +0 -152
  171. package/src/ui/screens/GitignoreScreen.js +0 -328
  172. package/src/ui/screens/McpRegistryScreen.js +0 -238
  173. package/src/ui/screens/McpScreen.js +0 -176
  174. package/src/ui/screens/ModelSelectorScreen.js +0 -296
  175. package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
  176. package/src/ui/screens/PluginsScreen.js +0 -766
  177. package/src/ui/screens/ProfilesScreen.js +0 -298
  178. package/src/ui/screens/SkillsScreen.js +0 -549
  179. package/src/ui/screens/StatusLineScreen.js +0 -206
  180. package/src/ui/screens/StatusLineScreen.tsx +0 -416
  181. package/src/ui/screens/index.js +0 -10
  182. package/src/ui/state/AnimationContext.js +0 -34
  183. package/src/ui/state/AppContext.js +0 -162
  184. package/src/ui/state/DimensionsContext.js +0 -71
  185. package/src/ui/state/reducer.js +0 -543
  186. package/src/ui/state/types.js +0 -1
  187. package/src/ui/theme.js +0 -47
  188. package/src/utils/clipboard.js +0 -56
  189. package/src/utils/command-utils.js +0 -19
  190. package/src/utils/fuzzy-search.js +0 -101
  191. package/src/utils/string-utils.js +0 -62
@@ -1,549 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- import { useEffect, useCallback, useMemo, useState, useRef } from "react";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import fs from "fs-extra";
6
- import { useApp, useModal } from "../state/AppContext.js";
7
- import { useDimensions } from "../state/DimensionsContext.js";
8
- import { useKeyboard } from "../hooks/useKeyboard.js";
9
- import { ScreenLayout } from "../components/layout/index.js";
10
- import { EmptyFilterState } from "../components/EmptyFilterState.js";
11
- import { fetchAvailableSkills, fetchSkillFrontmatter, fetchSkillSetSkills, installSkill, uninstallSkill, } from "../../services/skills-manager.js";
12
- import { searchSkills } from "../../services/skillsmp-client.js";
13
- import { DEFAULT_SKILL_REPOS, RECOMMENDED_SKILLS, RECOMMENDED_SKILL_SETS, classifyStarReliability } from "../../data/skill-repos.js";
14
- import { buildSkillBrowserItems } from "../adapters/skillsAdapter.js";
15
- import { renderSkillRow, renderSkillDetail } from "../renderers/skillRenderers.js";
16
- import { ScrollableList } from "../components/ScrollableList.js";
17
- export function SkillsScreen() {
18
- const { state, dispatch } = useApp();
19
- const { skills: skillsState } = state;
20
- const modal = useModal();
21
- const dimensions = useDimensions();
22
- const isSearchActive = state.isSearching &&
23
- state.currentRoute.screen === "skills" &&
24
- !state.modal;
25
- // ── Data fetching ─────────────────────────────────────────────────────────
26
- const fetchData = useCallback(async () => {
27
- dispatch({ type: "SKILLS_DATA_LOADING" });
28
- try {
29
- const skills = await fetchAvailableSkills(DEFAULT_SKILL_REPOS, state.projectPath);
30
- dispatch({ type: "SKILLS_DATA_SUCCESS", skills });
31
- }
32
- catch (error) {
33
- dispatch({
34
- type: "SKILLS_DATA_ERROR",
35
- error: error instanceof Error ? error : new Error(String(error)),
36
- });
37
- }
38
- }, [dispatch, state.projectPath]);
39
- useEffect(() => {
40
- fetchData();
41
- }, [fetchData, state.dataRefreshVersion]);
42
- // ── Remote search (debounced, cached) ─────────────────────────────────────
43
- const [searchResults, setSearchResults] = useState([]);
44
- const [isSearchLoading, setIsSearchLoading] = useState(false);
45
- const [searchError, setSearchError] = useState(null);
46
- const searchTimerRef = useRef(null);
47
- const searchCacheRef = useRef(new Map());
48
- useEffect(() => {
49
- const query = skillsState.searchQuery.trim();
50
- if (query.length < 2) {
51
- setSearchResults([]);
52
- setIsSearchLoading(false);
53
- setSearchError(null);
54
- return;
55
- }
56
- const cached = searchCacheRef.current.get(query);
57
- if (cached) {
58
- setSearchResults(cached);
59
- setIsSearchLoading(false);
60
- setSearchError(null);
61
- return;
62
- }
63
- setIsSearchLoading(true);
64
- setSearchError(null);
65
- if (searchTimerRef.current)
66
- clearTimeout(searchTimerRef.current);
67
- searchTimerRef.current = setTimeout(async () => {
68
- try {
69
- const results = await searchSkills(query, { limit: 30 });
70
- const mapped = results.map((r) => {
71
- const source = {
72
- label: r.repo || "unknown",
73
- repo: r.repo || "unknown",
74
- skillsPath: "",
75
- };
76
- return {
77
- id: `remote:${r.repo}/${r.skillPath}`,
78
- name: r.name,
79
- description: r.description || "",
80
- source,
81
- repoPath: r.skillPath ? `${r.skillPath}/SKILL.md` : "SKILL.md",
82
- gitBlobSha: "",
83
- frontmatter: null,
84
- installed: false,
85
- installedScope: null,
86
- hasUpdate: false,
87
- stars: r.stars,
88
- starReliability: classifyStarReliability(r.repo || "unknown", r.stars),
89
- };
90
- });
91
- searchCacheRef.current.set(query, mapped);
92
- setSearchResults(mapped);
93
- setSearchError(null);
94
- }
95
- catch (error) {
96
- // A thrown error means the search service is unreachable — distinct
97
- // from a successful search that returned zero matches.
98
- setSearchResults([]);
99
- setSearchError(error instanceof Error ? error.message : "Search service unavailable");
100
- }
101
- setIsSearchLoading(false);
102
- }, 400);
103
- return () => {
104
- if (searchTimerRef.current)
105
- clearTimeout(searchTimerRef.current);
106
- };
107
- }, [skillsState.searchQuery]);
108
- // ── Disk scan for installed skills ────────────────────────────────────────
109
- const [installedFromDisk, setInstalledFromDisk] = useState({ user: new Set(), project: new Set() });
110
- const scanDisk = useCallback(async () => {
111
- const user = new Set();
112
- const project = new Set();
113
- const userDir = path.join(os.homedir(), ".claude", "skills");
114
- const projDir = path.join(state.projectPath || process.cwd(), ".claude", "skills");
115
- for (const [dir, set] of [[userDir, user], [projDir, project]]) {
116
- try {
117
- if (await fs.pathExists(dir)) {
118
- const entries = await fs.readdir(dir);
119
- for (const e of entries) {
120
- if (await fs.pathExists(path.join(dir, e, "SKILL.md")))
121
- set.add(e);
122
- }
123
- }
124
- }
125
- catch { /* ignore */ }
126
- }
127
- setInstalledFromDisk({ user, project });
128
- }, [state.projectPath]);
129
- useEffect(() => {
130
- scanDisk();
131
- }, [scanDisk, state.dataRefreshVersion]);
132
- // ── Skill Sets state ──────────────────────────────────────────────────────
133
- const [skillSets, setSkillSets] = useState(() => RECOMMENDED_SKILL_SETS.map((rs) => ({
134
- id: rs.repo,
135
- name: rs.name,
136
- description: rs.description,
137
- repo: rs.repo,
138
- icon: rs.icon,
139
- stars: rs.stars,
140
- skills: [],
141
- loaded: false,
142
- loading: false,
143
- })));
144
- const [expandedSets, setExpandedSets] = useState(new Set());
145
- // Re-mark installed status on child skills when disk state changes
146
- useEffect(() => {
147
- setSkillSets((prev) => prev.map((set) => {
148
- if (!set.loaded)
149
- return set;
150
- const updatedSkills = set.skills.map((skill) => {
151
- const slug = skill.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
152
- const isUser = installedFromDisk.user.has(slug) || installedFromDisk.user.has(skill.name);
153
- const isProj = installedFromDisk.project.has(slug) || installedFromDisk.project.has(skill.name);
154
- return {
155
- ...skill,
156
- installed: isUser || isProj,
157
- installedScope: isProj ? "project" : isUser ? "user" : null,
158
- };
159
- });
160
- return { ...set, skills: updatedSkills };
161
- }));
162
- }, [installedFromDisk]);
163
- const handleToggleSet = useCallback(async (setId) => {
164
- const isExpanded = expandedSets.has(setId);
165
- const newExpanded = new Set(expandedSets);
166
- if (isExpanded) {
167
- newExpanded.delete(setId);
168
- }
169
- else {
170
- newExpanded.add(setId);
171
- }
172
- setExpandedSets(newExpanded);
173
- // Fetch skills on first expand
174
- const set = skillSets.find((s) => s.id === setId);
175
- if (!isExpanded && set && !set.loaded && !set.loading) {
176
- setSkillSets((prev) => prev.map((s) => (s.id === setId ? { ...s, loading: true } : s)));
177
- try {
178
- const skills = await fetchSkillSetSkills(set.repo, state.projectPath);
179
- setSkillSets((prev) => prev.map((s) => s.id === setId
180
- ? { ...s, skills, loaded: true, loading: false, error: undefined }
181
- : s));
182
- }
183
- catch (error) {
184
- setSkillSets((prev) => prev.map((s) => s.id === setId
185
- ? {
186
- ...s,
187
- loading: false,
188
- error: error instanceof Error ? error.message : String(error),
189
- }
190
- : s));
191
- }
192
- }
193
- }, [expandedSets, skillSets, state.projectPath]);
194
- // Status bar message (auto-clears)
195
- const [statusMsg, setStatusMsg] = useState(null);
196
- const statusTimerRef = useRef(null);
197
- const showStatus = useCallback((text, tone = "success") => {
198
- setStatusMsg({ text, tone });
199
- if (statusTimerRef.current)
200
- clearTimeout(statusTimerRef.current);
201
- statusTimerRef.current = setTimeout(() => setStatusMsg(null), 3000);
202
- }, []);
203
- const handleInstallAllFromSet = useCallback(async (setId, scope) => {
204
- const set = skillSets.find((s) => s.id === setId);
205
- if (!set || !set.loaded)
206
- return;
207
- const toInstall = set.skills.filter((s) => !s.installed);
208
- if (toInstall.length === 0) {
209
- showStatus(`All ${set.name} skills already installed`);
210
- return;
211
- }
212
- showStatus(`Installing ${toInstall.length} skills from ${set.name}...`);
213
- let installed = 0;
214
- let failed = 0;
215
- for (const skill of toInstall) {
216
- try {
217
- await installSkill(skill, scope, state.projectPath);
218
- installed++;
219
- }
220
- catch {
221
- failed++;
222
- }
223
- }
224
- await scanDisk();
225
- if (failed > 0) {
226
- showStatus(`Installed ${installed}/${toInstall.length} (${failed} failed)`, "error");
227
- }
228
- else {
229
- showStatus(`Installed ${installed} skills from ${set.name} to ${scope}`);
230
- }
231
- }, [skillSets, state.projectPath, scanDisk, showStatus]);
232
- // ── Derived data ──────────────────────────────────────────────────────────
233
- const staticRecommended = useMemo(() => {
234
- return RECOMMENDED_SKILLS.map((r) => {
235
- const slug = r.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
236
- const isUser = installedFromDisk.user.has(slug) || installedFromDisk.user.has(r.name);
237
- const isProj = installedFromDisk.project.has(slug) || installedFromDisk.project.has(r.name);
238
- return {
239
- id: `rec:${r.repo}/${r.skillPath}`,
240
- name: r.name,
241
- description: r.description,
242
- source: { label: r.repo, repo: r.repo, skillsPath: "" },
243
- repoPath: `${r.skillPath}/SKILL.md`,
244
- gitBlobSha: "",
245
- frontmatter: null,
246
- installed: isUser || isProj,
247
- installedScope: isProj ? "project" : isUser ? "user" : null,
248
- hasUpdate: false,
249
- isRecommended: true,
250
- stars: r.stars,
251
- starReliability: classifyStarReliability(r.repo, r.stars),
252
- };
253
- });
254
- }, [installedFromDisk]);
255
- const mergedRecommended = useMemo(() => {
256
- if (skillsState.skills.status !== "success")
257
- return staticRecommended;
258
- const fetched = skillsState.skills.data.filter((s) => s.isRecommended);
259
- return staticRecommended.map((staticSkill) => {
260
- const match = fetched.find((f) => f.source.repo === staticSkill.source.repo && f.name === staticSkill.name);
261
- if (!match)
262
- return staticSkill;
263
- // Merge: prefer fetched data for description/stars, but disk scan is
264
- // authoritative for installed state (staticSkill reflects current disk)
265
- return {
266
- ...staticSkill,
267
- ...match,
268
- installed: staticSkill.installed,
269
- installedScope: staticSkill.installedScope,
270
- stars: match.stars || staticSkill.stars,
271
- };
272
- });
273
- }, [staticRecommended, skillsState.skills]);
274
- const installedSkills = useMemo(() => {
275
- const all = [];
276
- for (const [scope, names] of [
277
- ["user", installedFromDisk.user],
278
- ["project", installedFromDisk.project],
279
- ]) {
280
- for (const name of names) {
281
- if (all.some((s) => s.name === name))
282
- continue;
283
- all.push({
284
- id: `installed:${scope}/${name}`,
285
- name,
286
- description: "",
287
- source: { label: "local", repo: "local", skillsPath: "" },
288
- repoPath: "",
289
- gitBlobSha: "",
290
- frontmatter: null,
291
- installed: true,
292
- installedScope: scope,
293
- hasUpdate: false,
294
- stars: undefined,
295
- });
296
- }
297
- }
298
- return all;
299
- }, [installedFromDisk]);
300
- const popularSkills = useMemo(() => {
301
- if (skillsState.skills.status !== "success")
302
- return [];
303
- return skillsState.skills.data
304
- .filter((s) => !s.isRecommended)
305
- .sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
306
- }, [skillsState.skills]);
307
- // ── List items (built by adapter) ─────────────────────────────────────────
308
- const allItems = useMemo(() => buildSkillBrowserItems({
309
- recommended: mergedRecommended,
310
- popular: popularSkills,
311
- installed: installedSkills,
312
- searchResults,
313
- query: skillsState.searchQuery,
314
- isSearchLoading,
315
- skillSets,
316
- expandedSets,
317
- }), [
318
- mergedRecommended,
319
- popularSkills,
320
- installedSkills,
321
- searchResults,
322
- skillsState.searchQuery,
323
- isSearchLoading,
324
- skillSets,
325
- expandedSets,
326
- ]);
327
- // Auto-correct selection if it lands on a category header (e.g. initial load, reset)
328
- useEffect(() => {
329
- const item = allItems[skillsState.selectedIndex];
330
- if (item && item.kind === "category") {
331
- const firstSelectable = allItems.findIndex((i) => i.kind === "skill" || i.kind === "skillset");
332
- if (firstSelectable >= 0)
333
- dispatch({ type: "SKILLS_SELECT", index: firstSelectable });
334
- }
335
- }, [allItems, skillsState.selectedIndex, dispatch]);
336
- const selectedItem = allItems[skillsState.selectedIndex];
337
- const selectedSkill = selectedItem?.kind === "skill" ? selectedItem.skill : undefined;
338
- const selectedSet = selectedItem?.kind === "skillset" ? selectedItem.skillSet : undefined;
339
- // ── Lazy-load frontmatter for selected skill ───────────────────────────────
340
- // Check if selected skill belongs to a skill set (lives in local state, not reducer)
341
- const isSkillSetChild = selectedSkill
342
- ? skillSets.some((s) => s.loaded && s.skills.some((sk) => sk.id === selectedSkill.id))
343
- : false;
344
- useEffect(() => {
345
- if (!selectedSkill || selectedSkill.frontmatter)
346
- return;
347
- fetchSkillFrontmatter(selectedSkill).then((fm) => {
348
- if (isSkillSetChild) {
349
- // Update the local skillSets state
350
- setSkillSets((prev) => prev.map((set) => ({
351
- ...set,
352
- skills: set.skills.map((sk) => sk.id === selectedSkill.id ? { ...sk, frontmatter: fm } : sk),
353
- })));
354
- }
355
- else {
356
- dispatch({
357
- type: "SKILLS_UPDATE_ITEM",
358
- name: selectedSkill.name,
359
- updates: { frontmatter: fm },
360
- });
361
- }
362
- }).catch(() => { });
363
- }, [selectedSkill?.id, isSkillSetChild, dispatch]);
364
- // ── Action handlers ───────────────────────────────────────────────────────
365
- const handleInstall = useCallback(async (scope) => {
366
- if (!selectedSkill)
367
- return;
368
- try {
369
- await installSkill(selectedSkill, scope, state.projectPath);
370
- await scanDisk(); // Re-scan disk only — no network re-fetch needed
371
- showStatus(`Installed ${selectedSkill.name} to ${scope}`);
372
- }
373
- catch (error) {
374
- showStatus(`Failed: ${error}`, "error");
375
- }
376
- }, [selectedSkill, state.projectPath, scanDisk, showStatus]);
377
- const handleUninstall = useCallback(async () => {
378
- if (!selectedSkill || !selectedSkill.installed)
379
- return;
380
- const scope = selectedSkill.installedScope;
381
- if (!scope)
382
- return;
383
- try {
384
- await uninstallSkill(selectedSkill.name, scope, state.projectPath);
385
- await scanDisk(); // Re-scan disk only — no network re-fetch needed
386
- showStatus(`Removed ${selectedSkill.name} from ${scope}`);
387
- }
388
- catch (error) {
389
- showStatus(`Failed: ${error}`, "error");
390
- }
391
- }, [selectedSkill, state.projectPath, scanDisk, showStatus]);
392
- // ── Keyboard handling ─────────────────────────────────────────────────────
393
- useKeyboard((event) => {
394
- if (state.modal)
395
- return;
396
- const hasQuery = skillsState.searchQuery.length > 0;
397
- if (event.name === "escape") {
398
- if (hasQuery || isSearchActive) {
399
- dispatch({ type: "SKILLS_SET_SEARCH", query: "" });
400
- dispatch({ type: "SET_SEARCHING", isSearching: false });
401
- dispatch({ type: "SKILLS_SELECT", index: 0 });
402
- }
403
- return;
404
- }
405
- if (event.name === "backspace" || event.name === "delete") {
406
- if (hasQuery) {
407
- const newQuery = skillsState.searchQuery.slice(0, -1);
408
- dispatch({ type: "SKILLS_SET_SEARCH", query: newQuery });
409
- dispatch({ type: "SKILLS_SELECT", index: 0 });
410
- if (!newQuery)
411
- dispatch({ type: "SET_SEARCHING", isSearching: false });
412
- }
413
- return;
414
- }
415
- if (event.name === "up" || event.name === "k") {
416
- if (isSearchActive)
417
- dispatch({ type: "SET_SEARCHING", isSearching: false });
418
- let newIndex = skillsState.selectedIndex - 1;
419
- // Skip category headers
420
- while (newIndex >= 0 && allItems[newIndex]?.kind === "category")
421
- newIndex--;
422
- if (newIndex >= 0)
423
- dispatch({ type: "SKILLS_SELECT", index: newIndex });
424
- return;
425
- }
426
- if (event.name === "down" || event.name === "j") {
427
- if (isSearchActive)
428
- dispatch({ type: "SET_SEARCHING", isSearching: false });
429
- let newIndex = skillsState.selectedIndex + 1;
430
- // Skip category headers
431
- while (newIndex < allItems.length && allItems[newIndex]?.kind === "category")
432
- newIndex++;
433
- if (newIndex < allItems.length)
434
- dispatch({ type: "SKILLS_SELECT", index: newIndex });
435
- return;
436
- }
437
- if (event.name === "return" || event.name === "enter") {
438
- if (isSearchActive) {
439
- dispatch({ type: "SET_SEARCHING", isSearching: false });
440
- return;
441
- }
442
- if (selectedSet) {
443
- handleToggleSet(selectedSet.id);
444
- return;
445
- }
446
- if (selectedSkill && !selectedSkill.installed) {
447
- handleInstall("project");
448
- }
449
- return;
450
- }
451
- // Action keys only when NOT actively typing in search
452
- if (!isSearchActive) {
453
- if (event.name === "u") {
454
- if (selectedSet) {
455
- handleInstallAllFromSet(selectedSet.id, "user");
456
- return;
457
- }
458
- if (selectedSkill) {
459
- if (selectedSkill.installed && selectedSkill.installedScope === "user")
460
- handleUninstall();
461
- else
462
- handleInstall("user");
463
- return;
464
- }
465
- }
466
- else if (event.name === "p") {
467
- if (selectedSet) {
468
- handleInstallAllFromSet(selectedSet.id, "project");
469
- return;
470
- }
471
- if (selectedSkill) {
472
- if (selectedSkill.installed && selectedSkill.installedScope === "project")
473
- handleUninstall();
474
- else
475
- handleInstall("project");
476
- return;
477
- }
478
- }
479
- }
480
- if (isSearchActive) {
481
- if (event.name === "k" || event.name === "j") {
482
- const delta = event.name === "k" ? -1 : 1;
483
- const newIndex = Math.max(0, Math.min(allItems.length - 1, skillsState.selectedIndex + delta));
484
- dispatch({ type: "SKILLS_SELECT", index: newIndex });
485
- return;
486
- }
487
- if (event.name && event.name.length === 1 && !event.ctrl && !event.meta && !/[0-9]/.test(event.name)) {
488
- dispatch({ type: "SKILLS_SET_SEARCH", query: skillsState.searchQuery + event.name });
489
- dispatch({ type: "SKILLS_SELECT", index: 0 });
490
- }
491
- return;
492
- }
493
- if (event.name === "r") {
494
- fetchData();
495
- }
496
- else if (event.name === "o" && (selectedSkill || selectedSet)) {
497
- if (selectedSet) {
498
- const url = `https://github.com/${selectedSet.repo}`;
499
- import("node:child_process").then(({ execSync: exec }) => {
500
- try {
501
- exec(`open "${url}"`);
502
- }
503
- catch { /* ignore */ }
504
- });
505
- }
506
- else if (selectedSkill) {
507
- const repo = selectedSkill.source.repo;
508
- const repoPath = selectedSkill.repoPath?.replace("/SKILL.md", "") || "";
509
- if (repo && repo !== "local") {
510
- const url = `https://github.com/${repo}/tree/main/${repoPath}`;
511
- import("node:child_process").then(({ execSync: exec }) => {
512
- try {
513
- exec(`open "${url}"`);
514
- }
515
- catch { /* ignore */ }
516
- });
517
- }
518
- }
519
- }
520
- else if (event.name === "/") {
521
- dispatch({ type: "SET_SEARCHING", isSearching: true });
522
- }
523
- });
524
- // ── Status line ───────────────────────────────────────────────────────────
525
- const skills = skillsState.skills.status === "success" ? skillsState.skills.data : [];
526
- const installedCount = skills.filter((s) => s.installed).length;
527
- const query = skillsState.searchQuery.trim();
528
- const statusContent = statusMsg ? (_jsx("text", { children: _jsx("span", { fg: statusMsg.tone === "success" ? "green" : "red", children: statusMsg.text }) })) : (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Skills: " }), _jsxs("span", { fg: "cyan", children: [installedCount, " installed"] }), query.length >= 2 && isSearchLoading && (_jsx("span", { fg: "yellow", children: " \u2502 searching..." })), query.length >= 2 && !isSearchLoading && !searchError && searchResults.length > 0 && (_jsxs("span", { fg: "green", children: [" \u2502 ", searchResults.length, " found"] })), query.length >= 2 && !isSearchLoading && searchError && (_jsx("span", { fg: "red", children: " \u2502 search unavailable" })), !query && _jsx("span", { fg: "gray", children: " \u2502 89K+ searchable" })] }));
529
- // ── Render ────────────────────────────────────────────────────────────────
530
- return (_jsx(ScreenLayout, { title: "claudeup Skills", currentScreen: "skills", statusLine: statusContent, search: skillsState.searchQuery || isSearchActive
531
- ? {
532
- isActive: isSearchActive,
533
- query: skillsState.searchQuery,
534
- placeholder: "type to search",
535
- }
536
- : undefined, footerHints: isSearchActive
537
- ? [
538
- { keys: ["type"], label: "filter" },
539
- { keys: ["Enter"], label: "done" },
540
- { keys: ["Esc"], label: "clear" },
541
- ]
542
- : [
543
- { keys: ["u"], label: "user" },
544
- { keys: ["p"], label: "project" },
545
- { keys: ["o"], label: "open" },
546
- { keys: ["/"], label: "search" },
547
- ], listPanel: _jsxs("box", { flexDirection: "column", children: [_jsx(ScrollableList, { items: allItems, selectedIndex: skillsState.selectedIndex, renderItem: renderSkillRow, maxHeight: dimensions.listPanelHeight, getKey: (item, index) => `${index}:${item.id}` }), !query && skillsState.skills.status === "loading" && (_jsx("box", { marginTop: 2, paddingLeft: 2, children: _jsx("text", { fg: "yellow", children: "Loading popular skills..." }) })), query.length >= 2 && isSearchLoading && (_jsx("box", { marginTop: 2, paddingLeft: 2, children: _jsxs("text", { fg: "yellow", children: ["Searching for \"", skillsState.searchQuery, "\"..."] }) })), query.length >= 2 && !isSearchLoading && searchError && (_jsxs("box", { flexDirection: "column", marginTop: 2, paddingLeft: 2, paddingRight: 2, children: [_jsx("text", { fg: "red", children: "Skill search is unavailable right now." }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: "The search service could not be reached" }) }), _jsxs("text", { fg: "gray", children: ["(", searchError, "). Check your connection and retry."] }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: "Press Esc to clear the search." }) })] })), query.length >= 2 && !isSearchLoading && !searchError && searchResults.length === 0 && (_jsx(EmptyFilterState, { query: skillsState.searchQuery, entityName: "skills" }))] }), detailPanel: renderSkillDetail(selectedItem) }));
548
- }
549
- export default SkillsScreen;