diff --git a/feature-versions/state.json b/feature-versions/state.json index 47f9fd8e..1f9caaee 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:6cb6bde6ace7dd7e82b9397e6c9d580e00969091e8091d78fea591ebecf0f6cb", 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..c9ad7bef --- /dev/null +++ b/features/src/gemini-cli/install.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +set -o errexit +set -o nounset +set -o pipefail +set -o xtrace + +readonly USERNAME="${USERNAME:-"root"}" + +install_gemini_cli() { + local username="${1}" + echo "Installing Gemini CLI..." + + if [ "${username}" = "root" ]; then + 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 + fi + + if which gemini >/dev/null 2>&1; then + echo "Gemini CLI installed successfully!" + return 0 + else + echo "ERROR: Gemini CLI installation failed!" + return 1 + fi +} + +fix_permissions() { + local username="${1}" + + if [ "${username}" = "root" ]; then + return 0 + fi + + local user_home + user_home=$(eval echo "~${username}" 2>/dev/null || echo "/home/${username}") + + mkdir -p "${user_home}/.gemini" + printf '{"general.enableAutoUpdate": false, "ui": {"autoThemeSwitching": false, "theme": "ANSI Light"}}\n' > "${user_home}/.gemini/settings.json" + chown -R "${username}:" "${user_home}/.gemini" +} + +print_nodejs_requirement() { + cat </dev/null || ! command -v npm >/dev/null; then + print_nodejs_requirement +fi + +install_gemini_cli "${USERNAME}" || exit 1 + +fix_permissions "${USERNAME}" + +echo "Done!" \ No newline at end of file diff --git a/src/custom-workbench-jupyter-template/.devcontainer.json b/src/custom-workbench-jupyter-template/.devcontainer.json index 3384b3e2..49a7b6b4 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@sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6": { + "version": "24.11.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 "cloud": "${templateOption:cloud}", diff --git a/src/nemo_jupyter/.devcontainer.json b/src/nemo_jupyter/.devcontainer.json index f94ab953..20eed5d5 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@sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6": { + "version": "24.11.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 "cloud": "${templateOption:cloud}", diff --git a/src/r-analysis/.devcontainer.json b/src/r-analysis/.devcontainer.json index 00fea5a8..dba17eab 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@sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6": { + "version": "24.11.0" + }, + "ghcr.io/anthropics/devcontainer-features/claude-code@sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a": {}, + "./.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..45e02510 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@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@sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a": {}, + "./.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..fa16bd06 --- /dev/null +++ b/src/vscode/Dockerfile @@ -0,0 +1,19 @@ +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 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 +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..b94aeb29 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@sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6": { + "version": "24.11.0" + }, + "ghcr.io/anthropics/devcontainer-features/claude-code@sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a": {}, + "./.devcontainer/features/gemini-cli": { "username": "jupyter" }, "./.devcontainer/features/workbench-tools": { "cloud": "${templateOption:cloud}", "username": "jupyter", 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