From d9dde597c9b2cf5c20a0ca4d9cf10f598360c85e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 27 Feb 2026 01:21:53 +0100 Subject: [PATCH] repo-tools: Redirect child stdout to file for CLI reports Use a temp file instead of a pipe for child process stdout in createBinRunner. Node.js uses a SyncWriteStream for regular files, so all writes complete synchronously before process.exit() can discard them. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/repo-tools/src/commands/util.ts | 69 ++++++++++++++++-------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/packages/repo-tools/src/commands/util.ts b/packages/repo-tools/src/commands/util.ts index c1bf6e2a20..bce2359589 100644 --- a/packages/repo-tools/src/commands/util.ts +++ b/packages/repo-tools/src/commands/util.ts @@ -15,33 +15,58 @@ */ import { spawnSync } from 'node:child_process'; +import { openSync, closeSync, readFileSync, unlinkSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +/** + * Redirect stdout to a temp file so that Node.js creates a SyncWriteStream + * (synchronous writes) in the child instead of an async pipe stream. This + * prevents data loss when child processes call process.exit() before the + * async stream buffer has been flushed. + */ export function createBinRunner(cwd: string, path: string) { return async (...command: string[]) => { const args = path ? [path, ...command] : command; - const result = spawnSync('node', args, { - cwd, - stdio: ['ignore', 'pipe', 'pipe'], - maxBuffer: 10 * 1024 * 1024, - }); + const outPath = join( + tmpdir(), + `backstage-cli-out-${process.pid}-${Date.now()}.txt`, + ); + const outFd = openSync(outPath, 'w'); - if (result.error) { - throw new Error(`Process error: ${result.error.message}`); + try { + const result = spawnSync('node', args, { + cwd, + stdio: ['ignore', outFd, 'pipe'], + maxBuffer: 10 * 1024 * 1024, + }); + + closeSync(outFd); + const stdout = readFileSync(outPath, 'utf8'); + + if (result.error) { + throw new Error(`Process error: ${result.error.message}`); + } + + const stderr = result.stderr?.toString() ?? ''; + + if (result.signal) { + throw new Error( + `Process was killed with signal ${result.signal}\n${stderr}`, + ); + } else if (result.status !== 0) { + throw new Error(`Process exited with code ${result.status}\n${stderr}`); + } else if (stderr.trim()) { + throw new Error(`Command printed error output: ${stderr}`); + } + + return stdout; + } finally { + try { + unlinkSync(outPath); + } catch { + /* ignore cleanup errors */ + } } - - const stderr = result.stderr?.toString() ?? ''; - const stdout = result.stdout?.toString() ?? ''; - - if (result.signal) { - throw new Error( - `Process was killed with signal ${result.signal}\n${stderr}`, - ); - } else if (result.status !== 0) { - throw new Error(`Process exited with code ${result.status}\n${stderr}`); - } else if (stderr.trim()) { - throw new Error(`Command printed error output: ${stderr}`); - } - - return stdout; }; }