claudeup 4.8.0 → 4.10.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "4.8.0",
3
+ "version": "4.10.1",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -99,7 +99,7 @@ let mockGetAvailablePlugins: ReturnType<typeof mock>;
99
99
 
100
100
  mock.module("../services/claude-settings.js", () => ({
101
101
  recoverMarketplaceSettings: mock(() =>
102
- Promise.resolve({ enabledAutoUpdate: [], removed: [] }),
102
+ Promise.resolve({ enabledAutoUpdate: [], removed: [], reregistered: [] }),
103
103
  ),
104
104
  migrateMarketplaceRename: mock(() =>
105
105
  Promise.resolve({
@@ -214,6 +214,24 @@ export async function prerunClaude(claudeArgs, options = {}) {
214
214
  if (recovery.removed.length > 0) {
215
215
  console.log(`✓ Removed stale marketplaces: ${recovery.removed.join(", ")}`);
216
216
  }
217
+ if (recovery.reregistered.length > 0) {
218
+ console.log(`✓ Re-registered as GitHub source: ${recovery.reregistered.join(", ")}`);
219
+ // Trigger marketplace update for re-registered entries so the local
220
+ // clone gets refreshed with the latest plugins.
221
+ const cliAvailable = await isClaudeAvailable();
222
+ if (cliAvailable) {
223
+ const { updateMarketplace } = await import("../services/claude-cli.js");
224
+ for (const mpName of recovery.reregistered) {
225
+ try {
226
+ await updateMarketplace(mpName);
227
+ console.log(`✓ Updated marketplace: ${mpName}`);
228
+ }
229
+ catch (error) {
230
+ console.warn(`⚠ Failed to update marketplace ${mpName}:`, error instanceof Error ? error.message : "Unknown error");
231
+ }
232
+ }
233
+ }
234
+ }
217
235
  // STEP 2: Clear cache to force fresh plugin info
218
236
  clearMarketplaceCache();
219
237
  // STEP 3: Get updated plugin info (to detect versions)
@@ -293,6 +293,30 @@ export async function prerunClaude(
293
293
  `✓ Removed stale marketplaces: ${recovery.removed.join(", ")}`,
294
294
  );
295
295
  }
296
+ if (recovery.reregistered.length > 0) {
297
+ console.log(
298
+ `✓ Re-registered as GitHub source: ${recovery.reregistered.join(", ")}`,
299
+ );
300
+ // Trigger marketplace update for re-registered entries so the local
301
+ // clone gets refreshed with the latest plugins.
302
+ const cliAvailable = await isClaudeAvailable();
303
+ if (cliAvailable) {
304
+ const { updateMarketplace } = await import(
305
+ "../services/claude-cli.js"
306
+ );
307
+ for (const mpName of recovery.reregistered) {
308
+ try {
309
+ await updateMarketplace(mpName);
310
+ console.log(`✓ Updated marketplace: ${mpName}`);
311
+ } catch (error) {
312
+ console.warn(
313
+ `⚠ Failed to update marketplace ${mpName}:`,
314
+ error instanceof Error ? error.message : "Unknown error",
315
+ );
316
+ }
317
+ }
318
+ }
319
+ }
296
320
 
297
321
  // STEP 2: Clear cache to force fresh plugin info
298
322
  clearMarketplaceCache();
@@ -50,10 +50,39 @@ async function execClaude(args, timeoutMs = 30000) {
50
50
  }
51
51
  /**
52
52
  * Install a plugin using claude CLI
53
- * Handles enabling + version tracking + cache copy in one shot
53
+ * Handles enabling + version tracking + cache copy in one shot.
54
+ *
55
+ * If the install fails because the plugin is "not found in marketplace",
56
+ * recovers the marketplace registry (fixes stale "directory" sources),
57
+ * triggers a marketplace update, and retries once.
54
58
  */
55
59
  export async function installPlugin(pluginId, scope = "user") {
56
- await execClaude(["plugin", "install", pluginId, "--scope", scope]);
60
+ try {
61
+ await execClaude(["plugin", "install", pluginId, "--scope", scope]);
62
+ }
63
+ catch (error) {
64
+ const msg = error instanceof Error ? error.message : String(error);
65
+ if (msg.includes("not found in marketplace")) {
66
+ const parts = pluginId.split("@");
67
+ const marketplace = parts[1];
68
+ if (marketplace) {
69
+ // Fix known_marketplaces.json first (stale "directory" → "github"),
70
+ // then update, then retry
71
+ const { recoverMarketplaceSettings } = await import("./claude-settings.js");
72
+ await recoverMarketplaceSettings();
73
+ await updateMarketplace(marketplace);
74
+ await execClaude([
75
+ "plugin",
76
+ "install",
77
+ pluginId,
78
+ "--scope",
79
+ scope,
80
+ ]);
81
+ return;
82
+ }
83
+ }
84
+ throw error;
85
+ }
57
86
  }
58
87
  /**
59
88
  * Uninstall a plugin using claude CLI.
@@ -96,9 +125,28 @@ export async function disablePlugin(pluginId, scope = "user") {
96
125
  * originally installed via the CLI. `install` handles both fresh installs
97
126
  * and re-installs (upgrades) of existing plugins regardless of how they
98
127
  * were originally added.
128
+ *
129
+ * Retries with marketplace update on "not found" errors (same as installPlugin).
99
130
  */
100
131
  export async function updatePlugin(pluginId, scope = "user") {
101
- await execClaude(["plugin", "install", pluginId, "--scope", scope], 60000);
132
+ try {
133
+ await execClaude(["plugin", "install", pluginId, "--scope", scope], 60000);
134
+ }
135
+ catch (error) {
136
+ const msg = error instanceof Error ? error.message : String(error);
137
+ if (msg.includes("not found in marketplace")) {
138
+ const parts = pluginId.split("@");
139
+ const marketplace = parts[1];
140
+ if (marketplace) {
141
+ const { recoverMarketplaceSettings } = await import("./claude-settings.js");
142
+ await recoverMarketplaceSettings();
143
+ await updateMarketplace(marketplace);
144
+ await execClaude(["plugin", "install", pluginId, "--scope", scope], 60000);
145
+ return;
146
+ }
147
+ }
148
+ throw error;
149
+ }
102
150
  }
103
151
  /**
104
152
  * Add a marketplace by GitHub repo (e.g., "MadAppGang/magus")
@@ -67,13 +67,44 @@ async function execClaude(args: string[], timeoutMs = 30000): Promise<string> {
67
67
 
68
68
  /**
69
69
  * Install a plugin using claude CLI
70
- * Handles enabling + version tracking + cache copy in one shot
70
+ * Handles enabling + version tracking + cache copy in one shot.
71
+ *
72
+ * If the install fails because the plugin is "not found in marketplace",
73
+ * recovers the marketplace registry (fixes stale "directory" sources),
74
+ * triggers a marketplace update, and retries once.
71
75
  */
72
76
  export async function installPlugin(
73
77
  pluginId: string,
74
78
  scope: PluginScope = "user",
75
79
  ): Promise<void> {
76
- await execClaude(["plugin", "install", pluginId, "--scope", scope]);
80
+ try {
81
+ await execClaude(["plugin", "install", pluginId, "--scope", scope]);
82
+ } catch (error) {
83
+ const msg =
84
+ error instanceof Error ? error.message : String(error);
85
+ if (msg.includes("not found in marketplace")) {
86
+ const parts = pluginId.split("@");
87
+ const marketplace = parts[1];
88
+ if (marketplace) {
89
+ // Fix known_marketplaces.json first (stale "directory" → "github"),
90
+ // then update, then retry
91
+ const { recoverMarketplaceSettings } = await import(
92
+ "./claude-settings.js"
93
+ );
94
+ await recoverMarketplaceSettings();
95
+ await updateMarketplace(marketplace);
96
+ await execClaude([
97
+ "plugin",
98
+ "install",
99
+ pluginId,
100
+ "--scope",
101
+ scope,
102
+ ]);
103
+ return;
104
+ }
105
+ }
106
+ throw error;
107
+ }
77
108
  }
78
109
 
79
110
  /**
@@ -127,12 +158,39 @@ export async function disablePlugin(
127
158
  * originally installed via the CLI. `install` handles both fresh installs
128
159
  * and re-installs (upgrades) of existing plugins regardless of how they
129
160
  * were originally added.
161
+ *
162
+ * Retries with marketplace update on "not found" errors (same as installPlugin).
130
163
  */
131
164
  export async function updatePlugin(
132
165
  pluginId: string,
133
166
  scope: PluginScope = "user",
134
167
  ): Promise<void> {
135
- await execClaude(["plugin", "install", pluginId, "--scope", scope], 60000);
168
+ try {
169
+ await execClaude(
170
+ ["plugin", "install", pluginId, "--scope", scope],
171
+ 60000,
172
+ );
173
+ } catch (error) {
174
+ const msg =
175
+ error instanceof Error ? error.message : String(error);
176
+ if (msg.includes("not found in marketplace")) {
177
+ const parts = pluginId.split("@");
178
+ const marketplace = parts[1];
179
+ if (marketplace) {
180
+ const { recoverMarketplaceSettings } = await import(
181
+ "./claude-settings.js"
182
+ );
183
+ await recoverMarketplaceSettings();
184
+ await updateMarketplace(marketplace);
185
+ await execClaude(
186
+ ["plugin", "install", pluginId, "--scope", scope],
187
+ 60000,
188
+ );
189
+ return;
190
+ }
191
+ }
192
+ throw error;
193
+ }
136
194
  }
137
195
 
138
196
  /**
@@ -881,14 +881,18 @@ function isMadAppGangMarketplace(entry) {
881
881
  * Recover/sync marketplace settings:
882
882
  * - Enable autoUpdate for Magus marketplaces that don't have it set
883
883
  * - Remove entries for marketplaces whose installLocation no longer exists
884
+ * - Fix stale "directory" source for known Magus marketplaces that should be "github"
884
885
  */
885
886
  export async function recoverMarketplaceSettings() {
886
887
  const known = await readKnownMarketplaces();
887
888
  const result = {
888
889
  enabledAutoUpdate: [],
889
890
  removed: [],
891
+ reregistered: [],
890
892
  };
891
893
  const updatedKnown = {};
894
+ // Import defaultMarketplaces lazily to get canonical repo URLs
895
+ const { defaultMarketplaces } = await import("../data/marketplaces.js");
892
896
  for (const [name, entry] of Object.entries(known)) {
893
897
  // Check if install location still exists
894
898
  if (entry.installLocation &&
@@ -896,6 +900,23 @@ export async function recoverMarketplaceSettings() {
896
900
  result.removed.push(name);
897
901
  continue;
898
902
  }
903
+ // Fix stale "directory" source for known marketplaces.
904
+ // Some machines have magus registered as source: "directory" pointing to
905
+ // a local dev checkout. This prevents Claude Code's auto-update from
906
+ // refreshing the marketplace, causing "plugin not found" errors for
907
+ // newly added plugins. Re-register as "github" with the canonical repo.
908
+ if (entry.source?.source === "directory") {
909
+ const defaultMp = defaultMarketplaces.find((m) => m.name === name);
910
+ if (defaultMp?.source.repo) {
911
+ const marketplacesDir = path.join(os.homedir(), ".claude", "plugins", "marketplaces");
912
+ entry.source = {
913
+ source: "github",
914
+ repo: defaultMp.source.repo,
915
+ };
916
+ entry.installLocation = path.join(marketplacesDir, name);
917
+ result.reregistered.push(name);
918
+ }
919
+ }
899
920
  // Enable autoUpdate if not set - only for Magus (MadAppGang) marketplaces
900
921
  if (entry.autoUpdate === undefined && isMadAppGangMarketplace(entry)) {
901
922
  entry.autoUpdate = true;
@@ -904,7 +925,9 @@ export async function recoverMarketplaceSettings() {
904
925
  updatedKnown[name] = entry;
905
926
  }
906
927
  // Write back if any changes were made
907
- if (result.enabledAutoUpdate.length > 0 || result.removed.length > 0) {
928
+ if (result.enabledAutoUpdate.length > 0 ||
929
+ result.removed.length > 0 ||
930
+ result.reregistered.length > 0) {
908
931
  await writeKnownMarketplaces(updatedKnown);
909
932
  }
910
933
  return result;
@@ -808,6 +808,7 @@ export async function getMarketplaceAutoUpdate(
808
808
  export interface MarketplaceRecoveryResult {
809
809
  enabledAutoUpdate: string[];
810
810
  removed: string[];
811
+ reregistered: string[];
811
812
  }
812
813
 
813
814
  // =============================================================================
@@ -1163,16 +1164,21 @@ function isMadAppGangMarketplace(entry: KnownMarketplaceEntry): boolean {
1163
1164
  * Recover/sync marketplace settings:
1164
1165
  * - Enable autoUpdate for Magus marketplaces that don't have it set
1165
1166
  * - Remove entries for marketplaces whose installLocation no longer exists
1167
+ * - Fix stale "directory" source for known Magus marketplaces that should be "github"
1166
1168
  */
1167
1169
  export async function recoverMarketplaceSettings(): Promise<MarketplaceRecoveryResult> {
1168
1170
  const known = await readKnownMarketplaces();
1169
1171
  const result: MarketplaceRecoveryResult = {
1170
1172
  enabledAutoUpdate: [],
1171
1173
  removed: [],
1174
+ reregistered: [],
1172
1175
  };
1173
1176
 
1174
1177
  const updatedKnown: KnownMarketplaces = {};
1175
1178
 
1179
+ // Import defaultMarketplaces lazily to get canonical repo URLs
1180
+ const { defaultMarketplaces } = await import("../data/marketplaces.js");
1181
+
1176
1182
  for (const [name, entry] of Object.entries(known)) {
1177
1183
  // Check if install location still exists
1178
1184
  if (
@@ -1183,6 +1189,29 @@ export async function recoverMarketplaceSettings(): Promise<MarketplaceRecoveryR
1183
1189
  continue;
1184
1190
  }
1185
1191
 
1192
+ // Fix stale "directory" source for known marketplaces.
1193
+ // Some machines have magus registered as source: "directory" pointing to
1194
+ // a local dev checkout. This prevents Claude Code's auto-update from
1195
+ // refreshing the marketplace, causing "plugin not found" errors for
1196
+ // newly added plugins. Re-register as "github" with the canonical repo.
1197
+ if (entry.source?.source === "directory") {
1198
+ const defaultMp = defaultMarketplaces.find((m) => m.name === name);
1199
+ if (defaultMp?.source.repo) {
1200
+ const marketplacesDir = path.join(
1201
+ os.homedir(),
1202
+ ".claude",
1203
+ "plugins",
1204
+ "marketplaces",
1205
+ );
1206
+ entry.source = {
1207
+ source: "github",
1208
+ repo: defaultMp.source.repo,
1209
+ };
1210
+ entry.installLocation = path.join(marketplacesDir, name);
1211
+ result.reregistered.push(name);
1212
+ }
1213
+ }
1214
+
1186
1215
  // Enable autoUpdate if not set - only for Magus (MadAppGang) marketplaces
1187
1216
  if (entry.autoUpdate === undefined && isMadAppGangMarketplace(entry)) {
1188
1217
  entry.autoUpdate = true;
@@ -1193,7 +1222,11 @@ export async function recoverMarketplaceSettings(): Promise<MarketplaceRecoveryR
1193
1222
  }
1194
1223
 
1195
1224
  // Write back if any changes were made
1196
- if (result.enabledAutoUpdate.length > 0 || result.removed.length > 0) {
1225
+ if (
1226
+ result.enabledAutoUpdate.length > 0 ||
1227
+ result.removed.length > 0 ||
1228
+ result.reregistered.length > 0
1229
+ ) {
1197
1230
  await writeKnownMarketplaces(updatedKnown);
1198
1231
  }
1199
1232
 
package/src/ui/App.js CHANGED
@@ -7,7 +7,7 @@ import { DimensionsProvider, useDimensions, } from "./state/DimensionsContext.js
7
7
  import { ModalContainer } from "./components/modals/index.js";
8
8
  import { PluginsScreen, McpScreen, McpRegistryScreen, SettingsScreen, CliToolsScreen, ModelSelectorScreen, ProfilesScreen, SkillsScreen, } from "./screens/index.js";
9
9
  import { repairAllMarketplaces } from "../services/local-marketplace.js";
10
- import { migrateMarketplaceRename } from "../services/claude-settings.js";
10
+ import { migrateMarketplaceRename, recoverMarketplaceSettings, } from "../services/claude-settings.js";
11
11
  import { checkForUpdates, getCurrentVersion, } from "../services/version-check.js";
12
12
  import { useKeyboardHandler } from "./hooks/useKeyboardHandler.js";
13
13
  import { ProgressBar } from "./components/layout/ProgressBar.js";
@@ -179,6 +179,7 @@ function AppContentInner({ showDebug, onDebugToggle, updateInfo, onExit, }) {
179
179
  const { state, dispatch } = useApp();
180
180
  const { progress } = state;
181
181
  const dimensions = useDimensions();
182
+ const [recoveryReport, setRecoveryReport] = useState(null);
182
183
  // Auto-refresh marketplaces on startup
183
184
  useEffect(() => {
184
185
  const noRefresh = process.argv.includes("--no-refresh");
@@ -190,6 +191,35 @@ function AppContentInner({ showDebug, onDebugToggle, updateInfo, onExit, }) {
190
191
  });
191
192
  // Migrate old marketplace names → magus (idempotent), then repair plugin.json files
192
193
  migrateMarketplaceRename().catch(() => { }); // non-blocking, best-effort
194
+ // Recover stale marketplace registry entries (e.g. "directory" → "github")
195
+ recoverMarketplaceSettings()
196
+ .then(async (recovery) => {
197
+ const msgs = [];
198
+ if (recovery.reregistered.length > 0) {
199
+ msgs.push(`Re-registered as GitHub: ${recovery.reregistered.join(", ")}`);
200
+ // Update the marketplace clone now that the source is fixed
201
+ const { updateMarketplace } = await import("../services/claude-cli.js");
202
+ for (const mp of recovery.reregistered) {
203
+ try {
204
+ await updateMarketplace(mp);
205
+ msgs.push(`Updated marketplace: ${mp}`);
206
+ }
207
+ catch {
208
+ msgs.push(`Failed to update: ${mp}`);
209
+ }
210
+ }
211
+ }
212
+ if (recovery.enabledAutoUpdate.length > 0) {
213
+ msgs.push(`Enabled auto-update: ${recovery.enabledAutoUpdate.join(", ")}`);
214
+ }
215
+ if (recovery.removed.length > 0) {
216
+ msgs.push(`Removed stale: ${recovery.removed.join(", ")}`);
217
+ }
218
+ if (msgs.length > 0) {
219
+ setRecoveryReport(msgs.join("\n"));
220
+ }
221
+ })
222
+ .catch(() => { }); // non-fatal
193
223
  repairAllMarketplaces()
194
224
  .then(async () => {
195
225
  dispatch({ type: "HIDE_PROGRESS" });
@@ -199,7 +229,7 @@ function AppContentInner({ showDebug, onDebugToggle, updateInfo, onExit, }) {
199
229
  dispatch({ type: "HIDE_PROGRESS" });
200
230
  });
201
231
  }, [dispatch]);
202
- return (_jsxs("box", { flexDirection: "column", height: dimensions.terminalHeight, children: [updateInfo?.updateAvailable && _jsx(UpdateBanner, { result: updateInfo }), showDebug && (_jsx("box", { paddingLeft: 1, paddingRight: 1, children: _jsxs("text", { fg: "#888888", children: ["DEBUG: ", dimensions.terminalWidth, "x", dimensions.terminalHeight, " | content=", dimensions.contentHeight, " | screen=", state.currentRoute.screen] }) })), progress && _jsx(ProgressIndicator, { ...progress }), _jsx("box", { flexDirection: "column", height: dimensions.contentHeight, paddingLeft: 1, paddingRight: 1, children: _jsx(Router, {}) }), _jsx(GlobalKeyHandler, { onDebugToggle: onDebugToggle, onExit: onExit }), _jsx(ModalContainer, {})] }));
232
+ return (_jsxs("box", { flexDirection: "column", height: dimensions.terminalHeight, children: [updateInfo?.updateAvailable && _jsx(UpdateBanner, { result: updateInfo }), recoveryReport && (_jsxs("box", { paddingLeft: 1, paddingRight: 1, children: [_jsx("text", { bg: "green", fg: "black", children: _jsx("strong", { children: " RECOVERED " }) }), _jsxs("text", { fg: "green", children: [" ", recoveryReport.split("\n").join(" | ")] })] })), showDebug && (_jsx("box", { paddingLeft: 1, paddingRight: 1, children: _jsxs("text", { fg: "#888888", children: ["DEBUG: ", dimensions.terminalWidth, "x", dimensions.terminalHeight, " | content=", dimensions.contentHeight, " | screen=", state.currentRoute.screen] }) })), progress && _jsx(ProgressIndicator, { ...progress }), _jsx("box", { flexDirection: "column", height: dimensions.contentHeight, paddingLeft: 1, paddingRight: 1, children: _jsx(Router, {}) }), _jsx(GlobalKeyHandler, { onDebugToggle: onDebugToggle, onExit: onExit }), _jsx(ModalContainer, {})] }));
203
233
  }
204
234
  function AppContent({ onExit }) {
205
235
  const { state } = useApp();
package/src/ui/App.tsx CHANGED
@@ -24,7 +24,10 @@ import {
24
24
  } from "./screens/index.js";
25
25
  import type { Screen } from "./state/types.js";
26
26
  import { repairAllMarketplaces } from "../services/local-marketplace.js";
27
- import { migrateMarketplaceRename } from "../services/claude-settings.js";
27
+ import {
28
+ migrateMarketplaceRename,
29
+ recoverMarketplaceSettings,
30
+ } from "../services/claude-settings.js";
28
31
  import {
29
32
  checkForUpdates,
30
33
  getCurrentVersion,
@@ -258,6 +261,7 @@ function AppContentInner({
258
261
  const { state, dispatch } = useApp();
259
262
  const { progress } = state;
260
263
  const dimensions = useDimensions();
264
+ const [recoveryReport, setRecoveryReport] = useState<string | null>(null);
261
265
 
262
266
  // Auto-refresh marketplaces on startup
263
267
  useEffect(() => {
@@ -272,6 +276,43 @@ function AppContentInner({
272
276
  // Migrate old marketplace names → magus (idempotent), then repair plugin.json files
273
277
  migrateMarketplaceRename().catch(() => {}); // non-blocking, best-effort
274
278
 
279
+ // Recover stale marketplace registry entries (e.g. "directory" → "github")
280
+ recoverMarketplaceSettings()
281
+ .then(async (recovery) => {
282
+ const msgs: string[] = [];
283
+ if (recovery.reregistered.length > 0) {
284
+ msgs.push(
285
+ `Re-registered as GitHub: ${recovery.reregistered.join(", ")}`,
286
+ );
287
+ // Update the marketplace clone now that the source is fixed
288
+ const { updateMarketplace } = await import(
289
+ "../services/claude-cli.js"
290
+ );
291
+ for (const mp of recovery.reregistered) {
292
+ try {
293
+ await updateMarketplace(mp);
294
+ msgs.push(`Updated marketplace: ${mp}`);
295
+ } catch {
296
+ msgs.push(`Failed to update: ${mp}`);
297
+ }
298
+ }
299
+ }
300
+ if (recovery.enabledAutoUpdate.length > 0) {
301
+ msgs.push(
302
+ `Enabled auto-update: ${recovery.enabledAutoUpdate.join(", ")}`,
303
+ );
304
+ }
305
+ if (recovery.removed.length > 0) {
306
+ msgs.push(
307
+ `Removed stale: ${recovery.removed.join(", ")}`,
308
+ );
309
+ }
310
+ if (msgs.length > 0) {
311
+ setRecoveryReport(msgs.join("\n"));
312
+ }
313
+ })
314
+ .catch(() => {}); // non-fatal
315
+
275
316
  repairAllMarketplaces()
276
317
  .then(async () => {
277
318
  dispatch({ type: "HIDE_PROGRESS" });
@@ -285,6 +326,14 @@ function AppContentInner({
285
326
  return (
286
327
  <box flexDirection="column" height={dimensions.terminalHeight}>
287
328
  {updateInfo?.updateAvailable && <UpdateBanner result={updateInfo} />}
329
+ {recoveryReport && (
330
+ <box paddingLeft={1} paddingRight={1}>
331
+ <text bg="green" fg="black">
332
+ <strong> RECOVERED </strong>
333
+ </text>
334
+ <text fg="green"> {recoveryReport.split("\n").join(" | ")}</text>
335
+ </box>
336
+ )}
288
337
  {showDebug && (
289
338
  <box paddingLeft={1} paddingRight={1}>
290
339
  <text fg="#888888">
@@ -9,6 +9,9 @@ export function LoadingModal({ message }) {
9
9
  }, 80);
10
10
  return () => clearInterval(interval);
11
11
  }, []);
12
- return (_jsxs("box", { flexDirection: "row", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, children: [_jsx("text", { fg: "#A1A1AA", children: SPINNER_FRAMES[frame] }), _jsxs("text", { fg: "#EDEDED", children: [" ", message] })] }));
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] }));
13
16
  }
14
17
  export default LoadingModal;
@@ -1,7 +1,7 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
 
3
3
  interface LoadingModalProps {
4
- /** Loading message */
4
+ /** Loading message — supports multiline (first line = status, subsequent = detail) */
5
5
  message: string;
6
6
  }
7
7
 
@@ -18,9 +18,13 @@ export function LoadingModal({ message }: LoadingModalProps) {
18
18
  return () => clearInterval(interval);
19
19
  }, []);
20
20
 
21
+ const lines = message.split("\n");
22
+ const mainMessage = lines[0];
23
+ const detail = lines.slice(1).join("\n");
24
+
21
25
  return (
22
26
  <box
23
- flexDirection="row"
27
+ flexDirection="column"
24
28
  border
25
29
  borderStyle="rounded"
26
30
  borderColor="#525252"
@@ -30,8 +34,15 @@ export function LoadingModal({ message }: LoadingModalProps) {
30
34
  paddingTop={1}
31
35
  paddingBottom={1}
32
36
  >
33
- <text fg="#A1A1AA">{SPINNER_FRAMES[frame]}</text>
34
- <text fg="#EDEDED"> {message}</text>
37
+ <box flexDirection="row">
38
+ <text fg="#A1A1AA">{SPINNER_FRAMES[frame]}</text>
39
+ <text fg="#EDEDED"> {mainMessage}</text>
40
+ </box>
41
+ {detail ? (
42
+ <box marginTop={0} paddingLeft={2}>
43
+ <text fg="#525252">{detail}</text>
44
+ </box>
45
+ ) : null}
35
46
  </box>
36
47
  );
37
48
  }
@@ -443,22 +443,19 @@ export function PluginsScreen() {
443
443
  else {
444
444
  action = "install";
445
445
  }
446
- const actionLabel = action === "update"
447
- ? `Updating ${scopeLabel}`
448
- : action === "install"
449
- ? `Installing to ${scopeLabel}`
450
- : `Uninstalling from ${scopeLabel}`;
451
- modal.loading(`${actionLabel}...`);
452
446
  try {
453
447
  const scope = scopeValue;
454
448
  if (action === "uninstall") {
449
+ modal.loading(`Uninstalling ${plugin.name} from ${scopeLabel}…\nclaude plugin uninstall ${plugin.id} --scope ${scope}`);
455
450
  await cliUninstallPlugin(plugin.id, scope, state.projectPath);
456
451
  }
457
452
  else if (action === "update") {
453
+ modal.loading(`Updating ${plugin.name} in ${scopeLabel}…\nclaude plugin install ${plugin.id} --scope ${scope}`);
458
454
  await cliUpdatePlugin(plugin.id, scope);
459
455
  await saveVersionForScope(plugin.id, latestVersion, scope);
460
456
  }
461
457
  else {
458
+ modal.loading(`Installing ${plugin.name} to ${scopeLabel}…\nclaude plugin install ${plugin.id} --scope ${scope}`);
462
459
  await cliInstallPlugin(plugin.id, scope);
463
460
  await saveVersionForScope(plugin.id, latestVersion, scope);
464
461
  modal.hideModal();
@@ -482,7 +479,7 @@ export function PluginsScreen() {
482
479
  return;
483
480
  const plugin = item.plugin;
484
481
  const scope = pluginsState.scope === "global" ? "user" : "project";
485
- modal.loading(`Updating ${plugin.name}...`);
482
+ modal.loading(`Updating ${plugin.name}…\nclaude plugin install ${plugin.id} --scope ${scope}`);
486
483
  try {
487
484
  await cliUpdatePlugin(plugin.id, scope);
488
485
  if (plugin.version) {
@@ -503,9 +500,10 @@ export function PluginsScreen() {
503
500
  if (updatable.length === 0)
504
501
  return;
505
502
  const scope = pluginsState.scope === "global" ? "user" : "project";
506
- modal.loading(`Updating ${updatable.length} plugin(s)...`);
507
503
  try {
508
- for (const plugin of updatable) {
504
+ for (let i = 0; i < updatable.length; i++) {
505
+ const plugin = updatable[i];
506
+ modal.loading(`Updating ${plugin.name} (${i + 1}/${updatable.length})…\nclaude plugin install ${plugin.id} --scope ${scope}`);
509
507
  await cliUpdatePlugin(plugin.id, scope);
510
508
  if (plugin.version) {
511
509
  await saveVersionForScope(plugin.id, plugin.version, scope);
@@ -547,21 +545,18 @@ export function PluginsScreen() {
547
545
  else {
548
546
  action = "install";
549
547
  }
550
- const actionLabel = action === "update"
551
- ? `Updating ${scopeLabel}`
552
- : action === "install"
553
- ? `Installing to ${scopeLabel}`
554
- : `Uninstalling from ${scopeLabel}`;
555
- modal.loading(`${actionLabel}...`);
556
548
  try {
557
549
  if (action === "uninstall") {
550
+ modal.loading(`Uninstalling ${plugin.name} from ${scopeLabel}…\nclaude plugin uninstall ${plugin.id} --scope ${scope}`);
558
551
  await cliUninstallPlugin(plugin.id, scope, state.projectPath);
559
552
  }
560
553
  else if (action === "update") {
554
+ modal.loading(`Updating ${plugin.name} in ${scopeLabel}…\nclaude plugin install ${plugin.id} --scope ${scope}`);
561
555
  await cliUpdatePlugin(plugin.id, scope);
562
556
  await saveVersionForScope(plugin.id, latestVersion, scope);
563
557
  }
564
558
  else {
559
+ modal.loading(`Installing ${plugin.name} to ${scopeLabel}…\nclaude plugin install ${plugin.id} --scope ${scope}`);
565
560
  await cliInstallPlugin(plugin.id, scope);
566
561
  await saveVersionForScope(plugin.id, latestVersion, scope);
567
562
  modal.hideModal();
@@ -583,12 +578,12 @@ export function PluginsScreen() {
583
578
  if (!item || item.kind !== "plugin" || !item.plugin.isOrphaned)
584
579
  return;
585
580
  const plugin = item.plugin;
586
- modal.loading(`Removing ${plugin.name}...`);
587
581
  try {
588
582
  // Remove from all scopes — try all to clean up stale references
589
583
  const scopes = ["user", "project", "local"];
590
584
  for (const scope of scopes) {
591
585
  try {
586
+ modal.loading(`Removing ${plugin.name} from ${scope}…\nclaude plugin uninstall ${plugin.id} --scope ${scope}`);
592
587
  await cliUninstallPlugin(plugin.id, scope, state.projectPath);
593
588
  }
594
589
  catch {
@@ -559,22 +559,23 @@ export function PluginsScreen() {
559
559
  action = "install";
560
560
  }
561
561
 
562
- const actionLabel =
563
- action === "update"
564
- ? `Updating ${scopeLabel}`
565
- : action === "install"
566
- ? `Installing to ${scopeLabel}`
567
- : `Uninstalling from ${scopeLabel}`;
568
- modal.loading(`${actionLabel}...`);
569
-
570
562
  try {
571
563
  const scope = scopeValue as PluginScope;
572
564
  if (action === "uninstall") {
565
+ modal.loading(
566
+ `Uninstalling ${plugin.name} from ${scopeLabel}…\nclaude plugin uninstall ${plugin.id} --scope ${scope}`,
567
+ );
573
568
  await cliUninstallPlugin(plugin.id, scope, state.projectPath);
574
569
  } else if (action === "update") {
570
+ modal.loading(
571
+ `Updating ${plugin.name} in ${scopeLabel}…\nclaude plugin install ${plugin.id} --scope ${scope}`,
572
+ );
575
573
  await cliUpdatePlugin(plugin.id, scope);
576
574
  await saveVersionForScope(plugin.id, latestVersion, scope);
577
575
  } else {
576
+ modal.loading(
577
+ `Installing ${plugin.name} to ${scopeLabel}…\nclaude plugin install ${plugin.id} --scope ${scope}`,
578
+ );
578
579
  await cliInstallPlugin(plugin.id, scope);
579
580
  await saveVersionForScope(plugin.id, latestVersion, scope);
580
581
  modal.hideModal();
@@ -599,7 +600,9 @@ export function PluginsScreen() {
599
600
  const plugin = item.plugin;
600
601
  const scope: PluginScope = pluginsState.scope === "global" ? "user" : "project";
601
602
 
602
- modal.loading(`Updating ${plugin.name}...`);
603
+ modal.loading(
604
+ `Updating ${plugin.name}…\nclaude plugin install ${plugin.id} --scope ${scope}`,
605
+ );
603
606
  try {
604
607
  await cliUpdatePlugin(plugin.id, scope);
605
608
  if (plugin.version) {
@@ -620,10 +623,13 @@ export function PluginsScreen() {
620
623
  if (updatable.length === 0) return;
621
624
 
622
625
  const scope: PluginScope = pluginsState.scope === "global" ? "user" : "project";
623
- modal.loading(`Updating ${updatable.length} plugin(s)...`);
624
626
 
625
627
  try {
626
- for (const plugin of updatable) {
628
+ for (let i = 0; i < updatable.length; i++) {
629
+ const plugin = updatable[i];
630
+ modal.loading(
631
+ `Updating ${plugin.name} (${i + 1}/${updatable.length})…\nclaude plugin install ${plugin.id} --scope ${scope}`,
632
+ );
627
633
  await cliUpdatePlugin(plugin.id, scope);
628
634
  if (plugin.version) {
629
635
  await saveVersionForScope(plugin.id, plugin.version, scope);
@@ -670,21 +676,22 @@ export function PluginsScreen() {
670
676
  action = "install";
671
677
  }
672
678
 
673
- const actionLabel =
674
- action === "update"
675
- ? `Updating ${scopeLabel}`
676
- : action === "install"
677
- ? `Installing to ${scopeLabel}`
678
- : `Uninstalling from ${scopeLabel}`;
679
- modal.loading(`${actionLabel}...`);
680
-
681
679
  try {
682
680
  if (action === "uninstall") {
681
+ modal.loading(
682
+ `Uninstalling ${plugin.name} from ${scopeLabel}…\nclaude plugin uninstall ${plugin.id} --scope ${scope}`,
683
+ );
683
684
  await cliUninstallPlugin(plugin.id, scope, state.projectPath);
684
685
  } else if (action === "update") {
686
+ modal.loading(
687
+ `Updating ${plugin.name} in ${scopeLabel}…\nclaude plugin install ${plugin.id} --scope ${scope}`,
688
+ );
685
689
  await cliUpdatePlugin(plugin.id, scope);
686
690
  await saveVersionForScope(plugin.id, latestVersion, scope);
687
691
  } else {
692
+ modal.loading(
693
+ `Installing ${plugin.name} to ${scopeLabel}…\nclaude plugin install ${plugin.id} --scope ${scope}`,
694
+ );
688
695
  await cliInstallPlugin(plugin.id, scope);
689
696
  await saveVersionForScope(plugin.id, latestVersion, scope);
690
697
  modal.hideModal();
@@ -706,12 +713,14 @@ export function PluginsScreen() {
706
713
  if (!item || item.kind !== "plugin" || !item.plugin.isOrphaned) return;
707
714
 
708
715
  const plugin = item.plugin;
709
- modal.loading(`Removing ${plugin.name}...`);
710
716
  try {
711
717
  // Remove from all scopes — try all to clean up stale references
712
718
  const scopes: PluginScope[] = ["user", "project", "local"];
713
719
  for (const scope of scopes) {
714
720
  try {
721
+ modal.loading(
722
+ `Removing ${plugin.name} from ${scope}…\nclaude plugin uninstall ${plugin.id} --scope ${scope}`,
723
+ );
715
724
  await cliUninstallPlugin(plugin.id, scope, state.projectPath);
716
725
  } catch {
717
726
  // Ignore errors for scopes where it doesn't exist