Skip to content

fix(server): serve the studio shell from an install path holding a dot-directory - #173

Merged
MoebiusX merged 1 commit into
developfrom
codex/shell-dot-path
Oct 22, 2026
Merged

MoebiusX merged 1 commit into
developfrom
codex/shell-dot-path

Conversation

@MoebiusX

Copy link
Copy Markdown
Owner

What

sendShell (server/index.mjs) answered the unbranded studio shell with res.sendFile(SHELL_FILE), where SHELL_FILE is the absolute path of studio/index.html, and passed no root. Express 5's send (1.2.1) then applies its default dotfiles: 'ignore' to every segment of the absolute path (containsDotFile(parts) → 404). So wherever the install path contains a dot-directory, GET /, GET /index.html and every SPA-fallback path answered 404 NotFoundError:

  • an nvm global install: ~/.nvm/versions/node/…/lib/node_modules/observogram
  • ~/.local/…
  • a git worktree under .claude/worktrees/…. server/test-smoke.mjs fails there at its getText(base, '/') with HTTP 404, and server/test-brand-shell.mjs DEFAULT fails too.

Fix

res.sendFile('index.html', { root: STUDIO_DIR });

With a root, send checks only the relative part (index.html) for dot segments, the same way the express.static mounts name their files. The response is unchanged: same bytes and the same ETag, Last-Modified, Cache-Control: public, max-age=0 and Accept-Ranges: bytes. The existing DEFAULT test pins this "byte for byte with the static file headers", and it still passes. The branded shell is held in memory and never went through send.

Audit of other calls: this was the only res.sendFile in server/. Both express.static mounts (/lib and the studio assets) already pass a root through serve-static. No res.download or direct send( file call exists. Every other res.send(...) sends a body, not a file.

Regression test

server/test-brand-shell.mjs DOT PATH:

  • boots a child server from <tmp>/.install/observogram, a link to the checkout (a junction on Windows, which needs no privilege), with --preserve-symlinks, so the child's import.meta.url keeps the link path and STUDIO_DIR holds the dot segment on any checkout, including CI's dot-free one;
  • first checks that the server module resolves through the link under that flag, so the test can't pass without really running from the dot path;
  • asserts every shell path answers studio/index.html byte for byte with the static file headers, and that /app.css and /lib/brand.mjs are served;
  • removes the link first, then the temp tree (unlinkSync takes the link, never the checkout).

I confirmed it fails with the old line (404) and passes with the fix. serve() in server/fixtures/serve-child.mjs gains an index option (the server module to boot, by path). DEFAULT and DOT PATH share the shell-file assertions.

Docs

docs/CHANGELOG.md ## Unreleased and a new docs/UPDATE_JOURNEY.md follow-up section both quote 1486 → 1487, which tools/test-doc-test-totals.mjs chains.

Validation

All runs are from this .claude/worktrees/… checkout, which is itself a dot path:

  • npm run lint: 0 errors. The 153 warnings were there before this change.
  • npm test: 1487 tests, 1441 pass, 0 fail, 46 skipped (the Playwright browser legs, as before).
  • No tools/lib change, so no vendor manifest or golden change.

🤖 Generated with Claude Code

…t-directory

sendShell answered the unbranded shell with res.sendFile(<absolute path to
studio/index.html>) and no root. Express 5's send applies its default
dotfiles: 'ignore' to every segment of an absolute path, so wherever the
install path holds a dot-directory (an nvm global install under
~/.nvm/…/lib/node_modules/observogram, ~/.local/…, a .claude/worktrees
checkout) GET /, GET /index.html and every SPA path answered 404
NotFoundError; server/test-smoke.mjs failed at its GET / there.

The shell is now named relative to root: STUDIO_DIR, as the static mount
names its files, so send reads only 'index.html' for a dot segment. The
bytes and headers (ETag, Last-Modified, Cache-Control, Accept-Ranges) are
unchanged; the DEFAULT test pins them. No other res.sendFile or send in
server/ takes an absolute path: both express.static mounts pass a root.

DOT PATH in server/test-brand-shell.mjs boots a child from a link under
<tmp>/.install/ with --preserve-symlinks, so it runs from a dot path on any
checkout, and fails (404) at the parent commit. serve() takes `index`, the
server module to boot by path. Suite total 1486 -> 1487.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MoebiusX
MoebiusX merged commit f41a239 into develop Oct 22, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant