authorgravatar for github@torben.idTorben Wetter <github@torben.id> 2026-08-15 14:05:45+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-15 12:05:45+00:00
log9d0f2fc212858290e79710b44c0635a28dd92c2f
tree2c2b145caa5a6cbf2405de2e97ac95686adb6dff
parente9e46972fbabdee0eb39cbd86470a5b5f5f26910
signature Signed by PGP key B5690EEEBB952194

fix(ssh): surface a failed remote t3 install instead of a silent 0-byte server.log (#5132)


2 files changed, 30 insertions(+), 1 deletions(-)

packages/ssh/src/tunnel.test.ts+9
...@@ -108,6 +108,9 @@ describe("ssh tunnel scripts", () => {...@@ -108,6 +108,9 @@ describe("ssh tunnel scripts", () => {
108 assert.include(script, "exec npx --yes 't3@latest' \"$@\"");108 assert.include(script, "exec npx --yes 't3@latest' \"$@\"");
109 assert.include(script, "exec npm exec --yes 't3@latest' -- \"$@\"");109 assert.include(script, "exec npm exec --yes 't3@latest' -- \"$@\"");
110 assert.include(script, "could not install 't3@latest'");110 assert.include(script, "could not install 't3@latest'");
111 assert.include(script, "require_installed_t3_cli npx --yes --package 't3@latest'");
112 assert.include(script, "require_installed_t3_cli npm exec --yes --package 't3@latest'");
113 assert.include(script, "npm produced no t3 executable");
111 assert.include(script, 'prepend_path_if_dir "$HOME/.local/bin"');114 assert.include(script, 'prepend_path_if_dir "$HOME/.local/bin"');
112 assert.include(script, `T3_NODE_ENGINE_RANGE='${TEST_NODE_ENGINE_RANGE}'`);115 assert.include(script, `T3_NODE_ENGINE_RANGE='${TEST_NODE_ENGINE_RANGE}'`);
113 assert.include(script, "remote_node_satisfies_engine()");116 assert.include(script, "remote_node_satisfies_engine()");
...@@ -140,6 +143,10 @@ describe("ssh tunnel scripts", () => {...@@ -140,6 +143,10 @@ describe("ssh tunnel scripts", () => {
140143
141 assert.include(script, "exec npx --yes 't3@nightly; touch /tmp/t3-owned' \"$@\"");144 assert.include(script, "exec npx --yes 't3@nightly; touch /tmp/t3-owned' \"$@\"");
142 assert.include(script, "exec npm exec --yes 't3@nightly; touch /tmp/t3-owned' -- \"$@\"");145 assert.include(script, "exec npm exec --yes 't3@nightly; touch /tmp/t3-owned' -- \"$@\"");
146 assert.include(
147 script,
148 "require_installed_t3_cli npx --yes --package 't3@nightly; touch /tmp/t3-owned'",
149 );
143 assert.notInclude(script, "exec npx --yes t3@nightly; touch /tmp/t3-owned");150 assert.notInclude(script, "exec npx --yes t3@nightly; touch /tmp/t3-owned");
144 });151 });
145152
...@@ -185,6 +192,8 @@ describe("ssh tunnel scripts", () => {...@@ -185,6 +192,8 @@ describe("ssh tunnel scripts", () => {
185 assert.notInclude(buildRemoteLaunchScript(), "server-home");192 assert.notInclude(buildRemoteLaunchScript(), "server-home");
186 assert.include(buildRemoteLaunchScript(), "Remote T3 server did not become ready");193 assert.include(buildRemoteLaunchScript(), "Remote T3 server did not become ready");
187 assert.include(buildRemoteLaunchScript(), 'wait_ready "60000"');194 assert.include(buildRemoteLaunchScript(), 'wait_ready "60000"');
195 assert.include(buildRemoteLaunchScript(), 'if [ -s "$LOG_FILE" ]; then');
196 assert.include(buildRemoteLaunchScript(), "It wrote nothing to %s");
188 assert.include(buildRemoteLaunchScript({ packageSpec: "t3@nightly" }), "t3@nightly");197 assert.include(buildRemoteLaunchScript({ packageSpec: "t3@nightly" }), "t3@nightly");
189 assert.include(198 assert.include(
190 buildRemotePairingScript(target),199 buildRemotePairingScript(target),
packages/ssh/src/tunnel.ts+21-1
...@@ -426,10 +426,26 @@ fi...@@ -426,10 +426,26 @@ fi
426if command -v t3 >/dev/null 2>&1; then426if command -v t3 >/dev/null 2>&1; then
427 exec t3 "$@"427 exec t3 "$@"
428fi428fi
429# npm extracts a package before it runs the native builds of its dependencies,
430# so a failed build (t3 depends on node-pty, which needs a C toolchain) leaves
431# the npx cache without a t3 executable. \`npx --yes\` then exits 0 without
432# running anything at all, which the caller only ever sees as a server that
433# never becomes ready. Resolve the CLI once up front so that install failure is
434# reported here, with npm's own output on stderr.
435require_installed_t3_cli() {
436 T3_CLI_PATH="$("$@" -- sh -c 'command -v t3' || true)"
437 if [ -n "$T3_CLI_PATH" ]; then
438 return 0
439 fi
440 printf 'Remote host installed %s but npm produced no t3 executable, which usually means a native dependency (node-pty) failed to build. Install a C toolchain on the remote host (Debian/Ubuntu: build-essential, Fedora/RHEL: gcc-c++ make, macOS: xcode-select --install) and try again.\\n' @@T3_PACKAGE_SPEC@@ >&2
441 return 1
442}
429if command -v npx >/dev/null 2>&1; then443if command -v npx >/dev/null 2>&1; then
444 require_installed_t3_cli npx --yes --package @@T3_PACKAGE_SPEC@@ || exit 1
430 exec npx --yes @@T3_PACKAGE_SPEC@@ "$@"445 exec npx --yes @@T3_PACKAGE_SPEC@@ "$@"
431fi446fi
432if command -v npm >/dev/null 2>&1; then447if command -v npm >/dev/null 2>&1; then
448 require_installed_t3_cli npm exec --yes --package @@T3_PACKAGE_SPEC@@ || exit 1
433 exec npm exec --yes @@T3_PACKAGE_SPEC@@ -- "$@"449 exec npm exec --yes @@T3_PACKAGE_SPEC@@ -- "$@"
434fi450fi
435printf 'Remote host is missing the t3 CLI and could not install @@T3_PACKAGE_SPEC@@ because node/npm/npx are unavailable on PATH. Install Node or configure a supported version manager for non-interactive shells.\\n' >&2451printf 'Remote host is missing the t3 CLI and could not install @@T3_PACKAGE_SPEC@@ because node/npm/npx are unavailable on PATH. Install Node or configure a supported version manager for non-interactive shells.\\n' >&2
...@@ -581,7 +597,11 @@ if [ -z "$REMOTE_PORT" ]; then...@@ -581,7 +597,11 @@ if [ -z "$REMOTE_PORT" ]; then
581 printf 'managed\\n' >"$MANAGED_FILE"597 printf 'managed\\n' >"$MANAGED_FILE"
582 if ! wait_ready "@@T3_READY_TIMEOUT_MS@@"; then598 if ! wait_ready "@@T3_READY_TIMEOUT_MS@@"; then
583 printf 'Remote T3 server did not become ready on 127.0.0.1:%s.\\n' "$REMOTE_PORT" >&2599 printf 'Remote T3 server did not become ready on 127.0.0.1:%s.\\n' "$REMOTE_PORT" >&2
584 tail -n 80 "$LOG_FILE" >&2 2>/dev/null || true600 if [ -s "$LOG_FILE" ]; then
601 tail -n 80 "$LOG_FILE" >&2 2>/dev/null || true
602 else
603 printf 'It wrote nothing to %s, so it exited before producing any output.\\n' "$LOG_FILE" >&2
604 fi
585 kill "$REMOTE_PID" 2>/dev/null || true605 kill "$REMOTE_PID" 2>/dev/null || true
586 wait_for_pid_exit "$REMOTE_PID"606 wait_for_pid_exit "$REMOTE_PID"
587 rm -f "$PID_FILE" "$PORT_FILE" "$MANAGED_FILE"607 rm -f "$PID_FILE" "$PORT_FILE" "$MANAGED_FILE"