magus-cli 7.2.0 → 7.3.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 +4 -4
- package/scripts/build-binaries.ts +4 -2
- package/src/__tests__/blackbox-plugin-state.test.ts +3 -0
- package/src/__tests__/doctor.test.ts +31 -0
- package/src/__tests__/npm-package-name-api.test.ts +111 -0
- package/src/__tests__/npm-package-name.test.ts +280 -0
- package/src/__tests__/plugin-resolution.test.ts +431 -0
- package/src/__tests__/plugin-row-text.test.ts +66 -0
- package/src/__tests__/stale-load-repair.test.ts +1076 -0
- package/src/__tests__/update-apply.test.ts +146 -1
- package/src/__tests__/update-plan.test.ts +338 -0
- package/src/__tests__/upgrade-spawn.test.ts +355 -0
- package/src/__tests__/useStaleLoadModal.test.ts +177 -0
- package/src/cli/doctor.ts +128 -2
- package/src/cli/update-view.ts +31 -1
- package/src/cli/upgrade.ts +37 -9
- package/src/data/cli-tools.ts +4 -1
- package/src/data/npm-package.ts +15 -0
- package/src/services/active-closure.ts +23 -0
- package/src/services/claude-settings.ts +645 -34
- package/src/services/plugin-manager.ts +61 -0
- package/src/services/plugin-resolution.ts +280 -0
- package/src/services/update-engine.ts +104 -2
- package/src/services/update-plan.ts +188 -1
- package/src/services/version-check.ts +20 -10
- package/src/ui/App.tsx +63 -0
- package/src/ui/hooks/useStaleLoadModal.ts +157 -0
- package/src/ui/renderers/pluginRenderers.tsx +29 -0
- package/src/ui/renderers/pluginStatusText.ts +14 -0
- package/src/ui/screens/PluginsScreen.tsx +46 -13
package/src/cli/update-view.ts
CHANGED
|
@@ -81,6 +81,7 @@ type ActionKey =
|
|
|
81
81
|
| "upgrade"
|
|
82
82
|
| "repair"
|
|
83
83
|
| "refresh"
|
|
84
|
+
| "stale-load"
|
|
84
85
|
| "unknown";
|
|
85
86
|
|
|
86
87
|
const ACTIONS: Record<ActionKey, ActionStyle> = {
|
|
@@ -93,6 +94,11 @@ const ACTIONS: Record<ActionKey, ActionStyle> = {
|
|
|
93
94
|
upgrade: { glyph: "↑", label: "UPGRADE", color: brand.success },
|
|
94
95
|
repair: { glyph: "⟳", label: "REPAIR", color: brand.info },
|
|
95
96
|
refresh: { glyph: "⟳", label: "REFRESH", color: brand.info },
|
|
97
|
+
// The installed version is right and an older one loads, so this session runs
|
|
98
|
+
// a version nobody installed. Danger, and the word is BROKEN: "STALE" read as
|
|
99
|
+
// a mild version of `repair`, and this is not that. Six characters, one under
|
|
100
|
+
// UNKNOWN, so CHIP_WIDTH is unchanged and no other section pays for it.
|
|
101
|
+
"stale-load": { glyph: "✗", label: "BROKEN", color: brand.danger },
|
|
96
102
|
// Warning, not danger: "cannot tell" is not a failure, and red UNKNOWN read
|
|
97
103
|
// as "broken". The summary line and the TUI already use warning for it.
|
|
98
104
|
unknown: { glyph: "?", label: "UNKNOWN", color: brand.warning },
|
|
@@ -249,8 +255,12 @@ function pluginDetail(item: PluginUpdateItem): string {
|
|
|
249
255
|
// The note stays on the item. The TUI has no heading to lean on and renders
|
|
250
256
|
// it as that row's only explanation — this is a rendering decision about a
|
|
251
257
|
// table that carries the fact twice, not a change to what is known.
|
|
258
|
+
// `older-version-loads` is excluded for the same reason: its note says
|
|
259
|
+
// "loads v0.3.0, installed v0.5.0" and the two columns beside it already do.
|
|
252
260
|
const note =
|
|
253
|
-
item.note &&
|
|
261
|
+
item.note &&
|
|
262
|
+
item.reason !== "not-offered" &&
|
|
263
|
+
item.reason !== "older-version-loads"
|
|
254
264
|
? ` ${dim(`(${item.note})`)}`
|
|
255
265
|
: "";
|
|
256
266
|
switch (item.action) {
|
|
@@ -268,6 +278,16 @@ function pluginDetail(item: PluginUpdateItem): string {
|
|
|
268
278
|
case "repair":
|
|
269
279
|
case "unknown":
|
|
270
280
|
return `${item.installed ?? "—"}${note}`;
|
|
281
|
+
case "stale-load":
|
|
282
|
+
// Both versions, in the order the reader needs them: what runs, then
|
|
283
|
+
// what is installed. This is a report, not a one-line picker row — the
|
|
284
|
+
// TUI's row deliberately shows neither, because there the verdict and
|
|
285
|
+
// the key are all that fit. The note says the same thing in words; the
|
|
286
|
+
// columns are what makes a table of these scannable.
|
|
287
|
+
return `${fg(
|
|
288
|
+
groupStyle("Broken").text,
|
|
289
|
+
item.loaded ?? "?",
|
|
290
|
+
)} ${dim("≠")} ${item.installed ?? "—"}${note}`;
|
|
271
291
|
case "current":
|
|
272
292
|
return `${item.installed ?? "—"}${note}`;
|
|
273
293
|
}
|
|
@@ -347,6 +367,10 @@ function binDetail(item: BinUpdateItem, plan: UpdatePlan): string {
|
|
|
347
367
|
function pluginGroup(item: PluginUpdateItem): string {
|
|
348
368
|
if (item.action === "update" || item.action === "repair") return "Update";
|
|
349
369
|
if (item.action === "install") return "Fix required";
|
|
370
|
+
// Never "Current". The installed version matching is exactly what makes this
|
|
371
|
+
// state deceptive, and a row that reads as up to date under a CURRENT
|
|
372
|
+
// heading is the defect this state was added to end.
|
|
373
|
+
if (item.action === "stale-load") return "Broken";
|
|
350
374
|
if (item.action === "unknown") {
|
|
351
375
|
return item.reason === "not-offered" ? "Deprecated" : "Unverified";
|
|
352
376
|
}
|
|
@@ -354,6 +378,7 @@ function pluginGroup(item: PluginUpdateItem): string {
|
|
|
354
378
|
}
|
|
355
379
|
|
|
356
380
|
const GROUP_ORDER = [
|
|
381
|
+
"Broken",
|
|
357
382
|
"Update",
|
|
358
383
|
"Deprecated",
|
|
359
384
|
"Fix required",
|
|
@@ -373,6 +398,11 @@ const GROUP_KIND: Record<string, ReportBadgeKind> = {
|
|
|
373
398
|
Update: "update",
|
|
374
399
|
Deprecated: "deprecated",
|
|
375
400
|
"Fix required": "fix",
|
|
401
|
+
// Red earth, borrowed from `deprecated`. The kinds are measured contrast
|
|
402
|
+
// pairs, not meanings — "Hue, not meaning; the badge says the word", as
|
|
403
|
+
// theme-mode.ts puts it — so an unmeasured fourth palette would be the only
|
|
404
|
+
// thing here nobody had checked against both pages.
|
|
405
|
+
Broken: "deprecated",
|
|
376
406
|
};
|
|
377
407
|
|
|
378
408
|
interface GroupStyle {
|
package/src/cli/upgrade.ts
CHANGED
|
@@ -6,19 +6,49 @@
|
|
|
6
6
|
* and advancing the tool itself moved here.
|
|
7
7
|
*
|
|
8
8
|
* Detects magus installs across bun + npm global, warns on duplicates
|
|
9
|
-
* (active vs shadowed), then re-installs `
|
|
10
|
-
* package manager owns the active binary. Resolves
|
|
9
|
+
* (active vs shadowed), then re-installs the npm package (`MAGUS_NPM_PACKAGE`)
|
|
10
|
+
* at latest with whichever package manager owns the active binary. Resolves
|
|
11
|
+
* with the child's exit code.
|
|
12
|
+
*
|
|
13
|
+
* The npm package is not the command. The binary on disk and `which magus`
|
|
14
|
+
* stay `magus`; only what is handed to the package manager uses the package
|
|
15
|
+
* name, because bare `magus` on npm is an unrelated package.
|
|
11
16
|
*/
|
|
12
17
|
|
|
13
18
|
import { spawn } from "node:child_process";
|
|
14
19
|
import { constants } from "node:os";
|
|
20
|
+
import { MAGUS_NPM_PACKAGE } from "../data/npm-package.js";
|
|
21
|
+
import type { Command } from "../utils/command-utils.js";
|
|
22
|
+
|
|
23
|
+
/** The package managers `magus upgrade` detects and upgrades through. */
|
|
24
|
+
export type SelfUpgradeManager = "bun" | "npm";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The self-upgrade command for a package manager. Pure: no I/O, no PATH probe.
|
|
28
|
+
*/
|
|
29
|
+
export function selfUpgradeCommand(manager: SelfUpgradeManager): Command {
|
|
30
|
+
return {
|
|
31
|
+
cmd: manager,
|
|
32
|
+
args: ["install", "-g", `${MAGUS_NPM_PACKAGE}@latest`],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The command that removes a duplicate global install, for the hint printed
|
|
38
|
+
* when magus is installed through both package managers. No leading
|
|
39
|
+
* indentation — the caller indents.
|
|
40
|
+
*/
|
|
41
|
+
export function duplicateInstallRemoval(manager: SelfUpgradeManager): string {
|
|
42
|
+
return `${manager} uninstall -g ${MAGUS_NPM_PACKAGE}`;
|
|
43
|
+
}
|
|
15
44
|
|
|
16
45
|
export async function runUpgradeCommand(): Promise<number> {
|
|
17
46
|
const { execSync } = await import("node:child_process");
|
|
18
47
|
const { existsSync } = await import("node:fs");
|
|
19
48
|
|
|
20
49
|
// Detect all installations of magus across package managers
|
|
21
|
-
const installations: Array<{ manager:
|
|
50
|
+
const installations: Array<{ manager: SelfUpgradeManager; path: string }> =
|
|
51
|
+
[];
|
|
22
52
|
|
|
23
53
|
// Check bun global
|
|
24
54
|
try {
|
|
@@ -60,7 +90,7 @@ export async function runUpgradeCommand(): Promise<number> {
|
|
|
60
90
|
}
|
|
61
91
|
console.log("\nTo fix, keep one and remove the other:");
|
|
62
92
|
for (const inst of installations) {
|
|
63
|
-
console.log(` ${inst.manager}
|
|
93
|
+
console.log(` ${duplicateInstallRemoval(inst.manager)}`);
|
|
64
94
|
}
|
|
65
95
|
console.log();
|
|
66
96
|
}
|
|
@@ -76,13 +106,11 @@ export async function runUpgradeCommand(): Promise<number> {
|
|
|
76
106
|
// If which fails, default to npm
|
|
77
107
|
}
|
|
78
108
|
|
|
79
|
-
const
|
|
80
|
-
console.log(`Updating magus using ${
|
|
81
|
-
|
|
82
|
-
const installArgs = ["install", "-g", "magus@latest"];
|
|
109
|
+
const { cmd, args } = selfUpgradeCommand(usesBun ? "bun" : "npm");
|
|
110
|
+
console.log(`Updating magus using ${cmd}...`);
|
|
83
111
|
|
|
84
112
|
return new Promise<number>((resolve) => {
|
|
85
|
-
const proc = spawn(
|
|
113
|
+
const proc = spawn(cmd, args, {
|
|
86
114
|
stdio: "inherit",
|
|
87
115
|
shell: false, // Avoid shell for security (fixes DEP0190 warning)
|
|
88
116
|
});
|
package/src/data/cli-tools.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BinInstaller } from "../types/index.js";
|
|
2
|
+
import { MAGUS_NPM_PACKAGE } from "./npm-package.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* A CLI tool magus can install, check and update.
|
|
@@ -38,7 +39,9 @@ export const cliTools: CliTool[] = [
|
|
|
38
39
|
website: "https://github.com/MadAppGang/magus/tree/main/tools/magus",
|
|
39
40
|
category: "ai-coding",
|
|
40
41
|
packageManager: "bun",
|
|
41
|
-
|
|
42
|
+
// The npm package (`magus-cli`), not the command above: bare `magus` on
|
|
43
|
+
// npm is an unrelated package.
|
|
44
|
+
packageName: MAGUS_NPM_PACKAGE,
|
|
42
45
|
},
|
|
43
46
|
{
|
|
44
47
|
name: "mnemex",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import pkg from "../../package.json";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The npm package magus is published as. It is NOT the command.
|
|
5
|
+
*
|
|
6
|
+
* The command users type is `magus` (package.json `bin`). The npm package is
|
|
7
|
+
* `magus-cli`, because the bare `magus` name on npm belongs to an unrelated
|
|
8
|
+
* 2015 package, and npm never transfers names. Anything that talks to npm —
|
|
9
|
+
* the registry check, the self-upgrade, the CLI Tools row — must use this, and
|
|
10
|
+
* `git grep MAGUS_NPM_PACKAGE` lists every such call site.
|
|
11
|
+
*
|
|
12
|
+
* Static import so it resolves inside a `bun build --compile` binary — a
|
|
13
|
+
* dynamic require("../../package.json") returns nothing usable there.
|
|
14
|
+
*/
|
|
15
|
+
export const MAGUS_NPM_PACKAGE: string = (pkg as { name: string }).name;
|
|
@@ -115,3 +115,26 @@ export async function loadActiveClosure(
|
|
|
115
115
|
return null;
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The active profile's plugin pins — `pluginId -> exact version | "latest"` — and
|
|
121
|
+
* `{}` when no profile resolves.
|
|
122
|
+
*
|
|
123
|
+
* The one shape every surface that judges a plugin needs: `classifyPlugin`'s
|
|
124
|
+
* `pin`, the Plugins screen's `pins`, and the ceiling `findStaleLoads` caps its
|
|
125
|
+
* repair target at. Reading it through one helper is what keeps the startup popup
|
|
126
|
+
* and the Plugins header from capping differently — a pinned plugin read as
|
|
127
|
+
* BROKEN in one and current in the other is exactly the drift the classifier was
|
|
128
|
+
* centralised to end.
|
|
129
|
+
*
|
|
130
|
+
* `{}` means "latest for everything", which is what `pinForRow` assumes for an
|
|
131
|
+
* installed plugin no profile declares. `resolveBins: false` because no caller of
|
|
132
|
+
* this needs a plugin's binary requirements, and the default resolver reads every
|
|
133
|
+
* declared plugin's `plugin.json` to build them.
|
|
134
|
+
*/
|
|
135
|
+
export async function activePluginPins(
|
|
136
|
+
projectPath: string,
|
|
137
|
+
): Promise<Readonly<Record<string, string>>> {
|
|
138
|
+
const active = await loadActiveClosure(projectPath, { resolveBins: false });
|
|
139
|
+
return active?.closure.plugins ?? {};
|
|
140
|
+
}
|