Files
gemini-cli/.gemini/skills/offload/scripts/orchestrator.ts

127 lines
6.1 KiB
TypeScript

/**
* Universal Offload Orchestrator (Local)
*/
import { spawnSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '../../../..');
const q = (str: string) => `'${str.replace(/'/g, "'\\''")}'`;
export async function runOrchestrator(args: string[], env: NodeJS.ProcessEnv = process.env) {
const prNumber = args[0];
const action = args[1] || 'review'; // Default action is review
if (!prNumber) {
console.error('Usage: npm run offload <PR_NUMBER> [action]');
return 1;
}
// Load Settings
const settingsPath = path.join(REPO_ROOT, '.gemini/settings.json');
let settings: any = {};
if (fs.existsSync(settingsPath)) {
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (e) {}
}
let config = settings.maintainer?.deepReview;
if (!config) {
console.log('⚠️ Offload configuration not found. Launching setup...');
const setupResult = spawnSync('npm', ['run', 'offload:setup'], { stdio: 'inherit' });
if (setupResult.status !== 0) {
console.error('❌ Setup failed. Please run "npm run offload:setup" manually.');
return 1;
}
// Reload settings after setup
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
config = settings.maintainer.deepReview;
}
const { remoteHost, remoteWorkDir, terminalType, syncAuth, geminiSetup, ghSetup } = config;
console.log(`🔍 Fetching metadata for ${action === 'implement' ? 'Issue' : 'PR'} #${prNumber}...`);
const ghCmd = action === 'implement' ? ['issue', 'view', prNumber, '--json', 'title', '-q', '.title'] : ['pr', 'view', prNumber, '--json', 'headRefName', '-q', '.headRefName'];
const ghView = spawnSync('gh', ghCmd, { shell: true });
const metaName = ghView.stdout.toString().trim() || `task-${prNumber}`;
const branchName = action === 'implement' ? `impl-${prNumber}` : metaName;
const sessionName = `offload-${prNumber}-${branchName.replace(/[^a-zA-Z0-9]/g, '-')}`;
// 2. Sync Configuration Mirror (Isolated Profiles)
const ISOLATED_GEMINI = geminiSetup === 'isolated' ? '~/.offload/gemini-cli-config' : '~/.gemini';
const ISOLATED_GH = ghSetup === 'isolated' ? '~/.offload/gh-cli-config' : '~/.config/gh';
const remotePolicyPath = `${ISOLATED_GEMINI}/policies/offload-policy.toml`;
console.log(`📡 Mirroring environment to ${remoteHost}...`);
spawnSync('ssh', [remoteHost, `mkdir -p ${remoteWorkDir}/.gemini/skills/offload/scripts/ ${ISOLATED_GEMINI}/policies/`]);
// Sync the policy file specifically
spawnSync('rsync', ['-avz', path.join(REPO_ROOT, '.gemini/skills/offload/policy.toml'), `${remoteHost}:${remotePolicyPath}`]);
spawnSync('rsync', ['-avz', '--delete', path.join(REPO_ROOT, '.gemini/skills/offload/scripts/'), `${remoteHost}:${remoteWorkDir}/.gemini/skills/offload/scripts/`]);
if (syncAuth) {
const homeDir = env.HOME || '';
const localGeminiDir = path.join(homeDir, '.gemini');
const syncFiles = ['google_accounts.json', 'settings.json'];
for (const f of syncFiles) {
const lp = path.join(localGeminiDir, f);
if (fs.existsSync(lp)) spawnSync('rsync', ['-avz', lp, `${remoteHost}:${ISOLATED_GEMINI}/${f}`]);
}
const localPolicies = path.join(localGeminiDir, 'policies/');
if (fs.existsSync(localPolicies)) spawnSync('rsync', ['-avz', '--delete', localPolicies, `${remoteHost}:${ISOLATED_GEMINI}/policies/`]);
const localEnv = path.join(REPO_ROOT, '.env');
if (fs.existsSync(localEnv)) spawnSync('rsync', ['-avz', localEnv, `${remoteHost}:${remoteWorkDir}/.env`]);
}
// 3. Construct Clean Command
const envLoader = 'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && \\. "$NVM_DIR/nvm.sh"';
const remoteWorker = `export GEMINI_CLI_HOME=${ISOLATED_GEMINI} && export GH_CONFIG_DIR=${ISOLATED_GH} && ./node_modules/.bin/tsx .gemini/skills/offload/scripts/entrypoint.ts ${prNumber} ${branchName} ${remotePolicyPath} ${action}`;
const tmuxCmd = `cd ${remoteWorkDir} && ${envLoader} && ${remoteWorker}; exec $SHELL`;
const sshInternal = `tmux attach-session -t ${sessionName} 2>/dev/null || tmux new-session -s ${sessionName} -n ${q(branchName)} ${q(tmuxCmd)}`;
const sshCmd = `ssh -t ${remoteHost} ${q(sshInternal)}`;
// 4. Smart Context Execution
const isWithinGemini = !!env.GEMINI_SESSION_ID || !!env.GCLI_SESSION_ID;
const forceBackground = args.includes('--background');
if (isWithinGemini || forceBackground) {
if (process.platform === 'darwin' && terminalType !== 'none' && !forceBackground) {
// macOS: Use Window Automation
let appleScript = `on run argv\n set theCommand to item 1 of argv\n tell application "iTerm"\n set newWindow to (create window with default profile)\n tell current session of newWindow\n write text theCommand\n end tell\n activate\n end tell\n end run`;
if (terminalType === 'terminal') {
appleScript = `on run argv\n set theCommand to item 1 of argv\n tell application "Terminal"\n do script theCommand\n activate\n end tell\n end run`;
}
spawnSync('osascript', ['-', sshCmd], { input: appleScript });
console.log(`${terminalType.toUpperCase()} window opened for verification.`);
return 0;
}
// Cross-Platform Background Mode
console.log(`📡 Launching remote verification in background mode...`);
const logFile = path.join(REPO_ROOT, `.gemini/logs/offload-${prNumber}/background.log`);
fs.mkdirSync(path.dirname(logFile), { recursive: true });
const backgroundCmd = `ssh ${remoteHost} ${q(tmuxCmd)} > ${q(logFile)} 2>&1 &`;
spawnSync(backgroundCmd, { shell: true });
console.log(`⏳ Remote worker started in background.`);
console.log(`📄 Tailing logs to: .gemini/logs/offload-${prNumber}/background.log`);
return 0;
}
// Direct Shell Mode: Execute SSH in-place
console.log(`🚀 Launching offload session in current terminal...`);
const result = spawnSync(sshCmd, { stdio: 'inherit', shell: true });
return result.status || 0;
}
if (import.meta.url === `file://${process.argv[1]}`) {
runOrchestrator(process.argv.slice(2)).catch(console.error);
}