Skip to content

feat(nix): add a self-contained flake building pty-relay from source - #36

Merged
schickling merged 3 commits into
schickling-assistant/2026-07-20-completionsfrom
schickling-assistant/2026-07-20-nix-flake
Jul 20, 2026
Merged

feat(nix): add a self-contained flake building pty-relay from source#36
schickling merged 3 commits into
schickling-assistant/2026-07-20-completionsfrom
schickling-assistant/2026-07-20-nix-flake

Conversation

@schickling-assistant

@schickling-assistant schickling-assistant commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Stacked on #35 (the completions generator, which this flake calls at
build time). Review/merge that first; the diff here is flake.nix +
flake.lock, a CI workflow, and two housekeeping lines (.gitignore,
vitest.config.ts).

Why

pty-relay's Nix packaging lived out-of-tree in a private dotfiles wrapper.
Living outside the repo forced it to fetchFromGitHub a pinned rev with a
hand-maintained source hash, so packaging always lagged the code. In-repo
it builds from self and tracks the checkout it ships with.

What

A self-contained flake.nix at the repo root. Inputs are nixpkgs,
flake-utils and the sibling pty flake (nixpkgs follows) — no private
tooling — so nix build works standalone from a fresh clone.

Outputs: packages.{pty-relay,default},
checks.{typecheck,help,completions}, devShells.default,
meta.mainProgram = "pty-relay".

Notable differences from the out-of-tree wrapper

  • The npm dep name. The wrapper still referenced @myobie/pty; the
    package is @compoundingtech/pty since the org rename.
  • It is no longer a peerDependency. At HEAD it is a file:../pty link
    dependency. npm ci resolves that to a dangling symlink inside the
    sandbox and succeeds, so the wrapper's --legacy-peer-deps is obsolete
    and is dropped — that flag only ever affected peer resolution.
    installPhase replaces the dangling link with the store path from the
    pty flake, expressed as a declarative npm-name -> store-path attrset
    rather than ad-hoc ln -s lines.
  • Consistent Node. One nodejs binding feeds both the derivation and
    the bin shim. The wrapper hardcoded nodejs_24 in the shim while leaving
    the derivation on the stdenv default.
  • Generated completions. bash/zsh/fish are generated at build time by
    running the just-built binary and installed via installShellCompletion
    (with installShellFiles in nativeBuildInputs), so packaged completions
    cannot lag the CLI. This is what feat(cli): add pty-relay completions <shell> (bash/fish/zsh) #35 unblocks.
  • Version from package.json. Build identity beyond the semver is the
    org's shared build-identity contract, not something this flake invents.

# TODO(rust): markers flag the three pieces a planned Rust rewrite
deletes: npmDepsHash, the sibling-symlink block, and the strip-types bin
shim.

Why a custom installPhase

Not a stylistic deviation: pty-relay runs raw TypeScript under
--experimental-strip-types, and Node refuses to strip types for files
under node_modules/. buildNpmPackage's default install lands the tree
at lib/node_modules/<pname>, which would break that, so the package is
installed to lib/pty-relay instead.

One consequence worth flagging: because the default install hook is
bypassed, devDependencies are not pruned (~80 MB: typescript, happy-dom,
playwright, esbuild). That is what lets checks.typecheck run against the
built tree. For scale, the closure is 963 MiB total and 872 MiB of that is
the pty input itself, so pruning here is a small win — but it is a real
follow-up if closure size starts to matter.

CI — the repo's first

.github/workflows/nix.yml is modeled on compoundingtech/pty's workflow
(PR + push-to-main, ubuntu-latest, DeterminateSystems/determinate-nix-action@v3)
but runs nix flake check, not nix build, so checks.* actually gate.

The flake is what makes this practical. A plain Node workflow would have to
deal with @compoundingtech/pty being a file:../pty link dependency with
no sibling checkout on the runner; npm ci "succeeds" there by leaving a
dangling symlink, so anything touching the daemon fails at runtime. Flake
inputs resolve the sibling properly, so CI exercises a real install.

What CI does and does not cover

Covered: nix build, checks.typecheck (the repo's own tsc --noEmit,
run against the built tree — it cannot pass against a bare checkout, where
every @compoundingtech/pty import is unresolvable), and the two CLI smoke
checks.

Not covered: the vitest suite. This is a real gap, not an oversight.
I tried it as a checks.tests derivation: under the nix sandbox
test/daemon-runtime.test.ts hangs indefinitely at 0/14 and blocks
session-list-view and terminal behind it — the run was still stuck
after 25 minutes, with the other 69/72 files and 764 tests passing. The
same suite is fully green against the same built tree outside the sandbox.
Gating npm test needs that file made sandbox-safe (or excluded) first,
so I left it out rather than ship a hanging or flaky gate.

Verification

Run on x86_64-linux:

  • nix build .#default — succeeds. npmDepsHash is
    sha256-wDKiIRJivnTFd0dXCdKw+GoLJA6T53a/5sDCsbxvkUU=, taken from what
    the build computed.
  • nix flake check — passes: checks.typecheck, checks.help,
    checks.completions all green.
  • From the built output: pty-relay --help and pty-relay completions fish
    both work. Installed completion files are 5.7K / 5.1K / 34K for
    bash / zsh / fish.
  • With the sibling link resolved, the full vitest suite passes against the
    built tree — confirming the two doctor failures noted in feat(cli): add pty-relay completions <shell> (bash/fish/zsh) #35 are purely
    the missing @compoundingtech/pty link, not a code regression.
  • pty-relay doctor also works and reports pty: found — it imports
    @compoundingtech/pty, so this exercises the sibling link end to end,
    beyond what the checks cover.

schickling-assistant and others added 3 commits July 20, 2026 17:12
Packaging lived out-of-tree (in a dotfiles wrapper) and therefore had to
fetchFromGitHub a pinned rev. In-repo it builds from `self`, so the flake
tracks the checkout it ships with.

Inputs are nixpkgs, flake-utils and the sibling `pty` flake only — no
private tooling — so `nix build` works standalone.

Notes on what changed versus the out-of-tree wrapper:
- The npm name is `@compoundingtech/pty`, and it is now a `file:../pty`
  link dependency rather than a peerDependency. `npm ci` resolves it to a
  dangling symlink in the sandbox, so no `--legacy-peer-deps` is needed;
  installPhase replaces the link with the store path from the `pty` flake.
  The mapping is a declarative npm-name -> store-path attrset.
- One `nodejs` binding feeds both the derivation and the bin shim, so a
  build and a run cannot disagree on the interpreter.
- Completions are generated at build time from the just-built binary and
  installed via installShellCompletion, so they cannot lag the CLI.

Verified: `nix build .#default` and `nix flake check` pass, and
`pty-relay --help`, `pty-relay completions fish` and `pty-relay doctor`
(which resolves the sibling pty link) all work from the built output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T1xLDkqUDCYUMMADdMqVgP
agent-session-id: 312caff5-3274-4d97-baa4-8ff06ab03fc5
agent-tool: Claude Code
agent-tool-version: 2.1.215
agent-model: claude-opus-4-8
agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json
agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json
tooling-profile: dotfiles@unknown-dirty
`nix build` drops a `result` symlink holding a full copy of the source
tree, so `vitest run` collected and ran every test twice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T1xLDkqUDCYUMMADdMqVgP
agent-session-id: 312caff5-3274-4d97-baa4-8ff06ab03fc5
agent-tool: Claude Code
agent-tool-version: 2.1.215
agent-model: claude-opus-4-8
agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json
agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json
tooling-profile: dotfiles@unknown-dirty
pty-relay had no CI. Rather than a bespoke Node workflow — awkward while
`@compoundingtech/pty` is a `file:../pty` link dependency with no sibling
checkout on the runner — the flake is the gate: its inputs resolve the
sibling from the `pty` flake.

Modeled on compoundingtech/pty's .github/workflows/nix.yml, but running
`nix flake check` rather than `nix build` so checks.* actually gate.

Also adds checks.typecheck, running the repo's own `tsc --noEmit` against
the built tree (where the sibling resolves; it cannot pass against a bare
checkout).

The vitest suite is deliberately not gated: under the nix sandbox
test/daemon-runtime.test.ts hangs indefinitely at 0/14 and blocks two more
files, while the other 69/72 pass. The same suite is fully green against
the built tree outside the sandbox.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T1xLDkqUDCYUMMADdMqVgP
agent-session-id: 312caff5-3274-4d97-baa4-8ff06ab03fc5
agent-tool: Claude Code
agent-tool-version: 2.1.215
agent-model: claude-opus-4-8
agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json
agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json
tooling-profile: dotfiles@unknown-dirty
@schickling
schickling marked this pull request as ready for review July 20, 2026 20:20
@schickling
schickling merged commit 6924ad0 into schickling-assistant/2026-07-20-completions Jul 20, 2026
1 check 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.

2 participants