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
@@ -35,12 +35,14 @@ import type { MarketplacePlugin } from "./marketplace-catalog.js";
35
35
  /**
36
36
  * How long a cached catalog is served before we ask again.
37
37
  *
38
- * This is the ONLY refresh rhythm magus has, and it is why the TUI and
39
- * `magus update` can disagree about the same plugin for up to an hour:
40
- * `update` fast-forwards the clones and clears this cache before planning,
41
- * while the TUI serves whatever is here. That is not a bug in either path, but
42
- * anything that reports "no update" off a cached catalog has to say the answer
43
- * is up to an hour old.
38
+ * This is the ONLY refresh rhythm magus has, and it applies to both surfaces:
39
+ * `magus update` and the TUI read a catalog written here within the hour
40
+ * before they go to the network. `update` pulls the marketplace clones and
41
+ * drops its in-process memo before planning, but it does NOT clear this file,
42
+ * so a release published inside the TTL is not seen by `update` either. Only
43
+ * an explicit refresh (`r` in the TUI) clears it. Anything that reports "no
44
+ * update" — or decides a plugin is no longer published — off a cached catalog
45
+ * is working from an answer up to an hour old, by design.
44
46
  */
45
47
  export const CATALOG_TTL_MS = 60 * 60 * 1000;
46
48
 
@@ -10,6 +10,8 @@
10
10
  */
11
11
 
12
12
  import { execFile } from "node:child_process";
13
+ import { existsSync } from "node:fs";
14
+ import path from "node:path";
13
15
  import { promisify } from "node:util";
14
16
  import { defaultMarketplaces } from "../data/marketplaces.js";
15
17
  import { which } from "../utils/command-utils.js";
@@ -28,13 +30,12 @@ export type PluginScope = "user" | "project" | "local";
28
30
  * Get the path to the claude CLI binary
29
31
  * @throws Error if claude CLI is not found in PATH
30
32
  */
33
+ const CLAUDE_NOT_FOUND =
34
+ "claude CLI not found in PATH. Install with: npm install -g @anthropic-ai/claude-code";
35
+
31
36
  async function getClaudePath(): Promise<string> {
32
37
  const claudePath = await which("claude");
33
- if (!claudePath) {
34
- throw new Error(
35
- "claude CLI not found in PATH. Install with: npm install -g @anthropic-ai/claude-code",
36
- );
37
- }
38
+ if (!claudePath) throw new Error(CLAUDE_NOT_FOUND);
38
39
  return claudePath;
39
40
  }
40
41
 
@@ -42,29 +43,84 @@ async function getClaudePath(): Promise<string> {
42
43
  * Execute a claude CLI command and return stdout
43
44
  * @param args - Arguments to pass to claude
44
45
  * @param timeoutMs - Timeout in milliseconds (default: 30s)
46
+ * @param cwd - Working directory; Claude Code resolves project and local
47
+ * scope from it. Defaults to this process's.
45
48
  * @returns stdout from the command
46
49
  */
47
- async function execClaude(args: string[], timeoutMs = 30000): Promise<string> {
50
+ async function execClaude(
51
+ args: string[],
52
+ timeoutMs = 30000,
53
+ cwd?: string,
54
+ ): Promise<string> {
48
55
  const claudePath = await getClaudePath();
49
56
  try {
50
57
  const { stdout } = await execFileAsync(claudePath, args, {
51
58
  timeout: timeoutMs,
59
+ ...(cwd ? { cwd } : {}),
52
60
  });
53
61
  return stdout.trim();
54
62
  } catch (error: unknown) {
55
- const execError = error as {
56
- stderr?: string;
57
- stdout?: string;
58
- message?: string;
59
- code?: number;
60
- };
61
- const msg =
62
- execError.stderr?.trim() ||
63
- execError.stdout?.trim() ||
64
- execError.message ||
65
- "claude command failed";
66
- throw new Error(msg);
63
+ throw new Error(claudeFailureMessage(error, args, timeoutMs, cwd));
64
+ }
65
+ }
66
+
67
+ /**
68
+ * What a failed `claude` command says, in the words a user reads.
69
+ *
70
+ * Claude Code's own output first — stderr, then stdout — since that is the
71
+ * reason. Only when it printed nothing does Node's error message stand in, and
72
+ * that one opens with `Command failed: /absolute/path/to/claude plugin …`:
73
+ * the path and the argument list are the mechanism, not the reason, so that
74
+ * line is dropped and what follows it kept. A run that was killed for taking
75
+ * too long says so.
76
+ *
77
+ * A spawn that fails with ENOENT is not always a missing `claude`: Node and
78
+ * Bun both report a working directory that does not exist the same way,
79
+ * naming the binary. So when the run was given a `cwd` that is gone, that is
80
+ * what is said — by the folder's name, never its absolute path — rather than
81
+ * telling someone with Claude Code installed to install it.
82
+ */
83
+ export function claudeFailureMessage(
84
+ error: unknown,
85
+ args: readonly string[],
86
+ timeoutMs: number,
87
+ cwd?: string,
88
+ ): string {
89
+ const execError = error as {
90
+ code?: string | number;
91
+ stderr?: string;
92
+ stdout?: string;
93
+ message?: string;
94
+ killed?: boolean;
95
+ signal?: string | null;
96
+ };
97
+ const output = execError.stderr?.trim() || execError.stdout?.trim();
98
+ if (output) return output;
99
+ if (execError.killed) {
100
+ return `claude ${args.slice(0, 2).join(" ")} did not finish within ${Math.round(timeoutMs / 1000)}s`;
101
+ }
102
+ const message = execError.message ?? "";
103
+ // The binary vanished between the PATH lookup and the spawn (Node says
104
+ // `spawn <path> ENOENT`, Bun `Executable not found in $PATH`). Same fact as
105
+ // the lookup failing, so the same words — and never the absolute path.
106
+ if (
107
+ execError.code === "ENOENT" ||
108
+ /\bENOENT\b|Executable not found in \$PATH/.test(message)
109
+ ) {
110
+ if (cwd && !existsSync(cwd)) {
111
+ return `the folder ${path.basename(cwd) || cwd} no longer exists`;
112
+ }
113
+ return CLAUDE_NOT_FOUND;
114
+ }
115
+ if (execError.code === "EACCES" || /\bEACCES\b/.test(message)) {
116
+ return "claude could not be started: permission denied";
67
117
  }
118
+ const rest = message
119
+ .replace(/^Command failed: [^\n]*\n?/, "")
120
+ // Any leftover mention of the binary by absolute path is mechanism.
121
+ .replace(/(?:\/[^\s/:]+)+\/claude\b/g, "claude")
122
+ .trim();
123
+ return rest || `claude ${args.slice(0, 2).join(" ")} failed`;
68
124
  }
69
125
 
70
126
  /**
@@ -186,6 +242,32 @@ export async function uninstallPlugin(
186
242
  }
187
243
  }
188
244
 
245
+ /**
246
+ * Uninstall through `claude plugin uninstall` and NOTHING else: its error is
247
+ * thrown, never absorbed.
248
+ *
249
+ * {@link uninstallPlugin} falls back to editing the settings files directly
250
+ * when the command fails. That fallback is a direct writer of Claude Code's
251
+ * files, and it turns every failure into an apparent success — so a caller
252
+ * that reads the result back, as removing a deprecated plugin does, could
253
+ * never see why the plugin was still there. This one lets the command's own
254
+ * words reach the caller.
255
+ *
256
+ * Runs in `projectPath` when given, because Claude Code resolves project and
257
+ * local scope from its working directory.
258
+ */
259
+ export async function uninstallPluginViaCli(
260
+ pluginId: string,
261
+ scope: PluginScope,
262
+ projectPath?: string,
263
+ ): Promise<void> {
264
+ await execClaude(
265
+ ["plugin", "uninstall", pluginId, "--scope", scope],
266
+ 30000,
267
+ projectPath,
268
+ );
269
+ }
270
+
189
271
  /**
190
272
  * Repair a plugin whose files drifted from the marketplace under an unchanged
191
273
  * version — uninstall, then install again at the same scope.
@@ -270,7 +352,12 @@ export async function disablePlugin(
270
352
  * {@link installPlugin}, and content drift under an unchanged version still
271
353
  * needs {@link repairPlugin} — `update` has no version bump to act on there.
272
354
  *
273
- * Retries with marketplace update on "not found" errors (same as installPlugin).
355
+ * On a "not found in marketplace" error it recovers once and retries, exactly
356
+ * as {@link installPlugin} does (see `recoverMissingMarketplace`): a registered
357
+ * marketplace has its source repaired and its clone fast-forwarded with
358
+ * `git pull --ff-only` — never `claude plugin marketplace update`, which
359
+ * deletes the clone before re-cloning — and a missing clone or an unregistered
360
+ * default marketplace is added with `claude plugin marketplace add`.
274
361
  */
275
362
  export async function updatePlugin(
276
363
  pluginId: string,