@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
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The vocabulary for "I could not find out" — `@crewhaus/tool-host`'s
|
|
3
|
+
* `lib/unknown.ts`, reproduced here because that package exports its tools
|
|
4
|
+
* and not its library, and the rule it encodes is the one this package is
|
|
5
|
+
* most likely to break.
|
|
6
|
+
*
|
|
7
|
+
* A caller asks whether `curl` is installed in order to decide something:
|
|
8
|
+
* whether to install it, whether to pick a different strategy, whether to
|
|
9
|
+
* tell an operator the host is not ready. Every one of those decisions is
|
|
10
|
+
* made WRONGLY, and silently, if a probe that could not answer comes back as
|
|
11
|
+
* "no":
|
|
12
|
+
*
|
|
13
|
+
* - `dpkg-query` is not on the host (this is a Fedora box) -> "not installed"
|
|
14
|
+
* - `dnf --cacheonly` has no cached metadata -> "not available"
|
|
15
|
+
* - `apt-cache policy` printed German labels -> "no candidate"
|
|
16
|
+
* - `winget list` printed a row whose name was truncated -> wrong version
|
|
17
|
+
* - the command hit its deadline -> "not installed"
|
|
18
|
+
* - `brew info` exited non-zero for a reason nobody parsed -> "not installed"
|
|
19
|
+
*
|
|
20
|
+
* So: a fact that could not be established is `null` in the result AND
|
|
21
|
+
* carries an entry here naming the field, the probe that was tried, and why
|
|
22
|
+
* it did not answer. A boolean is never defaulted to false, a version is
|
|
23
|
+
* never defaulted to "latest", and a status is `"unknown"` rather than
|
|
24
|
+
* `"not-installed"` unless the manager gave its own documented not-found
|
|
25
|
+
* signal. `every null is explained` in index.test.ts walks each tool's output
|
|
26
|
+
* and fails if any null is missing its entry, so the rule cannot rot.
|
|
27
|
+
*/
|
|
28
|
+
/** Collects unknowns for one tool call. */
|
|
29
|
+
export class Unknowns {
|
|
30
|
+
facts = new Map();
|
|
31
|
+
add(field, probe, reason) {
|
|
32
|
+
// First writer wins: the innermost probe that failed is the specific one,
|
|
33
|
+
// and a later generic "this section is unavailable" must not overwrite it.
|
|
34
|
+
if (!this.facts.has(field))
|
|
35
|
+
this.facts.set(field, { field, probe, reason });
|
|
36
|
+
}
|
|
37
|
+
has(field) {
|
|
38
|
+
return this.facts.has(field);
|
|
39
|
+
}
|
|
40
|
+
get size() {
|
|
41
|
+
return this.facts.size;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Sorted by field, so two calls against the same machine return the same
|
|
45
|
+
* bytes rather than whatever order the probes happened to fail in.
|
|
46
|
+
*/
|
|
47
|
+
list() {
|
|
48
|
+
return [...this.facts.values()].sort((a, b) => a.field < b.field ? -1 : a.field > b.field ? 1 : 0);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Describe a failed command, keeping apart the cases a caller acts on
|
|
53
|
+
* differently: a manager that is NOT INSTALLED (this is the wrong host, or
|
|
54
|
+
* ask a different manager), one that TIMED OUT (the answer is unknown and
|
|
55
|
+
* retrying may work), and one that RAN AND FAILED (its stderr is the lead).
|
|
56
|
+
*/
|
|
57
|
+
export function commandFailureReason(result) {
|
|
58
|
+
if (result.missing)
|
|
59
|
+
return "the command is not installed on this host";
|
|
60
|
+
if (result.timedOut) {
|
|
61
|
+
return result.abandoned === true
|
|
62
|
+
? "the command did not finish within its timeout and could not be reaped"
|
|
63
|
+
: "the command did not finish within its timeout";
|
|
64
|
+
}
|
|
65
|
+
const detail = firstLine(result.stderr);
|
|
66
|
+
return detail === ""
|
|
67
|
+
? `the command exited ${result.code}`
|
|
68
|
+
: `the command exited ${result.code}: ${detail}`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The first non-empty line, capped.
|
|
72
|
+
*
|
|
73
|
+
* A manager's usage message can be forty lines long — `dnf` prints its whole
|
|
74
|
+
* option grammar on a bad flag — and none of it belongs in a result a model
|
|
75
|
+
* has to read.
|
|
76
|
+
*/
|
|
77
|
+
export function firstLine(text, max = 200) {
|
|
78
|
+
const line = text
|
|
79
|
+
.split("\n")
|
|
80
|
+
.map((l) => l.trim())
|
|
81
|
+
.find((l) => l.length > 0);
|
|
82
|
+
if (line === undefined)
|
|
83
|
+
return "";
|
|
84
|
+
return line.length <= max ? line : `${line.slice(0, max - 1)}…`;
|
|
85
|
+
}
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { CrewhausError } from "@crewhaus/errors";
|
|
2
|
+
/** Raised when a caller-supplied path resolves outside the workspace root. */
|
|
3
|
+
export declare class ToolPermissionError extends CrewhausError {
|
|
4
|
+
readonly name = "ToolPermissionError";
|
|
5
|
+
readonly toolName: string;
|
|
6
|
+
readonly path: string;
|
|
7
|
+
constructor(toolName: string, attemptedPath: string);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A validated path, in both forms the tools need.
|
|
11
|
+
*
|
|
12
|
+
* `real` has every in-workspace symlink already resolved and is what I/O
|
|
13
|
+
* should use — operating on it closes most of the check-to-use window.
|
|
14
|
+
* `abs` is the lexical resolution, which is what a tool must use when the
|
|
15
|
+
* question is about the link ITSELF (`Stat` reporting a symlink target,
|
|
16
|
+
* `RemovePath` deleting a link rather than its destination).
|
|
17
|
+
*/
|
|
18
|
+
export type SafePath = {
|
|
19
|
+
readonly abs: string;
|
|
20
|
+
readonly real: string;
|
|
21
|
+
/** The path as the caller wrote it, for echoing back in results. */
|
|
22
|
+
readonly given: string;
|
|
23
|
+
/** Slash-separated path relative to the workspace root. */
|
|
24
|
+
readonly rel: string;
|
|
25
|
+
};
|
|
26
|
+
/** The workspace root, resolved. Every path is contained within it. */
|
|
27
|
+
export declare function workspaceRoot(): string;
|
|
28
|
+
export declare function resolveSafe(toolName: string, rel: string, root?: string): SafePath;
|
|
29
|
+
/** A path relative to the workspace root, with `/` separators on every OS. */
|
|
30
|
+
export declare function toPosix(p: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* True when `candidate` is `parent` or lives underneath it. Both must already
|
|
33
|
+
* be absolute. Used to keep an archive entry, or a symlink inside an
|
|
34
|
+
* extracted tree, from reaching outside its destination.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isInside(parent: string, candidate: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Reject an archive member name that would escape its destination: an
|
|
39
|
+
* absolute path, a Windows drive prefix, or any `..` segment. Checked on the
|
|
40
|
+
* NAME, before extraction, because after extraction the damage is done.
|
|
41
|
+
*/
|
|
42
|
+
export declare function archiveEntryEscapes(name: string): boolean;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path containment — the gate every caller-supplied path in this package
|
|
3
|
+
* passes through before any syscall touches it.
|
|
4
|
+
*
|
|
5
|
+
* The approach is the one `@crewhaus/tool-fs` uses, and for the same reasons:
|
|
6
|
+
* a lexical check alone is fooled by a symlink that lives inside the
|
|
7
|
+
* workspace and points outside it (CWE-59), so the real path is checked too.
|
|
8
|
+
* The workspace root is `process.cwd()`, matching tool-fs, so a harness that
|
|
9
|
+
* trusts one tool's boundary gets the same boundary here.
|
|
10
|
+
*/
|
|
11
|
+
import { lstatSync, readlinkSync, realpathSync } from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { CrewhausError } from "@crewhaus/errors";
|
|
14
|
+
/** Raised when a caller-supplied path resolves outside the workspace root. */
|
|
15
|
+
export class ToolPermissionError extends CrewhausError {
|
|
16
|
+
name = "ToolPermissionError";
|
|
17
|
+
toolName;
|
|
18
|
+
path;
|
|
19
|
+
constructor(toolName, attemptedPath) {
|
|
20
|
+
super("tool", `tool "${toolName}" rejected path "${attemptedPath}": resolved location escapes the workspace root`);
|
|
21
|
+
this.toolName = toolName;
|
|
22
|
+
this.path = attemptedPath;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** The workspace root, resolved. Every path is contained within it. */
|
|
26
|
+
export function workspaceRoot() {
|
|
27
|
+
return path.resolve(process.cwd());
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve `rel` inside the workspace, or throw `ToolPermissionError`.
|
|
31
|
+
*
|
|
32
|
+
* The leaf is allowed not to exist — every tool here that creates something
|
|
33
|
+
* needs to validate a destination before it is made — so the deepest
|
|
34
|
+
* EXISTING ancestor is realpath'd and the missing tail re-appended. Any
|
|
35
|
+
* failure other than that walk fails closed.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* True when the NAME exists, whether or not it leads anywhere.
|
|
39
|
+
*
|
|
40
|
+
* `existsSync` FOLLOWS symlinks, so it answers false for a link whose target
|
|
41
|
+
* is missing — and that is exactly the case that matters: a dangling link is
|
|
42
|
+
* still a door, because `open(…, "w")` through `evil -> /outside/x` CREATES
|
|
43
|
+
* `/outside/x`. Probing with `lstat` keeps the link's name in the part of the
|
|
44
|
+
* path that gets resolved, rather than in the "does not exist yet" tail that
|
|
45
|
+
* is re-appended to the root verbatim and so passes the containment check.
|
|
46
|
+
*/
|
|
47
|
+
function nameExists(p) {
|
|
48
|
+
try {
|
|
49
|
+
lstatSync(p);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Where `target` would actually land, with every symlink on the way already
|
|
58
|
+
* followed — including one whose own target does not exist yet.
|
|
59
|
+
*
|
|
60
|
+
* `realpathSync` gives up with ENOENT on a dangling link, which would leave
|
|
61
|
+
* that link unresolved and let it stand in for a plain missing file. So the
|
|
62
|
+
* deepest ancestor that exists as a NAME is resolved, a dangling one is
|
|
63
|
+
* followed a hop by hand, and the components that do not exist are appended.
|
|
64
|
+
* The result is the path an `open` would create, which is the only path worth
|
|
65
|
+
* checking containment against.
|
|
66
|
+
*/
|
|
67
|
+
function resolveLocation(target, depth = 0) {
|
|
68
|
+
if (depth > 40)
|
|
69
|
+
throw new Error(`symlink chain at "${target}" is too long to resolve`);
|
|
70
|
+
let probe = target;
|
|
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
|
+
}
|
|
79
|
+
let probeReal;
|
|
80
|
+
try {
|
|
81
|
+
probeReal = realpathSync(probe);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
if (err.code !== "ENOENT")
|
|
85
|
+
throw err;
|
|
86
|
+
// The name is there but `realpath` cannot finish it: a symlink with a
|
|
87
|
+
// missing target. `readlinkSync` throws EINVAL on anything else, which
|
|
88
|
+
// fails closed.
|
|
89
|
+
const link = readlinkSync(probe);
|
|
90
|
+
// A RELATIVE target resolves against the directory that actually CONTAINS
|
|
91
|
+
// the link, which is not the link's lexical parent when that parent is
|
|
92
|
+
// itself reached through a symlink. `<root>/dirlink/x -> ../y` with
|
|
93
|
+
// `dirlink` pointing out of the root really lands at `<elsewhere>/y`, but
|
|
94
|
+
// measured from the lexical parent it reads as `<root>/y` — an in-root
|
|
95
|
+
// path the caller's path does not lead to. So the parent is made real
|
|
96
|
+
// first. An absolute target ignores the base.
|
|
97
|
+
const base = realpathSync(path.dirname(probe));
|
|
98
|
+
probeReal = resolveLocation(path.resolve(base, link), depth + 1);
|
|
99
|
+
}
|
|
100
|
+
return tail.length > 0 ? path.join(probeReal, ...tail) : probeReal;
|
|
101
|
+
}
|
|
102
|
+
export function resolveSafe(toolName, rel, root = workspaceRoot()) {
|
|
103
|
+
const rootResolved = path.resolve(root);
|
|
104
|
+
const abs = path.resolve(rootResolved, rel);
|
|
105
|
+
// 1) Lexical containment — rejects `..` and absolute escapes. The trailing
|
|
106
|
+
// separator avoids the `/root` vs `/root-sibling` prefix pitfall.
|
|
107
|
+
if (abs !== rootResolved && !abs.startsWith(`${rootResolved}${path.sep}`)) {
|
|
108
|
+
throw new ToolPermissionError(toolName, rel);
|
|
109
|
+
}
|
|
110
|
+
let real;
|
|
111
|
+
try {
|
|
112
|
+
const rootReal = realpathSync(rootResolved);
|
|
113
|
+
real = resolveLocation(abs);
|
|
114
|
+
// 2) Symlink-aware containment. The real path must land inside the real
|
|
115
|
+
// root, so an in-workspace symlink pointing at /etc is refused here.
|
|
116
|
+
if (real !== rootReal && !real.startsWith(`${rootReal}${path.sep}`)) {
|
|
117
|
+
throw new ToolPermissionError(toolName, rel);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
if (err instanceof ToolPermissionError)
|
|
122
|
+
throw err;
|
|
123
|
+
throw new ToolPermissionError(toolName, rel);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
abs,
|
|
127
|
+
real,
|
|
128
|
+
given: rel,
|
|
129
|
+
rel: toPosix(path.relative(rootResolved, abs)),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** A path relative to the workspace root, with `/` separators on every OS. */
|
|
133
|
+
export function toPosix(p) {
|
|
134
|
+
return path.sep === "/" ? p : p.split(path.sep).join("/");
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* True when `candidate` is `parent` or lives underneath it. Both must already
|
|
138
|
+
* be absolute. Used to keep an archive entry, or a symlink inside an
|
|
139
|
+
* extracted tree, from reaching outside its destination.
|
|
140
|
+
*/
|
|
141
|
+
export function isInside(parent, candidate) {
|
|
142
|
+
const p = path.resolve(parent);
|
|
143
|
+
const c = path.resolve(candidate);
|
|
144
|
+
return c === p || c.startsWith(`${p}${path.sep}`);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Reject an archive member name that would escape its destination: an
|
|
148
|
+
* absolute path, a Windows drive prefix, or any `..` segment. Checked on the
|
|
149
|
+
* NAME, before extraction, because after extraction the damage is done.
|
|
150
|
+
*/
|
|
151
|
+
export function archiveEntryEscapes(name) {
|
|
152
|
+
const normalized = name.replace(/\\/g, "/");
|
|
153
|
+
if (normalized.startsWith("/"))
|
|
154
|
+
return true;
|
|
155
|
+
if (/^[A-Za-z]:/.test(normalized))
|
|
156
|
+
return true;
|
|
157
|
+
return normalized.split("/").some((segment) => segment === "..");
|
|
158
|
+
}
|
package/dist/run.d.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place this package starts a child process, and the seam every test
|
|
3
|
+
* drives instead of the machine.
|
|
4
|
+
*
|
|
5
|
+
* The conventions are `@crewhaus/tool-proc`'s (`src/spawn.ts`) and
|
|
6
|
+
* `@crewhaus/tool-host`'s (`src/run.ts`), copied rather than re-invented —
|
|
7
|
+
* those packages export their tools and not their runners, and reaching past
|
|
8
|
+
* an exports map would be worse than a documented copy:
|
|
9
|
+
*
|
|
10
|
+
* - ARGV IS AN ARRAY. There is no shell in this package. No `sh -c`, no
|
|
11
|
+
* `cmd /c`, no interpolated command line, nothing that could turn a
|
|
12
|
+
* package name into syntax. `assertArgv` refuses anything that would make
|
|
13
|
+
* that untrue before a process exists.
|
|
14
|
+
* - EVERY RUN HAS A DEADLINE. SIGTERM at the deadline, SIGKILL after a
|
|
15
|
+
* grace period, and a bounded drain so a manager that forks (apt-get
|
|
16
|
+
* forks dpkg; brew forks git) cannot hold the pipe open past its death.
|
|
17
|
+
* - THE ENVIRONMENT IS PINNED, AND PINNED NON-INTERACTIVE. Every parser
|
|
18
|
+
* here reads English keywords out of a manager's output, and `apt-cache
|
|
19
|
+
* policy`, `dnf`, `pacman` and `brew` all translate theirs through
|
|
20
|
+
* gettext, so `LC_ALL=C` is what makes the bytes the same on a German
|
|
21
|
+
* laptop and an English CI box. `DEBIAN_FRONTEND=noninteractive` and
|
|
22
|
+
* `NONINTERACTIVE=1` are the other half: a manager that opens a dialog or
|
|
23
|
+
* asks a question on a pipe is a command that never exits, and this
|
|
24
|
+
* package's whole contract is that it does not hang and does not prompt.
|
|
25
|
+
*
|
|
26
|
+
* `_setRunner` replaces all of it. Every parser in this package is fed from
|
|
27
|
+
* output recorded on a real machine, because CI is a Linux container with no
|
|
28
|
+
* package manager at all and this was written on macOS: a parser checked
|
|
29
|
+
* against whatever the test host happens to answer is a parser checked
|
|
30
|
+
* against nothing.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Per-stream cap.
|
|
34
|
+
*
|
|
35
|
+
* `apt-get install -s` on a package with a large dependency closure prints a
|
|
36
|
+
* line per unpacked package, and `brew info --json=v2` for a formula with
|
|
37
|
+
* many bottles is tens of kilobytes. The cap is generous because the flag
|
|
38
|
+
* matters more than the number: a truncated simulation parses perfectly and
|
|
39
|
+
* is simply MISSING packages, so every reader below treats a truncated stream
|
|
40
|
+
* as "could not determine" rather than as the answer.
|
|
41
|
+
*/
|
|
42
|
+
export declare const MAX_OUTPUT_CHARS = 512000;
|
|
43
|
+
/** Default per-command deadline. Every command here reads a local index. */
|
|
44
|
+
export declare const DEFAULT_COMMAND_TIMEOUT_MS = 20000;
|
|
45
|
+
/** Ceiling a caller may raise a deadline to. An install is the slow case. */
|
|
46
|
+
export declare const MAX_COMMAND_TIMEOUT_MS = 600000;
|
|
47
|
+
/** `PATH` for a child when the harness itself has none. */
|
|
48
|
+
export declare const FALLBACK_PATH = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
|
|
49
|
+
export type RunRequest = {
|
|
50
|
+
/** The program and each argument as separate strings. Never a shell line. */
|
|
51
|
+
readonly argv: readonly string[];
|
|
52
|
+
readonly timeoutMs: number;
|
|
53
|
+
/** Extra pinned variables for this command (see `MANAGER_ENV`). */
|
|
54
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
55
|
+
readonly maxOutputChars?: number;
|
|
56
|
+
readonly signal?: AbortSignal;
|
|
57
|
+
};
|
|
58
|
+
export type RunResult = {
|
|
59
|
+
readonly code: number;
|
|
60
|
+
readonly stdout: string;
|
|
61
|
+
readonly stderr: string;
|
|
62
|
+
readonly timedOut: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* True when the program itself is not installed (ENOENT on spawn).
|
|
65
|
+
*
|
|
66
|
+
* This is the distinction the whole package turns on: "there is no `dnf` on
|
|
67
|
+
* this host" is a different fact from "`dnf` ran and said the package is
|
|
68
|
+
* not there", and reporting the first as the second is the bug class rule 6
|
|
69
|
+
* names.
|
|
70
|
+
*/
|
|
71
|
+
readonly missing: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* The stream hit the cap and what came back is a PREFIX of what the command
|
|
74
|
+
* printed. Optional because a recorded fixture states only what its case is
|
|
75
|
+
* about; absent reads as "not truncated", which a short fixture is.
|
|
76
|
+
*/
|
|
77
|
+
readonly stdoutTruncated?: boolean;
|
|
78
|
+
readonly stderrTruncated?: boolean;
|
|
79
|
+
/** Set when the child outlived even the SIGKILL grace and was left behind. */
|
|
80
|
+
readonly abandoned?: boolean;
|
|
81
|
+
};
|
|
82
|
+
export type Runner = (request: RunRequest) => Promise<RunResult>;
|
|
83
|
+
export declare function assertArgv(argv: readonly string[]): string | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* The child's environment: pinned, minimal, and the same on every machine.
|
|
86
|
+
*
|
|
87
|
+
* `PATH` and `HOME` are forwarded because a manager cannot be found without
|
|
88
|
+
* the first and Homebrew cannot find its prefix without the second. Nothing
|
|
89
|
+
* else from the harness is, so the harness's API keys are not in `apt-get`'s
|
|
90
|
+
* environment. The Windows four are forwarded because `winget.exe` and
|
|
91
|
+
* `choco.exe` are not resolvable without them.
|
|
92
|
+
*
|
|
93
|
+
* `LC_ALL`/`LANG`/`TZ` are pinned rather than forwarded — see the header.
|
|
94
|
+
*/
|
|
95
|
+
export declare function buildEnv(extra?: Readonly<Record<string, string>>, source?: NodeJS.ProcessEnv): Record<string, string>;
|
|
96
|
+
/** Cut a stream at the cap, saying whether anything was dropped. */
|
|
97
|
+
export declare function capText(text: string, max: number): {
|
|
98
|
+
text: string;
|
|
99
|
+
truncated: boolean;
|
|
100
|
+
};
|
|
101
|
+
/** Run `argv` to completion, or kill it when the deadline passes. */
|
|
102
|
+
export declare function runHostCommand(request: RunRequest): Promise<RunResult>;
|
|
103
|
+
/**
|
|
104
|
+
* Test seam. Pass a function to answer every command from recorded output;
|
|
105
|
+
* pass `undefined` to go back to spawning for real.
|
|
106
|
+
*/
|
|
107
|
+
export declare function _setRunner(runner: Runner | undefined): void;
|
|
108
|
+
/**
|
|
109
|
+
* True when a runner is installed.
|
|
110
|
+
*
|
|
111
|
+
* Read by `host.ts` to decide the platform default, and by the one smoke test
|
|
112
|
+
* that is allowed to touch the real host so it can skip itself. See
|
|
113
|
+
* `hostPlatform()` for why the two seams are tied together.
|
|
114
|
+
*/
|
|
115
|
+
export declare function _runnerInstalled(): boolean;
|