Files
gstack/lib/pregate/runtime.ts
T
garrytanandcapy-ai[bot] 1898142986 feat(pregate): repo guards a lane must pass, verified launch Bun and per-file serial runs in lanes, unknown flags exit 2
The guards check runs each .gstack/pregate.json guard whose globs match a
touched file in its own process with its own timeout (PREGATE_GUARD_FAILED,
never downgradable, incomplete without --allow-repo-commands). The lanes
runner and guards get the first Bun in $BUN_INSTALL/bin or ~/.bun/bin that
satisfies gstack's and the project's engines.bun; serial_patterns files run
one process each. price_file replaces the unreleased price_command key: a
paid gate is priced from a call-plan data file, never by running a command.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-13 00:45:28 +00:00

39 lines
2.3 KiB
TypeScript

/**
* pregate/runtime — the Bun a repo command runs under (field wave CD1, the
* TypeScript side of `gstack_bun_launch_dir` in bin/gstack-bun-version.sh).
* On a Capy cloud host the image's Bun is first on PATH and older than both
* gstack's minimum and most project pins, while the verified Bun the
* installer left in `$BUN_INSTALL/bin` or `~/.bun/bin` is off PATH; the same
* tree then passes or fails its lanes depending on the shell. The launch dir
* is the first of those two holding a Bun that satisfies gstack's own
* `engines.bun` and the project's `engines.bun`; when there is one, the
* lanes runner and every guard run with it first on PATH.
*/
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { satisfies } from '../runtime-pins';
const GSTACK_PACKAGE = path.resolve(import.meta.dir, '..', '..', 'package.json');
function enginesBun(file: string): string | null {
try { const v = JSON.parse(fs.readFileSync(file, 'utf8'))?.engines?.bun; return typeof v === 'string' && v.trim() ? v.trim() : null; } catch { return null; }
}
export interface LaunchRuntime { dir: string | null; version: string | null; env: NodeJS.ProcessEnv; label: string }
/** The launch dir and the env with it first on PATH; the env is unchanged when no candidate qualifies. */
export function launchRuntime(repoRoot: string, env: NodeJS.ProcessEnv): LaunchRuntime {
const pins = [enginesBun(GSTACK_PACKAGE), enginesBun(path.join(repoRoot, 'package.json'))].filter((p): p is string => !!p);
const candidates = [env.BUN_INSTALL ? path.join(env.BUN_INSTALL, 'bin') : null, env.HOME ? path.join(env.HOME, '.bun', 'bin') : null].filter((d): d is string => !!d);
for (const dir of [...new Set(candidates)]) {
const bun = path.join(dir, 'bun');
try { fs.accessSync(bun, fs.constants.X_OK); } catch { continue; }
const r = spawnSync(bun, ['--version'], { encoding: 'utf8', timeout: 15_000, env });
const version = r.status === 0 ? (r.stdout ?? '').trim() : '';
if (!version || pins.some(pin => satisfies(version, pin) !== true)) continue;
return { dir, version, env: { ...env, PATH: [dir, env.PATH ?? ''].filter(Boolean).join(path.delimiter) }, label: `bun ${version} from ${dir}` };
}
return { dir: null, version: null, env, label: 'PATH (no verified Bun in $BUN_INSTALL/bin or ~/.bun/bin)' };
}