vouchington-tooling 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -326,19 +326,24 @@ policy out of this package.
326
326
  dependency declared by a non-fixture package manifest. Assert dependency membership or placement,
327
327
  or derive a configuration or documentation package spec from that manifest instead.
328
328
 
329
- `dependency-license-policy` keeps legal policy in the consumer. `collectPnpmLicenseReport` creates
330
- an isolated, script-free temporary workspace and store, expands pnpm's supported architectures to
331
- every `os`, `cpu`, and `libc` selector represented in the lockfile, drops every `engines` constraint
332
- from the audit copy of the lockfile, and validates the JSON report. Dropping `engines` keeps
333
- `pnpm fetch` from skipping optional packages that exclude the running Node.js; pnpm 12's `fetch`
334
- ignores `force`, so their licenses would otherwise report as Unknown.
329
+ `dependency-license-policy` keeps legal policy in the consumer. `collectPnpmLicenseReport` returns
330
+ a promise. It creates an isolated, script-free temporary workspace, expands pnpm's supported
331
+ architectures to every `os`, `cpu`, and `libc` selector represented in the lockfile, drops every
332
+ `engines` constraint from the audit copy of the lockfile, and validates the JSON report. Dropping
333
+ `engines` keeps `pnpm fetch` from skipping optional packages that exclude the running Node.js;
334
+ pnpm 12's `fetch` ignores `force`, so their licenses would otherwise report as Unknown.
335
+ Packages are fetched into a dedicated owner-only store under the pnpm cache
336
+ (`dependency-license-audit-store`) so a later audit reuses content-addressed packages instead of
337
+ downloading every platform again, without writing those packages into the developer store.
335
338
  Pass explicit denied SPDX IDs and prefixes, exact aliases, and justified allowlist scopes to
336
339
  `evaluatePnpmLicenseReport`. Unknown, malformed, and custom SPDX references fail closed. Allowlist
337
340
  scopes are either intentionally global or an exact package-name set; the library returns structured
338
341
  violations and does not format CI-provider diagnostics.
339
- When present, the repository `.npmrc` is copied into the owner-private temporary directory so pnpm
340
- can authenticate to the same registries; normal cleanup removes the copy, and the caller remains
341
- responsible for terminating the process normally rather than abandoning temporary audit state.
342
+ When present, the repository `.npmrc` is copied into the owner-private temporary workspace so pnpm
343
+ can authenticate to the same registries. The workspace is removed when the audit finishes, when the
344
+ process receives SIGINT, SIGTERM, or SIGHUP, and on the next audit if the previous process died
345
+ first, including SIGKILL. Each workspace records its owner's PID so a later audit can delete
346
+ directories whose owner is gone.
342
347
 
343
348
  `session-friction` is an opt-in capture and reporting library. Callers supply the session id,
344
349
  absolute log directory, host-independent observation, and journal loader; it does not inspect host
@@ -0,0 +1,28 @@
1
+ export declare const LICENSE_AUDIT_DIRECTORY_PREFIX = "dependency-license-audit-";
2
+ export interface LicenseAuditReclaimOptions {
3
+ readonly graceMs?: number;
4
+ readonly isOwnerAlive?: (pid: number, directoryMtimeMs: number) => boolean;
5
+ readonly now?: number;
6
+ }
7
+ export interface LicenseAuditWorkspaceOptions extends LicenseAuditReclaimOptions {
8
+ readonly directory?: string;
9
+ readonly pid?: number;
10
+ }
11
+ interface OwnerAliveDependencies {
12
+ readonly isProcessAlive?: (pid: number) => boolean;
13
+ readonly readProcessStartMs?: (pid: number) => number | undefined;
14
+ }
15
+ /** Parses `ps -o lstart=` output. Exported for tests of unparseable dates. */
16
+ export declare function parseProcessStart(stdout: string): number | undefined;
17
+ export declare function readProcessStartMs(pid: number): number | undefined;
18
+ export declare function isProcessAlive(pid: number): boolean;
19
+ /**
20
+ * A directory still belongs to its creator when that PID is alive and the process started
21
+ * before the directory. A recycled PID belongs to a newer process and is not the owner.
22
+ */
23
+ export declare function isAuditOwnerAlive(pid: number, directoryMtimeMs: number, dependencies?: OwnerAliveDependencies): boolean;
24
+ export declare function writeAuditPid(directory: string, pid?: number): void;
25
+ export declare function removeAuditDirectory(directory: string): void;
26
+ /** Deletes leftover audit directories whose owner process is gone, including after SIGKILL. */
27
+ export declare function reclaimStaleLicenseAuditDirectories(directory: string, options?: LicenseAuditReclaimOptions): void;
28
+ export {};
@@ -0,0 +1,115 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { chmodSync, lstatSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync, } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ export const LICENSE_AUDIT_DIRECTORY_PREFIX = 'dependency-license-audit-';
5
+ const LICENSE_AUDIT_PID_FILE = 'audit.pid';
6
+ const STALE_AUDIT_GRACE_MS = 60_000;
7
+ const OWNER_START_SLACK_MS = 1_000;
8
+ const MAX_PID = 2_147_483_647;
9
+ function hasCode(error, code) {
10
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === code);
11
+ }
12
+ /** Parses `ps -o lstart=` output. Exported for tests of unparseable dates. */
13
+ export function parseProcessStart(stdout) {
14
+ const parsed = Date.parse(stdout.trim());
15
+ if (Number.isNaN(parsed))
16
+ return undefined;
17
+ return parsed;
18
+ }
19
+ export function readProcessStartMs(pid) {
20
+ const result = spawnSync('ps', ['-p', String(pid), '-o', 'lstart='], { encoding: 'utf8' });
21
+ if (result.status !== 0)
22
+ return undefined;
23
+ return parseProcessStart(result.stdout);
24
+ }
25
+ export function isProcessAlive(pid) {
26
+ if (!Number.isSafeInteger(pid) || pid <= 0 || pid > MAX_PID)
27
+ return false;
28
+ try {
29
+ process.kill(pid, 0);
30
+ return true;
31
+ }
32
+ catch (error) {
33
+ return !hasCode(error, 'ESRCH');
34
+ }
35
+ }
36
+ /**
37
+ * A directory still belongs to its creator when that PID is alive and the process started
38
+ * before the directory. A recycled PID belongs to a newer process and is not the owner.
39
+ */
40
+ export function isAuditOwnerAlive(pid, directoryMtimeMs, dependencies = {}) {
41
+ if (!(dependencies.isProcessAlive ?? isProcessAlive)(pid))
42
+ return false;
43
+ const started = (dependencies.readProcessStartMs ?? readProcessStartMs)(pid);
44
+ if (started === undefined)
45
+ return true;
46
+ return started <= directoryMtimeMs + OWNER_START_SLACK_MS;
47
+ }
48
+ export function writeAuditPid(directory, pid = process.pid) {
49
+ const path = join(directory, LICENSE_AUDIT_PID_FILE);
50
+ writeFileSync(path, `${String(pid)}\n`, { encoding: 'utf8', mode: 0o600 });
51
+ chmodSync(path, 0o600);
52
+ }
53
+ function readAuditPid(directory) {
54
+ try {
55
+ const text = readFileSync(join(directory, LICENSE_AUDIT_PID_FILE), 'utf8').trim();
56
+ if (!/^[1-9]\d*$/.test(text))
57
+ return undefined;
58
+ const pid = Number.parseInt(text, 10);
59
+ return Number.isSafeInteger(pid) && pid <= MAX_PID ? pid : undefined;
60
+ }
61
+ catch (error) {
62
+ if (hasCode(error, 'ENOENT'))
63
+ return undefined;
64
+ throw error;
65
+ }
66
+ }
67
+ export function removeAuditDirectory(directory) {
68
+ const stat = lstatSync(directory, { throwIfNoEntry: false });
69
+ if (!stat)
70
+ return;
71
+ if (stat.isSymbolicLink()) {
72
+ unlinkSync(directory);
73
+ return;
74
+ }
75
+ if (!stat.isDirectory())
76
+ return;
77
+ rmSync(directory, { force: true, recursive: true });
78
+ }
79
+ function reclaimNames(directory) {
80
+ try {
81
+ return readdirSync(directory, { encoding: 'utf8' });
82
+ }
83
+ catch (error) {
84
+ if (hasCode(error, 'ENOENT'))
85
+ return [];
86
+ throw error;
87
+ }
88
+ }
89
+ function shouldRemoveAuditDirectory(directory, mtimeMs, now, graceMs, isOwnerAlive) {
90
+ const pid = readAuditPid(directory);
91
+ if (pid === undefined)
92
+ return now - mtimeMs >= graceMs;
93
+ return !isOwnerAlive(pid, mtimeMs);
94
+ }
95
+ /** Deletes leftover audit directories whose owner process is gone, including after SIGKILL. */
96
+ export function reclaimStaleLicenseAuditDirectories(directory, options = {}) {
97
+ const now = options.now ?? Date.now();
98
+ const graceMs = options.graceMs ?? STALE_AUDIT_GRACE_MS;
99
+ const isOwnerAlive = options.isOwnerAlive ?? isAuditOwnerAlive;
100
+ for (const name of reclaimNames(directory)) {
101
+ if (!name.startsWith(LICENSE_AUDIT_DIRECTORY_PREFIX))
102
+ continue;
103
+ const path = join(directory, name);
104
+ const stat = lstatSync(path);
105
+ if (stat.isSymbolicLink()) {
106
+ unlinkSync(path);
107
+ continue;
108
+ }
109
+ if (!stat.isDirectory())
110
+ continue;
111
+ if (shouldRemoveAuditDirectory(path, stat.mtimeMs, now, graceMs, isOwnerAlive)) {
112
+ rmSync(path, { force: true, recursive: true });
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,12 @@
1
+ type SignalListener = () => void;
2
+ export interface AuditSignalDependencies {
3
+ readonly raiseSignal?: (signal: NodeJS.Signals) => void;
4
+ readonly subscribe?: (signal: NodeJS.Signals, listener: SignalListener) => void;
5
+ readonly unsubscribe?: (signal: NodeJS.Signals, listener: SignalListener) => void;
6
+ }
7
+ /**
8
+ * Runs an audit while SIGINT, SIGTERM, and SIGHUP abort it, remove its workspace, and are raised
9
+ * again so the process still exits from that signal.
10
+ */
11
+ export declare function withAuditSignalCleanup<T>(cleanup: () => void, run: (signal: AbortSignal) => Promise<T>, dependencies?: AuditSignalDependencies): Promise<T>;
12
+ export {};
@@ -0,0 +1,44 @@
1
+ const AUDIT_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
2
+ /**
3
+ * Runs an audit while SIGINT, SIGTERM, and SIGHUP abort it, remove its workspace, and are raised
4
+ * again so the process still exits from that signal.
5
+ */
6
+ export async function withAuditSignalCleanup(cleanup, run, dependencies = {}) {
7
+ const controller = new AbortController();
8
+ let raised;
9
+ const handlers = AUDIT_SIGNALS.map((signal) => {
10
+ const listener = () => {
11
+ if (raised !== undefined)
12
+ return;
13
+ raised = signal;
14
+ controller.abort(signal);
15
+ };
16
+ if (dependencies.subscribe)
17
+ dependencies.subscribe(signal, listener);
18
+ else
19
+ process.on(signal, listener);
20
+ return { listener, signal };
21
+ });
22
+ try {
23
+ return await run(controller.signal);
24
+ }
25
+ finally {
26
+ for (const { listener, signal } of handlers) {
27
+ if (dependencies.unsubscribe)
28
+ dependencies.unsubscribe(signal, listener);
29
+ else
30
+ process.removeListener(signal, listener);
31
+ }
32
+ try {
33
+ cleanup();
34
+ }
35
+ finally {
36
+ if (raised !== undefined) {
37
+ if (dependencies.raiseSignal)
38
+ dependencies.raiseSignal(raised);
39
+ else
40
+ process.kill(process.pid, raised);
41
+ }
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,12 @@
1
+ export interface DirectoryIdentity {
2
+ readonly isDirectory: () => boolean;
3
+ readonly isSymbolicLink: () => boolean;
4
+ readonly mode: number;
5
+ readonly uid: number;
6
+ }
7
+ /** pnpm's cache directory. The audit store stays beside it so it is not the developer store. */
8
+ export declare function pnpmCacheDirectory(env?: NodeJS.ProcessEnv, home?: string, hostPlatform?: NodeJS.Platform): string;
9
+ export declare function licenseAuditStoreDirectory(env?: NodeJS.ProcessEnv, home?: string, hostPlatform?: NodeJS.Platform): string;
10
+ export declare function assertOwnedDirectory(stat: DirectoryIdentity, uid: number | undefined, directory: string): void;
11
+ /** Creates the dedicated content-addressed store used by repeat license audits. */
12
+ export declare function ensurePrivateLicenseAuditStore(directory: string): void;
@@ -0,0 +1,32 @@
1
+ import { chmodSync, lstatSync, mkdirSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ const STORE_DIRECTORY_NAME = 'dependency-license-audit-store';
5
+ /** pnpm's cache directory. The audit store stays beside it so it is not the developer store. */
6
+ export function pnpmCacheDirectory(env = process.env, home = homedir(), hostPlatform = process.platform) {
7
+ if (hostPlatform === 'darwin')
8
+ return join(home, 'Library', 'Caches', 'pnpm');
9
+ if (hostPlatform === 'win32') {
10
+ return join(env.LOCALAPPDATA ?? join(home, 'AppData', 'Local'), 'pnpm-cache');
11
+ }
12
+ return join(env.XDG_CACHE_HOME ?? join(home, '.cache'), 'pnpm');
13
+ }
14
+ export function licenseAuditStoreDirectory(env = process.env, home = homedir(), hostPlatform = process.platform) {
15
+ return join(pnpmCacheDirectory(env, home, hostPlatform), STORE_DIRECTORY_NAME);
16
+ }
17
+ export function assertOwnedDirectory(stat, uid, directory) {
18
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
19
+ throw new Error(`dependency license audit store is not a real directory: ${directory}`);
20
+ }
21
+ if (uid !== undefined && stat.uid !== uid) {
22
+ throw new Error(`dependency license audit store is not owned by the current user: ${directory}`);
23
+ }
24
+ }
25
+ /** Creates the dedicated content-addressed store used by repeat license audits. */
26
+ export function ensurePrivateLicenseAuditStore(directory) {
27
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
28
+ const stat = lstatSync(directory);
29
+ assertOwnedDirectory(stat, process.getuid?.(), directory);
30
+ if ((stat.mode & 0o077) !== 0)
31
+ chmodSync(directory, 0o700);
32
+ }
@@ -1,9 +1,11 @@
1
1
  import type { PnpmExecutor, PnpmLicenseReport } from './types.mts';
2
2
  import { type PnpmLicenseAuditWorkspace } from './workspace.mts';
3
3
  export interface CollectPnpmLicenseReportOptions {
4
+ readonly ensureStore?: (directory: string) => void;
4
5
  readonly execute?: PnpmExecutor;
5
6
  readonly prepareWorkspace?: (repoRoot: string, lockfileSource: string, workspaceSource: string) => PnpmLicenseAuditWorkspace;
6
7
  readonly readFile?: (path: string, encoding: 'utf8') => string;
8
+ readonly storeDir?: string;
7
9
  }
8
10
  /** Collects licenses for every platform represented in a pnpm lockfile. */
9
- export declare function collectPnpmLicenseReport(repoRoot: string, options?: CollectPnpmLicenseReportOptions): PnpmLicenseReport;
11
+ export declare function collectPnpmLicenseReport(repoRoot: string, options?: CollectPnpmLicenseReportOptions): Promise<PnpmLicenseReport>;
@@ -1,10 +1,13 @@
1
- import { spawnSync } from 'node:child_process';
2
1
  import { readFileSync } from 'node:fs';
3
2
  import { join } from 'node:path';
3
+ import { withAuditSignalCleanup } from './audit-signals.mjs';
4
+ import { ensurePrivateLicenseAuditStore, licenseAuditStoreDirectory } from './audit-store.mjs';
5
+ import { executePnpm } from './execute-pnpm.mjs';
4
6
  import { parsePnpmLicenseReport } from './report.mjs';
5
7
  import { preparePnpmLicenseAuditWorkspace } from './workspace.mjs';
6
8
  const DEFAULT_OPTIONS = {
7
- execute: spawnSync,
9
+ ensureStore: ensurePrivateLicenseAuditStore,
10
+ execute: executePnpm,
8
11
  prepareWorkspace: preparePnpmLicenseAuditWorkspace,
9
12
  readFile: readFileSync,
10
13
  };
@@ -19,22 +22,17 @@ function assertCommandSucceeded(label, result) {
19
22
  }
20
23
  }
21
24
  /** Collects licenses for every platform represented in a pnpm lockfile. */
22
- export function collectPnpmLicenseReport(repoRoot, options = {}) {
23
- const { execute, prepareWorkspace, readFile } = { ...DEFAULT_OPTIONS, ...options };
24
- const lockfilePath = join(repoRoot, 'pnpm-lock.yaml');
25
- const workspacePath = join(repoRoot, 'pnpm-workspace.yaml');
26
- const auditWorkspace = prepareWorkspace(repoRoot, readFile(lockfilePath, 'utf8'), readFile(workspacePath, 'utf8'));
27
- try {
28
- const storeConfig = `--config.store-dir=${join(auditWorkspace.cwd, '.pnpm-store')}`;
29
- const fetchResult = execute('pnpm', [storeConfig, 'fetch', '--ignore-scripts'], {
30
- cwd: auditWorkspace.cwd,
31
- encoding: 'utf8',
32
- });
25
+ export async function collectPnpmLicenseReport(repoRoot, options = {}) {
26
+ const { ensureStore, execute, prepareWorkspace, readFile } = { ...DEFAULT_OPTIONS, ...options };
27
+ const auditWorkspace = prepareWorkspace(repoRoot, readFile(join(repoRoot, 'pnpm-lock.yaml'), 'utf8'), readFile(join(repoRoot, 'pnpm-workspace.yaml'), 'utf8'));
28
+ const storeDir = options.storeDir ?? licenseAuditStoreDirectory();
29
+ return withAuditSignalCleanup(auditWorkspace.cleanup, async (signal) => {
30
+ ensureStore(storeDir);
31
+ const storeConfig = `--config.store-dir=${storeDir}`;
32
+ const commandOptions = { cwd: auditWorkspace.cwd, encoding: 'utf8', signal };
33
+ const fetchResult = await execute('pnpm', [storeConfig, 'fetch', '--ignore-scripts'], commandOptions);
33
34
  assertCommandSucceeded('pnpm fetch', fetchResult);
34
- const result = execute('pnpm', [storeConfig, 'licenses', 'list', '--json'], {
35
- cwd: auditWorkspace.cwd,
36
- encoding: 'utf8',
37
- });
35
+ const result = await execute('pnpm', [storeConfig, 'licenses', 'list', '--json'], commandOptions);
38
36
  assertCommandSucceeded('pnpm licenses list --json', result);
39
37
  try {
40
38
  return parsePnpmLicenseReport(JSON.parse(result.stdout));
@@ -44,8 +42,5 @@ export function collectPnpmLicenseReport(repoRoot, options = {}) {
44
42
  cause: error,
45
43
  });
46
44
  }
47
- }
48
- finally {
49
- auditWorkspace.cleanup();
50
- }
45
+ });
51
46
  }
@@ -0,0 +1,10 @@
1
+ import type { Readable } from 'node:stream';
2
+ import type { PnpmExecutor } from './types.mts';
3
+ export declare function signalFromAbort(signal: AbortSignal): NodeJS.Signals;
4
+ export declare function stopChildProcess(child: {
5
+ kill: (signal?: NodeJS.Signals) => boolean;
6
+ pid?: number | undefined;
7
+ }, signal: NodeJS.Signals): void;
8
+ export declare function collectChildStream(stream: Readable | null, chunks: string[], onOverflow: () => void): void;
9
+ /** Runs pnpm in its own process group so a signal can reach it and its children. */
10
+ export declare const executePnpm: PnpmExecutor;
@@ -0,0 +1,83 @@
1
+ import { spawn } from 'node:child_process';
2
+ const MAX_BUFFER_BYTES = 1024 * 1024;
3
+ export function signalFromAbort(signal) {
4
+ if (signal.reason === 'SIGINT' || signal.reason === 'SIGTERM' || signal.reason === 'SIGHUP') {
5
+ return signal.reason;
6
+ }
7
+ return 'SIGTERM';
8
+ }
9
+ export function stopChildProcess(child, signal) {
10
+ if (child.pid === undefined) {
11
+ child.kill(signal);
12
+ return;
13
+ }
14
+ try {
15
+ process.kill(-child.pid, signal);
16
+ }
17
+ catch {
18
+ child.kill(signal);
19
+ }
20
+ }
21
+ export function collectChildStream(stream, chunks, onOverflow) {
22
+ if (stream === null)
23
+ throw new Error('license audit child is missing a stdio pipe');
24
+ stream.setEncoding('utf8');
25
+ stream.on('data', (chunk) => {
26
+ chunks.push(chunk);
27
+ if (Buffer.byteLength(chunks.join('')) > MAX_BUFFER_BYTES)
28
+ onOverflow();
29
+ });
30
+ }
31
+ function finishOnce(settle) {
32
+ let settled = false;
33
+ return (result) => {
34
+ if (settled)
35
+ return;
36
+ settled = true;
37
+ settle(result);
38
+ };
39
+ }
40
+ /** Runs pnpm in its own process group so a signal can reach it and its children. */
41
+ export const executePnpm = (command, args, options) => new Promise((resolve) => {
42
+ const child = spawn(command, args, {
43
+ cwd: options.cwd,
44
+ detached: true,
45
+ stdio: ['ignore', 'pipe', 'pipe'],
46
+ });
47
+ const stdout = [];
48
+ const stderr = [];
49
+ let overflow = false;
50
+ const stop = () => {
51
+ stopChildProcess(child, overflow ? 'SIGKILL' : signalFromAbort(options.signal));
52
+ };
53
+ if (options.signal.aborted)
54
+ stop();
55
+ else
56
+ options.signal.addEventListener('abort', stop, { once: true });
57
+ const markOverflow = () => {
58
+ overflow = true;
59
+ stop();
60
+ };
61
+ collectChildStream(child.stdout, stdout, markOverflow);
62
+ collectChildStream(child.stderr, stderr, markOverflow);
63
+ const finish = finishOnce((result) => {
64
+ options.signal.removeEventListener('abort', stop);
65
+ resolve(result);
66
+ });
67
+ child.once('error', (error) => {
68
+ finish({ error, status: null, stderr: stderr.join(''), stdout: stdout.join('') });
69
+ });
70
+ child.once('close', (status) => {
71
+ const result = { status, stderr: stderr.join(''), stdout: stdout.join('') };
72
+ if (!overflow) {
73
+ finish(result);
74
+ return;
75
+ }
76
+ finish({
77
+ ...result,
78
+ error: Object.assign(new Error('child output maxBuffer exceeded'), {
79
+ code: 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER',
80
+ }),
81
+ });
82
+ });
83
+ });
@@ -4,15 +4,17 @@ export interface PnpmLicenseReportEntry {
4
4
  }
5
5
  /** Shape emitted by `pnpm licenses list --json`. */
6
6
  export type PnpmLicenseReport = Record<string, PnpmLicenseReportEntry[]>;
7
- export type PnpmExecutor = (command: string, args: string[], options: {
8
- cwd: string;
9
- encoding: 'utf8';
10
- }) => {
11
- error?: Error;
12
- status: number | null;
13
- stderr: string;
14
- stdout: string;
15
- };
7
+ export interface PnpmCommandResult {
8
+ readonly error?: Error;
9
+ readonly status: number | null;
10
+ readonly stderr: string;
11
+ readonly stdout: string;
12
+ }
13
+ export type PnpmExecutor = (command: string, args: readonly string[], options: {
14
+ readonly cwd: string;
15
+ readonly encoding: 'utf8';
16
+ readonly signal: AbortSignal;
17
+ }) => PnpmCommandResult | Promise<PnpmCommandResult>;
16
18
  export interface DependencyLicenseAllowlistEntry {
17
19
  /** Exact SPDX atom allowed by this entry. */
18
20
  readonly licenseId: string;
@@ -1,3 +1,4 @@
1
+ import { type LicenseAuditWorkspaceOptions } from './audit-directory.mts';
1
2
  export interface PnpmLicenseAuditWorkspace {
2
3
  readonly cleanup: () => void;
3
4
  readonly cwd: string;
@@ -19,4 +20,5 @@ export declare function renderPnpmLicenseAuditFiles(lockfileSource: string, work
19
20
  lockfile: string;
20
21
  workspace: string;
21
22
  }): PnpmLicenseAuditFiles;
22
- export declare function preparePnpmLicenseAuditWorkspace(repoRoot: string, lockfileSource: string, workspaceSource: string): PnpmLicenseAuditWorkspace;
23
+ export declare function licenseAuditParentDirectory(directory?: string): string;
24
+ export declare function preparePnpmLicenseAuditWorkspace(repoRoot: string, lockfileSource: string, workspaceSource: string, options?: LicenseAuditWorkspaceOptions): PnpmLicenseAuditWorkspace;
@@ -1,7 +1,8 @@
1
- import { copyFileSync, existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
1
+ import { copyFileSync, existsSync, mkdtempSync, writeFileSync } from 'node:fs';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { isMap, parseDocument, stringify as stringifyYaml } from 'yaml';
5
+ import { LICENSE_AUDIT_DIRECTORY_PREFIX, reclaimStaleLicenseAuditDirectories, removeAuditDirectory, writeAuditPid, } from './audit-directory.mjs';
5
6
  const PLATFORM_KEYS = ['os', 'cpu', 'libc'];
6
7
  function parseYamlDocument(source, path) {
7
8
  const document = parseDocument(source);
@@ -71,9 +72,15 @@ export function renderPnpmLicenseAuditFiles(lockfileSource, workspaceSource, pat
71
72
  workspace: stringifyYaml({ ...workspace, packages: [], supportedArchitectures }),
72
73
  };
73
74
  }
74
- export function preparePnpmLicenseAuditWorkspace(repoRoot, lockfileSource, workspaceSource) {
75
- const auditRoot = mkdtempSync(join(tmpdir(), 'dependency-license-audit-'));
75
+ export function licenseAuditParentDirectory(directory) {
76
+ return directory ?? tmpdir();
77
+ }
78
+ export function preparePnpmLicenseAuditWorkspace(repoRoot, lockfileSource, workspaceSource, options = {}) {
79
+ const directory = licenseAuditParentDirectory(options.directory);
80
+ reclaimStaleLicenseAuditDirectories(directory, options);
81
+ const auditRoot = mkdtempSync(join(directory, LICENSE_AUDIT_DIRECTORY_PREFIX));
76
82
  try {
83
+ writeAuditPid(auditRoot, options.pid);
77
84
  copyFileSync(join(repoRoot, 'package.json'), join(auditRoot, 'package.json'));
78
85
  const npmrc = join(repoRoot, '.npmrc');
79
86
  if (existsSync(npmrc))
@@ -86,11 +93,11 @@ export function preparePnpmLicenseAuditWorkspace(repoRoot, lockfileSource, works
86
93
  writeFileSync(join(auditRoot, 'pnpm-workspace.yaml'), files.workspace, 'utf8');
87
94
  }
88
95
  catch (error) {
89
- rmSync(auditRoot, { force: true, recursive: true });
96
+ removeAuditDirectory(auditRoot);
90
97
  throw error;
91
98
  }
92
99
  return {
93
100
  cwd: auditRoot,
94
- cleanup: () => rmSync(auditRoot, { force: true, recursive: true }),
101
+ cleanup: () => removeAuditDirectory(auditRoot),
95
102
  };
96
103
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Vouchington CLI and extractable tooling libraries.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
6
6
  "bugs": {