diff --git a/.changeset/windows-arm64-launcher-error.md b/.changeset/windows-arm64-launcher-error.md new file mode 100644 index 000000000..a75266ded --- /dev/null +++ b/.changeset/windows-arm64-launcher-error.md @@ -0,0 +1,5 @@ +--- +"hunkdiff": patch +--- + +Report that Windows ARM64 is not supported by the current bundled runtime instead of crashing with a low-context Bun FFI error. diff --git a/packages/hunk/bin/hunk.cjs b/packages/hunk/bin/hunk.cjs index 19e943592..738cede35 100755 --- a/packages/hunk/bin/hunk.cjs +++ b/packages/hunk/bin/hunk.cjs @@ -102,6 +102,21 @@ function bundledBunRuntime() { } } +// Exits on Windows ARM64 before bundled Bun starts OpenTUI. Bun disables TinyCC there +// (oven-sh/bun#28055). Callers honor HUNK_BIN_PATH and any installed prebuilt first. +function rejectUnsupportedWindowsArm64Runtime() { + if (os.platform() !== "win32" || os.arch() !== "arm64") { + return; + } + + console.error( + "hunk: Windows ARM64 is not supported by the current bundled runtime. " + + "Bun disables TinyCC on this platform, so OpenTUI cannot load. " + + "Tracking: oven-sh/bun#28055", + ); + process.exit(1); +} + const forwardedArgs = process.argv.slice(2); if (forwardedArgs.length === 2 && forwardedArgs[0] === "skill" && forwardedArgs[1] === "path") { const skillPath = bundledSkillPath(); @@ -125,6 +140,8 @@ if (prebuiltBinary) { run(prebuiltBinary, forwardedArgs); } +rejectUnsupportedWindowsArm64Runtime(); + const bunBinary = bundledBunRuntime(); if (bunBinary) { const entrypoint = path.join(__dirname, "..", "dist", "npm", "main.js"); diff --git a/test/cli/entrypoint.test.ts b/test/cli/entrypoint.test.ts index b047284e4..d3b3b0216 100644 --- a/test/cli/entrypoint.test.ts +++ b/test/cli/entrypoint.test.ts @@ -272,6 +272,62 @@ describe("CLI entrypoint contracts", () => { } }); + test("bin wrapper reports Windows ARM64 before the bundled Bun fallback", () => { + const tempDir = mkdtempSync(join(tmpdir(), "hunk-wrapper-win-arm64-")); + const packageRoot = join(tempDir, "package"); + const tempWrapperPath = join(packageRoot, "bin", "hunk.cjs"); + const preloadPath = join(tempDir, "os-preload.cjs"); + const bunMarkerPath = join(tempDir, "bun-invoked"); + const fakeBunDir = join(packageRoot, "node_modules", "bun", "bin"); + const fakeBunPath = join(fakeBunDir, "bun.exe"); + + try { + mkdirSync(join(packageRoot, "bin"), { recursive: true }); + mkdirSync(fakeBunDir, { recursive: true }); + copyFileSync(join(process.cwd(), "packages", "hunk", "bin", "hunk.cjs"), tempWrapperPath); + writeFileSync( + join(packageRoot, "node_modules", "bun", "package.json"), + JSON.stringify({ name: "bun", version: "0.0.0" }), + ); + // Resolvable bundled runtime. Records a marker only if the wrapper actually spawns it. + writeFileSync( + fakeBunPath, + `#!/usr/bin/env node\nrequire("node:fs").writeFileSync(${JSON.stringify(bunMarkerPath)}, "invoked\\n");\n`, + { mode: 0o755 }, + ); + writeFileSync( + preloadPath, + 'const os = require("node:os");\nos.platform = () => "win32";\nos.arch = () => "arm64";\n', + ); + + const env = { ...process.env }; + delete env.HUNK_BIN_PATH; + + const proc = Bun.spawnSync(["node", "-r", preloadPath, tempWrapperPath, "diff"], { + cwd: packageRoot, + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + env, + }); + + const stdout = Buffer.from(proc.stdout).toString("utf8"); + const stderr = Buffer.from(proc.stderr).toString("utf8"); + + expect(proc.exitCode).toBe(1); + expect(stdout).toBe(""); + expect(stderr).toContain("Windows ARM64 is not supported by the current bundled runtime"); + expect(stderr).toContain("oven-sh/bun#28055"); + expect(stderr).not.toContain("bun:ffi"); + expect(stderr).not.toContain("dlopen"); + expect(stderr).not.toContain("Bun v"); + expect(stderr).not.toContain("Unsupported platform for prebuilt Hunk binaries"); + expect(existsSync(bunMarkerPath)).toBe(false); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + test("general pager mode falls back to plain text for non-diff stdin", () => { const proc = Bun.spawnSync(["bun", "run", "packages/hunk/src/main.tsx", "pager"], { cwd: process.cwd(),