mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-02 01:11:24 -07:00
128 lines
5.0 KiB
TypeScript
128 lines
5.0 KiB
TypeScript
/**
|
|
* Universal Offload Worker (Remote)
|
|
*
|
|
* Handles worktree provisioning and parallel task execution based on 'playbooks'.
|
|
*/
|
|
import { spawn, spawnSync } from 'child_process';
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
|
|
const prNumber = process.argv[2];
|
|
const branchName = process.argv[3];
|
|
const policyPath = process.argv[4];
|
|
const action = process.argv[5] || 'review';
|
|
|
|
async function main() {
|
|
if (!prNumber || !branchName || !policyPath) {
|
|
console.error('Usage: tsx worker.ts <PR_NUMBER> <BRANCH_NAME> <POLICY_PATH> [action]');
|
|
return 1;
|
|
}
|
|
|
|
const workDir = process.cwd(); // This is remoteWorkDir
|
|
const targetDir = path.join(workDir, branchName);
|
|
|
|
// 1. Provision PR Directory
|
|
if (!fs.existsSync(targetDir)) {
|
|
console.log(`🌿 Provisioning PR #${prNumber} into ${branchName}...`);
|
|
const cloneCmd = `git clone --filter=blob:none https://github.com/google-gemini/gemini-cli.git ${targetDir}`;
|
|
spawnSync(cloneCmd, { stdio: 'inherit', shell: true });
|
|
|
|
process.chdir(targetDir);
|
|
spawnSync('gh', ['pr', 'checkout', prNumber], { stdio: 'inherit' });
|
|
} else {
|
|
process.chdir(targetDir);
|
|
}
|
|
|
|
const logDir = path.join(targetDir, `.gemini/logs/offload-${prNumber}`);
|
|
fs.mkdirSync(logDir, { recursive: true });
|
|
|
|
const geminiBin = path.join(workDir, 'node_modules/.bin/gemini');
|
|
|
|
// 2. Define Playbooks
|
|
let tasks: any[] = [];
|
|
|
|
if (action === 'review') {
|
|
tasks = [
|
|
{ id: 'build', name: 'Fast Build', cmd: `cd ${targetDir} && npm ci && npm run build` },
|
|
{ id: 'ci', name: 'CI Checks', cmd: `gh pr checks ${prNumber}` },
|
|
{ id: 'review', name: 'Gemini Analysis', cmd: `${geminiBin} --policy ${policyPath} --cwd ${targetDir} -p "/review-frontend ${prNumber}"` },
|
|
{ id: 'verify', name: 'Behavioral Proof', cmd: `${geminiBin} --policy ${policyPath} --cwd ${targetDir} -p "Analyze the code in ${targetDir} and exercise it to prove it works."`, dep: 'build' }
|
|
];
|
|
} else if (action === 'fix') {
|
|
tasks = [
|
|
{ id: 'build', name: 'Fast Build', cmd: `cd ${targetDir} && npm ci && npm run build` },
|
|
{ id: 'failures', name: 'Find Failures', cmd: `gh run view --log-failed` },
|
|
{ id: 'fix', name: 'Iterative Fix', cmd: `${geminiBin} --policy ${policyPath} --cwd ${targetDir} -p "Address review comments and fix failing tests for PR ${prNumber}. Repeat until CI is green."`, dep: 'build' }
|
|
];
|
|
} else if (action === 'ready') {
|
|
tasks = [
|
|
{ id: 'clean', name: 'Clean Install', cmd: `npm run clean && npm ci` },
|
|
{ id: 'preflight', name: 'Full Preflight', cmd: `npm run preflight`, dep: 'clean' },
|
|
{ id: 'conflicts', name: 'Conflict Check', cmd: `git fetch origin main && git merge-base --is-ancestor origin/main HEAD || echo "CONFLICT"` }
|
|
];
|
|
} else if (action === 'open') {
|
|
console.log(`🚀 Dropping into manual session for ${branchName}...`);
|
|
process.exit(0);
|
|
}
|
|
|
|
const state: Record<string, any> = {};
|
|
tasks.forEach(t => state[t.id] = { status: 'PENDING' });
|
|
|
|
return new Promise((resolve) => {
|
|
function runTask(task: any) {
|
|
if (task.dep && state[task.dep].status !== 'SUCCESS') {
|
|
setTimeout(() => runTask(task), 1000);
|
|
return;
|
|
}
|
|
|
|
state[task.id].status = 'RUNNING';
|
|
const proc = spawn(task.cmd, { shell: true, env: { ...process.env, FORCE_COLOR: '1' } });
|
|
const logStream = fs.createWriteStream(path.join(logDir, `${task.id}.log`));
|
|
proc.stdout.pipe(logStream);
|
|
proc.stderr.pipe(logStream);
|
|
|
|
proc.on('close', (code) => {
|
|
const exitCode = code ?? 0;
|
|
state[task.id].status = exitCode === 0 ? 'SUCCESS' : 'FAILED';
|
|
fs.writeFileSync(path.join(logDir, `${task.id}.exit`), exitCode.toString());
|
|
render();
|
|
});
|
|
}
|
|
|
|
function render() {
|
|
console.clear();
|
|
console.log(`==================================================`);
|
|
console.log(`🚀 Offload | ${action.toUpperCase()} | PR #${prNumber}`);
|
|
console.log(`📂 Worktree: ${targetDir}`);
|
|
console.log(`==================================================\n`);
|
|
|
|
tasks.forEach(t => {
|
|
const s = state[t.id];
|
|
const icon = s.status === 'SUCCESS' ? '✅' : s.status === 'FAILED' ? '❌' : s.status === 'RUNNING' ? '⏳' : '💤';
|
|
console.log(` ${icon} ${t.name.padEnd(20)}: ${s.status}`);
|
|
});
|
|
|
|
const allDone = tasks.every(t => ['SUCCESS', 'FAILED'].includes(state[t.id].status));
|
|
if (allDone) {
|
|
console.log(`\n✨ Playbook complete. Launching interactive session...`);
|
|
resolve(0);
|
|
}
|
|
}
|
|
|
|
tasks.filter(t => !t.dep).forEach(runTask);
|
|
tasks.filter(t => t.dep).forEach(runTask);
|
|
const intervalId = setInterval(render, 1500);
|
|
|
|
const checkAllDone = setInterval(() => {
|
|
if (tasks.every(t => ['SUCCESS', 'FAILED'].includes(state[t.id].status))) {
|
|
clearInterval(intervalId);
|
|
clearInterval(checkAllDone);
|
|
}
|
|
}, 1000);
|
|
});
|
|
}
|
|
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
runWorker(process.argv.slice(2)).catch(console.error);
|
|
}
|