mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-30 01:51:20 +00:00
refactor(devcontainer): address ce-code-review findings (P0 + 4 × P1 + 8 × P2 + 2 × P3)
Walkthrough resolution of the 16-finding ce-code-review on PR #1875. 15 of 16 findings applied; one (F12, Anthropic Feature floating tag) was superseded by F6's Feature removal. P0 - F1: WSL2 is now REQUIRED for Windows hosts, not just recommended. ${localEnv:HOME} resolves to empty string on Windows-native (no HOME env var) — bind mounts then point at /.claude, /.codex etc. and silently break. ensure-host-config-dirs.cjs wrote to USERPROFILE-derived paths via os.homedir(), so the two surfaces disagreed about which env var was "home" on Windows. README header reframed; "Windows 11 — WSL2 is required" section explains the mismatch concretely. P1 - F2: Workspace `node_modules` volume names now include `-${devcontainerId}` so two GitNexus checkouts on the same host (~/work/GitNexus and ~/projects/GitNexus) don't share volumes and corrupt each other's installs. - F3 + F5: `postCreateCommand` extracted to `.devcontainer/post-create.sh` with `set -euo pipefail` and six labeled echo steps so failure logs name the step instead of an opaque &&-chain index. Chown step extended to cover /home/node/.npm, /commandhistory, and /home/node/.local — these named-volume mount points were owned by build-time UID 1000 but the container's `node` is re-IDed at runtime by updateRemoteUserUID on non-1000 Linux hosts, leaving them unwritable until now. - F4: Cursor installer downloaded to a temp file with curl --retry + --max-time; sha256 logged to build output before execution so drift across rebuilds is visible in CI logs. Full hard-pin (to a versioned downloads.cursor.com tarball with verified sha256) tracked as a follow-up in README "What's not included". P2 - F6: Anthropic Feature replaced with a direct `npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}` so CLAUDE_CODE_VERSION actually pins the installed binary (the Feature ignored the ARG and pulled latest at install time). Honors the earlier "pin known-good versions" decision and resolves F12's floating-tag concern for this Feature. - F7: Dockerfile ARG defaults dropped for the three version vars; `devcontainer.json` `build.args` is now the single source of truth. Standalone `docker build .devcontainer/` must pass --build-arg. - F8: ensure-host-config-dirs.cjs deleted; `initializeCommand` now uses POSIX `mkdir -p` + `touch ~/.gitconfig` directly, dropping the host-Node-on-PATH prerequisite that broke on fresh Windows+Docker Desktop installs without Node. - F9: ~/.gitconfig bind-mounted read-only so `git commit` inside the container uses the host's user.name / user.email. Read-only so container-side `git config --global` doesn't leak to host. - F10: ~/.config/gh bind-mounted (read-write) so `gh pr create` / `gh pr checks` / `gh issue create` work inside the container without re-auth. AGENTS.md's commit + PR workflow now fully functional for agents inside the container. - F11: CLAUDE_CONFIG_DIR removed from Dockerfile ENV; canonical value lives only in devcontainer.json containerEnv. Eliminates the two-file edit risk. - F13: Mounts comment now documents per-instance vs per-workspace-name scoping rationale so future contributors don't guess. - F14: README "Trust boundary, concretely" paragraph names the exfil path explicitly (malicious npm postinstall → OAuth tokens → ~/.claude/projects/<workspace>/memory/MEMORY.md secrets) and lists vendor-side rotation runbook entries. P3 - F15: Dockerfile pre-create + chown of /home/node/.claude, .codex, .cursor dropped — those paths are bind-mounted, which fully shadows any image-side ownership. Only .npm, .local, /commandhistory still benefit from the pre-create. - F16: README "Bumping CLI versions" section rewritten against the post-F6 reality: CLAUDE_CODE_VERSION and CODEX_VERSION are real pins; CURSOR_VERSION is informational only. Verified locally: `docker build .devcontainer/ --build-arg ...` succeeds. Smoke-tested image: `claude --version` (2.1.153), `codex --version` (0.134.0), `cursor-agent --version` all resolve as the non-root `node` user; named-volume mount points (/home/node/.npm, /commandhistory) are node-owned at build time so non-1000 host UIDs get the post-create.sh chown fix instead of EACCES.
This commit is contained in:
parent
35e61a9e66
commit
cd8ac5f6d1
5 changed files with 181 additions and 118 deletions
|
|
@ -5,23 +5,27 @@
|
|||
# `node` user (UID 1000), zsh + Oh My Zsh, eslint global, `gh` CLI.
|
||||
FROM mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm
|
||||
|
||||
# Build args drive both the image build and lifecycle-time references. The
|
||||
# matching ENV declarations below promote each ARG into the container's
|
||||
# runtime environment so postCreateCommand and shells can resolve them
|
||||
# (Docker ARG values are build-only by default).
|
||||
ARG CLAUDE_CODE_VERSION=2.1.153
|
||||
ARG CODEX_VERSION=0.134.0
|
||||
ARG CURSOR_VERSION=latest
|
||||
# Build args. Version defaults are NOT set here — devcontainer.json
|
||||
# `build.args` is the single source of truth. Standalone `docker build
|
||||
# .devcontainer/` (e.g., CI smoke) must pass each version via --build-arg
|
||||
# or the build will fail loudly rather than silently drift from the
|
||||
# devcontainer-canonical pin.
|
||||
ARG CLAUDE_CODE_VERSION
|
||||
ARG CODEX_VERSION
|
||||
ARG CURSOR_VERSION
|
||||
ARG TZ=UTC
|
||||
ARG USERNAME=node
|
||||
|
||||
# Promote build-only ARGs into runtime ENV so shells and lifecycle scripts
|
||||
# can read them. CLAUDE_CONFIG_DIR is intentionally NOT set here — the
|
||||
# canonical value lives in devcontainer.json `containerEnv` (single source
|
||||
# of truth; runtime-time wins anyway).
|
||||
ENV CLAUDE_CODE_VERSION=${CLAUDE_CODE_VERSION} \
|
||||
CODEX_VERSION=${CODEX_VERSION} \
|
||||
CURSOR_VERSION=${CURSOR_VERSION} \
|
||||
TZ=${TZ} \
|
||||
DEVCONTAINER=true \
|
||||
NODE_OPTIONS=--max-old-space-size=4096 \
|
||||
CLAUDE_CONFIG_DIR=/home/${USERNAME}/.claude \
|
||||
POWERLEVEL9K_DISABLE_GITSTATUS=true
|
||||
|
||||
# Native build toolchain required by gitnexus/postinstall: tree-sitter
|
||||
|
|
@ -33,41 +37,44 @@ RUN apt-get update \
|
|||
python3 make g++ git curl ca-certificates bash \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Pre-create credential and history mount points owned by `node` BEFORE the
|
||||
# devcontainer.json named volumes attach. Docker copies image-side ownership
|
||||
# onto an empty named volume on first mount, so first-run `claude login`,
|
||||
# `codex login --device-auth`, and `cursor-agent login` write into a
|
||||
# node-owned directory instead of EACCES'ing on a root-owned volume.
|
||||
# Pre-create + chown the named-volume mount points (~/.npm, ~/.local,
|
||||
# /commandhistory) so empty volumes inherit `node:node` ownership on first
|
||||
# mount. The three CLI config dirs (~/.claude, ~/.codex, ~/.cursor) are
|
||||
# bind-mounted from the host — bind mounts fully shadow image-side
|
||||
# ownership, so no chown is needed for those paths here.
|
||||
RUN mkdir -p \
|
||||
/home/${USERNAME}/.claude \
|
||||
/home/${USERNAME}/.codex \
|
||||
/home/${USERNAME}/.cursor \
|
||||
/home/${USERNAME}/.npm \
|
||||
/home/${USERNAME}/.local/bin \
|
||||
/commandhistory \
|
||||
&& chown -R ${USERNAME}:${USERNAME} \
|
||||
/home/${USERNAME}/.claude \
|
||||
/home/${USERNAME}/.codex \
|
||||
/home/${USERNAME}/.cursor \
|
||||
/home/${USERNAME}/.npm \
|
||||
/home/${USERNAME}/.local \
|
||||
/commandhistory
|
||||
|
||||
USER ${USERNAME}
|
||||
|
||||
# Install Codex CLI globally as the `node` user. The base image configures
|
||||
# /usr/local/share/npm-global as the npm-global prefix with the `npm` group
|
||||
# writable by `node`, so this works without sudo.
|
||||
RUN npm install -g @openai/codex@${CODEX_VERSION}
|
||||
# Install Claude Code and Codex CLI globally as the `node` user. The base
|
||||
# image configures /usr/local/share/npm-global as the npm-global prefix
|
||||
# with the `npm` group writable by `node`, so `npm install -g` works
|
||||
# without sudo. Both versions are pinned via build args — bump in
|
||||
# devcontainer.json and rebuild to upgrade.
|
||||
RUN npm install -g \
|
||||
@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} \
|
||||
@openai/codex@${CODEX_VERSION}
|
||||
|
||||
# Cursor CLI install. The official installer (cursor.com/install) does not
|
||||
# expose a clean version-pinning flag; CURSOR_VERSION is carried for
|
||||
# traceability only and the installer always pulls the latest at build time.
|
||||
# To bump Cursor: rebuild the image. Auto-update in the running container is
|
||||
# suppressed by not invoking `cursor-agent update`.
|
||||
RUN curl -fsS https://cursor.com/install | bash
|
||||
# Cursor CLI install. The official cursor.com/install script does not
|
||||
# expose version pinning or a checksum, so we download to a temp file,
|
||||
# log the sha256 to build output (so drift across rebuilds shows up in
|
||||
# CI logs), then execute. Trust assumption: cursor.com's TLS chain is
|
||||
# reliable. Long-term hardening (tracked as a follow-up): pin a specific
|
||||
# downloads.cursor.com/lab/<version>/<arch>/agent-cli-package.tar.gz URL
|
||||
# with a hard sha256 verification and skip the install script entirely.
|
||||
RUN curl -fsS --retry 3 --max-time 60 -o /tmp/cursor-install.sh https://cursor.com/install \
|
||||
&& echo "Cursor installer sha256:" \
|
||||
&& sha256sum /tmp/cursor-install.sh \
|
||||
&& bash /tmp/cursor-install.sh \
|
||||
&& rm -f /tmp/cursor-install.sh
|
||||
|
||||
# Ensure ~/.local/bin (where Cursor's installer drops `agent` and
|
||||
# `cursor-agent` symlinks) is on PATH for interactive shells and lifecycle
|
||||
# scripts.
|
||||
# ~/.local/bin (where Cursor's installer drops `agent` and `cursor-agent`
|
||||
# symlinks) on PATH for interactive shells and lifecycle scripts.
|
||||
ENV PATH=/home/${USERNAME}/.local/bin:${PATH}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# GitNexus Devcontainer
|
||||
|
||||
A cross-platform Dev Container that pre-installs Claude Code, OpenAI Codex CLI, and Cursor CLI alongside the GitNexus native build chain. Designed for Windows 11 (Docker Desktop + WSL2 backend) as the primary host with first-class support for macOS and Linux.
|
||||
A cross-platform Dev Container that pre-installs Claude Code, OpenAI Codex CLI, and Cursor CLI alongside the GitNexus native build chain. Supported hosts: **macOS, Linux, and Windows 11 via WSL2** (Windows-native is unsupported — see below).
|
||||
|
||||
## Quick start
|
||||
|
||||
|
|
@ -10,9 +10,9 @@ A cross-platform Dev Container that pre-installs Claude Code, OpenAI Codex CLI,
|
|||
4. Wait for the first build (~3–6 minutes) and `postCreateCommand` to finish installing workspace dependencies.
|
||||
5. Authenticate the three CLIs once — see [First-time CLI authentication](#first-time-cli-authentication) below.
|
||||
|
||||
## Windows 11 (primary host) — WSL2 setup
|
||||
## Windows 11 — WSL2 is required
|
||||
|
||||
**Clone the repo inside WSL2, not on the Windows side.** Bind-mounting a Windows-side path (`C:\…`) through Docker Desktop's WSL2 backend works but suffers from poor IO and unreliable file watchers (Vite/jest `--watch` will miss changes). The fix is to clone into the WSL2 filesystem.
|
||||
**Windows-native is unsupported.** The devcontainer bind-mounts host config dirs via `${localEnv:HOME}/.claude` (and `.codex`, `.cursor`, `.gitconfig`, `.config/gh`). On Windows-native, the host has `USERPROFILE` set but no `HOME` — VS Code resolves the missing `HOME` to an empty string and Docker tries to bind-mount paths from filesystem root, which silently breaks the host-sync feature. The same checkout-from-Windows-side path also has poor IO and unreliable file watchers (Vite/jest `--watch` will miss changes). The fix is to clone and open the repo inside WSL2:
|
||||
|
||||
```bash
|
||||
# 1. Install WSL2 and a Linux distro if you haven't already.
|
||||
|
|
@ -27,13 +27,12 @@ git clone https://github.com/abhigyanpatwari/GitNexus.git
|
|||
cd GitNexus
|
||||
|
||||
# 4. Launch VS Code from inside WSL — this opens VS Code attached to the WSL2
|
||||
# filesystem, so subsequent "Reopen in Container" uses the WSL2-side path.
|
||||
# filesystem, so `${localEnv:HOME}` resolves to the WSL user's home and
|
||||
# subsequent "Reopen in Container" uses the WSL2-side path.
|
||||
code .
|
||||
```
|
||||
|
||||
Then run **Dev Containers: Reopen in Container**. The workspace will be bind-mounted from `\\wsl$\Ubuntu\home\<user>\GitNexus`, which is fast and gives reliable file-system events.
|
||||
|
||||
**Make sure Docker Desktop's WSL integration is enabled** for your distro: Docker Desktop → Settings → Resources → WSL Integration → toggle on the distro you cloned into.
|
||||
Then run **Dev Containers: Reopen in Container**. The workspace will be bind-mounted from `\\wsl$\Ubuntu\home\<user>\GitNexus`, which is fast and gives reliable file-system events. **Make sure Docker Desktop's WSL integration is enabled** for your distro: Docker Desktop → Settings → Resources → WSL Integration → toggle on the distro you cloned into.
|
||||
|
||||
## macOS
|
||||
|
||||
|
|
@ -45,15 +44,38 @@ Same as macOS — open in VS Code and reopen in container. `updateRemoteUserUID:
|
|||
|
||||
## How CLI state is shared with your host
|
||||
|
||||
`~/.claude`, `~/.codex`, and `~/.cursor` inside the container are **bind-mounted directly from your host's `$HOME`**. That means:
|
||||
The following directories inside the container are **bind-mounted directly from your host's `$HOME`**:
|
||||
|
||||
- **Authentication is shared.** If you're already logged in on the host (`claude login`, `codex login`, `cursor-agent login`), you're already logged in inside the container. No second login step.
|
||||
| Container path | Host source | Mode |
|
||||
|---|---|---|
|
||||
| `~/.claude` | `$HOME/.claude` | read-write |
|
||||
| `~/.codex` | `$HOME/.codex` | read-write |
|
||||
| `~/.cursor` | `$HOME/.cursor` | read-write |
|
||||
| `~/.gitconfig` | `$HOME/.gitconfig` | **read-only** |
|
||||
| `~/.config/gh` | `$HOME/.config/gh` | read-write |
|
||||
|
||||
That means:
|
||||
|
||||
- **Authentication is shared.** If you're already logged in on the host (`claude login`, `codex login`, `cursor-agent login`, `gh auth login`), you're already logged in inside the container. No second login step.
|
||||
- **Plugins, skills, agents, memory, and settings sync both ways.** Install a plugin from inside the container and it shows up on the host; add a custom agent on the host and the container sees it immediately. The auto-memory store at `~/.claude/projects/<workspace>/memory/` is the same file tree from both sides.
|
||||
- **No per-workspace duplication.** All your devcontainers across all your projects see the same `.claude`/`.codex`/`.cursor` content, just like all your host shells do.
|
||||
- **Git identity comes from the host.** Commits from inside the container use your host's `user.name` / `user.email`. The mount is read-only so container-side `git config --global` doesn't leak to your host config — set those values from the host shell.
|
||||
- **`gh` auth is shared.** `gh pr create`, `gh pr checks`, `gh issue create` work inside the container without re-authenticating.
|
||||
- **No per-workspace duplication.** All your devcontainers across all your projects see the same host CLI state, just like all your host shells do.
|
||||
|
||||
The bind mount source paths are guaranteed to exist by `.devcontainer/ensure-host-config-dirs.cjs`, which `initializeCommand` runs on the host before the container is created.
|
||||
The bind mount source directories are guaranteed to exist by the `initializeCommand` (`mkdir -p $HOME/.claude $HOME/.codex $HOME/.cursor $HOME/.config/gh`), which runs on the host shell before container create.
|
||||
|
||||
For a high-trust enterprise environment where you don't want container code to be able to touch host credentials, replace the three `type=bind` entries for `.claude`/`.codex`/`.cursor` in `.devcontainer/devcontainer.json` with `type=volume` named volumes (Anthropic's reference pattern). Most personal-dev setups don't need that isolation — host and container share the same trust boundary.
|
||||
### Trust boundary, concretely
|
||||
|
||||
Host and container share a single trust boundary by design — fine for personal-dev, but the consequence is concrete: any malicious npm package or `postinstall` script in the workspace dep tree, running inside the container with these bind mounts active, has direct read access to your OAuth refresh tokens for all three CLIs, your `gh` token, and `~/.claude/projects/<workspace>/memory/MEMORY.md` (which may contain user-stored secrets if you've used the `/remember` skill). The egress firewall is deferred (see "What's not included (yet)" below) so a compromised package would also have unrestricted network to exfiltrate.
|
||||
|
||||
**If a workspace dep is ever found compromised**, rotate credentials at the vendor side — local file deletion is insufficient because tokens may have already left:
|
||||
|
||||
- Anthropic: [console.anthropic.com → Settings → Keys](https://console.anthropic.com/settings/keys), revoke the OAuth session under Account
|
||||
- OpenAI / Codex: [platform.openai.com/api-keys](https://platform.openai.com/api-keys), revoke session under Profile
|
||||
- Cursor: dashboard → Integrations, rotate API key + revoke CLI session
|
||||
- GitHub: `gh auth refresh` or revoke the token at github.com/settings/tokens
|
||||
|
||||
For high-trust enterprise environments where host and container should NOT share credentials, swap the three CLI bind mounts (`~/.claude`, `~/.codex`, `~/.cursor`) in `.devcontainer/devcontainer.json` for `type=volume` named volumes (Anthropic's reference pattern). You give up host plugin/skill/memory sync in exchange for credential isolation per devcontainer.
|
||||
|
||||
## First-time CLI authentication
|
||||
|
||||
|
|
@ -144,15 +166,12 @@ VS Code's Ports panel shows forwarded ports once their listener starts.
|
|||
|
||||
## Bumping CLI versions
|
||||
|
||||
Three build args control pinned versions:
|
||||
|
||||
- `CLAUDE_CODE_VERSION` — informational. Anthropic's official Feature (`ghcr.io/anthropics/devcontainer-features/claude-code:1`) installs the latest stable at build time; rebuild to pick up a newer Claude Code. `DISABLE_AUTOUPDATER=1` keeps it locked between rebuilds.
|
||||
- `CODEX_VERSION` — pinned in `.devcontainer/devcontainer.json` `build.args` and consumed by `npm install -g @openai/codex@${CODEX_VERSION}`. Bump the value and rebuild.
|
||||
- `CURSOR_VERSION` — informational only. The Cursor installer (`cursor.com/install`) does not expose version pinning; it always pulls latest at build time. To bump, rebuild the container; auto-update inside the running container is suppressed by not invoking `cursor-agent update`.
|
||||
Bump `CLAUDE_CODE_VERSION` and `CODEX_VERSION` in `.devcontainer/devcontainer.json` `build.args` and rebuild — both are real pins (Claude Code via `npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}`, Codex via `npm install -g @openai/codex@${CODEX_VERSION}`). `CURSOR_VERSION` is informational only because Cursor's official installer (`cursor.com/install`) doesn't expose version pinning; rebuild to pick up whatever the installer serves. To stop Cursor from auto-updating in the running container, don't call `cursor-agent update`.
|
||||
|
||||
## What's not included (yet)
|
||||
|
||||
- **Egress firewall.** The original plan included an opt-in iptables/ipset firewall adapted from Anthropic's reference devcontainer. It was deferred to a follow-up PR — `runArgs` is static in `devcontainer.json`, so toggling NET_ADMIN/NET_RAW capabilities cleanly requires either a separate `devcontainer-firewall.json` profile or an `initializeCommand`-generated overlay. Track at the project's issue tracker if you need this.
|
||||
- **Hard-pinned Cursor CLI.** Today the Dockerfile downloads `cursor.com/install` to a temp file, logs the sha256 to the build output (so drift across rebuilds is visible in CI logs), then executes — trust assumption: cursor.com's TLS chain is reliable. The full pin (download a specific `downloads.cursor.com/lab/<version>/<arch>/agent-cli-package.tar.gz` with a verified sha256, skip the install script entirely) is tracked as a follow-up because it requires per-arch handling and a SHA bump per Cursor release.
|
||||
- **Codespaces tuning.** The current config works in Codespaces incidentally (no privileged capabilities, no host-mount assumptions), but isn't actively tested there.
|
||||
- **Playwright e2e support.** `gitnexus-web`'s `npm run test:e2e` needs Chromium libs that the base image doesn't ship. Use the host for e2e until a Playwright layer is added.
|
||||
|
||||
|
|
@ -160,10 +179,12 @@ Three build args control pinned versions:
|
|||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|--------------|-----|
|
||||
| `EACCES` / `EPERM` writing into `~/.claude`, `~/.codex`, or `~/.cursor` inside the container | Windows-side bind-mount permission translation got out of sync after a UID change between rebuilds | On the host, ensure your user owns the directory tree; if it's truly stuck, move the affected dir aside and let the CLI rebuild it (`mv ~/.claude ~/.claude.bak` and log in again). Long-term: clone in WSL2 — bind-mount permission classes don't apply to WSL-side filesystems |
|
||||
| `EPERM: operation not permitted, copyfile ... '.husky/_/h'` in `postCreateCommand` | Leftover `.husky/_/` from a previous container run; Docker Desktop's Windows bind mount won't let the new container's `node` user overwrite it. `postCreateCommand` already runs `rm -rf .husky/_` defensively, but if you hit it on an older config, delete `.husky/_/` on the host (`rm -rf .husky/_`) and rebuild | Long-term: clone the repo inside WSL2 (see [Windows 11 WSL2 setup](#windows-11-primary-host--wsl2-setup)) — WSL-side filesystems don't have this bind-mount class of issue |
|
||||
| Vite never hot-reloads on Windows | Repo cloned on Windows side, not WSL2 | Re-clone inside WSL2 (see [WSL2 setup](#windows-11-primary-host--wsl2-setup)) |
|
||||
| `EACCES` / `EPERM` writing into `~/.claude`, `~/.codex`, or `~/.cursor` inside the container | Windows-side bind-mount permission translation got out of sync after a UID change between rebuilds | Move to WSL2 — Windows-native isn't supported. See [Windows 11 — WSL2 is required](#windows-11--wsl2-is-required) |
|
||||
| `EPERM: operation not permitted, copyfile ... '.husky/_/h'` in `postCreateCommand` | Leftover `.husky/_/` from a previous container run on a Windows-side bind mount | `post-create.sh` already runs `rm -rf .husky/_` defensively. If you hit this on an older config, delete `.husky/_/` on the host and rebuild. Long-term: clone in WSL2 |
|
||||
| Vite never hot-reloads | Repo cloned on Windows side, not WSL2 | Re-clone inside WSL2 |
|
||||
| `gitnexus-web` can't reach the backend | `4747` was remapped or backend isn't running | Verify the Ports panel shows `4747` forwarded with no remap; start the backend with `cd gitnexus && npx gitnexus serve` |
|
||||
| `npm install` fails on tree-sitter-swift / proto / dart | Native build toolchain missing | This shouldn't happen in the devcontainer — verify the apt layer installed `python3 make g++`. If iterating, set `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` to skip the vendored grammars |
|
||||
| Integration tests fail with `database busy` | LadybugDB single-writer constraint | Don't run host-side `gitnexus analyze` while the container is also analyzing the same repo; choose one writer |
|
||||
| API key env vars not visible inside the container | They are intentionally not auto-propagated from the host (so an empty/stale host var can't silently break `*-login` for everyone else) | `export ANTHROPIC_API_KEY=...` / `OPENAI_API_KEY=...` / `CURSOR_API_KEY=...` inside the container shell, or carry it via your VS Code [dotfiles repo](https://code.visualstudio.com/docs/devcontainers/containers#_personalizing-with-dotfile-repositories) for persistence |
|
||||
| `git commit` produces commits with empty author | `~/.gitconfig` source path missing on the host | Set `git config --global user.name` / `user.email` from the host shell, then rebuild. The bind mount is read-only so the values come from the host |
|
||||
| `gh: not logged in` inside the container | `~/.config/gh/` source path missing on the host | Run `gh auth login` from the host shell (or inside the container once); the auth file lands in the shared mount |
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// Devcontainer for GitNexus. Pre-installs Claude Code, OpenAI Codex CLI,
|
||||
// and Cursor CLI alongside the Node.js native build chain. Cross-platform
|
||||
// (Win11+WSL2 + macOS + Linux), opened via the VS Code Dev Containers
|
||||
// extension.
|
||||
// across macOS, Linux, and Windows-via-WSL2 (Windows-native is unsupported
|
||||
// — see .devcontainer/README.md § Windows 11 setup). Opens via the VS Code
|
||||
// Dev Containers extension.
|
||||
//
|
||||
// First-time setup, auth flows, and troubleshooting: .devcontainer/README.md.
|
||||
{
|
||||
|
|
@ -18,17 +19,14 @@
|
|||
}
|
||||
},
|
||||
|
||||
// Runs on the HOST before the container is created. Ensures the bind
|
||||
// mount sources (~/.claude, ~/.codex, ~/.cursor) exist so Docker doesn't
|
||||
// reject the mount when a CLI has never been used on this host. Safe
|
||||
// re-run; idempotent. See ensure-host-config-dirs.cjs.
|
||||
"initializeCommand": "node .devcontainer/ensure-host-config-dirs.cjs",
|
||||
// Runs on the HOST shell before the container is created. Guarantees the
|
||||
// bind-mount source directories below exist so Docker doesn't reject the
|
||||
// mount when a CLI has never been used on this host. Idempotent; safe to
|
||||
// re-run. POSIX-shell only (Linux / macOS / WSL2) — Windows-native is
|
||||
// out of scope per README's WSL2-required posture.
|
||||
"initializeCommand": "mkdir -p $HOME/.claude $HOME/.codex $HOME/.cursor $HOME/.config/gh && touch $HOME/.gitconfig",
|
||||
|
||||
// Anthropic's official Claude Code Feature pulls the latest stable at
|
||||
// build time; DISABLE_AUTOUPDATER below locks it inside the running
|
||||
// container so rebuild is the only way the version changes.
|
||||
"features": {
|
||||
"ghcr.io/anthropics/devcontainer-features/claude-code:1": {},
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||
},
|
||||
|
||||
|
|
@ -38,35 +36,45 @@
|
|||
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",
|
||||
"workspaceFolder": "/workspace",
|
||||
|
||||
// CLI config dirs are bind-mounted from the host so the developer's
|
||||
// existing plugins, skills, agents, memory, settings, and credentials
|
||||
// for Claude Code / Codex / Cursor are immediately available inside the
|
||||
// container, and changes inside the container flow back to the host.
|
||||
// The `initializeCommand` above guarantees these source paths exist on
|
||||
// first up so Docker never errors out on a missing bind source. Anthropic
|
||||
// recommends per-devcontainer named volumes instead in enterprise /
|
||||
// high-trust environments; for personal dev where the host + container
|
||||
// share the same trust boundary, bind mounts give the better daily-driver
|
||||
// experience.
|
||||
// Mount topology, by group:
|
||||
//
|
||||
// Shell history, npm cache, and node_modules stay in named volumes
|
||||
// scoped per-workspace — history doesn't need to escape the workspace,
|
||||
// node_modules belong off the bind mount for Win/Mac perf, and the npm
|
||||
// cache wants the container-native FS.
|
||||
// 1. CLI config dirs — bind-mounted from the host so the developer's
|
||||
// existing plugins, skills, agents, memory, settings, and credentials
|
||||
// for Claude Code / Codex / Cursor + git identity + gh auth are
|
||||
// immediately available inside the container, and changes inside the
|
||||
// container flow back to the host. ~/.gitconfig is read-only so
|
||||
// container-side `git config --global` doesn't leak to host config.
|
||||
// For high-trust environments where host and container should NOT
|
||||
// share credentials, swap these three CLI bind mounts for
|
||||
// per-devcontainer named volumes (Anthropic's reference pattern).
|
||||
//
|
||||
// 2. Per-instance state — `${devcontainerId}` scoped: history and npm
|
||||
// cache survive container rebuilds but stay isolated between
|
||||
// sibling devcontainer instances.
|
||||
//
|
||||
// 3. Per-workspace-name AND per-instance state — workspace `node_modules`
|
||||
// volumes use both `${localWorkspaceFolderBasename}` (debuggable in
|
||||
// `docker volume ls`) and `${devcontainerId}` (collision-free between
|
||||
// sibling instances of the same repo, e.g., ~/work/GitNexus vs
|
||||
// ~/projects/GitNexus). Keeps tree-sitter native binaries and
|
||||
// onnxruntime off the workspace bind mount (the real Win/Mac perf win).
|
||||
"mounts": [
|
||||
"source=${localEnv:HOME}/.claude,target=/home/node/.claude,type=bind",
|
||||
"source=${localEnv:HOME}/.codex,target=/home/node/.codex,type=bind",
|
||||
"source=${localEnv:HOME}/.cursor,target=/home/node/.cursor,type=bind",
|
||||
"source=${localEnv:HOME}/.gitconfig,target=/home/node/.gitconfig,type=bind,readonly",
|
||||
"source=${localEnv:HOME}/.config/gh,target=/home/node/.config/gh,type=bind",
|
||||
"source=commandhistory-${devcontainerId},target=/commandhistory,type=volume",
|
||||
"source=npm-cache-${devcontainerId},target=/home/node/.npm,type=volume",
|
||||
"source=${localWorkspaceFolderBasename}-root-node-modules,target=/workspace/node_modules,type=volume",
|
||||
"source=${localWorkspaceFolderBasename}-gitnexus-node-modules,target=/workspace/gitnexus/node_modules,type=volume",
|
||||
"source=${localWorkspaceFolderBasename}-gitnexus-web-node-modules,target=/workspace/gitnexus-web/node_modules,type=volume",
|
||||
"source=${localWorkspaceFolderBasename}-gitnexus-shared-node-modules,target=/workspace/gitnexus-shared/node_modules,type=volume"
|
||||
"source=${localWorkspaceFolderBasename}-root-node-modules-${devcontainerId},target=/workspace/node_modules,type=volume",
|
||||
"source=${localWorkspaceFolderBasename}-gitnexus-node-modules-${devcontainerId},target=/workspace/gitnexus/node_modules,type=volume",
|
||||
"source=${localWorkspaceFolderBasename}-gitnexus-web-node-modules-${devcontainerId},target=/workspace/gitnexus-web/node_modules,type=volume",
|
||||
"source=${localWorkspaceFolderBasename}-gitnexus-shared-node-modules-${devcontainerId},target=/workspace/gitnexus-shared/node_modules,type=volume"
|
||||
],
|
||||
|
||||
// Interactive login is the default auth path for all three CLIs;
|
||||
// credentials persist in the per-devcontainer named volumes mounted above.
|
||||
// credentials persist in the host-bind-mounted directories (~/.claude,
|
||||
// ~/.codex, ~/.cursor) declared in the mounts block above.
|
||||
// API keys (ANTHROPIC_API_KEY / OPENAI_API_KEY / CURSOR_API_KEY) are NOT
|
||||
// injected via containerEnv — `${localEnv:VAR}` resolves an unset host var
|
||||
// to an empty string, and Cursor in particular treats `CURSOR_API_KEY=""`
|
||||
|
|
@ -123,15 +131,9 @@
|
|||
}
|
||||
},
|
||||
|
||||
// Sequential setup: chown the workspace-side node_modules volumes (Docker
|
||||
// creates them root-owned), drop any stale `.husky/_` runtime cache (it
|
||||
// can be left over from prior runs and Docker Desktop's Windows bind-mount
|
||||
// permission translation refuses to let the new container's `node` user
|
||||
// overwrite a file the previous host UID created — husky's `prepare`
|
||||
// copyfile then EPERMs on `.husky/_/h`), and install in dependency order:
|
||||
// root (husky) → gitnexus-shared (install + build, consumed via file:..)
|
||||
// → gitnexus-web (must install BEFORE gitnexus, because gitnexus's
|
||||
// `prepare` script runs scripts/build.js which compiles gitnexus-web)
|
||||
// → gitnexus (last; its prepare hook needs gitnexus-web's node_modules).
|
||||
"postCreateCommand": "sudo chown -R node:node /workspace/node_modules /workspace/gitnexus/node_modules /workspace/gitnexus-web/node_modules /workspace/gitnexus-shared/node_modules && cd /workspace && rm -rf .husky/_ && npm install && cd /workspace/gitnexus-shared && npm install && npm run build && cd /workspace/gitnexus-web && npm install && cd /workspace/gitnexus && npm install"
|
||||
// Driver script with labeled steps lives at .devcontainer/post-create.sh
|
||||
// so each step's success/failure is visible in the log without parsing
|
||||
// an &&-chain. Run via `bash` explicitly so the script doesn't depend
|
||||
// on its executable bit surviving the workspace bind mount.
|
||||
"postCreateCommand": "bash .devcontainer/post-create.sh"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
// Runs on the HOST (not inside the container) before the dev container is
|
||||
// created, as the devcontainer.json `initializeCommand`. Ensures the host
|
||||
// has empty config directories for Claude Code, Codex CLI, and Cursor CLI
|
||||
// at the user's home directory so the bind mounts in devcontainer.json
|
||||
// always have a real source path (Docker fails the bind mount if the
|
||||
// source doesn't exist).
|
||||
//
|
||||
// Cross-platform via Node's `os.homedir()` and `fs.mkdirSync({recursive:
|
||||
// true})`. Node is already required on the host because the project's
|
||||
// Claude Code, the @devcontainers/cli reentry, and most repo scripts
|
||||
// depend on it.
|
||||
//
|
||||
// Safe to re-run: `recursive: true` is a no-op when the directory exists.
|
||||
//
|
||||
// No third-party dependencies; CommonJS so it runs on any Node ≥ 12
|
||||
// without ESM gymnastics.
|
||||
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
|
||||
for (const dir of [".claude", ".codex", ".cursor"]) {
|
||||
const target = path.join(os.homedir(), dir);
|
||||
fs.mkdirSync(target, { recursive: true });
|
||||
}
|
||||
58
.devcontainer/post-create.sh
Normal file
58
.devcontainer/post-create.sh
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
#!/usr/bin/env bash
|
||||
# Devcontainer postCreate driver. Runs once after the container is created
|
||||
# (per devcontainer.json `postCreateCommand`). Each labeled step is its own
|
||||
# command, so a failure log line names the step that failed instead of an
|
||||
# opaque `&&`-chain index.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd /workspace
|
||||
|
||||
echo "[post-create] 1/6: chown workspace node_modules + named-volume mount points"
|
||||
# `updateRemoteUserUID: true` realigns the `node` user's UID/GID at runtime
|
||||
# on Linux hosts (no-op on Mac/Windows where Docker Desktop translates UIDs
|
||||
# via its VM layer). The Dockerfile chown at build time targets the original
|
||||
# UID; empty named volumes created at first mount inherit that ownership and
|
||||
# end up owned by the stale UID after realignment. Re-chown here, post-
|
||||
# realignment, so npm install can write to ~/.npm and zsh history writes to
|
||||
# /commandhistory succeed on hosts with non-1000 UIDs.
|
||||
sudo chown -R node:node \
|
||||
/workspace/node_modules \
|
||||
/workspace/gitnexus/node_modules \
|
||||
/workspace/gitnexus-web/node_modules \
|
||||
/workspace/gitnexus-shared/node_modules \
|
||||
/home/node/.npm \
|
||||
/home/node/.local \
|
||||
/commandhistory
|
||||
|
||||
echo "[post-create] 2/6: clear stale .husky/_ runtime cache"
|
||||
# Docker Desktop's Windows bind-mount permission translation refuses to let
|
||||
# the new container's `node` user overwrite a `.husky/_/h` left by a prior
|
||||
# container with a different effective UID. `.husky/_` is gitignored runtime
|
||||
# cache; husky regenerates it during npm install.
|
||||
rm -rf .husky/_
|
||||
|
||||
echo "[post-create] 3/6: npm install at root (husky + lint-staged + prettier + eslint)"
|
||||
npm install
|
||||
|
||||
echo "[post-create] 4/6: npm install + build gitnexus-shared"
|
||||
# gitnexus and gitnexus-web both consume gitnexus-shared via
|
||||
# file:../gitnexus-shared, so it must be built before either installs.
|
||||
cd /workspace/gitnexus-shared
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
echo "[post-create] 5/6: npm install gitnexus-web"
|
||||
# Must install BEFORE gitnexus: gitnexus's `prepare` script runs
|
||||
# scripts/build.js, which compiles gitnexus-web when the directory is
|
||||
# present. In the devcontainer the full workspace is bind-mounted, so
|
||||
# gitnexus-web/ is present at gitnexus install time even though it
|
||||
# wouldn't be in the production Dockerfiles (which COPY selectively).
|
||||
cd /workspace/gitnexus-web
|
||||
npm install
|
||||
|
||||
echo "[post-create] 6/6: npm install gitnexus (triggers prepare -> scripts/build.js)"
|
||||
cd /workspace/gitnexus
|
||||
npm install
|
||||
|
||||
echo "[post-create] done"
|
||||
Loading…
Add table
Reference in a new issue