From 1463d20d996f8bf8024f37a4c0da23b18ba39b8f Mon Sep 17 00:00:00 2001 From: schickling-assistant <261620128+schickling-assistant@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:12:46 +0200 Subject: [PATCH 1/3] feat(nix): add a self-contained flake building pty-relay from source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 --- .gitignore | 2 + flake.lock | 82 ++++++++++++++++++++++++++++++++ flake.nix | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.gitignore b/.gitignore index 44d7e4a..edf660d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ dist/ !browser/dist/ *.tsbuildinfo test-results/ +result +result-* .env* .claude/ pty.toml diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..f1ba415 --- /dev/null +++ b/flake.lock @@ -0,0 +1,82 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1784497964, + "narHash": "sha256-vlHUuqAcbcH2RKmHbPiuQzbv1pnzzavXnI62RD0bqCU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "241313f4e8e508cb9b13278c2b0fa25b9ca27163", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "pty": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1784556029, + "narHash": "sha256-52SqV7ANR/r+gqGNo/e2JUmQImpmCgmMUScd0KZlTW0=", + "owner": "compoundingtech", + "repo": "pty", + "rev": "0a9be8c06bd9e3f0c1acbb34784762075ef53eb3", + "type": "github" + }, + "original": { + "owner": "compoundingtech", + "repo": "pty", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "pty": "pty" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..fb271d3 --- /dev/null +++ b/flake.nix @@ -0,0 +1,136 @@ +{ + description = "pty-relay — remote access to pty sessions over an end-to-end encrypted WebSocket tunnel"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + pty.url = "github:compoundingtech/pty"; + pty.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = + { + self, + nixpkgs, + flake-utils, + pty, + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = import nixpkgs { inherit system; }; + + # Node runtime for both the derivation's npm steps and the bin shim, + # so a build and a run can never disagree on the interpreter. + nodejs = pkgs.nodejs_24; + + # Unpublished sibling packages, resolved from their own flakes and + # linked into node_modules at install time. npm records + # `@compoundingtech/pty` as `file:../pty`, which is a dangling link + # inside the sandbox; these store paths are what actually resolve. + # TODO(rust): the Rust rewrite links this natively — drop the map. + siblingPackages = { + "@compoundingtech/pty" = "${pty.packages.${system}.default}/lib/pty"; + }; + + linkSiblings = pkgs.lib.concatStringsSep "\n" ( + pkgs.lib.mapAttrsToList (name: path: '' + mkdir -p "$out/lib/pty-relay/node_modules/${builtins.dirOf name}" + rm -rf "$out/lib/pty-relay/node_modules/${name}" + ln -s ${path} "$out/lib/pty-relay/node_modules/${name}" + '') siblingPackages + ); + + # Single source of truth: package.json. Build identity beyond the + # semver (commit rev, build date) is the org's shared build-identity + # contract, not something this flake invents. + version = (builtins.fromJSON (builtins.readFile ./package.json)).version; + + pty-relay = pkgs.buildNpmPackage { + pname = "pty-relay"; + inherit version nodejs; + + src = self; + + # TODO(rust): cargo's lockfile is content-addressed; this vendoring + # hash disappears with the npm dependency tree. + # Regenerate with: nix run nixpkgs#prefetch-npm-deps -- package-lock.json + npmDepsHash = "sha256-wDKiIRJivnTFd0dXCdKw+GoLJA6T53a/5sDCsbxvkUU="; + + # pty-relay ships as raw TypeScript executed by Node with native + # type stripping — no compile step. Only the browser bundle has one, + # and the daemon/CLI don't need it. + dontNpmBuild = true; + + nativeBuildInputs = [ pkgs.installShellFiles ]; + + # Installed outside node_modules so Node's type-stripping works on + # src/cli.ts (Node refuses to strip types inside node_modules). + installPhase = '' + runHook preInstall + + mkdir -p $out/lib/pty-relay + cp -r . $out/lib/pty-relay + + # TODO(rust): sibling linking is an npm-workspace workaround. + ${linkSiblings} + + # TODO(rust): a compiled binary needs no interpreter shim. + mkdir -p $out/bin + cat > $out/bin/pty-relay < /dev/null + touch $out + ''; + + completions = pkgs.runCommand "pty-relay-completions" { } '' + export HOME=$(mktemp -d) + for shell in bash zsh fish; do + ${pty-relay}/bin/pty-relay completions $shell > script + test -s script || { echo "empty $shell completions"; exit 1; } + done + touch $out + ''; + }; + + devShells.default = pkgs.mkShell { + packages = [ + nodejs + pty.packages.${system}.default + ]; + }; + } + ); +} From c8c197e593fcf04b893ac87077e70841ea028a89 Mon Sep 17 00:00:00 2001 From: schickling-assistant <261620128+schickling-assistant@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:15:17 +0200 Subject: [PATCH 2/3] chore(test): exclude the nix `result` symlink from vitest collection `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 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 --- vitest.config.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vitest.config.ts b/vitest.config.ts index b08afcb..8f9c51c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,7 +3,9 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { globals: true, - exclude: ["integration/**", "node_modules/**"], + // `result` is the nix build symlink; it contains a full copy of the + // source tree, so without this every test would be collected twice. + exclude: ["integration/**", "node_modules/**", "result/**"], setupFiles: ["./test/setup.ts"], globalSetup: ["./test/setup/vitest-global.ts"], }, From 892c3202b63f5c366d952e658f867a7c99ff9d11 Mon Sep 17 00:00:00 2001 From: schickling-assistant <261620128+schickling-assistant@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:51:26 +0200 Subject: [PATCH 3/3] ci: gate on `nix flake check` (first CI for this repo) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 --- .github/workflows/nix.yml | 15 +++++++++++++++ flake.nix | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 .github/workflows/nix.yml diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 0000000..a0aed28 --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,15 @@ +name: Nix +on: + pull_request: + push: + branches: [main] + +jobs: + check: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/determinate-nix-action@v3 + # `nix flake check` rather than `nix build`, so the checks.* gate too. + - run: nix flake check --print-build-logs diff --git a/flake.nix b/flake.nix index fb271d3..452d3cb 100644 --- a/flake.nix +++ b/flake.nix @@ -109,6 +109,25 @@ }; checks = { + # The repo's own `tsc --noEmit`. It only passes once + # @compoundingtech/pty resolves, which is exactly what the built + # output provides — so run it against that rather than the raw src. + typecheck = pkgs.runCommand "pty-relay-typecheck" { } '' + export HOME=$(mktemp -d) + cp -r ${pty-relay}/lib/pty-relay tree + chmod -R u+w tree + cd tree + ${nodejs}/bin/node node_modules/typescript/bin/tsc --noEmit + touch $out + ''; + + # NOTE: the vitest suite is deliberately NOT a check. It runs + # green against this same built tree outside the sandbox, but under + # the nix sandbox test/daemon-runtime.test.ts hangs indefinitely + # (0/14, blocking session-list-view and terminal); the other 69/72 + # files pass. Gating `npm test` needs that file made sandbox-safe + # first, so CI covers typecheck + the CLI smoke checks only. + help = pkgs.runCommand "pty-relay-help" { } '' export HOME=$(mktemp -d) ${pty-relay}/bin/pty-relay --help > /dev/null