From e51b4a8f80d2a3e067f2834a5681502f54adfc25 Mon Sep 17 00:00:00 2001 From: Ofek Wengrowicz Date: Sun, 10 May 2026 21:08:06 -0700 Subject: [PATCH 1/4] feat(scripts): port build/install bash scripts to TypeScript Replace ash ./scripts/{build-bin,build-npm,install-bin}.sh with Bun-runnable TypeScript so native Windows contributors no longer need Git Bash or WSL to build or install Hunk locally. The new scripts: - scripts/build-bin.ts writes dist/hunk (POSIX) or dist/hunk.exe (Windows), matching what un build --compile actually emits. - scripts/build-npm.ts skips the Unix-only chmod 0755 on Windows, iterates the type-declaration files instead of relying on a shell glob, and otherwise preserves the original build flags. - scripts/install-bin.ts defaults to %LOCALAPPDATA%\\Programs\\hunk on Windows and ~/.local/bin on Unix, still honours HUNK_INSTALL_DIR, and uses path.delimiter plus a case-insensitive comparison when checking PATH. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- package.json | 6 +-- scripts/build-bin.sh | 16 ------- scripts/build-bin.ts | 34 +++++++++++++++ scripts/build-npm.sh | 44 ------------------- scripts/build-npm.ts | 95 ++++++++++++++++++++++++++++++++++++++++++ scripts/install-bin.sh | 23 ---------- scripts/install-bin.ts | 61 +++++++++++++++++++++++++++ 7 files changed, 193 insertions(+), 86 deletions(-) delete mode 100644 scripts/build-bin.sh create mode 100644 scripts/build-bin.ts delete mode 100644 scripts/build-npm.sh create mode 100644 scripts/build-npm.ts delete mode 100644 scripts/install-bin.sh create mode 100644 scripts/install-bin.ts diff --git a/package.json b/package.json index 8f7845db..313acabf 100644 --- a/package.json +++ b/package.json @@ -46,12 +46,12 @@ "scripts": { "start": "bun run src/main.tsx", "dev": "bun --watch src/main.tsx", - "build:npm": "bash ./scripts/build-npm.sh", - "build:bin": "bash ./scripts/build-bin.sh", + "build:npm": "bun run ./scripts/build-npm.ts", + "build:bin": "bun run ./scripts/build-bin.ts", "build:prebuilt:npm": "bun run build:npm && bun run build:bin && bun run ./scripts/stage-prebuilt-npm.ts", "build:prebuilt:artifact": "bun run build:bin && bun run ./scripts/build-prebuilt-artifact.ts", "stage:prebuilt:release": "bun run build:npm && bun run ./scripts/stage-prebuilt-npm.ts --artifact-root ./dist/release/artifacts", - "install:bin": "bash ./scripts/install-bin.sh", + "install:bin": "bun run ./scripts/install-bin.ts", "typecheck": "tsc --noEmit", "format": "oxfmt --write .", "format:check": "oxfmt --check .", diff --git a/scripts/build-bin.sh b/scripts/build-bin.sh deleted file mode 100644 index c4c802fe..00000000 --- a/scripts/build-bin.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -set -Eeuo pipefail - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -dist_dir="${repo_root}/dist" -outfile="${dist_dir}/hunk" -legacy_outfile="${dist_dir}/otdiff" - -mkdir -p "${dist_dir}" -rm -f "${legacy_outfile}" - -BUN_TMPDIR="${repo_root}/.bun-tmp" \ -BUN_INSTALL="${repo_root}/.bun-install" \ -bun build --compile "${repo_root}/src/main.tsx" --outfile "${outfile}" - -printf 'Built %s\n' "${outfile}" diff --git a/scripts/build-bin.ts b/scripts/build-bin.ts new file mode 100644 index 00000000..2158d454 --- /dev/null +++ b/scripts/build-bin.ts @@ -0,0 +1,34 @@ +#!/usr/bin/env bun + +import { mkdirSync, rmSync } from "node:fs"; +import path from "node:path"; + +const repoRoot = path.resolve(import.meta.dir, ".."); +const distDir = path.join(repoRoot, "dist"); +const binaryName = process.platform === "win32" ? "hunk.exe" : "hunk"; +const outfile = path.join(distDir, binaryName); +const legacyOutfile = path.join(distDir, process.platform === "win32" ? "otdiff.exe" : "otdiff"); + +mkdirSync(distDir, { recursive: true }); +rmSync(legacyOutfile, { force: true }); + +const proc = Bun.spawnSync( + ["bun", "build", "--compile", path.join(repoRoot, "src", "main.tsx"), "--outfile", outfile], + { + cwd: repoRoot, + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + env: { + ...process.env, + BUN_TMPDIR: path.join(repoRoot, ".bun-tmp"), + BUN_INSTALL: path.join(repoRoot, ".bun-install"), + }, + }, +); + +if (proc.exitCode !== 0) { + throw new Error(`bun build --compile failed with exit ${proc.exitCode}`); +} + +console.log(`Built ${outfile}`); diff --git a/scripts/build-npm.sh b/scripts/build-npm.sh deleted file mode 100644 index 31697a8a..00000000 --- a/scripts/build-npm.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash -set -Eeuo pipefail - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -outdir="${repo_root}/dist/npm" -types_outdir="${repo_root}/dist/npm-types" - -rm -rf "${outdir}" -rm -rf "${types_outdir}" -mkdir -p "${outdir}/opentui" - -BUN_TMPDIR="${repo_root}/.bun-tmp" \ -BUN_INSTALL="${repo_root}/.bun-install" \ - bun build "${repo_root}/src/main.tsx" \ - --target bun \ - --format esm \ - --outdir "${outdir}" \ - --entry-naming main.js - -chmod 0755 "${outdir}/main.js" - -BUN_TMPDIR="${repo_root}/.bun-tmp" \ -BUN_INSTALL="${repo_root}/.bun-install" \ - bun build "${repo_root}/src/opentui/index.ts" \ - --target node \ - --format esm \ - --external react \ - --external react/jsx-runtime \ - --external react/jsx-dev-runtime \ - --external @opentui/core \ - --external @opentui/react \ - --external @opentui/react/jsx-runtime \ - --external @opentui/react/jsx-dev-runtime \ - --external @pierre/diffs \ - --outdir "${outdir}/opentui" \ - --entry-naming index.js - -bun x tsc -p "${repo_root}/tsconfig.opentui.json" - -cp "${types_outdir}/opentui/"*.d.ts "${outdir}/opentui/" -rm -rf "${types_outdir}" - -printf 'Built %s\n' "${outdir}/main.js" -printf 'Built %s\n' "${outdir}/opentui/index.js" diff --git a/scripts/build-npm.ts b/scripts/build-npm.ts new file mode 100644 index 00000000..c4be9c6d --- /dev/null +++ b/scripts/build-npm.ts @@ -0,0 +1,95 @@ +#!/usr/bin/env bun + +import { chmodSync, copyFileSync, mkdirSync, readdirSync, rmSync } from "node:fs"; +import path from "node:path"; + +const repoRoot = path.resolve(import.meta.dir, ".."); +const outdir = path.join(repoRoot, "dist", "npm"); +const typesOutdir = path.join(repoRoot, "dist", "npm-types"); +const opentuiOutdir = path.join(outdir, "opentui"); +const opentuiTypesDir = path.join(typesOutdir, "opentui"); + +const bunEnv = { + ...process.env, + BUN_TMPDIR: path.join(repoRoot, ".bun-tmp"), + BUN_INSTALL: path.join(repoRoot, ".bun-install"), +}; + +function runBun(args: string[]) { + const proc = Bun.spawnSync(["bun", ...args], { + cwd: repoRoot, + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + env: bunEnv, + }); + + if (proc.exitCode !== 0) { + throw new Error(`bun ${args.join(" ")} failed with exit ${proc.exitCode}`); + } +} + +rmSync(outdir, { recursive: true, force: true }); +rmSync(typesOutdir, { recursive: true, force: true }); +mkdirSync(opentuiOutdir, { recursive: true }); + +runBun([ + "build", + path.join(repoRoot, "src", "main.tsx"), + "--target", + "bun", + "--format", + "esm", + "--outdir", + outdir, + "--entry-naming", + "main.js", +]); + +const mainJs = path.join(outdir, "main.js"); +// chmod is a no-op on Windows; preserve exec bits on Unix so the bin runs in npm-installed packages. +if (process.platform !== "win32") { + chmodSync(mainJs, 0o755); +} + +runBun([ + "build", + path.join(repoRoot, "src", "opentui", "index.ts"), + "--target", + "node", + "--format", + "esm", + "--external", + "react", + "--external", + "react/jsx-runtime", + "--external", + "react/jsx-dev-runtime", + "--external", + "@opentui/core", + "--external", + "@opentui/react", + "--external", + "@opentui/react/jsx-runtime", + "--external", + "@opentui/react/jsx-dev-runtime", + "--external", + "@pierre/diffs", + "--outdir", + opentuiOutdir, + "--entry-naming", + "index.js", +]); + +runBun(["x", "tsc", "-p", path.join(repoRoot, "tsconfig.opentui.json")]); + +for (const entry of readdirSync(opentuiTypesDir)) { + if (entry.endsWith(".d.ts")) { + copyFileSync(path.join(opentuiTypesDir, entry), path.join(opentuiOutdir, entry)); + } +} + +rmSync(typesOutdir, { recursive: true, force: true }); + +console.log(`Built ${mainJs}`); +console.log(`Built ${path.join(opentuiOutdir, "index.js")}`); diff --git a/scripts/install-bin.sh b/scripts/install-bin.sh deleted file mode 100644 index cc1d1c76..00000000 --- a/scripts/install-bin.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash -set -Eeuo pipefail - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -binary_path="${repo_root}/dist/hunk" -install_dir="${HUNK_INSTALL_DIR:-${HOME}/.local/bin}" -install_path="${install_dir}/hunk" -legacy_install_path="${install_dir}/otdiff" - -bash "${repo_root}/scripts/build-bin.sh" - -mkdir -p "${install_dir}" -install -m 0755 "${binary_path}" "${install_path}" -rm -f "${legacy_install_path}" - -printf 'Installed %s\n' "${install_path}" - -case ":${PATH}:" in - *":${install_dir}:"*) ;; - *) - printf 'Warning: %s is not on PATH\n' "${install_dir}" >&2 - ;; -esac diff --git a/scripts/install-bin.ts b/scripts/install-bin.ts new file mode 100644 index 00000000..217f0b55 --- /dev/null +++ b/scripts/install-bin.ts @@ -0,0 +1,61 @@ +#!/usr/bin/env bun + +import { chmodSync, copyFileSync, mkdirSync, rmSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const repoRoot = path.resolve(import.meta.dir, ".."); +const isWindows = process.platform === "win32"; +const binaryName = isWindows ? "hunk.exe" : "hunk"; +const legacyBinaryName = isWindows ? "otdiff.exe" : "otdiff"; +const binaryPath = path.join(repoRoot, "dist", binaryName); + +function defaultInstallDir() { + if (isWindows) { + const base = process.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local"); + return path.join(base, "Programs", "hunk"); + } + + return path.join(os.homedir(), ".local", "bin"); +} + +const installDir = process.env.HUNK_INSTALL_DIR ?? defaultInstallDir(); +const installPath = path.join(installDir, binaryName); +const legacyInstallPath = path.join(installDir, legacyBinaryName); + +const buildScript = path.join(repoRoot, "scripts", "build-bin.ts"); +const build = Bun.spawnSync(["bun", "run", buildScript], { + cwd: repoRoot, + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + env: process.env, +}); + +if (build.exitCode !== 0) { + throw new Error(`scripts/build-bin.ts failed with exit ${build.exitCode}`); +} + +mkdirSync(installDir, { recursive: true }); +copyFileSync(binaryPath, installPath); +if (!isWindows) { + chmodSync(installPath, 0o755); +} +rmSync(legacyInstallPath, { force: true }); + +console.log(`Installed ${installPath}`); + +const pathEntries = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean); +const installDirOnPath = pathEntries.some((entry) => { + if (!entry) return false; + // Windows paths are case-insensitive; normalize both sides for the comparison. + const normalizedEntry = isWindows ? path.normalize(entry).toLowerCase() : path.normalize(entry); + const normalizedInstallDir = isWindows + ? path.normalize(installDir).toLowerCase() + : path.normalize(installDir); + return normalizedEntry === normalizedInstallDir; +}); + +if (!installDirOnPath) { + console.warn(`Warning: ${installDir} is not on PATH`); +} From 274752040caaff3e051a7cc78741d417aefb9134 Mon Sep 17 00:00:00 2001 From: Ofek Wengrowicz Date: Sun, 10 May 2026 21:08:24 -0700 Subject: [PATCH 2/4] fix(scripts): make repo scripts work on native Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a small scripts/script-helpers.ts module and wires it into the existing TS scripts to address four native-Windows quirks: - npm spawning: Bun.spawn cannot exec the Unix-style \ pm\ shell shim that Node ships alongside \ pm.cmd\. The new \ pmCommand\ constant resolves to \ pm.cmd\ on Windows and \ pm\ elsewhere, and is used from check-pack, check-prebuilt-pack, publish-prebuilt-npm, and the prebuilt-install smoke test. - PATH case-collision: \{ ...process.env, PATH: sanitized }\ leaves the inherited \Path\ key in place on Windows, so the child sees both the original and the sanitized PATH and resolves binaries via the unsanitized union. The new \nvWithPath\ helper strips every case-variant before setting PATH, restoring the smoke test's \un unexpectedly available\ guarantee on Windows. - Host artifact path: \stage-prebuilt-npm.ts\ hardcoded \dist/hunk\, but \un build --compile\ emits \dist/hunk.exe\ on Windows. Use \inaryFilenameForSpec(getHostPlatformPackageSpec())\ so the host-only staging path matches the on-disk filename. - bash on PATH: the prebuilt-install smoke test required \ash\ on PATH to build a sanitized PATH for the npm-installed wrapper. The Windows \hunk.cmd\ shim does not need bash, so \ashDir\ is now optional on Windows and filtered out of the sanitized PATH. With these fixes \un run build:prebuilt:npm\, \un run check:prebuilt-pack\, and \un run smoke:prebuilt-install\ all complete successfully on native Windows 11 x64. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/check-pack.ts | 4 +++- scripts/check-prebuilt-pack.ts | 3 ++- scripts/publish-prebuilt-npm.ts | 5 +++-- scripts/script-helpers.ts | 37 +++++++++++++++++++++++++++++++ scripts/smoke-prebuilt-install.ts | 18 +++++++-------- scripts/stage-prebuilt-npm.ts | 19 ++++++++++------ 6 files changed, 66 insertions(+), 20 deletions(-) create mode 100644 scripts/script-helpers.ts diff --git a/scripts/check-pack.ts b/scripts/check-pack.ts index 934846ab..d0baae92 100644 --- a/scripts/check-pack.ts +++ b/scripts/check-pack.ts @@ -1,5 +1,7 @@ #!/usr/bin/env bun +import { npmCommand } from "./script-helpers"; + interface PackedFile { path: string; size: number; @@ -13,7 +15,7 @@ interface PackResult { files: PackedFile[]; } -const proc = Bun.spawnSync(["npm", "pack", "--dry-run", "--json"], { +const proc = Bun.spawnSync([npmCommand, "pack", "--dry-run", "--json"], { cwd: process.cwd(), stdin: "ignore", stdout: "pipe", diff --git a/scripts/check-prebuilt-pack.ts b/scripts/check-prebuilt-pack.ts index 8b34a79e..808e6c40 100644 --- a/scripts/check-prebuilt-pack.ts +++ b/scripts/check-prebuilt-pack.ts @@ -3,6 +3,7 @@ import { existsSync, readdirSync } from "node:fs"; import path from "node:path"; import { releaseNpmDir } from "./prebuilt-package-helpers"; +import { npmCommand } from "./script-helpers"; interface PackedFile { path: string; @@ -15,7 +16,7 @@ interface PackResult { } function runPackDryRun(cwd: string) { - const proc = Bun.spawnSync(["npm", "pack", "--dry-run", "--json"], { + const proc = Bun.spawnSync([npmCommand, "pack", "--dry-run", "--json"], { cwd, stdin: "ignore", stdout: "pipe", diff --git a/scripts/publish-prebuilt-npm.ts b/scripts/publish-prebuilt-npm.ts index 1d6ebcab..d830c05e 100644 --- a/scripts/publish-prebuilt-npm.ts +++ b/scripts/publish-prebuilt-npm.ts @@ -3,6 +3,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs"; import path from "node:path"; import { releaseNpmDir } from "./prebuilt-package-helpers"; +import { npmCommand } from "./script-helpers"; type PackageJson = { name: string; @@ -41,7 +42,7 @@ function parseArgs(argv: string[]) { } function npmViewExists(name: string, version: string) { - const proc = Bun.spawnSync(["npm", "view", `${name}@${version}`, "version"], { + const proc = Bun.spawnSync([npmCommand, "view", `${name}@${version}`, "version"], { stdin: "ignore", stdout: "pipe", stderr: "ignore", @@ -70,7 +71,7 @@ function publishDirectory(directory: string, dryRun: boolean, npmTag: string) { args.push("--dry-run"); } - const proc = Bun.spawnSync(["npm", ...args], { + const proc = Bun.spawnSync([npmCommand, ...args], { cwd: directory, stdin: "ignore", stdout: "inherit", diff --git a/scripts/script-helpers.ts b/scripts/script-helpers.ts new file mode 100644 index 00000000..428b1763 --- /dev/null +++ b/scripts/script-helpers.ts @@ -0,0 +1,37 @@ +#!/usr/bin/env bun + +/** + * Shared cross-platform helpers for Bun-driven repo scripts. + * + * On Windows, Node ships `npm` (a shell-script shim) alongside `npm.cmd`. Bun + * (and Node `child_process` without `shell: true`) cannot execute the shim + * directly, so we must spawn the `.cmd` wrapper instead. The helpers below + * give scripts one place to resolve those names. + */ + +/** Command name to invoke `npm` from Bun.spawn on the current platform. */ +export const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm"; + +/** + * Build a child-process env that overrides PATH cleanly on every platform. + * + * Windows environment variables are case-insensitive, but `{ ...process.env }` + * preserves whatever case Bun reported (often `Path`). Setting `PATH: ...` on + * top of that produces an env object with both `Path` (the original system + * value) and `PATH` (the sanitized value); the child inherits both, and + * Windows resolves lookups against the un-normalized union, defeating any + * attempt to scope PATH. We strip every case-variant first, then set PATH. + */ +export function envWithPath( + path: string, + base: NodeJS.ProcessEnv = process.env, +): NodeJS.ProcessEnv { + const next: NodeJS.ProcessEnv = {}; + for (const [key, value] of Object.entries(base)) { + if (key.toLowerCase() !== "path") { + next[key] = value; + } + } + next.PATH = path; + return next; +} diff --git a/scripts/smoke-prebuilt-install.ts b/scripts/smoke-prebuilt-install.ts index 79c46d7c..4c9bcceb 100644 --- a/scripts/smoke-prebuilt-install.ts +++ b/scripts/smoke-prebuilt-install.ts @@ -16,6 +16,7 @@ import { getHostPlatformPackageSpec, releaseNpmDir, } from "./prebuilt-package-helpers"; +import { envWithPath, npmCommand } from "./script-helpers"; function run(command: string[], options?: { cwd?: string; env?: NodeJS.ProcessEnv }) { const proc = Bun.spawnSync(command, { @@ -80,9 +81,11 @@ try { const nodePath = commandPath("node"); const nodeDir = path.dirname(nodePath); - const bashDir = commandDirectory("bash"); + // bash is required on Unix where the npm-installed wrapper shells out via `#!/usr/bin/env bash`, + // but the Windows `hunk.cmd` shim does not need bash on PATH. + const bashDir = process.platform === "win32" ? undefined : commandDirectory("bash"); - run(["npm", "pack", "--pack-destination", packageDir], { + run([npmCommand, "pack", "--pack-destination", packageDir], { cwd: path.join(releaseRoot, hostSpec.packageName), }); @@ -102,19 +105,19 @@ try { }; writeFileSync(smokeManifestPath, `${JSON.stringify(smokeManifest, null, 2)}\n`); - run(["npm", "pack", "--pack-destination", packageDir], { + run([npmCommand, "pack", "--pack-destination", packageDir], { cwd: smokePackageDir, }); const metaTarball = path.join(packageDir, `hunkdiff-${packageVersion}.tgz`); - run(["npm", "install", "-g", "--prefix", installDir, metaTarball]); + run([npmCommand, "install", "-g", "--prefix", installDir, metaTarball]); const installedBinDir = process.platform === "win32" ? installDir : path.join(installDir, "bin"); const installedPackageRoot = process.platform === "win32" ? path.join(installDir, "node_modules", "hunkdiff") : path.join(installDir, "lib", "node_modules", "hunkdiff"); - const sanitizedPath = [installedBinDir, nodeDir, bashDir].join(path.delimiter); + const sanitizedPath = [installedBinDir, nodeDir, bashDir].filter(Boolean).join(path.delimiter); const installedHunk = path.join( installedBinDir, process.platform === "win32" ? "hunk.cmd" : "hunk", @@ -126,10 +129,7 @@ try { "bin", binaryFilenameForSpec(hostSpec), ); - const commandEnv = { - ...process.env, - PATH: sanitizedPath, - }; + const commandEnv = envWithPath(sanitizedPath); if (process.platform !== "win32") { const installedBinaryMode = statSync(installedPlatformBinary).mode & 0o777; diff --git a/scripts/stage-prebuilt-npm.ts b/scripts/stage-prebuilt-npm.ts index 66989747..70d93ab9 100644 --- a/scripts/stage-prebuilt-npm.ts +++ b/scripts/stage-prebuilt-npm.ts @@ -173,12 +173,15 @@ ensureDirectory(releaseRoot); const artifacts = artifactRoot ? collectArtifactSpecs(artifactRoot) - : [ - { - spec: getHostPlatformPackageSpec(), - compiledBinary: path.join(repoRoot, "dist", "hunk"), - }, - ]; + : (() => { + const hostSpec = getHostPlatformPackageSpec(); + return [ + { + spec: hostSpec, + compiledBinary: path.join(repoRoot, "dist", binaryFilenameForSpec(hostSpec)), + }, + ]; + })(); const stagedSpecs = sortPlatformPackageSpecs(artifacts.map((artifact) => artifact.spec)); stageMetaPackage(repoRoot, rootPackage, releaseRoot, stagedSpecs); @@ -195,5 +198,7 @@ for (const spec of stagedSpecs) { if (artifactRoot) { console.log(`Artifacts source: ${artifactRoot}`); } else { - console.log(`Artifacts source: ${path.join(repoRoot, "dist", "hunk")}`); + console.log( + `Artifacts source: ${path.join(repoRoot, "dist", binaryFilenameForSpec(getHostPlatformPackageSpec()))}`, + ); } From d088701fb923476fb11e206429b8b75f99cf1724 Mon Sep 17 00:00:00 2001 From: Ofek Wengrowicz Date: Sun, 10 May 2026 21:08:32 -0700 Subject: [PATCH 3/4] docs: declare native Windows support - README: list Windows alongside macOS and Linux under Requirements. - CONTRIBUTING: add Windows to the development-setup platforms and note that the native Windows path uses Node/Bun directly (no WSL or Git Bash needed) now that the build/install scripts are TypeScript. - CHANGELOG: record the build-script port and Windows documentation under [Unreleased]; the entries about the prebuilt Windows artifact pipeline (PR #282) remain. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 3 +++ CONTRIBUTING.md | 1 + README.md | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dfc532e..18e82d09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,12 @@ All notable user-visible changes to Hunk are documented in this file. - Added Windows x64 prebuilt artifact publishing to the release workflow. - Added Nix flake app outputs for `nix run` and a named `hunk` package output. +- Documented native Windows support in the README and contributor guide. ### Changed +- Ported `build:npm`, `build:bin`, and `install:bin` from bash scripts to cross-platform Bun-runnable TypeScript so native Windows contributors no longer need Git Bash to build or install Hunk locally. + ### Fixed ## [0.12.0-beta.1] - 2026-05-10 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f8d21869..91956e84 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,6 +11,7 @@ Requirements: - Bun 1.3+ - Node.js 18+ - Git +- macOS, Linux, or Windows. The Windows path uses native Node/Bun on Windows 10/11 (x64); no WSL or Git Bash required. > Nix users can use `nix develop` or [direnv](https://direnv.net/) to enter a development shell. diff --git a/README.md b/README.md index d1bdce87..8923ce54 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ brew install modem-dev/tap/hunk Requirements: - Node.js 18+ -- macOS or Linux +- macOS, Linux, or Windows - Git recommended for most workflows > Nix users can use the `default` package exported in `flake.nix` instead. See [nix/README.md](./nix/README.md) for details. From a38e51659b6a1d1b68ac8fc2e7e78d5b15118b6a Mon Sep 17 00:00:00 2001 From: Ofek Wengrowicz Date: Sun, 10 May 2026 22:10:38 -0700 Subject: [PATCH 4/4] fix(scripts): address review nits in install-bin and stage-prebuilt-npm - install-bin.ts: drop the unreachable \if (!entry) return false\ guard inside the PATH membership check. \.filter(Boolean)\ on the line above already strips every falsy entry. - stage-prebuilt-npm.ts: hoist \hostSpec\ out of the IIFE so the trailing \Artifacts source:\ log can reuse it instead of calling \getHostPlatformPackageSpec()\ a second time. Removes the IIFE pattern entirely. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/install-bin.ts | 1 - scripts/stage-prebuilt-npm.ts | 20 ++++++++------------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/scripts/install-bin.ts b/scripts/install-bin.ts index 217f0b55..e531d088 100644 --- a/scripts/install-bin.ts +++ b/scripts/install-bin.ts @@ -47,7 +47,6 @@ console.log(`Installed ${installPath}`); const pathEntries = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean); const installDirOnPath = pathEntries.some((entry) => { - if (!entry) return false; // Windows paths are case-insensitive; normalize both sides for the comparison. const normalizedEntry = isWindows ? path.normalize(entry).toLowerCase() : path.normalize(entry); const normalizedInstallDir = isWindows diff --git a/scripts/stage-prebuilt-npm.ts b/scripts/stage-prebuilt-npm.ts index 70d93ab9..fb892b21 100644 --- a/scripts/stage-prebuilt-npm.ts +++ b/scripts/stage-prebuilt-npm.ts @@ -171,17 +171,15 @@ const artifactRoot = options.artifactRoot ? path.resolve(options.artifactRoot) : rmSync(releaseRoot, { recursive: true, force: true }); ensureDirectory(releaseRoot); +const hostSpec = artifactRoot ? undefined : getHostPlatformPackageSpec(); const artifacts = artifactRoot ? collectArtifactSpecs(artifactRoot) - : (() => { - const hostSpec = getHostPlatformPackageSpec(); - return [ - { - spec: hostSpec, - compiledBinary: path.join(repoRoot, "dist", binaryFilenameForSpec(hostSpec)), - }, - ]; - })(); + : [ + { + spec: hostSpec!, + compiledBinary: path.join(repoRoot, "dist", binaryFilenameForSpec(hostSpec!)), + }, + ]; const stagedSpecs = sortPlatformPackageSpecs(artifacts.map((artifact) => artifact.spec)); stageMetaPackage(repoRoot, rootPackage, releaseRoot, stagedSpecs); @@ -198,7 +196,5 @@ for (const spec of stagedSpecs) { if (artifactRoot) { console.log(`Artifacts source: ${artifactRoot}`); } else { - console.log( - `Artifacts source: ${path.join(repoRoot, "dist", binaryFilenameForSpec(getHostPlatformPackageSpec()))}`, - ); + console.log(`Artifacts source: ${path.join(repoRoot, "dist", binaryFilenameForSpec(hostSpec!))}`); }