summaryrefslogtreecommitdiff
path: root/packages/cli/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src')
-rw-r--r--packages/cli/src/gemini.ts232
-rw-r--r--packages/cli/src/utils/sandbox.ts237
2 files changed, 238 insertions, 231 deletions
diff --git a/packages/cli/src/gemini.ts b/packages/cli/src/gemini.ts
index 9de81c08..e3b6c94b 100644
--- a/packages/cli/src/gemini.ts
+++ b/packages/cli/src/gemini.ts
@@ -4,11 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import os from 'os';
-import path from 'path';
-import fs from 'fs';
import React from 'react';
-import { quote } from 'shell-quote';
import { render } from 'ink';
import { App } from './ui/App.js';
import { loadCliConfig } from './config/config.js';
@@ -17,237 +13,11 @@ 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, spawn } from 'child_process';
+import { sandbox_command, start_sandbox } from './utils/sandbox.js';
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() ?? '';
- if (['1', 'true'].includes(sandbox)) {
- // look for docker or podman, in that order
- if (execSync('command -v docker || true').toString().trim()) {
- return 'docker'; // Set sandbox to 'docker' if found
- } else if (execSync('command -v podman || true').toString().trim()) {
- 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 (execSync(`command -v ${sandbox} || true`).toString().trim()) {
- return sandbox;
- } else {
- console.error(
- `ERROR: missing sandbox command '${sandbox}' (from GEMINI_CODE_SANDBOX)`,
- );
- process.exit(1);
- }
- } else {
- return ''; // no sandbox
- }
-}
-
-// node.js equivalent of scripts/start_sandbox.sh
-async 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();
-
- // if project is gemini-code, then switch to -dev image & run CLI from ${workdir}/packages/cli
- let image = 'gemini-code-sandbox';
- const project = path.basename(process.cwd());
- const workdir = process.cwd();
- let cliPath = '/usr/local/share/npm-global/lib/node_modules/@gemini-code/cli';
- if (project === 'gemini-code') {
- image += '-dev';
- cliPath = `${workdir}/packages/cli`;
- }
-
- // if BUILD_SANDBOX is set, then call scripts/build_sandbox.sh under gemini-code repo
- // note this can only be done with binary linked from gemini-code repo
- if (process.env.BUILD_SANDBOX) {
- if (!gcPath.includes('gemini-code/packages/')) {
- console.error(
- 'ERROR: cannot BUILD_SANDBOX using installed gemini-code binary; ' +
- 'run `npm link ./packages/cli` under gemini-code repo to switch to linked binary.',
- );
- process.exit(1);
- } else {
- console.log('building sandbox ...');
- const gcRoot = gcPath.split('/packages/')[0];
- spawnSync(`cd ${gcRoot} && scripts/build_sandbox.sh`, {
- stdio: 'inherit',
- shell: true,
- });
- }
- }
-
- // stop if image is missing
- 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);
- }
-
- // 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('--volume', `${process.cwd()}:${workdir}`);
-
- // mount os.tmpdir() as /tmp inside container
- args.push('--volume', `${os.tmpdir()}:/tmp`);
-
- // mount paths listed in SANDBOX_MOUNTS
- if (process.env.SANDBOX_MOUNTS) {
- for (let mount of process.env.SANDBOX_MOUNTS.split(',')) {
- if (mount.trim()) {
- // parse mount as from:to:opts
- let [from, to, opts] = mount.trim().split(':');
- to = to || from; // default to mount at same path inside container
- opts = opts || 'ro'; // default to read-only
- mount = `${from}:${to}:${opts}`;
- // check that from path is absolute
- if (!path.isAbsolute(from)) {
- console.error(
- `ERROR: path '${from}' listed in SANDBOX_MOUNTS must be absolute`,
- );
- process.exit(1);
- }
- // check that from path exists on host
- if (!fs.existsSync(from)) {
- console.error(
- `ERROR: missing mount path '${from}' listed in SANDBOX_MOUNTS`,
- );
- process.exit(1);
- }
- console.log(`SANDBOX_MOUNTS: ${from} -> ${to} (${opts})`);
- args.push('--volume', mount);
- }
- }
- }
-
- // 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 TERMINAL_TOOL to optionally enable shell tool
- if (process.env.TERMINAL_TOOL) {
- args.push('--env', `TERMINAL_TOOL=${process.env.TERMINAL_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}`);
- }
-
- // copy additional environment variables from SANDBOX_ENV
- if (process.env.SANDBOX_ENV) {
- for (let env of process.env.SANDBOX_ENV.split(',')) {
- if ((env = env.trim())) {
- if (env.includes('=')) {
- console.log(`SANDBOX_ENV: ${env}`);
- args.push('--env', env);
- } else {
- console.error(
- 'ERROR: SANDBOX_ENV must be a comma-separated list of key=value pairs',
- );
- process.exit(1);
- }
- }
- }
- }
-
- // set SANDBOX as container name
- args.push('--env', `SANDBOX=${image}-${index}`);
-
- // for podman only, use empty --authfile to skip unnecessary auth refresh overhead
- if (sandbox === 'podman') {
- 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('--publish', `${debugPort}:${debugPort}`);
- nodeArgs.push(`--inspect-brk=0.0.0.0:${debugPort}`);
- }
-
- // open additional ports if SANDBOX_PORTS is set
- // also set up redirects (via socat) so servers can listen on localhost instead of 0.0.0.0
- let bashCmd = '';
- if (process.env.SANDBOX_PORTS) {
- for (let port of process.env.SANDBOX_PORTS.split(',')) {
- if ((port = port.trim())) {
- console.log(`SANDBOX_PORTS: ${port}`);
- args.push('--publish', `${port}:${port}`);
- bashCmd += `socat TCP4-LISTEN:${port},bind=$(hostname -i),fork,reuseaddr TCP4:127.0.0.1:${port} 2> /dev/null & `;
- }
- }
- }
-
- // specify --user as "$(id -u):$(id -g)" if SANDBOX_SET_UID_GID is 1|true
- // only necessary if user mapping is not handled by sandboxing setup on host
- // (e.g. rootful docker on linux w/o userns-remap configured)
- if (['1', 'true'].includes(process.env.SANDBOX_SET_UID_GID ?? '')) {
- const uid = execSync('id -u').toString().trim();
- const gid = execSync('id -g').toString().trim();
- args.push('--user', `${uid}:${gid}`);
- }
-
- // append remaining args (image, bash -c "node node_args... cli path cli_args...")
- // node_args and cli_args need to be quoted before being inserted into bash_cmd
- const quotedNodeArgs = nodeArgs.map((arg) => quote([arg]));
- const quotedCliArgs = process.argv.slice(2).map((arg) => quote([arg]));
- bashCmd += `node ${quotedNodeArgs.join(' ')} ${quote([cliPath])} ${quotedCliArgs.join(' ')}`;
- args.push(image, 'bash', '-c', bashCmd);
-
- // spawn child and let it inherit stdio
- const child = spawn(sandbox, args, {
- stdio: 'inherit',
- detached: true,
- });
-
- // uncomment this line (and comment the await on following line) to let parent exit
- // child.unref();
- await new Promise((resolve) => {
- child.on('close', resolve);
- });
-}
-
async function main() {
const config = loadCliConfig();
let input = config.getQuestion();
diff --git a/packages/cli/src/utils/sandbox.ts b/packages/cli/src/utils/sandbox.ts
new file mode 100644
index 00000000..dddb7d89
--- /dev/null
+++ b/packages/cli/src/utils/sandbox.ts
@@ -0,0 +1,237 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { execSync, spawnSync, spawn } from 'node:child_process';
+import os from 'node:os';
+import path from 'node:path';
+import fs from 'node:fs';
+import { quote } from 'shell-quote';
+
+// node.js equivalent of scripts/sandbox_command.sh
+export function sandbox_command(): string {
+ const sandbox = process.env.GEMINI_CODE_SANDBOX?.toLowerCase().trim() ?? '';
+ if (['1', 'true'].includes(sandbox)) {
+ // look for docker or podman, in that order
+ if (execSync('command -v docker || true').toString().trim()) {
+ return 'docker'; // Set sandbox to 'docker' if found
+ } else if (execSync('command -v podman || true').toString().trim()) {
+ 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 (execSync(`command -v ${sandbox} || true`).toString().trim()) {
+ return sandbox;
+ } else {
+ console.error(
+ `ERROR: missing sandbox command '${sandbox}' (from GEMINI_CODE_SANDBOX)`,
+ );
+ process.exit(1);
+ }
+ } else {
+ return ''; // no sandbox
+ }
+}
+
+// node.js equivalent of scripts/start_sandbox.sh
+export async 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();
+
+ // if project is gemini-code, then switch to -dev image & run CLI from ${workdir}/packages/cli
+ let image = 'gemini-code-sandbox';
+ const project = path.basename(process.cwd());
+ const workdir = process.cwd();
+ let cliPath = '/usr/local/share/npm-global/lib/node_modules/@gemini-code/cli';
+ if (project === 'gemini-code') {
+ image += '-dev';
+ cliPath = `${workdir}/packages/cli`;
+ }
+
+ // if BUILD_SANDBOX is set, then call scripts/build_sandbox.sh under gemini-code repo
+ // note this can only be done with binary linked from gemini-code repo
+ if (process.env.BUILD_SANDBOX) {
+ if (!gcPath.includes('gemini-code/packages/')) {
+ console.error(
+ 'ERROR: cannot BUILD_SANDBOX using installed gemini-code binary; ' +
+ 'run `npm link ./packages/cli` under gemini-code repo to switch to linked binary.',
+ );
+ process.exit(1);
+ } else {
+ console.log('building sandbox ...');
+ const gcRoot = gcPath.split('/packages/')[0];
+ spawnSync(`cd ${gcRoot} && scripts/build_sandbox.sh`, {
+ stdio: 'inherit',
+ shell: true,
+ });
+ }
+ }
+
+ // stop if image is missing
+ 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);
+ }
+
+ // 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('--volume', `${process.cwd()}:${workdir}`);
+
+ // mount os.tmpdir() as /tmp inside container
+ args.push('--volume', `${os.tmpdir()}:/tmp`);
+
+ // mount paths listed in SANDBOX_MOUNTS
+ if (process.env.SANDBOX_MOUNTS) {
+ for (let mount of process.env.SANDBOX_MOUNTS.split(',')) {
+ if (mount.trim()) {
+ // parse mount as from:to:opts
+ let [from, to, opts] = mount.trim().split(':');
+ to = to || from; // default to mount at same path inside container
+ opts = opts || 'ro'; // default to read-only
+ mount = `${from}:${to}:${opts}`;
+ // check that from path is absolute
+ if (!path.isAbsolute(from)) {
+ console.error(
+ `ERROR: path '${from}' listed in SANDBOX_MOUNTS must be absolute`,
+ );
+ process.exit(1);
+ }
+ // check that from path exists on host
+ if (!fs.existsSync(from)) {
+ console.error(
+ `ERROR: missing mount path '${from}' listed in SANDBOX_MOUNTS`,
+ );
+ process.exit(1);
+ }
+ console.log(`SANDBOX_MOUNTS: ${from} -> ${to} (${opts})`);
+ args.push('--volume', mount);
+ }
+ }
+ }
+
+ // 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 TERMINAL_TOOL to optionally enable shell tool
+ if (process.env.TERMINAL_TOOL) {
+ args.push('--env', `TERMINAL_TOOL=${process.env.TERMINAL_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}`);
+ }
+
+ // copy additional environment variables from SANDBOX_ENV
+ if (process.env.SANDBOX_ENV) {
+ for (let env of process.env.SANDBOX_ENV.split(',')) {
+ if ((env = env.trim())) {
+ if (env.includes('=')) {
+ console.log(`SANDBOX_ENV: ${env}`);
+ args.push('--env', env);
+ } else {
+ console.error(
+ 'ERROR: SANDBOX_ENV must be a comma-separated list of key=value pairs',
+ );
+ process.exit(1);
+ }
+ }
+ }
+ }
+
+ // set SANDBOX as container name
+ args.push('--env', `SANDBOX=${image}-${index}`);
+
+ // for podman only, use empty --authfile to skip unnecessary auth refresh overhead
+ if (sandbox === 'podman') {
+ 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('--publish', `${debugPort}:${debugPort}`);
+ nodeArgs.push(`--inspect-brk=0.0.0.0:${debugPort}`);
+ }
+
+ // open additional ports if SANDBOX_PORTS is set
+ // also set up redirects (via socat) so servers can listen on localhost instead of 0.0.0.0
+ let bashCmd = '';
+ if (process.env.SANDBOX_PORTS) {
+ for (let port of process.env.SANDBOX_PORTS.split(',')) {
+ if ((port = port.trim())) {
+ console.log(`SANDBOX_PORTS: ${port}`);
+ args.push('--publish', `${port}:${port}`);
+ bashCmd += `socat TCP4-LISTEN:${port},bind=$(hostname -i),fork,reuseaddr TCP4:127.0.0.1:${port} 2> /dev/null & `;
+ }
+ }
+ }
+
+ // specify --user as "$(id -u):$(id -g)" if SANDBOX_SET_UID_GID is 1|true
+ // only necessary if user mapping is not handled by sandboxing setup on host
+ // (e.g. rootful docker on linux w/o userns-remap configured)
+ if (['1', 'true'].includes(process.env.SANDBOX_SET_UID_GID ?? '')) {
+ const uid = execSync('id -u').toString().trim();
+ const gid = execSync('id -g').toString().trim();
+ args.push('--user', `${uid}:${gid}`);
+ }
+
+ // append remaining args (image, bash -c "node node_args... cli path cli_args...")
+ // node_args and cli_args need to be quoted before being inserted into bash_cmd
+ const quotedNodeArgs = nodeArgs.map((arg) => quote([arg]));
+ const quotedCliArgs = process.argv.slice(2).map((arg) => quote([arg]));
+ bashCmd += `node ${quotedNodeArgs.join(' ')} ${quote([cliPath])} ${quotedCliArgs.join(' ')}`;
+ args.push(image, 'bash', '-c', bashCmd);
+
+ // spawn child and let it inherit stdio
+ const child = spawn(sandbox, args, {
+ stdio: 'inherit',
+ detached: true,
+ });
+
+ // uncomment this line (and comment the await on following line) to let parent exit
+ // child.unref();
+ await new Promise((resolve) => {
+ child.on('close', resolve);
+ });
+}