From 1f14f609eeb9e6f66e7a8662c0ca92123d31c3cd Mon Sep 17 00:00:00 2001 From: mkorwel Date: Sat, 14 Mar 2026 00:43:53 -0700 Subject: [PATCH] feat(offload): finalize GCE-only architecture and secure fleet management --- .gemini/settings.json | 2 +- .gemini/skills/offload/scripts/fleet.ts | 108 +++++++++++ .../skills/offload/scripts/orchestrator.ts | 145 +++++++------- .../offload/scripts/provision-worker.sh | 56 ++++++ .gemini/skills/offload/scripts/setup.ts | 159 +++------------- .gemini/skills/offload/tests/matrix.test.ts | 33 ++-- .../offload/tests/orchestration.test.ts | 178 ++++-------------- package.json | 1 + 8 files changed, 328 insertions(+), 354 deletions(-) create mode 100644 .gemini/skills/offload/scripts/fleet.ts create mode 100644 .gemini/skills/offload/scripts/provision-worker.sh diff --git a/.gemini/settings.json b/.gemini/settings.json index 77c4dab255..73d5767fc9 100644 --- a/.gemini/settings.json +++ b/.gemini/settings.json @@ -9,7 +9,7 @@ }, "maintainer": { "deepReview": { - "remoteHost": "cli", + "remoteHost": "bg1", "remoteWorkDir": "~/.offload/workspace", "terminalType": "iterm2", "syncAuth": true, diff --git a/.gemini/skills/offload/scripts/fleet.ts b/.gemini/skills/offload/scripts/fleet.ts new file mode 100644 index 0000000000..0c78cb0f63 --- /dev/null +++ b/.gemini/skills/offload/scripts/fleet.ts @@ -0,0 +1,108 @@ +/** + * Offload Fleet Manager + * + * Manages dynamic GCP workers for offloading tasks. + */ +import { spawnSync } from 'child_process'; + +const PROJECT_ID = 'gemini-cli-team-quota'; +const USER = process.env.USER || 'mattkorwel'; +const INSTANCE_PREFIX = `gcli-offload-${USER}`; + +async function listWorkers() { + console.log(`šŸ” Listing Offload Workers for ${USER} in ${PROJECT_ID}...`); + + const result = spawnSync('gcloud', [ + 'compute', 'instances', 'list', + '--project', PROJECT_ID, + '--filter', `name~^${INSTANCE_PREFIX}`, + '--format', 'table(name,zone,status,networkInterfaces[0].networkIP:label=INTERNAL_IP,creationTimestamp)' + ], { stdio: 'inherit' }); + + if (result.status !== 0) { + console.error('\nāŒ Failed to list workers. Ensure you have access to the project and gcloud is authenticated.'); + } +} + +async function provisionWorker() { + const instanceId = Math.floor(Date.now() / 1000); + const name = `${INSTANCE_PREFIX}-${instanceId}`; + const zone = 'us-west1-a'; + + console.log(`šŸš€ Provisioning secure offload worker: ${name}...`); + + // Hardened Metadata: Enable OS Login and 72h Self-Deletion + const startupScript = `#!/bin/bash + echo "gcloud compute instances delete ${name} --zone ${zone} --project ${PROJECT_ID} --quiet" | at now + 72 hours + `; + + const result = spawnSync('gcloud', [ + 'compute', 'instances', 'create', name, + '--project', PROJECT_ID, + '--zone', zone, + '--machine-type', 'n2-standard-8', + '--image-family', 'gcli-maintainer-worker', + '--image-project', PROJECT_ID, + '--metadata', `enable-oslogin=TRUE,startup-script=${startupScript}`, + '--labels', `owner=${USER.replace(/[^a-z0-9_-]/g, '_')},type=offload-worker`, + '--tags', `gcli-offload-${USER}`, + '--scopes', 'https://www.googleapis.com/auth/cloud-platform' + ], { stdio: 'inherit' }); + + if (result.status === 0) { + console.log(`\nāœ… Worker ${name} is being provisioned.`); + console.log(`šŸ‘‰ Access is restricted via OS Login and tags.`); + } +} + +async function createImage() { + const name = `gcli-maintainer-worker-build-${Math.floor(Date.now() / 1000)}`; + const zone = 'us-west1-a'; + const imageName = 'gcli-maintainer-worker-v1'; + + console.log(`šŸ—ļø Building Maintainer Image: ${imageName}...`); + + // 1. Create a temporary builder VM + console.log(' - Creating temporary builder VM...'); + spawnSync('gcloud', [ + 'compute', 'instances', 'create', name, + '--project', PROJECT_ID, + '--zone', zone, + '--machine-type', 'n2-standard-4', + '--image-family', 'ubuntu-2204-lts', + '--image-project', 'ubuntu-os-cloud', + '--metadata-from-file', `startup-script=.gemini/skills/offload/scripts/provision-worker.sh` + ], { stdio: 'inherit' }); + + console.log('\nā³ Waiting for provisioning to complete (this takes ~3-5 mins)...'); + console.log(' - You can tail the startup script via:'); + console.log(` gcloud compute instances get-serial-port-output ${name} --project ${PROJECT_ID} --zone ${zone} --follow`); + + // Note: For a true automation we'd poll here, but for a maintainer tool, + // we'll provide the instructions to finalize. + console.log(`\nšŸ‘‰ Once provisioning is DONE, run these commands to finalize:`); + console.log(` 1. gcloud compute instances stop ${name} --project ${PROJECT_ID} --zone ${zone}`); + console.log(` 2. gcloud compute images create ${imageName} --project ${PROJECT_ID} --source-disk ${name} --source-disk-zone ${zone} --family gcli-maintainer-worker`); + console.log(` 3. gcloud compute instances delete ${name} --project ${PROJECT_ID} --zone ${zone} --quiet`); +} + +async function main() { + const action = process.argv[2] || 'list'; + + switch (action) { + case 'list': + await listWorkers(); + break; + case 'provision': + await provisionWorker(); + break; + case 'create-image': + await createImage(); + break; + default: + console.error(`āŒ Unknown fleet action: ${action}`); + process.exit(1); + } +} + +main().catch(console.error); diff --git a/.gemini/skills/offload/scripts/orchestrator.ts b/.gemini/skills/offload/scripts/orchestrator.ts index 9b8cc23d7c..06d36d9f57 100644 --- a/.gemini/skills/offload/scripts/orchestrator.ts +++ b/.gemini/skills/offload/scripts/orchestrator.ts @@ -1,5 +1,7 @@ /** * Universal Offload Orchestrator (Local) + * + * Automatically detects and connects to your dynamic GCE fleet. */ import { spawnSync } from 'child_process'; import path from 'path'; @@ -13,55 +15,75 @@ 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 + const action = args[1] || 'review'; if (!prNumber) { console.error('Usage: npm run offload [action]'); return 1; } - // Load Settings + // 1. Load GCP 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.'); + if (!fs.existsSync(settingsPath)) { + console.error('āŒ Settings not found. Run "npm run offload:setup" first.'); return 1; - } - // Reload settings after setup - settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); - config = settings.maintainer.deepReview; + } + const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); + const config = settings.maintainer?.deepReview; + if (!config) { + console.error('āŒ Fleet settings not found. Run "npm run offload:setup" first.'); + return 1; } - const { remoteHost, remoteWorkDir, terminalType, syncAuth, geminiSetup, ghSetup } = config; + const { projectId, zone, terminalType, syncAuth } = config; + const userPrefix = `gcli-offload-${env.USER || 'mattkorwel'}`; + console.log(`šŸ” Finding active fleet workers for ${userPrefix}...`); + + // 2. Discover Worker VM + const gcloudList = spawnSync(`gcloud compute instances list --project ${projectId} --filter="name~^${userPrefix} AND status=RUNNING" --format="json"`, { shell: true }); + + let instances = []; + try { + instances = JSON.parse(gcloudList.stdout.toString()); + } catch (e) { + console.error('āŒ Failed to parse gcloud output. Ensure you are logged in.'); + return 1; + } + + if (instances.length === 0) { + console.log('āš ļø No active workers found. Please run "npm run offload:fleet provision" first.'); + return 1; + } + + // Default to the first found worker + const targetVM = instances[0].name; + const remoteWorkDir = '/home/ubuntu/.offload/workspace'; + const sessionName = `offload-${prNumber}-${action}`; + + // Fetch Metadata (local) 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 ghCmd = action === 'implement' + ? `gh issue view ${prNumber} --json title -q .title` + : `gh pr view ${prNumber} --json headRefName -q .headRefName`; + + const ghView = spawnSync(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/`]); + console.log(`šŸ“” Using worker: ${targetVM}`); + + // 3. Mirror logic + const ISOLATED_GEMINI = '~/.offload/gemini-cli-config'; + const ISOLATED_GH = '~/.offload/gh-cli-config'; + const remotePolicyPath = `${ISOLATED_GEMINI}/policies/offload-policy.toml`; + + console.log(`šŸ“¦ Synchronizing with ${targetVM}...`); + spawnSync(`gcloud compute ssh ${targetVM} --project ${projectId} --zone ${zone} --command "mkdir -p ${remoteWorkDir} ${ISOLATED_GEMINI}/policies/"`, { shell: true }); + + // Sync scripts and policy + spawnSync(`rsync -avz -e "gcloud compute ssh --project ${projectId} --zone ${zone}" .gemini/skills/offload/policy.toml ${targetVM}:${remotePolicyPath}`, { shell: true }); + spawnSync(`rsync -avz --delete -e "gcloud compute ssh --project ${projectId} --zone ${zone}" .gemini/skills/offload/scripts/ ${targetVM}:${remoteWorkDir}/.gemini/skills/offload/scripts/`, { shell: true }); if (syncAuth) { const homeDir = env.HOME || ''; @@ -69,56 +91,31 @@ export async function runOrchestrator(args: string[], env: NodeJS.ProcessEnv = p 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}`]); + if (fs.existsSync(lp)) { + spawnSync(`rsync -avz -e "gcloud compute ssh --project ${projectId} --zone ${zone}" ${lp} ${targetVM}:${ISOLATED_GEMINI}/${f}`, { shell: true }); + } } - 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 + // 4. Construct 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 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)}`; + + const sshInternal = `tmux attach-session -t ${sessionName} 2>/dev/null || tmux new-session -s ${sessionName} -n 'offload' ${q(tmuxCmd)}`; + const finalSSH = `gcloud compute ssh ${targetVM} --project ${projectId} --zone ${zone} -- -t ${q(sshInternal)}`; - // 4. Smart Context Execution + // 5. Terminal Automation 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`); + if (isWithinGemini) { + const appleScript = `on run argv\n tell application "iTerm"\n set newWindow to (create window with default profile)\n tell current session of newWindow\n write text (item 1 of argv)\n end tell\n activate\n end tell\n end run`; + spawnSync('osascript', ['-', finalSSH], { input: appleScript }); + console.log(`āœ… iTerm2 window opened on ${targetVM}.`); 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; + spawnSync(finalSSH, { stdio: 'inherit', shell: true }); + return 0; } if (import.meta.url === `file://${process.argv[1]}`) { diff --git a/.gemini/skills/offload/scripts/provision-worker.sh b/.gemini/skills/offload/scripts/provision-worker.sh new file mode 100644 index 0000000000..26b06f906a --- /dev/null +++ b/.gemini/skills/offload/scripts/provision-worker.sh @@ -0,0 +1,56 @@ +#!/bin/bash +set -e + +# Ensure we have a valid environment for non-interactive startup +export USER=${USER:-ubuntu} +export HOME=/home/$USER +export DEBIAN_FRONTEND=noninteractive + +echo "šŸ› ļø Provisioning Gemini CLI Maintainer Worker for user: $USER" + +# Wait for apt lock +wait_for_apt() { + echo "Waiting for apt lock..." + while sudo fuser /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock >/dev/null 2>&1 ; do + sleep 2 + done +} + +wait_for_apt + +# 1. System Essentials +apt-get update && apt-get install -y \ + curl git git-lfs tmux build-essential unzip jq gnupg cron + +# 2. GitHub CLI +if ! command -v gh &> /dev/null; then + curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg + chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null + wait_for_apt + apt-get update && apt-get install gh -y +fi + +# 3. Direct Node.js 20 Installation (NodeSource) +echo "Removing any existing nodejs/npm..." +wait_for_apt +apt-get purge -y nodejs npm || true +apt-get autoremove -y + +echo "Installing Node.js 20 via NodeSource..." +curl -fsSL https://deb.nodesource.com/setup_20.x | bash - +wait_for_apt +apt-get install -y nodejs + +# Verify installations +node -v +npm -v + +# 4. Install Gemini CLI (Nightly) +echo "Installing Gemini CLI..." +npm install -g @google/gemini-cli@nightly + +# 5. Self-Deletion Cron (Safety) +(crontab -u $USER -l 2>/dev/null; echo "0 0 * * * gcloud compute instances delete $(hostname) --zone $(curl -H Metadata-Flavor:Google http://metadata.google.internal/computeMetadata/v1/instance/zone | cut -d/ -f4) --quiet") | crontab -u $USER - + +echo "āœ… Provisioning Complete!" diff --git a/.gemini/skills/offload/scripts/setup.ts b/.gemini/skills/offload/scripts/setup.ts index 4c2b18fda0..3494a00fbc 100644 --- a/.gemini/skills/offload/scripts/setup.ts +++ b/.gemini/skills/offload/scripts/setup.ts @@ -1,5 +1,7 @@ /** - * Universal Deep Review Onboarding (Local) + * Universal Offload Onboarding (Local) + * + * Configures the GCP Project and Fleet defaults. */ import { spawnSync } from 'child_process'; import path from 'path'; @@ -10,8 +12,6 @@ import readline from 'readline'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(__dirname, '../../../..'); -const q = (str: string) => `'${str.replace(/'/g, "'\\''")}'`; - async function prompt(question: string, defaultValue: string): Promise { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve) => { @@ -33,141 +33,33 @@ async function confirm(question: string): Promise { } export async function runSetup(env: NodeJS.ProcessEnv = process.env) { - console.log('\n🌟 Initializing Offload Skill Settings...'); + console.log('\n🌟 Initializing GCE Offload Fleet Settings...'); - const OFFLOAD_BASE = '~/.offload'; - const remoteHost = await prompt('Remote SSH Host', 'cli'); - const remoteWorkDir = await prompt('Remote Work Directory', `${OFFLOAD_BASE}/workspace`); + const projectId = await prompt('GCP Project ID', 'gemini-cli-team-quota'); + const zone = await prompt('Compute Zone', 'us-west1-a'); + const machineType = await prompt('Machine Type', 'n2-standard-8'); - console.log(`šŸ” Checking state of ${remoteHost}...`); - const envLoader = 'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && \\. "$NVM_DIR/nvm.sh"'; - - // Probe remote for existing installations - const ghCheck = spawnSync('ssh', [remoteHost, 'sh -lc "command -v gh"'], { stdio: 'pipe' }); - const tmuxCheck = spawnSync('ssh', [remoteHost, 'sh -lc "command -v tmux"'], { stdio: 'pipe' }); - const geminiCheck = spawnSync('ssh', [remoteHost, `sh -lc "${envLoader} && command -v gemini"`], { stdio: 'pipe' }); - - const hasGH = ghCheck.status === 0; - const hasTmux = tmuxCheck.status === 0; - const hasGemini = geminiCheck.status === 0; - - // 1. Gemini CLI Isolation Choice - let geminiSetup = 'isolated'; - if (hasGemini) { - const geminiChoice = await prompt(`\nGemini CLI found on remote. Use [p]re-existing instance or [i]solated sandbox instance? (Isolated is recommended)`, 'i'); - geminiSetup = geminiChoice.toLowerCase() === 'p' ? 'preexisting' : 'isolated'; - } else { - console.log('\nšŸ’” Gemini CLI not found on remote. Defaulting to isolated sandbox instance.'); + console.log(`šŸ” Verifying project access for ${projectId}...`); + const projectCheck = spawnSync('gcloud', ['projects', 'describe', projectId], { stdio: 'pipe' }); + if (projectCheck.status !== 0) { + console.error(`āŒ Access denied to project: ${projectId}. Ensure you are logged in via gcloud.`); + return 1; } - // 2. GitHub CLI Isolation Choice - let ghSetup = 'isolated'; - if (hasGH) { - const ghChoice = await prompt(`GitHub CLI found on remote. Use [p]re-existing instance or [i]solated sandbox instance? (Isolated is recommended)`, 'i'); - ghSetup = ghChoice.toLowerCase() === 'p' ? 'preexisting' : 'isolated'; - } else { - console.log('šŸ’” GitHub CLI not found on remote. Defaulting to isolated sandbox instance.'); - } - - const ISOLATED_GEMINI_CONFIG = `${OFFLOAD_BASE}/gemini-cli-config`; - const ISOLATED_GH_CONFIG = `${OFFLOAD_BASE}/gh-cli-config`; - - if (!hasGH || !hasTmux) { - console.log('\nšŸ“„ System Requirements Check:'); - if (!hasGH) console.log(' āŒ GitHub CLI (gh) is not installed on remote.'); - if (!hasTmux) console.log(' āŒ tmux is not installed on remote.'); - - const shouldProvision = await confirm('\nWould you like Gemini to automatically provision missing requirements?'); - if (shouldProvision) { - console.log(`šŸš€ Attempting to provision dependencies on ${remoteHost}...`); - const osCheck = spawnSync('ssh', [remoteHost, 'uname -s'], { stdio: 'pipe' }); - const os = osCheck.stdout.toString().trim(); - - let installCmd = ''; - if (os === 'Linux') { - installCmd = 'sudo apt update && sudo apt install -y ' + [!hasGH ? 'gh' : '', !hasTmux ? 'tmux' : ''].filter(Boolean).join(' '); - } else if (os === 'Darwin') { - installCmd = 'brew install ' + [!hasGH ? 'gh' : '', !hasTmux ? 'tmux' : ''].filter(Boolean).join(' '); - } - - if (installCmd) { - spawnSync('ssh', ['-t', remoteHost, installCmd], { stdio: 'inherit' }); - } - } else { - console.log('āš ļø Please ensure gh and tmux are installed before running again.'); - return 1; - } - } - - // Ensure remote work dir and isolated config dirs exist - spawnSync('ssh', [remoteHost, `mkdir -p ${remoteWorkDir} ${ISOLATED_GEMINI_CONFIG}/policies/ ${ISOLATED_GH_CONFIG}`], { stdio: 'pipe' }); // Identity Synchronization Onboarding console.log('\nšŸ” Identity & Authentication:'); - - // GH Auth Check - const ghAuthCmd = ghSetup === 'isolated' ? `export GH_CONFIG_DIR=${ISOLATED_GH_CONFIG} && gh auth status` : 'gh auth status'; - const remoteGHAuth = spawnSync('ssh', [remoteHost, `sh -lc "${ghAuthCmd}"`], { stdio: 'pipe' }); - const isGHAuthRemote = remoteGHAuth.status === 0; - - if (isGHAuthRemote) { - console.log(` āœ… GitHub CLI is already authenticated on remote (${ghSetup}).`); - } else { - console.log(` āŒ GitHub CLI is NOT authenticated on remote (${ghSetup}).`); - // If it's isolated but global is authenticated, offer to sync - if (ghSetup === 'isolated') { - const globalGHAuth = spawnSync('ssh', [remoteHost, 'sh -lc "gh auth status"'], { stdio: 'pipe' }); - if (globalGHAuth.status === 0) { - if (await confirm(' Global GH auth found. Sync it to isolated instance?')) { - spawnSync('ssh', [remoteHost, `mkdir -p ${ISOLATED_GH_CONFIG} && cp -r ~/.config/gh/* ${ISOLATED_GH_CONFIG}/`]); - const verifySync = spawnSync('ssh', [remoteHost, `sh -lc "export GH_CONFIG_DIR=${ISOLATED_GH_CONFIG} && gh auth status"`], { stdio: 'pipe' }); - if (verifySync.status === 0) { - console.log(' āœ… GitHub CLI successfully authenticated via sync.'); - return; // Skip the "may need to login" message - } - } - } - } - console.log(' āš ļø GitHub CLI is not yet authenticated. You may need to run "gh auth login" on the remote machine later.'); - } - - // Gemini Auth Check - const geminiAuthCheck = geminiSetup === 'isolated' - ? `[ -f ${ISOLATED_GEMINI_CONFIG}/google_accounts.json ]` - : '[ -f ~/.gemini/google_accounts.json ]'; - const remoteGeminiAuth = spawnSync('ssh', [remoteHost, `sh -lc "${geminiAuthCheck}"`], { stdio: 'pipe' }); - const isGeminiAuthRemote = remoteGeminiAuth.status === 0; + const homeDir = env.HOME || ''; + const localAuth = path.join(homeDir, '.gemini/google_accounts.json'); + const hasAuth = fs.existsSync(localAuth); let syncAuth = false; - if (isGeminiAuthRemote) { - console.log(` āœ… Gemini CLI is already authenticated on remote (${geminiSetup}).`); - } else { - const homeDir = env.HOME || ''; - const localAuth = path.join(homeDir, '.gemini/google_accounts.json'); - const localEnv = path.join(REPO_ROOT, '.env'); - const hasAuth = fs.existsSync(localAuth); - const hasEnv = fs.existsSync(localEnv); - - if (hasAuth || hasEnv) { - console.log(` šŸ” Found local Gemini CLI credentials: ${[hasAuth ? 'Google Account' : '', hasEnv ? '.env' : ''].filter(Boolean).join(', ')}`); - syncAuth = await confirm(' Would you like Gemini to automatically sync your local credentials to the remote workstation for seamless authentication?'); - } + if (hasAuth) { + console.log(` šŸ” Found local Gemini CLI credentials.`); + syncAuth = await confirm(' Would you like to automatically sync your local credentials to new fleet workers for seamless authentication?'); } const terminalType = await prompt('\nTerminal Automation (iterm2 / terminal / none)', 'iterm2'); - // Local Dependencies Install (Isolated) - console.log(`\nšŸ“¦ Checking isolated dependencies in ${remoteWorkDir}...`); - const checkCmd = `ssh ${remoteHost} ${q(`${envLoader} && [ -x ${remoteWorkDir}/node_modules/.bin/tsx ] && [ -x ${remoteWorkDir}/node_modules/.bin/gemini ]`)}`; - const depCheck = spawnSync(checkCmd, { shell: true }); - - if (depCheck.status !== 0) { - console.log(`šŸ“¦ Installing isolated dependencies (nightly CLI & tsx) in ${remoteWorkDir}...`); - const installCmd = `ssh ${remoteHost} ${q(`${envLoader} && mkdir -p ${remoteWorkDir} && cd ${remoteWorkDir} && [ -f package.json ] || npm init -y > /dev/null && npm install tsx @google/gemini-cli@nightly`)}`; - spawnSync(installCmd, { stdio: 'inherit', shell: true }); - } else { - console.log('āœ… Isolated dependencies already present.'); - } - // Save Settings const settingsPath = path.join(REPO_ROOT, '.gemini/settings.json'); let settings: any = {}; @@ -175,10 +67,21 @@ export async function runSetup(env: NodeJS.ProcessEnv = process.env) { try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (e) {} } settings.maintainer = settings.maintainer || {}; - settings.maintainer.deepReview = { remoteHost, remoteWorkDir, terminalType, syncAuth, geminiSetup, ghSetup }; + settings.maintainer.deepReview = { + projectId, + zone, + machineType, + terminalType, + syncAuth, + setupType: 'isolated', + geminiSetup: 'isolated', + ghSetup: 'isolated' + }; fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); - console.log('\nāœ… Onboarding complete! Settings saved to .gemini/settings.json'); + + console.log('\nāœ… GCE Fleet Onboarding complete! Settings saved to .gemini/settings.json'); + console.log(`šŸ‘‰ Use 'npm run offload:fleet provision' to spin up your first worker.`); return 0; } diff --git a/.gemini/skills/offload/tests/matrix.test.ts b/.gemini/skills/offload/tests/matrix.test.ts index bf9e3e2686..e6e53dfbd9 100644 --- a/.gemini/skills/offload/tests/matrix.test.ts +++ b/.gemini/skills/offload/tests/matrix.test.ts @@ -13,12 +13,12 @@ describe('Offload Tooling Matrix', () => { const mockSettings = { maintainer: { deepReview: { - remoteHost: 'test-host', - remoteWorkDir: '~/test-dir', + projectId: 'test-project', + zone: 'us-west1-a', terminalType: 'none', syncAuth: false, - geminiSetup: 'preexisting', - ghSetup: 'preexisting' + geminiSetup: 'isolated', + ghSetup: 'isolated' } } }; @@ -33,8 +33,19 @@ describe('Offload Tooling Matrix', () => { vi.spyOn(process, 'chdir').mockImplementation(() => {}); vi.mocked(spawnSync).mockImplementation((cmd: any, args: any) => { - if (cmd === 'ssh' && args?.[1]?.includes('command -v')) return { status: 0 } as any; - return { status: 0, stdout: Buffer.from('test-meta\n'), stderr: Buffer.from('') } as any; + const callStr = JSON.stringify({ cmd, args }); + + // 1. Mock GCloud Instance List + if (callStr.includes('gcloud') && callStr.includes('instances') && callStr.includes('list')) { + return { status: 0, stdout: Buffer.from(JSON.stringify([{ name: 'gcli-offload-test-worker' }])), stderr: Buffer.from('') } as any; + } + + // 2. Mock GH Metadata Fetching (local or remote) + if (callStr.includes('gh') && callStr.includes('view')) { + return { status: 0, stdout: Buffer.from('test-meta\n'), stderr: Buffer.from('') } as any; + } + + return { status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') } as any; }); vi.mocked(spawn).mockImplementation(() => { @@ -53,14 +64,14 @@ describe('Offload Tooling Matrix', () => { const spawnCalls = vi.mocked(spawnSync).mock.calls; const ghCall = spawnCalls.find(call => { - const cmdStr = JSON.stringify(call); - return cmdStr.includes('issue') && cmdStr.includes('view') && cmdStr.includes('456'); + const s = JSON.stringify(call); + return s.includes('gh') && s.includes('issue') && s.includes('view') && s.includes('456'); }); expect(ghCall).toBeDefined(); const sshCall = spawnCalls.find(call => { - const cmdStr = JSON.stringify(call); - return cmdStr.includes('implement') && cmdStr.includes('offload-456-impl-456'); + const s = JSON.stringify(call); + return s.includes('gcloud') && s.includes('ssh') && s.includes('offload-456-implement'); }); expect(sshCall).toBeDefined(); }); @@ -69,9 +80,7 @@ describe('Offload Tooling Matrix', () => { describe('Fix Playbook', () => { it('should launch the agentic fix-pr skill', async () => { vi.mocked(fs.existsSync).mockReturnValue(true); - await runWorker(['123', 'test-branch', '/path/policy', 'fix']); - const spawnSyncCalls = vi.mocked(spawnSync).mock.calls; const fixCall = spawnSyncCalls.find(call => JSON.stringify(call).includes("activate the 'fix-pr' skill") diff --git a/.gemini/skills/offload/tests/orchestration.test.ts b/.gemini/skills/offload/tests/orchestration.test.ts index b1ac60228a..739c7428c1 100644 --- a/.gemini/skills/offload/tests/orchestration.test.ts +++ b/.gemini/skills/offload/tests/orchestration.test.ts @@ -5,50 +5,47 @@ import readline from 'readline'; import { runOrchestrator } from '../scripts/orchestrator.ts'; import { runSetup } from '../scripts/setup.ts'; import { runWorker } from '../scripts/worker.ts'; -import { runChecker } from '../scripts/check.ts'; -import { runCleanup } from '../scripts/clean.ts'; vi.mock('child_process'); vi.mock('fs'); vi.mock('readline'); -describe('Offload Orchestration', () => { +describe('Offload Orchestration (GCE)', () => { const mockSettings = { maintainer: { deepReview: { - remoteHost: 'test-host', - remoteWorkDir: '~/test-dir', + projectId: 'test-project', + zone: 'us-west1-a', terminalType: 'none', syncAuth: false, - geminiSetup: 'preexisting', - ghSetup: 'preexisting' + geminiSetup: 'isolated', + ghSetup: 'isolated' } } }; beforeEach(() => { vi.resetAllMocks(); - - // Mock settings file existence and content vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockSettings)); vi.mocked(fs.mkdirSync).mockReturnValue(undefined as any); vi.mocked(fs.writeFileSync).mockReturnValue(undefined as any); vi.mocked(fs.createWriteStream).mockReturnValue({ pipe: vi.fn() } as any); - - // Mock process methods vi.spyOn(process, 'chdir').mockImplementation(() => {}); - vi.spyOn(process, 'cwd').mockReturnValue('/test-cwd'); - - // Default mock for spawnSync + vi.mocked(spawnSync).mockImplementation((cmd: any, args: any) => { - if (cmd === 'gh' && args?.[0] === 'pr' && args?.[1] === 'view') { - return { status: 0, stdout: Buffer.from('test-branch\n'), stderr: Buffer.from('') } as any; + const callInfo = JSON.stringify({ cmd, args }); + // 1. Mock GCloud Instance List + if (callInfo.includes('gcloud') && callInfo.includes('instances') && callInfo.includes('list')) { + return { status: 0, stdout: Buffer.from(JSON.stringify([{ name: 'gcli-offload-test-worker' }])), stderr: Buffer.from('') } as any; + } + // 2. Mock GH Metadata Fetching (local or remote) + if (callInfo.includes('gh') && callInfo.includes('view')) { + return { status: 0, stdout: Buffer.from('test-meta\n'), stderr: Buffer.from('') } as any; } return { status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') } as any; }); - // Default mock for spawn vi.mocked(spawn).mockImplementation(() => { return { stdout: { pipe: vi.fn(), on: vi.fn() }, @@ -60,49 +57,24 @@ describe('Offload Orchestration', () => { }); describe('orchestrator.ts', () => { - it('should default to review action and pass it to remote', async () => { + it('should discover active workers and use gcloud compute ssh', async () => { await runOrchestrator(['123'], {}); + const spawnCalls = vi.mocked(spawnSync).mock.calls; - const sshCall = spawnCalls.find(call => typeof call[0] === 'string' && call[0].includes('entrypoint.ts 123')); - expect(sshCall![0]).toContain('review'); - }); - - it('should pass explicit actions (like fix) to remote', async () => { - await runOrchestrator(['123', 'fix'], {}); - const spawnCalls = vi.mocked(spawnSync).mock.calls; - const sshCall = spawnCalls.find(call => typeof call[0] === 'string' && call[0].includes('entrypoint.ts 123')); - expect(sshCall![0]).toContain('fix'); - }); - - it('should construct the correct tmux session name from branch', async () => { - await runOrchestrator(['123'], {}); - const spawnCalls = vi.mocked(spawnSync).mock.calls; - const sshCall = spawnCalls.find(call => typeof call[0] === 'string' && call[0].includes('tmux new-session')); - expect(sshCall![0]).toContain('offload-123-test-branch'); - }); - - it('should use isolated config path when geminiSetup is isolated', async () => { - const isolatedSettings = { - ...mockSettings, - maintainer: { - ...mockSettings.maintainer, - deepReview: { - ...mockSettings.maintainer.deepReview, - geminiSetup: 'isolated' - } - } - }; - vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(isolatedSettings)); - - await runOrchestrator(['123'], {}); - - const spawnCalls = vi.mocked(spawnSync).mock.calls; - const sshCall = spawnCalls.find(call => { - const cmdStr = typeof call[0] === 'string' ? call[0] : ''; - return cmdStr.includes('GEMINI_CLI_HOME=~/.offload/gemini-cli-config'); - }); + const sshCall = spawnCalls.find(call => + JSON.stringify(call).includes('gcloud') && JSON.stringify(call).includes('ssh') + ); expect(sshCall).toBeDefined(); + expect(JSON.stringify(sshCall)).toContain('gcli-offload-test-worker'); + expect(JSON.stringify(sshCall)).toContain('test-project'); + }); + + it('should construct the correct tmux session name', async () => { + await runOrchestrator(['123'], {}); + const spawnCalls = vi.mocked(spawnSync).mock.calls; + const sshCall = spawnCalls.find(call => JSON.stringify(call).includes('tmux new-session')); + expect(JSON.stringify(sshCall)).toContain('offload-123-review'); }); }); @@ -116,103 +88,31 @@ describe('Offload Orchestration', () => { vi.mocked(readline.createInterface).mockReturnValue(mockInterface as any); }); - it('should correctly detect pre-existing setup when everything is present', async () => { - vi.mocked(spawnSync).mockImplementation((cmd: any, args: any) => { - if (cmd === 'ssh') { - const remoteCmd = args[1]; - // Mock dependencies present (gh, tmux, gemini) - if (remoteCmd.includes('command -v')) return { status: 0 } as any; - if (remoteCmd.includes('gh auth status')) return { status: 0 } as any; - if (remoteCmd.includes('google_accounts.json')) return { status: 0 } as any; - } - return { status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') } as any; + it('should verify project access during setup', async () => { + vi.mocked(spawnSync).mockImplementation((cmd: any) => { + if (cmd === 'gcloud') return { status: 0 } as any; + return { status: 0, stdout: Buffer.from('') } as any; }); mockInterface.question - .mockImplementationOnce((q, cb) => cb('test-host')) - .mockImplementationOnce((q, cb) => cb('~/test-dir')) - .mockImplementationOnce((q, cb) => cb('p')) // gemini preexisting - .mockImplementationOnce((q, cb) => cb('p')) // gh preexisting + .mockImplementationOnce((q, cb) => cb('test-project')) + .mockImplementationOnce((q, cb) => cb('us-west1-a')) + .mockImplementationOnce((q, cb) => cb('n2-standard-8')) + .mockImplementationOnce((q, cb) => cb('y')) // syncAuth .mockImplementationOnce((q, cb) => cb('none')); await runSetup({ HOME: '/test-home' }); - const writeCall = vi.mocked(fs.writeFileSync).mock.calls.find(call => - call[0].toString().includes('.gemini/settings.json') - ); - expect(writeCall).toBeDefined(); - }); - - it('should default to isolated when dependencies are missing', async () => { - vi.mocked(spawnSync).mockImplementation((cmd: any, args: any) => { - if (cmd === 'ssh') { - const remoteCmd = args[1]; - // Mock dependencies missing - if (remoteCmd.includes('command -v')) return { status: 1 } as any; - } - return { status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') } as any; - }); - - // Only 3 questions now: host, dir, terminal (gemini/gh choice skipped) - mockInterface.question - .mockImplementationOnce((q, cb) => cb('test-host')) - .mockImplementationOnce((q, cb) => cb('~/test-dir')) - .mockImplementationOnce((q, cb) => cb('y')) // provision requirements - .mockImplementationOnce((q, cb) => cb('none')); - - await runSetup({ HOME: '/test-home' }); - - const writeCall = vi.mocked(fs.writeFileSync).mock.calls.find(call => - call[0].toString().includes('.gemini/settings.json') - ); - const savedSettings = JSON.parse(writeCall![1] as string); - expect(savedSettings.maintainer.deepReview.geminiSetup).toBe('isolated'); - expect(savedSettings.maintainer.deepReview.ghSetup).toBe('isolated'); + expect(vi.mocked(spawnSync)).toHaveBeenCalledWith('gcloud', expect.arrayContaining(['projects', 'describe', 'test-project']), expect.any(Object)); }); }); describe('worker.ts (playbooks)', () => { - it('should launch the review playbook by default', async () => { + it('should launch the review playbook', async () => { vi.mocked(fs.existsSync).mockReturnValue(true); await runWorker(['123', 'test-branch', '/test-policy.toml', 'review']); const spawnCalls = vi.mocked(spawn).mock.calls; - expect(spawnCalls.some(c => c[0].includes("activate the 'review-pr' skill"))).toBe(true); - }); - - it('should launch the fix playbook when requested', async () => { - vi.mocked(fs.existsSync).mockReturnValue(true); - await runWorker(['123', 'test-branch', '/test-policy.toml', 'fix']); - // runFixPlaybook uses spawnSync - const spawnSyncCalls = vi.mocked(spawnSync).mock.calls; - expect(spawnSyncCalls.some(c => JSON.stringify(c).includes("activate the 'fix-pr' skill"))).toBe(true); - }); - }); - - describe('check.ts', () => { - it('should report SUCCESS when exit files contain 0', async () => { - vi.mocked(spawnSync).mockImplementation((cmd: any, args: any) => { - if (cmd === 'gh') return { status: 0, stdout: Buffer.from('test-branch\n') } as any; - if (cmd === 'ssh' && args[1].includes('cat') && args[1].includes('.exit')) { - return { status: 0, stdout: Buffer.from('0\n') } as any; - } - return { status: 0, stdout: Buffer.from('') } as any; - }); - const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - await runChecker(['123']); - expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('āœ… build : SUCCESS')); - consoleSpy.mockRestore(); - }); - }); - - describe('clean.ts', () => { - it('should kill tmux server', async () => { - vi.mocked(readline.createInterface).mockReturnValue({ - question: vi.fn((q, cb) => cb('n')), - close: vi.fn() - } as any); - await runCleanup(); - const spawnCalls = vi.mocked(spawnSync).mock.calls; - expect(spawnCalls.some(call => Array.isArray(call[1]) && call[1].some(arg => arg === 'tmux kill-server'))).toBe(true); + expect(spawnCalls.some(c => JSON.stringify(c).includes("activate the 'review-pr' skill"))).toBe(true); }); }); }); diff --git a/package.json b/package.json index a452992eab..e3f0937fbb 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "offload:setup": "tsx .gemini/skills/offload/scripts/setup.ts", "offload:check": "tsx .gemini/skills/offload/scripts/check.ts", "offload:clean": "tsx .gemini/skills/offload/scripts/clean.ts", + "offload:fleet": "tsx .gemini/skills/offload/scripts/fleet.ts", "pre-commit": "node scripts/pre-commit.js" }, "overrides": {