import {execSync, ExecSyncOptions} from 'child_process' import {execa, type Result} from 'execa' import {als} from './context' import pino from 'pino' const logger = pino({ level: process.env.LOG_LEVEL || 'info' }) /** * Executes a shell command using the current context's working directory. * Thread-safe for Probot. */ export const prixExec = ( command: string, options: ExecSyncOptions = {} ): string => { const context = als.getStore() const cwd = options.cwd || context?.workingDir || process.cwd() try { const result = execSync(command, { ...options, cwd, encoding: 'utf8' }) return result ? result.toString() : '' } catch (err: any) { if (err.status === 1 && command.includes('grep')) { return '' } const errorMessage = err.stderr ? err.stderr.toString() : err.message logger.error({ command, cwd, error: errorMessage }, 'prixExec failed') throw err } } export interface ExecAsyncOptions { cwd?: string timeout?: number env?: Record } /** * Async version of prixExec using execa. * Non-blocking - suitable for production use. */ export const prixExecAsync = async ( command: string, args: string[] = [], options: ExecAsyncOptions = {} ): Promise => { const context = als.getStore() const cwd = options.cwd || context?.workingDir || process.cwd() const timeout = options.timeout || 60000 // Default 60s timeout logger.debug({ command, args, cwd, timeout }, 'Executing async command') try { const result = await execa(command, args, { cwd, timeout, env: { ...process.env, ...options.env }, reject: true }) logger.debug({ command, exitCode: result.exitCode }, 'Command completed') return result } catch (err: any) { if (err.exitCode === 1 && command.includes('grep')) { return { stdout: '', stderr: '', command, escapedCommand: command, cwd, durationMs: 0, failed: false, timedOut: false, isCanceled: false, killed: false, exitCode: 0 } as unknown as Result } logger.error({ command, args, cwd, error: err.message, exitCode: err.exitCode }, 'Async command failed') throw err } }