Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/nix.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ dist/
!browser/dist/
*.tsbuildinfo
test-results/
result
result-*
.env*
.claude/
pty.toml
Expand Down
82 changes: 82 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

155 changes: 155 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
{
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 <<EOF
#!${pkgs.runtimeShell}
exec ${nodejs}/bin/node --experimental-strip-types \\
$out/lib/pty-relay/src/cli.ts "\$@"
EOF
chmod +x $out/bin/pty-relay

# Generate completions from the binary we just built, so they can
# never lag the shipped command surface.
installShellCompletion --cmd pty-relay \
--bash <($out/bin/pty-relay completions bash) \
--zsh <($out/bin/pty-relay completions zsh) \
--fish <($out/bin/pty-relay completions fish)

runHook postInstall
'';

meta = {
description = "Remote access to pty sessions over an end-to-end encrypted WebSocket tunnel";
homepage = "https://github.com/compoundingtech/pty-relay";
license = pkgs.lib.licenses.mit;
mainProgram = "pty-relay";
};
};
in
{
packages = {
inherit pty-relay;
default = pty-relay;
};

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
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
];
};
}
);
}
13 changes: 12 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Commands:
server --help Show public-relay subcommands
client signin --email <addr> Register this device as an account-wide client
client --help Show client subcommands
completions <shell> Print a shell completion script (bash|fish|zsh)
version Print the pty-relay version

Options:
Expand Down Expand Up @@ -243,12 +244,14 @@ async function main(): Promise<void> {
// "--help" deeper in the argv (e.g. `pty-relay send h s "see --help"`)
// isn't swallowed.
// Namespaced commands like `server` handle their own subcommand help;
// short-circuiting here would hide per-subcommand usage.
// short-circuiting here would hide per-subcommand usage. `completions`
// is excluded for the same reason — it documents its own shell list.
if (
command &&
command !== "server" &&
command !== "client" &&
command !== "local" &&
command !== "completions" &&
(args[1] === "--help" || args[1] === "-h")
) {
usage();
Expand Down Expand Up @@ -784,6 +787,14 @@ async function main(): Promise<void> {
break;
}

case "completions": {
const { cmdCompletions } = await import("./completions.ts");
// Set exitCode rather than process.exit() so the generated script is
// fully flushed when stdout is a pipe or a redirect.
process.exitCode = cmdCompletions(args.slice(1));
break;
}

case "psk-gen": {
// Print a fresh 32-byte PSK to stdout as 43-char URL-safe-base64.
// Output is the bare base64 string — no banner, no newline-fence
Expand Down
Loading
Loading