claudeup 4.7.0 → 4.10.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.
@@ -50,10 +50,37 @@ 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
+ * triggers a marketplace update and retries once. This handles the case
57
+ * where claudeup's remote listing (from GitHub) has newer plugins than
58
+ * the local marketplace clone.
54
59
  */
55
60
  export async function installPlugin(pluginId, scope = "user") {
56
- await execClaude(["plugin", "install", pluginId, "--scope", scope]);
61
+ try {
62
+ await execClaude(["plugin", "install", pluginId, "--scope", scope]);
63
+ }
64
+ catch (error) {
65
+ const msg = error instanceof Error ? error.message : String(error);
66
+ if (msg.includes("not found in marketplace")) {
67
+ // Local marketplace clone is stale — update and retry
68
+ const parts = pluginId.split("@");
69
+ const marketplace = parts[1];
70
+ if (marketplace) {
71
+ await updateMarketplace(marketplace);
72
+ await execClaude([
73
+ "plugin",
74
+ "install",
75
+ pluginId,
76
+ "--scope",
77
+ scope,
78
+ ]);
79
+ return;
80
+ }
81
+ }
82
+ throw error;
83
+ }
57
84
  }
58
85
  /**
59
86
  * Uninstall a plugin using claude CLI.
@@ -96,9 +123,26 @@ export async function disablePlugin(pluginId, scope = "user") {
96
123
  * originally installed via the CLI. `install` handles both fresh installs
97
124
  * and re-installs (upgrades) of existing plugins regardless of how they
98
125
  * were originally added.
126
+ *
127
+ * Retries with marketplace update on "not found" errors (same as installPlugin).
99
128
  */
100
129
  export async function updatePlugin(pluginId, scope = "user") {
101
- await execClaude(["plugin", "install", pluginId, "--scope", scope], 60000);
130
+ try {
131
+ await execClaude(["plugin", "install", pluginId, "--scope", scope], 60000);
132
+ }
133
+ catch (error) {
134
+ const msg = error instanceof Error ? error.message : String(error);
135
+ if (msg.includes("not found in marketplace")) {
136
+ const parts = pluginId.split("@");
137
+ const marketplace = parts[1];
138
+ if (marketplace) {
139
+ await updateMarketplace(marketplace);
140
+ await execClaude(["plugin", "install", pluginId, "--scope", scope], 60000);
141
+ return;
142
+ }
143
+ }
144
+ throw error;
145
+ }
102
146
  }
103
147
  /**
104
148
  * Add a marketplace by GitHub repo (e.g., "MadAppGang/magus")
@@ -67,13 +67,40 @@ 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
+ * triggers a marketplace update and retries once. This handles the case
74
+ * where claudeup's remote listing (from GitHub) has newer plugins than
75
+ * the local marketplace clone.
71
76
  */
72
77
  export async function installPlugin(
73
78
  pluginId: string,
74
79
  scope: PluginScope = "user",
75
80
  ): Promise<void> {
76
- await execClaude(["plugin", "install", pluginId, "--scope", scope]);
81
+ try {
82
+ await execClaude(["plugin", "install", pluginId, "--scope", scope]);
83
+ } catch (error) {
84
+ const msg =
85
+ error instanceof Error ? error.message : String(error);
86
+ if (msg.includes("not found in marketplace")) {
87
+ // Local marketplace clone is stale — update and retry
88
+ const parts = pluginId.split("@");
89
+ const marketplace = parts[1];
90
+ if (marketplace) {
91
+ await updateMarketplace(marketplace);
92
+ await execClaude([
93
+ "plugin",
94
+ "install",
95
+ pluginId,
96
+ "--scope",
97
+ scope,
98
+ ]);
99
+ return;
100
+ }
101
+ }
102
+ throw error;
103
+ }
77
104
  }
78
105
 
79
106
  /**
@@ -127,12 +154,35 @@ export async function disablePlugin(
127
154
  * originally installed via the CLI. `install` handles both fresh installs
128
155
  * and re-installs (upgrades) of existing plugins regardless of how they
129
156
  * were originally added.
157
+ *
158
+ * Retries with marketplace update on "not found" errors (same as installPlugin).
130
159
  */
131
160
  export async function updatePlugin(
132
161
  pluginId: string,
133
162
  scope: PluginScope = "user",
134
163
  ): Promise<void> {
135
- await execClaude(["plugin", "install", pluginId, "--scope", scope], 60000);
164
+ try {
165
+ await execClaude(
166
+ ["plugin", "install", pluginId, "--scope", scope],
167
+ 60000,
168
+ );
169
+ } catch (error) {
170
+ const msg =
171
+ error instanceof Error ? error.message : String(error);
172
+ if (msg.includes("not found in marketplace")) {
173
+ const parts = pluginId.split("@");
174
+ const marketplace = parts[1];
175
+ if (marketplace) {
176
+ await updateMarketplace(marketplace);
177
+ await execClaude(
178
+ ["plugin", "install", pluginId, "--scope", scope],
179
+ 60000,
180
+ );
181
+ return;
182
+ }
183
+ }
184
+ throw error;
185
+ }
136
186
  }
137
187
 
138
188
  /**
@@ -1,6 +1,7 @@
1
1
  import fs from "fs-extra";
2
2
  import path from "node:path";
3
3
  import os from "node:os";
4
+ import semver from "semver";
4
5
  import { parsePluginId } from "../utils/string-utils.js";
5
6
  const CLAUDE_DIR = ".claude";
6
7
  const SETTINGS_FILE = "settings.json";
@@ -377,6 +378,79 @@ export async function saveGlobalInstalledPluginVersion(pluginId, version) {
377
378
  // Update registry for user scope
378
379
  await updateInstalledPluginsRegistry(pluginId, version, "user");
379
380
  }
381
+ /**
382
+ * Gap-fill: ensure global installedPluginVersions is at least as high as
383
+ * project and local scopes. When a plugin is updated at project scope,
384
+ * global may lag behind, causing Claude Code to resolve stale cache paths.
385
+ *
386
+ * @returns Array of { pluginId, oldVersion, newVersion } for plugins that were bumped
387
+ */
388
+ export async function gapFillInstalledPluginVersions(projectPath) {
389
+ const globalVersions = await getGlobalInstalledPluginVersions();
390
+ // Collect versions from project and local scopes
391
+ const allVersions = {};
392
+ // Start with global versions
393
+ for (const [id, ver] of Object.entries(globalVersions)) {
394
+ allVersions[id] = [ver];
395
+ }
396
+ // Add project scope versions
397
+ if (projectPath) {
398
+ try {
399
+ const settings = await readSettings(projectPath);
400
+ for (const [id, ver] of Object.entries(settings.installedPluginVersions || {})) {
401
+ if (!allVersions[id])
402
+ allVersions[id] = [];
403
+ allVersions[id].push(ver);
404
+ }
405
+ }
406
+ catch {
407
+ /* skip unreadable */
408
+ }
409
+ // Add local scope versions
410
+ try {
411
+ const localSettings = await readLocalSettings(projectPath);
412
+ for (const [id, ver] of Object.entries(localSettings.installedPluginVersions || {})) {
413
+ if (!allVersions[id])
414
+ allVersions[id] = [];
415
+ allVersions[id].push(ver);
416
+ }
417
+ }
418
+ catch {
419
+ /* skip unreadable */
420
+ }
421
+ }
422
+ // Find plugins where global needs bumping
423
+ const bumped = [];
424
+ for (const [pluginId, versions] of Object.entries(allVersions)) {
425
+ // Filter to valid semver versions and sort descending
426
+ const valid = versions.filter((v) => semver.valid(v));
427
+ if (valid.length === 0)
428
+ continue;
429
+ const highest = valid.sort((a, b) => semver.rcompare(a, b))[0];
430
+ const globalVer = globalVersions[pluginId];
431
+ // If global is missing, invalid semver, or lower than the highest, bump it
432
+ if (!globalVer ||
433
+ !semver.valid(globalVer) ||
434
+ semver.lt(globalVer, highest)) {
435
+ bumped.push({
436
+ pluginId,
437
+ oldVersion: globalVer || "(missing)",
438
+ newVersion: highest,
439
+ });
440
+ }
441
+ }
442
+ // Apply bumps in a single write
443
+ if (bumped.length > 0) {
444
+ const settings = await readGlobalSettings();
445
+ settings.installedPluginVersions =
446
+ settings.installedPluginVersions || {};
447
+ for (const { pluginId, newVersion } of bumped) {
448
+ settings.installedPluginVersions[pluginId] = newVersion;
449
+ }
450
+ await writeGlobalSettings(settings);
451
+ }
452
+ return bumped;
453
+ }
380
454
  export async function removeGlobalInstalledPluginVersion(pluginId) {
381
455
  const settings = await readGlobalSettings();
382
456
  if (settings.installedPluginVersions) {
@@ -807,14 +881,18 @@ function isMadAppGangMarketplace(entry) {
807
881
  * Recover/sync marketplace settings:
808
882
  * - Enable autoUpdate for Magus marketplaces that don't have it set
809
883
  * - Remove entries for marketplaces whose installLocation no longer exists
884
+ * - Fix stale "directory" source for known Magus marketplaces that should be "github"
810
885
  */
811
886
  export async function recoverMarketplaceSettings() {
812
887
  const known = await readKnownMarketplaces();
813
888
  const result = {
814
889
  enabledAutoUpdate: [],
815
890
  removed: [],
891
+ reregistered: [],
816
892
  };
817
893
  const updatedKnown = {};
894
+ // Import defaultMarketplaces lazily to get canonical repo URLs
895
+ const { defaultMarketplaces } = await import("../data/marketplaces.js");
818
896
  for (const [name, entry] of Object.entries(known)) {
819
897
  // Check if install location still exists
820
898
  if (entry.installLocation &&
@@ -822,6 +900,23 @@ export async function recoverMarketplaceSettings() {
822
900
  result.removed.push(name);
823
901
  continue;
824
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
+ }
825
920
  // Enable autoUpdate if not set - only for Magus (MadAppGang) marketplaces
826
921
  if (entry.autoUpdate === undefined && isMadAppGangMarketplace(entry)) {
827
922
  entry.autoUpdate = true;
@@ -830,7 +925,9 @@ export async function recoverMarketplaceSettings() {
830
925
  updatedKnown[name] = entry;
831
926
  }
832
927
  // Write back if any changes were made
833
- 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) {
834
931
  await writeKnownMarketplaces(updatedKnown);
835
932
  }
836
933
  return result;
@@ -1,6 +1,7 @@
1
1
  import fs from "fs-extra";
2
2
  import path from "node:path";
3
3
  import os from "node:os";
4
+ import semver from "semver";
4
5
  import type {
5
6
  ClaudeSettings,
6
7
  ClaudeLocalSettings,
@@ -544,6 +545,97 @@ export async function saveGlobalInstalledPluginVersion(
544
545
  await updateInstalledPluginsRegistry(pluginId, version, "user");
545
546
  }
546
547
 
548
+ /**
549
+ * Gap-fill: ensure global installedPluginVersions is at least as high as
550
+ * project and local scopes. When a plugin is updated at project scope,
551
+ * global may lag behind, causing Claude Code to resolve stale cache paths.
552
+ *
553
+ * @returns Array of { pluginId, oldVersion, newVersion } for plugins that were bumped
554
+ */
555
+ export async function gapFillInstalledPluginVersions(
556
+ projectPath?: string,
557
+ ): Promise<Array<{ pluginId: string; oldVersion: string; newVersion: string }>> {
558
+ const globalVersions = await getGlobalInstalledPluginVersions();
559
+
560
+ // Collect versions from project and local scopes
561
+ const allVersions: Record<string, string[]> = {};
562
+
563
+ // Start with global versions
564
+ for (const [id, ver] of Object.entries(globalVersions)) {
565
+ allVersions[id] = [ver];
566
+ }
567
+
568
+ // Add project scope versions
569
+ if (projectPath) {
570
+ try {
571
+ const settings = await readSettings(projectPath);
572
+ for (const [id, ver] of Object.entries(
573
+ settings.installedPluginVersions || {},
574
+ )) {
575
+ if (!allVersions[id]) allVersions[id] = [];
576
+ allVersions[id].push(ver);
577
+ }
578
+ } catch {
579
+ /* skip unreadable */
580
+ }
581
+
582
+ // Add local scope versions
583
+ try {
584
+ const localSettings = await readLocalSettings(projectPath);
585
+ for (const [id, ver] of Object.entries(
586
+ localSettings.installedPluginVersions || {},
587
+ )) {
588
+ if (!allVersions[id]) allVersions[id] = [];
589
+ allVersions[id].push(ver);
590
+ }
591
+ } catch {
592
+ /* skip unreadable */
593
+ }
594
+ }
595
+
596
+ // Find plugins where global needs bumping
597
+ const bumped: Array<{
598
+ pluginId: string;
599
+ oldVersion: string;
600
+ newVersion: string;
601
+ }> = [];
602
+
603
+ for (const [pluginId, versions] of Object.entries(allVersions)) {
604
+ // Filter to valid semver versions and sort descending
605
+ const valid = versions.filter((v) => semver.valid(v));
606
+ if (valid.length === 0) continue;
607
+
608
+ const highest = valid.sort((a, b) => semver.rcompare(a, b))[0];
609
+ const globalVer = globalVersions[pluginId];
610
+
611
+ // If global is missing, invalid semver, or lower than the highest, bump it
612
+ if (
613
+ !globalVer ||
614
+ !semver.valid(globalVer) ||
615
+ semver.lt(globalVer, highest)
616
+ ) {
617
+ bumped.push({
618
+ pluginId,
619
+ oldVersion: globalVer || "(missing)",
620
+ newVersion: highest,
621
+ });
622
+ }
623
+ }
624
+
625
+ // Apply bumps in a single write
626
+ if (bumped.length > 0) {
627
+ const settings = await readGlobalSettings();
628
+ settings.installedPluginVersions =
629
+ settings.installedPluginVersions || {};
630
+ for (const { pluginId, newVersion } of bumped) {
631
+ settings.installedPluginVersions[pluginId] = newVersion;
632
+ }
633
+ await writeGlobalSettings(settings);
634
+ }
635
+
636
+ return bumped;
637
+ }
638
+
547
639
  export async function removeGlobalInstalledPluginVersion(
548
640
  pluginId: string,
549
641
  ): Promise<void> {
@@ -716,6 +808,7 @@ export async function getMarketplaceAutoUpdate(
716
808
  export interface MarketplaceRecoveryResult {
717
809
  enabledAutoUpdate: string[];
718
810
  removed: string[];
811
+ reregistered: string[];
719
812
  }
720
813
 
721
814
  // =============================================================================
@@ -1071,16 +1164,21 @@ function isMadAppGangMarketplace(entry: KnownMarketplaceEntry): boolean {
1071
1164
  * Recover/sync marketplace settings:
1072
1165
  * - Enable autoUpdate for Magus marketplaces that don't have it set
1073
1166
  * - Remove entries for marketplaces whose installLocation no longer exists
1167
+ * - Fix stale "directory" source for known Magus marketplaces that should be "github"
1074
1168
  */
1075
1169
  export async function recoverMarketplaceSettings(): Promise<MarketplaceRecoveryResult> {
1076
1170
  const known = await readKnownMarketplaces();
1077
1171
  const result: MarketplaceRecoveryResult = {
1078
1172
  enabledAutoUpdate: [],
1079
1173
  removed: [],
1174
+ reregistered: [],
1080
1175
  };
1081
1176
 
1082
1177
  const updatedKnown: KnownMarketplaces = {};
1083
1178
 
1179
+ // Import defaultMarketplaces lazily to get canonical repo URLs
1180
+ const { defaultMarketplaces } = await import("../data/marketplaces.js");
1181
+
1084
1182
  for (const [name, entry] of Object.entries(known)) {
1085
1183
  // Check if install location still exists
1086
1184
  if (
@@ -1091,6 +1189,29 @@ export async function recoverMarketplaceSettings(): Promise<MarketplaceRecoveryR
1091
1189
  continue;
1092
1190
  }
1093
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
+
1094
1215
  // Enable autoUpdate if not set - only for Magus (MadAppGang) marketplaces
1095
1216
  if (entry.autoUpdate === undefined && isMadAppGangMarketplace(entry)) {
1096
1217
  entry.autoUpdate = true;
@@ -1101,7 +1222,11 @@ export async function recoverMarketplaceSettings(): Promise<MarketplaceRecoveryR
1101
1222
  }
1102
1223
 
1103
1224
  // Write back if any changes were made
1104
- 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
+ ) {
1105
1230
  await writeKnownMarketplaces(updatedKnown);
1106
1231
  }
1107
1232
 
@@ -5,7 +5,6 @@ import { getConfiguredMarketplaces, getEnabledPlugins, readSettings, writeSettin
5
5
  import { defaultMarketplaces } from "../data/marketplaces.js";
6
6
  import { scanLocalMarketplaces, repairAllMarketplaces, } from "./local-marketplace.js";
7
7
  import { formatMarketplaceName, isValidGitHubRepo, parsePluginId, } from "../utils/string-utils.js";
8
- import { updateMarketplace } from "./claude-cli.js";
9
8
  // Cache for local marketplaces (session-level) - Promise-based to prevent race conditions
10
9
  let localMarketplacesPromise = null;
11
10
  // Session-level cache for fetched marketplace data (no TTL - persists until explicit refresh)
@@ -434,9 +433,14 @@ export async function getLocalMarketplacesInfo() {
434
433
  const autoSyncedMarketplaces = new Set();
435
434
  /**
436
435
  * If the remote manifest lists plugins that aren't in the local cache,
437
- * the local clone is stale. Silently run `claude plugin marketplace update`
438
- * to pull the latest, then invalidate the local cache so the next scan
439
- * picks up the new plugins.
436
+ * the local clone is stale. Log a warning but do NOT trigger a marketplace
437
+ * update from claudeup.
438
+ *
439
+ * Previously this called `updateMarketplace()` which invokes Claude Code's
440
+ * `cacheMarketplaceFromGit()` — a non-atomic delete-then-clone that can
441
+ * permanently delete the marketplace directory on clone failure.
442
+ * Claude Code's own background autoupdate handles marketplace refreshing
443
+ * after session start. See: ai-docs/plugin-marketplace-bug-investigation.md
440
444
  */
441
445
  async function autoSyncIfStale(mpName, remotePluginNames, localMarketplaces) {
442
446
  if (autoSyncedMarketplaces.has(mpName))
@@ -445,20 +449,13 @@ async function autoSyncIfStale(mpName, remotePluginNames, localMarketplaces) {
445
449
  if (!localMp)
446
450
  return localMarketplaces;
447
451
  const localNames = new Set(localMp.plugins.map((p) => p.name));
448
- const hasMissing = remotePluginNames.some((name) => !localNames.has(name));
449
- if (!hasMissing)
452
+ const missingPlugins = remotePluginNames.filter((name) => !localNames.has(name));
453
+ if (missingPlugins.length === 0)
450
454
  return localMarketplaces;
451
455
  autoSyncedMarketplaces.add(mpName);
452
- try {
453
- await updateMarketplace(mpName);
454
- // Invalidate local cache so re-scan picks up new plugins
455
- localMarketplacesPromise = null;
456
- return getLocalMarketplaces();
457
- }
458
- catch {
459
- // Update failed (no network, CLI missing, etc.) — continue with stale data
460
- return localMarketplaces;
461
- }
456
+ // Log stale state but don't trigger update — Claude Code handles refresh
457
+ console.log(`ℹ Marketplace ${mpName} is stale (missing: ${missingPlugins.join(", ")}). Claude Code will auto-update on next session start.`);
458
+ return localMarketplaces;
462
459
  }
463
460
  /**
464
461
  * Refresh claudeup's internal cache
@@ -27,8 +27,6 @@ import {
27
27
  isValidGitHubRepo,
28
28
  parsePluginId,
29
29
  } from "../utils/string-utils.js";
30
- import { updateMarketplace } from "./claude-cli.js";
31
-
32
30
  // Cache for local marketplaces (session-level) - Promise-based to prevent race conditions
33
31
  let localMarketplacesPromise: Promise<Map<string, LocalMarketplace>> | null =
34
32
  null;
@@ -619,9 +617,14 @@ const autoSyncedMarketplaces = new Set<string>();
619
617
 
620
618
  /**
621
619
  * If the remote manifest lists plugins that aren't in the local cache,
622
- * the local clone is stale. Silently run `claude plugin marketplace update`
623
- * to pull the latest, then invalidate the local cache so the next scan
624
- * picks up the new plugins.
620
+ * the local clone is stale. Log a warning but do NOT trigger a marketplace
621
+ * update from claudeup.
622
+ *
623
+ * Previously this called `updateMarketplace()` which invokes Claude Code's
624
+ * `cacheMarketplaceFromGit()` — a non-atomic delete-then-clone that can
625
+ * permanently delete the marketplace directory on clone failure.
626
+ * Claude Code's own background autoupdate handles marketplace refreshing
627
+ * after session start. See: ai-docs/plugin-marketplace-bug-investigation.md
625
628
  */
626
629
  async function autoSyncIfStale(
627
630
  mpName: string,
@@ -634,19 +637,17 @@ async function autoSyncIfStale(
634
637
  if (!localMp) return localMarketplaces;
635
638
 
636
639
  const localNames = new Set(localMp.plugins.map((p) => p.name));
637
- const hasMissing = remotePluginNames.some((name) => !localNames.has(name));
638
- if (!hasMissing) return localMarketplaces;
640
+ const missingPlugins = remotePluginNames.filter(
641
+ (name) => !localNames.has(name),
642
+ );
643
+ if (missingPlugins.length === 0) return localMarketplaces;
639
644
 
640
645
  autoSyncedMarketplaces.add(mpName);
641
- try {
642
- await updateMarketplace(mpName);
643
- // Invalidate local cache so re-scan picks up new plugins
644
- localMarketplacesPromise = null;
645
- return getLocalMarketplaces();
646
- } catch {
647
- // Update failed (no network, CLI missing, etc.) — continue with stale data
648
- return localMarketplaces;
649
- }
646
+ // Log stale state but don't trigger update — Claude Code handles refresh
647
+ console.log(
648
+ `ℹ Marketplace ${mpName} is stale (missing: ${missingPlugins.join(", ")}). Claude Code will auto-update on next session start.`,
649
+ );
650
+ return localMarketplaces;
650
651
  }
651
652
 
652
653
  export interface RefreshAndRepairResult {
@@ -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
  }