diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9b1000b..c153a22 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -10,7 +10,8 @@ "dockerDashComposeVersion": "v2" }, "ghcr.io/dhoeric/features/hadolint:1": {}, - "ghcr.io/guiyomh/features/just:0": {} + "ghcr.io/guiyomh/features/just:0": {}, + "ghcr.io/gilmanlab/features/agents:1": {} }, "postCreateCommand": ".devcontainer/post-create.sh", "customizations": { diff --git a/.github/workflows/features.yml b/.github/workflows/features.yml new file mode 100644 index 0000000..340c90f --- /dev/null +++ b/.github/workflows/features.yml @@ -0,0 +1,90 @@ +name: Features + +on: + pull_request: + paths: + - 'images/features/**' + - '.github/workflows/features.yml' + push: + branches: + - master + paths: + - 'images/features/**' + - '.github/workflows/features.yml' + tags: + - 'features/*/v*' + +env: + REGISTRY: ghcr.io + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Validate devcontainer-feature.json + run: | + for feature_dir in images/features/src/*/; do + if [ -f "${feature_dir}devcontainer-feature.json" ]; then + echo "Validating ${feature_dir}devcontainer-feature.json" + jq empty "${feature_dir}devcontainer-feature.json" + fi + done + + - name: Lint install.sh scripts + run: | + find images/features/src -name "*.sh" -type f -exec shellcheck {} + + + test: + needs: lint + runs-on: ubuntu-latest + strategy: + matrix: + feature: [agents] + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: '22' + + - name: Install devcontainer CLI + run: npm install -g @devcontainers/cli + + - name: Test feature + run: | + devcontainer features test \ + --features ${{ matrix.feature }} \ + --base-image mcr.microsoft.com/devcontainers/base:ubuntu \ + --project-folder ./images/features + + publish: + needs: [lint, test] + if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/features/')) + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Log in to Container Registry + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish Features + uses: devcontainers/action@v1 + with: + publish-features: true + base-path-to-features: ./images/features/src + generate-docs: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/images/features/src/agents/README.md b/images/features/src/agents/README.md new file mode 100644 index 0000000..7ac7b2e --- /dev/null +++ b/images/features/src/agents/README.md @@ -0,0 +1,84 @@ +# AI Coding Agents (agents) + +Installs AI coding agent CLIs: Claude Code, Gemini CLI, and Codex CLI. + +## Usage + +Add this feature to your `devcontainer.json`: + +```json +{ + "features": { + "ghcr.io/gilmanlab/features/agents:1": {} + } +} +``` + +**Note:** This feature requires Node.js/npm. Add the Node.js feature if not already present: + +```json +{ + "features": { + "ghcr.io/devcontainers/features/node:1": {}, + "ghcr.io/gilmanlab/features/agents:1": {} + } +} +``` + +## Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `claudeCode` | boolean | `true` | Install Claude Code CLI | +| `claudeCodeVersion` | string | `latest` | Version of Claude Code | +| `geminiCli` | boolean | `true` | Install Gemini CLI | +| `geminiCliVersion` | string | `latest` | Version of Gemini CLI | +| `codexCli` | boolean | `true` | Install Codex CLI | +| `codexCliVersion` | string | `latest` | Version of Codex CLI | + +## Examples + +### Install all agents (default) + +```json +{ + "features": { + "ghcr.io/gilmanlab/features/agents:1": {} + } +} +``` + +### Install only Claude Code + +```json +{ + "features": { + "ghcr.io/gilmanlab/features/agents:1": { + "geminiCli": false, + "codexCli": false + } + } +} +``` + +### Install specific versions + +```json +{ + "features": { + "ghcr.io/gilmanlab/features/agents:1": { + "claudeCodeVersion": "2.0.76", + "geminiCliVersion": "0.22.5", + "codexCliVersion": "0.77.0" + } + } +} +``` + +## Installed CLIs + +| CLI | Package | Binary | +|-----|---------|--------| +| Claude Code | `@anthropic-ai/claude-code` | `claude` | +| Gemini CLI | `@google/gemini-cli` | `gemini` | +| Codex CLI | `@openai/codex` | `codex` | diff --git a/images/features/src/agents/devcontainer-feature.json b/images/features/src/agents/devcontainer-feature.json new file mode 100644 index 0000000..df08e2a --- /dev/null +++ b/images/features/src/agents/devcontainer-feature.json @@ -0,0 +1,52 @@ +{ + "id": "agents", + "version": "1.0.0", + "name": "AI Coding Agents", + "description": "Installs AI coding agent CLIs: Claude Code, Gemini CLI, and Codex CLI", + "documentationURL": "https://github.com/GilmanLab/headjack/tree/master/images/features/agents", + "keywords": [ + "ai", + "agents", + "claude", + "gemini", + "codex", + "anthropic", + "google", + "openai" + ], + "options": { + "claudeCode": { + "type": "boolean", + "default": true, + "description": "Install Claude Code CLI (@anthropic-ai/claude-code)" + }, + "claudeCodeVersion": { + "type": "string", + "default": "latest", + "description": "Version of Claude Code to install" + }, + "geminiCli": { + "type": "boolean", + "default": true, + "description": "Install Gemini CLI (@google/gemini-cli)" + }, + "geminiCliVersion": { + "type": "string", + "default": "latest", + "description": "Version of Gemini CLI to install" + }, + "codexCli": { + "type": "boolean", + "default": true, + "description": "Install Codex CLI (@openai/codex)" + }, + "codexCliVersion": { + "type": "string", + "default": "latest", + "description": "Version of Codex CLI to install" + } + }, + "installsAfter": [ + "ghcr.io/devcontainers/features/node" + ] +} diff --git a/images/features/src/agents/install.sh b/images/features/src/agents/install.sh new file mode 100755 index 0000000..298f122 --- /dev/null +++ b/images/features/src/agents/install.sh @@ -0,0 +1,121 @@ +#!/bin/bash +# AI Coding Agents Feature - install.sh +# Installs Claude Code, Gemini CLI, and Codex CLI via npm +set -e + +# Feature options (devcontainer converts option names to uppercase) +CLAUDE_CODE="${CLAUDECODE:-true}" +CLAUDE_CODE_VERSION="${CLAUDECODEVERSION:-latest}" +GEMINI_CLI="${GEMINICLI:-true}" +GEMINI_CLI_VERSION="${GEMINICLIVERSION:-latest}" +CODEX_CLI="${CODEXCLI:-true}" +CODEX_CLI_VERSION="${CODEXCLIVERSION:-latest}" + +echo "Installing AI Coding Agent CLIs..." + +# Install Node.js if npm is not available +if ! command -v npm > /dev/null 2>&1; then + echo "Node.js not found. Installing..." + + # Detect package manager and install Node.js + if command -v apt-get > /dev/null 2>&1; then + apt-get update + apt-get install -y curl ca-certificates gnupg + mkdir -p /etc/apt/keyrings + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" > /etc/apt/sources.list.d/nodesource.list + apt-get update + apt-get install -y nodejs + rm -rf /var/lib/apt/lists/* + elif command -v apk > /dev/null 2>&1; then + apk add --no-cache nodejs npm + elif command -v dnf > /dev/null 2>&1; then + dnf install -y nodejs npm + elif command -v yum > /dev/null 2>&1; then + curl -fsSL https://rpm.nodesource.com/setup_22.x | bash - + yum install -y nodejs + else + echo "Error: Could not install Node.js - unsupported package manager" + echo "Please add the Node.js feature manually:" + echo ' "ghcr.io/devcontainers/features/node:1": {}' + exit 1 + fi + + echo "Node.js installed successfully" +fi + +echo "Using npm version: $(npm --version)" +echo "Using Node.js version: $(node --version)" + +# Build list of packages to install +PACKAGES="" + +if [ "${CLAUDE_CODE}" = "true" ]; then + if [ "${CLAUDE_CODE_VERSION}" = "latest" ]; then + PACKAGES="${PACKAGES} @anthropic-ai/claude-code" + else + PACKAGES="${PACKAGES} @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" + fi + echo "Will install Claude Code: ${CLAUDE_CODE_VERSION}" +fi + +if [ "${GEMINI_CLI}" = "true" ]; then + if [ "${GEMINI_CLI_VERSION}" = "latest" ]; then + PACKAGES="${PACKAGES} @google/gemini-cli" + else + PACKAGES="${PACKAGES} @google/gemini-cli@${GEMINI_CLI_VERSION}" + fi + echo "Will install Gemini CLI: ${GEMINI_CLI_VERSION}" +fi + +if [ "${CODEX_CLI}" = "true" ]; then + if [ "${CODEX_CLI_VERSION}" = "latest" ]; then + PACKAGES="${PACKAGES} @openai/codex" + else + PACKAGES="${PACKAGES} @openai/codex@${CODEX_CLI_VERSION}" + fi + echo "Will install Codex CLI: ${CODEX_CLI_VERSION}" +fi + +# Install packages if any are enabled +if [ -n "${PACKAGES}" ]; then + echo "Installing:${PACKAGES}" + # shellcheck disable=SC2086 + npm install -g ${PACKAGES} + echo "Installation complete." +else + echo "No agents selected for installation." +fi + +# Verify installations +echo "" +echo "Verification:" +if [ "${CLAUDE_CODE}" = "true" ]; then + if command -v claude > /dev/null 2>&1; then + echo " Claude Code: installed" + else + echo " Claude Code: FAILED" + exit 1 + fi +fi + +if [ "${GEMINI_CLI}" = "true" ]; then + if command -v gemini > /dev/null 2>&1; then + echo " Gemini CLI: installed" + else + echo " Gemini CLI: FAILED" + exit 1 + fi +fi + +if [ "${CODEX_CLI}" = "true" ]; then + if command -v codex > /dev/null 2>&1; then + echo " Codex CLI: installed" + else + echo " Codex CLI: FAILED" + exit 1 + fi +fi + +echo "" +echo "AI Coding Agent CLIs installation finished." diff --git a/images/features/test/agents/scenarios.json b/images/features/test/agents/scenarios.json new file mode 100644 index 0000000..761ef18 --- /dev/null +++ b/images/features/test/agents/scenarios.json @@ -0,0 +1,19 @@ +{ + "test-default": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "ghcr.io/devcontainers/features/node:1": {}, + "agents": {} + } + }, + "test-claude-only": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "ghcr.io/devcontainers/features/node:1": {}, + "agents": { + "geminiCli": false, + "codexCli": false + } + } + } +} diff --git a/images/features/test/agents/test-claude-only.sh b/images/features/test/agents/test-claude-only.sh new file mode 100755 index 0000000..b4cc425 --- /dev/null +++ b/images/features/test/agents/test-claude-only.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Test script for claude-only scenario +set -e + +echo "Testing Claude-only configuration..." + +# Verify Claude Code is installed +if ! command -v claude > /dev/null 2>&1; then + echo "FAIL: claude command not found" + exit 1 +fi +echo "PASS: claude command found" + +# Verify Gemini CLI is NOT installed +if command -v gemini > /dev/null 2>&1; then + echo "FAIL: gemini command should not be installed" + exit 1 +fi +echo "PASS: gemini command correctly not installed" + +# Verify Codex CLI is NOT installed +if command -v codex > /dev/null 2>&1; then + echo "FAIL: codex command should not be installed" + exit 1 +fi +echo "PASS: codex command correctly not installed" + +echo "All tests passed!" diff --git a/images/features/test/agents/test-default.sh b/images/features/test/agents/test-default.sh new file mode 100755 index 0000000..ecbc4ef --- /dev/null +++ b/images/features/test/agents/test-default.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Test script for test-default scenario (all agents installed) +set -e + +echo "Testing default configuration (all agents)..." + +# Verify Claude Code +if ! command -v claude > /dev/null 2>&1; then + echo "FAIL: claude command not found" + exit 1 +fi +echo "PASS: claude command found" + +# Verify Gemini CLI +if ! command -v gemini > /dev/null 2>&1; then + echo "FAIL: gemini command not found" + exit 1 +fi +echo "PASS: gemini command found" + +# Verify Codex CLI +if ! command -v codex > /dev/null 2>&1; then + echo "FAIL: codex command not found" + exit 1 +fi +echo "PASS: codex command found" + +echo "All tests passed!" diff --git a/images/features/test/agents/test.sh b/images/features/test/agents/test.sh new file mode 100755 index 0000000..0a3443b --- /dev/null +++ b/images/features/test/agents/test.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Test script for default scenario (all agents installed) +set -e + +echo "Testing AI Coding Agents feature..." + +# Verify Claude Code +if ! command -v claude > /dev/null 2>&1; then + echo "FAIL: claude command not found" + exit 1 +fi +echo "PASS: claude command found" + +# Verify Gemini CLI +if ! command -v gemini > /dev/null 2>&1; then + echo "FAIL: gemini command not found" + exit 1 +fi +echo "PASS: gemini command found" + +# Verify Codex CLI +if ! command -v codex > /dev/null 2>&1; then + echo "FAIL: codex command not found" + exit 1 +fi +echo "PASS: codex command found" + +echo "All tests passed!"