magus-cli 7.7.0 → 7.8.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 (83) hide show
  1. package/package.json +4 -4
  2. package/src/__tests__/blackbox-parity.test.ts +29 -15
  3. package/src/__tests__/blackbox-plugin-state.test.ts +25 -6
  4. package/src/__tests__/blind-780-cli-tools.test.ts +808 -0
  5. package/src/__tests__/blind-780-deprecated.test.ts +631 -0
  6. package/src/__tests__/blind-780-failure-reasons.test.ts +1004 -0
  7. package/src/__tests__/blind-780-plugin-resolution.test.ts +431 -0
  8. package/src/__tests__/blind-780-review2.test.ts +1203 -0
  9. package/src/__tests__/cli-update-view.test.ts +3 -3
  10. package/src/__tests__/deprecated-remove.test.ts +827 -0
  11. package/src/__tests__/doctor.test.ts +159 -1
  12. package/src/__tests__/failure-popup-review2.test.tsx +920 -0
  13. package/src/__tests__/failure-popup.test.tsx +419 -0
  14. package/src/__tests__/final-fixes-780.test.tsx +724 -0
  15. package/src/__tests__/format-update-outcome.test.ts +6 -32
  16. package/src/__tests__/gitignore-profile.test.ts +1 -9
  17. package/src/__tests__/models-screen-state.test.ts +18 -40
  18. package/src/__tests__/plugin-resolution.test.ts +52 -13
  19. package/src/__tests__/plugin-row-text.test.ts +30 -37
  20. package/src/__tests__/profile-dependencies.test.ts +7 -0
  21. package/src/__tests__/profile-startup-old-format.test.ts +0 -3
  22. package/src/__tests__/profile-startup.test.ts +40 -12
  23. package/src/__tests__/stale-load-repair.test.ts +31 -23
  24. package/src/__tests__/update-apply.test.ts +13 -29
  25. package/src/__tests__/update-plan.test.ts +23 -17
  26. package/src/__tests__/useStaleLoadModal.test.ts +1 -57
  27. package/src/__tests__/version-unknown-state.test.ts +15 -17
  28. package/src/cli/doctor.ts +151 -30
  29. package/src/cli/install.ts +5 -6
  30. package/src/cli/update-view.ts +58 -15
  31. package/src/cli/update.ts +27 -5
  32. package/src/services/catalog-cache-store.ts +8 -6
  33. package/src/services/claude-cli.ts +106 -19
  34. package/src/services/claude-settings.ts +84 -36
  35. package/src/services/plugin-env-needs.ts +69 -0
  36. package/src/services/plugin-manager.ts +31 -19
  37. package/src/services/plugin-resolution.ts +34 -42
  38. package/src/services/profile-activate.ts +1 -2
  39. package/src/services/profile-startup.ts +18 -43
  40. package/src/services/profile-state.ts +72 -2
  41. package/src/services/resolver.ts +95 -1
  42. package/src/services/update-engine.ts +326 -31
  43. package/src/services/update-plan.ts +293 -33
  44. package/src/types/index.ts +7 -0
  45. package/src/ui/App.tsx +90 -146
  46. package/src/ui/adapters/cliToolsAdapter.ts +203 -1
  47. package/src/ui/adapters/failureReport.ts +137 -0
  48. package/src/ui/adapters/pluginsAdapter.ts +297 -45
  49. package/src/ui/adapters/startupReports.ts +282 -0
  50. package/src/ui/components/modals/FailureListModal.tsx +231 -0
  51. package/src/ui/components/modals/MessageModal.tsx +7 -12
  52. package/src/ui/components/modals/ModalContainer.tsx +10 -5
  53. package/src/ui/components/modals/index.ts +1 -0
  54. package/src/ui/hooks/startupQueue.ts +56 -0
  55. package/src/ui/hooks/useGitignoreModal.ts +30 -52
  56. package/src/ui/hooks/useProfileStartupModal.ts +119 -142
  57. package/src/ui/hooks/useStaleLoadModal.ts +35 -65
  58. package/src/ui/keymap.ts +14 -1
  59. package/src/ui/renderers/mcpRenderers.tsx +110 -3
  60. package/src/ui/renderers/pluginRenderers.tsx +77 -6
  61. package/src/ui/renderers/pluginRowText.ts +25 -25
  62. package/src/ui/renderers/pluginStatusText.ts +18 -1
  63. package/src/ui/screens/AliasScreen.tsx +38 -45
  64. package/src/ui/screens/CliToolsScreen.tsx +65 -54
  65. package/src/ui/screens/EnvVarsScreen.tsx +17 -5
  66. package/src/ui/screens/GitignoreScreen.tsx +48 -67
  67. package/src/ui/screens/McpRegistryScreen.tsx +67 -38
  68. package/src/ui/screens/McpScreen.tsx +46 -19
  69. package/src/ui/screens/ModelsScreen.tsx +46 -86
  70. package/src/ui/screens/PluginsScreen.tsx +313 -221
  71. package/src/ui/screens/ProfilesScreen.tsx +79 -40
  72. package/src/ui/screens/SkillsScreen.tsx +83 -80
  73. package/src/ui/screens/StylesScreen.tsx +211 -275
  74. package/src/ui/state/AppContext.tsx +66 -44
  75. package/src/ui/state/DimensionsContext.tsx +3 -22
  76. package/src/ui/state/reducer.ts +12 -100
  77. package/src/ui/state/types.ts +48 -69
  78. package/src/utils/user-path.ts +60 -0
  79. package/src/__tests__/notices-rules.test.ts +0 -476
  80. package/src/__tests__/notices.test.tsx +0 -604
  81. package/src/__tests__/styles-status-line.test.ts +0 -72
  82. package/src/__tests__/version-snapshot.test.ts +0 -212
  83. package/src/services/version-snapshot.ts +0 -189
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magus-cli",
3
- "version": "7.7.0",
3
+ "version": "7.8.0",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -65,8 +65,8 @@
65
65
  "typescript": "^5.6.3"
66
66
  },
67
67
  "optionalDependencies": {
68
- "magus-cli-darwin-arm64": "7.7.0",
69
- "magus-cli-darwin-x64": "7.7.0",
70
- "magus-cli-linux-x64": "7.7.0"
68
+ "magus-cli-darwin-arm64": "7.8.0",
69
+ "magus-cli-darwin-x64": "7.8.0",
70
+ "magus-cli-linux-x64": "7.8.0"
71
71
  }
72
72
  }
@@ -430,7 +430,15 @@ describe("R7 parity: one fixture through the CLI path and the TUI path", () => {
430
430
  if (!row) throw new Error(`no TUI row for ${id}`);
431
431
  const { text } = pluginRowText(row);
432
432
  expect({ id, empty: text.trim() === "" }).toEqual({ id, empty: false });
433
- if (cli.note) {
433
+ // A deprecated row says its verdict and the key that acts on it, in the
434
+ // same words on every row, rather than the note — which the CLI does not
435
+ // print for it either (its DEPRECATED heading carries the fact).
436
+ if (cli.action === "remove") {
437
+ expect({ id, text }).toEqual({
438
+ id,
439
+ text: expect.stringContaining("deprecated — press a to remove"),
440
+ });
441
+ } else if (cli.note) {
434
442
  expect({ id, containsNote: text.includes(cli.note) }).toEqual({
435
443
  id,
436
444
  containsNote: true,
@@ -490,13 +498,13 @@ describe("R7 parity: one fixture through the CLI path and the TUI path", () => {
490
498
  reason: "catalog-unverified",
491
499
  target: null,
492
500
  },
493
- // Was `moved`, which guessed a destination from a bare name match and
494
- // could not tell a move from a dual publication. An orphan is simply
495
- // not offered.
496
- "seo@magus": { action: "unknown", reason: "not-offered", target: null },
501
+ // Its marketplace no longer lists it, but magus-marketing lists the
502
+ // same name: it MOVED. Not removed — the plugin still exists — and
503
+ // nothing is installed in its place: `unknown`, never applied.
504
+ "seo@magus": { action: "unknown", reason: "moved", target: null },
497
505
  "legacy-tool@magus": {
498
- action: "unknown",
499
- reason: "not-offered",
506
+ action: "remove",
507
+ reason: "deprecated",
500
508
  target: null,
501
509
  },
502
510
  "pinned-behind@magus": {
@@ -535,7 +543,7 @@ describe("R7 parity: one fixture through the CLI path and the TUI path", () => {
535
543
  expect(undeliverable?.note).toBeTruthy();
536
544
 
537
545
  // declared but absent from the catalog: a CLI item, never a TUI row
538
- expect(p.cliPlugins.get("gone@magus")?.action).toBe("unknown");
546
+ expect(p.cliPlugins.get("gone@magus")?.action).toBe("remove");
539
547
  expect(p.tuiRows.has("gone@magus")).toBe(false);
540
548
  });
541
549
 
@@ -584,9 +592,11 @@ describe("R7 parity: one fixture through the CLI path and the TUI path", () => {
584
592
  // − pinned-unorderable (sha install against a semver pin)
585
593
  // What is left is unknown because something is genuinely wrong or unread:
586
594
  // claudish catalog-unverified — the check itself failed
587
- // seo@magus not-offered — an orphan
588
- // legacy-tool not-offered — an orphan
589
- expect(cliCounts.unknown).toBe(3);
595
+ // seo@magus moved — magus-marketing lists the same name
596
+ // The orphan nothing else lists is deprecated:
597
+ // legacy-tool remove — no longer published
598
+ expect(cliCounts.unknown).toBe(2);
599
+ expect(cliCounts.remove).toBe(1);
590
600
  expect(cliCounts.update).toBeGreaterThanOrEqual(2); // designer, pinned-behind
591
601
  });
592
602
 
@@ -716,12 +726,16 @@ describe("fixture discriminates the pre-change logic", () => {
716
726
 
717
727
  // Where they still disagree outright: an orphan carries neither
718
728
  // `hasUpdate` nor `updateCheckFailed`, so the flags call it healthy while
719
- // the classifier says no marketplace offers it at all.
720
- for (const id of ["seo@magus", "legacy-tool@magus"]) {
729
+ // the classifier says it moved, or is no longer published and removes it.
730
+ const orphans: Array<[string, string, string]> = [
731
+ ["seo@magus", "unknown", "moved"],
732
+ ["legacy-tool@magus", "remove", "deprecated"],
733
+ ];
734
+ for (const [id, action, reason] of orphans) {
721
735
  const info = p.catalog.find((c) => c.id === id) as PluginInfo;
722
736
  expect(legacyFlagState(info)).toBe("healthy");
723
- expect(p.tuiRows.get(id)?.status?.action).toBe("unknown");
724
- expect(p.tuiRows.get(id)?.status?.reason).toBe("not-offered");
737
+ expect(p.tuiRows.get(id)?.status?.action).toBe(action);
738
+ expect(p.tuiRows.get(id)?.status?.reason).toBe(reason);
725
739
  }
726
740
  });
727
741
  });
@@ -90,7 +90,7 @@ describe("indexCatalog", () => {
90
90
  });
91
91
 
92
92
  describe("classifyPlugin — one state plus a human reason", () => {
93
- test("declared, absent from the catalog, marketplace read: unknown / not-offered, no target", () => {
93
+ test("declared, absent from the catalog, marketplace read: remove / deprecated, no target", () => {
94
94
  const idx = indexCatalog([info("dev@magus", "1.0.0", "1.0.0")]);
95
95
 
96
96
  const got = classifyPlugin("gone@magus", "latest", idx);
@@ -100,8 +100,8 @@ describe("classifyPlugin — one state plus a human reason", () => {
100
100
  reason: got.reason,
101
101
  target: got.target,
102
102
  }).toEqual({
103
- action: "unknown",
104
- reason: "not-offered",
103
+ action: "remove",
104
+ reason: "deprecated",
105
105
  target: null,
106
106
  });
107
107
  expect(got.note).toBeTruthy();
@@ -219,7 +219,11 @@ describe("classifyPlugin — one state plus a human reason", () => {
219
219
  // version is `current` now, not unknown. It is the negative control
220
220
  // below — without it this loop would pass even if every state collapsed
221
221
  // into one.
222
- for (const id of ["b@magus", "c@magus", "d@magus", "e@nowhere"]) {
222
+ //
223
+ // Neither are `b@magus` (an orphan of a marketplace we read) and `d@magus`
224
+ // (declared, listed nowhere, same marketplace): both are DEPRECATED now, a
225
+ // `remove` — not a guess. They are the second control, after the loop.
226
+ for (const id of ["c@magus", "e@nowhere"]) {
223
227
  const got = classifyPlugin(id, "latest", idx);
224
228
  expect({ id, action: got.action }).toEqual({ id, action: "unknown" });
225
229
  expect({ id, target: got.target, hasNote: Boolean(got.note) }).toEqual({
@@ -234,6 +238,12 @@ describe("classifyPlugin — one state plus a human reason", () => {
234
238
  const a = classifyPlugin("a@magus", "latest", idx);
235
239
  expect(a.action).toBe("current");
236
240
  expect(a.note).toBeUndefined();
241
+ for (const id of ["b@magus", "d@magus"]) {
242
+ expect({ id, action: classifyPlugin(id, "latest", idx).action }).toEqual({
243
+ id,
244
+ action: "remove",
245
+ });
246
+ }
237
247
  });
238
248
  });
239
249
 
@@ -318,6 +328,7 @@ describe("countPluginStates", () => {
318
328
  repair: 1,
319
329
  current: 1,
320
330
  "stale-load": 0,
331
+ remove: 0,
321
332
  unknown: 1,
322
333
  });
323
334
  });
@@ -329,6 +340,7 @@ describe("countPluginStates", () => {
329
340
  repair: 0,
330
341
  current: 0,
331
342
  "stale-load": 0,
343
+ remove: 0,
332
344
  unknown: 0,
333
345
  });
334
346
  });
@@ -499,8 +511,12 @@ describe("pluginRowText", () => {
499
511
  // The fixture is an ORPHAN at 0.0.0, not a plain 0.0.0 install. A plain one
500
512
  // is `current` with no note now, which would leave nothing for this test to
501
513
  // find and let the gate come back unnoticed.
514
+ //
515
+ // Its marketplace is one we did NOT read, so it stays `unknown` with a note.
516
+ // An orphan of a marketplace we read is deprecated now, whose row says
517
+ // "deprecated — press a to remove" instead of its note.
502
518
  test("a note renders on a 0.0.0 install — no version gate", () => {
503
- const plugin = info("zero-version@magus", null, "0.0.0", {
519
+ const plugin = info("zero-version@unread-mp", null, "0.0.0", {
504
520
  isOrphaned: true,
505
521
  });
506
522
  const sibling = info("dev@magus", "1.0.0", "1.0.0");
@@ -529,8 +545,11 @@ describe("pluginRowText", () => {
529
545
  expect(out.text).toContain(pluginStatusText(status).suffix);
530
546
  });
531
547
 
548
+ // An orphan whose marketplace was not read; see the note on the 0.0.0 test.
532
549
  test("an orphan row renders its note", () => {
533
- const plugin = info("legacy@magus", null, "0.5.0", { isOrphaned: true });
550
+ const plugin = info("legacy@unread-mp", null, "0.5.0", {
551
+ isOrphaned: true,
552
+ });
534
553
  const sibling = info("dev@magus", "1.0.0", "1.0.0");
535
554
  const status = classifyPlugin(
536
555
  plugin.id,