Skip to content

feat: native Windows support (all phases, feature branch)#117

Closed
George-iam wants to merge 7 commits intomainfrom
feat/native-windows-support-20260417
Closed

feat: native Windows support (all phases, feature branch)#117
George-iam wants to merge 7 commits intomainfrom
feat/native-windows-support-20260417

Conversation

@George-iam
Copy link
Copy Markdown
Contributor

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-code runs 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)

  1. atomicWrite fsync fix — opened a read-only fd for fsync, which on Windows returns EPERM (FlushFileBuffers requires write access). Switched to write-through-fd pattern matching appendLine. Fixes 12 test failures in engine.test.ts.
  2. findClaudePath / whichwhere.exe on Windowswhich is not a Windows command; setup leaked 'which' is not recognized stderr at every run.
  3. execSync("sleep 0.05")Atomics.wait — POSIX sleep isn't on cmd.exe. SharedArrayBuffer + Atomics.wait is a real cross-platform sync sleep with no subprocess spawn.
  4. 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

  1. scripts/run-tests.mjs + package.json — replaces the POSIX shell glob test/*.test.ts with a cross-platform Node runner.
  2. test/audit-dedup.test.tsspawn("npx") needs .cmd + shell:true on Windows; generated worker script used backslash-bearing paths unescaped (interpreted as escape sequences); hardcoded /tmp.
  3. test/agent-sdk-paths.test.tsnew URL(...).pathname produces /C:/... on Windows; switched to fileURLToPath.
  4. test/telemetry.test.ts — skip sets file mode 0600 on Windows (POSIX modes are a no-op there).
  5. test/auth-config.test.ts — mocked only $HOME; Node's os.homedir() reads %USERPROFILE% on Windows. Mock both.

Phase 2 — Hooks cross-platform

  1. configureHooks() in src/cli.ts — generated hook commands now use absolute node.exe + absolute axme-code.js path, all segments quoted. Removes PATH dependency that was breaking on Windows (shebang-only entry can't be executed by cmd.exe).
  2. build.mjs — emits dist/axme-code.cmd and dist/plugin/bin/axme-code.cmd shims for direct-invoke on Windows.
  3. Plugin SessionStart — moved test -d ... || npm install POSIX shell fragment into check-init subcommand as Node code. Plugin hooks.json commands simplified to node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" <subcmd> — cross-platform by design.

Phase 3 — Distribution

  1. install.ps1 — downloads latest Node bundle from GitHub Releases, saves as axme-code.js under %LOCALAPPDATA%\Programs\axme-code\, generates the .cmd wrapper, adds to User PATH.
  2. release-binary.yml matrix — adds windows-x64 and windows-arm64 targets (built on ubuntu-latest since esbuild output is platform-agnostic).
  3. README Quick Start — parallel Linux/macOS and Windows one-liners.

Phase 4 — CI

  1. .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

  • Linux: npm test 511/511 pass (regression guard)
  • Windows native (Azure D2s_v5 Win11 Pro 24H2, Node 20.20.2):
    • npm test 509 pass, 1 skip (chmod 0o600), 0 fail
    • axme-code setup creates full .axme-code/ (deterministic + real LLM scanners via OAuth)
    • Generated hook command piped through cmd.exe /c correctly blocks rm -rf / and allows ls
    • Plugin check-init with CLAUDE_PLUGIN_ROOT set triggers lazy npm install of the SDK
    • install.ps1 parses clean (481 tokens) and end-to-end path works until 404 on the v0.2.9 asset that doesn't exist yet

What's NOT verified (blocking merge)

  • CI on all three platforms — this PR is the trigger. Will iterate until green.
  • End-to-end Windows release install (needs a release with axme-code-windows-* assets). Will be manually verified on the first release cut from this branch.
  • Beta test with a real Windows user (Phase 5).

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.ps1 end-to-end before this moves off draft.

🤖 Generated with Claude Code

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.
@George-iam George-iam closed this Apr 17, 2026
@George-iam
Copy link
Copy Markdown
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant