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
@@ -7,11 +7,13 @@ import {
7
7
  readModelsConfig,
8
8
  readModelsStatus,
9
9
  } from "../../services/models-manager.js";
10
+ import { failureReport } from "../adapters/failureReport.js";
10
11
  import {
11
12
  type ModelsBrowserItem,
12
13
  buildModelsItems,
13
14
  firstSelectableIndex,
14
15
  } from "../adapters/modelsAdapter.js";
16
+ import { stepFailureReport } from "../adapters/startupReports.js";
15
17
  import { EmptyFilterState } from "../components/EmptyFilterState.js";
16
18
  import { ScrollableList } from "../components/ScrollableList.js";
17
19
  import { ScreenLayout } from "../components/layout/index.js";
@@ -23,7 +25,7 @@ import {
23
25
  renderModelRow,
24
26
  renderModelsSummary,
25
27
  } from "../renderers/modelRenderers.js";
26
- import { useApp } from "../state/AppContext.js";
28
+ import { useApp, useFailurePopup } from "../state/AppContext.js";
27
29
  import { useDimensions } from "../state/DimensionsContext.js";
28
30
  import { asyncValue } from "../state/types.js";
29
31
  import { theme } from "../theme.js";
@@ -134,22 +136,15 @@ export function ModelsScreen() {
134
136
  };
135
137
  }, [dispatch]);
136
138
 
137
- // ── Status line ───────────────────────────────────────────────────────────
139
+ // ── Feedback ──────────────────────────────────────────────────────────────
138
140
 
139
141
  /**
140
- * Set the status line.
141
- *
142
- * No timer, matching the Styles screen: a message that erased itself after a
143
- * few seconds was usually gone by the time anyone looked. It is cleared when
144
- * the NEXT action starts instead.
142
+ * No status line and no messages. A success shows as the screen's state —
143
+ * the reloaded list, "Model tiers: on", "writing…" while a write runs. What
144
+ * did not happen goes into the one failure popup, with why and, when there
145
+ * is one, what to do. A key pressed where it does nothing does nothing.
145
146
  */
146
- const showStatus = useCallback(
147
- (text: string, tone: "success" | "error" = "success") => {
148
- dispatch({ type: "MODELS_STATUS_SET", status: { text, tone } });
149
- },
150
- [dispatch],
151
- );
152
- const statusMsg = modelsState.status;
147
+ const showFailures = useFailurePopup();
153
148
 
154
149
  // ── Derived ───────────────────────────────────────────────────────────────
155
150
 
@@ -189,18 +184,17 @@ export function ModelsScreen() {
189
184
  *
190
185
  * No `ensureManifest` is passed. That helper prompts on the terminal, and
191
186
  * this process's terminal belongs to OpenTUI — a project with no profiles
192
- * gets a plain `.claude/models.json`, and the status line says so.
187
+ * gets a plain `.claude/models.json`.
188
+ *
189
+ * A press while a write is in flight does nothing: the header already says
190
+ * "writing…". Neither does one with no preset under the cursor — the only
191
+ * other row is the status header, which the cursor never lands on, so that
192
+ * is an empty filtered list.
193
193
  */
194
194
  const handleApply = useCallback(async () => {
195
- if (modelsState.isApplying) {
196
- showStatus("Already writing the routing — one at a time", "error");
197
- return;
198
- }
199
- if (!selectedItem || selectedItem.kind !== "preset") {
200
- showStatus("Move to a preset row and press a", "error");
201
- return;
202
- }
203
- const { config, custom } = selectedItem;
195
+ if (modelsState.isApplying) return;
196
+ if (!selectedItem || selectedItem.kind !== "preset") return;
197
+ const { config, custom, label } = selectedItem;
204
198
 
205
199
  dispatch({ type: "MODELS_APPLY_START" });
206
200
  try {
@@ -213,24 +207,24 @@ export function ModelsScreen() {
213
207
  projectPath: state.projectPath,
214
208
  preset: custom ? config : config.preset,
215
209
  });
210
+ // Applied: the reloaded list and the header show it. Said only when
211
+ // the profile did NOT record it — the next magus install renders the
212
+ // live settings from the profile and drops the routing.
216
213
  await fetchData();
217
- const where = result.profile
218
- ? `profile "${result.profile}"`
219
- : "this project";
220
- // Say plainly when the manifest was NOT updated: without that record a
221
- // `magus install` rewrites the profile and the routing disappears.
222
- const durability =
223
- result.profile && !result.recordedInManifest
224
- ? " — not in profiles.json, so `magus install` will drop it"
225
- : "";
226
- showStatus(
227
- `Applied ${selectedItem.label} in ${where}, hook ${result.hook}${durability}`,
228
- result.profile && !result.recordedInManifest ? "error" : "success",
229
- );
214
+ if (result.profile && !result.recordedInManifest) {
215
+ await showFailures(
216
+ failureReport(`${label} is active, but not saved in the profile`, [
217
+ {
218
+ subject: `profile ${result.profile}`,
219
+ reason:
220
+ "the routing is not recorded in it, so the next magus install drops it",
221
+ },
222
+ ]),
223
+ );
224
+ }
230
225
  } catch (error) {
231
- showStatus(
232
- error instanceof Error ? error.message : String(error),
233
- "error",
226
+ await showFailures(
227
+ stepFailureReport("No model routing was applied", label, error),
234
228
  );
235
229
  } finally {
236
230
  dispatch({ type: "MODELS_APPLY_END" });
@@ -240,26 +234,24 @@ export function ModelsScreen() {
240
234
  selectedItem,
241
235
  state.projectPath,
242
236
  fetchData,
243
- showStatus,
237
+ showFailures,
244
238
  dispatch,
245
239
  ]);
246
240
 
241
+ // Off: the reloaded header reads "Model tiers: off".
247
242
  const handleClear = useCallback(async () => {
248
- if (modelsState.isApplying) {
249
- showStatus("Already writing the routing — one at a time", "error");
250
- return;
251
- }
243
+ if (modelsState.isApplying) return;
252
244
  dispatch({ type: "MODELS_APPLY_START" });
253
245
  try {
254
246
  await clearModels(state.projectPath);
255
247
  await fetchData();
256
- showStatus(
257
- "Model tiers off — subagents inherit the session model again. The hook stays registered and is a no-op without a config.",
258
- );
259
248
  } catch (error) {
260
- showStatus(
261
- error instanceof Error ? error.message : String(error),
262
- "error",
249
+ await showFailures(
250
+ stepFailureReport(
251
+ "Model tiers were not turned off",
252
+ "model routing",
253
+ error,
254
+ ),
263
255
  );
264
256
  } finally {
265
257
  dispatch({ type: "MODELS_APPLY_END" });
@@ -268,7 +260,7 @@ export function ModelsScreen() {
268
260
  modelsState.isApplying,
269
261
  state.projectPath,
270
262
  fetchData,
271
- showStatus,
263
+ showFailures,
272
264
  dispatch,
273
265
  ]);
274
266
 
@@ -301,67 +293,36 @@ export function ModelsScreen() {
301
293
  blur: () => dispatch({ type: "SET_SEARCHING", isSearching: false }),
302
294
  });
303
295
 
304
- /**
305
- * The previous message belongs to the previous ACTION, so every key this
306
- * screen acts on clears it first. Keys another layer takes — a digit or Tab
307
- * leaving the tab, text typed into the search box — leave it alone, so a
308
- * round trip through another tab keeps it.
309
- */
310
- const clearStatus = () => {
311
- if (modelsState.status) dispatch({ type: "MODELS_STATUS_CLEAR" });
312
- };
313
-
314
296
  useScreenKeys("models", {
315
297
  // Enter and `a` are the same action here. There is nothing to "open" on
316
298
  // this screen — a preset row has one thing it can do — so binding Enter
317
299
  // to anything else would leave the most obvious key doing nothing.
318
300
  apply: () => {
319
- clearStatus();
320
301
  void handleApply();
321
302
  },
322
303
  off: () => {
323
- clearStatus();
324
304
  void handleClear();
325
305
  },
326
306
  reload: () => {
327
- clearStatus();
328
307
  void fetchData();
329
308
  },
330
309
  search: () => {
331
- clearStatus();
332
310
  dispatch({ type: "SET_SEARCHING", isSearching: true });
333
311
  },
334
312
  // Only while a committed filter is set; otherwise Esc is the global back.
335
313
  "clear-filter": () => {
336
314
  if (!modelsState.searchQuery) return false;
337
- clearStatus();
338
315
  dispatch({ type: "MODELS_SET_SEARCH", query: "" });
339
316
  dispatch({ type: "MODELS_SELECT", index: 0 });
340
317
  return true;
341
318
  },
342
319
  move: (key) => {
343
- clearStatus();
344
320
  move(moveDirection(key));
345
321
  },
346
322
  });
347
323
 
348
324
  // ── Chrome ────────────────────────────────────────────────────────────────
349
325
 
350
- // Transient feedback takes the full-width notice row under the header, never
351
- // the header itself: a long message beside the title shrinks the title away
352
- // and the two read as one garbled sentence.
353
- const noticeContent = statusMsg ? (
354
- <text
355
- fg={
356
- statusMsg.tone === "success"
357
- ? theme.colors.success
358
- : theme.colors.danger
359
- }
360
- >
361
- {statusMsg.text}
362
- </text>
363
- ) : undefined;
364
-
365
326
  // The file's `preset` string is deliberately not shown beside the state: it records what
366
327
  // was last APPLIED, so a config edited afterwards would wear a built-in's name here while
367
328
  // the list correctly calls it `Custom`.
@@ -398,7 +359,6 @@ export function ModelsScreen() {
398
359
  title="magus Models"
399
360
  currentScreen="models"
400
361
  statusLine={statusContent}
401
- notice={noticeContent}
402
362
  search={
403
363
  modelsState.searchQuery || isSearchActive
404
364
  ? {
@@ -469,8 +429,8 @@ export function ModelsScreen() {
469
429
  configPath: snapshot?.path ?? null,
470
430
  width: listWidth,
471
431
  // Rows the list did not use. One held back because the
472
- // search bar and the notice each take a row from the panel
473
- // without taking one from `listPanelHeight` — and an
432
+ // search bar takes a row from the panel without taking
433
+ // one from `listPanelHeight` — and an
474
434
  // over-tall panel overprints rather than clipping.
475
435
  available:
476
436
  dimensions.listPanelHeight -