import { execSync } from 'node:child_process'; /** * Execute CLI command with proper isolation and error handling * * Disconnects stdin to ensure CLI cannot prompt for user input. * Any attempt to read from stdin will immediately fail, preventing hanging tests. * * When command fails, throws an error carrying stderr — where the CLI writes * its diagnostics. Both helpers used to concatenate stdout and stderr because * `@clack/prompts` put error messages on stdout, which left no way to tell a * result from a complaint about producing one (celilo#699). The streams are * separate now and this reads only the one that carries diagnostics. * * @param cli - CLI command prefix (includes env vars and path) * @param command - Command to execute (e.g., "system config get") * @param options - Additional exec options * @returns Command stdout as string * @throws Error with the stderr message when command fails */ export function runCli(cli: string, command: string, options: { encoding?: 'utf-8' } = {}): string { try { return execSync(`${cli} ${command}`, { encoding: options.encoding || 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], // stdin ignored, stdout/stderr captured timeout: 30000, // 30 second timeout to prevent tests hanging indefinitely }); } catch (error: unknown) { const stderr = (error as { stderr?: Buffer }).stderr?.toString() || ''; throw new Error(stderr.trim() || (error as Error).message); } } /** * Execute CLI command expecting failure * Returns stderr for assertion — that is where the CLI writes diagnostics. * * Disconnects stdin to ensure CLI cannot prompt for user input. * * @param cli - CLI command prefix * @param command - Command to execute * @returns stderr as string * @throws Error if command succeeds (when it should fail) */ export function runCliExpectingFailure(cli: string, command: string): string { try { execSync(`${cli} ${command}`, { stdio: ['ignore', 'pipe', 'pipe'] }); } catch (error: unknown) { const stderr = (error as { stderr?: Buffer }).stderr?.toString() || ''; return stderr.trim() || (error as Error).message; } // Previously thrown from inside the `try`, where the catch below swallowed it // and RETURNED the message — so a command that wrongly succeeded read as a // passing assertion about its own failure. throw new Error('Expected command to fail but it succeeded'); }