claudeup 4.18.0 → 4.22.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.
Files changed (191) hide show
  1. package/bin/claudeup.js +43 -19
  2. package/package.json +14 -19
  3. package/scripts/build-binaries.ts +77 -0
  4. package/scripts/check-optional-deps.ts +49 -0
  5. package/scripts/test-isolated.ts +43 -0
  6. package/src/__tests__/alias-adopt.test.ts +364 -0
  7. package/src/__tests__/alias-parser.test.ts +92 -0
  8. package/src/__tests__/alias-shell-writer.test.ts +7 -0
  9. package/src/__tests__/alias-store.test.ts +77 -0
  10. package/src/__tests__/cli-router.test.ts +65 -0
  11. package/src/__tests__/conventions-integration.test.ts +745 -0
  12. package/src/__tests__/conventions-manager.test.ts +1172 -0
  13. package/src/__tests__/doctor-bins.test.ts +35 -0
  14. package/src/__tests__/doctor.test.ts +450 -0
  15. package/src/__tests__/dual-write-prevention.test.ts +14 -4
  16. package/src/__tests__/file-locking.test.ts +208 -0
  17. package/src/__tests__/gitignore-prerun.test.ts +1 -0
  18. package/src/__tests__/install-command.test.ts +81 -0
  19. package/src/__tests__/install-plan.test.ts +98 -0
  20. package/src/__tests__/manifest.test.ts +136 -0
  21. package/src/__tests__/marketplace-refresh.test.ts +267 -0
  22. package/src/__tests__/plugin-requires.test.ts +133 -0
  23. package/src/__tests__/plugin-setup.test.ts +118 -0
  24. package/src/__tests__/profile-command.test.ts +116 -0
  25. package/src/__tests__/profile-materializer.test.ts +82 -0
  26. package/src/__tests__/profile-sync.test.ts +136 -0
  27. package/src/__tests__/registry-version-resolution.test.ts +74 -0
  28. package/src/__tests__/resolve-executable.test.ts +42 -0
  29. package/src/__tests__/resolver.test.ts +218 -0
  30. package/src/__tests__/symlink-manager.test.ts +99 -0
  31. package/src/__tests__/toolchain.test.ts +56 -0
  32. package/src/cli/claude.ts +21 -0
  33. package/src/cli/doctor.ts +132 -0
  34. package/src/cli/install.ts +360 -0
  35. package/src/cli/profile.ts +166 -0
  36. package/src/cli/router.ts +107 -0
  37. package/src/cli/update.ts +85 -0
  38. package/src/data/alias-flags.ts +11 -1
  39. package/src/data/cli-tools.ts +7 -7
  40. package/src/data/gitignore-defaults.ts +4 -0
  41. package/src/data/marketplaces.ts +12 -0
  42. package/src/data/predefined-profiles.ts +3 -4
  43. package/src/main.tsx +11 -154
  44. package/src/prerunner/index.ts +62 -27
  45. package/src/services/alias-shell-writer.ts +382 -8
  46. package/src/services/alias-store.ts +60 -0
  47. package/src/services/claude-cli.ts +19 -22
  48. package/src/services/claude-settings.ts +99 -26
  49. package/src/services/conventions-manager.ts +865 -0
  50. package/src/services/doctor-bins.ts +49 -0
  51. package/src/services/doctor.ts +509 -0
  52. package/src/services/install-plan.ts +107 -0
  53. package/src/services/manifest.ts +166 -0
  54. package/src/services/marketplace-refresh.ts +173 -0
  55. package/src/services/plugin-manager.ts +7 -1
  56. package/src/services/plugin-requires.ts +215 -0
  57. package/src/services/plugin-setup.ts +61 -4
  58. package/src/services/plugin-version-check.ts +9 -4
  59. package/src/services/profile-materializer.ts +61 -0
  60. package/src/services/profile-sync.ts +150 -0
  61. package/src/services/resolver.ts +293 -0
  62. package/src/services/symlink-manager.ts +146 -0
  63. package/src/services/toolchain.ts +97 -0
  64. package/src/services/version-check.ts +10 -11
  65. package/src/types/index.ts +138 -31
  66. package/src/ui/App.tsx +16 -11
  67. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  68. package/src/ui/components/modals/ConfirmModal.tsx +1 -1
  69. package/src/ui/screens/AliasScreen.tsx +491 -359
  70. package/src/ui/screens/CliToolsScreen.tsx +11 -3
  71. package/src/ui/screens/PluginsScreen.tsx +3 -1
  72. package/src/ui/screens/index.ts +0 -1
  73. package/src/ui/state/reducer.ts +6 -102
  74. package/src/ui/state/types.ts +8 -35
  75. package/src/utils/command-utils.ts +23 -0
  76. package/src/utils/file-locking.ts +144 -0
  77. package/src/data/alias-flags.js +0 -196
  78. package/src/data/cli-tools.js +0 -123
  79. package/src/data/gitignore-defaults.js +0 -24
  80. package/src/data/gitignore-reasons.js +0 -97
  81. package/src/data/gitignore-templates.js +0 -21
  82. package/src/data/marketplaces.js +0 -138
  83. package/src/data/mcp-servers.js +0 -509
  84. package/src/data/predefined-profiles.js +0 -248
  85. package/src/data/settings-catalog.js +0 -625
  86. package/src/data/skill-repos.js +0 -160
  87. package/src/data/statuslines.js +0 -159
  88. package/src/data/statuslines.ts +0 -188
  89. package/src/index.js +0 -4
  90. package/src/index.ts +0 -5
  91. package/src/main.js +0 -170
  92. package/src/prerunner/index.js +0 -252
  93. package/src/services/alias-settings.js +0 -51
  94. package/src/services/alias-shell-writer.js +0 -764
  95. package/src/services/alias-store.js +0 -77
  96. package/src/services/claude-cli.js +0 -189
  97. package/src/services/claude-runner.js +0 -28
  98. package/src/services/claude-settings.js +0 -1223
  99. package/src/services/gitignore-detector.js +0 -155
  100. package/src/services/gitignore-fixer.js +0 -231
  101. package/src/services/gitignore-prerun.js +0 -46
  102. package/src/services/gitignore-resolver.js +0 -143
  103. package/src/services/gitignore-service.js +0 -117
  104. package/src/services/local-marketplace.js +0 -339
  105. package/src/services/marketplace-fetcher.js +0 -96
  106. package/src/services/marketplace-sync.js +0 -94
  107. package/src/services/mcp-registry.js +0 -87
  108. package/src/services/plugin-manager.js +0 -473
  109. package/src/services/plugin-mcp-config.js +0 -177
  110. package/src/services/plugin-setup.js +0 -444
  111. package/src/services/plugin-version-check.js +0 -262
  112. package/src/services/profiles.js +0 -161
  113. package/src/services/settings-manager.js +0 -243
  114. package/src/services/skills-manager.js +0 -393
  115. package/src/services/skillsmp-client.js +0 -87
  116. package/src/services/update-cache.js +0 -52
  117. package/src/services/version-check.js +0 -99
  118. package/src/types/gitignore.js +0 -6
  119. package/src/types/index.js +0 -1
  120. package/src/ui/App.js +0 -346
  121. package/src/ui/adapters/pluginsAdapter.js +0 -139
  122. package/src/ui/adapters/settingsAdapter.js +0 -111
  123. package/src/ui/adapters/skillsAdapter.js +0 -154
  124. package/src/ui/components/CategoryHeader.js +0 -9
  125. package/src/ui/components/EmptyFilterState.js +0 -4
  126. package/src/ui/components/ScopeIndicator.js +0 -30
  127. package/src/ui/components/ScrollableList.js +0 -36
  128. package/src/ui/components/SearchInput.js +0 -19
  129. package/src/ui/components/StyledText.js +0 -39
  130. package/src/ui/components/TabBar.js +0 -19
  131. package/src/ui/components/layout/FooterHints.js +0 -29
  132. package/src/ui/components/layout/Panel.js +0 -6
  133. package/src/ui/components/layout/ProgressBar.js +0 -14
  134. package/src/ui/components/layout/ScopeTabs.js +0 -6
  135. package/src/ui/components/layout/ScreenLayout.js +0 -16
  136. package/src/ui/components/layout/index.js +0 -5
  137. package/src/ui/components/modals/ConfirmModal.js +0 -14
  138. package/src/ui/components/modals/InputModal.js +0 -5
  139. package/src/ui/components/modals/LoadingModal.js +0 -17
  140. package/src/ui/components/modals/MessageModal.js +0 -16
  141. package/src/ui/components/modals/ModalContainer.js +0 -137
  142. package/src/ui/components/modals/SelectModal.js +0 -18
  143. package/src/ui/components/modals/VersionMismatchModal.js +0 -36
  144. package/src/ui/components/modals/index.js +0 -6
  145. package/src/ui/components/primitives/ActionHints.js +0 -13
  146. package/src/ui/components/primitives/DetailSection.js +0 -7
  147. package/src/ui/components/primitives/KeyValueLine.js +0 -8
  148. package/src/ui/components/primitives/ListCategoryRow.js +0 -8
  149. package/src/ui/components/primitives/MetaText.js +0 -8
  150. package/src/ui/components/primitives/ScopeDetail.js +0 -32
  151. package/src/ui/components/primitives/ScopeSquares.js +0 -11
  152. package/src/ui/components/primitives/SelectableRow.js +0 -5
  153. package/src/ui/components/primitives/index.js +0 -8
  154. package/src/ui/hooks/index.js +0 -4
  155. package/src/ui/hooks/useAsyncData.js +0 -77
  156. package/src/ui/hooks/useGitignoreModal.js +0 -74
  157. package/src/ui/hooks/useKeyboard.js +0 -13
  158. package/src/ui/hooks/useKeyboardHandler.js +0 -39
  159. package/src/ui/hooks/useMismatchModal.js +0 -77
  160. package/src/ui/registry.js +0 -1
  161. package/src/ui/renderers/cliToolRenderers.js +0 -54
  162. package/src/ui/renderers/gitignoreRenderers.js +0 -46
  163. package/src/ui/renderers/mcpRenderers.js +0 -26
  164. package/src/ui/renderers/pluginRenderers.js +0 -124
  165. package/src/ui/renderers/profileRenderers.js +0 -177
  166. package/src/ui/renderers/settingsRenderers.js +0 -73
  167. package/src/ui/renderers/skillRenderers.js +0 -138
  168. package/src/ui/screens/AliasScreen.js +0 -1008
  169. package/src/ui/screens/CliToolsScreen.js +0 -338
  170. package/src/ui/screens/EnvVarsScreen.js +0 -152
  171. package/src/ui/screens/GitignoreScreen.js +0 -328
  172. package/src/ui/screens/McpRegistryScreen.js +0 -238
  173. package/src/ui/screens/McpScreen.js +0 -176
  174. package/src/ui/screens/ModelSelectorScreen.js +0 -296
  175. package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
  176. package/src/ui/screens/PluginsScreen.js +0 -766
  177. package/src/ui/screens/ProfilesScreen.js +0 -298
  178. package/src/ui/screens/SkillsScreen.js +0 -549
  179. package/src/ui/screens/StatusLineScreen.js +0 -206
  180. package/src/ui/screens/StatusLineScreen.tsx +0 -416
  181. package/src/ui/screens/index.js +0 -10
  182. package/src/ui/state/AnimationContext.js +0 -34
  183. package/src/ui/state/AppContext.js +0 -162
  184. package/src/ui/state/DimensionsContext.js +0 -71
  185. package/src/ui/state/reducer.js +0 -543
  186. package/src/ui/state/types.js +0 -1
  187. package/src/ui/theme.js +0 -47
  188. package/src/utils/clipboard.js +0 -56
  189. package/src/utils/command-utils.js +0 -19
  190. package/src/utils/fuzzy-search.js +0 -101
  191. package/src/utils/string-utils.js +0 -62
@@ -1,444 +0,0 @@
1
- /**
2
- * Plugin system dependency installer
3
- *
4
- * Auto-detects available package managers and installs plugin dependencies
5
- * declared in plugin.json's "setup" section.
6
- *
7
- * Supported managers:
8
- * - pip: Python packages (auto-detects uv > pip3 > pip)
9
- * - brew: Homebrew formulae (macOS/Linux)
10
- * - npm: Global npm packages
11
- * - cargo: Rust crates
12
- * - go: Go modules (via go install)
13
- *
14
- * Special keys:
15
- * - required: System binaries that must exist in PATH (not auto-installed)
16
- *
17
- * Example plugin.json:
18
- * {
19
- * "setup": {
20
- * "pip": ["browser-use", "mcp"],
21
- * "brew": ["memextech/tap/ht-mcp"],
22
- * "go": ["github.com/MadAppGang/tmux-mcp@latest"],
23
- * "required": ["tmux"]
24
- * }
25
- * }
26
- */
27
- import { execFile } from "node:child_process";
28
- import { promisify } from "node:util";
29
- import fs from "fs-extra";
30
- import path from "node:path";
31
- import os from "node:os";
32
- import { which } from "../utils/command-utils.js";
33
- const execFileAsync = promisify(execFile);
34
- /**
35
- * Run a command and return success/failure
36
- */
37
- async function run(cmd, args, timeoutMs = 120000) {
38
- try {
39
- const { stdout, stderr } = await execFileAsync(cmd, args, {
40
- timeout: timeoutMs,
41
- });
42
- return { ok: true, stdout: stdout.trim(), stderr: stderr.trim() };
43
- }
44
- catch (error) {
45
- const e = error;
46
- return {
47
- ok: false,
48
- stdout: e.stdout?.trim() || "",
49
- stderr: e.stderr?.trim() || e.message || "unknown error",
50
- };
51
- }
52
- }
53
- /**
54
- * Detect the best available Python package installer
55
- * Prefers: uv > pip3 > pip
56
- */
57
- async function detectPipCommand() {
58
- // Prefer uv (fast, modern)
59
- if (await which("uv")) {
60
- return {
61
- cmd: "uv",
62
- args: ["pip", "install", "--system", "--break-system-packages"],
63
- };
64
- }
65
- // Fall back to pip3
66
- if (await which("pip3")) {
67
- return {
68
- cmd: "pip3",
69
- args: ["install", "--break-system-packages"],
70
- };
71
- }
72
- // Fall back to pip
73
- if (await which("pip")) {
74
- return {
75
- cmd: "pip",
76
- args: ["install", "--break-system-packages"],
77
- };
78
- }
79
- return null;
80
- }
81
- /**
82
- * Check if a Python package is already installed
83
- */
84
- async function isPythonPackageInstalled(pkg) {
85
- // Convert package name to importable module name (e.g., browser-use → browser_use)
86
- const moduleName = pkg.replace(/-/g, "_");
87
- const result = await run("python3", ["-c", `import ${moduleName}`]);
88
- return result.ok;
89
- }
90
- /**
91
- * Check if a binary is available in PATH
92
- */
93
- async function isBinaryAvailable(name) {
94
- return (await which(name)) !== null;
95
- }
96
- /**
97
- * Extract binary name from a brew formula or npm package
98
- * e.g., "memextech/tap/ht-mcp" → "ht-mcp"
99
- */
100
- function extractBinaryName(pkg) {
101
- return pkg.split("/").pop() || pkg;
102
- }
103
- /**
104
- * Install pip packages
105
- */
106
- async function installPipPackages(packages, result) {
107
- const pip = await detectPipCommand();
108
- if (!pip) {
109
- for (const pkg of packages) {
110
- result.failed.push({ pkg: `pip:${pkg}`, error: "No pip/uv found" });
111
- }
112
- return;
113
- }
114
- // Filter out already installed
115
- const toInstall = [];
116
- for (const pkg of packages) {
117
- if (await isPythonPackageInstalled(pkg)) {
118
- result.skipped.push(`pip:${pkg}`);
119
- }
120
- else {
121
- toInstall.push(pkg);
122
- }
123
- }
124
- if (toInstall.length === 0)
125
- return;
126
- // Install all at once
127
- const { ok, stderr } = await run(pip.cmd, [...pip.args, ...toInstall]);
128
- if (ok) {
129
- for (const pkg of toInstall) {
130
- result.installed.push(`pip:${pkg}`);
131
- }
132
- }
133
- else {
134
- for (const pkg of toInstall) {
135
- result.failed.push({ pkg: `pip:${pkg}`, error: stderr });
136
- }
137
- }
138
- }
139
- /**
140
- * Install brew packages
141
- */
142
- async function installBrewPackages(packages, result) {
143
- const brewPath = await which("brew");
144
- if (!brewPath) {
145
- for (const pkg of packages) {
146
- result.failed.push({
147
- pkg: `brew:${pkg}`,
148
- error: "Homebrew not installed",
149
- });
150
- }
151
- return;
152
- }
153
- for (const pkg of packages) {
154
- const binaryName = extractBinaryName(pkg);
155
- if (await isBinaryAvailable(binaryName)) {
156
- result.skipped.push(`brew:${pkg}`);
157
- continue;
158
- }
159
- const { ok, stderr } = await run(brewPath, ["install", pkg]);
160
- if (ok) {
161
- result.installed.push(`brew:${pkg}`);
162
- }
163
- else {
164
- result.failed.push({ pkg: `brew:${pkg}`, error: stderr });
165
- }
166
- }
167
- }
168
- /**
169
- * Detect the best available Node.js package installer for globals
170
- * Prefers: bun > npm
171
- */
172
- async function detectNpmCommand() {
173
- const bunPath = await which("bun");
174
- if (bunPath) {
175
- return { cmd: bunPath, args: ["install", "-g"], label: "bun" };
176
- }
177
- const npmPath = await which("npm");
178
- if (npmPath) {
179
- return { cmd: npmPath, args: ["install", "-g"], label: "npm" };
180
- }
181
- return null;
182
- }
183
- /**
184
- * Install global Node.js packages (prefers bun > npm)
185
- */
186
- async function installNpmPackages(packages, result) {
187
- const installer = await detectNpmCommand();
188
- if (!installer) {
189
- for (const pkg of packages) {
190
- result.failed.push({ pkg: `npm:${pkg}`, error: "No bun or npm found" });
191
- }
192
- return;
193
- }
194
- for (const pkg of packages) {
195
- if (await isBinaryAvailable(pkg)) {
196
- result.skipped.push(`npm:${pkg}`);
197
- continue;
198
- }
199
- const { ok, stderr } = await run(installer.cmd, [...installer.args, pkg]);
200
- if (ok) {
201
- result.installed.push(`${installer.label}:${pkg}`);
202
- }
203
- else {
204
- result.failed.push({ pkg: `${installer.label}:${pkg}`, error: stderr });
205
- }
206
- }
207
- }
208
- /**
209
- * Install cargo packages
210
- */
211
- async function installCargoPackages(packages, result) {
212
- const cargoPath = await which("cargo");
213
- if (!cargoPath) {
214
- for (const pkg of packages) {
215
- result.failed.push({ pkg: `cargo:${pkg}`, error: "cargo not found" });
216
- }
217
- return;
218
- }
219
- for (const pkg of packages) {
220
- if (await isBinaryAvailable(pkg)) {
221
- result.skipped.push(`cargo:${pkg}`);
222
- continue;
223
- }
224
- const { ok, stderr } = await run(cargoPath, ["install", pkg], 300000);
225
- if (ok) {
226
- result.installed.push(`cargo:${pkg}`);
227
- }
228
- else {
229
- result.failed.push({ pkg: `cargo:${pkg}`, error: stderr });
230
- }
231
- }
232
- }
233
- /**
234
- * Extract binary name from a Go module path.
235
- * The binary name is the last path segment before @version.
236
- * e.g., "github.com/MadAppGang/tmux-mcp@latest" → "tmux-mcp"
237
- * "github.com/user/tool" → "tool"
238
- */
239
- export function extractGoBinaryName(pkg) {
240
- // Strip @version suffix first
241
- const withoutVersion = pkg.replace(/@[^/]*$/, "");
242
- return withoutVersion.split("/").pop() || pkg;
243
- }
244
- /**
245
- * Install Go packages via `go install`
246
- */
247
- async function installGoPackages(packages, result) {
248
- const goPath = await which("go");
249
- if (!goPath) {
250
- for (const pkg of packages) {
251
- result.failed.push({ pkg: `go:${pkg}`, error: "go not found" });
252
- }
253
- return;
254
- }
255
- for (const pkg of packages) {
256
- const binaryName = extractGoBinaryName(pkg);
257
- if (await isBinaryAvailable(binaryName)) {
258
- result.skipped.push(`go:${pkg}`);
259
- continue;
260
- }
261
- const { ok, stderr } = await run(goPath, ["install", pkg], 300000);
262
- if (ok) {
263
- result.installed.push(`go:${pkg}`);
264
- }
265
- else {
266
- result.failed.push({ pkg: `go:${pkg}`, error: stderr });
267
- }
268
- }
269
- }
270
- /**
271
- * Check required system binaries and report missing ones.
272
- * These are NOT installed by claudeup — the user must install them manually.
273
- */
274
- async function checkRequiredBinaries(binaries, result) {
275
- for (const name of binaries) {
276
- if (await isBinaryAvailable(name)) {
277
- result.skipped.push(`required:${name}`);
278
- }
279
- else {
280
- result.failed.push({
281
- pkg: `required:${name}`,
282
- error: `Required binary '${name}' not found in PATH. Please install it manually.`,
283
- });
284
- }
285
- }
286
- }
287
- /**
288
- * Read the setup config from a plugin's cached manifest
289
- */
290
- export async function getPluginSetupConfig(marketplace, pluginName) {
291
- const cacheDir = path.join(os.homedir(), ".claude", "plugins", "cache", marketplace);
292
- // Find the plugin directory (versioned)
293
- if (!(await fs.pathExists(cacheDir)))
294
- return null;
295
- const entries = await fs.readdir(cacheDir);
296
- // Look for the plugin name directory
297
- const pluginDir = entries.find((e) => e === pluginName);
298
- if (!pluginDir)
299
- return null;
300
- const pluginPath = path.join(cacheDir, pluginDir);
301
- const stat = await fs.stat(pluginPath);
302
- if (!stat.isDirectory())
303
- return null;
304
- // Find the version directory
305
- const versions = await fs.readdir(pluginPath);
306
- if (versions.length === 0)
307
- return null;
308
- // Use the latest version directory
309
- const latestVersion = versions.sort().pop();
310
- const manifestPath = path.join(pluginPath, latestVersion, "plugin.json");
311
- if (!(await fs.pathExists(manifestPath)))
312
- return null;
313
- try {
314
- const manifest = await fs.readJson(manifestPath);
315
- return manifest.setup || null;
316
- }
317
- catch {
318
- return null;
319
- }
320
- }
321
- /**
322
- * Read setup config from a local plugin directory (marketplace source)
323
- */
324
- export async function getPluginSetupFromSource(marketplace, pluginName) {
325
- const marketplaceDir = path.join(os.homedir(), ".claude", "plugins", "marketplaces", marketplace);
326
- if (!(await fs.pathExists(marketplaceDir)))
327
- return null;
328
- // Try common plugin locations
329
- for (const subdir of ["plugins", ""]) {
330
- const pluginJson = subdir
331
- ? path.join(marketplaceDir, subdir, pluginName, "plugin.json")
332
- : path.join(marketplaceDir, pluginName, "plugin.json");
333
- if (await fs.pathExists(pluginJson)) {
334
- try {
335
- const manifest = await fs.readJson(pluginJson);
336
- return manifest.setup || null;
337
- }
338
- catch {
339
- continue;
340
- }
341
- }
342
- }
343
- return null;
344
- }
345
- /**
346
- * Install all dependencies declared in a plugin's setup config.
347
- * Auto-detects available package managers.
348
- */
349
- export async function installPluginDeps(setup) {
350
- const result = {
351
- installed: [],
352
- skipped: [],
353
- failed: [],
354
- };
355
- if (setup.pip?.length) {
356
- await installPipPackages(setup.pip, result);
357
- }
358
- if (setup.brew?.length) {
359
- await installBrewPackages(setup.brew, result);
360
- }
361
- if (setup.npm?.length) {
362
- await installNpmPackages(setup.npm, result);
363
- }
364
- if (setup.cargo?.length) {
365
- await installCargoPackages(setup.cargo, result);
366
- }
367
- if (setup.go?.length) {
368
- await installGoPackages(setup.go, result);
369
- }
370
- if (setup.required?.length) {
371
- await checkRequiredBinaries(setup.required, result);
372
- }
373
- return result;
374
- }
375
- /**
376
- * Check which dependencies from a setup config are missing.
377
- * Returns a filtered SetupConfig with only missing deps.
378
- */
379
- export async function checkMissingDeps(setup) {
380
- const missing = {};
381
- if (setup.pip?.length) {
382
- const missingPip = [];
383
- for (const pkg of setup.pip) {
384
- if (!(await isPythonPackageInstalled(pkg))) {
385
- missingPip.push(pkg);
386
- }
387
- }
388
- if (missingPip.length > 0)
389
- missing.pip = missingPip;
390
- }
391
- if (setup.brew?.length) {
392
- const missingBrew = [];
393
- for (const pkg of setup.brew) {
394
- const bin = extractBinaryName(pkg);
395
- if (!(await isBinaryAvailable(bin))) {
396
- missingBrew.push(pkg);
397
- }
398
- }
399
- if (missingBrew.length > 0)
400
- missing.brew = missingBrew;
401
- }
402
- if (setup.npm?.length) {
403
- const missingNpm = [];
404
- for (const pkg of setup.npm) {
405
- if (!(await isBinaryAvailable(pkg))) {
406
- missingNpm.push(pkg);
407
- }
408
- }
409
- if (missingNpm.length > 0)
410
- missing.npm = missingNpm;
411
- }
412
- if (setup.cargo?.length) {
413
- const missingCargo = [];
414
- for (const pkg of setup.cargo) {
415
- if (!(await isBinaryAvailable(pkg))) {
416
- missingCargo.push(pkg);
417
- }
418
- }
419
- if (missingCargo.length > 0)
420
- missing.cargo = missingCargo;
421
- }
422
- if (setup.go?.length) {
423
- const missingGo = [];
424
- for (const pkg of setup.go) {
425
- const bin = extractGoBinaryName(pkg);
426
- if (!(await isBinaryAvailable(bin))) {
427
- missingGo.push(pkg);
428
- }
429
- }
430
- if (missingGo.length > 0)
431
- missing.go = missingGo;
432
- }
433
- if (setup.required?.length) {
434
- const missingRequired = [];
435
- for (const name of setup.required) {
436
- if (!(await isBinaryAvailable(name))) {
437
- missingRequired.push(name);
438
- }
439
- }
440
- if (missingRequired.length > 0)
441
- missing.required = missingRequired;
442
- }
443
- return missing;
444
- }
@@ -1,262 +0,0 @@
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
- import { getEnabledPlugins, getGlobalEnabledPlugins, getLocalEnabledPlugins, readInstalledPluginsRegistry, writeInstalledPluginsRegistry, } from "./claude-settings.js";
16
- const BUG_REPORT_URL = "https://github.com/anthropics/claude-code/issues/45997";
17
- // ── Detection ────────────────────────────────────────────────────────────────
18
- /**
19
- * Check for plugin version mismatches caused by the [0] index bug.
20
- *
21
- * For each enabled plugin in the current project:
22
- * 1. Reads the plugin's entry array from installed_plugins.json
23
- * 2. Finds the entry matching the current project
24
- * 3. Checks if entries[0] has a DIFFERENT version than the current project's entry
25
- * 4. If yes: this project will load the wrong version
26
- *
27
- * @param projectPath - The current project path to check
28
- * @returns Array of mismatch info for each affected plugin
29
- */
30
- export async function checkPluginVersionMismatches(projectPath) {
31
- const registry = await readInstalledPluginsRegistry();
32
- const enabledPluginIds = await collectEnabledPluginIds(projectPath);
33
- const mismatches = [];
34
- for (const pluginId of enabledPluginIds) {
35
- const entries = registry.plugins[pluginId];
36
- if (!entries || entries.length < 2)
37
- continue;
38
- // Find the entry for the current project
39
- const currentEntry = entries.find((e) => e.scope === "project" &&
40
- normalizePath(e.projectPath) === normalizePath(projectPath));
41
- if (!currentEntry)
42
- continue;
43
- const firstEntry = entries[0];
44
- // Compare the version at [0] with the current project's version
45
- if (firstEntry.version !== currentEntry.version) {
46
- mismatches.push({
47
- pluginId,
48
- firstEntryVersion: firstEntry.version,
49
- firstEntryProject: firstEntry.projectPath,
50
- currentProjectVersion: currentEntry.version,
51
- allEntries: entries,
52
- });
53
- }
54
- }
55
- return mismatches;
56
- }
57
- /**
58
- * Check a single plugin for version mismatch (used after plugin update).
59
- *
60
- * @param pluginId - The plugin ID to check
61
- * @param projectPath - The current project path
62
- * @returns Mismatch info or null if no mismatch
63
- */
64
- export async function checkSinglePluginMismatch(pluginId, projectPath) {
65
- const registry = await readInstalledPluginsRegistry();
66
- const entries = registry.plugins[pluginId];
67
- if (!entries || entries.length < 2)
68
- return null;
69
- const currentEntry = entries.find((e) => e.scope === "project" &&
70
- normalizePath(e.projectPath) === normalizePath(projectPath));
71
- if (!currentEntry)
72
- return null;
73
- const firstEntry = entries[0];
74
- if (firstEntry.version === currentEntry.version)
75
- return null;
76
- return {
77
- pluginId,
78
- firstEntryVersion: firstEntry.version,
79
- firstEntryProject: firstEntry.projectPath,
80
- currentProjectVersion: currentEntry.version,
81
- allEntries: entries,
82
- };
83
- }
84
- // ── Fix ──────────────────────────────────────────────────────────────────────
85
- /**
86
- * Fix a version mismatch by updating ALL entries for a plugin to use
87
- * the target version. This is a JSON-only operation that rewrites
88
- * installPath and version in each entry.
89
- *
90
- * IMPORTANT: This modifies installed_plugins.json which is Claude Code-owned.
91
- * Only call after explicit user consent.
92
- *
93
- * @param pluginId - The plugin ID to fix
94
- * @param targetVersion - The version to normalize all entries to
95
- * @returns Number of updated entries and which projects were affected
96
- */
97
- export async function fixPluginVersionMismatch(pluginId, targetVersion) {
98
- const registry = await readInstalledPluginsRegistry();
99
- const entries = registry.plugins[pluginId];
100
- if (!entries || entries.length === 0) {
101
- return { updated: 0, projects: [] };
102
- }
103
- // Find an existing entry with the target version to get the correct installPath
104
- const templateEntry = entries.find((e) => e.version === targetVersion);
105
- if (!templateEntry) {
106
- return { updated: 0, projects: [] };
107
- }
108
- const targetInstallPath = templateEntry.installPath;
109
- const updated = [];
110
- for (const entry of entries) {
111
- if (entry.version !== targetVersion) {
112
- entry.version = targetVersion;
113
- entry.installPath = targetInstallPath;
114
- entry.lastUpdated = new Date().toISOString();
115
- updated.push(entry.projectPath || "(global)");
116
- }
117
- }
118
- if (updated.length > 0) {
119
- await writeInstalledPluginsRegistry(registry);
120
- }
121
- return { updated: updated.length, projects: updated };
122
- }
123
- /**
124
- * Fix all version mismatches by normalizing each plugin to its latest version.
125
- *
126
- * @param mismatches - Array of mismatches from checkPluginVersionMismatches
127
- * @returns Map of pluginId to FixResult
128
- */
129
- export async function fixAllPluginVersionMismatches(mismatches) {
130
- const results = new Map();
131
- // Read registry once, apply all fixes, write once
132
- const registry = await readInstalledPluginsRegistry();
133
- for (const mismatch of mismatches) {
134
- const entries = registry.plugins[mismatch.pluginId];
135
- if (!entries || entries.length === 0) {
136
- results.set(mismatch.pluginId, { updated: 0, projects: [] });
137
- continue;
138
- }
139
- // Use the current project's version as the target (the version the user expects)
140
- const targetVersion = mismatch.currentProjectVersion;
141
- const templateEntry = entries.find((e) => e.version === targetVersion);
142
- if (!templateEntry) {
143
- results.set(mismatch.pluginId, { updated: 0, projects: [] });
144
- continue;
145
- }
146
- const targetInstallPath = templateEntry.installPath;
147
- const updated = [];
148
- for (const entry of entries) {
149
- if (entry.version !== targetVersion) {
150
- entry.version = targetVersion;
151
- entry.installPath = targetInstallPath;
152
- entry.lastUpdated = new Date().toISOString();
153
- updated.push(entry.projectPath || "(global)");
154
- }
155
- }
156
- results.set(mismatch.pluginId, {
157
- updated: updated.length,
158
- projects: updated,
159
- });
160
- }
161
- await writeInstalledPluginsRegistry(registry);
162
- return results;
163
- }
164
- // ── Formatting ───────────────────────────────────────────────────────────────
165
- /**
166
- * Format mismatch info for console output (prerunner).
167
- */
168
- export function formatMismatchWarning(mismatches) {
169
- const lines = [];
170
- lines.push("WARNING: Plugin version mismatch detected (Claude Code bug #45997)");
171
- lines.push("Claude Code loads the first entry from installed_plugins.json for ALL projects.");
172
- lines.push("The following plugins will load a different version than what this project has installed:\n");
173
- for (const m of mismatches) {
174
- const firstProject = m.firstEntryProject
175
- ? shortenPath(m.firstEntryProject)
176
- : "(global)";
177
- lines.push(` ${m.pluginId}:`);
178
- lines.push(` Loading v${m.firstEntryVersion} (from ${firstProject})`);
179
- lines.push(` Expected v${m.currentProjectVersion} (this project)`);
180
- }
181
- lines.push("");
182
- lines.push(`Bug report: ${BUG_REPORT_URL}`);
183
- lines.push("Run claudeup to fix — it can align all projects to the same version.");
184
- return lines.join("\n");
185
- }
186
- /**
187
- * Format mismatch info for TUI modal display.
188
- * Returns a multi-line string suitable for a modal message.
189
- */
190
- export function formatMismatchModal(mismatches) {
191
- const lines = [];
192
- lines.push("Your plugins will not work correctly in this project.");
193
- lines.push("");
194
- lines.push("Due to a Claude Code bug, the plugin loader ignores");
195
- lines.push("per-project versions and loads an older version instead.");
196
- lines.push("Commands, MCP tools, and skills will be outdated.");
197
- lines.push("");
198
- for (const m of mismatches) {
199
- const name = m.pluginId.split("@")[0];
200
- lines.push(` ${name}: v${m.firstEntryVersion} loaded, expected v${m.currentProjectVersion}`);
201
- }
202
- lines.push("");
203
- lines.push("Fix: align all projects to the same version.");
204
- lines.push("Details: github.com/anthropics/claude-code/issues/45997");
205
- return lines.join("\n");
206
- }
207
- // ── Helpers ──────────────────────────────────────────────────────────────────
208
- /**
209
- * Collect all enabled plugin IDs across all scopes for a project.
210
- */
211
- async function collectEnabledPluginIds(projectPath) {
212
- const pluginIds = new Set();
213
- try {
214
- const global = await getGlobalEnabledPlugins();
215
- for (const [id, enabled] of Object.entries(global)) {
216
- if (enabled)
217
- pluginIds.add(id);
218
- }
219
- }
220
- catch {
221
- /* skip unreadable */
222
- }
223
- try {
224
- const project = await getEnabledPlugins(projectPath);
225
- for (const [id, enabled] of Object.entries(project)) {
226
- if (enabled)
227
- pluginIds.add(id);
228
- }
229
- }
230
- catch {
231
- /* skip unreadable */
232
- }
233
- try {
234
- const local = await getLocalEnabledPlugins(projectPath);
235
- for (const [id, enabled] of Object.entries(local)) {
236
- if (enabled)
237
- pluginIds.add(id);
238
- }
239
- }
240
- catch {
241
- /* skip unreadable */
242
- }
243
- return pluginIds;
244
- }
245
- /**
246
- * Normalize a path for comparison (resolve and trailing-slash normalize).
247
- */
248
- function normalizePath(p) {
249
- if (!p)
250
- return "";
251
- return p.replace(/\/+$/, "");
252
- }
253
- /**
254
- * Shorten a path for display by replacing homedir with ~.
255
- */
256
- function shortenPath(p) {
257
- const home = process.env.HOME || process.env.USERPROFILE || "";
258
- if (home && p.startsWith(home)) {
259
- return `~${p.slice(home.length)}`;
260
- }
261
- return p;
262
- }