@crewhaus/tool-pkgmgr 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +11 -0
- package/dist/index.js +79 -7
- package/dist/lib/managers.d.ts +8 -4
- package/dist/lib/managers.js +34 -15
- package/dist/lib/names.d.ts +5 -1
- package/dist/lib/names.js +19 -3
- package/dist/lib/parse.d.ts +60 -4
- package/dist/lib/parse.js +126 -18
- package/dist/lib/query.d.ts +7 -0
- package/dist/lib/query.js +34 -5
- package/dist/paths.d.ts +21 -2
- package/dist/paths.js +72 -13
- package/package.json +11 -7
- package/dist/fixtures.d.ts +0 -174
- package/dist/fixtures.js +0 -334
package/dist/lib/parse.js
CHANGED
|
@@ -175,6 +175,106 @@ export function dpkgStatusIsInstalled(abbrev) {
|
|
|
175
175
|
export function dpkgSaysNoSuchPackage(stderr) {
|
|
176
176
|
return /no packages found matching/i.test(stderr);
|
|
177
177
|
}
|
|
178
|
+
/** The value after an indented `Label:`, trimmed; undefined if the line is not that label. */
|
|
179
|
+
function labelValue(line, label) {
|
|
180
|
+
if (line.length === 0 || (line[0] !== " " && line[0] !== "\t"))
|
|
181
|
+
return undefined;
|
|
182
|
+
const trimmed = line.trim();
|
|
183
|
+
if (!trimmed.startsWith(`${label}:`))
|
|
184
|
+
return undefined;
|
|
185
|
+
return trimmed.slice(label.length + 1).trim();
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* A version-table entry: ` *** <version> <priority>` for the installed one,
|
|
189
|
+
* ` <version> <priority>` for the others. The source lines under each
|
|
190
|
+
* are indented further (8 spaces) and are not versions.
|
|
191
|
+
*/
|
|
192
|
+
function versionEntry(line) {
|
|
193
|
+
let rest;
|
|
194
|
+
if (line.startsWith(" *** "))
|
|
195
|
+
rest = line.slice(5);
|
|
196
|
+
else if (line.startsWith(" ") && line.length > 5 && line[5] !== " ")
|
|
197
|
+
rest = line.slice(5);
|
|
198
|
+
else
|
|
199
|
+
return undefined;
|
|
200
|
+
const parts = rest.split(" ");
|
|
201
|
+
if (parts.length !== 2)
|
|
202
|
+
return undefined;
|
|
203
|
+
const [version, priority] = parts;
|
|
204
|
+
return version !== "" && /^-?[0-9]+$/.test(priority) ? version : undefined;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Split `apt-cache policy` output into its stanzas. A stanza starts at an
|
|
208
|
+
* unindented `<name>:` line. apt prints one per package the operand matched,
|
|
209
|
+
* and an operand it read as a pattern matches many: `apt-cache policy
|
|
210
|
+
* 'lib.+'` printed 116 on a Debian 12 image.
|
|
211
|
+
*/
|
|
212
|
+
export function parseAptPolicyStanzas(stdout) {
|
|
213
|
+
const drafts = [];
|
|
214
|
+
let current;
|
|
215
|
+
for (const line of lines(stdout)) {
|
|
216
|
+
if (line.length > 1 &&
|
|
217
|
+
line[0] !== " " &&
|
|
218
|
+
line[0] !== "\t" &&
|
|
219
|
+
line.endsWith(":") &&
|
|
220
|
+
!/\s/.test(line)) {
|
|
221
|
+
current = {
|
|
222
|
+
header: line.slice(0, -1),
|
|
223
|
+
installed: undefined,
|
|
224
|
+
candidate: undefined,
|
|
225
|
+
versions: [],
|
|
226
|
+
inTable: false,
|
|
227
|
+
};
|
|
228
|
+
drafts.push(current);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (current === undefined)
|
|
232
|
+
continue;
|
|
233
|
+
const installed = labelValue(line, "Installed");
|
|
234
|
+
if (installed !== undefined) {
|
|
235
|
+
current.installed = installed === "(none)" ? null : installed;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const candidate = labelValue(line, "Candidate");
|
|
239
|
+
if (candidate !== undefined) {
|
|
240
|
+
current.candidate = candidate === "(none)" ? null : candidate;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (labelValue(line, "Version table") !== undefined) {
|
|
244
|
+
current.inTable = true;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (current.inTable) {
|
|
248
|
+
const version = versionEntry(line);
|
|
249
|
+
if (version !== undefined)
|
|
250
|
+
current.versions.push(version);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return drafts.map((d) => ({
|
|
254
|
+
header: d.header,
|
|
255
|
+
installed: d.installed ?? null,
|
|
256
|
+
candidate: d.candidate ?? null,
|
|
257
|
+
versions: d.versions,
|
|
258
|
+
labelled: d.installed !== undefined || d.candidate !== undefined,
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The stanza for exactly `name`, or why there is none. `name:arch` also
|
|
263
|
+
* matches a lone stanza headed by the bare name, which is how apt prints the
|
|
264
|
+
* native architecture (`libc6:amd64` on amd64 prints `libc6:`). More than one
|
|
265
|
+
* stanza is never an answer about `name`, whatever their headers say.
|
|
266
|
+
*/
|
|
267
|
+
export function matchAptPolicy(stdout, name) {
|
|
268
|
+
const stanzas = parseAptPolicyStanzas(stdout);
|
|
269
|
+
if (stanzas.length === 0)
|
|
270
|
+
return { kind: "none" };
|
|
271
|
+
const only = stanzas.length === 1 ? stanzas[0] : undefined;
|
|
272
|
+
const bare = name.includes(":") ? name.slice(0, name.indexOf(":")) : undefined;
|
|
273
|
+
if (only !== undefined && (only.header === name || only.header === bare)) {
|
|
274
|
+
return only.labelled ? { kind: "exact", stanza: only } : { kind: "unlabelled" };
|
|
275
|
+
}
|
|
276
|
+
return { kind: "not-exact", headers: stanzas.map((st) => st.header) };
|
|
277
|
+
}
|
|
178
278
|
/**
|
|
179
279
|
* `apt-cache policy <name>` under `LC_ALL=C`.
|
|
180
280
|
*
|
|
@@ -190,24 +290,22 @@ export function dpkgSaysNoSuchPackage(stderr) {
|
|
|
190
290
|
* An unknown package produces an empty document (apt-cache prints nothing and
|
|
191
291
|
* exits 0), which parses to `undefined` rather than to two nulls, so "apt has
|
|
192
292
|
* never heard of this" cannot be confused with "apt has it, not installed".
|
|
293
|
+
* So does output with more than one stanza, or (given `name`) a stanza for a
|
|
294
|
+
* different name: that is apt answering about a PATTERN, and reading its last
|
|
295
|
+
* stanza as the answer reported another package's version (C021).
|
|
193
296
|
*/
|
|
194
|
-
export function parseAptCachePolicy(stdout) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
const candidateMatch = line.match(/^\s+Candidate:\s*(.+?)\s*$/);
|
|
204
|
-
if (candidateMatch?.[1] !== undefined) {
|
|
205
|
-
candidate = candidateMatch[1] === "(none)" ? null : candidateMatch[1];
|
|
206
|
-
}
|
|
297
|
+
export function parseAptCachePolicy(stdout, name) {
|
|
298
|
+
const stanzas = parseAptPolicyStanzas(stdout);
|
|
299
|
+
if (name !== undefined) {
|
|
300
|
+
const match = matchAptPolicy(stdout, name);
|
|
301
|
+
return match.kind === "exact"
|
|
302
|
+
? { installed: match.stanza.installed, candidate: match.stanza.candidate }
|
|
303
|
+
: undefined;
|
|
207
304
|
}
|
|
208
|
-
|
|
305
|
+
const only = stanzas.length === 1 ? stanzas[0] : undefined;
|
|
306
|
+
if (only === undefined || !only.labelled)
|
|
209
307
|
return undefined;
|
|
210
|
-
return { installed: installed
|
|
308
|
+
return { installed: only.installed, candidate: only.candidate };
|
|
211
309
|
}
|
|
212
310
|
/**
|
|
213
311
|
* `apt-get install -s` — the best dry run of the six managers.
|
|
@@ -521,14 +619,24 @@ export function parseWingetList(stdout) {
|
|
|
521
619
|
return rows;
|
|
522
620
|
}
|
|
523
621
|
/**
|
|
524
|
-
* winget's "no installed package matched" exit code
|
|
622
|
+
* winget's "no installed package matched" exit code:
|
|
623
|
+
* APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND, 0x8A150014, as a signed
|
|
525
624
|
* 32-bit integer.
|
|
526
625
|
*
|
|
527
626
|
* `winget list` exits with this rather than 0-and-empty, which makes presence
|
|
528
627
|
* decidable from the EXIT CODE alone — the one part of winget's answer that
|
|
529
|
-
* is neither localised nor column-aligned.
|
|
628
|
+
* is neither localised nor column-aligned. 0.7.0 had 0x8A15002B here, which
|
|
629
|
+
* is APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE (`winget upgrade` with
|
|
630
|
+
* nothing to do), so a package that was not installed came back unknown.
|
|
631
|
+
* Compare against `code | 0`: a runtime may report the DWORD unsigned.
|
|
632
|
+
*/
|
|
633
|
+
export const WINGET_NO_PACKAGES_FOUND = -1978335212;
|
|
634
|
+
/**
|
|
635
|
+
* APPINSTALLER_CLI_ERROR_SOURCE_AGREEMENTS_NOT_ACCEPTED, 0x8A150046: with
|
|
636
|
+
* `--disable-interactivity`, a configured source whose agreement nobody has
|
|
637
|
+
* accepted makes `winget list` refuse outright instead of prompting.
|
|
530
638
|
*/
|
|
531
|
-
export const
|
|
639
|
+
export const WINGET_SOURCE_AGREEMENTS_NOT_ACCEPTED = -1978335162;
|
|
532
640
|
// ---------------------------------------------------------------------------
|
|
533
641
|
// chocolatey
|
|
534
642
|
// ---------------------------------------------------------------------------
|
package/dist/lib/query.d.ts
CHANGED
|
@@ -96,6 +96,13 @@ export type PackageFacts = {
|
|
|
96
96
|
readonly outdated?: boolean;
|
|
97
97
|
readonly kind?: "formula" | "cask";
|
|
98
98
|
readonly notes: readonly string[];
|
|
99
|
+
/**
|
|
100
|
+
* apt only, and only when `apt-cache policy` answered with exactly one
|
|
101
|
+
* stanza for exactly this name: every version its version table lists.
|
|
102
|
+
* Absent means apt's answer was not about this exact name (or could not be
|
|
103
|
+
* read), which PackageInstall refuses to act on (C021).
|
|
104
|
+
*/
|
|
105
|
+
readonly versionTable?: readonly string[];
|
|
99
106
|
};
|
|
100
107
|
/**
|
|
101
108
|
* Ask one manager about one name.
|
package/dist/lib/query.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_COMMAND_TIMEOUT_MS, runHostCommand } from "../run";
|
|
2
2
|
import { MANAGERS, MANAGER_ENV, PKGMGR_COMMANDS, managersForPlatform, withOperand, } from "./managers";
|
|
3
|
-
import { WINGET_NO_PACKAGES_FOUND, aptSaysNoSuchPackage, brewSaysNoSuchPackage, dnfSaysNoCache, dnfSaysNoMatch, dpkgSaysNoSuchPackage, dpkgStatusIsInstalled,
|
|
3
|
+
import { WINGET_NO_PACKAGES_FOUND, WINGET_SOURCE_AGREEMENTS_NOT_ACCEPTED, aptSaysNoSuchPackage, brewSaysNoSuchPackage, dnfSaysNoCache, dnfSaysNoMatch, dpkgSaysNoSuchPackage, dpkgStatusIsInstalled, matchAptPolicy, pacmanSaysNoSuchPackage, parseBrewInfoJson, parseChocoList, parseChocoMajorVersion, parseDnfList, parseDpkgQuery, parsePacmanInfo, parsePacmanQuery, parseRpmQuery, parseWingetList, rpmSaysNotInstalled, } from "./parse";
|
|
4
4
|
import { commandFailureReason, firstLine } from "./unknown";
|
|
5
5
|
export function newContext(timeoutMs = DEFAULT_COMMAND_TIMEOUT_MS, signal) {
|
|
6
6
|
return { timeoutMs, ...(signal !== undefined ? { signal } : {}), probes: [] };
|
|
@@ -197,6 +197,7 @@ async function queryApt(ctx, name, unknowns, includeAvailable) {
|
|
|
197
197
|
let status = "unknown";
|
|
198
198
|
let installedVersions = null;
|
|
199
199
|
let knownToManager = null;
|
|
200
|
+
let versionTable;
|
|
200
201
|
const notes = [];
|
|
201
202
|
if (dpkg.missing || dpkg.timedOut || truncated(dpkg)) {
|
|
202
203
|
unknowns.add("status", dpkgProbe, truncated(dpkg)
|
|
@@ -256,9 +257,28 @@ async function queryApt(ctx, name, unknowns, includeAvailable) {
|
|
|
256
257
|
unknowns.add("available.version", policyProbe, commandFailureReason(policy));
|
|
257
258
|
}
|
|
258
259
|
else {
|
|
259
|
-
const
|
|
260
|
-
if (
|
|
261
|
-
|
|
260
|
+
const match = matchAptPolicy(policy.stdout, name);
|
|
261
|
+
if (match.kind === "not-exact") {
|
|
262
|
+
// apt read the name as a pattern (or the output answers about some
|
|
263
|
+
// other name). None of it is about THIS name, and the old parser
|
|
264
|
+
// reported the last stanza's candidate as this package's (C021).
|
|
265
|
+
const shown = match.headers.slice(0, 3).join(", ");
|
|
266
|
+
const reason = `apt-cache matched ${match.headers.length} package(s) as a pattern (${shown}${match.headers.length > 3 ? ", …" : ""}); none is named exactly ${JSON.stringify(name)}, so apt's answer is not about this name`;
|
|
267
|
+
// dpkg's own exact row, when it found one, still stands; anything
|
|
268
|
+
// else ("not installed" because dpkg matched nothing literally) is
|
|
269
|
+
// not an answer about a package name either.
|
|
270
|
+
if (status !== "installed") {
|
|
271
|
+
status = "unknown";
|
|
272
|
+
installedVersions = null;
|
|
273
|
+
knownToManager = null;
|
|
274
|
+
unknowns.add("status", policyProbe, reason);
|
|
275
|
+
}
|
|
276
|
+
available = null;
|
|
277
|
+
unknowns.add("available.version", policyProbe, reason);
|
|
278
|
+
}
|
|
279
|
+
else if (match.kind === "none" || match.kind === "unlabelled") {
|
|
280
|
+
if (match.kind === "none" &&
|
|
281
|
+
(policy.stdout.trim() === "" || aptSaysNoSuchPackage(policy.stderr))) {
|
|
262
282
|
// apt-cache prints nothing at all for a name it does not know.
|
|
263
283
|
if (knownToManager === null)
|
|
264
284
|
knownToManager = false;
|
|
@@ -270,6 +290,8 @@ async function queryApt(ctx, name, unknowns, includeAvailable) {
|
|
|
270
290
|
}
|
|
271
291
|
}
|
|
272
292
|
else {
|
|
293
|
+
const parsed = match.stanza;
|
|
294
|
+
versionTable = parsed.versions;
|
|
273
295
|
knownToManager = true;
|
|
274
296
|
available = parsed.candidate === null ? null : { version: parsed.candidate, source: "apt" };
|
|
275
297
|
if (parsed.candidate === null) {
|
|
@@ -295,6 +317,7 @@ async function queryApt(ctx, name, unknowns, includeAvailable) {
|
|
|
295
317
|
knownToManager,
|
|
296
318
|
available,
|
|
297
319
|
notes,
|
|
320
|
+
...(versionTable !== undefined ? { versionTable } : {}),
|
|
298
321
|
};
|
|
299
322
|
}
|
|
300
323
|
// ---- dnf / rpm ------------------------------------------------------------
|
|
@@ -451,7 +474,13 @@ async function queryWinget(ctx, name, unknowns, _includeAvailable) {
|
|
|
451
474
|
unknowns.add("status", probe, commandFailureReason(result));
|
|
452
475
|
return unknownFacts("winget", name);
|
|
453
476
|
}
|
|
454
|
-
|
|
477
|
+
// winget's codes are HRESULTs; a runtime may hand the DWORD back unsigned.
|
|
478
|
+
const code = result.code | 0;
|
|
479
|
+
if (code === WINGET_SOURCE_AGREEMENTS_NOT_ACCEPTED) {
|
|
480
|
+
unknowns.add("status", probe, "winget refused to list because a configured source's agreements have not been accepted on this host. Accepting a licence is the operator's decision, never a side effect of a read: run `winget list` once yourself and accept them (or remove that source), then ask again");
|
|
481
|
+
return unknownFacts("winget", name);
|
|
482
|
+
}
|
|
483
|
+
if (code === WINGET_NO_PACKAGES_FOUND) {
|
|
455
484
|
// The one part of winget's answer that is neither localised nor
|
|
456
485
|
// column-aligned: its documented "no installed package matched" code.
|
|
457
486
|
return {
|
package/dist/paths.d.ts
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import { CrewhausError } from "@crewhaus/errors";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* The longest path this resolves, in UTF-16 code units; a longer one is
|
|
4
|
+
* refused before it is walked. Nothing that long can be opened or created:
|
|
5
|
+
* PATH_MAX is 1,024 bytes on macOS and 4,096 on Linux, Windows stops at
|
|
6
|
+
* 32,767 UTF-16 units, and a path's UTF-8 form is never shorter than its
|
|
7
|
+
* UTF-16 one. The limit is twice that because the part of a path that exists
|
|
8
|
+
* is itself under PATH_MAX, so the part that does not is over PATH_MAX on its
|
|
9
|
+
* own: no spelling of the place reaches it, not even one through a symlink
|
|
10
|
+
* that shortens the part that exists.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MAX_PATH_CHARS: number;
|
|
13
|
+
/**
|
|
14
|
+
* Raised when a caller-supplied path resolves outside the workspace root, or
|
|
15
|
+
* is longer than any path can be.
|
|
16
|
+
*/
|
|
3
17
|
export declare class ToolPermissionError extends CrewhausError {
|
|
4
18
|
readonly name = "ToolPermissionError";
|
|
5
19
|
readonly toolName: string;
|
|
6
20
|
readonly path: string;
|
|
7
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Why, when it is not that the location escapes the workspace root: a path
|
|
23
|
+
* longer than any path can be. A caller that rewords the refusal says this.
|
|
24
|
+
*/
|
|
25
|
+
readonly reason: string | undefined;
|
|
26
|
+
constructor(toolName: string, attemptedPath: string, reason?: string);
|
|
8
27
|
}
|
|
9
28
|
/**
|
|
10
29
|
* A validated path, in both forms the tools need.
|
package/dist/paths.js
CHANGED
|
@@ -11,15 +11,45 @@
|
|
|
11
11
|
import { lstatSync, readlinkSync, realpathSync } from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { CrewhausError } from "@crewhaus/errors";
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* The longest path this resolves, in UTF-16 code units; a longer one is
|
|
16
|
+
* refused before it is walked. Nothing that long can be opened or created:
|
|
17
|
+
* PATH_MAX is 1,024 bytes on macOS and 4,096 on Linux, Windows stops at
|
|
18
|
+
* 32,767 UTF-16 units, and a path's UTF-8 form is never shorter than its
|
|
19
|
+
* UTF-16 one. The limit is twice that because the part of a path that exists
|
|
20
|
+
* is itself under PATH_MAX, so the part that does not is over PATH_MAX on its
|
|
21
|
+
* own: no spelling of the place reaches it, not even one through a symlink
|
|
22
|
+
* that shortens the part that exists.
|
|
23
|
+
*/
|
|
24
|
+
export const MAX_PATH_CHARS = 2 * (process.platform === "darwin" ? 1_024 : process.platform === "win32" ? 32_767 : 4_096);
|
|
25
|
+
/** A path as a refusal quotes it: whole, unless it is past {@link MAX_PATH_CHARS}. */
|
|
26
|
+
function shownPath(given) {
|
|
27
|
+
return given.length > MAX_PATH_CHARS ? `${given.slice(0, 64)}…` : given;
|
|
28
|
+
}
|
|
29
|
+
/** Why a path past {@link MAX_PATH_CHARS} is refused: its length and the limit. */
|
|
30
|
+
function tooLong(abs) {
|
|
31
|
+
return `it is ${abs.length} characters once resolved, over the ${MAX_PATH_CHARS}-character limit: no path that long can be opened or created`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Raised when a caller-supplied path resolves outside the workspace root, or
|
|
35
|
+
* is longer than any path can be.
|
|
36
|
+
*/
|
|
15
37
|
export class ToolPermissionError extends CrewhausError {
|
|
16
38
|
name = "ToolPermissionError";
|
|
17
39
|
toolName;
|
|
18
40
|
path;
|
|
19
|
-
|
|
20
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Why, when it is not that the location escapes the workspace root: a path
|
|
43
|
+
* longer than any path can be. A caller that rewords the refusal says this.
|
|
44
|
+
*/
|
|
45
|
+
reason;
|
|
46
|
+
constructor(toolName, attemptedPath, reason) {
|
|
47
|
+
super("tool", reason === undefined
|
|
48
|
+
? `tool "${toolName}" rejected path "${attemptedPath}": resolved location escapes the workspace root`
|
|
49
|
+
: `tool "${toolName}" rejected path "${shownPath(attemptedPath)}": ${reason}`);
|
|
21
50
|
this.toolName = toolName;
|
|
22
51
|
this.path = attemptedPath;
|
|
52
|
+
this.reason = reason;
|
|
23
53
|
}
|
|
24
54
|
}
|
|
25
55
|
/** The workspace root, resolved. Every path is contained within it. */
|
|
@@ -67,15 +97,7 @@ function nameExists(p) {
|
|
|
67
97
|
function resolveLocation(target, depth = 0) {
|
|
68
98
|
if (depth > 40)
|
|
69
99
|
throw new Error(`symlink chain at "${target}" is too long to resolve`);
|
|
70
|
-
|
|
71
|
-
const tail = [];
|
|
72
|
-
while (!nameExists(probe)) {
|
|
73
|
-
tail.unshift(path.basename(probe));
|
|
74
|
-
const parent = path.dirname(probe);
|
|
75
|
-
if (parent === probe)
|
|
76
|
-
break; // reached the filesystem root
|
|
77
|
-
probe = parent;
|
|
78
|
-
}
|
|
100
|
+
const { probe, tail } = deepestName(target);
|
|
79
101
|
let probeReal;
|
|
80
102
|
try {
|
|
81
103
|
probeReal = realpathSync(probe);
|
|
@@ -97,7 +119,41 @@ function resolveLocation(target, depth = 0) {
|
|
|
97
119
|
const base = realpathSync(path.dirname(probe));
|
|
98
120
|
probeReal = resolveLocation(path.resolve(base, link), depth + 1);
|
|
99
121
|
}
|
|
100
|
-
|
|
122
|
+
// One string, not one argument per name: Bun's `path.join` keeps only
|
|
123
|
+
// (argument count mod 65,536) of its arguments.
|
|
124
|
+
return tail.length > 0 ? path.join(probeReal, tail) : probeReal;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* `target`, an absolute path, split where its deepest prefix that exists as
|
|
128
|
+
* a NAME ends: that prefix, and the names below it, which do not exist, as
|
|
129
|
+
* one string ("" when `target` itself exists). This is `path.dirname` and
|
|
130
|
+
* `path.basename` worked by index on `target` itself: handing them each
|
|
131
|
+
* shorter prefix in turn copied the rest of the path once per missing name,
|
|
132
|
+
* the square of a count the caller picks.
|
|
133
|
+
*/
|
|
134
|
+
function deepestName(target) {
|
|
135
|
+
const top = path.parse(target).root.length;
|
|
136
|
+
const isSep = (i) => target[i] === "/" || target[i] === path.sep;
|
|
137
|
+
let probe = target;
|
|
138
|
+
let start = target.length;
|
|
139
|
+
let stop = -1;
|
|
140
|
+
while (!nameExists(probe)) {
|
|
141
|
+
// `path.basename(probe)` is `target.slice(start, end)`, and
|
|
142
|
+
// `path.dirname(probe)` is `target.slice(0, up)`.
|
|
143
|
+
let end = probe.length;
|
|
144
|
+
while (end > top && isSep(end - 1))
|
|
145
|
+
end--;
|
|
146
|
+
if (stop < 0)
|
|
147
|
+
stop = end;
|
|
148
|
+
start = end;
|
|
149
|
+
while (start > top && !isSep(start - 1))
|
|
150
|
+
start--;
|
|
151
|
+
const up = start > top ? start - 1 : top;
|
|
152
|
+
if (up === probe.length)
|
|
153
|
+
break; // reached the filesystem root
|
|
154
|
+
probe = target.slice(0, up);
|
|
155
|
+
}
|
|
156
|
+
return { probe, tail: stop < 0 ? "" : target.slice(start, stop) };
|
|
101
157
|
}
|
|
102
158
|
export function resolveSafe(toolName, rel, root = workspaceRoot()) {
|
|
103
159
|
const rootResolved = path.resolve(root);
|
|
@@ -107,6 +163,9 @@ export function resolveSafe(toolName, rel, root = workspaceRoot()) {
|
|
|
107
163
|
if (abs !== rootResolved && !abs.startsWith(`${rootResolved}${path.sep}`)) {
|
|
108
164
|
throw new ToolPermissionError(toolName, rel);
|
|
109
165
|
}
|
|
166
|
+
// Longer than any path can be: refused before the walk below reads it.
|
|
167
|
+
if (abs.length > MAX_PATH_CHARS)
|
|
168
|
+
throw new ToolPermissionError(toolName, rel, tooLong(abs));
|
|
110
169
|
let real;
|
|
111
170
|
try {
|
|
112
171
|
const rootReal = realpathSync(rootResolved);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/tool-pkgmgr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic system package-manager queries and installs, with no privilege escalation and a dry run that runs the real selection",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,17 +8,18 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js"
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
12
13
|
}
|
|
13
14
|
},
|
|
14
15
|
"scripts": {
|
|
15
16
|
"test": "bun test src"
|
|
16
17
|
},
|
|
17
18
|
"dependencies": {
|
|
18
|
-
"@crewhaus/errors": "0.
|
|
19
|
-
"@crewhaus/tool-builder": "0.
|
|
20
|
-
"@crewhaus/tool-catalog": "0.
|
|
21
|
-
"@crewhaus/tool-proc": "0.
|
|
19
|
+
"@crewhaus/errors": "0.8.1",
|
|
20
|
+
"@crewhaus/tool-builder": "0.8.1",
|
|
21
|
+
"@crewhaus/tool-catalog": "0.8.1",
|
|
22
|
+
"@crewhaus/tool-proc": "0.8.1",
|
|
22
23
|
"zod": "^3.23.8"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
@@ -42,5 +43,8 @@
|
|
|
42
43
|
"publishConfig": {
|
|
43
44
|
"access": "public"
|
|
44
45
|
},
|
|
45
|
-
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
46
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"],
|
|
47
|
+
"engines": {
|
|
48
|
+
"bun": ">=1.3.14"
|
|
49
|
+
}
|
|
46
50
|
}
|
package/dist/fixtures.d.ts
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Recorded manager output, and the runner that serves it.
|
|
3
|
-
*
|
|
4
|
-
* Every string below is what a real manager printed on a real machine. None
|
|
5
|
-
* of it is invented to make a parser pass: the trailing space in dpkg's
|
|
6
|
-
* `"ii "` status, the `(none)` apt prints for a package it has but has not
|
|
7
|
-
* installed, the `Operation aborted.` dnf ends an `--assumeno` run with, the
|
|
8
|
-
* ellipsis winget cuts a wide column with, and the fact that `rpm -q` writes
|
|
9
|
-
* "is not installed" to STDOUT rather than stderr are all facts about those
|
|
10
|
-
* programs that a hand-written fixture would have got wrong.
|
|
11
|
-
*
|
|
12
|
-
* WHY THIS FILE EXISTS AT ALL. CI is a Linux container with no package
|
|
13
|
-
* manager installed; this was written on macOS with Homebrew. A test that
|
|
14
|
-
* asked the real host anything would assert one thing here and another there,
|
|
15
|
-
* which is not a test. So the host is behind `../run.ts`'s seam, the platform
|
|
16
|
-
* is behind `../host.ts`'s (with a hostile default, so forgetting the seam
|
|
17
|
-
* fails identically everywhere), and this file is the world.
|
|
18
|
-
*
|
|
19
|
-
* `recordedRunner` refuses a command nobody recorded rather than returning an
|
|
20
|
-
* empty result: an unrecorded command that quietly answered "" would look
|
|
21
|
-
* exactly like a manager that had nothing to say, and every "not installed"
|
|
22
|
-
* in the suite would stop meaning anything.
|
|
23
|
-
*/
|
|
24
|
-
import type { RunRequest, Runner } from "./run";
|
|
25
|
-
export declare const BREW_VERSION = "Homebrew 4.3.8\nHomebrew/homebrew-core (git revision 2e6b2c3f4a1; last commit 2026-09-10)\n";
|
|
26
|
-
/** `brew info --json=v2 curl` — installed, and the tap has a newer version. */
|
|
27
|
-
export declare const BREW_INFO_CURL = "{\"formulae\":[{\"name\":\"curl\",\"full_name\":\"curl\",\"tap\":\"homebrew/core\",\"oldnames\":[],\"aliases\":[],\"versions\":{\"stable\":\"8.9.1\",\"head\":\"HEAD\",\"bottle\":true},\"installed\":[{\"version\":\"8.8.0\",\"used_options\":[],\"built_as_bottle\":true,\"poured_from_bottle\":true,\"installed_as_dependency\":false,\"installed_on_request\":true}],\"dependencies\":[\"brotli\",\"libnghttp2\",\"libssh2\",\"openldap\",\"rtmpdump\",\"zstd\"],\"pinned\":false,\"outdated\":true,\"deprecated\":false,\"disabled\":false}],\"casks\":[]}";
|
|
28
|
-
/** `brew info --json=v2 tree` — known to the tap, not installed. */
|
|
29
|
-
export declare const BREW_INFO_TREE = "{\"formulae\":[{\"name\":\"tree\",\"full_name\":\"tree\",\"tap\":\"homebrew/core\",\"versions\":{\"stable\":\"2.2.1\",\"head\":null,\"bottle\":true},\"installed\":[],\"dependencies\":[],\"pinned\":false,\"outdated\":false,\"deprecated\":false,\"disabled\":false}],\"casks\":[]}";
|
|
30
|
-
/**
|
|
31
|
-
* `brew info --json=v2 python@3.11` — two kegs of one formula.
|
|
32
|
-
*
|
|
33
|
-
* The reason `installedVersions` is an array: reporting the first as "the"
|
|
34
|
-
* installed version is wrong for exactly the formulae people ask about.
|
|
35
|
-
*/
|
|
36
|
-
export declare const BREW_INFO_PYTHON = "{\"formulae\":[{\"name\":\"python@3.11\",\"full_name\":\"python@3.11\",\"tap\":\"homebrew/core\",\"versions\":{\"stable\":\"3.11.9\",\"head\":null,\"bottle\":true},\"installed\":[{\"version\":\"3.11.9\",\"installed_on_request\":true},{\"version\":\"3.11.8\",\"installed_on_request\":false}],\"dependencies\":[\"mpdecimal\",\"openssl@3\",\"sqlite\",\"xz\"],\"pinned\":true,\"outdated\":false}],\"casks\":[]}";
|
|
37
|
-
/** `brew info --json=v2 firefox` — a cask, which spells everything differently. */
|
|
38
|
-
export declare const BREW_INFO_FIREFOX = "{\"formulae\":[],\"casks\":[{\"token\":\"firefox\",\"full_token\":\"homebrew/cask/firefox\",\"tap\":\"homebrew/cask\",\"name\":[\"Mozilla Firefox\"],\"version\":\"142.0.1\",\"installed\":\"141.0\",\"outdated\":true}]}";
|
|
39
|
-
/** `brew info --json=v2 nope-nope` — exit 1, and this exact sentence. */
|
|
40
|
-
export declare const BREW_NO_SUCH_FORMULA = "Error: No available formula with the name \"nope-nope\".\n==> Searching for similarly named formulae and casks...\n";
|
|
41
|
-
export declare const BREW_DEPS_CURL = "brotli\nlibnghttp2\nlibssh2\nopenldap\nrtmpdump\nzstd\n";
|
|
42
|
-
export declare const BREW_DEPS_TREE = "";
|
|
43
|
-
/** `brew list --formula --versions` — four of the six curl deps are present. */
|
|
44
|
-
export declare const BREW_LIST_VERSIONS = "brotli 1.1.0\nca-certificates 2026-08-12\nlibnghttp2 1.62.1\nopenldap 2.6.7\npython@3.11 3.11.9 3.11.8\nzstd 1.5.6\n";
|
|
45
|
-
export declare const BREW_INSTALL_TREE = "==> Fetching tree\n==> Downloading https://ghcr.io/v2/homebrew/core/tree/blobs/sha256:9f3c1ad\n==> Pouring tree--2.2.1.arm64_sonoma.bottle.tar.gz\n/opt/homebrew/Cellar/tree/2.2.1: 8 files, 162.5KB\n";
|
|
46
|
-
/** `brew info --json=v2 tree`, re-run after the install above succeeded. */
|
|
47
|
-
export declare const BREW_INFO_TREE_AFTER = "{\"formulae\":[{\"name\":\"tree\",\"full_name\":\"tree\",\"tap\":\"homebrew/core\",\"versions\":{\"stable\":\"2.2.1\",\"head\":null,\"bottle\":true},\"installed\":[{\"version\":\"2.2.1\",\"installed_on_request\":true}],\"dependencies\":[],\"pinned\":false,\"outdated\":false}],\"casks\":[]}";
|
|
48
|
-
export declare const DPKG_VERSION = "Debian dpkg-query package management program version 1.21.22 (amd64).\nThis is free software; see the GNU General Public License version 2 or\nlater for copying conditions. There is NO warranty.\n";
|
|
49
|
-
/** Note the trailing space: `${db:Status-Abbrev}` is three characters wide. */
|
|
50
|
-
export declare const DPKG_QUERY_CURL = "curl\t7.88.1-10+deb12u5\tii \n";
|
|
51
|
-
/**
|
|
52
|
-
* A package dpkg still has a RECORD of but does not have installed: removed,
|
|
53
|
-
* configuration files left behind. Reading `rc` as installed is the classic
|
|
54
|
-
* dpkg mistake — the binary is gone and only `/etc` remains.
|
|
55
|
-
*/
|
|
56
|
-
export declare const DPKG_QUERY_NANO_REMOVED = "nano\t7.2-1\trc \n";
|
|
57
|
-
/** dpkg has no record at all of a package that was never installed. */
|
|
58
|
-
export declare const DPKG_NO_SUCH_PACKAGE = "dpkg-query: no packages found matching htop\n";
|
|
59
|
-
export declare const APT_POLICY_CURL = "curl:\n Installed: 7.88.1-10+deb12u5\n Candidate: 7.88.1-10+deb12u7\n Version table:\n *** 7.88.1-10+deb12u5 100\n 100 /var/lib/dpkg/status\n 7.88.1-10+deb12u7 500\n 500 http://deb.debian.org/debian-security bookworm-security/main amd64 Packages\n";
|
|
60
|
-
export declare const APT_POLICY_HTOP = "htop:\n Installed: (none)\n Candidate: 3.2.2-2\n Version table:\n 3.2.2-2 500\n 500 http://deb.debian.org/debian bookworm/main amd64 Packages\n";
|
|
61
|
-
/** A package apt has in its lists with no installable candidate at all. */
|
|
62
|
-
export declare const APT_POLICY_NO_CANDIDATE = "phantom:\n Installed: (none)\n Candidate: (none)\n Version table:\n";
|
|
63
|
-
/** apt-cache prints NOTHING for a name it does not know, and exits 0. */
|
|
64
|
-
export declare const APT_POLICY_UNKNOWN_STDERR = "N: Unable to locate package nosuchpkg\n";
|
|
65
|
-
/**
|
|
66
|
-
* The same `apt-cache policy curl` on a de_DE host.
|
|
67
|
-
*
|
|
68
|
-
* This is the fixture that gives the localisation trap a name. Every parser
|
|
69
|
-
* in this package reads English labels, which is sound ONLY because
|
|
70
|
-
* `../run.ts` pins `LC_ALL=C` on every child. Without that pin the parse does
|
|
71
|
-
* not fail loudly — it finds nothing, and "nothing" reads as "not installed".
|
|
72
|
-
*/
|
|
73
|
-
export declare const APT_POLICY_CURL_GERMAN = "curl:\n Installiert: 7.88.1-10+deb12u5\n Installationskandidat: 7.88.1-10+deb12u7\n Versionstabelle:\n *** 7.88.1-10+deb12u5 100\n 100 /var/lib/dpkg/status\n";
|
|
74
|
-
export declare const APT_SIMULATE_HTOP = "NOTE: This is only a simulation!\n apt-get needs root privileges for real execution.\n Keep also in mind that locking is deactivated,\n so don't depend on the relevance to the real current situation!\nReading package lists...\nBuilding dependency tree...\nReading state information...\nThe following additional packages will be installed:\n libnl-3-200 libnl-genl-3-200\nThe following NEW packages will be installed:\n htop libnl-3-200 libnl-genl-3-200\n0 upgraded, 3 newly installed, 0 to remove and 0 not upgraded.\nInst libnl-3-200 (3.7.0-0.2+b1 Debian:12.5/stable [amd64])\nInst libnl-genl-3-200 (3.7.0-0.2+b1 Debian:12.5/stable [amd64])\nInst htop (3.2.2-2 Debian:12.5/stable [amd64])\nConf libnl-3-200 (3.7.0-0.2+b1 Debian:12.5/stable [amd64])\nConf libnl-genl-3-200 (3.7.0-0.2+b1 Debian:12.5/stable [amd64])\nConf htop (3.2.2-2 Debian:12.5/stable [amd64])\n";
|
|
75
|
-
/**
|
|
76
|
-
* An UPGRADE that also removes something — the case an operator most needs to
|
|
77
|
-
* see before approving an install. Note the `[old-version]` bracket between
|
|
78
|
-
* the name and the parenthesis, which only an upgrade has.
|
|
79
|
-
*/
|
|
80
|
-
export declare const APT_SIMULATE_CURL_UPGRADE = "Reading package lists...\nBuilding dependency tree...\nThe following packages will be REMOVED:\n curl-legacy\nThe following packages will be upgraded:\n curl\n1 upgraded, 0 newly installed, 1 to remove and 0 not upgraded.\nRemv curl-legacy [7.74.0-1]\nInst curl [7.88.1-10+deb12u5] (7.88.1-10+deb12u7 Debian-Security:12/stable-security [amd64])\nConf curl (7.88.1-10+deb12u7 Debian-Security:12/stable-security [amd64])\n";
|
|
81
|
-
export declare const APT_INSTALL_HTOP = "Reading package lists...\nBuilding dependency tree...\nThe following NEW packages will be installed:\n htop libnl-3-200 libnl-genl-3-200\nSetting up htop (3.2.2-2) ...\nProcessing triggers for man-db (2.11.2-2) ...\n";
|
|
82
|
-
export declare const DPKG_QUERY_HTOP_AFTER = "htop\t3.2.2-2\tii \n";
|
|
83
|
-
export declare const APT_NEEDS_ROOT = "E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)\nE: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), are you root?\n";
|
|
84
|
-
export declare const DNF_VERSION = "4.14.0\n Installed: dnf-0:4.14.0-9.el9.noarch at 2026-02-11 08:14\n Built : Rocky Enterprise Software Foundation at 2026-01-30 11:02\n";
|
|
85
|
-
export declare const RPM_QUERY_CURL = "curl\t7.76.1-29.el9_4\tx86_64\n";
|
|
86
|
-
/** The same name installed for two architectures — real, and common. */
|
|
87
|
-
export declare const RPM_QUERY_GLIBC = "glibc\t2.34-100.el9_4.2\tx86_64\nglibc\t2.34-100.el9_4.2\ti686\n";
|
|
88
|
-
/** rpm writes this to STDOUT, not stderr, and exits 1. */
|
|
89
|
-
export declare const RPM_NOT_INSTALLED = "package tree is not installed\n";
|
|
90
|
-
export declare const DNF_LIST_TREE = "Available Packages\ntree.x86_64 1.8.0-10.el9 baseos\n";
|
|
91
|
-
export declare const DNF_LIST_NO_MATCH = "Error: No matching Packages to list\n";
|
|
92
|
-
/**
|
|
93
|
-
* The case `--cacheonly` makes common on a fresh container, and the case
|
|
94
|
-
* house rule 6 is about: an empty metadata cache is NOT an absent package.
|
|
95
|
-
*/
|
|
96
|
-
export declare const DNF_NO_CACHE = "Error: Cache-only enabled but no cache for 'baseos'\n";
|
|
97
|
-
export declare const DNF_SIMULATE_TREE = "Last metadata expiration check: 0:05:12 ago on Wed 17 Sep 2026 09:00:00 AM UTC.\nDependencies resolved.\n================================================================================\n Package Arch Version Repository Size\n================================================================================\nInstalling:\n tree x86_64 1.8.0-10.el9 baseos 55 k\nInstalling dependencies:\n libfoo x86_64 1.2-3.el9 appstream 10 k\n\nTransaction Summary\n================================================================================\nInstall 2 Packages\n\nTotal download size: 65 k\nInstalled size: 143 k\nOperation aborted.\n";
|
|
98
|
-
export declare const DNF_NEEDS_ROOT = "Error: This command has to be run with superuser privileges (under the root user on most systems).\n";
|
|
99
|
-
export declare const DNF_INSTALL_TREE = "Dependencies resolved.\nInstalling:\n tree x86_64 1.8.0-10.el9 baseos 55 k\n\nComplete!\n";
|
|
100
|
-
export declare const RPM_QUERY_TREE_AFTER = "tree\t1.8.0-10.el9\tx86_64\n";
|
|
101
|
-
export declare const PACMAN_VERSION = " .--. Pacman v6.1.0 - libalpm v14.0.0\n/ _.-' .-. .-. .-. Copyright (C) 2006-2024 Pacman Development Team\n";
|
|
102
|
-
export declare const PACMAN_QUERY_CURL = "curl 8.9.1-1\n";
|
|
103
|
-
export declare const PACMAN_NOT_FOUND = "error: package 'htop' was not found\n";
|
|
104
|
-
export declare const PACMAN_SYNC_INFO_HTOP = "Repository : extra\nName : htop\nVersion : 3.3.0-1\nDescription : Interactive process viewer\nArchitecture : x86_64\nLicenses : GPL\nDepends On : glibc ncurses lm_sensors\nDownload Size : 158.44 KiB\n\nRepository : community-testing\nName : htop\nVersion : 3.4.0-0.1\n";
|
|
105
|
-
export declare const PACMAN_SYNC_NOT_FOUND = "error: package 'nosuch' was not found\n";
|
|
106
|
-
/** `pacman -S --print --print-format '%r/%n %v'` — targets AND dependencies. */
|
|
107
|
-
export declare const PACMAN_SIMULATE_HTOP = "core/ncurses 6.5-3\nextra/lm_sensors 1:3.6.0.r41-3\nextra/htop 3.3.0-1\n";
|
|
108
|
-
export declare const PACMAN_NEEDS_ROOT = "error: you cannot perform this operation unless you are root.\n";
|
|
109
|
-
export declare const PACMAN_INSTALL_HTOP = "resolving dependencies...\nlooking for conflicting packages...\n(3) installing ncurses lm_sensors htop\n:: Running post-transaction hooks...\n";
|
|
110
|
-
export declare const PACMAN_QUERY_HTOP_AFTER = "htop 3.3.0-1\n";
|
|
111
|
-
export declare const WINGET_VERSION = "v1.8.1911\n";
|
|
112
|
-
/** Five columns: Name, Id, Version, Available, Source. */
|
|
113
|
-
export declare const WINGET_LIST_GIT = "Name Id Version Available Source\n-------------------------------------------\nGit Git.Git 2.45.1 2.46.0 winget\n";
|
|
114
|
-
/**
|
|
115
|
-
* Four columns, which is the ambiguous case: this is `…Version Source`, but a
|
|
116
|
-
* row with an available upgrade and no source is `…Version Available` and
|
|
117
|
-
* looks identical. winget's headers are localised, so there is no way to tell
|
|
118
|
-
* — which is why the parser reports a null `available` with a reason rather
|
|
119
|
-
* than picking one.
|
|
120
|
-
*/
|
|
121
|
-
export declare const WINGET_LIST_FOUR_COLUMNS = "Name Id Version Source\n--------------------------------\n7-Zip 7zip.7zip 24.08 winget\n";
|
|
122
|
-
/**
|
|
123
|
-
* A row winget had to CUT, marked with U+2026. The survey's proposed
|
|
124
|
-
* header-offset parser would read `14.40.3` here as a complete version and
|
|
125
|
-
* the Id as a complete Id; both are prefixes.
|
|
126
|
-
*/
|
|
127
|
-
export declare const WINGET_LIST_TRUNCATED = "Name Id Version Source\n-------------------------------------------------------------------\nMicrosoft Visual C++ 20\u2026 Microsoft.VCRedist.2015\u2026 14.40.3 winget\n";
|
|
128
|
-
/**
|
|
129
|
-
* A CJK display name. Each glyph is DOUBLE-WIDTH in the console and one
|
|
130
|
-
* UTF-16 code unit in the string, so every character offset after it is
|
|
131
|
-
* wrong — and splitting on two-or-more spaces is unaffected.
|
|
132
|
-
*/
|
|
133
|
-
export declare const WINGET_LIST_CJK = "Name Id Version Source\n----------------------------------------\n\u30C6\u30AD\u30B9\u30C8 Vendor.Text 1.2.3 winget\n";
|
|
134
|
-
export declare const WINGET_NOT_FOUND = "No installed package found matching input criteria.\n";
|
|
135
|
-
export declare const CHOCO_VERSION_V2 = "2.2.2\n";
|
|
136
|
-
export declare const CHOCO_VERSION_V1 = "0.10.15\n";
|
|
137
|
-
export declare const CHOCO_LIST_GIT = "git|2.45.1\ngit.install|2.45.1\n";
|
|
138
|
-
export declare const CHOCO_LIST_EMPTY = "";
|
|
139
|
-
/** A recorded answer to one command. */
|
|
140
|
-
export type RecordedCase = {
|
|
141
|
-
readonly stdout?: string;
|
|
142
|
-
readonly stderr?: string;
|
|
143
|
-
readonly code?: number;
|
|
144
|
-
readonly timedOut?: boolean;
|
|
145
|
-
/** The program is not installed on this host (ENOENT on spawn). */
|
|
146
|
-
readonly missing?: boolean;
|
|
147
|
-
readonly stdoutTruncated?: boolean;
|
|
148
|
-
readonly stderrTruncated?: boolean;
|
|
149
|
-
};
|
|
150
|
-
export type RecordedRunner = {
|
|
151
|
-
readonly runner: Runner;
|
|
152
|
-
/** Every argv this runner was asked for, in order. */
|
|
153
|
-
readonly calls: string[][];
|
|
154
|
-
/** Every request, for the tests that check the pinned environment. */
|
|
155
|
-
readonly requests: RunRequest[];
|
|
156
|
-
};
|
|
157
|
-
/** The key a command is recorded under. NUL cannot appear in an argv element. */
|
|
158
|
-
export declare function argvKey(argv: readonly string[]): string;
|
|
159
|
-
/**
|
|
160
|
-
* Serve recorded output, and refuse anything nobody recorded.
|
|
161
|
-
*
|
|
162
|
-
* The refusal is the important half. A runner that answered an unrecorded
|
|
163
|
-
* command with an empty success would be indistinguishable from a manager
|
|
164
|
-
* that had nothing to say, and every "not installed" in the suite would stop
|
|
165
|
-
* meaning anything. Instead the promise rejects with the argv, so a missing
|
|
166
|
-
* fixture fails the test that needed it and names the command.
|
|
167
|
-
*
|
|
168
|
-
* `fallback` exists for the tests that are ABOUT an unrecognised host: pass
|
|
169
|
-
* `{ missing: true }` to make every unrecorded command behave like a program
|
|
170
|
-
* that is not installed.
|
|
171
|
-
*/
|
|
172
|
-
export declare function recordedRunner(cases: Readonly<Record<string, RecordedCase | readonly RecordedCase[]>>, fallback?: RecordedCase): RecordedRunner;
|
|
173
|
-
/** Shorthand: record a case under an argv rather than under a key. */
|
|
174
|
-
export declare function record(argv: readonly string[], outcome: RecordedCase | readonly RecordedCase[]): Record<string, RecordedCase | readonly RecordedCase[]>;
|