import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; export interface GitRunOptions { cwd?: string; signal?: AbortSignal; timeout?: number; acceptedExitCodes?: readonly number[]; } export interface GitRunResult { stdout: string; stderr: string; code: number; killed: boolean; } const MAX_GIT_ERROR_CHARS = 4_000; /* oxlint-disable no-control-regex -- 这些正则的目的就是识别并清除控制字符 */ const ANSI_CSI_SEQUENCE = /\u001B\[[0-?]*[ -/]*[@-~]/g; const ANSI_OSC_SEQUENCE = /\u001B\][^\u0007]*(?:\u0007|\u001B\\)/g; const UNSAFE_CONTROL_CHARACTERS = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g; /* oxlint-enable no-control-regex */ /** 清理终端控制序列并统一换行,避免 Git 或 hook 输出污染 TUI。 */ function sanitizeGitOutput(output: string): string { return output .replace(ANSI_OSC_SEQUENCE, "") .replace(ANSI_CSI_SEQUENCE, "") .replaceAll("\r\n", "\n") .replaceAll("\r", "\n") .replace(UNSAFE_CONTROL_CHARACTERS, "") .trim(); } /** 按 Unicode 码点保留首尾并限制错误详情总长度。 */ function truncateGitOutput(output: string, maxChars: number): string { const characters = Array.from(output); if (characters.length <= maxChars) { return output; } let notice = "\n...[Git output truncated]...\n"; let available = Math.max(0, maxChars - Array.from(notice).length); let omitted = characters.length - available; notice = `\n...[${omitted} code points omitted]...\n`; available = Math.max(0, maxChars - Array.from(notice).length); omitted = characters.length - available; notice = `\n...[${omitted} code points omitted]...\n`; const finalAvailable = Math.max(0, maxChars - Array.from(notice).length); const headLength = Math.ceil(finalAvailable / 2); const tailLength = Math.floor(finalAvailable / 2); return `${characters.slice(0, headLength).join("")}${notice}${characters.slice(-tailLength).join("")}`; } /** 生成有界、保留首尾且适合展示给用户的 Git 错误详情。 */ export function formatGitError(result: GitRunResult): string { const stderr = sanitizeGitOutput(result.stderr); const stdout = sanitizeGitOutput(result.stdout); const sections = [stderr ? `stderr:\n${stderr}` : "", stdout ? `stdout:\n${stdout}` : ""].filter( Boolean, ); const detail = sections.join("\n"); return detail ? truncateGitOutput(detail, MAX_GIT_ERROR_CHARS) : `exit code ${result.code}`; } /** 通过 Pi 的参数化执行接口调用 Git。 */ export class GitClient { private readonly pi: Pick; /** 保存扩展 API,确保所有 Git 命令都通过 pi.exec 执行。 */ constructor(pi: Pick) { this.pi = pi; } /** 执行 Git 子命令,并校验允许的退出码。 */ async run(args: string[], options: GitRunOptions = {}): Promise { const result = await this.pi.exec("git", args, { cwd: options.cwd, signal: options.signal, timeout: options.timeout ?? 15_000, }); const acceptedExitCodes = options.acceptedExitCodes ?? [0]; if (result.killed) { throw new Error( `Git command git ${args[0] ?? ""} was cancelled or timed out: ${formatGitError(result)}`, ); } if (!acceptedExitCodes.includes(result.code)) { throw new Error(`Git command git ${args[0] ?? ""} failed: ${formatGitError(result)}`); } return result; } }