@crewhaus/tool-pkgmgr 0.7.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/dist/fixtures.d.ts +174 -0
- package/dist/fixtures.js +334 -0
- package/dist/host.d.ts +21 -0
- package/dist/host.js +90 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +434 -0
- package/dist/lib/managers.d.ts +172 -0
- package/dist/lib/managers.js +266 -0
- package/dist/lib/names.d.ts +106 -0
- package/dist/lib/names.js +174 -0
- package/dist/lib/parse.d.ts +308 -0
- package/dist/lib/parse.js +572 -0
- package/dist/lib/plan.d.ts +88 -0
- package/dist/lib/plan.js +351 -0
- package/dist/lib/query.d.ts +106 -0
- package/dist/lib/query.js +552 -0
- package/dist/lib/unknown.d.ts +68 -0
- package/dist/lib/unknown.js +85 -0
- package/dist/paths.d.ts +42 -0
- package/dist/paths.js +158 -0
- package/dist/run.d.ts +115 -0
- package/dist/run.js +295 -0
- package/package.json +46 -0
package/dist/lib/plan.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an install WOULD do — resolved once, by one function, for both the dry
|
|
3
|
+
* run and the real thing.
|
|
4
|
+
*
|
|
5
|
+
* House rule 9 exists because a preview that re-derives the answer separately
|
|
6
|
+
* eventually describes something the real call does not do; TrashPath drifted
|
|
7
|
+
* exactly that way and predicted a destination the real call never used. The
|
|
8
|
+
* defence here is structural rather than disciplinary: `resolveInstallPlan`
|
|
9
|
+
* is the ONLY code that decides what gets installed, `dryRun` does not change
|
|
10
|
+
* which commands run to resolve it, and the single `if` that separates the
|
|
11
|
+
* two paths is in `../index.ts` AFTER this returns — it decides whether the
|
|
12
|
+
* install command runs, and nothing else.
|
|
13
|
+
*
|
|
14
|
+
* The cost is one extra command on the real path: a real install resolves its
|
|
15
|
+
* plan first, exactly as a dry run does. That is the point. The result then
|
|
16
|
+
* reports what it EXPECTED to install next to what it did, and a caller can
|
|
17
|
+
* see the difference rather than trust that there was none.
|
|
18
|
+
*
|
|
19
|
+
* ── WHAT "TRANSITIVE ADDITIONS" ACTUALLY COSTS, PER MANAGER ───────────────
|
|
20
|
+
*
|
|
21
|
+
* The catalogue implies every manager can preview a transaction. Four can,
|
|
22
|
+
* and they differ:
|
|
23
|
+
*
|
|
24
|
+
* apt `apt-get install -s` resolves the whole transaction WITHOUT
|
|
25
|
+
* root and prints every package with its version, including
|
|
26
|
+
* removals. This is the complete answer.
|
|
27
|
+
* pacman `pacman -S --print --print-format` prints every target with its
|
|
28
|
+
* version, dependencies included, without root. Also complete.
|
|
29
|
+
* dnf `dnf install --assumeno` prints the transaction table and then
|
|
30
|
+
* declines — but dnf may refuse before resolving anything when it
|
|
31
|
+
* is not root. Both outcomes are reported for what they are.
|
|
32
|
+
* brew has no dry run for `install` at all. The additions are derived
|
|
33
|
+
* from `brew deps` minus `brew list --versions`, which is the same
|
|
34
|
+
* resolution the real install performs, and the derivation is
|
|
35
|
+
* named in the result as `brew-deps` rather than passed off as a
|
|
36
|
+
* manager-provided plan.
|
|
37
|
+
*/
|
|
38
|
+
import { MANAGERS, PKGMGR_COMMANDS, withOperand } from "./managers";
|
|
39
|
+
import { aptSaysNeedsRoot, dnfSaysNeedsRoot, pacmanSaysNeedsRoot, parseAptSimulate, parseBrewDeps, parseBrewListVersions, parseDnfTransaction, parsePacmanSimulate, } from "./parse";
|
|
40
|
+
import { run } from "./query";
|
|
41
|
+
import { commandFailureReason, firstLine } from "./unknown";
|
|
42
|
+
/**
|
|
43
|
+
* Decide what the install would do, and ask the manager what else it would
|
|
44
|
+
* drag in.
|
|
45
|
+
*
|
|
46
|
+
* `facts` comes from `queryPackage` — the same function `PackageQuery` calls,
|
|
47
|
+
* so the installed-version this plan reports and the one a caller can read
|
|
48
|
+
* for themselves are produced by one code path, not two.
|
|
49
|
+
*/
|
|
50
|
+
export async function resolveInstallPlan(ctx, manager, name, operand, requestedVersion, facts, unknowns) {
|
|
51
|
+
const notes = [];
|
|
52
|
+
const current = facts.installedVersions;
|
|
53
|
+
let action;
|
|
54
|
+
if (facts.status === "unknown") {
|
|
55
|
+
// Could not determine is not "not installed": the plan says `install`
|
|
56
|
+
// because that is what the command would do, and the unknown travels with
|
|
57
|
+
// it so nobody reads the missing current version as "there was none".
|
|
58
|
+
action = "install";
|
|
59
|
+
notes.push("whether this package is already installed could not be determined, so this plan cannot say whether the install would replace a working version");
|
|
60
|
+
}
|
|
61
|
+
else if (facts.status === "not-installed" || current === null || current.length === 0) {
|
|
62
|
+
action = "install";
|
|
63
|
+
}
|
|
64
|
+
else if (requestedVersion !== null && current.includes(requestedVersion)) {
|
|
65
|
+
action = "none";
|
|
66
|
+
}
|
|
67
|
+
else if (requestedVersion !== null) {
|
|
68
|
+
action = "upgrade";
|
|
69
|
+
notes.push(`version ${requestedVersion} was requested and ${current.join(", ")} is installed; the manager decides whether that is an upgrade or a downgrade`);
|
|
70
|
+
}
|
|
71
|
+
else if (facts.outdated === true) {
|
|
72
|
+
action = "upgrade";
|
|
73
|
+
}
|
|
74
|
+
else if (facts.available?.version !== undefined && facts.available.version !== null) {
|
|
75
|
+
action = current.includes(facts.available.version) ? "none" : "upgrade";
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// Installed, and nothing said a newer version exists. `reinstall` rather
|
|
79
|
+
// than `none`: the manager may still do work, and claiming "nothing would
|
|
80
|
+
// happen" without evidence is the guess this package does not make.
|
|
81
|
+
action = "reinstall";
|
|
82
|
+
notes.push("this package is installed and no available version could be read, so whether the command would change anything is not known");
|
|
83
|
+
}
|
|
84
|
+
const targetVersion = requestedVersion ?? facts.available?.version ?? null;
|
|
85
|
+
if (targetVersion === null) {
|
|
86
|
+
unknowns.add("plan.targetVersion", MANAGERS[manager].label, "the version this install would land on could not be read from the local index");
|
|
87
|
+
}
|
|
88
|
+
const closure = await resolveClosure(ctx, manager, operand, unknowns);
|
|
89
|
+
if (closure.reason !== undefined) {
|
|
90
|
+
unknowns.add("plan.additions", closure.probe, closure.reason);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
manager,
|
|
94
|
+
name,
|
|
95
|
+
operand,
|
|
96
|
+
requestedVersion,
|
|
97
|
+
action,
|
|
98
|
+
currentVersions: current,
|
|
99
|
+
targetVersion,
|
|
100
|
+
additions: closure.additions,
|
|
101
|
+
removals: closure.removals,
|
|
102
|
+
planSource: closure.source,
|
|
103
|
+
...(closure.reason !== undefined ? { planUnavailableReason: closure.reason } : {}),
|
|
104
|
+
notes: [...notes, ...closure.notes],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function resolveClosure(ctx, manager, operand, unknowns) {
|
|
108
|
+
switch (manager) {
|
|
109
|
+
case "apt":
|
|
110
|
+
return closureApt(ctx, operand);
|
|
111
|
+
case "pacman":
|
|
112
|
+
return closurePacman(ctx, operand);
|
|
113
|
+
case "dnf":
|
|
114
|
+
return closureDnf(ctx, operand);
|
|
115
|
+
case "homebrew":
|
|
116
|
+
return closureBrew(ctx, operand, unknowns);
|
|
117
|
+
// Neither is ever reached: `../index.ts` refuses an install through these
|
|
118
|
+
// two before a plan is resolved. Kept total so the switch has no default.
|
|
119
|
+
case "winget":
|
|
120
|
+
case "choco":
|
|
121
|
+
return {
|
|
122
|
+
additions: null,
|
|
123
|
+
removals: null,
|
|
124
|
+
source: null,
|
|
125
|
+
probe: "not probed",
|
|
126
|
+
reason: MANAGERS[manager].install.supported
|
|
127
|
+
? "not probed"
|
|
128
|
+
: MANAGERS[manager].install.reason,
|
|
129
|
+
notes: [],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function closureApt(ctx, operand) {
|
|
134
|
+
const argv = withOperand(MANAGERS.apt, PKGMGR_COMMANDS.aptSimulate, operand);
|
|
135
|
+
const probe = argv.join(" ");
|
|
136
|
+
const result = await run(ctx, "apt", argv);
|
|
137
|
+
if (result.missing || result.timedOut) {
|
|
138
|
+
return {
|
|
139
|
+
additions: null,
|
|
140
|
+
removals: null,
|
|
141
|
+
source: null,
|
|
142
|
+
probe,
|
|
143
|
+
reason: commandFailureReason(result),
|
|
144
|
+
notes: [],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (result.stdoutTruncated === true) {
|
|
148
|
+
// A truncated simulation parses perfectly and is simply MISSING packages.
|
|
149
|
+
// Reporting the prefix as the plan is how an operator approves a
|
|
150
|
+
// transaction they have not seen.
|
|
151
|
+
return {
|
|
152
|
+
additions: null,
|
|
153
|
+
removals: null,
|
|
154
|
+
source: null,
|
|
155
|
+
probe,
|
|
156
|
+
reason: "apt's simulation output was cut at this package's capture ceiling, so the transaction it describes is incomplete and was not used",
|
|
157
|
+
notes: [],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (result.code !== 0) {
|
|
161
|
+
return {
|
|
162
|
+
additions: null,
|
|
163
|
+
removals: null,
|
|
164
|
+
source: null,
|
|
165
|
+
probe,
|
|
166
|
+
reason: aptSaysNeedsRoot(`${result.stdout}\n${result.stderr}`)
|
|
167
|
+
? "apt refused to simulate without root, which is unusual — `apt-get install -s` normally needs none"
|
|
168
|
+
: `${commandFailureReason(result)} (${firstLine(result.stderr) || firstLine(result.stdout)})`,
|
|
169
|
+
notes: [],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const changes = parseAptSimulate(result.stdout);
|
|
173
|
+
if (changes.length === 0) {
|
|
174
|
+
return {
|
|
175
|
+
additions: [],
|
|
176
|
+
removals: [],
|
|
177
|
+
source: "apt-simulate",
|
|
178
|
+
probe,
|
|
179
|
+
notes: ["apt's simulation lists no package changes, so the requested state already holds"],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const installs = changes.filter((c) => c.action === "install");
|
|
183
|
+
const removals = changes.filter((c) => c.action === "remove");
|
|
184
|
+
const notes = [];
|
|
185
|
+
if (removals.length > 0) {
|
|
186
|
+
notes.push(`this transaction REMOVES ${removals.length} package(s) — an install that removes something is the case worth reading before approving`);
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
additions: installs.map((c) => ({ name: c.name, version: c.version })),
|
|
190
|
+
removals: removals.map((c) => ({ name: c.name, version: c.version })),
|
|
191
|
+
source: "apt-simulate",
|
|
192
|
+
probe,
|
|
193
|
+
notes,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
async function closurePacman(ctx, operand) {
|
|
197
|
+
const argv = withOperand(MANAGERS.pacman, PKGMGR_COMMANDS.pacmanSimulate, operand);
|
|
198
|
+
const probe = argv.join(" ");
|
|
199
|
+
const result = await run(ctx, "pacman", argv);
|
|
200
|
+
if (result.missing || result.timedOut || result.stdoutTruncated === true) {
|
|
201
|
+
return {
|
|
202
|
+
additions: null,
|
|
203
|
+
removals: null,
|
|
204
|
+
source: null,
|
|
205
|
+
probe,
|
|
206
|
+
reason: result.stdoutTruncated === true
|
|
207
|
+
? "pacman's target list was cut at this package's capture ceiling, so it is incomplete and was not used"
|
|
208
|
+
: commandFailureReason(result),
|
|
209
|
+
notes: [],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (result.code !== 0) {
|
|
213
|
+
return {
|
|
214
|
+
additions: null,
|
|
215
|
+
removals: null,
|
|
216
|
+
source: null,
|
|
217
|
+
probe,
|
|
218
|
+
reason: pacmanSaysNeedsRoot(result.stderr)
|
|
219
|
+
? "pacman refused to print its targets without root, which is unusual — `pacman -S --print` normally needs none"
|
|
220
|
+
: `${commandFailureReason(result)}`,
|
|
221
|
+
notes: [],
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const targets = parsePacmanSimulate(result.stdout);
|
|
225
|
+
return {
|
|
226
|
+
additions: targets.map((t) => ({ name: t.name, version: t.version, source: t.repo })),
|
|
227
|
+
removals: [],
|
|
228
|
+
source: "pacman-print",
|
|
229
|
+
probe,
|
|
230
|
+
notes: targets.length === 0
|
|
231
|
+
? ["pacman printed no targets, so the requested state already holds"]
|
|
232
|
+
: [
|
|
233
|
+
"pacman's target list includes the named package itself as well as every dependency it would pull in",
|
|
234
|
+
],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
async function closureDnf(ctx, operand) {
|
|
238
|
+
const argv = withOperand(MANAGERS.dnf, PKGMGR_COMMANDS.dnfSimulate, operand);
|
|
239
|
+
const probe = argv.join(" ");
|
|
240
|
+
const result = await run(ctx, "dnf", argv);
|
|
241
|
+
const combined = `${result.stdout}\n${result.stderr}`;
|
|
242
|
+
if (result.missing || result.timedOut || result.stdoutTruncated === true) {
|
|
243
|
+
return {
|
|
244
|
+
additions: null,
|
|
245
|
+
removals: null,
|
|
246
|
+
source: null,
|
|
247
|
+
probe,
|
|
248
|
+
reason: result.stdoutTruncated === true
|
|
249
|
+
? "dnf's transaction table was cut at this package's capture ceiling, so it is incomplete and was not used"
|
|
250
|
+
: commandFailureReason(result),
|
|
251
|
+
notes: [],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const rows = parseDnfTransaction(result.stdout);
|
|
255
|
+
if (rows.length > 0) {
|
|
256
|
+
// `--assumeno` exits non-zero BY DESIGN after printing the table it then
|
|
257
|
+
// declines, so a non-zero exit here is the success case and the table is
|
|
258
|
+
// read before the exit code is consulted.
|
|
259
|
+
return {
|
|
260
|
+
additions: rows.map((r) => ({ name: r.name, version: r.version, source: r.repo })),
|
|
261
|
+
removals: [],
|
|
262
|
+
source: "dnf-assumeno",
|
|
263
|
+
probe,
|
|
264
|
+
notes: [
|
|
265
|
+
"dnf resolved this transaction and then declined it, which is what --assumeno does; nothing was installed",
|
|
266
|
+
],
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (dnfSaysNeedsRoot(combined)) {
|
|
270
|
+
return {
|
|
271
|
+
additions: null,
|
|
272
|
+
removals: null,
|
|
273
|
+
source: null,
|
|
274
|
+
probe,
|
|
275
|
+
reason: "dnf refused to resolve the transaction without root, so what it would install could not be determined — this tool does not acquire root",
|
|
276
|
+
notes: [],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
additions: null,
|
|
281
|
+
removals: null,
|
|
282
|
+
source: null,
|
|
283
|
+
probe,
|
|
284
|
+
reason: `dnf printed no transaction table this parser could read (${firstLine(combined) || "empty output"})`,
|
|
285
|
+
notes: [],
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Homebrew has no dry run, so the closure is DERIVED — and the result says so.
|
|
290
|
+
*
|
|
291
|
+
* `brew deps --formula <name>` prints the transitive runtime dependencies,
|
|
292
|
+
* and `brew list --formula --versions` prints what is already on disk. The
|
|
293
|
+
* additions are the difference. That is the same resolution `brew install`
|
|
294
|
+
* performs, but it is an inference rather than a manager-issued plan, which
|
|
295
|
+
* is why `planSource` is `brew-deps` and not something that sounds official.
|
|
296
|
+
*
|
|
297
|
+
* Two honest gaps, both stated in the notes rather than hidden: build-time
|
|
298
|
+
* dependencies are not listed (they are not installed for a bottle, which is
|
|
299
|
+
* the normal case, but are for a source build), and a dependency's own
|
|
300
|
+
* version is not resolved here.
|
|
301
|
+
*/
|
|
302
|
+
async function closureBrew(ctx, operand, unknowns) {
|
|
303
|
+
const depsArgv = withOperand(MANAGERS.homebrew, PKGMGR_COMMANDS.brewDeps, operand);
|
|
304
|
+
const depsProbe = depsArgv.join(" ");
|
|
305
|
+
const deps = await run(ctx, "homebrew", depsArgv);
|
|
306
|
+
if (deps.missing || deps.timedOut || deps.code !== 0 || deps.stdoutTruncated === true) {
|
|
307
|
+
return {
|
|
308
|
+
additions: null,
|
|
309
|
+
removals: null,
|
|
310
|
+
source: null,
|
|
311
|
+
probe: depsProbe,
|
|
312
|
+
reason: deps.stdoutTruncated === true
|
|
313
|
+
? "brew's dependency list was cut at this package's capture ceiling"
|
|
314
|
+
: commandFailureReason(deps),
|
|
315
|
+
notes: [],
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const wanted = parseBrewDeps(deps.stdout);
|
|
319
|
+
const listArgv = [...PKGMGR_COMMANDS.brewListVersions];
|
|
320
|
+
const listProbe = listArgv.join(" ");
|
|
321
|
+
const list = await run(ctx, "homebrew", listArgv);
|
|
322
|
+
if (list.missing || list.timedOut || list.code !== 0 || list.stdoutTruncated === true) {
|
|
323
|
+
// The dependency list is known and the installed set is not, so the
|
|
324
|
+
// ADDITIONS cannot be computed — reporting every dependency as an
|
|
325
|
+
// addition would overstate the change, which is the wrong direction to
|
|
326
|
+
// be wrong in when an operator is approving it.
|
|
327
|
+
unknowns.add("plan.additions", listProbe, commandFailureReason(list));
|
|
328
|
+
return {
|
|
329
|
+
additions: null,
|
|
330
|
+
removals: null,
|
|
331
|
+
source: null,
|
|
332
|
+
probe: listProbe,
|
|
333
|
+
reason: `brew's dependency list was read but the installed formulae could not be: ${commandFailureReason(list)}`,
|
|
334
|
+
notes: [],
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const installed = parseBrewListVersions(list.stdout);
|
|
338
|
+
const additions = wanted
|
|
339
|
+
.filter((dep) => !installed.has(dep))
|
|
340
|
+
.map((dep) => ({ name: dep, version: null }));
|
|
341
|
+
return {
|
|
342
|
+
additions,
|
|
343
|
+
removals: [],
|
|
344
|
+
source: "brew-deps",
|
|
345
|
+
probe: depsProbe,
|
|
346
|
+
notes: [
|
|
347
|
+
"Homebrew has no dry run for install; these additions are `brew deps` minus what `brew list --versions` reports installed, and their versions are not resolved",
|
|
348
|
+
"build-time dependencies are not listed — a bottle does not install them, a source build does",
|
|
349
|
+
],
|
|
350
|
+
};
|
|
351
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asking a manager about one package — the code both tools share.
|
|
3
|
+
*
|
|
4
|
+
* `PackageQuery` returns what this produces. `PackageInstall` calls exactly
|
|
5
|
+
* the same function to resolve what it would install, and again afterwards to
|
|
6
|
+
* report what it actually installed. That is not a convenience: house rule 9
|
|
7
|
+
* says a dry run must run the SAME selection code as the real path, and the
|
|
8
|
+
* only way to guarantee it is for there to be one path.
|
|
9
|
+
*
|
|
10
|
+
* ── THREE-VALUED, ALWAYS ──────────────────────────────────────────────────
|
|
11
|
+
*
|
|
12
|
+
* `status` is `"installed"`, `"not-installed"` or `"unknown"`, and the third
|
|
13
|
+
* one is never collapsed into the second. A manager that is not on this host,
|
|
14
|
+
* a command that hit its deadline, output that did not parse, a cache that
|
|
15
|
+
* was empty — every one of those is `"unknown"` with a reason, because a
|
|
16
|
+
* caller that reads "not installed" will install something, and installing
|
|
17
|
+
* because a probe timed out is how a tool breaks a machine.
|
|
18
|
+
*
|
|
19
|
+
* `knownToManager` is separate from `status` for the same reason: "apt has
|
|
20
|
+
* never heard of `curl-x`" and "apt has `curl` and it is not installed" are
|
|
21
|
+
* different answers, and only the second one means an install would work.
|
|
22
|
+
*/
|
|
23
|
+
import type { HostPlatform } from "../host";
|
|
24
|
+
import { type RunResult } from "../run";
|
|
25
|
+
import { type ManagerId } from "./managers";
|
|
26
|
+
import { type Unknowns } from "./unknown";
|
|
27
|
+
/** One command this package ran, kept so a result can show its own evidence. */
|
|
28
|
+
export type Probe = {
|
|
29
|
+
readonly argv: readonly string[];
|
|
30
|
+
readonly exitCode: number;
|
|
31
|
+
readonly ok: boolean;
|
|
32
|
+
/** Present when the program itself is not on this host. */
|
|
33
|
+
readonly missing?: boolean;
|
|
34
|
+
readonly timedOut?: boolean;
|
|
35
|
+
};
|
|
36
|
+
export type RunContext = {
|
|
37
|
+
readonly timeoutMs: number;
|
|
38
|
+
readonly signal?: AbortSignal;
|
|
39
|
+
/** Filled in as commands run, in order. */
|
|
40
|
+
readonly probes: Probe[];
|
|
41
|
+
};
|
|
42
|
+
export declare function newContext(timeoutMs?: number, signal?: AbortSignal): RunContext;
|
|
43
|
+
/** Run one command and record it. The only entry point to `../run.ts` here. */
|
|
44
|
+
export declare function run(ctx: RunContext, manager: ManagerId, argv: readonly string[]): Promise<RunResult>;
|
|
45
|
+
export type Detection = {
|
|
46
|
+
readonly kind: "found";
|
|
47
|
+
readonly manager: ManagerId;
|
|
48
|
+
/** Managers looked for before this one, and not found. */
|
|
49
|
+
readonly tried: readonly ManagerId[];
|
|
50
|
+
} | {
|
|
51
|
+
readonly kind: "unknown";
|
|
52
|
+
readonly tried: readonly ManagerId[];
|
|
53
|
+
readonly reason: string;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Find the system package manager, or say that there is none.
|
|
57
|
+
*
|
|
58
|
+
* A caller may name one, in which case only that one is probed and an absence
|
|
59
|
+
* is reported against it by name. Otherwise the platform's list is walked in
|
|
60
|
+
* order (see `managersForPlatform`) and the first one that answers its
|
|
61
|
+
* `--version` probe wins.
|
|
62
|
+
*
|
|
63
|
+
* On a platform with no entry — which includes the hostile `"other"` default
|
|
64
|
+
* a test gets when it forgets to declare a platform — nothing is probed and
|
|
65
|
+
* the answer is `unknown`. That is also the honest answer on a host with no
|
|
66
|
+
* package manager at all, which is what CI is: an unrecognised manager is
|
|
67
|
+
* reported as unknown rather than guessed at.
|
|
68
|
+
*/
|
|
69
|
+
export declare function detectManager(ctx: RunContext, platform: HostPlatform, requested?: ManagerId): Promise<Detection>;
|
|
70
|
+
export type Availability = {
|
|
71
|
+
/** The version the local index offers, or null when it could not be read. */
|
|
72
|
+
readonly version: string | null;
|
|
73
|
+
/** Which tap/repository/source it came from, or null. */
|
|
74
|
+
readonly source: string | null;
|
|
75
|
+
};
|
|
76
|
+
export type PackageFacts = {
|
|
77
|
+
readonly manager: ManagerId;
|
|
78
|
+
readonly name: string;
|
|
79
|
+
readonly status: "installed" | "not-installed" | "unknown";
|
|
80
|
+
/**
|
|
81
|
+
* Versions on disk. An ARRAY because Homebrew legitimately keeps several
|
|
82
|
+
* kegs of one formula, and reporting the first as "the" version would be
|
|
83
|
+
* wrong for exactly the formulae people ask about. `null` means could not
|
|
84
|
+
* determine, which is not the same as `[]`.
|
|
85
|
+
*/
|
|
86
|
+
readonly installedVersions: readonly string[] | null;
|
|
87
|
+
/**
|
|
88
|
+
* Whether the manager has a record of this name at all. `null` when that
|
|
89
|
+
* could not be determined either.
|
|
90
|
+
*/
|
|
91
|
+
readonly knownToManager: boolean | null;
|
|
92
|
+
readonly available: Availability | null;
|
|
93
|
+
/** Homebrew only: a pinned formula will not be upgraded. */
|
|
94
|
+
readonly pinned?: boolean;
|
|
95
|
+
/** The manager's own word for "there is a newer version", where it has one. */
|
|
96
|
+
readonly outdated?: boolean;
|
|
97
|
+
readonly kind?: "formula" | "cask";
|
|
98
|
+
readonly notes: readonly string[];
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Ask one manager about one name.
|
|
102
|
+
*
|
|
103
|
+
* `name` must already have passed `checkPackageName`; this function appends it
|
|
104
|
+
* to a frozen argv prefix and never inspects it again.
|
|
105
|
+
*/
|
|
106
|
+
export declare function queryPackage(ctx: RunContext, manager: ManagerId, name: string, unknowns: Unknowns, includeAvailable: boolean): Promise<PackageFacts>;
|