magus-cli 7.8.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 +16 -11
- 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 +36 -48
- package/src/__tests__/blind-780-review2.test.ts +0 -25
- 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__/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__/failure-popup-review2.test.tsx +1 -25
- package/src/__tests__/final-fixes-780.test.tsx +25 -9
- package/src/__tests__/fixtures/bins.ts +55 -0
- package/src/__tests__/install-plan.test.ts +15 -1
- package/src/__tests__/marketplace-refresh.test.ts +9 -3
- 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__/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__/tool-probe.test.ts +1 -1
- package/src/__tests__/toolchain.test.ts +123 -52
- package/src/__tests__/update-plan.test.ts +1 -1
- package/src/cli/ansi.ts +2 -7
- package/src/cli/deps-view.ts +213 -0
- package/src/cli/doctor.ts +169 -24
- package/src/cli/install.ts +51 -66
- package/src/cli/profile.ts +20 -2
- package/src/cli/router.ts +6 -2
- package/src/cli/update.ts +28 -7
- package/src/data/cli-tools.ts +24 -9
- package/src/services/active-closure.ts +3 -3
- package/src/services/claude-settings.ts +4 -348
- 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-manager.ts +4 -18
- package/src/services/plugin-requires.ts +93 -162
- package/src/services/resolver.ts +168 -58
- package/src/services/tool-probe.ts +18 -25
- package/src/services/toolchain.ts +117 -111
- package/src/services/update-engine.ts +19 -14
- package/src/services/update-plan.ts +3 -3
- 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 +53 -16
- package/src/ui/App.tsx +6 -40
- package/src/ui/adapters/cliToolsAdapter.ts +75 -37
- package/src/ui/adapters/depsAdapter.ts +129 -0
- package/src/ui/adapters/startupReports.ts +0 -22
- package/src/ui/renderers/cliToolRenderers.tsx +135 -2
- package/src/ui/screens/CliToolsScreen.tsx +141 -51
- package/src/ui/screens/PluginsScreen.tsx +12 -48
- 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/__tests__/go-module.test.ts +0 -43
- package/src/__tests__/plugin-setup.test.ts +0 -1016
- package/src/__tests__/shell-script-callers.test.ts +0 -92
- package/src/services/plugin-setup.ts +0 -965
- package/src/utils/go-module.ts +0 -36
package/src/services/resolver.ts
CHANGED
|
@@ -4,29 +4,38 @@
|
|
|
4
4
|
* Turns a manifest entry into a fully-resolved {@link ResolvedClosure}: the
|
|
5
5
|
* marketplaces, plugins, MCP servers, binary deps, skills, settings, and env a
|
|
6
6
|
* machine must have to match the profile. Binary deps are the union of the
|
|
7
|
-
* profile's own `cliTools` and every enabled plugin's declared `requires
|
|
8
|
-
*
|
|
7
|
+
* profile's own `cliTools` and every enabled plugin's declared `requires`,
|
|
8
|
+
* merged by {@link mergeBins} with the project's pin winning. Plugins include
|
|
9
9
|
* every plugin an enabled one declares as a dependency, transitively.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import semver from "semver";
|
|
13
|
-
import { cliTools } from "../data/cli-tools.js";
|
|
13
|
+
import { cliTools, isOwnedEntry } from "../data/cli-tools.js";
|
|
14
14
|
import { getMarketplaceByName } from "../data/marketplaces.js";
|
|
15
15
|
import { PREDEFINED_PROFILES } from "../data/predefined-profiles.js";
|
|
16
|
+
import {
|
|
17
|
+
type InstallOption,
|
|
18
|
+
type Requirement,
|
|
19
|
+
dedupeKey,
|
|
20
|
+
} from "../shared/plugin/hooks/lib/requires-probe.js";
|
|
16
21
|
import type {
|
|
17
|
-
BinRequirement,
|
|
18
22
|
DependencyReport,
|
|
19
23
|
ProfileManifest,
|
|
20
24
|
ProfileManifestEntry,
|
|
21
25
|
ResolvedBin,
|
|
22
26
|
ResolvedClosure,
|
|
27
|
+
SelectedBin,
|
|
23
28
|
} from "../types/index.js";
|
|
24
29
|
import { readManifest } from "./manifest.js";
|
|
25
30
|
import {
|
|
26
31
|
type PluginDependencyReader,
|
|
27
32
|
readPluginDependencies,
|
|
28
33
|
} from "./plugin-dependencies.js";
|
|
29
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
ownedRequirement,
|
|
36
|
+
resolvePluginBinRequirements,
|
|
37
|
+
selectBins,
|
|
38
|
+
} from "./plugin-requires.js";
|
|
30
39
|
import { activeProfile } from "./profile-live.js";
|
|
31
40
|
|
|
32
41
|
export interface ResolveOptions {
|
|
@@ -34,7 +43,15 @@ export interface ResolveOptions {
|
|
|
34
43
|
* Resolve a plugin id to its declared binary requirements. Injectable for
|
|
35
44
|
* tests; defaults to reading plugin.json from the marketplace cache.
|
|
36
45
|
*/
|
|
37
|
-
binResolver?: (pluginId: string) => Promise<
|
|
46
|
+
binResolver?: (pluginId: string) => Promise<SelectedBin[]>;
|
|
47
|
+
/**
|
|
48
|
+
* The requirement a plugin declares for a catalogue tool it owns
|
|
49
|
+
* (`ownedBy`). Defaults to reading that plugin's declaration here.
|
|
50
|
+
*/
|
|
51
|
+
ownedResolver?: (
|
|
52
|
+
pluginId: string,
|
|
53
|
+
name: string,
|
|
54
|
+
) => Promise<SelectedBin | null>;
|
|
38
55
|
/**
|
|
39
56
|
* Resolve a plugin id to the plugin ids its plugin.json declares as
|
|
40
57
|
* dependencies, or null when it cannot be read. Defaults to reading the
|
|
@@ -235,84 +252,163 @@ export function resolveExtends(
|
|
|
235
252
|
};
|
|
236
253
|
}
|
|
237
254
|
|
|
255
|
+
/** The pin a selected requirement installs at, if its chosen option carries one. */
|
|
256
|
+
export function pinOfBin(bin: SelectedBin): string | undefined {
|
|
257
|
+
const option = bin.chosen;
|
|
258
|
+
return option && "version" in option ? option.version : undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** The catalogue's check line as argv: split once, here, never by a shell. */
|
|
262
|
+
const splitCheck = (line: string): string[] => line.trim().split(/\s+/);
|
|
263
|
+
|
|
238
264
|
/**
|
|
239
|
-
* Map a manifest cliTools entry (name -> version)
|
|
265
|
+
* Map a manifest cliTools entry (name -> version) onto a requirement: the
|
|
266
|
+
* catalogue's installer as one option (`npm`/`pip`/`go` are the profile-only
|
|
267
|
+
* kinds), its check as argv, the pin carried in the option. A tool a plugin
|
|
268
|
+
* owns (`ownedBy`) is that plugin's declaration, passed in as `owned`, with
|
|
269
|
+
* the pin applied to its chosen option. An unknown tool installs with bun
|
|
270
|
+
* under its own name.
|
|
240
271
|
*
|
|
241
272
|
* Exported so the TUI's CLI Tools screen maps a catalogue tool the same way the
|
|
242
273
|
* resolver does, rather than keeping a second copy of the mapping.
|
|
243
274
|
*/
|
|
244
|
-
export function cliToolToBin(
|
|
275
|
+
export function cliToolToBin(
|
|
276
|
+
name: string,
|
|
277
|
+
version: string,
|
|
278
|
+
owned?: SelectedBin | null,
|
|
279
|
+
): SelectedBin {
|
|
280
|
+
const pin = version && version !== "latest" ? version : undefined;
|
|
281
|
+
const carriesPin = (
|
|
282
|
+
o: InstallOption,
|
|
283
|
+
): o is Extract<InstallOption, { via: "bun" | "npm" | "pip" | "go" }> =>
|
|
284
|
+
o.via === "bun" || o.via === "npm" || o.via === "pip" || o.via === "go";
|
|
285
|
+
const withPin = (o: InstallOption): InstallOption =>
|
|
286
|
+
pin && carriesPin(o) ? { ...o, version: pin } : o;
|
|
287
|
+
if (owned) {
|
|
288
|
+
if (!owned.chosen) return owned;
|
|
289
|
+
// The plugin's option decides the version: a pin it cannot carry is
|
|
290
|
+
// kept as refused, so it is reported rather than lost.
|
|
291
|
+
return pin && !carriesPin(owned.chosen)
|
|
292
|
+
? { ...owned, refusedPins: [pin] }
|
|
293
|
+
: { ...owned, chosen: withPin(owned.chosen) };
|
|
294
|
+
}
|
|
245
295
|
const tool = cliTools.find((t) => t.name === name);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
296
|
+
let option: InstallOption;
|
|
297
|
+
let check: string[];
|
|
298
|
+
if (tool && !isOwnedEntry(tool)) {
|
|
299
|
+
check = splitCheck(tool.checkCommand);
|
|
300
|
+
option =
|
|
301
|
+
tool.packageManager === "brew"
|
|
302
|
+
? { via: "brew", formula: tool.packageName }
|
|
303
|
+
: { via: tool.packageManager, package: tool.packageName };
|
|
304
|
+
} else {
|
|
305
|
+
check = [name, "--version"];
|
|
306
|
+
option = { via: "bun", package: name };
|
|
255
307
|
}
|
|
256
|
-
|
|
308
|
+
const requirement: Requirement = { name, check, install: [withPin(option)] };
|
|
309
|
+
return (
|
|
310
|
+
selectBins([requirement], undefined)[0] ?? {
|
|
311
|
+
...requirement,
|
|
312
|
+
chosen: null,
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** The declaration of a requirement, without what selection and provenance add. */
|
|
318
|
+
export function requirementOf(bin: Requirement): Requirement {
|
|
257
319
|
return {
|
|
258
|
-
name,
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
version: pinned,
|
|
320
|
+
name: bin.name,
|
|
321
|
+
check: bin.check,
|
|
322
|
+
...(bin.os ? { os: bin.os } : {}),
|
|
323
|
+
install: bin.install,
|
|
263
324
|
};
|
|
264
325
|
}
|
|
265
326
|
|
|
266
327
|
/**
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
|
|
328
|
+
* The identity two requirements merge by: requires-probe's `dedupeKey` of the
|
|
329
|
+
* declaration, plus the plugin directory when the check reads
|
|
330
|
+
* `${CLAUDE_PLUGIN_ROOT}` — such a check is per plugin by nature.
|
|
331
|
+
*/
|
|
332
|
+
export function binKey(bin: SelectedBin): string {
|
|
333
|
+
const key = dedupeKey(requirementOf(bin));
|
|
334
|
+
return bin.check.some((t) => t.includes("${CLAUDE_PLUGIN_ROOT}"))
|
|
335
|
+
? `${key}@${bin.root ?? ""}`
|
|
336
|
+
: key;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Merge selected requirements, in the order given — callers put higher
|
|
341
|
+
* precedence first (project pins before plugin declarations). Identical
|
|
342
|
+
* declarations merge (one entry, every source). A profile cliTools entry
|
|
343
|
+
* absorbs any later requirement of the same name: the project's pin is the
|
|
344
|
+
* decision. Two different plugin declarations of one name stay two entries,
|
|
345
|
+
* so neither hides the other.
|
|
271
346
|
*/
|
|
272
|
-
export function
|
|
273
|
-
entries: Array<{ bin:
|
|
347
|
+
export function mergeBins(
|
|
348
|
+
entries: Array<{ bin: SelectedBin; source: string }>,
|
|
274
349
|
): ResolvedBin[] {
|
|
275
|
-
const
|
|
350
|
+
const out: ResolvedBin[] = [];
|
|
351
|
+
const byKey = new Map<string, ResolvedBin>();
|
|
352
|
+
const pinnedByName = new Map<string, ResolvedBin>();
|
|
276
353
|
for (const { bin, source } of entries) {
|
|
277
|
-
const
|
|
354
|
+
const key = binKey(bin);
|
|
355
|
+
const existing = byKey.get(key) ?? pinnedByName.get(bin.name);
|
|
278
356
|
if (existing) {
|
|
279
357
|
if (!existing.sources.includes(source)) existing.sources.push(source);
|
|
280
|
-
// A later entry only fills a version the winner left unset.
|
|
281
|
-
if (existing.version === undefined && bin.version !== undefined) {
|
|
282
|
-
existing.version = bin.version;
|
|
283
|
-
}
|
|
284
358
|
continue;
|
|
285
359
|
}
|
|
286
|
-
|
|
360
|
+
const resolved: ResolvedBin = { ...bin, sources: [source] };
|
|
361
|
+
byKey.set(key, resolved);
|
|
362
|
+
if (source.startsWith("profile:")) pinnedByName.set(bin.name, resolved);
|
|
363
|
+
out.push(resolved);
|
|
287
364
|
}
|
|
288
|
-
return
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** A profile's own cliTools as selected requirements, with their source. */
|
|
369
|
+
export async function cliToolBins(
|
|
370
|
+
entry: Pick<ProfileManifestEntry, "name" | "cliTools">,
|
|
371
|
+
ownedResolver: (
|
|
372
|
+
pluginId: string,
|
|
373
|
+
name: string,
|
|
374
|
+
) => Promise<SelectedBin | null> = ownedRequirement,
|
|
375
|
+
): Promise<Array<{ bin: SelectedBin; source: string }>> {
|
|
376
|
+
const out: Array<{ bin: SelectedBin; source: string }> = [];
|
|
377
|
+
for (const [name, version] of Object.entries(entry.cliTools ?? {})) {
|
|
378
|
+
const tool = cliTools.find((t) => t.name === name);
|
|
379
|
+
const owned =
|
|
380
|
+
tool && isOwnedEntry(tool)
|
|
381
|
+
? await ownedResolver(tool.ownedBy, name)
|
|
382
|
+
: null;
|
|
383
|
+
out.push({
|
|
384
|
+
bin: cliToolToBin(name, version, owned),
|
|
385
|
+
source: `profile:${entry.name}.cliTools`,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return out;
|
|
289
389
|
}
|
|
290
390
|
|
|
291
391
|
/** Collect the binary requirements for a resolved plugin set + profile cliTools. */
|
|
292
392
|
async function resolveBins(
|
|
293
393
|
entry: ProfileManifestEntry,
|
|
294
394
|
pluginIds: string[],
|
|
295
|
-
binResolver: (pluginId: string) => Promise<
|
|
395
|
+
binResolver: (pluginId: string) => Promise<SelectedBin[]>,
|
|
396
|
+
ownedResolver: (
|
|
397
|
+
pluginId: string,
|
|
398
|
+
name: string,
|
|
399
|
+
) => Promise<SelectedBin | null>,
|
|
296
400
|
): Promise<ResolvedBin[]> {
|
|
297
|
-
const collected: Array<{ bin: BinRequirement; source: string }> = [];
|
|
298
|
-
|
|
299
401
|
// Project pins first (highest precedence).
|
|
300
|
-
|
|
301
|
-
collected.push({
|
|
302
|
-
bin: cliToolToBin(name, version),
|
|
303
|
-
source: `profile:${entry.name}.cliTools`,
|
|
304
|
-
});
|
|
305
|
-
}
|
|
402
|
+
const collected = await cliToolBins(entry, ownedResolver);
|
|
306
403
|
|
|
307
|
-
// Then each plugin's declared
|
|
404
|
+
// Then each plugin's declared requirements.
|
|
308
405
|
for (const pluginId of pluginIds) {
|
|
309
|
-
const
|
|
310
|
-
for (const bin of bins) {
|
|
406
|
+
for (const bin of await binResolver(pluginId)) {
|
|
311
407
|
collected.push({ bin, source: `plugin:${pluginId}` });
|
|
312
408
|
}
|
|
313
409
|
}
|
|
314
410
|
|
|
315
|
-
return
|
|
411
|
+
return mergeBins(collected);
|
|
316
412
|
}
|
|
317
413
|
|
|
318
414
|
/** Resolve a single profile (applying `extends`) into an install closure. */
|
|
@@ -386,6 +482,7 @@ export async function resolveProfile(
|
|
|
386
482
|
if (!raw) throw new Error(`Profile "${profileId}" not found in manifest`);
|
|
387
483
|
const entry = resolveExtends(raw);
|
|
388
484
|
const binResolver = opts.binResolver ?? resolvePluginBinRequirements;
|
|
485
|
+
const ownedResolver = opts.ownedResolver ?? ownedRequirement;
|
|
389
486
|
|
|
390
487
|
const plugins = normalizePlugins(entry.plugins);
|
|
391
488
|
const disabledPlugins = Object.entries(entry.plugins ?? {})
|
|
@@ -400,7 +497,12 @@ export async function resolveProfile(
|
|
|
400
497
|
);
|
|
401
498
|
for (const id of dependencies.derived) plugins[id] = "latest";
|
|
402
499
|
|
|
403
|
-
const bins = await resolveBins(
|
|
500
|
+
const bins = await resolveBins(
|
|
501
|
+
entry,
|
|
502
|
+
Object.keys(plugins),
|
|
503
|
+
binResolver,
|
|
504
|
+
ownedResolver,
|
|
505
|
+
);
|
|
404
506
|
|
|
405
507
|
return {
|
|
406
508
|
marketplaces: withDerivedMarketplaces(entry.marketplaces, plugins),
|
|
@@ -480,18 +582,26 @@ function mergeClosures(
|
|
|
480
582
|
continue;
|
|
481
583
|
}
|
|
482
584
|
const mergedSources = [...new Set([...existing.sources, ...bin.sources])];
|
|
483
|
-
|
|
484
|
-
const
|
|
585
|
+
// Every profile's refused pin survives the union, whichever entry wins.
|
|
586
|
+
const refused = [
|
|
587
|
+
...new Set([...(existing.refusedPins ?? []), ...(bin.refusedPins ?? [])]),
|
|
588
|
+
];
|
|
589
|
+
const merged = (winner: ResolvedBin): ResolvedBin => ({
|
|
590
|
+
...winner,
|
|
591
|
+
sources: mergedSources,
|
|
592
|
+
...(refused.length > 0 ? { refusedPins: refused } : {}),
|
|
593
|
+
});
|
|
594
|
+
const va = pinOfBin(existing);
|
|
595
|
+
const vb = pinOfBin(bin);
|
|
485
596
|
if (va === undefined && vb === undefined) {
|
|
486
|
-
|
|
597
|
+
binByName.set(bin.name, merged(existing));
|
|
487
598
|
} else if (va === undefined) {
|
|
488
|
-
binByName.set(bin.name,
|
|
599
|
+
binByName.set(bin.name, merged(bin));
|
|
489
600
|
} else if (vb === undefined) {
|
|
490
|
-
|
|
601
|
+
binByName.set(bin.name, merged(existing));
|
|
491
602
|
} else {
|
|
492
603
|
const { chosen, conflict } = pickVersion(bin.name, va, vb);
|
|
493
|
-
|
|
494
|
-
binByName.set(bin.name, { ...winner, sources: mergedSources });
|
|
604
|
+
binByName.set(bin.name, merged(chosen === va ? existing : bin));
|
|
495
605
|
if (conflict) conflicts.push(conflict);
|
|
496
606
|
}
|
|
497
607
|
}
|
|
@@ -43,11 +43,7 @@ import os from "node:os";
|
|
|
43
43
|
import path from "node:path";
|
|
44
44
|
import semver from "semver";
|
|
45
45
|
import { CLAUDE_CODE_NPM_PACKAGE, type CliTool } from "../data/cli-tools.js";
|
|
46
|
-
import type {
|
|
47
|
-
BinInstaller,
|
|
48
|
-
BinRequirement,
|
|
49
|
-
ResolvedBin,
|
|
50
|
-
} from "../types/index.js";
|
|
46
|
+
import type { BinInstaller, ManagedBin, ManagedTool } from "../types/index.js";
|
|
51
47
|
import type { Command } from "../utils/command-utils.js";
|
|
52
48
|
import type { InstallMethod } from "./cli-tool-commands.js";
|
|
53
49
|
import {
|
|
@@ -210,7 +206,7 @@ export function compareToolVersions(a: string, b: string): -1 | 0 | 1 | null {
|
|
|
210
206
|
// ── The classifier ───────────────────────────────────────────────────────────
|
|
211
207
|
|
|
212
208
|
/** The exact pin, or undefined for an unpinned (or "latest") requirement. */
|
|
213
|
-
function pinOf(spec:
|
|
209
|
+
function pinOf(spec: ManagedTool): string | undefined {
|
|
214
210
|
const v = spec.version?.trim();
|
|
215
211
|
return v && v !== "latest" ? v : undefined;
|
|
216
212
|
}
|
|
@@ -220,7 +216,7 @@ function withFailure(text: string, failure: string | undefined): string {
|
|
|
220
216
|
}
|
|
221
217
|
|
|
222
218
|
/** The `not-managed` sentence. Names the file and its owner, never the version. */
|
|
223
|
-
function notManagedNote(spec:
|
|
219
|
+
function notManagedNote(spec: ManagedTool, facts: ToolFacts): string {
|
|
224
220
|
return facts.managed === false
|
|
225
221
|
? `on PATH at ${facts.resolvedPath ?? "?"}, which ${facts.owner ?? spec.via} does not own — upgrading through ${spec.via} would install a second copy`
|
|
226
222
|
: withFailure(
|
|
@@ -247,10 +243,7 @@ function notManagedNote(spec: BinRequirement, facts: ToolFacts): string {
|
|
|
247
243
|
* rules are not displaced by it — they need a version to compare, and there is
|
|
248
244
|
* none.
|
|
249
245
|
*/
|
|
250
|
-
export function classifyTool(
|
|
251
|
-
spec: BinRequirement,
|
|
252
|
-
facts: ToolFacts,
|
|
253
|
-
): ToolStatus {
|
|
246
|
+
export function classifyTool(spec: ManagedTool, facts: ToolFacts): ToolStatus {
|
|
254
247
|
const pin = pinOf(spec);
|
|
255
248
|
const base = {
|
|
256
249
|
name: spec.name,
|
|
@@ -1027,7 +1020,7 @@ const SHELL_META = /[|&;<>()$`\\"'*?{}[\]~!#\n\r]/;
|
|
|
1027
1020
|
* Refuse a name that could be read as an option, carries a NUL, or (for a
|
|
1028
1021
|
* value joined into a path) climbs out of its directory. Null when fine.
|
|
1029
1022
|
*/
|
|
1030
|
-
function refusal(spec:
|
|
1023
|
+
function refusal(spec: ManagedTool): string | null {
|
|
1031
1024
|
const bad = (v: string | undefined, isPath: boolean): boolean =>
|
|
1032
1025
|
v !== undefined &&
|
|
1033
1026
|
(v.startsWith("-") ||
|
|
@@ -1102,7 +1095,7 @@ async function packageJsonVersion(
|
|
|
1102
1095
|
*/
|
|
1103
1096
|
async function versionFromCheck(
|
|
1104
1097
|
s: ProbeSession,
|
|
1105
|
-
spec:
|
|
1098
|
+
spec: ManagedTool,
|
|
1106
1099
|
f: ToolFacts,
|
|
1107
1100
|
onPath: string,
|
|
1108
1101
|
): Promise<void> {
|
|
@@ -1191,7 +1184,7 @@ async function containedIn(
|
|
|
1191
1184
|
|
|
1192
1185
|
async function probeBrew(
|
|
1193
1186
|
s: ProbeSession,
|
|
1194
|
-
spec:
|
|
1187
|
+
spec: ManagedTool,
|
|
1195
1188
|
f: ToolFacts,
|
|
1196
1189
|
resolvedPath: string,
|
|
1197
1190
|
): Promise<void> {
|
|
@@ -1230,7 +1223,7 @@ async function probeBrew(
|
|
|
1230
1223
|
|
|
1231
1224
|
async function probePip(
|
|
1232
1225
|
s: ProbeSession,
|
|
1233
|
-
spec:
|
|
1226
|
+
spec: ManagedTool,
|
|
1234
1227
|
f: ToolFacts,
|
|
1235
1228
|
onPath: string,
|
|
1236
1229
|
resolvedPath: string,
|
|
@@ -1361,7 +1354,7 @@ async function readNodePackage(
|
|
|
1361
1354
|
|
|
1362
1355
|
async function probeNodeGlobal(
|
|
1363
1356
|
s: ProbeSession,
|
|
1364
|
-
spec:
|
|
1357
|
+
spec: ManagedTool,
|
|
1365
1358
|
f: ToolFacts,
|
|
1366
1359
|
resolvedPath: string,
|
|
1367
1360
|
): Promise<void> {
|
|
@@ -1495,7 +1488,7 @@ async function isNativeLauncher(
|
|
|
1495
1488
|
*/
|
|
1496
1489
|
async function probeClaudeNative(
|
|
1497
1490
|
s: ProbeSession,
|
|
1498
|
-
spec:
|
|
1491
|
+
spec: ManagedTool,
|
|
1499
1492
|
f: ToolFacts,
|
|
1500
1493
|
onPath: string,
|
|
1501
1494
|
resolvedPath: string,
|
|
@@ -1532,7 +1525,7 @@ async function probeClaudeNative(
|
|
|
1532
1525
|
|
|
1533
1526
|
async function probeGo(
|
|
1534
1527
|
s: ProbeSession,
|
|
1535
|
-
spec:
|
|
1528
|
+
spec: ManagedTool,
|
|
1536
1529
|
f: ToolFacts,
|
|
1537
1530
|
resolvedPath: string,
|
|
1538
1531
|
pinned: boolean,
|
|
@@ -1585,7 +1578,7 @@ async function probeGo(
|
|
|
1585
1578
|
/** Probe one tool into `f`, mutating it as facts arrive. Never throws. */
|
|
1586
1579
|
async function probeInto(
|
|
1587
1580
|
s: ProbeSession,
|
|
1588
|
-
spec:
|
|
1581
|
+
spec: ManagedTool,
|
|
1589
1582
|
f: ToolFacts,
|
|
1590
1583
|
): Promise<void> {
|
|
1591
1584
|
const refused = refusal(spec);
|
|
@@ -1683,7 +1676,7 @@ function timedOut(f: ToolFacts, deadlineMs: number): ToolFacts {
|
|
|
1683
1676
|
|
|
1684
1677
|
async function probeWithDeadline(
|
|
1685
1678
|
s: ProbeSession,
|
|
1686
|
-
spec:
|
|
1679
|
+
spec: ManagedTool,
|
|
1687
1680
|
deadlineAt: number,
|
|
1688
1681
|
deadlineMs: number,
|
|
1689
1682
|
): Promise<ToolFacts> {
|
|
@@ -1721,7 +1714,7 @@ async function probeWithDeadline(
|
|
|
1721
1714
|
* facts with `failure` set, which classify as `unknown`, never `upgrade`.
|
|
1722
1715
|
*/
|
|
1723
1716
|
export async function probeTools(
|
|
1724
|
-
specs: readonly
|
|
1717
|
+
specs: readonly ManagedTool[],
|
|
1725
1718
|
io: ToolProbeIO = REAL_TOOL_PROBE_IO,
|
|
1726
1719
|
opts: ProbeOptions = {},
|
|
1727
1720
|
): Promise<ToolFacts[]> {
|
|
@@ -1774,7 +1767,7 @@ export async function probeTools(
|
|
|
1774
1767
|
|
|
1775
1768
|
/** One tool, with its own probe session. */
|
|
1776
1769
|
export async function probeTool(
|
|
1777
|
-
spec:
|
|
1770
|
+
spec: ManagedTool,
|
|
1778
1771
|
io: ToolProbeIO = REAL_TOOL_PROBE_IO,
|
|
1779
1772
|
opts: Omit<ProbeOptions, "concurrency" | "onResult"> = {},
|
|
1780
1773
|
): Promise<ToolFacts> {
|
|
@@ -1804,7 +1797,7 @@ function isBinInstaller(m: InstallMethod): m is BinInstaller & InstallMethod {
|
|
|
1804
1797
|
/**
|
|
1805
1798
|
* The spec a catalogue tool is probed through, in order:
|
|
1806
1799
|
*
|
|
1807
|
-
* 1. the active profile's `
|
|
1800
|
+
* 1. the active profile's `ManagedBin` of the same name — the same pin and
|
|
1808
1801
|
* the same installer `magus update` uses, so the two agree;
|
|
1809
1802
|
* 2. the DETECTED installer, when it is one magus models — "how it IS
|
|
1810
1803
|
* installed", the rule `cliToolUpdate` follows, so the row's state and the
|
|
@@ -1814,9 +1807,9 @@ function isBinInstaller(m: InstallMethod): m is BinInstaller & InstallMethod {
|
|
|
1814
1807
|
*/
|
|
1815
1808
|
export function specForCatalogTool(
|
|
1816
1809
|
tool: CliTool,
|
|
1817
|
-
closureBins: readonly
|
|
1810
|
+
closureBins: readonly ManagedBin[] | null,
|
|
1818
1811
|
install: InstallInfo | undefined,
|
|
1819
|
-
):
|
|
1812
|
+
): ManagedTool {
|
|
1820
1813
|
const declared = closureBins?.find((b) => b.name === tool.name);
|
|
1821
1814
|
if (declared) return declared;
|
|
1822
1815
|
if (install && isBinInstaller(install.primary)) {
|