import { spawnSync } from 'node:child_process'; /** * 執行外部指令並回傳結果(不會因為非零結束碼而 throw)。 * * @param {string} command 要執行的指令 * @param {string[]} args 指令參數 * @param {object} [options] spawnSync 額外設定(cwd、env、input、maxBuffer...) * @returns {{ status: number, stdout: string, stderr: string }} */ export function run(command, args = [], options = {}) { const result = spawnSync(command, args, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, // 64MB,避免大型 diff 被截斷 ...options, }); if (result.error) { return { status: 1, stdout: '', stderr: String(result.error.message || result.error) }; } return { status: typeof result.status === 'number' ? result.status : 1, stdout: result.stdout || '', stderr: result.stderr || '', }; } /** * 執行外部指令,若結束碼非零則 throw。 * * @param {string} command * @param {string[]} args * @param {object} [options] * @returns {string} stdout(已 trim) */ export function runOrThrow(command, args = [], options = {}) { const result = run(command, args, options); if (result.status !== 0) { const detail = (result.stderr || result.stdout || '').trim(); throw new Error(`指令失敗 (${result.status}): ${command} ${args.join(' ')}\n${detail}`); } return result.stdout.trim(); } const ICONS = { info: 'ℹ️', warn: '⚠️', error: '❌', success: '✅', step: '▶️' }; /** 簡單的分級日誌輸出。 */ export const log = { info: (msg) => console.log(`${ICONS.info} ${msg}`), warn: (msg) => console.log(`${ICONS.warn} ${msg}`), error: (msg) => console.error(`${ICONS.error} ${msg}`), success: (msg) => console.log(`${ICONS.success} ${msg}`), step: (msg) => console.log(`\n${ICONS.step} ${msg}`), }; /** * 將敏感字串(如 token)從文字中遮蔽,避免寫入日誌。 * * @param {string} text * @param {string[]} secrets * @returns {string} */ export function maskSecrets(text, secrets = []) { let masked = String(text ?? ''); for (const secret of secrets) { if (secret && secret.length >= 4) { masked = masked.split(secret).join('***'); } } return masked; }