feat(ai-pull-request): 以 opencode 分析 diff 自動產生並建立 Pull Request
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user