Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package bun-pty for openSUSE:Factory checked in at 2026-10-15 16:12:31 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/bun-pty (Old) and /work/SRC/openSUSE:Factory/.bun-pty.new.3698736 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "bun-pty" Thu Oct 15 16:12:31 2026 rev:4 rq:1384903 version:0.5.1 Changes: -------- --- /work/SRC/openSUSE:Factory/bun-pty/bun-pty.changes 2026-10-01 16:45:35.647391938 +0200 +++ /work/SRC/openSUSE:Factory/.bun-pty.new.3698736/bun-pty.changes 2026-10-15 16:12:56.560022511 +0200 @@ -1,0 +2,14 @@ +Thu Oct 15 05:02:21 UTC 2026 - Martin Pluskal <[email protected]> + +- Update to version 0.5.1: + * Breaking: env is the child's whole environment as in node-pty, + no longer merged over the parent's (#48); spread process.env + to keep inherited variables + * Build the child environment as node-pty builds it: TERM from + name, PWD from cwd; drop tmux/COLUMNS/LINES on inherit (#49) + * Leave src/*.test.ts out of the npm package (#50) + * Linux ABI unchanged: same eight dlsym entry points, same + private librust_pty.so path; no consumer rebuild needed + * Refresh the vendored dependencies + +------------------------------------------------------------------- Old: ---- bun-pty-0.4.11.tar.gz New: ---- bun-pty-0.5.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ bun-pty.spec ++++++ --- /var/tmp/diff_new_pack.9mJdGY/_old 2026-10-15 16:12:57.781073603 +0200 +++ /var/tmp/diff_new_pack.9mJdGY/_new 2026-10-15 16:12:57.783073687 +0200 @@ -17,7 +17,7 @@ Name: bun-pty -Version: 0.4.11 +Version: 0.5.1 Release: 0 Summary: Pseudo-terminal library for Bun FFI consumers License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.9mJdGY/_old 2026-10-15 16:12:57.826075486 +0200 +++ /var/tmp/diff_new_pack.9mJdGY/_new 2026-10-15 16:12:57.828075570 +0200 @@ -1,6 +1,6 @@ <services> <service name="cargo_vendor" mode="manual"> - <param name="srctar">bun-pty-0.4.11.tar.gz</param> + <param name="srctar">bun-pty-0.5.1.tar.gz</param> <param name="cargotoml">rust-pty/Cargo.toml</param> <param name="compression">zst</param> </service> ++++++ bun-pty-0.4.11.tar.gz -> bun-pty-0.5.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bun-pty-0.4.11/.github/workflows/test.yml new/bun-pty-0.5.1/.github/workflows/test.yml --- old/bun-pty-0.4.11/.github/workflows/test.yml 2026-09-30 07:52:47.000000000 +0200 +++ new/bun-pty-0.5.1/.github/workflows/test.yml 2026-10-12 08:44:31.000000000 +0200 @@ -77,7 +77,7 @@ run: bun run test:unit - name: Run unit tests with coverage - run: bun test --coverage --coverage-reporter=lcov src/interfaces.test.ts src/terminal.test.ts src/index.test.ts + run: bun test --coverage --coverage-reporter=lcov src/interfaces.test.ts src/terminal.test.ts src/index.test.ts src/env.test.ts - name: Run integration tests run: bun run test:integration diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bun-pty-0.4.11/CHANGELOG.md new/bun-pty-0.5.1/CHANGELOG.md --- old/bun-pty-0.4.11/CHANGELOG.md 2026-09-30 07:52:47.000000000 +0200 +++ new/bun-pty-0.5.1/CHANGELOG.md 2026-10-12 08:44:31.000000000 +0200 @@ -5,6 +5,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.1] - 2026-10-12 + +### Fixed +- Leave the `src/*.test.ts` files out of the npm package (#50) + - `"files"` in `package.json` lists all of `src`, and npm does not apply `.npmignore` patterns to the paths it lists, so earlier tarballs included the tests + +## [0.5.0] - 2026-10-12 + +### Changed +- **Breaking:** `env` is now the child's whole environment, as in node-pty, instead of being merged over the parent's (#48) + - To keep inherited variables, spread `process.env`: `env: { ...process.env, FOO: "bar" }` + - Without `env`, the child gets the current `process.env`, so changes made at runtime reach it (before, it got the environment the process started with) + - When the map has no `PATH`, a bare program name such as `sh` is still found with the parent's `PATH`, which does not reach the child; on Unix, `argv[0]` is then the resolved path + - On Windows, a map with no entries keeps the parent's environment, as in node-pty +- On Unix, the child's environment is now built as node-pty builds it (#49) + - `TERM` is set from the `name` option, then from `TERM` in the environment, then "xterm"; `name` was ignored before + - `PWD` is set from `cwd` + - When the child inherits `process.env`, the parent's tmux and screen variables (`TMUX`, `TMUX_PANE`, `STY`, `WINDOW`, `WINDOWID`, `TERMCAP`) and its `COLUMNS` and `LINES` are dropped +- `IPtyForkOptions.name` is optional, and `env` accepts `undefined` values, which are skipped, so `env: process.env` type-checks (#49) + +### Fixed +- Variables left out of `env`, such as tokens, no longer reach the child (#48) + - Fixes #47 + ## [0.4.11] - 2026-09-30 ### Added diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bun-pty-0.4.11/README.md new/bun-pty-0.5.1/README.md --- old/bun-pty-0.4.11/README.md 2026-09-30 07:52:47.000000000 +0200 +++ new/bun-pty-0.5.1/README.md 2026-10-12 08:44:31.000000000 +0200 @@ -146,11 +146,11 @@ - `file`: The executable to launch - `args`: Arguments to pass to the executable - `options`: Configuration options - - `name`: Terminal name (e.g., "xterm-256color") + - `name`: Terminal type (e.g., "xterm-256color"), set as `TERM` in the child's environment on Unix (default: `TERM` from `env`, then "xterm") - `cols`: Number of columns (default: 80) - `rows`: Number of rows (default: 24) - `cwd`: Working directory (default: process.cwd()) - - `env`: Environment variables + - `env`: The child's whole environment, as in node-pty (default: `process.env`). It replaces the parent's environment, so pass `{ ...process.env, FOO: "bar" }` to add variables. Entries whose value is `undefined` are skipped. On Windows, a map with no entries keeps the parent's environment, as in node-pty. On Unix, `PWD` is set from `cwd`, and when the child inherits `process.env`, variables from the parent's tmux or screen session (`TMUX`, `STY`, ...) are dropped Returns an `IPty` instance. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bun-pty-0.4.11/package.json new/bun-pty-0.5.1/package.json --- old/bun-pty-0.4.11/package.json 2026-09-30 07:52:47.000000000 +0200 +++ new/bun-pty-0.5.1/package.json 2026-10-12 08:44:31.000000000 +0200 @@ -1,6 +1,6 @@ { "name": "bun-pty", - "version": "0.4.11", + "version": "0.5.1", "description": "Cross-platform pseudoterminal (PTY) implementation for Bun with native performance", "main": "./src/index.ts", "types": "./dist/index.d.ts", @@ -47,10 +47,10 @@ "build:ts": "tsc --emitDeclarationOnly --declaration --outDir dist", "build": "bun run build:rust && bun run build:ts", "test": "bun test", - "test:unit": "bun test src/interfaces.test.ts src/terminal.test.ts src/index.test.ts", + "test:unit": "bun test src/interfaces.test.ts src/terminal.test.ts src/index.test.ts src/env.test.ts", "test:integration": "RUN_INTEGRATION_TESTS=true bun test src/terminal.integration.test.ts", "test:all": "bun run test:unit && bun run test:integration", - "test:coverage": "bun test --coverage src/interfaces.test.ts src/terminal.test.ts src/index.test.ts", + "test:coverage": "bun test --coverage src/interfaces.test.ts src/terminal.test.ts src/index.test.ts src/env.test.ts", "clean": "rm -rf ./dist && cd rust-pty && cargo clean" }, "engines": { @@ -58,6 +58,7 @@ }, "files": [ "src", + "!src/**/*.test.ts", "dist", "README.md", "LICENSE", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bun-pty-0.4.11/rust-pty/src/lib.rs new/bun-pty-0.5.1/rust-pty/src/lib.rs --- old/bun-pty-0.4.11/rust-pty/src/lib.rs 2026-09-30 07:52:47.000000000 +0200 +++ new/bun-pty-0.5.1/rust-pty/src/lib.rs 2026-10-12 08:44:31.000000000 +0200 @@ -8,9 +8,10 @@ use shell_words::split; // <-- NEW use std::{ collections::HashMap, - ffi::CStr, + ffi::{CStr, OsStr, OsString}, io::{Read, Write}, os::raw::{c_char, c_int}, + path::PathBuf, sync::{ atomic::{AtomicBool, AtomicI32, Ordering}, Arc, Mutex, @@ -91,7 +92,21 @@ } fn to_builder(&self) -> CommandBuilder { - let mut b = CommandBuilder::new(&self.cmd); + let mut b = CommandBuilder::new(self.program()); + // `CommandBuilder::new` seeds the environment from the parent's + // startup environment. The map the caller passed is the whole + // environment, as in node-pty, so drop the seeded one first. + // portable-pty still sets SHELL when the map holds no usable one. + b.env_clear(); + // On Windows an empty map keeps the parent's environment, as in + // node-pty; portable-pty would build an unterminated block for it. + // Copy the process environment, not portable-pty's seeded one, + // which also applies values from the registry. + if cfg!(windows) && self.env.is_empty() { + for (k, v) in std::env::vars_os() { + b.env(k, v); + } + } b.cwd(&self.cwd); for a in &self.args { b.arg(a); @@ -101,6 +116,71 @@ } b } + + /// The map's value for `key`. Windows matches the key without case. + fn map_var(&self, key: &str) -> Option<&str> { + self.env + .iter() + .find(|(k, _)| if cfg!(windows) { k.eq_ignore_ascii_case(key) } else { k.as_str() == key }) + .map(|(_, v)| v.as_str()) + } + + /// portable-pty looks up a relative program with the PATH in the + /// child's environment. Before `env_clear()`, a map with no PATH fell + /// back to the parent's PATH. Keep that fallback, so a bare name such + /// as `sh` or `cmd.exe` still resolves, without adding PATH to the child. + fn program(&self) -> OsString { + if self.cmd.is_empty() || self.map_var("PATH").is_some() { + return self.cmd.clone().into(); + } + std::env::var_os("PATH") + .and_then(|path| self.search_path(&path)) + .map_or_else(|| self.cmd.clone().into(), PathBuf::into_os_string) + } + + /// Mirrors portable-pty's Unix lookup: a relative program found in the + /// cwd stays as it is, else the first executable match in `path`. + #[cfg(unix)] + fn search_path(&self, path: &OsStr) -> Option<PathBuf> { + use std::{os::unix::fs::PermissionsExt, path::Path}; + + let exe = Path::new(&self.cmd); + if exe.is_absolute() || Path::new(&self.cwd).join(exe).exists() { + return None; + } + std::env::split_paths(path).map(|dir| dir.join(exe)).find(|candidate| { + candidate + .metadata() + .is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0) + }) + } + + /// Mirrors portable-pty's Windows lookup: each `path` entry with the + /// name as given, then with each PATHEXT extension. + #[cfg(windows)] + fn search_path(&self, path: &OsStr) -> Option<PathBuf> { + let pathext = self + .map_var("PATHEXT") + .map(OsString::from) + .or_else(|| std::env::var_os("PATHEXT")) + .unwrap_or_else(|| ".EXE".into()); + for dir in std::env::split_paths(path) { + let candidate = dir.join(&self.cmd); + if candidate.exists() { + return Some(candidate); + } + for ext in std::env::split_paths(&pathext) { + let Some(ext) = ext.to_str().and_then(|e| e.strip_prefix('.')) else { + continue; + }; + let candidate = dir.join(&self.cmd).with_extension(ext); + if candidate.exists() { + return Some(candidate); + } + } + } + None + } } /* ---------- async message channel ---------- */ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bun-pty-0.4.11/src/env.test.ts new/bun-pty-0.5.1/src/env.test.ts --- old/bun-pty-0.4.11/src/env.test.ts 1970-01-01 01:00:00.000000000 +0100 +++ new/bun-pty-0.5.1/src/env.test.ts 2026-10-12 08:44:31.000000000 +0200 @@ -0,0 +1,94 @@ +import { expect, test, describe } from "bun:test"; +import { DEFAULT_NAME, childEnv, formatEnv } from "./env"; + +const parent = { + PATH: "/usr/bin:/bin", + HOME: "/home/me", + TERM: "screen-256color", + PWD: "/somewhere/else", + TMUX: "/tmp/tmux-1000/default,1234,0", + TMUX_PANE: "%1", + STY: "1234.pts-0.host", + WINDOW: "0", + WINDOWID: "4194317", + TERMCAP: "SC|screen", + COLUMNS: "120", + LINES: "40", + UNSET: undefined, +}; + +describe("childEnv on Unix", () => { + test("a supplied map replaces the parent's environment", () => { + const env = childEnv({ name: "xterm-256color", env: { FOO: "bar" } }, "/work", "linux", parent); + expect(env).toEqual({ FOO: "bar", PWD: "/work", TERM: "xterm-256color" }); + }); + + test("without a map the child gets the parent's environment", () => { + const env = childEnv({ name: "xterm-256color" }, "/work", "darwin", parent); + expect(env).toEqual({ + PATH: "/usr/bin:/bin", + HOME: "/home/me", + TERM: "xterm-256color", + PWD: "/work", + }); + }); + + test("drops the parent's tmux and screen variables when the map is the parent's environment", () => { + const env = childEnv({ env: parent }, "/work", "linux", parent); + expect(Object.keys(env).sort()).toEqual(["HOME", "PATH", "PWD", "TERM"]); + }); + + test("keeps tmux and screen variables a supplied map sets", () => { + const env = childEnv({ env: { TMUX: "mine", COLUMNS: "80" } }, "/work", "linux", parent); + expect(env.TMUX).toBe("mine"); + expect(env.COLUMNS).toBe("80"); + }); + + test("TERM comes from name, then from the environment, then the default", () => { + expect(childEnv({ name: "vt100", env: { TERM: "dumb" } }, "/", "linux", parent).TERM).toBe("vt100"); + expect(childEnv({ env: { TERM: "dumb" } }, "/", "linux", parent).TERM).toBe("dumb"); + expect(childEnv({}, "/", "linux", parent).TERM).toBe("screen-256color"); + expect(childEnv({ env: {} }, "/", "linux", parent).TERM).toBe(DEFAULT_NAME); + }); + + test("PWD comes from cwd", () => { + expect(childEnv({ env: { PWD: "/old" } }, "/work", "linux", parent).PWD).toBe("/work"); + }); + + test("skips undefined values", () => { + const env = childEnv({ env: { KEEP: "1", SKIP: undefined } }, "/", "linux", parent); + expect(Object.keys(env).sort()).toEqual(["KEEP", "PWD", "TERM"]); + }); + + test("does not change the supplied map", () => { + const supplied = { FOO: "bar" }; + childEnv({ name: "xterm", env: supplied }, "/work", "linux", parent); + expect(supplied).toEqual({ FOO: "bar" }); + }); +}); + +describe("childEnv on Windows", () => { + test("passes the map as given, without TERM or PWD", () => { + const env = childEnv({ name: "xterm-256color", env: { FOO: "bar" } }, "C:\\work", "win32", parent); + expect(env).toEqual({ FOO: "bar" }); + }); + + test("keeps an empty map empty, so the native side keeps the parent's environment", () => { + expect(childEnv({ env: {} }, "C:\\work", "win32", parent)).toEqual({}); + }); + + test("without a map the child gets the parent's environment unchanged", () => { + const { UNSET, ...defined } = parent; + expect(childEnv({}, "C:\\work", "win32", parent)).toEqual(defined); + }); +}); + +describe("formatEnv", () => { + test("NUL-terminates each KEY=VALUE pair", () => { + expect(formatEnv({ A: "1", B: "x=y" })).toBe("A=1\0B=x=y\0"); + }); + + test("formats an empty environment as an empty string", () => { + expect(formatEnv({})).toBe(""); + }); +}); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bun-pty-0.4.11/src/env.ts new/bun-pty-0.5.1/src/env.ts --- old/bun-pty-0.4.11/src/env.ts 1970-01-01 01:00:00.000000000 +0100 +++ new/bun-pty-0.5.1/src/env.ts 2026-10-12 08:44:31.000000000 +0200 @@ -0,0 +1,58 @@ +// env.ts — the child's environment, kept out of terminal.ts so it can be +// tested without loading the native library + +import type { IPtyForkOptions } from "./interfaces"; + +export const DEFAULT_NAME = "xterm"; + +/** + * Variables that tie a shell to the terminal this process runs in. As in + * node-pty, they are dropped when the child inherits process.env, so a shell + * in the new PTY does not think it runs inside the parent's tmux or screen + * session, or take the parent terminal's size. + */ +const PARENT_TERMINAL_VARS = ["TMUX", "TMUX_PANE", "STY", "WINDOW", "WINDOWID", "TERMCAP", "COLUMNS", "LINES"]; + +/** + * The child's environment, as node-pty builds it. A supplied map replaces + * the parent's environment; without one the child gets `parentEnv`. On Unix, + * TERM comes from `name` and PWD from `cwd`. + * + * @param opts - The spawn options + * @param cwd - The child's working directory + * @param platform - The platform to build for + * @param parentEnv - This process's environment + * @returns The child's environment + */ +export function childEnv( + opts: Pick<IPtyForkOptions, "env" | "name">, + cwd: string, + platform: typeof process.platform = process.platform, + parentEnv: Record<string, string | undefined> = process.env, +): Record<string, string> { + const source = opts.env ?? parentEnv; + const env: Record<string, string> = {}; + for (const [key, value] of Object.entries(source)) { + if (value !== undefined) env[key] = value; + } + if (platform === "win32") return env; + + if (source === parentEnv) { + for (const key of PARENT_TERMINAL_VARS) delete env[key]; + } + env.PWD = cwd; + env.TERM = opts.name || env.TERM || DEFAULT_NAME; + return env; +} + +/** + * Format an environment for the native side: NUL-terminated KEY=VALUE pairs, + * or an empty string for an empty environment. + * + * @param env - The environment to format + * @returns The formatted environment + */ +export function formatEnv(env: Record<string, string>): string { + const pairs = Object.entries(env).map(([key, value]) => `${key}=${value}`); + return pairs.length > 0 ? `${pairs.join("\0")}\0` : ""; +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bun-pty-0.4.11/src/interfaces.ts new/bun-pty-0.5.1/src/interfaces.ts --- old/bun-pty-0.4.11/src/interfaces.ts 2026-09-30 07:52:47.000000000 +0200 +++ new/bun-pty-0.5.1/src/interfaces.ts 2026-10-12 08:44:31.000000000 +0200 @@ -40,9 +40,10 @@ */ export interface IPtyForkOptions { /** - * The name of the terminal to be set in environment variables. + * The terminal type, set as TERM in the child's environment on Unix. + * Defaults to TERM from the environment, then "xterm". */ - name: string; + name?: string; /** * The number of columns in the PTY. @@ -61,9 +62,13 @@ cwd?: string; /** - * Environment variables to set for the process. + * The child's whole environment, as in node-pty: it replaces the parent's + * environment instead of adding to it, so pass + * `{ ...process.env, FOO: "bar" }` to add variables. Entries whose value + * is undefined are skipped. On Windows, a map with no entries keeps the + * parent's environment, as in node-pty. Defaults to `process.env`. */ - env?: Record<string, string>; + env?: Record<string, string | undefined>; } /** diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bun-pty-0.4.11/src/terminal.integration.test.ts new/bun-pty-0.5.1/src/terminal.integration.test.ts --- old/bun-pty-0.4.11/src/terminal.integration.test.ts 2026-09-30 07:52:47.000000000 +0200 +++ new/bun-pty-0.5.1/src/terminal.integration.test.ts 2026-10-12 08:44:31.000000000 +0200 @@ -452,6 +452,175 @@ expect(dataReceived).toContain("Hello from PowerShell"); }, 15000); + test.skipIf(isWindows)("Terminal replaces the parent environment with a supplied env map", async () => { + let dataReceived = ""; + let hasExited = false; + + const terminal = new Terminal("/usr/bin/env", [], { + name: "xterm", + env: { + BUN_PTY_ONLY: "from-map", + }, + }); + terminals.push(terminal); + + terminal.onData((data) => { + dataReceived += data; + }); + + terminal.onExit(() => { + hasExited = true; + }); + + const timeout = 5000; + const start = Date.now(); + + while (!hasExited && Date.now() - start < timeout) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + await new Promise((resolve) => setTimeout(resolve, 200)); + + const names = dataReceived + .split(/\r?\n/) + .filter((line) => line.includes("=")) + .map((line) => line.slice(0, line.indexOf("="))) + .sort(); + + // portable-pty adds SHELL when the map holds none; PWD and TERM come from cwd and name + expect(names).toEqual(["BUN_PTY_ONLY", "PWD", "SHELL", "TERM"]); + expect(dataReceived).toContain("BUN_PTY_ONLY=from-map"); + expect(dataReceived).toContain("TERM=xterm"); + }); + + test.skipIf(isWindows)("Terminal passes the current process.env when no env map is supplied", async () => { + let dataReceived = ""; + let hasExited = false; + + process.env.BUN_PTY_RUNTIME = "set-at-runtime"; + const terminal = new Terminal("sh", ["-c", "echo \"RUNTIME:[$BUN_PTY_RUNTIME]\""]); + delete process.env.BUN_PTY_RUNTIME; + terminals.push(terminal); + + terminal.onData((data) => { + dataReceived += data; + }); + + terminal.onExit(() => { + hasExited = true; + }); + + const timeout = 5000; + const start = Date.now(); + + while (!hasExited && Date.now() - start < timeout) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + await new Promise((resolve) => setTimeout(resolve, 200)); + + expect(dataReceived).toContain("RUNTIME:[set-at-runtime]"); + }); + + test.skipIf(isWindows)("Terminal sets TERM from name and PWD from cwd", async () => { + let dataReceived = ""; + let hasExited = false; + + const terminal = new Terminal("sh", ["-c", "echo \"TERM:[$TERM] PWD:[$PWD]\""], { + name: "xterm-256color", + cwd: "/", + }); + terminals.push(terminal); + + terminal.onData((data) => { + dataReceived += data; + }); + + terminal.onExit(() => { + hasExited = true; + }); + + const timeout = 5000; + const start = Date.now(); + + while (!hasExited && Date.now() - start < timeout) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + await new Promise((resolve) => setTimeout(resolve, 200)); + + expect(dataReceived).toContain("TERM:[xterm-256color] PWD:[/]"); + }); + + test.skipIf(isWindows)("Terminal drops the parent's tmux variables when the child inherits process.env", async () => { + let dataReceived = ""; + let hasExited = false; + + const previous = process.env.TMUX; + process.env.TMUX = "/tmp/tmux-bun-pty/default,1,0"; + const terminal = new Terminal("sh", ["-c", "echo \"TMUX:[$TMUX]\""]); + if (previous === undefined) delete process.env.TMUX; + else process.env.TMUX = previous; + terminals.push(terminal); + + terminal.onData((data) => { + dataReceived += data; + }); + + terminal.onExit(() => { + hasExited = true; + }); + + const timeout = 5000; + const start = Date.now(); + + while (!hasExited && Date.now() - start < timeout) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + await new Promise((resolve) => setTimeout(resolve, 200)); + + expect(dataReceived).toContain("TMUX:[]"); + }); + + test.skipIf(isWindows)("Terminal finds a bare program name when the env map has no PATH", async () => { + let dataReceived = ""; + let hasExited = false; + + const terminal = new Terminal("env", [], { + env: { + BUN_PTY_ONLY: "from-map", + }, + }); + terminals.push(terminal); + + terminal.onData((data) => { + dataReceived += data; + }); + + terminal.onExit(() => { + hasExited = true; + }); + + const timeout = 5000; + const start = Date.now(); + + while (!hasExited && Date.now() - start < timeout) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + await new Promise((resolve) => setTimeout(resolve, 200)); + + const names = dataReceived + .split(/\r?\n/) + .filter((line) => line.includes("=")) + .map((line) => line.slice(0, line.indexOf("="))) + .sort(); + + // The parent's PATH finds `env`, but does not reach the child + expect(names).toEqual(["BUN_PTY_ONLY", "PWD", "SHELL", "TERM"]); + }); + // Windows-specific: Test environment variables test.skipIf(!isWindows)("Terminal passes environment variables on Windows", async () => { let dataReceived = ""; @@ -487,6 +656,37 @@ expect(dataReceived).toContain("HelloFromEnv"); }); + // Windows-specific: an empty env map keeps the parent environment, as in node-pty + test.skipIf(!isWindows)("Terminal keeps the parent environment for an empty env map on Windows", async () => { + let dataReceived = ""; + let hasExited = false; + + const terminal = new Terminal("cmd.exe", ["/c", "echo ROOT:[%SystemRoot%]"], { + name: "xterm", + env: {}, + }); + terminals.push(terminal); + + terminal.onData((data) => { + dataReceived += data; + }); + + terminal.onExit(() => { + hasExited = true; + }); + + const timeout = 5000; + const start = Date.now(); + + while (!hasExited && Date.now() - start < timeout) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + await new Promise((resolve) => setTimeout(resolve, 200)); + + expect(dataReceived).toContain(`ROOT:[${process.env.SystemRoot}]`); + }); + test("Terminal onData listener receives data when set immediately after construction", async () => { const start = Date.now(); const maxRuntime = 4000; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bun-pty-0.4.11/src/terminal.ts new/bun-pty-0.5.1/src/terminal.ts --- old/bun-pty-0.4.11/src/terminal.ts 2026-09-30 07:52:47.000000000 +0200 +++ new/bun-pty-0.5.1/src/terminal.ts 2026-10-12 08:44:31.000000000 +0200 @@ -5,13 +5,14 @@ import { EventEmitter } from "./interfaces"; import type { IPty, IPtyForkOptions, IExitEvent } from "./interfaces"; import { libraryFilenames } from "./library"; +import { DEFAULT_NAME, childEnv, formatEnv } from "./env"; import { join, dirname, basename } from "node:path"; import { existsSync, readFileSync } from "node:fs"; export const DEFAULT_COLS = 80; export const DEFAULT_ROWS = 24; export const DEFAULT_FILE = "sh"; -export const DEFAULT_NAME = "xterm"; +export { DEFAULT_NAME }; /** * Quote a string for shell-words compatible splitting on the Rust side. @@ -163,7 +164,7 @@ constructor( file = DEFAULT_FILE, args: string[] = [], - opts: IPtyForkOptions = { name: DEFAULT_NAME }, + opts: IPtyForkOptions = {}, ) { this._cols = opts.cols ?? DEFAULT_COLS; this._rows = opts.rows ?? DEFAULT_ROWS; @@ -171,12 +172,8 @@ // Properly quote file and arguments to preserve spaces and special characters const cmdline = [shQuote(file), ...args.map(shQuote)].join(" "); - // Format environment variables as null-terminated string - let envStr = ""; - if (opts.env) { - const envPairs = Object.entries(opts.env).map(([k, v]) => `${k}=${v}`); - envStr = envPairs.join("\0") + "\0"; - } + // The child's whole environment, built as node-pty builds it + const envStr = formatEnv(childEnv(opts, cwd)); this.handle = lib.symbols.bun_pty_spawn( Buffer.from(`${cmdline}\0`, "utf8"), ++++++ vendor.tar.zst ++++++ ++++ 476229 lines of diff (skipped)
