magus-cli 7.7.0 → 7.9.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 (140) hide show
  1. package/README.md +1 -1
  2. package/package.json +4 -4
  3. package/src/__tests__/blackbox-parity.test.ts +45 -26
  4. package/src/__tests__/blackbox-plugin-state.test.ts +25 -6
  5. package/src/__tests__/blind/plugin-deps/doctor.test.ts +622 -0
  6. package/src/__tests__/blind/plugin-deps/helpers.ts +340 -0
  7. package/src/__tests__/blind/plugin-deps/hook.test.ts +328 -0
  8. package/src/__tests__/blind/plugin-deps/probe.test.ts +694 -0
  9. package/src/__tests__/blind/plugin-deps/read-only.test.ts +96 -0
  10. package/src/__tests__/blind/plugin-deps/schema.test.ts +390 -0
  11. package/src/__tests__/blind-780-cli-tools.test.ts +796 -0
  12. package/src/__tests__/blind-780-deprecated.test.ts +631 -0
  13. package/src/__tests__/blind-780-failure-reasons.test.ts +1004 -0
  14. package/src/__tests__/blind-780-plugin-resolution.test.ts +431 -0
  15. package/src/__tests__/blind-780-review2.test.ts +1178 -0
  16. package/src/__tests__/cli-apply-seams.test.ts +2 -67
  17. package/src/__tests__/cli-tool-commands.test.ts +131 -6
  18. package/src/__tests__/cli-tools-adapter.test.ts +52 -2
  19. package/src/__tests__/cli-tools-plugin-managed.test.tsx +83 -0
  20. package/src/__tests__/cli-update-view.test.ts +3 -3
  21. package/src/__tests__/deprecated-remove.test.ts +827 -0
  22. package/src/__tests__/deps-adapter.test.ts +191 -0
  23. package/src/__tests__/deps-check-hook.test.ts +220 -0
  24. package/src/__tests__/deps-plan.test.ts +896 -0
  25. package/src/__tests__/deps-report.test.ts +197 -0
  26. package/src/__tests__/doctor-bins.test.ts +134 -36
  27. package/src/__tests__/doctor-json.test.ts +261 -0
  28. package/src/__tests__/doctor.test.ts +159 -1
  29. package/src/__tests__/failure-popup-review2.test.tsx +896 -0
  30. package/src/__tests__/failure-popup.test.tsx +419 -0
  31. package/src/__tests__/final-fixes-780.test.tsx +740 -0
  32. package/src/__tests__/fixtures/bins.ts +55 -0
  33. package/src/__tests__/format-update-outcome.test.ts +6 -32
  34. package/src/__tests__/gitignore-profile.test.ts +1 -9
  35. package/src/__tests__/install-plan.test.ts +15 -1
  36. package/src/__tests__/marketplace-refresh.test.ts +9 -3
  37. package/src/__tests__/models-screen-state.test.ts +18 -40
  38. package/src/__tests__/no-shell-spawn.test.ts +77 -0
  39. package/src/__tests__/npm-package-name.test.ts +6 -9
  40. package/src/__tests__/plugin-requires.test.ts +138 -153
  41. package/src/__tests__/plugin-resolution.test.ts +52 -13
  42. package/src/__tests__/plugin-row-text.test.ts +30 -37
  43. package/src/__tests__/profile-dependencies.test.ts +7 -0
  44. package/src/__tests__/profile-startup-old-format.test.ts +0 -3
  45. package/src/__tests__/profile-startup.test.ts +40 -12
  46. package/src/__tests__/requires-probe.test.ts +858 -0
  47. package/src/__tests__/resolver.test.ts +129 -52
  48. package/src/__tests__/run.test.ts +108 -0
  49. package/src/__tests__/stale-load-repair.test.ts +31 -23
  50. package/src/__tests__/tool-probe.test.ts +1 -1
  51. package/src/__tests__/toolchain.test.ts +123 -52
  52. package/src/__tests__/update-apply.test.ts +13 -29
  53. package/src/__tests__/update-plan.test.ts +24 -18
  54. package/src/__tests__/useStaleLoadModal.test.ts +1 -57
  55. package/src/__tests__/version-unknown-state.test.ts +15 -17
  56. package/src/cli/ansi.ts +2 -7
  57. package/src/cli/deps-view.ts +213 -0
  58. package/src/cli/doctor.ts +320 -54
  59. package/src/cli/install.ts +55 -71
  60. package/src/cli/profile.ts +20 -2
  61. package/src/cli/router.ts +6 -2
  62. package/src/cli/update-view.ts +58 -15
  63. package/src/cli/update.ts +55 -12
  64. package/src/data/cli-tools.ts +24 -9
  65. package/src/services/active-closure.ts +3 -3
  66. package/src/services/catalog-cache-store.ts +8 -6
  67. package/src/services/claude-cli.ts +106 -19
  68. package/src/services/claude-settings.ts +88 -384
  69. package/src/services/cli-tool-commands.ts +104 -3
  70. package/src/services/deps-plan.ts +962 -0
  71. package/src/services/deps-report.ts +265 -0
  72. package/src/services/doctor-bins.ts +115 -43
  73. package/src/services/install-plan.ts +11 -1
  74. package/src/services/local-marketplace.ts +12 -206
  75. package/src/services/marketplace-refresh.ts +4 -4
  76. package/src/services/plugin-env-needs.ts +69 -0
  77. package/src/services/plugin-manager.ts +35 -37
  78. package/src/services/plugin-requires.ts +93 -162
  79. package/src/services/plugin-resolution.ts +34 -42
  80. package/src/services/profile-activate.ts +1 -2
  81. package/src/services/profile-startup.ts +18 -43
  82. package/src/services/profile-state.ts +72 -2
  83. package/src/services/resolver.ts +263 -59
  84. package/src/services/tool-probe.ts +18 -25
  85. package/src/services/toolchain.ts +117 -111
  86. package/src/services/update-engine.ts +345 -45
  87. package/src/services/update-plan.ts +296 -36
  88. package/src/shared/plugin/hooks/bun-missing.sh +39 -0
  89. package/src/shared/plugin/hooks/deps-check.ts +224 -0
  90. package/src/shared/plugin/hooks/lib/requires-probe.ts +1183 -0
  91. package/src/types/index.ts +60 -16
  92. package/src/ui/App.tsx +81 -171
  93. package/src/ui/adapters/cliToolsAdapter.ts +252 -12
  94. package/src/ui/adapters/depsAdapter.ts +129 -0
  95. package/src/ui/adapters/failureReport.ts +137 -0
  96. package/src/ui/adapters/pluginsAdapter.ts +297 -45
  97. package/src/ui/adapters/startupReports.ts +260 -0
  98. package/src/ui/components/modals/FailureListModal.tsx +231 -0
  99. package/src/ui/components/modals/MessageModal.tsx +7 -12
  100. package/src/ui/components/modals/ModalContainer.tsx +10 -5
  101. package/src/ui/components/modals/index.ts +1 -0
  102. package/src/ui/hooks/startupQueue.ts +56 -0
  103. package/src/ui/hooks/useGitignoreModal.ts +30 -52
  104. package/src/ui/hooks/useProfileStartupModal.ts +119 -142
  105. package/src/ui/hooks/useStaleLoadModal.ts +35 -65
  106. package/src/ui/keymap.ts +14 -1
  107. package/src/ui/renderers/cliToolRenderers.tsx +135 -2
  108. package/src/ui/renderers/mcpRenderers.tsx +110 -3
  109. package/src/ui/renderers/pluginRenderers.tsx +77 -6
  110. package/src/ui/renderers/pluginRowText.ts +25 -25
  111. package/src/ui/renderers/pluginStatusText.ts +18 -1
  112. package/src/ui/screens/AliasScreen.tsx +38 -45
  113. package/src/ui/screens/CliToolsScreen.tsx +199 -98
  114. package/src/ui/screens/EnvVarsScreen.tsx +17 -5
  115. package/src/ui/screens/GitignoreScreen.tsx +48 -67
  116. package/src/ui/screens/McpRegistryScreen.tsx +67 -38
  117. package/src/ui/screens/McpScreen.tsx +46 -19
  118. package/src/ui/screens/ModelsScreen.tsx +46 -86
  119. package/src/ui/screens/PluginsScreen.tsx +315 -259
  120. package/src/ui/screens/ProfilesScreen.tsx +79 -40
  121. package/src/ui/screens/SkillsScreen.tsx +83 -80
  122. package/src/ui/screens/StylesScreen.tsx +211 -275
  123. package/src/ui/state/AppContext.tsx +66 -44
  124. package/src/ui/state/DimensionsContext.tsx +3 -22
  125. package/src/ui/state/reducer.ts +12 -100
  126. package/src/ui/state/types.ts +48 -69
  127. package/src/utils/command-utils.ts +4 -5
  128. package/src/utils/run.ts +123 -52
  129. package/src/utils/string-utils.ts +9 -0
  130. package/src/utils/user-path.ts +60 -0
  131. package/src/__tests__/go-module.test.ts +0 -43
  132. package/src/__tests__/notices-rules.test.ts +0 -476
  133. package/src/__tests__/notices.test.tsx +0 -604
  134. package/src/__tests__/plugin-setup.test.ts +0 -1016
  135. package/src/__tests__/shell-script-callers.test.ts +0 -92
  136. package/src/__tests__/styles-status-line.test.ts +0 -72
  137. package/src/__tests__/version-snapshot.test.ts +0 -212
  138. package/src/services/plugin-setup.ts +0 -965
  139. package/src/services/version-snapshot.ts +0 -189
  140. package/src/utils/go-module.ts +0 -36
@@ -162,9 +162,6 @@ function getGitRemote(marketplacePath: string): string | undefined {
162
162
  }
163
163
  }
164
164
 
165
- /**
166
- * Scan a single marketplace directory and return marketplace info
167
- */
168
165
  /**
169
166
  * Names of the skills under `skillsDir`, recursing through grouping directories.
170
167
  *
@@ -260,6 +257,11 @@ export async function scanMcpServers(pluginPath: string): Promise<string[]> {
260
257
  return found;
261
258
  }
262
259
 
260
+ /**
261
+ * Read one marketplace clone: its manifest, plus the component counts of each
262
+ * plugin whose source directory is on disk. Read-only — the manifest and the
263
+ * clone belong to Claude Code, and nothing here writes either.
264
+ */
263
265
  async function scanSingleMarketplace(
264
266
  marketplacePath: string,
265
267
  marketplaceName: string,
@@ -277,8 +279,6 @@ async function scanSingleMarketplace(
277
279
  const gitRepo = getGitRemote(marketplacePath);
278
280
 
279
281
  const plugins: LocalMarketplacePlugin[] = [];
280
- const validManifestPlugins: typeof manifest.plugins = [];
281
- let manifestModified = false;
282
282
 
283
283
  if (manifest.plugins && Array.isArray(manifest.plugins)) {
284
284
  for (const plugin of manifest.plugins) {
@@ -292,18 +292,13 @@ async function scanSingleMarketplace(
292
292
  const sourceStr =
293
293
  typeof plugin.source === "string" ? plugin.source : null;
294
294
 
295
- if (sourceStr) {
296
- const pluginPath = path.join(
297
- marketplacePath,
298
- sourceStr.replace("./", ""),
299
- );
300
-
301
- // Remove plugins whose source directory doesn't exist
302
- if (!(await fs.pathExists(pluginPath))) {
303
- manifestModified = true;
304
- continue;
305
- }
306
-
295
+ // A plugin whose source directory is not on disk is still listed: the
296
+ // manifest is Claude Code's, and a scan reads it, never rewrites it.
297
+ // Its component lists stay empty because there is nothing to count.
298
+ const pluginPath = sourceStr
299
+ ? path.join(marketplacePath, sourceStr.replace("./", ""))
300
+ : null;
301
+ if (pluginPath && (await fs.pathExists(pluginPath))) {
307
302
  try {
308
303
  // Scan for agents
309
304
  const agentsDir = path.join(pluginPath, "agents");
@@ -330,7 +325,6 @@ async function scanSingleMarketplace(
330
325
  }
331
326
  }
332
327
 
333
- validManifestPlugins.push(plugin);
334
328
  plugins.push({
335
329
  name: plugin.name,
336
330
  version: plugin.version || "0.0.0",
@@ -349,12 +343,6 @@ async function scanSingleMarketplace(
349
343
  }
350
344
  }
351
345
 
352
- // Update marketplace.json if we removed invalid plugins
353
- if (manifestModified) {
354
- manifest.plugins = validManifestPlugins;
355
- await fs.writeJson(manifestPath, manifest, { spaces: 2 });
356
- }
357
-
358
346
  return {
359
347
  name: manifest.name || marketplaceName,
360
348
  description: manifest.description || manifest.metadata?.description || "",
@@ -466,13 +454,6 @@ export async function hasLocalMarketplace(name: string): Promise<boolean> {
466
454
  return fs.pathExists(manifestPath);
467
455
  }
468
456
 
469
- export interface RefreshResult {
470
- name: string;
471
- success: boolean;
472
- updated: boolean;
473
- error?: string;
474
- }
475
-
476
457
  export interface RefreshProgress {
477
458
  current: number;
478
459
  total: number;
@@ -480,178 +461,3 @@ export interface RefreshProgress {
480
461
  }
481
462
 
482
463
  export type ProgressCallback = (progress: RefreshProgress) => void;
483
-
484
- export interface RepairResult {
485
- repaired: boolean;
486
- pluginName: string;
487
- added: {
488
- agents: string[];
489
- commands: string[];
490
- skills: string[];
491
- };
492
- }
493
-
494
- /**
495
- * Repair a plugin's plugin.json by adding missing agents/commands/skills arrays
496
- * based on what files exist on disk.
497
- *
498
- * This fixes plugins where the author forgot to add the arrays to plugin.json,
499
- * causing Claude Code to not load the agents/commands even though files exist.
500
- */
501
- export async function repairPluginJson(
502
- pluginPath: string,
503
- ): Promise<RepairResult> {
504
- const pluginJsonPath = path.join(pluginPath, ".claude-plugin", "plugin.json");
505
- const pluginName = path.basename(pluginPath);
506
-
507
- const result: RepairResult = {
508
- repaired: false,
509
- pluginName,
510
- added: { agents: [], commands: [], skills: [] },
511
- };
512
-
513
- if (!(await fs.pathExists(pluginJsonPath))) {
514
- return result;
515
- }
516
-
517
- try {
518
- const pluginJson = await fs.readJson(pluginJsonPath);
519
- let modified = false;
520
-
521
- // Scan and add agents if array is missing
522
- const agentsDir = path.join(pluginPath, "agents");
523
- if (await fs.pathExists(agentsDir)) {
524
- const files = (await fs.readdir(agentsDir))
525
- .filter((f) => f.endsWith(".md"))
526
- .map((f) => `./agents/${f}`);
527
-
528
- if (files.length > 0 && !pluginJson.agents) {
529
- pluginJson.agents = files;
530
- result.added.agents = files;
531
- modified = true;
532
- }
533
- }
534
-
535
- // Scan and add commands if array is missing
536
- const commandsDir = path.join(pluginPath, "commands");
537
- if (await fs.pathExists(commandsDir)) {
538
- const files = (await fs.readdir(commandsDir))
539
- .filter((f) => f.endsWith(".md"))
540
- .map((f) => `./commands/${f}`);
541
-
542
- if (files.length > 0 && !pluginJson.commands) {
543
- pluginJson.commands = files;
544
- result.added.commands = files;
545
- modified = true;
546
- }
547
- }
548
-
549
- // Scan and add skills if array is missing
550
- const skillsDir = path.join(pluginPath, "skills");
551
- if (await fs.pathExists(skillsDir)) {
552
- const entries = await fs.readdir(skillsDir, { withFileTypes: true });
553
- const skills = entries
554
- .filter((e) => e.isDirectory() || e.name.endsWith(".md"))
555
- .map((e) => `./skills/${e.name}`);
556
-
557
- if (skills.length > 0 && !pluginJson.skills) {
558
- pluginJson.skills = skills;
559
- result.added.skills = skills;
560
- modified = true;
561
- }
562
- }
563
-
564
- if (modified) {
565
- await fs.writeJson(pluginJsonPath, pluginJson, { spaces: 2 });
566
- result.repaired = true;
567
- }
568
- } catch {
569
- // Ignore repair errors for individual plugins
570
- }
571
-
572
- return result;
573
- }
574
-
575
- export interface RepairMarketplaceResult {
576
- marketplace: string;
577
- repaired: RepairResult[];
578
- }
579
-
580
- /**
581
- * Repair all plugins in a marketplace
582
- */
583
- export async function repairMarketplacePlugins(
584
- marketplaceName: string,
585
- ): Promise<RepairMarketplaceResult> {
586
- const result: RepairMarketplaceResult = {
587
- marketplace: marketplaceName,
588
- repaired: [],
589
- };
590
-
591
- const marketplacePath = path.join(CLAUDE_PLUGINS_DIR, marketplaceName);
592
- const manifestPath = path.join(
593
- marketplacePath,
594
- ".claude-plugin",
595
- "marketplace.json",
596
- );
597
-
598
- if (!(await fs.pathExists(manifestPath))) {
599
- return result;
600
- }
601
-
602
- try {
603
- const manifest = await fs.readJson(manifestPath);
604
-
605
- if (manifest.plugins && Array.isArray(manifest.plugins)) {
606
- for (const plugin of manifest.plugins) {
607
- if (plugin.source) {
608
- const pluginPath = path.join(
609
- marketplacePath,
610
- plugin.source.replace("./", ""),
611
- );
612
- const repairResult = await repairPluginJson(pluginPath);
613
-
614
- if (repairResult.repaired) {
615
- result.repaired.push(repairResult);
616
- }
617
- }
618
- }
619
- }
620
- } catch {
621
- // Ignore errors
622
- }
623
-
624
- return result;
625
- }
626
-
627
- /**
628
- * Repair all plugins in all marketplaces
629
- */
630
- export async function repairAllMarketplaces(): Promise<
631
- RepairMarketplaceResult[]
632
- > {
633
- const results: RepairMarketplaceResult[] = [];
634
-
635
- if (!(await fs.pathExists(CLAUDE_PLUGINS_DIR))) {
636
- return results;
637
- }
638
-
639
- try {
640
- const entries = await fs.readdir(CLAUDE_PLUGINS_DIR, {
641
- withFileTypes: true,
642
- });
643
-
644
- for (const entry of entries) {
645
- if (!entry.isDirectory()) continue;
646
-
647
- const repairResult = await repairMarketplacePlugins(entry.name);
648
- if (repairResult.repaired.length > 0) {
649
- results.push(repairResult);
650
- }
651
- }
652
- } catch {
653
- // Return empty on error
654
- }
655
-
656
- return results;
657
- }
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Keep on-disk marketplace clones current — safely.
3
3
  *
4
- * The plugin *list* magus shows is fetched fresh from GitHub, but a plugin's
5
- * `setup` block and Claude Code's own installer read the on-disk marketplace
6
- * clone (~/.claude/plugins/marketplaces/<name>). Nothing else keeps that clone
4
+ * The plugin *list* magus shows is fetched fresh from GitHub, but the
5
+ * `requires` of a plugin not installed yet and Claude Code's own installer read
6
+ * the on-disk marketplace clone (~/.claude/plugins/marketplaces/<name>). Nothing else keeps that clone
7
7
  * current — recoverMarketplaceSettings only pulls entries it had to re-register —
8
8
  * so a healthy marketplace's clone goes stale and its plugin metadata (e.g. a
9
- * pinned dependency version in a plugin's setup) is never seen, even while the
9
+ * dependency pin in a plugin's `requires`) is never seen, even while the
10
10
  * displayed version looks up to date.
11
11
  *
12
12
  * IMPORTANT: this refreshes the clone with a NON-DESTRUCTIVE `git pull --ff-only`,
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Which variables a plugin's MCP server needs and does not have yet.
3
+ *
4
+ * DERIVED FROM CURRENT CONFIG, every time: the plugin's own MCP config names
5
+ * the `${VAR}` references, and a variable counts as set when this project's
6
+ * MCP variables hold it or magus's environment carries it. Nothing records
7
+ * that the user declined to set them — so the Plugins screen's "needs N
8
+ * settings" state disappears on its own the moment they are set, however
9
+ * they were set.
10
+ *
11
+ * One function for both the install flow (which asks for exactly these) and
12
+ * the detail panel (which states them), so the two cannot disagree.
13
+ */
14
+
15
+ import { getMcpEnvVars } from "./claude-settings.js";
16
+ import {
17
+ type PluginEnvRequirement,
18
+ getPluginEnvRequirements,
19
+ getPluginSourcePath,
20
+ } from "./plugin-mcp-config.js";
21
+
22
+ /** The reads, injectable so a test needs no marketplace clone on disk. */
23
+ export interface PluginEnvNeedsDeps {
24
+ sourcePath: (
25
+ marketplace: string,
26
+ pluginName: string,
27
+ ) => Promise<string | undefined>;
28
+ requirements: (
29
+ marketplace: string,
30
+ pluginSource: string,
31
+ ) => Promise<PluginEnvRequirement[]>;
32
+ configured: (projectPath?: string) => Promise<Record<string, string>>;
33
+ }
34
+
35
+ const REAL_DEPS: PluginEnvNeedsDeps = {
36
+ sourcePath: getPluginSourcePath,
37
+ requirements: getPluginEnvRequirements,
38
+ configured: getMcpEnvVars,
39
+ };
40
+
41
+ /**
42
+ * The variables `pluginName`'s MCP servers reference that are set nowhere —
43
+ * empty when it has no MCP server, needs nothing, or has everything.
44
+ *
45
+ * Throws when the project's variables cannot be read (a settings file that
46
+ * does not parse): "needs nothing" would be a guess.
47
+ */
48
+ export async function missingPluginEnvVars(
49
+ marketplace: string,
50
+ pluginName: string,
51
+ projectPath?: string,
52
+ env: Readonly<Record<string, string | undefined>> = process.env,
53
+ deps: PluginEnvNeedsDeps = REAL_DEPS,
54
+ ): Promise<PluginEnvRequirement[]> {
55
+ const source = await deps.sourcePath(marketplace, pluginName);
56
+ if (!source) return [];
57
+ const requirements = await deps.requirements(marketplace, source);
58
+ if (requirements.length === 0) return [];
59
+ const configured = await deps.configured(projectPath);
60
+ return requirements.filter((req) => !configured[req.name] && !env[req.name]);
61
+ }
62
+
63
+ /**
64
+ * The detail panel's words for a plugin whose MCP server lacks variables:
65
+ * what is missing, then the one key that sets them.
66
+ */
67
+ export function mcpSettingsNeededText(count: number): string {
68
+ return `its MCP server needs ${count} ${count === 1 ? "setting" : "settings"} — press e to set ${count === 1 ? "it" : "them"}`;
69
+ }
@@ -33,8 +33,6 @@ import { inheritablePaths } from "./git-worktree.js";
33
33
  import {
34
34
  type LocalMarketplace,
35
35
  type ProgressCallback,
36
- type RepairMarketplaceResult,
37
- repairAllMarketplaces,
38
36
  scanLocalMarketplaces,
39
37
  } from "./local-marketplace.js";
40
38
  import {
@@ -162,17 +160,6 @@ export interface PluginInfo {
162
160
  * a reinstall delivers the current content. See content-drift.ts.
163
161
  */
164
162
  contentStale?: boolean;
165
- /**
166
- * Set when this plugin's installed version changed since magus last
167
- * rendered it — including updates made by Claude Code or a direct CLI call
168
- * rather than by magus itself. Drives the "updated" badge.
169
- */
170
- recentlyUpdatedFrom?: string;
171
- /**
172
- * Set when magus is seeing this plugin installed for the first time.
173
- * Never set on the very first run, when there is no baseline to compare to.
174
- */
175
- recentlyInstalled?: boolean;
176
163
  /**
177
164
  * Which catalog supplied `version` — i.e. the number this plugin's installed
178
165
  * version was compared against.
@@ -389,6 +376,28 @@ export function resolveScopeAction(
389
376
  return behind ? "update" : "uninstall";
390
377
  }
391
378
 
379
+ /**
380
+ * The plugin ids the Plugins list may show as installed-but-unlisted rows.
381
+ *
382
+ * Installed means a key in this project's `enabledPlugins`, or registry rows.
383
+ * A version in `installedPluginVersions` with neither behind it is not an
384
+ * install: Claude Code never writes that field, so such a claim is a leftover
385
+ * of magus's own, and building a row from it showed a plugin nothing had
386
+ * installed — the settings key treated as authority, which it never is.
387
+ */
388
+ export function orphanCandidateIds(
389
+ enabledPlugins: Readonly<Record<string, boolean>>,
390
+ installedVersions: Readonly<Record<string, string>>,
391
+ registryPlugins: InstalledPluginsRegistry["plugins"] | undefined,
392
+ ): Set<string> {
393
+ return new Set([
394
+ ...Object.keys(enabledPlugins),
395
+ ...Object.keys(installedVersions).filter(
396
+ (id) => (registryPlugins?.[id]?.length ?? 0) > 0,
397
+ ),
398
+ ]);
399
+ }
400
+
392
401
  export async function getAvailablePlugins(
393
402
  projectPath?: string,
394
403
  ): Promise<PluginInfo[]> {
@@ -402,8 +411,9 @@ export async function getAvailablePlugins(
402
411
  // ported in services/plugin-resolution.ts. Resolved once for the whole
403
412
  // registry and carried alongside `installedVersion` on every row below; where
404
413
  // the two disagree, the classifier calls it `stale-load`.
414
+ const registry = await readInstalledPluginsRegistry();
405
415
  const loadedVersions = resolveLoadedVersions(
406
- await readInstalledPluginsRegistry(),
416
+ registry,
407
417
  projectPath ?? process.cwd(),
408
418
  );
409
419
  const loadedVersionOf = (pluginId: string): string | null =>
@@ -568,13 +578,13 @@ export async function getAvailablePlugins(
568
578
  }
569
579
  }
570
580
 
571
- // Add orphaned plugins (in this project's settings or registry, but not in any cache)
572
- const allPluginIds = new Set([
573
- ...Object.keys(enabledPlugins),
574
- ...Object.keys(installedVersions),
575
- ]);
576
-
577
- for (const pluginId of allPluginIds) {
581
+ // Add orphaned plugins (in this project's settings or registry, but not in
582
+ // any cache) — see `orphanCandidateIds` for what counts as installed.
583
+ for (const pluginId of orphanCandidateIds(
584
+ enabledPlugins,
585
+ installedVersions,
586
+ registry.plugins,
587
+ )) {
578
588
  if (seenPluginIds.has(pluginId)) continue;
579
589
 
580
590
  const parsed = parsePluginId(pluginId);
@@ -1031,18 +1041,14 @@ async function autoSyncIfStale(
1031
1041
  return localMarketplaces;
1032
1042
  }
1033
1043
 
1034
- export interface RefreshAndRepairResult {
1035
- refresh: never[];
1036
- repair: RepairMarketplaceResult[];
1037
- }
1038
-
1039
1044
  /**
1040
- * Refresh magus's internal cache
1041
- * Note: Marketplace updates should be done via Claude Code's /plugin marketplace update command
1045
+ * Refresh magus's internal cache. Only magus's own caches are cleared: the
1046
+ * marketplace clones are Claude Code's, and refreshing them is
1047
+ * `claude plugin marketplace update`.
1042
1048
  */
1043
1049
  export async function refreshAllMarketplaces(
1044
1050
  onProgress?: ProgressCallback,
1045
- ): Promise<RefreshAndRepairResult> {
1051
+ ): Promise<void> {
1046
1052
  onProgress?.({ current: 1, total: 1, name: "Clearing cache..." });
1047
1053
 
1048
1054
  // Clear all caches to force fresh data. The on-disk catalog cache is awaited
@@ -1050,14 +1056,6 @@ export async function refreshAllMarketplaces(
1050
1056
  // entries this is discarding.
1051
1057
  clearMarketplaceCache();
1052
1058
  await clearPersistedCatalogs();
1053
-
1054
- // Auto-repair plugin.json files with missing agents/commands/skills
1055
- const repairResults = await repairAllMarketplaces();
1056
-
1057
- return {
1058
- refresh: [],
1059
- repair: repairResults,
1060
- };
1061
1059
  }
1062
1060
 
1063
1061
  // Re-export types for consumers