diff --git a/packages/cli/src/gemini.ts b/packages/cli/src/gemini.ts index ed6c2cf4..14e5db82 100644 --- a/packages/cli/src/gemini.ts +++ b/packages/cli/src/gemini.ts @@ -4,6 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +import os from 'os'; +import path from 'path'; +import fs from 'fs'; import React from 'react'; import { render } from 'ink'; import { App } from './ui/App.js'; @@ -13,20 +16,159 @@ import { GeminiClient } from '@gemini-code/server'; import { readPackageUp } from 'read-package-up'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; +import { execSync, spawnSync } from 'child_process'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +// node.js equivalent of scripts/sandbox_command.sh +function sandbox_command(): string { + const sandbox = process.env.GEMINI_CODE_SANDBOX?.toLowerCase().trim() ?? ''; + const opts: object = { stdio: 'ignore' }; + if (['1', 'true'].includes(sandbox)) { + // look for docker or podman, in that order + if (spawnSync('command', ['-v', 'docker'], opts).status === 0) { + return 'docker'; // Set sandbox to 'docker' if found + } else if (spawnSync('command', ['-v', 'podman'], opts).status === 0) { + return 'podman'; // Set sandbox to 'podman' if found + } else { + console.error( + 'ERROR: failed to determine command for sandbox; ' + + 'install docker or podman or specify command in GEMINI_CODE_SANDBOX', + ); + process.exit(1); + } + } else if (sandbox) { + // confirm that specfied command exists + if (spawnSync('command', ['-v', sandbox], opts).status !== 0) { + console.error( + `ERROR: missing sandbox command '${sandbox}' (from GEMINI_CODE_SANDBOX)`, + ); + process.exit(1); + } + return sandbox; + } else { + return ''; // no sandbox + } +} + +// node.js equivalent of scripts/start_sandbox.sh +function start_sandbox(sandbox: string) { + // determine full path for gemini-code to distinguish linked vs installed setting + const gcPath = execSync(`realpath $(which gemini-code)`).toString().trim(); + + // stop if image is missing + const image = 'gemini-code-sandbox'; + if (!execSync(`${sandbox} images -q ${image}`).toString().trim()) { + const remedy = gcPath.includes('gemini-code/packages/') + ? 'Try `scripts/build_sandbox.sh` under gemini-code repo.' + : 'Please notify gemini-code-dev@google.com.'; + console.error(`ERROR: ${image} is missing. ${remedy}`); + process.exit(1); + } + + // stop if debugging in sandbox using linked/installed gemini-code + // note this is because it does not work (unclear why, parent process interferes somehow) + // note `npm run debug` runs sandbox directly and avoids any interference from parent process + if (process.env.DEBUG) { + console.error( + 'ERROR: cannot debug in sandbox using linked/installed gemini-code; ' + + 'use `npm run debug` under gemini-code repo instead', + ); + process.exit(1); + } + + // if project is gemini-code, then run sandboxed CLI from ${workdir}/packages/cli + // otherwise refuse debug mode (see comments in launch.json around remoteRoot) + const project = path.basename(process.cwd()); + const workdir = `/sandbox/${project}`; + let cliPath = '/usr/local/share/npm-global/lib/node_modules/@gemini-code/cli'; + if (project === 'gemini-code') { + cliPath = `${workdir}/packages/cli`; + } else if (process.env.DEBUG) { + console.error('ERROR: cannot debug in sandbox outside gemini-code repo'); + process.exit(1); + } + + // use interactive tty mode and auto-remove container on exit + // run init binary inside container to forward signals & reap zombies + const args = ['run', '-it', '--rm', '--init', '--workdir', workdir]; + + // mount current directory as ${workdir} inside container + args.push('-v', `${process.cwd()}:${workdir}`); + + // mount os.tmpdir() as /tmp inside container + args.push('-v', `${os.tmpdir()}:/tmp`); + + // name container after image, plus numeric suffix to avoid conflicts + let index = 0; + while ( + execSync( + `${sandbox} ps -a --format "{{.Names}}" | grep "${image}-${index}" || true`, + ) + .toString() + .trim() + ) { + index++; + } + args.push('--name', `${image}-${index}`, '--hostname', `${image}-${index}`); + + // copy GEMINI_API_KEY + if (process.env.GEMINI_API_KEY) { + args.push('--env', `GEMINI_API_KEY=${process.env.GEMINI_API_KEY}`); + } + + // copy GEMINI_CODE_MODEL + if (process.env.GEMINI_CODE_MODEL) { + args.push('--env', `GEMINI_CODE_MODEL=${process.env.GEMINI_CODE_MODEL}`); + } + + // copy SHELL_TOOL to optionally enable shell tool + if (process.env.SHELL_TOOL) { + args.push('--env', `SHELL_TOOL=${process.env.SHELL_TOOL}`); + } + + // copy TERM and COLORTERM to try to maintain terminal setup + if (process.env.TERM) { + args.push('--env', `TERM=${process.env.TERM}`); + } + if (process.env.COLORTERM) { + args.push('--env', `COLORTERM=${process.env.COLORTERM}`); + } + + // set SANDBOX as container name + args.push('--env', `SANDBOX=${image}-${index}`); + + // for podman, use empty --authfile to skip unnecessary auth refresh overhead + const emptyAuthFilePath = path.join(os.tmpdir(), 'empty_auth.json'); + fs.writeFileSync(emptyAuthFilePath, '{}', 'utf-8'); + args.push('--authfile', emptyAuthFilePath); + + // enable debugging via node --inspect-brk if DEBUG is set + const nodeArgs = []; + const debugPort = process.env.DEBUG_PORT || '9229'; + if (process.env.DEBUG) { + args.push('-p', `${debugPort}:${debugPort}`); + nodeArgs.push('--inspect-brk', `0.0.0.0:${debugPort}`); + } + + // append remaining args (image, node, node args, cli path, cli args) + args.push(image, 'node', ...nodeArgs, cliPath, ...process.argv.slice(2)); + + // spawn child and let it inherit stdio + spawnSync(sandbox, args, { stdio: 'inherit' }); +} + async function main() { const config = loadCliConfig(); let input = config.getQuestion(); - const sandboxEnabled = - process.env.GEMINI_CODE_SANDBOX && - !['0', 'false'].includes(process.env.GEMINI_CODE_SANDBOX.toLowerCase()); - if (sandboxEnabled && !process.env.SANDBOX) { - console.log('WARNING: sandboxing is enabled, but still OUTSIDE sandbox'); - // TODO: get inside sandbox + // hop into sandbox if enabled but outside + const sandbox = sandbox_command(); + if (sandbox && !process.env.SANDBOX) { + console.log('hopping into sandbox ...'); + start_sandbox(sandbox); + process.exit(0); } // Render UI, passing necessary config values. Check that there is no command line question. diff --git a/scripts/build_sandbox.sh b/scripts/build_sandbox.sh index 710f884b..24291e7c 100755 --- a/scripts/build_sandbox.sh +++ b/scripts/build_sandbox.sh @@ -24,7 +24,7 @@ CMD=$(scripts/sandbox_command.sh) echo "using $CMD for sandboxing" IMAGE=gemini-code-sandbox -DOCKERFILE=${DOCKERFILE:-Dockerfile} +DOCKERFILE=Dockerfile SKIP_NPM_INSTALL_BUILD=false while getopts "sd" opt; do @@ -44,7 +44,7 @@ shift $((OPTIND - 1)) # npm install + npm run build unless skipping via -s option if [ "$SKIP_NPM_INSTALL_BUILD" = false ]; then npm install - npm run build + npm run build --workspaces fi # if using Dockerfile-dev, then skip rebuild unless REBUILD_SANDBOX is set