feat(offload): make setup script dynamic based on remote state

This commit is contained in:
mkorwel
2026-03-13 20:08:30 -07:00
parent 12824cb21c
commit 45902f5e8a
3 changed files with 66 additions and 23 deletions

View File

@@ -40,39 +40,54 @@ export async function runSetup(env: NodeJS.ProcessEnv = process.env) {
const remoteWorkDir = await prompt('Remote Work Directory', `${OFFLOAD_BASE}/workspace`);
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
const geminiChoice = await prompt('\nGemini CLI Setup: Use [p]re-existing instance or [i]solated sandbox instance? (Isolated is recommended)', 'i');
const geminiSetup = geminiChoice.toLowerCase() === 'p' ? 'preexisting' : 'isolated';
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.');
}
// 2. GitHub CLI Isolation Choice
const ghChoice = await prompt('GitHub CLI Setup: Use [p]re-existing instance or [i]solated sandbox instance? (Isolated is recommended)', 'i');
const ghSetup = ghChoice.toLowerCase() === 'p' ? 'preexisting' : 'isolated';
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`;
console.log(`🔍 Checking state of ${remoteHost}...`);
// Use a login shell to ensure the same PATH as the interactive user
const ghCheck = spawnSync('ssh', [remoteHost, 'sh -lc "command -v gh"'], { stdio: 'pipe' });
const tmuxCheck = spawnSync('ssh', [remoteHost, 'sh -lc "command -v tmux"'], { stdio: 'pipe' });
if (ghCheck.status !== 0 || tmuxCheck.status !== 0) {
if (!hasGH || !hasTmux) {
console.log('\n📥 System Requirements Check:');
if (ghCheck.status !== 0) console.log(' ❌ GitHub CLI (gh) is not installed on remote.');
if (tmuxCheck.status !== 0) console.log(' ❌ tmux is not installed on remote.');
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 ' + [ghCheck.status !== 0 ? 'gh' : '', tmuxCheck.status !== 0 ? 'tmux' : ''].filter(Boolean).join(' ');
installCmd = 'sudo apt update && sudo apt install -y ' + [!hasGH ? 'gh' : '', !hasTmux ? 'tmux' : ''].filter(Boolean).join(' ');
} else if (os === 'Darwin') {
installCmd = 'brew install ' + [ghCheck.status !== 0 ? 'gh' : '', tmuxCheck.status !== 0 ? 'tmux' : ''].filter(Boolean).join(' ');
installCmd = 'brew install ' + [!hasGH ? 'gh' : '', !hasTmux ? 'tmux' : ''].filter(Boolean).join(' ');
}
if (installCmd) {
@@ -141,8 +156,6 @@ export async function runSetup(env: NodeJS.ProcessEnv = process.env) {
const terminalType = await prompt('\nTerminal Automation (iterm2 / terminal / none)', 'iterm2');
// Local Dependencies Install (Isolated)
const envLoader = 'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && \\. "$NVM_DIR/nvm.sh"';
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 });

View File

@@ -33,6 +33,7 @@ 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;
});

View File

@@ -116,11 +116,11 @@ describe('Offload Orchestration', () => {
vi.mocked(readline.createInterface).mockReturnValue(mockInterface as any);
});
it('should correctly detect pre-existing setup', async () => {
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];
if (remoteCmd.includes('[ -d ~/test-dir/.git ]')) return { status: 0 } as any;
// 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;
@@ -131,15 +131,44 @@ describe('Offload Orchestration', () => {
mockInterface.question
.mockImplementationOnce((q, cb) => cb('test-host'))
.mockImplementationOnce((q, cb) => cb('~/test-dir'))
.mockImplementationOnce((q, cb) => cb('p'))
.mockImplementationOnce((q, cb) => cb('p'))
.mockImplementationOnce((q, cb) => cb('p')) // gemini preexisting
.mockImplementationOnce((q, cb) => cb('p')) // gh preexisting
.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 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');
});
});
describe('worker.ts (playbooks)', () => {