import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; import { spawn } from 'child_process'; export interface CommitWorkspaceOptions { workspacePath: string; branchName: string; commitMessage: string; ignoreEntries?: string[]; } export interface CommitWorkspaceResult { changed: boolean; committed: boolean; pushed: boolean; } interface GitResult { code: number; stdout: string; stderr: string; } function runGit(workspacePath: string, args: string[], extraEnv?: Record): Promise { return new Promise((resolve) => { const child = spawn('git', args, { cwd: workspacePath, stdio: ['ignore', 'pipe', 'pipe'], timeout: 30_000, env: { ...process.env, ...extraEnv }, }); let stdout = ''; let stderr = ''; child.stdout.on('data', (d: Buffer) => { stdout += d.toString(); }); child.stderr.on('data', (d: Buffer) => { stderr += d.toString(); }); child.on('error', () => resolve({ code: -1, stdout, stderr })); child.on('close', (code) => resolve({ code: code ?? -1, stdout, stderr })); }); } export async function ensureWorkspaceGitRepo(workspacePath: string): Promise { if (existsSync(join(workspacePath, '.git'))) return; mkdirSync(workspacePath, { recursive: true }); const init = await runGit(workspacePath, ['init', '--initial-branch=main']); if (init.code !== 0) { throw new Error(`git init failed: ${init.stderr.slice(0, 200)}`); } } export async function commitWorkspaceChanges(options: CommitWorkspaceOptions): Promise { const ignoreEntries = options.ignoreEntries ?? ['input/', 'logs/']; await ensureWorkspaceGitRepo(options.workspacePath); const status = await runGit(options.workspacePath, ['status', '--porcelain']); if (status.stdout.trim() === '') { return { changed: false, committed: false, pushed: false }; } const checkout = await runGit(options.workspacePath, ['checkout', '-b', options.branchName]); if (checkout.code !== 0) { await runGit(options.workspacePath, ['checkout', options.branchName]); } const excludePath = join(options.workspacePath, '.git', 'info', 'exclude'); let excludeContent = ''; try { excludeContent = readFileSync(excludePath, 'utf-8'); } catch { // ignore } const missingEntries = ignoreEntries.filter(entry => !excludeContent.includes(entry)); if (missingEntries.length > 0) { const nextContent = excludeContent.trimEnd() + '\n' + missingEntries.join('\n') + '\n'; writeFileSync(excludePath, nextContent); } await runGit(options.workspacePath, ['add', '-A']); const commit = await runGit(options.workspacePath, [ '-c', 'user.name=Agent Bot', '-c', 'user.email=maestro@noreply', 'commit', '-m', options.commitMessage, ]); if (commit.code !== 0) { return { changed: true, committed: false, pushed: false }; } return { changed: true, committed: true, pushed: false }; }