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,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;