diff --git a/.changeset/emudhra-bun-1-4-4-runtime.md b/.changeset/emudhra-bun-1-4-4-runtime.md new file mode 100644 index 000000000..3d6d4fda6 --- /dev/null +++ b/.changeset/emudhra-bun-1-4-4-runtime.md @@ -0,0 +1,5 @@ +--- +"hunkdiff": patch +--- + +Upgrade the bundled Bun runtime to 1.4.4 so Hunk's own HTTPS requests (GitHub review via `hunk gh`, extension fetches) reject certificates issued by the compromised eMudhra intermediate CAs. diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 8f3f3f5e6..f844f649e 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -29,7 +29,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Install dependencies run: bun install --frozen-lockfile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b883ca58..111c31f08 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Set up Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -101,7 +101,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Install dependencies run: bun install --frozen-lockfile @@ -126,7 +126,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Install dependencies run: bun install --frozen-lockfile @@ -151,7 +151,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Set up Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -204,7 +204,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Set up Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -240,7 +240,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Install dependencies run: bun install --frozen-lockfile diff --git a/.github/workflows/install-vm.yml b/.github/workflows/install-vm.yml index 5b2ff8e42..e8ffa602c 100644 --- a/.github/workflows/install-vm.yml +++ b/.github/workflows/install-vm.yml @@ -31,7 +31,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Install ordinary development dependencies run: bun install --frozen-lockfile diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 4f776feb8..fe3a58032 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -41,7 +41,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Verify release-note changeset env: @@ -64,7 +64,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Set up Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -117,7 +117,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Install dependencies run: bun install --frozen-lockfile @@ -142,7 +142,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Set up Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -247,7 +247,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Install dependencies run: bun install --frozen-lockfile diff --git a/.github/workflows/release-prebuilt-npm.yml b/.github/workflows/release-prebuilt-npm.yml index 3537b0787..665a8810e 100644 --- a/.github/workflows/release-prebuilt-npm.yml +++ b/.github/workflows/release-prebuilt-npm.yml @@ -48,7 +48,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Resolve npm dist-tag and GitHub latest status id: resolve @@ -87,7 +87,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Install dependencies run: bun install --frozen-lockfile @@ -155,7 +155,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Install dependencies run: bun install --frozen-lockfile @@ -195,7 +195,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Set up Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -255,7 +255,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Set up Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -302,7 +302,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Download platform artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 7e00075ae..a5a85a6cb 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -24,7 +24,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: 1.4.2 + bun-version: 1.4.4 - name: Install repository dependencies run: bun install --frozen-lockfile diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c943c3794..c77c39ae4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,7 +71,7 @@ Keep the change focused on one user problem. Before adding another helper, state Requirements: -- Bun 1.4.2+ +- Bun 1.4.4+ - Node.js 22+ for npm package verification and release tasks - Git - macOS, Linux, or Windows diff --git a/bun.lock b/bun.lock index a9b397601..968a689e4 100644 --- a/bun.lock +++ b/bun.lock @@ -19,10 +19,10 @@ "@opentui/react": "^0.5.6", "@pierre/diffs": "1.3.5", "@shikijs/themes": "3.23.0", - "@types/bun": "1.4.1", + "@types/bun": "1.4.3", "@types/react": "^19.2.14", "@types/ws": "^8.18.1", - "bun": "^1.4.2", + "bun": "^1.4.4", "dependency-cruiser": "18.2.0", "diff": "^8.0.3", "ghostty-opentui": "^1.5.0", @@ -48,7 +48,7 @@ }, "dependencies": { "acorn": "^8.19.0", - "bun": "^1.4.2", + "bun": "^1.4.4", "chokidar": "^4.0.3", "commander": "^14.0.3", "diff": "^8.0.3", @@ -64,7 +64,7 @@ "@opentui/react": "^0.5.6", "@pierre/diffs": "1.3.5", "@shikijs/themes": "3.23.0", - "@types/bun": "1.4.1", + "@types/bun": "1.4.3", "@types/eslint-scope": "^8.4.0", "@types/react": "^19.2.14", "react": "^19.2.4", @@ -212,29 +212,29 @@ "@opentui/react": ["@opentui/react@0.5.6", "", { "dependencies": { "@opentui/core": "0.5.6", "react-reconciler": "^0.33.0" }, "peerDependencies": { "react": ">=19.2.0", "react-devtools-core": "^7.0.1", "ws": "^8.18.0" } }, "sha512-dMQTlcD1zlDTQC8r9rZKlHD7CORkY2M5D7tqhgoccoOK9p6q05FqrdRCTYHu1+VZYafqIV0k2IpMY2e34sngiQ=="], - "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.4.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MXdZkP1featqxZ+/VTXWG1BVjM4OGBehVY2Q88EeUj/7L0UMeCGItmyPYTN+wxvlGJ6F66JEtzsw+GvQWewnag=="], + "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.4.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zW/hxwc8sJAILGwB5eTC4i48ouGXf0V4lrxmEOk3JIE6JkYVW1TZh1E2pIempqsezgW1sckTIIDudFON8bJ2Mw=="], - "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.4.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-gZTxZuLjkUhAWjTETu3tw0WhsEdNkJ64daj60ybhPf835a2yollV3yTkK9JozvzKPx4TRFzLSl8C+U525pxVbw=="], + "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.4.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-oKGcxLsvh4oHjAo28nPm8c/xN0o/OmGyj1IHYc94vprkWsW2hNg2keEZt5f40cI5Hvu4jDlSRTR0UNpv1UIAxw=="], - "@oven/bun-freebsd-aarch64": ["@oven/bun-freebsd-aarch64@1.4.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-SMNItMw1Z8QeeQVKnw8jA7xQNkeXdP+OPgin4Wi/QTx/B8RHHLnuZfqmFy7NtVeT2NF0kKYppW4WWd2CCYZjhQ=="], + "@oven/bun-freebsd-aarch64": ["@oven/bun-freebsd-aarch64@1.4.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-klvA9FlwLxZj3JYIOPQA4bbnHIC3uas8kBE4GaGGkeVgAQP/k8hAzYkOqkSaqHBDbxLrTyFEQArEAevJq6NJzg=="], - "@oven/bun-freebsd-x64": ["@oven/bun-freebsd-x64@1.4.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-THbPKXhO54N0DpFRKZNDZpQ7dpbX0bWASuARckAUS9wRtFIHsiY+uULXJvxJGo2YD1YewvXQ4G8Fj7XT5oBCiw=="], + "@oven/bun-freebsd-x64": ["@oven/bun-freebsd-x64@1.4.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-sF1xS/scbyN94P2d0eTiarO6JHxrIvhZNT9hnWjQXW5nwKRtfuP+UYnIMNyvW1BQPcZFP/cvfLBNUIe1ea4zPA=="], - "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.4.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-3BBP9ovJ2RGHFH6Ae1CAtxNtG1+YY6GD6rmYbsUosoAk9+OEl6zeDQ/k4fBkc6dYOJCtWnx8hUxzNzQATSmvYQ=="], + "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.4.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZTVtpf9zmHSBVPX1sBwe5agGb/rN34yPGg/Hby8e2Ax2uCUA2URZKlZH7qLntK7qaFJCMXh9v0GWNYN2zINz1w=="], - "@oven/bun-linux-aarch64-android": ["@oven/bun-linux-aarch64-android@1.4.2", "", { "os": "android", "cpu": "arm64" }, "sha512-3mZKO2rhsNgbAUtAHC1UKUlF2zTxFraDZT/Elv8wzyH0fJL9h+Iv3TgB9lO63w89PRn3eFe+NRA1bhVgikKNPQ=="], + "@oven/bun-linux-aarch64-android": ["@oven/bun-linux-aarch64-android@1.4.4", "", { "os": "android", "cpu": "arm64" }, "sha512-5lvn3at5vRC0GxvmiqEiV2/e23tnRi0q5pzR40yfDvDA3OW7PSkt4chF1nA9vnAJhro4/l+ol1FmNP6D3cf/oA=="], - "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.4.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-+Sm6y+lSiSFBOtXmnekp5Q6n1tUKlyv71FCPWBc61Cgb14T5eBs8SN/nh4MUCOKzONkI3O+as3MGUgikS4aCBQ=="], + "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.4.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-QFd0P+mHJxbZFbUla4MgiE5OwxysHaUIYxL2pPNkpWtaI7v4ShOpr8OhBmsuyB0xn5rv/ZJnPsj2y8+bB9+g/A=="], - "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.4.2", "", { "os": "linux", "cpu": "x64" }, "sha512-9/E/UXOTpSo3YsV5g+FhtTd/qTpiWoKuxS12cqtuYA1ssu9fRAoPQnipFgGyck3tWO63iUdxBiygq+kELFawng=="], + "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.4.4", "", { "os": "linux", "cpu": "x64" }, "sha512-CHbbkmHVM28T1GFq+6GL4Ao4ujwwYwj1CxFFwEFzLanPfNsId9AhbIP0Q4reWuWBkGimUkoQygrK7Rgjtj3vog=="], - "@oven/bun-linux-x64-android": ["@oven/bun-linux-x64-android@1.4.2", "", { "os": "android", "cpu": "x64" }, "sha512-6HC5tzcC79113n2IHCTJMWv+HsQImv4ZFEK2XpYLxY6HbT8tM4cUM2Zv1bHZBQsS3jv/zYBamDJ1UX7If0d5tw=="], + "@oven/bun-linux-x64-android": ["@oven/bun-linux-x64-android@1.4.4", "", { "os": "android", "cpu": "x64" }, "sha512-3dMicj9C1DhaIfDkwt/9GtpU0W+q+GazbG9OIlEyL3Q1BBEz2e6ZRz/JiO8D/ECC0caXAqvnpFudehcLlgIwWQ=="], - "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.4.2", "", { "os": "linux", "cpu": "x64" }, "sha512-vVTKUg1bnPhRP/Hp73jIVoFh2vPFNYEqYX0ERKfZBOQEEHitNAeukZzzuUDZS0SoDCIpuWUGSpd/CDMbjdR+Uw=="], + "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.4.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lEEjNOJJrbGZu2NVcPI4uXdILcpspezWkA8R/Jr0RCGhM6zQd0Y7JBIIKNAmbX59EeWpeEz7Q78iLbpcAUypUA=="], - "@oven/bun-windows-aarch64": ["@oven/bun-windows-aarch64@1.4.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-8EJ1ST7339WJE3poPW5nBgVW/lWf9HBz4W27ZUNhburKmcBLOByPyE6DP9fHD8FQGm5c+ilUN2hX1mrW0jxq9Q=="], + "@oven/bun-windows-aarch64": ["@oven/bun-windows-aarch64@1.4.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-n0ep5ZRPCZY1+dqFJpnnRUZB1wy4GYU6At8RF+pyzySCLjkN9R4rFh2PtBHAgF/lg1vOl3/CeER3pDM+p40ATA=="], - "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.4.2", "", { "os": "win32", "cpu": "x64" }, "sha512-+bN6OuVld/9diT/RLSXSW7JE6CvNE3gL9XsAEjULi1nUsXd6DNO6GuA9jNdNb3r8PdJFnYHr5aypNV1Oj3Rd9g=="], + "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.4.4", "", { "os": "win32", "cpu": "x64" }, "sha512-a3LNctBVO8PjePsFZfwDleF2JXYf2BfQ1u/PLRnBDLnC2Riml+4+LgJpKj4xRGhB92KweGyQI5PXaTkP9qypMQ=="], "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.142.0", "", { "os": "android", "cpu": "arm" }, "sha512-ZiRGDutGsv1G6bL/ozy/koC0Sv39T1DqyoC4KD1DOy9ZoACm1O5UWhEK2c02Qdk+4lfLVkvFa/mQ0fm/4h1BtQ=="], @@ -422,7 +422,7 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], - "@types/bun": ["@types/bun@1.4.1", "", { "dependencies": { "bun-types": "1.4.1" } }, "sha512-0AVGiTXGajf1rgKom3N+c5L7CBxuoyyv1i44M0nX4UDK0G/fnRAMiri93nHuVPIb429KKtAgj7HatVmmOjeQLA=="], + "@types/bun": ["@types/bun@1.4.3", "", { "dependencies": { "bun-types": "1.4.3" } }, "sha512-N67Pqy8JXbAY/j0tNraS3V5mubGvc6rrrNDmDCtZLlFzccbUJPDb0tvgUBC5q4m8ybkP93Jcn1QB+RYQ6PvBhw=="], "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], @@ -464,11 +464,11 @@ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "bun": ["bun@1.4.2", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.4.2", "@oven/bun-darwin-x64": "1.4.2", "@oven/bun-freebsd-aarch64": "1.4.2", "@oven/bun-freebsd-x64": "1.4.2", "@oven/bun-linux-aarch64": "1.4.2", "@oven/bun-linux-aarch64-android": "1.4.2", "@oven/bun-linux-aarch64-musl": "1.4.2", "@oven/bun-linux-x64": "1.4.2", "@oven/bun-linux-x64-android": "1.4.2", "@oven/bun-linux-x64-musl": "1.4.2", "@oven/bun-windows-aarch64": "1.4.2", "@oven/bun-windows-x64": "1.4.2" }, "os": [ "!aix", "!sunos", "!openbsd", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-TrSXo6HJfIEaczpb3kjX82I2pL47vK1QUNmHRCUdz9IzaOwa9lzOXSWwu2l18YHE3sNfGRapVLd4nNm+22vVVA=="], + "bun": ["bun@1.4.4", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.4.4", "@oven/bun-darwin-x64": "1.4.4", "@oven/bun-freebsd-aarch64": "1.4.4", "@oven/bun-freebsd-x64": "1.4.4", "@oven/bun-linux-aarch64": "1.4.4", "@oven/bun-linux-aarch64-android": "1.4.4", "@oven/bun-linux-aarch64-musl": "1.4.4", "@oven/bun-linux-x64": "1.4.4", "@oven/bun-linux-x64-android": "1.4.4", "@oven/bun-linux-x64-musl": "1.4.4", "@oven/bun-windows-aarch64": "1.4.4", "@oven/bun-windows-x64": "1.4.4" }, "os": [ "!aix", "!sunos", "!openbsd", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-hj0ASdG6PNjLZGFAXktWb3Uaj/dcv/1wCsIttoEXADNIQBsGjTr+5uLKYbrDkuc7NT0Tkj4yPIqJI2jDxXdvVg=="], "bun-ffi-structs": ["bun-ffi-structs@0.3.1", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-3gM7PpVWLyrwxWjcilSiGuhWanhZivvo6l0u573NziPH6f/gwk6McbaYgn7oJWov6pKGRTDbrg94W5DcJsKTtQ=="], - "bun-types": ["bun-types@1.4.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-loKuVrAFZKfEv+JvWkHRS9GW5IqLuLRjVXN9p+vZvBN86O5hf/pBZQ5hSoyipsrMmWObZBDvWnlmKvjKTM0PdA=="], + "bun-types": ["bun-types@1.4.3", "", { "dependencies": { "@types/node": "*", "undici-types": "*" } }, "sha512-tinRJNGOmUN+tEAsn2y2e942n9JHRHR42iUYzDH75NmhJqVBokGSQkkfSFdNWwsANIdrK6ACH9t01M452YyTwg=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], diff --git a/flake.nix b/flake.nix index effe508a5..727f4d074 100644 --- a/flake.nix +++ b/flake.nix @@ -38,6 +38,9 @@ system: let pkgs = import nixpkgs { inherit system; + # TEMPORARY: eMudhra root-store workaround for the dev shell's bun (all + # systems) and node (macOS); self-retiring, see nix/emudhra-overlay.nix. + overlays = [(import ./nix/emudhra-overlay.nix)]; }; hunk = pkgs.callPackage ./nix/package.nix { bun2nix = bun2nix.packages.${system}.default; diff --git a/nix/bun.lock.nix b/nix/bun.lock.nix index 07b9ab861..c4688a971 100644 --- a/nix/bun.lock.nix +++ b/nix/bun.lock.nix @@ -147,53 +147,53 @@ url = "https://registry.npmjs.org/@opentui/react/-/react-0.5.6.tgz"; hash = "sha512-dMQTlcD1zlDTQC8r9rZKlHD7CORkY2M5D7tqhgoccoOK9p6q05FqrdRCTYHu1+VZYafqIV0k2IpMY2e34sngiQ=="; }; - "@oven/bun-darwin-aarch64@1.4.2" = fetchurl { - url = "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.4.2.tgz"; - hash = "sha512-MXdZkP1featqxZ+/VTXWG1BVjM4OGBehVY2Q88EeUj/7L0UMeCGItmyPYTN+wxvlGJ6F66JEtzsw+GvQWewnag=="; + "@oven/bun-darwin-aarch64@1.4.4" = fetchurl { + url = "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.4.4.tgz"; + hash = "sha512-zW/hxwc8sJAILGwB5eTC4i48ouGXf0V4lrxmEOk3JIE6JkYVW1TZh1E2pIempqsezgW1sckTIIDudFON8bJ2Mw=="; }; - "@oven/bun-darwin-x64@1.4.2" = fetchurl { - url = "https://registry.npmjs.org/@oven/bun-darwin-x64/-/bun-darwin-x64-1.4.2.tgz"; - hash = "sha512-gZTxZuLjkUhAWjTETu3tw0WhsEdNkJ64daj60ybhPf835a2yollV3yTkK9JozvzKPx4TRFzLSl8C+U525pxVbw=="; + "@oven/bun-darwin-x64@1.4.4" = fetchurl { + url = "https://registry.npmjs.org/@oven/bun-darwin-x64/-/bun-darwin-x64-1.4.4.tgz"; + hash = "sha512-oKGcxLsvh4oHjAo28nPm8c/xN0o/OmGyj1IHYc94vprkWsW2hNg2keEZt5f40cI5Hvu4jDlSRTR0UNpv1UIAxw=="; }; - "@oven/bun-freebsd-aarch64@1.4.2" = fetchurl { - url = "https://registry.npmjs.org/@oven/bun-freebsd-aarch64/-/bun-freebsd-aarch64-1.4.2.tgz"; - hash = "sha512-SMNItMw1Z8QeeQVKnw8jA7xQNkeXdP+OPgin4Wi/QTx/B8RHHLnuZfqmFy7NtVeT2NF0kKYppW4WWd2CCYZjhQ=="; + "@oven/bun-freebsd-aarch64@1.4.4" = fetchurl { + url = "https://registry.npmjs.org/@oven/bun-freebsd-aarch64/-/bun-freebsd-aarch64-1.4.4.tgz"; + hash = "sha512-klvA9FlwLxZj3JYIOPQA4bbnHIC3uas8kBE4GaGGkeVgAQP/k8hAzYkOqkSaqHBDbxLrTyFEQArEAevJq6NJzg=="; }; - "@oven/bun-freebsd-x64@1.4.2" = fetchurl { - url = "https://registry.npmjs.org/@oven/bun-freebsd-x64/-/bun-freebsd-x64-1.4.2.tgz"; - hash = "sha512-THbPKXhO54N0DpFRKZNDZpQ7dpbX0bWASuARckAUS9wRtFIHsiY+uULXJvxJGo2YD1YewvXQ4G8Fj7XT5oBCiw=="; + "@oven/bun-freebsd-x64@1.4.4" = fetchurl { + url = "https://registry.npmjs.org/@oven/bun-freebsd-x64/-/bun-freebsd-x64-1.4.4.tgz"; + hash = "sha512-sF1xS/scbyN94P2d0eTiarO6JHxrIvhZNT9hnWjQXW5nwKRtfuP+UYnIMNyvW1BQPcZFP/cvfLBNUIe1ea4zPA=="; }; - "@oven/bun-linux-aarch64-android@1.4.2" = fetchurl { - url = "https://registry.npmjs.org/@oven/bun-linux-aarch64-android/-/bun-linux-aarch64-android-1.4.2.tgz"; - hash = "sha512-3mZKO2rhsNgbAUtAHC1UKUlF2zTxFraDZT/Elv8wzyH0fJL9h+Iv3TgB9lO63w89PRn3eFe+NRA1bhVgikKNPQ=="; + "@oven/bun-linux-aarch64-android@1.4.4" = fetchurl { + url = "https://registry.npmjs.org/@oven/bun-linux-aarch64-android/-/bun-linux-aarch64-android-1.4.4.tgz"; + hash = "sha512-5lvn3at5vRC0GxvmiqEiV2/e23tnRi0q5pzR40yfDvDA3OW7PSkt4chF1nA9vnAJhro4/l+ol1FmNP6D3cf/oA=="; }; - "@oven/bun-linux-aarch64-musl@1.4.2" = fetchurl { - url = "https://registry.npmjs.org/@oven/bun-linux-aarch64-musl/-/bun-linux-aarch64-musl-1.4.2.tgz"; - hash = "sha512-+Sm6y+lSiSFBOtXmnekp5Q6n1tUKlyv71FCPWBc61Cgb14T5eBs8SN/nh4MUCOKzONkI3O+as3MGUgikS4aCBQ=="; + "@oven/bun-linux-aarch64-musl@1.4.4" = fetchurl { + url = "https://registry.npmjs.org/@oven/bun-linux-aarch64-musl/-/bun-linux-aarch64-musl-1.4.4.tgz"; + hash = "sha512-QFd0P+mHJxbZFbUla4MgiE5OwxysHaUIYxL2pPNkpWtaI7v4ShOpr8OhBmsuyB0xn5rv/ZJnPsj2y8+bB9+g/A=="; }; - "@oven/bun-linux-aarch64@1.4.2" = fetchurl { - url = "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.4.2.tgz"; - hash = "sha512-3BBP9ovJ2RGHFH6Ae1CAtxNtG1+YY6GD6rmYbsUosoAk9+OEl6zeDQ/k4fBkc6dYOJCtWnx8hUxzNzQATSmvYQ=="; + "@oven/bun-linux-aarch64@1.4.4" = fetchurl { + url = "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.4.4.tgz"; + hash = "sha512-ZTVtpf9zmHSBVPX1sBwe5agGb/rN34yPGg/Hby8e2Ax2uCUA2URZKlZH7qLntK7qaFJCMXh9v0GWNYN2zINz1w=="; }; - "@oven/bun-linux-x64-android@1.4.2" = fetchurl { - url = "https://registry.npmjs.org/@oven/bun-linux-x64-android/-/bun-linux-x64-android-1.4.2.tgz"; - hash = "sha512-6HC5tzcC79113n2IHCTJMWv+HsQImv4ZFEK2XpYLxY6HbT8tM4cUM2Zv1bHZBQsS3jv/zYBamDJ1UX7If0d5tw=="; + "@oven/bun-linux-x64-android@1.4.4" = fetchurl { + url = "https://registry.npmjs.org/@oven/bun-linux-x64-android/-/bun-linux-x64-android-1.4.4.tgz"; + hash = "sha512-3dMicj9C1DhaIfDkwt/9GtpU0W+q+GazbG9OIlEyL3Q1BBEz2e6ZRz/JiO8D/ECC0caXAqvnpFudehcLlgIwWQ=="; }; - "@oven/bun-linux-x64-musl@1.4.2" = fetchurl { - url = "https://registry.npmjs.org/@oven/bun-linux-x64-musl/-/bun-linux-x64-musl-1.4.2.tgz"; - hash = "sha512-vVTKUg1bnPhRP/Hp73jIVoFh2vPFNYEqYX0ERKfZBOQEEHitNAeukZzzuUDZS0SoDCIpuWUGSpd/CDMbjdR+Uw=="; + "@oven/bun-linux-x64-musl@1.4.4" = fetchurl { + url = "https://registry.npmjs.org/@oven/bun-linux-x64-musl/-/bun-linux-x64-musl-1.4.4.tgz"; + hash = "sha512-lEEjNOJJrbGZu2NVcPI4uXdILcpspezWkA8R/Jr0RCGhM6zQd0Y7JBIIKNAmbX59EeWpeEz7Q78iLbpcAUypUA=="; }; - "@oven/bun-linux-x64@1.4.2" = fetchurl { - url = "https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-1.4.2.tgz"; - hash = "sha512-9/E/UXOTpSo3YsV5g+FhtTd/qTpiWoKuxS12cqtuYA1ssu9fRAoPQnipFgGyck3tWO63iUdxBiygq+kELFawng=="; + "@oven/bun-linux-x64@1.4.4" = fetchurl { + url = "https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-1.4.4.tgz"; + hash = "sha512-CHbbkmHVM28T1GFq+6GL4Ao4ujwwYwj1CxFFwEFzLanPfNsId9AhbIP0Q4reWuWBkGimUkoQygrK7Rgjtj3vog=="; }; - "@oven/bun-windows-aarch64@1.4.2" = fetchurl { - url = "https://registry.npmjs.org/@oven/bun-windows-aarch64/-/bun-windows-aarch64-1.4.2.tgz"; - hash = "sha512-8EJ1ST7339WJE3poPW5nBgVW/lWf9HBz4W27ZUNhburKmcBLOByPyE6DP9fHD8FQGm5c+ilUN2hX1mrW0jxq9Q=="; + "@oven/bun-windows-aarch64@1.4.4" = fetchurl { + url = "https://registry.npmjs.org/@oven/bun-windows-aarch64/-/bun-windows-aarch64-1.4.4.tgz"; + hash = "sha512-n0ep5ZRPCZY1+dqFJpnnRUZB1wy4GYU6At8RF+pyzySCLjkN9R4rFh2PtBHAgF/lg1vOl3/CeER3pDM+p40ATA=="; }; - "@oven/bun-windows-x64@1.4.2" = fetchurl { - url = "https://registry.npmjs.org/@oven/bun-windows-x64/-/bun-windows-x64-1.4.2.tgz"; - hash = "sha512-+bN6OuVld/9diT/RLSXSW7JE6CvNE3gL9XsAEjULi1nUsXd6DNO6GuA9jNdNb3r8PdJFnYHr5aypNV1Oj3Rd9g=="; + "@oven/bun-windows-x64@1.4.4" = fetchurl { + url = "https://registry.npmjs.org/@oven/bun-windows-x64/-/bun-windows-x64-1.4.4.tgz"; + hash = "sha512-a3LNctBVO8PjePsFZfwDleF2JXYf2BfQ1u/PLRnBDLnC2Riml+4+LgJpKj4xRGhB92KweGyQI5PXaTkP9qypMQ=="; }; "@oxc-parser/binding-android-arm-eabi@0.142.0" = fetchurl { url = "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.142.0.tgz"; @@ -567,9 +567,9 @@ url = "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz"; hash = "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="; }; - "@types/bun@1.4.1" = fetchurl { - url = "https://registry.npmjs.org/@types/bun/-/bun-1.4.1.tgz"; - hash = "sha512-0AVGiTXGajf1rgKom3N+c5L7CBxuoyyv1i44M0nX4UDK0G/fnRAMiri93nHuVPIb429KKtAgj7HatVmmOjeQLA=="; + "@types/bun@1.4.3" = fetchurl { + url = "https://registry.npmjs.org/@types/bun/-/bun-1.4.3.tgz"; + hash = "sha512-N67Pqy8JXbAY/j0tNraS3V5mubGvc6rrrNDmDCtZLlFzccbUJPDb0tvgUBC5q4m8ybkP93Jcn1QB+RYQ6PvBhw=="; }; "@types/eslint-scope@8.4.0" = fetchurl { url = "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-8.4.0.tgz"; @@ -667,13 +667,13 @@ url = "https://registry.npmjs.org/bun-ffi-structs/-/bun-ffi-structs-0.3.1.tgz"; hash = "sha512-3gM7PpVWLyrwxWjcilSiGuhWanhZivvo6l0u573NziPH6f/gwk6McbaYgn7oJWov6pKGRTDbrg94W5DcJsKTtQ=="; }; - "bun-types@1.4.1" = fetchurl { - url = "https://registry.npmjs.org/bun-types/-/bun-types-1.4.1.tgz"; - hash = "sha512-loKuVrAFZKfEv+JvWkHRS9GW5IqLuLRjVXN9p+vZvBN86O5hf/pBZQ5hSoyipsrMmWObZBDvWnlmKvjKTM0PdA=="; + "bun-types@1.4.3" = fetchurl { + url = "https://registry.npmjs.org/bun-types/-/bun-types-1.4.3.tgz"; + hash = "sha512-tinRJNGOmUN+tEAsn2y2e942n9JHRHR42iUYzDH75NmhJqVBokGSQkkfSFdNWwsANIdrK6ACH9t01M452YyTwg=="; }; - "bun@1.4.2" = fetchurl { - url = "https://registry.npmjs.org/bun/-/bun-1.4.2.tgz"; - hash = "sha512-TrSXo6HJfIEaczpb3kjX82I2pL47vK1QUNmHRCUdz9IzaOwa9lzOXSWwu2l18YHE3sNfGRapVLd4nNm+22vVVA=="; + "bun@1.4.4" = fetchurl { + url = "https://registry.npmjs.org/bun/-/bun-1.4.4.tgz"; + hash = "sha512-hj0ASdG6PNjLZGFAXktWb3Uaj/dcv/1wCsIttoEXADNIQBsGjTr+5uLKYbrDkuc7NT0Tkj4yPIqJI2jDxXdvVg=="; }; "ccount@2.0.1" = fetchurl { url = "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz"; diff --git a/nix/emudhra-overlay.nix b/nix/emudhra-overlay.nix new file mode 100644 index 000000000..b6c2a0a7b --- /dev/null +++ b/nix/emudhra-overlay.nix @@ -0,0 +1,77 @@ +# Swaps the Nix dev shell's Node and Bun for the first releases that distrust the +# compromised eMudhra (emSign) intermediates. +# +# TEMPORARY eMudhra workaround. Node and Bun carry their own root stores, so +# system trust-store fixes (such as nix-darwin's +# `security.pki.caCertificateBlacklist`) do not reach them; the first upstream +# releases whose stores reject the compromised chain are +# node 24.22.1 (NSS 3.131.1 roots + blocked-SPKI check, 2026-10-20) +# bun 1.4.4 (roots updated to NSS 3.131.1, 2026-10-22) +# while the flake's nixpkgs pin still carries older builds. Bun is replaced on +# every supported system. Node is replaced on macOS only, where the prebuilt +# nodejs.org tarball in nodejs-bin.nix applies; Linux shells keep nixpkgs' node. +# +# Invariants: +# - Self-retiring: each replacement applies only while the pinned nixpkgs version +# is older than the fixed release, so `nix flake update` turns it off on its own. +# - The attribute *names* this overlay defines never depend on `prev`; the gates +# live in the values. Conditional names would recurse through the fixpoint. +# - Version gates read `prev.nodejs-slim_24`, not `prev.nodejs_24`: at this +# nixpkgs `nodejs_24` is a symlinkJoin whose `version` resolves through `final`, +# which would recurse back into this overlay. +# +# Remove when nixpkgs' `nodejs_24` is >= 24.22.1 and `bun` is >= 1.4.4: delete +# this file, `nix/nodejs-bin.nix`, and the `overlays` line in `flake.nix`. +# Check with: +# curl -s https://raw.githubusercontent.com/NixOS/nixpkgs/nixpkgs-unstable/pkgs/development/web/nodejs/v24.nix | grep version +# curl -s https://raw.githubusercontent.com/NixOS/nixpkgs/nixpkgs-unstable/pkgs/by-name/bu/bun/package.nix | grep version +final: prev: +let + inherit (prev.stdenv.hostPlatform) system isDarwin; + wantNode = prev.lib.versionOlder (prev.nodejs-slim_24 or prev.nodejs_24).version "24.22.1"; + wantBun = prev.lib.versionOlder prev.bun.version "1.4.4"; + + # Official nodejs.org tarball; see the header of nodejs-bin.nix. + nodejsBin = import ./nodejs-bin.nix { pkgs = prev; }; + + # nixpkgs' bun is already "unzip the official GitHub release asset", so only + # version and src move; the autoPatchelf (Linux) and install_name_tool/codesign + # (macOS) hooks and completions are reused as-is. Each system takes the same + # asset flavor nixpkgs selects for it. Hashes come from SHASUMS256.txt on the + # bun-v1.4.4 release. + bunVersion = "1.4.4"; + bunSrc = { + "aarch64-darwin" = prev.fetchurl { + url = "https://github.com/oven-sh/bun/releases/download/bun-v${bunVersion}/bun-darwin-aarch64.zip"; + hash = "sha256-wkDX8My5DCY6hJ/NsJt+5iYw2SE0jtaq/ahrnVd7xRA="; + }; + "x86_64-darwin" = prev.fetchurl { + url = "https://github.com/oven-sh/bun/releases/download/bun-v${bunVersion}/bun-darwin-x64-baseline.zip"; + hash = "sha256-1KFVrfv9CRVes7oFyeOlRPOSl9emhhbTx3WfZ6tcR9w="; + }; + "aarch64-linux" = prev.fetchurl { + url = "https://github.com/oven-sh/bun/releases/download/bun-v${bunVersion}/bun-linux-aarch64.zip"; + hash = "sha256-uqa7NvUdblhWHgK76oJQOKnfRU9nPW13bou/hvjHhQ4="; + }; + "x86_64-linux" = prev.fetchurl { + url = "https://github.com/oven-sh/bun/releases/download/bun-v${bunVersion}/bun-linux-x64.zip"; + hash = "sha256-v4zL+ysLgBWeD5ptFcVTF63YB3U028YTCj7bUvzLOVw="; + }; + }; + bunBin = prev.bun.overrideAttrs (_: { + version = bunVersion; + src = bunSrc.${system} or (throw "emudhra-overlay.nix: no bun ${bunVersion} hash for ${system}"); + }); + + # Gates are evaluated lazily inside each value, so a system that keeps its + # nixpkgs package never forces the replacement derivation. + pick = want: replacement: orig: if want then replacement else orig; + wantNodeHere = isDarwin && wantNode; +in +{ + nodejs = pick wantNodeHere nodejsBin prev.nodejs; + nodejs_24 = pick wantNodeHere nodejsBin prev.nodejs_24; + nodejs-slim = pick wantNodeHere nodejsBin prev.nodejs-slim; + nodejs-slim_24 = pick wantNodeHere nodejsBin prev.nodejs-slim_24; + bun = pick wantBun bunBin prev.bun; +} diff --git a/nix/nodejs-bin.nix b/nix/nodejs-bin.nix new file mode 100644 index 000000000..d5a54e441 --- /dev/null +++ b/nix/nodejs-bin.nix @@ -0,0 +1,78 @@ +# Packages the official prebuilt Node.js tarball from nodejs.org for macOS. +# +# TEMPORARY eMudhra workaround, darwin-only. Node ships its own root store, so the +# nix-darwin `security.pki.caCertificateBlacklist` fix does not reach it. Node +# 24.22.1 (2026-10-20) is the first release whose root store (NSS 3.131.1 plus the +# blocked-SPKI check) rejects the compromised eMudhra intermediates; the flake's +# nixpkgs pin still carries 24.14.1. `nix/emudhra-overlay.nix` substitutes this +# derivation for `nodejs`/`nodejs_24` only while that pin is older than 24.22.1, +# so the file retires itself once `nix flake update` catches up. +# +# Remove it once nixpkgs' `nodejs_24` is >= 24.22.1 (24.22.1 merged into nixpkgs +# `staging` on 2026-10-21, NixOS/nixpkgs#576777): delete this file, +# `nix/emudhra-overlay.nix`, and the `overlays` line in `flake.nix`. Check with: +# curl -s https://raw.githubusercontent.com/NixOS/nixpkgs/nixpkgs-unstable/pkgs/development/web/nodejs/v24.nix | grep version +# +# Bumping: change `version`, then copy the darwin sha256 lines from +# https://nodejs.org/dist/v/SHASUMS256.txt +{ pkgs }: +let + version = "24.22.1"; + inherit (pkgs.stdenv.hostPlatform) system; + arch = + { + "aarch64-darwin" = "arm64"; + "x86_64-darwin" = "x64"; + } + .${system} or (throw "nodejs-bin.nix: no prebuilt Node.js tarball mapping for ${system}"); + sha256 = + { + "aarch64-darwin" = "89c682593d76cd44233fa53b1563264e7de9cbb189cad169135c5b4fadfbc0a7"; + "x86_64-darwin" = "d132557e7ee2b85af08b0c8c34338e65c6430f2238d01f727728ef42dc83b035"; + } + .${system}; +in +pkgs.stdenvNoCC.mkDerivation { + pname = "nodejs-bin"; + inherit version; + + src = pkgs.fetchurl { + url = "https://nodejs.org/dist/v${version}/node-v${version}-darwin-${arch}.tar.gz"; + inherit sha256; + }; + + dontConfigure = true; + dontBuild = true; + # Prebuilt Mach-O binaries; nothing to strip or fix up. + dontStrip = true; + dontFixup = true; + + installPhase = '' + runHook preInstall + mkdir -p $out + cp -R bin include lib share $out/ + # npm/npx/corepack ship with `#!/usr/bin/env node`; pin them to this node so + # they work regardless of what else is on PATH. + export PATH=$out/bin:$PATH + patchShebangs $out/lib/node_modules/npm/bin $out/lib/node_modules/corepack/dist + runHook postInstall + ''; + + passthru = { + # Mirror the attributes nixpkgs' nodejs exposes that callers commonly read. + inherit version; + python = pkgs.python3; + }; + + meta = with pkgs.lib; { + description = "Node.js ${version} (official prebuilt binary)"; + homepage = "https://nodejs.org"; + license = licenses.mit; + sourceProvenance = [ sourceTypes.binaryNativeCode ]; + platforms = [ + "aarch64-darwin" + "x86_64-darwin" + ]; + mainProgram = "node"; + }; +} diff --git a/nix/package.nix b/nix/package.nix index a61f87d97..627fadc9a 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -17,19 +17,19 @@ bunCompilerArchives = { "aarch64-darwin" = fetchurl { url = "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-${bunVersion}.tgz"; - hash = "sha512-MXdZkP1featqxZ+/VTXWG1BVjM4OGBehVY2Q88EeUj/7L0UMeCGItmyPYTN+wxvlGJ6F66JEtzsw+GvQWewnag=="; + hash = "sha512-zW/hxwc8sJAILGwB5eTC4i48ouGXf0V4lrxmEOk3JIE6JkYVW1TZh1E2pIempqsezgW1sckTIIDudFON8bJ2Mw=="; }; "x86_64-darwin" = fetchurl { url = "https://registry.npmjs.org/@oven/bun-darwin-x64-baseline/-/bun-darwin-x64-baseline-${bunVersion}.tgz"; - hash = "sha512-lVDbk/HkN+RP8QKXBe0aou7EifA2PC9+Z4cvy2VDpmR1ONdDm8e2h/HsU7ABq7mvVs/76iq5H86nMvAhwJDadQ=="; + hash = "sha512-uzSXckTnNNS7Un1ihiM31R7P5iKndZ65t6Zj3iI72SrfoV2uI97MKM8z1qer2oRHyqXHLUIJTIhj+qT5UeEjrw=="; }; "aarch64-linux" = fetchurl { url = "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-${bunVersion}.tgz"; - hash = "sha512-3BBP9ovJ2RGHFH6Ae1CAtxNtG1+YY6GD6rmYbsUosoAk9+OEl6zeDQ/k4fBkc6dYOJCtWnx8hUxzNzQATSmvYQ=="; + hash = "sha512-ZTVtpf9zmHSBVPX1sBwe5agGb/rN34yPGg/Hby8e2Ax2uCUA2URZKlZH7qLntK7qaFJCMXh9v0GWNYN2zINz1w=="; }; "x86_64-linux" = fetchurl { url = "https://registry.npmjs.org/@oven/bun-linux-x64-baseline/-/bun-linux-x64-baseline-${bunVersion}.tgz"; - hash = "sha512-OKKzX/SOVIUVARCzTg+2r4Rcc4fPyWnQmBsd3K08buxeu7DAof4iwW9zSb6Lt1NhTCu4Uhi8k9UeI6f7GEE4Zg=="; + hash = "sha512-FBU6T21NT+i+rESbi0Z8A8G5xN51c+WZ0v6pIjI6szSzOlqVNv64nf7FPnwNIyxKD1M7Wg5cEobLC/QbzCeLtA=="; }; }; bunCompilerArchive = bunCompilerArchives.${stdenv.hostPlatform.system}; diff --git a/package.json b/package.json index 75d787c63..9fddb8438 100644 --- a/package.json +++ b/package.json @@ -94,10 +94,10 @@ "@opentui/react": "^0.5.6", "@pierre/diffs": "1.3.5", "@shikijs/themes": "3.23.0", - "@types/bun": "1.4.1", + "@types/bun": "1.4.3", "@types/react": "^19.2.14", "@types/ws": "^8.18.1", - "bun": "^1.4.2", + "bun": "^1.4.4", "dependency-cruiser": "18.2.0", "diff": "^8.0.3", "ghostty-opentui": "^1.5.0", @@ -122,7 +122,7 @@ "engines": { "node": ">=22" }, - "packageManager": "bun@1.4.2", + "packageManager": "bun@1.4.4", "pi": { "skills": [ "./skills", diff --git a/packages/hunk/package.json b/packages/hunk/package.json index b56554bfc..b24033fc3 100644 --- a/packages/hunk/package.json +++ b/packages/hunk/package.json @@ -51,7 +51,7 @@ }, "dependencies": { "acorn": "^8.19.0", - "bun": "^1.4.2", + "bun": "^1.4.4", "chokidar": "^4.0.3", "commander": "^14.0.3", "diff": "^8.0.3", @@ -67,7 +67,7 @@ "@opentui/react": "^0.5.6", "@pierre/diffs": "1.3.5", "@shikijs/themes": "3.23.0", - "@types/bun": "1.4.1", + "@types/bun": "1.4.3", "@types/eslint-scope": "^8.4.0", "@types/react": "^19.2.14", "react": "^19.2.4", @@ -87,7 +87,7 @@ "engines": { "node": ">=22" }, - "packageManager": "bun@1.4.2", + "packageManager": "bun@1.4.4", "pi": { "skills": [ "./skills" diff --git a/packages/hunk/src/core/process/terminal.test.ts b/packages/hunk/src/core/process/terminal.test.ts index d98aca1fd..5cd800d00 100644 --- a/packages/hunk/src/core/process/terminal.test.ts +++ b/packages/hunk/src/core/process/terminal.test.ts @@ -1,8 +1,10 @@ import { describe, expect, test } from "bun:test"; import type { CliInput } from "../run/commandInputs"; import { + type ControllingTerminalDeps, installTerminalDisconnectSupport, openControllingTerminal, + resolveControllingTerminalDevice, resolveRuntimeCliInput, shouldUseMouseForApp, shouldUsePagerMode, @@ -102,47 +104,151 @@ describe("app mouse support", () => { }); }); -describe("controlling terminal attachment", () => { - test("opens /dev/tty for read and closes the input stream", () => { - const calls: Array<[string, string]> = []; - let stdinDestroyed = false; +/** Build controlling-terminal deps around an in-memory device so no real tty is touched. */ +function createTestControllingTerminalDeps( + overrides: Partial & { + /** Chunks the fake device delivers, in order; the stream stays open after the last one. */ + chunks?: Uint8Array[]; + } = {}, +) { + const log: string[] = []; + const rawModeCalls: boolean[] = []; + const { chunks = [], ...depOverrides } = overrides; + const pending = chunks.map((chunk) => chunk); + let cancelled = false; + let release: (() => void) | undefined; + const source = new ReadableStream({ + pull(controller) { + const next = pending.shift(); + if (next) { + controller.enqueue(next); + return; + } + // Emulate a terminal with nothing typed yet: the read stays pending until cancel. + return new Promise((resolve) => { + release = resolve; + }); + }, + cancel() { + cancelled = true; + release?.(); + }, + }); + const deps: ControllingTerminalDeps = { + candidateFds: [1], + isatty: () => true, + readlinkSync: () => { + throw new Error("no procfs"); + }, + fstatSync: () => ({ rdev: 42 }), + readdirSync: () => ["null", "tty", "ttys001", "ttys007"], + statSync: (path) => ({ + rdev: path === "/dev/ttys007" ? 42 : 7, + isCharacterDevice: () => true, + }), + openSync: (path, flags) => { + log.push(`open ${String(path)} ${String(flags)}`); + return 11; + }, + closeSync: (fd) => { + log.push(`close ${fd}`); + }, + createRawModeStream: () => { + const handle = { + isRaw: false, + setRawMode(flag: boolean) { + rawModeCalls.push(flag); + handle.isRaw = flag; + return handle as never; + }, + }; + return handle; + }, + openDeviceStream: (devicePath) => { + log.push(`stream ${devicePath}`); + return source; + }, + ...depOverrides, + }; + return { + deps, + log, + rawModeCalls, + get cancelled() { + return cancelled; + }, + }; +} - const stdin = { - destroy() { - stdinDestroyed = true; +describe("controlling terminal device resolution", () => { + test("prefers the procfs link for a terminal descriptor", () => { + const { deps } = createTestControllingTerminalDeps({ + readlinkSync: (path) => { + expect(path).toBe("/proc/self/fd/1"); + return "/dev/pts/4"; }, - } as never; + }); + expect(resolveControllingTerminalDevice(deps)).toBe("/dev/pts/4"); + }); - const controllingTerminal = openControllingTerminal({ - openSync(path, flags) { - calls.push([String(path), String(flags)]); - return 11; - }, - createReadStream(fd) { - expect(fd).toBe(11); - return stdin; - }, + test("matches the device by rdev when procfs is unavailable, skipping the /dev/tty alias", () => { + const { deps } = createTestControllingTerminalDeps(); + expect(resolveControllingTerminalDevice(deps)).toBe("/dev/ttys007"); + }); + + test("skips descriptors that are not terminals and reports null without a match", () => { + const { deps } = createTestControllingTerminalDeps({ + candidateFds: [1, 2], + isatty: (fd) => fd === 2, + fstatSync: () => ({ rdev: 99 }), }); + expect(resolveControllingTerminalDevice(deps)).toBeNull(); + }); +}); +describe("controlling terminal attachment", () => { + test("opens the resolved device for raw mode and streams it without a blocking read", async () => { + const harness = createTestControllingTerminalDeps({ + chunks: [new TextEncoder().encode("q")], + }); + const controllingTerminal = openControllingTerminal(harness.deps); expect(controllingTerminal).not.toBeNull(); - expect(calls).toEqual([["/dev/tty", "r"]]); - expect(controllingTerminal?.stdin).toBe(stdin); + expect(harness.log).toEqual(["open /dev/ttys007 r", "stream /dev/ttys007"]); + + const stdin = controllingTerminal!.stdin; + expect(stdin.isTTY).toBe(true); + stdin.setRawMode(true); + expect(stdin.isRaw).toBe(true); + expect(harness.rawModeCalls).toEqual([true]); + + const first = await new Promise((resolve) => stdin.once("data", resolve)); + expect(first.toString()).toBe("q"); - controllingTerminal?.close(); - expect(stdinDestroyed).toBe(true); + // Teardown lands on a later tick while the next read is already pending, which is the + // shape that used to strand the process behind a blocking read. + await new Promise((resolve) => setTimeout(resolve, 0)); + const closed = new Promise((resolve) => stdin.once("close", () => resolve())); + controllingTerminal!.close(); + await closed; + expect(harness.cancelled).toBe(true); + expect(harness.log.at(-1)).toBe("close 11"); + expect(stdin.destroyed).toBe(true); }); - test("returns null when the controlling terminal cannot be opened", () => { - const controllingTerminal = openControllingTerminal({ - openSync() { - throw new Error("no tty"); - }, - createReadStream() { - throw new Error("unreachable"); + test("returns null when no terminal device resolves", () => { + const { deps, log } = createTestControllingTerminalDeps({ isatty: () => false }); + expect(openControllingTerminal(deps)).toBeNull(); + expect(log).toEqual([]); + }); + + test("returns null and releases the descriptor when the device stream cannot open", () => { + const { deps, log } = createTestControllingTerminalDeps({ + openDeviceStream: () => { + throw new Error("EINVAL"); }, }); - - expect(controllingTerminal).toBeNull(); + expect(openControllingTerminal(deps)).toBeNull(); + expect(log).toEqual(["open /dev/ttys007 r", "close 11"]); }); }); diff --git a/packages/hunk/src/core/process/terminal.ts b/packages/hunk/src/core/process/terminal.ts index c7bdac778..29d49b45d 100644 --- a/packages/hunk/src/core/process/terminal.ts +++ b/packages/hunk/src/core/process/terminal.ts @@ -1,4 +1,5 @@ import fs from "node:fs"; +import { Readable } from "node:stream"; import tty from "node:tty"; import type { CliInput } from "../run/commandInputs"; @@ -40,7 +41,7 @@ export function shouldUseMouseForApp({ } export interface ControllingTerminal { - stdin: tty.ReadStream; + stdin: ControllingTerminalInput; close: () => void; } @@ -97,25 +98,204 @@ export function installTerminalDisconnectSupport( }; } -/** Minimal terminal construction hooks so tests can cover `/dev/tty` attach behavior. */ +/** Terminal construction hooks so tests can cover device resolution and attach behavior. */ export interface ControllingTerminalDeps { + /** Descriptors that may be attached to the terminal, checked in order. */ + candidateFds: number[]; + isatty: typeof tty.isatty; + readlinkSync: (path: string) => string; + fstatSync: (fd: number) => { rdev: number }; + readdirSync: (path: string) => string[]; + statSync: (path: string) => { rdev: number; isCharacterDevice(): boolean }; openSync: typeof fs.openSync; - createReadStream: (fd: number) => tty.ReadStream; + closeSync: typeof fs.closeSync; + /** Builds the dormant raw-mode handle for an open device descriptor. */ + createRawModeStream: (fd: number) => Pick; + /** Opens the poll-driven byte source for a terminal device path. */ + openDeviceStream: (devicePath: string) => ReadableStream; +} + +const defaultControllingTerminalDeps: ControllingTerminalDeps = { + candidateFds: [1, 2], + isatty: tty.isatty, + readlinkSync: (path) => fs.readlinkSync(path), + fstatSync: (fd) => fs.fstatSync(fd), + readdirSync: (path) => fs.readdirSync(path), + statSync: (path) => fs.statSync(path), + openSync: fs.openSync, + closeSync: fs.closeSync, + createRawModeStream: (fd) => new tty.ReadStream(fd), + openDeviceStream: (devicePath) => Bun.file(devicePath).stream(), +}; + +/** + * Resolve the real device node (`/dev/ttys003`, `/dev/pts/4`) behind the terminal that + * `candidateFds` write to. The `/dev/tty` alias is not enough: macOS kqueue refuses to poll it + * with EINVAL, so the non-blocking reader below needs the concrete node. Linux answers through + * `/proc/self/fd`; elsewhere the device is matched by `rdev` among the `/dev` terminal entries. + */ +export function resolveControllingTerminalDevice( + deps: ControllingTerminalDeps = defaultControllingTerminalDeps, +): string | null { + for (const fd of deps.candidateFds) { + if (!deps.isatty(fd)) continue; + try { + const target = deps.readlinkSync(`/proc/self/fd/${fd}`); + if (target.startsWith("/dev/")) return target; + } catch { + // No procfs; fall through to the device scan. + } + let rdev: number; + try { + rdev = deps.fstatSync(fd).rdev; + } catch { + continue; + } + let entries: string[]; + try { + entries = deps.readdirSync("/dev"); + } catch { + continue; + } + for (const name of entries) { + // `tty` itself is the alias we must avoid; the per-session nodes carry a suffix. + if (name === "tty" || !(name.startsWith("tty") || name.startsWith("pts"))) continue; + const devicePath = `/dev/${name}`; + try { + const stat = deps.statSync(devicePath); + if (stat.isCharacterDevice() && stat.rdev === rdev) return devicePath; + } catch { + // Entries can disappear mid-scan; keep looking. + } + } + } + return null; +} + +/** + * Streams keyboard input from a terminal device without parking a blocking read on Bun's thread + * pool. + * + * Bun implements `tty.ReadStream(fd)` as `fs.ReadStream`, so each read is a blocking `read(2)` on a + * worker thread and `destroy()` waits for that read to return, which only happens when the + * terminal delivers more input. Bun >= 1.4.3 issues the next read before the app's asynchronous + * teardown runs, so `git diff | hunk pager` would stay alive after `q` until the user pressed + * Enter. Only `process.stdin` gets Bun's poll-driven native reader; this class rebuilds that path + * for an arbitrary device: bytes come from `Bun.file(devicePath).stream()`, whose pending read is + * poll-driven and whose `cancel()` releases the event loop, while raw mode goes through a dormant + * `tty.ReadStream` on a second descriptor because termios state belongs to the device, not to a + * descriptor. + * + * Invariants: + * - At most one native read is in flight; `_read` is a no-op while one is pending. + * - `_destroy` cancels the native reader before closing the raw-mode descriptor, so an in-flight + * read settles as done instead of holding the process open. + * - The raw-mode `tty.ReadStream` stays dormant: Bun's `fs.ReadStream` only issues `read(2)` from + * `_read`, which runs once something calls `read()`/`resume()`/`pipe()` or attaches a + * `data`/`readable` listener. Never do any of those on it; a flowing handle would steal + * keystrokes from the device stream and park the blocking read this class exists to avoid. + * - That handle owns no native resource or finalizer, so `_destroy` closes its descriptor with + * `closeSync` instead of destroying the stream. Callers leave raw mode before destroying + * (OpenTUI does so before `onDestroy`); after `_destroy` the descriptor number may belong to an + * unrelated file, so the handle must not be touched again. + */ +export class ControllingTerminalInput extends Readable { + readonly isTTY = true; + isRaw = false; + readonly fd: number; + readonly #rawMode: Pick; + readonly #source: ReadableStream; + readonly #closeSync: typeof fs.closeSync; + #reader: ReadableStreamDefaultReader | undefined; + #reading = false; + + constructor(devicePath: string, deps: ControllingTerminalDeps = defaultControllingTerminalDeps) { + super(); + this.fd = deps.openSync(devicePath, "r"); + this.#closeSync = deps.closeSync; + try { + this.#rawMode = deps.createRawModeStream(this.fd); + this.#source = deps.openDeviceStream(devicePath); + } catch (error) { + deps.closeSync(this.fd); + throw error; + } + } + + /** Switch the terminal between raw and cooked mode, mirroring `tty.ReadStream#setRawMode`. */ + setRawMode(flag: boolean): this { + this.#rawMode.setRawMode(flag); + this.isRaw = this.#rawMode.isRaw; + return this; + } + + /** + * Accept `ref()` for `tty.ReadStream` shape compatibility only; it does not change event-loop + * liveness. Once the first read is issued, the device stream keeps the loop alive until + * `destroy()` cancels the reader; completed reads, `pause()`, and `unref()` do not release it. + * That keeps the pager alive while idle (OpenTUI's keep-alive timer only runs for + * `process.stdin`), so callers must `destroy()` through `ControllingTerminal.close` to exit. + */ + ref(): this { + return this; + } + + /** Accept `unref()` as a no-op; see `ref()`. Only `destroy()` releases the loop. */ + unref(): this { + return this; + } + + override _read(): void { + if (this.#reading) return; + const reader = (this.#reader ??= this.#source.getReader()); + this.#reading = true; + reader.read().then( + ({ value, done }) => { + this.#reading = false; + // A cancelled reader settles as done after destroy; the stream already finished. + if (reader !== this.#reader) return; + if (done || value === undefined) { + this.push(null); + return; + } + this.push(Buffer.from(value.buffer, value.byteOffset, value.byteLength)); + }, + (error: unknown) => { + this.#reading = false; + if (reader === this.#reader) + this.destroy(error instanceof Error ? error : new Error(String(error))); + }, + ); + } + + override _destroy(error: Error | null, callback: (error?: Error | null) => void): void { + const reader = this.#reader; + this.#reader = undefined; + const release = () => { + try { + this.#closeSync(this.fd); + } catch { + // The descriptor is already gone; nothing else holds the device. + } + callback(error); + }; + const cancelled = reader ? reader.cancel() : this.#source.cancel(); + cancelled.then(release, release); + } } /** * Open the controlling terminal for input so the UI can stay interactive while stdin carries patch - * data. Rendering can continue through the existing stdout stream. + * data. Rendering continues through the existing stdout stream. Returns null when no terminal + * device can be resolved or opened, which callers treat as "no interactive terminal". */ export function openControllingTerminal( - deps: ControllingTerminalDeps = { - openSync: fs.openSync, - createReadStream: (fd) => new tty.ReadStream(fd), - }, + deps: ControllingTerminalDeps = defaultControllingTerminalDeps, ): ControllingTerminal | null { try { - const stdinFd = deps.openSync("/dev/tty", "r"); - const stdin = deps.createReadStream(stdinFd); + const devicePath = resolveControllingTerminalDevice(deps); + if (!devicePath) return null; + const stdin = new ControllingTerminalInput(devicePath, deps); return { stdin, diff --git a/packages/hunk/src/ui/runInteractiveApp.tsx b/packages/hunk/src/ui/runInteractiveApp.tsx index 730e714d2..096ac76ae 100644 --- a/packages/hunk/src/ui/runInteractiveApp.tsx +++ b/packages/hunk/src/ui/runInteractiveApp.tsx @@ -7,7 +7,7 @@ import type { InteractiveSessionInitialization } from "../core/session/initializ import { createExtensionSession } from "../extensions/session"; import type { ExtensionLoadResult } from "../extensions/types"; import { HunkSessionHost, type StandaloneReviewSurfaceRoute } from "./session/HunkSessionHost"; -import { runHunkSession } from "./session/runHunkSession"; +import { runHunkSession, type RendererInput } from "./session/runHunkSession"; export interface InteractiveAppInput { bootstrap: AppBootstrap; @@ -33,7 +33,8 @@ export async function runInteractiveApp( ): Promise { const createReviewRuntime = deps.createReviewRuntime ?? createReviewSessionRuntime; const runSession = deps.runSession ?? runHunkSession; - const rendererStdin = controllingTerminal?.stdin ?? process.stdin; + // Checked structurally so a controlling-terminal input missing a renderer member fails to compile. + const rendererStdin: RendererInput = controllingTerminal?.stdin ?? process.stdin; let terminalClosed = false; const closeTerminal = () => { if (terminalClosed) return; diff --git a/packages/hunk/src/ui/session/runHunkSession.tsx b/packages/hunk/src/ui/session/runHunkSession.tsx index c5529691e..ac4c6b3f3 100644 --- a/packages/hunk/src/ui/session/runHunkSession.tsx +++ b/packages/hunk/src/ui/session/runHunkSession.tsx @@ -23,8 +23,24 @@ export interface HunkSessionRenderContext { finish(exitCode?: number): void; } +/** + * Describes the stdin surface the renderer and disconnect watcher rely on, so both `process.stdin` + * and the controlling-terminal input are checked structurally instead of cast. + * + * OpenTUI 0.5.x reads `isTTY`, `setRawMode`, `on`/`removeListener("data")`, `pause`/`resume`, + * `read`, and `readableLength`; the disconnect watcher adds its `TerminalInputEvents`. OpenTUI + * declares the whole socket-backed `NodeJS.ReadStream`, so this type cannot catch it reading a new + * member: re-audit `CliRenderer`'s stdin use when bumping `@opentui/core`. + */ +export type RendererInput = NodeJS.ReadableStream & + TerminalInputEvents & { + readonly isTTY: boolean; + readonly readableLength: number; + setRawMode(mode: boolean): unknown; + }; + export interface HunkSessionRunnerOptions { - stdin: NodeJS.ReadStream & TerminalInputEvents; + stdin: RendererInput; stdout: NodeJS.WriteStream; useMouse: boolean; signals: readonly NodeJS.Signals[]; @@ -117,7 +133,8 @@ export async function runHunkSession( try { renderer = await createRenderer({ - stdin: options.stdin, + // OpenTUI declares the socket-backed `NodeJS.ReadStream` but only touches `RendererInput`. + stdin: options.stdin as NodeJS.ReadStream, stdout: options.stdout, useMouse: options.useMouse, screenMode: "alternate-screen",