summaryrefslogtreecommitdiff
path: root/packages/cli
diff options
context:
space:
mode:
authorOlcan <[email protected]>2025-04-26 21:27:36 -0700
committerGitHub <[email protected]>2025-04-26 21:27:36 -0700
commit7828e813a81778d9eda1942af9c9eedf09e56f4e (patch)
tree3b4754cc5f1997a8f3fdfed641dd02b4eeb71e94 /packages/cli
parent688b2d0da7f59421312e132461b33c8c593698a0 (diff)
hop into sandbox (#186)
Diffstat (limited to 'packages/cli')
-rw-r--r--packages/cli/src/gemini.ts154
1 files changed, 148 insertions, 6 deletions
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 [email protected].';
+ 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.