From 9c07ef9cf9e1c89e69019497faa332b84f4f608f Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Thu, 28 May 2026 09:13:56 +0100 Subject: [PATCH] feat(devcontainer): add cross-platform devcontainer for Claude Code, Codex, and Cursor CLIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a Dev Container that pre-installs Claude Code (2.1.153, via Anthropic's official Feature), OpenAI Codex CLI (pinned 0.134.0), and Cursor CLI alongside the GitNexus native build chain. Opens via VS Code's Dev Containers extension on Windows 11 (Docker Desktop + WSL2), macOS, or Linux without OS-specific branches in devcontainer.json. Topology and base - Base image `mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm` (multi-arch, monthly patched, ships the `node` non-root user, zsh, `gh`). - Node 22 LTS satisfies `gitnexus/`'s engines `>=22.0.0` and matches the `node:22-bookworm-slim` SHA-pinned base used by `Dockerfile.cli`. - Single container with all three CLIs co-installed (vs. docker-compose per-tool) — prevailing 2026 community pattern, lowest daily-driver friction. Persistence and auth - Per-devcontainer named volumes scoped by `${devcontainerId}` for `/home/node/.claude`, `/home/node/.codex`, `/home/node/.cursor`, `/commandhistory`, and `/home/node/.npm`. Authentication survives rebuilds without leaking between workspaces. - Four sub-workspace `node_modules` volumes (root, gitnexus, gitnexus-web, gitnexus-shared) keep tree-sitter native bindings and onnxruntime off the bind mount — the actual Win/Mac perf win. - Credential mount paths are pre-created in the Dockerfile with `chown node:node` BEFORE `USER node`, so empty named volumes inherit correct ownership on first mount and first-run logins don't EACCES. - `CURSOR_API_KEY` is injected via `containerEnv: ${localEnv:CURSOR_API_KEY}` (Cursor's documented headless path); falls back to interactive `cursor-agent login` when the host env var is unset. Build-arg promotion - Build args (`CLAUDE_CODE_VERSION`, `CODEX_VERSION`, `CURSOR_VERSION`, `TZ`) are promoted to ENV in the Dockerfile so lifecycle commands and shells can resolve them. Without this promotion, Docker ARG values are build-only and silently no-op at lifecycle time. Workspace setup - `postCreateCommand` chowns the four workspace `node_modules` volumes (Docker creates them root-owned), then installs in dependency order: root → gitnexus-shared (install + build) → gitnexus → gitnexus-web. The shared package must build before its consumers (`file:../gitnexus-shared`). Ports - 5173 (Vite dev) and 4173 (Vite preview) auto-forwarded. - 4747 (`gitnexus serve`) marked `requireLocalPort: true` because `gitnexus-web/src/services/backend-client.ts` hardcodes `http://localhost:4747` as the default backend URL; a remapped port would silently break the web UI. VS Code integration - Recommended extensions: `anthropic.claude-code`, `dbaeumer.vscode-eslint`, `esbenp.prettier-vscode`, `eamodio.gitlens`. - Settings: format-on-save with Prettier, ESLint auto-fix on save, zsh as default terminal profile, persistent zsh history via `HISTFILE` → `/commandhistory`. Documentation - `.devcontainer/README.md` covers WSL2 setup (clone inside WSL2 for IO and file-watcher reliability), first-time auth flows for each CLI, port- forwarding notes, LadybugDB container limitations, and the bumping procedure for each CLI version. - `CONTRIBUTING.md` gets a "Containerized development (optional)" subsection pointing at the devcontainer README. Deferred to a follow-up PR - Opt-in egress firewall (originally planned as a fourth implementation unit). The Dev Containers spec makes `runArgs` static — toggling `NET_ADMIN`/`NET_RAW` capabilities cleanly requires either a separate `devcontainer-firewall.json` profile or an `initializeCommand`-generated overlay. Keeping this PR focused on the working baseline. - Codespaces-specific tuning (works incidentally when the firewall is off, not actively tested). - Inside-container Playwright e2e (needs Chromium libs not in the base image). Verification deferred to user - This change introduces a new dev tooling artifact. Validate by running `docker build .devcontainer/`, opening the repo in VS Code via "Dev Containers: Reopen in Container", confirming `claude --version`, `codex --version`, `cursor-agent --version` resolve inside the container, and `cd gitnexus && npm run test:unit` runs clean against the named-volume `node_modules`. --- .devcontainer/Dockerfile | 73 +++++++++++++++++ .devcontainer/README.md | 141 ++++++++++++++++++++++++++++++++ .devcontainer/devcontainer.json | 108 ++++++++++++++++++++++++ CONTRIBUTING.md | 4 + 4 files changed, 326 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/README.md create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..9e50f7092 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,73 @@ +# syntax=docker/dockerfile:1 + +# Base image: Microsoft's TypeScript+Node devcontainer image. Multi-arch +# (linux/amd64, linux/arm64), monthly security patching, ships the non-root +# `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 +ARG TZ=UTC +ARG USERNAME=node + +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 +# native bindings, vendored Dart/Proto/Swift grammars, @ladybugdb/core +# N-API addon. python3/make/g++ are non-negotiable; mirrors the apt block +# in the existing Dockerfile.cli / gitnexus/Dockerfile.test images. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + 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. +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} + +# 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 + +# Ensure ~/.local/bin (where Cursor's installer drops `agent` and +# `cursor-agent` symlinks) is on PATH for interactive shells and lifecycle +# scripts. +ENV PATH=/home/${USERNAME}/.local/bin:${PATH} diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 000000000..1d0e2fd6e --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,141 @@ +# 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. + +## Quick start + +1. Install [Docker Desktop](https://docs.docker.com/desktop/) (Windows/macOS) or Docker Engine (Linux). +2. Install [VS Code](https://code.visualstudio.com/) with the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers). +3. Open the repo in VS Code → Command Palette → **Dev Containers: Reopen in Container**. +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 + +**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. + +```bash +# 1. Install WSL2 and a Linux distro if you haven't already. +wsl --install -d Ubuntu + +# 2. Enter WSL. +wsl + +# 3. Clone the repo inside your WSL2 home directory. +cd ~ +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. +code . +``` + +Then run **Dev Containers: Reopen in Container**. The workspace will be bind-mounted from `\\wsl$\Ubuntu\home\\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 + +Open the repo folder in VS Code → **Reopen in Container**. The image is multi-arch; on Apple Silicon you'll pull the `linux/arm64` variant automatically. + +## Linux + +Same as macOS — open in VS Code and reopen in container. `updateRemoteUserUID: true` (default) shifts the container's `node` user UID/GID to match your host user, so bind-mounted files stay writable without extra setup. + +## First-time CLI authentication + +Each CLI persists its config in a per-devcontainer named volume scoped by `${devcontainerId}`, so authentication survives container rebuilds. You only need to log in once per workspace. + +### Claude Code + +```bash +claude login +``` + +This opens a browser auth flow; VS Code's port forwarding handles the callback. After auth, `~/.claude/` is populated in the named volume and persists across rebuilds. The `DISABLE_AUTOUPDATER=1` env var prevents the running CLI from updating itself — rebuild the container to pick up a newer Claude Code. + +### OpenAI Codex CLI + +The browser-callback flow can be flaky inside containers; the device-code flow is more reliable: + +```bash +codex login --device-auth +``` + +Visit the URL it prints on your host browser and paste the displayed code. After auth, `~/.codex/auth.json` persists in the named volume. + +For CI/headless use: set `OPENAI_API_KEY` in your host shell before opening the devcontainer — VS Code's `${localEnv:OPENAI_API_KEY}` would resolve it, but it's currently not wired into `containerEnv`. If you need this, add it to `.devcontainer/devcontainer.json` and rebuild. + +### Cursor CLI + +Preferred path is the `CURSOR_API_KEY` environment variable: + +1. Generate an API key from the Cursor dashboard → Integrations. +2. Set it on the host shell that launches VS Code: `export CURSOR_API_KEY=...` (or set in your shell rc / Windows env vars). +3. **Rebuild the container** — `containerEnv` resolves `${localEnv:CURSOR_API_KEY}` at container-create time, so a new key only lands after a rebuild. + +Verify with `cursor-agent status` inside the container. + +If you'd rather log in interactively: + +```bash +cursor-agent login +``` + +This opens a browser flow and stores credentials under `~/.cursor/` in the named volume. + +## Port forwarding + +| Port | Service | Notes | +|------|---------|-------| +| `5173` | Vite dev server (`gitnexus-web`) | Auto-forwarded with notification | +| `4747` | `gitnexus serve` HTTP API | **Must not be remapped** — `gitnexus-web` hardcodes `http://localhost:4747` as the default backend URL | +| `4173` | Static web (Vite preview) | Silently forwarded | + +VS Code's Ports panel shows forwarded ports once their listener starts. + +## Known gotchas + +- **LadybugDB integration tests may fail in containers** (file-locking, `AGENTS.md` § Testing). Default to `npm run test:unit` inside the container; run integration tests on the host. Tracking issue: documented as a known limitation. +- **Single-writer LadybugDB constraint** (`GUARDRAILS.md` § LadybugDB lock). Don't run `gitnexus analyze` on the host and inside the container against the same `.gitnexus/` directory simultaneously — the second writer will get `database busy`. +- **Native grammar builds add ~30s to first install.** Tree-sitter Dart/Proto/Swift grammars build during `gitnexus`'s `postinstall`. To skip them (loses parsing for those three languages), set `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` in your shell or add it to `remoteEnv` and rebuild. +- **`tree-sitter-kotlin` warnings on install** are expected (per `AGENTS.md`). Ignore them. +- **`.mcp.json` works inside the container**: `npx -y gitnexus@latest mcp` resolves cleanly because npm registry is reachable and the workspace bind mount exposes the same `.mcp.json` the host sees. +- **Husky pre-commit fires inside the container** without extra setup. The root `npm install` (run automatically in `postCreateCommand`) installs the hook via `package.json` `prepare`. + +## Rebuild / reset + +- **Rebuild Container** (Command Palette) — re-runs the Dockerfile build and `postCreateCommand` against the existing named volumes (auth + history persist). +- **Rebuild Container Without Cache** — fresh image layers, same volumes. +- **To clear a stale named volume** (e.g., force a re-login): + ```bash + docker volume ls | grep gitnexus # find the per-devcontainer volume + docker volume rm + ``` + Then rebuild. + +## 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`. + +## 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. +- **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. + +## Troubleshooting + +| Symptom | Likely cause | Fix | +|---------|--------------|-----| +| `EACCES` on first `claude login` / `codex login` / `cursor-agent login` | Named volume mount got a stale state | `docker volume rm` the relevant `*-config-` volume and rebuild | +| 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)) | +| `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 | +| `CURSOR_API_KEY` set on host but not visible in container | Host env not propagated, or container started before key was set | Rebuild the container after setting the host env var | diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..3bde747da --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,108 @@ +// 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. +// +// First-time setup, auth flows, and troubleshooting: .devcontainer/README.md. +{ + "name": "GitNexus AI CLI Devcontainer", + + "build": { + "dockerfile": "Dockerfile", + "context": ".", + "args": { + "CLAUDE_CODE_VERSION": "2.1.153", + "CODEX_VERSION": "0.134.0", + "CURSOR_VERSION": "latest", + "TZ": "${localEnv:TZ:UTC}" + } + }, + + // 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": {} + }, + + "remoteUser": "node", + "updateRemoteUserUID": true, + + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated", + "workspaceFolder": "/workspace", + + // Named volumes scoped per-devcontainer keep auth tokens, history, and + // node_modules persistent across rebuilds without leaking between + // workspaces. Sub-workspace node_modules volumes keep tree-sitter native + // binaries and onnxruntime off the bind mount (the real Win/Mac perf win). + "mounts": [ + "source=claude-config-${devcontainerId},target=/home/node/.claude,type=volume", + "source=codex-config-${devcontainerId},target=/home/node/.codex,type=volume", + "source=cursor-config-${devcontainerId},target=/home/node/.cursor,type=volume", + "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" + ], + + // CURSOR_API_KEY resolves at container-create time from the host shell + // that opened VS Code (empty fallback triggers interactive cursor-agent + // login). Set CURSOR_API_KEY on the host and rebuild to inject a new key. + "containerEnv": { + "CLAUDE_CONFIG_DIR": "/home/node/.claude", + "DISABLE_AUTOUPDATER": "1", + "CURSOR_API_KEY": "${localEnv:CURSOR_API_KEY}", + "HISTFILE": "/commandhistory/.zsh_history" + }, + + "customizations": { + "vscode": { + "extensions": [ + "anthropic.claude-code", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "eamodio.gitlens" + ], + "settings": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "files.eol": "\n", + "terminal.integrated.defaultProfile.linux": "zsh", + "terminal.integrated.profiles.linux": { + "bash": { "path": "bash", "icon": "terminal-bash" }, + "zsh": { "path": "zsh" } + } + } + } + }, + + // 4747 (gitnexus serve) must not be remapped: gitnexus-web hardcodes + // http://localhost:4747 as the default backend URL. + "forwardPorts": [5173, 4747, 4173], + "portsAttributes": { + "5173": { + "label": "Vite dev (gitnexus-web)", + "onAutoForward": "notify" + }, + "4747": { + "label": "gitnexus serve HTTP API", + "onAutoForward": "notify", + "requireLocalPort": true + }, + "4173": { + "label": "Static web (Vite preview)", + "onAutoForward": "silent" + } + }, + + // Sequential setup: chown the workspace-side node_modules volumes (Docker + // creates them root-owned), then install in dependency order + // (gitnexus-shared must build before its consumers). + "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 && npm install && cd /workspace/gitnexus-shared && npm install && npm run build && cd /workspace/gitnexus && npm install && cd /workspace/gitnexus-web && npm install" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e048d4f2f..4cb50c8c3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,6 +18,10 @@ This project uses the [PolyForm Noncommercial License 1.0.0](https://polyformpro 3. **Web UI (if needed):** `cd gitnexus-web && npm install` 4. Run tests as described in [TESTING.md](TESTING.md). +### Containerized development (optional) + +If you prefer an isolated environment with Claude Code, OpenAI Codex CLI, and Cursor CLI pre-installed, open the repo in VS Code with the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) and run **Dev Containers: Reopen in Container**. See [`.devcontainer/README.md`](.devcontainer/README.md) for first-time auth flows and Windows WSL2 setup. + ## Branch and pull requests - Use short-lived branches off the default branch of the repo you are targeting.