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.
- package/package.json +1 -1
- package/src/__tests__/dual-write-prevention.test.ts +1 -1
- package/src/__tests__/gap-fill-versions.test.ts +382 -0
- package/src/prerunner/index.js +49 -17
- package/src/prerunner/index.ts +59 -18
- package/src/services/claude-cli.js +47 -3
- package/src/services/claude-cli.ts +53 -3
- package/src/services/claude-settings.js +98 -1
- package/src/services/claude-settings.ts +126 -1
- package/src/services/plugin-manager.js +13 -16
- package/src/services/plugin-manager.ts +17 -16
- package/src/ui/components/modals/LoadingModal.js +4 -1
- package/src/ui/components/modals/LoadingModal.tsx +15 -4
- package/src/ui/screens/PluginsScreen.js +11 -16
- package/src/ui/screens/PluginsScreen.tsx +29 -20
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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 (
|
|
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.
|
|
438
|
-
*
|
|
439
|
-
*
|
|
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
|
|
449
|
-
if (
|
|
452
|
+
const missingPlugins = remotePluginNames.filter((name) => !localNames.has(name));
|
|
453
|
+
if (missingPlugins.length === 0)
|
|
450
454
|
return localMarketplaces;
|
|
451
455
|
autoSyncedMarketplaces.add(mpName);
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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.
|
|
623
|
-
*
|
|
624
|
-
*
|
|
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
|
|
638
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
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="
|
|
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
|
-
<
|
|
34
|
-
|
|
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
|
}
|