From f558f79789eccdc4253322bcaa467a2c00d30444 Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Thu, 11 Jun 2026 10:29:02 +0000 Subject: [PATCH 1/2] feat: add pnpm feature; persist Claude config in agents (agents v1.2.0) - New `pnpm` feature: corepack enable + `corepack install` (pins pnpm from packageManager at postCreate) with an optional `removeNpm` to enforce pnpm. - agents v1.2.0: mount a persistent ~/.claude volume + set CLAUDE_CONFIG_DIR and chown it, mirroring the gh-login persistence. Together these let a consumer repo drop its post-create.sh / post-start.sh and the .claude mount entirely. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 44 +++++++++++++++++++++------- src/agents/devcontainer-feature.json | 14 +++++++-- src/pnpm/devcontainer-feature.json | 16 ++++++++++ src/pnpm/install.sh | 39 ++++++++++++++++++++++++ test/pnpm/test.sh | 12 ++++++++ 5 files changed, 111 insertions(+), 14 deletions(-) create mode 100644 src/pnpm/devcontainer-feature.json create mode 100755 src/pnpm/install.sh create mode 100755 test/pnpm/test.sh diff --git a/README.md b/README.md index b59330c..7843e16 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. +- With `removeNpm: true`, removes the `npm`/`npx` binaries (after build-time installs have + run) to enforce pnpm-only usage. + +```jsonc +"features": { + "ghcr.io/rocicorp/devcontainer-features/pnpm:1": { "removeNpm": true } +} +``` + +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..e92cb7e --- /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": false, + "description": "Remove the npm and npx binaries after setup to enforce pnpm-only usage." + } + }, + "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..9587e58 --- /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:-false}" + +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..47082fc --- /dev/null +++ b/test/pnpm/test.sh @@ -0,0 +1,12 @@ +#!/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" +# Default removeNpm=false, so npm should still be present. +check "npm still present (removeNpm default false)" bash -c "command -v npm" + +reportResults From e2bb034e417499344211a997bbeaf836da984c42 Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Thu, 11 Jun 2026 10:35:10 +0000 Subject: [PATCH 2/2] fix(pnpm): default removeNpm to true Addresses PR review: enforce pnpm-only by default. Updates the option default, the install.sh fallback, the test, and README usage. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 6 +++--- src/pnpm/devcontainer-feature.json | 4 ++-- src/pnpm/install.sh | 2 +- test/pnpm/test.sh | 5 +++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7843e16..ee008a1 100644 --- a/README.md +++ b/README.md @@ -50,12 +50,12 @@ Sets up [pnpm](https://pnpm.io) via [Corepack](https://github.com/nodejs/corepac - 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. -- With `removeNpm: true`, removes the `npm`/`npx` binaries (after build-time installs have - run) to enforce pnpm-only usage. +- 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": { "removeNpm": true } + "ghcr.io/rocicorp/devcontainer-features/pnpm:1": {} } ``` diff --git a/src/pnpm/devcontainer-feature.json b/src/pnpm/devcontainer-feature.json index e92cb7e..0be616e 100644 --- a/src/pnpm/devcontainer-feature.json +++ b/src/pnpm/devcontainer-feature.json @@ -7,8 +7,8 @@ "options": { "removeNpm": { "type": "boolean", - "default": false, - "description": "Remove the npm and npx binaries after setup to enforce pnpm-only usage." + "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", diff --git a/src/pnpm/install.sh b/src/pnpm/install.sh index 9587e58..4f6b666 100755 --- a/src/pnpm/install.sh +++ b/src/pnpm/install.sh @@ -2,7 +2,7 @@ set -euo pipefail # Option (booleans arrive as the strings "true"/"false"). -REMOVE_NPM="${REMOVENPM:-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 diff --git a/test/pnpm/test.sh b/test/pnpm/test.sh index 47082fc..8da7807 100755 --- a/test/pnpm/test.sh +++ b/test/pnpm/test.sh @@ -6,7 +6,8 @@ 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" -# Default removeNpm=false, so npm should still be present. -check "npm still present (removeNpm default false)" bash -c "command -v npm" +# 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