diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 57572c7..5669e9d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -4,8 +4,9 @@ on: jobs: deploy: - if: ${{ github.ref == 'refs/heads/main' }} + if: ${{ github.ref_type == 'branch' && github.ref_name == github.event.repository.default_branch }} runs-on: ubuntu-latest + timeout-minutes: 30 permissions: contents: write pull-requests: write diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 699ba69..394ef59 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -6,11 +6,19 @@ on: pull_request: workflow_dispatch: +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: test-autogenerated: runs-on: ubuntu-latest - continue-on-error: true + timeout-minutes: 30 strategy: + fail-fast: false matrix: features: - adr-tools @@ -19,6 +27,7 @@ jobs: - codex - color - gemini-cli + - gitlab - hello - imagemagick - mc @@ -32,6 +41,16 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + + - name: Cache npm global packages + uses: actions/cache@v4 + with: + path: ~/.npm + key: npm-devcontainer-cli-${{ runner.os }} + - name: "Install latest devcontainer CLI" run: npm install -g @devcontainers/cli @@ -40,8 +59,9 @@ jobs: test-scenarios: runs-on: ubuntu-latest - continue-on-error: true + timeout-minutes: 30 strategy: + fail-fast: false matrix: features: - adr-tools @@ -50,6 +70,7 @@ jobs: - codex - color - gemini-cli + - gitlab - hello - imagemagick - mc @@ -60,6 +81,16 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + + - name: Cache npm global packages + uses: actions/cache@v4 + with: + path: ~/.npm + key: npm-devcontainer-cli-${{ runner.os }} + - name: "Install latest devcontainer CLI" run: npm install -g @devcontainers/cli @@ -68,10 +99,20 @@ jobs: test-global: runs-on: ubuntu-latest - continue-on-error: true + timeout-minutes: 30 steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + + - name: Cache npm global packages + uses: actions/cache@v4 + with: + path: ~/.npm + key: npm-devcontainer-cli-${{ runner.os }} + - name: "Install latest devcontainer CLI" run: npm install -g @devcontainers/cli diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yaml similarity index 66% rename from .github/workflows/validate.yml rename to .github/workflows/validate.yaml index 863418e..46c710f 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yaml @@ -3,9 +3,17 @@ on: workflow_dispatch: pull_request: +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: validate: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@v4 diff --git a/CLAUDE.md b/CLAUDE.md index 4422bb3..ad192cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,6 +34,7 @@ This is Tobias Maier's Dev Container Features collection - a repository for crea - `claude-code` - Claude Code CLI for AI-powered development assistance - `codex` - OpenAI Codex CLI for local AI-powered coding assistance - `gemini-cli` - Google Gemini CLI for AI-powered development assistance +- `gitlab` - GitLab CLI and workflow tools - `imagemagick` - ImageMagick image processing tools - `mc` - MinIO Client for object storage - `mcp-language-server` - MCP Language Server for semantic code navigation diff --git a/README.md b/README.md index 5a9d881..d829130 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A collection of custom [Dev Container Features](https://containers.dev/implement | [claude-code](https://github.com/tmaier/devcontainer-features/tree/main/src/claude-code) | Claude Code CLI for AI-powered development assistance | `ghcr.io/tmaier/devcontainer-features/claude-code` | | [codex](https://github.com/tmaier/devcontainer-features/tree/main/src/codex) | OpenAI Codex CLI for local AI-powered coding assistance | `ghcr.io/tmaier/devcontainer-features/codex` | | [gemini-cli](https://github.com/tmaier/devcontainer-features/tree/main/src/gemini-cli) | Google Gemini CLI for AI-powered development assistance | `ghcr.io/tmaier/devcontainer-features/gemini-cli` | +| [gitlab](https://github.com/tmaier/devcontainer-features/tree/main/src/gitlab) | GitLab CLI and workflow extension for GitLab integration | `ghcr.io/tmaier/devcontainer-features/gitlab` | | [imagemagick](https://github.com/tmaier/devcontainer-features/tree/main/src/imagemagick) | ImageMagick image processing library with PDF support | `ghcr.io/tmaier/devcontainer-features/imagemagick` | | [mc](https://github.com/tmaier/devcontainer-features/tree/main/src/mc) | MinIO Client for object storage operations | `ghcr.io/tmaier/devcontainer-features/mc` | | [mcp-language-server](https://github.com/tmaier/devcontainer-features/tree/main/src/mcp-language-server) | MCP Language Server for semantic code navigation | `ghcr.io/tmaier/devcontainer-features/mcp-language-server` | diff --git a/src/adr-tools/install.sh b/src/adr-tools/install.sh index 89ae2fa..90f91f7 100755 --- a/src/adr-tools/install.sh +++ b/src/adr-tools/install.sh @@ -1,5 +1,6 @@ #!/bin/sh set -e +export DEBIAN_FRONTEND=noninteractive # Update package list and install wget and ca-certificates if not available if ! command -v wget >/dev/null 2>&1; then diff --git a/src/chrome/NOTES.md b/src/chrome/NOTES.md index 351eeb4..61b4fec 100644 --- a/src/chrome/NOTES.md +++ b/src/chrome/NOTES.md @@ -2,19 +2,12 @@ Running Chrome in a container environment can be challenging due to sandbox, display, and security constraints. This feature installs: -1. Google Chrome stable version +1. Google Chrome (stable, beta, or dev channel) 2. A specialized wrapper script at `/usr/local/bin/chrome` that configures Chrome to work properly in containers 3. The necessary dependencies for Chrome to run in a container +4. `xdotool` for X11 automation (simulating keyboard/mouse input, managing windows) +5. `wmctrl` for X Window manager control (listing, moving, resizing windows) -## Required Configuration - -**Important**: Add the required run argument to your `devcontainer.json`: - -```json -"runArgs": ["--add-host=host.docker.internal:host-gateway"] -``` - -> The `--add-host=host.docker.internal:host-gateway` run argument is **required** for the Chrome wrapper to properly connect to the host machine for display forwarding and X11 connections. ## Using the Chrome Wrapper The key component is the Chrome wrapper script at `/usr/local/bin/chrome`. **Always use this wrapper** instead of calling `google-chrome-stable` directly. @@ -23,16 +16,16 @@ The key component is the Chrome wrapper script at `/usr/local/bin/chrome`. **Alw **Cline**: Automatically configured via VS Code settings. -**Playwright**: +**Puppeteer**: Auto-discovered via the `PUPPETEER_EXECUTABLE_PATH` environment variable set by this feature. No configuration needed: ```javascript -const browser = await playwright.chromium.launch({ - executablePath: '/usr/local/bin/chrome', -}) +const browser = await puppeteer.launch() ``` -**Puppeteer**: +**Karma / Angular CLI**: Auto-discovered via the `CHROME_BIN` environment variable. No configuration needed. + +**Playwright**: Does not support auto-discovery via environment variables. You must pass `executablePath` explicitly: ```javascript -const browser = await puppeteer.launch({ +const browser = await playwright.chromium.launch({ executablePath: '/usr/local/bin/chrome', }) ``` @@ -55,17 +48,257 @@ chrome --headless=new --screenshot=/tmp/screenshot.png https://example.com chrome --headless=new --print-to-pdf=/tmp/output.pdf https://example.com ``` +## Shared Memory for Heavy Workloads + +Docker containers default to 64MB of shared memory (`/dev/shm`), which can cause Chrome to crash or produce blank screenshots under heavy workloads (many tabs, large pages, parallel tests). Increase the shared memory size via `runArgs` in your `devcontainer.json`: + +```json +{ + "runArgs": ["--shm-size=2g"] +} +``` + +Alternatively, the wrapper script already passes `--disable-dev-shm-usage` to Chrome, which moves shared memory to `/tmp`. This works for most cases but may be slower under extreme load. + +## Chrome Release Channels + +Select which Chrome release channel to install using the `channel` option: + +```json +{ + "features": { + "ghcr.io/tmaier/devcontainer-features/chrome": { + "channel": "beta" + } + } +} +``` + +Available channels: `stable` (default), `beta`, `dev`. + +## Display Modes + +The `displayMode` option controls how Chrome handles display output. Three modes are available: + +### headless (default) + +No display server is installed. Chrome runs with `--headless=new` when no DISPLAY is set. Best for CI/CD, automated testing, and headless scraping. + +```json +{ + "features": { + "ghcr.io/tmaier/devcontainer-features/chrome": {} + } +} +``` + +### xvfb (Virtual Framebuffer) + +Installs Xvfb and auto-starts a virtual display on `:99` when no DISPLAY is set. Required for extensions that need a display server, and for automated testing that requires rendering. + +```json +{ + "features": { + "ghcr.io/tmaier/devcontainer-features/chrome": { + "displayMode": "xvfb", + "screenResolution": "1920x1080x24" + } + } +} +``` + +> **Note**: The Xvfb display is fixed at `:99`. If you need a different display number, set the `DISPLAY` environment variable before calling `chrome` — the wrapper will use it instead of starting its own Xvfb instance. + +### vnc (Desktop-Lite Integration) + +Delegates to the `ghcr.io/devcontainers/features/desktop-lite` feature for a full desktop environment with VNC access. Chrome runs in GUI mode on the desktop-lite display. View Chrome via: +- **noVNC** (browser): `http://localhost:6080` +- **VNC client**: port `5901` + +```json +{ + "features": { + "ghcr.io/devcontainers/features/desktop-lite:1": {}, + "ghcr.io/tmaier/devcontainer-features/chrome": { + "displayMode": "vnc" + } + } +} +``` + +> **Note**: VNC mode requires `desktop-lite` to be included in your devcontainer.json features. VNC password is configured via desktop-lite's `password` option (default: `vscode`). + +> **Important**: `desktop-lite` defaults to 16-bit color depth (`1440x768x16`). noVNC cursor rendering requires 24-bit depth. Override the resolution to use 24-bit: +> ```json +> "ghcr.io/devcontainers/features/desktop-lite:1": { +> "resolution": "1920x1080x24" +> } +> ``` + +### VNC Clipboard Sharing + +Clipboard copy/paste between the host and Chrome inside a VNC session is enabled automatically when `displayMode=vnc`. The clipboard bridge uses three components: + +- **vncconfig**: Bridges the VNC protocol clipboard with the X11 cutbuffer +- **autocutsel**: Syncs the X11 CLIPBOARD and PRIMARY selections with the cutbuffer +- **xclip**: Provides command-line clipboard access + +Together, these create a full clipboard pipeline: + +``` +Host clipboard ↔ VNC protocol ↔ vncconfig ↔ X11 cutbuffer ↔ autocutsel ↔ X11 CLIPBOARD ↔ Chrome +``` + +#### Using clipboard with noVNC (browser) + +1. Open the noVNC sidebar by clicking the arrow on the left edge of the screen +2. Open the **Clipboard** panel +3. Paste text into the clipboard panel — it becomes available to Chrome via Ctrl+V +4. Text copied in Chrome via Ctrl+C appears in the clipboard panel for copying to the host + +#### Using clipboard with a native VNC client + +Clipboard sharing is transparent with native VNC clients (e.g., TigerVNC Viewer, RealVNC). Ctrl+C/Ctrl+V work between the host and Chrome. + +#### Command-line clipboard + +`xclip` is installed for scripting clipboard access: + +```bash +# Copy to clipboard +echo "text" | xclip -selection clipboard + +# Paste from clipboard +xclip -selection clipboard -o +``` + +## Extension Management + +Chrome extensions can be pre-installed via managed enterprise policies using the `extensions` option. Extensions are configured through Chrome's `ExtensionSettings` policy, which handles installation mode, toolbar pinning, and incognito access. + +The `extensions` option takes a JSON object where keys are Chrome Web Store extension IDs and values are settings objects. An empty object `{}` applies the hardcoded defaults. + +### Default Settings + +| Key | Values | Default | +|-----|--------|---------| +| `mode` | `force_installed`, `normal_installed` | `force_installed` | +| `pin` | `true`, `false` | `true` | +| `incognito` | `force_allowed`, `allowed`, `not_allowed` | `allowed` | + +### Basic Usage + +Install one or more extensions with default settings: + +```json +{ + "features": { + "ghcr.io/tmaier/devcontainer-features/chrome": { + "extensions": "{\"fcoeoabgfenejglbffodgkkbkcdhcgfn\": {}, \"cjpalhdlnbpafiamejdnhcphjbkeiagm\": {}}" + } + } +} +``` + +### Per-Extension Settings + +Override settings for individual extensions by providing values in the settings object: + +```json +{ + "features": { + "ghcr.io/tmaier/devcontainer-features/chrome": { + "extensions": "{\"fcoeoabgfenejglbffodgkkbkcdhcgfn\": {}, \"cjpalhdlnbpafiamejdnhcphjbkeiagm\": {\"mode\": \"normal_installed\", \"pin\": false, \"incognito\": \"not_allowed\"}}" + } + } +} +``` + +In this example: +- `fcoeoabgfenejglbffodgkkbkcdhcgfn` (Claude) uses the default settings (force installed, pinned, incognito allowed) +- `cjpalhdlnbpafiamejdnhcphjbkeiagm` (uBlock Origin) uses custom settings (normal install, unpinned, no incognito) + +### Technical Details + +- Extension policies are written to `/etc/opt/chrome/policies/managed/extension_settings.json` (Chrome) and `/etc/chromium/policies/managed/extension_settings.json` (Chromium) +- The wrapper script automatically detects managed policy files at runtime: if policy files exist, extensions are enabled; otherwise `--disable-extensions` is used +- Extensions are downloaded from the Chrome Web Store update URL (`https://clients2.google.com/service/update2/crx`) + +## Additional Options + +### Remote Debugging Port + +Set a default remote debugging port that's always passed to Chrome: + +```json +{ + "features": { + "ghcr.io/tmaier/devcontainer-features/chrome": { + "debuggingPort": "9222" + } + } +} +``` + +### Extra Chrome Flags + +Pass additional Chrome flags to every invocation: + +```json +{ + "features": { + "ghcr.io/tmaier/devcontainer-features/chrome": { + "chromeFlags": "--disable-web-security --allow-running-insecure-content" + } + } +} +``` + +### Fonts + +Install additional font packages for proper rendering of international text, emoji, and symbols. This prevents tofu (missing glyph boxes) in screenshots and PDFs: + +```json +{ + "features": { + "ghcr.io/tmaier/devcontainer-features/chrome": { + "fonts": true + } + } +} +``` + +Installs: `fonts-noto-cjk` (Chinese/Japanese/Korean), `fonts-noto-color-emoji` (emoji), `fonts-liberation` (metric-compatible with Arial/Times/Courier), `fonts-dejavu-core` (extended Latin/Greek/Cyrillic). + +### Locale + +Set the Chrome UI language: + +```json +{ + "features": { + "ghcr.io/tmaier/devcontainer-features/chrome": { + "locale": "de-DE" + } + } +} +``` + ## Technical Details The wrapper script handles common issues with running Chrome in containers by: -- Disabling the sandbox for container compatibility -- Setting proper display configurations +- Automatically disabling the sandbox when running as root for container compatibility +- Setting proper display configurations based on display mode - Suppressing error messages for cleaner output - Optimizing for both headless and interactive use - Providing special handling for debugging/Puppeteer modes - Logging command arguments, errors, and exit codes -- Ensuring the --no-sandbox flag is always included +- Sourcing runtime config from `/etc/chrome-wrapper/config` + +### Runtime Configuration + +The install script writes a configuration file at `/etc/chrome-wrapper/config` that the wrapper sources at runtime. This contains the Chrome binary path, display mode, screen resolution, debugging port, extra flags, and locale settings. ### Logging @@ -81,4 +314,4 @@ Log files are stored in one of the following locations (in order of preference): - `$HOME/.local/state/chrome-wrapper/chrome-wrapper.log` - `/tmp/chrome-wrapper/chrome-wrapper.log` -For more detailed documentation, see `/usr/local/share/chrome-wrapper/README.md` after installation. \ No newline at end of file +For more detailed documentation, see `/usr/local/share/chrome-wrapper/README.md` after installation. diff --git a/src/chrome/README.md b/src/chrome/README.md index 81938b7..02fd6a8 100644 --- a/src/chrome/README.md +++ b/src/chrome/README.md @@ -7,29 +7,34 @@ Installs Google Chrome with container-specific configurations and wrapper script ```json "features": { - "ghcr.io/tmaier/devcontainer-features/chrome:1": {} + "ghcr.io/tmaier/devcontainer-features/chrome:2": {} } ``` +## Options +| Options Id | Description | Type | Default Value | +|-----|-----|-----|-----| +| channel | Chrome release channel to install | string | stable | +| extensions | JSON object mapping extension IDs to settings: {"extId": {}, "extId2": {"mode": "normal_installed", "pin": false, "incognito": "not_allowed"}}. Empty object {} uses defaults: mode=force_installed, pin=true, incognito=allowed. | string | - | +| displayMode | Display server: headless (no display), xvfb (virtual framebuffer :99), vnc (uses desktop-lite feature for browser-based VNC viewing) | string | headless | +| screenResolution | Virtual display resolution for xvfb mode (WIDTHxHEIGHTxDEPTH). VNC mode uses desktop-lite's resolution. | string | 1920x1080x24 | +| debuggingPort | Default remote debugging port (e.g. 9222). Empty = not set by default. | string | - | +| chromeFlags | Space-separated additional Chrome flags always passed to the browser | string | - | +| locale | Chrome locale/language (e.g. en-US, de-DE). Empty = system default. | string | - | +| vncClipboard | Enable clipboard sharing between host and Chrome in VNC mode (installs autocutsel and xclip). Only applies when displayMode=vnc. | boolean | true | +| fonts | Install additional font packages (Noto CJK, Noto Color Emoji, Liberation, DejaVu) for proper rendering of international text, emoji, and symbols in screenshots and PDFs. | boolean | false | ## What This Feature Installs Running Chrome in a container environment can be challenging due to sandbox, display, and security constraints. This feature installs: -1. Google Chrome stable version +1. Google Chrome (stable, beta, or dev channel) 2. A specialized wrapper script at `/usr/local/bin/chrome` that configures Chrome to work properly in containers 3. The necessary dependencies for Chrome to run in a container +4. `xdotool` for X11 automation (simulating keyboard/mouse input, managing windows) +5. `wmctrl` for X Window manager control (listing, moving, resizing windows) -## Required Configuration - -**Important**: Add the required run argument to your `devcontainer.json`: - -```json -"runArgs": ["--add-host=host.docker.internal:host-gateway"] -``` - -> The `--add-host=host.docker.internal:host-gateway` run argument is **required** for the Chrome wrapper to properly connect to the host machine for display forwarding and X11 connections. ## Using the Chrome Wrapper The key component is the Chrome wrapper script at `/usr/local/bin/chrome`. **Always use this wrapper** instead of calling `google-chrome-stable` directly. @@ -38,16 +43,16 @@ The key component is the Chrome wrapper script at `/usr/local/bin/chrome`. **Alw **Cline**: Automatically configured via VS Code settings. -**Playwright**: +**Puppeteer**: Auto-discovered via the `PUPPETEER_EXECUTABLE_PATH` environment variable set by this feature. No configuration needed: ```javascript -const browser = await playwright.chromium.launch({ - executablePath: '/usr/local/bin/chrome', -}) +const browser = await puppeteer.launch() ``` -**Puppeteer**: +**Karma / Angular CLI**: Auto-discovered via the `CHROME_BIN` environment variable. No configuration needed. + +**Playwright**: Does not support auto-discovery via environment variables. You must pass `executablePath` explicitly: ```javascript -const browser = await puppeteer.launch({ +const browser = await playwright.chromium.launch({ executablePath: '/usr/local/bin/chrome', }) ``` @@ -70,17 +75,257 @@ chrome --headless=new --screenshot=/tmp/screenshot.png https://example.com chrome --headless=new --print-to-pdf=/tmp/output.pdf https://example.com ``` +## Shared Memory for Heavy Workloads + +Docker containers default to 64MB of shared memory (`/dev/shm`), which can cause Chrome to crash or produce blank screenshots under heavy workloads (many tabs, large pages, parallel tests). Increase the shared memory size via `runArgs` in your `devcontainer.json`: + +```json +{ + "runArgs": ["--shm-size=2g"] +} +``` + +Alternatively, the wrapper script already passes `--disable-dev-shm-usage` to Chrome, which moves shared memory to `/tmp`. This works for most cases but may be slower under extreme load. + +## Chrome Release Channels + +Select which Chrome release channel to install using the `channel` option: + +```json +{ + "features": { + "ghcr.io/tmaier/devcontainer-features/chrome": { + "channel": "beta" + } + } +} +``` + +Available channels: `stable` (default), `beta`, `dev`. + +## Display Modes + +The `displayMode` option controls how Chrome handles display output. Three modes are available: + +### headless (default) + +No display server is installed. Chrome runs with `--headless=new` when no DISPLAY is set. Best for CI/CD, automated testing, and headless scraping. + +```json +{ + "features": { + "ghcr.io/tmaier/devcontainer-features/chrome": {} + } +} +``` + +### xvfb (Virtual Framebuffer) + +Installs Xvfb and auto-starts a virtual display on `:99` when no DISPLAY is set. Required for extensions that need a display server, and for automated testing that requires rendering. + +```json +{ + "features": { + "ghcr.io/tmaier/devcontainer-features/chrome": { + "displayMode": "xvfb", + "screenResolution": "1920x1080x24" + } + } +} +``` + +> **Note**: The Xvfb display is fixed at `:99`. If you need a different display number, set the `DISPLAY` environment variable before calling `chrome` — the wrapper will use it instead of starting its own Xvfb instance. + +### vnc (Desktop-Lite Integration) + +Delegates to the `ghcr.io/devcontainers/features/desktop-lite` feature for a full desktop environment with VNC access. Chrome runs in GUI mode on the desktop-lite display. View Chrome via: +- **noVNC** (browser): `http://localhost:6080` +- **VNC client**: port `5901` + +```json +{ + "features": { + "ghcr.io/devcontainers/features/desktop-lite:1": {}, + "ghcr.io/tmaier/devcontainer-features/chrome": { + "displayMode": "vnc" + } + } +} +``` + +> **Note**: VNC mode requires `desktop-lite` to be included in your devcontainer.json features. VNC password is configured via desktop-lite's `password` option (default: `vscode`). + +> **Important**: `desktop-lite` defaults to 16-bit color depth (`1440x768x16`). noVNC cursor rendering requires 24-bit depth. Override the resolution to use 24-bit: +> ```json +> "ghcr.io/devcontainers/features/desktop-lite:1": { +> "resolution": "1920x1080x24" +> } +> ``` + +### VNC Clipboard Sharing + +Clipboard copy/paste between the host and Chrome inside a VNC session is enabled automatically when `displayMode=vnc`. The clipboard bridge uses three components: + +- **vncconfig**: Bridges the VNC protocol clipboard with the X11 cutbuffer +- **autocutsel**: Syncs the X11 CLIPBOARD and PRIMARY selections with the cutbuffer +- **xclip**: Provides command-line clipboard access + +Together, these create a full clipboard pipeline: + +``` +Host clipboard ↔ VNC protocol ↔ vncconfig ↔ X11 cutbuffer ↔ autocutsel ↔ X11 CLIPBOARD ↔ Chrome +``` + +#### Using clipboard with noVNC (browser) + +1. Open the noVNC sidebar by clicking the arrow on the left edge of the screen +2. Open the **Clipboard** panel +3. Paste text into the clipboard panel — it becomes available to Chrome via Ctrl+V +4. Text copied in Chrome via Ctrl+C appears in the clipboard panel for copying to the host + +#### Using clipboard with a native VNC client + +Clipboard sharing is transparent with native VNC clients (e.g., TigerVNC Viewer, RealVNC). Ctrl+C/Ctrl+V work between the host and Chrome. + +#### Command-line clipboard + +`xclip` is installed for scripting clipboard access: + +```bash +# Copy to clipboard +echo "text" | xclip -selection clipboard + +# Paste from clipboard +xclip -selection clipboard -o +``` + +## Extension Management + +Chrome extensions can be pre-installed via managed enterprise policies using the `extensions` option. Extensions are configured through Chrome's `ExtensionSettings` policy, which handles installation mode, toolbar pinning, and incognito access. + +The `extensions` option takes a JSON object where keys are Chrome Web Store extension IDs and values are settings objects. An empty object `{}` applies the hardcoded defaults. + +### Default Settings + +| Key | Values | Default | +|-----|--------|---------| +| `mode` | `force_installed`, `normal_installed` | `force_installed` | +| `pin` | `true`, `false` | `true` | +| `incognito` | `force_allowed`, `allowed`, `not_allowed` | `allowed` | + +### Basic Usage + +Install one or more extensions with default settings: + +```json +{ + "features": { + "ghcr.io/tmaier/devcontainer-features/chrome": { + "extensions": "{\"fcoeoabgfenejglbffodgkkbkcdhcgfn\": {}, \"cjpalhdlnbpafiamejdnhcphjbkeiagm\": {}}" + } + } +} +``` + +### Per-Extension Settings + +Override settings for individual extensions by providing values in the settings object: + +```json +{ + "features": { + "ghcr.io/tmaier/devcontainer-features/chrome": { + "extensions": "{\"fcoeoabgfenejglbffodgkkbkcdhcgfn\": {}, \"cjpalhdlnbpafiamejdnhcphjbkeiagm\": {\"mode\": \"normal_installed\", \"pin\": false, \"incognito\": \"not_allowed\"}}" + } + } +} +``` + +In this example: +- `fcoeoabgfenejglbffodgkkbkcdhcgfn` (Claude) uses the default settings (force installed, pinned, incognito allowed) +- `cjpalhdlnbpafiamejdnhcphjbkeiagm` (uBlock Origin) uses custom settings (normal install, unpinned, no incognito) + +### Technical Details + +- Extension policies are written to `/etc/opt/chrome/policies/managed/extension_settings.json` (Chrome) and `/etc/chromium/policies/managed/extension_settings.json` (Chromium) +- The wrapper script automatically detects managed policy files at runtime: if policy files exist, extensions are enabled; otherwise `--disable-extensions` is used +- Extensions are downloaded from the Chrome Web Store update URL (`https://clients2.google.com/service/update2/crx`) + +## Additional Options + +### Remote Debugging Port + +Set a default remote debugging port that's always passed to Chrome: + +```json +{ + "features": { + "ghcr.io/tmaier/devcontainer-features/chrome": { + "debuggingPort": "9222" + } + } +} +``` + +### Extra Chrome Flags + +Pass additional Chrome flags to every invocation: + +```json +{ + "features": { + "ghcr.io/tmaier/devcontainer-features/chrome": { + "chromeFlags": "--disable-web-security --allow-running-insecure-content" + } + } +} +``` + +### Fonts + +Install additional font packages for proper rendering of international text, emoji, and symbols. This prevents tofu (missing glyph boxes) in screenshots and PDFs: + +```json +{ + "features": { + "ghcr.io/tmaier/devcontainer-features/chrome": { + "fonts": true + } + } +} +``` + +Installs: `fonts-noto-cjk` (Chinese/Japanese/Korean), `fonts-noto-color-emoji` (emoji), `fonts-liberation` (metric-compatible with Arial/Times/Courier), `fonts-dejavu-core` (extended Latin/Greek/Cyrillic). + +### Locale + +Set the Chrome UI language: + +```json +{ + "features": { + "ghcr.io/tmaier/devcontainer-features/chrome": { + "locale": "de-DE" + } + } +} +``` + ## Technical Details The wrapper script handles common issues with running Chrome in containers by: -- Disabling the sandbox for container compatibility -- Setting proper display configurations +- Automatically disabling the sandbox when running as root for container compatibility +- Setting proper display configurations based on display mode - Suppressing error messages for cleaner output - Optimizing for both headless and interactive use - Providing special handling for debugging/Puppeteer modes - Logging command arguments, errors, and exit codes -- Ensuring the --no-sandbox flag is always included +- Sourcing runtime config from `/etc/chrome-wrapper/config` + +### Runtime Configuration + +The install script writes a configuration file at `/etc/chrome-wrapper/config` that the wrapper sources at runtime. This contains the Chrome binary path, display mode, screen resolution, debugging port, extra flags, and locale settings. ### Logging @@ -98,6 +343,7 @@ Log files are stored in one of the following locations (in order of preference): For more detailed documentation, see `/usr/local/share/chrome-wrapper/README.md` after installation. + --- _Note: This file was auto-generated from the [devcontainer-feature.json](https://github.com/tmaier/devcontainer-features/blob/main/src/chrome/devcontainer-feature.json). Add additional notes to a `NOTES.md`._ diff --git a/src/chrome/devcontainer-feature.json b/src/chrome/devcontainer-feature.json index 899a77b..85b915b 100644 --- a/src/chrome/devcontainer-feature.json +++ b/src/chrome/devcontainer-feature.json @@ -1,8 +1,61 @@ { "id": "chrome", - "version": "1.0.2", + "version": "2.3.0", "name": "Google Chrome for Containers", "description": "Installs Google Chrome with container-specific configurations and wrapper script", + "options": { + "channel": { + "type": "string", + "enum": ["stable", "beta", "dev"], + "default": "stable", + "description": "Chrome release channel to install" + }, + "extensions": { + "type": "string", + "default": "", + "description": "JSON object mapping extension IDs to settings: {\"extId\": {}, \"extId2\": {\"mode\": \"normal_installed\", \"pin\": false, \"incognito\": \"not_allowed\"}}. Empty object {} uses defaults: mode=force_installed, pin=true, incognito=allowed." + }, + "displayMode": { + "type": "string", + "enum": ["headless", "xvfb", "vnc"], + "default": "headless", + "description": "Display server: headless (no display), xvfb (virtual framebuffer :99), vnc (uses desktop-lite feature for browser-based VNC viewing)" + }, + "screenResolution": { + "type": "string", + "default": "1920x1080x24", + "description": "Virtual display resolution for xvfb mode (WIDTHxHEIGHTxDEPTH). VNC mode uses desktop-lite's resolution." + }, + "debuggingPort": { + "type": "string", + "default": "", + "description": "Default remote debugging port (e.g. 9222). Empty = not set by default." + }, + "chromeFlags": { + "type": "string", + "default": "", + "description": "Space-separated additional Chrome flags always passed to the browser" + }, + "locale": { + "type": "string", + "default": "", + "description": "Chrome locale/language (e.g. en-US, de-DE). Empty = system default." + }, + "vncClipboard": { + "type": "boolean", + "default": true, + "description": "Enable clipboard sharing between host and Chrome in VNC mode (installs autocutsel and xclip). Only applies when displayMode=vnc." + }, + "fonts": { + "type": "boolean", + "default": false, + "description": "Install additional font packages (Noto CJK, Noto Color Emoji, Liberation, DejaVu) for proper rendering of international text, emoji, and symbols in screenshots and PDFs." + } + }, + "containerEnv": { + "CHROME_BIN": "/usr/local/bin/chrome", + "PUPPETEER_EXECUTABLE_PATH": "/usr/local/bin/chrome" + }, "customizations": { "vscode": { "settings": { @@ -10,5 +63,5 @@ } } }, - "installsAfter": [] + "installsAfter": ["ghcr.io/devcontainers/features/desktop-lite"] } diff --git a/src/chrome/install.sh b/src/chrome/install.sh index 4b62000..0cb9921 100755 --- a/src/chrome/install.sh +++ b/src/chrome/install.sh @@ -1,31 +1,206 @@ #!/bin/bash set -e +export DEBIAN_FRONTEND=noninteractive + +# ============================================================================= +# Section 1: Package Installation +# ============================================================================= + +# Chrome only ships for amd64 — detect arch and exit early on unsupported platforms +ARCH=$(dpkg --print-architecture) +case "$ARCH" in + amd64) ;; + *) + echo "ERROR: Google Chrome is only available for amd64, but this system is ${ARCH}." + exit 1 + ;; +esac + +CHANNEL="${CHANNEL:-stable}" +# The apt package for the "dev" channel is named "google-chrome-unstable" +if [ "$CHANNEL" = "dev" ]; then + CHROME_PACKAGE="google-chrome-unstable" +else + CHROME_PACKAGE="google-chrome-${CHANNEL}" +fi -echo "Installing Google Chrome stable version..." +echo "Installing Google Chrome (${CHANNEL} channel)..." # Install dependencies apt-get update -apt-get install -y wget gnupg2 apt-transport-https ca-certificates dbus-x11 +apt-get install -y --no-install-recommends wget gnupg2 apt-transport-https ca-certificates dbus-x11 xdotool wmctrl jq -# Add Google Chrome repository using modern approach (apt-key is deprecated) +# Add Google Chrome repository (using modern signed-by approach) mkdir -p /etc/apt/keyrings -wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /etc/apt/keyrings/google-chrome.gpg -chmod a+r /etc/apt/keyrings/google-chrome.gpg -echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list +wget -q -O /etc/apt/keyrings/google-chrome.asc https://dl.google.com/linux/linux_signing_key.pub +echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/google-chrome.asc] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list apt-get update -# Install Google Chrome stable -apt-get install -y google-chrome-stable +# Install Google Chrome +apt-get install -y --no-install-recommends "$CHROME_PACKAGE" + +# Verify Chrome installed correctly +if ! command -v "$CHROME_PACKAGE" >/dev/null 2>&1; then + echo "ERROR: ${CHROME_PACKAGE} binary not found after installation." + exit 1 +fi +echo "Chrome version: $("$CHROME_PACKAGE" --version)" + +# Install display-mode specific packages +case "${DISPLAYMODE:-headless}" in + xvfb) + echo "Installing Xvfb for virtual display support..." + apt-get install -y --no-install-recommends xvfb + echo "Xvfb installed successfully." + ;; + vnc) + echo "Installing cursor theme for VNC display..." + apt-get install -y --no-install-recommends adwaita-icon-theme + # Configure system-wide default cursor theme + mkdir -p /usr/share/icons/default + cat > /usr/share/icons/default/index.theme << CURSOREOF +[Icon Theme] +Inherits=Adwaita +CURSOREOF + # Configure GTK3 cursor theme for Chrome + mkdir -p /etc/gtk-3.0 + cat > /etc/gtk-3.0/settings.ini << GTKEOF +[Settings] +gtk-cursor-theme-name=Adwaita +gtk-cursor-theme-size=24 +GTKEOF + if [ "${VNCCLIPBOARD:-true}" = "true" ]; then + echo "Installing VNC clipboard support packages..." + apt-get install -y --no-install-recommends autocutsel xclip + echo "VNC clipboard packages installed successfully." + fi + # Desktop-lite provides VNC infrastructure + if [ ! -f /usr/local/share/desktop-init.sh ]; then + echo "WARNING: displayMode=vnc requires the 'ghcr.io/devcontainers/features/desktop-lite' feature." + echo "Add it to your devcontainer.json features to enable VNC access." + fi + ;; +esac + +# Install optional font packages for proper rendering in screenshots/PDFs +if [ "${FONTS:-false}" = "true" ]; then + echo "Installing font packages for CJK, emoji, and Latin rendering..." + apt-get install -y --no-install-recommends fonts-noto-cjk fonts-noto-color-emoji fonts-liberation fonts-dejavu-core + echo "Font packages installed successfully." +fi + +# Clean up APT cache to reduce image size +apt-get autoremove -y && rm -rf /var/lib/apt/lists/* + +# ============================================================================= +# Section 2: Runtime Config File +# ============================================================================= + +# Validate screenResolution format when xvfb mode is selected +if [ "${DISPLAYMODE:-headless}" = "xvfb" ]; then + if ! echo "${SCREENRESOLUTION:-1920x1080x24}" | grep -qE '^[0-9]+x[0-9]+x[0-9]+$'; then + echo "ERROR: screenResolution '${SCREENRESOLUTION}' does not match expected WIDTHxHEIGHTxDEPTH format (e.g. 1920x1080x24)." + exit 1 + fi +fi + +echo "Writing runtime configuration..." +mkdir -p /etc/chrome-wrapper +cat > /etc/chrome-wrapper/config << CONFIGEOF +CHROME_BINARY="${CHROME_PACKAGE}" +DISPLAY_MODE="${DISPLAYMODE:-headless}" +SCREEN_RESOLUTION="${SCREENRESOLUTION:-1920x1080x24}" +DEBUGGING_PORT="${DEBUGGINGPORT:-}" +EXTRA_FLAGS="${CHROMEFLAGS:-}" +LOCALE="${LOCALE:-}" +VNC_CLIPBOARD="$([ "${DISPLAYMODE:-headless}" = "vnc" ] && [ "${VNCCLIPBOARD:-true}" = "true" ] && echo "true" || echo "false")" +CONFIGEOF + +echo "Runtime config written to /etc/chrome-wrapper/config" + +# ============================================================================= +# Section 3: Extension Policy Generation +# ============================================================================= + +if [ -n "${EXTENSIONS}" ]; then + echo "Configuring Chrome extension policies..." + + # The devcontainer CLI may strip double quotes from JSON option values when + # generating the install wrapper script. Detect and repair corrupted JSON. + if ! echo "$EXTENSIONS" | jq empty 2>/dev/null; then + echo "Repairing extension JSON (quotes stripped by devcontainer CLI)..." + EXTENSIONS=$(echo "$EXTENSIONS" | sed -E \ + -e 's/([{,]) *([a-zA-Z_][a-zA-Z0-9_]*) *:/\1"\2":/g' \ + -e 's/: *([a-zA-Z_][a-zA-Z0-9_]*) *([,}])/: "\1"\2/g' \ + -e 's/"(true|false|null)"/\1/g') + fi + + CHROME_UPDATE_URL="https://clients2.google.com/service/update2/crx" + + # Build ExtensionSettings policy from JSON input using jq + POLICY_JSON=$(echo "$EXTENSIONS" | jq -r --arg update_url "$CHROME_UPDATE_URL" ' + { + "ExtensionSettings": ( + to_entries | map( + { + key: .key, + value: { + "installation_mode": (.value.mode // "force_installed"), + "update_url": $update_url, + "toolbar_pin": (if (.value | has("pin")) then (if .value.pin then "force_pinned" else "unpinned" end) else "force_pinned" end), + "incognito": (.value.incognito // "allowed") + } + } + ) | from_entries + ) + } + ') + + # Validate extension ID formats + for ext_id in $(echo "$EXTENSIONS" | jq -r 'keys[]'); do + if ! echo "$ext_id" | grep -qE '^[a-z]{32}$'; then + echo "========================================================================" + echo "WARNING: Extension ID '$ext_id' does not match expected format" + echo " Expected: 32 lowercase letters (e.g. fcoeoabgfenejglbffodgkkbkcdhcgfn)" + echo " Got: '$ext_id'" + echo " This may be valid for enterprise or custom extensions." + echo "========================================================================" + fi + done + + # Write Chrome managed policy + mkdir -p /etc/opt/chrome/policies/managed + echo "$POLICY_JSON" > /etc/opt/chrome/policies/managed/extension_settings.json + echo "Chrome extension policy written to /etc/opt/chrome/policies/managed/extension_settings.json" + + # Copy to Chromium policy directory for compatibility + mkdir -p /etc/chromium/policies/managed + cp /etc/opt/chrome/policies/managed/extension_settings.json /etc/chromium/policies/managed/extension_settings.json + echo "Chromium extension policy written to /etc/chromium/policies/managed/extension_settings.json" +fi + +# ============================================================================= +# Section 4: Wrapper Script +# ============================================================================= echo "Creating Chrome wrapper script at /usr/local/bin/chrome..." -# Create wrapper script cat > /usr/local/bin/chrome << 'EOF' #!/bin/bash # Wrapper script to run Chrome in a container environment # addressing sandbox and display issues +# Source runtime config +if [ -f /etc/chrome-wrapper/config ]; then + source /etc/chrome-wrapper/config +fi + +# Defaults if config is missing +CHROME_BINARY="${CHROME_BINARY:-google-chrome-stable}" +DISPLAY_MODE="${DISPLAY_MODE:-headless}" +SCREEN_RESOLUTION="${SCREEN_RESOLUTION:-1920x1080x24}" + # Logging setup LOG_DIR="/var/log/chrome-wrapper" # Fallback to user directory if system directory is not writable @@ -44,12 +219,116 @@ log() { # Log the command invocation log "Command: chrome $*" +log "Display mode: $DISPLAY_MODE" + +# Virtual display management +XVFB_DISPLAY=":99" +XVFB_PIDFILE="/tmp/.xvfb-display99.pid" +XVFB_LOCKFILE="/tmp/.X99-lock" + +is_xvfb_running() { + if [ -f "$XVFB_PIDFILE" ]; then + local pid + pid=$(cat "$XVFB_PIDFILE" 2>/dev/null) + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + return 0 + fi + rm -f "$XVFB_PIDFILE" + fi + if [ -f "$XVFB_LOCKFILE" ]; then + local pid + pid=$(cat "$XVFB_LOCKFILE" 2>/dev/null | tr -d ' ') + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + return 0 + fi + rm -f "$XVFB_LOCKFILE" + fi + return 1 +} -# Check if DISPLAY is set -if [ -z "$DISPLAY" ]; then - echo "DISPLAY environment variable is not set. Setting to host.docker.internal:0.0" - export DISPLAY="host.docker.internal:0.0" -fi +start_xvfb() { + log "Starting Xvfb on display $XVFB_DISPLAY with resolution $SCREEN_RESOLUTION" + Xvfb "$XVFB_DISPLAY" -screen 0 "$SCREEN_RESOLUTION" -ac +extension GLX +render -noreset & + local xvfb_pid=$! + echo "$xvfb_pid" > "$XVFB_PIDFILE" + local wait_count=0 + while [ $wait_count -lt 10 ]; do + if [ -f "$XVFB_LOCKFILE" ]; then + log "Xvfb started successfully (PID: $xvfb_pid)" + return 0 + fi + sleep 0.1 + wait_count=$((wait_count + 1)) + done + if kill -0 "$xvfb_pid" 2>/dev/null; then + log "Xvfb running (PID: $xvfb_pid)" + return 0 + fi + log "WARNING: Xvfb failed to start" + rm -f "$XVFB_PIDFILE" + return 1 +} + +# Display setup based on DISPLAY_MODE +case "$DISPLAY_MODE" in + xvfb) + if [ -z "$DISPLAY" ]; then + if command -v Xvfb >/dev/null 2>&1; then + if is_xvfb_running; then + log "Reusing existing Xvfb on $XVFB_DISPLAY" + export DISPLAY="$XVFB_DISPLAY" + elif start_xvfb; then + export DISPLAY="$XVFB_DISPLAY" + else + log "Xvfb failed, falling back to headless mode" + fi + else + log "Xvfb not available, falling back to headless mode" + fi + else + log "Using existing DISPLAY=$DISPLAY" + fi + ;; + vnc) + # VNC mode: desktop-lite sets DISPLAY=:1 + if [ -n "$DISPLAY" ]; then + log "VNC mode: using existing DISPLAY=$DISPLAY (set by desktop-lite)" + # Configure cursor theme for VNC display + export XCURSOR_THEME="${XCURSOR_THEME:-Adwaita}" + export XCURSOR_SIZE="${XCURSOR_SIZE:-24}" + # Initialize root window cursor so VNC server has a cursor to broadcast + if command -v xsetroot >/dev/null 2>&1; then + xsetroot -cursor_name left_ptr 2>/dev/null || true + fi + else + log "WARNING: VNC mode but no DISPLAY set. Is desktop-lite running?" + log "Falling back to headless mode" + fi + # Start VNC clipboard daemons if clipboard support was installed + if [ "$VNC_CLIPBOARD" = "true" ] && [ -n "$DISPLAY" ]; then + # vncconfig: bridges VNC protocol clipboard with X11 cutbuffer + if command -v vncconfig >/dev/null 2>&1 && ! pgrep -x vncconfig >/dev/null 2>&1; then + vncconfig -nowin & + log "Started vncconfig for VNC clipboard support (PID: $!)" + fi + # autocutsel: syncs X11 CLIPBOARD selection with cutbuffer + if command -v autocutsel >/dev/null 2>&1; then + if ! pgrep -f "autocutsel.*CLIPBOARD" >/dev/null 2>&1; then + autocutsel -selection CLIPBOARD -fork + log "Started autocutsel for CLIPBOARD selection sync" + fi + if ! pgrep -f "autocutsel.*PRIMARY" >/dev/null 2>&1; then + autocutsel -selection PRIMARY -fork + log "Started autocutsel for PRIMARY selection sync" + fi + fi + fi + ;; + headless|*) + # Headless mode: no display needed + log "Headless mode: skipping display setup" + ;; +esac # Create empty .Xauthority file if it doesn't exist if [ ! -f ~/.Xauthority ]; then @@ -59,30 +338,59 @@ fi # Disable DBus for Chrome to reduce error messages export DBUS_SESSION_BUS_ADDRESS="disabled:" -# Check if --no-sandbox is already in the arguments -if [[ "$*" != *"--no-sandbox"* ]]; then - log "Adding --no-sandbox flag as it was not provided" +# Add --no-sandbox only when running as root (uid 0). +# Chrome's sandbox works for non-root users; the flag triggers a warning bar. +if [ "$(id -u)" -eq 0 ] && [[ "$*" != *"--no-sandbox"* ]]; then + log "Running as root (uid 0), adding --no-sandbox flag" set -- --no-sandbox "$@" fi +# Add locale flag if configured and not already in args +if [ -n "$LOCALE" ] && [[ "$*" != *"--lang="* ]]; then + set -- --lang="$LOCALE" "$@" +fi + +# Add debugging port if configured and not already in args +if [ -n "$DEBUGGING_PORT" ] && [[ "$*" != *"--remote-debugging-port"* ]]; then + set -- --remote-debugging-port="$DEBUGGING_PORT" "$@" +fi + +# Add extra flags from config +if [ -n "$EXTRA_FLAGS" ]; then + # shellcheck disable=SC2086 + set -- $EXTRA_FLAGS "$@" +fi + +# Detect if a real desktop environment is available (e.g., desktop-lite VNC) +has_real_desktop() { + # If DISPLAY is the Xvfb virtual display we manage, it's not a real desktop + if [ "$DISPLAY" = "$XVFB_DISPLAY" ]; then + return 1 + fi + # Check if a display is set and accessible + if [ -n "$DISPLAY" ] && command -v xdpyinfo >/dev/null 2>&1 && xdpyinfo >/dev/null 2>&1; then + return 0 + fi + return 1 +} + # Check if this is being called from browser_action if [[ "$*" == *"--remote-debugging-port"* ]]; then # Running in Puppeteer/browser_action mode - keep remote debugging output visible stderr_log=$(mktemp) - # Run Chrome and capture stderr - google-chrome-stable \ - --no-sandbox \ + # Run Chrome and capture stderr to a temp file for reliable exit code + "$CHROME_BINARY" \ --disable-dev-shm-usage \ --disable-gpu \ - --disable-setuid-sandbox \ --disable-features=VizDisplayCompositor \ - "$@" 2> >(tee "$stderr_log" >&2) + "$@" 2>"$stderr_log" exit_code=$? - # Log any errors + # Replay stderr so callers can see it, then log if [ -s "$stderr_log" ]; then + cat "$stderr_log" >&2 log "STDERR: $(cat "$stderr_log")" fi @@ -99,30 +407,66 @@ else # Redirect stderr to a temporary file local_stderr_file=$(mktemp) - # Log the final command with all flags - log "Final command: google-chrome-stable --no-sandbox --disable-dev-shm-usage --disable-gpu --disable-software-rasterizer --disable-setuid-sandbox --no-first-run --no-default-browser-check --no-zygote --disable-features=VizDisplayCompositor --disable-extensions --disable-background-networking --disable-sync --disable-translate --hide-scrollbars --metrics-recording-only --mute-audio --disable-dbus --headless=$([[ "$*" == *--headless* ]] || echo "new") $*" + # Determine extensions flag: disable extensions unless managed policy files exist + EXTENSIONS_FLAG="--disable-extensions" + BACKGROUND_NETWORKING_FLAG="--disable-background-networking" + if [ -d /etc/opt/chrome/policies/managed ] && ls /etc/opt/chrome/policies/managed/*.json >/dev/null 2>&1; then + EXTENSIONS_FLAG="" + BACKGROUND_NETWORKING_FLAG="" + log "Extension policies detected, extensions and background networking enabled" + fi + + # Determine if user explicitly passed a --headless variant + USER_WANTS_HEADLESS=false + HEADLESS_FLAG="--headless=new" + if [[ "$*" == *"--headless"* ]]; then + USER_WANTS_HEADLESS=true + # User already passed their own --headless flag, don't add another + HEADLESS_FLAG="" + fi - # Run Chrome with all the flags, redirect stderr to our temp file - google-chrome-stable \ - --no-sandbox \ - --disable-dev-shm-usage \ - --disable-gpu \ - --disable-software-rasterizer \ - --disable-setuid-sandbox \ - --no-first-run \ - --no-default-browser-check \ - --no-zygote \ - --disable-features=VizDisplayCompositor \ - --disable-extensions \ - --disable-background-networking \ - --disable-sync \ - --disable-translate \ - --hide-scrollbars \ - --metrics-recording-only \ - --mute-audio \ - --disable-dbus \ - --headless="$([[ "$*" == *--headless* ]] || echo "new")" \ - "$@" 2>"$local_stderr_file" + # Choose GUI or headless mode based on desktop environment availability and display mode + if [ "$USER_WANTS_HEADLESS" = false ] && { [ "$DISPLAY_MODE" = "vnc" ] || [ "$DISPLAY_MODE" = "xvfb" ]; } && has_real_desktop; then + log "Real desktop detected (DISPLAY=$DISPLAY), launching in GUI mode" + log "Final command: $CHROME_BINARY --disable-dev-shm-usage --no-first-run --no-default-browser-check --disable-features=VizDisplayCompositor ${EXTENSIONS_FLAG} ${BACKGROUND_NETWORKING_FLAG} --disable-sync --disable-translate --mute-audio --disable-dbus $*" + + # GUI mode: launch Chrome with a visible window on the real desktop + "$CHROME_BINARY" \ + --disable-dev-shm-usage \ + --no-first-run \ + --no-default-browser-check \ + --disable-features=VizDisplayCompositor \ + ${EXTENSIONS_FLAG:+$EXTENSIONS_FLAG} \ + ${BACKGROUND_NETWORKING_FLAG:+$BACKGROUND_NETWORKING_FLAG} \ + --disable-sync \ + --disable-translate \ + --mute-audio \ + --disable-dbus \ + "$@" 2>"$local_stderr_file" + else + log "Launching in headless mode" + log "Final command: $CHROME_BINARY --disable-dev-shm-usage --disable-gpu --disable-software-rasterizer --no-first-run --no-default-browser-check --no-zygote --disable-features=VizDisplayCompositor ${EXTENSIONS_FLAG} ${BACKGROUND_NETWORKING_FLAG} --disable-sync --disable-translate --hide-scrollbars --metrics-recording-only --mute-audio --disable-dbus ${HEADLESS_FLAG} $*" + + # Headless mode: no visible window + "$CHROME_BINARY" \ + --disable-dev-shm-usage \ + --disable-gpu \ + --disable-software-rasterizer \ + --no-first-run \ + --no-default-browser-check \ + --no-zygote \ + --disable-features=VizDisplayCompositor \ + ${EXTENSIONS_FLAG:+$EXTENSIONS_FLAG} \ + ${BACKGROUND_NETWORKING_FLAG:+$BACKGROUND_NETWORKING_FLAG} \ + --disable-sync \ + --disable-translate \ + --hide-scrollbars \ + --metrics-recording-only \ + --mute-audio \ + --disable-dbus \ + ${HEADLESS_FLAG:+$HEADLESS_FLAG} \ + "$@" 2>"$local_stderr_file" + fi # Get the exit code local_exit_code=$? @@ -146,7 +490,10 @@ EOF # Make the wrapper script executable chmod +x /usr/local/bin/chrome -# Create documentation +# ============================================================================= +# Section 5: Embedded Documentation +# ============================================================================= + mkdir -p /usr/local/share/chrome-wrapper cat > /usr/local/share/chrome-wrapper/README.md << 'EOF' # Chrome in Dev Container @@ -168,24 +515,39 @@ chrome --headless=new --screenshot=/tmp/screenshot.png https://example.com chrome --headless=new --print-to-pdf=/tmp/output.pdf https://example.com ``` -### Interactive Mode (requires X server on host) +### Interactive Mode -Make sure you have an X server running on your host machine and that your DISPLAY variable is properly set. +#### Xvfb (Virtual Framebuffer) +With `displayMode: "xvfb"`, Chrome runs on a virtual display `:99`. Useful for +automated testing that requires a display server. -```bash -# Open Chrome browser (GUI mode) -chrome https://example.com -``` +#### VNC (Browser-Based Viewing) +With `displayMode: "vnc"` and the `desktop-lite` feature, Chrome runs in a +full desktop environment accessible via: +- noVNC (browser): http://localhost:6080 +- VNC client: port 5901 + +Clipboard sharing between host and Chrome is enabled automatically via +vncconfig and autocutsel. In noVNC, use the clipboard panel in the sidebar. ## Technical Details The wrapper script applies the following configurations to Chrome: -- Disables sandbox for container compatibility -- Optimizes for headless operation -- Suppresses common error messages related to dbus and display -- Sets up proper display configuration +- Automatically adds --no-sandbox when running as root (uid 0) for container compatibility +- Sources runtime config from /etc/chrome-wrapper/config +- Handles display setup based on configured display mode +- Supports channel selection (stable, beta, dev) - Logs command arguments, errors, and exit codes to a log file -- Ensures the --no-sandbox flag is always included + +### Configuration + +Runtime configuration is stored at `/etc/chrome-wrapper/config` and includes: +- CHROME_BINARY: Which Chrome binary to use +- DISPLAY_MODE: headless, xvfb, or vnc +- SCREEN_RESOLUTION: Virtual display resolution +- DEBUGGING_PORT: Remote debugging port +- EXTRA_FLAGS: Additional Chrome flags +- LOCALE: Chrome UI locale ### Logging @@ -200,7 +562,5 @@ Log file location: - Last resort: `/tmp/chrome-wrapper/chrome-wrapper.log` EOF -echo "Google Chrome installation and configuration complete." -echo "NOTE: To use this feature properly, ensure your devcontainer.json includes:" -echo " \"runArgs\": [\"--add-host=host.docker.internal:host-gateway\"]" -echo " This is required for host.docker.internal to resolve correctly." +echo "Google Chrome (${CHANNEL}) installation and configuration complete." +echo "Display mode: ${DISPLAYMODE:-headless}" diff --git a/src/claude-code/NOTES.md b/src/claude-code/NOTES.md index 1926254..2b682e4 100644 --- a/src/claude-code/NOTES.md +++ b/src/claude-code/NOTES.md @@ -1,14 +1,27 @@ # Claude Code Feature -This feature installs [Claude Code](https://www.anthropic.com/claude-code), Anthropic's official CLI for AI-powered development assistance. +This feature installs [Claude Code](https://www.anthropic.com/claude-code), Anthropic's official CLI for AI-powered development assistance, using the [native installer](https://code.claude.com/docs/en/setup). -Claude Code is installed using the [native installer](https://code.claude.com/docs/en/setup), which automatically keeps itself up to date. +No Node.js dependency is required. The native installer downloads a standalone binary. + +## Version Option + +By default, the `latest` release channel is installed. You can also specify: + +- `"stable"` — a release channel that is typically about one week behind latest, skipping releases with major regressions +- A specific semver version (e.g. `"1.0.58"`) + +The channel chosen at install time becomes the default for auto-updates. + +## Auto-Updates + +The native binary automatically updates in the background. Update checks are performed on startup and periodically while running. To disable auto-updates, set the `DISABLE_AUTOUPDATER=1` environment variable. ## Manual config for `devcontainer.json` Mount the local `~/.claude/` directory into the Dev Container. Add the following mount to the `devcontainer.json` file. -Replace `vscode` with the actual name of your user (see `remoteUser` property) +Replace `vscode` with the actual name of your user (see `remoteUser` property). ```json "mounts": [ diff --git a/src/claude-code/README.md b/src/claude-code/README.md index 8748a4f..7559aa9 100644 --- a/src/claude-code/README.md +++ b/src/claude-code/README.md @@ -7,7 +7,7 @@ Installs Claude Code CLI for AI-powered development assistance ```json "features": { - "ghcr.io/tmaier/devcontainer-features/claude-code:1": {} + "ghcr.io/tmaier/devcontainer-features/claude-code:2": {} } ``` @@ -15,7 +15,7 @@ Installs Claude Code CLI for AI-powered development assistance | Options Id | Description | Type | Default Value | |-----|-----|-----|-----| - +| version | Version to install. Use 'latest', 'stable', or a specific semver (e.g. '1.0.58'). | string | latest | ## Customizations @@ -25,17 +25,28 @@ Installs Claude Code CLI for AI-powered development assistance # Claude Code Feature -This feature installs [Claude Code](https://www.anthropic.com/claude-code), Anthropic's official CLI for AI-powered development assistance. +This feature installs [Claude Code](https://www.anthropic.com/claude-code), Anthropic's official CLI for AI-powered development assistance, using the [native installer](https://code.claude.com/docs/en/setup). + +No Node.js dependency is required. The native installer downloads a standalone binary. + +## Version Option + +By default, the `latest` release channel is installed. You can also specify: + +- `"stable"` — a release channel that is typically about one week behind latest, skipping releases with major regressions +- A specific semver version (e.g. `"1.0.58"`) + +The channel chosen at install time becomes the default for auto-updates. -## Requirements +## Auto-Updates -- Node.js +The native binary automatically updates in the background. Update checks are performed on startup and periodically while running. To disable auto-updates, set the `DISABLE_AUTOUPDATER=1` environment variable. ## Manual config for `devcontainer.json` -Mount the local `~/.claude/` directory into the Dev Contaier. +Mount the local `~/.claude/` directory into the Dev Container. Add the following mount to the `devcontainer.json` file. -Replace `vscode` with the actual name of your user (see `remoteUser` property) +Replace `vscode` with the actual name of your user (see `remoteUser` property). ```json "mounts": [ @@ -51,7 +62,7 @@ Replace `vscode` with the actual name of your user (see `remoteUser` property) After installation, run `claude` in your project directory to get started. -For detailed documentation, see the [Claude Code documentation](https://docs.anthropic.com/en/docs/claude-code/overview). +For detailed documentation, see the [Claude Code documentation](https://code.claude.com/docs/en/setup). --- diff --git a/src/claude-code/devcontainer-feature.json b/src/claude-code/devcontainer-feature.json index 8993c0f..91a7b63 100644 --- a/src/claude-code/devcontainer-feature.json +++ b/src/claude-code/devcontainer-feature.json @@ -3,7 +3,13 @@ "version": "2.0.0", "name": "Claude Code", "description": "Installs Claude Code CLI for AI-powered development assistance", - "options": {}, + "options": { + "version": { + "type": "string", + "default": "latest", + "description": "Version to install. Use 'latest', 'stable', or a specific semver (e.g. '1.0.58')." + } + }, "customizations": { "vscode": { "extensions": [ diff --git a/src/claude-code/install.sh b/src/claude-code/install.sh index 75e4969..a7e371b 100755 --- a/src/claude-code/install.sh +++ b/src/claude-code/install.sh @@ -1,32 +1,48 @@ #!/bin/bash set -e +export DEBIAN_FRONTEND=noninteractive echo "Installing Claude Code..." -# Ensure curl is available +# Ensure curl and other dependencies are available if ! command -v curl &> /dev/null; then echo "curl not found, installing..." if command -v apt-get &> /dev/null; then - apt-get update && apt-get install -y curl + apt-get update -y && apt-get install -y --no-install-recommends curl ca-certificates + rm -rf /var/lib/apt/lists/* elif command -v apk &> /dev/null; then - apk add --no-cache curl + apk add --no-cache curl ca-certificates elif command -v yum &> /dev/null; then - yum install -y curl + yum install -y curl ca-certificates else echo "Error: curl is required but could not be installed automatically." exit 1 fi fi +# Ensure tar and gzip are available (needed by the installer) +if ! command -v tar &> /dev/null || ! command -v gzip &> /dev/null; then + echo "Installing archive utilities..." + apt-get update -y + apt-get install -y --no-install-recommends tar gzip + rm -rf /var/lib/apt/lists/* +fi + +# Determine version argument for the installer +INSTALL_ARGS="" +if [ -n "$VERSION" ] && [ "$VERSION" != "latest" ]; then + INSTALL_ARGS="$VERSION" +fi + # Install Claude Code using the native installer # See https://code.claude.com/docs/en/setup if [ -n "$_REMOTE_USER" ] && [ "$_REMOTE_USER" != "root" ]; then echo "Installing Claude Code for user: $_REMOTE_USER" - su "$_REMOTE_USER" -c "curl -fsSL https://claude.ai/install.sh | bash" + su "$_REMOTE_USER" -c "curl -fsSL https://claude.ai/install.sh | bash -s $INSTALL_ARGS" INSTALL_HOME="$_REMOTE_USER_HOME" else echo "Installing Claude Code" - curl -fsSL https://claude.ai/install.sh | bash + curl -fsSL https://claude.ai/install.sh | bash -s $INSTALL_ARGS INSTALL_HOME="${HOME:-/root}" fi diff --git a/src/codex/install.sh b/src/codex/install.sh index d0a8961..94940af 100755 --- a/src/codex/install.sh +++ b/src/codex/install.sh @@ -1,15 +1,17 @@ #!/bin/bash set -e +export DEBIAN_FRONTEND=noninteractive echo "Installing Codex CLI..." -# Check if Node.js is available, install if missing +# Install Node.js if not available if ! command -v node &> /dev/null; then echo "Node.js not found. Installing Node.js 22.x..." - apt-get update + apt-get update -y apt-get install -y --no-install-recommends curl ca-certificates curl -fsSL https://deb.nodesource.com/setup_22.x | bash - apt-get install -y --no-install-recommends nodejs + rm -rf /var/lib/apt/lists/* fi # Check Node.js version (requires 18+) @@ -27,8 +29,7 @@ if ! command -v npm &> /dev/null; then exit 1 fi -# Install Codex CLI globally as root -# Note: Always install as root since npm's global dir requires root permissions +# Install Codex CLI globally (runs as root during container build) echo "Installing Codex CLI globally..." npm install -g @openai/codex diff --git a/src/gemini-cli/install.sh b/src/gemini-cli/install.sh index c864f78..edab4c4 100755 --- a/src/gemini-cli/install.sh +++ b/src/gemini-cli/install.sh @@ -1,15 +1,17 @@ #!/bin/bash set -e +export DEBIAN_FRONTEND=noninteractive echo "Installing Gemini CLI..." -# Check if Node.js is available, install if missing +# Install Node.js if not available if ! command -v node &> /dev/null; then echo "Node.js not found. Installing Node.js 22.x..." - apt-get update + apt-get update -y apt-get install -y --no-install-recommends curl ca-certificates curl -fsSL https://deb.nodesource.com/setup_22.x | bash - apt-get install -y --no-install-recommends nodejs + rm -rf /var/lib/apt/lists/* fi # Check Node.js version (requires 18+) @@ -27,8 +29,7 @@ if ! command -v npm &> /dev/null; then exit 1 fi -# Install Gemini CLI globally as root -# Note: Always install as root since npm's global dir requires root permissions +# Install Gemini CLI globally (runs as root during container build) echo "Installing Gemini CLI globally..." npm install -g @google/gemini-cli diff --git a/src/gitlab/NOTES.md b/src/gitlab/NOTES.md new file mode 100644 index 0000000..e84db87 --- /dev/null +++ b/src/gitlab/NOTES.md @@ -0,0 +1,40 @@ +## What This Feature Installs + +1. **glab CLI** - The official GitLab command-line tool (latest version) +2. **GitLab Workflow VS Code extension** - GitLab integration for VS Code (`GitLab.gitlab-workflow`) + +## Authentication + +After the container starts, authenticate with your GitLab instance: + +```bash +# Interactive login (gitlab.com) +glab auth login + +# Login to a self-hosted instance +glab auth login --hostname gitlab.example.com +``` + +## Basic Usage + +```bash +# Clone a repository +glab repo clone owner/repo + +# Create a merge request +glab mr create --fill + +# List open issues +glab issue list + +# View CI/CD pipeline status +glab ci status + +# Browse the repository in the browser +glab repo view --web +``` + +## Further Reading + +- [glab CLI documentation](https://gitlab.com/gitlab-org/cli/-/blob/main/README.md) +- [GitLab Workflow VS Code extension](https://marketplace.visualstudio.com/items?itemName=GitLab.gitlab-workflow) diff --git a/src/gitlab/README.md b/src/gitlab/README.md new file mode 100644 index 0000000..0390f37 --- /dev/null +++ b/src/gitlab/README.md @@ -0,0 +1,70 @@ + +# GitLab CLI & Workflow (gitlab) + +Installs glab CLI and GitLab Workflow VS Code extension for GitLab integration + +## Example Usage + +```json +"features": { + "ghcr.io/tmaier/devcontainer-features/gitlab:1": {} +} +``` + +## Options + +| Options Id | Description | Type | Default Value | +|-----|-----|-----|-----| + + +## Customizations + +### VS Code Extensions + +- `GitLab.gitlab-workflow` + +## What This Feature Installs + +1. **glab CLI** - The official GitLab command-line tool (latest version) +2. **GitLab Workflow VS Code extension** - GitLab integration for VS Code (`GitLab.gitlab-workflow`) + +## Authentication + +After the container starts, authenticate with your GitLab instance: + +```bash +# Interactive login (gitlab.com) +glab auth login + +# Login to a self-hosted instance +glab auth login --hostname gitlab.example.com +``` + +## Basic Usage + +```bash +# Clone a repository +glab repo clone owner/repo + +# Create a merge request +glab mr create --fill + +# List open issues +glab issue list + +# View CI/CD pipeline status +glab ci status + +# Browse the repository in the browser +glab repo view --web +``` + +## Further Reading + +- [glab CLI documentation](https://gitlab.com/gitlab-org/cli/-/blob/main/README.md) +- [GitLab Workflow VS Code extension](https://marketplace.visualstudio.com/items?itemName=GitLab.gitlab-workflow) + + +--- + +_Note: This file was auto-generated from the [devcontainer-feature.json](https://github.com/tmaier/devcontainer-features/blob/main/src/gitlab/devcontainer-feature.json). Add additional notes to a `NOTES.md`._ diff --git a/src/gitlab/devcontainer-feature.json b/src/gitlab/devcontainer-feature.json new file mode 100644 index 0000000..307aea5 --- /dev/null +++ b/src/gitlab/devcontainer-feature.json @@ -0,0 +1,15 @@ +{ + "id": "gitlab", + "version": "1.0.0", + "name": "GitLab CLI & Workflow", + "description": "Installs glab CLI and GitLab Workflow VS Code extension for GitLab integration", + "options": {}, + "installsAfter": [], + "customizations": { + "vscode": { + "extensions": [ + "GitLab.gitlab-workflow" + ] + } + } +} diff --git a/src/gitlab/install.sh b/src/gitlab/install.sh new file mode 100755 index 0000000..457dd86 --- /dev/null +++ b/src/gitlab/install.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -e +export DEBIAN_FRONTEND=noninteractive + +echo "Installing glab CLI (latest version)..." + +# Ensure curl and git are available (git is a runtime dependency of glab) +apt-get update +apt-get install -y --no-install-recommends curl git ca-certificates + +# Detect architecture +ARCH=$(dpkg --print-architecture) +case "$ARCH" in + amd64|arm64) ;; + *) + echo "Unsupported architecture: $ARCH" + exit 1 + ;; +esac + +# Get latest version from GitLab API +LATEST_JSON=$(curl -fsSL "https://gitlab.com/api/v4/projects/34675721/releases/permalink/latest") +VERSION=$(echo "$LATEST_JSON" | sed -n 's/.*"tag_name"\s*:\s*"v\([^"]*\)".*/\1/p') + +if [ -z "$VERSION" ]; then + echo "Failed to determine latest glab version" + exit 1 +fi + +echo "Latest glab version: $VERSION" + +# Download and install the .deb package +DEB_URL="https://gitlab.com/gitlab-org/cli/-/releases/v${VERSION}/downloads/glab_${VERSION}_linux_${ARCH}.deb" +TMP_DEB=$(mktemp /tmp/glab_XXXXXX.deb) + +echo "Downloading glab from: $DEB_URL" +curl -fsSL -o "$TMP_DEB" "$DEB_URL" + +dpkg -i "$TMP_DEB" +rm -f "$TMP_DEB" + +# Verify installation +if command -v glab &> /dev/null; then + echo "glab CLI installed successfully: $(glab version)" +else + echo "glab CLI installation failed" + exit 1 +fi diff --git a/src/imagemagick/README.md b/src/imagemagick/README.md index d345f3a..f860690 100644 --- a/src/imagemagick/README.md +++ b/src/imagemagick/README.md @@ -21,6 +21,8 @@ Installs imagemagick This feature installs [ImageMagick](https://imagemagick.org/), a powerful image processing and manipulation library, along with Ghostscript for enhanced PDF support in container environments. +**Supported Versions**: This feature supports both ImageMagick 6 and ImageMagick 7, automatically detecting and configuring the installed version. + ## Key Configuration **PDF Processing Support**: The feature automatically configures ImageMagick to process PDF files by modifying the security policy, enabling PDF manipulation operations which are often restricted by default. diff --git a/src/mc/install.sh b/src/mc/install.sh index cc6c097..9ce8364 100755 --- a/src/mc/install.sh +++ b/src/mc/install.sh @@ -1,5 +1,6 @@ #!/bin/sh set -e +export DEBIAN_FRONTEND=noninteractive # Update package list and install wget and ca-certificates if not available if ! command -v wget >/dev/null 2>&1; then diff --git a/src/mcp-language-server/NOTES.md b/src/mcp-language-server/NOTES.md index e36336c..13fd8a0 100644 --- a/src/mcp-language-server/NOTES.md +++ b/src/mcp-language-server/NOTES.md @@ -4,7 +4,7 @@ This feature installs [MCP Language Server](https://github.com/isaacphi/mcp-lang ## Requirements -- Go +- Go (automatically installed if not available) - A language server for your programming language (installed separately) ## Supported Language Servers diff --git a/src/mcp-language-server/README.md b/src/mcp-language-server/README.md index db6034b..22681a3 100644 --- a/src/mcp-language-server/README.md +++ b/src/mcp-language-server/README.md @@ -23,7 +23,7 @@ This feature installs [MCP Language Server](https://github.com/isaacphi/mcp-lang ## Requirements -- Go +- Go (automatically installed if not available) - A language server for your programming language (installed separately) ## Supported Language Servers diff --git a/src/mcp-language-server/install.sh b/src/mcp-language-server/install.sh index 67a7855..f22fc69 100755 --- a/src/mcp-language-server/install.sh +++ b/src/mcp-language-server/install.sh @@ -1,12 +1,37 @@ #!/bin/bash set -e +export DEBIAN_FRONTEND=noninteractive echo "Installing MCP Language Server..." -# Check if Go is available +# Install Go if not available if ! command -v go &> /dev/null; then - echo "Error: Go is required but not found. Please ensure the Go feature is installed." - exit 1 + echo "Go not found. Installing Go via official tarball..." + + apt-get update -y + apt-get install -y --no-install-recommends curl ca-certificates tar + + ARCH=$(dpkg --print-architecture) + case "$ARCH" in + amd64|arm64) ;; + *) + echo "Unsupported architecture: $ARCH" + exit 1 + ;; + esac + + GO_VERSION="1.24.1" + echo "Installing Go ${GO_VERSION} for ${ARCH}..." + curl -fsSL -o /tmp/go.tar.gz "https://go.dev/dl/go${GO_VERSION}.linux-${ARCH}.tar.gz" + tar -C /usr/local -xzf /tmp/go.tar.gz + rm -f /tmp/go.tar.gz + + ln -sf /usr/local/go/bin/go /usr/local/bin/go + ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt + export PATH="/usr/local/go/bin:$PATH" + + rm -rf /var/lib/apt/lists/* + echo "Go $(go version) installed successfully." fi # Install MCP Language Server @@ -19,25 +44,17 @@ else go install github.com/isaacphi/mcp-language-server@v${MCP_VERSION} fi -# Ensure Go bin directory is in PATH -GO_BIN_PATH=$(go env GOPATH)/bin -if ! echo "$PATH" | grep -q "$GO_BIN_PATH"; then - echo "Adding Go bin directory to PATH..." - echo "export PATH=\"\$PATH:$GO_BIN_PATH\"" >> /etc/bash.bashrc - export PATH="$PATH:$GO_BIN_PATH" -fi - -# Verify installation -if ! command -v mcp-language-server &> /dev/null; then - # Check if it's available in GOPATH/bin - if [ -f "$GO_BIN_PATH/mcp-language-server" ]; then - echo "MCP Language Server installed to $GO_BIN_PATH/mcp-language-server" - # Create a symlink to make it globally available - ln -sf "$GO_BIN_PATH/mcp-language-server" /usr/local/bin/mcp-language-server - else - echo "Error: MCP Language Server installation failed" - exit 1 - fi +# Copy binary to /usr/local/bin for system-wide access. +# go install places the binary in $GOPATH/bin (typically /root/go/bin), +# which is inaccessible to non-root users. We copy instead of symlink +# because /root has mode 700 (same approach as the yek feature). +GO_BIN_PATH="$(go env GOPATH)/bin" +if [ -f "$GO_BIN_PATH/mcp-language-server" ]; then + cp "$GO_BIN_PATH/mcp-language-server" /usr/local/bin/mcp-language-server + chmod 755 /usr/local/bin/mcp-language-server +else + echo "Error: MCP Language Server installation failed" + exit 1 fi echo "MCP Language Server installed successfully!" diff --git a/src/typst/install.sh b/src/typst/install.sh index 2884106..a3c423b 100755 --- a/src/typst/install.sh +++ b/src/typst/install.sh @@ -1,5 +1,6 @@ #!/bin/sh set -e +export DEBIAN_FRONTEND=noninteractive echo "Installing Typst..." diff --git a/src/yek/install.sh b/src/yek/install.sh index 1a520cd..b86dcfe 100755 --- a/src/yek/install.sh +++ b/src/yek/install.sh @@ -1,5 +1,6 @@ #!/bin/sh set -e +export DEBIAN_FRONTEND=noninteractive # Update package list and install curl and ca-certificates if not available if ! command -v curl >/dev/null 2>&1; then @@ -10,9 +11,21 @@ fi # Install yek curl -fsSL https://azimi.me/yek.sh | bash -# The installer places the binary in ~/.local/bin which may not be in PATH. -# Copy it to /usr/local/bin to ensure it's globally available. -if [ -f "$HOME/.local/bin/yek" ] && ! command -v yek >/dev/null 2>&1; then - cp "$HOME/.local/bin/yek" /usr/local/bin/yek - chmod +x /usr/local/bin/yek +# Ensure yek is accessible for all users by copying to /usr/local/bin. +# The upstream installer places the binary in a user-specific directory +# (e.g. /root/.local/bin) which is inaccessible to non-root users. +# We copy instead of symlink because /root has mode 700. +if [ ! -f /usr/local/bin/yek ]; then + for dir in "$HOME/.yek/bin" "/root/.yek/bin" "$HOME/.local/bin" "/root/.local/bin"; do + if [ -f "$dir/yek" ]; then + cp "$dir/yek" /usr/local/bin/yek + chmod 755 /usr/local/bin/yek + break + fi + done +fi + +if ! command -v yek >/dev/null 2>&1; then + echo "Error: yek installation failed" + exit 1 fi diff --git a/src/yq/install.sh b/src/yq/install.sh index 33d5775..788d37d 100755 --- a/src/yq/install.sh +++ b/src/yq/install.sh @@ -1,5 +1,6 @@ #!/bin/sh set -e +export DEBIAN_FRONTEND=noninteractive # Update package list and install wget and ca-certificates if not available if ! command -v wget >/dev/null 2>&1; then diff --git a/test/chrome/chrome_channel_test.sh b/test/chrome/chrome_channel_test.sh new file mode 100755 index 0000000..6cb452b --- /dev/null +++ b/test/chrome/chrome_channel_test.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e +source dev-container-features-test-lib + +# Basic checks +check "chrome wrapper exists" test -f /usr/local/bin/chrome +check "chrome wrapper is executable" test -x /usr/local/bin/chrome + +# Beta channel checks +check "google-chrome-beta is installed" bash -c "command -v google-chrome-beta" +check "google-chrome-beta shows version" bash -c "google-chrome-beta --version" +check "config file exists" test -f /etc/chrome-wrapper/config +check "config has beta binary" bash -c "grep -q 'CHROME_BINARY=\"google-chrome-beta\"' /etc/chrome-wrapper/config" + +# Wrapper should use the beta binary +check "wrapper uses beta channel" bash -c "chrome --version | grep -i beta" + +reportResults diff --git a/test/chrome/chrome_combined_test.sh b/test/chrome/chrome_combined_test.sh new file mode 100755 index 0000000..1f385fe --- /dev/null +++ b/test/chrome/chrome_combined_test.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e +source dev-container-features-test-lib + +# Basic checks +check "chrome wrapper exists" test -f /usr/local/bin/chrome +check "chrome wrapper is executable" test -x /usr/local/bin/chrome +check "chrome version works" bash -c "chrome --version" + +# Combined config checks - debugging port + locale + chromeFlags +check "config file exists" test -f /etc/chrome-wrapper/config +check "config has debugging port 9222" bash -c "grep -q 'DEBUGGING_PORT=\"9222\"' /etc/chrome-wrapper/config" +check "config has locale fr-FR" bash -c "grep -q 'LOCALE=\"fr-FR\"' /etc/chrome-wrapper/config" +check "config has disable-web-security flag" bash -c "grep -q 'disable-web-security' /etc/chrome-wrapper/config" + +# Environment variable checks +check "CHROME_BIN env var is set" bash -c "test -n \"\$CHROME_BIN\"" +check "CHROME_BIN points to wrapper" bash -c "test \"\$CHROME_BIN\" = '/usr/local/bin/chrome'" +check "PUPPETEER_EXECUTABLE_PATH is set" bash -c "test -n \"\$PUPPETEER_EXECUTABLE_PATH\"" +check "PUPPETEER_EXECUTABLE_PATH points to wrapper" bash -c "test \"\$PUPPETEER_EXECUTABLE_PATH\" = '/usr/local/bin/chrome'" + +reportResults diff --git a/test/chrome/chrome_debugging_port_test.sh b/test/chrome/chrome_debugging_port_test.sh new file mode 100755 index 0000000..0c94955 --- /dev/null +++ b/test/chrome/chrome_debugging_port_test.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e +source dev-container-features-test-lib + +# Basic checks +check "chrome wrapper exists" test -f /usr/local/bin/chrome +check "chrome wrapper is executable" test -x /usr/local/bin/chrome +check "chrome version works" bash -c "chrome --version" + +# Config file checks +check "config file exists" test -f /etc/chrome-wrapper/config +check "config contains debugging port 9222" bash -c "grep -q 'DEBUGGING_PORT=\"9222\"' /etc/chrome-wrapper/config" + +reportResults diff --git a/test/chrome/chrome_dev_channel_test.sh b/test/chrome/chrome_dev_channel_test.sh new file mode 100755 index 0000000..076438e --- /dev/null +++ b/test/chrome/chrome_dev_channel_test.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e +source dev-container-features-test-lib + +# Basic checks +check "chrome wrapper exists" test -f /usr/local/bin/chrome +check "chrome wrapper is executable" test -x /usr/local/bin/chrome + +# Dev channel checks — the apt package is "google-chrome-unstable" +check "google-chrome-unstable is installed" bash -c "command -v google-chrome-unstable" +check "google-chrome-unstable shows version" bash -c "google-chrome-unstable --version" +check "config file exists" test -f /etc/chrome-wrapper/config +check "config has unstable binary" bash -c "grep -q 'CHROME_BINARY=\"google-chrome-unstable\"' /etc/chrome-wrapper/config" + +# Wrapper should use the dev (unstable) binary +check "wrapper uses dev channel" bash -c "chrome --version | grep -i -E 'dev|canary|unstable'" + +reportResults diff --git a/test/chrome/chrome_extensions_inline_test.sh b/test/chrome/chrome_extensions_inline_test.sh new file mode 100755 index 0000000..5541f69 --- /dev/null +++ b/test/chrome/chrome_extensions_inline_test.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# This test file will be executed against the 'chrome_extensions_inline_test' scenario +# to verify that inline per-extension config is properly applied. + +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +POLICY_FILE="/etc/opt/chrome/policies/managed/extension_settings.json" + +# Basic Chrome checks +check "chrome wrapper script exists" test -f /usr/local/bin/chrome +check "chrome shows version" bash -c "chrome --version" + +# Extension policy file exists +check "extension policy file exists" test -f "$POLICY_FILE" + +# Claude extension (fcoeoabgfenejglbffodgkkbkcdhcgfn) should have global defaults +check "Claude extension has force_installed" bash -c "grep -A5 'fcoeoabgfenejglbffodgkkbkcdhcgfn' $POLICY_FILE | grep -q 'force_installed'" +check "Claude extension has force_pinned" bash -c "grep -A5 'fcoeoabgfenejglbffodgkkbkcdhcgfn' $POLICY_FILE | grep -q 'force_pinned'" +check "Claude extension has incognito allowed" bash -c "grep -A5 'fcoeoabgfenejglbffodgkkbkcdhcgfn' $POLICY_FILE | grep -q '\"incognito\": \"allowed\"'" + +# uBlock Origin (cjpalhdlnbpafiamejdnhcphjbkeiagm) should have inline overrides +check "uBlock Origin has normal_installed" bash -c "grep -A5 'cjpalhdlnbpafiamejdnhcphjbkeiagm' $POLICY_FILE | grep -q 'normal_installed'" +check "uBlock Origin has unpinned" bash -c "grep -A5 'cjpalhdlnbpafiamejdnhcphjbkeiagm' $POLICY_FILE | grep -q 'unpinned'" +check "uBlock Origin has not_allowed incognito" bash -c "grep -A5 'cjpalhdlnbpafiamejdnhcphjbkeiagm' $POLICY_FILE | grep -q 'not_allowed'" + +# Chromium policy should also exist +check "chromium policy file also exists" test -f /etc/chromium/policies/managed/extension_settings.json + +# Report results +reportResults diff --git a/test/chrome/chrome_extensions_test.sh b/test/chrome/chrome_extensions_test.sh new file mode 100755 index 0000000..aea8761 --- /dev/null +++ b/test/chrome/chrome_extensions_test.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# This test file will be executed against the 'chrome_extensions_test' scenario +# to verify that Chrome extension policies are properly configured. + +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +# Basic Chrome checks +check "chrome wrapper script exists" test -f /usr/local/bin/chrome +check "chrome wrapper script is executable" test -x /usr/local/bin/chrome +check "chrome shows version" bash -c "chrome --version" + +# Config file checks +check "config file exists" test -f /etc/chrome-wrapper/config +check "config has correct binary" bash -c "grep -q 'CHROME_BINARY=\"google-chrome-stable\"' /etc/chrome-wrapper/config" + +# Extension policy directory checks +check "chrome policy directory exists" test -d /etc/opt/chrome/policies/managed +check "chromium policy directory exists" test -d /etc/chromium/policies/managed + +# Extension policy file checks +check "chrome extension policy file exists" test -f /etc/opt/chrome/policies/managed/extension_settings.json +check "chromium extension policy file exists" test -f /etc/chromium/policies/managed/extension_settings.json + +# Policy content checks - Claude extension +check "policy contains Claude extension ID" bash -c "grep -q 'fcoeoabgfenejglbffodgkkbkcdhcgfn' /etc/opt/chrome/policies/managed/extension_settings.json" +check "policy contains force_installed" bash -c "grep -q 'force_installed' /etc/opt/chrome/policies/managed/extension_settings.json" +check "policy contains force_pinned" bash -c "grep -q 'force_pinned' /etc/opt/chrome/policies/managed/extension_settings.json" +check "policy contains update_url" bash -c "grep -q 'https://clients2.google.com/service/update2/crx' /etc/opt/chrome/policies/managed/extension_settings.json" +check "policy contains incognito allowed" bash -c "grep -q '\"incognito\": \"allowed\"' /etc/opt/chrome/policies/managed/extension_settings.json" + +# Report results +reportResults diff --git a/test/chrome/chrome_flags_test.sh b/test/chrome/chrome_flags_test.sh new file mode 100755 index 0000000..1b8b0fc --- /dev/null +++ b/test/chrome/chrome_flags_test.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e +source dev-container-features-test-lib + +# Basic checks +check "chrome wrapper exists" test -f /usr/local/bin/chrome +check "chrome wrapper is executable" test -x /usr/local/bin/chrome +check "chrome version works" bash -c "chrome --version" + +# Extra flags config checks +check "config file exists" test -f /etc/chrome-wrapper/config +check "config has disable-web-security flag" bash -c "grep -q 'disable-web-security' /etc/chrome-wrapper/config" +check "config has allow-running-insecure-content flag" bash -c "grep -q 'allow-running-insecure-content' /etc/chrome-wrapper/config" + +reportResults diff --git a/test/chrome/chrome_locale_test.sh b/test/chrome/chrome_locale_test.sh new file mode 100755 index 0000000..04615cb --- /dev/null +++ b/test/chrome/chrome_locale_test.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e +source dev-container-features-test-lib + +# Basic checks +check "chrome wrapper exists" test -f /usr/local/bin/chrome +check "chrome wrapper is executable" test -x /usr/local/bin/chrome +check "chrome version works" bash -c "chrome --version" + +# Locale config checks +check "config file exists" test -f /etc/chrome-wrapper/config +check "config has locale de-DE" bash -c "grep -q 'LOCALE=\"de-DE\"' /etc/chrome-wrapper/config" + +reportResults diff --git a/test/chrome/chrome_resolution_test.sh b/test/chrome/chrome_resolution_test.sh new file mode 100755 index 0000000..52f8702 --- /dev/null +++ b/test/chrome/chrome_resolution_test.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e +source dev-container-features-test-lib + +# Basic checks +check "chrome wrapper exists" test -f /usr/local/bin/chrome +check "chrome wrapper is executable" test -x /usr/local/bin/chrome +check "chrome version works" bash -c "chrome --version" + +# Xvfb and resolution checks +check "xvfb is installed" bash -c "command -v Xvfb" +check "config file exists" test -f /etc/chrome-wrapper/config +check "config has xvfb display mode" bash -c "grep -q 'DISPLAY_MODE=\"xvfb\"' /etc/chrome-wrapper/config" +check "config has custom resolution 1280x720x16" bash -c "grep -q 'SCREEN_RESOLUTION=\"1280x720x16\"' /etc/chrome-wrapper/config" + +reportResults diff --git a/test/chrome/chrome_test.sh b/test/chrome/chrome_test.sh index 3dc2ef0..fd46968 100644 --- a/test/chrome/chrome_test.sh +++ b/test/chrome/chrome_test.sh @@ -13,6 +13,8 @@ source dev-container-features-test-lib check "chrome wrapper script exists" test -f /usr/local/bin/chrome check "chrome wrapper script is executable" test -x /usr/local/bin/chrome check "chrome shows version" bash -c "chrome --version" +check "config file exists" test -f /etc/chrome-wrapper/config +check "config has headless display mode" bash -c "grep -q 'DISPLAY_MODE=\"headless\"' /etc/chrome-wrapper/config" # Report results # If any of the checks above exited with a non-zero exit code, the test will fail. diff --git a/test/chrome/chrome_vnc_no_clipboard_test.sh b/test/chrome/chrome_vnc_no_clipboard_test.sh new file mode 100755 index 0000000..a3af648 --- /dev/null +++ b/test/chrome/chrome_vnc_no_clipboard_test.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e +source dev-container-features-test-lib + +# Basic Chrome checks +check "chrome wrapper exists" test -f /usr/local/bin/chrome +check "chrome wrapper is executable" test -x /usr/local/bin/chrome +check "chrome version works" bash -c "chrome --version" + +# Config file checks +check "config file exists" test -f /etc/chrome-wrapper/config +check "config has vnc display mode" bash -c "grep -q 'DISPLAY_MODE=\"vnc\"' /etc/chrome-wrapper/config" +check "config has VNC_CLIPBOARD=false" bash -c "grep -q 'VNC_CLIPBOARD=\"false\"' /etc/chrome-wrapper/config" + +# Negative test: clipboard tools should NOT be installed +check "autocutsel is NOT installed" bash -c "! command -v autocutsel" +check "xclip is NOT installed" bash -c "! command -v xclip" + +reportResults diff --git a/test/chrome/chrome_vnc_test.sh b/test/chrome/chrome_vnc_test.sh new file mode 100755 index 0000000..947aeb4 --- /dev/null +++ b/test/chrome/chrome_vnc_test.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -e +source dev-container-features-test-lib + +# Basic Chrome checks +check "chrome wrapper exists" test -f /usr/local/bin/chrome +check "chrome wrapper is executable" test -x /usr/local/bin/chrome +check "chrome version works" bash -c "chrome --version" + +# Config file checks +check "config file exists" test -f /etc/chrome-wrapper/config +check "config has vnc display mode" bash -c "grep -q 'DISPLAY_MODE=\"vnc\"' /etc/chrome-wrapper/config" + +# Desktop-lite integration checks +check "desktop-lite init script exists" test -f /usr/local/share/desktop-init.sh + +# Xvfb should NOT be installed (vnc mode relies on desktop-lite) +check "xvfb is not installed by chrome feature" bash -c "! dpkg -l xvfb 2>/dev/null | grep -q '^ii'" + +# VNC clipboard support checks +check "autocutsel is installed" bash -c "command -v autocutsel" +check "xclip is installed" bash -c "command -v xclip" +check "vncconfig is available" bash -c "command -v vncconfig" +check "config has VNC_CLIPBOARD=true" bash -c "grep -q 'VNC_CLIPBOARD=\"true\"' /etc/chrome-wrapper/config" + +reportResults diff --git a/test/chrome/chrome_xvfb_test.sh b/test/chrome/chrome_xvfb_test.sh new file mode 100755 index 0000000..553a059 --- /dev/null +++ b/test/chrome/chrome_xvfb_test.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e +source dev-container-features-test-lib + +check "chrome wrapper exists" test -f /usr/local/bin/chrome +check "chrome wrapper is executable" test -x /usr/local/bin/chrome +check "chrome version works" bash -c "chrome --version" +check "xvfb is installed" bash -c "command -v Xvfb" +check "xdotool is installed" bash -c "command -v xdotool" +check "wmctrl is installed" bash -c "command -v wmctrl" +check "config file exists" test -f /etc/chrome-wrapper/config +check "config has xvfb display mode" bash -c "grep -q 'DISPLAY_MODE=\"xvfb\"' /etc/chrome-wrapper/config" +check "chrome runs with virtual display" bash -c "unset DISPLAY && chrome --headless=new --dump-dom https://example.com 2>/dev/null | head -1" + +reportResults diff --git a/test/chrome/scenarios.json b/test/chrome/scenarios.json index b177651..83630e6 100644 --- a/test/chrome/scenarios.json +++ b/test/chrome/scenarios.json @@ -4,5 +4,107 @@ "features": { "chrome": {} } + }, + "chrome_channel_test": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "chrome": { + "channel": "beta" + } + } + }, + "chrome_extensions_test": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "chrome": { + "extensions": "{\"fcoeoabgfenejglbffodgkkbkcdhcgfn\": {}}" + } + } + }, + "chrome_extensions_inline_test": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "chrome": { + "extensions": "{\"fcoeoabgfenejglbffodgkkbkcdhcgfn\": {}, \"cjpalhdlnbpafiamejdnhcphjbkeiagm\": {\"mode\": \"normal_installed\", \"pin\": false, \"incognito\": \"not_allowed\"}}" + } + } + }, + "chrome_xvfb_test": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "chrome": { + "displayMode": "xvfb" + } + } + }, + "chrome_vnc_test": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "ghcr.io/devcontainers/features/desktop-lite:1": {}, + "chrome": { + "displayMode": "vnc" + } + } + }, + "chrome_debugging_port_test": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "chrome": { + "debuggingPort": "9222" + } + } + }, + "chrome_locale_test": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "chrome": { + "locale": "de-DE" + } + } + }, + "chrome_flags_test": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "chrome": { + "chromeFlags": "--disable-web-security --allow-running-insecure-content" + } + } + }, + "chrome_resolution_test": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "chrome": { + "displayMode": "xvfb", + "screenResolution": "1280x720x16" + } + } + }, + "chrome_dev_channel_test": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "chrome": { + "channel": "dev" + } + } + }, + "chrome_combined_test": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "chrome": { + "debuggingPort": "9222", + "locale": "fr-FR", + "chromeFlags": "--disable-web-security" + } + } + }, + "chrome_vnc_no_clipboard_test": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "ghcr.io/devcontainers/features/desktop-lite:1": {}, + "chrome": { + "displayMode": "vnc", + "vncClipboard": false + } + } } -} \ No newline at end of file +} diff --git a/test/claude-code/claude_code_default.sh b/test/claude-code/claude_code_default.sh index e958bbd..f132388 100755 --- a/test/claude-code/claude_code_default.sh +++ b/test/claude-code/claude_code_default.sh @@ -5,14 +5,12 @@ set -e -# Optional: Import test library bundled with the devcontainer CLI +# Import test library bundled with the devcontainer CLI source dev-container-features-test-lib -# Feature-specific tests - simple smoke test -# The 'check' command comes from the dev-container-features-test-lib. +# Feature-specific tests check "claude command available" which claude check "claude shows version" bash -c "claude --version" # Report results -# If any of the checks above exited with a non-zero exit code, the test will fail. reportResults diff --git a/test/claude-code/claude_code_stable.sh b/test/claude-code/claude_code_stable.sh new file mode 100755 index 0000000..01c6099 --- /dev/null +++ b/test/claude-code/claude_code_stable.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# This test file will be executed against the 'claude_code_stable' scenario +# to verify that Claude Code installs correctly with version set to 'stable'. + +set -e + +# Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +# Feature-specific tests +check "claude command available" which claude +check "claude shows version" bash -c "claude --version" + +# Report results +reportResults diff --git a/test/claude-code/claude_code_with_custom_user.sh b/test/claude-code/claude_code_with_custom_user.sh index 9af9e29..deb49b9 100755 --- a/test/claude-code/claude_code_with_custom_user.sh +++ b/test/claude-code/claude_code_with_custom_user.sh @@ -1,14 +1,15 @@ #!/bin/bash # This test file will be executed against the 'claude_code_with_custom_user' scenario -# to verify that Claude Code is installed for the remote user. +# to verify that Claude Code is installed correctly for the remote user. set -e # Import test library bundled with the devcontainer CLI source dev-container-features-test-lib -# Basic functionality tests +# Feature-specific tests +check "claude binary exists in user home" test -f "$HOME/.local/bin/claude" check "claude command available" which claude check "claude shows version" bash -c "claude --version" diff --git a/test/claude-code/scenarios.json b/test/claude-code/scenarios.json index 40a82a9..7c4a444 100644 --- a/test/claude-code/scenarios.json +++ b/test/claude-code/scenarios.json @@ -5,6 +5,14 @@ "claude-code": {} } }, + "claude_code_stable": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "claude-code": { + "version": "stable" + } + } + }, "claude_code_with_custom_user": { "image": "mcr.microsoft.com/devcontainers/base:ubuntu", "remoteUser": "vscode", diff --git a/test/claude-code/test.sh b/test/claude-code/test.sh index e8811e0..a5405b7 100755 --- a/test/claude-code/test.sh +++ b/test/claude-code/test.sh @@ -1,37 +1,8 @@ #!/bin/bash -# This test file will be executed against an auto-generated devcontainer.json that -# includes the 'claude-code' Feature with no options. -# -# For more information, see: https://github.com/devcontainers/cli/blob/main/docs/features/test.md -# -# Eg: -# { -# "image": "<..some-base-image...>", -# "features": { -# "claude-code": {} -# }, -# "remoteUser": "root" -# } -# -# Thus, the value of all options will fall back to the default value in the -# Feature's 'devcontainer-feature.json'. -# -# These scripts are run as 'root' by default. Although that can be changed -# with the '--remote-user' flag. -# -# This test can be run with the following command: -# -# devcontainer features test \ -# --features claude-code \ -# --remote-user root \ -# --skip-scenarios \ -# --base-image mcr.microsoft.com/devcontainers/base:ubuntu \ -# /path/to/this/repo - set -e -# Optional: Import test library bundled with the devcontainer CLI +# Import test library bundled with the devcontainer CLI source dev-container-features-test-lib # Feature-specific tests diff --git a/test/codex/test.sh b/test/codex/test.sh index a3eac79..c245cb8 100755 --- a/test/codex/test.sh +++ b/test/codex/test.sh @@ -1,14 +1,13 @@ #!/bin/bash -# This test file will be executed against an auto-generated devcontainer.json that -# includes the 'codex' Feature with no options. - set -e -# Optional: Import test library bundled with the devcontainer CLI +# Import test library bundled with the devcontainer CLI source dev-container-features-test-lib # Feature-specific tests +check "node is available" which node +check "npm is available" which npm check "codex command available" which codex check "codex shows version" bash -c "codex --version" diff --git a/test/color/scenarios.json b/test/color/scenarios.json index bfd7952..3565284 100644 --- a/test/color/scenarios.json +++ b/test/color/scenarios.json @@ -1,6 +1,6 @@ { "my_favorite_color_is_green": { - "image": "mcr.microsoft.com/devcontainers/base:focal", + "image": "mcr.microsoft.com/devcontainers/base:noble", "features": { "ghcr.io/devcontainers/features/common-utils:1": { "installZsh": false, diff --git a/test/gemini-cli/test.sh b/test/gemini-cli/test.sh index 1726c7f..892852e 100755 --- a/test/gemini-cli/test.sh +++ b/test/gemini-cli/test.sh @@ -1,14 +1,13 @@ #!/bin/bash -# This test file will be executed against an auto-generated devcontainer.json that -# includes the 'gemini-cli' Feature with no options. - set -e -# Optional: Import test library bundled with the devcontainer CLI +# Import test library bundled with the devcontainer CLI source dev-container-features-test-lib # Feature-specific tests +check "node is available" which node +check "npm is available" which npm check "gemini command available" which gemini check "gemini shows version" bash -c "gemini --version" diff --git a/test/gitlab/gitlab_test.sh b/test/gitlab/gitlab_test.sh new file mode 100755 index 0000000..b157c6e --- /dev/null +++ b/test/gitlab/gitlab_test.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# This test file will be executed against the 'gitlab_test' scenario. + +set -e + +# Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +# Feature-specific tests +check "glab is installed" which glab +check "glab version" bash -c "glab version" + +# Report results +reportResults diff --git a/test/gitlab/scenarios.json b/test/gitlab/scenarios.json new file mode 100644 index 0000000..9435ba1 --- /dev/null +++ b/test/gitlab/scenarios.json @@ -0,0 +1,8 @@ +{ + "gitlab_test": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "gitlab": {} + } + } +} diff --git a/test/gitlab/test.sh b/test/gitlab/test.sh new file mode 100755 index 0000000..52fba7b --- /dev/null +++ b/test/gitlab/test.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# This test file will be executed against an auto-generated devcontainer.json that +# includes the 'gitlab' Feature with no options. + +set -e + +# Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +# Feature-specific tests +check "glab is installed" which glab +check "glab version" bash -c "glab version" + +# Report results +reportResults diff --git a/test/mcp-language-server/mcp_language_server_without_go.sh b/test/mcp-language-server/mcp_language_server_without_go.sh new file mode 100755 index 0000000..8f0b35c --- /dev/null +++ b/test/mcp-language-server/mcp_language_server_without_go.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# This test file will be executed against the 'mcp_language_server_without_go' scenario +# to verify that MCP Language Server installs Go automatically when not available. + +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +# Feature-specific tests - verify Go fallback installation works +# The 'check' command comes from the dev-container-features-test-lib. +check "go is available" which go +check "mcp-language-server command available" which mcp-language-server +check "mcp-language-server runs help" bash -c "mcp-language-server --help 2>&1 | grep -E '(usage|help|Usage|Help)' || true" + +# Report results +# If any of the checks above exited with a non-zero exit code, the test will fail. +reportResults diff --git a/test/mcp-language-server/scenarios.json b/test/mcp-language-server/scenarios.json index dd4044e..7f1536c 100644 --- a/test/mcp-language-server/scenarios.json +++ b/test/mcp-language-server/scenarios.json @@ -8,8 +8,14 @@ "mcp-language-server": {} } }, + "mcp_language_server_without_go": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "mcp-language-server": {} + } + }, "mcp_language_server_with_go_image": { - "image": "mcr.microsoft.com/devcontainers/go:1-1.24-bookworm", + "image": "mcr.microsoft.com/devcontainers/go:1-bookworm", "features": { "mcp-language-server": {} } diff --git a/test/mcp-language-server/test.sh b/test/mcp-language-server/test.sh new file mode 100755 index 0000000..93b44f8 --- /dev/null +++ b/test/mcp-language-server/test.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e + +# Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +# Feature-specific tests +check "mcp-language-server command available" which mcp-language-server +check "mcp-language-server runs help" bash -c "mcp-language-server --help 2>&1 | grep -E '(usage|help|Usage|Help)' || true" + +# Report results +reportResults