claudeup 4.10.2 → 4.11.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__/plugin-setup.test.ts +300 -0
- package/src/__tests__/plugin-version-check.test.ts +760 -0
- package/src/prerunner/index.js +17 -0
- package/src/prerunner/index.ts +20 -0
- package/src/services/plugin-setup.js +88 -1
- package/src/services/plugin-setup.ts +99 -1
- package/src/services/plugin-version-check.js +248 -0
- package/src/services/plugin-version-check.ts +340 -0
- package/src/ui/App.js +37 -18
- package/src/ui/App.tsx +94 -61
- package/src/ui/screens/PluginsScreen.js +86 -13
- package/src/ui/screens/PluginsScreen.tsx +135 -24
- package/src/ui/state/DimensionsContext.js +8 -6
- package/src/ui/state/DimensionsContext.tsx +10 -1
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin version mismatch detection and fix.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code's plugin loader (pluginLoader.ts:2051) takes the FIRST entry
|
|
5
|
+
* from installed_plugins.json for each plugin ID:
|
|
6
|
+
*
|
|
7
|
+
* const installEntry = installedPluginsData.plugins[pluginId]?.[0]
|
|
8
|
+
*
|
|
9
|
+
* This means the first project to install a plugin determines which version
|
|
10
|
+
* loads for ALL projects. When different projects install different versions,
|
|
11
|
+
* only the one at index [0] takes effect.
|
|
12
|
+
*
|
|
13
|
+
* Bug report: https://github.com/anthropics/claude-code/issues/45997
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { InstalledPluginEntry } from "../types/index.js";
|
|
17
|
+
import {
|
|
18
|
+
getEnabledPlugins,
|
|
19
|
+
getGlobalEnabledPlugins,
|
|
20
|
+
getLocalEnabledPlugins,
|
|
21
|
+
readInstalledPluginsRegistry,
|
|
22
|
+
writeInstalledPluginsRegistry,
|
|
23
|
+
} from "./claude-settings.js";
|
|
24
|
+
|
|
25
|
+
const BUG_REPORT_URL = "https://github.com/anthropics/claude-code/issues/45997";
|
|
26
|
+
|
|
27
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export interface VersionMismatchInfo {
|
|
30
|
+
/** Plugin ID, e.g. "terminal@magus" */
|
|
31
|
+
pluginId: string;
|
|
32
|
+
/** Version at entries[0] — the one Claude Code actually loads */
|
|
33
|
+
firstEntryVersion: string;
|
|
34
|
+
/** Project path of the entries[0] entry */
|
|
35
|
+
firstEntryProject: string | undefined;
|
|
36
|
+
/** Version this project has installed */
|
|
37
|
+
currentProjectVersion: string;
|
|
38
|
+
/** All entries for this plugin */
|
|
39
|
+
allEntries: InstalledPluginEntry[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface FixResult {
|
|
43
|
+
/** Number of entries updated */
|
|
44
|
+
updated: number;
|
|
45
|
+
/** Project paths that were updated */
|
|
46
|
+
projects: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Detection ────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check for plugin version mismatches caused by the [0] index bug.
|
|
53
|
+
*
|
|
54
|
+
* For each enabled plugin in the current project:
|
|
55
|
+
* 1. Reads the plugin's entry array from installed_plugins.json
|
|
56
|
+
* 2. Finds the entry matching the current project
|
|
57
|
+
* 3. Checks if entries[0] has a DIFFERENT version than the current project's entry
|
|
58
|
+
* 4. If yes: this project will load the wrong version
|
|
59
|
+
*
|
|
60
|
+
* @param projectPath - The current project path to check
|
|
61
|
+
* @returns Array of mismatch info for each affected plugin
|
|
62
|
+
*/
|
|
63
|
+
export async function checkPluginVersionMismatches(
|
|
64
|
+
projectPath: string,
|
|
65
|
+
): Promise<VersionMismatchInfo[]> {
|
|
66
|
+
const registry = await readInstalledPluginsRegistry();
|
|
67
|
+
const enabledPluginIds = await collectEnabledPluginIds(projectPath);
|
|
68
|
+
const mismatches: VersionMismatchInfo[] = [];
|
|
69
|
+
|
|
70
|
+
for (const pluginId of enabledPluginIds) {
|
|
71
|
+
const entries = registry.plugins[pluginId];
|
|
72
|
+
if (!entries || entries.length < 2) continue;
|
|
73
|
+
|
|
74
|
+
// Find the entry for the current project
|
|
75
|
+
const currentEntry = entries.find(
|
|
76
|
+
(e) =>
|
|
77
|
+
e.scope === "project" &&
|
|
78
|
+
normalizePath(e.projectPath) === normalizePath(projectPath),
|
|
79
|
+
);
|
|
80
|
+
if (!currentEntry) continue;
|
|
81
|
+
|
|
82
|
+
const firstEntry = entries[0];
|
|
83
|
+
|
|
84
|
+
// Compare the version at [0] with the current project's version
|
|
85
|
+
if (firstEntry.version !== currentEntry.version) {
|
|
86
|
+
mismatches.push({
|
|
87
|
+
pluginId,
|
|
88
|
+
firstEntryVersion: firstEntry.version,
|
|
89
|
+
firstEntryProject: firstEntry.projectPath,
|
|
90
|
+
currentProjectVersion: currentEntry.version,
|
|
91
|
+
allEntries: entries,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return mismatches;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check a single plugin for version mismatch (used after plugin update).
|
|
101
|
+
*
|
|
102
|
+
* @param pluginId - The plugin ID to check
|
|
103
|
+
* @param projectPath - The current project path
|
|
104
|
+
* @returns Mismatch info or null if no mismatch
|
|
105
|
+
*/
|
|
106
|
+
export async function checkSinglePluginMismatch(
|
|
107
|
+
pluginId: string,
|
|
108
|
+
projectPath: string,
|
|
109
|
+
): Promise<VersionMismatchInfo | null> {
|
|
110
|
+
const registry = await readInstalledPluginsRegistry();
|
|
111
|
+
const entries = registry.plugins[pluginId];
|
|
112
|
+
if (!entries || entries.length < 2) return null;
|
|
113
|
+
|
|
114
|
+
const currentEntry = entries.find(
|
|
115
|
+
(e) =>
|
|
116
|
+
e.scope === "project" &&
|
|
117
|
+
normalizePath(e.projectPath) === normalizePath(projectPath),
|
|
118
|
+
);
|
|
119
|
+
if (!currentEntry) return null;
|
|
120
|
+
|
|
121
|
+
const firstEntry = entries[0];
|
|
122
|
+
if (firstEntry.version === currentEntry.version) return null;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
pluginId,
|
|
126
|
+
firstEntryVersion: firstEntry.version,
|
|
127
|
+
firstEntryProject: firstEntry.projectPath,
|
|
128
|
+
currentProjectVersion: currentEntry.version,
|
|
129
|
+
allEntries: entries,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Fix ──────────────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Fix a version mismatch by updating ALL entries for a plugin to use
|
|
137
|
+
* the target version. This is a JSON-only operation that rewrites
|
|
138
|
+
* installPath and version in each entry.
|
|
139
|
+
*
|
|
140
|
+
* IMPORTANT: This modifies installed_plugins.json which is Claude Code-owned.
|
|
141
|
+
* Only call after explicit user consent.
|
|
142
|
+
*
|
|
143
|
+
* @param pluginId - The plugin ID to fix
|
|
144
|
+
* @param targetVersion - The version to normalize all entries to
|
|
145
|
+
* @returns Number of updated entries and which projects were affected
|
|
146
|
+
*/
|
|
147
|
+
export async function fixPluginVersionMismatch(
|
|
148
|
+
pluginId: string,
|
|
149
|
+
targetVersion: string,
|
|
150
|
+
): Promise<FixResult> {
|
|
151
|
+
const registry = await readInstalledPluginsRegistry();
|
|
152
|
+
const entries = registry.plugins[pluginId];
|
|
153
|
+
if (!entries || entries.length === 0) {
|
|
154
|
+
return { updated: 0, projects: [] };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Find an existing entry with the target version to get the correct installPath
|
|
158
|
+
const templateEntry = entries.find((e) => e.version === targetVersion);
|
|
159
|
+
if (!templateEntry) {
|
|
160
|
+
return { updated: 0, projects: [] };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const targetInstallPath = templateEntry.installPath;
|
|
164
|
+
const updated: string[] = [];
|
|
165
|
+
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
if (entry.version !== targetVersion) {
|
|
168
|
+
entry.version = targetVersion;
|
|
169
|
+
entry.installPath = targetInstallPath;
|
|
170
|
+
entry.lastUpdated = new Date().toISOString();
|
|
171
|
+
updated.push(entry.projectPath || "(global)");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (updated.length > 0) {
|
|
176
|
+
await writeInstalledPluginsRegistry(registry);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { updated: updated.length, projects: updated };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Fix all version mismatches by normalizing each plugin to its latest version.
|
|
184
|
+
*
|
|
185
|
+
* @param mismatches - Array of mismatches from checkPluginVersionMismatches
|
|
186
|
+
* @returns Map of pluginId to FixResult
|
|
187
|
+
*/
|
|
188
|
+
export async function fixAllPluginVersionMismatches(
|
|
189
|
+
mismatches: VersionMismatchInfo[],
|
|
190
|
+
): Promise<Map<string, FixResult>> {
|
|
191
|
+
const results = new Map<string, FixResult>();
|
|
192
|
+
|
|
193
|
+
// Read registry once, apply all fixes, write once
|
|
194
|
+
const registry = await readInstalledPluginsRegistry();
|
|
195
|
+
|
|
196
|
+
for (const mismatch of mismatches) {
|
|
197
|
+
const entries = registry.plugins[mismatch.pluginId];
|
|
198
|
+
if (!entries || entries.length === 0) {
|
|
199
|
+
results.set(mismatch.pluginId, { updated: 0, projects: [] });
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Use the current project's version as the target (the version the user expects)
|
|
204
|
+
const targetVersion = mismatch.currentProjectVersion;
|
|
205
|
+
const templateEntry = entries.find((e) => e.version === targetVersion);
|
|
206
|
+
if (!templateEntry) {
|
|
207
|
+
results.set(mismatch.pluginId, { updated: 0, projects: [] });
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const targetInstallPath = templateEntry.installPath;
|
|
212
|
+
const updated: string[] = [];
|
|
213
|
+
|
|
214
|
+
for (const entry of entries) {
|
|
215
|
+
if (entry.version !== targetVersion) {
|
|
216
|
+
entry.version = targetVersion;
|
|
217
|
+
entry.installPath = targetInstallPath;
|
|
218
|
+
entry.lastUpdated = new Date().toISOString();
|
|
219
|
+
updated.push(entry.projectPath || "(global)");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
results.set(mismatch.pluginId, {
|
|
224
|
+
updated: updated.length,
|
|
225
|
+
projects: updated,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
await writeInstalledPluginsRegistry(registry);
|
|
230
|
+
return results;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── Formatting ───────────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Format mismatch info for console output (prerunner).
|
|
237
|
+
*/
|
|
238
|
+
export function formatMismatchWarning(
|
|
239
|
+
mismatches: VersionMismatchInfo[],
|
|
240
|
+
): string {
|
|
241
|
+
const lines: string[] = [];
|
|
242
|
+
lines.push(
|
|
243
|
+
"WARNING: Plugin version mismatch detected (Claude Code bug #45997)",
|
|
244
|
+
);
|
|
245
|
+
lines.push(
|
|
246
|
+
"Claude Code loads the first entry from installed_plugins.json for ALL projects.",
|
|
247
|
+
);
|
|
248
|
+
lines.push(
|
|
249
|
+
"The following plugins will load a different version than what this project has installed:\n",
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
for (const m of mismatches) {
|
|
253
|
+
const firstProject = m.firstEntryProject
|
|
254
|
+
? shortenPath(m.firstEntryProject)
|
|
255
|
+
: "(global)";
|
|
256
|
+
lines.push(` ${m.pluginId}:`);
|
|
257
|
+
lines.push(` Loading v${m.firstEntryVersion} (from ${firstProject})`);
|
|
258
|
+
lines.push(` Expected v${m.currentProjectVersion} (this project)`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
lines.push("");
|
|
262
|
+
lines.push(`Bug report: ${BUG_REPORT_URL}`);
|
|
263
|
+
lines.push(
|
|
264
|
+
"Run claudeup to fix — it can align all projects to the same version.",
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
return lines.join("\n");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Format mismatch info for TUI display.
|
|
272
|
+
*/
|
|
273
|
+
export function formatMismatchBanner(
|
|
274
|
+
mismatches: VersionMismatchInfo[],
|
|
275
|
+
): string {
|
|
276
|
+
const parts = mismatches.map(
|
|
277
|
+
(m) =>
|
|
278
|
+
`${m.pluginId}: loading v${m.firstEntryVersion} instead of v${m.currentProjectVersion}`,
|
|
279
|
+
);
|
|
280
|
+
return `Version mismatch: ${parts.join(", ")}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Collect all enabled plugin IDs across all scopes for a project.
|
|
287
|
+
*/
|
|
288
|
+
async function collectEnabledPluginIds(
|
|
289
|
+
projectPath: string,
|
|
290
|
+
): Promise<Set<string>> {
|
|
291
|
+
const pluginIds = new Set<string>();
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const global = await getGlobalEnabledPlugins();
|
|
295
|
+
for (const [id, enabled] of Object.entries(global)) {
|
|
296
|
+
if (enabled) pluginIds.add(id);
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
/* skip unreadable */
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const project = await getEnabledPlugins(projectPath);
|
|
304
|
+
for (const [id, enabled] of Object.entries(project)) {
|
|
305
|
+
if (enabled) pluginIds.add(id);
|
|
306
|
+
}
|
|
307
|
+
} catch {
|
|
308
|
+
/* skip unreadable */
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const local = await getLocalEnabledPlugins(projectPath);
|
|
313
|
+
for (const [id, enabled] of Object.entries(local)) {
|
|
314
|
+
if (enabled) pluginIds.add(id);
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
/* skip unreadable */
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return pluginIds;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Normalize a path for comparison (resolve and trailing-slash normalize).
|
|
325
|
+
*/
|
|
326
|
+
function normalizePath(p: string | undefined): string {
|
|
327
|
+
if (!p) return "";
|
|
328
|
+
return p.replace(/\/+$/, "");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Shorten a path for display by replacing homedir with ~.
|
|
333
|
+
*/
|
|
334
|
+
function shortenPath(p: string): string {
|
|
335
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
336
|
+
if (home && p.startsWith(home)) {
|
|
337
|
+
return `~${p.slice(home.length)}`;
|
|
338
|
+
}
|
|
339
|
+
return p;
|
|
340
|
+
}
|
package/src/ui/App.js
CHANGED
|
@@ -8,6 +8,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
10
|
import { migrateMarketplaceRename, recoverMarketplaceSettings, } from "../services/claude-settings.js";
|
|
11
|
+
import { checkPluginVersionMismatches, formatMismatchBanner, } from "../services/plugin-version-check.js";
|
|
11
12
|
import { checkForUpdates, getCurrentVersion, } from "../services/version-check.js";
|
|
12
13
|
import { useKeyboardHandler } from "./hooks/useKeyboardHandler.js";
|
|
13
14
|
import { ProgressBar } from "./components/layout/ProgressBar.js";
|
|
@@ -175,11 +176,25 @@ function UpdateBanner({ result }) {
|
|
|
175
176
|
function ProgressIndicator({ message, current, total, }) {
|
|
176
177
|
return (_jsx("box", { paddingLeft: 1, paddingRight: 1, children: _jsx(ProgressBar, { message: message, current: current, total: total }) }));
|
|
177
178
|
}
|
|
178
|
-
function AppContentInner({ showDebug, onDebugToggle, updateInfo, onExit, }) {
|
|
179
|
-
const { state
|
|
179
|
+
function AppContentInner({ showDebug, onDebugToggle, updateInfo, onExit, recoveryReport, mismatchWarning, }) {
|
|
180
|
+
const { state } = useApp();
|
|
180
181
|
const { progress } = state;
|
|
181
182
|
const dimensions = useDimensions();
|
|
183
|
+
return (_jsxs("box", { flexDirection: "column", height: dimensions.terminalHeight, children: [updateInfo?.updateAvailable && _jsx(UpdateBanner, { result: updateInfo }), recoveryReport && (_jsx("box", { paddingLeft: 1, paddingRight: 1, children: _jsxs("text", { fg: "green", children: ["\u2713 Fixed: ", recoveryReport] }) })), mismatchWarning && (_jsx("box", { paddingLeft: 1, paddingRight: 1, children: _jsxs("text", { fg: "yellow", children: ["\u26A0 ", mismatchWarning, " (bug #45997)"] }) })), 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, {})] }));
|
|
184
|
+
}
|
|
185
|
+
function AppContent({ onExit }) {
|
|
186
|
+
const { state, dispatch } = useApp();
|
|
187
|
+
const { progress } = state;
|
|
188
|
+
const [showDebug, setShowDebug] = useState(false);
|
|
189
|
+
const [updateInfo, setUpdateInfo] = useState(null);
|
|
182
190
|
const [recoveryReport, setRecoveryReport] = useState(null);
|
|
191
|
+
const [mismatchWarning, setMismatchWarning] = useState(null);
|
|
192
|
+
// Check for updates on startup (non-blocking)
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
checkForUpdates()
|
|
195
|
+
.then(setUpdateInfo)
|
|
196
|
+
.catch(() => { });
|
|
197
|
+
}, []);
|
|
183
198
|
// Auto-dismiss recovery banner after 5 seconds
|
|
184
199
|
useEffect(() => {
|
|
185
200
|
if (!recoveryReport)
|
|
@@ -187,6 +202,13 @@ function AppContentInner({ showDebug, onDebugToggle, updateInfo, onExit, }) {
|
|
|
187
202
|
const timer = setTimeout(() => setRecoveryReport(null), 5000);
|
|
188
203
|
return () => clearTimeout(timer);
|
|
189
204
|
}, [recoveryReport]);
|
|
205
|
+
// Auto-dismiss mismatch warning after 8 seconds (longer — more important)
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
if (!mismatchWarning)
|
|
208
|
+
return;
|
|
209
|
+
const timer = setTimeout(() => setMismatchWarning(null), 8000);
|
|
210
|
+
return () => clearTimeout(timer);
|
|
211
|
+
}, [mismatchWarning]);
|
|
190
212
|
// Auto-refresh marketplaces on startup
|
|
191
213
|
useEffect(() => {
|
|
192
214
|
const noRefresh = process.argv.includes("--no-refresh");
|
|
@@ -196,9 +218,9 @@ function AppContentInner({ showDebug, onDebugToggle, updateInfo, onExit, }) {
|
|
|
196
218
|
type: "SHOW_PROGRESS",
|
|
197
219
|
state: { message: "Scanning marketplaces..." },
|
|
198
220
|
});
|
|
199
|
-
// Migrate old marketplace names
|
|
221
|
+
// Migrate old marketplace names -> magus (idempotent), then repair plugin.json files
|
|
200
222
|
migrateMarketplaceRename().catch(() => { }); // non-blocking, best-effort
|
|
201
|
-
// Recover stale marketplace registry entries (e.g. "directory"
|
|
223
|
+
// Recover stale marketplace registry entries (e.g. "directory" -> "github")
|
|
202
224
|
recoverMarketplaceSettings()
|
|
203
225
|
.then(async (recovery) => {
|
|
204
226
|
const parts = [];
|
|
@@ -226,6 +248,14 @@ function AppContentInner({ showDebug, onDebugToggle, updateInfo, onExit, }) {
|
|
|
226
248
|
}
|
|
227
249
|
})
|
|
228
250
|
.catch(() => { }); // non-fatal
|
|
251
|
+
// Check for plugin version mismatches (the [0] index bug)
|
|
252
|
+
checkPluginVersionMismatches(process.cwd())
|
|
253
|
+
.then((mismatches) => {
|
|
254
|
+
if (mismatches.length > 0) {
|
|
255
|
+
setMismatchWarning(formatMismatchBanner(mismatches));
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
.catch(() => { }); // non-fatal
|
|
229
259
|
repairAllMarketplaces()
|
|
230
260
|
.then(async () => {
|
|
231
261
|
dispatch({ type: "HIDE_PROGRESS" });
|
|
@@ -235,20 +265,9 @@ function AppContentInner({ showDebug, onDebugToggle, updateInfo, onExit, }) {
|
|
|
235
265
|
dispatch({ type: "HIDE_PROGRESS" });
|
|
236
266
|
});
|
|
237
267
|
}, [dispatch]);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const { state } = useApp();
|
|
242
|
-
const { progress } = state;
|
|
243
|
-
const [showDebug, setShowDebug] = useState(false);
|
|
244
|
-
const [updateInfo, setUpdateInfo] = useState(null);
|
|
245
|
-
// Check for updates on startup (non-blocking)
|
|
246
|
-
useEffect(() => {
|
|
247
|
-
checkForUpdates()
|
|
248
|
-
.then(setUpdateInfo)
|
|
249
|
-
.catch(() => { });
|
|
250
|
-
}, []);
|
|
251
|
-
return (_jsx(DimensionsProvider, { showProgress: !!progress, showDebug: showDebug, showUpdateBanner: !!updateInfo?.updateAvailable, children: _jsx(AppContentInner, { showDebug: showDebug, onDebugToggle: () => setShowDebug((s) => !s), updateInfo: updateInfo, onExit: onExit }) }));
|
|
268
|
+
// Count transient banners for dimension calculation
|
|
269
|
+
const transientBannerCount = (recoveryReport ? 1 : 0) + (mismatchWarning ? 1 : 0);
|
|
270
|
+
return (_jsx(DimensionsProvider, { showProgress: !!progress, showDebug: showDebug, showUpdateBanner: !!updateInfo?.updateAvailable, transientBannerCount: transientBannerCount, children: _jsx(AppContentInner, { showDebug: showDebug, onDebugToggle: () => setShowDebug((s) => !s), updateInfo: updateInfo, onExit: onExit, recoveryReport: recoveryReport, mismatchWarning: mismatchWarning }) }));
|
|
252
271
|
}
|
|
253
272
|
export function App({ onExit }) {
|
|
254
273
|
return (_jsx(AppProvider, { children: _jsx(AppContent, { onExit: onExit }) }));
|
package/src/ui/App.tsx
CHANGED
|
@@ -28,6 +28,10 @@ import {
|
|
|
28
28
|
migrateMarketplaceRename,
|
|
29
29
|
recoverMarketplaceSettings,
|
|
30
30
|
} from "../services/claude-settings.js";
|
|
31
|
+
import {
|
|
32
|
+
checkPluginVersionMismatches,
|
|
33
|
+
formatMismatchBanner,
|
|
34
|
+
} from "../services/plugin-version-check.js";
|
|
31
35
|
import {
|
|
32
36
|
checkForUpdates,
|
|
33
37
|
getCurrentVersion,
|
|
@@ -250,6 +254,8 @@ interface AppContentInnerProps {
|
|
|
250
254
|
onDebugToggle: () => void;
|
|
251
255
|
updateInfo: VersionCheckResult | null;
|
|
252
256
|
onExit: () => void;
|
|
257
|
+
recoveryReport: string | null;
|
|
258
|
+
mismatchWarning: string | null;
|
|
253
259
|
}
|
|
254
260
|
|
|
255
261
|
function AppContentInner({
|
|
@@ -257,11 +263,72 @@ function AppContentInner({
|
|
|
257
263
|
onDebugToggle,
|
|
258
264
|
updateInfo,
|
|
259
265
|
onExit,
|
|
266
|
+
recoveryReport,
|
|
267
|
+
mismatchWarning,
|
|
260
268
|
}: AppContentInnerProps) {
|
|
261
|
-
const { state
|
|
269
|
+
const { state } = useApp();
|
|
262
270
|
const { progress } = state;
|
|
263
271
|
const dimensions = useDimensions();
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<box flexDirection="column" height={dimensions.terminalHeight}>
|
|
275
|
+
{updateInfo?.updateAvailable && <UpdateBanner result={updateInfo} />}
|
|
276
|
+
{recoveryReport && (
|
|
277
|
+
<box paddingLeft={1} paddingRight={1}>
|
|
278
|
+
<text fg="green">✓ Fixed: {recoveryReport}</text>
|
|
279
|
+
</box>
|
|
280
|
+
)}
|
|
281
|
+
{mismatchWarning && (
|
|
282
|
+
<box paddingLeft={1} paddingRight={1}>
|
|
283
|
+
<text fg="yellow">⚠ {mismatchWarning} (bug #45997)</text>
|
|
284
|
+
</box>
|
|
285
|
+
)}
|
|
286
|
+
{showDebug && (
|
|
287
|
+
<box paddingLeft={1} paddingRight={1}>
|
|
288
|
+
<text fg="#888888">
|
|
289
|
+
DEBUG: {dimensions.terminalWidth}x{dimensions.terminalHeight} |
|
|
290
|
+
content={dimensions.contentHeight} | screen=
|
|
291
|
+
{state.currentRoute.screen}
|
|
292
|
+
</text>
|
|
293
|
+
</box>
|
|
294
|
+
)}
|
|
295
|
+
{progress && <ProgressIndicator {...progress} />}
|
|
296
|
+
<box
|
|
297
|
+
flexDirection="column"
|
|
298
|
+
height={dimensions.contentHeight}
|
|
299
|
+
paddingLeft={1}
|
|
300
|
+
paddingRight={1}
|
|
301
|
+
>
|
|
302
|
+
<Router />
|
|
303
|
+
</box>
|
|
304
|
+
<GlobalKeyHandler onDebugToggle={onDebugToggle} onExit={onExit} />
|
|
305
|
+
<ModalContainer />
|
|
306
|
+
</box>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* AppContent Component
|
|
312
|
+
* Wraps app with DimensionsProvider and manages state for debug/updates
|
|
313
|
+
*/
|
|
314
|
+
interface AppContentProps {
|
|
315
|
+
onExit: () => void;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function AppContent({ onExit }: AppContentProps) {
|
|
319
|
+
const { state, dispatch } = useApp();
|
|
320
|
+
const { progress } = state;
|
|
321
|
+
const [showDebug, setShowDebug] = useState(false);
|
|
322
|
+
const [updateInfo, setUpdateInfo] = useState<VersionCheckResult | null>(null);
|
|
264
323
|
const [recoveryReport, setRecoveryReport] = useState<string | null>(null);
|
|
324
|
+
const [mismatchWarning, setMismatchWarning] = useState<string | null>(null);
|
|
325
|
+
|
|
326
|
+
// Check for updates on startup (non-blocking)
|
|
327
|
+
useEffect(() => {
|
|
328
|
+
checkForUpdates()
|
|
329
|
+
.then(setUpdateInfo)
|
|
330
|
+
.catch(() => {});
|
|
331
|
+
}, []);
|
|
265
332
|
|
|
266
333
|
// Auto-dismiss recovery banner after 5 seconds
|
|
267
334
|
useEffect(() => {
|
|
@@ -270,6 +337,13 @@ function AppContentInner({
|
|
|
270
337
|
return () => clearTimeout(timer);
|
|
271
338
|
}, [recoveryReport]);
|
|
272
339
|
|
|
340
|
+
// Auto-dismiss mismatch warning after 8 seconds (longer — more important)
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
if (!mismatchWarning) return;
|
|
343
|
+
const timer = setTimeout(() => setMismatchWarning(null), 8000);
|
|
344
|
+
return () => clearTimeout(timer);
|
|
345
|
+
}, [mismatchWarning]);
|
|
346
|
+
|
|
273
347
|
// Auto-refresh marketplaces on startup
|
|
274
348
|
useEffect(() => {
|
|
275
349
|
const noRefresh = process.argv.includes("--no-refresh");
|
|
@@ -280,10 +354,10 @@ function AppContentInner({
|
|
|
280
354
|
state: { message: "Scanning marketplaces..." },
|
|
281
355
|
});
|
|
282
356
|
|
|
283
|
-
// Migrate old marketplace names
|
|
357
|
+
// Migrate old marketplace names -> magus (idempotent), then repair plugin.json files
|
|
284
358
|
migrateMarketplaceRename().catch(() => {}); // non-blocking, best-effort
|
|
285
359
|
|
|
286
|
-
// Recover stale marketplace registry entries (e.g. "directory"
|
|
360
|
+
// Recover stale marketplace registry entries (e.g. "directory" -> "github")
|
|
287
361
|
recoverMarketplaceSettings()
|
|
288
362
|
.then(async (recovery) => {
|
|
289
363
|
const parts: string[] = [];
|
|
@@ -302,14 +376,10 @@ function AppContentInner({
|
|
|
302
376
|
}
|
|
303
377
|
}
|
|
304
378
|
if (recovery.enabledAutoUpdate.length > 0) {
|
|
305
|
-
parts.push(
|
|
306
|
-
`auto-update: ${recovery.enabledAutoUpdate.join(", ")}`,
|
|
307
|
-
);
|
|
379
|
+
parts.push(`auto-update: ${recovery.enabledAutoUpdate.join(", ")}`);
|
|
308
380
|
}
|
|
309
381
|
if (recovery.removed.length > 0) {
|
|
310
|
-
parts.push(
|
|
311
|
-
`removed: ${recovery.removed.join(", ")}`,
|
|
312
|
-
);
|
|
382
|
+
parts.push(`removed: ${recovery.removed.join(", ")}`);
|
|
313
383
|
}
|
|
314
384
|
if (parts.length > 0) {
|
|
315
385
|
setRecoveryReport(parts.join(" | "));
|
|
@@ -317,6 +387,15 @@ function AppContentInner({
|
|
|
317
387
|
})
|
|
318
388
|
.catch(() => {}); // non-fatal
|
|
319
389
|
|
|
390
|
+
// Check for plugin version mismatches (the [0] index bug)
|
|
391
|
+
checkPluginVersionMismatches(process.cwd())
|
|
392
|
+
.then((mismatches) => {
|
|
393
|
+
if (mismatches.length > 0) {
|
|
394
|
+
setMismatchWarning(formatMismatchBanner(mismatches));
|
|
395
|
+
}
|
|
396
|
+
})
|
|
397
|
+
.catch(() => {}); // non-fatal
|
|
398
|
+
|
|
320
399
|
repairAllMarketplaces()
|
|
321
400
|
.then(async () => {
|
|
322
401
|
dispatch({ type: "HIDE_PROGRESS" });
|
|
@@ -327,70 +406,24 @@ function AppContentInner({
|
|
|
327
406
|
});
|
|
328
407
|
}, [dispatch]);
|
|
329
408
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
{recoveryReport && (
|
|
334
|
-
<box paddingLeft={1} paddingRight={1}>
|
|
335
|
-
<text fg="green">✓ Fixed: {recoveryReport}</text>
|
|
336
|
-
</box>
|
|
337
|
-
)}
|
|
338
|
-
{showDebug && (
|
|
339
|
-
<box paddingLeft={1} paddingRight={1}>
|
|
340
|
-
<text fg="#888888">
|
|
341
|
-
DEBUG: {dimensions.terminalWidth}x{dimensions.terminalHeight} |
|
|
342
|
-
content={dimensions.contentHeight} | screen=
|
|
343
|
-
{state.currentRoute.screen}
|
|
344
|
-
</text>
|
|
345
|
-
</box>
|
|
346
|
-
)}
|
|
347
|
-
{progress && <ProgressIndicator {...progress} />}
|
|
348
|
-
<box
|
|
349
|
-
flexDirection="column"
|
|
350
|
-
height={dimensions.contentHeight}
|
|
351
|
-
paddingLeft={1}
|
|
352
|
-
paddingRight={1}
|
|
353
|
-
>
|
|
354
|
-
<Router />
|
|
355
|
-
</box>
|
|
356
|
-
<GlobalKeyHandler onDebugToggle={onDebugToggle} onExit={onExit} />
|
|
357
|
-
<ModalContainer />
|
|
358
|
-
</box>
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* AppContent Component
|
|
364
|
-
* Wraps app with DimensionsProvider and manages state for debug/updates
|
|
365
|
-
*/
|
|
366
|
-
interface AppContentProps {
|
|
367
|
-
onExit: () => void;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function AppContent({ onExit }: AppContentProps) {
|
|
371
|
-
const { state } = useApp();
|
|
372
|
-
const { progress } = state;
|
|
373
|
-
const [showDebug, setShowDebug] = useState(false);
|
|
374
|
-
const [updateInfo, setUpdateInfo] = useState<VersionCheckResult | null>(null);
|
|
375
|
-
|
|
376
|
-
// Check for updates on startup (non-blocking)
|
|
377
|
-
useEffect(() => {
|
|
378
|
-
checkForUpdates()
|
|
379
|
-
.then(setUpdateInfo)
|
|
380
|
-
.catch(() => {});
|
|
381
|
-
}, []);
|
|
409
|
+
// Count transient banners for dimension calculation
|
|
410
|
+
const transientBannerCount =
|
|
411
|
+
(recoveryReport ? 1 : 0) + (mismatchWarning ? 1 : 0);
|
|
382
412
|
|
|
383
413
|
return (
|
|
384
414
|
<DimensionsProvider
|
|
385
415
|
showProgress={!!progress}
|
|
386
416
|
showDebug={showDebug}
|
|
387
417
|
showUpdateBanner={!!updateInfo?.updateAvailable}
|
|
418
|
+
transientBannerCount={transientBannerCount}
|
|
388
419
|
>
|
|
389
420
|
<AppContentInner
|
|
390
421
|
showDebug={showDebug}
|
|
391
422
|
onDebugToggle={() => setShowDebug((s) => !s)}
|
|
392
423
|
updateInfo={updateInfo}
|
|
393
424
|
onExit={onExit}
|
|
425
|
+
recoveryReport={recoveryReport}
|
|
426
|
+
mismatchWarning={mismatchWarning}
|
|
394
427
|
/>
|
|
395
428
|
</DimensionsProvider>
|
|
396
429
|
);
|