// node/workerd compatibility for command modules that still write through // process and console. browser-safe commands use run-rnx-core.ts, whose // explicit context carries its writer, cancellation signal, semantic // invocations, and artifacts without touching ambient process state. // // two things stand between the command modules and an in-process call, and // this module owns both: // // output — the modules write to console.* and process.std*.write, ~1,777 // sites. they are not rewritten to take a writer. instead the sinks are // resolved from async context, so two commands running concurrently in one // isolate keep their output separate. RAN: a global current-sink instead of // AsyncLocalStorage interleaves them, which is what makes the context // version necessary rather than merely tidy. // // exit — a command that calls process.exit ends the host process instead of // returning a code to its caller. `rnxExit` throws instead, and `runRnx` // turns it back into a code at the boundary. // // AsyncLocalStorage keeps this compatibility module node/workerd only. migrate // a command to the explicit core before exposing it through a browser backend. import { AsyncLocalStorage } from 'node:async_hooks' export interface RnxRunResult { stdout: string stderr: string exitCode: number } interface RnxSinks { stdout: string[] stderr: string[] } // thrown by `rnxExit` in place of process.exit. every catch that reports an // error must re-throw this before reporting, or it turns a deliberate exit // into a spurious "command failed" line with the sentinel's own text. export class RnxExit extends Error { readonly code: number constructor(code: number) { super(`rnx exited with code ${code}`) this.name = 'RnxExit' this.code = code } } const sinks = new AsyncLocalStorage() // stop a command's deliberate exit from being reported as a failure. the // guard every reporting catch needs, in one place so the call sites stay one // line. export function rethrowIfExit(error: unknown): void { if (error instanceof RnxExit) throw error } // the in-process replacement for process.exit. returns `never` exactly as // process.exit does, so it substitutes without changing any call site's types. export function rnxExit(code: number): never { throw new RnxExit(code) } function write(stream: 'stdout' | 'stderr', text: string): boolean { const current = sinks.getStore() if (!current) return false current[stream].push(text) return true } let installed = false // replace the process-wide writers once. each resolves its destination from // async context, so a call made outside any `runRnx` still reaches the real // terminal and the host CLI keeps behaving exactly as it did. function installCapture(): void { if (installed) return installed = true const realLog = console.log.bind(console) const realError = console.error.bind(console) const realWarn = console.warn.bind(console) const realStdout = process.stdout.write.bind(process.stdout) const realStderr = process.stderr.write.bind(process.stderr) const line = (args: unknown[]): string => `${args.map(String).join(' ')}\n` console.log = (...args: unknown[]) => { if (!write('stdout', line(args))) realLog(...args) } console.error = (...args: unknown[]) => { if (!write('stderr', line(args))) realError(...args) } console.warn = (...args: unknown[]) => { if (!write('stderr', line(args))) realWarn(...args) } process.stdout.write = (chunk: string | Uint8Array, ...rest: unknown[]) => { if ( write('stdout', typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk)) ) { return true } return Reflect.apply(realStdout, process.stdout, [chunk, ...rest]) } process.stderr.write = (chunk: string | Uint8Array, ...rest: unknown[]) => { if ( write('stderr', typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk)) ) { return true } return Reflect.apply(realStderr, process.stderr, [chunk, ...rest]) } } // run one rnx invocation and return what it wrote plus its exit code, instead // of writing to the terminal and ending the process. `argv` is the command and // its arguments with no node/script prefix, e.g. ['describe', '--json']. export async function runRnx( argv: string[], dispatch: (argv: string[]) => Promise, ): Promise { installCapture() const current: RnxSinks = { stdout: [], stderr: [] } const exitCode = await sinks.run(current, async () => { try { return await dispatch(argv) } catch (error) { if (error instanceof RnxExit) return error.code throw error } }) return { stdout: current.stdout.join(''), stderr: current.stderr.join(''), exitCode, } }