claudeup 3.17.0 → 4.0.1

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 (53) hide show
  1. package/package.json +1 -1
  2. package/src/data/predefined-profiles.js +69 -12
  3. package/src/data/predefined-profiles.ts +73 -14
  4. package/src/services/claude-cli.js +8 -1
  5. package/src/services/claude-cli.ts +10 -7
  6. package/src/services/plugin-manager.js +40 -4
  7. package/src/services/plugin-manager.ts +57 -6
  8. package/src/ui/adapters/pluginsAdapter.js +139 -0
  9. package/src/ui/adapters/pluginsAdapter.ts +202 -0
  10. package/src/ui/adapters/settingsAdapter.js +111 -0
  11. package/src/ui/adapters/settingsAdapter.ts +165 -0
  12. package/src/ui/components/ScrollableDetail.js +23 -0
  13. package/src/ui/components/ScrollableDetail.tsx +55 -0
  14. package/src/ui/components/ScrollableList.js +4 -4
  15. package/src/ui/components/ScrollableList.tsx +4 -4
  16. package/src/ui/components/SearchInput.js +2 -2
  17. package/src/ui/components/SearchInput.tsx +3 -3
  18. package/src/ui/components/StyledText.js +1 -1
  19. package/src/ui/components/StyledText.tsx +5 -1
  20. package/src/ui/components/layout/ProgressBar.js +1 -1
  21. package/src/ui/components/layout/ProgressBar.tsx +1 -5
  22. package/src/ui/components/layout/ScreenLayout.js +1 -1
  23. package/src/ui/components/layout/ScreenLayout.tsx +11 -8
  24. package/src/ui/components/modals/InputModal.tsx +1 -6
  25. package/src/ui/components/modals/LoadingModal.js +1 -1
  26. package/src/ui/components/modals/LoadingModal.tsx +1 -3
  27. package/src/ui/hooks/index.js +3 -3
  28. package/src/ui/hooks/index.ts +3 -3
  29. package/src/ui/hooks/useKeyboard.ts +1 -3
  30. package/src/ui/hooks/useKeyboardHandler.js +9 -9
  31. package/src/ui/hooks/useKeyboardHandler.ts +9 -9
  32. package/src/ui/renderers/cliToolRenderers.js +33 -0
  33. package/src/ui/renderers/cliToolRenderers.tsx +153 -0
  34. package/src/ui/renderers/mcpRenderers.js +26 -0
  35. package/src/ui/renderers/mcpRenderers.tsx +145 -0
  36. package/src/ui/renderers/pluginRenderers.js +124 -0
  37. package/src/ui/renderers/pluginRenderers.tsx +362 -0
  38. package/src/ui/renderers/profileRenderers.js +177 -0
  39. package/src/ui/renderers/profileRenderers.tsx +361 -0
  40. package/src/ui/renderers/settingsRenderers.js +69 -0
  41. package/src/ui/renderers/settingsRenderers.tsx +205 -0
  42. package/src/ui/screens/CliToolsScreen.js +14 -58
  43. package/src/ui/screens/CliToolsScreen.tsx +36 -196
  44. package/src/ui/screens/EnvVarsScreen.js +12 -168
  45. package/src/ui/screens/EnvVarsScreen.tsx +16 -327
  46. package/src/ui/screens/McpScreen.js +12 -62
  47. package/src/ui/screens/McpScreen.tsx +21 -190
  48. package/src/ui/screens/PluginsScreen.js +52 -425
  49. package/src/ui/screens/PluginsScreen.tsx +70 -758
  50. package/src/ui/screens/ProfilesScreen.js +32 -97
  51. package/src/ui/screens/ProfilesScreen.tsx +58 -328
  52. package/src/ui/screens/SkillsScreen.js +16 -16
  53. package/src/ui/screens/SkillsScreen.tsx +20 -23
@@ -9,24 +9,12 @@ import { listProfiles, applyProfile, renameProfile, deleteProfile, exportProfile
9
9
  import { readSettings, writeSettings, } from "../../services/claude-settings.js";
10
10
  import { writeClipboard, readClipboard, ClipboardUnavailableError, } from "../../utils/clipboard.js";
11
11
  import { PREDEFINED_PROFILES, } from "../../data/predefined-profiles.js";
12
- function buildListItems(profileList) {
13
- const predefined = PREDEFINED_PROFILES.map((p) => ({
14
- kind: "predefined",
15
- profile: p,
16
- }));
17
- const saved = profileList.map((e) => ({
18
- kind: "saved",
19
- entry: e,
20
- }));
21
- return [...predefined, ...saved];
22
- }
23
- // ─── Component ────────────────────────────────────────────────────────────────
12
+ import { buildProfileListItems, renderProfileRow, renderProfileDetail, } from "../renderers/profileRenderers.js";
24
13
  export function ProfilesScreen() {
25
14
  const { state, dispatch } = useApp();
26
15
  const { profiles: profilesState } = state;
27
16
  const modal = useModal();
28
17
  const dimensions = useDimensions();
29
- // Fetch data
30
18
  const fetchData = useCallback(async () => {
31
19
  dispatch({ type: "PROFILES_DATA_LOADING" });
32
20
  try {
@@ -46,25 +34,36 @@ export function ProfilesScreen() {
46
34
  const profileList = profilesState.profiles.status === "success"
47
35
  ? profilesState.profiles.data
48
36
  : [];
49
- const allItems = buildListItems(profileList);
37
+ const allItems = buildProfileListItems(profileList, PREDEFINED_PROFILES);
50
38
  const selectedItem = allItems[profilesState.selectedIndex];
51
- // Keyboard handling
39
+ const isNavigable = (item) => item.kind !== "header";
52
40
  useKeyboard((event) => {
53
41
  if (state.isSearching || state.modal)
54
42
  return;
55
43
  if (event.name === "up" || event.name === "k") {
56
- const newIndex = Math.max(0, profilesState.selectedIndex - 1);
57
- dispatch({ type: "PROFILES_SELECT", index: newIndex });
44
+ let newIndex = profilesState.selectedIndex - 1;
45
+ while (newIndex > 0 && !isNavigable(allItems[newIndex])) {
46
+ newIndex--;
47
+ }
48
+ if (newIndex >= 0 && isNavigable(allItems[newIndex])) {
49
+ dispatch({ type: "PROFILES_SELECT", index: newIndex });
50
+ }
58
51
  }
59
52
  else if (event.name === "down" || event.name === "j") {
60
- const newIndex = Math.min(Math.max(0, allItems.length - 1), profilesState.selectedIndex + 1);
61
- dispatch({ type: "PROFILES_SELECT", index: newIndex });
53
+ let newIndex = profilesState.selectedIndex + 1;
54
+ while (newIndex < allItems.length - 1 &&
55
+ !isNavigable(allItems[newIndex])) {
56
+ newIndex++;
57
+ }
58
+ if (newIndex < allItems.length && isNavigable(allItems[newIndex])) {
59
+ dispatch({ type: "PROFILES_SELECT", index: newIndex });
60
+ }
62
61
  }
63
62
  else if (event.name === "enter" || event.name === "a") {
64
63
  if (selectedItem?.kind === "predefined") {
65
64
  void handleApplyPredefined(selectedItem.profile);
66
65
  }
67
- else {
66
+ else if (selectedItem?.kind === "saved") {
68
67
  void handleApply();
69
68
  }
70
69
  }
@@ -84,7 +83,7 @@ export function ProfilesScreen() {
84
83
  void handleImport();
85
84
  }
86
85
  });
87
- // ─── Predefined profile apply ─────────────────────────────────────────────
86
+ // ─── Actions ──────────────────────────────────────────────────────────────
88
87
  const handleApplyPredefined = async (profile) => {
89
88
  const allPlugins = [
90
89
  ...profile.magusPlugins.map((p) => `${p}@magus`),
@@ -97,14 +96,12 @@ export function ProfilesScreen() {
97
96
  modal.loading(`Applying "${profile.name}"...`);
98
97
  try {
99
98
  const settings = await readSettings(state.projectPath);
100
- // Merge plugins (additive only)
101
99
  settings.enabledPlugins = settings.enabledPlugins ?? {};
102
100
  for (const plugin of allPlugins) {
103
101
  if (!settings.enabledPlugins[plugin]) {
104
102
  settings.enabledPlugins[plugin] = true;
105
103
  }
106
104
  }
107
- // Merge top-level settings (additive — only set if not already set)
108
105
  for (const [key, value] of Object.entries(profile.settings)) {
109
106
  if (key === "env") {
110
107
  const envMap = value;
@@ -134,7 +131,6 @@ export function ProfilesScreen() {
134
131
  await modal.message("Error", `Failed to apply profile: ${error}`, "error");
135
132
  }
136
133
  };
137
- // ─── Saved profile actions ────────────────────────────────────────────────
138
134
  const handleApply = async () => {
139
135
  if (selectedItem?.kind !== "saved")
140
136
  return;
@@ -163,7 +159,6 @@ export function ProfilesScreen() {
163
159
  try {
164
160
  await applyProfile(selectedProfile.id, selectedProfile.scope, targetScope, state.projectPath);
165
161
  modal.hideModal();
166
- // Trigger PluginsScreen to refetch
167
162
  dispatch({ type: "DATA_REFRESH_COMPLETE" });
168
163
  await modal.message("Applied", `Profile "${selectedProfile.name}" applied to ${scopeLabel}.`, "success");
169
164
  }
@@ -202,7 +197,6 @@ export function ProfilesScreen() {
202
197
  try {
203
198
  await deleteProfile(selectedProfile.id, selectedProfile.scope, state.projectPath);
204
199
  modal.hideModal();
205
- // Adjust selection if we deleted the last item
206
200
  const newIndex = Math.max(0, Math.min(profilesState.selectedIndex, allItems.length - 2));
207
201
  dispatch({ type: "PROFILES_SELECT", index: newIndex });
208
202
  await fetchData();
@@ -227,7 +221,6 @@ export function ProfilesScreen() {
227
221
  }
228
222
  catch (err) {
229
223
  if (err instanceof ClipboardUnavailableError) {
230
- // Fallback: show JSON in modal for manual copy
231
224
  await modal.message("Profile JSON (copy manually)", json, "info");
232
225
  }
233
226
  else {
@@ -242,13 +235,11 @@ export function ProfilesScreen() {
242
235
  };
243
236
  const handleImport = async () => {
244
237
  let json = null;
245
- // Try to read from clipboard first
246
238
  try {
247
239
  json = await readClipboard();
248
240
  }
249
241
  catch (err) {
250
242
  if (err instanceof ClipboardUnavailableError) {
251
- // Fallback: ask user to paste JSON manually
252
243
  json = await modal.input("Import Profile", "Paste profile JSON:");
253
244
  }
254
245
  else {
@@ -280,77 +271,21 @@ export function ProfilesScreen() {
280
271
  await modal.message("Error", `Failed to import: ${error}`, "error");
281
272
  }
282
273
  };
283
- // ─── Rendering helpers ────────────────────────────────────────────────────
284
- const formatDate = (iso) => {
285
- try {
286
- const d = new Date(iso);
287
- return d.toLocaleDateString("en-US", {
288
- month: "short",
289
- day: "numeric",
290
- year: "numeric",
291
- });
292
- }
293
- catch {
294
- return iso;
295
- }
296
- };
297
- const renderListItem = (item, _idx, isSelected) => {
298
- if (item.kind === "predefined") {
299
- const { profile } = item;
300
- const pluginCount = profile.magusPlugins.length + profile.anthropicPlugins.length;
301
- const skillCount = profile.skills.length;
302
- if (isSelected) {
303
- return (_jsxs("text", { bg: "blue", fg: "white", children: [" ", profile.icon, " ", profile.name, " \u2014 ", pluginCount, " plugins \u00B7 ", skillCount, " ", "skill", skillCount !== 1 ? "s" : "", " "] }));
304
- }
305
- return (_jsxs("text", { children: [_jsx("span", { fg: "blue", children: "[preset]" }), _jsx("span", { children: " " }), _jsxs("span", { fg: "white", children: [profile.icon, " ", profile.name] }), _jsxs("span", { fg: "gray", children: [" ", "\u2014 ", pluginCount, " plugins \u00B7 ", skillCount, " skill", skillCount !== 1 ? "s" : ""] })] }));
306
- }
307
- // Saved profile
308
- const { entry } = item;
309
- const pluginCount = Object.keys(entry.plugins).length;
310
- const dateStr = formatDate(entry.updatedAt);
311
- const scopeColor = entry.scope === "user" ? "cyan" : "green";
312
- const scopeLabel = entry.scope === "user" ? "[user]" : "[proj]";
313
- if (isSelected) {
314
- return (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", scopeLabel, " ", entry.name, " \u2014 ", pluginCount, " plugin", pluginCount !== 1 ? "s" : "", " \u00B7 ", dateStr, " "] }));
315
- }
316
- return (_jsxs("text", { children: [_jsx("span", { fg: scopeColor, children: scopeLabel }), _jsx("span", { children: " " }), _jsx("span", { fg: "white", children: entry.name }), _jsxs("span", { fg: "gray", children: [" ", "\u2014 ", pluginCount, " plugin", pluginCount !== 1 ? "s" : "", " \u00B7 ", dateStr] })] }));
317
- };
318
- const renderDetail = () => {
319
- if (profilesState.profiles.status === "loading") {
320
- return _jsx("text", { fg: "gray", children: "Loading profiles..." });
321
- }
322
- if (profilesState.profiles.status === "error") {
323
- return (_jsxs("text", { fg: "red", children: ["Error: ", profilesState.profiles.error.message] }));
324
- }
325
- if (!selectedItem) {
326
- return _jsx("text", { fg: "gray", children: "Select a profile to see details" });
327
- }
328
- if (selectedItem.kind === "predefined") {
329
- return renderPredefinedDetail(selectedItem.profile);
330
- }
331
- return renderSavedDetail(selectedItem.entry);
332
- };
333
- const renderPredefinedDetail = (profile) => {
334
- const allPlugins = [
335
- ...profile.magusPlugins.map((p) => `${p}@magus`),
336
- ...profile.anthropicPlugins.map((p) => `${p}@claude-plugins-official`),
337
- ];
338
- return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "blue", children: _jsxs("strong", { children: [profile.icon, " ", profile.name] }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: profile.description }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { fg: "gray", children: ["Magus plugins (", profile.magusPlugins.length, "):"] }), profile.magusPlugins.map((p) => (_jsx("box", { children: _jsxs("text", { fg: "cyan", children: [" ", p, "@magus"] }) }, p)))] }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { fg: "gray", children: ["Anthropic plugins (", profile.anthropicPlugins.length, "):"] }), profile.anthropicPlugins.map((p) => (_jsx("box", { children: _jsxs("text", { fg: "yellow", children: [" ", p, "@claude-plugins-official"] }) }, p)))] }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { fg: "gray", children: ["Skills (", profile.skills.length, "):"] }), profile.skills.map((s) => (_jsx("box", { children: _jsxs("text", { fg: "white", children: [" ", s] }) }, s)))] }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { fg: "gray", children: ["Settings (", Object.keys(profile.settings).length, "):"] }), Object.entries(profile.settings)
339
- .filter(([k]) => k !== "env")
340
- .map(([k, v]) => (_jsx("box", { children: _jsxs("text", { fg: "white", children: [" ", k, ": ", String(v)] }) }, k)))] }), _jsx("box", { marginTop: 2, flexDirection: "column", children: _jsxs("box", { children: [_jsxs("text", { bg: "blue", fg: "white", children: [" ", "Enter/a", " "] }), _jsxs("text", { fg: "gray", children: [" ", "Apply (merges ", allPlugins.length, " plugins into project settings)"] })] }) })] }));
341
- };
342
- const renderSavedDetail = (selectedProfile) => {
343
- const plugins = Object.keys(selectedProfile.plugins);
344
- const scopeColor = selectedProfile.scope === "user" ? "cyan" : "green";
345
- const scopeLabel = selectedProfile.scope === "user"
346
- ? "User (~/.claude/profiles.json)"
347
- : "Project (.claude/profiles.json — committed to git)";
348
- return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "cyan", children: _jsx("strong", { children: selectedProfile.name }) }), _jsxs("box", { marginTop: 1, children: [_jsx("text", { fg: "gray", children: "Scope: " }), _jsx("text", { fg: scopeColor, children: scopeLabel })] }), _jsx("box", { marginTop: 1, children: _jsxs("text", { fg: "gray", children: ["Created: ", formatDate(selectedProfile.createdAt), " \u00B7 Updated:", " ", formatDate(selectedProfile.updatedAt)] }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { fg: "gray", children: ["Plugins (", plugins.length, plugins.length === 0 ? " — applying will disable all plugins" : "", "):"] }), plugins.length === 0 ? (_jsx("text", { fg: "yellow", children: " (none)" })) : (plugins.map((p) => (_jsx("box", { children: _jsxs("text", { fg: "white", children: [" ", p] }) }, p))))] }), _jsxs("box", { marginTop: 2, flexDirection: "column", children: [_jsxs("box", { children: [_jsxs("text", { bg: "magenta", fg: "white", children: [" ", "Enter/a", " "] }), _jsx("text", { fg: "gray", children: " Apply profile" })] }), _jsxs("box", { marginTop: 1, children: [_jsxs("text", { bg: "#333333", fg: "white", children: [" ", "r", " "] }), _jsx("text", { fg: "gray", children: " Rename" })] }), _jsxs("box", { marginTop: 1, children: [_jsxs("text", { bg: "red", fg: "white", children: [" ", "d", " "] }), _jsx("text", { fg: "gray", children: " Delete" })] }), _jsxs("box", { marginTop: 1, children: [_jsxs("text", { bg: "blue", fg: "white", children: [" ", "c", " "] }), _jsx("text", { fg: "gray", children: " Copy JSON to clipboard" })] }), _jsxs("box", { marginTop: 1, children: [_jsxs("text", { bg: "green", fg: "white", children: [" ", "i", " "] }), _jsx("text", { fg: "gray", children: " Import from clipboard" })] })] })] }));
349
- };
274
+ // ─── Render ───────────────────────────────────────────────────────────────
350
275
  const profileCount = profileList.length;
351
276
  const userCount = profileList.filter((p) => p.scope === "user").length;
352
277
  const projCount = profileList.filter((p) => p.scope === "project").length;
353
278
  const statusContent = (_jsxs("text", { children: [_jsxs("span", { fg: "blue", children: [PREDEFINED_PROFILES.length, " presets"] }), _jsx("span", { fg: "gray", children: " + " }), _jsxs("span", { fg: "cyan", children: [userCount, " user"] }), _jsx("span", { fg: "gray", children: " + " }), _jsxs("span", { fg: "green", children: [projCount, " project"] }), _jsx("span", { fg: "gray", children: " = " }), _jsxs("span", { fg: "white", children: [PREDEFINED_PROFILES.length + profileCount, " total"] })] }));
354
- return (_jsx(ScreenLayout, { title: "claudeup Plugin Profiles", currentScreen: "profiles", statusLine: statusContent, footerHints: "\u2191\u2193:nav \u2502 Enter/a:apply \u2502 r:rename \u2502 d:delete \u2502 c:copy \u2502 i:import", listPanel: _jsx(ScrollableList, { items: allItems, selectedIndex: profilesState.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight }), detailPanel: renderDetail() }));
279
+ const firstNavigableIndex = allItems.findIndex(isNavigable);
280
+ const effectiveIndex = profilesState.selectedIndex === 0 && selectedItem?.kind === "header"
281
+ ? firstNavigableIndex
282
+ : profilesState.selectedIndex;
283
+ const loadingStatus = profilesState.profiles.status === "loading"
284
+ ? true
285
+ : false;
286
+ const errorMessage = profilesState.profiles.status === "error"
287
+ ? profilesState.profiles.error.message
288
+ : undefined;
289
+ return (_jsx(ScreenLayout, { title: "claudeup Plugin Profiles", currentScreen: "profiles", statusLine: statusContent, footerHints: "\u2191\u2193:nav \u2502 Enter/a:apply \u2502 r:rename \u2502 d:delete \u2502 c:copy \u2502 i:import", listPanel: _jsx(ScrollableList, { items: allItems, selectedIndex: effectiveIndex, renderItem: renderProfileRow, maxHeight: dimensions.listPanelHeight }), detailPanel: renderProfileDetail(allItems[effectiveIndex], loadingStatus, errorMessage) }));
355
290
  }
356
291
  export default ProfilesScreen;