From a29a1cfb9fe0c3e80bf7e8acd65aae9a0e89c2b6 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Thu, 28 May 2026 14:08:45 +0100 Subject: [PATCH] feat(devcontainer): support Windows-native via auto setx HOME on first run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverses the "WSL2 required on Windows" posture. Windows-native now works after a one-time auto-handled setup. The root cause of the bind-mount failure: VS Code resolves `${localEnv:HOME}` by reading its own process env, and Windows doesn't set `HOME` by default — Windows uses `USERPROFILE`. So the bind sources were collapsing to `/.claude`, `/.codex`, etc., and Docker rejected them. `ensure-host-config-dirs.cjs` now handles this automatically on Windows hosts where `HOME` is unset: 1. Runs `setx HOME "%USERPROFILE%"`, which writes to the user-level Windows environment (HKCU\Environment) — no admin required. Every future user process inherits HOME from there. 2. Prints a clear one-time setup banner explaining the user needs to fully restart VS Code (File > Exit, not just close the window) for VS Code to pick up the new env at its next startup. 3. Exits 1 so VS Code surfaces this as a clean container-create failure instead of letting Docker error opaquely later. On the second Reopen-in-Container attempt, `HOME` is now set in VS Code's env, the script skips the setup block, creates the bind-mount source dirs, and the container builds normally. Subsequent rebuilds have no extra steps. Mac, Linux, and WSL2 hosts have `HOME` set by the shell, so the new block is a no-op there. Same `devcontainer.json` works across all supported hosts. README rewritten to reflect the new posture: - Header lists Windows 11 (native) as a supported host alongside macOS, Linux, and WSL2, with a note that Windows-native gets a one-time HOME setup handled by the initializeCommand. - New "Windows 11 setup" section walks through the auto-handled setup flow + a manual `setx HOME "%USERPROFILE%"` fallback for users who want to do it themselves. - "Known trade-offs of Windows-native vs WSL2" subsection lays out the Docker Desktop Windows bind-mount edge cases (file watchers, npm install perf, husky/_ EPERM) so users opting into Windows-native do so eyes-open. WSL2 remains documented as the faster path for users who want it, but it's no longer the only supported one. - Troubleshooting table gets two new rows: the one-time setup banner (with "what to do" instructions) and the residual `bind source path does not exist` case (run setx manually + fully exit VS Code). --- .devcontainer/README.md | 38 ++++++++-- .devcontainer/ensure-host-config-dirs.cjs | 91 +++++++++++++++-------- 2 files changed, 92 insertions(+), 37 deletions(-) diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 2407fe629..28eb091df 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -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. Supported hosts: **macOS, Linux, and Windows 11 via WSL2** (Windows-native is unsupported — the bind mounts require `$HOME` to be set on the host shell, which cmd.exe on Windows-native lacks). +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, Windows 11 (native), and Windows 11 via WSL2.** Windows-native needs a **one-time `HOME` env var setup** — handled automatically by the `initializeCommand` on first run (see [Windows 11 setup](#windows-11-setup)). ## Quick start @@ -11,11 +11,38 @@ A cross-platform Dev Container that pre-installs Claude Code, OpenAI Codex CLI, 5. Wait for the first build (~3–6 minutes) and `postCreateCommand` to finish installing workspace dependencies. 6. Authenticate the three CLIs once — see [First-time CLI authentication](#first-time-cli-authentication) below. -## Windows 11 — WSL2 is required +## Windows 11 setup -**Windows-native is unsupported.** The host bind mounts use `${localEnv:HOME}/.claude` (and `.codex`, `.cursor`, `.ssh`, `.config/git`, `.config/gh`, `.gitconfig`). VS Code resolves `${localEnv:HOME}` by reading the host shell's `HOME` env var — and cmd.exe on Windows-native has no `HOME` set. The bind sources then collapse to filesystem-root paths (`/.claude`, `/.codex`, …) and Docker rejects them with `bind source path does not exist`. The `initializeCommand`'s Node script detects this case and fails fast with a pointer at this section instead of letting Docker error opaquely. +### Windows-native (one-time setup, then "just works") -Beyond the `HOME` resolution issue, WSL2 also gives you reliable file watchers (Vite/jest `--watch` work), 3-5× faster `npm install` IO, and avoids the Docker Desktop Windows bind-mount permission edge cases (e.g., the husky `.husky/_/h` EPERM class). +The host bind mounts use `${localEnv:HOME}/.claude` (and `.codex`, `.cursor`, `.ssh`, `.config/git`, `.config/gh`, `.gitconfig`). VS Code resolves `${localEnv:HOME}` by reading its own process env, and Windows doesn't set `HOME` by default — it uses `USERPROFILE`. So the bind mounts can't resolve until you tell Windows to also expose your profile as `HOME`. + +The `initializeCommand` (`node .devcontainer/ensure-host-config-dirs.cjs`) handles this automatically: + +1. **First time you Reopen in Container**, the script detects the missing `HOME`, runs `setx HOME "%USERPROFILE%"` (which writes to your user-level Windows env — no admin needed), prints a one-time setup banner, and exits. +2. **Close all VS Code windows** (File → Exit) and reopen. VS Code picks up the new `HOME` at startup. +3. **Reopen in Container again.** The script now sees `HOME=C:\Users\`, skips the setup block, creates the bind-mount source dirs, and Docker brings the container up. + +Subsequent rebuilds work normally with no extra steps. The `HOME` env var is set persistently in your Windows user environment, so it'll be there for every future VS Code session (and any other tool that wants `HOME`). + +If you'd rather set it manually before opening the container: + +```powershell +setx HOME "%USERPROFILE%" +# Close & reopen VS Code +``` + +### Known trade-offs of Windows-native vs WSL2 + +Windows-native works, but Docker Desktop's Windows bind-mount layer has rough edges that WSL2 avoids: + +- **File watchers can miss events.** Vite / jest `--watch` running inside the container watching workspace files mounted from `D:\...` may miss changes — chokidar polling (`CHOKIDAR_USEPOLLING=true`) is the usual workaround. +- **`npm install` is 3-5× slower** through the Windows-to-Linux bind-mount translation than on a WSL2-native filesystem. +- **Permission edge cases.** The husky `.husky/_/h` EPERM class we hit earlier in this PR is specific to Windows-side bind mounts changing UID ownership between container runs. `post-create.sh` clears the cache defensively to keep this from being fatal, but it's still a real source of friction. + +If you hit any of those and want to migrate to WSL2 later, the steps are below. + +### WSL2 (faster, fewer edge cases) To clone and open the repo inside WSL2: @@ -187,7 +214,8 @@ Bump `CLAUDE_CODE_VERSION` and `CODEX_VERSION` in `.devcontainer/devcontainer.js | Symptom | Likely cause | Fix | |---------|--------------|-----| -| `ERROR: GitNexus devcontainer requires WSL2 on Windows 11` from `initializeCommand` | You opened the repo from a Windows-native path; `cmd.exe` has no `$HOME`, so the bind mounts can't resolve | Clone the repo inside WSL2 and reopen — see [Windows 11 — WSL2 is required](#windows-11--wsl2-is-required) | +| `GitNexus devcontainer one-time Windows setup` banner from `initializeCommand` | First-time Windows-native Reopen-in-Container; `HOME` env var was missing | The script just ran `setx HOME "%USERPROFILE%"` for you. Close ALL VS Code windows (File → Exit) and reopen — see [Windows 11 setup](#windows-11-setup) | +| `bind source path does not exist: /.claude` (or similar) from Docker | Windows-native `HOME` env var is still missing even after one rebuild — `setx` may have failed or VS Code wasn't fully restarted | Run `setx HOME "%USERPROFILE%"` in a Windows shell manually, fully exit VS Code (check Task Manager that no `Code.exe` remains), reopen | | `EACCES` / `EPERM` writing into `~/.claude`, `~/.codex`, or `~/.cursor` inside the container | Stale state from a previous container with a different effective UID | Move the affected dir aside and let the CLI rebuild it (`mv ~/.claude ~/.claude.bak` and log in again). Long-term: WSL2 setup, which doesn't hit this class of issue | | `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 | diff --git a/.devcontainer/ensure-host-config-dirs.cjs b/.devcontainer/ensure-host-config-dirs.cjs index a5c8281eb..a320d31c3 100644 --- a/.devcontainer/ensure-host-config-dirs.cjs +++ b/.devcontainer/ensure-host-config-dirs.cjs @@ -16,39 +16,66 @@ const fs = require("fs"); const os = require("os"); const path = require("path"); -// Fail-fast on Windows-native (no $HOME set). VS Code resolves the bind -// mount sources via `${localEnv:HOME}` reading the host shell env, and -// cmd.exe on Windows has no HOME variable — so bind sources collapse to -// `/.claude`, `/.codex`, etc., and Docker rejects them with -// `bind source path does not exist`. Surface the actual root cause here -// instead of letting Docker error opaquely later. +// Windows-native auto-setup. VS Code resolves the bind-mount sources via +// `${localEnv:HOME}` reading its own process env, and Windows doesn't set +// `HOME` by default (it uses `USERPROFILE`). Without `HOME`, the bind +// sources collapse to filesystem-root paths (`/.claude`, `/.codex`, ...) +// and Docker rejects them with `bind source path does not exist`. +// +// Fix: persist `HOME=%USERPROFILE%` to the user's environment via `setx`. +// `setx` writes to `HKCU\Environment` and the new value is inherited by +// every process the user launches after — including VS Code after a +// restart. The current VS Code process can't see the update (its env was +// fixed at launch), so we instruct the user to restart VS Code once. +// +// Subsequent runs detect `HOME` is set, skip this block, and proceed +// normally. Mac/Linux/WSL hosts have `HOME` set by the shell, so this +// block is a no-op on those platforms. if (process.platform === "win32" && !process.env.HOME) { - console.error("ERROR: GitNexus devcontainer requires WSL2 on Windows 11."); - console.error(""); - console.error( - "You opened this from a Windows-native path. The host bind mounts use", - ); - console.error( - "`${localEnv:HOME}/.claude` etc., which VS Code resolves from the host shell", - ); - console.error( - "env. cmd.exe on Windows has no HOME variable, so the bind sources resolve", - ); - console.error( - "to filesystem-root paths (/.claude, /.codex, ...) and Docker rejects them.", - ); - console.error(""); - console.error("Clone the repo inside WSL2 and re-open from there:"); - console.error(" wsl"); - console.error( - " cd ~ && git clone https://github.com/abhigyanpatwari/GitNexus.git", - ); - console.error(" cd GitNexus && code ."); - console.error(""); - console.error( - "See .devcontainer/README.md - Windows 11 - WSL2 is required.", - ); - process.exit(1); + const userprofile = process.env.USERPROFILE; + if (userprofile) { + try { + require("child_process").execFileSync("setx", ["HOME", userprofile], { + stdio: "ignore", + }); + console.error(""); + console.error("=".repeat(70)); + console.error(" GitNexus devcontainer one-time Windows setup"); + console.error("=".repeat(70)); + console.error(""); + console.error(`HOME has been set to %USERPROFILE% (${userprofile}).`); + console.error( + "VS Code reads this at startup, so the current session can't pick it up.", + ); + console.error(""); + console.error( + " 1. Close ALL VS Code windows (File > Exit, not just the window).", + ); + console.error( + " 2. Reopen VS Code, open this folder, and re-run Reopen in Container.", + ); + console.error(""); + console.error( + "This is a one-time setup. Subsequent rebuilds work normally.", + ); + console.error("=".repeat(70)); + process.exit(1); + } catch (err) { + console.error("ERROR: failed to set HOME automatically: " + err.message); + console.error(""); + console.error("Run this in a Windows shell, then restart VS Code:"); + console.error(' setx HOME "%USERPROFILE%"'); + process.exit(1); + } + } else { + console.error("ERROR: neither HOME nor USERPROFILE is set on this host."); + console.error(""); + console.error( + "Set HOME to your user profile directory and restart VS Code:", + ); + console.error(' setx HOME "%USERPROFILE%"'); + process.exit(1); + } } const home = os.homedir();