@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,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every parser in this package, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* Each one takes a string that a real manager printed — recorded in
|
|
5
|
+
* `../fixtures.ts` — and returns a shape, or `undefined` when the bytes are
|
|
6
|
+
* not what it was told to expect. None of them reads the host, none of them
|
|
7
|
+
* spawns anything, and none of them guesses: a parser that cannot find what
|
|
8
|
+
* it is looking for says so, and the caller turns that into an `unknown` with
|
|
9
|
+
* a reason rather than into a `false`.
|
|
10
|
+
*
|
|
11
|
+
* Two traps recur across the six managers and are worth stating once.
|
|
12
|
+
*
|
|
13
|
+
* LOCALISATION. `apt-cache policy`, `dnf`, `pacman -Si` and `brew` all
|
|
14
|
+
* translate their labels through gettext. Every parser below reads ENGLISH
|
|
15
|
+
* keywords, which is sound only because `../run.ts` pins `LC_ALL=C` on every
|
|
16
|
+
* child. If that pin is ever removed these parsers do not fail loudly — they
|
|
17
|
+
* find nothing, and "nothing" reads as "not installed". The suite includes a
|
|
18
|
+
* German `apt-cache policy` capture precisely so that failure has a name.
|
|
19
|
+
*
|
|
20
|
+
* COLUMN OUTPUT. `winget list` and `dnf list` print aligned columns, and a
|
|
21
|
+
* column layout is not a data format. winget truncates a wide field with an
|
|
22
|
+
* ellipsis (U+2026) and pads with spaces indistinguishable from the
|
|
23
|
+
* separator, so the parser below refuses a row it cannot split unambiguously
|
|
24
|
+
* rather than returning a version that is a prefix of the real one.
|
|
25
|
+
*/
|
|
26
|
+
/** Split into lines, tolerating CRLF from a Windows manager. */
|
|
27
|
+
export declare function lines(text: string): string[];
|
|
28
|
+
export type BrewEntry = {
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly kind: "formula" | "cask";
|
|
31
|
+
/** Versions on disk, as brew prints them. Empty when absent. */
|
|
32
|
+
readonly installedVersions: readonly string[];
|
|
33
|
+
/** What the tap currently offers, or null when the document omits it. */
|
|
34
|
+
readonly stableVersion: string | null;
|
|
35
|
+
readonly pinned: boolean;
|
|
36
|
+
readonly outdated: boolean;
|
|
37
|
+
readonly dependencies: readonly string[];
|
|
38
|
+
readonly tap: string | null;
|
|
39
|
+
};
|
|
40
|
+
export type BrewInfo = {
|
|
41
|
+
readonly formula?: BrewEntry;
|
|
42
|
+
readonly cask?: BrewEntry;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* `brew info --json=v2` — the one call that answers both halves of a query.
|
|
46
|
+
*
|
|
47
|
+
* The document always has both arrays, and a name resolves into exactly one
|
|
48
|
+
* of them, so a caller does not have to know whether it is asking about a
|
|
49
|
+
* formula or a cask before it asks. A cask spells everything differently
|
|
50
|
+
* (`token` not `name`, a scalar `version` not a `versions` object, a scalar
|
|
51
|
+
* `installed` not an array), which is why the two are normalised here rather
|
|
52
|
+
* than at the call site.
|
|
53
|
+
*
|
|
54
|
+
* Returns `undefined` when the bytes are not a v2 document at all — which is
|
|
55
|
+
* what a failed `brew` prints, and must not be read as "no such package".
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseBrewInfoJson(stdout: string): BrewInfo | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* `brew list --formula --versions` — `name version [version…]`, one per line.
|
|
60
|
+
*
|
|
61
|
+
* A keg-only formula with several versions kegged prints all of them, which
|
|
62
|
+
* is why the value is an array: reporting the first as "the" installed
|
|
63
|
+
* version would be wrong for exactly the formulae (python@3.x, openssl@3) a
|
|
64
|
+
* caller is most likely to ask about.
|
|
65
|
+
*/
|
|
66
|
+
export declare function parseBrewListVersions(stdout: string): Map<string, string[]>;
|
|
67
|
+
/** `brew deps --formula <name>` — one dependency per line, already transitive. */
|
|
68
|
+
export declare function parseBrewDeps(stdout: string): string[];
|
|
69
|
+
/**
|
|
70
|
+
* Homebrew's own "no such package" signal.
|
|
71
|
+
*
|
|
72
|
+
* Recorded verbatim: `brew info --json=v2 nope-nope` exits 1 and prints
|
|
73
|
+
* `Error: No available formula with the name "nope-nope".` on stderr.
|
|
74
|
+
* Matching that specific sentence rather than "exit code 1" is what keeps a
|
|
75
|
+
* tap that failed to load from being reported as a missing package.
|
|
76
|
+
*/
|
|
77
|
+
export declare function brewSaysNoSuchPackage(stderr: string): boolean;
|
|
78
|
+
export type DpkgRow = {
|
|
79
|
+
readonly name: string;
|
|
80
|
+
readonly version: string;
|
|
81
|
+
/** dpkg's status abbreviation, e.g. "ii", "un", "rc". */
|
|
82
|
+
readonly statusAbbrev: string;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* `dpkg-query -W -f='${binary:Package}\t${Version}\t${db:Status-Abbrev}\n'`.
|
|
86
|
+
*
|
|
87
|
+
* Tab-separated because a Debian version can contain almost anything except a
|
|
88
|
+
* tab, and named fields because the abbrev's first character is the fact that
|
|
89
|
+
* matters: `i` means the files are on disk, while `u` (unknown) and `r`
|
|
90
|
+
* (removed, config files left) are packages dpkg KNOWS ABOUT but does not
|
|
91
|
+
* have installed. An `rc` package read as installed is the classic dpkg
|
|
92
|
+
* mistake — the binary is gone and only `/etc` remains.
|
|
93
|
+
*/
|
|
94
|
+
export declare function parseDpkgQuery(stdout: string): DpkgRow[];
|
|
95
|
+
/** True when dpkg's status abbreviation means the package is actually on disk. */
|
|
96
|
+
export declare function dpkgStatusIsInstalled(abbrev: string): boolean;
|
|
97
|
+
/** dpkg's own "I have never heard of this package" signal, on stderr. */
|
|
98
|
+
export declare function dpkgSaysNoSuchPackage(stderr: string): boolean;
|
|
99
|
+
export type AptPolicy = {
|
|
100
|
+
readonly installed: string | null;
|
|
101
|
+
readonly candidate: string | null;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* `apt-cache policy <name>` under `LC_ALL=C`.
|
|
105
|
+
*
|
|
106
|
+
* curl:
|
|
107
|
+
* Installed: 7.81.0-1ubuntu1.15
|
|
108
|
+
* Candidate: 7.81.0-1ubuntu1.16
|
|
109
|
+
* Version table: …
|
|
110
|
+
*
|
|
111
|
+
* `(none)` is apt's word for "not installed" and becomes `null` here — which
|
|
112
|
+
* is a DIFFERENT null from "the label was not found at all", and the caller
|
|
113
|
+
* tells them apart by whether the parse returned undefined.
|
|
114
|
+
*
|
|
115
|
+
* An unknown package produces an empty document (apt-cache prints nothing and
|
|
116
|
+
* exits 0), which parses to `undefined` rather than to two nulls, so "apt has
|
|
117
|
+
* never heard of this" cannot be confused with "apt has it, not installed".
|
|
118
|
+
*/
|
|
119
|
+
export declare function parseAptCachePolicy(stdout: string): AptPolicy | undefined;
|
|
120
|
+
export type AptSimulatedChange = {
|
|
121
|
+
readonly action: "install" | "remove";
|
|
122
|
+
readonly name: string;
|
|
123
|
+
readonly version: string | null;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* `apt-get install -s` — the best dry run of the six managers.
|
|
127
|
+
*
|
|
128
|
+
* It resolves the whole transaction without root and prints a line per
|
|
129
|
+
* package:
|
|
130
|
+
*
|
|
131
|
+
* Inst libnghttp2-14 (1.43.0-1 Debian:12 [amd64])
|
|
132
|
+
* Inst curl [7.88.1-10] (7.88.1-10+deb12u5 Debian-Security:12 [amd64])
|
|
133
|
+
* Conf curl (7.88.1-10+deb12u5 Debian-Security:12 [amd64])
|
|
134
|
+
* Remv oldthing [1.0-1]
|
|
135
|
+
*
|
|
136
|
+
* `Conf` lines are dropped: they name the same packages as `Inst`, and
|
|
137
|
+
* counting both would double every transitive addition. `Remv` is kept,
|
|
138
|
+
* because a transaction that REMOVES something is exactly the case an
|
|
139
|
+
* operator has to see before approving an install. The optional `[old]`
|
|
140
|
+
* bracket between the name and the parenthesis is what an UPGRADE looks like,
|
|
141
|
+
* and is skipped so the version reported is the one being installed.
|
|
142
|
+
*/
|
|
143
|
+
export declare function parseAptSimulate(stdout: string): AptSimulatedChange[];
|
|
144
|
+
/** apt's "there is no such package" signal, which it prints on stderr. */
|
|
145
|
+
export declare function aptSaysNoSuchPackage(stderr: string): boolean;
|
|
146
|
+
/** apt's "you are not root" signal. */
|
|
147
|
+
export declare function aptSaysNeedsRoot(text: string): boolean;
|
|
148
|
+
export type RpmRow = {
|
|
149
|
+
readonly name: string;
|
|
150
|
+
/** epoch:version-release, as rpm's `%{EVR}` renders it. */
|
|
151
|
+
readonly evr: string;
|
|
152
|
+
readonly arch: string;
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* `rpm -q --queryformat '%{NAME}\t%{EVR}\t%{ARCH}\n' <name>`.
|
|
156
|
+
*
|
|
157
|
+
* rpm prints "package foo is not installed" on STDOUT (not stderr) and exits
|
|
158
|
+
* 1, so that sentence has to be filtered out of the rows rather than detected
|
|
159
|
+
* by exit code alone — a query for several names prints some rows and some of
|
|
160
|
+
* those sentences in the same stream.
|
|
161
|
+
*/
|
|
162
|
+
export declare function parseRpmQuery(stdout: string): RpmRow[];
|
|
163
|
+
/** rpm's own not-installed sentence, which it prints on stdout. */
|
|
164
|
+
export declare function rpmSaysNotInstalled(stdout: string): boolean;
|
|
165
|
+
export type DnfListRow = {
|
|
166
|
+
readonly name: string;
|
|
167
|
+
readonly arch: string;
|
|
168
|
+
readonly version: string;
|
|
169
|
+
readonly repo: string;
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* `dnf list --available <name>` — three columns, `name.arch version repo`.
|
|
173
|
+
*
|
|
174
|
+
* Header lines ("Available Packages", "Last metadata expiration check: …")
|
|
175
|
+
* are skipped by SHAPE rather than by text: a line qualifies only when it
|
|
176
|
+
* splits into exactly three whitespace-separated fields, the first contains a
|
|
177
|
+
* `.` separating a name from an architecture, and the second starts with a
|
|
178
|
+
* digit. Matching on the header text would break the moment dnf renames it,
|
|
179
|
+
* and dnf5 did exactly that.
|
|
180
|
+
*/
|
|
181
|
+
export declare function parseDnfList(stdout: string): DnfListRow[];
|
|
182
|
+
/**
|
|
183
|
+
* dnf's "the cache is empty and I was told not to fetch" complaint.
|
|
184
|
+
*
|
|
185
|
+
* This is the difference between "not in the repositories" and "I do not
|
|
186
|
+
* know", and `--cacheonly` makes the second common on a fresh container.
|
|
187
|
+
*/
|
|
188
|
+
export declare function dnfSaysNoCache(text: string): boolean;
|
|
189
|
+
/** dnf's own "no such package" signal. */
|
|
190
|
+
export declare function dnfSaysNoMatch(text: string): boolean;
|
|
191
|
+
/** dnf's "you are not root" signal — checked before a transaction is read. */
|
|
192
|
+
export declare function dnfSaysNeedsRoot(text: string): boolean;
|
|
193
|
+
export type DnfTransactionRow = {
|
|
194
|
+
readonly name: string;
|
|
195
|
+
readonly arch: string;
|
|
196
|
+
readonly version: string;
|
|
197
|
+
readonly repo: string;
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* The transaction table `dnf install --assumeno` prints before it declines.
|
|
201
|
+
*
|
|
202
|
+
* Installing:
|
|
203
|
+
* tree x86_64 1.8.0-10.el9 baseos 55 k
|
|
204
|
+
* Installing dependencies:
|
|
205
|
+
* libfoo x86_64 1.2-3 baseos 10 k
|
|
206
|
+
*
|
|
207
|
+
* Section headers are matched on the English words, which is sound under the
|
|
208
|
+
* pinned `LC_ALL=C` and nowhere else. A row must have at least five fields;
|
|
209
|
+
* the size is two of them ("55 k") and is dropped, since a caller who wants a
|
|
210
|
+
* download total has dnf's own summary line.
|
|
211
|
+
*/
|
|
212
|
+
export declare function parseDnfTransaction(stdout: string): DnfTransactionRow[];
|
|
213
|
+
export type PacmanRow = {
|
|
214
|
+
readonly name: string;
|
|
215
|
+
readonly version: string;
|
|
216
|
+
};
|
|
217
|
+
/** `pacman -Q <name>` — `name version`, one per line. */
|
|
218
|
+
export declare function parsePacmanQuery(stdout: string): PacmanRow[];
|
|
219
|
+
/**
|
|
220
|
+
* `pacman -Si <name>` — a `Key : Value` block with indented continuations.
|
|
221
|
+
*
|
|
222
|
+
* Only the FIRST block is read: a name that matches in two repositories
|
|
223
|
+
* prints two blocks, and merging them would report `extra`'s version against
|
|
224
|
+
* `core`'s repository. The caller reports the first block and says the name
|
|
225
|
+
* was ambiguous.
|
|
226
|
+
*/
|
|
227
|
+
export declare function parsePacmanInfo(stdout: string): Map<string, string>;
|
|
228
|
+
export type PacmanTarget = {
|
|
229
|
+
readonly repo: string | null;
|
|
230
|
+
readonly name: string;
|
|
231
|
+
readonly version: string;
|
|
232
|
+
};
|
|
233
|
+
/**
|
|
234
|
+
* `pacman -S --print --print-format '%r/%n %v'` — every target, dependencies
|
|
235
|
+
* included, with versions, no root and no transaction.
|
|
236
|
+
*
|
|
237
|
+
* A package coming from a local file rather than a repository prints an empty
|
|
238
|
+
* repo, which becomes `null` rather than a made-up repository name.
|
|
239
|
+
*/
|
|
240
|
+
export declare function parsePacmanSimulate(stdout: string): PacmanTarget[];
|
|
241
|
+
/** pacman's own "no such package" signal. */
|
|
242
|
+
export declare function pacmanSaysNoSuchPackage(stderr: string): boolean;
|
|
243
|
+
/** pacman's "you are not root" signal. */
|
|
244
|
+
export declare function pacmanSaysNeedsRoot(text: string): boolean;
|
|
245
|
+
export type WingetRow = {
|
|
246
|
+
readonly name: string;
|
|
247
|
+
readonly id: string;
|
|
248
|
+
/** null when the row could not be split safely — see `parseWingetList`. */
|
|
249
|
+
readonly version: string | null;
|
|
250
|
+
readonly available: string | null;
|
|
251
|
+
/** Why `version` or `available` is null, when it is. */
|
|
252
|
+
readonly unparsedReason?: string;
|
|
253
|
+
};
|
|
254
|
+
/**
|
|
255
|
+
* `winget list --exact --query <name>` — an aligned table, which is not a
|
|
256
|
+
* data format.
|
|
257
|
+
*
|
|
258
|
+
* winget has no JSON output for `list`. The survey sketch proposed splitting
|
|
259
|
+
* by the header's character offsets; that is wrong, and this is the improved
|
|
260
|
+
* version. Offsets break on two things winget does routinely: it TRUNCATES a
|
|
261
|
+
* field that does not fit and marks the cut with `…`, and it pads with
|
|
262
|
+
* ordinary spaces, so a CJK or emoji display name — double-width in the
|
|
263
|
+
* console but one or two UTF-16 code units in the string — shifts every
|
|
264
|
+
* offset after it. A version read from a shifted offset is not an error, it
|
|
265
|
+
* is a WRONG VERSION, which is the worst answer a query tool can give.
|
|
266
|
+
*
|
|
267
|
+
* So rows are split on runs of TWO OR MORE spaces (one space is inside a
|
|
268
|
+
* display name; two is padding), and a row is refused rather than guessed at
|
|
269
|
+
* when it does not split cleanly:
|
|
270
|
+
*
|
|
271
|
+
* - any field containing `…` was truncated, so nothing in the row is
|
|
272
|
+
* trusted;
|
|
273
|
+
* - fewer than three fields is not a `Name Id Version` row at all;
|
|
274
|
+
* - the column ORDER is fixed across locales (`Name Id Version [Available]
|
|
275
|
+
* Source`) even though the header TEXT is localised, so position 2 is the
|
|
276
|
+
* installed version — but position 3 is ambiguous: a four-field row is
|
|
277
|
+
* either `…Version Source` or `…Version Available`, with no way to tell.
|
|
278
|
+
* Four fields therefore yields a null `available` with a reason.
|
|
279
|
+
*/
|
|
280
|
+
export declare function parseWingetList(stdout: string): WingetRow[];
|
|
281
|
+
/**
|
|
282
|
+
* winget's "no installed package matched" exit code, 0x8A15002B as a signed
|
|
283
|
+
* 32-bit integer.
|
|
284
|
+
*
|
|
285
|
+
* `winget list` exits with this rather than 0-and-empty, which makes presence
|
|
286
|
+
* decidable from the EXIT CODE alone — the one part of winget's answer that
|
|
287
|
+
* is neither localised nor column-aligned.
|
|
288
|
+
*/
|
|
289
|
+
export declare const WINGET_NO_PACKAGES_FOUND = -1978335189;
|
|
290
|
+
/**
|
|
291
|
+
* `choco list --limit-output` — `name|version`, one per line, no header.
|
|
292
|
+
*
|
|
293
|
+
* `--limit-output` is chocolatey's documented machine-readable mode and the
|
|
294
|
+
* reason choco is the easiest of the six to read. A trailing summary line
|
|
295
|
+
* ("2 packages installed.") does not appear in this mode, and is dropped by
|
|
296
|
+
* shape anyway in case an older build prints one.
|
|
297
|
+
*/
|
|
298
|
+
export declare function parseChocoList(stdout: string): PacmanRow[];
|
|
299
|
+
/**
|
|
300
|
+
* The major version of `choco --version`.
|
|
301
|
+
*
|
|
302
|
+
* Chocolatey 2.0 removed `--local-only` from `choco list`, so which flags are
|
|
303
|
+
* legal depends on this number. Guessing would mean either an unknown-option
|
|
304
|
+
* error on v2 or a REMOTE search on v1 — and a remote search answering "yes,
|
|
305
|
+
* 8.7.1" for a package that is not installed at all is precisely the wrong
|
|
306
|
+
* answer this package exists to avoid.
|
|
307
|
+
*/
|
|
308
|
+
export declare function parseChocoMajorVersion(stdout: string): number | undefined;
|