feat(nix): add a self-contained flake building pty-relay from source - #36
Merged
schickling merged 3 commits intoJul 20, 2026
Conversation
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
marked this pull request as ready for review
July 20, 2026 20:20
schickling
merged commit Jul 20, 2026
6924ad0
into
schickling-assistant/2026-07-20-completions
1 check passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #35 (the
completionsgenerator, which this flake calls atbuild 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
fetchFromGitHuba pinned rev with ahand-maintained source hash, so packaging always lagged the code. In-repo
it builds from
selfand tracks the checkout it ships with.What
A self-contained
flake.nixat the repo root. Inputs arenixpkgs,flake-utilsand the siblingptyflake (nixpkgs follows) — no privatetooling — so
nix buildworks 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
@myobie/pty; thepackage is
@compoundingtech/ptysince the org rename.file:../ptylinkdependency.
npm ciresolves that to a dangling symlink inside thesandbox and succeeds, so the wrapper's
--legacy-peer-depsis obsoleteand is dropped — that flag only ever affected peer resolution.
installPhasereplaces the dangling link with the store path from theptyflake, expressed as a declarative npm-name -> store-path attrsetrather than ad-hoc
ln -slines.nodejsbinding feeds both the derivation andthe bin shim. The wrapper hardcoded
nodejs_24in the shim while leavingthe derivation on the stdenv default.
running the just-built binary and installed via
installShellCompletion(with
installShellFilesinnativeBuildInputs), so packaged completionscannot lag the CLI. This is what feat(cli): add
pty-relay completions <shell>(bash/fish/zsh) #35 unblocks.package.json. Build identity beyond the semver is theorg's shared build-identity contract, not something this flake invents.
# TODO(rust):markers flag the three pieces a planned Rust rewritedeletes:
npmDepsHash, the sibling-symlink block, and the strip-types binshim.
Why a custom
installPhaseNot a stylistic deviation: pty-relay runs raw TypeScript under
--experimental-strip-types, and Node refuses to strip types for filesunder
node_modules/.buildNpmPackage's default install lands the treeat
lib/node_modules/<pname>, which would break that, so the package isinstalled to
lib/pty-relayinstead.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.typecheckrun against thebuilt tree. For scale, the closure is 963 MiB total and 872 MiB of that is
the
ptyinput itself, so pruning here is a small win — but it is a realfollow-up if closure size starts to matter.
CI — the repo's first
.github/workflows/nix.ymlis modeled on compoundingtech/pty's workflow(PR + push-to-main, ubuntu-latest,
DeterminateSystems/determinate-nix-action@v3)but runs
nix flake check, notnix build, sochecks.*actually gate.The flake is what makes this practical. A plain Node workflow would have to
deal with
@compoundingtech/ptybeing afile:../ptylink dependency withno sibling checkout on the runner;
npm ci"succeeds" there by leaving adangling 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 owntsc --noEmit,run against the built tree — it cannot pass against a bare checkout, where
every
@compoundingtech/ptyimport is unresolvable), and the two CLI smokechecks.
Not covered: the vitest suite. This is a real gap, not an oversight.
I tried it as a
checks.testsderivation: under the nix sandboxtest/daemon-runtime.test.tshangs indefinitely at 0/14 and blockssession-list-viewandterminalbehind it — the run was still stuckafter 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 testneeds 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.npmDepsHashissha256-wDKiIRJivnTFd0dXCdKw+GoLJA6T53a/5sDCsbxvkUU=, taken from whatthe build computed.
nix flake check— passes:checks.typecheck,checks.help,checks.completionsall green.pty-relay --helpandpty-relay completions fishboth work. Installed completion files are 5.7K / 5.1K / 34K for
bash / zsh / fish.
built tree — confirming the two
doctorfailures noted in feat(cli): addpty-relay completions <shell>(bash/fish/zsh) #35 are purelythe missing
@compoundingtech/ptylink, not a code regression.pty-relay doctoralso works and reportspty: found— it imports@compoundingtech/pty, so this exercises the sibling link end to end,beyond what the checks cover.