axstack 0.31.0 → 0.33.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.
@@ -1,10 +1,11 @@
1
- import { readFileSync } from 'node:fs';
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
2
 
3
- const flags = ['--role', '--roles', '--capabilities', '--picker', '--kind', '--verify'];
3
+ const flags = ['--role', '--roles', '--capabilities', '--picker', '--kind', '--verify', '--out'];
4
4
  const nonempty = (value) => typeof value === 'string' && value.trim().length > 0;
5
5
  const help = `Usage: bun dispatch-plan.js --role <id> --roles <roles.json>
6
6
  --capabilities <saved.json> --picker <picker.json> --kind task|launch
7
- [--verify <configuration.json>]
7
+ [--verify <configuration.json>] [--out <report.json>]
8
+ Example: bun dispatch-plan.js --role axstack-author --roles roles.json --capabilities capabilities.json --picker picker.json --kind launch
8
9
 
9
10
  --role Role ID from the supplied snapshot
10
11
  --roles Installed-style roles.json (version 1, roles array)
@@ -12,9 +13,13 @@ const help = `Usage: bun dispatch-plan.js --role <id> --roles <roles.json>
12
13
  --picker Saved picker JSON; uses chosenInstanceId
13
14
  --kind task emits target; launch emits modelSelection
14
15
  --verify Compare t3_thread_configuration JSON; print MATCH or each mismatch
16
+ --out Write full JSON here and print input/path/omitted-count summary
15
17
  --help Show all flags and exit codes
16
18
 
17
- Read-only: reads supplied JSON, spawns adjacent resolve-models.js, writes stdout/stderr.
19
+ Read-only inputs: reads supplied JSON, spawns adjacent resolve-models.js, writes stdout/stderr.
20
+ Writes only the caller's --out path when supplied; no retained state.
21
+ Results name input paths. Plan output over 4096 bytes requires --out.
22
+ Large verification retains its exit code, prints mismatches and advises --out on stderr.
18
23
  Never dispatches or changes snapshots. Reuse recorded exact model pins as given.
19
24
  Exit codes: 0 ok, 1 error, 2 hold (including incomplete read-back).
20
25
  `;
@@ -27,20 +32,23 @@ function argumentsFrom(args) {
27
32
  const options = {};
28
33
  for (let i = 0; i < args.length; i += 2) {
29
34
  const flag = args[i];
30
- if (!flags.includes(flag)) throw new Error(`unexpected argument ${flag}`);
35
+ if (!flags.includes(flag)) throw new Error(`unexpected argument ${flag}; use --help`);
31
36
  const value = args[i + 1];
32
- if (!nonempty(value) || value.startsWith('--')) throw new Error(`expected value after ${flag}`);
33
- if (flag in options) throw new Error(`duplicate argument ${flag}`);
37
+ if (!nonempty(value) || value.startsWith('--')) throw new Error(`expected value after ${flag}; pass ${flag} <value>`);
38
+ if (flag in options) throw new Error(`duplicate argument ${flag}; pass it once`);
34
39
  options[flag] = value;
35
40
  }
36
- for (const flag of flags.filter((flag) => flag !== '--verify')) {
37
- if (!options[flag]) throw new Error(`expected ${flag}`);
41
+ for (const flag of flags.filter((flag) => !['--verify', '--out'].includes(flag))) {
42
+ if (!options[flag]) throw new Error(`expected ${flag}; pass ${flag} <value>`);
38
43
  }
39
- if (!['task', 'launch'].includes(options['--kind'])) throw new Error('expected --kind task|launch');
44
+ if (!['task', 'launch'].includes(options['--kind'])) throw new Error('expected --kind task|launch; pass --kind task or --kind launch');
40
45
  return options;
41
46
  }
42
47
 
43
- const readJSON = (path) => JSON.parse(readFileSync(path, 'utf8'));
48
+ function readJSON(path) {
49
+ try { return JSON.parse(readFileSync(path, 'utf8')); }
50
+ catch (error) { throw new Error(`${error.message}; pass a readable, valid JSON file: ${path}`); }
51
+ }
44
52
 
45
53
  function normalizedOptions(options) {
46
54
  const entries = Array.isArray(options) ? options.map((option) => [option?.id, option?.value])
@@ -68,19 +76,36 @@ function verify(configuration, instanceId, provider, model, options) {
68
76
  compare('options', options, selection?.options,
69
77
  normalizedOptions(options) === normalizedOptions(selection?.options));
70
78
  compare('runtimeMode', 'full-access', configuration?.runtimeMode);
71
- console.log(mismatches.length ? mismatches.join('\n') : 'MATCH');
72
- return mismatches.length ? 2 : 0;
79
+ return mismatches;
73
80
  }
74
81
 
75
- function main() {
76
- if (process.argv.slice(2).includes('--help')) {
77
- process.stdout.write(help);
82
+ function plan(argv, io) {
83
+ if (argv.includes('--help')) {
84
+ io.stdout(help);
78
85
  return 0;
79
86
  }
80
- const args = argumentsFrom(process.argv.slice(2));
87
+ const args = argumentsFrom(argv);
88
+ const inputs = Object.fromEntries(Object.entries(args).filter(([flag]) => flag !== '--out').map(([flag, value]) => [flag.slice(2), value]));
89
+ const emit = (result) => {
90
+ const full = { inputs, ...result };
91
+ const inline = result.mismatches
92
+ ? `${JSON.stringify({ inputs })}\n${result.mismatches.join('\n') || 'MATCH'}\n` : `${JSON.stringify(full)}\n`;
93
+ if (!args['--out']) {
94
+ if (new TextEncoder().encode(inline).length > 4096) {
95
+ if (!result.mismatches) throw new Error('large output; pass --out <report.json> to save full JSON');
96
+ io.stderr('Large verification output; pass --out <path>.json for the full list\n');
97
+ }
98
+ io.stdout(inline);
99
+ return;
100
+ }
101
+ try { writeFileSync(args['--out'], `${JSON.stringify(full, null, 2)}\n`); }
102
+ catch (error) { throw new Error(`${error.message}; pass --out <writable report.json path>`); }
103
+ io.stdout(`${JSON.stringify({ inputs, reportPath: args['--out'],
104
+ omittedPayloadFields: Object.keys(result).length, omittedMismatches: result.mismatches?.length ?? 0 })}\n`);
105
+ };
81
106
  const snapshot = readJSON(args['--roles']);
82
107
  if (snapshot?.version !== 1 || !Array.isArray(snapshot.roles)) {
83
- throw new Error('roles snapshot must have version 1 and a roles array');
108
+ throw new Error('roles snapshot must have version 1 and a roles array; pass --roles <valid roles.json>');
84
109
  }
85
110
  const roles = snapshot.roles.filter((role) => role?.id === args['--role']);
86
111
  if (roles.length !== 1) hold(`role ${args['--role']} unavailable or ambiguous`);
@@ -100,7 +125,7 @@ function main() {
100
125
  const resolved = Bun.spawnSync([process.execPath, `${import.meta.dir}/resolve-models.js`,
101
126
  '--provider', role.provider, '--capabilities', args['--capabilities'],
102
127
  '--effort', role.thinkingOptionId, ...modelArgs], { stdout: 'pipe', stderr: 'pipe' });
103
- if (resolved.stderr.length) process.stderr.write(resolved.stderr);
128
+ if (resolved.stderr.length) io.stderr(resolved.stderr.toString());
104
129
  if (resolved.exitCode !== 0) return resolved.exitCode === 1 ? 2 : 1;
105
130
  const { model, effortOption, providerInstanceId } = JSON.parse(resolved.stdout.toString());
106
131
  const chosen = readJSON(args['--capabilities']).providers
@@ -117,18 +142,22 @@ function main() {
117
142
  }
118
143
  const options = effortOption ? [effortOption] : [];
119
144
  if (args['--verify']) {
120
- return verify(readJSON(args['--verify']), picker.chosenInstanceId, providerInstanceId, model, options);
145
+ const mismatches = verify(readJSON(args['--verify']), picker.chosenInstanceId, providerInstanceId, model, options);
146
+ emit({ mismatches });
147
+ return mismatches.length ? 2 : 0;
121
148
  }
122
149
  const selection = { [args['--kind'] === 'launch' ? 'instanceId' : 'providerInstanceId']: picker.chosenInstanceId,
123
150
  model, options };
124
- console.log(JSON.stringify({ [args['--kind'] === 'launch' ? 'modelSelection' : 'target']: selection,
125
- runtimeMode: 'full-access' }));
151
+ emit({ [args['--kind'] === 'launch' ? 'modelSelection' : 'target']: selection, runtimeMode: 'full-access' });
126
152
  return 0;
127
153
  }
128
154
 
129
- try {
130
- process.exitCode = main();
131
- } catch (error) {
132
- console.error(`dispatch-plan ${error.exitCode === 2 ? 'hold' : 'error'}: ${error.message}`);
133
- process.exitCode = error.exitCode ?? 1;
155
+ export function main(argv, io = { stdout: (text) => process.stdout.write(text), stderr: (text) => process.stderr.write(text) }) {
156
+ try { return plan(argv, io); }
157
+ catch (error) {
158
+ io.stderr(`dispatch-plan ${error.exitCode === 2 ? 'hold' : 'error'}: ${error.message}\n`);
159
+ return error.exitCode ?? 1;
160
+ }
134
161
  }
162
+
163
+ if (import.meta.main) process.exitCode = main(process.argv.slice(2));
@@ -0,0 +1,134 @@
1
+ import { existsSync } from 'node:fs';
2
+
3
+ const lockfiles = ['bun.lock', 'bun.lockb', 'package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock'];
4
+ const help = `Usage: bun scripts/pr-shape.js --base <ref> --head <ref>
5
+ Example: bun scripts/pr-shape.js --base main --head HEAD
6
+
7
+ --base Required actual PR base (parent branch for a stacked child)
8
+ --head Required candidate commit or branch
9
+ --help Show flags, example, bucket definitions and exit codes
10
+
11
+ Read-only Git; writes nothing, retains no state. Prints a bounded JSON summary:
12
+ input refs and resolved SHAs, merge-base, full additions/deletions, rename and
13
+ binary counts, and overlapping lockfile/generated buckets, with status and
14
+ reproducing argv plus calculation instructions. No path inventory is emitted.
15
+ Lockfiles match these exact basenames at any depth: ${lockfiles.join(', ')}.
16
+ Generated means linguist-generated set or true in .gitattributes at the head;
17
+ uses the destination path for renames. Unset, false and unspecified are excluded.
18
+ Ignores system/global attributes; local info/attributes causes an unknown bucket.
19
+ Never subtract buckets; formatter-only changes require manual disclosure.
20
+ No cohesion or approval verdict. Exit codes: 0 measured, 1 input/Git error,
21
+ 2 incomplete (no merge-base or unavailable committed-attribute evidence).
22
+ `;
23
+
24
+ function optionsFrom(argv) {
25
+ const options = {};
26
+ for (let i = 0; i < argv.length; i += 2) {
27
+ const flag = argv[i];
28
+ if (!['--base', '--head'].includes(flag)) throw new Error(`unexpected argument ${flag}; use --help`);
29
+ const value = argv[i + 1];
30
+ if (!value || value.startsWith('-')) throw new Error(`missing ref; pass ${flag} <commit-or-branch>`);
31
+ if (flag in options) throw new Error(`duplicate ${flag}; pass it once`);
32
+ options[flag] = value;
33
+ }
34
+ for (const flag of ['--base', '--head']) {
35
+ if (!options[flag]) throw new Error(`missing ref; pass ${flag} <commit-or-branch>`);
36
+ }
37
+ return options;
38
+ }
39
+
40
+ const command = (...args) => ['git', '--no-optional-locks', '-c', 'core.attributesFile=/dev/null', ...args];
41
+ function run(argv, input) {
42
+ const result = Bun.spawnSync(argv, { stdin: input === undefined ? 'ignore' : new TextEncoder().encode(input),
43
+ stdout: 'pipe', stderr: 'pipe', env: { ...process.env, GIT_ATTR_NOSYSTEM: '1' } });
44
+ return { code: result.exitCode, stdout: result.stdout.toString(), stderr: result.stderr.toString() };
45
+ }
46
+
47
+ function numstat(text) {
48
+ const tokens = text.split('\0');
49
+ const rows = [];
50
+ for (let i = 0; i < tokens.length - 1; i++) {
51
+ const firstTab = tokens[i].indexOf('\t'), secondTab = tokens[i].indexOf('\t', firstTab + 1);
52
+ const added = tokens[i].slice(0, firstTab), deleted = tokens[i].slice(firstTab + 1, secondTab);
53
+ let path = tokens[i].slice(secondTab + 1);
54
+ const renamed = path === '';
55
+ if (renamed) { i++; path = tokens[++i]; } // -z rename: empty path, old path, new path.
56
+ rows.push({ path, renamed, binary: added === '-', additions: added === '-' ? 0 : Number(added),
57
+ deletions: deleted === '-' ? 0 : Number(deleted) });
58
+ }
59
+ return rows;
60
+ }
61
+
62
+ function summarize(rows, evidence) {
63
+ return { status: 'pass', files: rows.length,
64
+ additions: rows.reduce((sum, row) => sum + row.additions, 0),
65
+ deletions: rows.reduce((sum, row) => sum + row.deletions, 0),
66
+ renames: rows.filter((row) => row.renamed).length,
67
+ binaries: rows.filter((row) => row.binary).length,
68
+ evidence: { ...evidence, calculations: {
69
+ files: 'count selected numstat records', additions: 'sum numeric column 1 of selected records',
70
+ deletions: 'sum numeric column 2 of selected records',
71
+ renames: 'count selected -z records with empty path followed by old/new NUL paths',
72
+ binaries: 'count selected records with - in both numeric columns (no estimated lines)',
73
+ } } };
74
+ }
75
+
76
+ export function main(argv, io = { stdout: (text) => process.stdout.write(text), stderr: (text) => process.stderr.write(text) }) {
77
+ try {
78
+ if (argv.includes('--help')) { io.stdout(help); return 0; }
79
+ const options = optionsFrom(argv);
80
+ const root = run(command('rev-parse', '--show-toplevel'));
81
+ if (root.code) throw new Error('run this helper from a Git checkout');
82
+ const repo = root.stdout.replace(/\n$/, '');
83
+ const gitCommand = (...args) => command('-C', repo, ...args);
84
+ const resolve = (flag) => {
85
+ const result = run(gitCommand('rev-parse', '--verify', '--end-of-options', `${options[flag]}^{commit}`));
86
+ if (result.code) throw new Error(`unresolved ${flag}; pass ${flag} <existing-commit-or-branch>`);
87
+ return result.stdout.trim();
88
+ };
89
+ const inputs = { repo, baseRef: options['--base'], headRef: options['--head'], base: resolve('--base'), head: resolve('--head') };
90
+ const mergeCommand = gitCommand('merge-base', inputs.base, inputs.head);
91
+ const merge = run(mergeCommand);
92
+ if (merge.code) {
93
+ const unknown = { status: 'unknown', evidence: { commands: [mergeCommand], reason: merge.stderr.trim() || 'no common ancestor' } };
94
+ io.stdout(`${JSON.stringify({ inputs, mergeBase: { ...unknown, value: null }, totals: unknown,
95
+ buckets: { lockfiles: unknown, generated: unknown } })}\n`);
96
+ return 2;
97
+ }
98
+ const mergeBase = merge.stdout.trim();
99
+ const diffCommand = gitCommand('diff', '-M', '--no-relative', '--no-ext-diff', '--no-textconv', '--numstat', '-z', `${mergeBase}..${inputs.head}`, '--');
100
+ const diff = run(diffCommand);
101
+ if (diff.code) throw new Error(`diff unavailable; verify --base and --head refer to readable commits: ${diff.stderr.trim()}`);
102
+ const rows = numstat(diff.stdout);
103
+ const attrCommand = gitCommand('check-attr', `--source=${inputs.head}`, '--stdin', '-z', 'linguist-generated');
104
+ const attrEvidence = { commands: [diffCommand, attrCommand], environment: { GIT_ATTR_NOSYSTEM: '1' },
105
+ stdin: 'feed check-attr the NUL-separated destination paths from the full numstat records',
106
+ select: 'destination paths whose head linguist-generated value is set or true; never filter diff pathspecs' };
107
+ const infoPath = run(gitCommand('rev-parse', '--path-format=absolute', '--git-path', 'info/attributes')).stdout.replace(/\n$/, '');
108
+ // Git still reads info/attributes with --source; do not claim these are committed facts.
109
+ const attrs = existsSync(infoPath) ? null : run(attrCommand, rows.map((row) => row.path).join('\0') + (rows.length ? '\0' : ''));
110
+ let generated;
111
+ if (!attrs || attrs.code) {
112
+ generated = { status: 'unknown', evidence: { ...attrEvidence,
113
+ reason: !attrs ? `local attribute override exists: ${infoPath}` : `committed attributes unavailable: ${attrs.stderr.trim()}` } };
114
+ } else {
115
+ const values = attrs.stdout.split('\0'), paths = new Set();
116
+ for (let i = 0; i < values.length - 1; i += 3) {
117
+ if (['set', 'true'].includes(values[i + 2])) paths.add(values[i]);
118
+ }
119
+ generated = summarize(rows.filter((row) => paths.has(row.path)), attrEvidence);
120
+ }
121
+ io.stdout(`${JSON.stringify({ inputs, mergeBase: { status: 'pass', value: mergeBase, evidence: { commands: [mergeCommand] } },
122
+ totals: summarize(rows, { commands: [diffCommand], select: 'all records; no exclusions' }),
123
+ buckets: { lockfiles: summarize(rows.filter((row) => lockfiles.includes(row.path.split('/').at(-1))),
124
+ { commands: [diffCommand], select: 'destination basename is exactly one of lockfileNames', lockfileNames: lockfiles }), generated },
125
+ disclosures: 'Buckets may overlap. Never subtract them from totals. Formatter-only changes require manual disclosure; cohesion is a manual judgment.',
126
+ })}\n`);
127
+ return generated.status === 'unknown' ? 2 : 0;
128
+ } catch (error) {
129
+ io.stderr(`pr-shape error: ${error.message}\n`);
130
+ return 1;
131
+ }
132
+ }
133
+
134
+ if (import.meta.main) process.exitCode = main(process.argv.slice(2));
@@ -0,0 +1,161 @@
1
+ import { chmodSync, closeSync, lstatSync, mkdirSync, mkdtempSync, openSync, realpathSync, rmSync, writeFileSync, writeSync } from 'node:fs';
2
+
3
+ const installs = {
4
+ bun: ['install', '--frozen-lockfile'], npm: ['ci'],
5
+ pnpm: ['install', '--frozen-lockfile'], yarn: ['install', '--immutable'],
6
+ };
7
+ const groups = [['env.build', ['build']], ['testing.test', ['test']], ['testing.lint-typecheck', ['lint', 'typecheck']]];
8
+ const drainGraceMs = 200;
9
+ // No shell interpolation, and the wrapper preserves the command's allowlisted env.
10
+ const networkWrapper = `const ip = Bun.spawnSync([process.argv[1], 'link', 'set', 'lo', 'up'], { stdin: 'ignore', stdout: 'inherit', stderr: 'inherit' });
11
+ if (ip.exitCode !== 0) process.exit(ip.exitCode || 1);
12
+ const command = Bun.spawn(process.argv.slice(2), { stdin: 'ignore', stdout: 'inherit', stderr: 'inherit' });
13
+ process.exit(await command.exited);`;
14
+
15
+ function killGroup(proc) {
16
+ try { process.kill(-proc.pid, 'SIGKILL'); }
17
+ catch (error) { if (error.code !== 'ESRCH') throw error; }
18
+ }
19
+
20
+ async function runCommand(commands, paths, kind, argv, cwd, env, limits, state, spawnArgv = argv) {
21
+ const started = performance.now();
22
+ const step = { kind, argv, spawnArgv, exitCode: null, durationMs: 0, logPath: `${paths.evidence}/${crypto.randomUUID()}-${kind}.log`, timedOut: false, logBytes: 0, omittedBytes: 0, logIncomplete: false };
23
+ step.receiptPath = `${step.logPath.slice(0, -4)}.json`;
24
+ commands.push(step);
25
+ const fd = openSync(step.logPath, 'wx', 0o600);
26
+ let proc;
27
+ let timer;
28
+ let graceTimer;
29
+ let logging = true;
30
+ const readers = [];
31
+ function log(chunk) {
32
+ if (!logging) return;
33
+ const kept = Math.min(chunk.length, limits.logBytes - step.logBytes);
34
+ if (kept) writeSync(fd, chunk.subarray(0, kept));
35
+ step.logBytes += kept;
36
+ step.omittedBytes += chunk.length - kept;
37
+ }
38
+ async function drain(reader) {
39
+ try {
40
+ while (true) {
41
+ const { done, value } = await reader.read();
42
+ if (done) return;
43
+ log(value);
44
+ }
45
+ } catch (error) {
46
+ if (logging) { step.error = error.message; killGroup(proc); }
47
+ }
48
+ }
49
+ try {
50
+ proc = Bun.spawn(spawnArgv, { cwd, env, stdin: 'ignore', stdout: 'pipe', stderr: 'pipe', detached: true });
51
+ state.active = proc;
52
+ readers.push(proc.stdout.getReader(), proc.stderr.getReader());
53
+ const drained = Promise.all(readers.map(drain));
54
+ const exited = proc.exited.then((code) => {
55
+ if (logging) step.exitCode = code;
56
+ clearTimeout(timer);
57
+ killGroup(proc);
58
+ });
59
+ const timeout = new Promise((resolve) => {
60
+ timer = setTimeout(() => { step.timedOut = true; killGroup(proc); resolve(); }, limits.timeoutMs);
61
+ });
62
+ await Promise.race([exited, timeout]);
63
+ const complete = await Promise.race([
64
+ Promise.all([exited, drained]).then(() => true),
65
+ new Promise((resolve) => { graceTimer = setTimeout(() => resolve(false), drainGraceMs); }),
66
+ ]);
67
+ step.logIncomplete = !complete;
68
+ } catch (error) {
69
+ step.error = error.message;
70
+ log(new TextEncoder().encode(error.message));
71
+ } finally {
72
+ clearTimeout(timer);
73
+ clearTimeout(graceTimer);
74
+ logging = false;
75
+ if (proc) killGroup(proc);
76
+ // Cancel without another EOF wait. Escaped writers may retain the other pipe end forever.
77
+ for (const reader of readers) reader.cancel().then(() => reader.releaseLock()).catch(() => {});
78
+ state.active = null;
79
+ closeSync(fd);
80
+ step.durationMs = Math.round(performance.now() - started);
81
+ writeFileSync(step.receiptPath, `${JSON.stringify(step, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
82
+ }
83
+ return step;
84
+ }
85
+
86
+ export async function executeDeclared(repo, revision, paths, criteria, manager, pkg, limits) {
87
+ const pending = groups.filter(([id]) => criteria.find((item) => item.id === id).reason === 'not run');
88
+ const commands = [];
89
+ const result = { commands, network: 'not isolated', descendants: 'not contained' };
90
+ if (!pending.length) return result;
91
+ mkdirSync(paths.evidence, { recursive: true, mode: 0o700 });
92
+ let scratch;
93
+ const state = { active: null, signal: null };
94
+ const interrupt = (signal) => { state.signal = signal; if (state.active) killGroup(state.active); };
95
+ const sigint = () => interrupt('SIGINT');
96
+ const sigterm = () => interrupt('SIGTERM');
97
+ process.on('SIGINT', sigint);
98
+ process.on('SIGTERM', sigterm);
99
+ try {
100
+ const home = realpathSync(process.env.HOME || '/root');
101
+ let temp = realpathSync(process.env.TMPDIR || '/tmp');
102
+ if (temp === home || temp.startsWith(`${home}/`)) temp = realpathSync('/tmp');
103
+ if (temp === home || temp.startsWith(`${home}/`)) throw new Error('no scratch directory outside HOME; set TMPDIR to an owned temp directory');
104
+ scratch = mkdtempSync(`${temp}/axstack-readiness-`);
105
+ chmodSync(scratch, 0o700);
106
+ if (lstatSync(scratch).uid !== process.getuid()) throw new Error('scratch directory is not owned');
107
+ result.scratch = scratch;
108
+ const env = { PATH: process.env.PATH || '/usr/bin:/bin', LANG: process.env.LANG || 'C.UTF-8', CI: '1', HOME: scratch, TMPDIR: scratch };
109
+ const cwd = `${scratch}/repo`;
110
+ const unshare = Bun.which('unshare');
111
+ const ip = Bun.which('ip');
112
+ const pidFlags = ['--pid', '--fork', '--kill-child'];
113
+ const contained = (argv) => [unshare, '-r', ...pidFlags, ...argv];
114
+ const isolated = (argv) => [unshare, '-rn', ...(result.descendants === 'contained' ? pidFlags : []), process.execPath, '-e', networkWrapper, ip, ...argv];
115
+ const run = async (kind, argv, directory = scratch, spawnArgv) => {
116
+ if (state.signal) throw new Error(`interrupted by ${state.signal}`);
117
+ const step = await runCommand(commands, paths, kind, argv, directory, env, limits, state, spawnArgv ?? (result.descendants === 'contained' ? contained(argv) : argv));
118
+ if (state.signal) throw new Error(`interrupted by ${state.signal}; see ${step.logPath}`);
119
+ return step;
120
+ };
121
+ if (unshare) {
122
+ const probe = await run('descendant-probe', contained(['/bin/true']));
123
+ if (probe.exitCode === 0 && !probe.timedOut && !probe.error) result.descendants = 'contained';
124
+ }
125
+ if (unshare && ip) {
126
+ const argv = isolated(['/bin/true']);
127
+ const probe = await run('network-probe', argv, scratch, argv);
128
+ if (probe.exitCode === 0 && !probe.timedOut && !probe.error) result.network = 'isolated';
129
+ }
130
+ const git = ['git', '--no-optional-locks', '-c', 'core.hooksPath=/dev/null', '-c', 'core.fsmonitor=false'];
131
+ const clone = await run('clone', [...git, 'clone', '--no-hardlinks', '--no-checkout', '--', realpathSync(repo), cwd], scratch);
132
+ if (clone.exitCode !== 0 || clone.timedOut) throw new Error(`clone failed; see ${clone.logPath}`);
133
+ const checkout = await run('checkout', [...git, '-C', cwd, 'checkout', '--detach', revision], scratch);
134
+ if (checkout.exitCode !== 0 || checkout.timedOut) throw new Error(`checkout failed; see ${checkout.logPath}`);
135
+ const install = await run('install', [manager.name, ...installs[manager.name]], cwd);
136
+ if (install.exitCode !== 0 || install.timedOut) {
137
+ for (const [id] of pending) Object.assign(criteria.find((item) => item.id === id), { reason: 'install failed', evidence: [install.logPath] });
138
+ return result;
139
+ }
140
+ for (const [id, names] of pending) {
141
+ const steps = [];
142
+ for (const name of names.filter((name) => typeof pkg.scripts?.[name] === 'string' && pkg.scripts[name].trim())) {
143
+ const argv = [manager.name, ...(manager.name === 'pnpm' ? ['--config.enable-pre-post-scripts=false'] : []), 'run', name];
144
+ steps.push(await run('script', argv, cwd, result.network === 'isolated' ? isolated(argv) : undefined));
145
+ }
146
+ const passed = steps.every((step) => step.exitCode === 0 && !step.timedOut && !step.error);
147
+ Object.assign(criteria.find((item) => item.id === id), { status: passed ? 'pass' : 'fail', reason: steps.some((step) => step.timedOut) ? 'timeout' : passed ? '' : 'command failed', evidence: steps.map((step) => step.logPath) });
148
+ }
149
+ } catch (error) {
150
+ result.error = error.message;
151
+ for (const [id] of pending) Object.assign(criteria.find((item) => item.id === id), { status: 'unknown', reason: 'execution unavailable', evidence: [error.message] });
152
+ } finally {
153
+ process.off('SIGINT', sigint);
154
+ process.off('SIGTERM', sigterm);
155
+ if (scratch) {
156
+ try { rmSync(scratch, { recursive: true }); }
157
+ catch (error) { result.error = `scratch removal failed: ${scratch}: ${error.message}`; }
158
+ }
159
+ }
160
+ return result;
161
+ }