claudeup 4.18.0 → 4.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/bin/claudeup.js +43 -19
  2. package/package.json +14 -19
  3. package/scripts/build-binaries.ts +77 -0
  4. package/scripts/check-optional-deps.ts +49 -0
  5. package/scripts/test-isolated.ts +43 -0
  6. package/src/__tests__/alias-adopt.test.ts +364 -0
  7. package/src/__tests__/alias-parser.test.ts +92 -0
  8. package/src/__tests__/alias-shell-writer.test.ts +7 -0
  9. package/src/__tests__/alias-store.test.ts +77 -0
  10. package/src/__tests__/cli-router.test.ts +65 -0
  11. package/src/__tests__/conventions-integration.test.ts +745 -0
  12. package/src/__tests__/conventions-manager.test.ts +1172 -0
  13. package/src/__tests__/doctor-bins.test.ts +35 -0
  14. package/src/__tests__/doctor.test.ts +450 -0
  15. package/src/__tests__/dual-write-prevention.test.ts +14 -4
  16. package/src/__tests__/file-locking.test.ts +208 -0
  17. package/src/__tests__/gitignore-prerun.test.ts +1 -0
  18. package/src/__tests__/install-command.test.ts +81 -0
  19. package/src/__tests__/install-plan.test.ts +98 -0
  20. package/src/__tests__/manifest.test.ts +136 -0
  21. package/src/__tests__/marketplace-refresh.test.ts +267 -0
  22. package/src/__tests__/plugin-requires.test.ts +133 -0
  23. package/src/__tests__/plugin-setup.test.ts +118 -0
  24. package/src/__tests__/profile-command.test.ts +116 -0
  25. package/src/__tests__/profile-materializer.test.ts +82 -0
  26. package/src/__tests__/profile-sync.test.ts +136 -0
  27. package/src/__tests__/registry-version-resolution.test.ts +74 -0
  28. package/src/__tests__/resolve-executable.test.ts +42 -0
  29. package/src/__tests__/resolver.test.ts +218 -0
  30. package/src/__tests__/symlink-manager.test.ts +99 -0
  31. package/src/__tests__/toolchain.test.ts +56 -0
  32. package/src/cli/claude.ts +21 -0
  33. package/src/cli/doctor.ts +132 -0
  34. package/src/cli/install.ts +360 -0
  35. package/src/cli/profile.ts +166 -0
  36. package/src/cli/router.ts +107 -0
  37. package/src/cli/update.ts +85 -0
  38. package/src/data/alias-flags.ts +11 -1
  39. package/src/data/cli-tools.ts +7 -7
  40. package/src/data/gitignore-defaults.ts +4 -0
  41. package/src/data/marketplaces.ts +12 -0
  42. package/src/data/predefined-profiles.ts +3 -4
  43. package/src/main.tsx +11 -154
  44. package/src/prerunner/index.ts +62 -27
  45. package/src/services/alias-shell-writer.ts +382 -8
  46. package/src/services/alias-store.ts +60 -0
  47. package/src/services/claude-cli.ts +19 -22
  48. package/src/services/claude-settings.ts +99 -26
  49. package/src/services/conventions-manager.ts +865 -0
  50. package/src/services/doctor-bins.ts +49 -0
  51. package/src/services/doctor.ts +509 -0
  52. package/src/services/install-plan.ts +107 -0
  53. package/src/services/manifest.ts +166 -0
  54. package/src/services/marketplace-refresh.ts +173 -0
  55. package/src/services/plugin-manager.ts +7 -1
  56. package/src/services/plugin-requires.ts +215 -0
  57. package/src/services/plugin-setup.ts +61 -4
  58. package/src/services/plugin-version-check.ts +9 -4
  59. package/src/services/profile-materializer.ts +61 -0
  60. package/src/services/profile-sync.ts +150 -0
  61. package/src/services/resolver.ts +293 -0
  62. package/src/services/symlink-manager.ts +146 -0
  63. package/src/services/toolchain.ts +97 -0
  64. package/src/services/version-check.ts +10 -11
  65. package/src/types/index.ts +138 -31
  66. package/src/ui/App.tsx +16 -11
  67. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  68. package/src/ui/components/modals/ConfirmModal.tsx +1 -1
  69. package/src/ui/screens/AliasScreen.tsx +491 -359
  70. package/src/ui/screens/CliToolsScreen.tsx +11 -3
  71. package/src/ui/screens/PluginsScreen.tsx +3 -1
  72. package/src/ui/screens/index.ts +0 -1
  73. package/src/ui/state/reducer.ts +6 -102
  74. package/src/ui/state/types.ts +8 -35
  75. package/src/utils/command-utils.ts +23 -0
  76. package/src/utils/file-locking.ts +144 -0
  77. package/src/data/alias-flags.js +0 -196
  78. package/src/data/cli-tools.js +0 -123
  79. package/src/data/gitignore-defaults.js +0 -24
  80. package/src/data/gitignore-reasons.js +0 -97
  81. package/src/data/gitignore-templates.js +0 -21
  82. package/src/data/marketplaces.js +0 -138
  83. package/src/data/mcp-servers.js +0 -509
  84. package/src/data/predefined-profiles.js +0 -248
  85. package/src/data/settings-catalog.js +0 -625
  86. package/src/data/skill-repos.js +0 -160
  87. package/src/data/statuslines.js +0 -159
  88. package/src/data/statuslines.ts +0 -188
  89. package/src/index.js +0 -4
  90. package/src/index.ts +0 -5
  91. package/src/main.js +0 -170
  92. package/src/prerunner/index.js +0 -252
  93. package/src/services/alias-settings.js +0 -51
  94. package/src/services/alias-shell-writer.js +0 -764
  95. package/src/services/alias-store.js +0 -77
  96. package/src/services/claude-cli.js +0 -189
  97. package/src/services/claude-runner.js +0 -28
  98. package/src/services/claude-settings.js +0 -1223
  99. package/src/services/gitignore-detector.js +0 -155
  100. package/src/services/gitignore-fixer.js +0 -231
  101. package/src/services/gitignore-prerun.js +0 -46
  102. package/src/services/gitignore-resolver.js +0 -143
  103. package/src/services/gitignore-service.js +0 -117
  104. package/src/services/local-marketplace.js +0 -339
  105. package/src/services/marketplace-fetcher.js +0 -96
  106. package/src/services/marketplace-sync.js +0 -94
  107. package/src/services/mcp-registry.js +0 -87
  108. package/src/services/plugin-manager.js +0 -473
  109. package/src/services/plugin-mcp-config.js +0 -177
  110. package/src/services/plugin-setup.js +0 -444
  111. package/src/services/plugin-version-check.js +0 -262
  112. package/src/services/profiles.js +0 -161
  113. package/src/services/settings-manager.js +0 -243
  114. package/src/services/skills-manager.js +0 -393
  115. package/src/services/skillsmp-client.js +0 -87
  116. package/src/services/update-cache.js +0 -52
  117. package/src/services/version-check.js +0 -99
  118. package/src/types/gitignore.js +0 -6
  119. package/src/types/index.js +0 -1
  120. package/src/ui/App.js +0 -346
  121. package/src/ui/adapters/pluginsAdapter.js +0 -139
  122. package/src/ui/adapters/settingsAdapter.js +0 -111
  123. package/src/ui/adapters/skillsAdapter.js +0 -154
  124. package/src/ui/components/CategoryHeader.js +0 -9
  125. package/src/ui/components/EmptyFilterState.js +0 -4
  126. package/src/ui/components/ScopeIndicator.js +0 -30
  127. package/src/ui/components/ScrollableList.js +0 -36
  128. package/src/ui/components/SearchInput.js +0 -19
  129. package/src/ui/components/StyledText.js +0 -39
  130. package/src/ui/components/TabBar.js +0 -19
  131. package/src/ui/components/layout/FooterHints.js +0 -29
  132. package/src/ui/components/layout/Panel.js +0 -6
  133. package/src/ui/components/layout/ProgressBar.js +0 -14
  134. package/src/ui/components/layout/ScopeTabs.js +0 -6
  135. package/src/ui/components/layout/ScreenLayout.js +0 -16
  136. package/src/ui/components/layout/index.js +0 -5
  137. package/src/ui/components/modals/ConfirmModal.js +0 -14
  138. package/src/ui/components/modals/InputModal.js +0 -5
  139. package/src/ui/components/modals/LoadingModal.js +0 -17
  140. package/src/ui/components/modals/MessageModal.js +0 -16
  141. package/src/ui/components/modals/ModalContainer.js +0 -137
  142. package/src/ui/components/modals/SelectModal.js +0 -18
  143. package/src/ui/components/modals/VersionMismatchModal.js +0 -36
  144. package/src/ui/components/modals/index.js +0 -6
  145. package/src/ui/components/primitives/ActionHints.js +0 -13
  146. package/src/ui/components/primitives/DetailSection.js +0 -7
  147. package/src/ui/components/primitives/KeyValueLine.js +0 -8
  148. package/src/ui/components/primitives/ListCategoryRow.js +0 -8
  149. package/src/ui/components/primitives/MetaText.js +0 -8
  150. package/src/ui/components/primitives/ScopeDetail.js +0 -32
  151. package/src/ui/components/primitives/ScopeSquares.js +0 -11
  152. package/src/ui/components/primitives/SelectableRow.js +0 -5
  153. package/src/ui/components/primitives/index.js +0 -8
  154. package/src/ui/hooks/index.js +0 -4
  155. package/src/ui/hooks/useAsyncData.js +0 -77
  156. package/src/ui/hooks/useGitignoreModal.js +0 -74
  157. package/src/ui/hooks/useKeyboard.js +0 -13
  158. package/src/ui/hooks/useKeyboardHandler.js +0 -39
  159. package/src/ui/hooks/useMismatchModal.js +0 -77
  160. package/src/ui/registry.js +0 -1
  161. package/src/ui/renderers/cliToolRenderers.js +0 -54
  162. package/src/ui/renderers/gitignoreRenderers.js +0 -46
  163. package/src/ui/renderers/mcpRenderers.js +0 -26
  164. package/src/ui/renderers/pluginRenderers.js +0 -124
  165. package/src/ui/renderers/profileRenderers.js +0 -177
  166. package/src/ui/renderers/settingsRenderers.js +0 -73
  167. package/src/ui/renderers/skillRenderers.js +0 -138
  168. package/src/ui/screens/AliasScreen.js +0 -1008
  169. package/src/ui/screens/CliToolsScreen.js +0 -338
  170. package/src/ui/screens/EnvVarsScreen.js +0 -152
  171. package/src/ui/screens/GitignoreScreen.js +0 -328
  172. package/src/ui/screens/McpRegistryScreen.js +0 -238
  173. package/src/ui/screens/McpScreen.js +0 -176
  174. package/src/ui/screens/ModelSelectorScreen.js +0 -296
  175. package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
  176. package/src/ui/screens/PluginsScreen.js +0 -766
  177. package/src/ui/screens/ProfilesScreen.js +0 -298
  178. package/src/ui/screens/SkillsScreen.js +0 -549
  179. package/src/ui/screens/StatusLineScreen.js +0 -206
  180. package/src/ui/screens/StatusLineScreen.tsx +0 -416
  181. package/src/ui/screens/index.js +0 -10
  182. package/src/ui/state/AnimationContext.js +0 -34
  183. package/src/ui/state/AppContext.js +0 -162
  184. package/src/ui/state/DimensionsContext.js +0 -71
  185. package/src/ui/state/reducer.js +0 -543
  186. package/src/ui/state/types.js +0 -1
  187. package/src/ui/theme.js +0 -47
  188. package/src/utils/clipboard.js +0 -56
  189. package/src/utils/command-utils.js +0 -19
  190. package/src/utils/fuzzy-search.js +0 -101
  191. package/src/utils/string-utils.js +0 -62
@@ -1,206 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/react/jsx-runtime";
2
- import { useEffect, useCallback, useState } from "react";
3
- import { useApp, useModal } from "../state/AppContext.js";
4
- import { useDimensions } from "../state/DimensionsContext.js";
5
- import { useKeyboard } from "../hooks/useKeyboard.js";
6
- import { ScreenLayout } from "../components/layout/index.js";
7
- import { statusLineCategories } from "../../data/statuslines.js";
8
- import { ScrollableList } from "../components/ScrollableList.js";
9
- import { setStatusLine, getStatusLine, setGlobalStatusLine, getGlobalStatusLine, } from "../../services/claude-settings.js";
10
- export function StatusLineScreen() {
11
- const { state, dispatch } = useApp();
12
- const { statusline: statusLine } = state;
13
- const modal = useModal();
14
- const dimensions = useDimensions();
15
- const [projectStatusLine, setProjectStatusLineState] = useState();
16
- const [globalStatusLine, setGlobalStatusLineState] = useState();
17
- const [isLoading, setIsLoading] = useState(true);
18
- // Fetch data
19
- const fetchData = useCallback(async () => {
20
- setIsLoading(true);
21
- try {
22
- const [project, global] = await Promise.all([
23
- getStatusLine(state.projectPath),
24
- getGlobalStatusLine(),
25
- ]);
26
- setProjectStatusLineState(project);
27
- setGlobalStatusLineState(global);
28
- }
29
- catch (error) {
30
- // Silent error handling - show empty state
31
- }
32
- setIsLoading(false);
33
- }, [state.projectPath]);
34
- useEffect(() => {
35
- fetchData();
36
- }, [fetchData]);
37
- // Get current status line based on scope
38
- const getCurrentStatusLine = () => {
39
- return statusLine.scope === "project"
40
- ? projectStatusLine
41
- : globalStatusLine;
42
- };
43
- // Build list items with categories
44
- const buildListItems = () => {
45
- const currentForScope = getCurrentStatusLine();
46
- const items = [];
47
- for (const category of statusLineCategories) {
48
- items.push({
49
- label: category.name,
50
- isCategory: true,
51
- });
52
- for (const preset of category.presets) {
53
- const isActive = currentForScope === preset.template;
54
- const status = isActive ? "●" : "○";
55
- items.push({
56
- label: ` ${status} ${preset.name}`,
57
- preset,
58
- });
59
- }
60
- }
61
- // Add custom option at the end
62
- items.push({
63
- label: "+ Custom Status Line",
64
- isCustom: true,
65
- });
66
- return items;
67
- };
68
- const listItems = buildListItems();
69
- // Keyboard handling
70
- useKeyboard((event) => {
71
- if (state.isSearching || state.modal)
72
- return;
73
- if (event.name === "up" || event.name === "k") {
74
- const newIndex = Math.max(0, statusLine.selectedIndex - 1);
75
- dispatch({ type: "STATUSLINE_SELECT", index: newIndex });
76
- }
77
- else if (event.name === "down" || event.name === "j") {
78
- const newIndex = Math.min(listItems.length - 1, statusLine.selectedIndex + 1);
79
- dispatch({ type: "STATUSLINE_SELECT", index: newIndex });
80
- }
81
- else if (event.name === "p") {
82
- dispatch({ type: "STATUSLINE_SET_SCOPE", scope: "project" });
83
- }
84
- else if (event.name === "g") {
85
- dispatch({ type: "STATUSLINE_SET_SCOPE", scope: "global" });
86
- }
87
- else if (event.name === "r") {
88
- handleReset();
89
- }
90
- else if (event.name === "enter") {
91
- handleSelect();
92
- }
93
- });
94
- const saveStatusLine = async (template) => {
95
- if (statusLine.scope === "global") {
96
- await setGlobalStatusLine(template);
97
- }
98
- else {
99
- await setStatusLine(template, state.projectPath);
100
- }
101
- };
102
- const handleSelect = async () => {
103
- const item = listItems[statusLine.selectedIndex];
104
- if (!item || item.isCategory)
105
- return;
106
- const scopeLabel = statusLine.scope === "global" ? "Global" : "Project";
107
- if (item.isCustom) {
108
- const template = await modal.input(`Custom Status Line (${scopeLabel})`, "Enter template (use {model}, {cost}, etc.):", getCurrentStatusLine() || "");
109
- if (template !== null) {
110
- modal.loading(`Saving custom status line...`);
111
- try {
112
- await saveStatusLine(template);
113
- modal.hideModal();
114
- await modal.message("Saved", `Custom status line saved to ${scopeLabel}.\n\nRestart Claude Code to apply changes.`, "success");
115
- fetchData();
116
- }
117
- catch (error) {
118
- modal.hideModal();
119
- await modal.message("Error", `Failed to save: ${error}`, "error");
120
- }
121
- }
122
- }
123
- else if (item.preset) {
124
- modal.loading(`Applying ${item.preset.name}...`);
125
- try {
126
- await saveStatusLine(item.preset.template);
127
- modal.hideModal();
128
- await modal.message("Applied", `"${item.preset.name}" applied to ${scopeLabel}.\n\nRestart Claude Code to apply changes.`, "success");
129
- fetchData();
130
- }
131
- catch (error) {
132
- modal.hideModal();
133
- await modal.message("Error", `Failed to apply: ${error}`, "error");
134
- }
135
- }
136
- };
137
- const handleReset = async () => {
138
- const scopeLabel = statusLine.scope === "global" ? "Global" : "Project";
139
- const confirmed = await modal.confirm("Reset Status Line", `Clear the ${scopeLabel} status line configuration?`);
140
- if (confirmed) {
141
- modal.loading("Resetting...");
142
- try {
143
- await saveStatusLine("");
144
- modal.hideModal();
145
- await modal.message("Reset", `${scopeLabel} status line has been cleared.`, "success");
146
- fetchData();
147
- }
148
- catch (error) {
149
- modal.hideModal();
150
- await modal.message("Error", `Failed to reset: ${error}`, "error");
151
- }
152
- }
153
- };
154
- // Get selected item
155
- const selectedItem = listItems[statusLine.selectedIndex];
156
- // Build preview
157
- const renderPreview = () => {
158
- if (isLoading) {
159
- return _jsx("text", { fg: "gray", children: "Loading status line settings..." });
160
- }
161
- if (!selectedItem || selectedItem.isCategory) {
162
- return (_jsx("box", { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 1, children: _jsx("text", { fg: "gray", children: "Select a theme to see preview" }) }));
163
- }
164
- if (selectedItem.isCustom) {
165
- return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "cyan", children: _jsx("strong", { children: "\u2728 Custom Status Line" }) }), _jsx("text", { fg: "gray", children: "Create your own unique status line!" }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("text", { fg: "yellow", children: _jsx("strong", { children: "Available variables:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { fg: "green", children: _jsx("strong", { children: "{model}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Model name"] }), _jsxs("text", { children: [_jsx("span", { fg: "green", children: _jsx("strong", { children: "{model_short}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Short name"] }), _jsxs("text", { children: [_jsx("span", { fg: "yellow", children: _jsx("strong", { children: "{cost}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Session cost"] }), _jsxs("text", { children: [_jsx("span", { fg: "#5c9aff", children: _jsx("strong", { children: "{cwd}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Working directory"] }), _jsxs("text", { children: [_jsx("span", { fg: "magenta", children: _jsx("strong", { children: "{git_branch}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Git branch"] }), _jsxs("text", { children: [_jsx("span", { fg: "cyan", children: _jsx("strong", { children: "{input_tokens}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Input tokens"] }), _jsxs("text", { children: [_jsx("span", { fg: "cyan", children: _jsx("strong", { children: "{output_tokens}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Output tokens"] }), _jsxs("text", { children: [_jsx("span", { fg: "red", children: _jsx("strong", { children: "{session_duration}" }) }), _jsx("span", { fg: "gray", children: "\u2192" }), " Duration"] })] })] })] }));
166
- }
167
- if (selectedItem.preset) {
168
- const example = selectedItem.preset.template
169
- .replace("{model}", "claude-sonnet-4")
170
- .replace("{model_short}", "sonnet")
171
- .replace("{cost}", "0.42")
172
- .replace("{cwd}", "~/myapp")
173
- .replace("{git_branch}", "main")
174
- .replace("{input_tokens}", "1.2k")
175
- .replace("{output_tokens}", "850")
176
- .replace("{session_duration}", "12m");
177
- return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "cyan", children: _jsxs("strong", { children: ["\u25C6 ", selectedItem.preset.name] }) }), _jsx("text", { fg: "gray", children: selectedItem.preset.description }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("text", { fg: "yellow", children: _jsx("strong", { children: "Preview:" }) }), _jsx("box", { marginTop: 1, paddingLeft: 1, paddingRight: 1, borderStyle: "rounded", borderColor: "green", children: _jsx("text", { fg: "white", children: example }) })] }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("text", { fg: "#666666", children: "Template:" }), _jsx("text", { fg: "gray", children: selectedItem.preset.template })] })] }));
178
- }
179
- return null;
180
- };
181
- const renderListItem = (item, _idx, isSelected) => {
182
- if (item.isCategory) {
183
- return (_jsx("text", { fg: "magenta", children: _jsxs("strong", { children: ["\u25B8 ", item.label] }) }));
184
- }
185
- if (item.isCustom) {
186
- return isSelected ? (_jsx("text", { bg: "cyan", fg: "black", children: _jsx("strong", { children: " \u2795 Custom Status Line " }) })) : (_jsx("text", { fg: "cyan", children: _jsxs("strong", { children: [" ", "\u2795 Custom Status Line"] }) }));
187
- }
188
- const currentForScope = getCurrentStatusLine();
189
- const isActive = item.preset && currentForScope === item.preset.template;
190
- return isSelected ? (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", isActive ? "●" : "○", " ", item.preset?.name || "", " "] })) : (_jsxs("text", { fg: isActive ? "green" : "white", children: [" ", isActive ? "●" : "○", " ", item.preset?.name || ""] }));
191
- };
192
- // Build status line content
193
- const scopeLabel = statusLine.scope === "project" ? "Project" : "Global";
194
- const currentValue = getCurrentStatusLine();
195
- const statusContent = (_jsxs(_Fragment, { children: [_jsx("text", { fg: "gray", children: "Scope: " }), _jsx("text", { fg: "cyan", children: scopeLabel }), _jsx("text", { fg: "gray", children: " \u2502 Current: " }), _jsx("text", { fg: "green", children: currentValue
196
- ? currentValue.slice(0, 35) + (currentValue.length > 35 ? "..." : "")
197
- : "(not set)" })] }));
198
- return (_jsx(ScreenLayout, { title: "claudeup Status Line", currentScreen: "statusline", statusLine: statusContent, footerHints: [
199
- { keys: ["↑", "↓"], label: "nav" },
200
- { keys: ["Enter"], label: "apply" },
201
- { keys: ["p"], label: "project" },
202
- { keys: ["g"], label: "global" },
203
- { keys: ["r"], label: "reset" },
204
- ], listPanel: _jsx(ScrollableList, { items: listItems, selectedIndex: statusLine.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight }), detailPanel: renderPreview() }));
205
- }
206
- export default StatusLineScreen;
@@ -1,416 +0,0 @@
1
- import React, { useEffect, useCallback, useState } from "react";
2
- import { useApp, useModal } from "../state/AppContext.js";
3
- import { useDimensions } from "../state/DimensionsContext.js";
4
- import { useKeyboard } from "../hooks/useKeyboard.js";
5
- import { ScreenLayout } from "../components/layout/index.js";
6
-
7
- import { statusLineCategories } from "../../data/statuslines.js";
8
- import type { StatusLineConfig } from "../../types/index.js";
9
- import { ScrollableList } from "../components/ScrollableList.js";
10
- import {
11
- setStatusLine,
12
- getStatusLine,
13
- setGlobalStatusLine,
14
- getGlobalStatusLine,
15
- } from "../../services/claude-settings.js";
16
-
17
- interface ListItem {
18
- label: string;
19
- preset?: StatusLineConfig;
20
- isCustom?: boolean;
21
- isCategory?: boolean;
22
- }
23
-
24
- export function StatusLineScreen() {
25
- const { state, dispatch } = useApp();
26
- const { statusline: statusLine } = state;
27
- const modal = useModal();
28
- const dimensions = useDimensions();
29
-
30
- const [projectStatusLine, setProjectStatusLineState] = useState<
31
- string | undefined
32
- >();
33
- const [globalStatusLine, setGlobalStatusLineState] = useState<
34
- string | undefined
35
- >();
36
- const [isLoading, setIsLoading] = useState(true);
37
-
38
- // Fetch data
39
- const fetchData = useCallback(async () => {
40
- setIsLoading(true);
41
- try {
42
- const [project, global] = await Promise.all([
43
- getStatusLine(state.projectPath),
44
- getGlobalStatusLine(),
45
- ]);
46
- setProjectStatusLineState(project);
47
- setGlobalStatusLineState(global);
48
- } catch (error) {
49
- // Silent error handling - show empty state
50
- }
51
- setIsLoading(false);
52
- }, [state.projectPath]);
53
-
54
- useEffect(() => {
55
- fetchData();
56
- }, [fetchData]);
57
-
58
- // Get current status line based on scope
59
- const getCurrentStatusLine = (): string | undefined => {
60
- return statusLine.scope === "project"
61
- ? projectStatusLine
62
- : globalStatusLine;
63
- };
64
-
65
- // Build list items with categories
66
- const buildListItems = (): ListItem[] => {
67
- const currentForScope = getCurrentStatusLine();
68
- const items: ListItem[] = [];
69
-
70
- for (const category of statusLineCategories) {
71
- items.push({
72
- label: category.name,
73
- isCategory: true,
74
- });
75
-
76
- for (const preset of category.presets) {
77
- const isActive = currentForScope === preset.template;
78
- const status = isActive ? "●" : "○";
79
- items.push({
80
- label: ` ${status} ${preset.name}`,
81
- preset,
82
- });
83
- }
84
- }
85
-
86
- // Add custom option at the end
87
- items.push({
88
- label: "+ Custom Status Line",
89
- isCustom: true,
90
- });
91
-
92
- return items;
93
- };
94
-
95
- const listItems = buildListItems();
96
-
97
- // Keyboard handling
98
- useKeyboard((event) => {
99
- if (state.isSearching || state.modal) return;
100
-
101
- if (event.name === "up" || event.name === "k") {
102
- const newIndex = Math.max(0, statusLine.selectedIndex - 1);
103
- dispatch({ type: "STATUSLINE_SELECT", index: newIndex });
104
- } else if (event.name === "down" || event.name === "j") {
105
- const newIndex = Math.min(
106
- listItems.length - 1,
107
- statusLine.selectedIndex + 1,
108
- );
109
- dispatch({ type: "STATUSLINE_SELECT", index: newIndex });
110
- } else if (event.name === "p") {
111
- dispatch({ type: "STATUSLINE_SET_SCOPE", scope: "project" });
112
- } else if (event.name === "g") {
113
- dispatch({ type: "STATUSLINE_SET_SCOPE", scope: "global" });
114
- } else if (event.name === "r") {
115
- handleReset();
116
- } else if (event.name === "enter") {
117
- handleSelect();
118
- }
119
- });
120
-
121
- const saveStatusLine = async (template: string): Promise<void> => {
122
- if (statusLine.scope === "global") {
123
- await setGlobalStatusLine(template);
124
- } else {
125
- await setStatusLine(template, state.projectPath);
126
- }
127
- };
128
-
129
- const handleSelect = async () => {
130
- const item = listItems[statusLine.selectedIndex];
131
- if (!item || item.isCategory) return;
132
-
133
- const scopeLabel = statusLine.scope === "global" ? "Global" : "Project";
134
-
135
- if (item.isCustom) {
136
- const template = await modal.input(
137
- `Custom Status Line (${scopeLabel})`,
138
- "Enter template (use {model}, {cost}, etc.):",
139
- getCurrentStatusLine() || "",
140
- );
141
-
142
- if (template !== null) {
143
- modal.loading(`Saving custom status line...`);
144
- try {
145
- await saveStatusLine(template);
146
- modal.hideModal();
147
- await modal.message(
148
- "Saved",
149
- `Custom status line saved to ${scopeLabel}.\n\nRestart Claude Code to apply changes.`,
150
- "success",
151
- );
152
- fetchData();
153
- } catch (error) {
154
- modal.hideModal();
155
- await modal.message("Error", `Failed to save: ${error}`, "error");
156
- }
157
- }
158
- } else if (item.preset) {
159
- modal.loading(`Applying ${item.preset.name}...`);
160
- try {
161
- await saveStatusLine(item.preset.template);
162
- modal.hideModal();
163
- await modal.message(
164
- "Applied",
165
- `"${item.preset.name}" applied to ${scopeLabel}.\n\nRestart Claude Code to apply changes.`,
166
- "success",
167
- );
168
- fetchData();
169
- } catch (error) {
170
- modal.hideModal();
171
- await modal.message("Error", `Failed to apply: ${error}`, "error");
172
- }
173
- }
174
- };
175
-
176
- const handleReset = async () => {
177
- const scopeLabel = statusLine.scope === "global" ? "Global" : "Project";
178
- const confirmed = await modal.confirm(
179
- "Reset Status Line",
180
- `Clear the ${scopeLabel} status line configuration?`,
181
- );
182
-
183
- if (confirmed) {
184
- modal.loading("Resetting...");
185
- try {
186
- await saveStatusLine("");
187
- modal.hideModal();
188
- await modal.message(
189
- "Reset",
190
- `${scopeLabel} status line has been cleared.`,
191
- "success",
192
- );
193
- fetchData();
194
- } catch (error) {
195
- modal.hideModal();
196
- await modal.message("Error", `Failed to reset: ${error}`, "error");
197
- }
198
- }
199
- };
200
-
201
- // Get selected item
202
- const selectedItem = listItems[statusLine.selectedIndex];
203
-
204
- // Build preview
205
- const renderPreview = () => {
206
- if (isLoading) {
207
- return <text fg="gray">Loading status line settings...</text>;
208
- }
209
-
210
- if (!selectedItem || selectedItem.isCategory) {
211
- return (
212
- <box
213
- flexDirection="column"
214
- alignItems="center"
215
- justifyContent="center"
216
- flexGrow={1}
217
- >
218
- <text fg="gray">Select a theme to see preview</text>
219
- </box>
220
- );
221
- }
222
-
223
- if (selectedItem.isCustom) {
224
- return (
225
- <box flexDirection="column">
226
- <text fg="cyan">
227
- <strong>✨ Custom Status Line</strong>
228
- </text>
229
- <text fg="gray">Create your own unique status line!</text>
230
-
231
- <box marginTop={1} flexDirection="column">
232
- <text fg="yellow">
233
- <strong>Available variables:</strong>
234
- </text>
235
- <box marginTop={1} flexDirection="column">
236
- <text>
237
- <span fg="green">
238
- <strong>{"{model}"}</strong>
239
- </span>{" "}
240
- <span fg="gray">→</span> Model name
241
- </text>
242
- <text>
243
- <span fg="green">
244
- <strong>{"{model_short}"}</strong>
245
- </span>{" "}
246
- <span fg="gray">→</span> Short name
247
- </text>
248
- <text>
249
- <span fg="yellow">
250
- <strong>{"{cost}"}</strong>
251
- </span>{" "}
252
- <span fg="gray">→</span> Session cost
253
- </text>
254
- <text>
255
- <span fg="#5c9aff">
256
- <strong>{"{cwd}"}</strong>
257
- </span>{" "}
258
- <span fg="gray">→</span> Working directory
259
- </text>
260
- <text>
261
- <span fg="magenta">
262
- <strong>{"{git_branch}"}</strong>
263
- </span>{" "}
264
- <span fg="gray">→</span> Git branch
265
- </text>
266
- <text>
267
- <span fg="cyan">
268
- <strong>{"{input_tokens}"}</strong>
269
- </span>{" "}
270
- <span fg="gray">→</span> Input tokens
271
- </text>
272
- <text>
273
- <span fg="cyan">
274
- <strong>{"{output_tokens}"}</strong>
275
- </span>{" "}
276
- <span fg="gray">→</span> Output tokens
277
- </text>
278
- <text>
279
- <span fg="red">
280
- <strong>{"{session_duration}"}</strong>
281
- </span>
282
- <span fg="gray">→</span> Duration
283
- </text>
284
- </box>
285
- </box>
286
- </box>
287
- );
288
- }
289
-
290
- if (selectedItem.preset) {
291
- const example = selectedItem.preset.template
292
- .replace("{model}", "claude-sonnet-4")
293
- .replace("{model_short}", "sonnet")
294
- .replace("{cost}", "0.42")
295
- .replace("{cwd}", "~/myapp")
296
- .replace("{git_branch}", "main")
297
- .replace("{input_tokens}", "1.2k")
298
- .replace("{output_tokens}", "850")
299
- .replace("{session_duration}", "12m");
300
-
301
- return (
302
- <box flexDirection="column">
303
- <text fg="cyan">
304
- <strong>◆ {selectedItem.preset.name}</strong>
305
- </text>
306
- <text fg="gray">{selectedItem.preset.description}</text>
307
-
308
- <box marginTop={1} flexDirection="column">
309
- <text fg="yellow">
310
- <strong>Preview:</strong>
311
- </text>
312
- <box
313
- marginTop={1}
314
- paddingLeft={1}
315
- paddingRight={1}
316
- borderStyle="rounded"
317
- borderColor="green"
318
- >
319
- <text fg="white">{example}</text>
320
- </box>
321
- </box>
322
-
323
- <box marginTop={1} flexDirection="column">
324
- <text fg="#666666">Template:</text>
325
- <text fg="gray">{selectedItem.preset.template}</text>
326
- </box>
327
- </box>
328
- );
329
- }
330
-
331
- return null;
332
- };
333
-
334
- const renderListItem = (
335
- item: ListItem,
336
- _idx: number,
337
- isSelected: boolean,
338
- ) => {
339
- if (item.isCategory) {
340
- return (
341
- <text fg="magenta">
342
- <strong>▸ {item.label}</strong>
343
- </text>
344
- );
345
- }
346
-
347
- if (item.isCustom) {
348
- return isSelected ? (
349
- <text bg="cyan" fg="black">
350
- <strong> ➕ Custom Status Line </strong>
351
- </text>
352
- ) : (
353
- <text fg="cyan">
354
- <strong>{" "}➕ Custom Status Line</strong>
355
- </text>
356
- );
357
- }
358
-
359
- const currentForScope = getCurrentStatusLine();
360
- const isActive = item.preset && currentForScope === item.preset.template;
361
-
362
- return isSelected ? (
363
- <text bg="magenta" fg="white">
364
- {" "}
365
- {isActive ? "●" : "○"} {item.preset?.name || ""}{" "}
366
- </text>
367
- ) : (
368
- <text fg={isActive ? "green" : "white"}>
369
- {" "}
370
- {isActive ? "●" : "○"} {item.preset?.name || ""}
371
- </text>
372
- );
373
- };
374
-
375
- // Build status line content
376
- const scopeLabel = statusLine.scope === "project" ? "Project" : "Global";
377
- const currentValue = getCurrentStatusLine();
378
- const statusContent = (
379
- <>
380
- <text fg="gray">Scope: </text>
381
- <text fg="cyan">{scopeLabel}</text>
382
- <text fg="gray"> │ Current: </text>
383
- <text fg="green">
384
- {currentValue
385
- ? currentValue.slice(0, 35) + (currentValue.length > 35 ? "..." : "")
386
- : "(not set)"}
387
- </text>
388
- </>
389
- );
390
-
391
- return (
392
- <ScreenLayout
393
- title="claudeup Status Line"
394
- currentScreen={"statusline" as never}
395
- statusLine={statusContent}
396
- footerHints={[
397
- { keys: ["↑", "↓"], label: "nav" },
398
- { keys: ["Enter"], label: "apply" },
399
- { keys: ["p"], label: "project" },
400
- { keys: ["g"], label: "global" },
401
- { keys: ["r"], label: "reset" },
402
- ]}
403
- listPanel={
404
- <ScrollableList
405
- items={listItems}
406
- selectedIndex={statusLine.selectedIndex}
407
- renderItem={renderListItem}
408
- maxHeight={dimensions.listPanelHeight}
409
- />
410
- }
411
- detailPanel={renderPreview()}
412
- />
413
- );
414
- }
415
-
416
- export default StatusLineScreen;
@@ -1,10 +0,0 @@
1
- export { PluginsScreen } from "./PluginsScreen.js";
2
- export { McpScreen } from "./McpScreen.js";
3
- export { McpRegistryScreen } from "./McpRegistryScreen.js";
4
- export { SettingsScreen } from "./EnvVarsScreen.js";
5
- export { CliToolsScreen } from "./CliToolsScreen.js";
6
- export { ModelSelectorScreen } from "./ModelSelectorScreen.js";
7
- export { ProfilesScreen } from "./ProfilesScreen.js";
8
- export { SkillsScreen } from "./SkillsScreen.js";
9
- export { GitignoreScreen } from "./GitignoreScreen.js";
10
- export { AliasScreen } from "./AliasScreen.js";
@@ -1,34 +0,0 @@
1
- import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- import { createContext, useContext, useState, } from "react";
3
- // ============================================================================
4
- // Context
5
- // ============================================================================
6
- const AnimationContext = createContext(null);
7
- export function AnimationProvider({ children }) {
8
- const [timelines, setTimelines] = useState(new Map());
9
- const registerTimeline = (id, timeline) => {
10
- setTimelines((prev) => {
11
- const next = new Map(prev);
12
- next.set(id, timeline);
13
- return next;
14
- });
15
- };
16
- const unregisterTimeline = (id) => {
17
- setTimelines((prev) => {
18
- const next = new Map(prev);
19
- next.delete(id);
20
- return next;
21
- });
22
- };
23
- return (_jsx(AnimationContext.Provider, { value: { timelines, registerTimeline, unregisterTimeline }, children: children }));
24
- }
25
- // ============================================================================
26
- // Hook: useAnimation
27
- // ============================================================================
28
- export function useAnimation() {
29
- const context = useContext(AnimationContext);
30
- if (!context) {
31
- throw new Error("useAnimation must be used within an AnimationProvider");
32
- }
33
- return context;
34
- }