diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 327762491..71e745ca8 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -24,6 +24,8 @@ Keep the host folder and selected configuration path stable: moving either can c The default workspace is `gitnexus-source-${devcontainerId}:/workspace`. The host `initializeCommand` runs `initialize-volumes.cjs`, starts a disposable helper with volume mounts only, and streams a tar archive through [Docker's copy API](https://docs.docker.com/reference/cli/docker/container/cp/). The helper uses the Dockerfile's digest-pinned base image. It is removed afterward; volumes and host files remain. +The workspace volume is declared in `mounts`, with `workspaceMount` set to an empty string to disable the automatic host mount. This lets the Dev Containers CLI substitute `${devcontainerId}` before starting Docker; affected CLI versions leave it literal when the volume is declared in `workspaceMount`. Default and generated profiles retain their existing volume names, so this placement does not trigger another source import. + The import preserves `.git`, branches, local commits, the index, stashes, uncommitted and ignored files, checkout-local indexes, and nested worktrees. Generated `node_modules`, `.venv`, `.husky/_`, `.gitnexus/parse-cache`, `.gitnexus/parse-cache.json`, and `.gitnexus/parsedfile-cache` paths are excluded at every depth, including under `.worktrees` and `.claude/worktrees`. GitNexus graph databases, embeddings, metadata, and runners are kept; parser caches regenerate on the next analysis. Git metadata is preserved except for source-volume completion markers. Reinstall dependencies inside each worktree as needed, and recreate Python environments with the evaluation project's `uv run` commands. Host files are left intact. Existing dependency, index-store, AI configuration, history, and session volume names stay unchanged. During archive transfers, `[volumes]` reports elapsed time, entries queued, excluded paths, symlinks, and MiB streamed to Docker every five seconds, followed by a completion or failure line. Streamed bytes measure data sent to Docker, not bytes committed to disk; there is no percentage because the total is not pre-scanned. Helper startup, the second copy into the source volume, Git normalization, snapshot finalization, and cleanup also report activity every five seconds. Output from the Linux helper is shown as it arrives. diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 542367e38..38f730db6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -41,8 +41,9 @@ "updateRemoteUserUID": true, // Reap orphaned subprocesses from CLI workers and test runs. "init": true, - // Keep the host folder/config path stable so existing volume IDs are reused. - "workspaceMount": "source=gitnexus-source-${devcontainerId},target=/workspace,type=volume", + // Disable the automatic workspace mount. Declare it in mounts below so the + // Dev Containers CLI substitutes devcontainerId before passing it to Docker. + "workspaceMount": "", "workspaceFolder": "/workspace", // Existing writable config, session, dependency and cache volumes retain @@ -50,6 +51,7 @@ // Snapshot credentials seed missing files only; container logins persist. // Session volumes overlay config volumes so resetting config preserves history. "mounts": [ + "source=gitnexus-source-${devcontainerId},target=/workspace,type=volume", "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", diff --git a/.devcontainer/profile.cjs b/.devcontainer/profile.cjs index 2695e3b01..c58c50da2 100644 --- a/.devcontainer/profile.cjs +++ b/.devcontainer/profile.cjs @@ -69,8 +69,8 @@ function profile( ); result = replaceOnce( result, - /"workspaceMount": "[^"\n]+"/, - '"workspaceMount": "source=gitnexus-workspace-${devcontainerId},target=/workspace,type=volume"', + /"source=gitnexus-source-\$\{devcontainerId\},target=\/workspace,type=volume"/, + '"source=gitnexus-workspace-${devcontainerId},target=/workspace,type=volume"', ); result = replaceOnce( result, diff --git a/.devcontainer/profiles.test.cjs b/.devcontainer/profiles.test.cjs index 0f843148d..d26692bb3 100644 --- a/.devcontainer/profiles.test.cjs +++ b/.devcontainer/profiles.test.cjs @@ -15,7 +15,7 @@ const parse = (text) => { return result.config; }; -test('every runtime profile has only volume mounts and no 9p capability or hook', () => { +test('runtime profiles use the substituted mounts list and disable the automatic workspace mount', () => { for (const text of [ source, ...[ @@ -23,10 +23,18 @@ test('every runtime profile has only volume mounts and no 9p capability or hook' { native: true }, { ui: true, playwright: '1.63.0' }, { native: true, image: 'example/toolchain' }, + { native: true, ui: true, playwright: '1.63.0' }, + { ui: true, image: 'example/toolchain' }, + { native: true, ui: true, image: 'example/toolchain' }, ].map((options) => profile(source, options)), ]) { const config = parse(text); - for (const mount of [config.workspaceMount, ...config.mounts]) { + assert.equal(config.workspaceMount, ''); + assert.equal( + config.mounts.filter((mount) => mount.split(',').includes('target=/workspace')).length, + 1, + ); + for (const mount of config.mounts) { assert.match(mount, /(?:^|,)type=volume(?:,|$)/); assert.doesNotMatch(mount, /localEnv|type=bind|workspace-host/); } @@ -44,9 +52,9 @@ test('default, UI and prebuilt profiles seed local work into a source volume', ( profile(source, { image: 'example/toolchain' }), ]) { const c = parse(text); - assert.equal( - c.workspaceMount, - 'source=gitnexus-source-${devcontainerId},target=/workspace,type=volume', + assert.deepEqual( + c.mounts.filter((mount) => mount.split(',').includes('target=/workspace')), + ['source=gitnexus-source-${devcontainerId},target=/workspace,type=volume'], ); assert.equal(c.workspaceFolder, '/workspace'); assert.equal( @@ -66,11 +74,11 @@ test('native/UI profile inherits hooks, storage and defaults while changing work const c = parse( profile(source, { native: true, ui: true, playwright: '1.63.0', ref: 'feature/test' }), ); - assert.match(c.workspaceMount, /type=volume/); + assert.equal(c.workspaceMount, ''); assert.match(c.onCreateCommand, /bootstrap-native/); - assert.equal( - c.workspaceMount, - 'source=gitnexus-workspace-${devcontainerId},target=/workspace,type=volume', + assert.deepEqual( + c.mounts.filter((mount) => mount.split(',').includes('target=/workspace')), + ['source=gitnexus-workspace-${devcontainerId},target=/workspace,type=volume'], ); assert.equal( c.mounts.some((m) => m.includes('target=/workspace-host,')),