summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rw-r--r--scripts/build.js54
-rwxr-xr-xscripts/build.sh30
-rw-r--r--scripts/build_package.js37
-rwxr-xr-xscripts/build_package.sh33
-rw-r--r--scripts/build_sandbox.js125
-rwxr-xr-xscripts/build_sandbox.sh102
-rw-r--r--scripts/clean.js32
-rwxr-xr-xscripts/clean.sh21
-rw-r--r--scripts/copy_bundle_assets.js48
-rwxr-xr-xscripts/copy_bundle_assets.sh13
-rw-r--r--scripts/generate-git-commit-info.js61
-rwxr-xr-xscripts/generate-git-commit-info.sh44
-rw-r--r--scripts/publish-sandbox.js55
-rwxr-xr-xscripts/publish-sandbox.sh41
-rw-r--r--scripts/sandbox.js123
-rwxr-xr-xscripts/sandbox.sh103
-rw-r--r--scripts/sandbox_command.js126
-rwxr-xr-xscripts/sandbox_command.sh122
-rw-r--r--scripts/setup-dev.js42
-rwxr-xr-xscripts/setup-dev.sh34
-rw-r--r--scripts/start.js61
-rwxr-xr-xscripts/start.sh37
22 files changed, 764 insertions, 580 deletions
diff --git a/scripts/build.js b/scripts/build.js
new file mode 100644
index 00000000..373e268e
--- /dev/null
+++ b/scripts/build.js
@@ -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
+}
diff --git a/scripts/build.sh b/scripts/build.sh
deleted file mode 100755
index 0eba264f..00000000
--- a/scripts/build.sh
+++ /dev/null
@@ -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
diff --git a/scripts/build_package.js b/scripts/build_package.js
new file mode 100644
index 00000000..9e1b05bf
--- /dev/null
+++ b/scripts/build_package.js
@@ -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);
diff --git a/scripts/build_package.sh b/scripts/build_package.sh
deleted file mode 100755
index e64d98cc..00000000
--- a/scripts/build_package.sh
+++ /dev/null
@@ -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
diff --git a/scripts/build_sandbox.js b/scripts/build_sandbox.js
new file mode 100644
index 00000000..bfcf1bf9
--- /dev/null
+++ b/scripts/build_sandbox.js
@@ -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' });
diff --git a/scripts/build_sandbox.sh b/scripts/build_sandbox.sh
deleted file mode 100755
index dabcffcf..00000000
--- a/scripts/build_sandbox.sh
+++ /dev/null
@@ -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
diff --git a/scripts/clean.js b/scripts/clean.js
new file mode 100644
index 00000000..dd2911e9
--- /dev/null
+++ b/scripts/clean.js
@@ -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 });
diff --git a/scripts/clean.sh b/scripts/clean.sh
deleted file mode 100755
index 2f6bc3c0..00000000
--- a/scripts/clean.sh
+++ /dev/null
@@ -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
diff --git a/scripts/copy_bundle_assets.js b/scripts/copy_bundle_assets.js
new file mode 100644
index 00000000..cb1ac197
--- /dev/null
+++ b/scripts/copy_bundle_assets.js
@@ -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/');
diff --git a/scripts/copy_bundle_assets.sh b/scripts/copy_bundle_assets.sh
deleted file mode 100755
index 93d46364..00000000
--- a/scripts/copy_bundle_assets.sh
+++ /dev/null
@@ -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/" \ No newline at end of file
diff --git a/scripts/generate-git-commit-info.js b/scripts/generate-git-commit-info.js
new file mode 100644
index 00000000..f046a7fb
--- /dev/null
+++ b/scripts/generate-git-commit-info.js
@@ -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);
diff --git a/scripts/generate-git-commit-info.sh b/scripts/generate-git-commit-info.sh
deleted file mode 100755
index 2a64830f..00000000
--- a/scripts/generate-git-commit-info.sh
+++ /dev/null
@@ -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
diff --git a/scripts/publish-sandbox.js b/scripts/publish-sandbox.js
new file mode 100644
index 00000000..916089be
--- /dev/null
+++ b/scripts/publish-sandbox.js
@@ -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' });
+}
diff --git a/scripts/publish-sandbox.sh b/scripts/publish-sandbox.sh
deleted file mode 100755
index dfc16353..00000000
--- a/scripts/publish-sandbox.sh
+++ /dev/null
@@ -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
diff --git a/scripts/sandbox.js b/scripts/sandbox.js
new file mode 100644
index 00000000..58223180
--- /dev/null
+++ b/scripts/sandbox.js
@@ -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' });
diff --git a/scripts/sandbox.sh b/scripts/sandbox.sh
deleted file mode 100755
index b6a52c14..00000000
--- a/scripts/sandbox.sh
+++ /dev/null
@@ -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[@]}"
diff --git a/scripts/sandbox_command.js b/scripts/sandbox_command.js
new file mode 100644
index 00000000..7f8e8381
--- /dev/null
+++ b/scripts/sandbox_command.js
@@ -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);
diff --git a/scripts/sandbox_command.sh b/scripts/sandbox_command.sh
deleted file mode 100755
index 468a4834..00000000
--- a/scripts/sandbox_command.sh
+++ /dev/null
@@ -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
diff --git a/scripts/setup-dev.js b/scripts/setup-dev.js
new file mode 100644
index 00000000..c4e2b22c
--- /dev/null
+++ b/scripts/setup-dev.js
@@ -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.');
diff --git a/scripts/setup-dev.sh b/scripts/setup-dev.sh
deleted file mode 100755
index de2ae336..00000000
--- a/scripts/setup-dev.sh
+++ /dev/null
@@ -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."
diff --git a/scripts/start.js b/scripts/start.js
new file mode 100644
index 00000000..f9f85c8e
--- /dev/null
+++ b/scripts/start.js
@@ -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 });
diff --git a/scripts/start.sh b/scripts/start.sh
deleted file mode 100755
index 2caf540d..00000000
--- a/scripts/start.sh
+++ /dev/null
@@ -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[@]}"