From 2beb5d6ef2d9cc5e76cfd9dafc172e35aa3cbeb1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 09:29:30 -0700 Subject: [PATCH 01/36] chore(deps-dev): bump @humanfs/node from 0.16.7 to 0.16.8 (#964) Bumps [@humanfs/node](https://github.com/humanwhocodes/humanfs/tree/HEAD/packages/node) from 0.16.7 to 0.16.8. - [Release notes](https://github.com/humanwhocodes/humanfs/releases) - [Changelog](https://github.com/humanwhocodes/humanfs/blob/main/packages/node/CHANGELOG.md) - [Commits](https://github.com/humanwhocodes/humanfs/commits/node-v0.16.8/packages/node) --- updated-dependencies: - dependency-name: "@humanfs/node" dependency-version: 0.16.8 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 737c4ca1c..9042a15de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -121,29 +121,43 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", From 82090661786b95d3ad4fd1310c2631a4f9a92b39 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:33:32 -0700 Subject: [PATCH 02/36] chore(deps-dev): bump js-yaml from 4.3.1 to 4.3.2 (#968) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.3.1 to 4.3.2. - [Changelog](https://github.com/nodeca/js-yaml/blob/4.3.2/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.3.1...4.3.2) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.3.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9042a15de..8734735cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1853,9 +1853,9 @@ } }, "node_modules/js-yaml": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", - "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "funding": [ { From b62d1cb5df03983d226fc139e65a921edc51b9a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:43:35 -0700 Subject: [PATCH 03/36] chore(deps-dev): bump ip-address from 10.4.0 to 10.7.2 (#979) Bumps [ip-address](https://github.com/beaugunderson/ip-address) from 10.4.0 to 10.7.2. - [Release notes](https://github.com/beaugunderson/ip-address/releases) - [Commits](https://github.com/beaugunderson/ip-address/compare/v10.4.0...v10.7.2) --- updated-dependencies: - dependency-name: ip-address dependency-version: 10.7.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8734735cc..e61a1a80d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1749,9 +1749,9 @@ "dev": true }, "node_modules/ip-address": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", - "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", "dev": true, "license": "MIT", "engines": { From bb5ac36e7b8b6a9774f6b16ac9c83e9e154b16ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:47:05 -0700 Subject: [PATCH 04/36] chore(deps): bump electron from 42.5.1 to 42.10.0 in /examples/electron (#980) Bumps [electron](https://github.com/electron/electron) from 42.5.1 to 42.10.0. - [Release notes](https://github.com/electron/electron/releases) - [Commits](https://github.com/electron/electron/compare/v42.5.1...v42.10.0) --- updated-dependencies: - dependency-name: electron dependency-version: 42.10.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/electron/package-lock.json | 8 ++++---- examples/electron/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/electron/package-lock.json b/examples/electron/package-lock.json index 2c337d036..2812190ca 100644 --- a/examples/electron/package-lock.json +++ b/examples/electron/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "dependencies": { "@xterm/xterm": "^6.0.0", - "electron": "^42.5.1" + "electron": "^42.10.0" } }, "node_modules/@electron-internal/extract-zip": { @@ -77,9 +77,9 @@ } }, "node_modules/electron": { - "version": "42.5.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-42.5.1.tgz", - "integrity": "sha512-2VFNJcHHbrhIpGsJHdkLoi/nWPZPxN3GHVPe+9At3Oz3/TJRwpr+7JL97ddBDbKyLmHGx3GfI2jvzcEQL28uFw==", + "version": "42.10.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-42.10.0.tgz", + "integrity": "sha512-yBWnZO8tKId0ADvGa4+imPmm3+8Kfs8MtkUPhQQJz1AjTnUUDwCTWkQEU0CrArVVL97Jo4Q/0mVcLHciicBDGQ==", "license": "MIT", "dependencies": { "@electron-internal/extract-zip": "^1.0.1", diff --git a/examples/electron/package.json b/examples/electron/package.json index 4777e19b8..b88d423a0 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -10,6 +10,6 @@ "author": "Tyriar", "dependencies": { "@xterm/xterm": "^6.0.0", - "electron": "^42.5.1" + "electron": "^42.10.0" } } From 84f59866ce03fb5bd5c462b7c99582fd6181c569 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 30 Sep 2026 01:45:48 +0000 Subject: [PATCH 05/36] Upgrade Mocha dependencies to v12 (#981) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rzhao271 <7199958+rzhao271@users.noreply.github.com> --- package-lock.json | 664 +++++++++------------------------------------- package.json | 4 +- 2 files changed, 123 insertions(+), 545 deletions(-) diff --git a/package-lock.json b/package-lock.json index e61a1a80d..07830d40b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,14 +13,14 @@ "node-addon-api": "^7.1.0" }, "devDependencies": { - "@types/mocha": "^7.0.2", + "@types/mocha": "^10.0.10", "@types/node": "12", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "cross-env": "^5.1.4", "eslint": "^10.4.0", "globals": "^14.0.0", - "mocha": "10", + "mocha": "12.0.2", "node-gyp": "^11.4.2", "ps-list": "^6.0.0", "typescript": "^5.0.0" @@ -365,10 +365,11 @@ "license": "MIT" }, "node_modules/@types/mocha": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-7.0.2.tgz", - "integrity": "sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==", - "dev": true + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { "version": "12.20.55", @@ -669,15 +670,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -694,19 +686,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -720,18 +699,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/brace-expansion": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", @@ -755,18 +722,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -844,57 +799,20 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, + "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^5.0.0" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" } }, "node_modules/chownr": { @@ -907,38 +825,6 @@ "node": ">=18" } }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1019,18 +905,6 @@ } } }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1039,9 +913,9 @@ "license": "MIT" }, "node_modules/diff": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", - "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -1103,15 +977,6 @@ "dev": true, "license": "MIT" }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1431,18 +1296,6 @@ "node": ">=16.0.0" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -1459,13 +1312,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", "dev": true, - "bin": { - "flat": "cli.js" + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/flat-cache": { @@ -1596,46 +1453,49 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "node_modules/glob/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": "20 || >=22" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/glob/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "is-glob": "^4.0.1" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">= 6" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/globals": { @@ -1668,15 +1528,6 @@ "node": ">=8" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "bin": { - "he": "bin/he" - } - }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -1731,23 +1582,6 @@ "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, "node_modules/ip-address": { "version": "10.7.2", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", @@ -1758,18 +1592,6 @@ "node": ">= 12" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1800,22 +1622,17 @@ "node": ">=0.10.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-unicode-supported": { @@ -1823,6 +1640,7 @@ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -1853,9 +1671,9 @@ } }, "node_modules/js-yaml": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", - "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.2.tgz", + "integrity": "sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==", "dev": true, "funding": [ { @@ -1872,7 +1690,7 @@ "argparse": "^2.0.1" }, "bin": { - "js-yaml": "bin/js-yaml.js" + "js-yaml": "bin/js-yaml.mjs" } }, "node_modules/json-buffer": { @@ -1934,22 +1752,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -1997,11 +1799,11 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -2150,81 +1952,34 @@ } }, "node_modules/mocha": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.7.3.tgz", - "integrity": "sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==", + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-12.0.2.tgz", + "integrity": "sha512-SjAulGHxlMJLCD1bhvJBAiYwYxglydRzA9PDQ5MALq1oLRMMtIhxSQJ0olCFgA7c9YbTdxC0KOV0wl8JEDYVhA==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-colors": "^4.1.3", "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", + "chokidar": "^5.0.0", "debug": "^4.3.5", - "diff": "^5.2.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^8.1.0", - "he": "^1.2.0", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", + "diff": "^9.0.0", + "find-up-simple": "^1.0.1", + "glob": "^13.0.0", + "is-path-inside": "^4.0.0", + "is-unicode-supported": "^0.1.0", + "js-yaml": "^5.0.0", + "minimatch": "^10.2.2", "ms": "^2.1.3", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", + "picocolors": "^1.1.1", + "serialize-javascript": "^7.1.1", + "strip-json-comments": "^5.0.3", "supports-color": "^8.1.1", - "workerpool": "^6.5.1", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9", - "yargs-unparser": "^2.0.0" + "workerpool": "^10.0.0" }, "bin": { - "_mocha": "bin/_mocha", "mocha": "bin/mocha.js" }, "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/mocha/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/mocha/node_modules/supports-color": { @@ -2342,24 +2097,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2463,18 +2200,12 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } + "license": "ISC" }, "node_modules/prelude-ls": { "version": "1.2.1", @@ -2529,34 +2260,18 @@ "node": ">=6" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/retry": { @@ -2569,26 +2284,6 @@ "node": ">= 4" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -2610,12 +2305,13 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.1.tgz", + "integrity": "sha512-k3CMsaIvvdSwm8oLB4MXSl0wH2/cwlH7xGcnRd2DaeRmBkbzYmyT8j0tsX60DwD1eRwHTpNpH8ljKu9oUT1MeQ==", "dev": true, - "dependencies": { - "randombytes": "^2.1.0" + "license": "BSD-3-Clause", + "engines": { + "node": ">=20.0.0" } }, "node_modules/shebang-command": { @@ -2792,30 +2488,18 @@ } }, "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/tar": { "version": "7.5.22", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", @@ -2881,18 +2565,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -2992,27 +2664,11 @@ } }, "node_modules/workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", - "dev": true - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-10.0.3.tgz", + "integrity": "sha512-6z2Iis68Wqth93/G/wJP9u+R3O+d2XTlgWChGCwuT1qLbBsOYueGRZuJ++v3mtDP5KjYdy+WzvWC+VWETSVXJA==", "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } + "license": "Apache-2.0" }, "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", @@ -3056,42 +2712,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -3102,48 +2722,6 @@ "node": ">=18" } }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index c47dad089..41e8f9959 100644 --- a/package.json +++ b/package.json @@ -51,14 +51,14 @@ "node-addon-api": "^7.1.0" }, "devDependencies": { - "@types/mocha": "^7.0.2", + "@types/mocha": "^10.0.10", "@types/node": "12", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "cross-env": "^5.1.4", "eslint": "^10.4.0", "globals": "^14.0.0", - "mocha": "10", + "mocha": "12.0.2", "node-gyp": "^11.4.2", "ps-list": "^6.0.0", "typescript": "^5.0.0" From 6553ee7f576798d052a1614b5d2c6e354447c217 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:57:14 -0700 Subject: [PATCH 06/36] chore(deps): bump brace-expansion (#984) Bumps and [brace-expansion](https://github.com/juliangruber/brace-expansion). These dependencies needed to be updated together. Updates `brace-expansion` from 5.0.9 to 5.0.12 - [Release notes](https://github.com/juliangruber/brace-expansion/releases) - [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.9...v5.0.12) Updates `brace-expansion` from 2.1.4 to 2.1.7 - [Release notes](https://github.com/juliangruber/brace-expansion/releases) - [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.9...v5.0.12) --- updated-dependencies: - dependency-name: brace-expansion dependency-version: 5.0.12 dependency-type: indirect - dependency-name: brace-expansion dependency-version: 2.1.7 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 07830d40b..c1ed56bc2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -700,9 +700,9 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { @@ -753,9 +753,9 @@ } }, "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, "license": "MIT", "dependencies": { From d1b0fe9705daa3dc1a8b04bf60ced131a15728fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:08:57 -0700 Subject: [PATCH 07/36] chore(deps-dev): bump serialize-javascript from 7.1.1 to 7.1.2 (#985) Bumps [serialize-javascript](https://github.com/yahoo/serialize-javascript) from 7.1.1 to 7.1.2. - [Release notes](https://github.com/yahoo/serialize-javascript/releases) - [Commits](https://github.com/yahoo/serialize-javascript/compare/v7.1.1...v7.1.2) --- updated-dependencies: - dependency-name: serialize-javascript dependency-version: 7.1.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c1ed56bc2..f05406975 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2305,9 +2305,9 @@ } }, "node_modules/serialize-javascript": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.1.tgz", - "integrity": "sha512-k3CMsaIvvdSwm8oLB4MXSl0wH2/cwlH7xGcnRd2DaeRmBkbzYmyT8j0tsX60DwD1eRwHTpNpH8ljKu9oUT1MeQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.2.tgz", + "integrity": "sha512-GL2BWwVa6JydKO6l/ljVgjAZF4QJ3S7dWDWi53s5GZT8PJzD2fNxi67HANQGKAqPrwEpL5ba7gUBGi0Ls/sEoQ==", "dev": true, "license": "BSD-3-Clause", "engines": { From d182c6079fc31bab364c1754ee0dca103cd0a7bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 20:20:07 -0700 Subject: [PATCH 08/36] chore(deps): bump undici from 7.29.0 to 7.30.0 in /examples/electron (#978) Bumps [undici](https://github.com/nodejs/undici) from 7.29.0 to 7.30.0. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v7.29.0...v7.30.0) --- updated-dependencies: - dependency-name: undici dependency-version: 7.30.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/electron/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/electron/package-lock.json b/examples/electron/package-lock.json index 2812190ca..42950d21d 100644 --- a/examples/electron/package-lock.json +++ b/examples/electron/package-lock.json @@ -152,9 +152,9 @@ } }, "node_modules/undici": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", - "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", "license": "MIT", "optional": true, "engines": { From e555f26ddd1a26e57f68d410087877a7078a34f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 15:51:19 -0700 Subject: [PATCH 09/36] chore(deps): bump actions/setup-node (#992) Bumps the github-actions group with 1 update: [actions/setup-node](https://github.com/actions/setup-node). Updates `actions/setup-node` from 7.0.0 to 7.1.0 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/820762786026740c76f36085b0efc47a31fe5020...949feb2413d6458794dcd2491c4babbbce0c15c1) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: 7.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 714acdfbf..04a0846cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: persist-credentials: false - name: Use Node.js 22.x - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: '22.x' @@ -38,7 +38,7 @@ jobs: persist-credentials: false - name: Use Node.js 22.x - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: '22.x' From ff15c3af9ad96960b6d8f3be9c102904fca7bb05 Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Mon, 19 Oct 2026 20:25:18 -0700 Subject: [PATCH 10/36] Add ConptyReparentPseudoConsole support Add a conptyParentWindow fork option and IPty.setParentWindow(), backed by ConptyReparentPseudoConsole from conpty.dll. The host window becomes the owner of the console window returned by GetConsoleWindow(), so windows that console apps parent to it (for example the WAM sign-in picker) open above the host instead of behind it. --- src/interfaces.ts | 7 ++++ src/native.d.ts | 1 + src/terminal.test.ts | 3 ++ src/terminal.ts | 1 + src/unixTerminal.ts | 4 +++ src/win/conpty.cc | 52 ++++++++++++++++++++++++++++++ src/windowsPtyAgent.ts | 15 +++++++++ src/windowsTerminal.test.ts | 64 ++++++++++++++++++++++++++++++++++++- src/windowsTerminal.ts | 11 +++++++ typings/node-pty.d.ts | 23 +++++++++++++ 10 files changed, 180 insertions(+), 1 deletion(-) diff --git a/src/interfaces.ts b/src/interfaces.ts index 461b713ef..1327e03e3 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -40,6 +40,12 @@ export interface ITerminal { */ clear(): void; + /** + * Sets the top-level window (HWND) that hosts this terminal. This is a no-op unless on Windows + * with `useConptyDll`. + */ + setParentWindow(handle: number | bigint): void; + /** * Close, kill and destroy the socket. */ @@ -132,6 +138,7 @@ export interface IWindowsPtyForkOptions extends IBasePtyForkOptions { useConpty?: boolean; useConptyDll?: boolean; conptyInheritCursor?: boolean; + conptyParentWindow?: number | bigint; } export interface IPtyOpenOptions { diff --git a/src/native.d.ts b/src/native.d.ts index 03bfbebae..0a83a43fd 100644 --- a/src/native.d.ts +++ b/src/native.d.ts @@ -7,6 +7,7 @@ interface IConptyNative { connect(ptyId: number, commandLine: string, cwd: string, env: string[], useConptyDll: boolean, onExitCallback: (exitCode: number) => void): { pid: number }; resize(ptyId: number, cols: number, rows: number, useConptyDll: boolean): void; clear(ptyId: number, useConptyDll: boolean): void; + reparent(ptyId: number, hwnd: number | bigint, useConptyDll: boolean): void; kill(ptyId: number, useConptyDll: boolean): void; } diff --git a/src/terminal.test.ts b/src/terminal.test.ts index c9e55177e..5ec843371 100644 --- a/src/terminal.test.ts +++ b/src/terminal.test.ts @@ -32,6 +32,9 @@ class TestTerminal extends Terminal { public clear(): void { throw new Error('Method not implemented.'); } + public setParentWindow(handle: number | bigint): void { + throw new Error('Method not implemented.'); + } public destroy(): void { throw new Error('Method not implemented.'); } diff --git a/src/terminal.ts b/src/terminal.ts index 487e37565..f1a0c4f82 100644 --- a/src/terminal.ts +++ b/src/terminal.ts @@ -180,6 +180,7 @@ export abstract class Terminal implements ITerminal { public abstract resize(cols: number, rows: number, pixelSize?: { width: number, height: number }): void; public abstract clear(): void; + public abstract setParentWindow(handle: number | bigint): void; public abstract destroy(): void; public abstract kill(signal?: string): void; diff --git a/src/unixTerminal.ts b/src/unixTerminal.ts index 2776d501e..3d84feb2f 100644 --- a/src/unixTerminal.ts +++ b/src/unixTerminal.ts @@ -283,6 +283,10 @@ export class UnixTerminal extends Terminal { } + public setParentWindow(handle: number | bigint): void { + // Windows only + } + private _sanitizeEnv(env: IProcessEnv): void { // Make sure we didn't start our server from inside tmux. delete env['TMUX']; diff --git a/src/win/conpty.cc b/src/win/conpty.cc index 931c5362d..7f5d3640e 100644 --- a/src/win/conpty.cc +++ b/src/win/conpty.cc @@ -40,6 +40,9 @@ typedef void (__stdcall *PFNRELEASEPSEUDOCONSOLE)(HPCON hpc); #endif +// Only exported by the bundled conpty.dll; kernel32 has no equivalent. +typedef HRESULT (__stdcall *PFNREPARENTPSEUDOCONSOLE)(HPCON hpc, HWND newParent); + struct pty_baton { int id; HANDLE hIn; @@ -553,6 +556,54 @@ static Napi::Value PtyClear(const Napi::CallbackInfo& info) { return env.Undefined(); } +static Napi::Value PtyReparent(const Napi::CallbackInfo& info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); + + if (info.Length() != 3 || + !info[0].IsNumber() || + !(info[1].IsNumber() || info[1].IsBigInt()) || + !info[2].IsBoolean()) { + throw Napi::Error::New(env, "Usage: pty.reparent(id, hwnd, useConptyDll)"); + } + + int id = info[0].As().Int32Value(); + uint64_t hwndValue; + if (info[1].IsBigInt()) { + bool lossless = false; + hwndValue = info[1].As().Uint64Value(&lossless); + } else { + hwndValue = static_cast(info[1].As().Int64Value()); + } + const bool useConptyDll = info[2].As().Value(); + + // ConptyReparentPseudoConsole only exists in the bundled conpty.dll. + if (!useConptyDll) { + return env.Undefined(); + } + + std::lock_guard lock(g_ptyHandlesMutex); + const pty_baton* handle = get_pty_baton(lock, id); + + if (handle != nullptr) { + HANDLE hLibrary = LoadConptyDll(info, useConptyDll); + bool fLoadedDll = hLibrary != nullptr; + if (fLoadedDll) + { + PFNREPARENTPSEUDOCONSOLE const pfnReparentPseudoConsole = (PFNREPARENTPSEUDOCONSOLE)GetProcAddress( + (HMODULE)hLibrary, "ConptyReparentPseudoConsole"); + if (pfnReparentPseudoConsole) + { + // Makes newParent the owner of the console window returned by + // GetConsoleWindow(), so windows parented to it stack above the host. + pfnReparentPseudoConsole(handle->hpc, reinterpret_cast(static_cast(hwndValue))); + } + } + } + + return env.Undefined(); +} + static Napi::Value PtyKill(const Napi::CallbackInfo& info) { Napi::Env env(info.Env()); Napi::HandleScope scope(env); @@ -609,6 +660,7 @@ Napi::Object init(Napi::Env env, Napi::Object exports) { exports.Set("connect", Napi::Function::New(env, PtyConnect)); exports.Set("resize", Napi::Function::New(env, PtyResize)); exports.Set("clear", Napi::Function::New(env, PtyClear)); + exports.Set("reparent", Napi::Function::New(env, PtyReparent)); exports.Set("kill", Napi::Function::New(env, PtyKill)); return exports; }; diff --git a/src/windowsPtyAgent.ts b/src/windowsPtyAgent.ts index 73f83d6ec..14a682a4c 100644 --- a/src/windowsPtyAgent.ts +++ b/src/windowsPtyAgent.ts @@ -43,6 +43,7 @@ export class WindowsPtyAgent { private _closeTimeout: NodeJS.Timer | undefined; private _connectionTimeout: NodeJS.Timeout | undefined; private _exitCode: number | undefined; + private _isKilled: boolean = false; private _conoutSocketWorker: IConoutConnection; private _onError = new EventEmitter2(); @@ -193,7 +194,21 @@ export class WindowsPtyAgent { this._ptyNative.clear(this._pty, this._useConptyDll); } + /** + * Tells ConPTY which top-level window hosts the terminal (ConptyReparentPseudoConsole). The + * window becomes the owner of the console window returned by GetConsoleWindow(), so dialogs + * that console apps parent to it (e.g. WAM sign-in) stack above the host window. + */ + public setParentWindow(handle: number | bigint): void { + // The pseudoconsole is closed (and its HPCON freed) once the pty is killed + if (this._isKilled || this._exitCode !== undefined) { + return; + } + this._ptyNative.reparent(this._pty, handle, this._useConptyDll); + } + public kill(): void { + this._isKilled = true; // Prevent deferred connection from completing after kill this._clearConnectionTimeout(); this._pendingPtyInfo = undefined; diff --git a/src/windowsTerminal.test.ts b/src/windowsTerminal.test.ts index c76196512..e05db2ddd 100644 --- a/src/windowsTerminal.test.ts +++ b/src/windowsTerminal.test.ts @@ -283,8 +283,67 @@ if (process.platform === 'win32') { }); }); + describe('parent window', () => { + // Prints the console window and GetAncestor(GetConsoleWindow(), GA_ROOTOWNER), which is the + // parent MSAL/WAM uses for sign-in dialogs. + function rootOwnerArgs(delayMs: number, keepAliveSeconds: number = 0): string { + const script = [ + `Start-Sleep -Milliseconds ${delayMs}`, + `Add-Type -Namespace NodePtyTest -Name W -MemberDefinition '[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow(); [DllImport("user32.dll")] public static extern IntPtr GetAncestor(IntPtr h, uint f);'`, + `$c = [NodePtyTest.W]::GetConsoleWindow()`, + `Write-Host ('console=' + $c.ToInt64() + ' rootOwner=' + [NodePtyTest.W]::GetAncestor($c, 3).ToInt64() + ' end')`, + `Start-Sleep -Seconds ${keepAliveSeconds}` + ].join('; '); + return `-NoProfile -NonInteractive -EncodedCommand ${Buffer.from(script, 'utf16le').toString('base64')}`; + } + + function readWindows(term: WindowsTerminal): Promise<{ console: number, rootOwner: number }> { + return new Promise(resolve => { + let output = ''; + term.on('data', (data) => { + output += data; + const match = /console=(\d+) rootOwner=(\d+) end/.exec(output); + if (match) { + resolve({ console: parseInt(match[1], 10), rootOwner: parseInt(match[2], 10) }); + } + }); + }); + } + + it('should make conptyParentWindow the owner of the console window before the shell starts', async function (): Promise { + this.timeout(60000); + // Another pty's hidden top-level console window stands in for the host app's window. + const host = new WindowsTerminal('powershell.exe', rootOwnerArgs(0, 60), { useConptyDll }); + try { + const hostWindows = await readWindows(host); + const term = new WindowsTerminal('powershell.exe', rootOwnerArgs(0), { useConptyDll, conptyParentWindow: hostWindows.console }); + const windows = await readWindows(term); + assert.strictEqual(windows.rootOwner, useConptyDll ? hostWindows.console : windows.console); + term.kill(); + } finally { + host.kill(); + } + }); + + it('should change the owner when setParentWindow is called while the shell runs', async function (): Promise { + this.timeout(60000); + const host = new WindowsTerminal('powershell.exe', rootOwnerArgs(0, 60), { useConptyDll }); + try { + const hostWindows = await readWindows(host); + const term = new WindowsTerminal('powershell.exe', rootOwnerArgs(3000), { useConptyDll }); + // The first data event means the shell is running and conpty has been released. + term.once('data', () => term.setParentWindow(BigInt(hostWindows.console))); + const windows = await readWindows(term); + assert.strictEqual(windows.rootOwner, useConptyDll ? hostWindows.console : windows.console); + term.kill(); + } finally { + host.kill(); + } + }); + }); + describe('Regression for #921', () => { - it('should not crash with concurrent kills while resizing/clearing', function (done) { + it('should not crash with concurrent kills while resizing/clearing/reparenting', function (done) { this.timeout(60000); const N = 30; const terms: WindowsTerminal[] = []; @@ -307,6 +366,9 @@ if (process.platform === 'win32') { try { t.clear(); } catch (e) { /* already exited */ } + try { + t.setParentWindow(0); + } catch (e) { /* already exited */ } } }, 1); for (const t of terms) { diff --git a/src/windowsTerminal.ts b/src/windowsTerminal.ts index 3f88f4dee..9df5b669e 100644 --- a/src/windowsTerminal.ts +++ b/src/windowsTerminal.ts @@ -49,6 +49,11 @@ export class WindowsTerminal extends Terminal { // Create new termal. this._agent = new WindowsPtyAgent(file, args, parsedEnv, cwd, this._cols, this._rows, false, opt.useConptyDll, opt.conptyInheritCursor); + if (opt.conptyParentWindow !== undefined) { + // Applied before the shell is created (connect() waits for the conout worker), so the + // shell's first GetConsoleWindow() already sees its owner. + this._agent.setParentWindow(opt.conptyParentWindow); + } this._socket = this._agent.outSocket; // The socket 'close' handler that drives 'exit' is only attached after // ready_datapipe, which never fires when connect() fails. @@ -163,6 +168,12 @@ export class WindowsTerminal extends Terminal { }); } + public setParentWindow(handle: number | bigint): void { + // Not deferred: the pseudoconsole exists as soon as the agent is created, and the owner + // should be in place before the shell starts asking for it. + this._agent.setParentWindow(handle); + } + public destroy(): void { this._deferNoArgs(() => { this.kill(); diff --git a/typings/node-pty.d.ts b/typings/node-pty.d.ts index a6fc5dd0b..2a051ad61 100644 --- a/typings/node-pty.d.ts +++ b/typings/node-pty.d.ts @@ -109,6 +109,18 @@ declare module 'node-pty' { * @see https://docs.microsoft.com/en-us/windows/console/createpseudoconsole */ conptyInheritCursor?: boolean; + + /** + * (EXPERIMENTAL) + * + * The handle (HWND) of the top-level window that hosts this terminal. ConPTY makes it the owner + * of the console window returned by `GetConsoleWindow()`, so windows that console applications + * parent to it (for example `GetAncestor(GetConsoleWindow(), GA_ROOTOWNER)`, which MSAL/WAM + * sign-in uses) open on top of the host window instead of behind it. The owner is set before + * the process starts. Use {@link IPty.setParentWindow} when the terminal moves to another + * window. Only supported when `useConptyDll` is true; ignored otherwise. + */ + conptyParentWindow?: number | bigint; } /** @@ -173,6 +185,17 @@ declare module 'node-pty' { */ clear(): void; + /** + * (EXPERIMENTAL) + * + * Sets the handle (HWND) of the top-level window that hosts this terminal, see + * `IWindowsPtyForkOptions.conptyParentWindow`. Call it again whenever the terminal moves to a + * different window. This is a no-op unless on Windows with `useConptyDll`. + * @param handle The window handle, e.g. read from Electron's + * `BrowserWindow.getNativeWindowHandle()`. + */ + setParentWindow(handle: number | bigint): void; + /** * Writes data to the pty. * @param data The data to write. From 909b13ee48c186ed58c8d1689eb54689b8ada5c4 Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Mon, 19 Oct 2026 22:42:22 -0700 Subject: [PATCH 11/36] Address review: native kill guard, return value, parent re-enable - Null the HPCON in PtyKill and skip resize/clear/reparent once it is closed, instead of relying on JS-side bookkeeping. - Return whether ConptyReparentPseudoConsole succeeded, and reject BigInt handles that don't fit in 64 bits. - Apply conptyParentWindow inside WindowsPtyAgent, right after the pseudoconsole is created, and clean up if that throws. - Re-enable the parent window when the shell exits if a modal dialog that died with it left the window disabled. - Make IPty.setParentWindow optional in the public typings and show how to convert Electron's native window handle. --- src/interfaces.ts | 6 ++-- src/native.d.ts | 2 +- src/terminal.test.ts | 2 +- src/terminal.ts | 2 +- src/unixTerminal.ts | 3 +- src/win/conpty.cc | 65 +++++++++++++++++++++++++++++----- src/windowsPtyAgent.test.ts | 3 ++ src/windowsPtyAgent.ts | 24 ++++++++----- src/windowsTerminal.test.ts | 69 ++++++++++++++++++++++++++++++++----- src/windowsTerminal.ts | 11 ++---- typings/node-pty.d.ts | 13 ++++--- 11 files changed, 155 insertions(+), 45 deletions(-) diff --git a/src/interfaces.ts b/src/interfaces.ts index 1327e03e3..1939f8748 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -41,10 +41,10 @@ export interface ITerminal { clear(): void; /** - * Sets the top-level window (HWND) that hosts this terminal. This is a no-op unless on Windows - * with `useConptyDll`. + * Sets the top-level window (HWND) that hosts this terminal. Returns whether ConPTY applied it, + * which only happens on Windows with `useConptyDll`. */ - setParentWindow(handle: number | bigint): void; + setParentWindow(handle: number | bigint): boolean; /** * Close, kill and destroy the socket. diff --git a/src/native.d.ts b/src/native.d.ts index 0a83a43fd..fe65e29d9 100644 --- a/src/native.d.ts +++ b/src/native.d.ts @@ -7,7 +7,7 @@ interface IConptyNative { connect(ptyId: number, commandLine: string, cwd: string, env: string[], useConptyDll: boolean, onExitCallback: (exitCode: number) => void): { pid: number }; resize(ptyId: number, cols: number, rows: number, useConptyDll: boolean): void; clear(ptyId: number, useConptyDll: boolean): void; - reparent(ptyId: number, hwnd: number | bigint, useConptyDll: boolean): void; + reparent(ptyId: number, hwnd: number | bigint, useConptyDll: boolean): boolean; kill(ptyId: number, useConptyDll: boolean): void; } diff --git a/src/terminal.test.ts b/src/terminal.test.ts index 5ec843371..fe96bddd8 100644 --- a/src/terminal.test.ts +++ b/src/terminal.test.ts @@ -32,7 +32,7 @@ class TestTerminal extends Terminal { public clear(): void { throw new Error('Method not implemented.'); } - public setParentWindow(handle: number | bigint): void { + public setParentWindow(handle: number | bigint): boolean { throw new Error('Method not implemented.'); } public destroy(): void { diff --git a/src/terminal.ts b/src/terminal.ts index f1a0c4f82..735315aa5 100644 --- a/src/terminal.ts +++ b/src/terminal.ts @@ -180,7 +180,7 @@ export abstract class Terminal implements ITerminal { public abstract resize(cols: number, rows: number, pixelSize?: { width: number, height: number }): void; public abstract clear(): void; - public abstract setParentWindow(handle: number | bigint): void; + public abstract setParentWindow(handle: number | bigint): boolean; public abstract destroy(): void; public abstract kill(signal?: string): void; diff --git a/src/unixTerminal.ts b/src/unixTerminal.ts index 3d84feb2f..72b20700d 100644 --- a/src/unixTerminal.ts +++ b/src/unixTerminal.ts @@ -283,8 +283,9 @@ export class UnixTerminal extends Terminal { } - public setParentWindow(handle: number | bigint): void { + public setParentWindow(handle: number | bigint): boolean { // Windows only + return false; } private _sanitizeEnv(env: IProcessEnv): void { diff --git a/src/win/conpty.cc b/src/win/conpty.cc index 7f5d3640e..5c4938d16 100644 --- a/src/win/conpty.cc +++ b/src/win/conpty.cc @@ -50,6 +50,7 @@ struct pty_baton { HPCON hpc; HANDLE hShell = nullptr; + HWND parentWindow = nullptr; pty_baton(int _id, HANDLE _hIn, HANDLE _hOut, HPCON _hpc) : id(_id), hIn(_hIn), hOut(_hOut), hpc(_hpc) {}; }; @@ -74,6 +75,34 @@ struct ExitEvent { int exit_code = 0; }; +struct OwnedWindowSearch { + HWND owner; + bool found; +}; + +static BOOL CALLBACK FindVisibleOwnedWindow(HWND hwnd, LPARAM lParam) { + auto* search = reinterpret_cast(lParam); + if (GetWindow(hwnd, GW_OWNER) == search->owner && IsWindowVisible(hwnd)) { + search->found = true; + return FALSE; + } + return TRUE; +} + +// A dialog that is modal to the parent window disables it until the dialog closes. If the dialog's +// process exits first, nothing re-enables the parent, so do that once no visible owned window is +// left (an owned window that is still visible may be another live modal dialog). +static void EnableParentWindowIfOrphaned(HWND parentWindow) { + if (!IsWindow(parentWindow) || IsWindowEnabled(parentWindow)) { + return; + } + OwnedWindowSearch search = { parentWindow, false }; + EnumWindows(FindVisibleOwnedWindow, reinterpret_cast(&search)); + if (!search.found) { + EnableWindow(parentWindow, TRUE); + } +} + void SetupExitCallback(Napi::Env env, Napi::Function cb, pty_baton* baton) { std::thread *th = new std::thread; // Don't use Napi::AsyncWorker which is limited by UV_THREADPOOL_SIZE. @@ -96,15 +125,20 @@ void SetupExitCallback(Napi::Env env, Napi::Function cb, pty_baton* baton) { ExitEvent *exit_event = new ExitEvent; // Wait for process to complete. WaitForSingleObject(baton->hShell, INFINITE); + HWND parentWindow = nullptr; { std::lock_guard lock(g_ptyHandlesMutex); GetExitCodeProcess(baton->hShell, (LPDWORD)(&exit_event->exit_code)); CloseHandle(baton->hShell); + parentWindow = baton->parentWindow; const int id = baton->id; std::erase_if(ptyHandles, [id](const auto& ptyHandle) { return ptyHandle->id == id; }); } + if (parentWindow != nullptr) { + EnableParentWindowIfOrphaned(parentWindow); + } auto status = tsfn.BlockingCall(exit_event, callback); // In main thread switch (status) { @@ -498,7 +532,7 @@ static Napi::Value PtyResize(const Napi::CallbackInfo& info) { std::lock_guard lock(g_ptyHandlesMutex); const pty_baton* handle = get_pty_baton(lock, id); - if (handle != nullptr) { + if (handle != nullptr && handle->hpc != nullptr) { HANDLE hLibrary = LoadConptyDll(info, useConptyDll); bool fLoadedDll = hLibrary != nullptr; if (fLoadedDll) @@ -540,7 +574,7 @@ static Napi::Value PtyClear(const Napi::CallbackInfo& info) { std::lock_guard lock(g_ptyHandlesMutex); const pty_baton* handle = get_pty_baton(lock, id); - if (handle != nullptr) { + if (handle != nullptr && handle->hpc != nullptr) { HANDLE hLibrary = LoadConptyDll(info, useConptyDll); bool fLoadedDll = hLibrary != nullptr; if (fLoadedDll) @@ -570,8 +604,16 @@ static Napi::Value PtyReparent(const Napi::CallbackInfo& info) { int id = info[0].As().Int32Value(); uint64_t hwndValue; if (info[1].IsBigInt()) { + // Accept signed and unsigned 64-bit values, but never truncate. + const Napi::BigInt hwndBigInt = info[1].As(); bool lossless = false; - hwndValue = info[1].As().Uint64Value(&lossless); + hwndValue = hwndBigInt.Uint64Value(&lossless); + if (!lossless) { + hwndValue = static_cast(hwndBigInt.Int64Value(&lossless)); + } + if (!lossless) { + throw Napi::Error::New(env, "pty.reparent: hwnd does not fit in 64 bits"); + } } else { hwndValue = static_cast(info[1].As().Int64Value()); } @@ -579,13 +621,14 @@ static Napi::Value PtyReparent(const Napi::CallbackInfo& info) { // ConptyReparentPseudoConsole only exists in the bundled conpty.dll. if (!useConptyDll) { - return env.Undefined(); + return Napi::Boolean::New(env, false); } std::lock_guard lock(g_ptyHandlesMutex); - const pty_baton* handle = get_pty_baton(lock, id); + pty_baton* handle = get_pty_baton(lock, id); - if (handle != nullptr) { + bool applied = false; + if (handle != nullptr && handle->hpc != nullptr) { HANDLE hLibrary = LoadConptyDll(info, useConptyDll); bool fLoadedDll = hLibrary != nullptr; if (fLoadedDll) @@ -596,12 +639,16 @@ static Napi::Value PtyReparent(const Napi::CallbackInfo& info) { { // Makes newParent the owner of the console window returned by // GetConsoleWindow(), so windows parented to it stack above the host. - pfnReparentPseudoConsole(handle->hpc, reinterpret_cast(static_cast(hwndValue))); + const HWND newParent = reinterpret_cast(static_cast(hwndValue)); + applied = SUCCEEDED(pfnReparentPseudoConsole(handle->hpc, newParent)); + if (applied) { + handle->parentWindow = newParent; + } } } } - return env.Undefined(); + return Napi::Boolean::New(env, applied); } static Napi::Value PtyKill(const Napi::CallbackInfo& info) { @@ -631,6 +678,8 @@ static Napi::Value PtyKill(const Napi::CallbackInfo& info) { if (pfnClosePseudoConsole) { pfnClosePseudoConsole(handle->hpc); + // Closing frees the pseudoconsole, but the baton lives until the exit thread erases it. + handle->hpc = nullptr; } } // Defensive: if PtyConnect was never called (or failed before closing the diff --git a/src/windowsPtyAgent.test.ts b/src/windowsPtyAgent.test.ts index 9d6979500..af4056308 100644 --- a/src/windowsPtyAgent.test.ts +++ b/src/windowsPtyAgent.test.ts @@ -145,6 +145,7 @@ if (process.platform === 'win32') { false, false, false, + undefined, createTestAgentOptions(connection, 10) ); @@ -174,6 +175,7 @@ if (process.platform === 'win32') { false, false, false, + undefined, createTestAgentOptions(connection, 1000) ); @@ -200,6 +202,7 @@ if (process.platform === 'win32') { false, false, false, + undefined, createTestAgentOptions(connection, 1000) ); let errorCount = 0; diff --git a/src/windowsPtyAgent.ts b/src/windowsPtyAgent.ts index 14a682a4c..717590cc9 100644 --- a/src/windowsPtyAgent.ts +++ b/src/windowsPtyAgent.ts @@ -43,7 +43,6 @@ export class WindowsPtyAgent { private _closeTimeout: NodeJS.Timer | undefined; private _connectionTimeout: NodeJS.Timeout | undefined; private _exitCode: number | undefined; - private _isKilled: boolean = false; private _conoutSocketWorker: IConoutConnection; private _onError = new EventEmitter2(); @@ -71,6 +70,7 @@ export class WindowsPtyAgent { debug: boolean, private _useConptyDll: boolean = false, conptyInheritCursor: boolean = false, + conptyParentWindow?: number | bigint, options: IWindowsPtyAgentOptions = defaultWindowsPtyAgentOptions ) { if (!conptyNative) { @@ -94,6 +94,16 @@ export class WindowsPtyAgent { // as a terminal id. this._pty = term.pty; + if (conptyParentWindow !== undefined) { + // Before connect() starts the shell, so its first GetConsoleWindow() already has its owner. + try { + this._ptyNative.reparent(this._pty, conptyParentWindow, this._useConptyDll); + } catch (e) { + this._ptyNative.kill(this._pty, this._useConptyDll); + throw e; + } + } + // Create terminal pipe IPC channel and forward to a local unix socket. this._outSocket = new Socket(); this._outSocket.setEncoding('utf8'); @@ -197,18 +207,14 @@ export class WindowsPtyAgent { /** * Tells ConPTY which top-level window hosts the terminal (ConptyReparentPseudoConsole). The * window becomes the owner of the console window returned by GetConsoleWindow(), so dialogs - * that console apps parent to it (e.g. WAM sign-in) stack above the host window. + * that console apps parent to it (e.g. WAM sign-in) stack above the host window. Returns + * whether ConPTY applied the new owner. */ - public setParentWindow(handle: number | bigint): void { - // The pseudoconsole is closed (and its HPCON freed) once the pty is killed - if (this._isKilled || this._exitCode !== undefined) { - return; - } - this._ptyNative.reparent(this._pty, handle, this._useConptyDll); + public setParentWindow(handle: number | bigint): boolean { + return this._ptyNative.reparent(this._pty, handle, this._useConptyDll); } public kill(): void { - this._isKilled = true; // Prevent deferred connection from completing after kill this._clearConnectionTimeout(); this._pendingPtyInfo = undefined; diff --git a/src/windowsTerminal.test.ts b/src/windowsTerminal.test.ts index e05db2ddd..37157753f 100644 --- a/src/windowsTerminal.test.ts +++ b/src/windowsTerminal.test.ts @@ -284,32 +284,41 @@ if (process.platform === 'win32') { }); describe('parent window', () => { + const user32 = `Add-Type -Namespace NodePtyTest -Name W -MemberDefinition '[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow(); [DllImport("user32.dll")] public static extern IntPtr GetAncestor(IntPtr h, uint f); [DllImport("user32.dll")] public static extern bool EnableWindow(IntPtr h, bool e); [DllImport("user32.dll")] public static extern bool IsWindowEnabled(IntPtr h);'`; + + function scriptArgs(...lines: string[]): string { + return `-NoProfile -NonInteractive -EncodedCommand ${Buffer.from([user32, ...lines].join('; '), 'utf16le').toString('base64')}`; + } + // Prints the console window and GetAncestor(GetConsoleWindow(), GA_ROOTOWNER), which is the // parent MSAL/WAM uses for sign-in dialogs. function rootOwnerArgs(delayMs: number, keepAliveSeconds: number = 0): string { - const script = [ + return scriptArgs( `Start-Sleep -Milliseconds ${delayMs}`, - `Add-Type -Namespace NodePtyTest -Name W -MemberDefinition '[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow(); [DllImport("user32.dll")] public static extern IntPtr GetAncestor(IntPtr h, uint f);'`, `$c = [NodePtyTest.W]::GetConsoleWindow()`, `Write-Host ('console=' + $c.ToInt64() + ' rootOwner=' + [NodePtyTest.W]::GetAncestor($c, 3).ToInt64() + ' end')`, `Start-Sleep -Seconds ${keepAliveSeconds}` - ].join('; '); - return `-NoProfile -NonInteractive -EncodedCommand ${Buffer.from(script, 'utf16le').toString('base64')}`; + ); } - function readWindows(term: WindowsTerminal): Promise<{ console: number, rootOwner: number }> { + function readOutput(term: WindowsTerminal, pattern: RegExp): Promise { return new Promise(resolve => { let output = ''; term.on('data', (data) => { output += data; - const match = /console=(\d+) rootOwner=(\d+) end/.exec(output); + const match = pattern.exec(output); if (match) { - resolve({ console: parseInt(match[1], 10), rootOwner: parseInt(match[2], 10) }); + resolve(match); } }); }); } + async function readWindows(term: WindowsTerminal): Promise<{ console: number, rootOwner: number }> { + const match = await readOutput(term, /console=(\d+) rootOwner=(\d+) end/); + return { console: parseInt(match[1], 10), rootOwner: parseInt(match[2], 10) }; + } + it('should make conptyParentWindow the owner of the console window before the shell starts', async function (): Promise { this.timeout(60000); // Another pty's hidden top-level console window stands in for the host app's window. @@ -331,17 +340,59 @@ if (process.platform === 'win32') { try { const hostWindows = await readWindows(host); const term = new WindowsTerminal('powershell.exe', rootOwnerArgs(3000), { useConptyDll }); + let applied: boolean | undefined; // The first data event means the shell is running and conpty has been released. - term.once('data', () => term.setParentWindow(BigInt(hostWindows.console))); + term.once('data', () => applied = term.setParentWindow(BigInt(hostWindows.console))); const windows = await readWindows(term); + assert.strictEqual(applied, useConptyDll); assert.strictEqual(windows.rootOwner, useConptyDll ? hostWindows.console : windows.console); term.kill(); } finally { host.kill(); } }); - }); + it('should not apply setParentWindow once the pty is killed', async function (): Promise { + this.timeout(20000); + const term = new WindowsTerminal('cmd.exe', [], { useConptyDll }); + const exited = new Promise(resolve => term.once('exit', () => resolve())); + await new Promise(resolve => term.once('data', () => resolve())); + term.kill(); + assert.strictEqual(term.setParentWindow(0), false); + await exited; + assert.strictEqual(term.setParentWindow(0), false); + }); + + it('should reject handles that do not fit in 64 bits', function (done: Mocha.Done): void { + const term = new WindowsTerminal('cmd.exe', [], { useConptyDll }); + assert.throws(() => term.setParentWindow(BigInt('18446744073709551616')), /does not fit in 64 bits/); + term.on('exit', () => done()); + term.kill(); + }); + + it('should re-enable the parent window if the process exits while a dialog left it disabled', async function (): Promise { + this.timeout(60000); + const host = new WindowsTerminal('powershell.exe', rootOwnerArgs(0, 60), { useConptyDll }); + try { + const hostWindows = await readWindows(host); + // Disabling the root owner and exiting stands in for a modal dialog whose process dies. + const term = new WindowsTerminal('powershell.exe', scriptArgs( + `$o = [NodePtyTest.W]::GetAncestor([NodePtyTest.W]::GetConsoleWindow(), 3)`, + `[void][NodePtyTest.W]::EnableWindow($o, $false)`, + `Write-Host ('enabled=' + [NodePtyTest.W]::IsWindowEnabled($o) + ' end')` + ), { useConptyDll, conptyParentWindow: hostWindows.console }); + const exited = new Promise(resolve => term.once('exit', () => resolve())); + assert.strictEqual((await readOutput(term, /enabled=(\w+) end/))[1], 'False'); + await exited; + const checker = new WindowsTerminal('powershell.exe', scriptArgs( + `Write-Host ('enabled=' + [NodePtyTest.W]::IsWindowEnabled([IntPtr]${hostWindows.console}) + ' end')` + ), { useConptyDll }); + assert.strictEqual((await readOutput(checker, /enabled=(\w+) end/))[1], 'True'); + } finally { + host.kill(); + } + }); + }); describe('Regression for #921', () => { it('should not crash with concurrent kills while resizing/clearing/reparenting', function (done) { this.timeout(60000); diff --git a/src/windowsTerminal.ts b/src/windowsTerminal.ts index 9df5b669e..9e0148a93 100644 --- a/src/windowsTerminal.ts +++ b/src/windowsTerminal.ts @@ -48,12 +48,7 @@ export class WindowsTerminal extends Terminal { this._deferreds = []; // Create new termal. - this._agent = new WindowsPtyAgent(file, args, parsedEnv, cwd, this._cols, this._rows, false, opt.useConptyDll, opt.conptyInheritCursor); - if (opt.conptyParentWindow !== undefined) { - // Applied before the shell is created (connect() waits for the conout worker), so the - // shell's first GetConsoleWindow() already sees its owner. - this._agent.setParentWindow(opt.conptyParentWindow); - } + this._agent = new WindowsPtyAgent(file, args, parsedEnv, cwd, this._cols, this._rows, false, opt.useConptyDll, opt.conptyInheritCursor, opt.conptyParentWindow); this._socket = this._agent.outSocket; // The socket 'close' handler that drives 'exit' is only attached after // ready_datapipe, which never fires when connect() fails. @@ -168,10 +163,10 @@ export class WindowsTerminal extends Terminal { }); } - public setParentWindow(handle: number | bigint): void { + public setParentWindow(handle: number | bigint): boolean { // Not deferred: the pseudoconsole exists as soon as the agent is created, and the owner // should be in place before the shell starts asking for it. - this._agent.setParentWindow(handle); + return this._agent.setParentWindow(handle); } public destroy(): void { diff --git a/typings/node-pty.d.ts b/typings/node-pty.d.ts index 2a051ad61..4f7b66d55 100644 --- a/typings/node-pty.d.ts +++ b/typings/node-pty.d.ts @@ -119,6 +119,9 @@ declare module 'node-pty' { * sign-in uses) open on top of the host window instead of behind it. The owner is set before * the process starts. Use {@link IPty.setParentWindow} when the terminal moves to another * window. Only supported when `useConptyDll` is true; ignored otherwise. + * + * When the process exits, the window is re-enabled if a modal dialog that died with the + * process left it disabled. */ conptyParentWindow?: number | bigint; } @@ -190,11 +193,13 @@ declare module 'node-pty' { * * Sets the handle (HWND) of the top-level window that hosts this terminal, see * `IWindowsPtyForkOptions.conptyParentWindow`. Call it again whenever the terminal moves to a - * different window. This is a no-op unless on Windows with `useConptyDll`. - * @param handle The window handle, e.g. read from Electron's - * `BrowserWindow.getNativeWindowHandle()`. + * different window. Optional so existing implementations of `IPty` keep compiling. + * @param handle The window handle, for example Electron's + * `BrowserWindow.getNativeWindowHandle().readBigUInt64LE(0)`. + * @returns Whether ConPTY applied the new owner. This is always false unless on Windows with + * `useConptyDll`, and false once the pty has been killed or has exited. */ - setParentWindow(handle: number | bigint): void; + setParentWindow?(handle: number | bigint): boolean; /** * Writes data to the pty. From 0e662792b2a96535bca850a2df5fa25e5323d4a4 Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Tue, 20 Oct 2026 10:54:59 -0700 Subject: [PATCH 12/36] Don't mark conptyParentWindow and setParentWindow as experimental conpty.dll is no longer experimental, so the APIs added on top of it are documented as regular APIs. --- typings/node-pty.d.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/typings/node-pty.d.ts b/typings/node-pty.d.ts index 4f7b66d55..b66320b62 100644 --- a/typings/node-pty.d.ts +++ b/typings/node-pty.d.ts @@ -111,8 +111,6 @@ declare module 'node-pty' { conptyInheritCursor?: boolean; /** - * (EXPERIMENTAL) - * * The handle (HWND) of the top-level window that hosts this terminal. ConPTY makes it the owner * of the console window returned by `GetConsoleWindow()`, so windows that console applications * parent to it (for example `GetAncestor(GetConsoleWindow(), GA_ROOTOWNER)`, which MSAL/WAM @@ -189,8 +187,6 @@ declare module 'node-pty' { clear(): void; /** - * (EXPERIMENTAL) - * * Sets the handle (HWND) of the top-level window that hosts this terminal, see * `IWindowsPtyForkOptions.conptyParentWindow`. Call it again whenever the terminal moves to a * different window. Optional so existing implementations of `IPty` keep compiling. From f164f25215e4f09c3aa65c39c52e56aab36b3618 Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Tue, 20 Oct 2026 12:55:48 -0700 Subject: [PATCH 13/36] Address review: make re-enabling the parent window opt-in - Add conptyReenableParentWindowOnExit (defaults to false). Re-enabling the window on exit is a heuristic, so hosts have to opt in. - Look for visible owned windows through the whole owner chain, and compare window handles by their low 32 bits. - Run the recovery off the exit thread, since EnableWindow waits for the window's thread. - Validate handles before creating the pseudoconsole: numbers must be safe integers, and bigints must fit in 64 bits. - Only close the pseudoconsole once in kill(). - setParentWindow returns whether the request was sent; ConPTY applies it asynchronously. - The parent window tests wait for every pty to exit, since exiting while a conout worker still flushes its pipe can hang mocha. --- src/interfaces.ts | 5 +- src/native.d.ts | 2 +- src/win/conpty.cc | 55 +++++++---- src/windowsPtyAgent.test.ts | 3 + src/windowsPtyAgent.ts | 30 +++++- src/windowsTerminal.test.ts | 179 +++++++++++++++++++++++++----------- src/windowsTerminal.ts | 4 +- typings/node-pty.d.ts | 26 ++++-- 8 files changed, 215 insertions(+), 89 deletions(-) diff --git a/src/interfaces.ts b/src/interfaces.ts index 1939f8748..e2fd75d51 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -41,8 +41,8 @@ export interface ITerminal { clear(): void; /** - * Sets the top-level window (HWND) that hosts this terminal. Returns whether ConPTY applied it, - * which only happens on Windows with `useConptyDll`. + * Sets the top-level window (HWND) that hosts this terminal. Returns whether the request was sent + * to ConPTY, which only happens on Windows with `useConptyDll`. */ setParentWindow(handle: number | bigint): boolean; @@ -139,6 +139,7 @@ export interface IWindowsPtyForkOptions extends IBasePtyForkOptions { useConptyDll?: boolean; conptyInheritCursor?: boolean; conptyParentWindow?: number | bigint; + conptyReenableParentWindowOnExit?: boolean; } export interface IPtyOpenOptions { diff --git a/src/native.d.ts b/src/native.d.ts index fe65e29d9..bf75a0935 100644 --- a/src/native.d.ts +++ b/src/native.d.ts @@ -7,7 +7,7 @@ interface IConptyNative { connect(ptyId: number, commandLine: string, cwd: string, env: string[], useConptyDll: boolean, onExitCallback: (exitCode: number) => void): { pid: number }; resize(ptyId: number, cols: number, rows: number, useConptyDll: boolean): void; clear(ptyId: number, useConptyDll: boolean): void; - reparent(ptyId: number, hwnd: number | bigint, useConptyDll: boolean): boolean; + reparent(ptyId: number, hwnd: number | bigint, useConptyDll: boolean, reenableOnExit: boolean): boolean; kill(ptyId: number, useConptyDll: boolean): void; } diff --git a/src/win/conpty.cc b/src/win/conpty.cc index 5c4938d16..0a69ac18b 100644 --- a/src/win/conpty.cc +++ b/src/win/conpty.cc @@ -50,7 +50,8 @@ struct pty_baton { HPCON hpc; HANDLE hShell = nullptr; - HWND parentWindow = nullptr; + // Only set when the host opted in to re-enabling its window when the shell exits. + HWND parentWindowToReenable = nullptr; pty_baton(int _id, HANDLE _hIn, HANDLE _hOut, HPCON _hpc) : id(_id), hIn(_hIn), hOut(_hOut), hpc(_hpc) {}; }; @@ -75,6 +76,11 @@ struct ExitEvent { int exit_code = 0; }; +// Only the low 32 bits of a window handle are significant. +static bool IsSameWindow(HWND a, HWND b) { + return static_cast(reinterpret_cast(a)) == static_cast(reinterpret_cast(b)); +} + struct OwnedWindowSearch { HWND owner; bool found; @@ -82,16 +88,24 @@ struct OwnedWindowSearch { static BOOL CALLBACK FindVisibleOwnedWindow(HWND hwnd, LPARAM lParam) { auto* search = reinterpret_cast(lParam); - if (GetWindow(hwnd, GW_OWNER) == search->owner && IsWindowVisible(hwnd)) { - search->found = true; - return FALSE; + if (!IsWindowVisible(hwnd)) { + return TRUE; + } + // Walk the whole owner chain, since a dialog can be owned through a hidden proxy window. + HWND owner = GetWindow(hwnd, GW_OWNER); + for (int depth = 0; owner != nullptr && depth < 64; depth++) { + if (IsSameWindow(owner, search->owner)) { + search->found = true; + return FALSE; + } + owner = GetWindow(owner, GW_OWNER); } return TRUE; } // A dialog that is modal to the parent window disables it until the dialog closes. If the dialog's -// process exits first, nothing re-enables the parent, so do that once no visible owned window is -// left (an owned window that is still visible may be another live modal dialog). +// process exits first, nothing re-enables the parent. This is a heuristic: it re-enables the parent +// when it is disabled and no visible window is owned by it, so hosts must opt in. static void EnableParentWindowIfOrphaned(HWND parentWindow) { if (!IsWindow(parentWindow) || IsWindowEnabled(parentWindow)) { return; @@ -130,14 +144,15 @@ void SetupExitCallback(Napi::Env env, Napi::Function cb, pty_baton* baton) { std::lock_guard lock(g_ptyHandlesMutex); GetExitCodeProcess(baton->hShell, (LPDWORD)(&exit_event->exit_code)); CloseHandle(baton->hShell); - parentWindow = baton->parentWindow; + parentWindow = baton->parentWindowToReenable; const int id = baton->id; std::erase_if(ptyHandles, [id](const auto& ptyHandle) { return ptyHandle->id == id; }); } if (parentWindow != nullptr) { - EnableParentWindowIfOrphaned(parentWindow); + // EnableWindow waits for the parent window's thread, so don't hold up the exit event. + std::thread([parentWindow] { EnableParentWindowIfOrphaned(parentWindow); }).detach(); } auto status = tsfn.BlockingCall(exit_event, callback); // In main thread @@ -594,11 +609,12 @@ static Napi::Value PtyReparent(const Napi::CallbackInfo& info) { Napi::Env env(info.Env()); Napi::HandleScope scope(env); - if (info.Length() != 3 || + if (info.Length() != 4 || !info[0].IsNumber() || !(info[1].IsNumber() || info[1].IsBigInt()) || - !info[2].IsBoolean()) { - throw Napi::Error::New(env, "Usage: pty.reparent(id, hwnd, useConptyDll)"); + !info[2].IsBoolean() || + !info[3].IsBoolean()) { + throw Napi::Error::New(env, "Usage: pty.reparent(id, hwnd, useConptyDll, reenableOnExit)"); } int id = info[0].As().Int32Value(); @@ -618,6 +634,7 @@ static Napi::Value PtyReparent(const Napi::CallbackInfo& info) { hwndValue = static_cast(info[1].As().Int64Value()); } const bool useConptyDll = info[2].As().Value(); + const bool reenableOnExit = info[3].As().Value(); // ConptyReparentPseudoConsole only exists in the bundled conpty.dll. if (!useConptyDll) { @@ -627,7 +644,7 @@ static Napi::Value PtyReparent(const Napi::CallbackInfo& info) { std::lock_guard lock(g_ptyHandlesMutex); pty_baton* handle = get_pty_baton(lock, id); - bool applied = false; + bool sent = false; if (handle != nullptr && handle->hpc != nullptr) { HANDLE hLibrary = LoadConptyDll(info, useConptyDll); bool fLoadedDll = hLibrary != nullptr; @@ -637,18 +654,18 @@ static Napi::Value PtyReparent(const Napi::CallbackInfo& info) { (HMODULE)hLibrary, "ConptyReparentPseudoConsole"); if (pfnReparentPseudoConsole) { - // Makes newParent the owner of the console window returned by - // GetConsoleWindow(), so windows parented to it stack above the host. + // Asks ConPTY to make newParent the owner of the console window returned by + // GetConsoleWindow(); ConPTY applies it asynchronously. const HWND newParent = reinterpret_cast(static_cast(hwndValue)); - applied = SUCCEEDED(pfnReparentPseudoConsole(handle->hpc, newParent)); - if (applied) { - handle->parentWindow = newParent; + sent = SUCCEEDED(pfnReparentPseudoConsole(handle->hpc, newParent)); + if (sent) { + handle->parentWindowToReenable = reenableOnExit ? newParent : nullptr; } } } } - return Napi::Boolean::New(env, applied); + return Napi::Boolean::New(env, sent); } static Napi::Value PtyKill(const Napi::CallbackInfo& info) { @@ -675,7 +692,7 @@ static Napi::Value PtyKill(const Napi::CallbackInfo& info) { PFNCLOSEPSEUDOCONSOLE const pfnClosePseudoConsole = (PFNCLOSEPSEUDOCONSOLE)GetProcAddress( (HMODULE)hLibrary, useConptyDll ? "ConptyClosePseudoConsole" : "ClosePseudoConsole"); - if (pfnClosePseudoConsole) + if (pfnClosePseudoConsole && handle->hpc != nullptr) { pfnClosePseudoConsole(handle->hpc); // Closing frees the pseudoconsole, but the baton lives until the exit thread erases it. diff --git a/src/windowsPtyAgent.test.ts b/src/windowsPtyAgent.test.ts index af4056308..8b4c687b5 100644 --- a/src/windowsPtyAgent.test.ts +++ b/src/windowsPtyAgent.test.ts @@ -146,6 +146,7 @@ if (process.platform === 'win32') { false, false, undefined, + false, createTestAgentOptions(connection, 10) ); @@ -176,6 +177,7 @@ if (process.platform === 'win32') { false, false, undefined, + false, createTestAgentOptions(connection, 1000) ); @@ -203,6 +205,7 @@ if (process.platform === 'win32') { false, false, undefined, + false, createTestAgentOptions(connection, 1000) ); let errorCount = 0; diff --git a/src/windowsPtyAgent.ts b/src/windowsPtyAgent.ts index 717590cc9..d88d07741 100644 --- a/src/windowsPtyAgent.ts +++ b/src/windowsPtyAgent.ts @@ -71,8 +71,14 @@ export class WindowsPtyAgent { private _useConptyDll: boolean = false, conptyInheritCursor: boolean = false, conptyParentWindow?: number | bigint, + private _reenableParentWindowOnExit: boolean = false, options: IWindowsPtyAgentOptions = defaultWindowsPtyAgentOptions ) { + // Validate before allocating the pseudoconsole, so bad input leaves nothing behind. + if (conptyParentWindow !== undefined) { + validateWindowHandle(conptyParentWindow); + } + if (!conptyNative) { conptyNative = loadNativeModule('conpty').module; } @@ -95,9 +101,9 @@ export class WindowsPtyAgent { this._pty = term.pty; if (conptyParentWindow !== undefined) { - // Before connect() starts the shell, so its first GetConsoleWindow() already has its owner. + // Requested before connect() launches the shell, like Windows Terminal does. try { - this._ptyNative.reparent(this._pty, conptyParentWindow, this._useConptyDll); + this._ptyNative.reparent(this._pty, conptyParentWindow, this._useConptyDll, this._reenableParentWindowOnExit); } catch (e) { this._ptyNative.kill(this._pty, this._useConptyDll); throw e; @@ -208,10 +214,11 @@ export class WindowsPtyAgent { * Tells ConPTY which top-level window hosts the terminal (ConptyReparentPseudoConsole). The * window becomes the owner of the console window returned by GetConsoleWindow(), so dialogs * that console apps parent to it (e.g. WAM sign-in) stack above the host window. Returns - * whether ConPTY applied the new owner. + * whether the request was sent to ConPTY, which applies it asynchronously. */ public setParentWindow(handle: number | bigint): boolean { - return this._ptyNative.reparent(this._pty, handle, this._useConptyDll); + validateWindowHandle(handle); + return this._ptyNative.reparent(this._pty, handle, this._useConptyDll, this._reenableParentWindowOnExit); } public kill(): void { @@ -371,3 +378,18 @@ function repeatText(text: string, count: number): string { function xOr(arg1: boolean, arg2: boolean): boolean { return ((arg1 && !arg2) || (!arg1 && arg2)); } + +/** + * Throws unless the handle is a safe integer or a bigint that fits in 64 bits. Signed and unsigned + * bigints are both accepted, since a window handle may be sign-extended. + */ +function validateWindowHandle(handle: number | bigint): void { + if (typeof handle === 'bigint') { + if (handle < BigInt('-9223372036854775808') || handle > BigInt('18446744073709551615')) { + throw new Error('Window handle does not fit in 64 bits'); + } + } else if (!Number.isSafeInteger(handle)) { + // Larger numbers can't hold an exact handle. + throw new Error('Window handle must be a safe integer or a bigint'); + } +} diff --git a/src/windowsTerminal.test.ts b/src/windowsTerminal.test.ts index 37157753f..5be964753 100644 --- a/src/windowsTerminal.test.ts +++ b/src/windowsTerminal.test.ts @@ -6,6 +6,7 @@ import * as fs from 'fs'; import * as assert from 'assert'; import { WindowsTerminal } from './windowsTerminal'; +import { IWindowsPtyForkOptions } from './interfaces'; import * as path from 'path'; import * as psList from 'ps-list'; @@ -284,7 +285,7 @@ if (process.platform === 'win32') { }); describe('parent window', () => { - const user32 = `Add-Type -Namespace NodePtyTest -Name W -MemberDefinition '[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow(); [DllImport("user32.dll")] public static extern IntPtr GetAncestor(IntPtr h, uint f); [DllImport("user32.dll")] public static extern bool EnableWindow(IntPtr h, bool e); [DllImport("user32.dll")] public static extern bool IsWindowEnabled(IntPtr h);'`; + const user32 = `Add-Type -Namespace NodePtyTest -Name W -MemberDefinition '[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow(); [DllImport("user32.dll")] public static extern IntPtr GetAncestor(IntPtr h, uint f); [DllImport("user32.dll")] public static extern bool EnableWindow(IntPtr h, bool e); [DllImport("user32.dll")] public static extern bool IsWindowEnabled(IntPtr h); [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern IntPtr CreateWindowExW(int ex, string cls, string title, int style, int x, int y, int w, int h, IntPtr owner, IntPtr menu, IntPtr inst, IntPtr param);'`; function scriptArgs(...lines: string[]): string { return `-NoProfile -NonInteractive -EncodedCommand ${Buffer.from([user32, ...lines].join('; '), 'utf16le').toString('base64')}`; @@ -301,6 +302,44 @@ if (process.platform === 'win32') { ); } + // Disabling the root owner and exiting stands in for a modal dialog whose process dies. + function disableRootOwnerArgs(): string { + return scriptArgs( + `$o = [NodePtyTest.W]::GetAncestor([NodePtyTest.W]::GetConsoleWindow(), 3)`, + `[void][NodePtyTest.W]::EnableWindow($o, $false)`, + `Write-Host ('enabled=' + [NodePtyTest.W]::IsWindowEnabled($o) + ' end')` + ); + } + + const started: Array<{ exited: Promise, kill: () => void }> = []; + + function startPty(file: string, args: string | string[], options: IWindowsPtyForkOptions = {}): WindowsTerminal { + const term = new WindowsTerminal(file, args, { useConptyDll, ...options }); + let hasExited = false; + const exited = new Promise(resolve => term.once('exit', () => { + hasExited = true; + resolve(); + })); + started.push({ + exited, + kill: () => { + if (!hasExited) { + term.kill(); + } + } + }); + return term; + } + + // Wait for every pty to exit, even after a failed test: exiting while a conout worker still + // flushes its pipe can hang mocha. + afterEach(async function (): Promise { + this.timeout(30000); + const ptys = started.splice(0); + ptys.forEach(pty => pty.kill()); + await Promise.all(ptys.map(pty => pty.exited)); + }); + function readOutput(term: WindowsTerminal, pattern: RegExp): Promise { return new Promise(resolve => { let output = ''; @@ -315,46 +354,54 @@ if (process.platform === 'win32') { } async function readWindows(term: WindowsTerminal): Promise<{ console: number, rootOwner: number }> { - const match = await readOutput(term, /console=(\d+) rootOwner=(\d+) end/); + const match = await readOutput(term, /console=(-?\d+) rootOwner=(-?\d+) end/); return { console: parseInt(match[1], 10), rootOwner: parseInt(match[2], 10) }; } + // Another pty's hidden top-level console window stands in for the host app's window. + async function startHostWindow(): Promise { + return (await readWindows(startPty('powershell.exe', rootOwnerArgs(0, 60)))).console; + } + + // Re-enabling happens off the exit path, so wait up to waitMs for the window to be enabled. + async function readEnabled(hwnd: number, waitMs: number): Promise { + const checker = startPty('powershell.exe', scriptArgs( + `$h = [IntPtr]${hwnd}`, + `for ($i = 0; $i -lt ${waitMs / 100} -and -not [NodePtyTest.W]::IsWindowEnabled($h); $i++) { Start-Sleep -Milliseconds 100 }`, + `Write-Host ('enabled=' + [NodePtyTest.W]::IsWindowEnabled($h) + ' end')` + )); + return (await readOutput(checker, /enabled=(\w+) end/))[1]; + } + + async function disableParentAndExit(parent: number, options: IWindowsPtyForkOptions): Promise { + const term = startPty('powershell.exe', disableRootOwnerArgs(), { conptyParentWindow: parent, ...options }); + const exited = new Promise(resolve => term.once('exit', () => resolve())); + assert.strictEqual((await readOutput(term, /enabled=(\w+) end/))[1], 'False'); + await exited; + } + it('should make conptyParentWindow the owner of the console window before the shell starts', async function (): Promise { this.timeout(60000); - // Another pty's hidden top-level console window stands in for the host app's window. - const host = new WindowsTerminal('powershell.exe', rootOwnerArgs(0, 60), { useConptyDll }); - try { - const hostWindows = await readWindows(host); - const term = new WindowsTerminal('powershell.exe', rootOwnerArgs(0), { useConptyDll, conptyParentWindow: hostWindows.console }); - const windows = await readWindows(term); - assert.strictEqual(windows.rootOwner, useConptyDll ? hostWindows.console : windows.console); - term.kill(); - } finally { - host.kill(); - } + const hostWindow = await startHostWindow(); + const windows = await readWindows(startPty('powershell.exe', rootOwnerArgs(0), { conptyParentWindow: hostWindow })); + assert.strictEqual(windows.rootOwner, useConptyDll ? hostWindow : windows.console); }); it('should change the owner when setParentWindow is called while the shell runs', async function (): Promise { this.timeout(60000); - const host = new WindowsTerminal('powershell.exe', rootOwnerArgs(0, 60), { useConptyDll }); - try { - const hostWindows = await readWindows(host); - const term = new WindowsTerminal('powershell.exe', rootOwnerArgs(3000), { useConptyDll }); - let applied: boolean | undefined; - // The first data event means the shell is running and conpty has been released. - term.once('data', () => applied = term.setParentWindow(BigInt(hostWindows.console))); - const windows = await readWindows(term); - assert.strictEqual(applied, useConptyDll); - assert.strictEqual(windows.rootOwner, useConptyDll ? hostWindows.console : windows.console); - term.kill(); - } finally { - host.kill(); - } + const hostWindow = await startHostWindow(); + const term = startPty('powershell.exe', rootOwnerArgs(3000)); + let sent: boolean | undefined; + // The first data event means the shell is running and conpty has been released. + term.once('data', () => sent = term.setParentWindow(BigInt(hostWindow))); + const windows = await readWindows(term); + assert.strictEqual(sent, useConptyDll); + assert.strictEqual(windows.rootOwner, useConptyDll ? hostWindow : windows.console); }); - it('should not apply setParentWindow once the pty is killed', async function (): Promise { + it('should not send setParentWindow once the pseudoconsole is closed', async function (): Promise { this.timeout(20000); - const term = new WindowsTerminal('cmd.exe', [], { useConptyDll }); + const term = startPty('cmd.exe', []); const exited = new Promise(resolve => term.once('exit', () => resolve())); await new Promise(resolve => term.once('data', () => resolve())); term.kill(); @@ -363,34 +410,60 @@ if (process.platform === 'win32') { assert.strictEqual(term.setParentWindow(0), false); }); - it('should reject handles that do not fit in 64 bits', function (done: Mocha.Done): void { - const term = new WindowsTerminal('cmd.exe', [], { useConptyDll }); - assert.throws(() => term.setParentWindow(BigInt('18446744073709551616')), /does not fit in 64 bits/); - term.on('exit', () => done()); - term.kill(); + it('should reject handles that are not safe integers or bigints that fit in 64 bits', () => { + const invalid: Array<[number | bigint, RegExp]> = [ + [BigInt('18446744073709551616'), /does not fit in 64 bits/], + [BigInt('-9223372036854775809'), /does not fit in 64 bits/], + [1.5, /must be a safe integer/], + [Number.MAX_SAFE_INTEGER + 1, /must be a safe integer/] + ]; + // Validated before the pseudoconsole is created, so nothing is left behind + for (const [handle, error] of invalid) { + assert.throws(() => startPty('cmd.exe', [], { conptyParentWindow: handle }), error); + } + const term = startPty('cmd.exe', []); + for (const [handle, error] of invalid) { + assert.throws(() => term.setParentWindow(handle), error); + } }); - it('should re-enable the parent window if the process exits while a dialog left it disabled', async function (): Promise { + it('should re-enable the parent window on exit when asked, if a dialog left it disabled', async function (): Promise { + if (!useConptyDll) { + this.skip(); + } this.timeout(60000); - const host = new WindowsTerminal('powershell.exe', rootOwnerArgs(0, 60), { useConptyDll }); - try { - const hostWindows = await readWindows(host); - // Disabling the root owner and exiting stands in for a modal dialog whose process dies. - const term = new WindowsTerminal('powershell.exe', scriptArgs( - `$o = [NodePtyTest.W]::GetAncestor([NodePtyTest.W]::GetConsoleWindow(), 3)`, - `[void][NodePtyTest.W]::EnableWindow($o, $false)`, - `Write-Host ('enabled=' + [NodePtyTest.W]::IsWindowEnabled($o) + ' end')` - ), { useConptyDll, conptyParentWindow: hostWindows.console }); - const exited = new Promise(resolve => term.once('exit', () => resolve())); - assert.strictEqual((await readOutput(term, /enabled=(\w+) end/))[1], 'False'); - await exited; - const checker = new WindowsTerminal('powershell.exe', scriptArgs( - `Write-Host ('enabled=' + [NodePtyTest.W]::IsWindowEnabled([IntPtr]${hostWindows.console}) + ' end')` - ), { useConptyDll }); - assert.strictEqual((await readOutput(checker, /enabled=(\w+) end/))[1], 'True'); - } finally { - host.kill(); + const hostWindow = await startHostWindow(); + await disableParentAndExit(hostWindow, { conptyReenableParentWindowOnExit: true }); + assert.strictEqual(await readEnabled(hostWindow, 5000), 'True'); + }); + + it('should leave the parent window disabled on exit unless asked to re-enable it', async function (): Promise { + if (!useConptyDll) { + this.skip(); } + this.timeout(60000); + const hostWindow = await startHostWindow(); + await disableParentAndExit(hostWindow, {}); + assert.strictEqual(await readEnabled(hostWindow, 3000), 'False'); + }); + + it('should not re-enable the parent window while it still owns a visible window', async function (): Promise { + if (!useConptyDll) { + this.skip(); + } + this.timeout(60000); + const hostWindow = await startHostWindow(); + // A visible off-screen window owned through a hidden proxy window, like a modal dialog + // shown by another process. + const dialog = startPty('powershell.exe', scriptArgs( + `$proxy = [NodePtyTest.W]::CreateWindowExW(0x80, 'STATIC', 'proxy', 0x80000000, 0, 0, 1, 1, [IntPtr]${hostWindow}, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero)`, + `$dialog = [NodePtyTest.W]::CreateWindowExW(0x08000080, 'STATIC', 'dialog', 0x90000000, -32000, -32000, 1, 1, $proxy, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero)`, + `Write-Host ('dialog=' + $dialog.ToInt64() + ' end')`, + `Start-Sleep -Seconds 60` + )); + assert.notStrictEqual((await readOutput(dialog, /dialog=(-?\d+) end/))[1], '0'); + await disableParentAndExit(hostWindow, { conptyReenableParentWindowOnExit: true }); + assert.strictEqual(await readEnabled(hostWindow, 3000), 'False'); }); }); describe('Regression for #921', () => { diff --git a/src/windowsTerminal.ts b/src/windowsTerminal.ts index 9e0148a93..eaeb8f1b2 100644 --- a/src/windowsTerminal.ts +++ b/src/windowsTerminal.ts @@ -48,7 +48,7 @@ export class WindowsTerminal extends Terminal { this._deferreds = []; // Create new termal. - this._agent = new WindowsPtyAgent(file, args, parsedEnv, cwd, this._cols, this._rows, false, opt.useConptyDll, opt.conptyInheritCursor, opt.conptyParentWindow); + this._agent = new WindowsPtyAgent(file, args, parsedEnv, cwd, this._cols, this._rows, false, opt.useConptyDll, opt.conptyInheritCursor, opt.conptyParentWindow, opt.conptyReenableParentWindowOnExit); this._socket = this._agent.outSocket; // The socket 'close' handler that drives 'exit' is only attached after // ready_datapipe, which never fires when connect() fails. @@ -165,7 +165,7 @@ export class WindowsTerminal extends Terminal { public setParentWindow(handle: number | bigint): boolean { // Not deferred: the pseudoconsole exists as soon as the agent is created, and the owner - // should be in place before the shell starts asking for it. + // should be requested before the shell starts asking for it. return this._agent.setParentWindow(handle); } diff --git a/typings/node-pty.d.ts b/typings/node-pty.d.ts index b66320b62..01d75d9db 100644 --- a/typings/node-pty.d.ts +++ b/typings/node-pty.d.ts @@ -114,14 +114,23 @@ declare module 'node-pty' { * The handle (HWND) of the top-level window that hosts this terminal. ConPTY makes it the owner * of the console window returned by `GetConsoleWindow()`, so windows that console applications * parent to it (for example `GetAncestor(GetConsoleWindow(), GA_ROOTOWNER)`, which MSAL/WAM - * sign-in uses) open on top of the host window instead of behind it. The owner is set before - * the process starts. Use {@link IPty.setParentWindow} when the terminal moves to another - * window. Only supported when `useConptyDll` is true; ignored otherwise. - * - * When the process exits, the window is re-enabled if a modal dialog that died with the - * process left it disabled. + * sign-in uses) open on top of the host window instead of behind it. The owner is requested + * before the process is launched. Use {@link IPty.setParentWindow} when the terminal moves to + * another window. Must be a safe integer, or a bigint that fits in 64 bits. Only supported when + * `useConptyDll` is true; ignored otherwise. */ conptyParentWindow?: number | bigint; + + /** + * Whether to re-enable the parent window when the process exits, if the window is still + * disabled and no visible window is owned by it, directly or through other owned windows. The + * parent window is the last one set with `conptyParentWindow` or {@link IPty.setParentWindow}. + * This recovers from a modal dialog that died with its process, which otherwise leaves the + * parent window disabled for good. It's a heuristic that can't tell why the window is disabled, + * so only set it if the host never disables its window without showing a window it owns. Only + * supported when `useConptyDll` is true. Defaults to false. + */ + conptyReenableParentWindowOnExit?: boolean; } /** @@ -192,8 +201,9 @@ declare module 'node-pty' { * different window. Optional so existing implementations of `IPty` keep compiling. * @param handle The window handle, for example Electron's * `BrowserWindow.getNativeWindowHandle().readBigUInt64LE(0)`. - * @returns Whether ConPTY applied the new owner. This is always false unless on Windows with - * `useConptyDll`, and false once the pty has been killed or has exited. + * @returns Whether the request was sent to ConPTY, which applies it asynchronously. This is + * always false unless on Windows with `useConptyDll`, and false once the pseudoconsole is + * closed. */ setParentWindow?(handle: number | bigint): boolean; From 327e24c997a6039dabe49abef73dc23d867ae1f4 Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Tue, 20 Oct 2026 13:43:40 -0700 Subject: [PATCH 14/36] Don't call the parent window re-enable a heuristic It's an exact check: the window is still disabled and no visible window is owned by it. What it can't tell is why the window is disabled, which is why hosts opt in. --- src/win/conpty.cc | 4 ++-- typings/node-pty.d.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/win/conpty.cc b/src/win/conpty.cc index 0a69ac18b..1439dc278 100644 --- a/src/win/conpty.cc +++ b/src/win/conpty.cc @@ -104,8 +104,8 @@ static BOOL CALLBACK FindVisibleOwnedWindow(HWND hwnd, LPARAM lParam) { } // A dialog that is modal to the parent window disables it until the dialog closes. If the dialog's -// process exits first, nothing re-enables the parent. This is a heuristic: it re-enables the parent -// when it is disabled and no visible window is owned by it, so hosts must opt in. +// process exits first, nothing re-enables the parent, so re-enable it when it is still disabled and +// no visible window is owned by it. This can't tell why the window is disabled, so hosts opt in. static void EnableParentWindowIfOrphaned(HWND parentWindow) { if (!IsWindow(parentWindow) || IsWindowEnabled(parentWindow)) { return; diff --git a/typings/node-pty.d.ts b/typings/node-pty.d.ts index 01d75d9db..cafa70bcf 100644 --- a/typings/node-pty.d.ts +++ b/typings/node-pty.d.ts @@ -126,9 +126,9 @@ declare module 'node-pty' { * disabled and no visible window is owned by it, directly or through other owned windows. The * parent window is the last one set with `conptyParentWindow` or {@link IPty.setParentWindow}. * This recovers from a modal dialog that died with its process, which otherwise leaves the - * parent window disabled for good. It's a heuristic that can't tell why the window is disabled, - * so only set it if the host never disables its window without showing a window it owns. Only - * supported when `useConptyDll` is true. Defaults to false. + * parent window disabled for good. The check can't tell why the window is disabled, so only set + * it if the host never disables its window without showing a window it owns. Only supported + * when `useConptyDll` is true. Defaults to false. */ conptyReenableParentWindowOnExit?: boolean; } From 95ed9a49a64e106770995760d902ccb9aecc8823 Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Tue, 20 Oct 2026 14:36:15 -0700 Subject: [PATCH 15/36] Keep setParentWindow as the only new API Remove conptyParentWindow. setParentWindow() isn't deferred, and the shell is only launched once the conout worker is ready, so calling it right after spawn() sends the request before the shell starts. Remove conptyReenableParentWindowOnExit and re-enabling the parent window on exit. That moves to a follow-up issue, so this PR stays focused on the parenting change. --- src/interfaces.ts | 2 - src/native.d.ts | 2 +- src/win/conpty.cc | 62 ++-------------------------- src/windowsPtyAgent.test.ts | 6 --- src/windowsPtyAgent.ts | 19 +-------- src/windowsTerminal.test.ts | 82 ++++--------------------------------- src/windowsTerminal.ts | 2 +- typings/node-pty.d.ts | 36 ++++------------ 8 files changed, 24 insertions(+), 187 deletions(-) diff --git a/src/interfaces.ts b/src/interfaces.ts index e2fd75d51..d69b947bb 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -138,8 +138,6 @@ export interface IWindowsPtyForkOptions extends IBasePtyForkOptions { useConpty?: boolean; useConptyDll?: boolean; conptyInheritCursor?: boolean; - conptyParentWindow?: number | bigint; - conptyReenableParentWindowOnExit?: boolean; } export interface IPtyOpenOptions { diff --git a/src/native.d.ts b/src/native.d.ts index bf75a0935..fe65e29d9 100644 --- a/src/native.d.ts +++ b/src/native.d.ts @@ -7,7 +7,7 @@ interface IConptyNative { connect(ptyId: number, commandLine: string, cwd: string, env: string[], useConptyDll: boolean, onExitCallback: (exitCode: number) => void): { pid: number }; resize(ptyId: number, cols: number, rows: number, useConptyDll: boolean): void; clear(ptyId: number, useConptyDll: boolean): void; - reparent(ptyId: number, hwnd: number | bigint, useConptyDll: boolean, reenableOnExit: boolean): boolean; + reparent(ptyId: number, hwnd: number | bigint, useConptyDll: boolean): boolean; kill(ptyId: number, useConptyDll: boolean): void; } diff --git a/src/win/conpty.cc b/src/win/conpty.cc index 1439dc278..1fa7189a1 100644 --- a/src/win/conpty.cc +++ b/src/win/conpty.cc @@ -50,8 +50,6 @@ struct pty_baton { HPCON hpc; HANDLE hShell = nullptr; - // Only set when the host opted in to re-enabling its window when the shell exits. - HWND parentWindowToReenable = nullptr; pty_baton(int _id, HANDLE _hIn, HANDLE _hOut, HPCON _hpc) : id(_id), hIn(_hIn), hOut(_hOut), hpc(_hpc) {}; }; @@ -76,47 +74,6 @@ struct ExitEvent { int exit_code = 0; }; -// Only the low 32 bits of a window handle are significant. -static bool IsSameWindow(HWND a, HWND b) { - return static_cast(reinterpret_cast(a)) == static_cast(reinterpret_cast(b)); -} - -struct OwnedWindowSearch { - HWND owner; - bool found; -}; - -static BOOL CALLBACK FindVisibleOwnedWindow(HWND hwnd, LPARAM lParam) { - auto* search = reinterpret_cast(lParam); - if (!IsWindowVisible(hwnd)) { - return TRUE; - } - // Walk the whole owner chain, since a dialog can be owned through a hidden proxy window. - HWND owner = GetWindow(hwnd, GW_OWNER); - for (int depth = 0; owner != nullptr && depth < 64; depth++) { - if (IsSameWindow(owner, search->owner)) { - search->found = true; - return FALSE; - } - owner = GetWindow(owner, GW_OWNER); - } - return TRUE; -} - -// A dialog that is modal to the parent window disables it until the dialog closes. If the dialog's -// process exits first, nothing re-enables the parent, so re-enable it when it is still disabled and -// no visible window is owned by it. This can't tell why the window is disabled, so hosts opt in. -static void EnableParentWindowIfOrphaned(HWND parentWindow) { - if (!IsWindow(parentWindow) || IsWindowEnabled(parentWindow)) { - return; - } - OwnedWindowSearch search = { parentWindow, false }; - EnumWindows(FindVisibleOwnedWindow, reinterpret_cast(&search)); - if (!search.found) { - EnableWindow(parentWindow, TRUE); - } -} - void SetupExitCallback(Napi::Env env, Napi::Function cb, pty_baton* baton) { std::thread *th = new std::thread; // Don't use Napi::AsyncWorker which is limited by UV_THREADPOOL_SIZE. @@ -139,21 +96,15 @@ void SetupExitCallback(Napi::Env env, Napi::Function cb, pty_baton* baton) { ExitEvent *exit_event = new ExitEvent; // Wait for process to complete. WaitForSingleObject(baton->hShell, INFINITE); - HWND parentWindow = nullptr; { std::lock_guard lock(g_ptyHandlesMutex); GetExitCodeProcess(baton->hShell, (LPDWORD)(&exit_event->exit_code)); CloseHandle(baton->hShell); - parentWindow = baton->parentWindowToReenable; const int id = baton->id; std::erase_if(ptyHandles, [id](const auto& ptyHandle) { return ptyHandle->id == id; }); } - if (parentWindow != nullptr) { - // EnableWindow waits for the parent window's thread, so don't hold up the exit event. - std::thread([parentWindow] { EnableParentWindowIfOrphaned(parentWindow); }).detach(); - } auto status = tsfn.BlockingCall(exit_event, callback); // In main thread switch (status) { @@ -609,12 +560,11 @@ static Napi::Value PtyReparent(const Napi::CallbackInfo& info) { Napi::Env env(info.Env()); Napi::HandleScope scope(env); - if (info.Length() != 4 || + if (info.Length() != 3 || !info[0].IsNumber() || !(info[1].IsNumber() || info[1].IsBigInt()) || - !info[2].IsBoolean() || - !info[3].IsBoolean()) { - throw Napi::Error::New(env, "Usage: pty.reparent(id, hwnd, useConptyDll, reenableOnExit)"); + !info[2].IsBoolean()) { + throw Napi::Error::New(env, "Usage: pty.reparent(id, hwnd, useConptyDll)"); } int id = info[0].As().Int32Value(); @@ -634,7 +584,6 @@ static Napi::Value PtyReparent(const Napi::CallbackInfo& info) { hwndValue = static_cast(info[1].As().Int64Value()); } const bool useConptyDll = info[2].As().Value(); - const bool reenableOnExit = info[3].As().Value(); // ConptyReparentPseudoConsole only exists in the bundled conpty.dll. if (!useConptyDll) { @@ -642,7 +591,7 @@ static Napi::Value PtyReparent(const Napi::CallbackInfo& info) { } std::lock_guard lock(g_ptyHandlesMutex); - pty_baton* handle = get_pty_baton(lock, id); + const pty_baton* handle = get_pty_baton(lock, id); bool sent = false; if (handle != nullptr && handle->hpc != nullptr) { @@ -658,9 +607,6 @@ static Napi::Value PtyReparent(const Napi::CallbackInfo& info) { // GetConsoleWindow(); ConPTY applies it asynchronously. const HWND newParent = reinterpret_cast(static_cast(hwndValue)); sent = SUCCEEDED(pfnReparentPseudoConsole(handle->hpc, newParent)); - if (sent) { - handle->parentWindowToReenable = reenableOnExit ? newParent : nullptr; - } } } } diff --git a/src/windowsPtyAgent.test.ts b/src/windowsPtyAgent.test.ts index 8b4c687b5..9d6979500 100644 --- a/src/windowsPtyAgent.test.ts +++ b/src/windowsPtyAgent.test.ts @@ -145,8 +145,6 @@ if (process.platform === 'win32') { false, false, false, - undefined, - false, createTestAgentOptions(connection, 10) ); @@ -176,8 +174,6 @@ if (process.platform === 'win32') { false, false, false, - undefined, - false, createTestAgentOptions(connection, 1000) ); @@ -204,8 +200,6 @@ if (process.platform === 'win32') { false, false, false, - undefined, - false, createTestAgentOptions(connection, 1000) ); let errorCount = 0; diff --git a/src/windowsPtyAgent.ts b/src/windowsPtyAgent.ts index d88d07741..e1068bca4 100644 --- a/src/windowsPtyAgent.ts +++ b/src/windowsPtyAgent.ts @@ -70,15 +70,8 @@ export class WindowsPtyAgent { debug: boolean, private _useConptyDll: boolean = false, conptyInheritCursor: boolean = false, - conptyParentWindow?: number | bigint, - private _reenableParentWindowOnExit: boolean = false, options: IWindowsPtyAgentOptions = defaultWindowsPtyAgentOptions ) { - // Validate before allocating the pseudoconsole, so bad input leaves nothing behind. - if (conptyParentWindow !== undefined) { - validateWindowHandle(conptyParentWindow); - } - if (!conptyNative) { conptyNative = loadNativeModule('conpty').module; } @@ -100,16 +93,6 @@ export class WindowsPtyAgent { // as a terminal id. this._pty = term.pty; - if (conptyParentWindow !== undefined) { - // Requested before connect() launches the shell, like Windows Terminal does. - try { - this._ptyNative.reparent(this._pty, conptyParentWindow, this._useConptyDll, this._reenableParentWindowOnExit); - } catch (e) { - this._ptyNative.kill(this._pty, this._useConptyDll); - throw e; - } - } - // Create terminal pipe IPC channel and forward to a local unix socket. this._outSocket = new Socket(); this._outSocket.setEncoding('utf8'); @@ -218,7 +201,7 @@ export class WindowsPtyAgent { */ public setParentWindow(handle: number | bigint): boolean { validateWindowHandle(handle); - return this._ptyNative.reparent(this._pty, handle, this._useConptyDll, this._reenableParentWindowOnExit); + return this._ptyNative.reparent(this._pty, handle, this._useConptyDll); } public kill(): void { diff --git a/src/windowsTerminal.test.ts b/src/windowsTerminal.test.ts index 5be964753..5fa497339 100644 --- a/src/windowsTerminal.test.ts +++ b/src/windowsTerminal.test.ts @@ -285,7 +285,7 @@ if (process.platform === 'win32') { }); describe('parent window', () => { - const user32 = `Add-Type -Namespace NodePtyTest -Name W -MemberDefinition '[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow(); [DllImport("user32.dll")] public static extern IntPtr GetAncestor(IntPtr h, uint f); [DllImport("user32.dll")] public static extern bool EnableWindow(IntPtr h, bool e); [DllImport("user32.dll")] public static extern bool IsWindowEnabled(IntPtr h); [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern IntPtr CreateWindowExW(int ex, string cls, string title, int style, int x, int y, int w, int h, IntPtr owner, IntPtr menu, IntPtr inst, IntPtr param);'`; + const user32 = `Add-Type -Namespace NodePtyTest -Name W -MemberDefinition '[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow(); [DllImport("user32.dll")] public static extern IntPtr GetAncestor(IntPtr h, uint f);'`; function scriptArgs(...lines: string[]): string { return `-NoProfile -NonInteractive -EncodedCommand ${Buffer.from([user32, ...lines].join('; '), 'utf16le').toString('base64')}`; @@ -302,15 +302,6 @@ if (process.platform === 'win32') { ); } - // Disabling the root owner and exiting stands in for a modal dialog whose process dies. - function disableRootOwnerArgs(): string { - return scriptArgs( - `$o = [NodePtyTest.W]::GetAncestor([NodePtyTest.W]::GetConsoleWindow(), 3)`, - `[void][NodePtyTest.W]::EnableWindow($o, $false)`, - `Write-Host ('enabled=' + [NodePtyTest.W]::IsWindowEnabled($o) + ' end')` - ); - } - const started: Array<{ exited: Promise, kill: () => void }> = []; function startPty(file: string, args: string | string[], options: IWindowsPtyForkOptions = {}): WindowsTerminal { @@ -331,8 +322,8 @@ if (process.platform === 'win32') { return term; } - // Wait for every pty to exit, even after a failed test: exiting while a conout worker still - // flushes its pipe can hang mocha. + // Kill and wait for every pty, even after a failed test: ptys left running can keep mocha + // from exiting. afterEach(async function (): Promise { this.timeout(30000); const ptys = started.splice(0); @@ -363,27 +354,13 @@ if (process.platform === 'win32') { return (await readWindows(startPty('powershell.exe', rootOwnerArgs(0, 60)))).console; } - // Re-enabling happens off the exit path, so wait up to waitMs for the window to be enabled. - async function readEnabled(hwnd: number, waitMs: number): Promise { - const checker = startPty('powershell.exe', scriptArgs( - `$h = [IntPtr]${hwnd}`, - `for ($i = 0; $i -lt ${waitMs / 100} -and -not [NodePtyTest.W]::IsWindowEnabled($h); $i++) { Start-Sleep -Milliseconds 100 }`, - `Write-Host ('enabled=' + [NodePtyTest.W]::IsWindowEnabled($h) + ' end')` - )); - return (await readOutput(checker, /enabled=(\w+) end/))[1]; - } - - async function disableParentAndExit(parent: number, options: IWindowsPtyForkOptions): Promise { - const term = startPty('powershell.exe', disableRootOwnerArgs(), { conptyParentWindow: parent, ...options }); - const exited = new Promise(resolve => term.once('exit', () => resolve())); - assert.strictEqual((await readOutput(term, /enabled=(\w+) end/))[1], 'False'); - await exited; - } - - it('should make conptyParentWindow the owner of the console window before the shell starts', async function (): Promise { + it('should make the window the owner of the console window when set right after spawn', async function (): Promise { this.timeout(60000); const hostWindow = await startHostWindow(); - const windows = await readWindows(startPty('powershell.exe', rootOwnerArgs(0), { conptyParentWindow: hostWindow })); + const term = startPty('powershell.exe', rootOwnerArgs(0)); + // Not deferred, and the shell is only launched later, so this is sent before the shell starts. + assert.strictEqual(term.setParentWindow(hostWindow), useConptyDll); + const windows = await readWindows(term); assert.strictEqual(windows.rootOwner, useConptyDll ? hostWindow : windows.console); }); @@ -417,54 +394,11 @@ if (process.platform === 'win32') { [1.5, /must be a safe integer/], [Number.MAX_SAFE_INTEGER + 1, /must be a safe integer/] ]; - // Validated before the pseudoconsole is created, so nothing is left behind - for (const [handle, error] of invalid) { - assert.throws(() => startPty('cmd.exe', [], { conptyParentWindow: handle }), error); - } const term = startPty('cmd.exe', []); for (const [handle, error] of invalid) { assert.throws(() => term.setParentWindow(handle), error); } }); - - it('should re-enable the parent window on exit when asked, if a dialog left it disabled', async function (): Promise { - if (!useConptyDll) { - this.skip(); - } - this.timeout(60000); - const hostWindow = await startHostWindow(); - await disableParentAndExit(hostWindow, { conptyReenableParentWindowOnExit: true }); - assert.strictEqual(await readEnabled(hostWindow, 5000), 'True'); - }); - - it('should leave the parent window disabled on exit unless asked to re-enable it', async function (): Promise { - if (!useConptyDll) { - this.skip(); - } - this.timeout(60000); - const hostWindow = await startHostWindow(); - await disableParentAndExit(hostWindow, {}); - assert.strictEqual(await readEnabled(hostWindow, 3000), 'False'); - }); - - it('should not re-enable the parent window while it still owns a visible window', async function (): Promise { - if (!useConptyDll) { - this.skip(); - } - this.timeout(60000); - const hostWindow = await startHostWindow(); - // A visible off-screen window owned through a hidden proxy window, like a modal dialog - // shown by another process. - const dialog = startPty('powershell.exe', scriptArgs( - `$proxy = [NodePtyTest.W]::CreateWindowExW(0x80, 'STATIC', 'proxy', 0x80000000, 0, 0, 1, 1, [IntPtr]${hostWindow}, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero)`, - `$dialog = [NodePtyTest.W]::CreateWindowExW(0x08000080, 'STATIC', 'dialog', 0x90000000, -32000, -32000, 1, 1, $proxy, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero)`, - `Write-Host ('dialog=' + $dialog.ToInt64() + ' end')`, - `Start-Sleep -Seconds 60` - )); - assert.notStrictEqual((await readOutput(dialog, /dialog=(-?\d+) end/))[1], '0'); - await disableParentAndExit(hostWindow, { conptyReenableParentWindowOnExit: true }); - assert.strictEqual(await readEnabled(hostWindow, 3000), 'False'); - }); }); describe('Regression for #921', () => { it('should not crash with concurrent kills while resizing/clearing/reparenting', function (done) { diff --git a/src/windowsTerminal.ts b/src/windowsTerminal.ts index eaeb8f1b2..bc28e9acc 100644 --- a/src/windowsTerminal.ts +++ b/src/windowsTerminal.ts @@ -48,7 +48,7 @@ export class WindowsTerminal extends Terminal { this._deferreds = []; // Create new termal. - this._agent = new WindowsPtyAgent(file, args, parsedEnv, cwd, this._cols, this._rows, false, opt.useConptyDll, opt.conptyInheritCursor, opt.conptyParentWindow, opt.conptyReenableParentWindowOnExit); + this._agent = new WindowsPtyAgent(file, args, parsedEnv, cwd, this._cols, this._rows, false, opt.useConptyDll, opt.conptyInheritCursor); this._socket = this._agent.outSocket; // The socket 'close' handler that drives 'exit' is only attached after // ready_datapipe, which never fires when connect() fails. diff --git a/typings/node-pty.d.ts b/typings/node-pty.d.ts index cafa70bcf..bdc012175 100644 --- a/typings/node-pty.d.ts +++ b/typings/node-pty.d.ts @@ -109,28 +109,6 @@ declare module 'node-pty' { * @see https://docs.microsoft.com/en-us/windows/console/createpseudoconsole */ conptyInheritCursor?: boolean; - - /** - * The handle (HWND) of the top-level window that hosts this terminal. ConPTY makes it the owner - * of the console window returned by `GetConsoleWindow()`, so windows that console applications - * parent to it (for example `GetAncestor(GetConsoleWindow(), GA_ROOTOWNER)`, which MSAL/WAM - * sign-in uses) open on top of the host window instead of behind it. The owner is requested - * before the process is launched. Use {@link IPty.setParentWindow} when the terminal moves to - * another window. Must be a safe integer, or a bigint that fits in 64 bits. Only supported when - * `useConptyDll` is true; ignored otherwise. - */ - conptyParentWindow?: number | bigint; - - /** - * Whether to re-enable the parent window when the process exits, if the window is still - * disabled and no visible window is owned by it, directly or through other owned windows. The - * parent window is the last one set with `conptyParentWindow` or {@link IPty.setParentWindow}. - * This recovers from a modal dialog that died with its process, which otherwise leaves the - * parent window disabled for good. The check can't tell why the window is disabled, so only set - * it if the host never disables its window without showing a window it owns. Only supported - * when `useConptyDll` is true. Defaults to false. - */ - conptyReenableParentWindowOnExit?: boolean; } /** @@ -196,11 +174,15 @@ declare module 'node-pty' { clear(): void; /** - * Sets the handle (HWND) of the top-level window that hosts this terminal, see - * `IWindowsPtyForkOptions.conptyParentWindow`. Call it again whenever the terminal moves to a - * different window. Optional so existing implementations of `IPty` keep compiling. - * @param handle The window handle, for example Electron's - * `BrowserWindow.getNativeWindowHandle().readBigUInt64LE(0)`. + * Sets the handle (HWND) of the top-level window that hosts this terminal. ConPTY makes it the + * owner of the console window returned by `GetConsoleWindow()`, so windows that console + * applications parent to it (for example `GetAncestor(GetConsoleWindow(), GA_ROOTOWNER)`, which + * MSAL/WAM sign-in uses) open on top of the host window instead of behind it. Calling it right + * after spawning sends the request before the process is launched. Call it again whenever the + * terminal moves to a different window. Optional so existing implementations of `IPty` keep + * compiling. + * @param handle The window handle, as a safe integer or a bigint that fits in 64 bits, for + * example Electron's `BrowserWindow.getNativeWindowHandle().readBigUInt64LE(0)`. * @returns Whether the request was sent to ConPTY, which applies it asynchronously. This is * always false unless on Windows with `useConptyDll`, and false once the pseudoconsole is * closed. From bffa3a568c7c111d72d394a077dd1f28f9c41f24 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 20 Oct 2026 16:27:08 -0700 Subject: [PATCH 16/36] Upgrade node-gyp for Visual Studio 2026 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- package-lock.json | 76 +++++++++++++++++++++++++++-------------------- package.json | 2 +- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index f05406975..91cb46315 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "eslint": "^10.4.0", "globals": "^14.0.0", "mocha": "12.0.2", - "node-gyp": "^11.4.2", + "node-gyp": "^12.1.0", "ps-list": "^6.0.0", "typescript": "^5.0.0" } @@ -611,13 +611,13 @@ } }, "node_modules/abbrev": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/acorn": { @@ -2031,70 +2031,70 @@ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" }, "node_modules/node-gyp": { - "version": "11.4.2", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.4.2.tgz", - "integrity": "sha512-3gD+6zsrLQH7DyYOUIutaauuXrcyxeTPyQuZQCQoNPZMHMMS5m4y0xclNpvYzoK3VNzuyxT6eF4mkIL4WSZ1eQ==", + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", "dev": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^14.0.3", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", "semver": "^7.3.5", - "tar": "^7.4.3", + "tar": "^7.5.4", "tinyglobby": "^0.2.12", - "which": "^5.0.0" + "undici": "^6.25.0", + "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/node-gyp/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" + "node": ">=20" } }, "node_modules/node-gyp/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" + "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/nopt": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", "dev": true, "license": "ISC", "dependencies": { - "abbrev": "^3.0.0" + "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/optionator": { @@ -2218,13 +2218,13 @@ } }, "node_modules/proc-log": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/promise-retry": { @@ -2605,6 +2605,16 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "6.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.1.tgz", + "integrity": "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/unique-filename": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", diff --git a/package.json b/package.json index 41e8f9959..5099ef7f2 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "eslint": "^10.4.0", "globals": "^14.0.0", "mocha": "12.0.2", - "node-gyp": "^11.4.2", + "node-gyp": "^12.1.0", "ps-list": "^6.0.0", "typescript": "^5.0.0" } From 7ce8a6c6993dbbbfef3afd11b5cbbe8cb7a43b44 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 20 Oct 2026 16:52:00 -0700 Subject: [PATCH 17/36] Delay newly published npm dependencies Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .npmrc | 1 + package-lock.json | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 .npmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..7253a5cee --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +min-release-age=7 diff --git a/package-lock.json b/package-lock.json index 91cb46315..3a740bcb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2606,9 +2606,9 @@ } }, "node_modules/undici": { - "version": "6.29.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.1.tgz", - "integrity": "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==", + "version": "6.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz", + "integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==", "dev": true, "license": "MIT", "engines": { From c56760c15181c19b3f3636508233317fecd54d87 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 21 Oct 2026 11:34:58 -0700 Subject: [PATCH 18/36] build: use Node 24's bundled node-gyp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 8 +- .npmrc | 1 - package-lock.json | 1192 +------------------------------ package.json | 13 +- pipelines/build.yml | 4 +- scripts/gen-compile-commands.js | 7 +- 6 files changed, 53 insertions(+), 1172 deletions(-) delete mode 100644 .npmrc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04a0846cf..652e36a2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,10 +13,10 @@ jobs: with: persist-credentials: false - - name: Use Node.js 22.x + - name: Use Node.js 24.x uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '22.x' + node-version: '24.x' - name: Install dependencies (skip build) run: npm ci --ignore-scripts @@ -37,10 +37,10 @@ jobs: with: persist-credentials: false - - name: Use Node.js 22.x + - name: Use Node.js 24.x uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '22.x' + node-version: '24.x' - name: Set architecture id: arch diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 7253a5cee..000000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -min-release-age=7 diff --git a/package-lock.json b/package-lock.json index 3a740bcb8..9cd363bab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,6 @@ "eslint": "^10.4.0", "globals": "^14.0.0", "mocha": "12.0.2", - "node-gyp": "^12.1.0", "ps-list": "^6.0.0", "typescript": "^5.0.0" } @@ -186,163 +185,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@npmcli/agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", - "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@npmcli/fs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", - "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -610,16 +452,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/abbrev": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", - "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -643,16 +475,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -670,22 +492,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -693,12 +499,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, "node_modules/brace-expansion": { "version": "5.0.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", @@ -728,77 +528,6 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, - "node_modules/cacache": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", - "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^4.0.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^12.0.0", - "tar": "^7.4.3", - "unique-filename": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", - "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", @@ -815,36 +544,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/cross-env": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.1.tgz", @@ -922,61 +621,6 @@ "node": ">=0.3.1" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT" - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1255,13 +899,6 @@ "node": ">=0.10.0" } }, - "node_modules/exponential-backoff": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", - "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1346,162 +983,55 @@ "dev": true, "license": "ISC" }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": ">=14" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/foreground-child/node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/foreground-child/node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/glob/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=8" + "node": "20 || >=22" } }, - "node_modules/foreground-child/node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "node_modules/glob/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "shebang-regex": "^3.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/foreground-child/node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/lru-cache": { - "version": "11.5.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", - "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/glob/node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", "engines": { @@ -1511,13 +1041,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1528,41 +1051,6 @@ "node": ">=8" } }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -1582,16 +1070,6 @@ "node": ">=0.8.19" } }, - "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1601,15 +1079,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1654,22 +1123,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/js-yaml": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.2.tgz", @@ -1752,36 +1205,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/make-fetch-happen": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", - "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/agent": "^3.0.0", - "cacache": "^19.0.1", - "http-cache-semantics": "^4.1.1", - "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^1.0.0", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1", - "ssri": "^12.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", @@ -1808,149 +1231,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/minipass-collect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", - "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-fetch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", - "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^3.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-flush/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/mocha": { "version": "12.0.2", "resolved": "https://registry.npmjs.org/mocha/-/mocha-12.0.2.tgz", @@ -2009,16 +1289,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -2030,73 +1300,6 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" }, - "node_modules/node-gyp": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", - "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "graceful-fs": "^4.2.6", - "nopt": "^9.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "tar": "^7.5.4", - "tinyglobby": "^0.2.12", - "undici": "^6.25.0", - "which": "^6.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/node-gyp/node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=20" - } - }, - "node_modules/node-gyp/node_modules/which": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^4.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/nopt": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", - "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^4.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2145,26 +1348,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-map": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", - "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2183,23 +1366,6 @@ "node": ">=4" } }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2217,30 +1383,6 @@ "node": ">= 0.8.0" } }, - "node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/ps-list": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/ps-list/-/ps-list-6.3.0.tgz", @@ -2274,23 +1416,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "optional": true - }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -2335,158 +1460,6 @@ "node": ">=0.10.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.0.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/ssri": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", - "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/strip-json-comments": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", @@ -2500,23 +1473,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tar": { - "version": "7.5.22", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", - "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -2605,42 +1561,6 @@ "node": ">=14.17" } }, - "node_modules/undici": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz", - "integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/unique-filename": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", - "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/unique-slug": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", - "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -2680,58 +1600,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 5099ef7f2..08e2d0687 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,18 @@ "bugs": { "url": "https://github.com/microsoft/node-pty/issues" }, + "devEngines": { + "runtime": { + "name": "node", + "version": ">=24", + "onFail": "error" + }, + "packageManager": { + "name": "npm", + "version": ">=11.6.3", + "onFail": "error" + } + }, "keywords": [ "pty", "tty", @@ -59,7 +71,6 @@ "eslint": "^10.4.0", "globals": "^14.0.0", "mocha": "12.0.2", - "node-gyp": "^12.1.0", "ps-list": "^6.0.0", "typescript": "^5.0.0" } diff --git a/pipelines/build.yml b/pipelines/build.yml index 691960104..47d11fafb 100644 --- a/pipelines/build.yml +++ b/pipelines/build.yml @@ -4,8 +4,8 @@ parameters: steps: - task: UseNode@1 inputs: - version: '22.x' - displayName: 'Install Node.js 22.x' + version: '24.x' + displayName: 'Install Node.js 24.x' - task: UsePythonVersion@0 inputs: diff --git a/scripts/gen-compile-commands.js b/scripts/gen-compile-commands.js index 84a60ea51..d0d020ff8 100644 --- a/scripts/gen-compile-commands.js +++ b/scripts/gen-compile-commands.js @@ -2,7 +2,10 @@ * Copyright (c) 2025, Microsoft Corporation (MIT License). */ -const { execSync } = require('child_process'); +const { execFileSync } = require('child_process'); console.log(`\x1b[32m> Generating compile_commands.json...\x1b[0m`); -execSync('npx --offline node-gyp configure -- -f compile_commands_json'); +if (!process.env.npm_config_node_gyp) { + throw new Error('npm_config_node_gyp is not set; run this script with npm run compileCommands'); +} +execFileSync(process.execPath, [process.env.npm_config_node_gyp, 'configure', '--', '-f', 'compile_commands_json'], { stdio: 'inherit' }); From af1202071f3292bcd2bb5c7acb1c65ddae22bbae Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 21 Oct 2026 11:53:10 -0700 Subject: [PATCH 19/36] build: pin Node 24.18.0 in CI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 8 ++++---- pipelines/build.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 652e36a2c..5026bb73a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,10 +13,10 @@ jobs: with: persist-credentials: false - - name: Use Node.js 24.x + - name: Use Node.js 24.18.0 uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '24.x' + node-version: '24.18.0' - name: Install dependencies (skip build) run: npm ci --ignore-scripts @@ -37,10 +37,10 @@ jobs: with: persist-credentials: false - - name: Use Node.js 24.x + - name: Use Node.js 24.18.0 uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '24.x' + node-version: '24.18.0' - name: Set architecture id: arch diff --git a/pipelines/build.yml b/pipelines/build.yml index 47d11fafb..75b2b3b9e 100644 --- a/pipelines/build.yml +++ b/pipelines/build.yml @@ -4,8 +4,8 @@ parameters: steps: - task: UseNode@1 inputs: - version: '24.x' - displayName: 'Install Node.js 24.x' + version: '24.18.0' + displayName: 'Install Node.js 24.18.0' - task: UsePythonVersion@0 inputs: From 271faf38a6ee1f80f570cfc96d60367ed4a5bb5f Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 21 Oct 2026 13:27:55 -0700 Subject: [PATCH 20/36] fix(win): collect console processes before closing ConPTY Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windowsPtyAgent.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/windowsPtyAgent.ts b/src/windowsPtyAgent.ts index e1068bca4..aef34bf04 100644 --- a/src/windowsPtyAgent.ts +++ b/src/windowsPtyAgent.ts @@ -43,6 +43,7 @@ export class WindowsPtyAgent { private _closeTimeout: NodeJS.Timer | undefined; private _connectionTimeout: NodeJS.Timeout | undefined; private _exitCode: number | undefined; + private _killStarted = false; private _conoutSocketWorker: IConoutConnection; private _onError = new EventEmitter2(); @@ -205,6 +206,11 @@ export class WindowsPtyAgent { } public kill(): void { + if (this._killStarted) { + return; + } + this._killStarted = true; + // Prevent deferred connection from completing after kill this._clearConnectionTimeout(); this._pendingPtyInfo = undefined; @@ -213,7 +219,7 @@ export class WindowsPtyAgent { if (!this._useConptyDll) { this._inSocket.readable = false; this._outSocket.readable = false; - this._getConsoleProcessList().then(consoleProcessList => { + const kill = (consoleProcessList: number[]): void => { consoleProcessList.forEach((pid: number) => { try { process.kill(pid); @@ -221,9 +227,16 @@ export class WindowsPtyAgent { // Ignore if process cannot be found (kill ESRCH error) } }); - }); - this._ptyNative.kill(this._pty, this._useConptyDll); - this._conoutSocketWorker.dispose(); + this._ptyNative.kill(this._pty, this._useConptyDll); + this._conoutSocketWorker.dispose(); + }; + if (this._innerPid <= 0) { + kill([]); + } else { + // Get the process list before closing the pseudoconsole. Closing it lets process IDs be + // reused, so the asynchronous helper could otherwise return unrelated processes. + this._getConsoleProcessList().then(kill, () => kill([])); + } } else { // Close the input write handle to signal the end of session. this._inSocket.destroy(); From fae2ee92560e82feca3b999f907d2f8a4b3622aa Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 21 Oct 2026 13:46:55 -0700 Subject: [PATCH 21/36] Revert "fix(win): collect console processes before closing ConPTY" This reverts commit 271faf38a6ee1f80f570cfc96d60367ed4a5bb5f. --- src/windowsPtyAgent.ts | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/windowsPtyAgent.ts b/src/windowsPtyAgent.ts index aef34bf04..e1068bca4 100644 --- a/src/windowsPtyAgent.ts +++ b/src/windowsPtyAgent.ts @@ -43,7 +43,6 @@ export class WindowsPtyAgent { private _closeTimeout: NodeJS.Timer | undefined; private _connectionTimeout: NodeJS.Timeout | undefined; private _exitCode: number | undefined; - private _killStarted = false; private _conoutSocketWorker: IConoutConnection; private _onError = new EventEmitter2(); @@ -206,11 +205,6 @@ export class WindowsPtyAgent { } public kill(): void { - if (this._killStarted) { - return; - } - this._killStarted = true; - // Prevent deferred connection from completing after kill this._clearConnectionTimeout(); this._pendingPtyInfo = undefined; @@ -219,7 +213,7 @@ export class WindowsPtyAgent { if (!this._useConptyDll) { this._inSocket.readable = false; this._outSocket.readable = false; - const kill = (consoleProcessList: number[]): void => { + this._getConsoleProcessList().then(consoleProcessList => { consoleProcessList.forEach((pid: number) => { try { process.kill(pid); @@ -227,16 +221,9 @@ export class WindowsPtyAgent { // Ignore if process cannot be found (kill ESRCH error) } }); - this._ptyNative.kill(this._pty, this._useConptyDll); - this._conoutSocketWorker.dispose(); - }; - if (this._innerPid <= 0) { - kill([]); - } else { - // Get the process list before closing the pseudoconsole. Closing it lets process IDs be - // reused, so the asynchronous helper could otherwise return unrelated processes. - this._getConsoleProcessList().then(kill, () => kill([])); - } + }); + this._ptyNative.kill(this._pty, this._useConptyDll); + this._conoutSocketWorker.dispose(); } else { // Close the input write handle to signal the end of session. this._inSocket.destroy(); From cd33f21cf9e6a60af9f9a10d22bd8087ab152d7b Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Wed, 21 Oct 2026 15:15:15 -0700 Subject: [PATCH 22/36] Address review: accept only unsigned, pointer-sized window handles HWNDs are pointers, so negative handles are now rejected and the handle is converted to an HWND through uintptr_t in C++, which also covers 32-bit builds. Numbers must be non-negative safe integers, since Node-API's integer getters silently truncate fractions, map NaN and Infinity to 0 and lose precision above 2^53. Remove the JS check, so the native binding is the only place that validates the handle. --- src/win/conpty.cc | 51 +++++++++++++++++++++++++------------ src/windowsPtyAgent.ts | 16 ------------ src/windowsTerminal.test.ts | 38 ++++++++++++++------------- typings/node-pty.d.ts | 5 ++-- 4 files changed, 59 insertions(+), 51 deletions(-) diff --git a/src/win/conpty.cc b/src/win/conpty.cc index 1fa7189a1..aa2328bdf 100644 --- a/src/win/conpty.cc +++ b/src/win/conpty.cc @@ -15,6 +15,7 @@ #include #include // PathCombine, PathIsRelative #include +#include #include #include #include @@ -556,32 +557,51 @@ static Napi::Value PtyClear(const Napi::CallbackInfo& info) { return env.Undefined(); } +// A window handle is a pointer, so it's only valid as a non-negative integer that fits in one. +static bool TryGetWindowHandle(const Napi::Value& value, HWND* hwnd) { + uint64_t handle; + if (value.IsBigInt()) { + // Not lossless when negative or above 2^64 - 1. + bool lossless = false; + handle = value.As().Uint64Value(&lossless); + if (!lossless) { + return false; + } + } else if (value.IsNumber()) { + // Node-API's integer getters silently truncate fractions, map NaN and Infinity to 0 and lose + // precision above 2^53, so validate the double itself. + constexpr double maxSafeInteger = 9007199254740991.0; // Number.MAX_SAFE_INTEGER + const double number = value.As().DoubleValue(); + if (!(number >= 0 && number <= maxSafeInteger) || std::trunc(number) != number) { + return false; + } + handle = static_cast(number); + } else { + return false; + } + // Rejects handles that don't round-trip through a pointer, i.e. above 2^32 - 1 in 32-bit builds. + const uintptr_t pointer = static_cast(handle); + if (pointer != handle) { + return false; + } + *hwnd = reinterpret_cast(pointer); + return true; +} + static Napi::Value PtyReparent(const Napi::CallbackInfo& info) { Napi::Env env(info.Env()); Napi::HandleScope scope(env); if (info.Length() != 3 || !info[0].IsNumber() || - !(info[1].IsNumber() || info[1].IsBigInt()) || !info[2].IsBoolean()) { throw Napi::Error::New(env, "Usage: pty.reparent(id, hwnd, useConptyDll)"); } int id = info[0].As().Int32Value(); - uint64_t hwndValue; - if (info[1].IsBigInt()) { - // Accept signed and unsigned 64-bit values, but never truncate. - const Napi::BigInt hwndBigInt = info[1].As(); - bool lossless = false; - hwndValue = hwndBigInt.Uint64Value(&lossless); - if (!lossless) { - hwndValue = static_cast(hwndBigInt.Int64Value(&lossless)); - } - if (!lossless) { - throw Napi::Error::New(env, "pty.reparent: hwnd does not fit in 64 bits"); - } - } else { - hwndValue = static_cast(info[1].As().Int64Value()); + HWND newParent; + if (!TryGetWindowHandle(info[1], &newParent)) { + throw Napi::Error::New(env, "pty.reparent: hwnd must be a non-negative safe integer or a bigint that fits in a pointer"); } const bool useConptyDll = info[2].As().Value(); @@ -605,7 +625,6 @@ static Napi::Value PtyReparent(const Napi::CallbackInfo& info) { { // Asks ConPTY to make newParent the owner of the console window returned by // GetConsoleWindow(); ConPTY applies it asynchronously. - const HWND newParent = reinterpret_cast(static_cast(hwndValue)); sent = SUCCEEDED(pfnReparentPseudoConsole(handle->hpc, newParent)); } } diff --git a/src/windowsPtyAgent.ts b/src/windowsPtyAgent.ts index e1068bca4..901e7644b 100644 --- a/src/windowsPtyAgent.ts +++ b/src/windowsPtyAgent.ts @@ -200,7 +200,6 @@ export class WindowsPtyAgent { * whether the request was sent to ConPTY, which applies it asynchronously. */ public setParentWindow(handle: number | bigint): boolean { - validateWindowHandle(handle); return this._ptyNative.reparent(this._pty, handle, this._useConptyDll); } @@ -361,18 +360,3 @@ function repeatText(text: string, count: number): string { function xOr(arg1: boolean, arg2: boolean): boolean { return ((arg1 && !arg2) || (!arg1 && arg2)); } - -/** - * Throws unless the handle is a safe integer or a bigint that fits in 64 bits. Signed and unsigned - * bigints are both accepted, since a window handle may be sign-extended. - */ -function validateWindowHandle(handle: number | bigint): void { - if (typeof handle === 'bigint') { - if (handle < BigInt('-9223372036854775808') || handle > BigInt('18446744073709551615')) { - throw new Error('Window handle does not fit in 64 bits'); - } - } else if (!Number.isSafeInteger(handle)) { - // Larger numbers can't hold an exact handle. - throw new Error('Window handle must be a safe integer or a bigint'); - } -} diff --git a/src/windowsTerminal.test.ts b/src/windowsTerminal.test.ts index 5fa497339..a491494bc 100644 --- a/src/windowsTerminal.test.ts +++ b/src/windowsTerminal.test.ts @@ -285,19 +285,19 @@ if (process.platform === 'win32') { }); describe('parent window', () => { - const user32 = `Add-Type -Namespace NodePtyTest -Name W -MemberDefinition '[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow(); [DllImport("user32.dll")] public static extern IntPtr GetAncestor(IntPtr h, uint f);'`; + const user32 = `Add-Type -Namespace NodePtyTest -Name W -MemberDefinition '[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow(); [DllImport("user32.dll")] public static extern IntPtr GetAncestor(IntPtr h, uint f); public static ulong U(IntPtr h) { return unchecked((ulong)h.ToInt64()); }'`; function scriptArgs(...lines: string[]): string { return `-NoProfile -NonInteractive -EncodedCommand ${Buffer.from([user32, ...lines].join('; '), 'utf16le').toString('base64')}`; } // Prints the console window and GetAncestor(GetConsoleWindow(), GA_ROOTOWNER), which is the - // parent MSAL/WAM uses for sign-in dialogs. + // parent MSAL/WAM uses for sign-in dialogs. Handles are pointers, so print them unsigned. function rootOwnerArgs(delayMs: number, keepAliveSeconds: number = 0): string { return scriptArgs( `Start-Sleep -Milliseconds ${delayMs}`, `$c = [NodePtyTest.W]::GetConsoleWindow()`, - `Write-Host ('console=' + $c.ToInt64() + ' rootOwner=' + [NodePtyTest.W]::GetAncestor($c, 3).ToInt64() + ' end')`, + `Write-Host ('console=' + [NodePtyTest.W]::U($c) + ' rootOwner=' + [NodePtyTest.W]::U([NodePtyTest.W]::GetAncestor($c, 3)) + ' end')`, `Start-Sleep -Seconds ${keepAliveSeconds}` ); } @@ -344,22 +344,24 @@ if (process.platform === 'win32') { }); } - async function readWindows(term: WindowsTerminal): Promise<{ console: number, rootOwner: number }> { - const match = await readOutput(term, /console=(-?\d+) rootOwner=(-?\d+) end/); - return { console: parseInt(match[1], 10), rootOwner: parseInt(match[2], 10) }; + async function readWindows(term: WindowsTerminal): Promise<{ console: bigint, rootOwner: bigint }> { + const match = await readOutput(term, /console=(\d+) rootOwner=(\d+) end/); + return { console: BigInt(match[1]), rootOwner: BigInt(match[2]) }; } // Another pty's hidden top-level console window stands in for the host app's window. - async function startHostWindow(): Promise { + async function startHostWindow(): Promise { return (await readWindows(startPty('powershell.exe', rootOwnerArgs(0, 60)))).console; } it('should make the window the owner of the console window when set right after spawn', async function (): Promise { this.timeout(60000); const hostWindow = await startHostWindow(); + // Pass a number when it's exact, so both the number and the bigint path are covered. + const handle = hostWindow <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(hostWindow) : hostWindow; const term = startPty('powershell.exe', rootOwnerArgs(0)); // Not deferred, and the shell is only launched later, so this is sent before the shell starts. - assert.strictEqual(term.setParentWindow(hostWindow), useConptyDll); + assert.strictEqual(term.setParentWindow(handle), useConptyDll); const windows = await readWindows(term); assert.strictEqual(windows.rootOwner, useConptyDll ? hostWindow : windows.console); }); @@ -370,7 +372,7 @@ if (process.platform === 'win32') { const term = startPty('powershell.exe', rootOwnerArgs(3000)); let sent: boolean | undefined; // The first data event means the shell is running and conpty has been released. - term.once('data', () => sent = term.setParentWindow(BigInt(hostWindow))); + term.once('data', () => sent = term.setParentWindow(hostWindow)); const windows = await readWindows(term); assert.strictEqual(sent, useConptyDll); assert.strictEqual(windows.rootOwner, useConptyDll ? hostWindow : windows.console); @@ -387,16 +389,18 @@ if (process.platform === 'win32') { assert.strictEqual(term.setParentWindow(0), false); }); - it('should reject handles that are not safe integers or bigints that fit in 64 bits', () => { - const invalid: Array<[number | bigint, RegExp]> = [ - [BigInt('18446744073709551616'), /does not fit in 64 bits/], - [BigInt('-9223372036854775809'), /does not fit in 64 bits/], - [1.5, /must be a safe integer/], - [Number.MAX_SAFE_INTEGER + 1, /must be a safe integer/] + it('should reject handles that are not non-negative integers that fit in a pointer', () => { + const invalid: Array = [ + -1, + BigInt('-1'), + 1.5, + NaN, + Number.MAX_SAFE_INTEGER + 1, + BigInt('18446744073709551616') ]; const term = startPty('cmd.exe', []); - for (const [handle, error] of invalid) { - assert.throws(() => term.setParentWindow(handle), error); + for (const handle of invalid) { + assert.throws(() => term.setParentWindow(handle), /hwnd must be a non-negative safe integer or a bigint that fits in a pointer/); } }); }); diff --git a/typings/node-pty.d.ts b/typings/node-pty.d.ts index bdc012175..102ceab65 100644 --- a/typings/node-pty.d.ts +++ b/typings/node-pty.d.ts @@ -181,8 +181,9 @@ declare module 'node-pty' { * after spawning sends the request before the process is launched. Call it again whenever the * terminal moves to a different window. Optional so existing implementations of `IPty` keep * compiling. - * @param handle The window handle, as a safe integer or a bigint that fits in 64 bits, for - * example Electron's `BrowserWindow.getNativeWindowHandle().readBigUInt64LE(0)`. + * @param handle The window handle. HWNDs are pointers, so this must be a non-negative safe + * integer or a bigint that fits in a pointer, for example Electron's + * `BrowserWindow.getNativeWindowHandle().readBigUInt64LE(0)`. Anything else throws. * @returns Whether the request was sent to ConPTY, which applies it asynchronously. This is * always false unless on Windows with `useConptyDll`, and false once the pseudoconsole is * closed. From 447be77be40c8119f24047d74469f9fcf2dcc919 Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Wed, 21 Oct 2026 17:17:58 -0700 Subject: [PATCH 23/36] Reword the window handle comment and test name Only Int64Value() loses precision past Number.MAX_SAFE_INTEGER; the 32-bit getters wrap instead. The invalid handle test also rejects unsafe integers, so say safe integers in its name. --- src/win/conpty.cc | 4 ++-- src/windowsTerminal.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/win/conpty.cc b/src/win/conpty.cc index aa2328bdf..529d1cb23 100644 --- a/src/win/conpty.cc +++ b/src/win/conpty.cc @@ -568,8 +568,8 @@ static bool TryGetWindowHandle(const Napi::Value& value, HWND* hwnd) { return false; } } else if (value.IsNumber()) { - // Node-API's integer getters silently truncate fractions, map NaN and Infinity to 0 and lose - // precision above 2^53, so validate the double itself. + // Napi::Number::Int64Value() silently truncates fractions, maps NaN and Infinity to 0 and + // loses precision past Number.MAX_SAFE_INTEGER, so validate the double itself. constexpr double maxSafeInteger = 9007199254740991.0; // Number.MAX_SAFE_INTEGER const double number = value.As().DoubleValue(); if (!(number >= 0 && number <= maxSafeInteger) || std::trunc(number) != number) { diff --git a/src/windowsTerminal.test.ts b/src/windowsTerminal.test.ts index a491494bc..1ab9bacce 100644 --- a/src/windowsTerminal.test.ts +++ b/src/windowsTerminal.test.ts @@ -389,7 +389,7 @@ if (process.platform === 'win32') { assert.strictEqual(term.setParentWindow(0), false); }); - it('should reject handles that are not non-negative integers that fit in a pointer', () => { + it('should reject handles that are not non-negative safe integers or bigints that fit in a pointer', () => { const invalid: Array = [ -1, BigInt('-1'), From f6991751bd86b4e40437917f6ae5f9a44738f6bb Mon Sep 17 00:00:00 2001 From: anthonykim1 Date: Thu, 22 Oct 2026 07:55:55 -0700 Subject: [PATCH 24/36] Address review: take the window handle as a Buffer setParentWindow() now takes the HWND as a Buffer the size of an HWND, in native byte order, which is how Electron's BrowserWindow.getNativeWindowHandle() returns it, so hosts can pass that Buffer directly. The native side copies it into an HWND, which replaces the number and bigint parsing. --- src/interfaces.ts | 2 +- src/native.d.ts | 2 +- src/terminal.test.ts | 2 +- src/terminal.ts | 2 +- src/unixTerminal.ts | 2 +- src/win/conpty.cc | 33 ++++++++------------------------- src/windowsPtyAgent.ts | 2 +- src/windowsTerminal.test.ts | 34 +++++++++++++++++----------------- src/windowsTerminal.ts | 2 +- typings/node-pty.d.ts | 8 ++++---- 10 files changed, 36 insertions(+), 53 deletions(-) diff --git a/src/interfaces.ts b/src/interfaces.ts index d69b947bb..44b4c49b2 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -44,7 +44,7 @@ export interface ITerminal { * Sets the top-level window (HWND) that hosts this terminal. Returns whether the request was sent * to ConPTY, which only happens on Windows with `useConptyDll`. */ - setParentWindow(handle: number | bigint): boolean; + setParentWindow(handle: Buffer): boolean; /** * Close, kill and destroy the socket. diff --git a/src/native.d.ts b/src/native.d.ts index fe65e29d9..9bd646376 100644 --- a/src/native.d.ts +++ b/src/native.d.ts @@ -7,7 +7,7 @@ interface IConptyNative { connect(ptyId: number, commandLine: string, cwd: string, env: string[], useConptyDll: boolean, onExitCallback: (exitCode: number) => void): { pid: number }; resize(ptyId: number, cols: number, rows: number, useConptyDll: boolean): void; clear(ptyId: number, useConptyDll: boolean): void; - reparent(ptyId: number, hwnd: number | bigint, useConptyDll: boolean): boolean; + reparent(ptyId: number, hwnd: Buffer, useConptyDll: boolean): boolean; kill(ptyId: number, useConptyDll: boolean): void; } diff --git a/src/terminal.test.ts b/src/terminal.test.ts index fe96bddd8..9d6fe704d 100644 --- a/src/terminal.test.ts +++ b/src/terminal.test.ts @@ -32,7 +32,7 @@ class TestTerminal extends Terminal { public clear(): void { throw new Error('Method not implemented.'); } - public setParentWindow(handle: number | bigint): boolean { + public setParentWindow(handle: Buffer): boolean { throw new Error('Method not implemented.'); } public destroy(): void { diff --git a/src/terminal.ts b/src/terminal.ts index 735315aa5..7e268521a 100644 --- a/src/terminal.ts +++ b/src/terminal.ts @@ -180,7 +180,7 @@ export abstract class Terminal implements ITerminal { public abstract resize(cols: number, rows: number, pixelSize?: { width: number, height: number }): void; public abstract clear(): void; - public abstract setParentWindow(handle: number | bigint): boolean; + public abstract setParentWindow(handle: Buffer): boolean; public abstract destroy(): void; public abstract kill(signal?: string): void; diff --git a/src/unixTerminal.ts b/src/unixTerminal.ts index 72b20700d..1670d8e29 100644 --- a/src/unixTerminal.ts +++ b/src/unixTerminal.ts @@ -283,7 +283,7 @@ export class UnixTerminal extends Terminal { } - public setParentWindow(handle: number | bigint): boolean { + public setParentWindow(handle: Buffer): boolean { // Windows only return false; } diff --git a/src/win/conpty.cc b/src/win/conpty.cc index 529d1cb23..7447f8f47 100644 --- a/src/win/conpty.cc +++ b/src/win/conpty.cc @@ -15,7 +15,7 @@ #include #include // PathCombine, PathIsRelative #include -#include +#include #include #include #include @@ -557,34 +557,17 @@ static Napi::Value PtyClear(const Napi::CallbackInfo& info) { return env.Undefined(); } -// A window handle is a pointer, so it's only valid as a non-negative integer that fits in one. +// The handle is the bytes of an HWND in native byte order, which is how Electron's +// BrowserWindow.getNativeWindowHandle() returns it. static bool TryGetWindowHandle(const Napi::Value& value, HWND* hwnd) { - uint64_t handle; - if (value.IsBigInt()) { - // Not lossless when negative or above 2^64 - 1. - bool lossless = false; - handle = value.As().Uint64Value(&lossless); - if (!lossless) { - return false; - } - } else if (value.IsNumber()) { - // Napi::Number::Int64Value() silently truncates fractions, maps NaN and Infinity to 0 and - // loses precision past Number.MAX_SAFE_INTEGER, so validate the double itself. - constexpr double maxSafeInteger = 9007199254740991.0; // Number.MAX_SAFE_INTEGER - const double number = value.As().DoubleValue(); - if (!(number >= 0 && number <= maxSafeInteger) || std::trunc(number) != number) { - return false; - } - handle = static_cast(number); - } else { + if (!value.IsBuffer()) { return false; } - // Rejects handles that don't round-trip through a pointer, i.e. above 2^32 - 1 in 32-bit builds. - const uintptr_t pointer = static_cast(handle); - if (pointer != handle) { + const auto buffer = value.As>(); + if (buffer.Length() != sizeof(HWND)) { return false; } - *hwnd = reinterpret_cast(pointer); + std::memcpy(hwnd, buffer.Data(), sizeof(*hwnd)); return true; } @@ -601,7 +584,7 @@ static Napi::Value PtyReparent(const Napi::CallbackInfo& info) { int id = info[0].As().Int32Value(); HWND newParent; if (!TryGetWindowHandle(info[1], &newParent)) { - throw Napi::Error::New(env, "pty.reparent: hwnd must be a non-negative safe integer or a bigint that fits in a pointer"); + throw Napi::Error::New(env, "pty.reparent: hwnd must be a Buffer the size of an HWND"); } const bool useConptyDll = info[2].As().Value(); diff --git a/src/windowsPtyAgent.ts b/src/windowsPtyAgent.ts index 901e7644b..5b4f366c0 100644 --- a/src/windowsPtyAgent.ts +++ b/src/windowsPtyAgent.ts @@ -199,7 +199,7 @@ export class WindowsPtyAgent { * that console apps parent to it (e.g. WAM sign-in) stack above the host window. Returns * whether the request was sent to ConPTY, which applies it asynchronously. */ - public setParentWindow(handle: number | bigint): boolean { + public setParentWindow(handle: Buffer): boolean { return this._ptyNative.reparent(this._pty, handle, this._useConptyDll); } diff --git a/src/windowsTerminal.test.ts b/src/windowsTerminal.test.ts index 1ab9bacce..5aa6c843a 100644 --- a/src/windowsTerminal.test.ts +++ b/src/windowsTerminal.test.ts @@ -354,14 +354,20 @@ if (process.platform === 'win32') { return (await readWindows(startPty('powershell.exe', rootOwnerArgs(0, 60)))).console; } + // An HWND the way setParentWindow takes it, like Electron's getNativeWindowHandle(): its bytes + // in native byte order. The tests run in 64-bit processes. + function toHandle(hwnd: bigint): Buffer { + const handle = Buffer.alloc(8); + handle.writeBigUInt64LE(hwnd); + return handle; + } + it('should make the window the owner of the console window when set right after spawn', async function (): Promise { this.timeout(60000); const hostWindow = await startHostWindow(); - // Pass a number when it's exact, so both the number and the bigint path are covered. - const handle = hostWindow <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(hostWindow) : hostWindow; const term = startPty('powershell.exe', rootOwnerArgs(0)); // Not deferred, and the shell is only launched later, so this is sent before the shell starts. - assert.strictEqual(term.setParentWindow(handle), useConptyDll); + assert.strictEqual(term.setParentWindow(toHandle(hostWindow)), useConptyDll); const windows = await readWindows(term); assert.strictEqual(windows.rootOwner, useConptyDll ? hostWindow : windows.console); }); @@ -372,7 +378,7 @@ if (process.platform === 'win32') { const term = startPty('powershell.exe', rootOwnerArgs(3000)); let sent: boolean | undefined; // The first data event means the shell is running and conpty has been released. - term.once('data', () => sent = term.setParentWindow(hostWindow)); + term.once('data', () => sent = term.setParentWindow(toHandle(hostWindow))); const windows = await readWindows(term); assert.strictEqual(sent, useConptyDll); assert.strictEqual(windows.rootOwner, useConptyDll ? hostWindow : windows.console); @@ -384,23 +390,16 @@ if (process.platform === 'win32') { const exited = new Promise(resolve => term.once('exit', () => resolve())); await new Promise(resolve => term.once('data', () => resolve())); term.kill(); - assert.strictEqual(term.setParentWindow(0), false); + assert.strictEqual(term.setParentWindow(Buffer.alloc(8)), false); await exited; - assert.strictEqual(term.setParentWindow(0), false); + assert.strictEqual(term.setParentWindow(Buffer.alloc(8)), false); }); - it('should reject handles that are not non-negative safe integers or bigints that fit in a pointer', () => { - const invalid: Array = [ - -1, - BigInt('-1'), - 1.5, - NaN, - Number.MAX_SAFE_INTEGER + 1, - BigInt('18446744073709551616') - ]; + it('should reject handles that are not buffers the size of an HWND', () => { + const invalid: unknown[] = [Buffer.alloc(4), Buffer.alloc(9), Buffer.alloc(0), 1, BigInt(1), '0x1234', undefined]; const term = startPty('cmd.exe', []); for (const handle of invalid) { - assert.throws(() => term.setParentWindow(handle), /hwnd must be a non-negative safe integer or a bigint that fits in a pointer/); + assert.throws(() => term.setParentWindow(handle as Buffer), /hwnd must be a Buffer the size of an HWND/); } }); }); @@ -412,6 +411,7 @@ if (process.platform === 'win32') { let ready = 0; let exited = 0; let spamInterval: NodeJS.Timeout | undefined; + const noWindow = Buffer.alloc(8); const cleanup = (err?: Error): void => { if (spamInterval) { clearInterval(spamInterval); @@ -429,7 +429,7 @@ if (process.platform === 'win32') { t.clear(); } catch (e) { /* already exited */ } try { - t.setParentWindow(0); + t.setParentWindow(noWindow); } catch (e) { /* already exited */ } } }, 1); diff --git a/src/windowsTerminal.ts b/src/windowsTerminal.ts index bc28e9acc..c01be4221 100644 --- a/src/windowsTerminal.ts +++ b/src/windowsTerminal.ts @@ -163,7 +163,7 @@ export class WindowsTerminal extends Terminal { }); } - public setParentWindow(handle: number | bigint): boolean { + public setParentWindow(handle: Buffer): boolean { // Not deferred: the pseudoconsole exists as soon as the agent is created, and the owner // should be requested before the shell starts asking for it. return this._agent.setParentWindow(handle); diff --git a/typings/node-pty.d.ts b/typings/node-pty.d.ts index 102ceab65..f46c02968 100644 --- a/typings/node-pty.d.ts +++ b/typings/node-pty.d.ts @@ -181,14 +181,14 @@ declare module 'node-pty' { * after spawning sends the request before the process is launched. Call it again whenever the * terminal moves to a different window. Optional so existing implementations of `IPty` keep * compiling. - * @param handle The window handle. HWNDs are pointers, so this must be a non-negative safe - * integer or a bigint that fits in a pointer, for example Electron's - * `BrowserWindow.getNativeWindowHandle().readBigUInt64LE(0)`. Anything else throws. + * @param handle The window handle (HWND) as its bytes in native byte order, the way Electron's + * `BrowserWindow.getNativeWindowHandle()` returns it. Throws unless it's a Buffer the size of + * an HWND (8 bytes in a 64-bit process). * @returns Whether the request was sent to ConPTY, which applies it asynchronously. This is * always false unless on Windows with `useConptyDll`, and false once the pseudoconsole is * closed. */ - setParentWindow?(handle: number | bigint): boolean; + setParentWindow?(handle: Buffer): boolean; /** * Writes data to the pty. From 79320e6399dfb7149f9354d10142004bde4706f5 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 22 Oct 2026 09:02:37 -0700 Subject: [PATCH 25/36] Use Node 24 in the publish pipeline Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- publish.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/publish.yml b/publish.yml index 499a6625b..ff05a8103 100644 --- a/publish.yml +++ b/publish.yml @@ -37,6 +37,13 @@ extends: parameters: npmPackages: - name: node-pty + nodeVersion: '24.18.0' + + testPlatforms: + - name: Linux + nodeVersions: + - '24.18.0' + useSysroot: true buildSteps: - task: DownloadPipelineArtifact@2 From 3f1d547277efb43d6afdd840db0c264f381d85a1 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 22 Oct 2026 12:38:38 -0700 Subject: [PATCH 26/36] Update Node.js to 24.22.1 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 8 ++++---- pipelines/build.yml | 4 ++-- publish.yml | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5026bb73a..1d8625ae4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,10 +13,10 @@ jobs: with: persist-credentials: false - - name: Use Node.js 24.18.0 + - name: Use Node.js 24.22.1 uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '24.18.0' + node-version: '24.22.1' - name: Install dependencies (skip build) run: npm ci --ignore-scripts @@ -37,10 +37,10 @@ jobs: with: persist-credentials: false - - name: Use Node.js 24.18.0 + - name: Use Node.js 24.22.1 uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '24.18.0' + node-version: '24.22.1' - name: Set architecture id: arch diff --git a/pipelines/build.yml b/pipelines/build.yml index 75b2b3b9e..33a1ef431 100644 --- a/pipelines/build.yml +++ b/pipelines/build.yml @@ -4,8 +4,8 @@ parameters: steps: - task: UseNode@1 inputs: - version: '24.18.0' - displayName: 'Install Node.js 24.18.0' + version: '24.22.1' + displayName: 'Install Node.js 24.22.1' - task: UsePythonVersion@0 inputs: diff --git a/publish.yml b/publish.yml index ff05a8103..84e6b6f73 100644 --- a/publish.yml +++ b/publish.yml @@ -37,12 +37,12 @@ extends: parameters: npmPackages: - name: node-pty - nodeVersion: '24.18.0' + nodeVersion: '24.22.1' testPlatforms: - name: Linux nodeVersions: - - '24.18.0' + - '24.22.1' useSysroot: true buildSteps: From eaeab615af05175147d140719cc5f66589b96a85 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 22 Oct 2026 15:20:34 -0700 Subject: [PATCH 27/36] Use npm 11.6.3 for Azure Artifacts publishing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/publish.yml b/publish.yml index 84e6b6f73..7c89a7964 100644 --- a/publish.yml +++ b/publish.yml @@ -38,6 +38,7 @@ extends: npmPackages: - name: node-pty nodeVersion: '24.22.1' + publishNpmVersion: '11.6.3' testPlatforms: - name: Linux From a8715a6cd0d3e781fdc9e63020d67372a60ac97d Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 22 Oct 2026 18:08:17 -0700 Subject: [PATCH 28/36] Document beta version selection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/publish.yml b/publish.yml index 7c89a7964..660beb5b0 100644 --- a/publish.yml +++ b/publish.yml @@ -65,8 +65,8 @@ extends: displayName: 'Ensure spawn-helper is executable' - script: npm ci displayName: 'Install dependencies and build' - # The following script leaves the version unchanged for - # stable releases, but increments the version for beta releases. + # Version selection queries the configured Monaco feed, so a failed mirror retry reuses + # the missing beta version instead of skipping it. Stable releases remain unchanged. - script: node scripts/increment-version.js displayName: 'Increment version' From bc4119e2129220513ba990248d529d5358d8aada Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Thu, 22 Oct 2026 18:10:11 -0700 Subject: [PATCH 29/36] Keep beta version documentation with its lookup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- publish.yml | 4 ++-- scripts/increment-version.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/publish.yml b/publish.yml index 660beb5b0..7c89a7964 100644 --- a/publish.yml +++ b/publish.yml @@ -65,8 +65,8 @@ extends: displayName: 'Ensure spawn-helper is executable' - script: npm ci displayName: 'Install dependencies and build' - # Version selection queries the configured Monaco feed, so a failed mirror retry reuses - # the missing beta version instead of skipping it. Stable releases remain unchanged. + # The following script leaves the version unchanged for + # stable releases, but increments the version for beta releases. - script: node scripts/increment-version.js displayName: 'Increment version' diff --git a/scripts/increment-version.js b/scripts/increment-version.js index 27ab8d8b4..3610d1010 100644 --- a/scripts/increment-version.js +++ b/scripts/increment-version.js @@ -42,6 +42,7 @@ function getNextBetaVersion() { } function getPublishedVersions(version, tag) { + // npm uses the registry configured by the publishing pipeline, so beta numbering follows that feed. const isWin32 = process.platform === 'win32'; const versionsProcess = isWin32 ? cp.spawnSync('npm.cmd', ['view', packageJson.name, 'versions', '--json'], { shell: true }) : From f541e15c81b67dca2740a8c35570e6a0d776ae9e Mon Sep 17 00:00:00 2001 From: Northern Man <19808920+NorthernMan54@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:30:29 -0400 Subject: [PATCH 30/36] Reapply Homebridge prebuilt-multiarch changes on upstream 3d2987d Rebuilt on microsoft/node-pty main (2026-10-22, 36 commits past the previous fork point 8da72bb), with the net Homebridge changes from beta-0.15.0 applied on top: - package name @homebridge/node-pty-prebuilt-multiarch, typings module - prebuild loader (scripts/prebuild.js) and node-gyp rebuild fallback - glibc 2.28 sysroot pin for Linux prebuilds - clearer native module load errors in src/utils.ts - Stage 1 (prebuild + publish) and Stage 2 (validate) workflows - README, CODEOWNERS, dependabot config Conflicts: ci.yml takes upstream's Node 24.22.1; package.json takes upstream's mocha 12.0.2 and keeps node-gyp as a devDependency (the Stage 1 workflow calls it), bumped to 13.1.0 for VS 2026. package-lock.json regenerated. Co-Authored-By: Claude Opus 5.5 --- .github/CODEOWNERS | 7 + .github/dependabot.yml | 37 +- .../Stage 1 - Prebuilds - Publish.yml | 413 ++++++++++++++++++ .github/workflows/Stage 2 - Validate.yml | 149 +++++++ README.md | 189 ++------ package-lock.json | 208 ++++++++- package.json | 22 +- scripts/linux/checksums.txt | 1 + scripts/linux/install-sysroot.js | 18 +- scripts/prebuild.js | 30 +- src/utils.ts | 23 +- typings/node-pty.d.ts | 2 +- 12 files changed, 928 insertions(+), 171 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/Stage 1 - Prebuilds - Publish.yml create mode 100644 .github/workflows/Stage 2 - Validate.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..1a513dafc --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#example-of-a-codeowners-file + +* @NorthernMan54 + +# Only a collaborator can review / approve changes to this list + +/.github/CODEOWNERS @homebridge/collaborators diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2c48305b7..ff3676506 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,11 +1,42 @@ +# Hybrid dependency management configuration +# - npm dependencies: grouped into a single update +# - GitHub Actions: grouped into a single update + version: 2 updates: - - package-ecosystem: "github-actions" + - package-ecosystem: npm directory: "/" + schedule: + interval: daily + time: "10:00" + open-pull-requests-limit: 10 + labels: + - "dependencies" groups: - github-actions: - patterns: ["*"] + packages: + patterns: + - "*" + rebase-strategy: "auto" + commit-message: + prefix: "chore" + prefix-development: "chore" + include: "scope" + + - package-ecosystem: "github-actions" + directory: "/" schedule: interval: "weekly" + open-pull-requests-limit: 10 + labels: + - "dependencies" + groups: + actions: + patterns: + - "*" + rebase-strategy: "auto" + commit-message: + prefix: "chore" + prefix-development: "chore" + include: "scope" cooldown: default-days: 7 diff --git a/.github/workflows/Stage 1 - Prebuilds - Publish.yml b/.github/workflows/Stage 1 - Prebuilds - Publish.yml new file mode 100644 index 000000000..3aeb1a558 --- /dev/null +++ b/.github/workflows/Stage 1 - Prebuilds - Publish.yml @@ -0,0 +1,413 @@ +name: Stage 1 - Prebuilds & Publish + +on: + push: + branches: + - 'beta-*.*.*' + - 'alpha-*.*.*' + workflow_dispatch: + inputs: + branch: + description: 'Release branch' + required: true + type: choice + options: + - main + - beta + - alpha + default: alpha + publishPackage: + description: '🚀 Publish to npm and GitHub' + required: true + type: boolean + default: false + +concurrency: + group: prebuilds-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build and Test(${{ matrix.platform }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - platform: win32-x64 + runner: windows-2022 + arch: x64 + - platform: win32-arm64 + runner: windows-11-arm + arch: arm64 + - platform: darwin-x64 + runner: macos-15-intel + arch: x64 + - platform: darwin-arm64 + runner: macos-15 + arch: arm64 + - platform: linux-x64 + runner: ubuntu-22.04 + arch: x64 + - platform: linux-arm64 + runner: ubuntu-22.04-arm + arch: arm64 + - platform: linux-arm + runner: ubuntu-22.04 + arch: arm + crossCompile: true + - platform: linux-ia32 + runner: ubuntu-22.04 + arch: ia32 + crossCompile: true + - platform: linux-musl-x64 + runner: ubuntu-22.04 + arch: x64 + musl: true + - platform: linux-musl-arm64 + runner: ubuntu-22.04 + arch: arm64 + musl: true + - platform: linux-musl-arm + runner: ubuntu-22.04 + arch: arm + musl: true + + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Use Node.js 22.x + uses: actions/setup-node@v7 + with: + node-version: '22.x' + + # NOTE: sysroot pinning covers the native x64/arm64 and cross-compiled + # linux-arm targets. linux-ia32 is built separately in a native i386 + # Debian bullseye container because the VS Code sysroot release does + # not publish an i686 toolchain. + # + # This is what + # keeps the resulting binaries linked against an old-enough glibc + # (Debian 11/bullseye baseline) to run on Raspberry Pi OS bullseye + # and similar older armhf/ia32 hosts, instead of whatever glibc the + # ubuntu-22.04 runner's default cross-toolchain ships with. + - name: Install sysroot (Linux x64/arm64/arm) + if: runner.os == 'Linux' && !matrix.musl && matrix.arch != 'ia32' + run: | + set -eo pipefail + sudo apt-get update -qq + sudo apt-get install -y gcc-10 g++-10 + echo "CC=gcc-10" >> $GITHUB_ENV + echo "CXX=g++-10" >> $GITHUB_ENV + SYSROOT_PATH=$(node scripts/linux/install-sysroot.js ${{ matrix.arch }} | grep "SYSROOT_PATH=" | cut -d= -f2) + echo "SYSROOT_PATH=$SYSROOT_PATH" >> $GITHUB_ENV + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # NOTE: this sysroot uses a *flat* (non-multiarch) layout - libc.so + # and libc.so.6 live directly under usr/lib and lib, not under the + # Debian-multiarch path usr/lib/arm-linux-gnueabihf that + # arm-linux-gnueabihf-g++ derives by default from --sysroot alone. + # That mismatch was letting the compiler's own bundled default + # sysroot (glibc 2.34 on the ubuntu-22.04 runner) silently win the + # library search - the link succeeded with no error, just against + # the wrong libc. Explicit -B/-L/-rpath-link flags force resolution + # against this sysroot's actual paths instead of relying on gcc's + # multiarch-based default derivation from --sysroot. + - name: Install cross-compiler (Linux arm) + if: matrix.crossCompile && matrix.arch == 'arm' + run: | + set -eo pipefail + sudo apt-get update -qq + sudo apt-get install -y gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf + echo "CC=arm-linux-gnueabihf-gcc" >> $GITHUB_ENV + echo "CXX=arm-linux-gnueabihf-g++" >> $GITHUB_ENV + SYSROOT_FLAGS="--sysroot=${{ env.SYSROOT_PATH }} -B${{ env.SYSROOT_PATH }}/usr/lib -B${{ env.SYSROOT_PATH }}/lib -L${{ env.SYSROOT_PATH }}/usr/lib -L${{ env.SYSROOT_PATH }}/lib" + echo "CFLAGS=$SYSROOT_FLAGS" >> $GITHUB_ENV + echo "CXXFLAGS=$SYSROOT_FLAGS" >> $GITHUB_ENV + echo "LDFLAGS=$SYSROOT_FLAGS -Wl,-rpath-link=${{ env.SYSROOT_PATH }}/lib:${{ env.SYSROOT_PATH }}/usr/lib" >> $GITHUB_ENV + + # DIAGNOSTIC - remove once the GLIBC_2.34 mismatch is resolved. + # Moved to run AFTER cross-compiler install (previously ran before + # it in an earlier version of this workflow, so + # arm-linux-gnueabihf-g++ didn't exist yet and these commands + # silently failed with "command not found"). + - name: Debug - inspect compiler search paths + if: matrix.crossCompile && matrix.arch == 'arm' + run: | + echo "--- what -print-sysroot / -print-multiarch report ---" + arm-linux-gnueabihf-g++ -print-sysroot + arm-linux-gnueabihf-g++ -print-multiarch + echo "--- resolved search dirs WITH our explicit flags ---" + arm-linux-gnueabihf-g++ ${{ env.CXXFLAGS }} -print-search-dirs + echo "--- resolved search dirs WITHOUT our flags (baseline) ---" + arm-linux-gnueabihf-g++ -print-search-dirs + + - name: Configure compiler (macOS) + if: runner.os == 'macOS' + run: | + echo "CC=clang" >> $GITHUB_ENV + echo "CXX=clang++" >> $GITHUB_ENV + + - name: Set up QEMU (musl arm/arm64 and Linux ia32) + if: (matrix.musl && matrix.arch != 'x64') || matrix.arch == 'ia32' + uses: docker/setup-qemu-action@v4 + + # Official Node.js Linux ia32 binaries are no longer published. This + # reproduces the project's known-good ia32 build environment: native + # i386 Debian bullseye under QEMU with the last suitable unofficial + # x86 Node.js runtime. The produced addon uses Node-API, so the runtime + # is only the build host; the artifact is packaged once per architecture. + - name: Build in Debian bullseye/i386 container + if: matrix.arch == 'ia32' + run: | + docker run --rm \ + --platform linux/386 \ + -v "${{ github.workspace }}:/workspace" \ + -w /workspace \ + -e ARCH=ia32 \ + -e npm_config_arch=ia32 \ + i386/debian:bullseye-slim sh -c ' + set -e + apt-get update + apt-get install -y --no-install-recommends ca-certificates curl python3 make g++ + curl -fsSLO https://unofficial-builds.nodejs.org/download/release/v18.4.0/node-v18.4.0-linux-x86.tar.gz + echo "76062287b938d02f839fe82e60829703b852eefc692b4254982f8d2d18dd5532 node-v18.4.0-linux-x86.tar.gz" | sha256sum -c - + tar -xzf node-v18.4.0-linux-x86.tar.gz -C /usr/local --strip-components=1 --no-same-owner + rm node-v18.4.0-linux-x86.tar.gz + npm ci --ignore-scripts --verbose + ./node_modules/.bin/node-gyp rebuild --verbose + node scripts/post-install.js + ' + + - name: Build in Alpine/musl container + if: matrix.musl + run: | + case "${{ matrix.arch }}" in + x64) PLATFORM="linux/amd64" ;; + arm64) PLATFORM="linux/arm64" ;; + arm) PLATFORM="linux/arm/v7" ;; + esac + docker run --rm \ + --platform "$PLATFORM" \ + -v "${{ github.workspace }}:/workspace" \ + -w /workspace \ + -e ARCH=${{ matrix.arch }} \ + -e npm_config_arch=${{ matrix.arch }} \ + node:22-alpine sh -c " + set -e + apk add --no-cache python3 make g++ linux-headers + npm ci + " + + # V=1 forces gyp's generated Makefiles to print the full compiler and + # linker invocations, so we can confirm whether --sysroot (via + # LDFLAGS) is actually reaching the final link command that produces + # pty.node - not just the intermediate compile steps. Remove once the + # GLIBC_2.34 mismatch is resolved; this is diagnostic-only. + - name: Install dependencies and build + if: '!matrix.musl && matrix.arch != ''ia32''' + run: npm ci --verbose + env: + ARCH: ${{ matrix.arch }} + npm_config_arch: ${{ matrix.arch }} + CC: ${{ env.CC }} + CXX: ${{ env.CXX }} + SYSROOT_PATH: ${{ env.SYSROOT_PATH }} + CFLAGS: ${{ env.CFLAGS }} + CXXFLAGS: ${{ env.CXXFLAGS }} + LDFLAGS: ${{ env.LDFLAGS }} + V: 1 + + # crossCompile targets are no longer excluded here - this is exactly + # the check that would have caught the GLIBC_2.34 mismatch before it + # ever reached npm. If install-sysroot.js's bullseye baseline isn't + # honored correctly by the cross-toolchain's --sysroot flag, this + # step is what will now fail the build instead of publishing broken + # binaries silently. + - name: Verify GLIBC requirements (Linux) + if: runner.os == 'Linux' && !matrix.musl + run: | + EXPECTED_GLIBC_VERSION="2.28" \ + EXPECTED_GLIBCXX_VERSION="3.4.25" \ + SEARCH_PATH="build" \ + ./scripts/linux/verify-glibc-requirements.sh + + - name: Make spawn-helper executable (macOS/Linux) + if: runner.os != 'Windows' && !matrix.musl + run: chmod +x build/Release/spawn-helper || true + + - name: Build TypeScript + if: '!matrix.crossCompile && !matrix.musl' + run: npm run build + + - name: Test + if: '!matrix.crossCompile && !matrix.musl' + run: npm test + + - name: Lint + if: matrix.platform == 'linux-x64' + run: npm run lint + + - name: Stage platform prebuild + shell: bash + run: | + node -e "require('fs').cpSync('build/Release', 'prebuilds/${{ matrix.platform }}', { recursive: true })" + + - name: Upload platform artifact + uses: actions/upload-artifact@v7 + with: + name: prebuild-${{ matrix.platform }} + path: prebuilds/ + retention-days: 7 + + publish: + name: Publish to npm + if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' + needs: build + runs-on: ubuntu-22.04 + environment: ${{ (github.event_name == 'workflow_dispatch' && inputs.branch == 'main') && 'npm-publish-stable' || 'npm-publish-test' }} + permissions: + contents: write # Required to create tags/releases + id-token: write # Required for OIDC publishing to npm + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + persist-credentials: true # Needed so git push can authenticate + fetch-depth: 0 # Full history so tag/push works cleanly + + # ---- Release parameters ---- + # workflow_dispatch: driven by the `branch` and `publishPackage` inputs. + # push (to beta-*.*.* / alpha-*.*.*): branch tier is derived from the + # branch name itself, and is always a full publish (npm publish + tag + release). + + - name: Determine release parameters + id: release + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "branch=${{ inputs.branch }}" >> "$GITHUB_OUTPUT" + echo "publish=${{ inputs.publishPackage }}" >> "$GITHUB_OUTPUT" + else + echo "branch=${GITHUB_REF_NAME%%-*}" >> "$GITHUB_OUTPUT" + echo "publish=true" >> "$GITHUB_OUTPUT" + fi + # ---- end release parameters ---- + + - name: Use Node.js 24.x + uses: actions/setup-node@v7 + with: + node-version: '24.x' + registry-url: 'https://registry.npmjs.org' + + - name: Download and merge platform prebuilds + uses: actions/download-artifact@v8 + with: + pattern: prebuild-* + path: prebuilds/ + merge-multiple: true + + - name: Show merged prebuilds + run: find prebuilds -type f -print + + - name: Make spawn-helper executable + run: chmod +x prebuilds/darwin-*/spawn-helper || true + + - name: Install dependencies (skip native build) + run: npm ci --ignore-scripts + + - name: Build TypeScript + run: npm run build + + # ---- Versioning logic ---- + # Pre-release only: derive/adjust version from the shared homebridge script, + # then bump the prerelease counter. Stable (branch == main) releases publish + # whatever version is already committed in package.json. + + - name: Fetch shared version script + if: steps.release.outputs.branch != 'main' + run: wget https://raw.githubusercontent.com/homebridge/.github/latest/.github/npm-version-script.js + working-directory: .github + + - name: Install semver (dependency for shared version script) + if: steps.release.outputs.branch != 'main' + run: npm install semver + + - name: Adjust version to latest published base + tag + if: steps.release.outputs.branch != 'main' + run: node .github/npm-version-script.js ${{ github.ref }} ${{ steps.release.outputs.branch }} + + - name: Increment prerelease number + if: steps.release.outputs.branch != 'main' + run: npm version pre --preid=${{ steps.release.outputs.branch }} --no-git-tag-version + + - name: Show version to be published + run: node -p "require('./package.json').version" + # ---- end versioning logic ---- + + - name: Publish to npm (test) + if: steps.release.outputs.publish == 'true' && steps.release.outputs.branch != 'main' + run: npm publish --tag ${{ steps.release.outputs.branch }} --provenance --access public + + - name: Publish to npm (stable) + if: steps.release.outputs.publish == 'true' && steps.release.outputs.branch == 'main' + run: npm publish --tag latest --provenance --access public + + - name: Dry run (publishPackage=false) + if: steps.release.outputs.publish != 'true' + run: npm publish --tag ${{ steps.release.outputs.branch == 'main' && 'latest' || steps.release.outputs.branch }} --provenance --dry-run + + # ---- Tag + Release only happen on a real publish ---- + + # Use the exact local version passed to npm publish. Looking up the + # dist-tag immediately after publishing can briefly return its previous + # value while registry metadata propagates. + - name: Get package version + if: steps.release.outputs.publish == 'true' + id: version + run: | + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Add publish versions to job summary + if: steps.release.outputs.publish == 'true' + run: | + echo "## Published versions" >> "$GITHUB_STEP_SUMMARY" + echo "| Target | Version / tag |" >> "$GITHUB_STEP_SUMMARY" + echo "| --- | --- |" >> "$GITHUB_STEP_SUMMARY" + echo "| npm package version | \`${{ steps.version.outputs.version }}\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| npm dist-tag | \`${{ steps.release.outputs.branch == 'main' && 'latest' || steps.release.outputs.branch }}\` |" >> "$GITHUB_STEP_SUMMARY" + echo "| GitHub tag | \`v${{ steps.version.outputs.version }}\` |" >> "$GITHUB_STEP_SUMMARY" + + - name: Create and push git tag + if: steps.release.outputs.publish == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag "v${{ steps.version.outputs.version }}" + git push origin "v${{ steps.version.outputs.version }}" + + - name: Archive prebuilds per platform + if: steps.release.outputs.publish == 'true' + run: | + cd prebuilds + for dir in */; do + platform="${dir%/}" + tar -czf "../prebuilds-${platform}.tar.gz" "$platform" + done + cd .. + ls -la prebuilds-*.tar.gz + + - name: Create GitHub Release + if: steps.release.outputs.publish == 'true' + uses: softprops/action-gh-release@v3 + with: + tag_name: v${{ steps.version.outputs.version }} + name: v${{ steps.version.outputs.version }} + generate_release_notes: true + prerelease: ${{ steps.release.outputs.branch != 'main' }} + files: | + prebuilds-*.tar.gz diff --git a/.github/workflows/Stage 2 - Validate.yml b/.github/workflows/Stage 2 - Validate.yml new file mode 100644 index 000000000..efbc01dce --- /dev/null +++ b/.github/workflows/Stage 2 - Validate.yml @@ -0,0 +1,149 @@ +name: Stage 2 - Validate NPM Package contents +run-name: Stage 2 - Validate against ${{ github.ref_name }} + +on: + pull_request: + branches: [latest, beta-*.*.*, beta, alpha-*.*.*, alpha] + types: [review_requested, ready_for_review] + workflow_dispatch: + inputs: + tag: + description: 'npm dist-tag to validate (leave as auto to detect from branch)' + required: true + type: choice + options: + - auto + - latest + - beta + - alpha + default: auto + workflow_run: + workflows: ["Stage 1 - Prebuilds & Publish"] + types: + - completed + branches: [main, beta-*.*.*, beta, alpha-*.*.*, alpha] + +concurrency: + group: validate + cancel-in-progress: true + +jobs: + # Validate that node-pty can be installed and ran across all platforms + determine_tag: + name: Determine Tag + runs-on: ubuntu-latest + # Only run if the Stage 1 workflow succeeded (for workflow_run) or always for other triggers + if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} + outputs: + tag: ${{ steps.tag.outputs.tag }} + package_name: ${{ steps.pkg.outputs.name }} + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Get package name from package.json + id: pkg + run: | + NAME=$(node -p "require('./package.json').name") + echo "name=$NAME" >> "$GITHUB_OUTPUT" + echo "::notice::Using package name: $NAME" + + - name: Tag + id: tag + run: | + # If manually dispatched with an explicit tag, use it directly + if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.tag }}" != "auto" ]]; then + TAG="${{ inputs.tag }}" + echo "Using manually selected TAG: $TAG" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "::notice::Using manually selected TAG: $TAG" + exit 0 + fi + + # github.ref_name is not the release branch on every event: on + # pull_request it is the merge ref (e.g. 91/merge) and on workflow_run + # it is the default branch, so resolve the real branch first + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + BRANCH="${{ github.base_ref }}" + elif [[ "${{ github.event_name }}" == "workflow_run" ]]; then + BRANCH="${{ github.event.workflow_run.head_branch }}" + else + BRANCH="${{ github.ref_name }}" + fi + if [[ "$BRANCH" == beta-* || "$BRANCH" == "beta" ]]; then + TAG="beta" + elif [[ "$BRANCH" == alpha-* || "$BRANCH" == "alpha" ]]; then + TAG="alpha" + else + TAG="latest" + fi + echo "Using TAG: $TAG" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "::notice::Using TAG: $TAG" + + validate_service_starts: + name: Validate + needs: determine_tag + strategy: + fail-fast: false + matrix: + node-version: [ + 26.x, + 25.x, + 24.x, + 23.x, + 22.x, + 21.x, + 20.x, + 19.x] + os: [ + ubuntu-latest, + ubuntu-24.04, + ubuntu-22.04, + ubuntu-24.04-arm, + ubuntu-22.04-arm, + macos-latest, + macos-26, + macos-15, + macos-26-intel, + macos-15-intel, + windows-latest, + windows-2025, + windows-2022, + windows-11-arm] + exclude: + - os: windows-11-arm + node-version: 19.x + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/setup-node@v7 + with: + node-version: ${{ matrix.node-version }} + - if: runner.os == 'Linux' + run: cat /etc/os-release + + - if: runner.os == 'Windows' + run: systeminfo + + - if: runner.os == 'macOS' + run: sw_vers + + - name: Install published package without build fallback + run: npm install --ignore-scripts ${{ needs.determine_tag.outputs.package_name }}@${{ needs.determine_tag.outputs.tag }} + + - name: Display structure of downloaded files + run: ls -R + + - name: Validate packaged native prebuild + run: | + cd ./node_modules/${{ needs.determine_tag.outputs.package_name }} + npm run validate + + - run: | + cd ./node_modules/${{ needs.determine_tag.outputs.package_name }} + npm run postinstall + + - run: | + echo "::notice::Successfully tested ${{ github.ref_name }} on ${{ matrix.os }} with Node.js ${{ matrix.node-version }}" diff --git a/README.md b/README.md index 4e29e84b2..ae88e3ab7 100644 --- a/README.md +++ b/README.md @@ -1,164 +1,67 @@ -# node-pty +

+ +

+ -[![Build Status](https://dev.azure.com/vscode/node-pty/_apis/build/status/Microsoft.node-pty?branchName=main)](https://dev.azure.com/vscode/node-pty/_build/latest?definitionId=11&branchName=main) +# node-pty-prebuilt-multiarch -`forkpty(3)` bindings for node.js. This allows you to fork processes with pseudoterminal file descriptors. It returns a terminal object which allows reads and writes. +[![npm](https://badgen.net/npm/v/@homebridge/node-pty-prebuilt-multiarch/latest)](https://www.npmjs.com/package/@homebridge/node-pty-prebuilt-multiarch) +[![npm](https://badgen.net/npm/dt/@homebridge/node-pty-prebuilt-multiarch?label=downloads)](https://www.npmjs.com/package/@homebridge/node-pty-prebuilt-multiarch) +![Prebuild Binaries](https://github.com/homebridge/node-pty-prebuilt-multiarch/workflows/Build%20and%20Test/badge.svg) +[![Discord](https://badgen.net/discord/online-members/C87Pvq3?icon=discord&label=discord)](https://discord.gg/C87Pvq3) -This is useful for: + -- Writing a terminal emulator (eg. via [xterm.js](https://github.com/sourcelair/xterm.js)). -- Getting certain programs to *think* you're a terminal, such as when you need a program to send you control sequences. +This project is a parallel fork of [`node-pty`](https://github.com/Microsoft/node-pty) providing prebuilt packages for certain Node.js versions. -`node-pty` supports Linux, macOS and Windows. Windows support is possible by utilizing the [Windows conpty API](https://blogs.msdn.microsoft.com/commandline/2018/08/02/windows-command-line-introducing-the-windows-pseudo-console-conpty/) on Windows 1809+. +Inspired by [daviwil/node-pty-prebuilt](https://github.com/daviwil/node-pty-prebuilt). -> **Note:** Support for the `winpty` library has been removed. Windows 10 version 1809 (build 18309) or later is now required. +## Usage -## API +Thanks to the excellent [`prebuild`](https://github.com/prebuild/prebuild), [`prebuild-install`](https://github.com/prebuild/prebuild) modules, and [`prebuildify`](https://github.com/prebuild/prebuildify) using this module is extremely easy. +You merely have to change your `node-pty` dependency to `@homebridge/node-pty-prebuilt-multiarch` and then change any `require` statements in your code from `require('node-pty')` to `require('@homebridge/node-pty-prebuilt-multiarch')`. -The full API for node-pty is contained within the [TypeScript declaration file](https://github.com/microsoft/node-pty/blob/main/typings/node-pty.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API. +## How It Works -## Example Usage +We maintain a parallel fork of the `node-pty` codebase that will be updated as new releases are shipped. +When we merge new updates to the code into the `prebuilt-multiarch` branch, new prebuilt packages for our supported Node.js versions are updated to the corresponding [GitHub release](https://github.com/homebridge/node-pty-prebuilt-multiarch/releases). -```js -import * as os from 'node:os'; -import * as pty from 'node-pty'; +When `@homebridge/node-pty-prebuilt-multiarch` is installed as a package dependency, the installation script checks to see if there's a prebuilt package on this repo for the OS, ABI version, and architecture of the current process and then downloads it, extracting it into the module path. +If a corresponding prebuilt package is not found, `node-gyp` is invoked to build the package for the current platform. -const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash'; +## Prebuilt Versions -const ptyProcess = pty.spawn(shell, [], { - name: 'xterm-color', - cols: 80, - rows: 30, - cwd: process.env.HOME, - env: process.env -}); +| OS | Architectures | +|---------------|---------------------------| +| macOS | x64, arm64 | +| Linux (glibc) | ia32, x64, aarch64 | +| Linux (musl) | x64, aarch64 | +| Windows | x64, arm64 | -ptyProcess.onData((data) => { - process.stdout.write(data); -}); +We only provide prebuilt binaries for Node.js 20 and higher. -ptyProcess.write('ls\r'); -ptyProcess.resize(100, 40); -ptyProcess.write('ls\r'); -``` +## Build / Package -## Real-world Uses +Please note releasing this package uses the GitHub action `Stage 1 - Run prebuild's and Create GitHub and NPM release` and `Stage 2 - Validate NPM Package contents`. -`node-pty` powers many different terminal emulators, including: +The github action takes the branch selected from the workflow start drop down, and creates a GitHub and NPM Release containing the prebuild artifacts. +The version of the Release comes from the package.json, and in the case of a BETA release automatically appends the beta release version. +During processing, it leverages a branch called `release-candidate` as a holding area for prebuilds. -- [Microsoft Visual Studio Code](https://code.visualstudio.com) -- [Hyper](https://hyper.is/) -- [Upterm](https://github.com/railsware/upterm) -- [Script Runner](https://github.com/ioquatix/script-runner) for Atom. -- [Theia](https://github.com/theia-ide/theia) -- [FreeMAN](https://github.com/matthew-matvei/freeman) file manager -- [terminus](https://atom.io/packages/terminus) - An Atom plugin for providing terminals inside your Atom workspace. -- [x-terminal](https://atom.io/packages/x-terminal) - Also an Atom plugin that provides terminals inside your Atom workspace. -- [Termination](https://atom.io/packages/termination) - Also an Atom plugin that provides terminals inside your Atom workspace. -- [atom-xterm](https://atom.io/packages/atom-xterm) - Also an Atom plugin that provides terminals inside your Atom workspace. -- [electerm](https://github.com/electerm/electerm) Terminal/SSH/SFTP client(Linux, macOS, Windows). -- [Extraterm](http://extraterm.org/) -- [Wetty](https://github.com/krishnasrinivas/wetty) Browser based Terminal over HTTP and HTTPS -- [nomad](https://github.com/lukebarnard1/nomad-term) -- [DockerStacks](https://github.com/sfx101/docker-stacks) Local LAMP/LEMP stack using Docker -- [TeleType](https://github.com/akshaykmr/TeleType): cli tool that allows you to share your terminal online conveniently. Show off mad cli-fu, help a colleague, teach, or troubleshoot. -- [mesos-term](https://github.com/criteo/mesos-term): A web terminal for Apache Mesos. It allows to execute commands within containers. -- [Commas](https://github.com/CyanSalt/commas): A hackable terminal and command runner. -- [ENiGMA½ BBS Software](https://github.com/NuSkooler/enigma-bbs): A modern BBS software with a nostalgic flair! -- [Tinkerun](https://github.com/tinkerun/tinkerun): A new way of running Tinker. -- [Tess](https://tessapp.dev): Hackable, simple and rapid terminal for the new era of technology 👍 -- [NxShell](https://nxshell.github.io/): An easy to use new terminal for Windows/Linux/MacOS platform. -- [OpenSumi](https://github.com/opensumi/core): A framework helps you quickly build Cloud or Desktop IDE products. -- [Enjoy Git](https://github.com/huangcs427/enjoy-git-release): A modern Git client featuring an intuitive user interface, built with Electron, Vue 3, and TypeScript. -- [Logos](https://github.com/zixiao-labs/logos): A Modern, Lightweight Code Editor, built with Electron, Vue 3, and TypeScript. +When running the job, most times a couple of the instances of the sub step `Commit & Push Changes` within `Prebuild NPM and GitHub Release artifacts` fails. +When this occurs just re-run. This is due to concurrency issues between the steps and GitHub. +A typical run has 3-4 steps fail. -Do you use node-pty in your application as well? Please open a [Pull Request](https://github.com/Tyriar/node-pty/pulls) to include it here. We would love to have it in our list. - -## Building - -```bash -# Install dependencies and build C++ -npm install -# Compile TypeScript -> JavaScript -npm run build -``` - -## Dependencies - -Node.JS 16 or Electron 19 is required to use `node-pty`. What version of node is supported is currently mostly bound to [whatever version Visual Studio Code is using](https://github.com/microsoft/node-pty/issues/557#issuecomment-1332193541). - -### Linux (apt) - -```sh -sudo apt install -y make python build-essential -``` - -### macOS - -Xcode is needed to compile the sources, this can be installed from the App Store. - -### Windows - -`npm install` requires some tools to be present in the system like Python and C++ compiler. Windows users can easily install them by running the following command in PowerShell as administrator. For more information see https://github.com/felixrieseberg/windows-build-tools: - -```sh -npm install --global --production windows-build-tools -``` - -The following are also needed: - -- [Windows SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk) - only the "Desktop C++ Apps" components are needed to be installed -- Spectre-mitigated libraries - In order to avoid the build error "MSB8040: Spectre-mitigated libraries are required for this project", open the Visual Studio Installer, press the Modify button, navigate to the "Individual components" tab, search "Spectre", and install an option like "MSVC v143 - VS 2022 C++ x64/x86 Spectre-mitigated libs (Latest)" (the exact option to install will depend on your version of Visual Studio as well as your operating system architecture) - -## Debugging - -[The wiki](https://github.com/Microsoft/node-pty/wiki/Debugging) contains instructions for debugging node-pty. - -## Security - -All processes launched from node-pty will launch at the same permission level of the parent process. Take care particularly when using node-pty inside a server that's accessible on the internet. We recommend launching the pty inside a container to protect your host machine. - -## Thread Safety - -Note that node-pty is not thread safe so running it across multiple worker threads in node.js could cause issues. - -## Flow Control - -Automatic flow control can be enabled by either providing `handleFlowControl = true` in the constructor options or setting it later on: - -```js -const PAUSE = '\x13'; // XOFF -const RESUME = '\x11'; // XON - -const ptyProcess = pty.spawn(shell, [], {handleFlowControl: true}); - -// flow control in action -ptyProcess.write(PAUSE); // pty will block and pause the child program -... -ptyProcess.write(RESUME); // pty will enter flow mode and resume the child program - -// temporarily disable/re-enable flow control -ptyProcess.handleFlowControl = false; -... -ptyProcess.handleFlowControl = true; -``` - -By default `PAUSE` and `RESUME` are XON/XOFF control codes (as shown above). To avoid conflicts in environments that use these control codes for different purposes the messages can be customized as `flowControlPause: string` and `flowControlResume: string` in the constructor options. `PAUSE` and `RESUME` are not passed to the underlying pseudoterminal if flow control is enabled. - -## Troubleshooting - -### Powershell gives error 8009001d - -> Internal Windows PowerShell error. Loading managed Windows PowerShell failed with error 8009001d. - -This happens when PowerShell is launched with no `SystemRoot` environment variable present. - -## pty.js - -This project is forked from [chjj/pty.js](https://github.com/chjj/pty.js) with the primary goals being to provide better support for later Node.js versions and Windows. +1. Create branch `release-candidate` if not existing (the script deletes it before starting and will fail if it isn't present) +2. Ensure version tag within package.json reflects version you want to publish, please note beta tags are added by the action. +3. Run Action `Run prebuild's and Create GitHub and NPM release`, and select branch you wish to publish, and if it needs to be BETA tagged and versioned +4. This will run for about an hour, and create a GitHub release with the prebuild artifacts attached, and a npm release with the prebuild artifacts attached ## License -Copyright (c) 2012-2015, Christopher Jeffrey (MIT License).
-Copyright (c) 2016, Daniel Imms (MIT License).
-Copyright (c) 2018, Microsoft Corporation (MIT License). +* Copyright (c) 2012-2015, Christopher Jeffrey (MIT License). +* Copyright (c) 2016, Daniel Imms (MIT License). +* Copyright (c) 2018, Microsoft Corporation (MIT License). +* Copyright (c) 2018, David Wilson (MIT License). +* Copyright (c) 2018, oznu (MIT License). +* Copyright (c) 2025, Homebridge (MIT License). diff --git a/package-lock.json b/package-lock.json index 9cd363bab..2063b6be5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { - "name": "node-pty", - "version": "1.1.0", + "name": "@homebridge/node-pty-prebuilt-multiarch", + "version": "0.15.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "node-pty", - "version": "1.1.0", + "name": "@homebridge/node-pty-prebuilt-multiarch", + "version": "0.15.0", "hasInstallScript": true, "license": "MIT", "dependencies": { - "node-addon-api": "^7.1.0" + "node-addon-api": "^8.9.0" }, "devDependencies": { "@types/mocha": "^10.0.10", @@ -21,8 +21,12 @@ "eslint": "^10.4.0", "globals": "^14.0.0", "mocha": "12.0.2", + "node-gyp": "^13.1.0", "ps-list": "^6.0.0", "typescript": "^5.0.0" + }, + "engines": { + "node": ">=20.0.0 <27.0.0" } }, "node_modules/@eslint-community/eslint-utils": { @@ -185,6 +189,19 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -452,6 +469,16 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/abbrev": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-5.0.0.tgz", + "integrity": "sha512-/XrFJgzQQQHpti1raDJC6m4ws6aNktmjBlhk8Fdlk7LwCEuDoieEJJY9OFHjfiFJFFRM2tK+Ky/IsfbbmlMu1w==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -544,6 +571,16 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/cross-env": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.1.tgz", @@ -621,6 +658,16 @@ "node": ">=0.3.1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -899,6 +946,13 @@ "node": ">=0.10.0" } }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1041,6 +1095,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1231,6 +1292,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mocha": { "version": "12.0.2", "resolved": "https://registry.npmjs.org/mocha/-/mocha-12.0.2.tgz", @@ -1296,9 +1370,80 @@ "dev": true }, "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" + "version": "8.9.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz", + "integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-13.1.0.tgz", + "integrity": "sha512-FpNzOLFACKG0oRBYbZ5WOeWD1EqRTIWX90MaRIWTO18lVYPx8fqIjrdS3pihjnCkSmgop6zVGkSqg1vXZLksuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^10.0.0", + "proc-log": "^7.0.0", + "semver": "^7.3.5", + "tar": "^7.5.7", + "tinyglobby": "^0.2.12", + "undici": "^8.4.1", + "which": "^7.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-7.0.0.tgz", + "integrity": "sha512-RancgH2dmbLdHl6LRhEqvklWMgl/Hdnun0Y90KhBOLkMefg8Qa7/Zel8Sm+8HEcP6DEjzsWzpkuBQEZok58isA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/nopt": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-10.0.1.tgz", + "integrity": "sha512-df3sBr/6ax9hSGuC3CspvLlbnX8cP5L5nZwXF8cGN8l0zSWR6BvzmQ6jPUKjvo6+/xdpkNvEcucBNUdBeeV13g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^5.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } }, "node_modules/optionator": { "version": "0.9.4", @@ -1383,6 +1528,16 @@ "node": ">= 0.8.0" } }, + "node_modules/proc-log": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-7.0.0.tgz", + "integrity": "sha512-FYgfaA69XZ93zaXLoMNQ+ViDXGGBgR8aLh03txzcFhV+9xOXx7+8DLCULrKKpR9+GsH9ZfHm82aSUPpozX0Ztg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/ps-list": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/ps-list/-/ps-list-6.3.0.tgz", @@ -1473,6 +1628,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -1561,6 +1733,16 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "8.12.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.3.tgz", + "integrity": "sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -1600,6 +1782,16 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 08e2d0687..d333a21af 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,16 @@ { - "name": "node-pty", + "name": "@homebridge/node-pty-prebuilt-multiarch", "description": "Fork pseudoterminals in Node.JS", "author": { "name": "Microsoft Corporation" }, - "version": "1.1.0", + "version": "0.15.0", "license": "MIT", "main": "./lib/index.js", "types": "./typings/node-pty.d.ts", "repository": { "type": "git", - "url": "git://github.com/microsoft/node-pty.git" + "url": "git://github.com/homebridge/node-pty-prebuilt-multiarch.git" }, "files": [ "binding.gyp", @@ -20,12 +20,13 @@ "scripts/prebuild.js", "src/**/*.{cc,h}", "prebuilds/", + "!prebuilds/**/*.pdb", "third_party/", "typings/" ], - "homepage": "https://github.com/microsoft/node-pty", + "homepage": "https://github.com/homebridge/node-pty-prebuilt-multiarch", "bugs": { - "url": "https://github.com/microsoft/node-pty/issues" + "url": "https://github.com/homebridge/node-pty-prebuilt-multiarch/issues" }, "devEngines": { "runtime": { @@ -52,6 +53,7 @@ "watch": "tsc -b -w ./src/tsconfig.json", "lint": "eslint src/", "install": "node scripts/prebuild.js || node-gyp rebuild", + "validate": "node scripts/prebuild.js", "postinstall": "node scripts/post-install.js", "compileCommands": "node scripts/gen-compile-commands.js", "test": "cross-env NODE_ENV=test mocha -R spec --exit lib/*.test.js", @@ -60,7 +62,10 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "node-addon-api": "^7.1.0" + "node-addon-api": "^8.9.0" + }, + "engines": { + "node": ">=20.0.0 <27.0.0" }, "devDependencies": { "@types/mocha": "^10.0.10", @@ -71,7 +76,12 @@ "eslint": "^10.4.0", "globals": "^14.0.0", "mocha": "12.0.2", + "node-gyp": "^13.1.0", "ps-list": "^6.0.0", "typescript": "^5.0.0" + }, + "allowScripts": { + "fsevents@2.1.3": true, + "es5-ext": false } } diff --git a/scripts/linux/checksums.txt b/scripts/linux/checksums.txt index 66504f0a7..cf9a2c428 100644 --- a/scripts/linux/checksums.txt +++ b/scripts/linux/checksums.txt @@ -1,2 +1,3 @@ 3122af49c493c5c767c2b0772a41119cbdc9803125a705683445b4066dc88b82 x86_64-linux-gnu-glibc-2.28-gcc-10.5.0.tar.gz 3baac81a39b69e0929e4700f4f78f022adefc515010054ec393565657c4fff32 aarch64-linux-gnu-glibc-2.28-gcc-10.5.0.tar.gz +6e251200607ac4c4709ebd08b2dc0d9a353ddcfdb47f43a10c2b4cc4b49920c0 arm-rpi-linux-gnueabihf-glibc-2.28-gcc-10.5.0.tar.gz diff --git a/scripts/linux/install-sysroot.js b/scripts/linux/install-sysroot.js index 97ac3022c..df1060579 100755 --- a/scripts/linux/install-sysroot.js +++ b/scripts/linux/install-sysroot.js @@ -104,6 +104,22 @@ async function getSysroot(arch) { expectedName = `aarch64-linux-gnu${prefix}.tar.gz`; triple = 'aarch64-linux-gnu'; break; + // NOTE: 'arm-rpi-linux-gnueabihf' is the triple VS Code's own build + // pipeline uses for its Raspberry Pi / armhf sysroot in the same + // vscode-linux-build-agent release. Confirm the exact asset name + // against scripts/linux/checksums.txt before relying on this - + // if the release uses a different triple (e.g. plain + // 'arm-linux-gnueabihf'), update expectedName/triple to match. + case 'arm': + expectedName = `arm-rpi-linux-gnueabihf${prefix}.tar.gz`; + triple = 'arm-rpi-linux-gnueabihf'; + break; + // NOTE: same caveat as 'arm' above - verify 'i686-linux-gnu' is the + // actual asset/triple name present in checksums.txt for this release. + case 'ia32': + expectedName = `i686-linux-gnu${prefix}.tar.gz`; + triple = 'i686-linux-gnu'; + break; default: throw new Error(`Unsupported architecture: ${arch}`); } @@ -155,4 +171,4 @@ if (require.main === module) { main(); } -module.exports = { getSysroot }; +module.exports = { getSysroot }; \ No newline at end of file diff --git a/scripts/prebuild.js b/scripts/prebuild.js index 17f1d980e..14cf65b03 100644 --- a/scripts/prebuild.js +++ b/scripts/prebuild.js @@ -16,19 +16,39 @@ const path = require('path'); const PREBUILDS_ROOT = path.join(__dirname, '..', 'prebuilds'); const PREBUILD_DIR = path.join(__dirname, '..', 'prebuilds', `${process.platform}-${process.arch}`); +const PREBUILD_FILES = (process.platform === 'win32' + ? ['conpty.node', 'conpty_console_list.node'] + : ['pty.node'] +).map(file => path.join(PREBUILD_DIR, file)); // Do not use prebuilds when npm_config_build_from_source is set if (process.env.npm_config_build_from_source === 'true') { - console.log('\x1b[33m> Removing prebuilds and rebuilding because npm_config_build_from_source is set\x1b[0m'); + console.log(`\x1b[33m> Removing prebuilds from ${PREBUILDS_ROOT} and rebuilding because npm_config_build_from_source is set\x1b[0m`); fs.rmSync(PREBUILDS_ROOT, { recursive: true, force: true }); process.exit(1); } // Check whether the correct prebuilt files exist -console.log('\x1b[32m> Checking prebuilds...\x1b[0m'); -if (!fs.existsSync(PREBUILD_DIR)) { - console.log(`\x1b[33m> Rebuilding because directory ${PREBUILD_DIR} does not exist\x1b[0m`); - process.exit(1); +console.error(`\x1b[32m> Checking for prebuilds in directory ${PREBUILD_DIR}\x1b[0m`); +for (const prebuildFile of PREBUILD_FILES) { + if (!fs.existsSync(prebuildFile)) { + console.error(`\x1b[33m> Rebuilding because prebuild ${prebuildFile} does not exist\x1b[0m`); + process.exit(1); + } + + // Loading the addon catches ABI and shared-library incompatibilities that a + // file existence check cannot, such as a GLIBC version newer than the host + // provides. Exiting non-zero triggers the node-gyp rebuild fallback from the + // package install script. Runtime module loading prefers build/Release over + // prebuilds, so the locally rebuilt addon will be selected afterwards. + try { + require(prebuildFile); + console.error(`\x1b[32m> Successfully loaded prebuild ${prebuildFile}\x1b[0m`); + } catch (error) { + console.error(`\x1b[33m> Rebuilding because prebuild ${prebuildFile} could not be loaded\x1b[0m`); + console.error(error); + process.exit(1); + } } process.exit(0); diff --git a/src/utils.ts b/src/utils.ts index 1d2d7c933..a4d0fa797 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -14,16 +14,31 @@ export function loadNativeModule(name: string): {dir: string, module: any} { const dirs = ['build/Release', 'build/Debug', `prebuilds/${process.platform}-${process.arch}`]; // Check relative to the parent dir for unbundled and then the current dir for bundled const relative = ['..', '.']; - let lastError: unknown; + const checked: string[] = []; + const loadErrors: Error[] = []; for (const d of dirs) { for (const r of relative) { const dir = `${r}/${d}`; + const modulePath = `${dir}/${name}.node`; + checked.push(modulePath); try { - return { dir, module: require(`${dir}/${name}.node`) }; + return { dir, module: require(modulePath) }; } catch (e) { - lastError = e; + // A missing candidate is expected while searching the fallback paths. + // Preserve actual loader failures so they are not hidden by a later + // MODULE_NOT_FOUND from another candidate. + if (e instanceof Error && (e as NodeJS.ErrnoException).code !== 'MODULE_NOT_FOUND') { + loadErrors.push(e); + } } } } - throw new Error(`Failed to load native module: ${name}.node, checked: ${dirs.join(', ')}: ${lastError}`); + + if (loadErrors.length > 0) { + const cause = loadErrors[0]; + console.error(cause); + throw new Error(`Failed to load native module ${name}.node. Checked: ${checked.join(', ')}`); + } + + throw new Error(`Failed to load native module ${name}.node because no candidate file was found. Checked: ${checked.join(', ')}`); } diff --git a/typings/node-pty.d.ts b/typings/node-pty.d.ts index f46c02968..f0137bf6d 100644 --- a/typings/node-pty.d.ts +++ b/typings/node-pty.d.ts @@ -3,7 +3,7 @@ * Copyright (c) 2018, Microsoft Corporation (MIT License). */ -declare module 'node-pty' { +declare module '@homebridge/node-pty-prebuilt-multiarch' { /** * Forks a process as a pseudoterminal. * @param file The file to launch. From 0d98b5191c57a5454a96a6aba6df183d5274fa13 Mon Sep 17 00:00:00 2001 From: Northern Man <19808920+NorthernMan54@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:30:31 -0400 Subject: [PATCH 31/36] chore: drop the Node <27 engines cap Prebuilds are N-API, so a new Node major needs no new binaries. Co-Authored-By: Claude Opus 5.5 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2063b6be5..35758d4a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "typescript": "^5.0.0" }, "engines": { - "node": ">=20.0.0 <27.0.0" + "node": ">=20.0.0" } }, "node_modules/@eslint-community/eslint-utils": { diff --git a/package.json b/package.json index d333a21af..7b7ffa8e4 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "node-addon-api": "^8.9.0" }, "engines": { - "node": ">=20.0.0 <27.0.0" + "node": ">=20.0.0" }, "devDependencies": { "@types/mocha": "^10.0.10", From 8267a1f3e8477bb1b970f33e022ee9ea1c96771b Mon Sep 17 00:00:00 2001 From: Northern Man <19808920+NorthernMan54@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:38:33 -0400 Subject: [PATCH 32/36] chore: warn instead of failing on devEngines Upstream requires Node >=24 / npm >=11.6.3 for development. Our prebuild matrix deliberately builds on older hosts (Node 22 runners, node:22-alpine, and Node 18.4.0 for linux-ia32, the last x86 runtime available), so the hard check stopped every prebuild job before it started. The output is Node-API, so the build host's Node version does not affect consumers. Co-Authored-By: Claude Opus 5.5 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7b7ffa8e4..bd4ebd10e 100644 --- a/package.json +++ b/package.json @@ -32,12 +32,12 @@ "runtime": { "name": "node", "version": ">=24", - "onFail": "error" + "onFail": "warn" }, "packageManager": { "name": "npm", "version": ">=11.6.3", - "onFail": "error" + "onFail": "warn" } }, "keywords": [ From f59a37795c21a0a64f2d8985403c3f74736d937f Mon Sep 17 00:00:00 2001 From: Northern Man <19808920+NorthernMan54@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:54:25 -0400 Subject: [PATCH 33/36] ci(ia32): build from archive.debian.org with node-gyp 11 Bullseye's security pool has left deb.debian.org (apt got 404s), so the i386 container now installs from archive.debian.org; staying on bullseye keeps the ia32 prebuild on glibc 2.31. That container's Node 18.4.0 (the last x86 runtime) can't run node-gyp 13, so it uses node-gyp 11.5.0 explicitly. Co-Authored-By: Claude Opus 5.5 --- .../workflows/Stage 1 - Prebuilds - Publish.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Stage 1 - Prebuilds - Publish.yml b/.github/workflows/Stage 1 - Prebuilds - Publish.yml index 3aeb1a558..ad80c6b5d 100644 --- a/.github/workflows/Stage 1 - Prebuilds - Publish.yml +++ b/.github/workflows/Stage 1 - Prebuilds - Publish.yml @@ -172,6 +172,16 @@ jobs: -e npm_config_arch=ia32 \ i386/debian:bullseye-slim sh -c ' set -e + # Bullseye has left deb.debian.org (security pool files now 404), so + # install from archive.debian.org. Staying on bullseye keeps the ia32 + # prebuild on glibc 2.31. + rm -f /etc/apt/sources.list.d/* + printf "%s\n" \ + "deb http://archive.debian.org/debian bullseye main" \ + "deb http://archive.debian.org/debian bullseye-updates main" \ + "deb http://archive.debian.org/debian-security bullseye-security main" \ + > /etc/apt/sources.list + echo "Acquire::Check-Valid-Until \"false\";" > /etc/apt/apt.conf.d/99archive apt-get update apt-get install -y --no-install-recommends ca-certificates curl python3 make g++ curl -fsSLO https://unofficial-builds.nodejs.org/download/release/v18.4.0/node-v18.4.0-linux-x86.tar.gz @@ -179,7 +189,9 @@ jobs: tar -xzf node-v18.4.0-linux-x86.tar.gz -C /usr/local --strip-components=1 --no-same-owner rm node-v18.4.0-linux-x86.tar.gz npm ci --ignore-scripts --verbose - ./node_modules/.bin/node-gyp rebuild --verbose + # node-gyp 13 (the devDependency) needs Node >=22.22; 11.x is the + # last line that runs on this Node 18 x86 build host. + npx --yes node-gyp@11.5.0 rebuild --verbose node scripts/post-install.js ' From 0f2750deffd5d1cc16867c33f8326717394812c0 Mon Sep 17 00:00:00 2001 From: Northern Man <19808920+NorthernMan54@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:54:25 -0400 Subject: [PATCH 34/36] ci: build and test prebuilds on Node 24.22.1, as upstream CI does win32-arm64 died in the "Regression for #921" race test on Node 22.x, while upstream's windows-11-arm job passes it on 24.22.1. The addon is Node-API: a build from a newer Node loads and runs on Node 20, 22 and 24 (checked locally), and Stage 2 validates Node 19-26 after publishing. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/Stage 1 - Prebuilds - Publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Stage 1 - Prebuilds - Publish.yml b/.github/workflows/Stage 1 - Prebuilds - Publish.yml index ad80c6b5d..1c16276f6 100644 --- a/.github/workflows/Stage 1 - Prebuilds - Publish.yml +++ b/.github/workflows/Stage 1 - Prebuilds - Publish.yml @@ -79,10 +79,10 @@ jobs: with: persist-credentials: false - - name: Use Node.js 22.x + - name: Use Node.js 24.22.1 uses: actions/setup-node@v7 with: - node-version: '22.x' + node-version: '24.22.1' # NOTE: sysroot pinning covers the native x64/arm64 and cross-compiled # linux-arm targets. linux-ia32 is built separately in a native i386 From 5fb6cc2e348e192f536993de380dca4086797a8c Mon Sep 17 00:00:00 2001 From: Northern Man <19808920+NorthernMan54@users.noreply.github.com> Date: Sun, 25 Oct 2026 19:02:03 -0400 Subject: [PATCH 35/36] ci(ia32): run node-gyp 11 from its own prefix npm 8's npx reused the project's node-gyp 13 instead of fetching node-gyp@11.5.0, which then failed on Node 18.4. Install 11.5.0 into /tmp/node-gyp-11 and call it by path. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/Stage 1 - Prebuilds - Publish.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Stage 1 - Prebuilds - Publish.yml b/.github/workflows/Stage 1 - Prebuilds - Publish.yml index 1c16276f6..35e5da1fa 100644 --- a/.github/workflows/Stage 1 - Prebuilds - Publish.yml +++ b/.github/workflows/Stage 1 - Prebuilds - Publish.yml @@ -190,8 +190,10 @@ jobs: rm node-v18.4.0-linux-x86.tar.gz npm ci --ignore-scripts --verbose # node-gyp 13 (the devDependency) needs Node >=22.22; 11.x is the - # last line that runs on this Node 18 x86 build host. - npx --yes node-gyp@11.5.0 rebuild --verbose + # last line that runs on this Node 18 x86 build host. Install it + # outside the project: npm 8's npx would reuse the local 13.x. + npm install --no-save --no-audit --no-fund --prefix /tmp/node-gyp-11 node-gyp@11.5.0 + /tmp/node-gyp-11/node_modules/.bin/node-gyp rebuild --verbose node scripts/post-install.js ' From 96ac634b6630e4b62e80ae37b81411e24b1f26c7 Mon Sep 17 00:00:00 2001 From: Northern Man <19808920+NorthernMan54@users.noreply.github.com> Date: Sun, 25 Oct 2026 19:07:47 -0400 Subject: [PATCH 36/36] ci(ia32): drop an apostrophe that broke the container's quoted script Co-Authored-By: Claude Opus 5.5 --- .github/workflows/Stage 1 - Prebuilds - Publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Stage 1 - Prebuilds - Publish.yml b/.github/workflows/Stage 1 - Prebuilds - Publish.yml index 35e5da1fa..84c12bef5 100644 --- a/.github/workflows/Stage 1 - Prebuilds - Publish.yml +++ b/.github/workflows/Stage 1 - Prebuilds - Publish.yml @@ -191,7 +191,7 @@ jobs: npm ci --ignore-scripts --verbose # node-gyp 13 (the devDependency) needs Node >=22.22; 11.x is the # last line that runs on this Node 18 x86 build host. Install it - # outside the project: npm 8's npx would reuse the local 13.x. + # outside the project, as npx in npm 8 would reuse the local 13.x. npm install --no-save --no-audit --no-fund --prefix /tmp/node-gyp-11 node-gyp@11.5.0 /tmp/node-gyp-11/node_modules/.bin/node-gyp rebuild --verbose node scripts/post-install.js