feat: complete native Windows 11 support#285
Conversation
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>
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>
- 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 modem-dev#282) remain. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Greptile SummaryThis PR completes native Windows 11 support by porting three bash scripts (
Confidence Score: 4/5Safe to merge. All changed paths are build/install scripts; no production runtime code is touched. The Windows-specific logic is well-reasoned and the No files require special attention beyond the two minor style nits in Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["bun run build:bin\nbuild-bin.ts"] --> B{"process.platform"}
B -- win32 --> C["dist/hunk.exe"]
B -- unix --> D["dist/hunk"]
E["bun run build:npm\nbuild-npm.ts"] --> F["dist/npm/main.js"]
E --> G["dist/npm/opentui/index.js + *.d.ts"]
F -- "non-Windows only" --> H["chmodSync 0755"]
I["bun run install:bin\ninstall-bin.ts"] --> A
I --> J{"HUNK_INSTALL_DIR?"}
J -- set --> K["custom dir"]
J -- unset + win32 --> L["%LOCALAPPDATA%\\Programs\\hunk"]
J -- unset + unix --> M["~/.local/bin"]
K & L & M --> N["copyFileSync binary to installPath"]
N --> O["PATH check\ncase-insensitive on win32"]
P["bun run build:prebuilt:npm\nstage-prebuilt-npm.ts"] --> E
P --> A
P --> Q["binaryFilenameForSpec(hostSpec)\ndist/hunk OR dist/hunk.exe"]
R["smoke-prebuilt-install.ts"] --> S["npmCommand pack/install"]
S --> T["envWithPath\nstrips PATH case-variants"]
T --> U["run hunk --help / --version"]
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
scripts/install-bin.ts:48-51
The `.filter(Boolean)` on line 48 already removes all falsy entries (including empty strings) from `pathEntries`, so the `if (!entry) return false` guard inside the `.some()` callback is unreachable. The inner check can be dropped.
```suggestion
const pathEntries = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
const installDirOnPath = pathEntries.some((entry) => {
// Windows paths are case-insensitive; normalize both sides for the comparison.
```
### Issue 2 of 2
scripts/stage-prebuilt-npm.ts:174-184
`getHostPlatformPackageSpec()` is called twice in the non-`artifactRoot` path: once inside the IIFE (line 177) and again in the closing log (line 202). The IIFE's `hostSpec` is out of scope by then, so the function is invoked a second time. Hoisting `hostSpec` eliminates the duplicate call and removes the need for the IIFE pattern entirely.
```suggestion
const hostSpec = artifactRoot ? undefined : getHostPlatformPackageSpec();
const artifacts = artifactRoot
? collectArtifactSpecs(artifactRoot)
: [
{
spec: hostSpec!,
compiledBinary: path.join(repoRoot, "dist", binaryFilenameForSpec(hostSpec!)),
},
];
```
Reviews (1): Last reviewed commit: "docs: declare native Windows support" | Re-trigger Greptile |
- 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>

Heads-up on scope
The biggest call here is replacing three bash scripts (
build-bin,build-npm,install-bin) with TypeScript. The smaller alternatives I considered:bash: not founderrors on stock Windows..ps1and.shpairs. More files, two ways to drift, and the rest of this directory is already TS.If you'd rather take a smaller version of this PR, the docs and the small fixes (
stage-prebuilt-npm.tsdist/hunk.exelookup, npm.cmd resolution, PATH case-collision) can land first and the bash → TS port can wait. Happy to split.Builds on #282. With that merged, the release pipeline knows how to publish a Windows binary, but the local build and packaging scripts still assume bash. They fall over before reaching the new code path. After this PR,
bun run build:prebuilt:npm,bun run check:prebuilt-pack, andbun run smoke:prebuilt-installall complete on native Windows 11 x64.Changes
feat(scripts): port build/install bash scripts to TypeScriptscripts/build-bin.sh→scripts/build-bin.ts. Writesdist/hunkon Unix anddist/hunk.exeon Windows, matching whatbun build --compileactually emits.scripts/build-npm.sh→scripts/build-npm.ts. Skipschmod 0755on Windows and replaces the shell glob withreaddirSync.scripts/install-bin.sh→scripts/install-bin.ts. Defaults to%LOCALAPPDATA%\Programs\hunkon Windows and~/.local/binon Unix, still honorsHUNK_INSTALL_DIR, and case-folds before checking PATH on Windows.package.jsoninvokesbun run ./scripts/*.tsinstead ofbash ./scripts/*.sh.fix(scripts): make repo scripts work on native WindowsA new
scripts/script-helpers.tsholds two helpers, both threaded through the existing scripts:npmCommandresolves tonpm.cmdon Windows.Bun.spawncan't exec the Unix-stylenpmshim that Node ships next tonpm.cmd(same applies to Nodechild_processwithoutshell: true).envWithPath(path)strips every case-variant ofPATH/Path/etc. before settingPATH. The reason: on Windows,{ ...process.env, PATH: sanitized }leaves the originalPathkey in the object, the child inherits both, and binaries resolve from the un-sanitized union. The smoke test'sbun unexpectedly availableguard was silently passing the wrong PATH because of this.Three smaller fixes:
stage-prebuilt-npm.tshardcodeddist/hunkfor the host-only artifact. Now callsbinaryFilenameForSpec(getHostPlatformPackageSpec()).smoke-prebuilt-install.tsno longer requiresbashon PATH on Windows. The npm-installedhunk.cmdshim doesn't shell out via bash.check-pack.ts,check-prebuilt-pack.ts, andpublish-prebuilt-npm.tsall usenpmCommand.docs: declare native Windows support[Unreleased]: records the port and the docs alongside the existing PR feat(release): publish Windows prebuilt artifacts #282 entry.Verification (Windows 11 x64, Bun 1.3.11, Node 24.14.0)
bun run build:binbash: not founddist/hunk.exebun run build:npmbash: not founddist/npm/main.js+ opentui typesbun run check:packspawn npm ENOENTVerified npm pack output for hunkdiff@…bun run build:prebuilt:npmMissing compiled binary at dist/hunkhunkdiff+hunkdiff-windows-x64bun run check:prebuilt-packspawn npm ENOENTVerified prebuilt npm packagesbun run smoke:prebuilt-installbash: not found, orbun unexpectedly availableonce bash is on PATHVerified prebuilt npm install smoke test with hunkdiff-windows-x64bun run install:binwithHUNK_INSTALL_DIR=<tmp>Installed …\hunk.exebun run typecheckandbun run lintbun run format:checkflags the same pre-existingCLAUDE.mdissue onmain; this PR doesn't touch it.Out of scope
hunkdiff-windows-x64to npm. That's a tag, not a code change. The release workflow handles it on the next tag.jjtest failures locally. They need Jujutsu on the host; CI installsjj-cliviataiki-e/install-action.