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 +1 -1
- package/src/__tests__/dual-write-prevention.test.ts +1 -1
- package/src/prerunner/index.js +18 -0
- package/src/prerunner/index.ts +24 -0
- package/src/services/claude-cli.js +51 -3
- package/src/services/claude-cli.ts +61 -3
- package/src/services/claude-settings.js +24 -1
- package/src/services/claude-settings.ts +34 -1
- package/src/ui/App.js +32 -2
- package/src/ui/App.tsx +50 -1
- 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
package/package.json
CHANGED
|
@@ -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({
|
package/src/prerunner/index.js
CHANGED
|
@@ -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)
|
package/src/prerunner/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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 (
|
|
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 {
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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 (
|
|
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(
|
|
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 (
|
|
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
|