claudeup 4.19.0 → 4.23.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 (182) 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__/cli-router.test.ts +65 -0
  7. package/src/__tests__/conventions-integration.test.ts +745 -0
  8. package/src/__tests__/conventions-manager.test.ts +1172 -0
  9. package/src/__tests__/doctor-bins.test.ts +35 -0
  10. package/src/__tests__/doctor.test.ts +450 -0
  11. package/src/__tests__/dual-write-prevention.test.ts +14 -4
  12. package/src/__tests__/file-locking.test.ts +208 -0
  13. package/src/__tests__/gitignore-prerun.test.ts +1 -0
  14. package/src/__tests__/install-command.test.ts +81 -0
  15. package/src/__tests__/install-plan.test.ts +98 -0
  16. package/src/__tests__/manifest.test.ts +136 -0
  17. package/src/__tests__/marketplace-badge.test.ts +91 -0
  18. package/src/__tests__/marketplace-refresh.test.ts +267 -0
  19. package/src/__tests__/plugin-requires.test.ts +133 -0
  20. package/src/__tests__/plugin-setup.test.ts +7 -0
  21. package/src/__tests__/profile-command.test.ts +116 -0
  22. package/src/__tests__/profile-materializer.test.ts +82 -0
  23. package/src/__tests__/profile-sync.test.ts +136 -0
  24. package/src/__tests__/registry-version-resolution.test.ts +74 -0
  25. package/src/__tests__/resolve-executable.test.ts +42 -0
  26. package/src/__tests__/resolver.test.ts +218 -0
  27. package/src/__tests__/symlink-manager.test.ts +99 -0
  28. package/src/__tests__/toolchain.test.ts +56 -0
  29. package/src/cli/claude.ts +21 -0
  30. package/src/cli/doctor.ts +132 -0
  31. package/src/cli/install.ts +360 -0
  32. package/src/cli/profile.ts +166 -0
  33. package/src/cli/router.ts +107 -0
  34. package/src/cli/update.ts +85 -0
  35. package/src/data/cli-tools.ts +7 -7
  36. package/src/data/gitignore-defaults.ts +4 -0
  37. package/src/data/marketplaces.ts +12 -0
  38. package/src/data/predefined-profiles.ts +3 -4
  39. package/src/main.tsx +11 -154
  40. package/src/prerunner/index.ts +62 -27
  41. package/src/services/claude-cli.ts +19 -22
  42. package/src/services/claude-settings.ts +99 -26
  43. package/src/services/conventions-manager.ts +865 -0
  44. package/src/services/doctor-bins.ts +49 -0
  45. package/src/services/doctor.ts +509 -0
  46. package/src/services/install-plan.ts +107 -0
  47. package/src/services/manifest.ts +166 -0
  48. package/src/services/marketplace-refresh.ts +173 -0
  49. package/src/services/plugin-manager.ts +7 -1
  50. package/src/services/plugin-requires.ts +215 -0
  51. package/src/services/plugin-version-check.ts +9 -4
  52. package/src/services/profile-materializer.ts +61 -0
  53. package/src/services/profile-sync.ts +150 -0
  54. package/src/services/resolver.ts +293 -0
  55. package/src/services/symlink-manager.ts +146 -0
  56. package/src/services/toolchain.ts +97 -0
  57. package/src/services/version-check.ts +10 -11
  58. package/src/types/index.ts +138 -31
  59. package/src/ui/App.tsx +0 -4
  60. package/src/ui/adapters/pluginsAdapter.ts +13 -0
  61. package/src/ui/screens/CliToolsScreen.tsx +11 -3
  62. package/src/ui/screens/index.ts +0 -1
  63. package/src/ui/state/reducer.ts +0 -101
  64. package/src/ui/state/types.ts +0 -35
  65. package/src/utils/command-utils.ts +23 -0
  66. package/src/utils/file-locking.ts +144 -0
  67. package/src/data/alias-flags.js +0 -205
  68. package/src/data/cli-tools.js +0 -123
  69. package/src/data/gitignore-defaults.js +0 -24
  70. package/src/data/gitignore-reasons.js +0 -97
  71. package/src/data/gitignore-templates.js +0 -21
  72. package/src/data/marketplaces.js +0 -138
  73. package/src/data/mcp-servers.js +0 -509
  74. package/src/data/predefined-profiles.js +0 -248
  75. package/src/data/settings-catalog.js +0 -625
  76. package/src/data/skill-repos.js +0 -160
  77. package/src/data/statuslines.js +0 -159
  78. package/src/data/statuslines.ts +0 -188
  79. package/src/index.js +0 -4
  80. package/src/index.ts +0 -5
  81. package/src/main.js +0 -170
  82. package/src/prerunner/index.js +0 -252
  83. package/src/services/alias-settings.js +0 -51
  84. package/src/services/alias-shell-writer.js +0 -1018
  85. package/src/services/alias-store.js +0 -129
  86. package/src/services/claude-cli.js +0 -189
  87. package/src/services/claude-runner.js +0 -28
  88. package/src/services/claude-settings.js +0 -1223
  89. package/src/services/gitignore-detector.js +0 -155
  90. package/src/services/gitignore-fixer.js +0 -231
  91. package/src/services/gitignore-prerun.js +0 -46
  92. package/src/services/gitignore-resolver.js +0 -143
  93. package/src/services/gitignore-service.js +0 -117
  94. package/src/services/local-marketplace.js +0 -339
  95. package/src/services/marketplace-fetcher.js +0 -96
  96. package/src/services/marketplace-sync.js +0 -94
  97. package/src/services/mcp-registry.js +0 -87
  98. package/src/services/plugin-manager.js +0 -473
  99. package/src/services/plugin-mcp-config.js +0 -177
  100. package/src/services/plugin-setup.js +0 -499
  101. package/src/services/plugin-version-check.js +0 -262
  102. package/src/services/profiles.js +0 -161
  103. package/src/services/settings-manager.js +0 -243
  104. package/src/services/skills-manager.js +0 -393
  105. package/src/services/skillsmp-client.js +0 -87
  106. package/src/services/update-cache.js +0 -52
  107. package/src/services/version-check.js +0 -99
  108. package/src/types/gitignore.js +0 -6
  109. package/src/types/index.js +0 -1
  110. package/src/ui/App.js +0 -355
  111. package/src/ui/adapters/pluginsAdapter.js +0 -139
  112. package/src/ui/adapters/settingsAdapter.js +0 -111
  113. package/src/ui/adapters/skillsAdapter.js +0 -154
  114. package/src/ui/components/CategoryHeader.js +0 -9
  115. package/src/ui/components/EmptyFilterState.js +0 -4
  116. package/src/ui/components/FlagDetailEditor.js +0 -0
  117. package/src/ui/components/ScopeIndicator.js +0 -30
  118. package/src/ui/components/ScrollableList.js +0 -36
  119. package/src/ui/components/SearchInput.js +0 -19
  120. package/src/ui/components/StyledText.js +0 -39
  121. package/src/ui/components/TabBar.js +0 -19
  122. package/src/ui/components/layout/FooterHints.js +0 -29
  123. package/src/ui/components/layout/Panel.js +0 -6
  124. package/src/ui/components/layout/ProgressBar.js +0 -14
  125. package/src/ui/components/layout/ScopeTabs.js +0 -6
  126. package/src/ui/components/layout/ScreenLayout.js +0 -16
  127. package/src/ui/components/layout/index.js +0 -5
  128. package/src/ui/components/modals/ConfirmModal.js +0 -14
  129. package/src/ui/components/modals/InputModal.js +0 -5
  130. package/src/ui/components/modals/LoadingModal.js +0 -17
  131. package/src/ui/components/modals/MessageModal.js +0 -16
  132. package/src/ui/components/modals/ModalContainer.js +0 -137
  133. package/src/ui/components/modals/SelectModal.js +0 -18
  134. package/src/ui/components/modals/VersionMismatchModal.js +0 -36
  135. package/src/ui/components/modals/index.js +0 -6
  136. package/src/ui/components/primitives/ActionHints.js +0 -13
  137. package/src/ui/components/primitives/DetailSection.js +0 -7
  138. package/src/ui/components/primitives/KeyValueLine.js +0 -8
  139. package/src/ui/components/primitives/ListCategoryRow.js +0 -8
  140. package/src/ui/components/primitives/MetaText.js +0 -8
  141. package/src/ui/components/primitives/ScopeDetail.js +0 -32
  142. package/src/ui/components/primitives/ScopeSquares.js +0 -11
  143. package/src/ui/components/primitives/SelectableRow.js +0 -5
  144. package/src/ui/components/primitives/index.js +0 -8
  145. package/src/ui/hooks/index.js +0 -4
  146. package/src/ui/hooks/useAsyncData.js +0 -77
  147. package/src/ui/hooks/useGitignoreModal.js +0 -74
  148. package/src/ui/hooks/useKeyboard.js +0 -13
  149. package/src/ui/hooks/useKeyboardHandler.js +0 -39
  150. package/src/ui/hooks/useMismatchModal.js +0 -77
  151. package/src/ui/registry.js +0 -1
  152. package/src/ui/renderers/cliToolRenderers.js +0 -54
  153. package/src/ui/renderers/gitignoreRenderers.js +0 -46
  154. package/src/ui/renderers/mcpRenderers.js +0 -26
  155. package/src/ui/renderers/pluginRenderers.js +0 -124
  156. package/src/ui/renderers/profileRenderers.js +0 -177
  157. package/src/ui/renderers/settingsRenderers.js +0 -73
  158. package/src/ui/renderers/skillRenderers.js +0 -138
  159. package/src/ui/screens/AliasScreen.js +0 -1111
  160. package/src/ui/screens/CliToolsScreen.js +0 -338
  161. package/src/ui/screens/EnvVarsScreen.js +0 -152
  162. package/src/ui/screens/GitignoreScreen.js +0 -328
  163. package/src/ui/screens/McpRegistryScreen.js +0 -238
  164. package/src/ui/screens/McpScreen.js +0 -176
  165. package/src/ui/screens/ModelSelectorScreen.js +0 -296
  166. package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
  167. package/src/ui/screens/PluginsScreen.js +0 -769
  168. package/src/ui/screens/ProfilesScreen.js +0 -298
  169. package/src/ui/screens/SkillsScreen.js +0 -549
  170. package/src/ui/screens/StatusLineScreen.js +0 -206
  171. package/src/ui/screens/StatusLineScreen.tsx +0 -416
  172. package/src/ui/screens/index.js +0 -10
  173. package/src/ui/state/AnimationContext.js +0 -34
  174. package/src/ui/state/AppContext.js +0 -162
  175. package/src/ui/state/DimensionsContext.js +0 -71
  176. package/src/ui/state/reducer.js +0 -547
  177. package/src/ui/state/types.js +0 -1
  178. package/src/ui/theme.js +0 -47
  179. package/src/utils/clipboard.js +0 -56
  180. package/src/utils/command-utils.js +0 -19
  181. package/src/utils/fuzzy-search.js +0 -101
  182. package/src/utils/string-utils.js +0 -62
@@ -1,328 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- import { useEffect, useState, useCallback, useMemo } from "react";
3
- import { spawn } from "node:child_process";
4
- import { appendFileSync } from "node:fs";
5
- import { useRenderer } from "@opentui/react";
6
- import { useApp, useModal } from "../state/AppContext.js";
7
- import { useKeyboardHandler } from "../hooks/useKeyboardHandler.js";
8
- import { ScreenLayout } from "../components/layout/index.js";
9
- import { loadGitignoreState, applyDestructiveFix, ensureProjectManifestForEdit, applyTemplate, } from "../../services/gitignore-service.js";
10
- import { BUILTIN_TEMPLATES } from "../../data/gitignore-templates.js";
11
- import { getGitignoreRuleReason } from "../../data/gitignore-reasons.js";
12
- import { renderRuleRow, } from "../renderers/gitignoreRenderers.js";
13
- // Clean rows are grouped by the manifest that defined them. Order = display
14
- // order, top to bottom. "Default" is the user-facing name for the built-in
15
- // rules. Each label names where you'd go to change that group's rules.
16
- const SOURCE_ORDER = ["project", "global", "profile", "builtin"];
17
- const SOURCE_LABELS = {
18
- project: "Project · .claude/gitignore.json",
19
- global: "Global · ~/.claude/gitignore.json",
20
- profile: "Profile",
21
- builtin: "Default · built into claudeup",
22
- };
23
- export function GitignoreScreen() {
24
- const { state } = useApp();
25
- const modal = useModal();
26
- const renderer = useRenderer();
27
- const cwd = state.projectPath;
28
- const [data, setData] = useState(null);
29
- const [loading, setLoading] = useState(true);
30
- const [error, setError] = useState(null);
31
- const [selectedIdx, setSelectedIdx] = useState(0);
32
- const reload = useCallback(async () => {
33
- setLoading(true);
34
- setError(null);
35
- try {
36
- const s = await loadGitignoreState(cwd);
37
- setData(s);
38
- }
39
- catch (err) {
40
- setError(err instanceof Error ? err.message : String(err));
41
- }
42
- finally {
43
- setLoading(false);
44
- }
45
- }, [cwd]);
46
- useEffect(() => {
47
- void reload();
48
- }, [reload]);
49
- // Build the managed list: every desired gitignore rule, with all matching
50
- // violations grouped under the rule that owns them.
51
- const rows = useMemo(() => {
52
- if (!data)
53
- return [];
54
- const annotated = data.resolved.rules.map((rule) => {
55
- const violations = data.violations.filter((v) => violationMatchesRule(rule, v));
56
- return { rule, violations };
57
- });
58
- // Display order = flat index order (the keyboard handler walks this array),
59
- // so the on-screen layout must match this sort exactly:
60
- // 1. violations first (most violations first)
61
- // 2. then clean rows grouped by source manifest (SOURCE_ORDER)
62
- // 3. alphabetical within each group
63
- const sourceRank = (tier) => {
64
- const i = SOURCE_ORDER.indexOf(tier);
65
- return i === -1 ? SOURCE_ORDER.length : i;
66
- };
67
- return annotated.sort((a, b) => {
68
- const aViol = a.violations.length > 0;
69
- const bViol = b.violations.length > 0;
70
- if (aViol !== bViol)
71
- return aViol ? -1 : 1;
72
- if (aViol && bViol && a.violations.length !== b.violations.length) {
73
- return b.violations.length - a.violations.length;
74
- }
75
- if (!aViol) {
76
- const sr = sourceRank(a.rule.source) - sourceRank(b.rule.source);
77
- if (sr !== 0)
78
- return sr;
79
- }
80
- return a.rule.pattern.localeCompare(b.rule.pattern);
81
- });
82
- }, [data]);
83
- // Clamp selection when rows shrink.
84
- useEffect(() => {
85
- if (selectedIdx >= rows.length) {
86
- setSelectedIdx(Math.max(0, rows.length - 1));
87
- }
88
- }, [rows.length, selectedIdx]);
89
- const selectedRow = rows[selectedIdx];
90
- const ignoreRuleCount = data?.resolved.rules.filter((rule) => rule.action === "ignore").length ?? 0;
91
- const trackRuleCount = data?.resolved.rules.filter((rule) => rule.action === "track").length ?? 0;
92
- const toFixCount = data?.violations.length ?? 0;
93
- // Group rows for display while preserving each row's flat index in `rows`
94
- // (the keyboard handler walks `rows` by index, so the highlight must track
95
- // the original index, not the position within a rendered group).
96
- const indexed = rows.map((row, index) => ({ row, index }));
97
- const toFixRows = indexed.filter((r) => r.row.violations.length > 0);
98
- // Clean rows grouped by which manifest defined them. Sections render only
99
- // when non-empty, so e.g. the Profile group stays hidden until profiles
100
- // actually contribute rules.
101
- const cleanIndexed = indexed.filter((r) => r.row.violations.length === 0);
102
- const sourceGroups = SOURCE_ORDER.map((tier) => ({
103
- tier,
104
- label: SOURCE_LABELS[tier],
105
- items: cleanIndexed.filter((r) => r.row.rule.source === tier),
106
- })).filter((g) => g.items.length > 0);
107
- const cleanCount = cleanIndexed.length;
108
- // ── Action handlers ───────────────────────────────────────────────────
109
- // Fix every out-of-sync row. Iterates the grouped rows (not the flat
110
- // violation list) so each .gitignore entry uses the rule's pattern/glob,
111
- // not one specific path under it — same association handleApplyOne relies on.
112
- const handleFixAll = useCallback(async () => {
113
- if (!data || toFixRows.length === 0)
114
- return;
115
- try {
116
- const messages = [];
117
- const failures = [];
118
- for (const { row } of toFixRows) {
119
- for (const v of row.violations) {
120
- const r = await applyDestructiveFix(cwd, v, row.rule.pattern);
121
- (r.applied ? messages : failures).push(r.message);
122
- }
123
- }
124
- await reload();
125
- if (failures.length > 0) {
126
- await modal.message("Some fixes didn't apply", [...failures, ...messages].join("\n"), "error");
127
- }
128
- }
129
- catch (err) {
130
- await modal.message("Fix failed", err instanceof Error ? err.message : String(err), "error");
131
- }
132
- }, [cwd, data, toFixRows, reload, modal]);
133
- // Fix just the highlighted row.
134
- const handleFixOne = useCallback(async () => {
135
- if (!selectedRow) {
136
- await modal.message("No row selected", "Use ↑↓ to highlight a row that needs fixing, then press f.", "info");
137
- return;
138
- }
139
- if (selectedRow.violations.length === 0) {
140
- await modal.message("Nothing to fix", `${selectedRow.rule.pattern} already matches. f only applies to rows that need fixing.`, "info");
141
- return;
142
- }
143
- try {
144
- const results = [];
145
- for (const v of selectedRow.violations) {
146
- results.push(await applyDestructiveFix(cwd, v, selectedRow.rule.pattern));
147
- }
148
- const failed = results.filter((r) => !r.applied);
149
- await reload();
150
- // Success is silent — the reload removes the fixed row. Only surface a
151
- // modal when something didn't apply.
152
- if (failed.length > 0) {
153
- await modal.message("Some fixes didn't apply", results.map((r) => r.message).join("\n"), "error");
154
- }
155
- }
156
- catch (err) {
157
- await modal.message("Fix failed", err instanceof Error ? err.message : String(err), "error");
158
- }
159
- }, [selectedRow, modal, cwd, reload]);
160
- const handleApplyTemplate = useCallback(async () => {
161
- const names = Object.keys(BUILTIN_TEMPLATES);
162
- if (names.length === 0) {
163
- await modal.message("No templates", "No built-in templates available.", "info");
164
- return;
165
- }
166
- const selected = await modal.select("Apply template", "Merge the template into the project manifest at .claude/gitignore.json.", names.map((n) => ({ label: n, value: n })));
167
- if (!selected)
168
- return;
169
- try {
170
- const r = await applyTemplate(cwd, selected);
171
- const conflictNote = r.conflicts.length > 0
172
- ? `\n\nSkipped due to conflicts: ${r.conflicts.join(", ")}`
173
- : "";
174
- await modal.message("Template applied", `Wrote ${r.appliedTo}\n\n` +
175
- `Added ${r.added.ignore.length} ignore + ${r.added.track.length} track entries.` +
176
- conflictNote, "success");
177
- }
178
- catch (err) {
179
- await modal.message("Template failed", err instanceof Error ? err.message : String(err), "error");
180
- }
181
- await reload();
182
- }, [cwd, modal, reload]);
183
- const handleEditManifest = useCallback(async () => {
184
- const current = data ?? await loadGitignoreState(cwd);
185
- const projectManifest = await ensureProjectManifestForEdit(cwd, current.resolved);
186
- const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
187
- renderer.suspend();
188
- try {
189
- await new Promise((resolveEdit) => {
190
- const child = spawn(editor, [projectManifest], { stdio: "inherit" });
191
- child.on("exit", () => resolveEdit());
192
- child.on("error", () => resolveEdit());
193
- });
194
- }
195
- finally {
196
- renderer.resume();
197
- }
198
- await reload();
199
- renderer.requestRender();
200
- }, [cwd, data, reload, renderer]);
201
- useKeyboardHandler((input, key) => {
202
- if (process.env.CLAUDEUP_DEBUG_KEYS === "1") {
203
- try {
204
- appendFileSync("/tmp/claudeup-keys.log", `${new Date().toISOString()} screen=gitignore input=${JSON.stringify(input)} name=${JSON.stringify(key.name)} shift=${key.shift} ctrl=${key.ctrl} meta=${key.meta}\n`);
205
- }
206
- catch {
207
- // best-effort logging — never break the UI
208
- }
209
- }
210
- if (state.modal)
211
- return;
212
- if (input === "j" || key.downArrow) {
213
- setSelectedIdx((i) => Math.min(i + 1, Math.max(0, rows.length - 1)));
214
- return;
215
- }
216
- if (input === "k" || key.upArrow) {
217
- setSelectedIdx((i) => Math.max(i - 1, 0));
218
- return;
219
- }
220
- // F = fix everything out of sync; f = fix just the selected row.
221
- if (input === "F" || (input === "f" && key.shift)) {
222
- void handleFixAll();
223
- return;
224
- }
225
- if (input === "f") {
226
- void handleFixOne();
227
- return;
228
- }
229
- if (input === "t") {
230
- void handleApplyTemplate();
231
- return;
232
- }
233
- if (input === "e") {
234
- void handleEditManifest();
235
- return;
236
- }
237
- if (input === "r") {
238
- void reload();
239
- }
240
- });
241
- // ── Rendering ─────────────────────────────────────────────────────────
242
- const summary = data
243
- ? renderSummary(rows.length, ignoreRuleCount, trackRuleCount, toFixCount)
244
- : null;
245
- const listBody = loading ? (_jsx("text", { fg: "gray", children: "Loading..." })) : error ? (_jsxs("text", { fg: "red", children: ["Error: ", error] })) : rows.length === 0 ? (_jsx("text", { fg: "gray", children: "No rules resolved." })) : (_jsxs("box", { flexDirection: "column", children: [toFixRows.length > 0 && (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "red", children: `Needs fixing (${toFixRows.length})` }), toFixRows
246
- .slice(0, 100)
247
- .map(({ row, index }) => renderRuleRow(row, index === selectedIdx)), _jsx("text", { children: " " })] })), _jsx("text", { fg: "gray", children: `Managed items (${cleanCount})` }), sourceGroups.map((group, gi) => (_jsxs("box", { flexDirection: "column", children: [gi > 0 && _jsx("text", { children: " " }), _jsx("text", { fg: "gray", children: ` ${group.label}` }), group.items.map(({ row, index }) => renderRuleRow(row, index === selectedIdx))] }, group.tier)))] }));
248
- const listPanel = (_jsxs("box", { flexDirection: "column", children: [summary, _jsx("text", { children: " " }), listBody] }));
249
- const detailPanel = renderDetail(selectedRow, data?.warnings ?? []);
250
- const subtitle = data
251
- ? toFixCount === 0
252
- ? `${rows.length} managed`
253
- : `${toFixCount} to fix`
254
- : "";
255
- return (_jsx(ScreenLayout, { title: "Managed Git State", subtitle: subtitle, currentScreen: "gitignore", footerHints: [
256
- { keys: ["↑", "↓"], label: "move" },
257
- { keys: ["f"], label: "fix selected" },
258
- { keys: ["F"], label: "fix all" },
259
- { keys: ["t"], label: "template" },
260
- { keys: ["e"], label: "edit" },
261
- { keys: ["r"], label: "reload" },
262
- ], listPanel: listPanel, detailPanel: detailPanel }));
263
- }
264
- function violationMatchesRule(rule, violation) {
265
- if (violation.path === rule.pattern)
266
- return true;
267
- if (!rule.pattern.endsWith("/") && !rule.pattern.includes("*"))
268
- return false;
269
- const prefix = rule.pattern.replace(/\*+$/, "");
270
- return violation.path.startsWith(prefix);
271
- }
272
- function renderSummary(rules, ignoreRules, trackRules, toFix) {
273
- return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { children: _jsx("span", { fg: "white", children: `${rules} managed item${rules === 1 ? "" : "s"}` }) }), _jsx("text", { children: _jsx("span", { fg: "gray", children: `${ignoreRules} ignored ${trackRules} tracked` }) }), toFix === 0 ? (_jsx("text", { fg: "green", children: "Everything matches \u2014 nothing to fix." })) : (_jsx("text", { fg: "yellow", children: `${toFix} need${toFix === 1 ? "s" : ""} fixing — f one, F all` }))] }));
274
- }
275
- function renderDetail(row, warnings) {
276
- if (!row) {
277
- return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "gray", children: "No rule selected." }), warnings.length > 0 && (_jsxs("box", { flexDirection: "column", marginTop: 1, children: [_jsx("text", { fg: "yellow", children: "Warnings:" }), warnings.map((w, i) => (_jsx("text", { fg: "gray", children: w }, i)))] }))] }));
278
- }
279
- const { rule, violations } = row;
280
- const reason = getGitignoreRuleReason(rule.action, rule.pattern);
281
- const setIn = sourceName(rule.source);
282
- if (violations.length === 0) {
283
- return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "white", children: _jsx("strong", { children: rule.pattern }) }), _jsx("text", { children: " " }), _jsx("text", { fg: "green", children: `✓ Already ${rule.action === "ignore" ? "ignored" : "tracked"} — nothing to do.` }), _jsx("text", { children: " " }), _jsx("text", { fg: "gray", children: reason }), _jsx("text", { children: " " }), _jsx("text", { fg: "gray", children: `Set in: ${setIn}` }), renderWarnings(warnings)] }));
284
- }
285
- return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "white", children: _jsx("strong", { children: rule.pattern }) }), _jsx("text", { children: " " }), _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Should be: " }), _jsx("span", { fg: "white", children: rule.action === "ignore" ? "ignored by git" : "tracked by git" })] }), _jsx("text", { fg: "gray", children: reason }), _jsx("text", { fg: "gray", children: `Set in: ${setIn}` }), _jsx("text", { children: " " }), _jsx("text", { fg: "yellow", children: `Needs fixing (${violations.length}):` }), violations.map((v, i) => (_jsxs("box", { flexDirection: "column", marginTop: i === 0 ? 0 : 1, children: [_jsx("text", { fg: "white", children: v.path }), _jsx("text", { fg: "gray", children: describeKind(v.kind) }), _jsx("text", { fg: v.severity === "safe" ? "cyan" : "red", children: actionHint(v.kind) })] }, `${v.kind}:${v.path}:${i}`))), renderWarnings(warnings)] }));
286
- }
287
- /** Friendly, where-to-change-it name for a rule's source manifest. */
288
- function sourceName(source) {
289
- switch (source) {
290
- case "project":
291
- return "Project (.claude/gitignore.json)";
292
- case "global":
293
- return "Global (~/.claude/gitignore.json)";
294
- case "profile":
295
- return "Profile";
296
- case "builtin":
297
- return "Default (built into claudeup)";
298
- }
299
- }
300
- function describeKind(kind) {
301
- switch (kind) {
302
- case "tracked-but-should-ignore":
303
- return "Git is tracking it, but it should be ignored.";
304
- case "ignored-but-should-track":
305
- return "A .gitignore rule hides it, but it should be in git.";
306
- case "untracked-and-should-track":
307
- return "It's on disk but git isn't tracking it yet.";
308
- case "missing-from-gitignore":
309
- return "It should be ignored, but nothing in .gitignore covers it.";
310
- }
311
- }
312
- function actionHint(kind) {
313
- switch (kind) {
314
- case "tracked-but-should-ignore":
315
- return "Press f to stop tracking it and add it to .gitignore.";
316
- case "ignored-but-should-track":
317
- return "Press f to remove the .gitignore line and add it to git.";
318
- case "untracked-and-should-track":
319
- return "Press f to add it to git.";
320
- case "missing-from-gitignore":
321
- return "Press f to add it to .gitignore (F to fix all).";
322
- }
323
- }
324
- function renderWarnings(warnings) {
325
- if (warnings.length === 0)
326
- return null;
327
- return (_jsxs("box", { flexDirection: "column", marginTop: 1, children: [_jsx("text", { fg: "yellow", children: "Warnings:" }), warnings.map((w, i) => (_jsx("text", { fg: "gray", children: w }, i)))] }));
328
- }
@@ -1,238 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- import { useEffect, useCallback, useState, useRef } from "react";
3
- import { useApp, useModal, useNavigation } 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 { searchMcpServers, formatDate } from "../../services/mcp-registry.js";
8
- import { addMcpServer, setAllowMcp } from "../../services/claude-settings.js";
9
- import { ScrollableList } from "../components/ScrollableList.js";
10
- /**
11
- * Deduplicate servers by name, keeping only the latest version.
12
- * Uses version string comparison, falling back to published_at date.
13
- */
14
- function deduplicateServers(servers) {
15
- const serverMap = new Map();
16
- for (const server of servers) {
17
- const existing = serverMap.get(server.name);
18
- if (!existing) {
19
- serverMap.set(server.name, server);
20
- continue;
21
- }
22
- // Compare versions - keep the newer one
23
- const isNewer = compareVersions(server, existing) > 0;
24
- if (isNewer) {
25
- serverMap.set(server.name, server);
26
- }
27
- }
28
- return Array.from(serverMap.values());
29
- }
30
- /**
31
- * Compare two servers by version. Returns:
32
- * > 0 if a is newer
33
- * < 0 if b is newer
34
- * 0 if equal or cannot determine
35
- */
36
- function compareVersions(a, b) {
37
- // Try semver comparison first
38
- if (a.version && b.version) {
39
- const aParts = a.version.split(".").map((n) => parseInt(n, 10) || 0);
40
- const bParts = b.version.split(".").map((n) => parseInt(n, 10) || 0);
41
- for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
42
- const aVal = aParts[i] || 0;
43
- const bVal = bParts[i] || 0;
44
- if (aVal !== bVal) {
45
- return aVal - bVal;
46
- }
47
- }
48
- }
49
- // Fall back to published_at date
50
- if (a.published_at && b.published_at) {
51
- return (new Date(a.published_at).getTime() - new Date(b.published_at).getTime());
52
- }
53
- // If one has a date and other doesn't, prefer the one with date
54
- if (a.published_at && !b.published_at)
55
- return 1;
56
- if (!a.published_at && b.published_at)
57
- return -1;
58
- return 0;
59
- }
60
- export function McpRegistryScreen() {
61
- const { state, dispatch } = useApp();
62
- const { mcpRegistry } = state;
63
- const modal = useModal();
64
- const { navigateToScreen } = useNavigation();
65
- const dimensions = useDimensions();
66
- const [servers, setServers] = useState([]);
67
- const [isLoading, setIsLoading] = useState(true);
68
- const [error, setError] = useState(null);
69
- const [searchQuery, setSearchQuery] = useState(mcpRegistry.searchQuery || "");
70
- const isSearchActive = state.isSearching &&
71
- state.currentRoute.screen === "mcp-registry" &&
72
- !state.modal;
73
- const searchTimeoutRef = useRef(null);
74
- // Load servers
75
- const loadServers = useCallback(async (query) => {
76
- setIsLoading(true);
77
- setError(null);
78
- try {
79
- const response = await searchMcpServers({ query, limit: 50 });
80
- if (!response || !Array.isArray(response.servers)) {
81
- throw new Error("Invalid response from MCP Registry API");
82
- }
83
- // Deduplicate to show only latest version of each server
84
- const deduplicated = deduplicateServers(response.servers);
85
- setServers(deduplicated);
86
- }
87
- catch (err) {
88
- setError(err instanceof Error ? err.message : "Failed to load servers");
89
- setServers([]);
90
- }
91
- setIsLoading(false);
92
- }, []);
93
- // Debounced search
94
- const debouncedSearch = useCallback((query) => {
95
- if (searchTimeoutRef.current) {
96
- clearTimeout(searchTimeoutRef.current);
97
- }
98
- searchTimeoutRef.current = setTimeout(() => {
99
- loadServers(query);
100
- }, 300);
101
- }, [loadServers]);
102
- useEffect(() => {
103
- loadServers(searchQuery);
104
- return () => {
105
- if (searchTimeoutRef.current) {
106
- clearTimeout(searchTimeoutRef.current);
107
- }
108
- };
109
- }, []);
110
- // Keyboard handling
111
- useKeyboard((event) => {
112
- // Handle search mode
113
- if (isSearchActive) {
114
- if (event.name === "escape") {
115
- dispatch({ type: "SET_SEARCHING", isSearching: false });
116
- }
117
- else if (event.name === "enter") {
118
- // Exit search mode and stay on list for navigation
119
- dispatch({ type: "SET_SEARCHING", isSearching: false });
120
- }
121
- else if (event.name === "up") {
122
- // Allow navigation while searching
123
- const newIndex = Math.max(0, mcpRegistry.selectedIndex - 1);
124
- dispatch({ type: "MCPREGISTRY_SELECT", index: newIndex });
125
- }
126
- else if (event.name === "down") {
127
- // Allow navigation while searching
128
- const newIndex = Math.min(Math.max(0, servers.length - 1), mcpRegistry.selectedIndex + 1);
129
- dispatch({ type: "MCPREGISTRY_SELECT", index: newIndex });
130
- }
131
- else if (event.name === "backspace" || event.name === "delete") {
132
- const newQuery = searchQuery.slice(0, -1);
133
- setSearchQuery(newQuery);
134
- dispatch({ type: "MCPREGISTRY_SEARCH", query: newQuery });
135
- debouncedSearch(newQuery);
136
- }
137
- else if (event.name.length === 1 && !event.ctrl && !event.meta) {
138
- const newQuery = searchQuery + event.name;
139
- setSearchQuery(newQuery);
140
- dispatch({ type: "MCPREGISTRY_SEARCH", query: newQuery });
141
- debouncedSearch(newQuery);
142
- }
143
- return;
144
- }
145
- if (state.modal)
146
- return;
147
- // Start search with /
148
- if (event.name === "/") {
149
- dispatch({ type: "SET_SEARCHING", isSearching: true });
150
- return;
151
- }
152
- // Navigation
153
- if (event.name === "up" || event.name === "k") {
154
- const newIndex = Math.max(0, mcpRegistry.selectedIndex - 1);
155
- dispatch({ type: "MCPREGISTRY_SELECT", index: newIndex });
156
- }
157
- else if (event.name === "down" || event.name === "j") {
158
- const newIndex = Math.min(Math.max(0, servers.length - 1), mcpRegistry.selectedIndex + 1);
159
- dispatch({ type: "MCPREGISTRY_SELECT", index: newIndex });
160
- }
161
- else if (event.name === "l") {
162
- navigateToScreen("mcp");
163
- }
164
- else if (event.name === "R") {
165
- loadServers(searchQuery);
166
- }
167
- else if (event.name === "enter") {
168
- handleInstall();
169
- }
170
- });
171
- const handleInstall = async () => {
172
- const server = servers[mcpRegistry.selectedIndex];
173
- if (!server)
174
- return;
175
- const config = {
176
- type: "http",
177
- url: server.url,
178
- };
179
- modal.loading(`Installing ${server.name}...`);
180
- try {
181
- await setAllowMcp(true, state.projectPath);
182
- await addMcpServer(server.name, config, state.projectPath);
183
- modal.hideModal();
184
- await modal.message("Installed", `${server.name} has been configured.\n\nRestart Claude Code to activate.`, "success");
185
- }
186
- catch (error) {
187
- modal.hideModal();
188
- await modal.message("Error", `Failed to install: ${error}`, "error");
189
- }
190
- };
191
- // Get selected server
192
- const selectedServer = servers[mcpRegistry.selectedIndex];
193
- const renderDetail = () => {
194
- if (isLoading) {
195
- return _jsx("text", { fg: "gray", children: "Loading..." });
196
- }
197
- if (error) {
198
- return _jsxs("text", { fg: "red", children: ["Error: ", error] });
199
- }
200
- if (!selectedServer) {
201
- return _jsx("text", { fg: "gray", children: "Select a server to see details" });
202
- }
203
- const dateDisplay = selectedServer.published_at
204
- ? formatDate(selectedServer.published_at)
205
- : "unknown";
206
- const versionDisplay = selectedServer.version
207
- ? `v${selectedServer.version}`
208
- : "unknown";
209
- return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "magenta", children: _jsx("strong", { children: selectedServer.name }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { children: selectedServer.short_description }) }), _jsxs("box", { marginTop: 1, children: [_jsx("text", { children: _jsx("strong", { children: "Version: " }) }), _jsx("text", { fg: "green", children: versionDisplay })] }), _jsxs("box", { children: [_jsx("text", { children: _jsx("strong", { children: "Published: " }) }), _jsx("text", { fg: "cyan", children: dateDisplay })] }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("text", { children: _jsx("strong", { children: "URL:" }) }), _jsx("text", { fg: "cyan", children: selectedServer.url })] }), selectedServer.source_code_url && (_jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("text", { children: _jsx("strong", { children: "Source:" }) }), _jsx("text", { fg: "gray", children: selectedServer.source_code_url })] })), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "green", children: "Press Enter to install" }) })] }));
210
- };
211
- const renderListItem = (server, _idx, isSelected) => {
212
- const version = server.version ? ` v${server.version}` : "";
213
- return isSelected ? (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", server.name, version, " "] })) : (_jsxs("text", { children: [_jsx("span", { children: _jsx("strong", { children: server.name }) }), _jsx("span", { fg: "green", children: version })] }));
214
- };
215
- // Footer hints
216
- const footerHints = isSearchActive
217
- ? [
218
- { keys: ["type"], label: "search" },
219
- { keys: ["↑", "↓"], label: "nav" },
220
- { keys: ["Enter"], label: "done" },
221
- { keys: ["Esc"], label: "cancel" },
222
- ]
223
- : [
224
- { keys: ["↑", "↓"], label: "nav" },
225
- { keys: ["Enter"], label: "install" },
226
- { keys: ["/"], label: "search" },
227
- { keys: ["R"], label: "refresh" },
228
- { keys: ["l"], label: "local" },
229
- ];
230
- // Status for search placeholder
231
- const searchPlaceholder = `${servers.length} servers │ / to search`;
232
- return (_jsx(ScreenLayout, { title: "claudeup MCP Registry", subtitle: "Powered by MCP Registry", currentScreen: "mcp-registry", search: {
233
- isActive: isSearchActive,
234
- query: searchQuery,
235
- placeholder: searchPlaceholder,
236
- }, footerHints: footerHints, listPanel: isLoading ? (_jsx("text", { fg: "gray", children: "Loading..." })) : error ? (_jsxs("text", { fg: "red", children: ["Error: ", error] })) : servers.length === 0 ? (_jsx("text", { fg: "gray", children: "No servers found" })) : (_jsx(ScrollableList, { items: servers, selectedIndex: mcpRegistry.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight, getKey: (server) => server.name })), detailPanel: renderDetail() }));
237
- }
238
- export default McpRegistryScreen;