Windows: Refactor Shell Scripts to Node.js for Cross-Platform Compatibility (#784)

This commit is contained in:
matt korwel
2025-06-09 12:19:42 -07:00
committed by GitHub
parent 2182a1cd2c
commit 3b943c1582
38 changed files with 1723 additions and 853 deletions

54
scripts/build.js Normal file
View File

@@ -0,0 +1,54 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { execSync } from 'child_process';
import { existsSync } from 'fs';
import { join } from 'path';
const root = join(import.meta.dirname, '..');
// npm install if node_modules was removed (e.g. via npm run clean or scripts/clean.js)
if (!existsSync(join(root, 'node_modules'))) {
execSync('npm install', { stdio: 'inherit', cwd: root });
}
// build all workspaces/packages
execSync('npm run generate', { stdio: 'inherit', cwd: root });
execSync('npm run build --workspaces', { stdio: 'inherit', cwd: root });
// also build container image if sandboxing is enabled
// skip (-s) npm install + build since we did that above
try {
execSync('node scripts/sandbox_command.js -q', {
stdio: 'inherit',
cwd: root,
});
if (
process.env.BUILD_SANDBOX === '1' ||
process.env.BUILD_SANDBOX === 'true'
) {
execSync('node scripts/build_sandbox.js -s', {
stdio: 'inherit',
cwd: root,
});
}
} catch {
// ignore
}

View File

@@ -1,30 +0,0 @@
#!/bin/bash
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -euo pipefail
# npm install if node_modules was removed (e.g. via npm run clean or scripts/clean.sh)
if [ ! -d "node_modules" ]; then
npm install
fi
# build all workspaces/packages
npm run build --workspaces
# also build container image if sandboxing is enabled
# skip (-s) npm install + build since we did that above
if scripts/sandbox_command.sh -q && [[ "${BUILD_SANDBOX:-}" =~ ^(1|true)$ ]]; then
scripts/build_sandbox.sh -s
fi

37
scripts/build_package.js Normal file
View File

@@ -0,0 +1,37 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { execSync } from 'child_process';
import { writeFileSync } from 'fs';
import { join } from 'path';
if (!process.cwd().includes('packages')) {
console.error('must be invoked from a package directory');
process.exit(1);
}
// build typescript files
execSync('tsc --build', { stdio: 'inherit' });
// copy .{md,json} files
execSync('node ../../scripts/copy_files.js', { stdio: 'inherit' });
// touch dist/.last_build
writeFileSync(join(process.cwd(), 'dist', '.last_build'), '');
process.exit(0);

View File

@@ -1,33 +0,0 @@
#!/bin/bash
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -euo pipefail
if [[ $(pwd) != *"/packages/"* ]]; then
echo "must be invoked from a package directory"
exit 1
fi
# clean dist directory
# rm -rf dist/*
# build typescript files
tsc --build
# copy .{md,json} files
node ../../scripts/copy_files.js
# touch dist/.last_build
touch dist/.last_build

125
scripts/build_sandbox.js Normal file
View File

@@ -0,0 +1,125 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { execSync } from 'child_process';
import { chmodSync, readFileSync, rmSync } from 'fs';
import { join } from 'path';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
const argv = yargs(hideBin(process.argv))
.option('s', {
alias: 'skip-npm-install-build',
type: 'boolean',
default: false,
description: 'skip npm install + npm run build',
})
.option('f', {
alias: 'dockerfile',
type: 'string',
description: 'use <dockerfile> for custom image',
})
.option('i', {
alias: 'image',
type: 'string',
description: 'use <image> name for custom image',
}).argv;
let sandboxCommand;
try {
sandboxCommand = execSync('node scripts/sandbox_command.js')
.toString()
.trim();
} catch {
console.warn(
'WARNING: container-based sandboxing is disabled (see README.md#sandboxing)',
);
process.exit(0);
}
if (sandboxCommand === 'sandbox-exec') {
console.warn(
'WARNING: container-based sandboxing is disabled (see README.md#sandboxing)',
);
process.exit(0);
}
console.log(`using ${sandboxCommand} for sandboxing`);
const baseImage = 'gemini-cli-sandbox';
const customImage = argv.i;
const baseDockerfile = 'Dockerfile';
const customDockerfile = argv.f;
if (!argv.s) {
execSync('npm install', { stdio: 'inherit' });
execSync('npm run build --workspaces', { stdio: 'inherit' });
}
console.log('packing @gemini-cli/cli ...');
const cliPackageDir = join('packages', 'cli');
rmSync(join(cliPackageDir, 'dist', 'gemini-cli-cli-*.tgz'), { force: true });
execSync(`npm pack -w @gemini-cli/cli --pack-destination ./packages/cli/dist`, {
stdio: 'ignore',
});
console.log('packing @gemini-cli/core ...');
const corePackageDir = join('packages', 'core');
rmSync(join(corePackageDir, 'dist', 'gemini-cli-core-*.tgz'), { force: true });
execSync(
`npm pack -w @gemini-cli/core --pack-destination ./packages/core/dist`,
{ stdio: 'ignore' },
);
const packageVersion = JSON.parse(
readFileSync(join(process.cwd(), 'package.json'), 'utf-8'),
).version;
chmodSync(
join(cliPackageDir, 'dist', `gemini-cli-cli-${packageVersion}.tgz`),
0o755,
);
chmodSync(
join(corePackageDir, 'dist', `gemini-cli-core-${packageVersion}.tgz`),
0o755,
);
const buildStdout = process.env.VERBOSE ? 'inherit' : 'ignore';
function buildImage(imageName, dockerfile) {
console.log(`building ${imageName} ... (can be slow first time)`);
const buildCommand =
sandboxCommand === 'podman'
? `${sandboxCommand} build --authfile=<(echo '{}')`
: `${sandboxCommand} --config=".docker" buildx build`;
execSync(
`${buildCommand} ${process.env.BUILD_SANDBOX_FLAGS || ''} -f "${dockerfile}" -t "${imageName}" .`,
{ stdio: buildStdout },
);
console.log(`built ${imageName}`);
}
buildImage(baseImage, baseDockerfile);
if (customDockerfile && customImage) {
buildImage(customImage, customDockerfile);
}
execSync(`${sandboxCommand} image prune -f`, { stdio: 'ignore' });

View File

@@ -1,102 +0,0 @@
#!/bin/bash
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -euo pipefail
# exit with warning if container-based sandboxing is disabled
# note this includes the case where sandbox-exec (seatbelt) is used
# this happens most commonly when user runs `npm run build:all` without enabling sandboxing
if ! scripts/sandbox_command.sh -q || [ "$(scripts/sandbox_command.sh)" == "sandbox-exec" ]; then
echo "WARNING: container-based sandboxing is disabled (see CONTRIBUTING.md#enabling-sandboxing)"
exit 0
fi
CMD=$(scripts/sandbox_command.sh)
echo "using $CMD for sandboxing"
BASE_IMAGE=gemini-cli-sandbox
CUSTOM_IMAGE=''
BASE_DOCKERFILE=Dockerfile
CUSTOM_DOCKERFILE=''
SKIP_NPM_INSTALL_BUILD=false
while getopts "sf:i:" opt; do
case ${opt} in
s) SKIP_NPM_INSTALL_BUILD=true ;;
f)
CUSTOM_DOCKERFILE=$OPTARG
;;
i)
CUSTOM_IMAGE=$OPTARG
;;
\?)
echo "usage: $(basename "$0") [-s] [-f <dockerfile>]"
echo " -s: skip npm install + npm run build"
echo " -f <dockerfile>: use <dockerfile> for custom image"
echo " -i <image>: use <image> name for custom image"
exit 1
;;
esac
done
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 --workspaces
fi
# prepare global installation files for prod builds
# pack cli
echo "packing @gemini-cli/cli ..."
rm -f packages/cli/dist/gemini-cli-cli-*.tgz
npm pack -w @gemini-cli/cli --pack-destination ./packages/cli/dist &>/dev/null
# pack core
echo "packing @gemini-cli/core ..."
rm -f packages/core/dist/gemini-cli-core-*.tgz
npm pack -w @gemini-cli/core --pack-destination ./packages/core/dist &>/dev/null
# give node user (used during installation, see Dockerfile) access to these files
chmod 755 packages/*/dist/gemini-cli-*.tgz
# redirect build output to /dev/null unless VERBOSE is set
BUILD_STDOUT="/dev/null"
if [ -n "${VERBOSE:-}" ]; then
BUILD_STDOUT="/dev/stdout"
fi
build_image() {
if [[ "$CMD" == "podman" ]]; then
# use empty --authfile to skip unnecessary auth refresh overhead
$CMD build --authfile=<(echo '{}') "$@" >$BUILD_STDOUT
elif [[ "$CMD" == "docker" ]]; then
$CMD --config=".docker" buildx build "$@" >$BUILD_STDOUT
else
$CMD build "$@" >$BUILD_STDOUT
fi
}
echo "building $BASE_IMAGE ... (can be slow first time)"
# shellcheck disable=SC2086 # allow globbing and word splitting for BUILD_SANDBOX_FLAGS
build_image ${BUILD_SANDBOX_FLAGS:-} -f "$BASE_DOCKERFILE" -t "$BASE_IMAGE" .
echo "built $BASE_IMAGE"
if [[ -n "$CUSTOM_DOCKERFILE" && -n "$CUSTOM_IMAGE" ]]; then
echo "building $CUSTOM_IMAGE ... (can be slow first time)"
# shellcheck disable=SC2086 # allow globbing and word splitting for BUILD_SANDBOX_FLAGS
build_image ${BUILD_SANDBOX_FLAGS:-} -f "$CUSTOM_DOCKERFILE" -t "$CUSTOM_IMAGE" .
echo "built $CUSTOM_IMAGE"
fi
$CMD image prune -f >/dev/null

32
scripts/clean.js Normal file
View File

@@ -0,0 +1,32 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { execSync } from 'child_process';
import { rmSync } from 'fs';
import { join } from 'path';
const root = join(import.meta.dirname, '..');
// remove npm install/build artifacts
rmSync(join(root, 'node_modules'), { recursive: true, force: true });
rmSync(join(root, 'packages/cli/src/generated/'), {
recursive: true,
force: true,
});
execSync('npm run clean --workspaces', { stdio: 'inherit', cwd: root });

View File

@@ -1,21 +0,0 @@
#!/bin/bash
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -euo pipefail
# remove npm install/build artifacts
rm -rf node_modules
rm -rf packages/cli/src/generated/
npm run clean --workspaces

View File

@@ -0,0 +1,48 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { copyFileSync, existsSync, mkdirSync } from 'fs';
import { join, basename } from 'path';
import { glob } from 'glob';
const root = join(import.meta.dirname, '..');
const bundleDir = join(root, 'bundle');
// Create the bundle directory if it doesn't exist
if (!existsSync(bundleDir)) {
mkdirSync(bundleDir);
}
// Copy specific shell files to the root of the bundle directory
copyFileSync(
join(root, 'packages/core/src/tools/shell.md'),
join(bundleDir, 'shell.md'),
);
copyFileSync(
join(root, 'packages/core/src/tools/shell.json'),
join(bundleDir, 'shell.json'),
);
// Find and copy all .sb files from packages to the root of the bundle directory
const sbFiles = glob.sync('packages/**/*.sb', { cwd: root });
for (const file of sbFiles) {
copyFileSync(join(root, file), join(bundleDir, basename(file)));
}
console.log('Assets copied to bundle/');

View File

@@ -1,13 +0,0 @@
#!/bin/bash
# Create the bundle directory if it doesn't exist
mkdir -p bundle
# Copy specific shell files to the root of the bundle directory
cp "packages/core/src/tools/shell.md" "bundle/shell.md"
cp "packages/core/src/tools/shell.json" "bundle/shell.json"
# Find and copy all .sb files from packages to the root of the bundle directory
find packages -name '*.sb' -exec cp -f {} bundle/ \;
echo "Assets copied to bundle/"

View File

@@ -0,0 +1,61 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { execSync } from 'child_process';
import { existsSync, mkdirSync, writeFileSync } from 'fs';
import { join } from 'path';
const root = join(import.meta.dirname, '..');
const generatedDir = join(root, 'packages/cli/src/generated');
const gitCommitFile = join(generatedDir, 'git-commit.ts');
let gitCommitInfo = 'N/A';
if (!existsSync(generatedDir)) {
mkdirSync(generatedDir, { recursive: true });
}
try {
const gitHash = execSync('git rev-parse --short HEAD', {
encoding: 'utf-8',
}).trim();
if (gitHash) {
gitCommitInfo = gitHash;
const gitStatus = execSync('git status --porcelain', {
encoding: 'utf-8',
}).trim();
if (gitStatus) {
gitCommitInfo = `${gitHash} (local modifications)`;
}
}
} catch {
// ignore
}
const fileContent = `/**
* @license
* Copyright ${new Date().getFullYear()} Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
// This file is auto-generated by the build script (scripts/build.js)
// Do not edit this file manually.
export const GIT_COMMIT_INFO = '${gitCommitInfo}';
`;
writeFileSync(gitCommitFile, fileContent);

View File

@@ -1,44 +0,0 @@
#!/bin/bash
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -euo pipefail
GENERATED_DIR="packages/cli/src/generated"
GIT_COMMIT_FILE="$GENERATED_DIR/git-commit.ts"
GIT_COMMIT_INFO="N/A"
mkdir -p "$GENERATED_DIR"
if command -v git &> /dev/null && git rev-parse --is-inside-work-tree &> /dev/null; then
GIT_HASH=$(git rev-parse --short HEAD 2>/dev/null || echo "")
if [ -n "$GIT_HASH" ]; then
GIT_COMMIT_INFO="$GIT_HASH"
if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
GIT_COMMIT_INFO="$GIT_HASH (local modifications)"
fi
fi
fi
cat <<EOL > "$GIT_COMMIT_FILE"
/**
* @license
* Copyright $(date +%Y) Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
// This file is auto-generated by the build script (scripts/build.sh)
// Do not edit this file manually.
export const GIT_COMMIT_INFO = '$GIT_COMMIT_INFO';
EOL

View File

@@ -0,0 +1,55 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { execSync } from 'child_process';
const {
SANDBOX_IMAGE_REGISTRY,
SANDBOX_IMAGE_NAME,
npm_package_version,
DOCKER_DRY_RUN,
} = process.env;
if (!SANDBOX_IMAGE_REGISTRY) {
console.error(
'Error: SANDBOX_IMAGE_REGISTRY environment variable is not set.',
);
process.exit(1);
}
if (!SANDBOX_IMAGE_NAME) {
console.error('Error: SANDBOX_IMAGE_NAME environment variable is not set.');
process.exit(1);
}
if (!npm_package_version) {
console.error(
'Error: npm_package_version environment variable is not set (should be run via npm).',
);
process.exit(1);
}
const imageUri = `${SANDBOX_IMAGE_REGISTRY}/${SANDBOX_IMAGE_NAME}:${npm_package_version}`;
if (DOCKER_DRY_RUN) {
console.log(`DRY RUN: Would execute: docker push "${imageUri}"`);
} else {
console.log(`Executing: docker push "${imageUri}"`);
execSync(`docker push "${imageUri}"`, { stdio: 'inherit' });
}

View File

@@ -1,41 +0,0 @@
#!/bin/bash
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -euo pipefail
# Ensure required environment variables are set
if [ -z "${SANDBOX_IMAGE_REGISTRY}" ]; then
echo "Error: SANDBOX_IMAGE_REGISTRY environment variable is not set." >&2
exit 1
fi
if [ -z "${SANDBOX_IMAGE_NAME}" ]; then
echo "Error: SANDBOX_IMAGE_NAME environment variable is not set." >&2
exit 1
fi
if [ -z "${npm_package_version}" ]; then
echo "Error: npm_package_version environment variable is not set (should be run via npm)." >&2
exit 1
fi
IMAGE_URI="${SANDBOX_IMAGE_REGISTRY}/${SANDBOX_IMAGE_NAME}:${npm_package_version}"
if [ -n "${DOCKER_DRY_RUN:-}" ]; then
echo "DRY RUN: Would execute: docker push \"${IMAGE_URI}\""
else
echo "Executing: docker push \"${IMAGE_URI}\""
docker push "${IMAGE_URI}"
fi

123
scripts/sandbox.js Normal file
View File

@@ -0,0 +1,123 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { execSync, spawn } from 'child_process';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
try {
execSync('node scripts/sandbox_command.js -q');
} catch {
console.error('ERROR: sandboxing disabled. See docs to enable sandboxing.');
process.exit(1);
}
const argv = yargs(hideBin(process.argv)).option('i', {
alias: 'interactive',
type: 'boolean',
default: false,
}).argv;
if (argv.i && !process.stdin.isTTY) {
console.error(
'ERROR: interactive mode (-i) requested without a terminal attached',
);
process.exit(1);
}
const image = 'gemini-cli-sandbox';
const sandboxCommand = execSync('node scripts/sandbox_command.js')
.toString()
.trim();
const sandboxes = execSync(
`${sandboxCommand} ps --filter "ancestor=${image}" --format "{{.Names}}"`,
)
.toString()
.trim()
.split('\n')
.filter(Boolean);
let sandboxName;
const firstArg = argv._[0];
if (firstArg) {
if (firstArg.startsWith(image) || /^\d+$/.test(firstArg)) {
sandboxName = firstArg.startsWith(image)
? firstArg
: `${image}-${firstArg}`;
argv._.shift();
}
}
if (!sandboxName) {
if (sandboxes.length === 0) {
console.error(
'No sandboxes found. Are you running gemini-cli with sandboxing enabled?',
);
process.exit(1);
}
if (sandboxes.length > 1) {
console.error('Multiple sandboxes found:');
sandboxes.forEach((s) => console.error(` ${s}`));
console.error(
'Sandbox name or index (0,1,...) must be specified as first argument',
);
process.exit(1);
}
sandboxName = sandboxes[0];
}
if (!sandboxes.includes(sandboxName)) {
console.error(`unknown sandbox ${sandboxName}`);
console.error('known sandboxes:');
sandboxes.forEach((s) => console.error(` ${s}`));
process.exit(1);
}
const execArgs = [];
let commandToRun = [];
// Determine interactive flags.
// If a command is provided, only be interactive if -i is passed.
// If no command is provided, always be interactive.
if (argv._.length > 0) {
if (argv.i) {
execArgs.push('-it');
}
} else {
execArgs.push('-it');
}
// Determine the command to run inside the container.
if (argv._.length > 0) {
// Join all positional arguments into a single command string.
const userCommand = argv._.join(' ');
// The container is Linux, so we use bash -l -c to execute the command string.
// This is cross-platform because it's what the container runs, not the host.
commandToRun = ['bash', '-l', '-c', userCommand];
} else {
// No command provided, so we start an interactive bash login shell.
commandToRun = ['bash', '-l'];
}
const spawnArgs = ['exec', ...execArgs, sandboxName, ...commandToRun];
// Use spawn to avoid shell injection issues and handle arguments correctly.
spawn(sandboxCommand, spawnArgs, { stdio: 'inherit' });

View File

@@ -1,103 +0,0 @@
#!/bin/bash
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -euo pipefail
if ! scripts/sandbox_command.sh -q; then
echo "ERROR: sandboxing disabled. See docs to enable sandboxing."
exit 1
fi
# parse flags
interactive=false
while getopts "i" opt; do
case "$opt" in
\?)
echo "usage: sandbox.sh [-i] [sandbox-name-or-index = AUTO] [command... = bash -l]"
echo " -i: enable interactive mode for custom command (enabled by default for login shell)"
echo " (WARNING: interactive mode causes stderr to be redirected to stdout)"
exit 1
;;
i)
interactive=true
if [ ! -t 0 ]; then
echo "ERROR: interactive mode (-i) requested without a terminal attached"
exit 1
fi
;;
esac
done
shift $((OPTIND - 1))
IMAGE=gemini-cli-sandbox
CMD=$(scripts/sandbox_command.sh)
# list all containers running on sandbox image
sandboxes=()
while IFS= read -r line; do
sandboxes+=("$line")
done < <($CMD ps --filter "ancestor=$IMAGE" --format "{{.Names}}")
# take first argument as sandbox name if it starts with image name or is an integer
# otherwise require a unique sandbox to be running and take its name
if [[ "${1:-}" =~ ^$IMAGE(-[0-9]+)?$ ]]; then
SANDBOX=$1
shift
elif [[ "${1:-}" =~ ^[0-9]+$ ]]; then
SANDBOX=$IMAGE-$1
shift
else
# exit if no sandbox is running
if [ ${#sandboxes[@]} -eq 0 ]; then
echo "No sandboxes found. Are you running gemini-cli with sandboxing enabled?"
exit 1
fi
# exit if multiple sandboxes are running
if [ ${#sandboxes[@]} -gt 1 ]; then
echo "Multiple sandboxes found:"
for sandbox in "${sandboxes[@]}"; do
echo " $sandbox"
done
echo "Sandbox name or index (0,1,...) must be specified as first argument"
exit 1
fi
SANDBOX=${sandboxes[0]}
fi
# check that sandbox exists
if ! [[ " ${sandboxes[*]} " == *" $SANDBOX "* ]]; then
echo "unknown sandbox $SANDBOX"
echo "known sandboxes:"
for sandbox in "${sandboxes[@]}"; do
echo " $sandbox"
done
exit 1
fi
# determine command and args for exec
if [ $# -gt 0 ]; then
cmd=(bash -l -c "$(printf '%q ' "$@")") # fixes quoting, e.g. bash -c 'echo $SANDBOX'
exec_args=()
if [ "$interactive" = true ]; then
exec_args=(-it)
fi
else
cmd=(bash -l)
exec_args=(-it)
fi
# run command in sandbox
exec_args+=("$SANDBOX" "${cmd[@]}")
$CMD exec "${exec_args[@]}"

126
scripts/sandbox_command.js Normal file
View File

@@ -0,0 +1,126 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { execSync } from 'child_process';
import { existsSync, readFileSync } from 'fs';
import { join, dirname } from 'path';
import os from 'os';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import dotenv from 'dotenv';
const argv = yargs(hideBin(process.argv)).option('q', {
alias: 'quiet',
type: 'boolean',
default: false,
}).argv;
let geminiSandbox = process.env.GEMINI_SANDBOX;
if (!geminiSandbox) {
const userSettingsFile = join(os.homedir(), '.gemini', 'settings.json');
if (existsSync(userSettingsFile)) {
const settings = JSON.parse(readFileSync(userSettingsFile, 'utf-8'));
if (settings.sandbox) {
geminiSandbox = settings.sandbox;
}
}
}
if (!geminiSandbox) {
let currentDir = process.cwd();
while (currentDir !== '/') {
const geminiEnv = join(currentDir, '.gemini', '.env');
const regularEnv = join(currentDir, '.env');
if (existsSync(geminiEnv)) {
dotenv.config({ path: geminiEnv });
break;
} else if (existsSync(regularEnv)) {
dotenv.config({ path: regularEnv });
break;
}
currentDir = dirname(currentDir);
}
geminiSandbox = process.env.GEMINI_SANDBOX;
}
if (process.env.GEMINI_CODE_SANDBOX) {
console.warn(
'WARNING: GEMINI_CODE_SANDBOX is deprecated. Use GEMINI_SANDBOX instead.',
);
geminiSandbox = process.env.GEMINI_CODE_SANDBOX;
}
geminiSandbox = (geminiSandbox || '').toLowerCase();
const commandExists = (cmd) => {
const checkCommand = os.platform() === 'win32' ? 'where' : 'command -v';
try {
execSync(`${checkCommand} ${cmd}`, { stdio: 'ignore' });
return true;
} catch {
if (os.platform() === 'win32') {
try {
execSync(`${checkCommand} ${cmd}.exe`, { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
return false;
}
};
let command = '';
if (['1', 'true'].includes(geminiSandbox)) {
if (commandExists('docker')) {
command = 'docker';
} else if (commandExists('podman')) {
command = 'podman';
} else {
console.error(
'ERROR: install docker or podman or specify command in GEMINI_SANDBOX',
);
process.exit(1);
}
} else if (geminiSandbox && !['0', 'false'].includes(geminiSandbox)) {
if (commandExists(geminiSandbox)) {
command = geminiSandbox;
} else {
console.error(
`ERROR: missing sandbox command '${geminiSandbox}' (from GEMINI_SANDBOX)`,
);
process.exit(1);
}
} else {
if (os.platform() === 'darwin' && process.env.SEATBELT_PROFILE !== 'none') {
if (commandExists('sandbox-exec')) {
command = 'sandbox-exec';
} else {
process.exit(1);
}
} else {
process.exit(1);
}
}
if (!argv.q) {
console.log(command);
}
process.exit(0);

View File

@@ -1,122 +0,0 @@
#!/bin/bash
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# usage: scripts/sandbox_command.sh [-q]
# -q: quiet mode (do not print command, just exit w/ code 0 or 1)
set -euo pipefail
# parse flags
QUIET=false
while getopts ":q" opt; do
case ${opt} in
q) QUIET=true ;;
\?)
echo "Usage: $0 [-q]"
exit 1
;;
esac
done
shift $((OPTIND - 1))
# if GEMINI_SANDBOX is not set, see if it is set in user settings
# note it can be string or boolean, and if missing `npx json` will return empty string
USER_SETTINGS_FILE="$HOME/.gemini/settings.json"
if [ -z "${GEMINI_SANDBOX:-}" ] && [ -f "$USER_SETTINGS_FILE" ]; then
# Check if jq is available (more reliable than npx json)
if command -v jq &>/dev/null; then
USER_SANDBOX_SETTING=$(jq -r '.sandbox // empty' "$USER_SETTINGS_FILE" 2>/dev/null || echo "")
else
# Fallback to npx json with error handling
USER_SANDBOX_SETTING=$(sed -e 's/\/\/.*//' -e 's/\/\*.*\*\///g' -e '/^[[:space:]]*\/\//d' "$USER_SETTINGS_FILE" | npx json 'sandbox' 2>/dev/null || echo "")
fi
# Avoid setting GEMINI_SANDBOX to complex objects
if [ -n "$USER_SANDBOX_SETTING" ] && [[ ! "$USER_SANDBOX_SETTING" =~ ^\{.*\}$ ]]; then
GEMINI_SANDBOX=$USER_SANDBOX_SETTING
fi
fi
# if GEMINI_SANDBOX is not set, try to source .env in case set there
# allow .env to be in any ancestor directory (same as findEnvFile in config.ts)
# prefer gemini-specific .env under .gemini folder (also same as in findEnvFile)
if [ -z "${GEMINI_SANDBOX:-}" ]; then
current_dir=$(pwd)
dot_env_sourced=false
while [ "$current_dir" != "/" ]; do
if [ -f "$current_dir/.gemini/.env" ]; then
source "$current_dir/.gemini/.env"
dot_env_sourced=true
break
elif [ -f "$current_dir/.env" ]; then
source "$current_dir/.env"
dot_env_sourced=true
break
fi
current_dir=$(dirname "$current_dir")
done
# if .env is not found in any ancestor directory, try home as fallback
if [ "$dot_env_sourced" = false ]; then
if [ -f "$HOME/.gemini/.env" ]; then
source "$HOME/.gemini/.env"
dot_env_sourced=true
elif [ -f "$HOME/.env" ]; then
source "$HOME/.env"
dot_env_sourced=true
fi
fi
fi
# copy and warn about deprecated GEMINI_CODE_SANDBOX
if [ -n "${GEMINI_CODE_SANDBOX:-}" ]; then
echo "WARNING: GEMINI_CODE_SANDBOX is deprecated. Use GEMINI_SANDBOX instead." >&2
GEMINI_SANDBOX=$GEMINI_CODE_SANDBOX
export GEMINI_SANDBOX
fi
# lowercase GEMINI_SANDBOX
GEMINI_SANDBOX=$(echo "${GEMINI_SANDBOX:-}" | tr '[:upper:]' '[:lower:]')
# if GEMINI_SANDBOX is set to 1|true, then try to use docker or podman
# if non-empty and not 0|false, treat as custom command and check that it exists
# if empty or 0|false, then fail silently (after checking for possible fallbacks)
command=""
if [[ "${GEMINI_SANDBOX:-}" =~ ^(1|true)$ ]]; then
if command -v docker &>/dev/null; then
command="docker"
elif command -v podman &>/dev/null; then
command="podman"
else
echo "ERROR: install docker or podman or specify command in GEMINI_SANDBOX" >&2
exit 1
fi
elif [ -n "${GEMINI_SANDBOX:-}" ] && [[ ! "${GEMINI_SANDBOX:-}" =~ ^(0|false)$ ]]; then
if ! command -v "$GEMINI_SANDBOX" &>/dev/null; then
echo "ERROR: missing sandbox command '$GEMINI_SANDBOX' (from GEMINI_SANDBOX)" >&2
exit 1
fi
command="$GEMINI_SANDBOX"
else
# if we are on macOS and sandbox-exec is available, use that for minimal sandboxing
# unless SEATBELT_PROFILE is set to 'none', which we allow as an escape hatch
if [ "$(uname)" = "Darwin" ] && command -v sandbox-exec &>/dev/null && [ "${SEATBELT_PROFILE:-}" != "none" ]; then
command="sandbox-exec"
else # GEMINI_SANDBOX is empty or 0|false, so we fail w/o error msg
exit 1
fi
fi
if [ "$QUIET" = false ]; then echo "$command"; fi
exit 0

42
scripts/setup-dev.js Normal file
View File

@@ -0,0 +1,42 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { execSync } from 'child_process';
try {
execSync('command -v npm', { stdio: 'ignore' });
} catch {
console.log('npm not found. Installing npm via nvm...');
try {
execSync(
'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash',
{ stdio: 'inherit' },
);
const nvmsh = `\\. "$HOME/.nvm/nvm.sh"`;
execSync(`${nvmsh} && nvm install 22`, { stdio: 'inherit' });
execSync(`${nvmsh} && node -v`, { stdio: 'inherit' });
execSync(`${nvmsh} && nvm current`, { stdio: 'inherit' });
execSync(`${nvmsh} && npm -v`, { stdio: 'inherit' });
} catch {
console.error('Failed to install nvm or node.');
process.exit(1);
}
}
console.log('Development environment setup complete.');

View File

@@ -1,34 +0,0 @@
#!/bin/bash
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -euo pipefail
# Check if npm is installed
if ! command -v npm &>/dev/null; then
echo "npm not found. Installing npm via nvm..."
# Download and install nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
# in lieu of restarting the shell
\. "$HOME/.nvm/nvm.sh"
# Download and install Node.js:
nvm install 22
# Verify the Node.js version:
node -v # Should print "v22.15.0".
nvm current # Should print "v22.15.0".
# Verify npm version:
npm -v # Should print "10.9.2".
fi
echo "Development environment setup complete."

61
scripts/start.js Normal file
View File

@@ -0,0 +1,61 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law_or_agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { spawn, execSync } from 'child_process';
import { join } from 'path';
const root = join(import.meta.dirname, '..');
// check build status, write warnings to file for app to display if needed
execSync('node ./scripts/check-build-status.js', {
stdio: 'inherit',
cwd: root,
});
// if debugging is enabled and sandboxing is disabled, use --inspect-brk flag
// note with sandboxing this flag is passed to the binary inside the sandbox
// inside sandbox SANDBOX should be set and sandbox_command.js should fail
const nodeArgs = [];
try {
execSync('node scripts/sandbox_command.js -q', {
stdio: 'inherit',
cwd: root,
});
if (process.env.DEBUG) {
if (process.env.SANDBOX) {
const port = process.env.DEBUG_PORT || '9229';
nodeArgs.push(`--inspect-brk=0.0.0.0:${port}`);
} else {
nodeArgs.push('--inspect-brk');
}
}
} catch {
// ignore
}
nodeArgs.push('./packages/cli');
nodeArgs.push(...process.argv.slice(2));
const env = {
...process.env,
CLI_VERSION: 'development',
DEV: 'true',
};
spawn('node', nodeArgs, { stdio: 'inherit', env });

View File

@@ -1,37 +0,0 @@
#!/bin/bash
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -euo pipefail
# check build status, write warnings to file for app to display if needed
node ./scripts/check-build-status.js
# if debugging is enabled and sandboxing is disabled, use --inspect-brk flag
# note with sandboxing this flag is passed to the binary inside the sandbox
# inside sandbox SANDBOX should be set and sandbox_command.sh should fail
node_args=()
if [ -n "${DEBUG:-}" ] && ! scripts/sandbox_command.sh -q; then
if [ -n "${SANDBOX:-}" ]; then
port="${DEBUG_PORT:-9229}"
node_args=("--inspect-brk=0.0.0.0:$port")
else
node_args=(--inspect-brk)
fi
fi
node_args+=("./packages/cli" "$@")
# DEV=true to enable React Dev Tools (https://github.com/vadimdemedes/ink?tab=readme-ov-file#using-react-devtools)
# CLI_VERSION to display in the app ui footer
CLI_VERSION='development' DEV=true node "${node_args[@]}"