magus-cli 7.10.3 → 7.10.4
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 +4 -4
- package/src/__tests__/blackbox-convergence.test.ts +0 -1
- package/src/__tests__/blackbox-parity.test.ts +2 -2
- package/src/__tests__/blackbox-tool-state.test.ts +8 -18
- package/src/__tests__/blind/c14-cli-row-label.test.ts +8 -3
- package/src/__tests__/blind/c15-probe-ownership.test.ts +11 -11
- package/src/__tests__/blind/c16-probe-adopts-node-global.test.ts +16 -12
- package/src/__tests__/blind/c17-enter-label.test.ts +42 -7
- package/src/__tests__/blind/c18-probe-adopts-homebrew-keg.test.ts +36 -19
- package/src/__tests__/blind/c19-probe-owner-version-shadow.test.ts +38 -35
- package/src/__tests__/cli-tool-commands.test.ts +2 -2
- package/src/__tests__/cli-tool-external-row.test.tsx +112 -0
- package/src/__tests__/cli-tools-adapter.test.ts +32 -22
- package/src/__tests__/cli-update-view.test.ts +124 -51
- package/src/__tests__/fixtures/bins.ts +9 -2
- package/src/__tests__/npm-package-name.test.ts +3 -2
- package/src/__tests__/tool-adopt-brew-owner.test.ts +167 -0
- package/src/__tests__/tool-probe.test.ts +231 -144
- package/src/__tests__/tool-rows-followups.test.ts +315 -0
- package/src/__tests__/tool-rows-reproduction.test.ts +630 -0
- package/src/__tests__/tool-versions.test.ts +164 -0
- package/src/__tests__/toolchain.test.ts +0 -2
- package/src/__tests__/update-plan.test.ts +40 -9
- package/src/cli/update-view.ts +180 -76
- package/src/cli/update.ts +44 -37
- package/src/data/cli-tools.ts +5 -10
- package/src/data/tool-versions.ts +137 -0
- package/src/services/cli-tool-commands.ts +1 -2
- package/src/services/cli-tool-version.ts +10 -88
- package/src/services/resolver.ts +9 -15
- package/src/services/tool-probe.ts +537 -691
- package/src/services/toolchain.ts +10 -4
- package/src/services/update-engine.ts +9 -15
- package/src/services/update-plan.ts +49 -21
- package/src/types/index.ts +0 -2
- package/src/ui/adapters/cliToolsAdapter.ts +31 -22
- package/src/ui/renderers/cliToolRenderers.tsx +15 -21
- package/src/ui/state/types.ts +0 -1
- package/src/__tests__/blind/c9-cli-tool-version.test.ts +0 -239
|
@@ -4,6 +4,22 @@
|
|
|
4
4
|
* (uv's receipt), and reports a newer copy hidden later on PATH.
|
|
5
5
|
*
|
|
6
6
|
* Written from the spec only, through a scripted fake `ToolProbeIO`.
|
|
7
|
+
*
|
|
8
|
+
* Merged with the tool-rows design (docs/plans/2026-10-08-magus-update-tool-rows.md,
|
|
9
|
+
* decision 7). Kept as written: the uv receipt cases (A1–A5) and the guards.
|
|
10
|
+
* Changed, because they contradict that design's rules:
|
|
11
|
+
* - An `external` row carries no note, so the owner sentences (A6, B7–B10,
|
|
12
|
+
* C11–C12) now expect none. They were read off Cellar, Caskroom and cargo
|
|
13
|
+
* paths, which the design forbids. A pinned tool no installer has (B10) is
|
|
14
|
+
* `external`, not `unknown`.
|
|
15
|
+
* - zsh's version comes from magus's own version command (`zsh --version`,
|
|
16
|
+
* bare name), not from running a file chosen by a path test, so C's fake
|
|
17
|
+
* world answers that command.
|
|
18
|
+
* - A copy later on PATH is counted (`otherCopies`), never described: its
|
|
19
|
+
* owner and version were read off its path, and the warning named a PATH
|
|
20
|
+
* folder with nothing for the user to run. So D expects a count, and an
|
|
21
|
+
* older copy (D14) counts too — telling older from newer would need that
|
|
22
|
+
* same guess.
|
|
7
23
|
*/
|
|
8
24
|
import { describe, expect, test } from "bun:test";
|
|
9
25
|
import {
|
|
@@ -208,11 +224,11 @@ describe("C19 A: uv's own installer is a driven owner", () => {
|
|
|
208
224
|
expect(f.upgradeCommand).toBeUndefined();
|
|
209
225
|
const s = classifyTool(spec, f);
|
|
210
226
|
expect(s.state).toBe("external");
|
|
211
|
-
expect(s.note).
|
|
227
|
+
expect(s.note).toBeUndefined();
|
|
212
228
|
});
|
|
213
229
|
});
|
|
214
230
|
|
|
215
|
-
describe("C19 B:
|
|
231
|
+
describe("C19 B: a file no driven installer has is installed elsewhere, with no note", () => {
|
|
216
232
|
const spec = (
|
|
217
233
|
name: string,
|
|
218
234
|
extra: Partial<BinRequirement> = {},
|
|
@@ -239,7 +255,7 @@ describe("C19 B: KEPT notes name who installed it", () => {
|
|
|
239
255
|
const f = await probeTool(sp, fakeIO(world("madbench", file)));
|
|
240
256
|
const s = classifyTool(sp, f);
|
|
241
257
|
expect(s.state).toBe("external");
|
|
242
|
-
expect(s.note).
|
|
258
|
+
expect(s.note).toBeUndefined();
|
|
243
259
|
});
|
|
244
260
|
|
|
245
261
|
test("8a. cargo bin under the home directory", async () => {
|
|
@@ -247,7 +263,7 @@ describe("C19 B: KEPT notes name who installed it", () => {
|
|
|
247
263
|
const f = await probeTool(sp, fakeIO(world("rg", "/home/u/.cargo/bin/rg")));
|
|
248
264
|
const s = classifyTool(sp, f);
|
|
249
265
|
expect(s.state).toBe("external");
|
|
250
|
-
expect(s.note).
|
|
266
|
+
expect(s.note).toBeUndefined();
|
|
251
267
|
});
|
|
252
268
|
|
|
253
269
|
test("8b. cargo bin under CARGO_HOME", async () => {
|
|
@@ -258,7 +274,7 @@ describe("C19 B: KEPT notes name who installed it", () => {
|
|
|
258
274
|
);
|
|
259
275
|
const s = classifyTool(sp, f);
|
|
260
276
|
expect(s.state).toBe("external");
|
|
261
|
-
expect(s.note).
|
|
277
|
+
expect(s.note).toBeUndefined();
|
|
262
278
|
});
|
|
263
279
|
|
|
264
280
|
test("9. unrecognised place, never says 'outside'", async () => {
|
|
@@ -269,18 +285,15 @@ describe("C19 B: KEPT notes name who installed it", () => {
|
|
|
269
285
|
);
|
|
270
286
|
const s = classifyTool(sp, f);
|
|
271
287
|
expect(s.state).toBe("external");
|
|
272
|
-
expect(s.note).
|
|
273
|
-
expect(s.note ?? "").not.toContain("outside");
|
|
288
|
+
expect(s.note).toBeUndefined();
|
|
274
289
|
});
|
|
275
290
|
|
|
276
291
|
test("10. pinned spec with a cargo file", async () => {
|
|
277
292
|
const sp = spec("rg", { version: "1.0.0" });
|
|
278
293
|
const f = await probeTool(sp, fakeIO(world("rg", "/home/u/.cargo/bin/rg")));
|
|
279
294
|
const s = classifyTool(sp, f);
|
|
280
|
-
expect(s.state).toBe("
|
|
281
|
-
expect(s.note).
|
|
282
|
-
"installed by cargo, so magus cannot move it to the pin 1.0.0",
|
|
283
|
-
);
|
|
295
|
+
expect(s.state).toBe("external");
|
|
296
|
+
expect(s.note).toBeUndefined();
|
|
284
297
|
});
|
|
285
298
|
});
|
|
286
299
|
|
|
@@ -291,7 +304,7 @@ describe("C19 C: operating-system programs show their version", () => {
|
|
|
291
304
|
real: { "/bin/zsh": "/bin/zsh", ...CELLAR },
|
|
292
305
|
exec: {
|
|
293
306
|
...BREW_PREFIX_OK,
|
|
294
|
-
...(zshExec ? { "
|
|
307
|
+
...(zshExec ? { "zsh --version": zshExec } : {}),
|
|
295
308
|
},
|
|
296
309
|
});
|
|
297
310
|
|
|
@@ -303,7 +316,7 @@ describe("C19 C: operating-system programs show their version", () => {
|
|
|
303
316
|
expect(f.installed).toBe("5.9");
|
|
304
317
|
const s = classifyTool(zsh, f);
|
|
305
318
|
expect(s.state).toBe("external");
|
|
306
|
-
expect(s.note).
|
|
319
|
+
expect(s.note).toBeUndefined();
|
|
307
320
|
});
|
|
308
321
|
|
|
309
322
|
test("12a. --version exits non-zero: still external, installed null", async () => {
|
|
@@ -314,7 +327,7 @@ describe("C19 C: operating-system programs show their version", () => {
|
|
|
314
327
|
expect(f.installed).toBeNull();
|
|
315
328
|
const s = classifyTool(zsh, f);
|
|
316
329
|
expect(s.state).toBe("external");
|
|
317
|
-
expect(s.note).
|
|
330
|
+
expect(s.note).toBeUndefined();
|
|
318
331
|
});
|
|
319
332
|
|
|
320
333
|
test("12b. --version missing: still external, installed null", async () => {
|
|
@@ -322,11 +335,11 @@ describe("C19 C: operating-system programs show their version", () => {
|
|
|
322
335
|
expect(f.installed).toBeNull();
|
|
323
336
|
const s = classifyTool(zsh, f);
|
|
324
337
|
expect(s.state).toBe("external");
|
|
325
|
-
expect(s.note).
|
|
338
|
+
expect(s.note).toBeUndefined();
|
|
326
339
|
});
|
|
327
340
|
});
|
|
328
341
|
|
|
329
|
-
describe("C19 D:
|
|
342
|
+
describe("C19 D: another copy later on PATH is counted", () => {
|
|
330
343
|
const BREW_BIN = "/opt/homebrew/bin/uv";
|
|
331
344
|
const cellarFile = (v: string) => `/opt/homebrew/Cellar/uv/${v}/bin/uv`;
|
|
332
345
|
const world = (second: string, secondReal: string): FakeSetup =>
|
|
@@ -338,52 +351,42 @@ describe("C19 D: a newer copy later on PATH is reported", () => {
|
|
|
338
351
|
env: { HOMEBREW_PREFIX: PREFIX },
|
|
339
352
|
},
|
|
340
353
|
});
|
|
341
|
-
const NOTE =
|
|
342
|
-
"Homebrew also has 0.13.0, unused: ~/.local/bin comes first on PATH";
|
|
343
354
|
|
|
344
|
-
test("13.
|
|
355
|
+
test("13. a Homebrew copy later on PATH: counted, no path, no warning", async () => {
|
|
345
356
|
const f = await probeTool(
|
|
346
357
|
UV,
|
|
347
358
|
fakeIO(world(BREW_BIN, cellarFile("0.13.0"))),
|
|
348
359
|
);
|
|
349
|
-
expect(f.
|
|
350
|
-
expect(f
|
|
351
|
-
{ owner: "Homebrew", version: "0.13.0", dir: "/opt/homebrew/bin" },
|
|
352
|
-
]);
|
|
353
|
-
expect(classifyTool(UV, f).shadowed).toBe(NOTE);
|
|
360
|
+
expect(f.otherCopies).toBe(1);
|
|
361
|
+
expect(JSON.stringify(classifyTool(UV, f))).not.toContain("/");
|
|
354
362
|
});
|
|
355
363
|
|
|
356
|
-
test("14. older later copy:
|
|
364
|
+
test("14. an older later copy is counted the same: no version is guessed", async () => {
|
|
357
365
|
const f = await probeTool(
|
|
358
366
|
UV,
|
|
359
367
|
fakeIO(world(BREW_BIN, cellarFile("0.10.0"))),
|
|
360
368
|
);
|
|
361
|
-
expect(f.otherCopies).
|
|
362
|
-
{ owner: "Homebrew", version: "0.10.0", dir: "/opt/homebrew/bin" },
|
|
363
|
-
]);
|
|
364
|
-
expect(classifyTool(UV, f).shadowed).toBeUndefined();
|
|
369
|
+
expect(f.otherCopies).toBe(1);
|
|
365
370
|
});
|
|
366
371
|
|
|
367
372
|
test("15. a symlink to the running file is not another copy", async () => {
|
|
368
373
|
const f = await probeTool(UV, fakeIO(world("/usr/local/bin/uv", UV_FILE)));
|
|
369
|
-
expect(f.otherCopies
|
|
370
|
-
expect(classifyTool(UV, f).shadowed).toBeUndefined();
|
|
374
|
+
expect(f.otherCopies).toBeUndefined();
|
|
371
375
|
});
|
|
372
376
|
|
|
373
377
|
test("16. IO without resolveAllExecutables: nothing reported, nothing throws", async () => {
|
|
374
378
|
const w = world(BREW_BIN, cellarFile("0.13.0"));
|
|
375
379
|
w.noAll = true;
|
|
376
380
|
const f = await probeTool(UV, fakeIO(w));
|
|
377
|
-
expect(f.otherCopies
|
|
378
|
-
expect(classifyTool(UV, f).shadowed).toBeUndefined();
|
|
381
|
+
expect(f.otherCopies).toBeUndefined();
|
|
379
382
|
});
|
|
380
383
|
|
|
381
|
-
test("17.
|
|
384
|
+
test("17. a current row is counted too", async () => {
|
|
382
385
|
const w = world(BREW_BIN, cellarFile("0.13.0"));
|
|
383
386
|
w.fetch = pypi("0.11.15");
|
|
384
387
|
const f = await probeTool(UV, fakeIO(w));
|
|
385
388
|
const s = classifyTool(UV, f);
|
|
386
389
|
expect(s.state).toBe("current");
|
|
387
|
-
expect(
|
|
390
|
+
expect(f.otherCopies).toBe(1);
|
|
388
391
|
});
|
|
389
392
|
});
|
|
@@ -93,11 +93,12 @@ describe("the catalogue is well-formed", () => {
|
|
|
93
93
|
const declared = declaredRequirements();
|
|
94
94
|
expect(declared.size).toBeGreaterThan(0);
|
|
95
95
|
for (const entry of catalogue) {
|
|
96
|
+
// No entry carries a version command: that is data/tool-versions.ts.
|
|
97
|
+
expect("checkCommand" in entry).toBe(false);
|
|
96
98
|
if (isOwnedEntry(entry)) {
|
|
97
99
|
const [plugin] = entry.ownedBy.split("@");
|
|
98
100
|
expect(declared.get(entry.name) ?? []).toContain(plugin);
|
|
99
101
|
expect("packageName" in entry).toBe(false);
|
|
100
|
-
expect("checkCommand" in entry).toBe(false);
|
|
101
102
|
} else {
|
|
102
103
|
expect(declared.has(entry.name)).toBe(false);
|
|
103
104
|
}
|
|
@@ -124,7 +125,6 @@ describe("the catalogue is well-formed", () => {
|
|
|
124
125
|
expect(
|
|
125
126
|
claudish && !isPluginManagedItem(claudish) && claudish.tool,
|
|
126
127
|
).toMatchObject({
|
|
127
|
-
checkCommand: "claudish --version",
|
|
128
128
|
packageManager: "bun",
|
|
129
129
|
packageName: "claudish",
|
|
130
130
|
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tool installed another way (`external`), and one held back, on the CLI
|
|
3
|
+
* Tools screen (docs/plans/2026-10-08-magus-update-tool-rows.md, decisions 5
|
|
4
|
+
* and 6).
|
|
5
|
+
*
|
|
6
|
+
* The screen is an inventory, so the row stays — but it is not a warning and
|
|
7
|
+
* not "Up to date": no `?`, a muted icon, "installed elsewhere", a detail
|
|
8
|
+
* status that says the same, and no Enter action. The list icon and the
|
|
9
|
+
* detail Status line are if-chains the compiler does not check, which is why
|
|
10
|
+
* each branch is pinned here. Rendered through OpenTUI's test renderer; no IO.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, expect, test } from "bun:test";
|
|
14
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
15
|
+
import { act } from "react";
|
|
16
|
+
import { cliTools } from "../data/cli-tools.js";
|
|
17
|
+
import type { CliTool } from "../data/cli-tools.js";
|
|
18
|
+
import type { ToolStatus } from "../services/tool-probe.js";
|
|
19
|
+
import type { CliToolRow } from "../ui/adapters/cliToolsAdapter.js";
|
|
20
|
+
import {
|
|
21
|
+
renderCliToolDetail,
|
|
22
|
+
renderCliToolRow,
|
|
23
|
+
} from "../ui/renderers/cliToolRenderers.js";
|
|
24
|
+
|
|
25
|
+
const TOOL = cliTools.find((t) => t.name === "crush") as CliTool;
|
|
26
|
+
|
|
27
|
+
function row(status: Pick<ToolStatus, "state" | "reason">): CliToolRow {
|
|
28
|
+
return {
|
|
29
|
+
tool: TOOL,
|
|
30
|
+
spec: { name: TOOL.name, via: "npm", package: TOOL.packageName },
|
|
31
|
+
checking: false,
|
|
32
|
+
status: {
|
|
33
|
+
name: TOOL.name,
|
|
34
|
+
installed: "0.55.0",
|
|
35
|
+
latest: null,
|
|
36
|
+
target: null,
|
|
37
|
+
...status,
|
|
38
|
+
},
|
|
39
|
+
updateCommand: { cmd: "npm", args: ["install", "-g", "x@latest"] },
|
|
40
|
+
installCommand: { cmd: "npm", args: ["install", "-g", "x"] },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const EXTERNAL = row({ state: "external", reason: "installed-elsewhere" });
|
|
45
|
+
const CURRENT = row({ state: "current", reason: "up-to-date" });
|
|
46
|
+
|
|
47
|
+
async function render(node: React.ReactNode, width = 80) {
|
|
48
|
+
const { captureCharFrame, captureSpans, renderOnce, renderer } =
|
|
49
|
+
await testRender(<box flexDirection="column">{node}</box>, {
|
|
50
|
+
width,
|
|
51
|
+
height: 24,
|
|
52
|
+
});
|
|
53
|
+
await act(async () => {
|
|
54
|
+
await renderOnce();
|
|
55
|
+
await renderOnce();
|
|
56
|
+
});
|
|
57
|
+
const out = { chars: captureCharFrame(), lines: captureSpans().lines };
|
|
58
|
+
await act(async () => renderer.destroy());
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The colour of the row's ● icon. */
|
|
63
|
+
async function iconColour(r: CliToolRow): Promise<string> {
|
|
64
|
+
const { lines } = await render(renderCliToolRow(r, 0, false));
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
const span = line.spans.find((s) => s.text.includes("●"));
|
|
67
|
+
if (span) return JSON.stringify(span.fg);
|
|
68
|
+
}
|
|
69
|
+
throw new Error("no ● icon on the row");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe("an external CLI tool row", () => {
|
|
73
|
+
test("the row says installed elsewhere, with no ?", async () => {
|
|
74
|
+
const { chars } = await render(renderCliToolRow(EXTERNAL, 0, false));
|
|
75
|
+
expect(chars).toContain("● Crush v0.55.0 installed elsewhere");
|
|
76
|
+
expect(chars).not.toContain("?");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("its icon is muted, never the success colour of a current tool", async () => {
|
|
80
|
+
expect(await iconColour(EXTERNAL)).not.toBe(await iconColour(CURRENT));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("the detail Status says Installed elsewhere, never Up to date, and offers no Enter", async () => {
|
|
84
|
+
const { chars } = await render(renderCliToolDetail(EXTERNAL, 70), 70);
|
|
85
|
+
expect(chars).toContain("Status ● Installed elsewhere");
|
|
86
|
+
expect(chars).not.toContain("Up to date");
|
|
87
|
+
expect(chars).not.toContain("Enter");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("control: a current tool's detail still says Up to date", async () => {
|
|
91
|
+
const { chars } = await render(renderCliToolDetail(CURRENT, 70), 70);
|
|
92
|
+
expect(chars).toContain("Up to date");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// A held-back tool was checked — brew says it is behind and pinned — so it is
|
|
97
|
+
// "held back" here as in `magus update`: never `?`, never "not checked".
|
|
98
|
+
const HELD = row({ state: "unknown", reason: "held-back" });
|
|
99
|
+
|
|
100
|
+
describe("a held-back CLI tool row", () => {
|
|
101
|
+
test("the row says held back, with no ?", async () => {
|
|
102
|
+
const { chars } = await render(renderCliToolRow(HELD, 0, false));
|
|
103
|
+
expect(chars).toContain("‖ Crush v0.55.0 held back");
|
|
104
|
+
expect(chars).not.toContain("?");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("the detail Status says Held back, never not checked", async () => {
|
|
108
|
+
const { chars } = await render(renderCliToolDetail(HELD, 70), 70);
|
|
109
|
+
expect(chars).toContain("Status ‖ Held back");
|
|
110
|
+
expect(chars).not.toContain("not checked");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -19,6 +19,7 @@ import type { ResolvedBin } from "../types/index.js";
|
|
|
19
19
|
import {
|
|
20
20
|
type PluginManagedRow,
|
|
21
21
|
buildToolRow,
|
|
22
|
+
cliToolEnterAction,
|
|
22
23
|
cliToolStatusText,
|
|
23
24
|
countToolRows,
|
|
24
25
|
isPluginManagedRow,
|
|
@@ -85,22 +86,21 @@ describe("buildToolRow — which installer a catalogue row is judged through", (
|
|
|
85
86
|
const spec = specForCatalogTool(codex, [declared], install);
|
|
86
87
|
expect(spec).toBe(declared);
|
|
87
88
|
|
|
88
|
-
// Through npm, the bun-installed file is not npm's:
|
|
89
|
+
// Through npm, the bun-installed file is not npm's: installed elsewhere.
|
|
89
90
|
const f = facts("codex", {
|
|
90
91
|
managed: false,
|
|
91
92
|
resolvedPath:
|
|
92
93
|
"/Users/j/.bun/install/global/node_modules/@openai/codex/bin/codex.js",
|
|
93
|
-
owner: "npm (root /Users/j/.local/lib/node_modules)",
|
|
94
94
|
});
|
|
95
95
|
const row = buildToolRow(codex, spec, f, install);
|
|
96
96
|
const [cli] = planBinUpdates([declared], [f]);
|
|
97
97
|
expect(row.status).toMatchObject({
|
|
98
98
|
state: cli!.action,
|
|
99
99
|
reason: cli!.reason,
|
|
100
|
-
note: cli!.note,
|
|
101
100
|
target: cli!.target,
|
|
102
101
|
});
|
|
103
|
-
expect(row.status?.
|
|
102
|
+
expect(row.status?.note).toBe(cli!.note);
|
|
103
|
+
expect(row.status?.reason).toBe("installed-elsewhere");
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
test("pinned and behind: the row's key installs the PIN, exactly what the CLI runs", () => {
|
|
@@ -197,7 +197,7 @@ describe("buildToolRow — which installer a catalogue row is judged through", (
|
|
|
197
197
|
expect(row.installCommand).toEqual(cliToolInstall(codex));
|
|
198
198
|
});
|
|
199
199
|
|
|
200
|
-
test("detected pnpm → the catalogue's via, and
|
|
200
|
+
test("detected pnpm → the catalogue's via, and an installed-elsewhere row with no path", () => {
|
|
201
201
|
const opencode = catalogue("opencode");
|
|
202
202
|
const install = { primary: "pnpm" as const, all: ["pnpm" as const] };
|
|
203
203
|
const spec = specForCatalogTool(opencode, null, install);
|
|
@@ -207,17 +207,17 @@ describe("buildToolRow — which installer a catalogue row is judged through", (
|
|
|
207
207
|
managed: false,
|
|
208
208
|
resolvedPath:
|
|
209
209
|
"/Users/j/.local/share/pnpm/global/5/node_modules/opencode-ai/bin/opencode",
|
|
210
|
-
owner: "npm (root /Users/j/.local/lib/node_modules)",
|
|
211
210
|
});
|
|
212
211
|
const row = buildToolRow(opencode, spec, f, install);
|
|
213
|
-
expect(row.status
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
expect(row
|
|
220
|
-
//
|
|
212
|
+
expect(row.status).toMatchObject({
|
|
213
|
+
state: "external",
|
|
214
|
+
reason: "installed-elsewhere",
|
|
215
|
+
});
|
|
216
|
+
expect(row.status?.note).toBeUndefined();
|
|
217
|
+
// Enter offers nothing for a tool installed another way.
|
|
218
|
+
expect(cliToolEnterAction(row)).toBeNull();
|
|
219
|
+
// The row still builds its update command for how it IS installed
|
|
220
|
+
// (pnpm), even though Enter offers nothing for it.
|
|
221
221
|
expect(row.updateCommand?.cmd).toBe("pnpm");
|
|
222
222
|
});
|
|
223
223
|
|
|
@@ -254,26 +254,30 @@ describe("cliToolStatusText — the row's words", () => {
|
|
|
254
254
|
primary: "npm",
|
|
255
255
|
all: ["npm"],
|
|
256
256
|
});
|
|
257
|
-
const f = facts("crush", {
|
|
257
|
+
const f = facts("crush", {
|
|
258
|
+
managed: null,
|
|
259
|
+
failure: "npm timed out after 8s",
|
|
260
|
+
});
|
|
258
261
|
const row = buildToolRow(tool, spec, f);
|
|
259
262
|
const text = cliToolStatusText(row);
|
|
260
263
|
expect(text.tone).toBe("warning");
|
|
261
|
-
expect(text.suffix).toBe(" ? owner
|
|
264
|
+
expect(text.suffix).toBe(" ? owner unknown");
|
|
262
265
|
// The note is for the detail pane's "Why", not the row.
|
|
263
266
|
expect(row.status?.note).toBe(classifyTool(spec, f).note);
|
|
264
267
|
expect(text.suffix).not.toContain("/");
|
|
265
268
|
});
|
|
266
269
|
|
|
267
|
-
test("external renders muted
|
|
270
|
+
test("external renders muted as `installed elsewhere`, with no ?", () => {
|
|
268
271
|
const tool = catalogue("crush");
|
|
269
272
|
const spec = specForCatalogTool(tool, null, {
|
|
270
273
|
primary: "npm",
|
|
271
274
|
all: ["npm"],
|
|
272
275
|
});
|
|
273
276
|
const row = buildToolRow(tool, spec, facts("crush", { managed: false }));
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
+
expect(cliToolStatusText(row)).toEqual({
|
|
278
|
+
suffix: " installed elsewhere",
|
|
279
|
+
tone: "muted",
|
|
280
|
+
});
|
|
277
281
|
});
|
|
278
282
|
|
|
279
283
|
test("upgrade names the target once, with one v", () => {
|
|
@@ -305,13 +309,19 @@ describe("countToolRows — header counts from states", () => {
|
|
|
305
309
|
buildToolRow(tool, spec, facts("mnemex", { behind: false })),
|
|
306
310
|
buildToolRow(tool, spec, facts("mnemex", { behind: true })),
|
|
307
311
|
buildToolRow(tool, spec, facts("mnemex", { managed: null })),
|
|
308
|
-
//
|
|
312
|
+
// Installed elsewhere: installed, but neither an update nor unverified.
|
|
309
313
|
buildToolRow(tool, spec, facts("mnemex", { managed: false })),
|
|
314
|
+
// Held back: checked (behind and pinned), so not unverified either.
|
|
315
|
+
buildToolRow(
|
|
316
|
+
tool,
|
|
317
|
+
spec,
|
|
318
|
+
facts("mnemex", { behind: true, heldBack: "held by brew pin" }),
|
|
319
|
+
),
|
|
310
320
|
buildToolRow(tool, spec, facts("mnemex", { present: false })),
|
|
311
321
|
buildToolRow(tool, spec, undefined),
|
|
312
322
|
];
|
|
313
323
|
expect(countToolRows(rows)).toEqual({
|
|
314
|
-
installed:
|
|
324
|
+
installed: 5,
|
|
315
325
|
updates: 1,
|
|
316
326
|
unverified: 1,
|
|
317
327
|
});
|