claudeup 4.19.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 (180) 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-refresh.test.ts +267 -0
  18. package/src/__tests__/plugin-requires.test.ts +133 -0
  19. package/src/__tests__/plugin-setup.test.ts +7 -0
  20. package/src/__tests__/profile-command.test.ts +116 -0
  21. package/src/__tests__/profile-materializer.test.ts +82 -0
  22. package/src/__tests__/profile-sync.test.ts +136 -0
  23. package/src/__tests__/registry-version-resolution.test.ts +74 -0
  24. package/src/__tests__/resolve-executable.test.ts +42 -0
  25. package/src/__tests__/resolver.test.ts +218 -0
  26. package/src/__tests__/symlink-manager.test.ts +99 -0
  27. package/src/__tests__/toolchain.test.ts +56 -0
  28. package/src/cli/claude.ts +21 -0
  29. package/src/cli/doctor.ts +132 -0
  30. package/src/cli/install.ts +360 -0
  31. package/src/cli/profile.ts +166 -0
  32. package/src/cli/router.ts +107 -0
  33. package/src/cli/update.ts +85 -0
  34. package/src/data/cli-tools.ts +7 -7
  35. package/src/data/gitignore-defaults.ts +4 -0
  36. package/src/data/marketplaces.ts +12 -0
  37. package/src/data/predefined-profiles.ts +3 -4
  38. package/src/main.tsx +11 -154
  39. package/src/prerunner/index.ts +62 -27
  40. package/src/services/claude-cli.ts +19 -22
  41. package/src/services/claude-settings.ts +99 -26
  42. package/src/services/conventions-manager.ts +865 -0
  43. package/src/services/doctor-bins.ts +49 -0
  44. package/src/services/doctor.ts +509 -0
  45. package/src/services/install-plan.ts +107 -0
  46. package/src/services/manifest.ts +166 -0
  47. package/src/services/marketplace-refresh.ts +173 -0
  48. package/src/services/plugin-manager.ts +7 -1
  49. package/src/services/plugin-requires.ts +215 -0
  50. package/src/services/plugin-version-check.ts +9 -4
  51. package/src/services/profile-materializer.ts +61 -0
  52. package/src/services/profile-sync.ts +150 -0
  53. package/src/services/resolver.ts +293 -0
  54. package/src/services/symlink-manager.ts +146 -0
  55. package/src/services/toolchain.ts +97 -0
  56. package/src/services/version-check.ts +10 -11
  57. package/src/types/index.ts +138 -31
  58. package/src/ui/App.tsx +0 -4
  59. package/src/ui/screens/CliToolsScreen.tsx +11 -3
  60. package/src/ui/screens/index.ts +0 -1
  61. package/src/ui/state/reducer.ts +0 -101
  62. package/src/ui/state/types.ts +0 -35
  63. package/src/utils/command-utils.ts +23 -0
  64. package/src/utils/file-locking.ts +144 -0
  65. package/src/data/alias-flags.js +0 -205
  66. package/src/data/cli-tools.js +0 -123
  67. package/src/data/gitignore-defaults.js +0 -24
  68. package/src/data/gitignore-reasons.js +0 -97
  69. package/src/data/gitignore-templates.js +0 -21
  70. package/src/data/marketplaces.js +0 -138
  71. package/src/data/mcp-servers.js +0 -509
  72. package/src/data/predefined-profiles.js +0 -248
  73. package/src/data/settings-catalog.js +0 -625
  74. package/src/data/skill-repos.js +0 -160
  75. package/src/data/statuslines.js +0 -159
  76. package/src/data/statuslines.ts +0 -188
  77. package/src/index.js +0 -4
  78. package/src/index.ts +0 -5
  79. package/src/main.js +0 -170
  80. package/src/prerunner/index.js +0 -252
  81. package/src/services/alias-settings.js +0 -51
  82. package/src/services/alias-shell-writer.js +0 -1018
  83. package/src/services/alias-store.js +0 -129
  84. package/src/services/claude-cli.js +0 -189
  85. package/src/services/claude-runner.js +0 -28
  86. package/src/services/claude-settings.js +0 -1223
  87. package/src/services/gitignore-detector.js +0 -155
  88. package/src/services/gitignore-fixer.js +0 -231
  89. package/src/services/gitignore-prerun.js +0 -46
  90. package/src/services/gitignore-resolver.js +0 -143
  91. package/src/services/gitignore-service.js +0 -117
  92. package/src/services/local-marketplace.js +0 -339
  93. package/src/services/marketplace-fetcher.js +0 -96
  94. package/src/services/marketplace-sync.js +0 -94
  95. package/src/services/mcp-registry.js +0 -87
  96. package/src/services/plugin-manager.js +0 -473
  97. package/src/services/plugin-mcp-config.js +0 -177
  98. package/src/services/plugin-setup.js +0 -499
  99. package/src/services/plugin-version-check.js +0 -262
  100. package/src/services/profiles.js +0 -161
  101. package/src/services/settings-manager.js +0 -243
  102. package/src/services/skills-manager.js +0 -393
  103. package/src/services/skillsmp-client.js +0 -87
  104. package/src/services/update-cache.js +0 -52
  105. package/src/services/version-check.js +0 -99
  106. package/src/types/gitignore.js +0 -6
  107. package/src/types/index.js +0 -1
  108. package/src/ui/App.js +0 -355
  109. package/src/ui/adapters/pluginsAdapter.js +0 -139
  110. package/src/ui/adapters/settingsAdapter.js +0 -111
  111. package/src/ui/adapters/skillsAdapter.js +0 -154
  112. package/src/ui/components/CategoryHeader.js +0 -9
  113. package/src/ui/components/EmptyFilterState.js +0 -4
  114. package/src/ui/components/FlagDetailEditor.js +0 -0
  115. package/src/ui/components/ScopeIndicator.js +0 -30
  116. package/src/ui/components/ScrollableList.js +0 -36
  117. package/src/ui/components/SearchInput.js +0 -19
  118. package/src/ui/components/StyledText.js +0 -39
  119. package/src/ui/components/TabBar.js +0 -19
  120. package/src/ui/components/layout/FooterHints.js +0 -29
  121. package/src/ui/components/layout/Panel.js +0 -6
  122. package/src/ui/components/layout/ProgressBar.js +0 -14
  123. package/src/ui/components/layout/ScopeTabs.js +0 -6
  124. package/src/ui/components/layout/ScreenLayout.js +0 -16
  125. package/src/ui/components/layout/index.js +0 -5
  126. package/src/ui/components/modals/ConfirmModal.js +0 -14
  127. package/src/ui/components/modals/InputModal.js +0 -5
  128. package/src/ui/components/modals/LoadingModal.js +0 -17
  129. package/src/ui/components/modals/MessageModal.js +0 -16
  130. package/src/ui/components/modals/ModalContainer.js +0 -137
  131. package/src/ui/components/modals/SelectModal.js +0 -18
  132. package/src/ui/components/modals/VersionMismatchModal.js +0 -36
  133. package/src/ui/components/modals/index.js +0 -6
  134. package/src/ui/components/primitives/ActionHints.js +0 -13
  135. package/src/ui/components/primitives/DetailSection.js +0 -7
  136. package/src/ui/components/primitives/KeyValueLine.js +0 -8
  137. package/src/ui/components/primitives/ListCategoryRow.js +0 -8
  138. package/src/ui/components/primitives/MetaText.js +0 -8
  139. package/src/ui/components/primitives/ScopeDetail.js +0 -32
  140. package/src/ui/components/primitives/ScopeSquares.js +0 -11
  141. package/src/ui/components/primitives/SelectableRow.js +0 -5
  142. package/src/ui/components/primitives/index.js +0 -8
  143. package/src/ui/hooks/index.js +0 -4
  144. package/src/ui/hooks/useAsyncData.js +0 -77
  145. package/src/ui/hooks/useGitignoreModal.js +0 -74
  146. package/src/ui/hooks/useKeyboard.js +0 -13
  147. package/src/ui/hooks/useKeyboardHandler.js +0 -39
  148. package/src/ui/hooks/useMismatchModal.js +0 -77
  149. package/src/ui/registry.js +0 -1
  150. package/src/ui/renderers/cliToolRenderers.js +0 -54
  151. package/src/ui/renderers/gitignoreRenderers.js +0 -46
  152. package/src/ui/renderers/mcpRenderers.js +0 -26
  153. package/src/ui/renderers/pluginRenderers.js +0 -124
  154. package/src/ui/renderers/profileRenderers.js +0 -177
  155. package/src/ui/renderers/settingsRenderers.js +0 -73
  156. package/src/ui/renderers/skillRenderers.js +0 -138
  157. package/src/ui/screens/AliasScreen.js +0 -1111
  158. package/src/ui/screens/CliToolsScreen.js +0 -338
  159. package/src/ui/screens/EnvVarsScreen.js +0 -152
  160. package/src/ui/screens/GitignoreScreen.js +0 -328
  161. package/src/ui/screens/McpRegistryScreen.js +0 -238
  162. package/src/ui/screens/McpScreen.js +0 -176
  163. package/src/ui/screens/ModelSelectorScreen.js +0 -296
  164. package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
  165. package/src/ui/screens/PluginsScreen.js +0 -769
  166. package/src/ui/screens/ProfilesScreen.js +0 -298
  167. package/src/ui/screens/SkillsScreen.js +0 -549
  168. package/src/ui/screens/StatusLineScreen.js +0 -206
  169. package/src/ui/screens/StatusLineScreen.tsx +0 -416
  170. package/src/ui/screens/index.js +0 -10
  171. package/src/ui/state/AnimationContext.js +0 -34
  172. package/src/ui/state/AppContext.js +0 -162
  173. package/src/ui/state/DimensionsContext.js +0 -71
  174. package/src/ui/state/reducer.js +0 -547
  175. package/src/ui/state/types.js +0 -1
  176. package/src/ui/theme.js +0 -47
  177. package/src/utils/clipboard.js +0 -56
  178. package/src/utils/command-utils.js +0 -19
  179. package/src/utils/fuzzy-search.js +0 -101
  180. package/src/utils/string-utils.js +0 -62
@@ -1,36 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- const COLOR_BG = "#1C1C1E";
3
- const COLOR_BORDER = "#525252";
4
- const COLOR_TITLE = "#EDEDED";
5
- const COLOR_MUTED = "#A1A1AA";
6
- const COLOR_DIM = "#71717A";
7
- const COLOR_WARN = "#FBBF24";
8
- const COLOR_BAD = "#F87171";
9
- const COLOR_GOOD = "#4ADE80";
10
- const COLOR_ACCENT = "#F4F4F5";
11
- /** Truncate a plugin name to fit the column width */
12
- function shortName(pluginId, max) {
13
- const name = pluginId.split("@")[0];
14
- if (name.length <= max)
15
- return name;
16
- return `${name.slice(0, max - 1)}…`;
17
- }
18
- /** Truncate a version string to fit the column width */
19
- function shortVersion(v, max) {
20
- if (v.length <= max)
21
- return v;
22
- return `${v.slice(0, max - 1)}…`;
23
- }
24
- export function VersionMismatchModal({ mismatches, defaultIndex, }) {
25
- const selectedIndex = defaultIndex ?? 0;
26
- // Column widths chosen to fit a 64-wide modal with padding
27
- const NAME_W = 22;
28
- const VER_W = 12;
29
- return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: COLOR_BORDER, backgroundColor: COLOR_BG, paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, width: 64, children: [_jsx("box", { marginBottom: 1, children: _jsxs("text", { fg: COLOR_TITLE, children: [_jsx("span", { fg: COLOR_WARN, children: "\u26A0 " }), _jsx("strong", { children: "Plugin Version Mismatch" })] }) }), _jsxs("box", { flexDirection: "column", marginBottom: 1, children: [_jsxs("text", { fg: COLOR_MUTED, children: ["Plugins in this project will load ", _jsx("strong", { children: "wrong versions" })] }), _jsx("text", { fg: COLOR_MUTED, children: "due to a Claude Code bug (#45997)." })] }), _jsxs("box", { flexDirection: "column", marginBottom: 1, children: [_jsx("text", { fg: COLOR_DIM, children: _jsxs("strong", { children: ["Plugin".padEnd(NAME_W), "Loaded".padEnd(VER_W), "Expected".padEnd(VER_W)] }) }), _jsxs("text", { fg: COLOR_BORDER, children: ["─".repeat(NAME_W - 1), " ", "─".repeat(VER_W - 1), " ", "─".repeat(VER_W - 1)] }), mismatches.map((m) => {
30
- const name = shortName(m.pluginId, NAME_W - 1);
31
- const loaded = `v${shortVersion(m.firstEntryVersion, VER_W - 2)}`;
32
- const expected = `v${shortVersion(m.currentProjectVersion, VER_W - 2)}`;
33
- return (_jsxs("text", { fg: COLOR_MUTED, children: [_jsx("span", { fg: COLOR_ACCENT, children: name.padEnd(NAME_W) }), _jsx("span", { fg: COLOR_BAD, children: loaded.padEnd(VER_W) }), _jsx("span", { fg: COLOR_GOOD, children: expected.padEnd(VER_W) })] }, m.pluginId));
34
- })] }), _jsx("box", { marginBottom: 1, children: _jsx("text", { fg: COLOR_DIM, children: "Details: github.com/anthropics/claude-code/issues/45997" }) }), _jsxs("box", { flexDirection: "column", paddingLeft: 1, children: [_jsxs("text", { fg: selectedIndex === 0 ? COLOR_ACCENT : COLOR_MUTED, children: [_jsx("span", { fg: selectedIndex === 0 ? COLOR_ACCENT : COLOR_DIM, children: selectedIndex === 0 ? "❯ " : " " }), selectedIndex === 0 ? (_jsx("strong", { children: "Fix all projects" })) : ("Fix all projects")] }), _jsxs("text", { fg: selectedIndex === 1 ? COLOR_ACCENT : COLOR_MUTED, children: [_jsx("span", { fg: selectedIndex === 1 ? COLOR_ACCENT : COLOR_DIM, children: selectedIndex === 1 ? "❯ " : " " }), selectedIndex === 1 ? _jsx("strong", { children: "Dismiss" }) : "Dismiss"] })] }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: COLOR_DIM, children: "\u2191\u2193 Select \u2022 \u21B5 Confirm \u2022 Esc Cancel" }) })] }));
35
- }
36
- export default VersionMismatchModal;
@@ -1,6 +0,0 @@
1
- export { ConfirmModal } from "./ConfirmModal.js";
2
- export { InputModal } from "./InputModal.js";
3
- export { SelectModal } from "./SelectModal.js";
4
- export { MessageModal } from "./MessageModal.js";
5
- export { LoadingModal } from "./LoadingModal.js";
6
- export { ModalContainer } from "./ModalContainer.js";
@@ -1,13 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- import { theme } from "../../theme.js";
3
- /**
4
- * Keyboard shortcut badges for detail panels.
5
- * Renders: [bg] key [/bg] label
6
- */
7
- export function ActionHints({ hints }) {
8
- return (_jsx("box", { flexDirection: "column", marginTop: 1, children: hints.map((hint) => (_jsxs("box", { children: [_jsxs("text", { bg: hint.tone === "danger"
9
- ? theme.hints.dangerBg
10
- : hint.tone === "primary"
11
- ? theme.hints.primaryBg
12
- : theme.hints.defaultBg, fg: "black", children: [" ", hint.key, " "] }), _jsxs("text", { fg: "gray", children: [" ", hint.label] })] }, `${hint.key}:${hint.label}`))) }));
13
- }
@@ -1,7 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- /**
3
- * A labeled section in a detail panel with optional title header.
4
- */
5
- export function DetailSection({ title, children }) {
6
- return (_jsxs("box", { flexDirection: "column", marginTop: 1, children: [title ? (_jsx("text", { fg: "cyan", children: _jsx("strong", { children: title }) })) : null, children] }));
7
- }
@@ -1,8 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- /**
3
- * Aligned label-value pair for detail panels.
4
- * Label is padded to 10 chars for consistent alignment.
5
- */
6
- export function KeyValueLine({ label, value }) {
7
- return (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: label.padEnd(10) }), value] }));
8
- }
@@ -1,8 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- import { theme } from "../../theme.js";
3
- import { SelectableRow } from "./SelectableRow.js";
4
- export function ListCategoryRow({ title, expanded = false, count, badge, tone = "gray", selected, }) {
5
- const colors = theme.category[tone];
6
- const label = `${expanded ? "▼" : "▶"} ${title}${count !== undefined ? ` (${count})` : ""}`;
7
- return (_jsxs(SelectableRow, { selected: selected, children: [!selected && (_jsx("span", { bg: colors.bg, fg: colors.fg, children: _jsxs("strong", { children: [" ", label, " "] }) })), selected && _jsxs("strong", { children: [" ", label, " "] }), !selected && badge ? (_jsxs("span", { fg: colors.badgeFg, children: [" ", badge] })) : null] }));
8
- }
@@ -1,8 +0,0 @@
1
- import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- import { theme } from "../../theme.js";
3
- /**
4
- * Subdued text for versions, stars, status indicators.
5
- */
6
- export function MetaText({ text, tone = "muted" }) {
7
- return _jsx("span", { fg: theme.meta[tone], children: text });
8
- }
@@ -1,32 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- import { theme } from "../../theme.js";
3
- /**
4
- * The u/p/l badges used in detail panels.
5
- * Renders each scope as: [bg=color] key [/bg] ● or ○ Label path
6
- */
7
- export function ScopeDetail({ scopes, paths }) {
8
- const items = [
9
- {
10
- key: "u",
11
- label: "User",
12
- color: theme.scopes.user,
13
- active: scopes.user,
14
- path: paths?.user,
15
- },
16
- {
17
- key: "p",
18
- label: "Project",
19
- color: theme.scopes.project,
20
- active: scopes.project,
21
- path: paths?.project,
22
- },
23
- {
24
- key: "l",
25
- label: "Local",
26
- color: theme.scopes.local,
27
- active: scopes.local,
28
- path: paths?.local,
29
- },
30
- ].filter((i) => i.path !== undefined || i.active !== undefined);
31
- return (_jsx("box", { flexDirection: "column", children: items.map((item) => (_jsxs("text", { children: [_jsxs("span", { bg: item.color, fg: "black", children: [" ", item.key, " "] }), _jsx("span", { fg: item.active ? item.color : "gray", children: item.active ? " ● " : " ○ " }), _jsx("span", { fg: item.color, children: item.label }), item.path && _jsxs("span", { fg: "gray", children: [" ", item.path] })] }, item.key))) }));
32
- }
@@ -1,11 +0,0 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- import { theme } from "../../theme.js";
3
- /**
4
- * Colored filled/empty squares for scope display in list items.
5
- * ■ = installed in that scope, □ = not installed.
6
- */
7
- export function ScopeSquares({ user, project, local, selected = false, }) {
8
- const inactive = selected ? "white" : theme.colors.dim;
9
- // Returns fragments of <span> — safe to embed inside a <text> parent
10
- return (_jsxs(_Fragment, { children: [_jsx("span", { fg: user ? theme.scopes.user : inactive, children: "\u25A0" }), _jsx("span", { fg: project ? theme.scopes.project : inactive, children: "\u25A0" }), local !== undefined && (_jsx("span", { fg: local ? theme.scopes.local : inactive, children: "\u25A0" }))] }));
11
- }
@@ -1,5 +0,0 @@
1
- import { jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- import { theme } from "../../theme.js";
3
- export function SelectableRow({ selected, indent = 0, children, }) {
4
- return (_jsxs("text", { bg: selected ? theme.selection.bg : undefined, fg: selected ? theme.selection.fg : undefined, children: [" ".repeat(indent), children] }));
5
- }
@@ -1,8 +0,0 @@
1
- export { SelectableRow } from "./SelectableRow.js";
2
- export { ListCategoryRow } from "./ListCategoryRow.js";
3
- export { ScopeSquares } from "./ScopeSquares.js";
4
- export { ScopeDetail } from "./ScopeDetail.js";
5
- export { ActionHints } from "./ActionHints.js";
6
- export { MetaText } from "./MetaText.js";
7
- export { KeyValueLine } from "./KeyValueLine.js";
8
- export { DetailSection } from "./DetailSection.js";
@@ -1,4 +0,0 @@
1
- export { useAsyncData, useDebouncedAsyncData } from "./useAsyncData.js";
2
- export { useKeyboardHandler } from "./useKeyboardHandler.js";
3
- export { useKeyboard } from "./useKeyboard.js";
4
- export { useMismatchModal } from "./useMismatchModal.js";
@@ -1,77 +0,0 @@
1
- import { useState, useEffect, useCallback } from "react";
2
- /**
3
- * Hook for fetching async data with loading/error states
4
- */
5
- export function useAsyncData({ fetcher, deps = [], immediate = true, }) {
6
- const [data, setData] = useState({ status: "idle" });
7
- const refetch = useCallback(async () => {
8
- setData({ status: "loading" });
9
- try {
10
- const result = await fetcher();
11
- setData({ status: "success", data: result });
12
- }
13
- catch (err) {
14
- setData({
15
- status: "error",
16
- error: err instanceof Error ? err : new Error(String(err)),
17
- });
18
- }
19
- }, [fetcher]);
20
- useEffect(() => {
21
- if (immediate) {
22
- refetch();
23
- }
24
- // eslint-disable-next-line react-hooks/exhaustive-deps
25
- }, deps);
26
- return {
27
- data,
28
- isLoading: data.status === "loading",
29
- isSuccess: data.status === "success",
30
- isError: data.status === "error",
31
- error: data.status === "error" ? data.error : null,
32
- refetch,
33
- value: data.status === "success" ? data.data : null,
34
- };
35
- }
36
- /**
37
- * Hook for fetching async data with debounced query
38
- */
39
- export function useDebouncedAsyncData({ fetcher, query, debounceMs = 300, minQueryLength = 1, }) {
40
- const [data, setData] = useState({ status: "idle" });
41
- const refetch = useCallback(async () => {
42
- if (query.length < minQueryLength) {
43
- setData({ status: "idle" });
44
- return;
45
- }
46
- setData({ status: "loading" });
47
- try {
48
- const result = await fetcher(query);
49
- setData({ status: "success", data: result });
50
- }
51
- catch (err) {
52
- setData({
53
- status: "error",
54
- error: err instanceof Error ? err : new Error(String(err)),
55
- });
56
- }
57
- }, [fetcher, query, minQueryLength]);
58
- useEffect(() => {
59
- if (query.length < minQueryLength) {
60
- setData({ status: "idle" });
61
- return;
62
- }
63
- const timeoutId = setTimeout(() => {
64
- refetch();
65
- }, debounceMs);
66
- return () => clearTimeout(timeoutId);
67
- }, [query, debounceMs, minQueryLength, refetch]);
68
- return {
69
- data,
70
- isLoading: data.status === "loading",
71
- isSuccess: data.status === "success",
72
- isError: data.status === "error",
73
- error: data.status === "error" ? data.error : null,
74
- refetch,
75
- value: data.status === "success" ? data.data : null,
76
- };
77
- }
@@ -1,74 +0,0 @@
1
- import { useCallback } from "react";
2
- import { useApp, useNavigation } from "../state/AppContext.js";
3
- export function buildGitignoreModal(args) {
4
- const { violationCount, safeCount, onFixSafe, onOpenTab, onDismiss } = args;
5
- const options = [
6
- {
7
- label: safeCount > 0
8
- ? `Add ${safeCount} missing .gitignore entr${safeCount === 1 ? "y" : "ies"}`
9
- : "(no missing .gitignore entries)",
10
- value: "fix-safe",
11
- },
12
- {
13
- label: "Open Git State tab to review",
14
- value: "open-tab",
15
- },
16
- {
17
- label: "Dismiss",
18
- value: "dismiss",
19
- },
20
- ];
21
- return {
22
- type: "select",
23
- title: "Gitignore violations detected",
24
- message: `Found ${violationCount} managed gitignore item(s) that do not match ` +
25
- `the current repo state.\n\n` +
26
- `Missing .gitignore entries can be added now. Items that change git ` +
27
- `tracking are reviewed and fixed from the Gitignore tab.`,
28
- options,
29
- onSelect: (value) => {
30
- if (value === "fix-safe")
31
- onFixSafe();
32
- else if (value === "open-tab")
33
- onOpenTab();
34
- else
35
- onDismiss();
36
- },
37
- onCancel: onDismiss,
38
- };
39
- }
40
- export function useGitignoreModal() {
41
- const { dispatch } = useApp();
42
- const { navigateToScreen } = useNavigation();
43
- const show = useCallback((args) => {
44
- if (args.violationCount === 0)
45
- return;
46
- const dismiss = () => dispatch({ type: "HIDE_MODAL" });
47
- const modal = buildGitignoreModal({
48
- violationCount: args.violationCount,
49
- safeCount: args.safeCount,
50
- onFixSafe: () => {
51
- dispatch({ type: "HIDE_MODAL" });
52
- void Promise.resolve(args.onFixSafe()).catch((err) => {
53
- dispatch({
54
- type: "SHOW_MODAL",
55
- modal: {
56
- type: "message",
57
- title: "Safe fixes failed",
58
- message: err instanceof Error ? err.message : String(err),
59
- variant: "error",
60
- onDismiss: dismiss,
61
- },
62
- });
63
- });
64
- },
65
- onOpenTab: () => {
66
- dispatch({ type: "HIDE_MODAL" });
67
- navigateToScreen("gitignore");
68
- },
69
- onDismiss: dismiss,
70
- });
71
- dispatch({ type: "SHOW_MODAL", modal });
72
- }, [dispatch, navigateToScreen]);
73
- return { show };
74
- }
@@ -1,13 +0,0 @@
1
- import { useKeyboard as useOpenTUIKeyboard } from "@opentui/react";
2
- /**
3
- * Simple re-export of OpenTUI's useKeyboard hook
4
- * Used by modals for direct keyboard event handling
5
- *
6
- * Key names in OpenTUI:
7
- * - Single characters: "a", "1", etc.
8
- * - Arrow keys: "up", "down", "left", "right"
9
- * - Special: "enter", "escape", "tab", "backspace", "delete"
10
- */
11
- export function useKeyboard(handler) {
12
- useOpenTUIKeyboard(handler);
13
- }
@@ -1,39 +0,0 @@
1
- import { useKeyboard as useOpenTUIKeyboard } from "@opentui/react";
2
- /**
3
- * useKeyboardHandler hook
4
- * Provides Ink-like API for keyboard events
5
- *
6
- * @param handler - Callback function with Ink-compatible signature
7
- *
8
- * @example
9
- * ```tsx
10
- * useKeyboardHandler((input, key) => {
11
- * if (key.return) handleEnter();
12
- * if (key.upArrow) handleUp();
13
- * if (key.escape) handleEscape();
14
- * if (input === 'q') handleQuit();
15
- * });
16
- * ```
17
- */
18
- export function useKeyboardHandler(handler) {
19
- useOpenTUIKeyboard((event) => {
20
- // Map OpenTUI key names to Ink-like key names
21
- const key = {
22
- name: event.name,
23
- ctrl: event.ctrl || false,
24
- shift: event.shift || false,
25
- meta: event.meta || false,
26
- return: event.name === "enter" || event.name === "return",
27
- upArrow: event.name === "up",
28
- downArrow: event.name === "down",
29
- leftArrow: event.name === "left",
30
- rightArrow: event.name === "right",
31
- escape: event.name === "escape",
32
- tab: event.name === "tab",
33
- };
34
- // For printable characters, pass the character as input
35
- // For special keys, pass empty string
36
- const input = event.name.length === 1 ? event.name : "";
37
- handler(input, key);
38
- });
39
- }
@@ -1,77 +0,0 @@
1
- import { useCallback } from "react";
2
- import { useApp } from "../state/AppContext.js";
3
- import { fixAllPluginVersionMismatches, } from "../../services/plugin-version-check.js";
4
- /**
5
- * Shared hook for showing the plugin version mismatch modal.
6
- *
7
- * Used in two places:
8
- * 1. App.tsx — startup check shows mismatches found across all enabled plugins
9
- * 2. PluginsScreen.tsx — post-install/update check shows a single plugin mismatch
10
- *
11
- * Both flows now share the same interactive modal with a "Fix all projects"
12
- * button, so users never have to restart claudeup to access the fix.
13
- *
14
- * The fix uses fixAllPluginVersionMismatches() which rewrites
15
- * installed_plugins.json in-place to align all entries to the current
16
- * project's expected versions. After success, shows a hint to restart
17
- * Claude Code so the new versions actually load (the running session
18
- * has the old plugin code in memory).
19
- */
20
- export function useMismatchModal() {
21
- const { dispatch } = useApp();
22
- const show = useCallback((mismatches) => {
23
- if (mismatches.length === 0)
24
- return;
25
- const dismiss = () => dispatch({ type: "HIDE_MODAL" });
26
- const fix = async () => {
27
- dispatch({ type: "HIDE_MODAL" });
28
- dispatch({
29
- type: "SHOW_PROGRESS",
30
- state: { message: "Aligning plugin versions across projects..." },
31
- });
32
- try {
33
- const results = await fixAllPluginVersionMismatches(mismatches);
34
- dispatch({ type: "HIDE_PROGRESS" });
35
- // Count how many projects were updated across all plugins
36
- const totalUpdated = Array.from(results.values()).reduce((sum, r) => sum + r.updated, 0);
37
- dispatch({
38
- type: "SHOW_MODAL",
39
- modal: {
40
- type: "message",
41
- title: "Plugin Versions Aligned",
42
- message: `Updated ${totalUpdated} project entr${totalUpdated === 1 ? "y" : "ies"} in ` +
43
- `installed_plugins.json.\n\n` +
44
- `Restart Claude Code for the new versions to load. ` +
45
- `The running session still has the old plugin code in memory.`,
46
- variant: "success",
47
- onDismiss: dismiss,
48
- },
49
- });
50
- }
51
- catch (error) {
52
- dispatch({ type: "HIDE_PROGRESS" });
53
- dispatch({
54
- type: "SHOW_MODAL",
55
- modal: {
56
- type: "message",
57
- title: "Fix Failed",
58
- message: `Could not align plugin versions: ${error instanceof Error ? error.message : String(error)}\n\n` +
59
- `Try running claudeup again, or manually update the plugin in each project.`,
60
- variant: "error",
61
- onDismiss: dismiss,
62
- },
63
- });
64
- }
65
- };
66
- dispatch({
67
- type: "SHOW_MODAL",
68
- modal: {
69
- type: "version-mismatch",
70
- mismatches,
71
- onFix: fix,
72
- onDismiss: dismiss,
73
- },
74
- });
75
- }, [dispatch]);
76
- return { show };
77
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,54 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- import { theme } from "../theme.js";
3
- // ─── Helpers ────────────────────────────────────────────────────────────────
4
- function getUninstallHint(tool, method, brewFormula) {
5
- switch (method) {
6
- case "bun": return `bun remove -g ${tool.packageName}`;
7
- case "npm": return `npm uninstall -g ${tool.packageName}`;
8
- case "pnpm": return `pnpm remove -g ${tool.packageName}`;
9
- case "yarn": return `yarn global remove ${tool.packageName}`;
10
- case "brew": return `brew uninstall ${brewFormula || tool.name}`;
11
- case "pip": return `pip uninstall ${tool.packageName}`;
12
- default: return "";
13
- }
14
- }
15
- // ─── Row renderer ──────────────────────────────────────────────────────────────
16
- export function renderCliToolRow(status, _index, isSelected) {
17
- const { tool, installed, installedVersion, hasUpdate, checking, allMethods } = status;
18
- const hasConflict = allMethods && allMethods.length > 1;
19
- let icon;
20
- let iconColor;
21
- if (!installed) {
22
- icon = "○";
23
- iconColor = theme.colors.muted;
24
- }
25
- else if (hasConflict) {
26
- icon = "!";
27
- iconColor = theme.colors.danger;
28
- }
29
- else if (hasUpdate) {
30
- icon = "*";
31
- iconColor = theme.colors.warning;
32
- }
33
- else {
34
- icon = "●";
35
- iconColor = theme.colors.success;
36
- }
37
- const versionText = installedVersion ? ` v${installedVersion}` : "";
38
- const methodTag = installed && allMethods?.length
39
- ? ` ${allMethods.join("+")}`
40
- : "";
41
- if (isSelected) {
42
- return (_jsxs("text", { bg: theme.selection.bg, fg: theme.selection.fg, children: [" ", icon, " ", tool.displayName, versionText, methodTag, checking ? " ..." : "", " "] }));
43
- }
44
- return (_jsxs("text", { children: [_jsxs("span", { fg: iconColor, children: [" ", icon] }), _jsxs("span", { fg: theme.colors.text, children: [" ", tool.displayName] }), versionText ? _jsx("span", { fg: theme.colors.success, children: versionText }) : null, methodTag ? _jsx("span", { fg: hasConflict ? theme.colors.danger : theme.colors.dim, children: methodTag }) : null, checking ? _jsx("span", { fg: theme.colors.muted, children: " ..." }) : null] }));
45
- }
46
- // ─── Detail renderer ───────────────────────────────────────────────────────────
47
- export function renderCliToolDetail(status) {
48
- if (!status) {
49
- return (_jsx("box", { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 1, children: _jsx("text", { fg: theme.colors.muted, children: "Select a tool to see details" }) }));
50
- }
51
- const { tool, installed, installedVersion, latestVersion, hasUpdate, checking, installMethod, allMethods, updateCommand, brewFormula } = status;
52
- const hasConflict = allMethods && allMethods.length > 1;
53
- return (_jsxs("box", { flexDirection: "column", children: [_jsxs("box", { marginBottom: 1, children: [_jsx("text", { fg: theme.colors.info, children: _jsxs("strong", { children: ["⚙ ", tool.displayName] }) }), hasUpdate ? _jsx("text", { fg: theme.colors.warning, children: " \u2B06" }) : null, hasConflict ? _jsx("text", { fg: theme.colors.danger, children: " !" }) : null] }), _jsx("text", { fg: theme.colors.muted, children: tool.description }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Status " }), !installed ? (_jsx("text", { fg: theme.colors.muted, children: "○ Not installed" })) : checking ? (_jsx("text", { fg: theme.colors.success, children: "● Checking..." })) : hasUpdate ? (_jsx("text", { fg: theme.colors.warning, children: "● Update available" })) : (_jsx("text", { fg: theme.colors.success, children: "● Up to date" }))] }), installedVersion ? (_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Version " }), _jsxs("text", { children: [_jsxs("span", { fg: theme.colors.success, children: ["v", installedVersion] }), latestVersion && hasUpdate ? (_jsxs("span", { fg: theme.colors.warning, children: [" \u2192 v", latestVersion] })) : null] })] })) : latestVersion ? (_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Latest " }), _jsxs("text", { fg: theme.colors.text, children: ["v", latestVersion] })] })) : null, installed && updateCommand ? (_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Update " }), _jsx("text", { fg: theme.colors.accent, children: updateCommand })] })) : !installed ? (_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Install " }), _jsx("text", { fg: theme.colors.accent, children: tool.installCommand })] })) : null, _jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Website " }), _jsx("text", { fg: theme.colors.link, children: tool.website })] })] }), hasConflict ? (_jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("box", { children: _jsx("text", { bg: theme.colors.danger, fg: "white", children: _jsxs("strong", { children: [" ", "Conflict: installed via ", allMethods.join(" + "), " "] }) }) }), _jsx("box", { marginTop: 1, children: _jsxs("text", { fg: theme.colors.muted, children: ["Multiple installs can cause version mismatches.", "\n", "Keep one, remove the rest:"] }) }), allMethods.map((method, i) => (_jsx("box", { children: _jsxs("text", { children: [_jsx("span", { fg: i === 0 ? theme.colors.success : theme.colors.danger, children: i === 0 ? " ● keep " : " ○ remove " }), _jsx("span", { fg: theme.colors.warning, children: method }), i > 0 ? (_jsx("span", { fg: theme.colors.dim, children: ` ${getUninstallHint(tool, method, brewFormula)}` })) : (_jsx("span", { fg: theme.colors.dim, children: " (active in PATH)" }))] }) }, method))), _jsxs("box", { marginTop: 1, children: [_jsxs("text", { bg: theme.colors.danger, fg: "white", children: [" ", "c", " "] }), _jsx("text", { fg: theme.colors.muted, children: " Resolve \u2014 pick which to keep" })] })] })) : null, _jsx("box", { marginTop: 2, flexDirection: "column", children: !installed ? (_jsxs("box", { children: [_jsxs("text", { bg: theme.colors.success, fg: "black", children: [" ", "Enter", " "] }), _jsx("text", { fg: theme.colors.muted, children: " Install" }), _jsxs("text", { fg: theme.colors.dim, children: [" ", tool.installCommand] })] })) : hasUpdate ? (_jsxs("box", { children: [_jsxs("text", { bg: theme.colors.warning, fg: "black", children: [" ", "Enter", " "] }), _jsxs("text", { fg: theme.colors.muted, children: [" Update to v", latestVersion] })] })) : (_jsxs("box", { children: [_jsxs("text", { bg: theme.colors.muted, fg: "white", children: [" ", "Enter", " "] }), _jsx("text", { fg: theme.colors.muted, children: " Reinstall" })] })) })] }));
54
- }
@@ -1,46 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- // Two color channels that never collide:
3
- // • Badge color = direction (where the item ends up): green = tracked by git,
4
- // reddish = ignored/dropped from git. Same on clean AND violation rows.
5
- // • Filename color = severity: dim gray when fine, red when it's a violation.
6
- // No row background tints — red is reserved entirely for "needs attention".
7
- const TRACK_FG = "#5fd787"; // green — item ends up tracked by git
8
- const IGNORE_FG = "#d78787"; // reddish — item ends up ignored / dropped
9
- const VIOLATION_PATH_FG = "#ff5f5f"; // bright red filename = look here
10
- const CLEAN_PATH_FG = "gray";
11
- const SELECTED_BG = "#2a2a3a"; // faint slate highlight for the cursor row
12
- // Violation badges are solid chips so the required direction reads at a glance:
13
- // green chip = will be tracked, red chip = will be ignored/dropped.
14
- const TRACK_CHIP_BG = "#1f5132";
15
- const IGNORE_CHIP_BG = "#5a2323";
16
- const CHIP_FG = "white";
17
- // The only marker is the cursor: ▶ on the selected row, blank otherwise.
18
- // Action (track/ignore) is read from the badge word and, for clean items,
19
- // from the group the row sits under — so no per-row dot is needed.
20
- const MARKER_SELECTED = "▶";
21
- const MARKER_NONE = " ";
22
- // Layout: " M action path" — keep the prefix compact so the narrow terminal
23
- // case still has room for the path without wrapping.
24
- const MAX_PATH_LEN = 22;
25
- function truncatePath(p) {
26
- return p.length > MAX_PATH_LEN ? p.slice(0, MAX_PATH_LEN - 1) + "…" : p;
27
- }
28
- export function renderRuleRow(row, isSelected) {
29
- const { rule, violations } = row;
30
- const path = truncatePath(rule.pattern);
31
- const key = `${rule.action}:${rule.pattern}`;
32
- // Badge text padded to a fixed width so paths line up in a column.
33
- const badge = rule.action === "ignore" ? "ignore" : "track ";
34
- const badgeFg = rule.action === "ignore" ? IGNORE_FG : TRACK_FG;
35
- const suffix = violations.length > 1 ? ` +${violations.length - 1}` : "";
36
- const rowBg = isSelected ? SELECTED_BG : undefined;
37
- const marker = isSelected ? MARKER_SELECTED : MARKER_NONE;
38
- const isViolation = violations.length > 0;
39
- if (isViolation) {
40
- // Solid badge chip + red filename. The chip color is the fix direction.
41
- const chipBg = rule.action === "ignore" ? IGNORE_CHIP_BG : TRACK_CHIP_BG;
42
- return (_jsxs("text", { bg: rowBg, children: [_jsx("span", { fg: VIOLATION_PATH_FG, children: ` ${marker} ` }), _jsx("span", { bg: chipBg, fg: CHIP_FG, children: ` ${badge} ` }), _jsx("span", { fg: VIOLATION_PATH_FG, children: ` ${path}${suffix}` })] }, key));
43
- }
44
- // Clean row: flat colored badge word, dim path, no chip.
45
- return (_jsxs("text", { bg: rowBg, children: [_jsx("span", { fg: badgeFg, children: ` ${marker} ` }), _jsx("span", { fg: badgeFg, children: badge }), _jsx("span", { fg: CLEAN_PATH_FG, children: ` ${path}` })] }, key));
46
- }
@@ -1,26 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- import { theme } from "../theme.js";
3
- // ─── Row renderers ─────────────────────────────────────────────────────────────
4
- export function renderMcpRow(item, _index, isSelected) {
5
- if (item.kind === "category") {
6
- return (_jsx("text", { fg: theme.selection.bg, children: _jsxs("strong", { children: ["▸ ", item.label] }) }));
7
- }
8
- const { server, isInstalled } = item;
9
- const icon = isInstalled ? "●" : "○";
10
- const iconColor = isInstalled ? theme.colors.success : theme.colors.muted;
11
- if (isSelected) {
12
- return (_jsxs("text", { bg: theme.selection.bg, fg: theme.selection.fg, children: [" ", icon, " ", server.name, " "] }));
13
- }
14
- return (_jsxs("text", { children: [_jsx("span", { fg: iconColor, children: icon }), _jsxs("span", { fg: theme.colors.text, children: [" ", server.name] }), server.requiresConfig ? (_jsx("span", { fg: theme.colors.warning, children: " \u2699" })) : null] }));
15
- }
16
- // ─── Detail renderer ───────────────────────────────────────────────────────────
17
- export function renderMcpDetail(item, loading) {
18
- if (loading) {
19
- return _jsx("text", { fg: theme.colors.muted, children: "Loading MCP servers..." });
20
- }
21
- if (!item || item.kind === "category") {
22
- return (_jsx("box", { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 1, children: _jsx("text", { fg: theme.colors.muted, children: "Select a server to see details" }) }));
23
- }
24
- const { server, isInstalled } = item;
25
- return (_jsxs("box", { flexDirection: "column", children: [_jsxs("box", { marginBottom: 1, children: [_jsx("text", { fg: theme.colors.info, children: _jsxs("strong", { children: ["⚡ ", server.name] }) }), server.requiresConfig ? (_jsx("text", { fg: theme.colors.warning, children: " \u2699" })) : null] }), _jsx("text", { fg: theme.colors.muted, children: server.description }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Status " }), isInstalled ? (_jsx("text", { fg: theme.colors.success, children: "● Installed" })) : (_jsx("text", { fg: theme.colors.muted, children: "○ Not installed" }))] }), _jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Type " }), _jsx("text", { fg: theme.colors.text, children: server.type === "http" ? "HTTP" : "Command" })] }), server.type === "http" ? (_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "URL " }), _jsx("text", { fg: theme.colors.link, children: server.url })] })) : (_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Command " }), _jsx("text", { fg: theme.colors.info, children: server.command })] })), server.requiresConfig ? (_jsxs("box", { children: [_jsx("text", { fg: theme.colors.muted, children: "Config " }), _jsxs("text", { fg: theme.colors.warning, children: [server.configFields?.length ?? 0, " fields required"] })] })) : null] }), _jsx("box", { marginTop: 2, children: isInstalled ? (_jsxs("box", { children: [_jsxs("text", { bg: theme.colors.danger, fg: "white", children: [" ", "Enter", " "] }), _jsx("text", { fg: theme.colors.muted, children: " Remove server" })] })) : (_jsxs("box", { children: [_jsxs("text", { bg: theme.colors.success, fg: "black", children: [" ", "Enter", " "] }), _jsx("text", { fg: theme.colors.muted, children: " Install server" })] })) })] }));
26
- }