mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-03 18:00:48 -07:00
127 lines
6.1 KiB
TypeScript
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);
|
|
}
|