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
package/src/cli/install.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* Put a committed team profile from .claude/profiles.json onto this machine:
|
|
5
5
|
* register marketplaces, activate the profile (render its settings.json,
|
|
6
6
|
* .mcp.json and models.json, install its skills), install its pinned plugins
|
|
7
|
-
* and
|
|
8
|
-
* then collect
|
|
7
|
+
* and what those plugins declare in `requires` plus the profile's cliTools
|
|
8
|
+
* (through the deps executor that `magus doctor --fix` uses), then collect
|
|
9
|
+
* required env.
|
|
9
10
|
*
|
|
10
11
|
* With no argument it installs the union of every profile's deps (so `profile
|
|
11
12
|
* switch` needs no reinstall) and keeps the active profile active, asking only
|
|
@@ -13,6 +14,7 @@
|
|
|
13
14
|
* the confirmation prompt.
|
|
14
15
|
*/
|
|
15
16
|
|
|
17
|
+
import { homedir } from "node:os";
|
|
16
18
|
import { installPlugin, isClaudeAvailable } from "../services/claude-cli.js";
|
|
17
19
|
import {
|
|
18
20
|
getLocalSettingsPath,
|
|
@@ -20,6 +22,9 @@ import {
|
|
|
20
22
|
readLocalSettings,
|
|
21
23
|
writeLocalSettings,
|
|
22
24
|
} from "../services/claude-settings.js";
|
|
25
|
+
import type { PlanEnv } from "../services/deps-plan.js";
|
|
26
|
+
import { assessBins } from "../services/deps-report.js";
|
|
27
|
+
import { machineEnv } from "../services/doctor-bins.js";
|
|
23
28
|
import {
|
|
24
29
|
computeEnvStatus,
|
|
25
30
|
computeVersionDrift,
|
|
@@ -40,16 +45,13 @@ import {
|
|
|
40
45
|
import { resolveAllProfiles, resolveProfile } from "../services/resolver.js";
|
|
41
46
|
import { SettingsParseError, writableMap } from "../services/settings-file.js";
|
|
42
47
|
import {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
} from "../
|
|
47
|
-
import { bringLoadForward } from "../services/update-engine.js";
|
|
48
|
-
import type { ResolvedBin, ResolvedClosure } from "../types/index.js";
|
|
49
|
-
import { formatCommand, resolveExecutable } from "../utils/command-utils.js";
|
|
50
|
-
import { runCommand, runShellScript } from "../utils/run.js";
|
|
48
|
+
bringLoadForward,
|
|
49
|
+
describeLoadNotForward,
|
|
50
|
+
} from "../services/update-engine.js";
|
|
51
|
+
import type { ResolvedClosure } from "../types/index.js";
|
|
51
52
|
import { bold } from "./ansi.js";
|
|
52
53
|
import { ensureManifest, printActivationNotes } from "./bootstrap.js";
|
|
54
|
+
import { depsSectionLines, fixDeps, renderApplyText } from "./deps-view.js";
|
|
53
55
|
import { askYesNo, promptValue } from "./prompt.js";
|
|
54
56
|
import { selectOption } from "./select.js";
|
|
55
57
|
import {
|
|
@@ -105,48 +107,6 @@ async function runCheck(
|
|
|
105
107
|
|
|
106
108
|
// ── install steps ───────────────────────────────────────────────────────────
|
|
107
109
|
|
|
108
|
-
async function ensureToolchains(
|
|
109
|
-
closure: ResolvedClosure,
|
|
110
|
-
yes: boolean,
|
|
111
|
-
): Promise<void> {
|
|
112
|
-
// Only the binaries actually missing can need a toolchain. Checking all of
|
|
113
|
-
// them warns "pip isn't installed" on a machine whose pip-provided binaries
|
|
114
|
-
// are already on PATH — an alarming message about work that will not happen.
|
|
115
|
-
const missing: ResolvedBin[] = [];
|
|
116
|
-
for (const bin of closure.bins) {
|
|
117
|
-
if (!(await resolveExecutable(bin.name))) missing.push(bin);
|
|
118
|
-
}
|
|
119
|
-
if (missing.length === 0) return;
|
|
120
|
-
|
|
121
|
-
const toolchains = await detectToolchains(missing);
|
|
122
|
-
for (const tc of toolchains) {
|
|
123
|
-
if (tc.present) continue;
|
|
124
|
-
const bootstrap = TOOLCHAIN_BOOTSTRAP[tc.name];
|
|
125
|
-
if (!bootstrap) {
|
|
126
|
-
console.warn(
|
|
127
|
-
`⚠ ${tc.name} is required by ${tc.requiredBy.join(", ")} but isn't installed and can't be auto-installed. Install it and re-run.`,
|
|
128
|
-
);
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
const ok =
|
|
132
|
-
yes ||
|
|
133
|
-
(await askYesNo(
|
|
134
|
-
question(
|
|
135
|
-
`${tc.name} is missing — ${tc.requiredBy.join(", ")} need${tc.requiredBy.length === 1 ? "s" : ""} it. Install it?`,
|
|
136
|
-
),
|
|
137
|
-
));
|
|
138
|
-
if (ok) {
|
|
139
|
-
console.log(`Installing ${tc.name}…`);
|
|
140
|
-
// A bootstrap line IS shell text — bun's contains a pipe, Homebrew's a
|
|
141
|
-
// command substitution — and it is a constant in this repo, not
|
|
142
|
-
// profile data. This is the only legitimate caller.
|
|
143
|
-
if (!(await runShellScript(bootstrap))) {
|
|
144
|
-
console.warn(`⚠ Failed to install ${tc.name}. Continuing.`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
110
|
/**
|
|
151
111
|
* Install the closure's plugins at PROJECT scope.
|
|
152
112
|
*
|
|
@@ -210,28 +170,47 @@ async function bringLoadsForward(
|
|
|
210
170
|
} moved forward)`,
|
|
211
171
|
);
|
|
212
172
|
} else {
|
|
213
|
-
console.warn(
|
|
214
|
-
`⚠ ${pluginId}: installed v${here}, but Claude Code still loads v${
|
|
215
|
-
forward.loaded ?? "an older version"
|
|
216
|
-
} here — ${forward.error ?? "the older records could not be moved forward"}`,
|
|
217
|
-
);
|
|
173
|
+
console.warn(`⚠ ${pluginId}: ${describeLoadNotForward(forward)}`);
|
|
218
174
|
}
|
|
219
175
|
}
|
|
220
176
|
}
|
|
221
177
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Install what the closure's requirements say is missing here: probe, plan,
|
|
180
|
+
* list every step and ask once (unless `--yes`), apply, then report from a
|
|
181
|
+
* fresh probe — the same flow and rendering as `magus doctor --fix`.
|
|
182
|
+
*/
|
|
183
|
+
async function installDeps(
|
|
184
|
+
closure: ResolvedClosure,
|
|
185
|
+
env: PlanEnv,
|
|
186
|
+
yes: boolean,
|
|
187
|
+
): Promise<void> {
|
|
188
|
+
const collected = {
|
|
189
|
+
plugins: Object.keys(closure.plugins),
|
|
190
|
+
bins: closure.bins,
|
|
191
|
+
problems: [],
|
|
192
|
+
};
|
|
193
|
+
const state = await assessBins(collected, env);
|
|
194
|
+
if (state.plan.steps.length === 0) {
|
|
195
|
+
for (const line of depsSectionLines(state, env).lines) console.log(line);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const result = await fixDeps(state, env, { yes, interactive: true }, () =>
|
|
199
|
+
assessBins(collected, env),
|
|
200
|
+
);
|
|
201
|
+
if (result.kind === "busy") {
|
|
202
|
+
console.warn(`⚠ ${result.message}`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (result.kind === "declined") {
|
|
206
|
+
console.log(
|
|
207
|
+
"Plugin dependencies not installed — `magus doctor --fix` installs them later.",
|
|
208
|
+
);
|
|
209
|
+
return;
|
|
234
210
|
}
|
|
211
|
+
for (const line of renderApplyText(result.outcomes)) console.log(line);
|
|
212
|
+
for (const line of depsSectionLines(result.after, env).lines)
|
|
213
|
+
console.log(line);
|
|
235
214
|
}
|
|
236
215
|
|
|
237
216
|
async function collectEnv(
|
|
@@ -374,7 +353,6 @@ export async function runInstallCommand(
|
|
|
374
353
|
return 1;
|
|
375
354
|
}
|
|
376
355
|
|
|
377
|
-
await ensureToolchains(closure, flags.yes);
|
|
378
356
|
await registerClosureMarketplaces(closure.marketplaces);
|
|
379
357
|
// A marketplace added just now carries the plugin.json files that name
|
|
380
358
|
// each plugin's dependencies; resolve again so those are installed too.
|
|
@@ -439,7 +417,13 @@ export async function runInstallCommand(
|
|
|
439
417
|
);
|
|
440
418
|
// Only what installed: a failed install already printed its own warning.
|
|
441
419
|
await bringLoadsForward(installed, projectPath);
|
|
442
|
-
|
|
420
|
+
// Resolved again: the requirements are read from the copies just installed.
|
|
421
|
+
const env = machineEnv(
|
|
422
|
+
process.env.HOME || homedir(),
|
|
423
|
+
process.env.PATH ?? "",
|
|
424
|
+
process.env.SHELL ?? "",
|
|
425
|
+
);
|
|
426
|
+
if (env) await installDeps(await resolveClosure(), env, flags.yes);
|
|
443
427
|
|
|
444
428
|
if (activeId) {
|
|
445
429
|
await collectEnv(
|
package/src/cli/profile.ts
CHANGED
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
* live config first goes into the profile being left.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import {
|
|
12
|
+
checkBinaries,
|
|
13
|
+
machineEnv,
|
|
14
|
+
missingBinaries,
|
|
15
|
+
} from "../services/doctor-bins.js";
|
|
11
16
|
import { summarizeClosure } from "../services/install-plan.js";
|
|
12
17
|
import { getManifestPath, readManifest } from "../services/manifest.js";
|
|
13
18
|
import { reportDisplaced } from "../services/profile-activate.js";
|
|
@@ -173,7 +178,20 @@ export async function switchProfile(
|
|
|
173
178
|
// succeeds (the config is in place), but warn so "switched" is not misleading.
|
|
174
179
|
try {
|
|
175
180
|
const closure = await resolveProfile(manifest, name);
|
|
176
|
-
const
|
|
181
|
+
const env = machineEnv(
|
|
182
|
+
process.env.HOME || homedir(),
|
|
183
|
+
process.env.PATH ?? "",
|
|
184
|
+
process.env.SHELL ?? "",
|
|
185
|
+
);
|
|
186
|
+
const missing = env
|
|
187
|
+
? missingBinaries(
|
|
188
|
+
await checkBinaries(closure.bins, {
|
|
189
|
+
env,
|
|
190
|
+
depth: "presence",
|
|
191
|
+
capMs: 3_000,
|
|
192
|
+
}),
|
|
193
|
+
)
|
|
194
|
+
: [];
|
|
177
195
|
if (missing.length > 0) {
|
|
178
196
|
console.log(
|
|
179
197
|
`\n⚠ ${missing.length} binary dependenc${missing.length === 1 ? "y is" : "ies are"} missing:`,
|
package/src/cli/router.ts
CHANGED
|
@@ -282,8 +282,12 @@ without one is offered adoption on first run. See docs/team-configuration.md.
|
|
|
282
282
|
Changes Claude Code makes to .claude/settings.json or .mcp.json (a plugin
|
|
283
283
|
installed, a setting changed) are changes to the active profile: every command
|
|
284
284
|
above records them in .claude/profiles.json before it acts.
|
|
285
|
-
doctor Check
|
|
286
|
-
--fix Apply the repairs it can make itself
|
|
285
|
+
doctor Check plugin dependencies, the active profile, and conventions
|
|
286
|
+
--fix Apply the repairs it can make itself, including installing
|
|
287
|
+
missing plugin dependencies (asks once, listing each)
|
|
288
|
+
--yes Install without asking
|
|
289
|
+
--json Plugin dependencies only, as one JSON document
|
|
290
|
+
--project <dir> Check the project in <dir> instead of this directory
|
|
287
291
|
|
|
288
292
|
Per-subagent model routing — give each subagent its own model and effort. The
|
|
289
293
|
routing lives on the active profile (committed in .claude/profiles.json) and is
|
package/src/cli/update-view.ts
CHANGED
|
@@ -27,11 +27,12 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { binCommandFor } from "../services/update-engine.js";
|
|
30
|
-
import
|
|
31
|
-
BinUpdateItem,
|
|
32
|
-
PluginUpdateItem,
|
|
33
|
-
SkillUpdateItem,
|
|
34
|
-
UpdatePlan,
|
|
30
|
+
import {
|
|
31
|
+
type BinUpdateItem,
|
|
32
|
+
type PluginUpdateItem,
|
|
33
|
+
type SkillUpdateItem,
|
|
34
|
+
type UpdatePlan,
|
|
35
|
+
isKeptDeprecated,
|
|
35
36
|
} from "../services/update-plan.js";
|
|
36
37
|
import { type ReportBadgeKind, reportBadge } from "../ui/theme-mode.js";
|
|
37
38
|
import { brand } from "../ui/theme.js";
|
|
@@ -84,6 +85,7 @@ type ActionKey =
|
|
|
84
85
|
| "repair"
|
|
85
86
|
| "refresh"
|
|
86
87
|
| "stale-load"
|
|
88
|
+
| "remove"
|
|
87
89
|
| "unknown";
|
|
88
90
|
|
|
89
91
|
const ACTIONS: Record<ActionKey, ActionStyle> = {
|
|
@@ -101,6 +103,10 @@ const ACTIONS: Record<ActionKey, ActionStyle> = {
|
|
|
101
103
|
// a mild version of `repair`, and this is not that. Six characters, one under
|
|
102
104
|
// UNKNOWN, so CHIP_WIDTH is unchanged and no other section pays for it.
|
|
103
105
|
"stale-load": { glyph: "✗", label: "BROKEN", color: brand.danger },
|
|
106
|
+
// No longer published, and removed by the apply. ✗ in danger: the plugin is
|
|
107
|
+
// dead weight Claude Code refuses to load, and the row is work, not a shrug.
|
|
108
|
+
// The plugin table shows the group's DEPRECATED chip, not this label.
|
|
109
|
+
remove: { glyph: "✗", label: "REMOVE", color: brand.danger },
|
|
104
110
|
// Warning, not danger: "cannot tell" is not a failure, and red UNKNOWN read
|
|
105
111
|
// as "broken". The summary line and the TUI already use warning for it.
|
|
106
112
|
unknown: { glyph: "?", label: "UNKNOWN", color: brand.warning },
|
|
@@ -373,10 +379,15 @@ function pluginDetail(item: PluginUpdateItem): string {
|
|
|
373
379
|
// "loads v0.3.0, installed v0.5.0" and the two columns beside it already do.
|
|
374
380
|
const note =
|
|
375
381
|
item.note &&
|
|
376
|
-
item.reason !== "
|
|
382
|
+
item.reason !== "deprecated" &&
|
|
377
383
|
item.reason !== "older-version-loads"
|
|
378
384
|
? ` ${dim(`(${item.note})`)}`
|
|
379
385
|
: "";
|
|
386
|
+
// Kept on purpose — moved, or still required. Under Deprecated, and the
|
|
387
|
+
// word says nothing will happen to it; the note says why.
|
|
388
|
+
if (isKeptDeprecated(item)) {
|
|
389
|
+
return `${item.installed ?? "—"} ${dim(`kept — ${item.note ?? "no longer published"}`)}`;
|
|
390
|
+
}
|
|
380
391
|
switch (item.action) {
|
|
381
392
|
case "install":
|
|
382
393
|
return `${item.target ?? "latest"}${note}`;
|
|
@@ -391,6 +402,7 @@ function pluginDetail(item: PluginUpdateItem): string {
|
|
|
391
402
|
)} ${fg(groupStyle("Update").text, item.target ?? "?")}${note}`;
|
|
392
403
|
case "repair":
|
|
393
404
|
case "unknown":
|
|
405
|
+
case "remove":
|
|
394
406
|
return `${item.installed ?? "—"}${note}`;
|
|
395
407
|
case "stale-load":
|
|
396
408
|
// Both versions, in the order the reader needs them: what runs, then
|
|
@@ -459,7 +471,7 @@ function binDetail(item: BinUpdateItem, plan: UpdatePlan): string {
|
|
|
459
471
|
* note. Five headings, and no more:
|
|
460
472
|
*
|
|
461
473
|
* Update something an apply would move forward
|
|
462
|
-
* Deprecated no
|
|
474
|
+
* Deprecated no longer published — an apply removes it
|
|
463
475
|
* Fix required declared or installed, with its files missing
|
|
464
476
|
* Unverified the check itself did not complete
|
|
465
477
|
* Current settled — the version was compared, or there was none to compare
|
|
@@ -485,9 +497,11 @@ function pluginGroup(item: PluginUpdateItem): string {
|
|
|
485
497
|
// state deceptive, and a row that reads as up to date under a CURRENT
|
|
486
498
|
// heading is the defect this state was added to end.
|
|
487
499
|
if (item.action === "stale-load") return "Broken";
|
|
488
|
-
if (item.action === "
|
|
489
|
-
|
|
490
|
-
|
|
500
|
+
if (item.action === "remove") return "Deprecated";
|
|
501
|
+
// Moved or still required: no longer published here, and KEPT. That is a
|
|
502
|
+
// determined fact, not a check that failed, so never "Unverified".
|
|
503
|
+
if (isKeptDeprecated(item)) return "Deprecated";
|
|
504
|
+
if (item.action === "unknown") return "Unverified";
|
|
491
505
|
return "Current";
|
|
492
506
|
}
|
|
493
507
|
|
|
@@ -590,9 +604,12 @@ function pluginRow(
|
|
|
590
604
|
: badge(groupBadge(group), style.fill ?? brand.muted, style.ink),
|
|
591
605
|
chipWidth,
|
|
592
606
|
);
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
607
|
+
// A kept row is not work: the ✗ of a removal would say it is, and the "?"
|
|
608
|
+
// of an unknown that nobody knows. It gets the settled row's muted glyph.
|
|
609
|
+
const mark = isKeptDeprecated(item)
|
|
610
|
+
? fg(brand.muted, ACTIONS.current.glyph)
|
|
611
|
+
: fg(style.text, ACTIONS[item.action].glyph);
|
|
612
|
+
return `${PAD}${mark} ${style.chrome ? dim(name) : name} ${chip}${pluginDetail(item)}`;
|
|
596
613
|
}
|
|
597
614
|
|
|
598
615
|
/** The Plugins section: grouped by state, `Current` last. */
|
|
@@ -681,7 +698,7 @@ export function planReport(plan: UpdatePlan): string[] {
|
|
|
681
698
|
// EVERY planned plugin gets a row. There is no filter here and there must
|
|
682
699
|
// not be one.
|
|
683
700
|
//
|
|
684
|
-
// A previous revision dropped
|
|
701
|
+
// A previous revision dropped unlisted, not-installed rows
|
|
685
702
|
// — a pin naming a plugin no marketplace offers — on the grounds that a
|
|
686
703
|
// notice named them instead. That notice did not exist: the function the
|
|
687
704
|
// comment cited was never written. The row appeared in no table and no
|
|
@@ -778,10 +795,11 @@ export function summaryLine(
|
|
|
778
795
|
failed: number,
|
|
779
796
|
ms: number,
|
|
780
797
|
unchanged = 0,
|
|
798
|
+
kept = 0,
|
|
781
799
|
): string[] {
|
|
782
800
|
const bar = stackedBar(BAR * 2, [
|
|
783
801
|
{ value: changed, color: brand.success },
|
|
784
|
-
{ value: current, color: brand.muted },
|
|
802
|
+
{ value: current + kept, color: brand.muted },
|
|
785
803
|
{ value: unchanged + unknown, color: brand.warning },
|
|
786
804
|
{ value: failed, color: brand.danger },
|
|
787
805
|
]);
|
|
@@ -789,6 +807,9 @@ export function summaryLine(
|
|
|
789
807
|
changed > 0 ? fg(brand.success, `${changed} changed`) : "",
|
|
790
808
|
unchanged > 0 ? fg(brand.warning, `${unchanged} unchanged`) : "",
|
|
791
809
|
current > 0 ? dim(`${current} current`) : "",
|
|
810
|
+
// Deprecated but kept on purpose (moved, still required): settled, so
|
|
811
|
+
// grey beside `current`, and never folded into "unverified".
|
|
812
|
+
kept > 0 ? dim(`${kept} kept`) : "",
|
|
792
813
|
unknown > 0 ? fg(brand.warning, `${unknown} unverified`) : "",
|
|
793
814
|
failed > 0 ? fg(brand.danger, `${failed} failed`) : "",
|
|
794
815
|
].filter(Boolean);
|
|
@@ -860,3 +881,25 @@ export function nothingToApplyNotice(unknownPlugins: number): string {
|
|
|
860
881
|
`Nothing to apply. ${unknownPlugins} plugin(s) could not be checked. They are NOT reported as up to date. Reasons are shown above.`,
|
|
861
882
|
);
|
|
862
883
|
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* The words `--check` fails with, chosen by what is pending. "Updates
|
|
887
|
+
* available" over a plan whose only work is removing plugins that are no
|
|
888
|
+
* longer published claims an update nobody can find in the table above.
|
|
889
|
+
*/
|
|
890
|
+
export function pendingHeadline(plan: UpdatePlan): string {
|
|
891
|
+
const pendingPlugins = plan.plugins.filter(
|
|
892
|
+
(p) =>
|
|
893
|
+
p.action === "install" ||
|
|
894
|
+
p.action === "update" ||
|
|
895
|
+
p.action === "repair" ||
|
|
896
|
+
p.action === "stale-load" ||
|
|
897
|
+
p.action === "remove",
|
|
898
|
+
);
|
|
899
|
+
const onlyRemovals =
|
|
900
|
+
pendingPlugins.length > 0 &&
|
|
901
|
+
pendingPlugins.every((p) => p.action === "remove") &&
|
|
902
|
+
!plan.bins.some((b) => b.action === "install" || b.action === "upgrade") &&
|
|
903
|
+
!plan.skills.some((s) => s.action === "install");
|
|
904
|
+
return onlyRemovals ? "Changes pending" : "Updates available";
|
|
905
|
+
}
|
package/src/cli/update.ts
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
import { selectProfileId } from "../services/active-closure.js";
|
|
38
38
|
import { isClaudeAvailable } from "../services/claude-cli.js";
|
|
39
39
|
import { clearContentDriftCache } from "../services/content-drift.js";
|
|
40
|
+
import { refusedPinWhy } from "../services/deps-plan.js";
|
|
40
41
|
import { validateManifest } from "../services/manifest.js";
|
|
41
42
|
import {
|
|
42
43
|
refreshRegisteredMarketplaces,
|
|
@@ -47,6 +48,7 @@ import {
|
|
|
47
48
|
type PluginInfo,
|
|
48
49
|
clearMarketplaceCache,
|
|
49
50
|
getAvailablePlugins,
|
|
51
|
+
isInstalledAnywhere,
|
|
50
52
|
} from "../services/plugin-manager.js";
|
|
51
53
|
import {
|
|
52
54
|
recordRefetchedSkills,
|
|
@@ -61,14 +63,16 @@ import {
|
|
|
61
63
|
switchHint,
|
|
62
64
|
withCliBookkeeping,
|
|
63
65
|
} from "../services/profile-state.js";
|
|
64
|
-
import { resolveProfile } from "../services/resolver.js";
|
|
66
|
+
import { requirersHere, resolveProfile } from "../services/resolver.js";
|
|
65
67
|
import { getInstalledSkillNames } from "../services/skills-manager.js";
|
|
66
68
|
import { REAL_TOOL_PROBE_IO, probeTools } from "../services/tool-probe.js";
|
|
69
|
+
import { managedBins } from "../services/toolchain.js";
|
|
67
70
|
// The apply half lives in services/ so the TUI runs the SAME code — see the
|
|
68
71
|
// header of update-engine.ts for what drifted while it did not.
|
|
69
72
|
import {
|
|
70
73
|
type ApplyReporter,
|
|
71
74
|
applyPlan,
|
|
75
|
+
describeRemoved,
|
|
72
76
|
planWorkItems,
|
|
73
77
|
} from "../services/update-engine.js";
|
|
74
78
|
import {
|
|
@@ -95,6 +99,7 @@ import {
|
|
|
95
99
|
note as noteLine,
|
|
96
100
|
nothingToApplyNotice,
|
|
97
101
|
ok as okLine,
|
|
102
|
+
pendingHeadline,
|
|
98
103
|
planReport,
|
|
99
104
|
stepFrame,
|
|
100
105
|
stepRecord,
|
|
@@ -315,6 +320,14 @@ export async function runUpdateCommand(
|
|
|
315
320
|
return resolved;
|
|
316
321
|
});
|
|
317
322
|
|
|
323
|
+
// A pin the owning plugin's option cannot carry: whatever installs or
|
|
324
|
+
// upgrades the tool ignores it, so say so rather than let it read as kept.
|
|
325
|
+
for (const bin of closure.bins) {
|
|
326
|
+
for (const pin of bin.refusedPins ?? []) {
|
|
327
|
+
notices.push(warnLine(`${bold(bin.name)}: ${refusedPinWhy(bin, pin)}`));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
318
331
|
// "latest" is only as fresh as the catalog. Fast-forward the marketplace
|
|
319
332
|
// clones first, then drop every cached answer derived from the old HEAD —
|
|
320
333
|
// skipping this is how a clone sits days behind while every plugin reads as
|
|
@@ -386,13 +399,23 @@ export async function runUpdateCommand(
|
|
|
386
399
|
// "behind?" answer — the same probe the TUI's CLI Tools screen uses. It used
|
|
387
400
|
// to be a presence check, which is why every present unpinned tool planned
|
|
388
401
|
// as an upgrade on every run.
|
|
402
|
+
//
|
|
403
|
+
// Only the requirements installed by a versioned package manager have a
|
|
404
|
+
// version to move. The rest (apt, a pinned release, a toolchain bootstrap,
|
|
405
|
+
// a plugin's uv env) are as current as their declaration, and
|
|
406
|
+
// `magus doctor --fix` installs them.
|
|
407
|
+
const tools = managedBins(closure.bins);
|
|
408
|
+
const declaredOnly = closure.bins.length - tools.length;
|
|
389
409
|
const toolFacts = await runStep(steps.binaries, async (say) => {
|
|
390
|
-
if (
|
|
391
|
-
steps.binaries.detail =
|
|
410
|
+
if (tools.length === 0) {
|
|
411
|
+
steps.binaries.detail =
|
|
412
|
+
declaredOnly > 0
|
|
413
|
+
? `${declaredOnly} managed by their declaration`
|
|
414
|
+
: "none declared";
|
|
392
415
|
return [];
|
|
393
416
|
}
|
|
394
|
-
steps.binaries.progress = { done: 0, total:
|
|
395
|
-
const facts = await probeTools(
|
|
417
|
+
steps.binaries.progress = { done: 0, total: tools.length };
|
|
418
|
+
const facts = await probeTools(tools, REAL_TOOL_PROBE_IO, {
|
|
396
419
|
// `settled`, not `index`: results arrive in completion order, and a
|
|
397
420
|
// meter driven by index would jump backwards.
|
|
398
421
|
onResult: (f, { settled, total }) => {
|
|
@@ -400,7 +423,7 @@ export async function runUpdateCommand(
|
|
|
400
423
|
say(f.name);
|
|
401
424
|
},
|
|
402
425
|
});
|
|
403
|
-
const items = planBinUpdates(
|
|
426
|
+
const items = planBinUpdates(tools, facts);
|
|
404
427
|
const n = (a: string) => items.filter((i) => i.action === a).length;
|
|
405
428
|
const behind = n("upgrade");
|
|
406
429
|
const unknown = n("unknown");
|
|
@@ -410,6 +433,7 @@ export async function runUpdateCommand(
|
|
|
410
433
|
behind > 0 ? `${behind} behind` : "",
|
|
411
434
|
unknown > 0 ? `${unknown} unknown` : "",
|
|
412
435
|
missing > 0 ? `${missing} missing` : "",
|
|
436
|
+
declaredOnly > 0 ? `${declaredOnly} managed by their declaration` : "",
|
|
413
437
|
]
|
|
414
438
|
.filter(Boolean)
|
|
415
439
|
.join(" · ");
|
|
@@ -441,12 +465,20 @@ export async function runUpdateCommand(
|
|
|
441
465
|
targetId,
|
|
442
466
|
closure.skills,
|
|
443
467
|
);
|
|
468
|
+
// Every installed plugin's dependencies count, not only the profile's: a
|
|
469
|
+
// plugin still required by one that stays is never planned for removal, and
|
|
470
|
+
// retired plugins that only require each other are removed together.
|
|
471
|
+
const requiredBy = await requirersHere(
|
|
472
|
+
closure.dependencies?.requiredBy,
|
|
473
|
+
[...catalog.values()].filter(isInstalledAnywhere).map((p) => p.id),
|
|
474
|
+
projectPath,
|
|
475
|
+
);
|
|
444
476
|
const plan: UpdatePlan = {
|
|
445
477
|
profileId: targetId,
|
|
446
|
-
plugins: planPluginUpdates(closure.plugins, catalog),
|
|
447
|
-
bins: planBinUpdates(
|
|
478
|
+
plugins: planPluginUpdates(closure.plugins, catalog, requiredBy),
|
|
479
|
+
bins: planBinUpdates(tools, toolFacts),
|
|
448
480
|
skills: planSkillUpdates(skillRefs, installedSkills),
|
|
449
|
-
binSpecs: new Map(
|
|
481
|
+
binSpecs: new Map(tools.map((b) => [b.name, b])),
|
|
450
482
|
};
|
|
451
483
|
|
|
452
484
|
for (const line of planReport(plan)) console.log(line);
|
|
@@ -489,7 +521,9 @@ export async function runUpdateCommand(
|
|
|
489
521
|
return 0;
|
|
490
522
|
}
|
|
491
523
|
console.error(
|
|
492
|
-
failLine(
|
|
524
|
+
failLine(
|
|
525
|
+
`${pendingHeadline(plan)}. Run ${bold("magus update")} to apply.`,
|
|
526
|
+
),
|
|
493
527
|
);
|
|
494
528
|
return 1;
|
|
495
529
|
}
|
|
@@ -506,6 +540,8 @@ export async function runUpdateCommand(
|
|
|
506
540
|
counts.unknown,
|
|
507
541
|
0,
|
|
508
542
|
Date.now() - startedAt,
|
|
543
|
+
0,
|
|
544
|
+
counts.kept,
|
|
509
545
|
))
|
|
510
546
|
console.log(line);
|
|
511
547
|
// `planHasWork` counts an `unknown` plugin as work, because from the
|
|
@@ -539,8 +575,10 @@ export async function runUpdateCommand(
|
|
|
539
575
|
|
|
540
576
|
// No confirmation. `update` moves the machine toward what the profile
|
|
541
577
|
// declares and can only go FORWARD: `claude plugin install` takes no version
|
|
542
|
-
// and installs the newest published one, so there is no downgrade
|
|
543
|
-
//
|
|
578
|
+
// and installs the newest published one, so there is no downgrade for a
|
|
579
|
+
// prompt to protect against. The one delete it makes is a DEPRECATED plugin:
|
|
580
|
+
// its marketplace no longer publishes it and Claude Code refuses to load it
|
|
581
|
+
// (measured, 2.1.284), so removing it loses nothing. The plan is printed above, so the
|
|
544
582
|
// run is still legible after the fact — and `--dry-run` shows it beforehand.
|
|
545
583
|
//
|
|
546
584
|
// Note this also runs a profile's CLI-tool installers unattended (`brew
|
|
@@ -637,9 +675,14 @@ export async function runUpdateCommand(
|
|
|
637
675
|
failed.length,
|
|
638
676
|
Date.now() - startedAt,
|
|
639
677
|
outcome.unchanged.length,
|
|
678
|
+
counts.kept,
|
|
640
679
|
))
|
|
641
680
|
console.log(line);
|
|
642
681
|
|
|
682
|
+
// Only the CLI says it: on the Plugins screen the removed rows are simply gone.
|
|
683
|
+
const removed = describeRemoved(outcome.plugins.removed);
|
|
684
|
+
if (removed) console.log(okLine(removed));
|
|
685
|
+
|
|
643
686
|
// Exited 0, read back unmoved. Reported, never fatal — the same precedent
|
|
644
687
|
// as the pin mismatches below: this is a standing property of the machine
|
|
645
688
|
// (a stale tap, a shadowing PATH entry), not something this run did wrong,
|
package/src/data/cli-tools.ts
CHANGED
|
@@ -13,13 +13,16 @@ import { MAGUS_NPM_PACKAGE } from "./npm-package.js";
|
|
|
13
13
|
* catalogue entry can no longer smuggle shell syntax (that same entry's command
|
|
14
14
|
* was a `&&` chain) into something that gets executed.
|
|
15
15
|
*/
|
|
16
|
-
|
|
16
|
+
interface CatalogueDisplay {
|
|
17
17
|
name: string;
|
|
18
18
|
displayName: string;
|
|
19
19
|
description: string;
|
|
20
|
-
checkCommand: string;
|
|
21
20
|
website: string;
|
|
22
21
|
category: "ai-coding" | "utility";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CliTool extends CatalogueDisplay {
|
|
25
|
+
checkCommand: string;
|
|
23
26
|
/**
|
|
24
27
|
* Reuses `BinInstaller` rather than restating its members, so the
|
|
25
28
|
* correspondence with the resolver in `services/toolchain.ts` — which is
|
|
@@ -29,6 +32,22 @@ export interface CliTool {
|
|
|
29
32
|
packageName: string;
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
/**
|
|
36
|
+
* A catalogue tool a plugin declares in its `requires`. The entry carries
|
|
37
|
+
* display fields only: how the tool is checked and installed is that plugin's
|
|
38
|
+
* declaration, read through requires-probe (`resolveCatalogue`), so the two
|
|
39
|
+
* can never disagree.
|
|
40
|
+
*/
|
|
41
|
+
export interface OwnedCatalogueEntry extends CatalogueDisplay {
|
|
42
|
+
/** `<plugin>@<marketplace>` whose `requires` declares this tool. */
|
|
43
|
+
ownedBy: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type CatalogueEntry = CliTool | OwnedCatalogueEntry;
|
|
47
|
+
|
|
48
|
+
export const isOwnedEntry = (e: CatalogueEntry): e is OwnedCatalogueEntry =>
|
|
49
|
+
"ownedBy" in e;
|
|
50
|
+
|
|
32
51
|
/**
|
|
33
52
|
* Claude Code's npm package. Named once because two readers need it: the
|
|
34
53
|
* catalogue entry below, and the probe, which asks this package's `latest`
|
|
@@ -37,7 +56,7 @@ export interface CliTool {
|
|
|
37
56
|
*/
|
|
38
57
|
export const CLAUDE_CODE_NPM_PACKAGE = "@anthropic-ai/claude-code";
|
|
39
58
|
|
|
40
|
-
export const cliTools:
|
|
59
|
+
export const cliTools: CatalogueEntry[] = [
|
|
41
60
|
{
|
|
42
61
|
name: "magus",
|
|
43
62
|
displayName: "magus",
|
|
@@ -56,21 +75,17 @@ export const cliTools: CliTool[] = [
|
|
|
56
75
|
displayName: "Mnemex",
|
|
57
76
|
description:
|
|
58
77
|
"AST-aware code search with PageRank, callers/callees, and semantic embeddings",
|
|
59
|
-
checkCommand: "mnemex --version",
|
|
60
78
|
website: "https://github.com/MadAppGang/mnemex",
|
|
61
79
|
category: "ai-coding",
|
|
62
|
-
|
|
63
|
-
packageName: "mnemex",
|
|
80
|
+
ownedBy: "mnemex@magus",
|
|
64
81
|
},
|
|
65
82
|
{
|
|
66
83
|
name: "claudish",
|
|
67
84
|
displayName: "Claudish",
|
|
68
85
|
description: "Run Claude Code with OpenRouter models (Grok, GPT-5, Gemini)",
|
|
69
|
-
checkCommand: "claudish --version",
|
|
70
86
|
website: "https://github.com/MadAppGang/claudish",
|
|
71
87
|
category: "ai-coding",
|
|
72
|
-
|
|
73
|
-
packageName: "claudish",
|
|
88
|
+
ownedBy: "claudish@magus",
|
|
74
89
|
},
|
|
75
90
|
{
|
|
76
91
|
name: "claude",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type {
|
|
18
|
-
BinRequirement,
|
|
19
18
|
ProfileManifest,
|
|
20
19
|
ResolvedClosure,
|
|
20
|
+
SelectedBin,
|
|
21
21
|
} from "../types/index.js";
|
|
22
22
|
import { readManifest, validateManifest } from "./manifest.js";
|
|
23
23
|
import { activeProfile } from "./profile-live.js";
|
|
@@ -54,7 +54,7 @@ export interface ActiveClosureDeps {
|
|
|
54
54
|
manifest: ProfileManifest,
|
|
55
55
|
id: string,
|
|
56
56
|
opts?: {
|
|
57
|
-
binResolver?: (pluginId: string) => Promise<
|
|
57
|
+
binResolver?: (pluginId: string) => Promise<SelectedBin[]>;
|
|
58
58
|
},
|
|
59
59
|
): Promise<ResolvedClosure>;
|
|
60
60
|
}
|
|
@@ -79,7 +79,7 @@ export interface LoadActiveClosureOptions {
|
|
|
79
79
|
deps?: Partial<ActiveClosureDeps>;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
const NO_BINS = async (): Promise<
|
|
82
|
+
const NO_BINS = async (): Promise<SelectedBin[]> => [];
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
85
|
* The active profile's closure for a project.
|