mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-25 20:44:46 -07:00
feat(offload): make setup script dynamic based on remote state
This commit is contained in:
@@ -40,27 +40,42 @@ export async function runSetup(env: NodeJS.ProcessEnv = process.env) {
|
|||||||
const remoteWorkDir = await prompt('Remote Work Directory', `${OFFLOAD_BASE}/workspace`);
|
const remoteWorkDir = await prompt('Remote Work Directory', `${OFFLOAD_BASE}/workspace`);
|
||||||
|
|
||||||
console.log(`🔍 Checking state of ${remoteHost}...`);
|
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
|
// 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');
|
let geminiSetup = 'isolated';
|
||||||
const geminiSetup = geminiChoice.toLowerCase() === 'p' ? 'preexisting' : '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
|
// 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');
|
let ghSetup = 'isolated';
|
||||||
const ghSetup = ghChoice.toLowerCase() === 'p' ? 'preexisting' : '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_GEMINI_CONFIG = `${OFFLOAD_BASE}/gemini-cli-config`;
|
||||||
const ISOLATED_GH_CONFIG = `${OFFLOAD_BASE}/gh-cli-config`;
|
const ISOLATED_GH_CONFIG = `${OFFLOAD_BASE}/gh-cli-config`;
|
||||||
|
|
||||||
console.log(`🔍 Checking state of ${remoteHost}...`);
|
if (!hasGH || !hasTmux) {
|
||||||
// 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) {
|
|
||||||
console.log('\n📥 System Requirements Check:');
|
console.log('\n📥 System Requirements Check:');
|
||||||
if (ghCheck.status !== 0) console.log(' ❌ GitHub CLI (gh) is not installed on remote.');
|
if (!hasGH) console.log(' ❌ GitHub CLI (gh) is not installed on remote.');
|
||||||
if (tmuxCheck.status !== 0) console.log(' ❌ tmux 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?');
|
const shouldProvision = await confirm('\nWould you like Gemini to automatically provision missing requirements?');
|
||||||
if (shouldProvision) {
|
if (shouldProvision) {
|
||||||
@@ -70,9 +85,9 @@ export async function runSetup(env: NodeJS.ProcessEnv = process.env) {
|
|||||||
|
|
||||||
let installCmd = '';
|
let installCmd = '';
|
||||||
if (os === 'Linux') {
|
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') {
|
} 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) {
|
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');
|
const terminalType = await prompt('\nTerminal Automation (iterm2 / terminal / none)', 'iterm2');
|
||||||
|
|
||||||
// Local Dependencies Install (Isolated)
|
// 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}...`);
|
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 checkCmd = `ssh ${remoteHost} ${q(`${envLoader} && [ -x ${remoteWorkDir}/node_modules/.bin/tsx ] && [ -x ${remoteWorkDir}/node_modules/.bin/gemini ]`)}`;
|
||||||
const depCheck = spawnSync(checkCmd, { shell: true });
|
const depCheck = spawnSync(checkCmd, { shell: true });
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ describe('Offload Tooling Matrix', () => {
|
|||||||
vi.spyOn(process, 'chdir').mockImplementation(() => {});
|
vi.spyOn(process, 'chdir').mockImplementation(() => {});
|
||||||
|
|
||||||
vi.mocked(spawnSync).mockImplementation((cmd: any, args: any) => {
|
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;
|
return { status: 0, stdout: Buffer.from('test-meta\n'), stderr: Buffer.from('') } as any;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -116,11 +116,11 @@ describe('Offload Orchestration', () => {
|
|||||||
vi.mocked(readline.createInterface).mockReturnValue(mockInterface as any);
|
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) => {
|
vi.mocked(spawnSync).mockImplementation((cmd: any, args: any) => {
|
||||||
if (cmd === 'ssh') {
|
if (cmd === 'ssh') {
|
||||||
const remoteCmd = args[1];
|
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('command -v')) return { status: 0 } as any;
|
||||||
if (remoteCmd.includes('gh auth status')) 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;
|
if (remoteCmd.includes('google_accounts.json')) return { status: 0 } as any;
|
||||||
@@ -131,15 +131,44 @@ describe('Offload Orchestration', () => {
|
|||||||
mockInterface.question
|
mockInterface.question
|
||||||
.mockImplementationOnce((q, cb) => cb('test-host'))
|
.mockImplementationOnce((q, cb) => cb('test-host'))
|
||||||
.mockImplementationOnce((q, cb) => cb('~/test-dir'))
|
.mockImplementationOnce((q, cb) => cb('~/test-dir'))
|
||||||
.mockImplementationOnce((q, cb) => cb('p'))
|
.mockImplementationOnce((q, cb) => cb('p')) // gemini preexisting
|
||||||
.mockImplementationOnce((q, cb) => cb('p'))
|
.mockImplementationOnce((q, cb) => cb('p')) // gh preexisting
|
||||||
.mockImplementationOnce((q, cb) => cb('none'));
|
.mockImplementationOnce((q, cb) => cb('none'));
|
||||||
|
|
||||||
await runSetup({ HOME: '/test-home' });
|
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();
|
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)', () => {
|
describe('worker.ts (playbooks)', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user