diff --git a/features/src/gemini-cli/README.md b/features/src/gemini-cli/README.md new file mode 100644 index 00000000..ddc0d413 --- /dev/null +++ b/features/src/gemini-cli/README.md @@ -0,0 +1,24 @@ + +# Gemini CLI (gemini-cli) + +Installs the Gemini CLI globally + +## Example Usage + +```json +"features": { + "./.devcontainer/features/gemini-cli": {} +} +``` + +## Options + +| Options Id | Description | Type | Default Value | +|-----|-----|-----|-----| +| username | Username of the container user | string | root | + + + +--- + +_Note: This file was auto-generated from the [devcontainer-feature.json](devcontainer-feature.json). Add additional notes to a `NOTES.md`._ diff --git a/features/src/gemini-cli/devcontainer-feature.json b/features/src/gemini-cli/devcontainer-feature.json new file mode 100644 index 00000000..303fe55e --- /dev/null +++ b/features/src/gemini-cli/devcontainer-feature.json @@ -0,0 +1,17 @@ +{ + "name": "Gemini CLI", + "id": "gemini-cli", + "version": "1.0.0", + "description": "Installs the Gemini CLI globally", + "documentationURL": "https://github.com/google-gemini/gemini-cli", + "options": { + "username": { + "type": "string", + "default": "root", + "description": "Username of the container user" + } + }, + "installsAfter": [ + "ghcr.io/devcontainers/features/node" + ] +} diff --git a/features/src/gemini-cli/install.sh b/features/src/gemini-cli/install.sh new file mode 100755 index 00000000..2cbee5da --- /dev/null +++ b/features/src/gemini-cli/install.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -eu + +# Function to install Gemini CLI +install_gemini_cli() { + echo "Installing Gemini CLI..." + npm install -g @google/gemini-cli + + if command -v gemini >/dev/null; then + echo "Gemini CLI installed successfully!" + return 0 + else + echo "ERROR: Gemini CLI installation failed!" + return 1 + fi +} + +# Function to fix permissions for non-root users +fix_permissions() { + local username="${1:-root}" + + if [ "${username}" = "root" ]; then + return 0 + fi + + # Fix NVM permissions: node feature installs as root, causing "Permission denied" in non-root containers + local nvm_dir="${NVM_DIR:-/usr/local/share/nvm}" + if [ -d "${nvm_dir}" ]; then + echo "Fixing NVM permissions for user ${username}..." + chown -R "${username}:" "${nvm_dir}" + fi + + # Fix npm cache: npm install -g as root creates root-owned files in user's ~/.npm + local user_home + user_home=$(eval echo "~${username}" 2>/dev/null || echo "/home/${username}") + if [ -d "${user_home}/.npm" ]; then + echo "Fixing npm cache ownership for user ${username}..." + chown -R "${username}:" "${user_home}/.npm" + fi + + # Edge case: Disable auto-update to prevent gemini from trying to re-exec + # itself on first run, which fails on freshly provisioned machines. + mkdir -p "${user_home}/.gemini" + printf '{"general.enableAutoUpdate": false}\n' > "${user_home}/.gemini/settings.json" + chown -R "${username}:" "${user_home}/.gemini" +} + +# Print error message about requiring Node.js feature +print_nodejs_requirement() { + cat </dev/null || ! command -v npm >/dev/null; then + print_nodejs_requirement +fi + +install_gemini_cli || exit 1 + +fix_permissions "${USERNAME:-root}" + +echo "Done!" \ No newline at end of file diff --git a/features/src/workbench-tools/install.sh b/features/src/workbench-tools/install.sh index 7b8e557e..d1e2786f 100755 --- a/features/src/workbench-tools/install.sh +++ b/features/src/workbench-tools/install.sh @@ -180,6 +180,18 @@ chown -R "${USERNAME}:" "${LIBRARIES_ENV_DIR}" # Set CROMWELL_JAR environment variable printf 'export CROMWELL_JAR="%s"\n' "${BINARIES_ENV_DIR}/share/cromwell/cromwell.jar" + + # Wrap gcloud to unset DISPLAY per-invocation so auth commands don't try + # to open a browser via X11 in headless devcontainer environments. + printf 'function gcloud() { DISPLAY= command gcloud "$@"; }\n' + + # Wrap claude to clear BROWSER per-invocation so any app-managed browser + # handler does not intercept the auth URL in headless environments. + printf 'function claude() { BROWSER= command claude "$@"; }\n' + + # Wrap gemini to set NO_BROWSER=1 per-invocation to force device code flow + # instead of opening a browser window. + printf 'function gemini() { NO_BROWSER=1 NO_COLOR=1 command gemini "$@"; }\n' } >> "${USER_HOME_DIR}/.bashrc" # Allow .bashrc to be sourced in non-interactive shells diff --git a/src/custom-workbench-jupyter-template/.devcontainer.json b/src/custom-workbench-jupyter-template/.devcontainer.json index 3384b3e2..4a0b4449 100644 --- a/src/custom-workbench-jupyter-template/.devcontainer.json +++ b/src/custom-workbench-jupyter-template/.devcontainer.json @@ -21,6 +21,11 @@ "${templateOption:login}" ], "features": { + "ghcr.io/devcontainers/features/node": { + "version": "24.11.0" + }, + "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}, + "./.devcontainer/features/gemini-cli": { "username": "jupyter" }, "./.devcontainer/features/workbench-tools": { "libEnv": "/opt/conda/envs/jupyter", // Use the jupyter conda environment "cloud": "${templateOption:cloud}", diff --git a/src/custom-workbench-jupyter-template/Dockerfile b/src/custom-workbench-jupyter-template/Dockerfile index 5d6d6a04..5b33c8d1 100644 --- a/src/custom-workbench-jupyter-template/Dockerfile +++ b/src/custom-workbench-jupyter-template/Dockerfile @@ -1,4 +1,4 @@ -FROM us-west2-docker.pkg.dev/shared-pub-buckets-94mvrf/workbench-artifacts/app-workbench-jupyter@sha256:229166b2be902aef0a2a621fae38037fe0e069875df1b8bfd5bcff8766d6036c +FROM us-west2-docker.pkg.dev/shared-pub-buckets-94mvrf/workbench-artifacts/app-workbench-jupyter@sha256:229166b2be902aef0a2a621fae38037fe0e069875df1b8bfd5bcff8766d6036c # Install jupyter extensions RUN --mount=type=bind,from=jupyter-extension-builder,source=/dist,target=/tmp/extensions \ diff --git a/src/nemo_jupyter/.devcontainer.json b/src/nemo_jupyter/.devcontainer.json index f94ab953..1f4d2656 100644 --- a/src/nemo_jupyter/.devcontainer.json +++ b/src/nemo_jupyter/.devcontainer.json @@ -24,6 +24,11 @@ "version": "17" }, "ghcr.io/dhoeric/features/google-cloud-cli@sha256:fa5d894718825c5ad8009ac8f2c9f0cea3d1661eb108a9d465cba9f3fc48965f": {}, + "ghcr.io/devcontainers/features/node": { + "version": "24.11.0" + }, + "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}, + "./.devcontainer/features/gemini-cli": { "username": "jupyter" }, "./.devcontainer/features/workbench-tools": { "libPythonVersion": "3.12", // Must match python version in nemo image "cloud": "${templateOption:cloud}", diff --git a/src/r-analysis/.devcontainer.json b/src/r-analysis/.devcontainer.json index 00fea5a8..0a783e09 100644 --- a/src/r-analysis/.devcontainer.json +++ b/src/r-analysis/.devcontainer.json @@ -29,6 +29,11 @@ }, "ghcr.io/devcontainers/features/aws-cli@sha256:17cb4a40151f59144b46957b9264683663b0214371a041ecd53dccc015a4b923": {}, "ghcr.io/dhoeric/features/google-cloud-cli@sha256:fa5d894718825c5ad8009ac8f2c9f0cea3d1661eb108a9d465cba9f3fc48965f": {}, + "ghcr.io/devcontainers/features/node": { + "version": "24.11.0" + }, + "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}, + "./.devcontainer/features/gemini-cli": { "username": "rstudio" }, "./.devcontainer/features/workbench-tools": { "cloud": "${templateOption:cloud}", "username": "rstudio", diff --git a/src/vscode/.devcontainer.json b/src/vscode/.devcontainer.json index c3c92027..fb205b51 100644 --- a/src/vscode/.devcontainer.json +++ b/src/vscode/.devcontainer.json @@ -18,8 +18,13 @@ "ghcr.io/devcontainers/features/java@sha256:e75d274ac969b29a59ba6f34c2d098f6a52144d0ec027ef326b724ea4b8b7b4e": { "version": "17" }, + "ghcr.io/devcontainers/features/node": { + "version": "24.11.0" + }, "ghcr.io/devcontainers/features/aws-cli@sha256:17cb4a40151f59144b46957b9264683663b0214371a041ecd53dccc015a4b923": {}, "ghcr.io/dhoeric/features/google-cloud-cli@sha256:fa5d894718825c5ad8009ac8f2c9f0cea3d1661eb108a9d465cba9f3fc48965f": {}, + "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}, + "./.devcontainer/features/gemini-cli": { "username": "abc" }, "./.devcontainer/features/workbench-tools": { "cloud": "${templateOption:cloud}", "username": "abc", diff --git a/src/vscode/Dockerfile b/src/vscode/Dockerfile new file mode 100644 index 00000000..a7759754 --- /dev/null +++ b/src/vscode/Dockerfile @@ -0,0 +1,18 @@ +FROM lscr.io/linuxserver/code-server:4.100.3 + +# Gemini: https://open-vsx.org/extension/Google/geminicodeassist +# Claude: https://open-vsx.org/extension/Anthropic/claude-code +RUN GEMINI_VERSION=$(curl -fsSL "https://open-vsx.org/api/Google/geminicodeassist/latest" | grep -o '"version":"[^"]*"' | head -1 | cut -d'"' -f4) && \ + curl -fL --compressed \ + "https://open-vsx.org/api/Google/geminicodeassist/${GEMINI_VERSION}/file/Google.geminicodeassist-${GEMINI_VERSION}.vsix" \ + -o /opt/geminicodeassist.vsix && \ + CLAUDE_VERSION=$(curl -fsSL "https://open-vsx.org/api/Anthropic/claude-code/latest" | grep -o '"version":"[^"]*"' | head -1 | cut -d'"' -f4) && \ + curl -fL --compressed \ + "https://open-vsx.org/api/Anthropic/claude-code/${CLAUDE_VERSION}/file/Anthropic.claude-code-${CLAUDE_VERSION}.vsix" \ + -o /opt/claudecode.vsix + +# Install extensions during container init, before code-server starts +COPY install-extensions.sh /etc/cont-init.d/99-install-extensions +RUN chmod +x /etc/cont-init.d/99-install-extensions + +WORKDIR /config diff --git a/src/vscode/docker-compose.yaml b/src/vscode/docker-compose.yaml index d6ef4668..01f6af64 100644 --- a/src/vscode/docker-compose.yaml +++ b/src/vscode/docker-compose.yaml @@ -2,7 +2,9 @@ version: "2.4" services: app: container_name: "application-server" - image: "lscr.io/linuxserver/code-server:4.100.3" + build: + context: . + dockerfile: Dockerfile restart: always volumes: - .:/workspace:cached diff --git a/src/vscode/install-extensions.sh b/src/vscode/install-extensions.sh new file mode 100644 index 00000000..0126e604 --- /dev/null +++ b/src/vscode/install-extensions.sh @@ -0,0 +1,10 @@ +#!/usr/bin/with-contenv bash +# shellcheck shell=bash +# Installs VS Code extensions once on first container boot. + +[ -f /config/.extensions-installed ] && exit 0 + +HOME=/config s6-setuidgid abc /app/code-server/bin/code-server --extensions-dir /config/extensions --install-extension /opt/geminicodeassist.vsix +HOME=/config s6-setuidgid abc /app/code-server/bin/code-server --extensions-dir /config/extensions --install-extension /opt/claudecode.vsix + +touch /config/.extensions-installed diff --git a/src/workbench-jupyter-parabricks/.devcontainer.json b/src/workbench-jupyter-parabricks/.devcontainer.json index 516ac8e0..0f05a651 100644 --- a/src/workbench-jupyter-parabricks/.devcontainer.json +++ b/src/workbench-jupyter-parabricks/.devcontainer.json @@ -25,6 +25,11 @@ }, "ghcr.io/dhoeric/features/google-cloud-cli@sha256:fa5d894718825c5ad8009ac8f2c9f0cea3d1661eb108a9d465cba9f3fc48965f": {}, "ghcr.io/devcontainers/features/aws-cli@sha256:17cb4a40151f59144b46957b9264683663b0214371a041ecd53dccc015a4b923":{}, + "ghcr.io/devcontainers/features/node": { + "version": "24.11.0" + }, + "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}, + "./.devcontainer/features/gemini-cli": { "username": "jupyter" }, "./.devcontainer/features/workbench-tools": { "cloud": "${templateOption:cloud}", "username": "jupyter",