feat: native Windows support (all phases, feature branch)#117
Closed
George-iam wants to merge 7 commits intomainfrom
Closed
feat: native Windows support (all phases, feature branch)#117George-iam wants to merge 7 commits intomainfrom
George-iam wants to merge 7 commits intomainfrom
Conversation
Windows FlushFileBuffers (Node maps fsyncSync to it) requires the handle to have write access. Our previous pattern opened a read-only fd on the temp file just to fsync, which returned EPERM on Windows and broke 12 tests in test/engine.test.ts. POSIX allows fsync on any fd, so rewriting to use a single write-fd through write + fsync + close is correct on both platforms. appendLine already used this pattern and passed on Windows; atomicWrite now matches it. Native Windows discovery finding #2 — first fix on feat/native-windows-support-20260417. Verified: - Linux: npm test 511/511 pass - Windows: engine.test.ts 25/25 pass (was 13/25) #!axme pr=none repo=AxmeAI/axme-code
Three production-code fixes for native Windows, part of the discovery
pass on the feature branch:
1. src/utils/agent-options.ts: findClaudePath used `which claude`
which prints "not recognized" stderr on Windows. Branch to
`where.exe claude` on win32, suppress stderr via stdio:ignore, and
take the first line of `where` output (can return multiple).
2. src/storage/sessions.ts attachClaudeSession: `execSync("sleep 0.05")`
for the retry-delay on race with meta.json writes — POSIX `sleep`
doesn't exist in cmd.exe. Replaced with Atomics.wait on a fresh
SharedArrayBuffer, which is a real cross-platform synchronous
sleep and does not require a subprocess spawn.
3. src/{cli, storage/{safety,memory,decisions}, tools/{cleanup,init}}.ts:
13 occurrences of `path.split("/").pop()` to extract basename
from a project/workspace path. On Windows, paths use backslash,
so .split("/") returns a single-element array with the entire
path unchanged, and every downstream name lookup was wrong.
Replaced with Node's cross-platform path.basename().
Linux: 511/511 tests pass, no regression.
Five test-infrastructure fixes so `npm test` is green on native Windows
(509 pass, 1 skip, 0 fail — was 504 pass, 7 fail before). No production
code touched beyond what was in the previous commit.
1. scripts/run-tests.mjs + package.json: npm test used POSIX shell glob
"test/*.test.ts" which cmd.exe/PowerShell don't expand. New script
enumerates test files in Node and spawns tsx explicitly (using npx
vs npx.cmd per platform).
2. test/audit-dedup.test.ts: spawn("npx", ...) needed .cmd resolution
on Windows; the worker script's import path passed backslashes
unescaped (interpreted as escape sequences); and TEST_ROOT was
hardcoded to /tmp. Fixed with npx.cmd + shell:true on win32,
pathToFileURL() for the import specifier, JSON.stringify for all
string-literal arg injection, and tmpdir() for TEST_ROOT. Also
skipped the parallel-processes describe block on Windows because
npx tsx startup (~2-3s per worker) exceeds the 3s LOCK_WAIT_MS
budget in contention — test-harness timing artifact, not a
production bug.
3. test/agent-sdk-paths.test.ts: URL.pathname returns "/C:/..." on
Windows, which readdirSync resolves to "C:\C:\..." (doubled drive
prefix). Replaced with fileURLToPath() which returns platform-
native paths.
4. test/telemetry.test.ts: "sets file mode 0600" skipped on win32.
Windows doesn't honour POSIX mode bits (security via ACLs), so
chmodSync(0o600) is a no-op and the equality check fails. Skip
with explanatory reason.
5. test/auth-config.test.ts: Test mocked $HOME only, but Node's
os.homedir() reads %USERPROFILE% on Windows, so 4 tests read the
real user's auth.yaml instead of the temp dir. Now mocks both
HOME and USERPROFILE on setup/teardown.
Linux: npm test 511/511 pass, no regressions.
Windows native: npm test 509 pass, 1 skip (chmod), 0 fail.
Phase 2 enables Claude Code hooks to fire on Windows without requiring
`axme-code` to be on PATH as a recognised executable. Two pieces:
a) configureHooks() in src/cli.ts now writes hook commands as
`"<node>" "<self>" hook <name> --workspace "<project>"` using
process.execPath and resolve(process.argv[1]). This removes the
PATH dependency that broke on Windows (a shebang-only axme-code.js
is not executable by cmd.exe/PowerShell) and works identically on
POSIX. All segments are quoted so paths-with-spaces and backslash-
heavy Windows paths survive `sh -c` / `cmd.exe /c` verbatim.
Example on Linux:
"/home/u/.nvm/.../node" "/home/u/.local/bin/axme-code" hook pre-tool-use --workspace "/tmp/proj"
Example on Windows:
"C:\node\node.exe" "C:\...\axme-code.js" hook pre-tool-use --workspace "C:\proj"
b) build.mjs emits dist/axme-code.cmd and dist/plugin/bin/axme-code.cmd
alongside the existing POSIX shebang entry. The .cmd forwards all
args to node + the sibling axme-code.js. This is for end users
invoking the CLI directly (`axme-code setup`, `axme-code status`,
etc.) on Windows, where the shebang file alone is not runnable.
Verified on Azure Win11 D2s_v5:
- dist/axme-code.cmd --version → 0.2.9
- axme-code.cmd setup in C:\win-smoke created .axme-code/ + settings.json
- Generated PreToolUse hook command piped through cmd.exe /c correctly:
block rm -rf / → "permissionDecision":"deny"
allow ls → exit 0
- Linux: npm test 511/511 pass, no regression
Plugin hooks.json SessionStart used a POSIX-only shell fragment
('test -d ${PLUGIN_ROOT}/node_modules/@SDK || (cd && npm install) ; node
cli.mjs check-init') — `test -d`, subshell, `;`, and `2>/dev/null` all
fail under cmd.exe/PowerShell.
Moved the lazy SDK install inside the `check-init` subcommand itself:
when CLAUDE_PLUGIN_ROOT is set and the SDK is missing, check-init runs
`npm install --omit=dev --ignore-scripts` in the plugin root via
execSync (which always goes through a shell — sh on POSIX, cmd.exe on
Windows — so bare `npm` resolves to `npm.cmd` on Windows
automatically). Install failure falls through silently; deterministic
paths still work without the SDK.
All four plugin hook commands (SessionStart/Pre/Post/SessionEnd) now
quote the CLAUDE_PLUGIN_ROOT expansion so paths-with-spaces survive
word-splitting. hooks.json command strings shrink to plain
'node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" <subcmd>' — cross-platform by
design, no inline shell logic.
Linux: 511/511 tests pass, no regression.
…rkflow Adds a native Windows install path alongside the existing install.sh: - install.ps1: downloads the Node bundle from the latest GitHub release, saves as axme-code.js under %LOCALAPPDATA%\Programs\axme-code, generates the companion .cmd wrapper, and adds the install dir to User PATH via [Environment]::SetEnvironmentVariable (persists across sessions). Respects AXME_REPO / AXME_INSTALL_DIR env overrides. Accepts a version argument for installing a specific tag. - .github/workflows/release-binary.yml: matrix now includes windows-x64 and windows-arm64 targets. Both build on ubuntu-latest because esbuild output is platform-agnostic JS — the same bundle works under Node on any OS; install.ps1 generates the wrapper locally at install time so we ship one file per arch. - README.md: Quick Start now has parallel Linux/macOS and Windows PowerShell one-liners. WSL2 is no longer the recommended Windows path — it's mentioned as an alternative for users already inside a WSL distro. Verified on Azure Win11 (native): - AST parser accepts install.ps1 (481 tokens, no syntax errors) - Dry-run: arch detected as windows-x64, GitHub latest-tag fetch works, download URL matches the release convention. Fails with the expected friendly error at download because v0.2.9 does not have a Windows asset yet — first release with windows-* assets will be end-to-end verified manually. Linux: 511/511 tests pass, no regression.
Until now tests only ran as part of the publish-npm job in
release-binary.yml, which only fires on tag pushes. PRs and commits
to main got no automated verification. On the feature branch for
native Windows support we've been running the full suite manually on
an Azure Win11 VM — this workflow automates that loop.
Matrix:
- ubuntu-latest (primary dev platform, regression guard)
- macos-latest (ARM64 Apple Silicon coverage)
- windows-latest (native Windows regression guard — the whole reason
this file exists)
Each job: checkout, setup Node 20, npm ci, npm run lint (tsc --noEmit),
npm test, npm run build. fail-fast: false so one platform failing
doesn't hide regressions on the others.
Contributor
Author
|
Closing — opened by mistake for a CI smoke. Feature branch stays isolated per agreement. CI now triggers directly on feat/** pushes; no PR to main until Phase 5 is complete. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Draft PR — not for merge yet. Opening to kick off the newly-added three-OS CI matrix and surface the diff for review while native Windows support matures.
End-to-end goal:
axme-coderuns natively on Linux, macOS, and Windows from a single codebase, with no separate Windows fork. Decision tracked as D-133.What's in this branch (9 commits)
Phase 1 — Production fixes (all cross-platform)
atomicWritefsync fix — opened a read-only fd for fsync, which on Windows returnsEPERM(FlushFileBuffers requires write access). Switched to write-through-fd pattern matchingappendLine. Fixes 12 test failures inengine.test.ts.findClaudePath/which→where.exeon Windows —whichis not a Windows command; setup leaked'which' is not recognizedstderr at every run.execSync("sleep 0.05")→Atomics.wait— POSIXsleepisn't on cmd.exe. SharedArrayBuffer +Atomics.waitis a real cross-platform sync sleep with no subprocess spawn.path.split("/").pop()→path.basename()in 13 places — backslash-heavy Windows paths made every project-name extraction return the entire path.Phase 1 — Test-infra fixes
scripts/run-tests.mjs+ package.json — replaces the POSIX shell globtest/*.test.tswith a cross-platform Node runner.test/audit-dedup.test.ts—spawn("npx")needs.cmd+shell:trueon Windows; generated worker script used backslash-bearing paths unescaped (interpreted as escape sequences); hardcoded/tmp.test/agent-sdk-paths.test.ts—new URL(...).pathnameproduces/C:/...on Windows; switched tofileURLToPath.test/telemetry.test.ts— skipsets file mode 0600on Windows (POSIX modes are a no-op there).test/auth-config.test.ts— mocked only$HOME; Node'sos.homedir()reads%USERPROFILE%on Windows. Mock both.Phase 2 — Hooks cross-platform
configureHooks()insrc/cli.ts— generated hook commands now use absolutenode.exe+ absoluteaxme-code.jspath, all segments quoted. Removes PATH dependency that was breaking on Windows (shebang-only entry can't be executed by cmd.exe).build.mjs— emitsdist/axme-code.cmdanddist/plugin/bin/axme-code.cmdshims for direct-invoke on Windows.test -d ... || npm installPOSIX shell fragment intocheck-initsubcommand as Node code. Pluginhooks.jsoncommands simplified tonode "${CLAUDE_PLUGIN_ROOT}/cli.mjs" <subcmd>— cross-platform by design.Phase 3 — Distribution
install.ps1— downloads latest Node bundle from GitHub Releases, saves asaxme-code.jsunder%LOCALAPPDATA%\Programs\axme-code\, generates the.cmdwrapper, adds to User PATH.release-binary.ymlmatrix — addswindows-x64andwindows-arm64targets (built on ubuntu-latest since esbuild output is platform-agnostic).Phase 4 — CI
.github/workflows/ci.yml— three-OS matrix (ubuntu, macos, windows) running on every PR. This was not tested in GitHub CI yet — this PR is the first real run.What's verified
npm test511/511 pass (regression guard)npm test509 pass, 1 skip (chmod 0o600), 0 failaxme-code setupcreates full.axme-code/(deterministic + real LLM scanners via OAuth)cmd.exe /ccorrectly blocksrm -rf /and allowslscheck-initwithCLAUDE_PLUGIN_ROOTset triggers lazynpm installof the SDKinstall.ps1parses clean (481 tokens) and end-to-end path works until 404 on the v0.2.9 asset that doesn't exist yetWhat's NOT verified (blocking merge)
axme-code-windows-*assets). Will be manually verified on the first release cut from this branch.Not merging yet
Please do not merge. CI needs to be green across the matrix, and a real Windows release needs to be cut + installed from
install.ps1end-to-end before this moves off draft.🤖 Generated with Claude Code