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,154 +0,0 @@
1
- /**
2
- * Builds the flat list of items for the SkillsScreen list panel.
3
- * Extracted from SkillsScreen so it can be tested independently.
4
- */
5
- export function buildSkillBrowserItems({ recommended, popular, installed, searchResults, query, isSearchLoading, skillSets = [], expandedSets = new Set(), }) {
6
- const lowerQuery = query.toLowerCase();
7
- const items = [];
8
- // ── INSTALLED: always shown at top (if any) ──
9
- const installedFiltered = lowerQuery
10
- ? installed.filter((s) => s.name.toLowerCase().includes(lowerQuery))
11
- : installed;
12
- if (installedFiltered.length > 0) {
13
- items.push({
14
- id: "cat:installed",
15
- kind: "category",
16
- label: `Installed (${installedFiltered.length})`,
17
- title: "Installed",
18
- categoryKey: "installed",
19
- count: installedFiltered.length,
20
- tone: "purple",
21
- star: "\u25CF ",
22
- });
23
- for (const skill of installedFiltered) {
24
- items.push({
25
- id: `skill:${skill.id}`,
26
- kind: "skill",
27
- label: skill.name,
28
- skill,
29
- });
30
- }
31
- }
32
- // ── RECOMMENDED: skill sets + individual skills, all as first-class items ──
33
- const filteredSets = lowerQuery
34
- ? skillSets.filter((s) => {
35
- if (s.name.toLowerCase().includes(lowerQuery))
36
- return true;
37
- if (s.description.toLowerCase().includes(lowerQuery))
38
- return true;
39
- if (s.loaded && s.skills.some((sk) => sk.name.toLowerCase().includes(lowerQuery)))
40
- return true;
41
- return false;
42
- })
43
- : skillSets;
44
- const filteredRec = lowerQuery
45
- ? recommended.filter((s) => s.name.toLowerCase().includes(lowerQuery) ||
46
- (s.description || "").toLowerCase().includes(lowerQuery))
47
- : recommended;
48
- const recommendedCount = filteredSets.length + filteredRec.length;
49
- items.push({
50
- id: "cat:recommended",
51
- kind: "category",
52
- label: "Recommended",
53
- title: "Recommended",
54
- categoryKey: "recommended",
55
- count: recommendedCount,
56
- tone: "green",
57
- star: "\u2605 ",
58
- });
59
- // Skill sets first within recommended
60
- for (const set of filteredSets) {
61
- const isExpanded = expandedSets.has(set.id);
62
- items.push({
63
- id: `skillset:${set.id}`,
64
- kind: "skillset",
65
- label: set.name,
66
- skillSet: set,
67
- expanded: isExpanded,
68
- });
69
- // When expanded, show child skills as indented skill items
70
- if (isExpanded && set.loaded) {
71
- const childSkills = lowerQuery
72
- ? set.skills.filter((sk) => sk.name.toLowerCase().includes(lowerQuery))
73
- : set.skills;
74
- for (const skill of childSkills) {
75
- items.push({
76
- id: `skill:${skill.id}`,
77
- kind: "skill",
78
- label: skill.name,
79
- skill,
80
- indent: 2,
81
- });
82
- }
83
- }
84
- }
85
- // Then individual recommended skills
86
- for (const skill of filteredRec) {
87
- items.push({
88
- id: `skill:${skill.id}`,
89
- kind: "skill",
90
- label: skill.name,
91
- skill,
92
- });
93
- }
94
- // ── SEARCH MODE ──
95
- if (query.length >= 2) {
96
- if (!isSearchLoading && searchResults.length > 0) {
97
- const recNames = new Set(recommended.map((s) => s.name));
98
- const deduped = searchResults
99
- .filter((s) => !recNames.has(s.name))
100
- .sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
101
- if (deduped.length > 0) {
102
- items.push({
103
- id: "cat:search",
104
- kind: "category",
105
- label: `Search (${deduped.length})`,
106
- title: "Search",
107
- categoryKey: "popular",
108
- count: deduped.length,
109
- tone: "teal",
110
- });
111
- for (const skill of deduped) {
112
- items.push({
113
- id: `skill:${skill.id}`,
114
- kind: "skill",
115
- label: skill.name,
116
- skill,
117
- });
118
- }
119
- }
120
- }
121
- return items;
122
- }
123
- // ── POPULAR (default, no search query) — only skills with meaningful stars ──
124
- // Dedup by name — API can return same skill name from different repos
125
- const seenPopular = new Set();
126
- const popularDeduped = popular
127
- .filter((s) => (s.stars ?? 0) >= 5)
128
- .filter((s) => {
129
- if (seenPopular.has(s.name))
130
- return false;
131
- seenPopular.add(s.name);
132
- return true;
133
- });
134
- if (popularDeduped.length > 0) {
135
- items.push({
136
- id: "cat:popular",
137
- kind: "category",
138
- label: "Popular",
139
- title: "Popular",
140
- categoryKey: "popular",
141
- count: popularDeduped.length,
142
- tone: "teal",
143
- });
144
- for (const skill of popularDeduped) {
145
- items.push({
146
- id: `skill:${skill.id}`,
147
- kind: "skill",
148
- label: skill.name,
149
- skill,
150
- });
151
- }
152
- }
153
- return items;
154
- }
@@ -1,9 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- export function CategoryHeader({ title, status, statusColor = "green", expanded = true, count, }) {
3
- const expandIcon = expanded ? "▼" : "▶";
4
- const countBadge = count !== undefined ? ` (${count})` : "";
5
- const statusText = status ? ` ${status}` : "";
6
- // Simple format without dynamic line calculation
7
- return (_jsxs("text", { children: [_jsx("span", { fg: "#666666", children: expandIcon }), _jsx("span", { fg: "white", children: _jsxs("strong", { children: [" ", title] }) }), _jsx("span", { fg: "#666666", children: countBadge }), _jsx("span", { fg: "#666666", children: " \u2500\u2500\u2500\u2500" }), _jsx("span", { fg: statusColor, children: statusText })] }));
8
- }
9
- export default CategoryHeader;
@@ -1,4 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- export function EmptyFilterState({ query, entityName = "items", }) {
3
- return (_jsxs("box", { flexDirection: "column", marginTop: 2, paddingLeft: 2, paddingRight: 2, children: [_jsxs("text", { fg: "yellow", children: ["No ", entityName, " found for \"", query, "\""] }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: "Try a different search term, or if you think" }) }), _jsx("text", { fg: "gray", children: "this is a bug, report it at:" }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "#5c9aff", children: "github.com/MadAppGang/magus/issues" }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: "Press Esc to clear the filter." }) })] }));
4
- }
@@ -1,30 +0,0 @@
1
- import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- /**
3
- * Scope indicator for list items.
4
- * Shows colored letter badges when installed, empty space when not.
5
- *
6
- * Installed in user+project: u p
7
- * Installed in project only: p
8
- * Not installed: (3 spaces)
9
- *
10
- * Colors match the detail panel: cyan=user, green=project, yellow=local
11
- */
12
- export function scopeIndicatorText(user, project, local) {
13
- const segments = [
14
- user
15
- ? { char: "u", bg: "cyan", fg: "black" }
16
- : { char: " ", bg: "", fg: "" },
17
- project
18
- ? { char: "p", bg: "green", fg: "black" }
19
- : { char: " ", bg: "", fg: "" },
20
- local
21
- ? { char: "l", bg: "yellow", fg: "black" }
22
- : { char: " ", bg: "", fg: "" },
23
- ];
24
- const text = segments.map((s) => s.char).join("");
25
- return { text, segments };
26
- }
27
- export function ScopeIndicator({ user, project, local }) {
28
- const { segments } = scopeIndicatorText(user, project, local);
29
- return (_jsx("text", { children: segments.map((s, i) => s.bg ? (_jsx("span", { bg: s.bg, fg: s.fg, children: s.char }, i)) : (_jsx("span", { children: " " }, i))) }));
30
- }
@@ -1,36 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- import { useState, useEffect, useMemo } from "react";
3
- export function ScrollableList({ items, selectedIndex, renderItem, maxHeight, showScrollIndicators = true, getKey, }) {
4
- const [scrollOffset, setScrollOffset] = useState(0);
5
- const hasItemsAbove = scrollOffset > 0;
6
- const hasItemsBelow = scrollOffset + maxHeight < items.length;
7
- const indicatorLines = (showScrollIndicators && hasItemsAbove ? 1 : 0) +
8
- (showScrollIndicators && hasItemsBelow ? 1 : 0);
9
- const effectiveMaxHeight = Math.max(1, maxHeight - indicatorLines);
10
- // Adjust scroll offset to keep selected item visible
11
- useEffect(() => {
12
- if (selectedIndex < scrollOffset) {
13
- setScrollOffset(selectedIndex);
14
- }
15
- else if (selectedIndex >= scrollOffset + effectiveMaxHeight) {
16
- setScrollOffset(selectedIndex - effectiveMaxHeight + 1);
17
- }
18
- }, [selectedIndex, effectiveMaxHeight, scrollOffset]);
19
- // Reset scroll when items change drastically (e.g. search)
20
- useEffect(() => {
21
- if (scrollOffset >= items.length) {
22
- setScrollOffset(Math.max(0, items.length - effectiveMaxHeight));
23
- }
24
- }, [items.length, scrollOffset, effectiveMaxHeight]);
25
- const visibleItems = useMemo(() => {
26
- const start = scrollOffset;
27
- const end = Math.min(scrollOffset + effectiveMaxHeight, items.length);
28
- return items.slice(start, end).map((item, idx) => ({
29
- item,
30
- originalIndex: start + idx,
31
- }));
32
- }, [items, scrollOffset, effectiveMaxHeight]);
33
- const itemsBelow = items.length - scrollOffset - effectiveMaxHeight;
34
- return (_jsxs("box", { flexDirection: "column", children: [showScrollIndicators && hasItemsAbove && (_jsxs("text", { fg: "cyan", children: ["\u2191 ", scrollOffset, " more"] })), visibleItems.map(({ item, originalIndex }) => (_jsx("box", { width: "100%", overflow: "hidden", children: renderItem(item, originalIndex, originalIndex === selectedIndex) }, getKey ? getKey(item, originalIndex) : `${originalIndex}`))), showScrollIndicators && hasItemsBelow && (_jsxs("text", { fg: "cyan", children: ["\u2193 ", itemsBelow, " more"] }))] }));
35
- }
36
- export default ScrollableList;
@@ -1,19 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- import { useKeyboardHandler } from "../hooks/useKeyboardHandler.js";
3
- export function SearchInput({ value, onChange, placeholder = "Search...", isActive, onExit, onSubmit, }) {
4
- // Handle keyboard shortcuts when active
5
- useKeyboardHandler((_input, key) => {
6
- if (!isActive)
7
- return;
8
- if (key.escape) {
9
- onExit?.();
10
- return;
11
- }
12
- if (key.return) {
13
- onSubmit?.();
14
- return;
15
- }
16
- });
17
- return (_jsxs("box", { flexDirection: "row", children: [_jsx("text", { fg: "cyan", children: "\u276F " }), _jsx("input", { value: value, onChange: onChange, placeholder: placeholder, focused: isActive, width: 40 })] }));
18
- }
19
- export default SearchInput;
@@ -1,39 +0,0 @@
1
- import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- /**
3
- * StyledText - Utility component to simplify text styling with OpenTUI
4
- *
5
- * Converts props to nested tag pattern required by OpenTUI.
6
- *
7
- * Example:
8
- * <StyledText bold color="green">Success</StyledText>
9
- * becomes: <text fg="green"><strong>Success</strong></text>
10
- */
11
- export function StyledText({ children, bold = false, color, bgColor, dim = false, italic = false, underline = false, inverse = false, }) {
12
- let content = children;
13
- // Apply text modifiers (innermost to outermost)
14
- if (bold) {
15
- content = _jsx("strong", { children: content });
16
- }
17
- if (italic) {
18
- content = _jsx("em", { children: content });
19
- }
20
- if (underline) {
21
- content = _jsx("u", { children: content });
22
- }
23
- // Note: dim and inverse use span with colors since OpenTUI doesn't have these as React elements
24
- if (dim) {
25
- content = _jsx("span", { fg: "#666666", children: content });
26
- }
27
- if (inverse) {
28
- // Inverse effect approximated with contrasting colors
29
- content = (_jsx("span", { fg: "black", bg: "white", children: content }));
30
- }
31
- // Apply colors (outer layer)
32
- const textProps = {};
33
- if (color)
34
- textProps.fg = color;
35
- if (bgColor)
36
- textProps.bg = bgColor;
37
- return _jsx("text", { ...textProps, children: content });
38
- }
39
- export default StyledText;
@@ -1,19 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- const TABS = [
3
- { key: "1", label: "Plugins", screen: "plugins" },
4
- { key: "2", label: "Skills", screen: "skills" },
5
- { key: "3", label: "MCP", screen: "mcp" },
6
- { key: "4", label: "Settings", screen: "settings" },
7
- { key: "5", label: "Profiles", screen: "profiles" },
8
- { key: "6", label: "CLI", screen: "cli-tools" },
9
- { key: "7", label: "Git State", screen: "gitignore" },
10
- { key: "8", label: "Alias", screen: "alias" },
11
- ];
12
- export function TabBar({ currentScreen }) {
13
- return (_jsx("box", { flexDirection: "row", gap: 0, children: TABS.map((tab, index) => {
14
- const isSelected = tab.screen === currentScreen;
15
- const isLast = index === TABS.length - 1;
16
- return (_jsxs("box", { flexDirection: "row", children: [isSelected ? (_jsx("box", { children: _jsx("text", { bg: "#7e57c2", fg: "white", children: _jsxs("strong", { children: [" ", tab.key, ":", tab.label, " "] }) }) })) : (_jsx("box", { children: _jsxs("text", { fg: "gray", children: [" ", tab.key, ":", tab.label, " "] }) })), !isLast && _jsx("text", { fg: "#666666", children: "\u2502" })] }, tab.key));
17
- }) }));
18
- }
19
- export default TabBar;
@@ -1,29 +0,0 @@
1
- import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- const KEY_BG = "#3f3f46";
3
- const KEY_FG = "#fafafa";
4
- const LABEL_FG = "gray";
5
- const GAP = " ";
6
- /**
7
- * Render a row of footer hints as key badges + labels:
8
- *
9
- * [↑↓] move [U] update [/] search
10
- *
11
- * Each key is shown in a chip so it reads as a pressable key, not prose.
12
- * Shared across screens so every footer looks the same.
13
- */
14
- export function FooterHints({ hints }) {
15
- const nodes = [];
16
- hints.forEach((hint, i) => {
17
- if (i > 0) {
18
- nodes.push(_jsx("span", { fg: LABEL_FG, children: GAP }, `gap-${i}`));
19
- }
20
- nodes.push(_jsx("span", { bg: KEY_BG, fg: KEY_FG, children: ` ${hint.keys.join("")} ` }, `k-${i}`));
21
- nodes.push(_jsx("span", { fg: LABEL_FG, children: ` ${hint.label}` }, `l-${i}`));
22
- });
23
- return _jsx("text", { children: nodes });
24
- }
25
- /** Convenience for callers that already have a node: same as <FooterHints>. */
26
- export function renderFooterHints(hints) {
27
- return _jsx(FooterHints, { hints: hints });
28
- }
29
- export default FooterHints;
@@ -1,6 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- export function Panel({ title, children, borderColor = "#7e57c2", titleColor = "#7e57c2", width, height, flexGrow = 1, focused = false, }) {
3
- const activeColor = focused ? "#7e57c2" : borderColor;
4
- return (_jsxs("box", { flexDirection: "column", width: width, height: height, flexGrow: flexGrow, border: true, borderStyle: "single", borderColor: activeColor, padding: 1, children: [title && (_jsx("box", { marginBottom: 0, children: _jsx("text", { fg: titleColor, children: _jsx("strong", { children: title }) }) })), _jsx("box", { flexDirection: "column", flexGrow: 1, children: children })] }));
5
- }
6
- export default Panel;
@@ -1,14 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- export function ProgressBar({ message, current, total }) {
3
- const isDeterminate = current !== undefined && total !== undefined && total > 0;
4
- if (isDeterminate) {
5
- const barWidth = 20;
6
- const filled = Math.round((current / total) * barWidth);
7
- const empty = barWidth - filled;
8
- const bar = "█".repeat(filled) + "░".repeat(empty);
9
- return (_jsxs("box", { children: [_jsx("text", { fg: "cyan", children: "\u27F3" }), _jsxs("text", { children: [" ", message, " "] }), _jsxs("text", { fg: "cyan", children: ["[", bar, "] ", current, "/", total] })] }));
10
- }
11
- // Indeterminate progress
12
- return (_jsxs("box", { children: [_jsx("text", { fg: "cyan", children: "\u27F3" }), _jsxs("text", { children: [" ", message] }), _jsx("text", { fg: "gray", children: " ..." })] }));
13
- }
14
- export default ProgressBar;
@@ -1,6 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- export function ScopeTabs({ scope, onToggle: _onToggle, toggleHint, }) {
3
- const isProject = scope === "project";
4
- return (_jsxs("box", { marginBottom: 1, flexDirection: "row", gap: 1, children: [_jsx("box", { children: isProject ? (_jsx("text", { bg: "cyan", fg: "black", children: _jsx("strong", { children: " \u25C6 Project " }) })) : (_jsx("text", { fg: "gray", children: " \u25CB Project " })) }), _jsx("box", { children: !isProject ? (_jsx("text", { bg: "magenta", fg: "white", children: _jsx("strong", { children: " \u25C6 Global " }) })) : (_jsx("text", { fg: "gray", children: " \u25CB Global " })) }), toggleHint && (_jsx("box", { marginLeft: 2, children: _jsxs("text", { fg: "#666666", children: ["(", toggleHint, ")"] }) }))] }));
5
- }
6
- export default ScopeTabs;
@@ -1,16 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- import { useDimensions } from "../../state/DimensionsContext.js";
3
- import { TabBar } from "../TabBar.js";
4
- import { FooterHints } from "./FooterHints.js";
5
- const HEADER_COLOR = "#7e57c2";
6
- export function ScreenLayout({ title, subtitle, currentScreen, search, statusLine, footerHints, listPanel, detailPanel, }) {
7
- const dimensions = useDimensions();
8
- const hasSearchBar = search && (search.isActive || search.query);
9
- // Fixed chrome: top line + tabs + line + header + separator + footer = 6
10
- // Search bar adds 1 when active
11
- const fixedHeight = 6 + (hasSearchBar ? 1 : 0);
12
- const panelHeight = Math.max(5, dimensions.contentHeight - fixedHeight);
13
- const lineWidth = Math.max(10, dimensions.terminalWidth - 4);
14
- return (_jsxs("box", { flexDirection: "column", height: dimensions.contentHeight, children: [_jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: _jsx("text", { fg: "#333333", children: "─".repeat(lineWidth) }) }), _jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: _jsx(TabBar, { currentScreen: currentScreen }) }), _jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: _jsx("text", { fg: "#333333", children: "─".repeat(lineWidth) }) }), _jsxs("box", { height: 1, paddingLeft: 1, paddingRight: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsx("text", { fg: HEADER_COLOR, children: _jsx("strong", { children: title }) }), subtitle && _jsx("text", { fg: "gray", children: subtitle }), !subtitle && statusLine ? statusLine : null] }), hasSearchBar && (_jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: search.isActive ? (_jsxs("text", { children: [_jsx("span", { fg: "green", children: "Filter: " }), _jsx("span", { fg: "white", children: search.query }), _jsx("span", { bg: "white", fg: "black", children: " " })] })) : (_jsxs("text", { children: [_jsx("span", { fg: "green", children: "Filter: " }), _jsx("span", { fg: "yellow", children: search.query }), _jsx("span", { fg: "gray", children: " (Esc to clear)" })] })) })), _jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: _jsx("text", { fg: "#444444", children: "─".repeat(lineWidth) }) }), _jsxs("box", { flexDirection: "row", height: panelHeight, children: [_jsx("box", { flexDirection: "column", width: "49%", height: panelHeight, paddingRight: 1, children: listPanel }), _jsx("box", { flexDirection: "column", width: 1, height: panelHeight, children: _jsx("text", { fg: "#444444", children: "│".repeat(panelHeight) }) }), _jsx("box", { width: "50%", height: panelHeight, paddingLeft: 1, children: _jsx("scrollbox", { height: panelHeight, scrollY: true, scrollX: false, children: _jsx("box", { flexDirection: "column", children: detailPanel }) }) })] }), _jsx("box", { height: 1, paddingLeft: 1, children: Array.isArray(footerHints) ? (_jsx(FooterHints, { hints: footerHints })) : typeof footerHints === "string" ? (_jsx("text", { fg: "gray", children: footerHints })) : (footerHints) })] }));
15
- }
16
- export default ScreenLayout;
@@ -1,5 +0,0 @@
1
- export { Panel } from "./Panel.js";
2
- export { ScopeTabs } from "./ScopeTabs.js";
3
- export { ProgressBar } from "./ProgressBar.js";
4
- export { ScreenLayout } from "./ScreenLayout.js";
5
- export { FooterHints, renderFooterHints, } from "./FooterHints.js";
@@ -1,14 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- import { useKeyboard } from "../../hooks/useKeyboard.js";
3
- export function ConfirmModal({ title, message, onConfirm, onCancel, }) {
4
- useKeyboard((key) => {
5
- if (key.name === "y" || key.name === "Y") {
6
- onConfirm();
7
- }
8
- else if (key.name === "n" || key.name === "N" || key.name === "escape") {
9
- onCancel();
10
- }
11
- });
12
- return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, width: 60, children: [_jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#EDEDED", children: _jsx("strong", { children: title }) }) }), _jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#A1A1AA", children: message }) }), _jsxs("box", { marginTop: 1, flexDirection: "row", children: [_jsx("text", { fg: "#71717A", children: "Press " }), _jsx("text", { fg: "#EDEDED", children: "Y" }), _jsx("text", { fg: "#71717A", children: " to confirm \u2022 " }), _jsx("text", { fg: "#EDEDED", children: "N" }), _jsx("text", { fg: "#71717A", children: " to cancel" })] })] }));
13
- }
14
- export default ConfirmModal;
@@ -1,5 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- export function InputModal({ title, label, defaultValue = "", onSubmit, }) {
3
- return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, width: 60, children: [_jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#EDEDED", children: _jsx("strong", { children: title }) }) }), _jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#A1A1AA", children: label }) }), _jsx("box", { border: true, borderStyle: "rounded", borderColor: "#3F3F46", paddingLeft: 1, paddingRight: 1, width: 54, children: _jsx("input", { value: defaultValue, onSubmit: onSubmit, focused: true, width: 50 }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "#71717A", children: "\u21B5 to confirm \u2022 Esc to cancel" }) })] }));
4
- }
5
- export default InputModal;
@@ -1,17 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- import { useState, useEffect } from "react";
3
- const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
4
- export function LoadingModal({ message }) {
5
- const [frame, setFrame] = useState(0);
6
- useEffect(() => {
7
- const interval = setInterval(() => {
8
- setFrame((prev) => (prev + 1) % SPINNER_FRAMES.length);
9
- }, 80);
10
- return () => clearInterval(interval);
11
- }, []);
12
- const lines = message.split("\n");
13
- const mainMessage = lines[0];
14
- const detail = lines.slice(1).join("\n");
15
- return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, children: [_jsxs("box", { flexDirection: "row", children: [_jsx("text", { fg: "#A1A1AA", children: SPINNER_FRAMES[frame] }), _jsxs("text", { fg: "#EDEDED", children: [" ", mainMessage] })] }), detail ? (_jsx("box", { marginTop: 0, paddingLeft: 2, children: _jsx("text", { fg: "#525252", children: detail }) })) : null] }));
16
- }
17
- export default LoadingModal;
@@ -1,16 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- import { useKeyboard } from "../../hooks/useKeyboard.js";
3
- const variantConfig = {
4
- info: { icon: "ℹ", color: "#60A5FA" },
5
- success: { icon: "✓", color: "#4ADE80" },
6
- error: { icon: "✗", color: "#F87171" },
7
- };
8
- export function MessageModal({ title, message, variant, onDismiss, }) {
9
- const config = variantConfig[variant];
10
- useKeyboard(() => {
11
- // Any key dismisses
12
- onDismiss();
13
- });
14
- return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, width: 60, children: [_jsxs("box", { marginBottom: 1, children: [_jsxs("text", { fg: config.color, children: [config.icon, " "] }), _jsx("text", { fg: "#EDEDED", children: _jsx("strong", { children: title }) })] }), _jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#A1A1AA", children: message }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "#71717A", children: "Press any key to continue" }) })] }));
15
- }
16
- export default MessageModal;
@@ -1,137 +0,0 @@
1
- import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- import React, { useState } from "react";
3
- import { useApp } from "../../state/AppContext.js";
4
- import { useKeyboard } from "../../hooks/useKeyboard.js";
5
- import { ConfirmModal } from "./ConfirmModal.js";
6
- import { InputModal } from "./InputModal.js";
7
- import { SelectModal } from "./SelectModal.js";
8
- import { MessageModal } from "./MessageModal.js";
9
- import { LoadingModal } from "./LoadingModal.js";
10
- import { VersionMismatchModal } from "./VersionMismatchModal.js";
11
- /**
12
- * Container that renders the active modal as an overlay
13
- * Handles ALL keyboard events when a modal is open to avoid
14
- * conflicts with multiple useKeyboard hooks in child components
15
- *
16
- * Input modal: Enter/Escape handled by OpenTUI <input> onSubmit + ModalContainer escape
17
- * Select modal: keyboard fully handled here (index tracking + Enter/arrows)
18
- */
19
- export function ModalContainer() {
20
- const { state } = useApp();
21
- const { modal } = state;
22
- // Track select modal index here (lifted from SelectModal)
23
- const [selectIndex, setSelectIndex] = useState(0);
24
- // Reset select index when modal changes
25
- const modalRef = React.useRef(modal);
26
- if (modal !== modalRef.current) {
27
- modalRef.current = modal;
28
- if (modal?.type === "select") {
29
- setSelectIndex(modal.defaultIndex ?? 0);
30
- }
31
- else if (modal?.type === "version-mismatch") {
32
- setSelectIndex(0);
33
- }
34
- }
35
- // Handle keyboard events for modals
36
- useKeyboard((key) => {
37
- if (!modal)
38
- return;
39
- if (modal.type === "loading")
40
- return;
41
- // Escape — close any modal
42
- if (key.name === "escape") {
43
- if (modal.type === "confirm")
44
- modal.onCancel();
45
- else if (modal.type === "input")
46
- modal.onCancel();
47
- else if (modal.type === "select")
48
- modal.onCancel();
49
- else if (modal.type === "message")
50
- modal.onDismiss();
51
- else if (modal.type === "version-mismatch")
52
- modal.onDismiss();
53
- return;
54
- }
55
- // 'q' to close — but NOT for input modals (need to type 'q')
56
- if (key.name === "q" && modal.type !== "input") {
57
- if (modal.type === "confirm")
58
- modal.onCancel();
59
- else if (modal.type === "select")
60
- modal.onCancel();
61
- else if (modal.type === "message")
62
- modal.onDismiss();
63
- else if (modal.type === "version-mismatch")
64
- modal.onDismiss();
65
- return;
66
- }
67
- // Input modal — let OpenTUI <input> handle Enter via onSubmit
68
- if (modal.type === "input") {
69
- return;
70
- }
71
- // Select modal — handle navigation and selection
72
- if (modal.type === "select") {
73
- if (key.name === "return" || key.name === "enter") {
74
- modal.onSelect(modal.options[selectIndex].value);
75
- }
76
- else if (key.name === "up" || key.name === "k") {
77
- setSelectIndex((prev) => Math.max(0, prev - 1));
78
- }
79
- else if (key.name === "down" || key.name === "j") {
80
- setSelectIndex((prev) => Math.min(modal.options.length - 1, prev + 1));
81
- }
82
- return;
83
- }
84
- // Version-mismatch modal — handle navigation and selection (2 options)
85
- if (modal.type === "version-mismatch") {
86
- if (key.name === "return" || key.name === "enter") {
87
- if (selectIndex === 0)
88
- modal.onFix();
89
- else
90
- modal.onDismiss();
91
- }
92
- else if (key.name === "up" || key.name === "k") {
93
- setSelectIndex((prev) => Math.max(0, prev - 1));
94
- }
95
- else if (key.name === "down" || key.name === "j") {
96
- setSelectIndex((prev) => Math.min(1, prev + 1));
97
- }
98
- return;
99
- }
100
- // Message modal — Enter to dismiss
101
- if (modal.type === "message") {
102
- if (key.name === "return" || key.name === "enter") {
103
- modal.onDismiss();
104
- }
105
- }
106
- // Confirm modal — Enter to confirm
107
- if (modal.type === "confirm") {
108
- if (key.name === "return" || key.name === "enter") {
109
- modal.onConfirm();
110
- }
111
- }
112
- });
113
- if (!modal) {
114
- return null;
115
- }
116
- const renderModal = () => {
117
- switch (modal.type) {
118
- case "confirm":
119
- return (_jsx(ConfirmModal, { title: modal.title, message: modal.message, onConfirm: modal.onConfirm, onCancel: modal.onCancel }));
120
- case "input":
121
- return (_jsx(InputModal, { title: modal.title, label: modal.label, defaultValue: modal.defaultValue, onSubmit: modal.onSubmit, onCancel: modal.onCancel }));
122
- case "select":
123
- return (_jsx(SelectModal, { title: modal.title, message: modal.message, options: modal.options, defaultIndex: selectIndex, onSelect: modal.onSelect, onCancel: modal.onCancel }));
124
- case "message":
125
- return (_jsx(MessageModal, { title: modal.title, message: modal.message, variant: modal.variant, onDismiss: modal.onDismiss }));
126
- case "loading":
127
- return _jsx(LoadingModal, { message: modal.message });
128
- case "version-mismatch":
129
- return (_jsx(VersionMismatchModal, { mismatches: modal.mismatches, defaultIndex: selectIndex }));
130
- default:
131
- return null;
132
- }
133
- };
134
- // Center the modal on screen
135
- return (_jsx("box", { position: "absolute", width: "100%", height: "100%", justifyContent: "center", alignItems: "center", children: renderModal() }));
136
- }
137
- export default ModalContainer;
@@ -1,18 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- import { useDimensions } from "../../state/DimensionsContext.js";
3
- export function SelectModal({ title, message, options, defaultIndex, onSelect: _onSelect, onCancel: _onCancel, }) {
4
- // Keyboard handling is done by ModalContainer
5
- // defaultIndex is the live selectedIndex from ModalContainer state
6
- const selectedIndex = defaultIndex ?? 0;
7
- // Size to terminal width: 60% of available, clamped to [50, 90].
8
- // Previously hardcoded at 50, which forced descriptions and long item
9
- // labels (e.g. plugin specs) to wrap mid-sentence and produce orphan
10
- // punctuation lines.
11
- const dimensions = useDimensions();
12
- const width = Math.max(50, Math.min(90, Math.floor(dimensions.terminalWidth * 0.6)));
13
- return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, width: width, children: [_jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#EDEDED", children: _jsx("strong", { children: title }) }) }), _jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#A1A1AA", children: message }) }), _jsx("box", { flexDirection: "column", paddingLeft: 1, children: options.map((option, idx) => {
14
- const isSelected = idx === selectedIndex;
15
- return (_jsxs("text", { fg: isSelected ? "#F4F4F5" : "#A1A1AA", children: [_jsx("span", { fg: isSelected ? "#F4F4F5" : "#71717A", children: isSelected ? "❯ " : " " }), isSelected ? _jsx("strong", { children: option.label }) : option.label] }, option.value));
16
- }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "#71717A", children: "\u2191\u2193 Select \u2022 \u21B5 Confirm \u2022 Esc Cancel" }) })] }));
17
- }
18
- export default SelectModal;