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.
@@ -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 && item.reason !== "not-offered"
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 {
@@ -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 `magus@latest` with whichever
10
- * package manager owns the active binary. Resolves with the child's exit code.
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: "bun" | "npm"; path: string }> = [];
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} uninstall -g magus`);
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 pkgManager = usesBun ? "bun" : "npm";
80
- console.log(`Updating magus using ${pkgManager}...`);
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(pkgManager, installArgs, {
113
+ const proc = spawn(cmd, args, {
86
114
  stdio: "inherit",
87
115
  shell: false, // Avoid shell for security (fixes DEP0190 warning)
88
116
  });
@@ -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
- packageName: "magus",
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
+ }