diff --git a/README.md b/README.md index b59330c..ee008a1 100644 --- a/README.md +++ b/README.md @@ -14,16 +14,17 @@ Installs the AI coding agents we standardize on: - **GitHub CLI** (`gh`, via `dependsOn` on the official `ghcr.io/devcontainers/features/github-cli` feature) -### Persistent `gh` login +### Persistent Claude + `gh` logins -The feature mounts a named volume (`devcontainer-gh-config`) at -`/home/node/.config/gh`, so a `gh auth login` survives container **rebuilds** — you log -in once instead of after every rebuild. A `postCreateCommand` `chown`s the volume so the -`node` user can write to it. +The feature mounts named volumes (`devcontainer-claude-config` at `/home/node/.claude`, +`devcontainer-gh-config` at `/home/node/.config/gh`) and sets `CLAUDE_CONFIG_DIR`, so the +Claude and `gh auth login` sessions survive container **rebuilds** — you log in once +instead of after every rebuild. A `postCreateCommand` `chown`s the volumes so the `node` +user can write to them. -> The volume is **shared across all repos** that use this feature (gh auth is account-level, -> not repo-level), so logging in from one container carries over to the others. The mount -> path assumes the `node` remote user (the base image we standardize on). +> The volumes are **shared across all repos** that use this feature (these logins are +> account-level, not repo-level), so logging in from one container carries over to the +> others. The mount paths assume the `node` remote user (the base image we standardize on). ### Usage @@ -38,10 +39,31 @@ In any repo's `.devcontainer/devcontainer.json`: } ``` -This single line replaces the official `claude-code` and `github-cli` feature lines *and* -the inline `npm install -g @openai/codex` in `post-create.sh`. +This single line replaces the official `claude-code` and `github-cli` feature lines, the +inline `npm install -g @openai/codex`, **and** the `.claude` volume / `CLAUDE_CONFIG_DIR` / +`chown` wiring that otherwise lives in each repo's `devcontainer.json` + `post-create.sh`. -### Updating the agent versions everywhere +## `pnpm` + +Sets up [pnpm](https://pnpm.io) via [Corepack](https://github.com/nodejs/corepack): + +- Runs `corepack enable` (adds the `pnpm` shim) at build time. +- At `postCreate`, runs `corepack install` to pin the pnpm version from the workspace's + `package.json` `packageManager` field. +- Removes the `npm`/`npx` binaries (after build-time installs have run) to enforce + pnpm-only usage. This is the default; set `removeNpm: false` to keep npm available. + +```jsonc +"features": { + "ghcr.io/rocicorp/devcontainer-features/pnpm:1": {} +} +``` + +This replaces the corepack/pnpm/npm-removal block that otherwise lives in each repo's +`post-create.sh`. Combined with `agents`, a consumer repo's `devcontainer.json` needs no +lifecycle scripts at all. + +## Updating the feature versions everywhere 1. Bump `codexVersion` default (and/or the `dependsOn` claude-code pin) in `src/agents/devcontainer-feature.json`, raise the feature `version`, merge to `main`. diff --git a/src/agents/devcontainer-feature.json b/src/agents/devcontainer-feature.json index 7e526b9..3b09924 100644 --- a/src/agents/devcontainer-feature.json +++ b/src/agents/devcontainer-feature.json @@ -1,8 +1,8 @@ { "id": "agents", - "version": "1.1.0", + "version": "1.2.0", "name": "AI Coding Agents (Codex + Claude Code + GitHub CLI)", - "description": "Installs the OpenAI Codex CLI, Claude Code, and the GitHub CLI so every dev container ships with the agents pre-installed. Persists the `gh` login across rebuilds via a named volume.", + "description": "Installs the OpenAI Codex CLI, Claude Code, and the GitHub CLI so every dev container ships with the agents pre-installed. Persists the Claude and `gh` logins across rebuilds via named volumes.", "documentationURL": "https://github.com/rocicorp/devcontainer-features/tree/main/src/agents", "options": { "codexVersion": { @@ -15,13 +15,21 @@ "ghcr.io/anthropics/devcontainer-features/claude-code:1": {}, "ghcr.io/devcontainers/features/github-cli:1": {} }, + "containerEnv": { + "CLAUDE_CONFIG_DIR": "/home/node/.claude" + }, "mounts": [ { "source": "devcontainer-gh-config", "target": "/home/node/.config/gh", "type": "volume" + }, + { + "source": "devcontainer-claude-config", + "target": "/home/node/.claude", + "type": "volume" } ], - "postCreateCommand": "sudo chown -R node:node /home/node/.config/gh 2>/dev/null || true", + "postCreateCommand": "sudo chown -R node:node /home/node/.config/gh /home/node/.claude 2>/dev/null || true", "installsAfter": ["ghcr.io/devcontainers/features/node"] } diff --git a/src/pnpm/devcontainer-feature.json b/src/pnpm/devcontainer-feature.json new file mode 100644 index 0000000..0be616e --- /dev/null +++ b/src/pnpm/devcontainer-feature.json @@ -0,0 +1,16 @@ +{ + "id": "pnpm", + "version": "1.0.0", + "name": "pnpm via Corepack", + "description": "Enables Corepack and installs the pnpm version pinned in the workspace's package.json `packageManager` field. Optionally removes npm/npx to enforce pnpm.", + "documentationURL": "https://github.com/rocicorp/devcontainer-features/tree/main/src/pnpm", + "options": { + "removeNpm": { + "type": "boolean", + "default": true, + "description": "Remove the npm and npx binaries after setup to enforce pnpm-only usage. Set to false to keep npm available." + } + }, + "postCreateCommand": "/usr/local/share/rocicorp-pnpm/post-create.sh", + "installsAfter": ["ghcr.io/devcontainers/features/node"] +} diff --git a/src/pnpm/install.sh b/src/pnpm/install.sh new file mode 100755 index 0000000..4f6b666 --- /dev/null +++ b/src/pnpm/install.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Option (booleans arrive as the strings "true"/"false"). +REMOVE_NPM="${REMOVENPM:-true}" + +if ! command -v corepack >/dev/null 2>&1; then + echo "ERROR: corepack not found. This feature needs a Node.js install (base image or node feature)." >&2 + exit 1 +fi + +echo "Enabling Corepack..." +corepack enable + +# `corepack install` (pins pnpm from the workspace packageManager field) and the optional +# npm removal both have to run at postCreate, not here: +# - the workspace isn't mounted during the build, so package.json isn't readable yet; +# - npm must survive until other features (e.g. global npm installs) have run at build time. +# So we bake a hook script that the feature's postCreateCommand invokes. +HOOK_DIR=/usr/local/share/rocicorp-pnpm +HOOK="$HOOK_DIR/post-create.sh" +mkdir -p "$HOOK_DIR" + +cat > "$HOOK" <<'EOS' +#!/usr/bin/env bash +set -euo pipefail +# Install the pnpm version pinned in the workspace's package.json (packageManager field). +corepack install 2>/dev/null || echo "pnpm feature: no packageManager field found; skipping 'corepack install'." +EOS + +if [ "$REMOVE_NPM" = "true" ]; then + cat >> "$HOOK" <<'EOS' +# Enforce pnpm: drop npm/npx so they can't be used by mistake. +sudo rm -rf /usr/local/bin/npm /usr/local/bin/npx /usr/local/lib/node_modules/npm 2>/dev/null || true +EOS +fi + +chmod +x "$HOOK" +echo "Wrote postCreate hook to $HOOK (removeNpm=$REMOVE_NPM)." diff --git a/test/pnpm/test.sh b/test/pnpm/test.sh new file mode 100755 index 0000000..8da7807 --- /dev/null +++ b/test/pnpm/test.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +source dev-container-features-test-lib + +# corepack enable installs the pnpm shim onto PATH at build time. +check "pnpm shim on PATH" bash -c "command -v pnpm" +check "postCreate hook generated" bash -c "test -x /usr/local/share/rocicorp-pnpm/post-create.sh" +# removeNpm defaults to true, so the generated postCreate hook should drop npm/npx. +# (The removal itself runs at postCreate, after build-time installs.) +check "hook removes npm (removeNpm default true)" bash -c "grep -q 'rm -rf /usr/local/bin/npm' /usr/local/share/rocicorp-pnpm/post-create.sh" + +reportResults