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