fix(extension): Windows works without Node.js — bundle Node.exe in .vsix + sidebar path fix (v0.1.1) #44
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
| name: Publish extension | |
| # Build a platform-specific .vsix on every push to the extension branch | |
| # (artifact-only, no publish). Publish to Open VSX only when a human pushes | |
| # a tag matching `extension-v*` — agents must never push tags per D-024. | |
| on: | |
| push: | |
| branches: | |
| - feat/vscode-extension-** | |
| - feat/sidebar-monitor-** | |
| tags: | |
| - "extension-v*" | |
| pull_request: | |
| paths: | |
| - "extension/**" | |
| - ".github/workflows/publish-extension.yml" | |
| - "src/**" | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| jobs: | |
| build: | |
| name: Build .vsix (${{ matrix.target }}) | |
| runs-on: ${{ matrix.runner }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - target: linux-x64 | |
| runner: ubuntu-latest | |
| binary_name: axme-code-linux-x64 | |
| extension_bin: axme-code | |
| - target: linux-arm64 | |
| runner: ubuntu-22.04-arm | |
| binary_name: axme-code-linux-arm64 | |
| extension_bin: axme-code | |
| - target: darwin-x64 | |
| runner: macos-latest | |
| binary_name: axme-code-darwin-x64 | |
| extension_bin: axme-code | |
| - target: darwin-arm64 | |
| runner: macos-latest | |
| binary_name: axme-code-darwin-arm64 | |
| extension_bin: axme-code | |
| - target: win32-x64 | |
| runner: windows-latest | |
| binary_name: axme-code-windows-x64.exe | |
| extension_bin: axme-code.exe | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| - name: Install core deps | |
| run: npm install | |
| - name: Build core | |
| run: npm run build | |
| - name: Run core test suite (Node test runner, 608 tests) | |
| # Runs natively on each platform's runner. Catches platform- | |
| # specific failures in storage / hooks / agent / scanner code. | |
| # Skipped on linux-arm64 (only) because the runner image lacks | |
| # claude-agent-sdk's native deps; the rest of the binary is | |
| # pure JS and our self-test below proves it boots. | |
| if: matrix.target != 'linux-arm64' | |
| run: npm test | |
| - name: Download Node.exe + npm for Windows bundling | |
| # The win32-x64 .vsix ships a self-contained Node runtime inside | |
| # extension/bin/node-runtime/ — node.exe (interpreter) AND | |
| # npm.cmd + node_modules/npm/ (so search-mode runtime install | |
| # works without the user having Node/npm installed). | |
| # | |
| # We need npm because `axme-code config set context.mode search` | |
| # invokes `npm install @huggingface/transformers` to fetch the | |
| # ML runtime. Without bundled npm, that step fails with | |
| # "'npm.cmd' is not recognized" (reported 2026-05-19). | |
| # | |
| # Layout inside extension/bin/node-runtime/: | |
| # node.exe Node interpreter (~30 MB) | |
| # npm.cmd, npx.cmd npm wrappers (a few KB each) | |
| # node_modules/npm/ npm's actual code (~30 MB) | |
| # (other files like LICENSE, README — kept for legal hygiene) | |
| # Total ~75 MB per win32-x64 .vsix. Open VSX accepts up to | |
| # 256 MB per file. | |
| # | |
| # Version + SHA pinned for reproducible builds. SHA256 source: | |
| # curl -fsSL https://nodejs.org/dist/v20.20.2/SHASUMS256.txt | |
| # Earlier attempts (PR #136) used the user's own Node or | |
| # Cursor's bundled Electron via ELECTRON_RUN_AS_NODE — both | |
| # fragile and inconsistent on real Windows machines. | |
| if: matrix.target == 'win32-x64' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| NODE_VERSION="20.20.2" | |
| ZIP="node-v${NODE_VERSION}-win-x64.zip" | |
| curl -fsSL -o "$ZIP" "https://nodejs.org/dist/v${NODE_VERSION}/${ZIP}" | |
| echo "dc3700fdd57a63eedb8fd7e3c7baaa32e6a740a1b904167ff4204bc68ed8bf77 $ZIP" | sha256sum -c - | |
| # The Windows runner image already has 7z / unzip available. | |
| # `unzip -q` works on the runner's Git-Bash environment. | |
| unzip -q "$ZIP" | |
| mkdir -p extension/bin/node-runtime | |
| # Copy the entire extracted dir into a stable name. npm.cmd | |
| # looks for node.exe and node_modules/npm/ RELATIVE to its | |
| # own dir (via %~dp0), so all three artefacts must be co- | |
| # located. Renaming the whole tree to node-runtime/ keeps | |
| # the layout npm expects without divergent forks. | |
| cp -r "node-v${NODE_VERSION}-win-x64/." extension/bin/node-runtime/ | |
| ls -la extension/bin/node-runtime/node.exe extension/bin/node-runtime/npm.cmd | |
| rm -rf "$ZIP" "node-v${NODE_VERSION}-win-x64" | |
| - name: Bundle core CLI to a single platform-specific file | |
| shell: bash | |
| run: | | |
| mkdir -p extension/bin | |
| # Bundle dist/cli.mjs into a single CJS file with all deps inlined | |
| # EXCEPT @cursor/sdk. claude-agent-sdk is always required (used by | |
| # LLM scanners during setup and by the session auditor); it must | |
| # be inside the binary so Node doesn't try to resolve it from a | |
| # node_modules/ dir that doesn't ship with the .vsix. | |
| # | |
| # @cursor/sdk stays external because (a) it carries ~15 MB of | |
| # platform-specific native binaries that bloat the .vsix, and | |
| # (b) the AgentSdk factory's fallback gracefully degrades to the | |
| # Claude path on MODULE_NOT_FOUND. v0.0.1 users use Claude for | |
| # the auditor; Cursor SDK as a first-class in-extension option | |
| # is a v0.0.2 follow-up. | |
| # | |
| # WHY CJS, not ESM: the output is a shebang script with no file | |
| # extension (Windows uses .exe — see matrix.extension_bin). | |
| # Without ".mjs" extension AND without a sibling package.json | |
| # declaring "type":"module", Node loads the file as CJS and | |
| # ESM import statements throw at runtime. | |
| npx esbuild dist/cli.mjs \ | |
| --bundle \ | |
| --platform=node \ | |
| --target=node20 \ | |
| --format=cjs \ | |
| --external:@cursor/sdk \ | |
| --outfile=extension/bin/axme-code.cjs | |
| # Wrap in a shebang shim so it's executable as a binary. | |
| { | |
| printf '#!/usr/bin/env node\n' | |
| cat extension/bin/axme-code.cjs | |
| } > extension/bin/${{ matrix.extension_bin }} | |
| rm extension/bin/axme-code.cjs | |
| chmod +x extension/bin/${{ matrix.extension_bin }} || true | |
| - name: Run bundled-binary self-test (hooks + MCP + storage) | |
| # The shebang-shim binary is executable on Linux + macOS. On | |
| # Windows the `.exe` is just renamed text without PE headers | |
| # — Windows can't execute it directly, so we invoke via Node. | |
| # Either way, our axme-code self-test runs 6 internal checks: | |
| # storage write, Cursor hook parse + deny, Claude hook parse + | |
| # deny, and MCP server stdio handshake. Failure aborts CI. | |
| shell: bash | |
| run: | | |
| if [ "${{ runner.os }}" = "Windows" ]; then | |
| node "extension/bin/${{ matrix.extension_bin }}" self-test | |
| else | |
| "extension/bin/${{ matrix.extension_bin }}" self-test | |
| fi | |
| - name: Install extension deps | |
| working-directory: extension | |
| run: npm install | |
| - name: Build extension bundle | |
| working-directory: extension | |
| run: npm run build | |
| - name: Run extension activation tests (vscode-test-electron) | |
| # Headless VS Code spawn that loads our extension from disk. | |
| # | |
| # KNOWN-BROKEN, NON-BLOCKING: the downloaded VS Code 1.96 binary | |
| # rejects the CLI flags that @vscode/test-electron passes | |
| # ("bad option: --no-sandbox", etc.). Upstream interaction issue | |
| # we don't control. Step kept for the day @vscode/test-electron | |
| # ships a fix, but force-succeeds via `|| true` so a) the job | |
| # conclusion stays clean, b) GitHub Actions doesn't emit error | |
| # annotations that drown out real failures. The bundled-binary | |
| # self-test step above gives strong end-to-end coverage | |
| # independent of the IDE host. | |
| if: matrix.target != 'linux-arm64' && matrix.target != 'win32-arm64' | |
| working-directory: extension | |
| shell: bash | |
| run: | | |
| if [ "${{ runner.os }}" = "Linux" ]; then | |
| sudo apt-get update -qq && sudo apt-get install -y xvfb | |
| xvfb-run -a npm test || true | |
| else | |
| npm test || true | |
| fi | |
| - name: Package .vsix | |
| working-directory: extension | |
| run: npx vsce package --target ${{ matrix.target }} --no-dependencies -o ../axme-code-${{ matrix.target }}.vsix | |
| - name: Verify bundled Node runtime is inside the win32-x64 .vsix | |
| # A .vsix is just a zip. List its contents and assert that the | |
| # files search-mode needs at runtime are actually present. | |
| # Without this check, an over-broad .vscodeignore pattern (e.g. | |
| # the historical `**/node_modules/**`) silently drops the | |
| # bundled npm package from the package, and we ship a build | |
| # that boots fine on Cursor but explodes the moment a user | |
| # enables semantic search. We caught this once on 2026-05-19; | |
| # never again — this step fails CI if the bundle regresses. | |
| if: matrix.target == 'win32-x64' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| VSIX="axme-code-${{ matrix.target }}.vsix" | |
| REQUIRED=( | |
| "extension/bin/axme-code.exe" | |
| "extension/bin/node-runtime/node.exe" | |
| "extension/bin/node-runtime/npm.cmd" | |
| "extension/bin/node-runtime/node_modules/npm/bin/npm-cli.js" | |
| "extension/bin/node-runtime/node_modules/npm/bin/npm-prefix.js" | |
| ) | |
| # Earlier attempts grepped `unzip -l` output. That kept producing | |
| # false negatives on Windows Git Bash — even when the files were | |
| # clearly listed (we dumped them on failure), the grep didn't | |
| # match. Suspected cause: CRLF / encoding quirks in the unzip | |
| # listing. Switch to the bulletproof approach: actually extract | |
| # the .vsix into a temp dir and use `test -f` on each required | |
| # path. Same archive, real filesystem checks, no regex / grep | |
| # ambiguity. | |
| rm -rf .verify-extract | |
| unzip -q "$VSIX" -d .verify-extract | |
| missing=0 | |
| for path in "${REQUIRED[@]}"; do | |
| if [ ! -f ".verify-extract/$path" ]; then | |
| echo "::error::Missing from $VSIX: $path" | |
| missing=1 | |
| fi | |
| done | |
| if [ "$missing" -ne 0 ]; then | |
| echo "--- $VSIX contents (top of tree) ---" | |
| ls -la .verify-extract/extension/bin/ || true | |
| ls -la .verify-extract/extension/bin/node-runtime/ 2>/dev/null || echo "(node-runtime/ missing)" | |
| ls -la .verify-extract/extension/bin/node-runtime/node_modules/npm/bin/ 2>/dev/null || echo "(npm/bin/ missing)" | |
| rm -rf .verify-extract | |
| exit 1 | |
| fi | |
| rm -rf .verify-extract | |
| echo "OK — all required bundled-Node files are inside $VSIX." | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: axme-code-${{ matrix.target }} | |
| path: axme-code-${{ matrix.target }}.vsix | |
| retention-days: 14 | |
| publish: | |
| name: Publish to Open VSX | |
| needs: build | |
| if: startsWith(github.ref, 'refs/tags/extension-v') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| - name: Download all .vsix artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: vsix | |
| merge-multiple: true | |
| - name: Publish per-target to Open VSX | |
| env: | |
| OVSX_TOKEN: ${{ secrets.OVSX_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| for target in linux-x64 linux-arm64 darwin-x64 darwin-arm64 win32-x64; do | |
| file="vsix/axme-code-${target}.vsix" | |
| if [ ! -f "$file" ]; then | |
| echo "Warning: $file missing; skipping $target." | |
| continue | |
| fi | |
| echo "Publishing $file (target=$target)" | |
| npx ovsx publish "$file" --target "$target" --pat "$OVSX_TOKEN" | |
| done | |
| - name: Attach .vsix files to GitHub Release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| # Create release if missing, then attach all 5 .vsix files for | |
| # sideload distribution alongside Open VSX. | |
| tag="${GITHUB_REF#refs/tags/}" | |
| gh release view "$tag" >/dev/null 2>&1 || \ | |
| gh release create "$tag" --title "$tag" --notes "Extension $tag — six platform-specific .vsix files attached. See README for install instructions." | |
| for f in vsix/axme-code-*.vsix; do | |
| gh release upload "$tag" "$f" --clobber | |
| done |