magus-cli 7.11.0 → 7.11.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magus-cli",
3
- "version": "7.11.0",
3
+ "version": "7.11.1",
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.11.0",
69
- "magus-cli-darwin-x64": "7.11.0",
70
- "magus-cli-linux-x64": "7.11.0"
68
+ "magus-cli-darwin-arm64": "7.11.1",
69
+ "magus-cli-darwin-x64": "7.11.1",
70
+ "magus-cli-linux-x64": "7.11.1"
71
71
  }
72
72
  }
@@ -39,8 +39,9 @@ describe("shipped plugin manifests under schema 2", () => {
39
39
  });
40
40
  }
41
41
 
42
- // REQ-26: claudish gets minVersion 10.7.0; no other requirement gets a floor.
43
- test("REQ-26 claudish's claudish requirement has minVersion 10.7.0", () => {
42
+ // REQ-26: claudish gets a minVersion floor; no other requirement gets one. The floor
43
+ // moved from 10.7.0 to 11.1.0 with claudish plugin 2.5.0, which needs SlotRow.recent.
44
+ test("REQ-26 claudish's claudish requirement has minVersion 11.1.0", () => {
44
45
  const claudish = declaring.find(({ name }) => name === "claudish");
45
46
  if (!claudish) throw new Error("plugins/claudish declares no requires");
46
47
  const result = parseRequires(claudish.json);
@@ -50,7 +51,7 @@ describe("shipped plugin manifests under schema 2", () => {
50
51
  | (Record<string, unknown> & { name: string })
51
52
  | undefined;
52
53
  expect(req).toBeDefined();
53
- expect(req?.minVersion).toBe("10.7.0");
54
+ expect(req?.minVersion).toBe("11.1.0");
54
55
  });
55
56
 
56
57
  test("REQ-26 no other shipped requirement declares a floor", () => {
@@ -0,0 +1,105 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ type ScopeStatus,
4
+ isInstalledInScope,
5
+ resolveScopeAction,
6
+ } from "../services/plugin-manager.js";
7
+
8
+ /**
9
+ * MTL-70: a deprecated plugin (no longer in any marketplace,
10
+ * `PluginInfo.isOrphaned === true`) can only be removed from a scope key.
11
+ * Installed in the scope → "uninstall"; not installed → "none". `behind` never
12
+ * matters for a deprecated plugin. Written blind from the spec.
13
+ */
14
+
15
+ const installedScopes: Array<[string, ScopeStatus]> = [
16
+ ["installed with a registry version", { installed: true, version: "3.0.2" }],
17
+ ["installed with no version (files missing)", { installed: true }],
18
+ ['installed at "0.0.0"', { installed: true, version: "0.0.0" }],
19
+ [
20
+ "installed with an unorderable sha",
21
+ { installed: true, version: "3ea32df27be7" },
22
+ ],
23
+ ];
24
+
25
+ const notInstalledScopes: Array<[string, ScopeStatus | undefined]> = [
26
+ ["an absent scope", undefined],
27
+ ["installed: false", { installed: false }],
28
+ [
29
+ "installed: false with a registry version",
30
+ { installed: false, version: "3.0.2" },
31
+ ],
32
+ ];
33
+
34
+ describe("resolveScopeAction — deprecated plugin installed in the scope (REQ-1)", () => {
35
+ for (const [label, scope] of installedScopes) {
36
+ test(`${label} is installed per isInstalledInScope (precondition)`, () => {
37
+ expect(isInstalledInScope(scope)).toBe(true);
38
+ });
39
+
40
+ for (const behind of [false, true]) {
41
+ test(`REQ-1: ${label}, behind=${behind} resolves to uninstall`, () => {
42
+ expect(resolveScopeAction(scope, behind, true)).toBe("uninstall");
43
+ });
44
+ }
45
+ }
46
+ });
47
+
48
+ describe("resolveScopeAction — deprecated plugin NOT installed in the scope (REQ-2)", () => {
49
+ for (const [label, scope] of notInstalledScopes) {
50
+ test(`${label} is not installed per isInstalledInScope (precondition)`, () => {
51
+ expect(isInstalledInScope(scope)).toBe(false);
52
+ });
53
+
54
+ for (const behind of [false, true]) {
55
+ test(`REQ-2: ${label}, behind=${behind} resolves to none — no install, no update`, () => {
56
+ const action = resolveScopeAction(scope, behind, true);
57
+ expect(action).toBe("none");
58
+ expect(action).not.toBe("install");
59
+ expect(action).not.toBe("update");
60
+ });
61
+ }
62
+ }
63
+ });
64
+
65
+ describe("resolveScopeAction — orphaned false or omitted is unchanged (REQ-3)", () => {
66
+ const cases: Array<[string, ScopeStatus | undefined, boolean, string]> = [
67
+ ["files missing", { installed: true }, false, "install"],
68
+ ["files missing, behind", { installed: true }, true, "install"],
69
+ ["never installed (absent)", undefined, false, "install"],
70
+ ["never installed (flag off)", { installed: false }, false, "install"],
71
+ [
72
+ "healthy at latest",
73
+ { installed: true, version: "3.0.2" },
74
+ false,
75
+ "uninstall",
76
+ ],
77
+ [
78
+ "healthy and behind",
79
+ { installed: true, version: "2.9.0" },
80
+ true,
81
+ "update",
82
+ ],
83
+ ];
84
+
85
+ for (const [label, scope, behind, expected] of cases) {
86
+ test(`REQ-3: ${label} with orphaned omitted resolves to ${expected}`, () => {
87
+ expect(resolveScopeAction(scope, behind)).toBe(expected as never);
88
+ });
89
+ test(`REQ-3: ${label} with orphaned=false resolves to ${expected}`, () => {
90
+ expect(resolveScopeAction(scope, behind, false)).toBe(expected as never);
91
+ });
92
+ }
93
+
94
+ test("REQ-3: a non-deprecated plugin never resolves to none", () => {
95
+ const all: Array<ScopeStatus | undefined> = [
96
+ ...installedScopes.map(([, s]) => s),
97
+ ...notInstalledScopes.map(([, s]) => s),
98
+ ];
99
+ for (const scope of all) {
100
+ for (const behind of [false, true]) {
101
+ expect(resolveScopeAction(scope, behind, false)).not.toBe("none");
102
+ }
103
+ }
104
+ });
105
+ });
package/src/cli/router.ts CHANGED
@@ -103,7 +103,12 @@ const SUBCOMMANDS = new Set([
103
103
  "doctor",
104
104
  ]);
105
105
 
106
- /** Subcommands that read or write profile state, and so pass the gate first. */
106
+ /**
107
+ * Subcommands that read or write profile state, and so pass the gate first.
108
+ * code-search setup runs `magus profile list` to regenerate settings.json after it
109
+ * edits profiles.json, so `profile` must keep running the gate
110
+ * (ai-docs/code-search-setup-profiles.md).
111
+ */
107
112
  const PROFILE_GATED = new Set(["install", "update", "profile", "models"]);
108
113
 
109
114
  export async function route(
@@ -371,7 +371,11 @@ export function buildScopeStatus(
371
371
  export function resolveScopeAction(
372
372
  scope: ScopeStatus | undefined,
373
373
  behind: boolean,
374
- ): "install" | "update" | "uninstall" {
374
+ orphaned = false,
375
+ ): "install" | "update" | "uninstall" | "none" {
376
+ // A deprecated plugin is no longer in any marketplace, so install and update
377
+ // can only fail with a raw CLI error. Removing it is the one action left.
378
+ if (orphaned) return isInstalledInScope(scope) ? "uninstall" : "none";
375
379
  if (!isInstalledInScope(scope) || scopeFilesMissing(scope)) return "install";
376
380
  return behind ? "update" : "uninstall";
377
381
  }
@@ -1009,7 +1009,9 @@ export function PluginsScreen() {
1009
1009
  const action = resolveScopeAction(
1010
1010
  selectedScope,
1011
1011
  scopeIsBehind(status, scopeValue as PluginScope),
1012
+ plugin.isOrphaned,
1012
1013
  );
1014
+ if (action === "none") return;
1013
1015
 
1014
1016
  try {
1015
1017
  const scope = scopeValue as PluginScope;
@@ -1174,7 +1176,9 @@ export function PluginsScreen() {
1174
1176
  const action = resolveScopeAction(
1175
1177
  scopeData,
1176
1178
  scopeIsBehind(statuses.get(plugin.id), scope),
1179
+ plugin.isOrphaned,
1177
1180
  );
1181
+ if (action === "none") return;
1178
1182
 
1179
1183
  try {
1180
1184
  if (action === "uninstall") {