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.
- package/README.md +1 -1
- package/package.json +4 -4
- package/src/__tests__/blackbox-parity.test.ts +45 -26
- package/src/__tests__/blackbox-plugin-state.test.ts +25 -6
- package/src/__tests__/blind/plugin-deps/doctor.test.ts +622 -0
- package/src/__tests__/blind/plugin-deps/helpers.ts +340 -0
- package/src/__tests__/blind/plugin-deps/hook.test.ts +328 -0
- package/src/__tests__/blind/plugin-deps/probe.test.ts +694 -0
- package/src/__tests__/blind/plugin-deps/read-only.test.ts +96 -0
- package/src/__tests__/blind/plugin-deps/schema.test.ts +390 -0
- package/src/__tests__/blind-780-cli-tools.test.ts +796 -0
- package/src/__tests__/blind-780-deprecated.test.ts +631 -0
- package/src/__tests__/blind-780-failure-reasons.test.ts +1004 -0
- package/src/__tests__/blind-780-plugin-resolution.test.ts +431 -0
- package/src/__tests__/blind-780-review2.test.ts +1178 -0
- package/src/__tests__/cli-apply-seams.test.ts +2 -67
- package/src/__tests__/cli-tool-commands.test.ts +131 -6
- package/src/__tests__/cli-tools-adapter.test.ts +52 -2
- package/src/__tests__/cli-tools-plugin-managed.test.tsx +83 -0
- package/src/__tests__/cli-update-view.test.ts +3 -3
- package/src/__tests__/deprecated-remove.test.ts +827 -0
- package/src/__tests__/deps-adapter.test.ts +191 -0
- package/src/__tests__/deps-check-hook.test.ts +220 -0
- package/src/__tests__/deps-plan.test.ts +896 -0
- package/src/__tests__/deps-report.test.ts +197 -0
- package/src/__tests__/doctor-bins.test.ts +134 -36
- package/src/__tests__/doctor-json.test.ts +261 -0
- package/src/__tests__/doctor.test.ts +159 -1
- package/src/__tests__/failure-popup-review2.test.tsx +896 -0
- package/src/__tests__/failure-popup.test.tsx +419 -0
- package/src/__tests__/final-fixes-780.test.tsx +740 -0
- package/src/__tests__/fixtures/bins.ts +55 -0
- package/src/__tests__/format-update-outcome.test.ts +6 -32
- package/src/__tests__/gitignore-profile.test.ts +1 -9
- package/src/__tests__/install-plan.test.ts +15 -1
- package/src/__tests__/marketplace-refresh.test.ts +9 -3
- package/src/__tests__/models-screen-state.test.ts +18 -40
- package/src/__tests__/no-shell-spawn.test.ts +77 -0
- package/src/__tests__/npm-package-name.test.ts +6 -9
- package/src/__tests__/plugin-requires.test.ts +138 -153
- package/src/__tests__/plugin-resolution.test.ts +52 -13
- package/src/__tests__/plugin-row-text.test.ts +30 -37
- package/src/__tests__/profile-dependencies.test.ts +7 -0
- package/src/__tests__/profile-startup-old-format.test.ts +0 -3
- package/src/__tests__/profile-startup.test.ts +40 -12
- package/src/__tests__/requires-probe.test.ts +858 -0
- package/src/__tests__/resolver.test.ts +129 -52
- package/src/__tests__/run.test.ts +108 -0
- package/src/__tests__/stale-load-repair.test.ts +31 -23
- package/src/__tests__/tool-probe.test.ts +1 -1
- package/src/__tests__/toolchain.test.ts +123 -52
- package/src/__tests__/update-apply.test.ts +13 -29
- package/src/__tests__/update-plan.test.ts +24 -18
- package/src/__tests__/useStaleLoadModal.test.ts +1 -57
- package/src/__tests__/version-unknown-state.test.ts +15 -17
- package/src/cli/ansi.ts +2 -7
- package/src/cli/deps-view.ts +213 -0
- package/src/cli/doctor.ts +320 -54
- package/src/cli/install.ts +55 -71
- package/src/cli/profile.ts +20 -2
- package/src/cli/router.ts +6 -2
- package/src/cli/update-view.ts +58 -15
- package/src/cli/update.ts +55 -12
- package/src/data/cli-tools.ts +24 -9
- package/src/services/active-closure.ts +3 -3
- package/src/services/catalog-cache-store.ts +8 -6
- package/src/services/claude-cli.ts +106 -19
- package/src/services/claude-settings.ts +88 -384
- package/src/services/cli-tool-commands.ts +104 -3
- package/src/services/deps-plan.ts +962 -0
- package/src/services/deps-report.ts +265 -0
- package/src/services/doctor-bins.ts +115 -43
- package/src/services/install-plan.ts +11 -1
- package/src/services/local-marketplace.ts +12 -206
- package/src/services/marketplace-refresh.ts +4 -4
- package/src/services/plugin-env-needs.ts +69 -0
- package/src/services/plugin-manager.ts +35 -37
- package/src/services/plugin-requires.ts +93 -162
- package/src/services/plugin-resolution.ts +34 -42
- package/src/services/profile-activate.ts +1 -2
- package/src/services/profile-startup.ts +18 -43
- package/src/services/profile-state.ts +72 -2
- package/src/services/resolver.ts +263 -59
- package/src/services/tool-probe.ts +18 -25
- package/src/services/toolchain.ts +117 -111
- package/src/services/update-engine.ts +345 -45
- package/src/services/update-plan.ts +296 -36
- package/src/shared/plugin/hooks/bun-missing.sh +39 -0
- package/src/shared/plugin/hooks/deps-check.ts +224 -0
- package/src/shared/plugin/hooks/lib/requires-probe.ts +1183 -0
- package/src/types/index.ts +60 -16
- package/src/ui/App.tsx +81 -171
- package/src/ui/adapters/cliToolsAdapter.ts +252 -12
- package/src/ui/adapters/depsAdapter.ts +129 -0
- package/src/ui/adapters/failureReport.ts +137 -0
- package/src/ui/adapters/pluginsAdapter.ts +297 -45
- package/src/ui/adapters/startupReports.ts +260 -0
- package/src/ui/components/modals/FailureListModal.tsx +231 -0
- package/src/ui/components/modals/MessageModal.tsx +7 -12
- package/src/ui/components/modals/ModalContainer.tsx +10 -5
- package/src/ui/components/modals/index.ts +1 -0
- package/src/ui/hooks/startupQueue.ts +56 -0
- package/src/ui/hooks/useGitignoreModal.ts +30 -52
- package/src/ui/hooks/useProfileStartupModal.ts +119 -142
- package/src/ui/hooks/useStaleLoadModal.ts +35 -65
- package/src/ui/keymap.ts +14 -1
- package/src/ui/renderers/cliToolRenderers.tsx +135 -2
- package/src/ui/renderers/mcpRenderers.tsx +110 -3
- package/src/ui/renderers/pluginRenderers.tsx +77 -6
- package/src/ui/renderers/pluginRowText.ts +25 -25
- package/src/ui/renderers/pluginStatusText.ts +18 -1
- package/src/ui/screens/AliasScreen.tsx +38 -45
- package/src/ui/screens/CliToolsScreen.tsx +199 -98
- package/src/ui/screens/EnvVarsScreen.tsx +17 -5
- package/src/ui/screens/GitignoreScreen.tsx +48 -67
- package/src/ui/screens/McpRegistryScreen.tsx +67 -38
- package/src/ui/screens/McpScreen.tsx +46 -19
- package/src/ui/screens/ModelsScreen.tsx +46 -86
- package/src/ui/screens/PluginsScreen.tsx +315 -259
- package/src/ui/screens/ProfilesScreen.tsx +79 -40
- package/src/ui/screens/SkillsScreen.tsx +83 -80
- package/src/ui/screens/StylesScreen.tsx +211 -275
- package/src/ui/state/AppContext.tsx +66 -44
- package/src/ui/state/DimensionsContext.tsx +3 -22
- package/src/ui/state/reducer.ts +12 -100
- package/src/ui/state/types.ts +48 -69
- package/src/utils/command-utils.ts +4 -5
- package/src/utils/run.ts +123 -52
- package/src/utils/string-utils.ts +9 -0
- package/src/utils/user-path.ts +60 -0
- package/src/__tests__/go-module.test.ts +0 -43
- package/src/__tests__/notices-rules.test.ts +0 -476
- package/src/__tests__/notices.test.tsx +0 -604
- package/src/__tests__/plugin-setup.test.ts +0 -1016
- package/src/__tests__/shell-script-callers.test.ts +0 -92
- package/src/__tests__/styles-status-line.test.ts +0 -72
- package/src/__tests__/version-snapshot.test.ts +0 -212
- package/src/services/plugin-setup.ts +0 -965
- package/src/services/version-snapshot.ts +0 -189
- package/src/utils/go-module.ts +0 -36
|
@@ -1,187 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plugin-requires locates the plugin.json Claude Code loads and hands its
|
|
3
|
+
* `requires` to requires-probe; it parses nothing itself.
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
7
|
+
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
8
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
9
|
import { tmpdir } from "node:os";
|
|
4
10
|
import { join } from "node:path";
|
|
5
11
|
import fs from "fs-extra";
|
|
6
12
|
import {
|
|
7
|
-
parsePluginRequires,
|
|
8
13
|
readPluginRequires,
|
|
14
|
+
resolvePluginRequires,
|
|
15
|
+
selectBins,
|
|
9
16
|
} from "../services/plugin-requires.js";
|
|
17
|
+
import type { Requirement } from "../shared/plugin/hooks/lib/requires-probe.js";
|
|
10
18
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
},
|
|
21
|
-
});
|
|
22
|
-
expect(requires.bin).toHaveLength(2);
|
|
23
|
-
const tmux = requires.bin!.find((b) => b.name === "tmux")!;
|
|
24
|
-
expect(tmux.via).toBe("brew");
|
|
25
|
-
expect(tmux.check).toBe("tmux -V");
|
|
26
|
-
const mcp = requires.bin!.find((b) => b.name === "tmux-mcp")!;
|
|
27
|
-
// check defaulted from the name
|
|
28
|
-
expect(mcp.check).toBe("tmux-mcp --version");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("drops entries with no name or an unknown installer", () => {
|
|
32
|
-
const requires = parsePluginRequires({
|
|
33
|
-
requires: {
|
|
34
|
-
bin: [
|
|
35
|
-
{ via: "bun" }, // no name
|
|
36
|
-
{ name: "x", via: "cargo" }, // unknown installer
|
|
37
|
-
{ name: "ok", via: "npm" },
|
|
38
|
-
],
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
expect(requires.bin).toHaveLength(1);
|
|
42
|
-
expect(requires.bin![0]!.name).toBe("ok");
|
|
43
|
-
});
|
|
19
|
+
const BUN: Requirement = {
|
|
20
|
+
name: "bun",
|
|
21
|
+
check: ["bun", "--version"],
|
|
22
|
+
install: [{ via: "bootstrap", tool: "bun" }],
|
|
23
|
+
};
|
|
24
|
+
const declares = (bin: Requirement[]) => ({
|
|
25
|
+
name: "p",
|
|
26
|
+
requires: { schema: 1, bin },
|
|
27
|
+
});
|
|
44
28
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
});
|
|
29
|
+
let dir: string;
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
dir = await fs.realpath(await mkdtemp(join(tmpdir(), "plugreq-")));
|
|
32
|
+
});
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
await rm(dir, { recursive: true, force: true });
|
|
51
35
|
});
|
|
52
36
|
|
|
53
|
-
describe("
|
|
54
|
-
test("
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
37
|
+
describe("readPluginRequires", () => {
|
|
38
|
+
test("parses .claude-plugin/plugin.json through requires-probe", async () => {
|
|
39
|
+
await fs.outputJson(
|
|
40
|
+
join(dir, ".claude-plugin", "plugin.json"),
|
|
41
|
+
declares([BUN]),
|
|
42
|
+
);
|
|
43
|
+
expect(await readPluginRequires(dir)).toEqual({
|
|
44
|
+
ok: true,
|
|
45
|
+
requirements: [BUN],
|
|
62
46
|
});
|
|
63
|
-
const mcp = r.bin!.find((b) => b.name === "tmux-mcp")!;
|
|
64
|
-
expect(mcp.via).toBe("go");
|
|
65
|
-
expect(mcp.module).toBe("github.com/MadAppGang/tmux-mcp");
|
|
66
|
-
expect(mcp.version).toBe("latest");
|
|
67
|
-
const tmux = r.bin!.find((b) => b.name === "tmux")!;
|
|
68
|
-
expect(tmux.via).toBe("brew");
|
|
69
|
-
expect(tmux.formula).toBe("tmux");
|
|
70
47
|
});
|
|
71
48
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const r = parsePluginRequires({
|
|
76
|
-
name: "terminal",
|
|
49
|
+
test("a setup block is read by nothing: without requires the plugin declares nothing", async () => {
|
|
50
|
+
await fs.outputJson(join(dir, ".claude-plugin", "plugin.json"), {
|
|
51
|
+
name: "p",
|
|
77
52
|
setup: { go: ["github.com/MadAppGang/tmux-mcp/v2@v2.0.0"] },
|
|
78
53
|
});
|
|
79
|
-
expect(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
via: "go",
|
|
83
|
-
module: "github.com/MadAppGang/tmux-mcp/v2",
|
|
84
|
-
version: "v2.0.0",
|
|
85
|
-
check: "tmux-mcp --version",
|
|
86
|
-
},
|
|
87
|
-
]);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// One manifest per spec: entries are keyed by binary name, so a v1 and a v2
|
|
91
|
-
// pin of the same module in ONE setup would (correctly) collapse to one.
|
|
92
|
-
test.each([
|
|
93
|
-
[
|
|
94
|
-
"github.com/MadAppGang/tmux-mcp@v1.7.1",
|
|
95
|
-
{
|
|
96
|
-
name: "tmux-mcp",
|
|
97
|
-
module: "github.com/MadAppGang/tmux-mcp",
|
|
98
|
-
version: "v1.7.1",
|
|
99
|
-
},
|
|
100
|
-
],
|
|
101
|
-
[
|
|
102
|
-
"github.com/MadAppGang/tmux-mcp/v2",
|
|
103
|
-
{
|
|
104
|
-
name: "tmux-mcp",
|
|
105
|
-
module: "github.com/MadAppGang/tmux-mcp/v2",
|
|
106
|
-
version: undefined,
|
|
107
|
-
},
|
|
108
|
-
],
|
|
109
|
-
[
|
|
110
|
-
"github.com/x/v2tool@v1.0.0",
|
|
111
|
-
{ name: "v2tool", module: "github.com/x/v2tool", version: "v1.0.0" },
|
|
112
|
-
],
|
|
113
|
-
])("go spec %s → name/module/version", (spec, expected) => {
|
|
114
|
-
const r = parsePluginRequires({ setup: { go: [spec] } });
|
|
115
|
-
expect(
|
|
116
|
-
r.bin!.map((b) => ({
|
|
117
|
-
name: b.name,
|
|
118
|
-
module: b.module,
|
|
119
|
-
version: b.version,
|
|
120
|
-
})),
|
|
121
|
-
).toEqual([expected]);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("browser-use's setup: pip packages", () => {
|
|
125
|
-
const r = parsePluginRequires({
|
|
126
|
-
name: "browser-use",
|
|
127
|
-
setup: { pip: ["browser-use", "mcp"] },
|
|
54
|
+
expect(await readPluginRequires(dir)).toEqual({
|
|
55
|
+
ok: true,
|
|
56
|
+
requirements: [],
|
|
128
57
|
});
|
|
129
|
-
expect(r.bin!.map((b) => b.name).sort()).toEqual(["browser-use", "mcp"]);
|
|
130
|
-
expect(r.bin!.every((b) => b.via === "pip")).toBe(true);
|
|
131
58
|
});
|
|
132
59
|
|
|
133
|
-
test("
|
|
134
|
-
|
|
135
|
-
|
|
60
|
+
test("an invalid declaration is reported, never dropped to empty", async () => {
|
|
61
|
+
await fs.outputJson(join(dir, ".claude-plugin", "plugin.json"), {
|
|
62
|
+
name: "p",
|
|
63
|
+
requires: { schema: 1, bin: [{ name: "x", via: "brew" }] },
|
|
136
64
|
});
|
|
137
|
-
const
|
|
138
|
-
expect(
|
|
139
|
-
const scoped = r.bin!.find((b) => b.name === "tool")!;
|
|
140
|
-
expect(scoped.package).toBe("@scope/tool");
|
|
141
|
-
expect(scoped.version).toBe("3.0.0");
|
|
65
|
+
const r = await readPluginRequires(dir);
|
|
66
|
+
expect(r?.ok).toBe(false);
|
|
142
67
|
});
|
|
143
68
|
|
|
144
|
-
test("
|
|
145
|
-
|
|
146
|
-
setup: { required: ["tmux"] }, // → brew
|
|
147
|
-
requires: {
|
|
148
|
-
bin: [{ name: "tmux", via: "go", module: "example.com/tmux" }],
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
const tmux = r.bin!.find((b) => b.name === "tmux")!;
|
|
152
|
-
expect(tmux.via).toBe("go"); // requires.bin won
|
|
69
|
+
test("no manifest → null", async () => {
|
|
70
|
+
expect(await readPluginRequires(dir)).toBeNull();
|
|
153
71
|
});
|
|
154
72
|
});
|
|
155
73
|
|
|
156
|
-
describe("
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
74
|
+
describe("resolvePluginRequires reads the copy Claude Code loads", () => {
|
|
75
|
+
test("the installed copy wins over a newer marketplace clone", async () => {
|
|
76
|
+
const root = join(dir, "plugins-root");
|
|
77
|
+
const installed = join(root, "cache", "fixture", "app", "1.0.0");
|
|
78
|
+
const clone = join(dir, "clone", "app");
|
|
79
|
+
await fs.outputJson(
|
|
80
|
+
join(installed, ".claude-plugin", "plugin.json"),
|
|
81
|
+
declares([BUN]),
|
|
82
|
+
);
|
|
83
|
+
await fs.outputJson(join(clone, ".claude-plugin", "plugin.json"), {
|
|
84
|
+
name: "app",
|
|
85
|
+
});
|
|
86
|
+
await fs.outputJson(join(root, "installed_plugins.json"), {
|
|
87
|
+
version: 2,
|
|
88
|
+
plugins: {
|
|
89
|
+
"app@fixture": [
|
|
90
|
+
{
|
|
91
|
+
scope: "project",
|
|
92
|
+
projectPath: dir,
|
|
93
|
+
installPath: installed,
|
|
94
|
+
version: "1.0.0",
|
|
95
|
+
installedAt: "2026-01-01T00:00:00.000Z",
|
|
96
|
+
lastUpdated: "2026-01-01T00:00:00.000Z",
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
const found = await resolvePluginRequires("app@fixture", dir, {
|
|
102
|
+
root,
|
|
103
|
+
clone: async () => clone,
|
|
104
|
+
});
|
|
105
|
+
expect(found?.dir).toBe(installed);
|
|
106
|
+
expect(found?.parse).toEqual({ ok: true, requirements: [BUN] });
|
|
164
107
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
requires: { bin: [{ name: "tmux", via: "brew" }] },
|
|
108
|
+
const notInstalled = await resolvePluginRequires("app@fixture", dir, {
|
|
109
|
+
root: join(dir, "no-registry"),
|
|
110
|
+
clone: async () => clone,
|
|
169
111
|
});
|
|
170
|
-
|
|
171
|
-
expect(r.bin?.[0]?.name).toBe("tmux");
|
|
112
|
+
expect(notInstalled?.dir).toBe(clone);
|
|
172
113
|
});
|
|
114
|
+
});
|
|
173
115
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
116
|
+
describe("selectBins", () => {
|
|
117
|
+
function pathWith(...tools: string[]): string {
|
|
118
|
+
const bin = join(dir, "bin");
|
|
119
|
+
mkdirSync(bin, { recursive: true });
|
|
120
|
+
for (const t of tools) {
|
|
121
|
+
writeFileSync(join(bin, t), "#!/bin/sh\nexit 0\n");
|
|
122
|
+
chmodSync(join(bin, t), 0o755);
|
|
123
|
+
}
|
|
124
|
+
return bin;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
test("selects each requirement's first usable option, once, and records its root", () => {
|
|
128
|
+
const reqs: Requirement[] = [
|
|
129
|
+
BUN,
|
|
130
|
+
{
|
|
131
|
+
name: "claudish",
|
|
132
|
+
check: ["claudish", "--version"],
|
|
133
|
+
install: [{ via: "bun", package: "claudish", version: "6.0.0" }],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "jq",
|
|
137
|
+
check: ["jq", "--version"],
|
|
138
|
+
install: [{ via: "brew", formula: "jq" }],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "zsh",
|
|
142
|
+
check: ["zsh", "--version"],
|
|
143
|
+
install: [{ via: "apt", packages: ["zsh"] }],
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
const selected = selectBins(reqs, "/plugin", pathWith("brew", "apt-get"));
|
|
147
|
+
expect(selected.map((b) => [b.name, b.chosen, b.root])).toEqual([
|
|
148
|
+
// Bun's installer needs curl and unzip, and this PATH has neither.
|
|
149
|
+
["bun", null, "/plugin"],
|
|
150
|
+
[
|
|
151
|
+
"claudish",
|
|
152
|
+
{ via: "bun", package: "claudish", version: "6.0.0" },
|
|
153
|
+
"/plugin",
|
|
154
|
+
],
|
|
155
|
+
["jq", { via: "brew", formula: "jq" }, "/plugin"],
|
|
156
|
+
["zsh", { via: "apt", packages: ["zsh"] }, "/plugin"],
|
|
157
|
+
]);
|
|
158
|
+
// The declaration itself is carried unchanged.
|
|
159
|
+
expect(selected[1]?.check).toEqual(["claudish", "--version"]);
|
|
182
160
|
});
|
|
183
161
|
|
|
184
|
-
test("
|
|
185
|
-
|
|
162
|
+
test("a brew option is not selected without brew", () => {
|
|
163
|
+
const reqs: Requirement[] = [
|
|
164
|
+
{
|
|
165
|
+
name: "jq",
|
|
166
|
+
check: ["jq", "--version"],
|
|
167
|
+
install: [{ via: "brew", formula: "jq" }],
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
expect(selectBins(reqs, undefined, pathWith())[0]?.chosen).toBeNull();
|
|
186
171
|
});
|
|
187
172
|
});
|
|
@@ -191,17 +191,11 @@ describe("isRelevantInstallation", () => {
|
|
|
191
191
|
).toBe(false);
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
-
test("two rootless paths
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
// a
|
|
199
|
-
//
|
|
200
|
-
// The port used to answer false here, which made magus name a
|
|
201
|
-
// `loadedVersion` the loader would not pick in any non-git directory. It is
|
|
202
|
-
// the loader's answer this module owes, not the more cautious one; what
|
|
203
|
-
// magus may WRITE is a separate question, pinned in stale-load-repair.test.ts
|
|
204
|
-
// ("a rootless row from another directory is relevant but NOT rewritten").
|
|
194
|
+
test("two rootless paths do NOT match each other (Claude Code 2.1.284)", () => {
|
|
195
|
+
// 2.1.284 guards the root comparison with `cwdRoot !== null`. Before it,
|
|
196
|
+
// two nulls compared equal and every rootless folder shared every other
|
|
197
|
+
// rootless folder's rows. Measured with `--debug-file` in a rootless folder:
|
|
198
|
+
// a row from an existing non-git folder did not load there.
|
|
205
199
|
const loose = path.join(base, "loose");
|
|
206
200
|
expect(
|
|
207
201
|
isRelevantInstallation(
|
|
@@ -212,12 +206,57 @@ describe("isRelevantInstallation", () => {
|
|
|
212
206
|
loose,
|
|
213
207
|
repoRootForPath(loose),
|
|
214
208
|
),
|
|
209
|
+
).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("in a rootless folder its own row and USER rows still count", () => {
|
|
213
|
+
const loose = path.join(base, "loose");
|
|
214
|
+
const looseRoot = repoRootForPath(loose);
|
|
215
|
+
expect(looseRoot).toBeNull();
|
|
216
|
+
expect(
|
|
217
|
+
isRelevantInstallation(
|
|
218
|
+
entry({ version: "1.0.0", projectPath: loose }),
|
|
219
|
+
loose,
|
|
220
|
+
looseRoot,
|
|
221
|
+
),
|
|
222
|
+
).toBe(true);
|
|
223
|
+
expect(
|
|
224
|
+
isRelevantInstallation(
|
|
225
|
+
entry({ version: "1.0.0", scope: "user", projectPath: undefined }),
|
|
226
|
+
loose,
|
|
227
|
+
looseRoot,
|
|
228
|
+
),
|
|
229
|
+
).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("a row's path is compared RAW, as the binary compares it: '/p/a/' is not '/p/a' in a rootless folder", () => {
|
|
233
|
+
// Normalising the row's path made this pair relevant — and in a rootless
|
|
234
|
+
// folder nothing else could, since the root comparison matches nothing —
|
|
235
|
+
// so the port named a row the loader rejects.
|
|
236
|
+
const loose = path.join(base, "loose");
|
|
237
|
+
const looseRoot = repoRootForPath(loose);
|
|
238
|
+
expect(looseRoot).toBeNull();
|
|
239
|
+
expect(
|
|
240
|
+
isRelevantInstallation(
|
|
241
|
+
entry({ version: "1.0.0", projectPath: `${loose}/` }),
|
|
242
|
+
loose,
|
|
243
|
+
looseRoot,
|
|
244
|
+
),
|
|
245
|
+
).toBe(false);
|
|
246
|
+
// The caller's directory stands in for the binary's own working
|
|
247
|
+
// directory, which is always absolute with no trailing slash.
|
|
248
|
+
expect(
|
|
249
|
+
isRelevantInstallation(
|
|
250
|
+
entry({ version: "1.0.0", projectPath: loose }),
|
|
251
|
+
`${loose}/`,
|
|
252
|
+
looseRoot,
|
|
253
|
+
),
|
|
215
254
|
).toBe(true);
|
|
216
255
|
});
|
|
217
256
|
|
|
218
257
|
test("a rootless row is still irrelevant to a directory inside a repository", () => {
|
|
219
|
-
//
|
|
220
|
-
//
|
|
258
|
+
// `cwdRoot` is a real root here, so a rootless row's null root does not
|
|
259
|
+
// equal it.
|
|
221
260
|
expect(
|
|
222
261
|
isRelevantInstallation(
|
|
223
262
|
entry({ version: "1.0.0", projectPath: path.join(base, "loose") }),
|
|
@@ -140,7 +140,7 @@ describe("pluginRowText — orphans say what the CLI says (F9)", () => {
|
|
|
140
140
|
// match, and it cannot tell a move from a dual publication: claudish ships
|
|
141
141
|
// to two marketplaces with both copies installed, and the row announced
|
|
142
142
|
// "moved → magus" about a plugin that had not moved anywhere.
|
|
143
|
-
test("an orphan is
|
|
143
|
+
test("an orphan is deprecated, and never names a destination", () => {
|
|
144
144
|
const seo = info({
|
|
145
145
|
id: "seo@magus",
|
|
146
146
|
version: "1.8.0",
|
|
@@ -149,16 +149,15 @@ describe("pluginRowText — orphans say what the CLI says (F9)", () => {
|
|
|
149
149
|
projectScope: { installed: true, version: "1.8.0" },
|
|
150
150
|
});
|
|
151
151
|
const row = rows([seo]).get(seo.id) as PluginPluginItem;
|
|
152
|
-
expect(cliNote([seo], seo.id)).toBe(
|
|
153
|
-
|
|
154
|
-
);
|
|
152
|
+
expect(cliNote([seo], seo.id)).toBe("no longer published");
|
|
153
|
+
// The row's own words, not the note: the verdict and the key that acts.
|
|
155
154
|
expect(pluginRowText(row).text).toBe(
|
|
156
|
-
" v1.8.0
|
|
155
|
+
" v1.8.0 deprecated — press a to remove",
|
|
157
156
|
);
|
|
158
157
|
expect(pluginRowText(row).text).not.toMatch(/moved/i);
|
|
159
158
|
});
|
|
160
159
|
|
|
161
|
-
test("an orphan from a marketplace we DID read is
|
|
160
|
+
test("an orphan from a marketplace we DID read is deprecated", () => {
|
|
162
161
|
const gone = info({
|
|
163
162
|
id: "gone@magus",
|
|
164
163
|
version: "2.0.0",
|
|
@@ -168,14 +167,12 @@ describe("pluginRowText — orphans say what the CLI says (F9)", () => {
|
|
|
168
167
|
});
|
|
169
168
|
const row = rows([gone]).get(gone.id) as PluginPluginItem;
|
|
170
169
|
expect(pluginRowText(row).text).toBe(
|
|
171
|
-
|
|
172
|
-
);
|
|
173
|
-
expect(pluginRowText(row).text).toContain(
|
|
174
|
-
"not offered by any configured marketplace",
|
|
170
|
+
" v2.0.0 deprecated — press a to remove",
|
|
175
171
|
);
|
|
172
|
+
expect(row.status?.action).toBe("remove");
|
|
176
173
|
});
|
|
177
174
|
|
|
178
|
-
test("the old words are gone: no '
|
|
175
|
+
test("the old words are gone: no 'moved →', and no marketplace wording", () => {
|
|
179
176
|
const seo = info({
|
|
180
177
|
id: "seo@magus",
|
|
181
178
|
installedVersion: "1.8.0",
|
|
@@ -183,14 +180,14 @@ describe("pluginRowText — orphans say what the CLI says (F9)", () => {
|
|
|
183
180
|
projectScope: { installed: true, version: "1.8.0" },
|
|
184
181
|
});
|
|
185
182
|
const { text } = pluginRowText(rows([seo]).get(seo.id) as PluginPluginItem);
|
|
186
|
-
expect(text).not.
|
|
183
|
+
expect(text).not.toMatch(/marketplace/i);
|
|
187
184
|
expect(text).not.toContain("moved →");
|
|
188
185
|
});
|
|
189
186
|
});
|
|
190
187
|
|
|
191
|
-
describe("pluginRowText —
|
|
192
|
-
const
|
|
193
|
-
info({
|
|
188
|
+
describe("pluginRowText — the current version and the state, no history", () => {
|
|
189
|
+
const row = (over: Partial<PluginInfo>) => {
|
|
190
|
+
const p = info({
|
|
194
191
|
id: "terminal@magus",
|
|
195
192
|
version: "3.3.1",
|
|
196
193
|
installedVersion: "3.0.1",
|
|
@@ -198,40 +195,36 @@ describe("pluginRowText — history badges combine with the state", () => {
|
|
|
198
195
|
author: { name: "MadAppGang" },
|
|
199
196
|
...over,
|
|
200
197
|
});
|
|
198
|
+
return rows([p]).get(p.id) as PluginPluginItem;
|
|
199
|
+
};
|
|
201
200
|
|
|
202
|
-
test("
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const { text, segments } = pluginRowText(row);
|
|
207
|
-
expect(text).toBe(" v2.9.0 → v3.0.1 updated → v3.3.1 available");
|
|
208
|
-
expect(segments.map((s) => s.tone)).toEqual(["success", "warning"]);
|
|
201
|
+
test("a pending update: the installed version, then what is available", () => {
|
|
202
|
+
const { text, segments } = pluginRowText(row({}));
|
|
203
|
+
expect(text).toBe(" v3.0.1 → v3.3.1 available");
|
|
204
|
+
expect(segments.map((s) => s.tone)).toEqual(["muted", "warning"]);
|
|
209
205
|
});
|
|
210
206
|
|
|
211
|
-
test("'new'
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
207
|
+
test("NEGATIVE CONTROL: no 'updated' or 'new' badge — the row has no memory of earlier versions", () => {
|
|
208
|
+
// The badge rendered "v1.8.0 → v1.7.4 updated → v1.8.0 available": a
|
|
209
|
+
// downgrade called "updated". A row now says nothing about what changed.
|
|
210
|
+
const text = pluginRowText(row({})).text;
|
|
211
|
+
expect(text).not.toContain("updated");
|
|
212
|
+
expect(text).not.toContain(" new");
|
|
215
213
|
});
|
|
216
214
|
|
|
217
|
-
test("
|
|
218
|
-
const
|
|
219
|
-
recentlyInstalled: true,
|
|
215
|
+
test("an unknown version is not printed", () => {
|
|
216
|
+
const r = row({
|
|
220
217
|
version: "0.0.0",
|
|
221
218
|
installedVersion: "0.0.0",
|
|
222
219
|
projectScope: { installed: true, version: "0.0.0" },
|
|
223
220
|
});
|
|
224
|
-
|
|
225
|
-
expect(pluginRowText(row).text).toBe(" new");
|
|
221
|
+
expect(pluginRowText(r).text).not.toContain("v0.0.0");
|
|
226
222
|
});
|
|
227
223
|
|
|
228
224
|
test("a current row shows only its version", () => {
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const row = rows([p]).get(p.id) as PluginPluginItem;
|
|
233
|
-
expect(row.status?.action).toBe("current");
|
|
234
|
-
expect(pluginRowText(row).text).toBe(" v3.0.1");
|
|
225
|
+
const r = row({ version: "3.0.1" });
|
|
226
|
+
expect(r.status?.action).toBe("current");
|
|
227
|
+
expect(pluginRowText(r).text).toBe(" v3.0.1");
|
|
235
228
|
});
|
|
236
229
|
});
|
|
237
230
|
|
|
@@ -141,6 +141,13 @@ describe("the render enables what the profile's plugins depend on", () => {
|
|
|
141
141
|
"lib@mkt",
|
|
142
142
|
"shared@other",
|
|
143
143
|
]);
|
|
144
|
+
// Which plugin asked for each — what `magus update` names when a derived
|
|
145
|
+
// dependency is no longer published.
|
|
146
|
+
expect(closure.dependencies?.requiredBy).toEqual({
|
|
147
|
+
"lib@mkt": "app@mkt",
|
|
148
|
+
"core@mkt": "lib@mkt",
|
|
149
|
+
"shared@other": "core@mkt",
|
|
150
|
+
});
|
|
144
151
|
await start(manifest, "only");
|
|
145
152
|
expect(await enabled()).toEqual({
|
|
146
153
|
"app@mkt": true,
|
|
@@ -60,7 +60,6 @@ const out = {
|
|
|
60
60
|
described: null,
|
|
61
61
|
options: null,
|
|
62
62
|
updateFormatValue: startup.UPDATE_FORMAT_VALUE,
|
|
63
|
-
notices: null,
|
|
64
63
|
error: null,
|
|
65
64
|
};
|
|
66
65
|
try {
|
|
@@ -78,7 +77,6 @@ try {
|
|
|
78
77
|
mode === "convert" ? { convert: true } : undefined,
|
|
79
78
|
);
|
|
80
79
|
out.need = { state: prepared.need.state };
|
|
81
|
-
out.notices = prepared.notices;
|
|
82
80
|
}
|
|
83
81
|
} catch (e) {
|
|
84
82
|
out.error = e instanceof Error ? (e.stack ?? e.message) : String(e);
|
|
@@ -93,7 +91,6 @@ interface OldFormatRun {
|
|
|
93
91
|
described: { title: string; message: string } | null;
|
|
94
92
|
options: Array<{ label: string; value: string; meta?: string }> | null;
|
|
95
93
|
updateFormatValue: string;
|
|
96
|
-
notices: string[] | null;
|
|
97
94
|
error: string | null;
|
|
98
95
|
}
|
|
99
96
|
|