vouchington-tooling 0.0.4 → 0.0.6

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 (52) hide show
  1. package/README.md +10 -0
  2. package/dist/cli/commands/allocate-browser-safe-ports.slot-fixtures.test-helpers.d.mts +6 -0
  3. package/dist/cli/commands/allocate-browser-safe-ports.slot-fixtures.test-helpers.mjs +103 -0
  4. package/dist/cli/commands/github-output.test-helpers.d.mts +1 -0
  5. package/dist/cli/commands/github-output.test-helpers.mjs +32 -0
  6. package/dist/cli/commands/pnpm-install.d.mts +1 -0
  7. package/dist/cli/commands/pnpm-install.mjs +27 -0
  8. package/dist/cli/commands/spawn-script.d.mts +5 -0
  9. package/dist/cli/commands/spawn-script.mjs +7 -0
  10. package/dist/cli/commands/vitest-blob-manifest.d.mts +1 -0
  11. package/dist/cli/commands/vitest-blob-manifest.mjs +11 -0
  12. package/dist/cli/index.mjs +28 -0
  13. package/dist/cli/parse.d.mts +11 -0
  14. package/dist/cli/parse.mjs +14 -0
  15. package/dist/cli/script-path.d.mts +1 -0
  16. package/dist/cli/script-path.mjs +4 -0
  17. package/dist/cli/usage.d.mts +1 -1
  18. package/dist/cli/usage.mjs +16 -1
  19. package/dist/index.d.mts +6 -0
  20. package/dist/index.mjs +3 -0
  21. package/dist/pnpm-install/exec.d.mts +8 -0
  22. package/dist/pnpm-install/exec.mjs +88 -0
  23. package/dist/pnpm-install/index.d.mts +7 -0
  24. package/dist/pnpm-install/index.mjs +6 -0
  25. package/dist/pnpm-install/metadata.d.mts +5 -0
  26. package/dist/pnpm-install/metadata.mjs +91 -0
  27. package/dist/pnpm-install/pnpm-install-fixture.test-helpers.d.mts +22 -0
  28. package/dist/pnpm-install/pnpm-install-fixture.test-helpers.mjs +174 -0
  29. package/dist/pnpm-install/process.d.mts +11 -0
  30. package/dist/pnpm-install/process.mjs +81 -0
  31. package/dist/pnpm-install/release-age.d.mts +9 -0
  32. package/dist/pnpm-install/release-age.mjs +59 -0
  33. package/dist/pnpm-install/runner.d.mts +2 -0
  34. package/dist/pnpm-install/runner.mjs +106 -0
  35. package/dist/pnpm-install/support.d.mts +30 -0
  36. package/dist/pnpm-install/support.mjs +143 -0
  37. package/dist/shared-context/fake-git.test-helpers.d.mts +11 -0
  38. package/dist/shared-context/fake-git.test-helpers.mjs +59 -0
  39. package/dist/shared-context/index.d.mts +26 -0
  40. package/dist/shared-context/index.mjs +89 -0
  41. package/dist/vitest-blob-manifest/cli.d.mts +2 -0
  42. package/dist/vitest-blob-manifest/cli.mjs +33 -0
  43. package/dist/vitest-blob-manifest/index.d.mts +37 -0
  44. package/dist/vitest-blob-manifest/index.mjs +134 -0
  45. package/package.json +16 -1
  46. package/scripts/allocate-browser-safe-ports.py +661 -0
  47. package/scripts/fetch-forbidden-ports.json +7 -0
  48. package/scripts/gha/check-needs-results.sh +19 -0
  49. package/scripts/gha/download-with-diagnostics.sh +52 -0
  50. package/scripts/gha/host-pressure-diagnostics.sh +179 -0
  51. package/scripts/gha/write-github-multiline-output.sh +60 -0
  52. package/scripts/runner-port-policy.json +7 -0
@@ -0,0 +1,174 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { mkdtemp, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { runPnpmInstallCli } from '../cli/commands/pnpm-install.mjs';
7
+ const execFileAsync = promisify(execFile);
8
+ async function writeJson(path, value) {
9
+ await mkdir(dirname(path), { recursive: true });
10
+ await writeFile(path, `${JSON.stringify(value)}\n`);
11
+ }
12
+ export async function makeFixture() {
13
+ const root = await mkdtemp(join(tmpdir(), 'pnpm-install-'));
14
+ const consumer = join(root, 'packages', 'consumer');
15
+ const dependency = join(root, 'packages', 'dependency');
16
+ const dependencyLink = join(consumer, 'node_modules', '@fixture', 'dependency');
17
+ const pnpmBin = join(root, 'bin');
18
+ const pnpmLog = join(root, 'pnpm.log');
19
+ const summary = join(root, 'summary.md');
20
+ await Promise.all([
21
+ writeJson(join(root, 'package.json'), { name: 'fixture-root', private: true }),
22
+ writeFile(join(root, 'pnpm-workspace.yaml'), 'packages:\n - packages/*\nminimumReleaseAge: 2880\n'),
23
+ writeJson(join(consumer, 'package.json'), {
24
+ name: '@fixture/consumer',
25
+ dependencies: { '@fixture/dependency': 'workspace:^' },
26
+ }),
27
+ writeJson(join(dependency, 'package.json'), { name: '@fixture/dependency', version: '1.0.0' }),
28
+ mkdir(dirname(dependencyLink), { recursive: true }),
29
+ mkdir(pnpmBin),
30
+ ]);
31
+ await symlink(dependency, dependencyLink, 'dir');
32
+ await writeFile(join(pnpmBin, 'pnpm'), `#!/usr/bin/env bash
33
+ set -euo pipefail
34
+ if [ "\${1:-}" = m ]; then
35
+ if [ -n "\${PNPM_LIST_WARNING:-}" ]; then printf '%s\\n' "$PNPM_LIST_WARNING" >&2; fi
36
+ printf '%s\\n' "$PNPM_WORKSPACES_JSON"
37
+ exit 0
38
+ fi
39
+ if [ "\${1:-}" = --version ]; then
40
+ printf '%s\\n' "\${PNPM_VERSION:-11.0.0}"
41
+ exit 0
42
+ fi
43
+ printf '%s\\n' "$*" >> "$PNPM_LOG"
44
+ calls=0
45
+ if [ -f "$PNPM_CALLS" ]; then calls="$(cat "$PNPM_CALLS")"; fi
46
+ calls=$((calls + 1))
47
+ printf '%s' "$calls" > "$PNPM_CALLS"
48
+ print_release_age_violation() {
49
+ printf '%s\\n' '✗ Lockfile failed supply-chain policy check (1 entries in 0.1s)'
50
+ printf '%s\\n' '[ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION] 1 lockfile entries failed verification:'
51
+ printf '%s\\n' ' undici@8.10.0 was published at 2026-08-03T15:06:33.000Z, within the minimumReleaseAge cutoff (2026-08-02T04:48:10.357Z)'
52
+ }
53
+ if [ "\${PNPM_FAIL_CALL:-0}" = "$calls" ]; then
54
+ if [ -n "\${PNPM_FAIL_RELEASE_AGE:-}" ]; then print_release_age_violation; fi
55
+ exit "\${PNPM_FAIL_CODE:-1}"
56
+ fi
57
+ if [ "\${PNPM_FAIL_RELEASE_AGE_CALL:-0}" = "$calls" ]; then
58
+ print_release_age_violation
59
+ exit 1
60
+ fi
61
+ if [ -n "\${PNPM_SLEEP_SECONDS:-}" ]; then sleep "$PNPM_SLEEP_SECONDS"; fi
62
+ case " $* " in
63
+ *' --force '*)
64
+ if [ "\${PNPM_REPAIR_LINK:-0}" = 1 ]; then
65
+ mkdir -p "$(dirname "$PNPM_DEPENDENCY_LINK")"
66
+ rm -f "$PNPM_DEPENDENCY_LINK"
67
+ ln -s "$PNPM_DEPENDENCY" "$PNPM_DEPENDENCY_LINK"
68
+ fi
69
+ ;;
70
+ esac
71
+ `);
72
+ await execFileAsync('chmod', ['+x', join(pnpmBin, 'pnpm')]);
73
+ const workspaces = [
74
+ { name: 'fixture-root', path: root },
75
+ { name: '@fixture/consumer', path: consumer },
76
+ { name: '@fixture/dependency', path: dependency },
77
+ ];
78
+ const env = {
79
+ ...process.env,
80
+ GITHUB_STEP_SUMMARY: summary,
81
+ PATH: `${pnpmBin}:${process.env.PATH ?? ''}`,
82
+ PNPM_CALLS: join(root, 'pnpm.calls'),
83
+ PNPM_DEPENDENCY: dependency,
84
+ PNPM_DEPENDENCY_LINK: dependencyLink,
85
+ PNPM_LOG: pnpmLog,
86
+ PNPM_REPAIR_LINK: '0',
87
+ PNPM_WORKSPACES_JSON: JSON.stringify(workspaces),
88
+ };
89
+ return {
90
+ consumer,
91
+ dependency,
92
+ dependencyLink,
93
+ env,
94
+ pnpmLog,
95
+ root,
96
+ summary,
97
+ };
98
+ }
99
+ export async function runInstaller(fixture, options = {}) {
100
+ const lifecycle = options.lifecycle ?? 'persistent';
101
+ const installScripts = options.installScripts ?? true;
102
+ const args = [
103
+ '--runner-lifecycle',
104
+ lifecycle,
105
+ '--install-scripts',
106
+ String(installScripts),
107
+ '--command-timeout-seconds',
108
+ String(options.commandTimeoutSeconds ?? 0),
109
+ '--max-attempts',
110
+ String(options.maxAttempts ?? 1),
111
+ ];
112
+ if (options.selectors !== undefined)
113
+ args.push('--ephemeral-workspaces', options.selectors);
114
+ const previousCwd = process.cwd();
115
+ const previousEnv = { ...process.env };
116
+ const stdoutChunks = [];
117
+ const stderrChunks = [];
118
+ const write = (chunk, sink) => {
119
+ sink.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString());
120
+ return true;
121
+ };
122
+ const stdout = process.stdout.write.bind(process.stdout);
123
+ const stderr = process.stderr.write.bind(process.stderr);
124
+ const warn = console.warn;
125
+ const error = console.error;
126
+ process.stdout.write = ((chunk) => write(chunk, stdoutChunks));
127
+ process.stderr.write = ((chunk) => write(chunk, stderrChunks));
128
+ console.warn = (...values) => {
129
+ stderrChunks.push(values.map(String).join(' '));
130
+ };
131
+ console.error = (...values) => {
132
+ stderrChunks.push(values.map(String).join(' '));
133
+ };
134
+ process.chdir(fixture.root);
135
+ Object.assign(process.env, fixture.env);
136
+ try {
137
+ const code = await runPnpmInstallCli(args);
138
+ const result = { stdout: stdoutChunks.join(''), stderr: stderrChunks.join('') };
139
+ if (code !== 0) {
140
+ throw Object.assign(new Error(result.stderr || 'pnpm-install failed'), {
141
+ code,
142
+ stdout: result.stdout,
143
+ stderr: result.stderr,
144
+ });
145
+ }
146
+ return result;
147
+ }
148
+ finally {
149
+ process.stdout.write = stdout;
150
+ process.stderr.write = stderr;
151
+ console.warn = warn;
152
+ console.error = error;
153
+ process.chdir(previousCwd);
154
+ for (const key of Object.keys(process.env)) {
155
+ if (!(key in previousEnv))
156
+ delete process.env[key];
157
+ }
158
+ Object.assign(process.env, previousEnv);
159
+ }
160
+ }
161
+ export async function installCalls(fixture) {
162
+ try {
163
+ return (await readFile(fixture.pnpmLog, 'utf8')).trim().split('\n').filter(Boolean);
164
+ }
165
+ catch {
166
+ return [];
167
+ }
168
+ }
169
+ export async function resetInstallCalls(fixture) {
170
+ await Promise.all([
171
+ rm(fixture.pnpmLog, { force: true }),
172
+ rm(fixture.env.PNPM_CALLS, { force: true }),
173
+ ]);
174
+ }
@@ -0,0 +1,11 @@
1
+ export declare const INSTALL_TERMINATION_FAILED = -1;
2
+ export declare function safeProcessGroup(pid: number | undefined): number | undefined;
3
+ export declare function installExitCode(code: number | null, timedOut: boolean): number;
4
+ export declare function startInstallHeartbeat(): NodeJS.Timeout;
5
+ export type ProcessGroupSupervisor = {
6
+ isAlive: (processGroup: number) => boolean;
7
+ signal: (processGroup: number, signal: NodeJS.Signals) => void;
8
+ waitForExit: (processGroup: number, timeoutMs: number) => Promise<boolean>;
9
+ };
10
+ export declare function terminateProcessGroup(processGroup: number, supervisor?: ProcessGroupSupervisor): Promise<boolean>;
11
+ export declare function terminateSafeProcessGroup(pid: number | undefined): Promise<boolean>;
@@ -0,0 +1,81 @@
1
+ import { scheduler } from 'node:timers/promises';
2
+ const TERM_GRACE_SECONDS = 10;
3
+ const KILL_GRACE_SECONDS = 10;
4
+ const HEARTBEAT_SECONDS = 30;
5
+ export const INSTALL_TERMINATION_FAILED = -1;
6
+ export function safeProcessGroup(pid) {
7
+ return typeof pid === 'number' && Number.isSafeInteger(pid) && pid > 0 ? pid : undefined;
8
+ }
9
+ export function installExitCode(code, timedOut) {
10
+ return timedOut ? 1 : (code ?? 1);
11
+ }
12
+ export function startInstallHeartbeat() {
13
+ const started = performance.now();
14
+ const heartbeat = setInterval(() => {
15
+ console.warn(`pnpm install still running after ${Math.floor((performance.now() - started) / 1000)}s`);
16
+ }, HEARTBEAT_SECONDS * 1000);
17
+ heartbeat.unref();
18
+ return heartbeat;
19
+ }
20
+ function processGroupExists(processGroup) {
21
+ try {
22
+ // oxlint-disable-next-line no-restricted-properties -- the installer must verify its detached process group has stopped
23
+ process.kill(-processGroup, 0);
24
+ return true;
25
+ }
26
+ catch (error) {
27
+ return error.code !== 'ESRCH';
28
+ }
29
+ }
30
+ async function waitForProcessGroupExit(processGroup, timeoutMs) {
31
+ const deadline = performance.now() + timeoutMs;
32
+ while (processGroupExists(processGroup)) {
33
+ /* v8 ignore next -- a live group that outlasts both grace windows is host-specific */
34
+ if (performance.now() >= deadline)
35
+ return false;
36
+ await scheduler.wait(100);
37
+ }
38
+ return true;
39
+ }
40
+ const processGroupSupervisor = {
41
+ isAlive: processGroupExists,
42
+ signal(processGroup, signal) {
43
+ // oxlint-disable-next-line no-restricted-properties -- the installer must terminate the detached process group before retrying
44
+ process.kill(-processGroup, signal);
45
+ },
46
+ waitForExit: waitForProcessGroupExit,
47
+ };
48
+ export async function terminateProcessGroup(processGroup, supervisor = processGroupSupervisor) {
49
+ const signal = (value) => {
50
+ try {
51
+ supervisor.signal(processGroup, value);
52
+ return true;
53
+ }
54
+ catch (error) {
55
+ if (error.code === 'ESRCH')
56
+ return false;
57
+ throw error;
58
+ }
59
+ };
60
+ if (!supervisor.isAlive(processGroup))
61
+ return true;
62
+ console.warn(`pnpm install process group ${processGroup} exceeded its deadline; sending TERM`);
63
+ if (!signal('SIGTERM'))
64
+ return true;
65
+ if (await supervisor.waitForExit(processGroup, TERM_GRACE_SECONDS * 1000))
66
+ return true;
67
+ console.warn(`pnpm install process group ${processGroup} ignored TERM; sending KILL`);
68
+ if (!signal('SIGKILL'))
69
+ return true;
70
+ if (await supervisor.waitForExit(processGroup, KILL_GRACE_SECONDS * 1000))
71
+ return true;
72
+ console.error(`pnpm install process group ${processGroup} survived SIGKILL for ${KILL_GRACE_SECONDS}s; refusing to overlap another attempt`);
73
+ return false;
74
+ }
75
+ export function terminateSafeProcessGroup(pid) {
76
+ const processGroup = safeProcessGroup(pid);
77
+ if (processGroup !== undefined)
78
+ return terminateProcessGroup(processGroup);
79
+ console.error('pnpm install timed out before a safe child process group was available');
80
+ return Promise.resolve(false);
81
+ }
@@ -0,0 +1,9 @@
1
+ export interface ReleaseAgeViolation {
2
+ cutoff: string;
3
+ packageSpec: string;
4
+ publishedAt: string;
5
+ }
6
+ /** Gate on the stable pnpm error code; detail-line shape is parsed best-effort separately. */
7
+ export declare function isReleaseAgeViolation(log: string): boolean;
8
+ export declare function parseReleaseAgeViolations(log: string): ReleaseAgeViolation[];
9
+ export declare function formatReleaseAgeFailure(label: string, log: string, docsLink?: string): string;
@@ -0,0 +1,59 @@
1
+ import { readFileSync } from 'node:fs';
2
+ // pnpm's supply-chain policy check is permanent-until-timestamp, not transient: retrying an
3
+ // ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION cannot succeed before the flagged release ages past
4
+ // pnpm-workspace.yaml's minimumReleaseAge. Fail those attempts immediately instead of burning
5
+ // retries against a deterministic wall-clock gate.
6
+ const TOKEN = 'ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION';
7
+ // Targets pnpm's stable detail-line wording; if pnpm rephrases it, parseReleaseAgeViolations()
8
+ // falls back to an empty list and formatReleaseAgeFailure() still reports terminal (see below).
9
+ const DETAIL_LINE = /^\s*(?<packageSpec>\S+) was published at (?<publishedAt>\S+), within the minimumReleaseAge cutoff \((?<cutoff>\S+)\)\s*$/gm;
10
+ const defaultDocsLink = process.env['PNPM_INSTALL_DOCS_URL'] ?? 'pnpm minimumReleaseAge';
11
+ // This module loads before `pnpm install` has ever run, so it cannot depend on a package pnpm
12
+ // would install — including yaml. minimumReleaseAge is a single top-level scalar, so a targeted
13
+ // line match avoids needing a YAML parser at all.
14
+ const MINIMUM_RELEASE_AGE_LINE = /^minimumReleaseAge:\s*(?<minutes>\d+)\s*(?:#.*)?$/m;
15
+ /** Gate on the stable pnpm error code; detail-line shape is parsed best-effort separately. */
16
+ export function isReleaseAgeViolation(log) {
17
+ return log.includes(TOKEN);
18
+ }
19
+ export function parseReleaseAgeViolations(log) {
20
+ const violations = [];
21
+ for (const match of log.matchAll(DETAIL_LINE)) {
22
+ const { cutoff, packageSpec, publishedAt } = match.groups ?? {};
23
+ if (cutoff && packageSpec && publishedAt)
24
+ violations.push({ cutoff, packageSpec, publishedAt });
25
+ }
26
+ return violations;
27
+ }
28
+ function workspaceMinimumReleaseAgeMinutes() {
29
+ try {
30
+ const workspace = readFileSync(`${process.cwd()}/pnpm-workspace.yaml`, 'utf8');
31
+ const minutes = MINIMUM_RELEASE_AGE_LINE.exec(workspace)?.groups?.minutes;
32
+ return minutes === undefined ? undefined : Number(minutes);
33
+ }
34
+ catch (error) {
35
+ console.warn(`unable to read minimumReleaseAge from pnpm-workspace.yaml: ${String(error)}`);
36
+ return undefined;
37
+ }
38
+ }
39
+ function eligibleAt(publishedAt, minutes) {
40
+ const published = new Date(publishedAt);
41
+ if (minutes === undefined || Number.isNaN(published.getTime()))
42
+ return undefined;
43
+ return new Date(published.getTime() + minutes * 60_000).toISOString();
44
+ }
45
+ export function formatReleaseAgeFailure(label, log, docsLink = defaultDocsLink) {
46
+ const violations = parseReleaseAgeViolations(log);
47
+ const minutes = workspaceMinimumReleaseAgeMinutes();
48
+ const lines = violations.length > 0
49
+ ? violations.map((violation) => {
50
+ const eligible = eligibleAt(violation.publishedAt, minutes);
51
+ return ` ${violation.packageSpec} published ${violation.publishedAt}${eligible ? `, eligible at ${eligible}` : ''}`;
52
+ })
53
+ : [' (violation details were not present in the captured log)'];
54
+ return [
55
+ `${label} failed: the lockfile has entries that violate pnpm's minimumReleaseAge supply-chain policy. This is not transient and will not pass until the flagged release ages past the cutoff:`,
56
+ ...lines,
57
+ `See ${docsLink}.`,
58
+ ].join('\n');
59
+ }
@@ -0,0 +1,2 @@
1
+ import { type InstallOptions } from './support.mts';
2
+ export declare function runInstallLifecycle(options: InstallOptions): Promise<string>;
@@ -0,0 +1,106 @@
1
+ import { scheduler } from 'node:timers/promises';
2
+ import { persistentDependencyTreeIsCold, persistentMetadataFingerprint, persistentMetadataMatches, writePersistentMetadataStamp, } from './metadata.mjs';
3
+ import { runPnpm } from './exec.mjs';
4
+ import { INSTALL_TERMINATION_FAILED } from './process.mjs';
5
+ import { formatReleaseAgeFailure, isReleaseAgeViolation } from './release-age.mjs';
6
+ import { baseInstallArgs, findWorkspaceLinkMismatches, logWorkspaceLinkMismatches, } from './support.mjs';
7
+ // oxfmt-ignore
8
+ const fail = (message) => { throw new Error(message); };
9
+ async function install(args, options, label) {
10
+ for (let attempt = 1; attempt <= options.maxAttempts; attempt += 1) {
11
+ const attemptResult = await runPnpm(args, options);
12
+ if (attemptResult.code === 0)
13
+ return;
14
+ if (attemptResult.code === INSTALL_TERMINATION_FAILED)
15
+ fail(`${label} could not terminate safely`);
16
+ const combinedOutput = `${attemptResult.output}\n${attemptResult.errorOutput ?? ''}`;
17
+ if (isReleaseAgeViolation(combinedOutput))
18
+ fail(formatReleaseAgeFailure(label, combinedOutput));
19
+ if (attempt < options.maxAttempts) {
20
+ console.warn(`${label} failed (attempt ${attempt}/${options.maxAttempts}); retrying`);
21
+ await scheduler.wait(5000);
22
+ }
23
+ }
24
+ fail(`${label} failed after ${options.maxAttempts} attempt${options.maxAttempts === 1 ? '' : 's'}`);
25
+ }
26
+ function withScriptPolicy(args, installScripts) {
27
+ return installScripts ? args : [...args, '--ignore-scripts'];
28
+ }
29
+ async function reconcileAndFindMismatches(options, runCapture) {
30
+ const forced = ['install', '--frozen-lockfile', '--force', ...baseInstallArgs.slice(2)];
31
+ // oxfmt-ignore
32
+ await install([...forced, '--ignore-scripts', '--ignore-pnpmfile'], options, 'script-free reconciliation');
33
+ // oxfmt-ignore
34
+ await install(withScriptPolicy(forced, options.installScripts), options, 'strict persistent reconciliation');
35
+ return findWorkspaceLinkMismatches(runCapture);
36
+ }
37
+ async function reconcileOrFail(options, runCapture) {
38
+ const remaining = await reconcileAndFindMismatches(options, runCapture);
39
+ if (remaining.length > 0) {
40
+ logWorkspaceLinkMismatches(remaining);
41
+ fail('persistent reconciliation completed with invalid workspace links');
42
+ }
43
+ }
44
+ async function persistent(options) {
45
+ if (options.ephemeralWorkspaces.trim())
46
+ fail('ephemeral-workspaces is only valid for ephemeral runners');
47
+ const runCapture = (args) => runPnpm(args, options, true);
48
+ const fingerprint = await persistentMetadataFingerprint(runCapture, options.installScripts);
49
+ const stamped = await persistentMetadataMatches(fingerprint);
50
+ // An absent tree has nothing to repair, so one ordinary install below matches the
51
+ // reconciled end state. Check first: an install would otherwise make the tree non-cold.
52
+ const cold = !stamped && (await persistentDependencyTreeIsCold());
53
+ if (!stamped && !cold) {
54
+ console.warn('persistent dependency metadata provenance is missing or changed; reconciling');
55
+ await reconcileOrFail(options, runCapture);
56
+ await writePersistentMetadataStamp(fingerprint);
57
+ return 'persistent metadata reconciled';
58
+ }
59
+ if (!stamped)
60
+ console.warn('persistent dependency tree is absent; installing cold');
61
+ await install(withScriptPolicy([...baseInstallArgs], options.installScripts), options, 'ordinary persistent install');
62
+ const stale = await findWorkspaceLinkMismatches(runCapture);
63
+ if (stale.length === 0) {
64
+ if (!stamped)
65
+ await writePersistentMetadataStamp(fingerprint);
66
+ return stamped ? 'persistent ordinary' : 'persistent cold';
67
+ }
68
+ logWorkspaceLinkMismatches(stale);
69
+ await reconcileOrFail(options, runCapture);
70
+ await writePersistentMetadataStamp(fingerprint);
71
+ return 'persistent reconciled';
72
+ }
73
+ // A path selector's `...` (dependency closure) suffix is silently ignored by pnpm unless the
74
+ // path is brace-wrapped, e.g. `{./web}...` — `./web...` installs only `web` itself. Reject the
75
+ // unbraced form outright rather than let it resolve to a smaller-than-intended scope.
76
+ function isUnbracedPathClosureSelector(selector) {
77
+ return /^\.{0,2}\//.test(selector) && selector.endsWith('...') && !selector.startsWith('{');
78
+ }
79
+ async function ephemeral(options) {
80
+ const selectors = options.ephemeralWorkspaces
81
+ .split('\n')
82
+ .map((value) => value.trim())
83
+ .filter(Boolean);
84
+ if (selectors.length === 0)
85
+ fail('ephemeral-workspaces must contain at least one selector');
86
+ if (selectors.some((selector) => selector.startsWith('!') || selector.startsWith('-') || /\s/.test(selector)))
87
+ fail('ephemeral-workspaces selectors must be positive and not flags');
88
+ if (selectors.some(isUnbracedPathClosureSelector))
89
+ fail('ephemeral-workspaces path selectors must be brace-wrapped, e.g. {./web}... — bare ./web... silently drops its workspace dependencies');
90
+ const args = withScriptPolicy([...baseInstallArgs], options.installScripts);
91
+ for (const selector of selectors)
92
+ args.push('--filter', selector);
93
+ await install([...args, '--fail-if-no-match'], options, 'ephemeral filtered install');
94
+ return 'ephemeral filtered';
95
+ }
96
+ async function ephemeralFull(options) {
97
+ if (options.ephemeralWorkspaces.trim())
98
+ fail('ephemeral-workspaces is only valid for filtered ephemeral runners');
99
+ await install(withScriptPolicy([...baseInstallArgs], options.installScripts), options, 'ephemeral full install');
100
+ return 'ephemeral full';
101
+ }
102
+ export function runInstallLifecycle(options) {
103
+ if (options.runnerLifecycle === 'persistent')
104
+ return persistent(options);
105
+ return options.runnerLifecycle === 'ephemeral-full' ? ephemeralFull(options) : ephemeral(options);
106
+ }
@@ -0,0 +1,30 @@
1
+ export type Lifecycle = 'ephemeral' | 'ephemeral-full' | 'persistent';
2
+ export type InstallOptions = {
3
+ commandTimeoutSeconds: number;
4
+ ephemeralWorkspaces: string;
5
+ installScripts: boolean;
6
+ maxAttempts: number;
7
+ runnerLifecycle: Lifecycle;
8
+ };
9
+ export type CommandResult = {
10
+ code: number;
11
+ errorOutput?: string;
12
+ output: string;
13
+ };
14
+ export type CaptureCommand = (args: string[]) => Promise<CommandResult>;
15
+ export type WorkspaceLinkMismatch = {
16
+ actual: string;
17
+ dependency: string;
18
+ expected: string;
19
+ workspace: string;
20
+ };
21
+ export type Workspace = {
22
+ name: string;
23
+ path: string;
24
+ };
25
+ export declare const baseInstallArgs: string[];
26
+ export declare function parseInstallOptions(argv: string[]): InstallOptions;
27
+ export declare function listWorkspaces(runCapture: CaptureCommand): Promise<Workspace[]>;
28
+ export declare function findWorkspaceLinkMismatches(runCapture: CaptureCommand): Promise<WorkspaceLinkMismatch[]>;
29
+ export declare function logWorkspaceLinkMismatches(mismatches: WorkspaceLinkMismatch[]): void;
30
+ export declare function reportGlibcVersionRuntime(report: object | undefined): string;
@@ -0,0 +1,143 @@
1
+ import { readFile, realpath } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ export const baseInstallArgs = [
4
+ 'install',
5
+ '--frozen-lockfile',
6
+ '--prefer-offline',
7
+ '--prod=false',
8
+ '--config.disallow-workspace-cycles=false',
9
+ ];
10
+ const usage = 'usage: vouchington pnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false [--ephemeral-workspaces <newline-separated selectors>] [--command-timeout-seconds <nonnegative integer>] [--max-attempts <positive integer>]';
11
+ const MAX_COMMAND_TIMEOUT_SECONDS = 3600;
12
+ const MAX_ATTEMPTS = 10;
13
+ function fail(message) {
14
+ throw new Error(message);
15
+ }
16
+ function integer(value, name, allowZero, maximum) {
17
+ if (!/^\d+$/.test(value) || (!allowZero && value === '0'))
18
+ fail(`${name} must be ${allowZero ? 'a nonnegative' : 'a positive'} integer`);
19
+ const parsed = Number(value);
20
+ if (!Number.isSafeInteger(parsed) || parsed > maximum)
21
+ fail(`${name} must not exceed ${maximum}`);
22
+ return parsed;
23
+ }
24
+ export function parseInstallOptions(argv) {
25
+ const values = new Map();
26
+ for (let index = 0; index < argv.length; index += 2) {
27
+ const key = argv[index];
28
+ const value = argv[index + 1];
29
+ if (!key?.startsWith('--') || value === undefined || values.has(key))
30
+ fail(usage);
31
+ values.set(key, value);
32
+ }
33
+ const supported = new Set([
34
+ '--command-timeout-seconds',
35
+ '--ephemeral-workspaces',
36
+ '--install-scripts',
37
+ '--max-attempts',
38
+ '--runner-lifecycle',
39
+ ]);
40
+ if ([...values.keys()].some((key) => !supported.has(key)))
41
+ fail(usage);
42
+ const runnerLifecycle = values.get('--runner-lifecycle');
43
+ const installScripts = values.get('--install-scripts');
44
+ if ((runnerLifecycle !== 'ephemeral' &&
45
+ runnerLifecycle !== 'ephemeral-full' &&
46
+ runnerLifecycle !== 'persistent') ||
47
+ (installScripts !== 'true' && installScripts !== 'false'))
48
+ fail(usage);
49
+ return {
50
+ commandTimeoutSeconds: integer(values.get('--command-timeout-seconds') ?? '120', '--command-timeout-seconds', true, MAX_COMMAND_TIMEOUT_SECONDS),
51
+ ephemeralWorkspaces: values.get('--ephemeral-workspaces') ?? '',
52
+ installScripts: installScripts === 'true',
53
+ maxAttempts: integer(values.get('--max-attempts') ?? '3', '--max-attempts', false, MAX_ATTEMPTS),
54
+ runnerLifecycle,
55
+ };
56
+ }
57
+ export async function listWorkspaces(runCapture) {
58
+ const result = await runCapture(['m', 'ls', '--depth=-1', '--json']);
59
+ if (result.code !== 0)
60
+ fail(`pnpm m ls failed: ${result.errorOutput?.trim() || result.output.trim() || 'unknown error'}`);
61
+ let entries;
62
+ try {
63
+ entries = JSON.parse(result.output);
64
+ }
65
+ catch {
66
+ fail('pnpm m ls returned invalid workspace JSON');
67
+ }
68
+ if (!Array.isArray(entries) || entries.length === 0)
69
+ fail('pnpm m ls returned an invalid or empty workspace list');
70
+ const listed = entries.flatMap((entry) => typeof entry === 'object' &&
71
+ entry !== null &&
72
+ typeof entry.name === 'string' &&
73
+ typeof entry.path === 'string'
74
+ ? [entry]
75
+ : []);
76
+ if (listed.length !== entries.length)
77
+ fail('pnpm m ls returned an invalid workspace list');
78
+ return listed;
79
+ }
80
+ async function workspaceDependencies(workspace) {
81
+ const pkg = JSON.parse(await readFile(path.join(workspace.path, 'package.json'), 'utf8'));
82
+ return Object.entries({
83
+ ...pkg.dependencies,
84
+ ...pkg.devDependencies,
85
+ ...pkg.optionalDependencies,
86
+ })
87
+ .flatMap(([name, spec]) => (typeof spec === 'string' ? [{ name, spec }] : []))
88
+ .toSorted((left, right) => left.name.localeCompare(right.name));
89
+ }
90
+ export async function findWorkspaceLinkMismatches(runCapture) {
91
+ const workspaces = await listWorkspaces(runCapture);
92
+ const targets = new Map(await Promise.all(workspaces.map(async (workspace) => [workspace.name, await realpath(workspace.path)])));
93
+ const mismatches = [];
94
+ for (const workspace of workspaces) {
95
+ for (const dependency of await workspaceDependencies(workspace)) {
96
+ if (!dependency.spec.startsWith('workspace:'))
97
+ continue;
98
+ const expected = targets.get(dependency.name);
99
+ const link = path.join(workspace.path, 'node_modules', dependency.name);
100
+ if (!expected) {
101
+ mismatches.push({
102
+ actual: 'unknown workspace',
103
+ dependency: dependency.name,
104
+ expected: dependency.name,
105
+ workspace: workspace.name,
106
+ });
107
+ continue;
108
+ }
109
+ try {
110
+ const actual = await realpath(link);
111
+ if (actual !== expected)
112
+ mismatches.push({
113
+ actual,
114
+ dependency: dependency.name,
115
+ expected,
116
+ workspace: workspace.name,
117
+ });
118
+ }
119
+ catch {
120
+ mismatches.push({
121
+ actual: 'missing or broken link',
122
+ dependency: dependency.name,
123
+ expected,
124
+ workspace: workspace.name,
125
+ });
126
+ }
127
+ }
128
+ }
129
+ return mismatches;
130
+ }
131
+ export function logWorkspaceLinkMismatches(mismatches) {
132
+ for (const mismatch of mismatches)
133
+ console.warn(`workspace link mismatch: ${mismatch.workspace} -> ${mismatch.dependency}; expected ${mismatch.expected}, got ${mismatch.actual}`);
134
+ }
135
+ export function reportGlibcVersionRuntime(report) {
136
+ if (!report || !('header' in report))
137
+ return '';
138
+ const { header } = report;
139
+ if (typeof header !== 'object' || header === null || !('glibcVersionRuntime' in header))
140
+ return '';
141
+ const { glibcVersionRuntime } = header;
142
+ return typeof glibcVersionRuntime === 'string' ? glibcVersionRuntime : '';
143
+ }
@@ -0,0 +1,11 @@
1
+ export type FakeGitOptions = {
2
+ binDir: string;
3
+ isInsideWorkTree?: boolean;
4
+ lsFilesExitCode?: number;
5
+ lsFilesStderr?: string;
6
+ pathPrefix?: string;
7
+ repoRoot?: string;
8
+ trackedFiles?: readonly string[];
9
+ };
10
+ export declare function installFakeGit({ binDir, isInsideWorkTree, lsFilesExitCode, lsFilesStderr, pathPrefix, repoRoot, trackedFiles, }: FakeGitOptions): void;
11
+ export declare function clearFakeGitEnv(): void;