From d36589779dc6d0b3af64240ce207722daa79542c Mon Sep 17 00:00:00 2001 From: Michael Xiao Date: Wed, 11 Mar 2026 16:50:29 -0400 Subject: [PATCH 1/3] Add Claude Code and Gemini CLI to VSCode, RStudio, JupyterLab, NeMo Jupyter and Parabricks Jupyter. Install AI extensions (Claude Code, Gemini) in VSCode app --- features/src/gemini-cli/README.md | 24 ++++++ .../src/gemini-cli/devcontainer-feature.json | 17 +++++ features/src/gemini-cli/install.sh | 74 +++++++++++++++++++ features/src/workbench-tools/install.sh | 12 +++ .../.devcontainer.json | 5 ++ .../Dockerfile | 2 +- src/nemo_jupyter/.devcontainer.json | 5 ++ src/r-analysis/.devcontainer.json | 5 ++ src/vscode/.devcontainer.json | 5 ++ src/vscode/Dockerfile | 18 +++++ src/vscode/docker-compose.yaml | 4 +- src/vscode/install-extensions.sh | 10 +++ .../.devcontainer.json | 5 ++ 13 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 features/src/gemini-cli/README.md create mode 100644 features/src/gemini-cli/devcontainer-feature.json create mode 100755 features/src/gemini-cli/install.sh create mode 100644 src/vscode/Dockerfile create mode 100644 src/vscode/install-extensions.sh 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", From 603d5f0d2643693e1c7a744392d3eb80ad9ec028 Mon Sep 17 00:00:00 2001 From: Michael Xiao <44096341+michaelxiao7@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:57:53 -0400 Subject: [PATCH 2/3] Address PR comments -- all fixes Address PR comments, testing separately before merging into PR --- feature-versions/state.json | 10 +++++ features/src/gemini-cli/install.sh | 41 +++++++++---------- features/src/workbench-tools/install.sh | 12 ------ .../.devcontainer.json | 4 +- src/nemo_jupyter/.devcontainer.json | 4 +- src/r-analysis/.devcontainer.json | 4 +- src/vscode/.devcontainer.json | 4 +- src/vscode/Dockerfile | 17 ++++---- .../.devcontainer.json | 4 +- startupscript/aws/post-startup-hook.sh | 5 +++ startupscript/setup-bashrc.sh | 11 +++++ 11 files changed, 64 insertions(+), 52 deletions(-) diff --git a/feature-versions/state.json b/feature-versions/state.json index 5935dada..05916cc8 100644 --- a/feature-versions/state.json +++ b/feature-versions/state.json @@ -24,6 +24,16 @@ "installed": "sha256:dbf431d6b42d55cde50fa1df75c7f7c3999a90cde6d73f7a7071174b3c3d0cc4", "filter": ".*\\/\\.devcontainer\\.json" }, + "ghcr.io/devcontainers/features/node": { + "tag": "1", + "installed": "sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6", + "filter": ".*\\/\\.devcontainer\\.json" + }, + "ghcr.io/anthropics/devcontainer-features/claude-code": { + "tag": "1.0", + "installed": "sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a", + "filter": ".*\\/\\.devcontainer\\.json" + }, "us-west2-docker.pkg.dev/shared-pub-buckets-94mvrf/workbench-artifacts/app-wondershaper": { "tag": "latest", "installed": "sha256:7eafa2c2dfc853b0bbd9c3b002160ab6597b62d5727f419aeec951539e74f452", diff --git a/features/src/gemini-cli/install.sh b/features/src/gemini-cli/install.sh index 2cbee5da..8dcc9a47 100755 --- a/features/src/gemini-cli/install.sh +++ b/features/src/gemini-cli/install.sh @@ -1,12 +1,25 @@ #!/usr/bin/env bash -set -eu +set -o errexit +set -o nounset +set -o pipefail +set -o xtrace -# Function to install Gemini CLI install_gemini_cli() { + local username="${1:-root}" echo "Installing Gemini CLI..." - npm install -g @google/gemini-cli - if command -v gemini >/dev/null; then + if [ "${username}" = "root" ]; then + npm install -g @google/gemini-cli@0.34.0 + else + local nvm_dir="${NVM_DIR:-/usr/local/share/nvm}" + local user_home + user_home=$(eval echo "~${username}" 2>/dev/null || echo "/home/${username}") + [ -d "${nvm_dir}" ] && chown -R "${username}:" "${nvm_dir}" + [ -d "${user_home}/.npm" ] && chown -R "${username}:" "${user_home}/.npm" + sudo -u "${username}" env PATH="${PATH}" npm install -g @google/gemini-cli@0.34.0 + fi + + if which gemini >/dev/null 2>&1; then echo "Gemini CLI installed successfully!" return 0 else @@ -15,7 +28,6 @@ install_gemini_cli() { fi } -# Function to fix permissions for non-root users fix_permissions() { local username="${1:-root}" @@ -23,29 +35,14 @@ fix_permissions() { 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" + printf '{"general.enableAutoUpdate": false, "ui": {"autoThemeSwitching": false, "theme": "ANSI Light"}}\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 +install_gemini_cli "${USERNAME:-root}" || exit 1 fix_permissions "${USERNAME:-root}" diff --git a/features/src/workbench-tools/install.sh b/features/src/workbench-tools/install.sh index d1e2786f..7b8e557e 100755 --- a/features/src/workbench-tools/install.sh +++ b/features/src/workbench-tools/install.sh @@ -180,18 +180,6 @@ 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 4a0b4449..49a7b6b4 100644 --- a/src/custom-workbench-jupyter-template/.devcontainer.json +++ b/src/custom-workbench-jupyter-template/.devcontainer.json @@ -21,10 +21,10 @@ "${templateOption:login}" ], "features": { - "ghcr.io/devcontainers/features/node": { + "ghcr.io/devcontainers/features/node@sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6": { "version": "24.11.0" }, - "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}, + "ghcr.io/anthropics/devcontainer-features/claude-code@sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a": {}, "./.devcontainer/features/gemini-cli": { "username": "jupyter" }, "./.devcontainer/features/workbench-tools": { "libEnv": "/opt/conda/envs/jupyter", // Use the jupyter conda environment diff --git a/src/nemo_jupyter/.devcontainer.json b/src/nemo_jupyter/.devcontainer.json index 1f4d2656..20eed5d5 100644 --- a/src/nemo_jupyter/.devcontainer.json +++ b/src/nemo_jupyter/.devcontainer.json @@ -24,10 +24,10 @@ "version": "17" }, "ghcr.io/dhoeric/features/google-cloud-cli@sha256:fa5d894718825c5ad8009ac8f2c9f0cea3d1661eb108a9d465cba9f3fc48965f": {}, - "ghcr.io/devcontainers/features/node": { + "ghcr.io/devcontainers/features/node@sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6": { "version": "24.11.0" }, - "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}, + "ghcr.io/anthropics/devcontainer-features/claude-code@sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a": {}, "./.devcontainer/features/gemini-cli": { "username": "jupyter" }, "./.devcontainer/features/workbench-tools": { "libPythonVersion": "3.12", // Must match python version in nemo image diff --git a/src/r-analysis/.devcontainer.json b/src/r-analysis/.devcontainer.json index 0a783e09..dba17eab 100644 --- a/src/r-analysis/.devcontainer.json +++ b/src/r-analysis/.devcontainer.json @@ -29,10 +29,10 @@ }, "ghcr.io/devcontainers/features/aws-cli@sha256:17cb4a40151f59144b46957b9264683663b0214371a041ecd53dccc015a4b923": {}, "ghcr.io/dhoeric/features/google-cloud-cli@sha256:fa5d894718825c5ad8009ac8f2c9f0cea3d1661eb108a9d465cba9f3fc48965f": {}, - "ghcr.io/devcontainers/features/node": { + "ghcr.io/devcontainers/features/node@sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6": { "version": "24.11.0" }, - "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}, + "ghcr.io/anthropics/devcontainer-features/claude-code@sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a": {}, "./.devcontainer/features/gemini-cli": { "username": "rstudio" }, "./.devcontainer/features/workbench-tools": { "cloud": "${templateOption:cloud}", diff --git a/src/vscode/.devcontainer.json b/src/vscode/.devcontainer.json index fb205b51..45e02510 100644 --- a/src/vscode/.devcontainer.json +++ b/src/vscode/.devcontainer.json @@ -18,12 +18,12 @@ "ghcr.io/devcontainers/features/java@sha256:e75d274ac969b29a59ba6f34c2d098f6a52144d0ec027ef326b724ea4b8b7b4e": { "version": "17" }, - "ghcr.io/devcontainers/features/node": { + "ghcr.io/devcontainers/features/node@sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6": { "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": {}, + "ghcr.io/anthropics/devcontainer-features/claude-code@sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a": {}, "./.devcontainer/features/gemini-cli": { "username": "abc" }, "./.devcontainer/features/workbench-tools": { "cloud": "${templateOption:cloud}", diff --git a/src/vscode/Dockerfile b/src/vscode/Dockerfile index a7759754..fa16bd06 100644 --- a/src/vscode/Dockerfile +++ b/src/vscode/Dockerfile @@ -2,14 +2,15 @@ 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 +RUN apt-get update \ + && apt-get install -y --no-install-recommends jq \ + && rm -rf /var/lib/apt/lists/* \ + && curl -fsSL "https://open-vsx.org/api/Google/geminicodeassist/latest" \ + | jq -r '.files.download' \ + | xargs curl -fL --compressed -o /opt/geminicodeassist.vsix \ + && curl -fsSL "https://open-vsx.org/api/Anthropic/claude-code/latest" \ + | jq -r '.files.download' \ + | xargs curl -fL --compressed -o /opt/claudecode.vsix # Install extensions during container init, before code-server starts COPY install-extensions.sh /etc/cont-init.d/99-install-extensions diff --git a/src/workbench-jupyter-parabricks/.devcontainer.json b/src/workbench-jupyter-parabricks/.devcontainer.json index 0f05a651..b94aeb29 100644 --- a/src/workbench-jupyter-parabricks/.devcontainer.json +++ b/src/workbench-jupyter-parabricks/.devcontainer.json @@ -25,10 +25,10 @@ }, "ghcr.io/dhoeric/features/google-cloud-cli@sha256:fa5d894718825c5ad8009ac8f2c9f0cea3d1661eb108a9d465cba9f3fc48965f": {}, "ghcr.io/devcontainers/features/aws-cli@sha256:17cb4a40151f59144b46957b9264683663b0214371a041ecd53dccc015a4b923":{}, - "ghcr.io/devcontainers/features/node": { + "ghcr.io/devcontainers/features/node@sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6": { "version": "24.11.0" }, - "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}, + "ghcr.io/anthropics/devcontainer-features/claude-code@sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a": {}, "./.devcontainer/features/gemini-cli": { "username": "jupyter" }, "./.devcontainer/features/workbench-tools": { "cloud": "${templateOption:cloud}", diff --git a/startupscript/aws/post-startup-hook.sh b/startupscript/aws/post-startup-hook.sh index a4952cf3..db1b230d 100755 --- a/startupscript/aws/post-startup-hook.sh +++ b/startupscript/aws/post-startup-hook.sh @@ -47,6 +47,11 @@ export WORKBENCH_INSTALL_PATH="${WORKBENCH_INSTALL_PATH}" export AWS_CONFIG_FILE="${AWS_CONFIG_FILE}" export AWS_VAULT_BACKEND="file" export AWS_VAULT_FILE_PASSPHRASE="" + +# Headless Workbench (same as setup-bashrc.sh on GCP; this file replaces ~/.bashrc on AWS) +export DISPLAY= +export BROWSER= +export NO_BROWSER=1 EOF ####################################### diff --git a/startupscript/setup-bashrc.sh b/startupscript/setup-bashrc.sh index c1cdb21f..ad1d4575 100644 --- a/startupscript/setup-bashrc.sh +++ b/startupscript/setup-bashrc.sh @@ -91,3 +91,14 @@ export AWS_VAULT_FILE_PASSPHRASE="" EOF fi + +# Headless devcontainers: no real display/browser for gcloud, Claude Code, Gemini CLI. +emit "Adding headless CLI environment to ~/.bashrc ..." + +cat << 'EOF' >> "${USER_BASHRC}" + +# Headless Workbench (gcloud, Claude Code, Gemini CLI) +export DISPLAY= +export BROWSER= +export NO_BROWSER=1 +EOF From 38b668cfd0f89033fe9a241eed88f378e86fcbb6 Mon Sep 17 00:00:00 2001 From: Michael Xiao Date: Wed, 25 Mar 2026 11:00:21 -0400 Subject: [PATCH 3/3] Address PR suggestions --- features/src/gemini-cli/install.sh | 14 ++++++++------ startupscript/aws/post-startup-hook.sh | 5 ----- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/features/src/gemini-cli/install.sh b/features/src/gemini-cli/install.sh index 8dcc9a47..c9ad7bef 100755 --- a/features/src/gemini-cli/install.sh +++ b/features/src/gemini-cli/install.sh @@ -4,19 +4,21 @@ set -o nounset set -o pipefail set -o xtrace +readonly USERNAME="${USERNAME:-"root"}" + install_gemini_cli() { - local username="${1:-root}" + local username="${1}" echo "Installing Gemini CLI..." if [ "${username}" = "root" ]; then - npm install -g @google/gemini-cli@0.34.0 + npm install -g @google/gemini-cli else local nvm_dir="${NVM_DIR:-/usr/local/share/nvm}" local user_home user_home=$(eval echo "~${username}" 2>/dev/null || echo "/home/${username}") [ -d "${nvm_dir}" ] && chown -R "${username}:" "${nvm_dir}" [ -d "${user_home}/.npm" ] && chown -R "${username}:" "${user_home}/.npm" - sudo -u "${username}" env PATH="${PATH}" npm install -g @google/gemini-cli@0.34.0 + sudo -u "${username}" env PATH="${PATH}" npm install -g @google/gemini-cli fi if which gemini >/dev/null 2>&1; then @@ -29,7 +31,7 @@ install_gemini_cli() { } fix_permissions() { - local username="${1:-root}" + local username="${1}" if [ "${username}" = "root" ]; then return 0 @@ -64,8 +66,8 @@ if ! command -v node >/dev/null || ! command -v npm >/dev/null; then print_nodejs_requirement fi -install_gemini_cli "${USERNAME:-root}" || exit 1 +install_gemini_cli "${USERNAME}" || exit 1 -fix_permissions "${USERNAME:-root}" +fix_permissions "${USERNAME}" echo "Done!" \ No newline at end of file diff --git a/startupscript/aws/post-startup-hook.sh b/startupscript/aws/post-startup-hook.sh index db1b230d..a4952cf3 100755 --- a/startupscript/aws/post-startup-hook.sh +++ b/startupscript/aws/post-startup-hook.sh @@ -47,11 +47,6 @@ export WORKBENCH_INSTALL_PATH="${WORKBENCH_INSTALL_PATH}" export AWS_CONFIG_FILE="${AWS_CONFIG_FILE}" export AWS_VAULT_BACKEND="file" export AWS_VAULT_FILE_PASSPHRASE="" - -# Headless Workbench (same as setup-bashrc.sh on GCP; this file replaces ~/.bashrc on AWS) -export DISPLAY= -export BROWSER= -export NO_BROWSER=1 EOF #######################################