@mawesome/npm-vm 0.0.0-alpha.0 → 0.1.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -1
  3. package/dist/cli.js +4157 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/index-B071ZJDk.d.cts +164 -0
  6. package/dist/index-B071ZJDk.d.ts +164 -0
  7. package/dist/index.cjs +7 -0
  8. package/dist/index.d.cts +2 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +2 -0
  11. package/dist/internal.cjs +2709 -0
  12. package/dist/internal.cjs.map +1 -0
  13. package/dist/internal.d.cts +139 -0
  14. package/dist/internal.d.ts +139 -0
  15. package/dist/internal.js +2706 -0
  16. package/dist/internal.js.map +1 -0
  17. package/dist/provenance--sOYCeF2.js +322 -0
  18. package/dist/provenance--sOYCeF2.js.map +1 -0
  19. package/dist/provenance-DDegWCyS.js +2 -0
  20. package/dist/runtime/atomic-C3shKdH9.cjs +88 -0
  21. package/dist/runtime/bootstrap.cjs +271 -0
  22. package/dist/runtime/commonjs-AANzyd21.cjs +7393 -0
  23. package/dist/runtime/dispatch-B9OSfqsn.cjs +25156 -0
  24. package/dist/runtime/dist-CXwBT6rE.cjs +30766 -0
  25. package/dist/runtime/home-BaoZZSBy.cjs +91 -0
  26. package/dist/runtime/install-C_M5WK98.cjs +13598 -0
  27. package/dist/runtime/journal-_bs-eJRJ.cjs +282 -0
  28. package/dist/runtime/launchers/cmd.tmpl +2 -0
  29. package/dist/runtime/launchers/ps1.tmpl +5 -0
  30. package/dist/runtime/launchers/sh.tmpl +3 -0
  31. package/dist/runtime/lease-HBD4CmBx.cjs +224 -0
  32. package/dist/runtime/lock-DBeYN-Zg.cjs +141 -0
  33. package/dist/runtime/messages-vUDkl1-0.cjs +174 -0
  34. package/dist/runtime/p-map-b8pXdaFZ.cjs +105 -0
  35. package/dist/runtime/proc-hB8N9WjN.cjs +127 -0
  36. package/dist/runtime/provenance-B4bajGXQ.cjs +2 -0
  37. package/dist/runtime/rolldown-runtime-BGT596CS.cjs +64 -0
  38. package/dist/runtime/runtime.cjs +143 -0
  39. package/dist/runtime/src-SpeGbMJx.cjs +314 -0
  40. package/dist/src-Bc9nN1Kl.cjs +2125 -0
  41. package/dist/src-Bc9nN1Kl.cjs.map +1 -0
  42. package/dist/src-NhNG2mnQ.js +1798 -0
  43. package/dist/src-NhNG2mnQ.js.map +1 -0
  44. package/package.json +87 -17
  45. package/index.js +0 -3
package/dist/cli.js ADDED
@@ -0,0 +1,4157 @@
1
+ #!/usr/bin/env node
2
+ import { C as redactUrls, D as EXIT, E as unsignedMessage, O as NpmVmError, S as noMatchMessage, T as transportMessage, _ as dirPermsMessage, b as keyMessage, g as changedMessage, h as abiMessage, k as isNpmVmError, m as verifyProvenance, v as engineMessage, w as restartMessage, x as lockedMessage, y as integrityMessage } from "./provenance--sOYCeF2.js";
3
+ import { accessSync, chmodSync, closeSync, constants, copyFileSync, createReadStream, createWriteStream, existsSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, utimesSync, writeFileSync, writeSync } from "node:fs";
4
+ import { parseArgs } from "node:util";
5
+ import { basename, dirname, isAbsolute, join, matchesGlob, posix, relative, resolve, sep, win32 } from "node:path";
6
+ import { createHash, createPublicKey, randomBytes, verify } from "node:crypto";
7
+ import semver from "semver";
8
+ import { homedir, hostname } from "node:os";
9
+ import { execFile, execFileSync } from "node:child_process";
10
+ import { Transform } from "node:stream";
11
+ import { pipeline } from "node:stream/promises";
12
+ import { createGunzip } from "node:zlib";
13
+ import { Parser } from "tar";
14
+ import Config from "@npmcli/config";
15
+ import npmDefinitions from "@npmcli/config/lib/definitions/index.js";
16
+ import { createInterface } from "node:readline/promises";
17
+ //#region src/own-npm.ts
18
+ /**
19
+ * Locates Node's own npm from the running binary (spec section 1).
20
+ * The lib dir, not the `bin/npm` link: under `--link` that link is the shim.
21
+ */
22
+ function ownNpmRoot(execPath, platform = process.platform) {
23
+ if (platform === "win32") return win32.join(win32.dirname(execPath), "node_modules", "npm");
24
+ let real = execPath;
25
+ try {
26
+ real = realpathSync(execPath);
27
+ } catch {}
28
+ return posix.join(posix.dirname(real), "..", "lib", "node_modules", "npm");
29
+ }
30
+ function findOwnNpm(execPath = process.execPath, platform = process.platform) {
31
+ const root = ownNpmRoot(execPath, platform);
32
+ const { join } = platform === "win32" ? win32 : posix;
33
+ const manifest = join(root, "package.json");
34
+ if (!existsSync(manifest)) return null;
35
+ let version;
36
+ try {
37
+ version = JSON.parse(readFileSync(manifest, "utf8")).version;
38
+ } catch {
39
+ return null;
40
+ }
41
+ if (typeof version !== "string") return null;
42
+ return {
43
+ root,
44
+ version,
45
+ cliPath: join(root, "bin", "npm-cli.js"),
46
+ npxCliPath: join(root, "bin", "npx-cli.js")
47
+ };
48
+ }
49
+ //#endregion
50
+ //#region src/state/proc.ts
51
+ function powershellPath(env = process.env) {
52
+ return join(env["SystemRoot"] ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
53
+ }
54
+ const SHELL_DIRS = [
55
+ "/bin",
56
+ "/usr/bin",
57
+ "/usr/local/bin",
58
+ "/opt/homebrew/bin",
59
+ "/snap/bin"
60
+ ];
61
+ /**
62
+ * An absolute path for a shell to probe (spec SEC-8): the login shell when `$SHELL` is absolute, else system dirs.
63
+ * Never PATH, which inside an npm script starts with `node_modules/.bin`.
64
+ */
65
+ function shellBinary(shell, env = process.env, platform = process.platform) {
66
+ const name = shell === "powershell" ? "pwsh" : shell;
67
+ if (platform === "win32") {
68
+ const pwsh = join(env["ProgramFiles"] ?? "C:\\Program Files", "PowerShell", "7", "pwsh.exe");
69
+ return shell === "powershell" && existsSync(pwsh) ? pwsh : null;
70
+ }
71
+ const login = env["SHELL"];
72
+ return [...login !== void 0 && isAbsolute(login) && basename(login) === name ? [login] : [], ...SHELL_DIRS.map((dir) => join(dir, name))].find((candidate) => existsSync(candidate)) ?? null;
73
+ }
74
+ /** The start identity of `pid`, or null when the lookup fails or is ambiguous. */
75
+ function startIdentity(pid, platform = process.platform, env = process.env) {
76
+ try {
77
+ if (platform === "linux") {
78
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
79
+ const start = stat.slice(stat.lastIndexOf(")") + 1).trim().split(/\s+/)[19];
80
+ return start !== void 0 && /^\d+$/.test(start) ? start : null;
81
+ }
82
+ if (platform === "darwin") {
83
+ const out = execFileSync("/bin/ps", [
84
+ "-o",
85
+ "lstart=",
86
+ "-p",
87
+ String(pid)
88
+ ], {
89
+ encoding: "utf8",
90
+ env: {
91
+ LC_ALL: "C",
92
+ PATH: "/usr/bin:/bin"
93
+ }
94
+ }).trim();
95
+ return out === "" || out.includes("\n") ? null : out;
96
+ }
97
+ if (platform === "win32") {
98
+ const out = execFileSync(powershellPath(env), [
99
+ "-NoProfile",
100
+ "-NonInteractive",
101
+ "-Command",
102
+ `(Get-Process -Id ${pid}).StartTime.ToFileTimeUtc()`
103
+ ], {
104
+ encoding: "utf8",
105
+ windowsHide: true
106
+ }).trim();
107
+ return /^\d+$/.test(out) ? out : null;
108
+ }
109
+ } catch {
110
+ return null;
111
+ }
112
+ return null;
113
+ }
114
+ function ownIdentity(platform, env) {
115
+ return {
116
+ pid: process.pid,
117
+ start: startIdentity(process.pid, platform, env)
118
+ };
119
+ }
120
+ /**
121
+ * Whether the recorded process still runs: dead only when the PID is gone or its start identity differs.
122
+ * Any failure to look up means live, so nothing ever expires a process that merely cannot be inspected.
123
+ */
124
+ function isLive(identity, platform = process.platform) {
125
+ try {
126
+ process.kill(identity.pid, 0);
127
+ } catch (error) {
128
+ if (error.code === "ESRCH") return false;
129
+ return true;
130
+ }
131
+ if (identity.start === null) return true;
132
+ const current = startIdentity(identity.pid, platform);
133
+ return current === null || current === identity.start;
134
+ }
135
+ //#endregion
136
+ //#region src/state/lease.ts
137
+ function leasesDir(entry) {
138
+ return `${entry}.leases`;
139
+ }
140
+ function gatePath(entry) {
141
+ return `${entry}.prune`;
142
+ }
143
+ function newToken() {
144
+ return randomBytes(8).toString("hex");
145
+ }
146
+ function ownerRecord(machineId, identity = ownIdentity()) {
147
+ return {
148
+ ...identity,
149
+ host: hostname(),
150
+ machineId,
151
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
152
+ };
153
+ }
154
+ function readOwner(file) {
155
+ try {
156
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
157
+ if (typeof parsed.pid !== "number") return null;
158
+ return {
159
+ pid: parsed.pid,
160
+ start: typeof parsed.start === "string" ? parsed.start : null,
161
+ host: typeof parsed.host === "string" ? parsed.host : "?",
162
+ machineId: typeof parsed.machineId === "string" ? parsed.machineId : "?",
163
+ createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : ""
164
+ };
165
+ } catch {
166
+ return null;
167
+ }
168
+ }
169
+ /** A file in the per-machine cache belongs to a dead owner only when PROC-1 says so; unreadable means live. */
170
+ function ownerLive(file) {
171
+ const owner = readOwner(file);
172
+ return owner === null ? true : isLive(owner);
173
+ }
174
+ /** Creates a shared lease on `entry`; the file name is unique, so a prune can never delete a live one by mistake. */
175
+ function createLease(entry, owner) {
176
+ const dir = leasesDir(entry);
177
+ for (let attempt = 0;; attempt += 1) {
178
+ mkdirSync(dir, {
179
+ recursive: true,
180
+ mode: 448
181
+ });
182
+ const path = join(dir, newToken());
183
+ try {
184
+ writeFileSync(path, JSON.stringify(owner), {
185
+ mode: 384,
186
+ flag: "wx"
187
+ });
188
+ } catch (error) {
189
+ rmSync(path, { force: true });
190
+ if (error.code === "ENOENT" && attempt < 3) continue;
191
+ throw error;
192
+ }
193
+ return {
194
+ path,
195
+ release() {
196
+ rmSync(path, { force: true });
197
+ }
198
+ };
199
+ }
200
+ }
201
+ /**
202
+ * Whether a lease created before a gate check is still in place (spec LEASE-1).
203
+ * A prune that passed its own lease check first moves the whole leases dir away, taking the file with it.
204
+ */
205
+ function leaseSurvived(lease) {
206
+ return existsSync(lease.path);
207
+ }
208
+ function listLeases(entry) {
209
+ try {
210
+ return readdirSync(leasesDir(entry)).map((name) => join(leasesDir(entry), name));
211
+ } catch {
212
+ return [];
213
+ }
214
+ }
215
+ function sleep$1(ms) {
216
+ return new Promise((resolve) => setTimeout(resolve, ms));
217
+ }
218
+ const GATE_WAIT_MS = 5e3;
219
+ /**
220
+ * The runtime's shared lease (spec LEASE-1): lease first, then look for a live prune gate.
221
+ * A live gate means the entry is going away: wait briefly, retry once, else return null so the caller fetches a private entry.
222
+ */
223
+ async function acquireShared(entry, owner, options = {}) {
224
+ for (let attempt = 0; attempt < 3; attempt += 1) {
225
+ const lease = createLease(entry, owner);
226
+ const gate = gatePath(entry);
227
+ let gated = false;
228
+ try {
229
+ gated = ownerLive(gate);
230
+ } catch {
231
+ gated = false;
232
+ }
233
+ if (!gated || !gateExists(gate)) {
234
+ if (leaseSurvived(lease)) return lease;
235
+ continue;
236
+ }
237
+ lease.release();
238
+ const deadline = Date.now() + (options.waitMs ?? GATE_WAIT_MS);
239
+ while (Date.now() < deadline && gateExists(gate)) await sleep$1(50);
240
+ }
241
+ return null;
242
+ }
243
+ function gateExists(gate) {
244
+ try {
245
+ return readOwner(gate) !== null;
246
+ } catch {
247
+ return false;
248
+ }
249
+ }
250
+ /** Creates the prune gate with O_EXCL; false when another gate exists (there is only ever one prune, so a leftover). */
251
+ function createGate(entry, owner) {
252
+ try {
253
+ writeFileSync(gatePath(entry), JSON.stringify(owner), {
254
+ flag: "wx",
255
+ mode: 384
256
+ });
257
+ } catch (error) {
258
+ if (error.code === "EEXIST") return false;
259
+ rmSync(gatePath(entry), { force: true });
260
+ throw error;
261
+ }
262
+ return true;
263
+ }
264
+ function removeGate(entry) {
265
+ rmSync(gatePath(entry), { force: true });
266
+ }
267
+ //#endregion
268
+ //#region src/spec.ts
269
+ var SpecError = class extends Error {
270
+ name = "SpecError";
271
+ field;
272
+ /** The project's policy for this failure, when the entry declaring it could still be read. */
273
+ onFail = "error";
274
+ constructor(field, message) {
275
+ super(message);
276
+ this.field = field;
277
+ }
278
+ };
279
+ const MAX_RANGE_LENGTH = 256;
280
+ const HASH_HEX_LENGTH = {
281
+ sha512: 128,
282
+ sha256: 64,
283
+ sha224: 56
284
+ };
285
+ const ON_FAIL = /* @__PURE__ */ new Set([
286
+ "ignore",
287
+ "warn",
288
+ "error",
289
+ "download"
290
+ ]);
291
+ /**
292
+ * Validates one version string before it reaches a transport argv or a message (spec RES-4).
293
+ * Build metadata other than a well-formed hash suffix is rejected: semver would silently drop it.
294
+ */
295
+ function parseVersionSpec(raw, field) {
296
+ if (raw.length > MAX_RANGE_LENGTH) throw new SpecError(field, `${field}: version is longer than ${MAX_RANGE_LENGTH} characters`);
297
+ const plus = raw.indexOf("+");
298
+ const range = (plus === -1 ? raw : raw.slice(0, plus)).trim();
299
+ let hash = null;
300
+ if (plus !== -1) hash = parseHash(raw.slice(plus + 1), field);
301
+ if (semver.validRange(range) === null) throw new SpecError(field, `${field}: ${JSON.stringify(raw)} is not a semver range`);
302
+ const exact = semver.valid(range);
303
+ if (hash !== null && exact === null) throw new SpecError(field, `${field}: a hash needs an exact version, got ${JSON.stringify(raw)}`);
304
+ return {
305
+ range,
306
+ exact,
307
+ hash
308
+ };
309
+ }
310
+ function parseHash(suffix, field) {
311
+ const match = /^(sha512|sha256|sha224)\.([0-9a-fA-F]+)$/.exec(suffix);
312
+ if (match === null) throw new SpecError(field, `${field}: unsupported hash suffix ${JSON.stringify(`+${suffix}`)}`);
313
+ const algorithm = match[1];
314
+ const hex = match[2].toLowerCase();
315
+ if (hex.length !== HASH_HEX_LENGTH[algorithm]) throw new SpecError(field, `${field}: ${algorithm} hash must be ${HASH_HEX_LENGTH[algorithm]} hex characters`);
316
+ return {
317
+ algorithm,
318
+ hex
319
+ };
320
+ }
321
+ function isObject(value) {
322
+ return typeof value === "object" && value !== null && !Array.isArray(value);
323
+ }
324
+ /**
325
+ * Reads the npm requirements of a parsed `package.json` (spec RES-4).
326
+ * Returns null when the project declares nothing for npm, including when it declares only pnpm or yarn.
327
+ */
328
+ function parseSpecs(pkg) {
329
+ if (!isObject(pkg)) return null;
330
+ const devEngines = pkg["devEngines"];
331
+ if (isObject(devEngines) && devEngines["packageManager"] !== void 0) {
332
+ const fromDevEngines = parseDevEngines(devEngines["packageManager"]);
333
+ if (fromDevEngines !== null) return fromDevEngines;
334
+ }
335
+ const packageManager = pkg["packageManager"];
336
+ if (typeof packageManager === "string" && packageManager.startsWith("npm@")) {
337
+ const field = "packageManager";
338
+ return {
339
+ specs: [parseVersionSpec(packageManager.slice(4), field)],
340
+ onFail: "error",
341
+ field
342
+ };
343
+ }
344
+ return null;
345
+ }
346
+ function parseDevEngines(value) {
347
+ const field = "devEngines.packageManager";
348
+ const entries = Array.isArray(value) ? value : [value];
349
+ if (entries.length === 0) return null;
350
+ const specs = [];
351
+ const last = entries[entries.length - 1];
352
+ const onFail = isObject(last) ? parseOnFail(last, field) : "error";
353
+ try {
354
+ for (const entry of entries) {
355
+ if (!isObject(entry)) throw new SpecError(field, `${field}: every entry must be an object`);
356
+ if (entry["name"] !== "npm") continue;
357
+ const version = entry["version"];
358
+ if (version === void 0) specs.push({
359
+ range: "*",
360
+ exact: null,
361
+ hash: null
362
+ });
363
+ else if (typeof version === "string") specs.push(parseVersionSpec(version, field));
364
+ else throw new SpecError(field, `${field}: version must be a string`);
365
+ }
366
+ } catch (error) {
367
+ if (error instanceof SpecError) error.onFail = onFail;
368
+ throw error;
369
+ }
370
+ if (specs.length === 0) return null;
371
+ return {
372
+ specs,
373
+ onFail,
374
+ field: "devEngines"
375
+ };
376
+ }
377
+ function parseOnFail(entry, field) {
378
+ const onFail = entry.onFail;
379
+ if (onFail === void 0) return "error";
380
+ if (typeof onFail !== "string" || !ON_FAIL.has(onFail)) throw new SpecError(field, `${field}: onFail must be ignore, warn, error or download`);
381
+ return onFail;
382
+ }
383
+ /** Whether a concrete version satisfies the spec's range, hash aside (spec RES-5 and RES-6). */
384
+ function matchesRange(version, spec) {
385
+ return semver.satisfies(version, spec.range);
386
+ }
387
+ //#endregion
388
+ //#region src/select.ts
389
+ function engineAccepts(candidate, nodeVersion) {
390
+ return candidate.enginesNode === null || semver.satisfies(nodeVersion, candidate.enginesNode, { includePrerelease: true });
391
+ }
392
+ function hashAccepts(candidate, spec) {
393
+ if (spec.hash === null) return candidate.keyid !== null;
394
+ if (spec.hash.algorithm === "sha512") return sriHex(candidate.integrity) === spec.hash.hex;
395
+ if (candidate.digests === void 0 || candidate.digests === null) return true;
396
+ return candidate.digests[spec.hash.algorithm] === spec.hash.hex;
397
+ }
398
+ /** The hex digest of a `sha512-<base64>` SRI string, or null for any other form. */
399
+ function sriHex(integrity) {
400
+ const match = /^sha512-([A-Za-z0-9+/=]+)$/.exec(integrity.trim());
401
+ return match === null ? null : Buffer.from(match[1], "base64").toString("hex");
402
+ }
403
+ /** Whether Node's own npm satisfies a spec without a hash (spec RES-5). */
404
+ function ownNpmSatisfies(ownVersion, specs) {
405
+ return specs.some((spec) => spec.hash === null && matchesRange(ownVersion, spec));
406
+ }
407
+ /**
408
+ * Picks the highest candidate matching any spec and the active Node (spec RES-6), cached first.
409
+ * Returns the reason when nothing fits, distinguishing an `engines.node` exclusion from no match at all.
410
+ */
411
+ function selectCandidate(specs, candidates, nodeVersion) {
412
+ let best = null;
413
+ let excluded = null;
414
+ for (const candidate of candidates) {
415
+ const matching = specs.filter((entry) => matchesRange(candidate.version, entry) && hashAccepts(candidate, entry));
416
+ if (matching.length === 0) continue;
417
+ if (!engineAccepts(candidate, nodeVersion)) {
418
+ if (excluded === null || semver.gt(candidate.version, excluded.version)) excluded = candidate;
419
+ continue;
420
+ }
421
+ if (best === null || semver.gt(candidate.version, best.candidate.version)) best = {
422
+ candidate,
423
+ hashes: matching.some((entry) => entry.hash === null) ? [] : matching.flatMap((entry) => entry.hash === null ? [] : [entry.hash])
424
+ };
425
+ }
426
+ if (best !== null) return {
427
+ kind: "selected",
428
+ selection: best
429
+ };
430
+ if (excluded !== null) return {
431
+ kind: "failed",
432
+ failure: {
433
+ kind: "engine",
434
+ version: excluded.version,
435
+ enginesNode: excluded.enginesNode ?? "*"
436
+ }
437
+ };
438
+ return {
439
+ kind: "failed",
440
+ failure: {
441
+ kind: "nomatch",
442
+ reason: "no published version matches"
443
+ }
444
+ };
445
+ }
446
+ //#endregion
447
+ //#region src/install/verify.ts
448
+ /** Every digest a spec or the registry may ask for, in one pass over the file. */
449
+ async function fileDigests(file) {
450
+ const sha512 = createHash("sha512");
451
+ const sha256 = createHash("sha256");
452
+ const sha224 = createHash("sha224");
453
+ await pipeline(createReadStream(file), new Transform({ transform(chunk, _encoding, callback) {
454
+ sha512.update(chunk);
455
+ sha256.update(chunk);
456
+ sha224.update(chunk);
457
+ callback();
458
+ } }));
459
+ const sha512Digest = sha512.digest();
460
+ return {
461
+ sha512: `sha512-${sha512Digest.toString("base64")}`,
462
+ sha512hex: sha512Digest.toString("hex"),
463
+ sha256hex: sha256.digest("hex"),
464
+ sha224hex: sha224.digest("hex")
465
+ };
466
+ }
467
+ function integrityFailure(expected, actual, detail) {
468
+ return new NpmVmError("M-INTEGRITY", EXIT.verification, integrityMessage(expected, actual, detail));
469
+ }
470
+ /** The tarball must match the registry's integrity and, when the project pins hashes, one of them (spec SEC-2). */
471
+ function checkIntegrity(digests, integrity, hashes) {
472
+ if (digests.sha512 !== integrity.trim()) throw integrityFailure(integrity, digests.sha512, "the tarball does not match the registry integrity");
473
+ if (hashes.length === 0) return;
474
+ const actual = {
475
+ sha512: digests.sha512hex,
476
+ sha256: digests.sha256hex,
477
+ sha224: digests.sha224hex
478
+ };
479
+ if (!hashes.some((hash) => actual[hash.algorithm] === hash.hex)) {
480
+ const [first] = hashes;
481
+ throw integrityFailure(`${first.algorithm}.${first.hex}`, `${first.algorithm}.${actual[first.algorithm]}`, "the tarball does not match the pinned hash");
482
+ }
483
+ }
484
+ /**
485
+ * Checks the registry signatures over `name@version:integrity` against the pinned keyring (spec SEC-1).
486
+ * Returns the key id that verified; throws M-KEY for an unknown key and M-UNSIGNED when no signature is usable.
487
+ */
488
+ async function verifySignatures(input) {
489
+ if (input.signatures.length === 0) {
490
+ if (input.pinned) return null;
491
+ throw new NpmVmError("M-UNSIGNED", EXIT.verification, unsignedMessage(input.registry, input.version));
492
+ }
493
+ const payload = Buffer.from(`${input.name}@${input.version}:${input.integrity}`, "utf8");
494
+ let unknown = null;
495
+ for (const signature of input.signatures) {
496
+ const key = input.keys.find((entry) => entry.keyid === signature.keyid);
497
+ if (key === void 0) {
498
+ unknown ??= signature.keyid;
499
+ continue;
500
+ }
501
+ if (key.revoked === true || !await keyCurrent(key, input.publishedAt)) continue;
502
+ const publicKey = createPublicKey({
503
+ key: Buffer.from(key.key, "base64"),
504
+ format: "der",
505
+ type: "spki"
506
+ });
507
+ if (verify("sha256", payload, publicKey, Buffer.from(signature.sig, "base64"))) return key.keyid;
508
+ }
509
+ if (unknown !== null) throw new NpmVmError("M-KEY", EXIT.verification, keyMessage(unknown));
510
+ throw new NpmVmError("M-UNSIGNED", EXIT.verification, unsignedMessage(input.registry, input.version));
511
+ }
512
+ /** An expired key still vouches for versions published before it expired; an unknown time fails closed. */
513
+ async function keyCurrent(key, publishedAt) {
514
+ if (key.expires === null) return true;
515
+ const published = await publishedAt();
516
+ return published !== null && Date.parse(published) < Date.parse(key.expires);
517
+ }
518
+ const DEFAULT_LIMITS = {
519
+ maxEntries: 1e4,
520
+ maxBytes: 268435456
521
+ };
522
+ function unsafe(detail) {
523
+ return integrityFailure("a safe npm tarball", "an unsafe entry", detail);
524
+ }
525
+ /** The destination-relative path of a tarball entry, or a thrown M-INTEGRITY for anything outside `package/` (spec SEC-3). */
526
+ function entryRelativePath(path) {
527
+ if (path.includes("\\") || [...path].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127)) throw unsafe(`entry name contains a control character or backslash: ${JSON.stringify(path)}`);
528
+ if (path.startsWith("/") || /^[A-Za-z]:/.test(path)) throw unsafe(`absolute entry name: ${path}`);
529
+ const segments = path.split("/").filter((segment) => segment !== "" && segment !== ".");
530
+ if (segments.includes("..")) throw unsafe(`entry name climbs out of the archive: ${path}`);
531
+ if (segments[0] !== "package") throw unsafe(`entry outside package/: ${path}`);
532
+ return segments.length === 1 ? "" : posix.join(...segments.slice(1));
533
+ }
534
+ /**
535
+ * Extracts regular files and directories under `package/` into `dest`, rejecting everything else (spec SEC-3).
536
+ * Modes are `0644` or `0755` by the owner exec bit; ownership and extended attributes are ignored.
537
+ */
538
+ async function safeExtract(tarball, dest, limits = DEFAULT_LIMITS) {
539
+ let entries = 0;
540
+ let bytes = 0;
541
+ let decompressed = 0;
542
+ const writes = [];
543
+ const parser = new Parser({ strict: true });
544
+ let aborted = null;
545
+ const abort = (error) => {
546
+ aborted ??= error;
547
+ parser.abort(error);
548
+ };
549
+ parser.on("entry", (entry) => {
550
+ try {
551
+ entries += 1;
552
+ bytes += entry.size;
553
+ if (entries > limits.maxEntries) throw unsafe(`more than ${limits.maxEntries} entries`);
554
+ if (bytes > limits.maxBytes) throw unsafe(`more than ${limits.maxBytes} bytes`);
555
+ const relative = entryRelativePath(entry.path);
556
+ const target = relative === "" ? dest : join(dest, relative);
557
+ if (entry.type === "Directory") {
558
+ mkdirSync(target, {
559
+ recursive: true,
560
+ mode: 493
561
+ });
562
+ entry.resume();
563
+ return;
564
+ }
565
+ if (entry.type !== "File" || relative === "") throw unsafe(`${entry.type} entry: ${entry.path}`);
566
+ mkdirSync(dirname(target), {
567
+ recursive: true,
568
+ mode: 493
569
+ });
570
+ const mode = ((entry.mode ?? 420) & 64) === 0 ? 420 : 493;
571
+ writes.push(pipeline(entry, createWriteStream(target, { mode })));
572
+ } catch (error) {
573
+ abort(error);
574
+ }
575
+ });
576
+ const counter = new Transform({ transform(chunk, _encoding, callback) {
577
+ decompressed += chunk.length;
578
+ if (decompressed > limits.maxBytes) {
579
+ callback(unsafe(`more than ${limits.maxBytes} bytes after decompression`));
580
+ return;
581
+ }
582
+ callback(null, chunk);
583
+ } });
584
+ const done = new Promise((resolve, reject) => {
585
+ parser.on("end", () => resolve());
586
+ parser.on("error", (error) => reject(error));
587
+ });
588
+ done.catch(() => void 0);
589
+ try {
590
+ await pipeline(createReadStream(tarball), createGunzip(), counter, parser);
591
+ await done;
592
+ await Promise.all(writes);
593
+ } catch (error) {
594
+ await Promise.allSettled(writes);
595
+ throw aborted ?? (error instanceof NpmVmError ? error : unsafe(`cannot read the tarball: ${error.message}`));
596
+ }
597
+ if (aborted !== null) throw aborted;
598
+ }
599
+ const VERIFIED_FILE = ".verified";
600
+ function writeVerified(dir, verified) {
601
+ writeFileSync(join(dir, VERIFIED_FILE), JSON.stringify(verified), { mode: 384 });
602
+ }
603
+ function readVerified(dir) {
604
+ try {
605
+ const parsed = JSON.parse(readFileSync(join(dir, VERIFIED_FILE), "utf8"));
606
+ if (typeof parsed.version !== "string" || typeof parsed.integrity !== "string") return null;
607
+ if (typeof parsed.sha256 !== "string" || typeof parsed.sha224 !== "string") return null;
608
+ return {
609
+ version: parsed.version,
610
+ integrity: parsed.integrity,
611
+ keyid: typeof parsed.keyid === "string" ? parsed.keyid : null,
612
+ sha256: parsed.sha256,
613
+ sha224: parsed.sha224,
614
+ verifiedAt: typeof parsed.verifiedAt === "string" ? parsed.verifiedAt : ""
615
+ };
616
+ } catch {
617
+ return null;
618
+ }
619
+ }
620
+ /** `<version>-<16 hex of the integrity>`: two registries serving different bytes get two entries (spec SEC-4). */
621
+ function entryName(version, integrity) {
622
+ const hex = sriHex(integrity);
623
+ if (hex === null) throw new Error(`not a sha512 integrity: ${integrity}`);
624
+ return `${version}-${hex.slice(0, 16)}`;
625
+ }
626
+ /** Where verified entries are staged before the rename into `npm/`: same filesystem, so the rename stays atomic. */
627
+ function stagingDir(cacheRoot) {
628
+ return join(cacheRoot, "staging");
629
+ }
630
+ function canonicalEntry(cacheRoot, version, integrity) {
631
+ return join(cacheRoot, "npm", entryName(version, integrity));
632
+ }
633
+ function privateEntry(canonical, token) {
634
+ return `${canonical}.${token}`;
635
+ }
636
+ /** Whether a directory name under `npm/` is an entry rather than a lease dir, a gate or a deletion in progress. */
637
+ function isEntryName(name) {
638
+ return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?-[0-9a-f]{16}(?:\.[0-9a-f]{16})?$/.test(name);
639
+ }
640
+ function readManifest$2(dir) {
641
+ try {
642
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
643
+ const engines = pkg["engines"];
644
+ const node = typeof engines === "object" && engines !== null ? engines["node"] : void 0;
645
+ return {
646
+ name: pkg["name"],
647
+ version: pkg["version"],
648
+ enginesNode: typeof node === "string" ? node : null
649
+ };
650
+ } catch {
651
+ return null;
652
+ }
653
+ }
654
+ /**
655
+ * Whether an entry may run (spec SEC-5): verified for this integrity, owned by the current user,
656
+ * and holding the npm it claims. Never deletes anything.
657
+ */
658
+ function validateEntry(entryPath, integrity) {
659
+ try {
660
+ const stat = statSync(entryPath);
661
+ if (!stat.isDirectory() || process.getuid !== void 0 && stat.uid !== process.getuid()) return false;
662
+ } catch {
663
+ return false;
664
+ }
665
+ const verified = readVerified(entryPath);
666
+ if (verified === null || verified.integrity !== integrity) return false;
667
+ const manifest = readManifest$2(entryPath);
668
+ return manifest !== null && manifest.name === "npm" && manifest.version === verified.version;
669
+ }
670
+ /** Every verified entry in the cache as a candidate, whatever its validity; use-time validation decides. */
671
+ function listEntries(cacheRoot, keys = []) {
672
+ let names;
673
+ try {
674
+ names = readdirSync(join(cacheRoot, "npm"));
675
+ } catch {
676
+ return [];
677
+ }
678
+ const candidates = [];
679
+ for (const name of names) {
680
+ if (!isEntryName(name)) continue;
681
+ const entryPath = join(cacheRoot, "npm", name);
682
+ const verified = readVerified(entryPath);
683
+ const manifest = readManifest$2(entryPath);
684
+ if (verified === null || manifest === null || manifest.version !== verified.version) continue;
685
+ if (verified.keyid !== null && keys.some((key) => key.keyid === verified.keyid && key.revoked === true)) continue;
686
+ candidates.push({
687
+ version: verified.version,
688
+ enginesNode: manifest.enginesNode,
689
+ integrity: verified.integrity,
690
+ entryPath,
691
+ signatures: null,
692
+ keyid: verified.keyid,
693
+ digests: {
694
+ sha256: verified.sha256,
695
+ sha224: verified.sha224
696
+ }
697
+ });
698
+ }
699
+ return candidates;
700
+ }
701
+ function isCollision(error) {
702
+ const code = error.code;
703
+ return code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM" || code === "EISDIR";
704
+ }
705
+ /**
706
+ * Moves a verified temp dir into the cache under a lease (spec SEC-4). The lease exists before the
707
+ * rename, so a prune never sees the entry without it; a lost race reuses or sidesteps the winner's entry.
708
+ */
709
+ function publishEntry(tempDir, canonical, integrity, owner) {
710
+ const lease = createLease(canonical, owner);
711
+ if (existsSync(gatePath(canonical)) && ownerLive(gatePath(canonical))) {
712
+ lease.release();
713
+ return publishPrivately(tempDir, canonical, owner);
714
+ }
715
+ if (!leaseSurvived(lease)) return publishEntry(tempDir, canonical, integrity, owner);
716
+ try {
717
+ renameSync(tempDir, canonical);
718
+ return {
719
+ entryPath: canonical,
720
+ lease
721
+ };
722
+ } catch (error) {
723
+ if (!isCollision(error)) {
724
+ lease.release();
725
+ throw error;
726
+ }
727
+ }
728
+ if (validateEntry(canonical, integrity)) {
729
+ rmSync(tempDir, {
730
+ recursive: true,
731
+ force: true
732
+ });
733
+ return {
734
+ entryPath: canonical,
735
+ lease
736
+ };
737
+ }
738
+ lease.release();
739
+ return publishPrivately(tempDir, canonical, owner);
740
+ }
741
+ function publishPrivately(tempDir, canonical, owner) {
742
+ for (;;) {
743
+ const entryPath = privateEntry(canonical, newToken());
744
+ const privateLease = createLease(entryPath, owner);
745
+ if (existsSync(gatePath(entryPath)) && ownerLive(gatePath(entryPath))) {
746
+ privateLease.release();
747
+ continue;
748
+ }
749
+ if (!leaseSurvived(privateLease)) continue;
750
+ try {
751
+ renameSync(tempDir, entryPath);
752
+ } catch (error) {
753
+ privateLease.release();
754
+ throw error;
755
+ }
756
+ return {
757
+ entryPath,
758
+ lease: privateLease
759
+ };
760
+ }
761
+ }
762
+ //#endregion
763
+ //#region src/state/atomic.ts
764
+ const RETRY_CODES = /* @__PURE__ */ new Set([
765
+ "EBUSY",
766
+ "EPERM",
767
+ "EACCES"
768
+ ]);
769
+ const RETRY_LIMIT_MS = 5e3;
770
+ function sleepSync(ms) {
771
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
772
+ }
773
+ /**
774
+ * Runs a file operation, retrying Windows sharing violations with backoff for up to 5 s (spec MUT-3).
775
+ * Elsewhere the same codes are real permission errors and surface at once.
776
+ */
777
+ function withSharingRetry(operation, platform = process.platform, limitMs = RETRY_LIMIT_MS) {
778
+ const started = Date.now();
779
+ let delay = 20;
780
+ for (;;) try {
781
+ return operation();
782
+ } catch (error) {
783
+ const code = error.code ?? "";
784
+ if (platform !== "win32" || !RETRY_CODES.has(code) || Date.now() - started + delay > limitMs) throw error;
785
+ sleepSync(delay);
786
+ delay = Math.min(delay * 2, 500);
787
+ }
788
+ }
789
+ /** Writes `content` to `path` through a temp file in the same dir, fsynced and renamed into place. */
790
+ function atomicWrite(path, content, mode = 420) {
791
+ const dir = dirname(path);
792
+ mkdirSync(dir, { recursive: true });
793
+ const temp = join(dir, `.${newToken()}.tmp`);
794
+ try {
795
+ const fd = openSync(temp, "wx", mode);
796
+ try {
797
+ const buffer = typeof content === "string" ? Buffer.from(content, "utf8") : content;
798
+ for (let offset = 0; offset < buffer.length;) {
799
+ const written = writeSync(fd, buffer, offset, buffer.length - offset);
800
+ if (written <= 0) throw new Error(`short write to ${temp}: ${offset} of ${buffer.length} bytes`);
801
+ offset += written;
802
+ }
803
+ fsyncSync(fd);
804
+ } finally {
805
+ closeSync(fd);
806
+ }
807
+ withSharingRetry(() => renameSync(temp, path));
808
+ } catch (error) {
809
+ rmSync(temp, { force: true });
810
+ throw error;
811
+ }
812
+ }
813
+ /** Renames with the Windows retry; a cross-device rename is not retried, it is a layout error. */
814
+ function atomicRename(from, to) {
815
+ withSharingRetry(() => renameSync(from, to));
816
+ }
817
+ function removeWithRetry(path) {
818
+ withSharingRetry(() => rmSync(path, {
819
+ recursive: true,
820
+ force: true
821
+ }));
822
+ }
823
+ //#endregion
824
+ //#region src/state/prune.ts
825
+ const USED_SUFFIX = ".used";
826
+ /** Records a resolution of `entry` for the prune age, in a sidecar so the entry itself stays untouched. */
827
+ function touchUsed(entry) {
828
+ const marker = `${entry}${USED_SUFFIX}`;
829
+ try {
830
+ const now = /* @__PURE__ */ new Date();
831
+ utimesSync(marker, now, now);
832
+ } catch {
833
+ writeFileSync(marker, "", { flag: "a" });
834
+ }
835
+ }
836
+ function lastUsed(entry) {
837
+ try {
838
+ return statSync(`${entry}${USED_SUFFIX}`).mtimeMs;
839
+ } catch {
840
+ return null;
841
+ }
842
+ }
843
+ /** Why one entry would go, or null to keep it. */
844
+ function pruneReason(cacheRoot, name, options) {
845
+ const entry = join(cacheRoot, "npm", name);
846
+ if (name.includes(".deleting.")) return "leftover";
847
+ if (!isEntryName(name)) return null;
848
+ const verified = readVerified(entry);
849
+ if (verified === null || !validateEntry(entry, verified.integrity)) return "invalid";
850
+ if (/\.[0-9a-f]{16}$/.test(name) && !listLeases(entry).some((lease) => ownerLive(lease))) return "orphan-private";
851
+ const used = lastUsed(entry);
852
+ return (options.now ?? Date.now()) - (used ?? statSync(entry).mtimeMs) > options.maxAgeMs ? "stale" : null;
853
+ }
854
+ /**
855
+ * Removes one entry the exclusive way (spec LEASE-1): gate, drop dead leases, bail on a live one, rename aside, drop the gate, delete.
856
+ * After the gate is gone nothing here touches the entry's paths again.
857
+ */
858
+ function pruneEntry(entry, owner) {
859
+ if (existsSync(gatePath(entry)) && !ownerLive(gatePath(entry))) removeGate(entry);
860
+ if (!createGate(entry, owner)) return false;
861
+ for (const lease of listLeases(entry)) if (!ownerLive(lease)) rmSync(lease, { force: true });
862
+ if (listLeases(entry).length > 0) {
863
+ removeGate(entry);
864
+ return false;
865
+ }
866
+ const token = newToken();
867
+ const deletingEntry = `${entry}.deleting.${token}`;
868
+ const deletingLeases = `${entry}.leases.deleting.${token}`;
869
+ if (existsSync(entry)) atomicRename(entry, deletingEntry);
870
+ if (existsSync(leasesDir(entry))) atomicRename(leasesDir(entry), deletingLeases);
871
+ removeGate(entry);
872
+ removeWithRetry(deletingEntry);
873
+ removeWithRetry(deletingLeases);
874
+ rmSync(`${entry}${USED_SUFFIX}`, { force: true });
875
+ return true;
876
+ }
877
+ const STAGING_MAX_AGE_MS = 864e5;
878
+ /** Work dirs a crashed extraction left under `staging/`; a live one is never a day old. */
879
+ function pruneStaging(cacheRoot, now) {
880
+ let names;
881
+ try {
882
+ names = readdirSync(stagingDir(cacheRoot));
883
+ } catch {
884
+ return;
885
+ }
886
+ for (const name of names) {
887
+ const dir = join(stagingDir(cacheRoot), name);
888
+ try {
889
+ if (now - statSync(dir).mtimeMs > STAGING_MAX_AGE_MS) removeWithRetry(dir);
890
+ } catch {}
891
+ }
892
+ }
893
+ /** Prunes the whole cache; the caller holds the exclusive lock (MUT-1) so only one prune ever runs. */
894
+ function pruneCache(cacheRoot, owner, options) {
895
+ const report = {
896
+ removed: [],
897
+ kept: [],
898
+ skipped: []
899
+ };
900
+ pruneStaging(cacheRoot, options.now ?? Date.now());
901
+ let names;
902
+ try {
903
+ names = readdirSync(join(cacheRoot, "npm"));
904
+ } catch {
905
+ return report;
906
+ }
907
+ for (const name of names) {
908
+ const entry = join(cacheRoot, "npm", name);
909
+ if (name.endsWith(".leases") && existsSync(entry) && !existsSync(entry.slice(0, -7))) {
910
+ (pruneEntry(entry.slice(0, -7), owner) ? report.removed : report.skipped).push(name);
911
+ continue;
912
+ }
913
+ if (name.endsWith(".prune") && !existsSync(entry.slice(0, -6)) && !ownerLive(entry)) {
914
+ rmSync(entry, { force: true });
915
+ report.removed.push(name);
916
+ continue;
917
+ }
918
+ const reason = pruneReason(cacheRoot, name, options);
919
+ if (reason === null) {
920
+ if (isEntryName(name)) report.kept.push(name);
921
+ continue;
922
+ }
923
+ if (reason === "leftover") {
924
+ removeWithRetry(entry);
925
+ report.removed.push(name);
926
+ continue;
927
+ }
928
+ if (pruneEntry(entry, owner)) report.removed.push(name);
929
+ else report.skipped.push(name);
930
+ }
931
+ return report;
932
+ }
933
+ //#endregion
934
+ //#region src/setup/home.ts
935
+ function localAppData(env) {
936
+ return env["LOCALAPPDATA"] ?? join(homedir(), "AppData", "Local");
937
+ }
938
+ function defaultHome(env, platform = process.platform) {
939
+ if (platform === "win32") return join(localAppData(env), "npm-vm");
940
+ return join(env["XDG_DATA_HOME"] || join(homedir(), ".local", "share"), "npm-vm");
941
+ }
942
+ function cacheBase(env, platform = process.platform) {
943
+ if (platform === "win32") return join(localAppData(env), "npm-vm-cache");
944
+ return join(env["XDG_CACHE_HOME"] || join(homedir(), ".cache"), "npm-vm");
945
+ }
946
+ function platformId(platform, env) {
947
+ try {
948
+ if (platform === "linux") {
949
+ for (const file of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) try {
950
+ const id = readFileSync(file, "utf8").trim();
951
+ if (id !== "") return id;
952
+ } catch {}
953
+ return null;
954
+ }
955
+ if (platform === "darwin") {
956
+ const out = execFileSync("/usr/sbin/ioreg", [
957
+ "-rd1",
958
+ "-c",
959
+ "IOPlatformExpertDevice"
960
+ ], {
961
+ encoding: "utf8",
962
+ env: { PATH: "/usr/bin:/bin:/usr/sbin:/sbin" }
963
+ });
964
+ return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1] ?? null;
965
+ }
966
+ if (platform === "win32") {
967
+ const reg = join(env["SystemRoot"] ?? "C:\\Windows", "System32", "reg.exe");
968
+ const out = execFileSync(reg, [
969
+ "query",
970
+ "HKLM\\SOFTWARE\\Microsoft\\Cryptography",
971
+ "/v",
972
+ "MachineGuid"
973
+ ], { encoding: "utf8" });
974
+ return /MachineGuid\s+REG_SZ\s+(\S+)/.exec(out)?.[1] ?? null;
975
+ }
976
+ } catch {}
977
+ return null;
978
+ }
979
+ /** The hashed machine id, or a hashed hostname when the platform id is unreadable. */
980
+ function readMachineId(env = process.env, platform = process.platform) {
981
+ const raw = platformId(platform, env);
982
+ const source = raw ?? hostname();
983
+ return {
984
+ id: createHash("sha256").update(source).digest("hex").slice(0, 16),
985
+ fallback: raw === null
986
+ };
987
+ }
988
+ function resolveDirs(env = process.env, home = env["NPM_VM_HOME"] || defaultHome(env), platform = process.platform, machine = readMachineId(env, platform)) {
989
+ const absoluteHome = resolve(home);
990
+ const state = join(absoluteHome, "state");
991
+ return {
992
+ home: absoluteHome,
993
+ cache: join(cacheBase(env, platform), machine.id),
994
+ state,
995
+ tmp: join(state, "tmp"),
996
+ journal: join(state, "journal"),
997
+ machine
998
+ };
999
+ }
1000
+ //#endregion
1001
+ //#region src/install/config.ts
1002
+ const DROPPED = /* @__PURE__ */ new Set(["NODE_OPTIONS", "NODE_EXTRA_CA_CERTS"]);
1003
+ /** Proxy settings reach the transport only from the user and global npm config (spec DL-2), never the env. */
1004
+ const PROXY_VARS = /^(https?_proxy|all_proxy|no_proxy|proxy)$/i;
1005
+ /** The env without anything that would steer npm or Node: `npm_config_*`, proxies, `NODE_OPTIONS`, `NODE_EXTRA_CA_CERTS`. */
1006
+ function sanitizeEnv(env) {
1007
+ const clean = {};
1008
+ for (const [key, value] of Object.entries(env)) {
1009
+ if (/^npm_config_/i.test(key) || DROPPED.has(key.toUpperCase()) || PROXY_VARS.test(key)) continue;
1010
+ clean[key] = value;
1011
+ }
1012
+ return clean;
1013
+ }
1014
+ function optionalString(value) {
1015
+ const text = Array.isArray(value) ? value.map(String).filter((item) => item !== "").join(",") : value;
1016
+ return typeof text === "string" && text !== "" ? text : null;
1017
+ }
1018
+ /**
1019
+ * Loads npm's config the way npm does, minus the project: `cwd` is an empty dir under the home,
1020
+ * the env is sanitized and nothing is printed. Credentials come only from `getCredentialsByURI`.
1021
+ */
1022
+ async function loadNpmConfig(own, env, cwd, nodePath = process.execPath) {
1023
+ const { definitions, shorthands, flatten, nerfDarts } = npmDefinitions;
1024
+ const config = new Config({
1025
+ definitions,
1026
+ shorthands,
1027
+ flatten,
1028
+ nerfDarts,
1029
+ npmPath: own.root,
1030
+ argv: [
1031
+ nodePath,
1032
+ own.cliPath,
1033
+ "--location=global"
1034
+ ],
1035
+ execPath: nodePath,
1036
+ env: { ...sanitizeEnv(env) },
1037
+ cwd,
1038
+ excludeNpmCwd: true
1039
+ });
1040
+ await config.load();
1041
+ const registry = String(config.get("registry"));
1042
+ const ca = config.get("ca");
1043
+ return {
1044
+ registry,
1045
+ userconfig: String(config.get("userconfig")),
1046
+ globalconfig: String(config.get("globalconfig")),
1047
+ cache: String(config.get("cache")),
1048
+ credentials: config.getCredentialsByURI(registry),
1049
+ httpsProxy: optionalString(config.get("https-proxy")),
1050
+ proxy: optionalString(config.get("proxy")),
1051
+ noproxy: optionalString(config.get("noproxy")),
1052
+ ca: Array.isArray(ca) ? ca.map(String) : typeof ca === "string" ? [ca] : null,
1053
+ cafile: optionalString(config.get("cafile")),
1054
+ strictSsl: config.get("strict-ssl") !== false
1055
+ };
1056
+ }
1057
+ //#endregion
1058
+ //#region src/install/fetch.ts
1059
+ const MIN_TRANSPORT = "8.0.0";
1060
+ const MAX_OUTPUT = 268435456;
1061
+ /** Whether Node's own npm can serve as the transport (spec DL-1). */
1062
+ function transportUsable(own) {
1063
+ return own !== null && semver.gte(own.version, MIN_TRANSPORT);
1064
+ }
1065
+ /** A fresh private working dir under `state/tmp/`, never the shared system temp dir (spec DL-2). */
1066
+ function newWorkDir(tmpRoot) {
1067
+ mkdirSync(tmpRoot, {
1068
+ recursive: true,
1069
+ mode: 448
1070
+ });
1071
+ return mkdtempSync(join(tmpRoot, "work-"));
1072
+ }
1073
+ /**
1074
+ * Runs Node's own npm as the download transport, isolated from the project (spec DL-1, DL-2).
1075
+ * Every call runs in its own empty working dir, with that dir as the prefix and the user and global config named explicitly.
1076
+ */
1077
+ var Transport = class {
1078
+ #own;
1079
+ #config;
1080
+ #env;
1081
+ #tmpRoot;
1082
+ #nodePath;
1083
+ constructor(own, config, env, tmpRoot, nodePath = process.execPath) {
1084
+ this.#own = own;
1085
+ this.#config = config;
1086
+ this.#env = sanitizeEnv(env);
1087
+ this.#tmpRoot = tmpRoot;
1088
+ this.#nodePath = nodePath;
1089
+ }
1090
+ /** Versions of npm matching `range`, newest last as npm lists them; an unmatched range is an empty list. */
1091
+ view(range) {
1092
+ return this.viewPackage("npm", range);
1093
+ }
1094
+ /** The same for any package, used by self-update for its own package. */
1095
+ async viewPackage(name, range) {
1096
+ const output = await this.#run([
1097
+ "view",
1098
+ `${name}@${range}`,
1099
+ "version",
1100
+ "engines",
1101
+ "dist"
1102
+ ], true);
1103
+ if (output === null) return [];
1104
+ return asList(output).flatMap((item) => {
1105
+ const version = readString(item, "version");
1106
+ const dist = readObject(item, "dist");
1107
+ const integrity = dist === null ? null : readString(dist, "integrity");
1108
+ const tarball = dist === null ? null : readString(dist, "tarball");
1109
+ if (version === null || integrity === null || tarball === null) return [];
1110
+ const engines = readObject(item, "engines");
1111
+ const signatures = (dist === null ? null : dist["signatures"]) ?? [];
1112
+ const attestations = dist === null ? null : readObject(dist, "attestations");
1113
+ return [{
1114
+ version,
1115
+ enginesNode: engines === null ? null : readString(engines, "node"),
1116
+ integrity,
1117
+ tarball,
1118
+ signatures: Array.isArray(signatures) ? signatures.filter(isSignature) : [],
1119
+ attestationsUrl: attestations === null ? null : readString(attestations, "url")
1120
+ }];
1121
+ });
1122
+ }
1123
+ /** Publication time per version, used only to judge signatures by expired keys. */
1124
+ async times() {
1125
+ const output = await this.#run([
1126
+ "view",
1127
+ "npm",
1128
+ "time"
1129
+ ], true);
1130
+ const map = output === null ? null : asList(output)[0] ?? null;
1131
+ const times = {};
1132
+ if (map !== null && typeof map === "object") {
1133
+ for (const [version, time] of Object.entries(map)) if (typeof time === "string") times[version] = time;
1134
+ }
1135
+ return times;
1136
+ }
1137
+ /** Downloads one npm version's tarball into `dest` through `npm pack`. */
1138
+ pack(version, dest) {
1139
+ return this.packPackage("npm", version, dest);
1140
+ }
1141
+ async packPackage(name, version, dest) {
1142
+ const item = asList(await this.#run([
1143
+ "pack",
1144
+ `${name}@${version}`,
1145
+ `--pack-destination=${dest}`
1146
+ ], false)).flatMap((item) => readString(item, "filename") === null ? asList(Object.values(item)) : [item]).find((candidate) => readString(candidate, "filename") !== null) ?? null;
1147
+ const filename = item === null ? null : readString(item, "filename");
1148
+ const integrity = item === null ? null : readString(item, "integrity");
1149
+ if (filename === null || integrity === null) throw this.#failure(`npm pack returned no tarball for ${name}@${version}`);
1150
+ return {
1151
+ file: join(dest, filename),
1152
+ integrity
1153
+ };
1154
+ }
1155
+ #failure(detail) {
1156
+ return new NpmVmError("M-TRANSPORT", EXIT.transport, transportMessage(this.#own.version, detail));
1157
+ }
1158
+ /** Runs npm with the isolation flags; returns parsed JSON, or null for a 404 when `missingIsEmpty`. */
1159
+ async #run(args, missingIsEmpty) {
1160
+ const cwd = newWorkDir(this.#tmpRoot);
1161
+ const argv = [
1162
+ this.#own.cliPath,
1163
+ ...args,
1164
+ "--json",
1165
+ "--ignore-scripts",
1166
+ "--loglevel=error",
1167
+ "--replace-registry-host=always",
1168
+ ...proxyFlags(this.#config),
1169
+ "--fetch-retry-mintimeout=1000",
1170
+ "--fetch-retry-maxtimeout=10000",
1171
+ `--prefix=${cwd}`,
1172
+ `--cache=${this.#config.cache}`,
1173
+ `--userconfig=${this.#config.userconfig}`,
1174
+ `--globalconfig=${this.#config.globalconfig}`
1175
+ ];
1176
+ try {
1177
+ const { stdout: output } = await new Promise((resolve, reject) => {
1178
+ execFile(this.#nodePath, argv, {
1179
+ cwd,
1180
+ env: this.#env,
1181
+ maxBuffer: MAX_OUTPUT,
1182
+ timeout: 3e5,
1183
+ windowsHide: true
1184
+ }, (error, stdout, stderr) => {
1185
+ if (error === null) {
1186
+ resolve({ stdout });
1187
+ return;
1188
+ }
1189
+ reject(Object.assign(error, {
1190
+ stdout,
1191
+ stderr
1192
+ }));
1193
+ });
1194
+ });
1195
+ return parseJson(output);
1196
+ } catch (error) {
1197
+ const stderr = String(error.stderr ?? "");
1198
+ if (missingIsEmpty && /\bE404\b/.test(stderr)) return null;
1199
+ const line = (stderr.split("\n").find((text) => /^npm (ERR!|error)/.test(text)) ?? error.message).replace(/^npm (ERR!|error) /, "").trim();
1200
+ const redacted = redactUrls(line);
1201
+ if (/\bEINTEGRITY\b/.test(stderr)) throw new NpmVmError("M-INTEGRITY", EXIT.verification, integrityMessage("the registry integrity", "other bytes", redacted));
1202
+ throw this.#failure(redacted);
1203
+ } finally {
1204
+ rmSync(cwd, {
1205
+ recursive: true,
1206
+ force: true
1207
+ });
1208
+ }
1209
+ }
1210
+ };
1211
+ /** Proxy settings as flags, since the env the transport sees carries none (spec DL-2). */
1212
+ function proxyFlags(config) {
1213
+ const flags = [];
1214
+ if (config.httpsProxy !== null) flags.push(`--https-proxy=${config.httpsProxy}`);
1215
+ if (config.proxy !== null) flags.push(`--proxy=${config.proxy}`);
1216
+ if (config.noproxy !== null) flags.push(`--noproxy=${config.noproxy}`);
1217
+ return flags;
1218
+ }
1219
+ function parseJson(text) {
1220
+ const trimmed = text.trim();
1221
+ if (trimmed === "") return null;
1222
+ return JSON.parse(trimmed);
1223
+ }
1224
+ function asList(value) {
1225
+ return (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "object" && item !== null);
1226
+ }
1227
+ function readString(record, key) {
1228
+ const value = record[key];
1229
+ return typeof value === "string" ? value : null;
1230
+ }
1231
+ function readObject(record, key) {
1232
+ const value = record[key];
1233
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1234
+ }
1235
+ function isSignature(value) {
1236
+ return typeof value === "object" && value !== null && typeof value.keyid === "string" && typeof value.sig === "string";
1237
+ }
1238
+ //#endregion
1239
+ //#region src/install/keys.ts
1240
+ const REGISTRY_KEYS = [{
1241
+ keyid: "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA",
1242
+ key: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==",
1243
+ expires: "2025-01-29T00:00:00.000Z"
1244
+ }, {
1245
+ keyid: "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U",
1246
+ key: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEY6Ya7W++7aUPzvMTrezH6Ycx3c+HOKYCcNGybJZSCJq/fd7Qa8uuAKtdIkUQtQiEKERhAmE5lMMJhP8OkDOa2g==",
1247
+ expires: null
1248
+ }];
1249
+ const EXTRA_KEYS_FILE = "keys.json";
1250
+ /**
1251
+ * Keys added beside the built-in ones from `<home>/state/keys.json`, for registries that sign with their own keys.
1252
+ * Anything able to write there could replace the runtime itself, so this adds no trust the home does not already hold.
1253
+ */
1254
+ function loadKeyring(stateDir) {
1255
+ let extra;
1256
+ try {
1257
+ extra = JSON.parse(readFileSync(join(stateDir, EXTRA_KEYS_FILE), "utf8"));
1258
+ } catch {
1259
+ return [...REGISTRY_KEYS];
1260
+ }
1261
+ const keys = Array.isArray(extra) ? extra : [];
1262
+ return [...REGISTRY_KEYS, ...keys.filter((key) => typeof key === "object" && key !== null && typeof key.keyid === "string" && typeof key.key === "string" && (key.expires === null || typeof key.expires === "string"))];
1263
+ }
1264
+ //#endregion
1265
+ //#region src/install/index.ts
1266
+ function createContext(env = process.env, home, node = {
1267
+ execPath: process.execPath,
1268
+ version: process.version
1269
+ }) {
1270
+ const dirs = resolveDirs(env, home);
1271
+ return {
1272
+ dirs,
1273
+ own: findOwnNpm(node.execPath),
1274
+ env,
1275
+ nodeVersion: node.version,
1276
+ nodePath: node.execPath,
1277
+ keys: loadKeyring(dirs.state)
1278
+ };
1279
+ }
1280
+ function toCandidate(version) {
1281
+ return {
1282
+ version: version.version,
1283
+ enginesNode: version.enginesNode,
1284
+ integrity: version.integrity,
1285
+ entryPath: null,
1286
+ signatures: version.signatures
1287
+ };
1288
+ }
1289
+ /** Lazily loads the npm config and builds the transport once per process. */
1290
+ var TransportFactory = class {
1291
+ #context;
1292
+ #ready = null;
1293
+ constructor(context) {
1294
+ this.#context = context;
1295
+ }
1296
+ get() {
1297
+ this.#ready ??= this.#create();
1298
+ return this.#ready;
1299
+ }
1300
+ async #create() {
1301
+ const { own, env, dirs } = this.#context;
1302
+ const found = own?.version ?? null;
1303
+ if (!transportUsable(own)) throw new NpmVmError("M-TRANSPORT", EXIT.transport, transportMessage(found, "too old or missing"));
1304
+ const cwd = newWorkDir(dirs.tmp);
1305
+ try {
1306
+ const config = await loadNpmConfig(own, env, cwd, this.#context.nodePath);
1307
+ return {
1308
+ transport: new Transport(own, config, env, dirs.tmp, this.#context.nodePath),
1309
+ config
1310
+ };
1311
+ } finally {
1312
+ rmSync(cwd, {
1313
+ recursive: true,
1314
+ force: true
1315
+ });
1316
+ }
1317
+ }
1318
+ };
1319
+ function createInstaller(context) {
1320
+ const factory = new TransportFactory(context);
1321
+ async function registryVersions(specs) {
1322
+ const { transport } = await factory.get();
1323
+ const byVersion = /* @__PURE__ */ new Map();
1324
+ for (const spec of specs) for (const version of await transport.view(spec.exact ?? spec.range)) byVersion.set(version.version, version);
1325
+ return [...byVersion.values()];
1326
+ }
1327
+ async function installTarball(selection, source, onPacked) {
1328
+ const { candidate } = selection;
1329
+ const owner = ownerRecord(context.dirs.machine.id);
1330
+ if (source.published.version !== candidate.version || source.published.integrity !== candidate.integrity) throw new NpmVmError("M-INTEGRITY", EXIT.verification, integrityMessage(candidate.integrity, source.published.integrity, `the registry no longer serves npm ${candidate.version} with this integrity`));
1331
+ const digests = await fileDigests(source.file);
1332
+ checkIntegrity(digests, candidate.integrity, selection.hashes);
1333
+ const keyid = await verifySignatures({
1334
+ name: "npm",
1335
+ version: candidate.version,
1336
+ integrity: candidate.integrity,
1337
+ signatures: source.published.signatures,
1338
+ publishedAt: source.publishedAt,
1339
+ keys: context.keys,
1340
+ registry: source.registry,
1341
+ pinned: selection.hashes.length > 0
1342
+ });
1343
+ if (onPacked !== void 0) await onPacked(source, digests);
1344
+ const work = newWorkDir(stagingDir(context.dirs.cache));
1345
+ try {
1346
+ const extracted = join(work, "package");
1347
+ mkdirSync(extracted, { mode: 448 });
1348
+ await safeExtract(source.file, extracted);
1349
+ checkExtracted(extracted, candidate, context.nodeVersion);
1350
+ writeVerified(extracted, {
1351
+ version: candidate.version,
1352
+ integrity: candidate.integrity,
1353
+ keyid,
1354
+ sha256: digests.sha256hex,
1355
+ sha224: digests.sha224hex,
1356
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
1357
+ });
1358
+ const canonical = canonicalEntry(context.dirs.cache, candidate.version, candidate.integrity);
1359
+ mkdirSync(join(canonical, ".."), {
1360
+ recursive: true,
1361
+ mode: 448
1362
+ });
1363
+ const published = publishEntry(extracted, canonical, candidate.integrity, owner);
1364
+ touchUsed(published.entryPath);
1365
+ return published;
1366
+ } finally {
1367
+ rmSync(work, {
1368
+ recursive: true,
1369
+ force: true
1370
+ });
1371
+ }
1372
+ }
1373
+ return {
1374
+ nodeVersion: context.nodeVersion,
1375
+ ownNpm: () => context.own,
1376
+ cached: async () => listEntries(context.dirs.cache, context.keys),
1377
+ registry: async (specs) => (await registryVersions(specs)).map(toCandidate),
1378
+ entryPathFor: (candidate) => canonicalEntry(context.dirs.cache, candidate.version, candidate.integrity),
1379
+ async registryIdentity() {
1380
+ const { config } = await factory.get();
1381
+ return createHash("sha256").update(config.registry).digest("hex").slice(0, 12);
1382
+ },
1383
+ installTarball: (selection, source) => installTarball(selection, source),
1384
+ async ensure(selection, options = {}) {
1385
+ const { candidate } = selection;
1386
+ const owner = ownerRecord(context.dirs.machine.id);
1387
+ if (candidate.entryPath !== null && validateEntry(candidate.entryPath, candidate.integrity)) {
1388
+ const lease = await acquireShared(candidate.entryPath, owner);
1389
+ if (lease !== null && validateEntry(candidate.entryPath, candidate.integrity)) {
1390
+ touchUsed(candidate.entryPath);
1391
+ return {
1392
+ entryPath: candidate.entryPath,
1393
+ lease
1394
+ };
1395
+ }
1396
+ lease?.release();
1397
+ }
1398
+ if (options.offline ?? (context.env["NPM_VM_OFFLINE"] ?? "") !== "") throw new NpmVmError("M-NOMATCH", EXIT.resolution, `npm ${candidate.version} is not usable from the cache and NPM_VM_OFFLINE forbids the registry.`);
1399
+ const { transport, config } = await factory.get();
1400
+ const published = (await transport.view(candidate.version)).find((item) => item.version === candidate.version);
1401
+ if (published === void 0) throw new NpmVmError("M-INTEGRITY", EXIT.verification, integrityMessage(candidate.integrity, "nothing", `the registry no longer serves npm ${candidate.version}`));
1402
+ const work = newWorkDir(context.dirs.tmp);
1403
+ try {
1404
+ return await installTarball(selection, {
1405
+ file: (await transport.pack(candidate.version, work)).file,
1406
+ published,
1407
+ publishedAt: async () => (await transport.times())[candidate.version] ?? null,
1408
+ registry: config.registry
1409
+ }, options.onPacked);
1410
+ } finally {
1411
+ rmSync(work, {
1412
+ recursive: true,
1413
+ force: true
1414
+ });
1415
+ }
1416
+ }
1417
+ };
1418
+ }
1419
+ /** The extracted manifest is authoritative for name, version and engines (spec SEC-5). */
1420
+ function checkExtracted(dir, candidate, nodeVersion) {
1421
+ let pkg;
1422
+ try {
1423
+ pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
1424
+ } catch (error) {
1425
+ throw new NpmVmError("M-INTEGRITY", EXIT.verification, integrityMessage("npm package.json", "unreadable", error.message));
1426
+ }
1427
+ if (pkg["name"] !== "npm" || pkg["version"] !== candidate.version) throw new NpmVmError("M-INTEGRITY", EXIT.verification, integrityMessage(`npm@${candidate.version}`, `${String(pkg["name"])}@${String(pkg["version"])}`, "the tarball holds a different package"));
1428
+ const engines = pkg["engines"];
1429
+ const node = typeof engines === "object" && engines !== null ? engines["node"] : void 0;
1430
+ if (typeof node === "string" && !semver.satisfies(nodeVersion, node, { includePrerelease: true })) throw new NpmVmError("M-ENGINE", EXIT.resolution, engineMessage(candidate.version, node, nodeVersion));
1431
+ }
1432
+ //#endregion
1433
+ //#region src/project.ts
1434
+ const BOOLEANS = /* @__PURE__ */ new Set(["global", "workspaces"]);
1435
+ const SHORTHANDS = {
1436
+ C: "prefix",
1437
+ g: "global",
1438
+ L: "location",
1439
+ ws: "workspaces"
1440
+ };
1441
+ /**
1442
+ * The subset of nopt that decides the prefix: `--flag value`, `--flag=value`, `--no-flag`, `--flag true|false` for booleans, npm's shorthands and `--`.
1443
+ * Later flags win, as in nopt.
1444
+ */
1445
+ function parsePrefixFlags(argv, cwd) {
1446
+ const flags = {
1447
+ prefix: null,
1448
+ global: null,
1449
+ location: null,
1450
+ workspaces: null
1451
+ };
1452
+ for (let i = 0; i < argv.length; i += 1) {
1453
+ const arg = argv[i];
1454
+ if (arg === "--") break;
1455
+ let name;
1456
+ let value = null;
1457
+ if (arg.startsWith("--")) {
1458
+ const eq = arg.indexOf("=");
1459
+ name = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
1460
+ value = eq === -1 ? null : arg.slice(eq + 1);
1461
+ name = SHORTHANDS[name] ?? name;
1462
+ } else if (arg.startsWith("-") && arg.length > 1 && SHORTHANDS[arg.slice(1)] !== void 0) name = SHORTHANDS[arg.slice(1)];
1463
+ else continue;
1464
+ let negated = false;
1465
+ if (name.startsWith("no-") && BOOLEANS.has(name.slice(3))) {
1466
+ negated = true;
1467
+ name = name.slice(3);
1468
+ }
1469
+ if (BOOLEANS.has(name)) {
1470
+ let bool = !negated;
1471
+ if (value !== null) bool = value !== "false";
1472
+ else if (!negated && (argv[i + 1] === "true" || argv[i + 1] === "false")) {
1473
+ bool = argv[i + 1] === "true";
1474
+ i += 1;
1475
+ }
1476
+ if (name === "global") flags.global = bool;
1477
+ else flags.workspaces = bool;
1478
+ continue;
1479
+ }
1480
+ if (name !== "prefix" && name !== "location") continue;
1481
+ if (value === null) {
1482
+ if (i + 1 >= argv.length) continue;
1483
+ value = argv[i + 1];
1484
+ i += 1;
1485
+ }
1486
+ if (name === "prefix") flags.prefix = resolve(cwd, value);
1487
+ else flags.location = value;
1488
+ }
1489
+ return flags;
1490
+ }
1491
+ /** Global mode from the command line and the npm env, CLI winning (spec RES-3). */
1492
+ function isGlobalMode(flags, env) {
1493
+ const fromEnv = env["npm_config_global"];
1494
+ const global = flags.global ?? (fromEnv === void 0 || fromEnv === "" ? null : ![
1495
+ "false",
1496
+ "0",
1497
+ "null"
1498
+ ].includes(fromEnv));
1499
+ const location = flags.location ?? env["npm_config_location"] ?? null;
1500
+ return global === true || location === "global";
1501
+ }
1502
+ function hasPackageJson(dir) {
1503
+ try {
1504
+ return statSync(join(dir, "package.json")).isFile();
1505
+ } catch {
1506
+ return false;
1507
+ }
1508
+ }
1509
+ function hasNodeModules(dir) {
1510
+ try {
1511
+ return statSync(join(dir, "node_modules")).isDirectory();
1512
+ } catch {
1513
+ return false;
1514
+ }
1515
+ }
1516
+ function* walkUp(start) {
1517
+ let dir = resolve(start);
1518
+ for (;;) {
1519
+ yield dir;
1520
+ const parent = dirname(dir);
1521
+ if (parent === dir) return;
1522
+ dir = parent;
1523
+ }
1524
+ }
1525
+ /** The `workspaces` patterns of a manifest: the array form or `{ packages: [] }`, else none. */
1526
+ function workspacePatterns(pkg) {
1527
+ if (typeof pkg !== "object" || pkg === null) return [];
1528
+ let raw = pkg.workspaces;
1529
+ if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) raw = raw.packages;
1530
+ if (!Array.isArray(raw)) return [];
1531
+ return raw.filter((entry) => typeof entry === "string");
1532
+ }
1533
+ /**
1534
+ * Whether `candidate` is one of the workspaces `root` declares, matched like `@npmcli/map-workspaces`.
1535
+ * A pattern selects directories holding a `package.json`, `!pattern` removes matches, and `node_modules` never matches.
1536
+ */
1537
+ function isWorkspaceOf(root, candidate, patterns) {
1538
+ const rel = relative(root, candidate);
1539
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel) || !hasPackageJson(candidate)) return false;
1540
+ if (rel.split(sep).includes("node_modules")) return false;
1541
+ const posix = rel.split(sep).join("/");
1542
+ const { positive, negative } = splitPatterns(patterns);
1543
+ return positive.some((pattern) => matchesGlob(posix, pattern)) && !negative.some((pattern) => matchesGlob(posix, pattern));
1544
+ }
1545
+ function splitPatterns(patterns) {
1546
+ const positive = [];
1547
+ const negative = [];
1548
+ for (const raw of patterns) {
1549
+ const bangs = /^!+/.exec(raw)?.[0].length ?? 0;
1550
+ const pattern = normalizePattern(raw.slice(bangs));
1551
+ if (bangs % 2 === 1) {
1552
+ negative.push(pattern);
1553
+ continue;
1554
+ }
1555
+ for (let i = negative.length - 1; i >= 0; i -= 1) if (matchesGlob(pattern, negative[i])) negative.splice(i, 1);
1556
+ positive.push(pattern);
1557
+ }
1558
+ return {
1559
+ positive,
1560
+ negative
1561
+ };
1562
+ }
1563
+ function normalizePattern(pattern) {
1564
+ return pattern.replace(/\\/g, "/").replace(/^\.?\/+/, "").replace(/\/+$/, "");
1565
+ }
1566
+ function readManifest$1(dir) {
1567
+ try {
1568
+ return JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
1569
+ } catch {
1570
+ return null;
1571
+ }
1572
+ }
1573
+ /**
1574
+ * Mirrors `@npmcli/config` `loadLocalPrefix` (spec RES-2): a CLI prefix is final.
1575
+ * Otherwise the nearest ancestor with `package.json` or `node_modules` wins, promoted to a parent whose `workspaces` include it unless `--no-workspaces` or global mode.
1576
+ */
1577
+ function findProject(argv, cwd, env = process.env) {
1578
+ const flags = parsePrefixFlags(argv, cwd);
1579
+ const global = isGlobalMode(flags, env);
1580
+ if (flags.prefix !== null) return {
1581
+ root: flags.prefix,
1582
+ global,
1583
+ cliPrefix: flags.prefix
1584
+ };
1585
+ let localPrefix = null;
1586
+ for (const dir of walkUp(cwd)) {
1587
+ const manifest = hasPackageJson(dir);
1588
+ if (localPrefix === null) {
1589
+ if (manifest || hasNodeModules(dir)) {
1590
+ localPrefix = dir;
1591
+ if (flags.workspaces === false || global) break;
1592
+ }
1593
+ continue;
1594
+ }
1595
+ if (!manifest) continue;
1596
+ const pkg = readManifest$1(dir);
1597
+ if (pkg !== null && isWorkspaceOf(dir, localPrefix, workspacePatterns(pkg))) {
1598
+ localPrefix = dir;
1599
+ break;
1600
+ }
1601
+ }
1602
+ return {
1603
+ root: localPrefix ?? resolve(cwd),
1604
+ global,
1605
+ cliPrefix: null
1606
+ };
1607
+ }
1608
+ //#endregion
1609
+ //#region src/index.ts
1610
+ /** Resolves the npm to run for `argv` in `cwd` (spec section 5); writes only under the cache, spawns npm only as the transport. */
1611
+ function resolveNpm(argv, cwd, options = {}) {
1612
+ return resolveNpmWith(argv, cwd, options, createInstaller(createContext(options.env ?? process.env, options.home)));
1613
+ }
1614
+ async function resolveNpmWith(argv, cwd, options, providers) {
1615
+ const env = options.env ?? process.env;
1616
+ const own = providers.ownNpm();
1617
+ const ownResult = (projectRoot, source, warning) => {
1618
+ if (own === null) throw new NpmVmError("M-TRANSPORT", EXIT.transport, "No npm found beside the active Node; reinstall Node.");
1619
+ return {
1620
+ kind: "own",
1621
+ version: own.version,
1622
+ cliPath: own.cliPath,
1623
+ source,
1624
+ projectRoot,
1625
+ selection: null,
1626
+ specs: null,
1627
+ warning
1628
+ };
1629
+ };
1630
+ if (nonEmpty(env["NPM_VM_DISABLE"])) return ownResult(null, null, null);
1631
+ const project = findProject(argv, cwd, env);
1632
+ if (project.global) return ownResult(project.root, null, null);
1633
+ let parsed;
1634
+ let source;
1635
+ if (nonEmpty(env["NPM_VM_VERSION"])) {
1636
+ parsed = {
1637
+ specs: [parseVersionSpec(env["NPM_VM_VERSION"], "NPM_VM_VERSION")],
1638
+ onFail: "error",
1639
+ field: "devEngines"
1640
+ };
1641
+ source = {
1642
+ file: "NPM_VM_VERSION",
1643
+ field: "env"
1644
+ };
1645
+ } else {
1646
+ const file = join(project.root, "package.json");
1647
+ let read;
1648
+ try {
1649
+ read = readSpecs(file);
1650
+ } catch (error) {
1651
+ if (!(error instanceof SpecError)) throw error;
1652
+ const message = `${file}: ${error.message}`;
1653
+ const failure = new NpmVmError("M-NOMATCH", EXIT.resolution, message);
1654
+ return applyPolicy(error.onFail, failure, () => ownResult(project.root, null, message));
1655
+ }
1656
+ if (read === null) return ownResult(project.root, null, null);
1657
+ parsed = read;
1658
+ source = {
1659
+ file,
1660
+ field: read.field
1661
+ };
1662
+ }
1663
+ if (own !== null && ownNpmSatisfies(own.version, parsed.specs)) return ownResult(project.root, source, null);
1664
+ const cached = await providers.cached(parsed.specs);
1665
+ let result = selectCandidate(parsed.specs, cached, providers.nodeVersion);
1666
+ if (result.kind === "failed" && !options.offline && !nonEmpty(env["NPM_VM_OFFLINE"])) {
1667
+ let fromRegistry;
1668
+ try {
1669
+ fromRegistry = await providers.registry(parsed.specs);
1670
+ } catch (error) {
1671
+ if (!(error instanceof NpmVmError) || error.id !== "M-TRANSPORT") throw error;
1672
+ return applyPolicy(parsed.onFail, error, () => ownResult(project.root, source, error.message));
1673
+ }
1674
+ result = selectCandidate(parsed.specs, fromRegistry, providers.nodeVersion);
1675
+ }
1676
+ if (result.kind === "selected") {
1677
+ const { candidate } = result.selection;
1678
+ const entry = candidate.entryPath ?? providers.entryPathFor(candidate);
1679
+ return {
1680
+ kind: candidate.entryPath === null ? "download" : "cached",
1681
+ version: candidate.version,
1682
+ cliPath: join(entry, "bin", "npm-cli.js"),
1683
+ source,
1684
+ projectRoot: project.root,
1685
+ selection: result.selection,
1686
+ specs: parsed.specs,
1687
+ warning: null
1688
+ };
1689
+ }
1690
+ const message = result.failure.kind === "engine" ? engineMessage(result.failure.version, result.failure.enginesNode, providers.nodeVersion) : noMatchMessage(parsed.specs, source.file, result.failure.reason);
1691
+ const id = result.failure.kind === "engine" ? "M-ENGINE" : "M-NOMATCH";
1692
+ const failure = new NpmVmError(id, EXIT.resolution, message);
1693
+ return applyPolicy(parsed.onFail, failure, () => ownResult(project.root, source, message));
1694
+ }
1695
+ /** `onFail` makes a failed resolution fatal, a warning before Node's own npm, or silent (spec RES-7). */
1696
+ function applyPolicy(onFail, failure, fallback) {
1697
+ if (onFail === "ignore") return {
1698
+ ...fallback(),
1699
+ warning: null
1700
+ };
1701
+ if (onFail === "warn") return fallback();
1702
+ throw failure;
1703
+ }
1704
+ /** Reads and validates the project's specs; a broken file or range follows its own `onFail` when readable, else errors. */
1705
+ function readSpecs(file) {
1706
+ let text;
1707
+ try {
1708
+ text = readFileSync(file, "utf8");
1709
+ } catch {
1710
+ return null;
1711
+ }
1712
+ let pkg;
1713
+ try {
1714
+ pkg = JSON.parse(text);
1715
+ } catch (error) {
1716
+ throw new NpmVmError("M-NOMATCH", EXIT.resolution, `${file} is not valid JSON: ${error.message}`);
1717
+ }
1718
+ return parseSpecs(pkg);
1719
+ }
1720
+ function nonEmpty(value) {
1721
+ return value !== void 0 && value !== "";
1722
+ }
1723
+ const SHIM_DIR = "shims";
1724
+ const LAUNCHERS_DIR = "launchers";
1725
+ const SELECTORS = [
1726
+ "npm",
1727
+ "npx",
1728
+ "npm-vm"
1729
+ ];
1730
+ const UNSAFE = /[\n\r"'`$%!]/;
1731
+ /** Refuses a home no quoting makes safe in all four shells and cmd (spec SET-2, M-HOMEPATH). */
1732
+ function assertSafeHome(home) {
1733
+ const match = UNSAFE.exec(home);
1734
+ if (match !== null) {
1735
+ const shown = match[0] === "\n" || match[0] === "\r" ? "a line break" : `"${match[0]}"`;
1736
+ throw new NpmVmError("M-HOMEPATH", EXIT.usage, `The npm-vm home ${home} contains ${shown}, which cannot be quoted safely in every shell. Set NPM_VM_HOME to a plain path and run setup again.`);
1737
+ }
1738
+ }
1739
+ function selectorOf(name) {
1740
+ return name === "npm-vm" ? "cli" : name;
1741
+ }
1742
+ /**
1743
+ * The launcher templates shipped beside this code: `src/launchers/` from source, `dist/runtime/launchers/`
1744
+ * from the bin, and `launchers/` beside an installed `runtime.cjs`.
1745
+ */
1746
+ function templatesDir() {
1747
+ const here = import.meta.dirname;
1748
+ for (const candidate of [
1749
+ join(here, LAUNCHERS_DIR),
1750
+ join(here, "..", LAUNCHERS_DIR),
1751
+ join(here, "runtime", LAUNCHERS_DIR)
1752
+ ]) if (existsSync(join(candidate, "sh.tmpl"))) return candidate;
1753
+ throw new Error(`npm-vm launcher templates not found near ${here}`);
1754
+ }
1755
+ function readTemplate(dir, kind) {
1756
+ const text = readFileSync(join(dir, `${kind}.tmpl`), "utf8");
1757
+ return kind === "sh" ? text.replace(/\r\n/g, "\n") : text;
1758
+ }
1759
+ /** Fills one launcher template; the ABI comes from the launcher generation the templates belong to. */
1760
+ function renderTemplate(template, values) {
1761
+ return template.replace(/\{\{ABI\}\}/g, String(values.abi)).replace(/\{\{BOOTSTRAP\}\}/g, values.bootstrap).replace(/\{\{SELECTOR\}\}/g, values.selector);
1762
+ }
1763
+ /** The POSIX shim: `sh` on every OS, so Git Bash and MSYS on Windows get one too. */
1764
+ function posixShim(home, name, templates = templatesDir(), abi = 1, platform = process.platform) {
1765
+ const { join: joinPath } = platform === "win32" ? win32 : posix;
1766
+ return renderTemplate(readTemplate(templates, "sh"), {
1767
+ bootstrap: joinPath(home, "bootstrap.cjs"),
1768
+ selector: selectorOf(name),
1769
+ abi
1770
+ });
1771
+ }
1772
+ /**
1773
+ * The cmd shim resolves node.exe through PATH only (`%~$PATH:i`): cmd would otherwise run a `node.exe`
1774
+ * committed to the current directory. One command line, so `%*` keeps its quoting.
1775
+ */
1776
+ function cmdShim(home, name, templates = templatesDir(), abi = 1) {
1777
+ return renderTemplate(readTemplate(templates, "cmd"), {
1778
+ bootstrap: win32.join(home, "bootstrap.cjs"),
1779
+ selector: selectorOf(name),
1780
+ abi
1781
+ });
1782
+ }
1783
+ /** PowerShell never searches the current directory for a command, so `Get-Command` is enough. */
1784
+ function powershellShim(home, name, templates = templatesDir(), abi = 1) {
1785
+ return renderTemplate(readTemplate(templates, "ps1"), {
1786
+ bootstrap: win32.join(home, "bootstrap.cjs"),
1787
+ selector: selectorOf(name),
1788
+ abi
1789
+ });
1790
+ }
1791
+ /** Every shim file for the platform (spec SET-2); `.ps1` files are included only when the policy allows them (WIN-2). */
1792
+ function renderShims(home, platform, options = {}) {
1793
+ assertSafeHome(home);
1794
+ const templates = options.templates ?? templatesDir();
1795
+ const abi = options.abi ?? 1;
1796
+ const files = [];
1797
+ for (const name of SELECTORS) {
1798
+ files.push({
1799
+ name,
1800
+ content: posixShim(home, name, templates, abi, platform),
1801
+ mode: 493
1802
+ });
1803
+ if (platform === "win32") {
1804
+ files.push({
1805
+ name: `${name}.cmd`,
1806
+ content: cmdShim(home, name, templates, abi),
1807
+ mode: 420
1808
+ });
1809
+ if (options.powershell !== false) files.push({
1810
+ name: `${name}.ps1`,
1811
+ content: powershellShim(home, name, templates, abi),
1812
+ mode: 420
1813
+ });
1814
+ }
1815
+ }
1816
+ return files;
1817
+ }
1818
+ /** The ABI a shim or bootstrap file declares, or null when the file is not ours. */
1819
+ function shimAbi(content) {
1820
+ const match = /npm-vm launcher abi=(\d+)|LAUNCHER_ABI = (\d+)/.exec(content);
1821
+ return match === null ? null : Number(match[1] ?? match[2]);
1822
+ }
1823
+ function shimDir(home) {
1824
+ return join(home, SHIM_DIR);
1825
+ }
1826
+ /** Our `.ps1` shims left from a permissive policy; PowerShell would still prefer them over `.cmd` (WIN-2). */
1827
+ function stalePowershellShims(home) {
1828
+ return SELECTORS.map((name) => join(shimDir(home), `${name}.ps1`)).filter((file) => {
1829
+ try {
1830
+ return shimAbi(readFileSync(file, "utf8")) !== null;
1831
+ } catch {
1832
+ return false;
1833
+ }
1834
+ });
1835
+ }
1836
+ //#endregion
1837
+ //#region src/setup/shell.ts
1838
+ const SHELLS = [
1839
+ "zsh",
1840
+ "bash",
1841
+ "fish",
1842
+ "powershell"
1843
+ ];
1844
+ function isShell(value) {
1845
+ return SHELLS.includes(value);
1846
+ }
1847
+ const BLOCK_START = "# >>> npm-vm >>>";
1848
+ const BLOCK_END = "# <<< npm-vm <<<";
1849
+ /** Escapes a path for use inside a bash or zsh `${var//pattern/}` pattern, where `/` and glob characters are special. */
1850
+ function patternOf(path) {
1851
+ return path.replace(/[\\/*?[\]]/g, (char) => `\\${char}`);
1852
+ }
1853
+ /** Adds `shims/` to the front of PATH when absent; sourcing it twice changes nothing (spec SH-1). */
1854
+ function shimsFor(shell, home) {
1855
+ if (shell === "powershell") return win32.join(home, SHIM_DIR);
1856
+ return process.platform === "win32" ? shimDir(home).replace(/\\/g, "/") : shimDir(home);
1857
+ }
1858
+ function pathSnippet(shell, home) {
1859
+ assertSafeHome(home);
1860
+ const shims = shimsFor(shell, home);
1861
+ switch (shell) {
1862
+ case "zsh":
1863
+ case "bash": return [
1864
+ `case ":$PATH:" in`,
1865
+ ` *":${shims}:"*) ;;`,
1866
+ ` *) export PATH="${shims}:$PATH" ;;`,
1867
+ "esac",
1868
+ ""
1869
+ ].join("\n");
1870
+ case "fish": return [
1871
+ `if not contains -- "${shims}" $PATH`,
1872
+ ` set -gx PATH "${shims}" $PATH`,
1873
+ "end",
1874
+ ""
1875
+ ].join("\n");
1876
+ case "powershell": return [
1877
+ `if (($env:Path -split [IO.Path]::PathSeparator) -notcontains '${shims}') {`,
1878
+ ` $env:Path = '${shims}' + [IO.Path]::PathSeparator + $env:Path`,
1879
+ "}",
1880
+ ""
1881
+ ].join("\n");
1882
+ }
1883
+ }
1884
+ /**
1885
+ * The prompt hook (spec SH-2): moves `shims/` back to the front when something prepended over it,
1886
+ * with parameter expansion only, keeping `$?` and chaining whatever hook was there.
1887
+ */
1888
+ function hookSnippet(shell, home) {
1889
+ assertSafeHome(home);
1890
+ const shims = shimsFor(shell, home);
1891
+ switch (shell) {
1892
+ case "zsh": return [
1893
+ "_npm_vm_guard() {",
1894
+ " local ret=$?",
1895
+ ` if [[ "$path[1]" != "${shims}" ]]; then`,
1896
+ ` path=("${shims}" \${path:#${shims}})`,
1897
+ " fi",
1898
+ " return $ret",
1899
+ "}",
1900
+ "if [[ -z \"${precmd_functions[(r)_npm_vm_guard]}\" ]]; then",
1901
+ " precmd_functions+=(_npm_vm_guard)",
1902
+ "fi",
1903
+ ""
1904
+ ].join("\n");
1905
+ case "bash": return [
1906
+ "_npm_vm_guard() {",
1907
+ " local ret=$?",
1908
+ ` case ":$PATH:" in`,
1909
+ ` ":${shims}:"*) ;;`,
1910
+ " *)",
1911
+ " local rest=\":$PATH:\"",
1912
+ ` rest="\${rest//:${patternOf(shims)}:/:}"`,
1913
+ " rest=\"${rest#:}\"",
1914
+ " rest=\"${rest%:}\"",
1915
+ ` PATH="${shims}\${rest:+:$rest}"`,
1916
+ " ;;",
1917
+ " esac",
1918
+ " return $ret",
1919
+ "}",
1920
+ "case \";${PROMPT_COMMAND};\" in",
1921
+ " *\";_npm_vm_guard;\"*) ;;",
1922
+ " *) PROMPT_COMMAND=\"_npm_vm_guard${PROMPT_COMMAND:+;$PROMPT_COMMAND}\" ;;",
1923
+ "esac",
1924
+ ""
1925
+ ].join("\n");
1926
+ case "fish": return [
1927
+ "if status is-interactive",
1928
+ " function _npm_vm_guard --on-event fish_prompt",
1929
+ ` if test "$PATH[1]" != "${shims}"`,
1930
+ ` set -l found (contains -i -- "${shims}" $PATH)`,
1931
+ " if test -n \"$found\"",
1932
+ " set -e PATH[$found]",
1933
+ " end",
1934
+ ` set -gx PATH "${shims}" $PATH`,
1935
+ " end",
1936
+ " end",
1937
+ "end",
1938
+ ""
1939
+ ].join("\n");
1940
+ case "powershell": return [
1941
+ "if (-not (Test-Path Function:\\global:_npm_vm_original_prompt)) {",
1942
+ " $prev = Get-Item Function:\\prompt -ErrorAction SilentlyContinue",
1943
+ " if ($prev) { Set-Item Function:\\global:_npm_vm_original_prompt $prev.ScriptBlock }",
1944
+ " else { function global:_npm_vm_original_prompt { \"PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) \" } }",
1945
+ " function global:prompt {",
1946
+ " $ok = $?",
1947
+ " $exit = $global:LASTEXITCODE",
1948
+ ` $shims = '${shims}'`,
1949
+ " $parts = @($env:Path -split [IO.Path]::PathSeparator)",
1950
+ " if ($parts.Length -eq 0 -or $parts[0] -ne $shims) {",
1951
+ " $env:Path = (@($shims) + @($parts | Where-Object { $_ -ne $shims })) -join [IO.Path]::PathSeparator",
1952
+ " }",
1953
+ " $out = & $function:global:_npm_vm_original_prompt",
1954
+ " $global:LASTEXITCODE = $exit",
1955
+ " $out",
1956
+ " if (-not $ok) { Write-Error 'npm-vm: preserving $?' -ErrorAction SilentlyContinue }",
1957
+ " }",
1958
+ "}",
1959
+ ""
1960
+ ].join("\n");
1961
+ }
1962
+ }
1963
+ /** Both snippets inside the marked block `setup` writes and `uninstall` removes (spec SH-4). */
1964
+ function profileBlock(shell, home, parts) {
1965
+ const body = [parts.path ? pathSnippet(shell, home) : "", parts.hook ? hookSnippet(shell, home) : ""].filter((text) => text !== "").join("");
1966
+ return `${BLOCK_START}\n${body}${BLOCK_END}\n`;
1967
+ }
1968
+ //#endregion
1969
+ //#region src/state/meta.ts
1970
+ function sha256(content) {
1971
+ return createHash("sha256").update(content).digest("hex");
1972
+ }
1973
+ function isUtf8(content) {
1974
+ return !content.includes(0) && Buffer.from(content.toString("utf8"), "utf8").equals(content);
1975
+ }
1976
+ function lineEndingsOf(text) {
1977
+ const crlf = (text.match(/\r\n/g) ?? []).length;
1978
+ const lf = (text.match(/\n/g) ?? []).length - crlf;
1979
+ if (crlf === 0 && lf === 0) return "none";
1980
+ if (crlf > 0 && lf > 0) return "mixed";
1981
+ return crlf > 0 ? "crlf" : "lf";
1982
+ }
1983
+ const MISSING = {
1984
+ type: "missing",
1985
+ mode: 0,
1986
+ sha256: null,
1987
+ encoding: null,
1988
+ lineEndings: null,
1989
+ symlinkTarget: null
1990
+ };
1991
+ /** Captures `path` without following symlinks; a missing path is a valid state, not an error. */
1992
+ function captureMeta(path) {
1993
+ let stat;
1994
+ try {
1995
+ stat = lstatSync(path);
1996
+ } catch {
1997
+ return MISSING;
1998
+ }
1999
+ const mode = process.platform === "win32" ? 0 : stat.mode & 4095;
2000
+ if (stat.isSymbolicLink()) return {
2001
+ type: "symlink",
2002
+ mode,
2003
+ sha256: null,
2004
+ encoding: null,
2005
+ lineEndings: null,
2006
+ symlinkTarget: readlinkSync(path)
2007
+ };
2008
+ if (stat.isDirectory()) return {
2009
+ type: "directory",
2010
+ mode,
2011
+ sha256: null,
2012
+ encoding: null,
2013
+ lineEndings: null,
2014
+ symlinkTarget: null
2015
+ };
2016
+ if (!stat.isFile()) return {
2017
+ type: "other",
2018
+ mode,
2019
+ sha256: null,
2020
+ encoding: null,
2021
+ lineEndings: null,
2022
+ symlinkTarget: null
2023
+ };
2024
+ const content = readFileSync(path);
2025
+ const utf8 = isUtf8(content);
2026
+ return {
2027
+ type: "file",
2028
+ mode,
2029
+ sha256: sha256(content),
2030
+ encoding: utf8 ? "utf8" : "binary",
2031
+ lineEndings: utf8 ? lineEndingsOf(content.toString("utf8")) : null,
2032
+ symlinkTarget: null
2033
+ };
2034
+ }
2035
+ /** Whether the path still holds exactly what `meta` recorded (spec MUT-5): same type, same bytes or link target. */
2036
+ function metaMatches(path, meta) {
2037
+ const now = captureMeta(path);
2038
+ return now.type === meta.type && now.sha256 === meta.sha256 && now.symlinkTarget === meta.symlinkTarget;
2039
+ }
2040
+ //#endregion
2041
+ //#region src/state/journal.ts
2042
+ function journalPath(journalDir, id) {
2043
+ return join(journalDir, `${id}.json`);
2044
+ }
2045
+ function readJournal(file) {
2046
+ try {
2047
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
2048
+ return typeof parsed.id === "string" && typeof parsed.target === "string" ? parsed : null;
2049
+ } catch {
2050
+ return null;
2051
+ }
2052
+ }
2053
+ function listJournals(journalDir) {
2054
+ let names;
2055
+ try {
2056
+ names = readdirSync(journalDir);
2057
+ } catch {
2058
+ return [];
2059
+ }
2060
+ return names.filter((name) => name.endsWith(".json")).map((name) => readJournal(join(journalDir, name))).filter((journal) => journal !== null);
2061
+ }
2062
+ /**
2063
+ * Applies one file change under a journal: backup and staged copy first, the journal, then the write.
2064
+ * The journal and its sidecars disappear only after the target holds the intended bytes.
2065
+ */
2066
+ function mutateFile(journalDir, kind, owner, target, content, mode = 420) {
2067
+ mkdirSync(journalDir, {
2068
+ recursive: true,
2069
+ mode: 448
2070
+ });
2071
+ const id = `${kind}-${newToken()}`;
2072
+ const original = captureMeta(target);
2073
+ let backup = null;
2074
+ if (original.type === "file") {
2075
+ backup = join(journalDir, `${id}.bak`);
2076
+ copyFileSync(target, backup);
2077
+ }
2078
+ let staged = null;
2079
+ if (content !== null) {
2080
+ staged = join(journalDir, `${id}.new`);
2081
+ atomicWrite(staged, content, 384);
2082
+ }
2083
+ const journal = {
2084
+ id,
2085
+ kind,
2086
+ owner,
2087
+ target,
2088
+ original,
2089
+ intended: content === null ? null : sha256(content),
2090
+ staged,
2091
+ backup,
2092
+ mode,
2093
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2094
+ };
2095
+ atomicWrite(journalPath(journalDir, id), JSON.stringify(journal), 384);
2096
+ apply(journal, content);
2097
+ discard(journalDir, journal);
2098
+ }
2099
+ function apply(journal, content) {
2100
+ if (content === null) {
2101
+ withSharingRetry(() => {
2102
+ if (existsSync(journal.target)) unlinkSync(journal.target);
2103
+ });
2104
+ return;
2105
+ }
2106
+ atomicWrite(journal.target, content, journal.mode);
2107
+ }
2108
+ function discard(journalDir, journal) {
2109
+ for (const sidecar of [journal.backup, journal.staged]) if (sidecar !== null) rmSync(sidecar, { force: true });
2110
+ rmSync(journalPath(journalDir, journal.id), { force: true });
2111
+ }
2112
+ function targetSha(target) {
2113
+ const meta = captureMeta(target);
2114
+ return meta.type === "file" ? meta.sha256 : null;
2115
+ }
2116
+ /**
2117
+ * Finishes one journal (spec MUT-2): a target already at the intended bytes is done; one still at the
2118
+ * original is replayed from the staged copy; anything else is left alone and reported as changed (MUT-5).
2119
+ */
2120
+ function recoverJournal(journalDir, journal, report) {
2121
+ const current = targetSha(journal.target);
2122
+ const exists = captureMeta(journal.target).type !== "missing";
2123
+ if (journal.intended === null ? !exists : current === journal.intended) {
2124
+ discard(journalDir, journal);
2125
+ report.finished.push(journal.id);
2126
+ return;
2127
+ }
2128
+ if (metaMatches(journal.target, journal.original)) {
2129
+ if (journal.intended === null) {
2130
+ apply(journal, null);
2131
+ report.replayed.push(journal.id);
2132
+ } else if (journal.staged !== null && existsSync(journal.staged) && sha256(readFileSync(journal.staged)) === journal.intended) {
2133
+ apply(journal, readFileSync(journal.staged));
2134
+ report.replayed.push(journal.id);
2135
+ } else report.rolledBack.push(journal.id);
2136
+ discard(journalDir, journal);
2137
+ return;
2138
+ }
2139
+ report.changed.push(journal.target);
2140
+ discard(journalDir, journal);
2141
+ }
2142
+ /** Recovers every journal whose owner is dead on this machine; foreign and live ones are skipped. */
2143
+ function recoverDeadJournals(journalDir, machineId, kinds) {
2144
+ const report = {
2145
+ finished: [],
2146
+ replayed: [],
2147
+ rolledBack: [],
2148
+ changed: [],
2149
+ skipped: []
2150
+ };
2151
+ for (const journal of listJournals(journalDir)) {
2152
+ if (kinds !== void 0 && !kinds.includes(journal.kind)) continue;
2153
+ if (journal.owner.machineId !== machineId || isLive(journal.owner)) {
2154
+ report.skipped.push(journal.id);
2155
+ continue;
2156
+ }
2157
+ recoverJournal(journalDir, journal, report);
2158
+ }
2159
+ return report;
2160
+ }
2161
+ /** Recovers one journal the operator vouched for, whatever machine owns it (the `recover` command). */
2162
+ function recoverForeignJournal(journalDir, id, confirmedMachineId) {
2163
+ const report = {
2164
+ finished: [],
2165
+ replayed: [],
2166
+ rolledBack: [],
2167
+ changed: [],
2168
+ skipped: []
2169
+ };
2170
+ const journal = readJournal(journalPath(journalDir, id));
2171
+ if (journal === null) throw new Error(`no journal ${id} in ${journalDir}`);
2172
+ if (journal.owner.machineId !== confirmedMachineId) throw new Error(`journal ${id} belongs to machine ${journal.owner.machineId}, not ${confirmedMachineId}`);
2173
+ recoverJournal(journalDir, journal, report);
2174
+ return report;
2175
+ }
2176
+ //#endregion
2177
+ //#region src/state/lock.ts
2178
+ const LOCK_FILE = "lock";
2179
+ const WAIT_LIMIT_MS = 1e4;
2180
+ function lockPath(stateDir) {
2181
+ return join(stateDir, LOCK_FILE);
2182
+ }
2183
+ function readLock(stateDir) {
2184
+ try {
2185
+ const parsed = JSON.parse(readFileSync(lockPath(stateDir), "utf8"));
2186
+ if (typeof parsed.pid !== "number" || typeof parsed.token !== "string") return null;
2187
+ return {
2188
+ pid: parsed.pid,
2189
+ start: typeof parsed.start === "string" ? parsed.start : null,
2190
+ host: typeof parsed.host === "string" ? parsed.host : "?",
2191
+ machineId: typeof parsed.machineId === "string" ? parsed.machineId : "?",
2192
+ createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : "",
2193
+ token: parsed.token
2194
+ };
2195
+ } catch {
2196
+ return null;
2197
+ }
2198
+ }
2199
+ /** Whether the lock owner can be shown to be dead: only on this machine, and only by PROC-1. */
2200
+ function lockOwnerLive(record, machineId) {
2201
+ if (record === null) return true;
2202
+ if (record.machineId !== machineId) return true;
2203
+ return isLive(record);
2204
+ }
2205
+ function tryCreate(path, record) {
2206
+ try {
2207
+ writeFileSync(path, JSON.stringify(record), {
2208
+ flag: "wx",
2209
+ mode: 384
2210
+ });
2211
+ } catch (error) {
2212
+ if (error.code === "EEXIST") return false;
2213
+ rmSync(path, { force: true });
2214
+ throw error;
2215
+ }
2216
+ return true;
2217
+ }
2218
+ function sleep(ms) {
2219
+ return new Promise((resolve) => setTimeout(resolve, ms));
2220
+ }
2221
+ /**
2222
+ * Takes the exclusive lock (spec MUT-1): created with O_EXCL, released only by its owner, never reclaimed.
2223
+ * Waits with backoff for up to 10 s, then fails with M-LOCKED naming the file and whether the owner is live.
2224
+ */
2225
+ async function acquireLock(stateDir, owner, options = {}) {
2226
+ mkdirSync(stateDir, {
2227
+ recursive: true,
2228
+ mode: 448
2229
+ });
2230
+ const path = lockPath(stateDir);
2231
+ const record = {
2232
+ ...owner,
2233
+ token: newToken()
2234
+ };
2235
+ const deadline = Date.now() + (options.waitMs ?? WAIT_LIMIT_MS);
2236
+ let delay = 50;
2237
+ for (;;) {
2238
+ if (tryCreate(path, record)) return {
2239
+ path,
2240
+ release() {
2241
+ if (readLock(stateDir)?.token === record.token) rmSync(path, { force: true });
2242
+ }
2243
+ };
2244
+ if (Date.now() + delay > deadline) {
2245
+ const holder = readLock(stateDir);
2246
+ const foreign = holder !== null && holder.machineId !== owner.machineId;
2247
+ const live = lockOwnerLive(holder, owner.machineId);
2248
+ throw new NpmVmError("M-LOCKED", EXIT.problems, lockedMessage(path, holder, live, foreign));
2249
+ }
2250
+ await sleep(delay);
2251
+ delay = Math.min(delay * 2, 1e3);
2252
+ }
2253
+ }
2254
+ //#endregion
2255
+ //#region src/commands/cache.ts
2256
+ const DAY_MS = 864e5;
2257
+ /** `install [<spec>]`: fetches and verifies the project's npm, or the given spec, into the cache. */
2258
+ async function install(context, spec, cwd = process.cwd()) {
2259
+ const env = spec === void 0 ? context.env : {
2260
+ ...context.env,
2261
+ NPM_VM_VERSION: spec
2262
+ };
2263
+ const installer = createInstaller(createContext(env, context.dirs.home));
2264
+ const result = await resolveNpmWith([], cwd, { env }, installer);
2265
+ if (result.selection === null) {
2266
+ context.io.out(context.json ? JSON.stringify(result) : `Node's own npm ${result.version} satisfies the requirement; nothing to download.`);
2267
+ return EXIT.ok;
2268
+ }
2269
+ const ensured = await installer.ensure(result.selection);
2270
+ ensured.lease.release();
2271
+ context.io.out(context.json ? JSON.stringify({
2272
+ ...result,
2273
+ entryPath: ensured.entryPath
2274
+ }) : `npm ${result.version} is ready in ${ensured.entryPath}`);
2275
+ return EXIT.ok;
2276
+ }
2277
+ function cacheList(context) {
2278
+ const entries = listEntries(context.dirs.cache).map((entry) => {
2279
+ const used = lastUsed(entry.entryPath);
2280
+ return {
2281
+ version: entry.version,
2282
+ integrity: entry.integrity,
2283
+ path: entry.entryPath,
2284
+ lastUsed: used === null ? null : new Date(used).toISOString(),
2285
+ liveLeases: listLeases(entry.entryPath).filter((lease) => ownerLive(lease)).length,
2286
+ verifiedAt: readVerified(entry.entryPath)?.verifiedAt ?? ""
2287
+ };
2288
+ });
2289
+ if (context.json) context.io.out(JSON.stringify({
2290
+ cache: context.dirs.cache,
2291
+ entries
2292
+ }, null, 2));
2293
+ else {
2294
+ const lines = [`${context.dirs.cache}`];
2295
+ for (const entry of entries) lines.push(` npm ${entry.version} ${entry.path} last used ${entry.lastUsed ?? "never"}${entry.liveLeases > 0 ? ` in use by ${entry.liveLeases}` : ""}`);
2296
+ if (entries.length === 0) lines.push(" (empty)");
2297
+ context.io.out(lines.join("\n"));
2298
+ }
2299
+ return EXIT.ok;
2300
+ }
2301
+ async function cachePrune(context, olderThanDays = 90) {
2302
+ if (!Number.isFinite(olderThanDays) || olderThanDays < 0) throw new NpmVmError("M-NOMATCH", EXIT.usage, "--older-than needs a non-negative number of days.");
2303
+ const lock = await acquireLock(context.dirs.state, context.owner);
2304
+ try {
2305
+ recoverDeadJournals(context.dirs.journal, context.dirs.machine.id);
2306
+ const cacheLock = await acquireLock(context.dirs.cache, context.owner);
2307
+ let report;
2308
+ try {
2309
+ report = pruneCache(context.dirs.cache, context.owner, { maxAgeMs: olderThanDays * DAY_MS });
2310
+ } finally {
2311
+ cacheLock.release();
2312
+ }
2313
+ context.io.out(context.json ? JSON.stringify(report, null, 2) : `Removed ${report.removed.length}, kept ${report.kept.length}, skipped ${report.skipped.length} (in use).`);
2314
+ } finally {
2315
+ lock.release();
2316
+ }
2317
+ return EXIT.ok;
2318
+ }
2319
+ /** `recover --journal <id> --confirm-dead-machine <machine-id>` (spec PROC-1, CLI). */
2320
+ async function recover(context, journalId, confirmedMachine) {
2321
+ const journal = listJournals(context.dirs.journal).find((entry) => entry.id === journalId);
2322
+ if (journal === void 0) throw new NpmVmError("M-NOMATCH", EXIT.usage, `No journal ${journalId} under ${context.dirs.journal}.`);
2323
+ if (journal.owner.machineId !== confirmedMachine) throw new NpmVmError("M-NOMATCH", EXIT.usage, `Journal ${journalId} belongs to machine ${journal.owner.machineId}, not ${confirmedMachine}.`);
2324
+ if (journal.owner.machineId === context.dirs.machine.id && isLive(journal.owner)) throw new NpmVmError("M-LOCKED", EXIT.problems, `Journal ${journalId} belongs to PID ${journal.owner.pid} on this machine, which is still running.`);
2325
+ const lock = await acquireLock(context.dirs.state, context.owner);
2326
+ try {
2327
+ const report = recoverForeignJournal(context.dirs.journal, journalId, confirmedMachine);
2328
+ context.io.out(context.json ? JSON.stringify(report) : `Recovered ${journalId}: ${describeReport(report)}`);
2329
+ } finally {
2330
+ lock.release();
2331
+ }
2332
+ return EXIT.ok;
2333
+ }
2334
+ function describeReport(report) {
2335
+ if (report.replayed.length > 0) return "replayed";
2336
+ if (report.rolledBack.length > 0) return "rolled back";
2337
+ if (report.changed.length > 0) return "target changed since, left untouched";
2338
+ return "already finished";
2339
+ }
2340
+ //#endregion
2341
+ //#region src/commands/context.ts
2342
+ function terminalIo() {
2343
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
2344
+ return {
2345
+ out: (text) => process.stdout.write(`${text}\n`),
2346
+ err: (text) => process.stderr.write(`${text}\n`),
2347
+ interactive,
2348
+ async confirm(question) {
2349
+ if (!interactive) return false;
2350
+ const rl = createInterface({
2351
+ input: process.stdin,
2352
+ output: process.stdout
2353
+ });
2354
+ try {
2355
+ const answer = await rl.question(`${question} [y/N] `);
2356
+ return /^y(es)?$/i.test(answer.trim());
2357
+ } finally {
2358
+ rl.close();
2359
+ }
2360
+ }
2361
+ };
2362
+ }
2363
+ function createCommandContext(options) {
2364
+ const env = options.env ?? process.env;
2365
+ const dirs = resolveDirs(env, options.home);
2366
+ return {
2367
+ dirs,
2368
+ env,
2369
+ platform: process.platform,
2370
+ owner: ownerRecord(dirs.machine.id),
2371
+ io: options.io ?? terminalIo(),
2372
+ json: options.json ?? false,
2373
+ version: options.version
2374
+ };
2375
+ }
2376
+ //#endregion
2377
+ //#region src/setup/link.ts
2378
+ const ORIGINAL_SUFFIX = ".npm-vm-original";
2379
+ const LINKS_FILE = "links.json";
2380
+ function versionsUnder(dir) {
2381
+ try {
2382
+ return readdirSync(dir).filter((name) => /^v?\d+\.\d+\.\d+$/.test(name));
2383
+ } catch {
2384
+ return [];
2385
+ }
2386
+ }
2387
+ function fnmDir(env, platform, home) {
2388
+ if (env["FNM_DIR"]) return env["FNM_DIR"];
2389
+ if (platform === "win32") return join(env["APPDATA"] ?? join(home, "AppData", "Roaming"), "fnm");
2390
+ if (platform === "darwin") return join(home, "Library", "Application Support", "fnm");
2391
+ return join(env["XDG_DATA_HOME"] || join(home, ".local", "share"), "fnm");
2392
+ }
2393
+ /** The root nvm-windows keeps its versions in, from its settings.txt (spec LNK-1). */
2394
+ function nvmWindowsRoot(env) {
2395
+ const nvmHome = env["NVM_HOME"];
2396
+ if (!nvmHome) return null;
2397
+ try {
2398
+ const settings = readFileSync(join(nvmHome, "settings.txt"), "utf8");
2399
+ return /^root:\s*(.+)$/m.exec(settings)?.[1]?.trim() ?? nvmHome;
2400
+ } catch {
2401
+ return nvmHome;
2402
+ }
2403
+ }
2404
+ /** Windows paths resolved through junctions and compared without case, so one install is never linked twice (LNK-4). */
2405
+ function installKey(binDir, platform) {
2406
+ let real = binDir;
2407
+ try {
2408
+ real = realpathSync(binDir);
2409
+ } catch {}
2410
+ return platform === "win32" ? real.toLowerCase() : real;
2411
+ }
2412
+ function findNodeInstalls(env = process.env, platform = process.platform, home = env["HOME"] || homedir()) {
2413
+ const installs = [];
2414
+ const seen = /* @__PURE__ */ new Set();
2415
+ const add = (install) => {
2416
+ const key = installKey(install.binDir, platform);
2417
+ if (!seen.has(key) && existsSync(install.binDir)) {
2418
+ seen.add(key);
2419
+ installs.push(install);
2420
+ }
2421
+ };
2422
+ const nvmDir = env["NVM_DIR"] || join(home, ".nvm");
2423
+ for (const version of versionsUnder(join(nvmDir, "versions", "node"))) add({
2424
+ manager: "nvm",
2425
+ version: version.replace(/^v/, ""),
2426
+ binDir: join(nvmDir, "versions", "node", version, "bin")
2427
+ });
2428
+ const fnm = join(fnmDir(env, platform, home), "node-versions");
2429
+ for (const version of versionsUnder(fnm)) {
2430
+ const installation = join(fnm, version, "installation");
2431
+ add({
2432
+ manager: "fnm",
2433
+ version: version.replace(/^v/, ""),
2434
+ binDir: platform === "win32" ? installation : join(installation, "bin")
2435
+ });
2436
+ }
2437
+ if (platform === "win32") {
2438
+ const root = nvmWindowsRoot(env);
2439
+ if (root !== null) for (const version of versionsUnder(root)) add({
2440
+ manager: "nvm-windows",
2441
+ version: version.replace(/^v/, ""),
2442
+ binDir: join(root, version)
2443
+ });
2444
+ }
2445
+ return installs;
2446
+ }
2447
+ /** The files `--link` replaces in one install; blocked `.ps1` files are moved aside so `.cmd` wins. */
2448
+ function linkTargets(home, platform, powershell) {
2449
+ const targets = [];
2450
+ for (const name of ["npm", "npx"]) {
2451
+ targets.push({
2452
+ name,
2453
+ content: posixShim(home, name),
2454
+ mode: 493
2455
+ });
2456
+ if (platform === "win32") {
2457
+ targets.push({
2458
+ name: `${name}.cmd`,
2459
+ content: cmdShim(home, name),
2460
+ mode: 420
2461
+ });
2462
+ targets.push({
2463
+ name: `${name}.ps1`,
2464
+ content: powershell ? powershellShim(home, name) : null,
2465
+ mode: 420
2466
+ });
2467
+ }
2468
+ }
2469
+ return targets;
2470
+ }
2471
+ /** Keeps a copy of the original beside the target; a symlink is copied as a symlink. */
2472
+ function preserveOriginal(path, originalPath, meta) {
2473
+ if (meta.type === "symlink") symlinkSync(readlinkSync(path), originalPath);
2474
+ else copyFileSync(path, originalPath);
2475
+ }
2476
+ /** Puts the preserved original back in place of whatever is (or is not) at the target. */
2477
+ function restoreOriginal(dirs, owner, entry) {
2478
+ const originalPath = entry.originalPath;
2479
+ if (entry.original.type === "symlink") {
2480
+ mutateFile(dirs.journal, "link", owner, entry.target, null);
2481
+ withSharingRetry(() => renameSync(originalPath, entry.target));
2482
+ return;
2483
+ }
2484
+ mutateFile(dirs.journal, "link", owner, entry.target, readFileSync(originalPath), entry.original.mode || 493);
2485
+ rmSync(originalPath, { force: true });
2486
+ }
2487
+ /**
2488
+ * Replaces one entry point with a shim (spec LNK-2, LNK-3). The original is copied beside the target and
2489
+ * recorded before the journaled write, so a crash at any step leaves either a restorable original or the shim.
2490
+ * A file matching neither our shim nor the recorded original is reported and left alone.
2491
+ */
2492
+ function linkOne(dirs, owner, binDir, target) {
2493
+ const path = join(binDir, target.name);
2494
+ const originalPath = `${path}${ORIGINAL_SUFFIX}`;
2495
+ const known = readLinks(dirs).entries.find((entry) => entry.target === path);
2496
+ let current = captureMeta(path);
2497
+ if (current.type === "missing" && known !== void 0 && known.shim.type !== "missing" && known.originalPath !== null && existsSync(known.originalPath)) {
2498
+ restoreOriginal(dirs, owner, known);
2499
+ forget$1(dirs, known);
2500
+ current = captureMeta(path);
2501
+ }
2502
+ if (target.content === null) return moveAside(dirs, owner, binDir, target.name, known);
2503
+ if (current.type === "missing" && known !== void 0 && known.shim.type === "missing" && known.originalPath !== null && metaMatches(known.originalPath, known.original)) {
2504
+ mutateFile(dirs.journal, "link", owner, path, Buffer.from(target.content, "utf8"), target.mode);
2505
+ record(dirs, {
2506
+ ...known,
2507
+ shim: captureMeta(path),
2508
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
2509
+ });
2510
+ return {
2511
+ target: path,
2512
+ outcome: "linked"
2513
+ };
2514
+ }
2515
+ if (current.type === "missing") return {
2516
+ target: path,
2517
+ outcome: "missing"
2518
+ };
2519
+ if (current.type === "file" && readFileSync(path, "utf8") === target.content) {
2520
+ if (known === void 0) record(dirs, {
2521
+ binDir,
2522
+ name: target.name,
2523
+ target: path,
2524
+ originalPath: existsSync(originalPath) ? originalPath : null,
2525
+ original: captureMeta(originalPath),
2526
+ shim: current,
2527
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
2528
+ });
2529
+ else if (known.shim.type === "missing") record(dirs, {
2530
+ ...known,
2531
+ shim: current,
2532
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
2533
+ });
2534
+ return {
2535
+ target: path,
2536
+ outcome: "already"
2537
+ };
2538
+ }
2539
+ if (known !== void 0 && metaMatches(path, known.shim)) {
2540
+ mutateFile(dirs.journal, "link", owner, path, Buffer.from(target.content, "utf8"), target.mode);
2541
+ record(dirs, {
2542
+ ...known,
2543
+ shim: captureMeta(path),
2544
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
2545
+ });
2546
+ return {
2547
+ target: path,
2548
+ outcome: "linked"
2549
+ };
2550
+ }
2551
+ if (known !== void 0 && !metaMatches(path, known.original)) return {
2552
+ target: path,
2553
+ outcome: "foreign",
2554
+ detail: "changed since npm-vm linked it"
2555
+ };
2556
+ if (existsSync(originalPath) && !metaMatches(originalPath, current)) return {
2557
+ target: path,
2558
+ outcome: "foreign",
2559
+ detail: `${originalPath} already exists`
2560
+ };
2561
+ try {
2562
+ if (!existsSync(originalPath)) preserveOriginal(path, originalPath, current);
2563
+ } catch (error) {
2564
+ return {
2565
+ target: path,
2566
+ outcome: "unwritable",
2567
+ detail: error.message
2568
+ };
2569
+ }
2570
+ const entry = {
2571
+ binDir,
2572
+ name: target.name,
2573
+ target: path,
2574
+ originalPath,
2575
+ original: current,
2576
+ shim: MISSING,
2577
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
2578
+ };
2579
+ record(dirs, entry);
2580
+ try {
2581
+ mutateFile(dirs.journal, "link", owner, path, Buffer.from(target.content, "utf8"), target.mode);
2582
+ } catch (error) {
2583
+ return {
2584
+ target: path,
2585
+ outcome: "unwritable",
2586
+ detail: error.message
2587
+ };
2588
+ }
2589
+ record(dirs, {
2590
+ ...entry,
2591
+ shim: captureMeta(path)
2592
+ });
2593
+ return {
2594
+ target: path,
2595
+ outcome: "linked"
2596
+ };
2597
+ }
2598
+ /** Moves a file aside without a replacement, for Node's `.ps1` entry points when scripts are blocked. */
2599
+ function moveAside(dirs, owner, binDir, name, known) {
2600
+ const path = join(binDir, name);
2601
+ const originalPath = `${path}${ORIGINAL_SUFFIX}`;
2602
+ const current = captureMeta(path);
2603
+ if (known !== void 0 && known.shim.type !== "missing" && metaMatches(path, known.shim)) {
2604
+ mutateFile(dirs.journal, "link", owner, path, null);
2605
+ record(dirs, {
2606
+ ...known,
2607
+ shim: MISSING,
2608
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
2609
+ });
2610
+ return {
2611
+ target: path,
2612
+ outcome: "linked"
2613
+ };
2614
+ }
2615
+ if (current.type === "missing") {
2616
+ if (known === void 0 && existsSync(originalPath)) {
2617
+ record(dirs, {
2618
+ binDir,
2619
+ name,
2620
+ target: path,
2621
+ originalPath,
2622
+ original: captureMeta(originalPath),
2623
+ shim: MISSING,
2624
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
2625
+ });
2626
+ return {
2627
+ target: path,
2628
+ outcome: "already"
2629
+ };
2630
+ }
2631
+ return {
2632
+ target: path,
2633
+ outcome: known !== void 0 && existsSync(originalPath) ? "already" : "missing"
2634
+ };
2635
+ }
2636
+ if (known !== void 0 && !metaMatches(path, known.original)) return {
2637
+ target: path,
2638
+ outcome: "foreign",
2639
+ detail: "changed since npm-vm linked it"
2640
+ };
2641
+ if (existsSync(originalPath) && !metaMatches(originalPath, current)) return {
2642
+ target: path,
2643
+ outcome: "foreign",
2644
+ detail: `${originalPath} already exists`
2645
+ };
2646
+ try {
2647
+ withSharingRetry(() => renameSync(path, originalPath));
2648
+ } catch (error) {
2649
+ return {
2650
+ target: path,
2651
+ outcome: "unwritable",
2652
+ detail: error.message
2653
+ };
2654
+ }
2655
+ record(dirs, {
2656
+ binDir,
2657
+ name,
2658
+ target: path,
2659
+ originalPath,
2660
+ original: current,
2661
+ shim: MISSING,
2662
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
2663
+ });
2664
+ return {
2665
+ target: path,
2666
+ outcome: "linked"
2667
+ };
2668
+ }
2669
+ /** Puts the original back only when the shim is still ours (spec MUT-5); a replaced file is left as is. */
2670
+ function unlinkOne(dirs, owner, entry) {
2671
+ const current = captureMeta(entry.target);
2672
+ const hasOriginal = entry.originalPath !== null && existsSync(entry.originalPath);
2673
+ if (current.type === "missing") {
2674
+ if (hasOriginal && metaMatches(entry.originalPath, entry.original)) {
2675
+ restoreOriginal(dirs, owner, entry);
2676
+ forget$1(dirs, entry);
2677
+ return "restored";
2678
+ }
2679
+ if (hasOriginal) return "changed";
2680
+ forget$1(dirs, entry);
2681
+ return "missing";
2682
+ }
2683
+ if (entry.shim.type === "missing") return "changed";
2684
+ if (!metaMatches(entry.target, entry.shim)) return "changed";
2685
+ if (hasOriginal) restoreOriginal(dirs, owner, entry);
2686
+ else mutateFile(dirs.journal, "link", owner, entry.target, null);
2687
+ forget$1(dirs, entry);
2688
+ return "restored";
2689
+ }
2690
+ function record(dirs, entry) {
2691
+ const links = readLinks(dirs);
2692
+ links.entries = [...links.entries.filter((item) => item.target !== entry.target), entry];
2693
+ writeLinks(dirs, links);
2694
+ }
2695
+ function forget$1(dirs, entry) {
2696
+ const links = readLinks(dirs);
2697
+ links.entries = links.entries.filter((item) => item.target !== entry.target);
2698
+ writeLinks(dirs, links);
2699
+ }
2700
+ function readLinks(dirs) {
2701
+ try {
2702
+ const parsed = JSON.parse(readFileSync(join(dirs.state, LINKS_FILE), "utf8"));
2703
+ return Array.isArray(parsed.entries) ? parsed : {
2704
+ version: 1,
2705
+ entries: []
2706
+ };
2707
+ } catch {
2708
+ return {
2709
+ version: 1,
2710
+ entries: []
2711
+ };
2712
+ }
2713
+ }
2714
+ function writeLinks(dirs, links) {
2715
+ atomicWrite(join(dirs.state, LINKS_FILE), JSON.stringify(links, null, " "), 384);
2716
+ }
2717
+ /** Whether a recorded link still holds our shim, for `doctor` (spec CLI). */
2718
+ function linkState(entry) {
2719
+ const type = captureMeta(entry.target).type;
2720
+ if (entry.shim.type === "missing") {
2721
+ const preserved = entry.originalPath !== null && metaMatches(entry.originalPath, entry.original);
2722
+ return type === "missing" ? preserved ? "intact" : "missing" : "changed";
2723
+ }
2724
+ if (type === "missing") return "missing";
2725
+ return metaMatches(entry.target, entry.shim) ? "intact" : "changed";
2726
+ }
2727
+ //#endregion
2728
+ //#region src/setup/layout.ts
2729
+ const BOOTSTRAP_FILE = "bootstrap.cjs";
2730
+ const CURRENT_FILE = "current";
2731
+ const PREVIOUS_FILE = "previous";
2732
+ const MANIFEST_FILE = "manifest.json";
2733
+ const VERSIONS_DIR = "versions";
2734
+ /**
2735
+ * Where this code's runtime build lives: beside an installed `runtime.cjs`, under `dist/runtime/` for the
2736
+ * bin, or `../../dist/runtime/` when running from source. Null when nothing has been built.
2737
+ */
2738
+ function runtimeDistDir() {
2739
+ const here = import.meta.dirname;
2740
+ const candidates = [
2741
+ here,
2742
+ join(here, "runtime"),
2743
+ join(here, "..", "dist", "runtime"),
2744
+ join(here, "..", "..", "dist", "runtime")
2745
+ ];
2746
+ for (const candidate of candidates) if (existsSync(join(candidate, "runtime.cjs")) && existsSync(join(candidate, "bootstrap.cjs"))) return candidate;
2747
+ return null;
2748
+ }
2749
+ function versionDir(home, version) {
2750
+ return join(home, VERSIONS_DIR, version);
2751
+ }
2752
+ function copyTree(from, to, files, prefix = "") {
2753
+ mkdirSync(to, {
2754
+ recursive: true,
2755
+ mode: 493
2756
+ });
2757
+ for (const name of readdirSync(from)) {
2758
+ const source = join(from, name);
2759
+ const target = join(to, name);
2760
+ if (statSync(source).isDirectory()) copyTree(source, target, files, `${prefix}${name}/`);
2761
+ else {
2762
+ copyFileSync(source, target);
2763
+ files.push(`${prefix}${name}`);
2764
+ }
2765
+ }
2766
+ }
2767
+ /**
2768
+ * Installs one runtime version under `versions/<version>/` atomically (spec SET-1): copied into a
2769
+ * temp dir beside it and renamed into place, so a crash never leaves a half-written version.
2770
+ */
2771
+ function installVersion(home, version, distDir, abi) {
2772
+ const versions = join(home, VERSIONS_DIR);
2773
+ mkdirSync(versions, {
2774
+ recursive: true,
2775
+ mode: 493
2776
+ });
2777
+ const target = versionDir(home, version);
2778
+ const staging = join(versions, `.${version}.${process.pid}.${Date.now()}`);
2779
+ rmSync(staging, {
2780
+ recursive: true,
2781
+ force: true
2782
+ });
2783
+ const files = [];
2784
+ copyTree(distDir, staging, files);
2785
+ const manifest = {
2786
+ version,
2787
+ abi,
2788
+ files: files.toSorted()
2789
+ };
2790
+ writeFileSync(join(staging, MANIFEST_FILE), JSON.stringify(manifest, null, " "));
2791
+ const replaced = join(versions, `.${version}.replaced.${process.pid}.${Date.now()}`);
2792
+ if (existsSync(target)) atomicRename(target, replaced);
2793
+ try {
2794
+ atomicRename(staging, target);
2795
+ } catch (error) {
2796
+ if (existsSync(replaced)) atomicRename(replaced, target);
2797
+ throw error;
2798
+ }
2799
+ removeWithRetry(replaced);
2800
+ return target;
2801
+ }
2802
+ function readCurrent(home) {
2803
+ try {
2804
+ return readFileSync(join(home, CURRENT_FILE), "utf8").trim();
2805
+ } catch {
2806
+ return null;
2807
+ }
2808
+ }
2809
+ function readPrevious(home) {
2810
+ try {
2811
+ return readFileSync(join(home, PREVIOUS_FILE), "utf8").trim();
2812
+ } catch {
2813
+ return null;
2814
+ }
2815
+ }
2816
+ function readManifest(home, version) {
2817
+ try {
2818
+ return JSON.parse(readFileSync(join(versionDir(home, version), MANIFEST_FILE), "utf8"));
2819
+ } catch {
2820
+ return null;
2821
+ }
2822
+ }
2823
+ /** Removes copies a crashed reinstall moved aside; the launcher only reads them while `versions/<v>` is missing. */
2824
+ function sweepReplaced(home) {
2825
+ let names;
2826
+ try {
2827
+ names = readdirSync(join(home, VERSIONS_DIR));
2828
+ } catch {
2829
+ return;
2830
+ }
2831
+ for (const name of names) {
2832
+ const match = /^\.(.+)\.replaced\./.exec(name);
2833
+ if (match !== null && existsSync(join(home, "versions", match[1], "runtime.cjs"))) removeWithRetry(join(home, VERSIONS_DIR, name));
2834
+ }
2835
+ }
2836
+ /** The ABI an installed runtime declares, read from its manifest. */
2837
+ function runtimeAbi(home, version) {
2838
+ return readManifest(home, version)?.abi ?? null;
2839
+ }
2840
+ //#endregion
2841
+ //#region src/setup/profile.ts
2842
+ const PROFILES_FILE = "profiles.json";
2843
+ const BLOCK_RE = /^# >>> npm-vm >>>\n[\s\S]*?^# <<< npm-vm <<<\n?/m;
2844
+ function bashLoginFile(home) {
2845
+ for (const name of [
2846
+ ".bash_profile",
2847
+ ".bash_login",
2848
+ ".profile"
2849
+ ]) if (existsSync(join(home, name))) return join(home, name);
2850
+ return join(home, ".bash_profile");
2851
+ }
2852
+ /** The user profile of one PowerShell host, asked with `-NoProfile` so nothing of the user's runs (spec SEC-8). */
2853
+ function powershellProfile(host, env, platform = process.platform) {
2854
+ const bin = host === "powershell" ? powershellPath(env) : shellBinary("powershell", env, platform);
2855
+ if (bin === null) return null;
2856
+ try {
2857
+ const out = execFileSync(bin, [
2858
+ "-NoProfile",
2859
+ "-NonInteractive",
2860
+ "-Command",
2861
+ "$PROFILE.CurrentUserAllHosts"
2862
+ ], {
2863
+ encoding: "utf8",
2864
+ env,
2865
+ windowsHide: true,
2866
+ stdio: [
2867
+ "ignore",
2868
+ "pipe",
2869
+ "ignore"
2870
+ ]
2871
+ }).trim();
2872
+ return out === "" ? null : out;
2873
+ } catch {
2874
+ return null;
2875
+ }
2876
+ }
2877
+ /** The files to edit for `shells` (spec SH-3); PowerShell hosts that are not installed yield nothing. */
2878
+ function profileTargets(shells, env = process.env, platform = process.platform, home = env["HOME"] || homedir()) {
2879
+ const targets = [];
2880
+ for (const shell of shells) switch (shell) {
2881
+ case "zsh": {
2882
+ const zdot = env["ZDOTDIR"] || home;
2883
+ targets.push({
2884
+ shell,
2885
+ file: join(zdot, ".zshenv"),
2886
+ parts: {
2887
+ path: true,
2888
+ hook: false
2889
+ }
2890
+ });
2891
+ targets.push({
2892
+ shell,
2893
+ file: join(zdot, ".zshrc"),
2894
+ parts: {
2895
+ path: false,
2896
+ hook: true
2897
+ }
2898
+ });
2899
+ break;
2900
+ }
2901
+ case "bash": {
2902
+ targets.push({
2903
+ shell,
2904
+ file: join(home, ".bashrc"),
2905
+ parts: {
2906
+ path: true,
2907
+ hook: true
2908
+ }
2909
+ });
2910
+ const login = bashLoginFile(home);
2911
+ if (login !== join(home, ".bashrc")) targets.push({
2912
+ shell,
2913
+ file: login,
2914
+ parts: {
2915
+ path: true,
2916
+ hook: false
2917
+ }
2918
+ });
2919
+ break;
2920
+ }
2921
+ case "fish": {
2922
+ const config = env["XDG_CONFIG_HOME"] || join(home, ".config");
2923
+ targets.push({
2924
+ shell,
2925
+ file: join(config, "fish", "conf.d", "npm-vm.fish"),
2926
+ parts: {
2927
+ path: true,
2928
+ hook: true
2929
+ }
2930
+ });
2931
+ break;
2932
+ }
2933
+ case "powershell": {
2934
+ const hosts = platform === "win32" ? ["powershell", "pwsh"] : ["pwsh"];
2935
+ for (const host of hosts) {
2936
+ const file = powershellProfile(host, env, platform);
2937
+ if (file !== null && !targets.some((target) => target.file === file)) targets.push({
2938
+ shell,
2939
+ file,
2940
+ parts: {
2941
+ path: true,
2942
+ hook: true
2943
+ }
2944
+ });
2945
+ }
2946
+ break;
2947
+ }
2948
+ }
2949
+ return targets;
2950
+ }
2951
+ /** Why a profile is printed instead of edited (spec SH-5), or null when editing it is fine. */
2952
+ function profileGuard(file, env = process.env, platform = process.platform) {
2953
+ let stat;
2954
+ try {
2955
+ stat = lstatSync(file);
2956
+ } catch {
2957
+ stat = null;
2958
+ }
2959
+ if (stat !== null && stat.isSymbolicLink()) return "it is a symlink, so a dotfile manager probably owns it";
2960
+ let probe = stat === null ? dirname(file) : file;
2961
+ while (!existsSync(probe) && dirname(probe) !== probe) probe = dirname(probe);
2962
+ try {
2963
+ accessSync(probe, constants.W_OK);
2964
+ } catch {
2965
+ return "it is not writable";
2966
+ }
2967
+ try {
2968
+ if (realpathSync(probe).startsWith("/nix/store")) return "it lives in the Nix store (home-manager)";
2969
+ } catch {}
2970
+ const chezmoi = join(env["HOME"] || homedir(), ".local", "share", "chezmoi");
2971
+ if (existsSync(join(chezmoi, `dot_${basename(file).replace(/^\./, "")}`))) return "chezmoi manages it";
2972
+ if (platform === "win32" && stat !== null && file.endsWith(".ps1")) {
2973
+ if (readFileSync(file, "utf8").includes("# SIG # Begin signature block") && executionPolicy(env) === "AllSigned") return "it is signed and the execution policy is AllSigned";
2974
+ }
2975
+ return null;
2976
+ }
2977
+ /** The effective PowerShell execution policy, asked without a profile (spec WIN-2). */
2978
+ function executionPolicy(env = process.env) {
2979
+ try {
2980
+ return execFileSync(powershellPath(env), [
2981
+ "-NoProfile",
2982
+ "-NonInteractive",
2983
+ "-Command",
2984
+ "Get-ExecutionPolicy"
2985
+ ], {
2986
+ encoding: "utf8",
2987
+ windowsHide: true,
2988
+ stdio: [
2989
+ "ignore",
2990
+ "pipe",
2991
+ "ignore"
2992
+ ]
2993
+ }).trim();
2994
+ } catch {
2995
+ return null;
2996
+ }
2997
+ }
2998
+ /** The content with our block replaced or appended; a trailing newline is guaranteed before the block. */
2999
+ function withBlock(content, block) {
3000
+ const text = content ?? "";
3001
+ if (BLOCK_RE.test(text)) return text.replace(BLOCK_RE, block);
3002
+ return `${text}${text === "" || text.endsWith("\n") ? "" : "\n"}${text === "" ? "" : "\n"}${block}`;
3003
+ }
3004
+ /** The content without our block and the blank line that separated it; nothing else moves. */
3005
+ function withoutBlock(content) {
3006
+ const match = BLOCK_RE.exec(content);
3007
+ if (match === null) return content;
3008
+ let before = content.slice(0, match.index).replace(/\n+$/, "\n");
3009
+ if (before === "\n") before = "";
3010
+ const after = content.slice(match.index + match[0].length);
3011
+ return before !== "" && after !== "" ? `${before}\n${after}` : `${before}${after}`;
3012
+ }
3013
+ function currentBlock(content) {
3014
+ const match = BLOCK_RE.exec(content);
3015
+ return match === null ? null : match[0];
3016
+ }
3017
+ function renderDiff(file, before, after) {
3018
+ const oldBlock = before === null ? null : currentBlock(before);
3019
+ const newBlock = currentBlock(after) ?? "";
3020
+ const lines = [`--- ${file}`, `+++ ${file}`];
3021
+ if (oldBlock !== null) lines.push(...oldBlock.trimEnd().split("\n").map((line) => `-${line}`));
3022
+ lines.push(...newBlock.trimEnd().split("\n").map((line) => `+${line}`));
3023
+ return `${lines.join("\n")}\n`;
3024
+ }
3025
+ /** What `setup` would write for `target`, or null when the file already holds exactly this block. */
3026
+ function planProfileEdit(target, home) {
3027
+ const before = existsSync(target.file) ? readFileSync(target.file, "utf8") : null;
3028
+ const after = withBlock(before, profileBlock(target.shell, home, target.parts));
3029
+ if (before === after) return null;
3030
+ return {
3031
+ target,
3032
+ before,
3033
+ after,
3034
+ diff: renderDiff(target.file, before, after)
3035
+ };
3036
+ }
3037
+ function uniqueBackup(file) {
3038
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3039
+ let candidate = `${file}.npm-vm-backup-${stamp}`;
3040
+ let counter = 1;
3041
+ while (existsSync(candidate)) {
3042
+ candidate = `${file}.npm-vm-backup-${stamp}-${counter}`;
3043
+ counter += 1;
3044
+ }
3045
+ return candidate;
3046
+ }
3047
+ /** Applies one consented edit under a journal and a unique backup, and records it (spec SH-4). */
3048
+ function applyProfileEdit(dirs, owner, edit) {
3049
+ let backup = null;
3050
+ if (edit.before !== null) {
3051
+ backup = uniqueBackup(edit.target.file);
3052
+ atomicWrite(backup, edit.before, 384);
3053
+ }
3054
+ const mode = edit.before === null ? 420 : captureMeta(edit.target.file).mode || 420;
3055
+ mutateFile(dirs.journal, "profile", owner, edit.target.file, Buffer.from(edit.after, "utf8"), mode);
3056
+ const record = {
3057
+ file: edit.target.file,
3058
+ shell: edit.target.shell,
3059
+ parts: edit.target.parts,
3060
+ writtenSha256: sha256(edit.after),
3061
+ backup,
3062
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3063
+ };
3064
+ const profiles = readProfiles(dirs);
3065
+ profiles.entries = [...profiles.entries.filter((entry) => entry.file !== record.file), record];
3066
+ writeProfiles(dirs, profiles);
3067
+ return record;
3068
+ }
3069
+ /** Removes our block only when the file is still what we wrote (spec MUT-5); otherwise it is left alone. */
3070
+ function removeProfileBlock(dirs, owner, record) {
3071
+ if (!existsSync(record.file)) {
3072
+ forget(dirs, record);
3073
+ return "missing";
3074
+ }
3075
+ const content = readFileSync(record.file, "utf8");
3076
+ if (sha256(content) !== record.writtenSha256) return "changed";
3077
+ const after = withoutBlock(content);
3078
+ mutateFile(dirs.journal, "profile", owner, record.file, after.trim() === "" ? null : Buffer.from(after, "utf8"), captureMeta(record.file).mode || 420);
3079
+ forget(dirs, record);
3080
+ return "removed";
3081
+ }
3082
+ function forget(dirs, record) {
3083
+ const profiles = readProfiles(dirs);
3084
+ profiles.entries = profiles.entries.filter((entry) => entry.file !== record.file);
3085
+ writeProfiles(dirs, profiles);
3086
+ }
3087
+ function readProfiles(dirs) {
3088
+ try {
3089
+ const parsed = JSON.parse(readFileSync(join(dirs.state, PROFILES_FILE), "utf8"));
3090
+ return Array.isArray(parsed.entries) ? parsed : {
3091
+ version: 1,
3092
+ entries: []
3093
+ };
3094
+ } catch {
3095
+ return {
3096
+ version: 1,
3097
+ entries: []
3098
+ };
3099
+ }
3100
+ }
3101
+ function writeProfiles(dirs, profiles) {
3102
+ atomicWrite(join(dirs.state, PROFILES_FILE), JSON.stringify(profiles, null, " "), 384);
3103
+ }
3104
+ //#endregion
3105
+ //#region src/setup/windows.ts
3106
+ function powershell(env, command) {
3107
+ return execFileSync(powershellPath(env), [
3108
+ "-NoProfile",
3109
+ "-NonInteractive",
3110
+ "-Command",
3111
+ command
3112
+ ], {
3113
+ encoding: "utf8",
3114
+ env,
3115
+ windowsHide: true,
3116
+ stdio: [
3117
+ "ignore",
3118
+ "pipe",
3119
+ "pipe"
3120
+ ]
3121
+ }).trim();
3122
+ }
3123
+ function regExe(env) {
3124
+ return join(env["SystemRoot"] ?? "C:\\Windows", "System32", "reg.exe");
3125
+ }
3126
+ function quote(value) {
3127
+ return `'${value.replace(/'/g, "''")}'`;
3128
+ }
3129
+ function readUserPath(env) {
3130
+ return powershell(env, "[Environment]::GetEnvironmentVariable('Path', 'User')");
3131
+ }
3132
+ function writeUserPath(env, value) {
3133
+ powershell(env, `[Environment]::SetEnvironmentVariable('Path', ${quote(value)}, 'User')`);
3134
+ }
3135
+ function sameDir(a, b) {
3136
+ return a.replace(/[\\/]+$/, "").toLowerCase() === b.replace(/[\\/]+$/, "").toLowerCase();
3137
+ }
3138
+ /** Prepends `dir` to the user PATH unless present; returns whether anything changed. */
3139
+ function addToUserPath(env, dir) {
3140
+ const parts = readUserPath(env).split(";").filter((part) => part !== "");
3141
+ if (parts.some((part) => sameDir(part, dir))) return false;
3142
+ writeUserPath(env, [dir, ...parts].join(";"));
3143
+ return true;
3144
+ }
3145
+ function removeFromUserPath(env, dir) {
3146
+ const parts = readUserPath(env).split(";").filter((part) => part !== "");
3147
+ const kept = parts.filter((part) => !sameDir(part, dir));
3148
+ if (kept.length === parts.length) return false;
3149
+ writeUserPath(env, kept.join(";"));
3150
+ return true;
3151
+ }
3152
+ const AUTORUN_KEY = "HKCU\\Software\\Microsoft\\Command Processor";
3153
+ const AUTORUN_MARK = "npm-vm";
3154
+ function readCmdAutoRun(env) {
3155
+ try {
3156
+ const out = execFileSync(regExe(env), [
3157
+ "query",
3158
+ AUTORUN_KEY,
3159
+ "/v",
3160
+ "AutoRun"
3161
+ ], {
3162
+ encoding: "utf8",
3163
+ windowsHide: true,
3164
+ stdio: [
3165
+ "ignore",
3166
+ "pipe",
3167
+ "ignore"
3168
+ ]
3169
+ });
3170
+ return /AutoRun\s+REG_(?:EXPAND_)?SZ\s+(.*)$/m.exec(out)?.[1]?.trim() ?? null;
3171
+ } catch {
3172
+ return null;
3173
+ }
3174
+ }
3175
+ /** The cmd AutoRun entry that puts shims first in every cmd session; only with `--cmd-autorun` (WIN-3). */
3176
+ function autoRunCommand(shimsDir) {
3177
+ return `set "PATH=${shimsDir};%PATH%" & rem ${AUTORUN_MARK}`;
3178
+ }
3179
+ /** `value` without the fragment `autoRunCommand` generated, whatever home it named; the user's own commands stay. */
3180
+ function withoutAutoRunCommand(value) {
3181
+ if (value === null) return "";
3182
+ return value.replace(/^set "PATH=[^"]*;%PATH%" & rem npm-vm(?: & )?/, "").trim();
3183
+ }
3184
+ function setCmdAutoRun(env, shimsDir) {
3185
+ const ours = autoRunCommand(shimsDir);
3186
+ const kept = withoutAutoRunCommand(readCmdAutoRun(env));
3187
+ const value = kept === "" ? ours : `${ours} & ${kept}`;
3188
+ execFileSync(regExe(env), [
3189
+ "add",
3190
+ AUTORUN_KEY,
3191
+ "/v",
3192
+ "AutoRun",
3193
+ "/t",
3194
+ "REG_SZ",
3195
+ "/d",
3196
+ value,
3197
+ "/f"
3198
+ ], {
3199
+ windowsHide: true,
3200
+ stdio: "ignore"
3201
+ });
3202
+ }
3203
+ function clearCmdAutoRun(env) {
3204
+ const existing = readCmdAutoRun(env);
3205
+ if (existing === null || !existing.includes(`rem ${AUTORUN_MARK}`)) return false;
3206
+ const rest = withoutAutoRunCommand(existing);
3207
+ if (rest === "") execFileSync(regExe(env), [
3208
+ "delete",
3209
+ AUTORUN_KEY,
3210
+ "/v",
3211
+ "AutoRun",
3212
+ "/f"
3213
+ ], {
3214
+ windowsHide: true,
3215
+ stdio: "ignore"
3216
+ });
3217
+ else execFileSync(regExe(env), [
3218
+ "add",
3219
+ AUTORUN_KEY,
3220
+ "/v",
3221
+ "AutoRun",
3222
+ "/t",
3223
+ "REG_SZ",
3224
+ "/d",
3225
+ rest,
3226
+ "/f"
3227
+ ], {
3228
+ windowsHide: true,
3229
+ stdio: "ignore"
3230
+ });
3231
+ return true;
3232
+ }
3233
+ /** Whether the effective execution policy lets our unsigned `.ps1` shims run (spec WIN-2). */
3234
+ function powershellAllowsScripts(policy) {
3235
+ return policy !== null && ![
3236
+ "Restricted",
3237
+ "AllSigned",
3238
+ "Undefined"
3239
+ ].includes(policy);
3240
+ }
3241
+ /** Which `npm` each Windows context resolves first, for `doctor`. */
3242
+ function windowsWinners(env) {
3243
+ const results = [];
3244
+ try {
3245
+ const out = execFileSync(join(env["SystemRoot"] ?? "C:\\Windows", "System32", "where.exe"), ["npm"], {
3246
+ encoding: "utf8",
3247
+ env,
3248
+ windowsHide: true,
3249
+ stdio: [
3250
+ "ignore",
3251
+ "pipe",
3252
+ "ignore"
3253
+ ]
3254
+ });
3255
+ results.push({
3256
+ context: "cmd",
3257
+ path: out.split(/\r?\n/)[0] ?? null
3258
+ });
3259
+ } catch {
3260
+ results.push({
3261
+ context: "cmd",
3262
+ path: null
3263
+ });
3264
+ }
3265
+ try {
3266
+ results.push({
3267
+ context: "PowerShell",
3268
+ path: powershell(env, "(Get-Command npm -All | Select-Object -First 1).Source") || null
3269
+ });
3270
+ } catch {
3271
+ results.push({
3272
+ context: "PowerShell",
3273
+ path: null
3274
+ });
3275
+ }
3276
+ return results;
3277
+ }
3278
+ //#endregion
3279
+ //#region src/commands/doctor.ts
3280
+ const PROBE_TIMEOUT_MS = 5e3;
3281
+ /** The `npm` an interactive login shell resolves, by asking that shell (its profiles decide the PATH). */
3282
+ function shellWinner(shell, env) {
3283
+ const bin = shellBinary(shell, env);
3284
+ if (bin === null) return null;
3285
+ const args = shell === "fish" ? [
3286
+ "-l",
3287
+ "-i",
3288
+ "-c",
3289
+ "emit fish_prompt; command -v npm"
3290
+ ] : shell === "powershell" ? [
3291
+ "-NoProfile",
3292
+ "-Command",
3293
+ "(Get-Command npm).Source"
3294
+ ] : shell === "zsh" ? [
3295
+ "-l",
3296
+ "-i",
3297
+ "-c",
3298
+ "for f in $precmd_functions; do \"$f\"; done; command -v npm"
3299
+ ] : [
3300
+ "-l",
3301
+ "-i",
3302
+ "-c",
3303
+ "eval \"$PROMPT_COMMAND\"; command -v npm"
3304
+ ];
3305
+ try {
3306
+ const out = execFileSync(bin, args, {
3307
+ encoding: "utf8",
3308
+ env,
3309
+ timeout: PROBE_TIMEOUT_MS,
3310
+ stdio: [
3311
+ "ignore",
3312
+ "pipe",
3313
+ "ignore"
3314
+ ]
3315
+ }).trim().split("\n").pop();
3316
+ return out === void 0 || out === "" ? null : out;
3317
+ } catch {
3318
+ return null;
3319
+ }
3320
+ }
3321
+ /** A Windows path for comparison: `where` answers with the long name, the home may carry an 8.3 short name. */
3322
+ function canonical(path) {
3323
+ try {
3324
+ return realpathSync.native(path).toLowerCase();
3325
+ } catch {
3326
+ return path.toLowerCase();
3327
+ }
3328
+ }
3329
+ /** What `command -v npm` printed: an alias or a function wins over PATH, so no PATH fix can help. */
3330
+ function winnerKind(winner) {
3331
+ if (winner.startsWith("alias ")) return "alias";
3332
+ return winner === "npm" ? "function" : "path";
3333
+ }
3334
+ function samePath(a, b, platform) {
3335
+ return platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
3336
+ }
3337
+ async function collect(context) {
3338
+ const { dirs, env, platform } = context;
3339
+ const report = {
3340
+ home: dirs.home,
3341
+ version: readCurrent(dirs.home),
3342
+ problems: [],
3343
+ notes: []
3344
+ };
3345
+ const setupHint = "Run `npx --prefix ~ -y @mawesome/npm-vm setup`.";
3346
+ const problem = (check, detail, fix) => {
3347
+ report.problems.push({
3348
+ check,
3349
+ detail,
3350
+ fix
3351
+ });
3352
+ };
3353
+ if (report.version === null || readManifest(dirs.home, report.version) === null) problem("runtime", `no runtime is installed under ${dirs.home}`, setupHint);
3354
+ else {
3355
+ const root = join(dirs.home, BOOTSTRAP_FILE);
3356
+ const shipped = join(versionDir(dirs.home, report.version), BOOTSTRAP_FILE);
3357
+ if (!existsSync(root) || !readFileSync(root).equals(readFileSync(shipped))) problem("launcher", `${root} differs from the installed version's launcher`, setupHint);
3358
+ }
3359
+ for (const name of SELECTORS) {
3360
+ const file = join(shimDir(dirs.home), name);
3361
+ const abi = existsSync(file) ? shimAbi(readFileSync(file, "utf8")) : null;
3362
+ if (abi === null) problem("shims", `${file} is missing or not an npm-vm shim`, setupHint);
3363
+ else if (abi !== 1) problem("shims", `${file} declares launcher ABI ${abi}, this version needs 1`, setupHint);
3364
+ }
3365
+ if (dirs.machine.fallback) report.notes.push("The machine id falls back to the hostname; cloned machines or containers sharing this home could collide in the cache.");
3366
+ const lock = readLock(dirs.state);
3367
+ if (lock !== null && !lockOwnerLive(lock, dirs.machine.id)) problem("lock", `${lockPath(dirs.state)} is held by PID ${lock.pid}, which no longer runs`, `Delete ${lockPath(dirs.state)}.`);
3368
+ for (const journal of listJournals(dirs.journal)) {
3369
+ const foreign = journal.owner.machineId !== dirs.machine.id;
3370
+ problem("journal", `${journal.id} (${journal.kind}) for ${journal.target} was left by ${foreign ? `another machine (${journal.owner.host})` : `PID ${journal.owner.pid}`}`, foreign ? `Confirm no npm-vm runs on ${journal.owner.host}, then run \`npm-vm recover --journal ${journal.id} --confirm-dead-machine ${journal.owner.machineId}\`.` : "Run any npm-vm command; dead owners are recovered automatically.");
3371
+ }
3372
+ const profiles = readProfiles(dirs);
3373
+ for (const entry of profiles.entries) if (!existsSync(entry.file) || !readFileSync(entry.file, "utf8").includes("# >>> npm-vm >>>")) problem("profile", `${entry.file} no longer holds the npm-vm block`, `Run \`npm-vm init ${entry.shell}\` and add the block, or run setup again.`);
3374
+ const shimsDir = shimDir(dirs.home);
3375
+ if (platform === "win32") {
3376
+ const shimsCanonical = canonical(shimsDir);
3377
+ for (const winner of windowsWinners(env)) if (winner.path === null || !canonical(winner.path).startsWith(shimsCanonical)) problem("path", `${winner.context} resolves npm to ${winner.path ?? "nothing"}`, "Run setup again, then restart the terminal (M-RESTART).");
3378
+ } else {
3379
+ const login = basename(env["SHELL"] ?? "");
3380
+ const shells = /* @__PURE__ */ new Set([...profiles.entries.map((entry) => entry.shell), ...isShell(login) ? [login] : []]);
3381
+ for (const shell of shells) {
3382
+ if (shell === "powershell") continue;
3383
+ const winner = shellWinner(shell, env);
3384
+ if (winner === null) continue;
3385
+ const kind = winnerKind(winner);
3386
+ if (kind !== "path") problem("path", `${shell} resolves npm to a shell ${kind}: ${winner}`, `A shell ${kind} wins over PATH. Remove it from your ${shell} profile, or make it run ${join(shimsDir, "npm")}.`);
3387
+ else if (!samePath(winner, join(shimsDir, "npm"), platform)) problem("path", `${shell} resolves npm to ${winner}${winner.includes("corepack") ? " (a Corepack shim)" : ""}`, `Add the snippet from \`npm-vm init ${shell}\` after your Node manager's init line, or run setup with --link.`);
3388
+ }
3389
+ }
3390
+ const links = readLinks(dirs);
3391
+ if (links.entries.length > 0) {
3392
+ for (const entry of links.entries) {
3393
+ const state = linkState(entry);
3394
+ if (state !== "intact") problem("link", `${entry.target} is ${state === "missing" ? "missing" : "no longer the npm-vm shim"}`, "Run `npm-vm setup --link` to relink, or `npm-vm uninstall` to stop linking.");
3395
+ }
3396
+ const linked = new Set(links.entries.map((entry) => entry.binDir));
3397
+ for (const install of findNodeInstalls(env, platform)) if (!linked.has(install.binDir)) problem("link", `${install.manager} ${install.version} at ${install.binDir} is not linked`, "Run `npm-vm setup --link`.");
3398
+ }
3399
+ report.notes.push(...findNodeInstalls(env, platform).map((install) => `Found ${install.manager} ${install.version} at ${install.binDir}`));
3400
+ const project = findProject([], process.cwd(), env);
3401
+ if (existsSync(join(project.root, "node_modules", ".bin", "npm"))) report.notes.push(`${project.root}/node_modules/.bin/npm exists; npm scripts in this project run it instead of the shim.`);
3402
+ return report;
3403
+ }
3404
+ async function doctor(context, options = {}) {
3405
+ const report = await collect(context);
3406
+ if (options.quiet) return report.problems.length === 0 ? EXIT.ok : EXIT.problems;
3407
+ if (context.json) context.io.out(JSON.stringify(report, null, 2));
3408
+ else {
3409
+ const lines = [`npm-vm ${report.version ?? "(not installed)"} in ${report.home}`];
3410
+ for (const problem of report.problems) lines.push(` problem [${problem.check}]: ${problem.detail}`, ` fix: ${problem.fix}`);
3411
+ for (const note of report.notes) lines.push(` note: ${note}`);
3412
+ lines.push(report.problems.length === 0 ? " healthy" : ` ${report.problems.length} problem(s)`);
3413
+ context.io.out(lines.join("\n"));
3414
+ }
3415
+ return report.problems.length === 0 ? EXIT.ok : EXIT.problems;
3416
+ }
3417
+ //#endregion
3418
+ //#region src/commands/pin.ts
3419
+ /** The indentation and newline a JSON file uses, so the rewrite keeps its shape. */
3420
+ function detectFormat(text) {
3421
+ return {
3422
+ indent: /^[ \t]+(?=")/m.exec(text)?.[0] ?? " ",
3423
+ newline: text.includes("\r\n") ? "\r\n" : "\n",
3424
+ trailingNewline: text.endsWith("\n")
3425
+ };
3426
+ }
3427
+ /** `devEngines.packageManager` with one npm entry, keeping other managers and the field's shape. */
3428
+ function pinnedManifest(pkg, version) {
3429
+ const entry = {
3430
+ name: "npm",
3431
+ version,
3432
+ onFail: "error"
3433
+ };
3434
+ const devEngines = typeof pkg["devEngines"] === "object" && pkg["devEngines"] !== null ? { ...pkg["devEngines"] } : {};
3435
+ const existing = devEngines["packageManager"];
3436
+ if (Array.isArray(existing)) devEngines["packageManager"] = [...existing.filter((item) => !(typeof item === "object" && item !== null && item.name === "npm")), entry];
3437
+ else if (typeof existing === "object" && existing !== null && existing.name !== "npm") devEngines["packageManager"] = [existing, entry];
3438
+ else devEngines["packageManager"] = entry;
3439
+ return {
3440
+ ...pkg,
3441
+ devEngines
3442
+ };
3443
+ }
3444
+ function nearestManifestDir(cwd) {
3445
+ let dir = cwd;
3446
+ for (;;) {
3447
+ if (existsSync(join(dir, "package.json"))) return dir;
3448
+ const parent = dirname(dir);
3449
+ if (parent === dir) return null;
3450
+ dir = parent;
3451
+ }
3452
+ }
3453
+ /** `^<latest npm whose engines accept the active Node>`, asked from the registry. */
3454
+ async function defaultRange(context) {
3455
+ const latest = (await createInstaller(createContext(context.env, context.dirs.home)).registry([parseVersionSpec(">=8", "pin")])).filter((candidate) => candidate.enginesNode === null || semver.satisfies(process.version, candidate.enginesNode, { includePrerelease: true })).map((candidate) => candidate.version).toSorted(semver.rcompare)[0];
3456
+ if (latest === void 0) throw new NpmVmError("M-NOMATCH", EXIT.resolution, `No npm release supports Node ${process.version}.`);
3457
+ return `^${latest}`;
3458
+ }
3459
+ async function pin(context, range, cwd = process.cwd()) {
3460
+ const project = findProject([], cwd, context.env);
3461
+ const nearest = nearestManifestDir(cwd);
3462
+ if (nearest !== null && nearest !== project.root) throw new NpmVmError("M-NOMATCH", EXIT.usage, `${nearest} is a workspace of ${project.root}; npm reads devEngines from the root. Run pin from ${project.root} to pin it there.`);
3463
+ const file = join(project.root, "package.json");
3464
+ if (!existsSync(file)) throw new NpmVmError("M-NOMATCH", EXIT.usage, `No package.json in ${project.root}.`);
3465
+ const version = range ?? await defaultRange(context);
3466
+ try {
3467
+ parseVersionSpec(version, "pin");
3468
+ } catch (error) {
3469
+ throw new NpmVmError("M-NOMATCH", EXIT.usage, error.message);
3470
+ }
3471
+ const lock = await acquireLock(context.dirs.state, context.owner);
3472
+ try {
3473
+ recoverDeadJournals(context.dirs.journal, context.dirs.machine.id);
3474
+ const text = readFileSync(file, "utf8");
3475
+ let pkg;
3476
+ try {
3477
+ pkg = JSON.parse(text);
3478
+ } catch (error) {
3479
+ throw new NpmVmError("M-NOMATCH", EXIT.usage, `${file} is not valid JSON: ${error.message}`);
3480
+ }
3481
+ const format = detectFormat(text);
3482
+ const output = `${JSON.stringify(pinnedManifest(pkg, version), null, format.indent).replace(/\n/g, format.newline)}${format.trailingNewline ? format.newline : ""}`;
3483
+ if (output !== text) {
3484
+ const mode = process.platform === "win32" ? 420 : statSync(file).mode & 4095;
3485
+ mutateFile(context.dirs.journal, "project", context.owner, file, Buffer.from(output, "utf8"), mode);
3486
+ }
3487
+ } finally {
3488
+ lock.release();
3489
+ }
3490
+ context.io.out(context.json ? JSON.stringify({
3491
+ file,
3492
+ version
3493
+ }) : `Pinned npm ${version} in ${file}`);
3494
+ return EXIT.ok;
3495
+ }
3496
+ //#endregion
3497
+ //#region src/commands/self-update.ts
3498
+ const PACKAGE_NAME = "@mawesome/npm-vm";
3499
+ function run$1(file, args) {
3500
+ return new Promise((resolve) => {
3501
+ execFile(process.execPath, [file, ...args], {
3502
+ encoding: "utf8",
3503
+ timeout: 6e4,
3504
+ windowsHide: true
3505
+ }, (error, stdout, stderr) => {
3506
+ resolve({
3507
+ code: error === null ? 0 : error.code ?? 1,
3508
+ stdout,
3509
+ stderr
3510
+ });
3511
+ });
3512
+ });
3513
+ }
3514
+ /** The launcher generation a bootstrap file belongs to, read from its `LAUNCHER_ABI` constant. */
3515
+ function launcherAbiOf(bootstrapFile) {
3516
+ const abi = shimAbi(readFileSync(bootstrapFile, "utf8"));
3517
+ if (abi === null) throw new NpmVmError("M-ABI", EXIT.abi, abiMessage(`${bootstrapFile} declares no launcher ABI`));
3518
+ return abi;
3519
+ }
3520
+ /**
3521
+ * Replaces the home's launcher files with those of `version` (spec SUP-3): the bootstrap at the root and
3522
+ * every shim, rendered from that version's templates, each under a journal.
3523
+ */
3524
+ function installLaunchers(context, version) {
3525
+ const { dirs, platform, env } = context;
3526
+ const dir = versionDir(dirs.home, version);
3527
+ const bootstrap = join(dir, BOOTSTRAP_FILE);
3528
+ const abi = launcherAbiOf(bootstrap);
3529
+ mutateFile(dirs.journal, "launcher", context.owner, join(dirs.home, BOOTSTRAP_FILE), readFileSync(bootstrap), 420);
3530
+ const powershell = platform !== "win32" || powershellAllowsScripts(executionPolicy(env));
3531
+ mkdirSync(shimDir(dirs.home), {
3532
+ recursive: true,
3533
+ mode: 493
3534
+ });
3535
+ for (const file of renderShims(dirs.home, platform, {
3536
+ powershell,
3537
+ templates: join(dir, LAUNCHERS_DIR),
3538
+ abi
3539
+ })) mutateFile(dirs.journal, "launcher", context.owner, join(shimDir(dirs.home), file.name), Buffer.from(file.content), file.mode);
3540
+ if (platform === "win32" && !powershell) for (const stale of stalePowershellShims(dirs.home)) mutateFile(dirs.journal, "launcher", context.owner, stale, null);
3541
+ }
3542
+ function currentLauncherAbi(context) {
3543
+ const file = join(context.dirs.home, BOOTSTRAP_FILE);
3544
+ return existsSync(file) ? shimAbi(readFileSync(file, "utf8")) : null;
3545
+ }
3546
+ async function selfUpdate(context, options, deps = {}) {
3547
+ const lock = await acquireLock(context.dirs.state, context.owner);
3548
+ try {
3549
+ recoverDeadJournals(context.dirs.journal, context.dirs.machine.id);
3550
+ return options.rollback ? rollback(context) : await update(context, deps);
3551
+ } finally {
3552
+ lock.release();
3553
+ }
3554
+ }
3555
+ /** `self-update --rollback` (spec SUP-4): offline, swaps `current` back and restores the previous launchers if the ABI differs. */
3556
+ function rollback(context) {
3557
+ const { dirs, io } = context;
3558
+ const current = readCurrent(dirs.home);
3559
+ const previous = readPrevious(dirs.home);
3560
+ if (previous === null || !existsSync(versionDir(dirs.home, previous)) || current === null) throw new NpmVmError("M-ABI", EXIT.usage, "No previous version to roll back to.");
3561
+ mutateFile(dirs.journal, "launcher", context.owner, join(dirs.home, "current"), Buffer.from(`${previous}\n`));
3562
+ if (launcherAbiOf(join(versionDir(dirs.home, previous), "bootstrap.cjs")) !== currentLauncherAbi(context)) installLaunchers(context, previous);
3563
+ atomicWrite(join(dirs.home, PREVIOUS_FILE), `${current}\n`);
3564
+ io.out(context.json ? JSON.stringify({
3565
+ current: previous,
3566
+ previous: current
3567
+ }) : `Rolled back to npm-vm ${previous} (from ${current}).`);
3568
+ return EXIT.ok;
3569
+ }
3570
+ async function update(context, deps) {
3571
+ const { dirs, env, io } = context;
3572
+ const installed = readCurrent(dirs.home) ?? context.version;
3573
+ const own = findOwnNpm();
3574
+ const found = own?.version ?? null;
3575
+ if (!transportUsable(own)) throw new NpmVmError("M-TRANSPORT", EXIT.transport, transportMessage(found, "too old or missing"));
3576
+ const work = newWorkDir(dirs.tmp);
3577
+ try {
3578
+ const config = await loadNpmConfig(own, env, work);
3579
+ const transport = new Transport(own, config, env, dirs.tmp);
3580
+ const latest = (await transport.viewPackage(PACKAGE_NAME, "latest"))[0];
3581
+ if (latest === void 0) throw new NpmVmError("M-TRANSPORT", EXIT.transport, transportMessage(own.version, `${PACKAGE_NAME}@latest is not published`));
3582
+ if (!semver.gt(latest.version, installed)) {
3583
+ io.out(context.json ? JSON.stringify({
3584
+ current: installed,
3585
+ latest: latest.version,
3586
+ updated: false
3587
+ }) : `npm-vm ${installed} is up to date (latest is ${latest.version}).`);
3588
+ return EXIT.ok;
3589
+ }
3590
+ const packed = await transport.packPackage(PACKAGE_NAME, latest.version, work);
3591
+ const digests = await fileDigests(packed.file);
3592
+ if (digests.sha512 !== latest.integrity) throw new NpmVmError("M-INTEGRITY", EXIT.verification, `${PACKAGE_NAME}@${latest.version}: tarball does not match the registry integrity.`);
3593
+ await verifySignatures({
3594
+ name: PACKAGE_NAME,
3595
+ version: latest.version,
3596
+ integrity: latest.integrity,
3597
+ signatures: latest.signatures,
3598
+ publishedAt: async () => null,
3599
+ keys: loadKeyring(dirs.state),
3600
+ registry: config.registry,
3601
+ pinned: false
3602
+ });
3603
+ await verifyProvenance({
3604
+ name: PACKAGE_NAME,
3605
+ version: latest.version,
3606
+ sha512hex: digests.sha512hex,
3607
+ attestationsUrl: latest.attestationsUrl,
3608
+ config,
3609
+ stateDir: dirs.state
3610
+ }, {
3611
+ ...deps.fetchAttestations === void 0 ? {} : { fetch: deps.fetchAttestations },
3612
+ ...deps.verifyBundle === void 0 ? {} : { verify: deps.verifyBundle },
3613
+ env
3614
+ });
3615
+ const extracted = join(work, "package");
3616
+ mkdirSync(extracted, { mode: 448 });
3617
+ await safeExtract(packed.file, extracted);
3618
+ const runtimeSource = join(extracted, "dist", "runtime");
3619
+ if (!existsSync(join(runtimeSource, "runtime.cjs")) || !existsSync(join(runtimeSource, "bootstrap.cjs"))) throw new NpmVmError("M-INTEGRITY", EXIT.verification, `${PACKAGE_NAME}@${latest.version} ships no runtime build.`);
3620
+ const candidateAbi = launcherAbiOf(join(runtimeSource, BOOTSTRAP_FILE));
3621
+ const installedRuntimeAbi = runtimeAbi(dirs.home, installed) ?? currentLauncherAbi(context) ?? candidateAbi;
3622
+ if (installedRuntimeAbi < candidateAbi - 1) throw new NpmVmError("M-ABI", EXIT.abi, abiMessage(`npm-vm ${latest.version} skips launcher generations (${installedRuntimeAbi} to ${candidateAbi})`));
3623
+ if (candidateAbi < installedRuntimeAbi) throw new NpmVmError("M-ABI", EXIT.abi, abiMessage(`npm-vm ${latest.version} ships launcher ABI ${candidateAbi}, older than the installed runtime's ${installedRuntimeAbi}`));
3624
+ const target = installVersion(dirs.home, latest.version, runtimeSource, candidateAbi);
3625
+ const health = await run$1(join(target, BOOTSTRAP_FILE), [
3626
+ "--candidate",
3627
+ "cli",
3628
+ "--version"
3629
+ ]);
3630
+ if (health.code !== 0 || health.stdout.trim() !== latest.version) {
3631
+ rmSync(target, {
3632
+ recursive: true,
3633
+ force: true
3634
+ });
3635
+ throw new NpmVmError("M-ABI", EXIT.abi, abiMessage(`the downloaded runtime failed its health check: ${health.stderr.trim() || health.stdout.trim() || `exit ${health.code}`}`));
3636
+ }
3637
+ if (candidateAbi !== currentLauncherAbi(context)) installLaunchers(context, latest.version);
3638
+ atomicWrite(join(dirs.home, PREVIOUS_FILE), `${installed}\n`);
3639
+ mutateFile(dirs.journal, "launcher", context.owner, join(dirs.home, "current"), Buffer.from(`${latest.version}\n`));
3640
+ io.out(context.json ? JSON.stringify({
3641
+ current: latest.version,
3642
+ previous: installed,
3643
+ updated: true
3644
+ }) : `Updated npm-vm ${installed} to ${latest.version}.`);
3645
+ return EXIT.ok;
3646
+ } finally {
3647
+ rmSync(work, {
3648
+ recursive: true,
3649
+ force: true
3650
+ });
3651
+ }
3652
+ }
3653
+ //#endregion
3654
+ //#region src/commands/setup.ts
3655
+ /** Refuses a home or cache another user could write to (spec SEC-9). */
3656
+ function assertPrivateDir(dir, platform) {
3657
+ if (platform === "win32") return;
3658
+ const stat = statSync(dir);
3659
+ if (process.getuid !== void 0 && stat.uid !== process.getuid()) throw new NpmVmError("M-DIRPERMS", EXIT.problems, dirPermsMessage(dir, "is not owned by you", `chown $(id -u) ${dir}`));
3660
+ if ((stat.mode & 18) !== 0) throw new NpmVmError("M-DIRPERMS", EXIT.problems, dirPermsMessage(dir, "is writable by other users", `chmod go-w ${dir}`));
3661
+ }
3662
+ function ensurePrivateDir(dir, platform) {
3663
+ if (!existsSync(dir)) {
3664
+ mkdirSync(dir, {
3665
+ recursive: true,
3666
+ mode: 448
3667
+ });
3668
+ if (platform !== "win32") chmodSync(dir, 448);
3669
+ }
3670
+ assertPrivateDir(dir, platform);
3671
+ }
3672
+ /** The shells to configure: `--shell` values, else the login shell, plus PowerShell on Windows. */
3673
+ function defaultShells(env, platform) {
3674
+ if (platform === "win32") return ["powershell"];
3675
+ const login = basename(env["SHELL"] ?? "");
3676
+ return isShell(login) ? [login] : ["bash"];
3677
+ }
3678
+ function sameFiles(home, version, distDir) {
3679
+ const manifest = readManifest(home, version);
3680
+ if (manifest === null) return false;
3681
+ return manifest.files.every((file) => {
3682
+ try {
3683
+ return readFileSync(join(distDir, file)).equals(readFileSync(join(versionDir(home, version), file)));
3684
+ } catch {
3685
+ return false;
3686
+ }
3687
+ });
3688
+ }
3689
+ async function setup(context, options) {
3690
+ const { dirs, env, platform, io } = context;
3691
+ assertSafeHome(dirs.home);
3692
+ const distDir = runtimeDistDir();
3693
+ if (distDir === null) throw new NpmVmError("M-ABI", EXIT.problems, "This npm-vm installation ships no runtime build; reinstall the package.");
3694
+ for (const dir of [
3695
+ dirs.home,
3696
+ dirs.state,
3697
+ dirs.cache
3698
+ ]) if (options.dryRun) {
3699
+ if (existsSync(dir)) assertPrivateDir(dir, platform);
3700
+ } else ensurePrivateDir(dir, platform);
3701
+ const report = {
3702
+ home: dirs.home,
3703
+ version: context.version,
3704
+ profiles: [],
3705
+ links: [],
3706
+ skipped: 0
3707
+ };
3708
+ const shells = options.shells ?? defaultShells(env, platform);
3709
+ const targets = options.modifyProfile ? profileTargets(shells, env, platform) : [];
3710
+ const policy = platform === "win32" ? executionPolicy(env) : null;
3711
+ const powershell = platform !== "win32" || powershellAllowsScripts(policy);
3712
+ const plans = targets.map((target) => ({
3713
+ target,
3714
+ guard: profileGuard(target.file, env, platform),
3715
+ edit: planProfileEdit(target, dirs.home)
3716
+ }));
3717
+ const lock = options.dryRun ? null : await acquireLock(dirs.state, context.owner);
3718
+ try {
3719
+ if (lock !== null) recoverDeadJournals(dirs.journal, dirs.machine.id);
3720
+ if (!options.dryRun) {
3721
+ installRuntime(context, distDir);
3722
+ mkdirSync(shimDir(dirs.home), {
3723
+ recursive: true,
3724
+ mode: 493
3725
+ });
3726
+ for (const file of renderShims(dirs.home, platform, { powershell })) mutateFile(dirs.journal, "launcher", context.owner, join(shimDir(dirs.home), file.name), Buffer.from(file.content), file.mode);
3727
+ if (platform === "win32" && !powershell) for (const stale of stalePowershellShims(dirs.home)) mutateFile(dirs.journal, "launcher", context.owner, stale, null);
3728
+ }
3729
+ for (const plan of plans) report.profiles.push(await applyPlan(context, options, plan));
3730
+ if (options.link) for (const install of findNodeInstalls(env, platform)) for (const target of linkTargets(dirs.home, platform, powershell)) if (options.dryRun) report.links.push({
3731
+ target: join(install.binDir, target.name),
3732
+ outcome: "linked",
3733
+ detail: "dry run"
3734
+ });
3735
+ else report.links.push(linkOne(dirs, context.owner, install.binDir, target));
3736
+ if (platform === "win32" && !options.dryRun && options.userPath) {
3737
+ if (addToUserPath(env, shimDir(dirs.home))) {
3738
+ env["Path"] = `${shimDir(dirs.home)};${env["Path"] ?? ""}`;
3739
+ io.err(restartMessage());
3740
+ }
3741
+ if (options.cmdAutorun) setCmdAutoRun(env, shimDir(dirs.home));
3742
+ if (!powershell) io.err(`PowerShell's execution policy (${policy ?? "unknown"}) blocks unsigned scripts, so no .ps1 shims were installed; cmd shims take over.`);
3743
+ }
3744
+ } finally {
3745
+ lock?.release();
3746
+ }
3747
+ report.skipped = report.profiles.filter((entry) => entry.outcome === "printed" || entry.outcome === "declined" || entry.outcome === "changed").length + report.links.filter((link) => [
3748
+ "foreign",
3749
+ "unwritable",
3750
+ "missing"
3751
+ ].includes(link.outcome)).length;
3752
+ if (context.json) io.out(JSON.stringify(report, null, 2));
3753
+ else io.out(describe$1(report));
3754
+ if (!options.dryRun) {
3755
+ if (await doctor(context, { quiet: context.json }) !== EXIT.ok && options.strict) return EXIT.partial;
3756
+ }
3757
+ return options.strict && report.skipped > 0 ? EXIT.partial : EXIT.ok;
3758
+ }
3759
+ /** Installs this version's runtime and launcher unless the identical files are already in place. */
3760
+ function installRuntime(context, distDir) {
3761
+ const { dirs, version } = context;
3762
+ if (!sameFiles(dirs.home, version, distDir)) installVersion(dirs.home, version, distDir, 1);
3763
+ sweepReplaced(dirs.home);
3764
+ if (readCurrent(dirs.home) !== version) mutateFile(dirs.journal, "launcher", context.owner, join(dirs.home, "current"), Buffer.from(`${version}\n`));
3765
+ const bootstrap = readFileSync(join(versionDir(dirs.home, version), BOOTSTRAP_FILE));
3766
+ const installed = existsSync(join(dirs.home, "bootstrap.cjs")) ? readFileSync(join(dirs.home, BOOTSTRAP_FILE)) : null;
3767
+ if (installed === null || !installed.equals(bootstrap)) mutateFile(dirs.journal, "launcher", context.owner, join(dirs.home, BOOTSTRAP_FILE), bootstrap, 420);
3768
+ }
3769
+ async function applyPlan(context, options, plan) {
3770
+ const { io, dirs } = context;
3771
+ const { target } = plan;
3772
+ if (plan.guard !== null) {
3773
+ io.out(`Not editing ${target.file}: ${plan.guard}. Add this yourself:\n${profileBlock(target.shell, dirs.home, target.parts)}`);
3774
+ return {
3775
+ file: target.file,
3776
+ outcome: "printed",
3777
+ reason: plan.guard
3778
+ };
3779
+ }
3780
+ if (plan.edit === null) return {
3781
+ file: target.file,
3782
+ outcome: "unchanged"
3783
+ };
3784
+ if (options.dryRun) {
3785
+ io.out(`Would edit ${target.file}:\n${plan.edit.diff}`);
3786
+ return {
3787
+ file: target.file,
3788
+ outcome: "dry-run"
3789
+ };
3790
+ }
3791
+ if (!options.yes) {
3792
+ io.out(plan.edit.diff);
3793
+ if (!await io.confirm(`Apply this change to ${target.file}?`)) {
3794
+ io.out(`Skipped ${target.file}. Add this yourself:\n${profileBlock(target.shell, dirs.home, target.parts)}`);
3795
+ return {
3796
+ file: target.file,
3797
+ outcome: "declined"
3798
+ };
3799
+ }
3800
+ }
3801
+ const guard = profileGuard(target.file, context.env, context.platform);
3802
+ const fresh = planProfileEdit(target, dirs.home);
3803
+ if (fresh === null) return {
3804
+ file: target.file,
3805
+ outcome: "unchanged"
3806
+ };
3807
+ if (guard !== null || fresh.before !== plan.edit.before && !options.yes) {
3808
+ io.out(`${target.file} changed while the prompt was open; run setup again to review the new change.`);
3809
+ return {
3810
+ file: target.file,
3811
+ outcome: "changed"
3812
+ };
3813
+ }
3814
+ applyProfileEdit(dirs, context.owner, fresh);
3815
+ return {
3816
+ file: target.file,
3817
+ outcome: "written"
3818
+ };
3819
+ }
3820
+ function describe$1(report) {
3821
+ const lines = [`npm-vm ${report.version} installed in ${report.home}`];
3822
+ for (const profile of report.profiles) lines.push(` ${profile.file}: ${profile.outcome}${profile.reason ? ` (${profile.reason})` : ""}`);
3823
+ for (const link of report.links) lines.push(` ${link.target}: ${link.outcome}${link.detail ? ` (${link.detail})` : ""}`);
3824
+ return lines.join("\n");
3825
+ }
3826
+ //#endregion
3827
+ //#region src/commands/uninstall.ts
3828
+ /** Removes the cache's directories only while empty: an npm publishing right now keeps its entry. */
3829
+ function removeEmptyTree(cache) {
3830
+ for (const dir of [
3831
+ join(cache, "npm"),
3832
+ stagingDir(cache),
3833
+ cache
3834
+ ]) try {
3835
+ rmdirSync(dir);
3836
+ } catch {}
3837
+ }
3838
+ async function uninstall(context, options) {
3839
+ const { dirs, env, platform, io } = context;
3840
+ if (!options.yes) {
3841
+ if (!await io.confirm(`Remove npm-vm from ${dirs.home}, its profile blocks and links${options.purgeCache ? ", and the cache" : ""}?`)) {
3842
+ io.out("Nothing removed.");
3843
+ return EXIT.ok;
3844
+ }
3845
+ }
3846
+ const report = {
3847
+ profiles: [],
3848
+ links: [],
3849
+ homeRemoved: false,
3850
+ cacheRemoved: false,
3851
+ cacheKept: []
3852
+ };
3853
+ const lock = await acquireLock(dirs.state, context.owner);
3854
+ let clean = true;
3855
+ try {
3856
+ recoverDeadJournals(dirs.journal, dirs.machine.id);
3857
+ for (const entry of readProfiles(dirs).entries) {
3858
+ const outcome = removeProfileBlock(dirs, context.owner, entry);
3859
+ report.profiles.push({
3860
+ file: entry.file,
3861
+ outcome
3862
+ });
3863
+ if (outcome === "changed") {
3864
+ clean = false;
3865
+ io.err(changedMessage(entry.file, "npm-vm block"));
3866
+ }
3867
+ }
3868
+ for (const entry of readLinks(dirs).entries) {
3869
+ const outcome = unlinkOne(dirs, context.owner, entry);
3870
+ report.links.push({
3871
+ target: entry.target,
3872
+ outcome
3873
+ });
3874
+ if (outcome === "changed") {
3875
+ clean = false;
3876
+ io.err(changedMessage(entry.target, "shim"));
3877
+ }
3878
+ }
3879
+ if (platform === "win32") {
3880
+ removeFromUserPath(env, shimDir(dirs.home));
3881
+ clearCmdAutoRun(env);
3882
+ }
3883
+ if (options.purgeCache) {
3884
+ const cacheLock = await acquireLock(dirs.cache, context.owner);
3885
+ try {
3886
+ const pruned = pruneCache(dirs.cache, context.owner, { maxAgeMs: 0 });
3887
+ report.cacheKept = pruned.kept.concat(pruned.skipped);
3888
+ if (report.cacheKept.length === 0) {
3889
+ removeEmptyTree(dirs.cache);
3890
+ report.cacheRemoved = true;
3891
+ }
3892
+ } finally {
3893
+ cacheLock.release();
3894
+ }
3895
+ if (report.cacheRemoved) removeEmptyTree(dirs.cache);
3896
+ if (report.cacheKept.length > 0) {
3897
+ clean = false;
3898
+ io.err(`Cache kept: ${report.cacheKept.join(", ")} ${report.cacheKept.length === 1 ? "is" : "are"} in use by a running npm.`);
3899
+ }
3900
+ }
3901
+ const tombstone = `${dirs.home}.removing.${process.pid}.${Date.now()}`;
3902
+ atomicRename(dirs.home, tombstone);
3903
+ removeWithRetry(tombstone);
3904
+ report.homeRemoved = true;
3905
+ } finally {
3906
+ lock.release();
3907
+ }
3908
+ io.out(context.json ? JSON.stringify(report, null, 2) : describe(report, dirs.cache));
3909
+ return clean ? EXIT.ok : EXIT.problems;
3910
+ }
3911
+ function describe(report, cache) {
3912
+ const lines = ["npm-vm removed."];
3913
+ for (const profile of report.profiles) lines.push(` ${profile.file}: ${profile.outcome}`);
3914
+ for (const link of report.links) lines.push(` ${link.target}: ${link.outcome}`);
3915
+ if (report.cacheRemoved) lines.push(` cache removed (${cache})`);
3916
+ else if (report.cacheKept.length > 0) lines.push(` cache kept at ${cache}: ${report.cacheKept.join(", ")} in use by a running npm`);
3917
+ else lines.push(` cache kept at ${cache}; add --purge-cache to remove it`);
3918
+ return lines.join("\n");
3919
+ }
3920
+ //#endregion
3921
+ //#region src/commands/dispatch.ts
3922
+ const VERSION = readSelfVersion();
3923
+ const USAGE = `npm-vm v${VERSION}: run the npm version a project declares in devEngines
3924
+
3925
+ Usage:
3926
+ npm-vm <command> [options]
3927
+
3928
+ Commands:
3929
+ setup Install the launcher, shims and shell snippets; --link also replaces npm
3930
+ inside every nvm/fnm Node. --shell <s> (repeatable), --yes, --dry-run,
3931
+ --no-modify-profile, --cmd-autorun and --no-user-path (Windows), --strict (exit 2 on skips).
3932
+ init <shell> Print the snippet for zsh, bash, fish or powershell (--path, --hook; default both).
3933
+ doctor Check the installation; exit 1 when something needs fixing.
3934
+ which Print the npm that would run for the current directory.
3935
+ pin [<range>] Write devEngines.packageManager for npm into the project's package.json.
3936
+ install [<spec>] Download and verify the project's npm, or <spec>, into the cache.
3937
+ cache list List cached npm versions.
3938
+ cache prune Remove unused entries; --older-than <days> (default 90).
3939
+ self-update Update npm-vm with provenance verification; --rollback restores the previous version.
3940
+ recover Finish a journal left by another machine: --journal <id> --confirm-dead-machine <id>.
3941
+ uninstall Remove npm-vm (--yes to skip the prompt, --purge-cache to drop the cache too).
3942
+
3943
+ Global options:
3944
+ --json Machine-readable output where a command offers it.
3945
+ --version, -v Print the version.
3946
+ --help, -h Print this help.
3947
+
3948
+ Environment:
3949
+ NPM_VM_VERSION Use this range or version instead of the project's.
3950
+ NPM_VM_DISABLE Any value: always run Node's own npm.
3951
+ NPM_VM_OFFLINE Any value: never contact the registry.
3952
+ NPM_VM_HOME The home directory (setup only).
3953
+
3954
+ Exit codes: 0 ok, 1 problems found, 2 partial setup under --strict, 3 resolution failed, 4 transport
3955
+ unavailable, 5 verification failed, 6 launcher mismatch, 64 usage error.`;
3956
+ function parse(argv) {
3957
+ return parseArgs({
3958
+ args: argv,
3959
+ allowPositionals: true,
3960
+ allowNegative: true,
3961
+ options: {
3962
+ json: { type: "boolean" },
3963
+ path: { type: "boolean" },
3964
+ hook: { type: "boolean" },
3965
+ link: { type: "boolean" },
3966
+ shell: {
3967
+ type: "string",
3968
+ multiple: true
3969
+ },
3970
+ yes: { type: "boolean" },
3971
+ "dry-run": { type: "boolean" },
3972
+ "modify-profile": { type: "boolean" },
3973
+ "cmd-autorun": { type: "boolean" },
3974
+ "user-path": { type: "boolean" },
3975
+ strict: { type: "boolean" },
3976
+ "older-than": { type: "string" },
3977
+ rollback: { type: "boolean" },
3978
+ journal: { type: "string" },
3979
+ "confirm-dead-machine": { type: "string" },
3980
+ "purge-cache": { type: "boolean" },
3981
+ bundle: { type: "string" },
3982
+ "tuf-cache": { type: "string" },
3983
+ version: {
3984
+ type: "boolean",
3985
+ short: "v"
3986
+ },
3987
+ help: {
3988
+ type: "boolean",
3989
+ short: "h"
3990
+ }
3991
+ }
3992
+ });
3993
+ }
3994
+ /** Command-specific options, so a flag meant for another command is an error rather than silently ignored. */
3995
+ const COMMAND_OPTIONS = {
3996
+ setup: [
3997
+ "link",
3998
+ "shell",
3999
+ "yes",
4000
+ "dry-run",
4001
+ "modify-profile",
4002
+ "cmd-autorun",
4003
+ "user-path",
4004
+ "strict"
4005
+ ],
4006
+ init: ["path", "hook"],
4007
+ doctor: [],
4008
+ which: [],
4009
+ pin: [],
4010
+ install: [],
4011
+ cache: ["older-than"],
4012
+ "self-update": ["rollback"],
4013
+ recover: ["journal", "confirm-dead-machine"],
4014
+ uninstall: ["yes", "purge-cache"],
4015
+ "verify-attestation": ["bundle", "tuf-cache"]
4016
+ };
4017
+ function misplacedOptions(command, values) {
4018
+ const allowed = new Set(COMMAND_OPTIONS[command] ?? []);
4019
+ return Object.values(COMMAND_OPTIONS).flat().filter((option) => !allowed.has(option) && values[option] !== void 0).map((option) => `--${option}`);
4020
+ }
4021
+ async function run(argv, options) {
4022
+ let parsed;
4023
+ try {
4024
+ parsed = parse(argv);
4025
+ } catch (error) {
4026
+ console.error(error.message);
4027
+ console.error(USAGE);
4028
+ return EXIT.usage;
4029
+ }
4030
+ const { values, positionals } = parsed;
4031
+ if (values.version) {
4032
+ console.log(VERSION);
4033
+ return EXIT.ok;
4034
+ }
4035
+ const command = positionals[0];
4036
+ if (values.help || command === void 0) {
4037
+ console.log(USAGE);
4038
+ return command === void 0 && !values.help ? EXIT.usage : EXIT.ok;
4039
+ }
4040
+ if (COMMAND_OPTIONS[command] === void 0) {
4041
+ console.error(`Unknown command: ${command}`);
4042
+ console.error(USAGE);
4043
+ return EXIT.usage;
4044
+ }
4045
+ const misplaced = misplacedOptions(command, values);
4046
+ if (misplaced.length > 0) {
4047
+ console.error(`${misplaced.join(", ")} cannot be used with ${command}`);
4048
+ return EXIT.usage;
4049
+ }
4050
+ const context = createCommandContext({
4051
+ ...options.home === void 0 ? {} : { home: options.home },
4052
+ ...options.env === void 0 ? {} : { env: options.env },
4053
+ ...options.io === void 0 ? {} : { io: options.io },
4054
+ json: values.json ?? false,
4055
+ version: VERSION
4056
+ });
4057
+ try {
4058
+ return await dispatch(command, positionals.slice(1), values, context, options);
4059
+ } catch (error) {
4060
+ return reportError(error, context);
4061
+ }
4062
+ }
4063
+ async function dispatch(command, rest, values, context, options) {
4064
+ switch (command) {
4065
+ case "setup": {
4066
+ const shells = values.shell ?? null;
4067
+ const bad = shells?.filter((shell) => !isShell(shell)) ?? [];
4068
+ if (bad.length > 0) {
4069
+ context.io.err(`--shell needs one of: ${SHELLS.join(", ")} (got ${bad.join(", ")})`);
4070
+ return EXIT.usage;
4071
+ }
4072
+ return setup(context, {
4073
+ link: values.link ?? false,
4074
+ shells,
4075
+ yes: values.yes ?? false,
4076
+ dryRun: values["dry-run"] ?? false,
4077
+ modifyProfile: values["modify-profile"] ?? true,
4078
+ cmdAutorun: values["cmd-autorun"] ?? false,
4079
+ strict: values.strict ?? false,
4080
+ userPath: values["user-path"] ?? true
4081
+ });
4082
+ }
4083
+ case "init": {
4084
+ const shell = rest[0];
4085
+ if (shell === void 0 || !isShell(shell)) {
4086
+ context.io.err(`init needs one of: ${SHELLS.join(", ")}`);
4087
+ return EXIT.usage;
4088
+ }
4089
+ const both = values.path !== true && values.hook !== true;
4090
+ process.stdout.write(profileBlock(shell, context.dirs.home, {
4091
+ path: both || values.path === true,
4092
+ hook: both || values.hook === true
4093
+ }));
4094
+ return EXIT.ok;
4095
+ }
4096
+ case "doctor": return doctor(context);
4097
+ case "which": {
4098
+ const result = await resolveNpm([], process.cwd(), {
4099
+ home: context.dirs.home,
4100
+ ...options.env === void 0 ? {} : { env: options.env }
4101
+ });
4102
+ context.io.out(context.json ? JSON.stringify(result, null, 2) : describeWhich(result));
4103
+ return EXIT.ok;
4104
+ }
4105
+ case "pin": return pin(context, rest[0]);
4106
+ case "install": return install(context, rest[0]);
4107
+ case "cache":
4108
+ if (rest[0] === "list") return cacheList(context);
4109
+ if (rest[0] === "prune") return cachePrune(context, values["older-than"] === void 0 ? 90 : Number(values["older-than"]));
4110
+ context.io.err("cache needs list or prune");
4111
+ return EXIT.usage;
4112
+ case "self-update": return selfUpdate(context, { rollback: values.rollback ?? false });
4113
+ case "recover":
4114
+ if (values.journal === void 0 || values["confirm-dead-machine"] === void 0) {
4115
+ context.io.err("recover needs --journal <id> and --confirm-dead-machine <machine-id>");
4116
+ return EXIT.usage;
4117
+ }
4118
+ return recover(context, values.journal, values["confirm-dead-machine"]);
4119
+ case "verify-attestation": {
4120
+ if (values.bundle === void 0 || values["tuf-cache"] === void 0) return EXIT.usage;
4121
+ const { verifyBundleHere } = await import("./provenance-DDegWCyS.js");
4122
+ await verifyBundleHere(JSON.parse(readFileSync(values.bundle, "utf8")), values["tuf-cache"]);
4123
+ return EXIT.ok;
4124
+ }
4125
+ case "uninstall": return uninstall(context, {
4126
+ yes: values.yes ?? false,
4127
+ purgeCache: values["purge-cache"] ?? false
4128
+ });
4129
+ default: return EXIT.usage;
4130
+ }
4131
+ }
4132
+ function describeWhich(result) {
4133
+ const where = result.kind === "own" ? "Node's own npm" : result.kind === "cached" ? "cached" : "to download";
4134
+ const from = result.source === null ? "no project requirement" : `${result.source.field} in ${result.source.file}`;
4135
+ const lines = [`npm ${result.version} (${where}): ${result.cliPath}`, `Source: ${from}`];
4136
+ if (result.warning !== null) lines.push(`Warning: ${result.warning}`);
4137
+ return lines.join("\n");
4138
+ }
4139
+ function reportError(error, context) {
4140
+ if (isNpmVmError(error)) {
4141
+ context.io.err(`npm-vm: ${error.message}`);
4142
+ return error.exitCode;
4143
+ }
4144
+ context.io.err(`npm-vm: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
4145
+ return EXIT.problems;
4146
+ }
4147
+ /** The build injects the package version; running from source reads the manifest instead. */
4148
+ function readSelfVersion() {
4149
+ return "0.1.0";
4150
+ }
4151
+ //#endregion
4152
+ //#region src/cli.ts
4153
+ process.exitCode = await run(process.argv.slice(2), {});
4154
+ //#endregion
4155
+ export {};
4156
+
4157
+ //# sourceMappingURL=cli.js.map