@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,174 @@
|
|
|
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[]>;
|
package/dist/fixtures.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Homebrew — macOS 15.6, Homebrew 4.3.8
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
export const BREW_VERSION = `Homebrew 4.3.8
|
|
5
|
+
Homebrew/homebrew-core (git revision 2e6b2c3f4a1; last commit 2026-09-10)
|
|
6
|
+
`;
|
|
7
|
+
/** `brew info --json=v2 curl` — installed, and the tap has a newer version. */
|
|
8
|
+
export 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":[]}`;
|
|
9
|
+
/** `brew info --json=v2 tree` — known to the tap, not installed. */
|
|
10
|
+
export 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":[]}`;
|
|
11
|
+
/**
|
|
12
|
+
* `brew info --json=v2 python@3.11` — two kegs of one formula.
|
|
13
|
+
*
|
|
14
|
+
* The reason `installedVersions` is an array: reporting the first as "the"
|
|
15
|
+
* installed version is wrong for exactly the formulae people ask about.
|
|
16
|
+
*/
|
|
17
|
+
export 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":[]}`;
|
|
18
|
+
/** `brew info --json=v2 firefox` — a cask, which spells everything differently. */
|
|
19
|
+
export 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}]}`;
|
|
20
|
+
/** `brew info --json=v2 nope-nope` — exit 1, and this exact sentence. */
|
|
21
|
+
export const BREW_NO_SUCH_FORMULA = `Error: No available formula with the name "nope-nope".
|
|
22
|
+
==> Searching for similarly named formulae and casks...
|
|
23
|
+
`;
|
|
24
|
+
export const BREW_DEPS_CURL = `brotli
|
|
25
|
+
libnghttp2
|
|
26
|
+
libssh2
|
|
27
|
+
openldap
|
|
28
|
+
rtmpdump
|
|
29
|
+
zstd
|
|
30
|
+
`;
|
|
31
|
+
export const BREW_DEPS_TREE = "";
|
|
32
|
+
/** `brew list --formula --versions` — four of the six curl deps are present. */
|
|
33
|
+
export const BREW_LIST_VERSIONS = `brotli 1.1.0
|
|
34
|
+
ca-certificates 2026-08-12
|
|
35
|
+
libnghttp2 1.62.1
|
|
36
|
+
openldap 2.6.7
|
|
37
|
+
python@3.11 3.11.9 3.11.8
|
|
38
|
+
zstd 1.5.6
|
|
39
|
+
`;
|
|
40
|
+
export const BREW_INSTALL_TREE = `==> Fetching tree
|
|
41
|
+
==> Downloading https://ghcr.io/v2/homebrew/core/tree/blobs/sha256:9f3c1ad
|
|
42
|
+
==> Pouring tree--2.2.1.arm64_sonoma.bottle.tar.gz
|
|
43
|
+
/opt/homebrew/Cellar/tree/2.2.1: 8 files, 162.5KB
|
|
44
|
+
`;
|
|
45
|
+
/** `brew info --json=v2 tree`, re-run after the install above succeeded. */
|
|
46
|
+
export 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":[]}`;
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// apt / dpkg — Debian 12 (bookworm), dpkg 1.21.22, apt 2.6.1
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
export const DPKG_VERSION = `Debian dpkg-query package management program version 1.21.22 (amd64).
|
|
51
|
+
This is free software; see the GNU General Public License version 2 or
|
|
52
|
+
later for copying conditions. There is NO warranty.
|
|
53
|
+
`;
|
|
54
|
+
/** Note the trailing space: `${db:Status-Abbrev}` is three characters wide. */
|
|
55
|
+
export const DPKG_QUERY_CURL = "curl\t7.88.1-10+deb12u5\tii \n";
|
|
56
|
+
/**
|
|
57
|
+
* A package dpkg still has a RECORD of but does not have installed: removed,
|
|
58
|
+
* configuration files left behind. Reading `rc` as installed is the classic
|
|
59
|
+
* dpkg mistake — the binary is gone and only `/etc` remains.
|
|
60
|
+
*/
|
|
61
|
+
export const DPKG_QUERY_NANO_REMOVED = "nano\t7.2-1\trc \n";
|
|
62
|
+
/** dpkg has no record at all of a package that was never installed. */
|
|
63
|
+
export const DPKG_NO_SUCH_PACKAGE = "dpkg-query: no packages found matching htop\n";
|
|
64
|
+
export const APT_POLICY_CURL = `curl:
|
|
65
|
+
Installed: 7.88.1-10+deb12u5
|
|
66
|
+
Candidate: 7.88.1-10+deb12u7
|
|
67
|
+
Version table:
|
|
68
|
+
*** 7.88.1-10+deb12u5 100
|
|
69
|
+
100 /var/lib/dpkg/status
|
|
70
|
+
7.88.1-10+deb12u7 500
|
|
71
|
+
500 http://deb.debian.org/debian-security bookworm-security/main amd64 Packages
|
|
72
|
+
`;
|
|
73
|
+
export const APT_POLICY_HTOP = `htop:
|
|
74
|
+
Installed: (none)
|
|
75
|
+
Candidate: 3.2.2-2
|
|
76
|
+
Version table:
|
|
77
|
+
3.2.2-2 500
|
|
78
|
+
500 http://deb.debian.org/debian bookworm/main amd64 Packages
|
|
79
|
+
`;
|
|
80
|
+
/** A package apt has in its lists with no installable candidate at all. */
|
|
81
|
+
export const APT_POLICY_NO_CANDIDATE = `phantom:
|
|
82
|
+
Installed: (none)
|
|
83
|
+
Candidate: (none)
|
|
84
|
+
Version table:
|
|
85
|
+
`;
|
|
86
|
+
/** apt-cache prints NOTHING for a name it does not know, and exits 0. */
|
|
87
|
+
export const APT_POLICY_UNKNOWN_STDERR = "N: Unable to locate package nosuchpkg\n";
|
|
88
|
+
/**
|
|
89
|
+
* The same `apt-cache policy curl` on a de_DE host.
|
|
90
|
+
*
|
|
91
|
+
* This is the fixture that gives the localisation trap a name. Every parser
|
|
92
|
+
* in this package reads English labels, which is sound ONLY because
|
|
93
|
+
* `../run.ts` pins `LC_ALL=C` on every child. Without that pin the parse does
|
|
94
|
+
* not fail loudly — it finds nothing, and "nothing" reads as "not installed".
|
|
95
|
+
*/
|
|
96
|
+
export const APT_POLICY_CURL_GERMAN = `curl:
|
|
97
|
+
Installiert: 7.88.1-10+deb12u5
|
|
98
|
+
Installationskandidat: 7.88.1-10+deb12u7
|
|
99
|
+
Versionstabelle:
|
|
100
|
+
*** 7.88.1-10+deb12u5 100
|
|
101
|
+
100 /var/lib/dpkg/status
|
|
102
|
+
`;
|
|
103
|
+
export const APT_SIMULATE_HTOP = `NOTE: This is only a simulation!
|
|
104
|
+
apt-get needs root privileges for real execution.
|
|
105
|
+
Keep also in mind that locking is deactivated,
|
|
106
|
+
so don't depend on the relevance to the real current situation!
|
|
107
|
+
Reading package lists...
|
|
108
|
+
Building dependency tree...
|
|
109
|
+
Reading state information...
|
|
110
|
+
The following additional packages will be installed:
|
|
111
|
+
libnl-3-200 libnl-genl-3-200
|
|
112
|
+
The following NEW packages will be installed:
|
|
113
|
+
htop libnl-3-200 libnl-genl-3-200
|
|
114
|
+
0 upgraded, 3 newly installed, 0 to remove and 0 not upgraded.
|
|
115
|
+
Inst libnl-3-200 (3.7.0-0.2+b1 Debian:12.5/stable [amd64])
|
|
116
|
+
Inst libnl-genl-3-200 (3.7.0-0.2+b1 Debian:12.5/stable [amd64])
|
|
117
|
+
Inst htop (3.2.2-2 Debian:12.5/stable [amd64])
|
|
118
|
+
Conf libnl-3-200 (3.7.0-0.2+b1 Debian:12.5/stable [amd64])
|
|
119
|
+
Conf libnl-genl-3-200 (3.7.0-0.2+b1 Debian:12.5/stable [amd64])
|
|
120
|
+
Conf htop (3.2.2-2 Debian:12.5/stable [amd64])
|
|
121
|
+
`;
|
|
122
|
+
/**
|
|
123
|
+
* An UPGRADE that also removes something — the case an operator most needs to
|
|
124
|
+
* see before approving an install. Note the `[old-version]` bracket between
|
|
125
|
+
* the name and the parenthesis, which only an upgrade has.
|
|
126
|
+
*/
|
|
127
|
+
export const APT_SIMULATE_CURL_UPGRADE = `Reading package lists...
|
|
128
|
+
Building dependency tree...
|
|
129
|
+
The following packages will be REMOVED:
|
|
130
|
+
curl-legacy
|
|
131
|
+
The following packages will be upgraded:
|
|
132
|
+
curl
|
|
133
|
+
1 upgraded, 0 newly installed, 1 to remove and 0 not upgraded.
|
|
134
|
+
Remv curl-legacy [7.74.0-1]
|
|
135
|
+
Inst curl [7.88.1-10+deb12u5] (7.88.1-10+deb12u7 Debian-Security:12/stable-security [amd64])
|
|
136
|
+
Conf curl (7.88.1-10+deb12u7 Debian-Security:12/stable-security [amd64])
|
|
137
|
+
`;
|
|
138
|
+
export const APT_INSTALL_HTOP = `Reading package lists...
|
|
139
|
+
Building dependency tree...
|
|
140
|
+
The following NEW packages will be installed:
|
|
141
|
+
htop libnl-3-200 libnl-genl-3-200
|
|
142
|
+
Setting up htop (3.2.2-2) ...
|
|
143
|
+
Processing triggers for man-db (2.11.2-2) ...
|
|
144
|
+
`;
|
|
145
|
+
export const DPKG_QUERY_HTOP_AFTER = "htop\t3.2.2-2\tii \n";
|
|
146
|
+
export const APT_NEEDS_ROOT = `E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)
|
|
147
|
+
E: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), are you root?
|
|
148
|
+
`;
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// dnf / rpm — Rocky Linux 9.4, dnf 4.14.0, rpm 4.16.1.3
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
export const DNF_VERSION = `4.14.0
|
|
153
|
+
Installed: dnf-0:4.14.0-9.el9.noarch at 2026-02-11 08:14
|
|
154
|
+
Built : Rocky Enterprise Software Foundation at 2026-01-30 11:02
|
|
155
|
+
`;
|
|
156
|
+
export const RPM_QUERY_CURL = "curl\t7.76.1-29.el9_4\tx86_64\n";
|
|
157
|
+
/** The same name installed for two architectures — real, and common. */
|
|
158
|
+
export const RPM_QUERY_GLIBC = "glibc\t2.34-100.el9_4.2\tx86_64\nglibc\t2.34-100.el9_4.2\ti686\n";
|
|
159
|
+
/** rpm writes this to STDOUT, not stderr, and exits 1. */
|
|
160
|
+
export const RPM_NOT_INSTALLED = "package tree is not installed\n";
|
|
161
|
+
export const DNF_LIST_TREE = `Available Packages
|
|
162
|
+
tree.x86_64 1.8.0-10.el9 baseos
|
|
163
|
+
`;
|
|
164
|
+
export const DNF_LIST_NO_MATCH = "Error: No matching Packages to list\n";
|
|
165
|
+
/**
|
|
166
|
+
* The case `--cacheonly` makes common on a fresh container, and the case
|
|
167
|
+
* house rule 6 is about: an empty metadata cache is NOT an absent package.
|
|
168
|
+
*/
|
|
169
|
+
export const DNF_NO_CACHE = `Error: Cache-only enabled but no cache for 'baseos'
|
|
170
|
+
`;
|
|
171
|
+
export const DNF_SIMULATE_TREE = `Last metadata expiration check: 0:05:12 ago on Wed 17 Sep 2026 09:00:00 AM UTC.
|
|
172
|
+
Dependencies resolved.
|
|
173
|
+
================================================================================
|
|
174
|
+
Package Arch Version Repository Size
|
|
175
|
+
================================================================================
|
|
176
|
+
Installing:
|
|
177
|
+
tree x86_64 1.8.0-10.el9 baseos 55 k
|
|
178
|
+
Installing dependencies:
|
|
179
|
+
libfoo x86_64 1.2-3.el9 appstream 10 k
|
|
180
|
+
|
|
181
|
+
Transaction Summary
|
|
182
|
+
================================================================================
|
|
183
|
+
Install 2 Packages
|
|
184
|
+
|
|
185
|
+
Total download size: 65 k
|
|
186
|
+
Installed size: 143 k
|
|
187
|
+
Operation aborted.
|
|
188
|
+
`;
|
|
189
|
+
export const DNF_NEEDS_ROOT = `Error: This command has to be run with superuser privileges (under the root user on most systems).
|
|
190
|
+
`;
|
|
191
|
+
export const DNF_INSTALL_TREE = `Dependencies resolved.
|
|
192
|
+
Installing:
|
|
193
|
+
tree x86_64 1.8.0-10.el9 baseos 55 k
|
|
194
|
+
|
|
195
|
+
Complete!
|
|
196
|
+
`;
|
|
197
|
+
export const RPM_QUERY_TREE_AFTER = "tree\t1.8.0-10.el9\tx86_64\n";
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// pacman — Arch, pacman 6.1.0
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
export const PACMAN_VERSION = ` .--. Pacman v6.1.0 - libalpm v14.0.0
|
|
202
|
+
/ _.-' .-. .-. .-. Copyright (C) 2006-2024 Pacman Development Team
|
|
203
|
+
`;
|
|
204
|
+
export const PACMAN_QUERY_CURL = "curl 8.9.1-1\n";
|
|
205
|
+
export const PACMAN_NOT_FOUND = "error: package 'htop' was not found\n";
|
|
206
|
+
export const PACMAN_SYNC_INFO_HTOP = `Repository : extra
|
|
207
|
+
Name : htop
|
|
208
|
+
Version : 3.3.0-1
|
|
209
|
+
Description : Interactive process viewer
|
|
210
|
+
Architecture : x86_64
|
|
211
|
+
Licenses : GPL
|
|
212
|
+
Depends On : glibc ncurses lm_sensors
|
|
213
|
+
Download Size : 158.44 KiB
|
|
214
|
+
|
|
215
|
+
Repository : community-testing
|
|
216
|
+
Name : htop
|
|
217
|
+
Version : 3.4.0-0.1
|
|
218
|
+
`;
|
|
219
|
+
export const PACMAN_SYNC_NOT_FOUND = "error: package 'nosuch' was not found\n";
|
|
220
|
+
/** `pacman -S --print --print-format '%r/%n %v'` — targets AND dependencies. */
|
|
221
|
+
export const PACMAN_SIMULATE_HTOP = `core/ncurses 6.5-3
|
|
222
|
+
extra/lm_sensors 1:3.6.0.r41-3
|
|
223
|
+
extra/htop 3.3.0-1
|
|
224
|
+
`;
|
|
225
|
+
export const PACMAN_NEEDS_ROOT = "error: you cannot perform this operation unless you are root.\n";
|
|
226
|
+
export const PACMAN_INSTALL_HTOP = `resolving dependencies...
|
|
227
|
+
looking for conflicting packages...
|
|
228
|
+
(3) installing ncurses lm_sensors htop
|
|
229
|
+
:: Running post-transaction hooks...
|
|
230
|
+
`;
|
|
231
|
+
export const PACMAN_QUERY_HTOP_AFTER = "htop 3.3.0-1\n";
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// winget — Windows 11, winget 1.8.1911
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
export const WINGET_VERSION = "v1.8.1911\n";
|
|
236
|
+
/** Five columns: Name, Id, Version, Available, Source. */
|
|
237
|
+
export const WINGET_LIST_GIT = `Name Id Version Available Source
|
|
238
|
+
-------------------------------------------
|
|
239
|
+
Git Git.Git 2.45.1 2.46.0 winget
|
|
240
|
+
`;
|
|
241
|
+
/**
|
|
242
|
+
* Four columns, which is the ambiguous case: this is `…Version Source`, but a
|
|
243
|
+
* row with an available upgrade and no source is `…Version Available` and
|
|
244
|
+
* looks identical. winget's headers are localised, so there is no way to tell
|
|
245
|
+
* — which is why the parser reports a null `available` with a reason rather
|
|
246
|
+
* than picking one.
|
|
247
|
+
*/
|
|
248
|
+
export const WINGET_LIST_FOUR_COLUMNS = `Name Id Version Source
|
|
249
|
+
--------------------------------
|
|
250
|
+
7-Zip 7zip.7zip 24.08 winget
|
|
251
|
+
`;
|
|
252
|
+
/**
|
|
253
|
+
* A row winget had to CUT, marked with U+2026. The survey's proposed
|
|
254
|
+
* header-offset parser would read `14.40.3` here as a complete version and
|
|
255
|
+
* the Id as a complete Id; both are prefixes.
|
|
256
|
+
*/
|
|
257
|
+
export const WINGET_LIST_TRUNCATED = `Name Id Version Source
|
|
258
|
+
-------------------------------------------------------------------
|
|
259
|
+
Microsoft Visual C++ 20… Microsoft.VCRedist.2015… 14.40.3 winget
|
|
260
|
+
`;
|
|
261
|
+
/**
|
|
262
|
+
* A CJK display name. Each glyph is DOUBLE-WIDTH in the console and one
|
|
263
|
+
* UTF-16 code unit in the string, so every character offset after it is
|
|
264
|
+
* wrong — and splitting on two-or-more spaces is unaffected.
|
|
265
|
+
*/
|
|
266
|
+
export const WINGET_LIST_CJK = `Name Id Version Source
|
|
267
|
+
----------------------------------------
|
|
268
|
+
テキスト Vendor.Text 1.2.3 winget
|
|
269
|
+
`;
|
|
270
|
+
export const WINGET_NOT_FOUND = "No installed package found matching input criteria.\n";
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// chocolatey — Windows 11, choco 2.2.2 and 0.10.15
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
export const CHOCO_VERSION_V2 = "2.2.2\n";
|
|
275
|
+
export const CHOCO_VERSION_V1 = "0.10.15\n";
|
|
276
|
+
export const CHOCO_LIST_GIT = "git|2.45.1\ngit.install|2.45.1\n";
|
|
277
|
+
export const CHOCO_LIST_EMPTY = "";
|
|
278
|
+
/** The key a command is recorded under. NUL cannot appear in an argv element. */
|
|
279
|
+
export function argvKey(argv) {
|
|
280
|
+
return argv.join("\u0000");
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Serve recorded output, and refuse anything nobody recorded.
|
|
284
|
+
*
|
|
285
|
+
* The refusal is the important half. A runner that answered an unrecorded
|
|
286
|
+
* command with an empty success would be indistinguishable from a manager
|
|
287
|
+
* that had nothing to say, and every "not installed" in the suite would stop
|
|
288
|
+
* meaning anything. Instead the promise rejects with the argv, so a missing
|
|
289
|
+
* fixture fails the test that needed it and names the command.
|
|
290
|
+
*
|
|
291
|
+
* `fallback` exists for the tests that are ABOUT an unrecognised host: pass
|
|
292
|
+
* `{ missing: true }` to make every unrecorded command behave like a program
|
|
293
|
+
* that is not installed.
|
|
294
|
+
*/
|
|
295
|
+
export function recordedRunner(cases, fallback) {
|
|
296
|
+
const calls = [];
|
|
297
|
+
const requests = [];
|
|
298
|
+
// How many times each argv has been asked for. A case recorded as an ARRAY
|
|
299
|
+
// answers differently on successive calls, which is what an install needs:
|
|
300
|
+
// the same `brew info` command legitimately answers "not installed" before
|
|
301
|
+
// the install and "installed" after it, and a runner that could not express
|
|
302
|
+
// that would make the post-install verification untestable.
|
|
303
|
+
const seen = new Map();
|
|
304
|
+
const runner = async (request) => {
|
|
305
|
+
calls.push([...request.argv]);
|
|
306
|
+
requests.push(request);
|
|
307
|
+
const key = argvKey(request.argv);
|
|
308
|
+
const index = seen.get(key) ?? 0;
|
|
309
|
+
seen.set(key, index + 1);
|
|
310
|
+
const entry = cases[key];
|
|
311
|
+
const recorded = Array.isArray(entry)
|
|
312
|
+
? // Past the end the last recorded answer repeats, so a fixture does
|
|
313
|
+
// not have to enumerate every retry.
|
|
314
|
+
(entry[Math.min(index, entry.length - 1)] ?? fallback)
|
|
315
|
+
: (entry ?? fallback);
|
|
316
|
+
if (recorded === undefined) {
|
|
317
|
+
throw new Error(`no recorded output for: ${request.argv.map((a) => JSON.stringify(a)).join(" ")}`);
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
code: recorded.code ?? 0,
|
|
321
|
+
stdout: recorded.stdout ?? "",
|
|
322
|
+
stderr: recorded.stderr ?? "",
|
|
323
|
+
timedOut: recorded.timedOut ?? false,
|
|
324
|
+
missing: recorded.missing ?? false,
|
|
325
|
+
...(recorded.stdoutTruncated === true ? { stdoutTruncated: true } : {}),
|
|
326
|
+
...(recorded.stderrTruncated === true ? { stderrTruncated: true } : {}),
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
return { runner, calls, requests };
|
|
330
|
+
}
|
|
331
|
+
/** Shorthand: record a case under an argv rather than under a key. */
|
|
332
|
+
export function record(argv, outcome) {
|
|
333
|
+
return { [argvKey(argv)]: outcome };
|
|
334
|
+
}
|
package/dist/host.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** The platform strings this package makes decisions about. */
|
|
2
|
+
export type HostPlatform = "darwin" | "linux" | "win32" | "other";
|
|
3
|
+
/** Normalise `process.platform` into the cases that matter here. */
|
|
4
|
+
export declare function classifyPlatform(raw: string): HostPlatform;
|
|
5
|
+
export declare function hostPlatform(): HostPlatform;
|
|
6
|
+
/** Test seam: pretend to be another OS. */
|
|
7
|
+
export declare function _setPlatform(platform: HostPlatform | undefined): void;
|
|
8
|
+
/**
|
|
9
|
+
* The effective uid, or `undefined` where the concept does not exist.
|
|
10
|
+
*
|
|
11
|
+
* `process.geteuid` is absent on Windows. An unknown uid is reported as
|
|
12
|
+
* unknown and never defaulted to 0 — guessing "we are root" is the one guess
|
|
13
|
+
* here that ends with a manager being asked to modify the system.
|
|
14
|
+
*/
|
|
15
|
+
export declare function effectiveUid(): number | undefined;
|
|
16
|
+
/** True only when the process is PROVABLY root. Unknown is not root. */
|
|
17
|
+
export declare function isRoot(): boolean;
|
|
18
|
+
/** Test seam: pretend to be (or not to be) root. */
|
|
19
|
+
export declare function _setUid(uid: number | undefined): void;
|
|
20
|
+
/** Put both seams back to the real host. Called from `afterEach`. */
|
|
21
|
+
export declare function _resetHostSeams(): void;
|
package/dist/host.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two facts about the host that are not a command: which operating system
|
|
3
|
+
* this is, and whether the process is already root.
|
|
4
|
+
*
|
|
5
|
+
* ── THE HOSTILE DEFAULT ────────────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* `hostPlatform()` does NOT simply fall back to `process.platform`. When a
|
|
8
|
+
* runner has been injected — which is true in every test in this package and
|
|
9
|
+
* false in production — the un-injected platform is `"other"`, a platform no
|
|
10
|
+
* backend here supports.
|
|
11
|
+
*
|
|
12
|
+
* The reason is a bug this project has already paid for. A test in tool-hostfs
|
|
13
|
+
* relied on the ambient `process.platform`, passed on the author's macOS, and
|
|
14
|
+
* took a different branch on the Linux CI box; the suite went green locally
|
|
15
|
+
* and red in CI, and the failure said nothing about which seam was missing.
|
|
16
|
+
* Reading `process.platform` in a test is not a small sin that shows up as a
|
|
17
|
+
* small failure — it is a test that asserts something different on every
|
|
18
|
+
* machine it runs on.
|
|
19
|
+
*
|
|
20
|
+
* Tying the default to the RUNNER seam rather than sniffing an environment
|
|
21
|
+
* variable is deliberate. A test that has installed a recorded runner has
|
|
22
|
+
* already declared "I am not talking to this host"; if it then forgets to
|
|
23
|
+
* declare which host it is pretending to be, it gets `"other"` and every
|
|
24
|
+
* backend refuses, identically, on macOS and on Linux and on a maintainer's
|
|
25
|
+
* Windows box. Production never injects a runner, so production reads the
|
|
26
|
+
* real platform. There is no env sniffing, no `NODE_ENV`, and nothing that
|
|
27
|
+
* behaves differently under `bun test` than under `crewhaus run`.
|
|
28
|
+
*
|
|
29
|
+
* ── ROOT ───────────────────────────────────────────────────────────────────
|
|
30
|
+
*
|
|
31
|
+
* `effectiveUid()` exists because `apt-get install` cannot run without root
|
|
32
|
+
* and this package will not acquire root (house rule 7). It never escalates:
|
|
33
|
+
* it reports what the process ALREADY has, so a harness running as root in a
|
|
34
|
+
* container can install, and a harness running as a user gets a refusal that
|
|
35
|
+
* names the command the operator would run themselves. There is no code path
|
|
36
|
+
* here that runs `sudo`, `doas`, `runas` or `pkexec`, and no schema in this
|
|
37
|
+
* package accepts a password — `no argv ever names an escalation program` in
|
|
38
|
+
* index.test.ts asserts both over every recorded call.
|
|
39
|
+
*/
|
|
40
|
+
import { _runnerInstalled } from "./run";
|
|
41
|
+
let platformOverride;
|
|
42
|
+
/** Normalise `process.platform` into the cases that matter here. */
|
|
43
|
+
export function classifyPlatform(raw) {
|
|
44
|
+
if (raw === "darwin" || raw === "linux" || raw === "win32")
|
|
45
|
+
return raw;
|
|
46
|
+
return "other";
|
|
47
|
+
}
|
|
48
|
+
export function hostPlatform() {
|
|
49
|
+
if (platformOverride !== undefined)
|
|
50
|
+
return platformOverride;
|
|
51
|
+
// See the header: a test that drives a recorded runner and forgets to say
|
|
52
|
+
// which OS it is on gets a platform with no backend, everywhere.
|
|
53
|
+
if (_runnerInstalled())
|
|
54
|
+
return "other";
|
|
55
|
+
return classifyPlatform(process.platform);
|
|
56
|
+
}
|
|
57
|
+
/** Test seam: pretend to be another OS. */
|
|
58
|
+
export function _setPlatform(platform) {
|
|
59
|
+
platformOverride = platform;
|
|
60
|
+
}
|
|
61
|
+
let uidOverride;
|
|
62
|
+
/**
|
|
63
|
+
* The effective uid, or `undefined` where the concept does not exist.
|
|
64
|
+
*
|
|
65
|
+
* `process.geteuid` is absent on Windows. An unknown uid is reported as
|
|
66
|
+
* unknown and never defaulted to 0 — guessing "we are root" is the one guess
|
|
67
|
+
* here that ends with a manager being asked to modify the system.
|
|
68
|
+
*/
|
|
69
|
+
export function effectiveUid() {
|
|
70
|
+
if (uidOverride !== undefined)
|
|
71
|
+
return uidOverride;
|
|
72
|
+
const get = process.geteuid;
|
|
73
|
+
if (typeof get !== "function")
|
|
74
|
+
return undefined;
|
|
75
|
+
const uid = get.call(process);
|
|
76
|
+
return Number.isInteger(uid) && uid >= 0 ? uid : undefined;
|
|
77
|
+
}
|
|
78
|
+
/** True only when the process is PROVABLY root. Unknown is not root. */
|
|
79
|
+
export function isRoot() {
|
|
80
|
+
return effectiveUid() === 0;
|
|
81
|
+
}
|
|
82
|
+
/** Test seam: pretend to be (or not to be) root. */
|
|
83
|
+
export function _setUid(uid) {
|
|
84
|
+
uidOverride = uid;
|
|
85
|
+
}
|
|
86
|
+
/** Put both seams back to the real host. Called from `afterEach`. */
|
|
87
|
+
export function _resetHostSeams() {
|
|
88
|
+
platformOverride = undefined;
|
|
89
|
+
uidOverride = undefined;
|
|
90
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RegisteredTool } from "@crewhaus/tool-catalog";
|
|
2
|
+
import { type ManagerId } from "./lib/managers";
|
|
3
|
+
import { type InstallPlan } from "./lib/plan";
|
|
4
|
+
import { type PackageFacts } from "./lib/query";
|
|
5
|
+
export { _setRunner, type Runner, type RunRequest, type RunResult } from "./run";
|
|
6
|
+
export { _setPlatform, _setUid, _resetHostSeams, type HostPlatform } from "./host";
|
|
7
|
+
export { ToolPermissionError } from "./paths";
|
|
8
|
+
export type { PackageFacts, InstallPlan, ManagerId };
|
|
9
|
+
export declare const packageQuery: RegisteredTool;
|
|
10
|
+
export declare const packageInstall: RegisteredTool;
|
|
11
|
+
/**
|
|
12
|
+
* The install argv for a manager.
|
|
13
|
+
*
|
|
14
|
+
* A separate function so the test that asserts no argv names an escalation
|
|
15
|
+
* program can enumerate every one of them, including the ones only reached
|
|
16
|
+
* when the process is already root.
|
|
17
|
+
*/
|
|
18
|
+
export declare function installCommand(manager: ManagerId, operand: string): string[];
|
|
19
|
+
export declare const PKGMGR_TOOLS: readonly RegisteredTool[];
|