Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jobs:
with:
filters: |
flutter: ./**/flutter/**
claude-code: ./**/claude-code/**

test:
needs: detect-changes
Expand Down
92 changes: 92 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

A collection of [Dev Container Features](https://containers.dev/implementors/features/) published to GitHub Container Registry (GHCR).

## Table of Contents

- [Available Features](#available-features)
- [Flutter SDK](#flutter-sdk)
- [Claude Code](#claude-code)
- [Repository Structure](#repository-structure)
- [Local Testing](#local-testing)
- [Contributing](#contributing)
- [Publishing](#publishing)
- [License](#license)

## Available Features

### Flutter SDK
Expand Down Expand Up @@ -60,14 +71,92 @@ Install with web precache for faster first build:

Flutter and Dart binaries are added to `PATH` automatically.

### Claude Code

Installs [Claude Code](https://docs.anthropic.com/en/docs/claude-code), Anthropic's official CLI for Claude. Supports latest and pinned npm versions.

```jsonc
// devcontainer.json
{
"features": {
"ghcr.io/awf-project/devcontainer-features/claude-code:1": {}
}
}
```

#### Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `version` | string | `latest` | npm version: `latest` or a specific version (e.g. `1.0.3`) |

#### Examples

Pin a specific version:

```jsonc
{
"features": {
"ghcr.io/awf-project/devcontainer-features/claude-code:1": {
"version": "1.0.3"
}
}
}
```

#### API Key

The feature does not set `ANTHROPIC_API_KEY` automatically. Pass it from your host via `remoteEnv` in your `devcontainer.json`:

```jsonc
{
"remoteEnv": {
"ANTHROPIC_API_KEY": "${localEnv:ANTHROPIC_API_KEY}"
}
}
```

This injects the key at runtime without baking it into the Docker image layer.

#### Persisting Configuration

To persist your Claude config across container rebuilds, add bind mounts to your `devcontainer.json`:

```jsonc
{
"mounts": [
{
"source": "${localEnv:HOME}/.claude",
"target": "/home/vscode/.claude",
"type": "bind"
},
{
"source": "${localEnv:HOME}/.claude.json",
"target": "/home/vscode/.claude.json",
"type": "bind"
}
]
}
```

Requires the [Node.js feature](https://github.com/devcontainers/features/tree/main/src/node) (`ghcr.io/devcontainers/features/node`).

---

## Repository Structure

```
src/
claude-code/ # Claude Code feature source
devcontainer-feature.json
install.sh
flutter/ # Flutter feature source
devcontainer-feature.json
install.sh
test/
claude-code/ # Claude Code feature tests
scenarios.json
test.sh
flutter/ # Flutter feature tests
scenarios.json
test.sh
Expand All @@ -89,6 +178,9 @@ Test features locally before pushing:

# Specific scenario
./test-local.sh flutter install_flutter_stable

# Force rebuild without Docker cache
./test-local.sh --no-cache claude-code
```

Requires Docker and the [Dev Container CLI](https://github.com/devcontainers/cli) (`npm install -g @devcontainers/cli`).
Expand Down
30 changes: 30 additions & 0 deletions src/claude-code/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"id": "claude-code",
"version": "1.0.0",
"name": "Claude Code",
"description": "Installs Claude Code, Anthropic's official CLI for Claude. Supports latest and pinned npm versions. To pass your API key, add '\"remoteEnv\": { \"ANTHROPIC_API_KEY\": \"${localEnv:ANTHROPIC_API_KEY}\" }' in your devcontainer.json. To persist config, bind mount ~/.claude and ~/.claude.json.",
"documentationURL": "https://docs.anthropic.com/en/docs/claude-code",
"licenseURL": "https://github.com/anthropics/claude-code/blob/main/LICENSE",
"options": {
"version": {
"type": "string",
"default": "latest",
"description": "npm version to install: 'latest' or a specific version (e.g. '1.0.3')"
}
},
"customizations": {
"vscode": {
"extensions": [
"anthropic.claude-code"
]
},
"jetbrains": {
"plugins": [
"com.anthropic.code.plugin"
]
}
},
"installsAfter": [
"ghcr.io/devcontainers/features/node"
]
}
44 changes: 44 additions & 0 deletions src/claude-code/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/bash
set -e

# --- Options injected by devcontainer feature engine ---
VERSION="${VERSION:-latest}"

echo "==> Claude Code feature: version=${VERSION}"

# Node.js and npm are required — install if missing
if ! command -v npm &>/dev/null; then
echo "==> npm not found, installing Node.js..."
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
fi

echo "==> npm version: $(npm --version)"
echo "==> node version: $(node --version)"

# Build the npm package reference: latest or pinned
if [ "$VERSION" = "latest" ]; then
NPM_PACKAGE="@anthropic-ai/claude-code"
else
NPM_PACKAGE="@anthropic-ai/claude-code@${VERSION}"
fi

echo "==> Installing ${NPM_PACKAGE}..."
npm install -g "$NPM_PACKAGE"

# Verify the binary is accessible
if ! command -v claude &>/dev/null; then
echo "ERROR: 'claude' binary not found on PATH after install."
echo "npm global bin: $(npm bin -g 2>/dev/null || echo 'unknown')"
exit 1
fi

echo "==> Claude Code $(claude --version) installed at $(command -v claude)"
echo "==> Claude Code feature install complete"
16 changes: 16 additions & 0 deletions test-local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ set -e
# Requires: devcontainer CLI (npm install -g @devcontainers/cli) and Docker

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
NO_CACHE=false

# Parse flags
while [[ "$1" == -* ]]; do
case "$1" in
--no-cache) NO_CACHE=true; shift ;;
*) echo -e "${RED}Unknown option: $1${NC}"; exit 1 ;;
esac
done

FEATURE="${1:-flutter}"
SCENARIO="${2:-}"

Expand Down Expand Up @@ -55,4 +65,10 @@ run_tests() {
}

check_prerequisites

if [ "$NO_CACHE" = true ]; then
echo -e "${YELLOW}==> Pruning Docker build cache...${NC}"
docker builder prune -f >/dev/null 2>&1
fi

run_tests
7 changes: 7 additions & 0 deletions test/claude-code/install_claude_code_latest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash
set -e

# Scenario: install_claude_code_latest
# Verifies default (latest) installation works.

source "$(dirname "$0")/test.sh"
16 changes: 16 additions & 0 deletions test/claude-code/install_claude_code_specific_version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash
set -e

# Scenario: install_claude_code_specific_version
# Verifies pinned version (2.1.30) installation works.

source "$(dirname "$0")/test.sh"

# Additional check: verify the exact version was installed
INSTALLED=$(npm list -g @anthropic-ai/claude-code --depth=0 2>/dev/null | grep @anthropic-ai/claude-code | grep -oP '\d+\.\d+\.\d+')
EXPECTED="2.1.30"
if [ "$INSTALLED" != "$EXPECTED" ]; then
echo "FAIL: Expected @anthropic-ai/claude-code@${EXPECTED}, got ${INSTALLED}"
exit 1
fi
echo "PASS: Correct version ${EXPECTED} installed"
16 changes: 16 additions & 0 deletions test/claude-code/scenarios.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"install_claude_code_latest": {
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"claude-code": {}
}
},
"install_claude_code_specific_version": {
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"claude-code": {
"version": "2.1.30"
}
}
}
}
31 changes: 31 additions & 0 deletions test/claude-code/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/bin/bash
set -e

# Devcontainer feature test — run by devcontainers/action in CI
# Verifies Claude Code CLI is correctly installed and on PATH.

echo "==> Testing Claude Code feature..."

# claude binary must be on PATH
if ! command -v claude &>/dev/null; then
echo "FAIL: claude not found on PATH"
exit 1
fi
echo "PASS: claude on PATH ($(command -v claude))"

# claude --version must succeed and return a version string
CLAUDE_VERSION=$(claude --version 2>&1)
if [ -z "$CLAUDE_VERSION" ]; then
echo "FAIL: claude --version returned empty output"
exit 1
fi
echo "PASS: claude --version => ${CLAUDE_VERSION}"

# npm package must be registered globally
if ! npm list -g @anthropic-ai/claude-code --depth=0 &>/dev/null; then
echo "FAIL: @anthropic-ai/claude-code not found in npm global packages"
exit 1
fi
echo "PASS: @anthropic-ai/claude-code present in npm global packages"

echo "==> All Claude Code feature tests passed"
Loading