magus-cli 7.5.3 → 7.6.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 (68) hide show
  1. package/package.json +4 -4
  2. package/src/__tests__/blackbox-convergence.test.ts +1 -2
  3. package/src/__tests__/blackbox-parity.test.ts +16 -14
  4. package/src/__tests__/blackbox-plugin-state.test.ts +13 -15
  5. package/src/__tests__/cli-apply-seams.test.ts +3 -3
  6. package/src/__tests__/cli-profile-prompts.test.ts +1165 -0
  7. package/src/__tests__/content-drift.test.ts +0 -1
  8. package/src/__tests__/files-missing-scope.test.ts +235 -0
  9. package/src/__tests__/{enabled-not-installed.test.ts → files-missing.test.ts} +32 -26
  10. package/src/__tests__/format-update-outcome.test.ts +367 -0
  11. package/src/__tests__/installed-version-scope.test.tsx +425 -0
  12. package/src/__tests__/key-focus-guard.test.tsx +260 -0
  13. package/src/__tests__/mate-availability.test.ts +11 -13
  14. package/src/__tests__/plugin-contents.test.ts +0 -1
  15. package/src/__tests__/plugin-detail-scopes.test.tsx +270 -0
  16. package/src/__tests__/plugin-row-text.test.ts +18 -21
  17. package/src/__tests__/profile-command.test.ts +2 -2
  18. package/src/__tests__/profile-migration-offer.test.ts +5 -4
  19. package/src/__tests__/profile-name-on-save.test.ts +443 -0
  20. package/src/__tests__/profile-skills.test.ts +3 -3
  21. package/src/__tests__/profile-startup-nested.test.ts +329 -0
  22. package/src/__tests__/profile-startup-old-format.test.ts +390 -0
  23. package/src/__tests__/profile-startup.test.ts +1 -1
  24. package/src/__tests__/profile-state.test.ts +27 -22
  25. package/src/__tests__/prompt-eof.test.ts +245 -0
  26. package/src/__tests__/scope-action.test.ts +35 -38
  27. package/src/__tests__/scope-row-lookup.test.ts +126 -0
  28. package/src/__tests__/select-restore.test.ts +209 -0
  29. package/src/__tests__/stale-load-repair.test.ts +1 -2
  30. package/src/__tests__/update-apply.test.ts +18 -2
  31. package/src/__tests__/update-null-readback.test.ts +300 -0
  32. package/src/__tests__/update-plan-parity.test.ts +249 -0
  33. package/src/__tests__/update-plan.test.ts +66 -67
  34. package/src/__tests__/version-unknown-state.test.ts +38 -39
  35. package/src/cli/bootstrap.ts +271 -66
  36. package/src/cli/doctor.ts +4 -3
  37. package/src/cli/install.ts +83 -27
  38. package/src/cli/prompt.ts +19 -5
  39. package/src/cli/router.ts +1 -0
  40. package/src/cli/select.ts +168 -0
  41. package/src/cli/update-view.ts +120 -6
  42. package/src/cli/update.ts +6 -4
  43. package/src/services/claude-settings.ts +28 -0
  44. package/src/services/manifest.ts +31 -0
  45. package/src/services/mate-availability.ts +23 -29
  46. package/src/services/plugin-manager.ts +247 -105
  47. package/src/services/profile-absorb.ts +10 -7
  48. package/src/services/profile-activate.ts +41 -19
  49. package/src/services/profile-adopt.ts +9 -2
  50. package/src/services/profile-startup.ts +206 -5
  51. package/src/services/profile-state.ts +8 -7
  52. package/src/services/tracked-live-files.ts +24 -3
  53. package/src/services/update-engine.ts +28 -10
  54. package/src/services/update-plan.ts +78 -94
  55. package/src/ui/App.tsx +96 -56
  56. package/src/ui/adapters/pluginsAdapter.ts +1 -1
  57. package/src/ui/components/modals/ModalContainer.tsx +3 -0
  58. package/src/ui/components/primitives/ScopeSquares.tsx +10 -5
  59. package/src/ui/hooks/useGitignoreModal.ts +21 -3
  60. package/src/ui/hooks/useProfileStartupModal.ts +214 -15
  61. package/src/ui/hooks/useStaleLoadModal.ts +5 -1
  62. package/src/ui/input/KeyRouter.tsx +43 -2
  63. package/src/ui/keymap.ts +1 -1
  64. package/src/ui/renderers/pluginRenderers.tsx +39 -45
  65. package/src/ui/renderers/pluginRowText.ts +6 -2
  66. package/src/ui/renderers/pluginStatusText.ts +4 -3
  67. package/src/ui/screens/PluginsScreen.tsx +28 -41
  68. package/src/cli/tui-prompt.tsx +0 -143
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magus-cli",
3
- "version": "7.5.3",
3
+ "version": "7.6.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.5.3",
69
- "magus-cli-darwin-x64": "7.5.3",
70
- "magus-cli-linux-x64": "7.5.3"
68
+ "magus-cli-darwin-arm64": "7.6.0",
69
+ "magus-cli-darwin-x64": "7.6.0",
70
+ "magus-cli-linux-x64": "7.6.0"
71
71
  }
72
72
  }
@@ -220,9 +220,8 @@ function designer(installed: string): PluginInfo {
220
220
  description: "",
221
221
  marketplace: "magus",
222
222
  marketplaceDisplay: "magus",
223
- enabled: true,
224
223
  installedVersion: installed,
225
- projectScope: { enabled: true, version: installed },
224
+ projectScope: { installed: true, version: installed },
226
225
  };
227
226
  }
228
227
 
@@ -96,11 +96,10 @@ function row(
96
96
  description: "",
97
97
  marketplace: mp,
98
98
  marketplaceDisplay: mp,
99
- enabled: installed !== null,
100
99
  ...(installed !== null
101
100
  ? {
102
101
  installedVersion: installed,
103
- projectScope: { enabled: true, version: installed },
102
+ projectScope: { installed: true, version: installed },
104
103
  }
105
104
  : {}),
106
105
  ...extra,
@@ -141,9 +140,8 @@ function makeCatalog(): PluginInfo[] {
141
140
  row("pinned-undeliverable@magus", "3.0.0", "1.0.0"),
142
141
  // _synthetic TUI-only rows
143
142
  row("installed-undeclared@magus", "1.0.0", "1.0.0"),
144
- row("enabled-only@magus", "1.0.0", null, {
145
- enabled: true,
146
- userScope: { enabled: true },
143
+ row("files-missing@magus", "1.0.0", null, {
144
+ userScope: { installed: true },
147
145
  }),
148
146
  row("browse-only@magus", "1.0.0", null),
149
147
  ];
@@ -541,23 +539,25 @@ describe("R7 parity: one fixture through the CLI path and the TUI path", () => {
541
539
  expect(p.tuiRows.has("gone@magus")).toBe(false);
542
540
  });
543
541
 
544
- test("TUI-only rows: installed or enabled rows get a status, browse-only rows get none", async () => {
542
+ test("TUI-only rows: installed rows, files missing included, get a status, browse-only rows get none", async () => {
545
543
  const p = await runBothPaths();
546
544
 
547
545
  expect(p.tuiRows.get("installed-undeclared@magus")?.status?.action).toBe(
548
546
  "current",
549
547
  );
550
- const enabledOnly = p.tuiRows.get("enabled-only@magus")?.status;
548
+ const filesMissing = p.tuiRows.get("files-missing@magus")?.status;
551
549
  expect({
552
- action: enabledOnly?.action,
553
- reason: enabledOnly?.reason,
550
+ action: filesMissing?.action,
551
+ reason: filesMissing?.reason,
554
552
  }).toEqual({
555
553
  action: "install",
556
- reason: "enabled-not-installed",
554
+ reason: "files-missing",
557
555
  });
558
- const enabledRow = p.tuiRows.get("enabled-only@magus");
559
- if (!enabledRow) throw new Error("no enabled-only row");
560
- expect(pluginRowText(enabledRow).text).not.toContain("profile declares it");
556
+ const filesMissingRow = p.tuiRows.get("files-missing@magus");
557
+ if (!filesMissingRow) throw new Error("no files-missing row");
558
+ expect(pluginRowText(filesMissingRow).text).not.toContain(
559
+ "profile declares it",
560
+ );
561
561
  expect(p.tuiRows.get("browse-only@magus")?.status).toBeNull();
562
562
  expect(p.tuiRows.get("seo@magus-marketing")?.status).toBeNull();
563
563
  });
@@ -573,7 +573,9 @@ describe("R7 parity: one fixture through the CLI path and the TUI path", () => {
573
573
 
574
574
  const cliCounts = countPluginStates(cliItems);
575
575
  expect(countPluginStates(tuiItems)).toEqual(cliCounts);
576
- expect(cliCounts.install).toBe(1); // bunjs
576
+ // bunjs (declared, not installed) and files-missing (installed, no files).
577
+ // The second was TUI-only until the CLI planned every installed plugin too.
578
+ expect(cliCounts.install).toBe(2);
577
579
  expect(cliCounts.repair).toBe(1); // madbench
578
580
  // Was 10. Seven of those rows are `current` now, and each for the same
579
581
  // reason: nothing could be compared, so nothing is known to be behind.
@@ -35,11 +35,10 @@ function info(
35
35
  description: "",
36
36
  marketplace: at >= 0 ? id.slice(at + 1) : "",
37
37
  marketplaceDisplay: at >= 0 ? id.slice(at + 1) : "",
38
- enabled: installed !== null,
39
38
  ...(installed !== null
40
39
  ? {
41
40
  installedVersion: installed,
42
- projectScope: { enabled: true, version: installed },
41
+ projectScope: { installed: true, version: installed },
43
42
  }
44
43
  : {}),
45
44
  ...extra,
@@ -273,23 +272,22 @@ describe("planPluginUpdates is classifyPlugin over every pin, sorted", () => {
273
272
  describe("pinForRow / classifyPluginRows", () => {
274
273
  const declared = info("dev@magus", "1.0.0", "1.0.0");
275
274
  const installedOnly = info("extra@magus", "1.0.0", "1.0.0");
276
- const enabledOnly = info("en@magus", "1.0.0", null, {
277
- enabled: true,
278
- userScope: { enabled: true },
275
+ const filesMissing = info("en@magus", "1.0.0", null, {
276
+ userScope: { installed: true },
279
277
  });
280
278
  const browseOnly = info("browse@magus", "1.0.0", null);
281
279
  const pins = { "dev@magus": "1.0.0" };
282
280
 
283
- test("closure pin wins; installed or enabled rows are judged against latest; browse-only has no pin", () => {
281
+ test("closure pin wins; installed rows, files missing included, are judged against latest; browse-only has no pin", () => {
284
282
  expect(pinForRow(declared, pins)).toBe("1.0.0");
285
283
  expect(pinForRow(installedOnly, pins)).toBe("latest");
286
- expect(pinForRow(enabledOnly, pins)).toBe("latest");
284
+ expect(pinForRow(filesMissing, pins)).toBe("latest");
287
285
  expect(pinForRow(browseOnly, pins)).toBeNull();
288
286
  });
289
287
 
290
288
  test("classifyPluginRows has one status per pinned row and none for browse-only rows", () => {
291
289
  const rows = classifyPluginRows(
292
- [declared, installedOnly, enabledOnly, browseOnly],
290
+ [declared, installedOnly, filesMissing, browseOnly],
293
291
  pins,
294
292
  );
295
293
 
@@ -299,7 +297,7 @@ describe("pinForRow / classifyPluginRows", () => {
299
297
  "extra@magus",
300
298
  ]);
301
299
  expect(rows.get("en@magus")?.action).toBe("install");
302
- expect(rows.get("en@magus")?.reason).toBe("enabled-not-installed");
300
+ expect(rows.get("en@magus")?.reason).toBe("files-missing");
303
301
  });
304
302
  });
305
303
 
@@ -450,22 +448,22 @@ describe("pluginStatusText — the TUI suffix contract (§8.7)", () => {
450
448
  ).toBe(" stale — reinstall (files moved)");
451
449
  });
452
450
 
453
- test("install: absent claims the profile declares it; enabled-not-installed shows its note instead", () => {
451
+ test("install: absent claims the profile declares it; files-missing shows its note instead", () => {
454
452
  expect(
455
453
  pluginStatusText(
456
454
  item({ action: "install", reason: "absent", installed: null }),
457
455
  ).suffix,
458
456
  ).toBe(" not installed — profile declares it");
459
- const enabled = pluginStatusText(
457
+ const filesMissing = pluginStatusText(
460
458
  item({
461
459
  action: "install",
462
- reason: "enabled-not-installed",
460
+ reason: "files-missing",
463
461
  installed: null,
464
- note: "enabled in user settings",
462
+ note: "files missing",
465
463
  }),
466
464
  ).suffix;
467
- expect(enabled).toBe(" not installed (enabled in user settings)");
468
- expect(enabled).not.toContain("profile declares");
465
+ expect(filesMissing).toBe(" files missing");
466
+ expect(filesMissing).not.toContain("profile declares");
469
467
  });
470
468
 
471
469
  test("current: empty, or ' (<note>)' for pin-undeliverable", () => {
@@ -115,7 +115,7 @@ describe("the apply meter's denominator", () => {
115
115
  item({ pluginId: "b", action: "unknown" }),
116
116
  item({ pluginId: "c", action: "update", scopes: ["project"] }),
117
117
  item({ pluginId: "d", action: "update", scopes: [] }),
118
- item({ pluginId: "e", action: "install", scopes: [] }),
118
+ item({ pluginId: "e", action: "install", reason: "absent", scopes: [] }),
119
119
  item({ pluginId: "f", action: "repair", scopes: ["user", "project"] }),
120
120
  ];
121
121
 
@@ -128,8 +128,8 @@ describe("the apply meter's denominator", () => {
128
128
  .map((e) => e.slice("begin:".length));
129
129
 
130
130
  expect(actual).toEqual(predicted);
131
- // And specifically: an install with no scopes IS work (it goes in at
132
- // project scope), while an update with no scopes is not.
131
+ // And specifically: an `absent` install with no scopes IS work (it goes
132
+ // in at project scope), while an update with no scopes is not.
133
133
  expect(predicted).toEqual(["c", "e", "f"]);
134
134
  });
135
135