feat: add phase 1 Windows native support#227
feat: add phase 1 Windows native support#227sergio-sisternes-epam merged 17 commits intomicrosoft:mainfrom
Conversation
70a75b7 to
1444ec6
Compare
1444ec6 to
496c9f1
Compare
Phase 3: Windows Package Manager Distribution — Setup RequirementsThe CI pipeline includes disabled dispatch jobs for Scoop, Chocolatey, and winget (gated with Scoop
Chocolatey
winget
Enabling the PipelineOnce the above is in place, remove the |
Why Scoop?Scoop is included specifically for enterprise developers without admin privileges — a common scenario and the exact use case that motivated #88. Unlike Chocolatey and winget, Scoop installs to The maintenance cost is minimal: a Scoop bucket is a single Git repo with one JSON manifest file — no account registration, no moderation queue, no approval process. Built-in While nobody has explicitly requested Scoop, this is a low-effort addition that covers an important gap in the distribution matrix:
|
PR Update: Windows native support — cross-platform validation on real Windows machineValidated all changes on a native Windows 11 machine (Python 3.12, Windows cp1252 locale). This commit addresses every test failure found during that validation. What changed (69 files, +1187 / -1030):Source fixes:
Test fixes:
Results on Windows:
|
There was a problem hiding this comment.
Pull request overview
Adds “phase 1” native Windows support by shipping a Windows x86_64 release artifact, introducing PowerShell-based installers/runtime setup scripts, and making update/install logic + tests platform-aware (plus ASCII-safe CLI output updates for Windows consoles).
Changes:
- Add Windows release build (zip) + installer (
install.ps1) and PowerShell runtime setup scripts. - Make
apm updatedownload/execute the correct installer per platform; add/update unit tests for platform branching. - Improve cross-platform behavior in core code/tests (path separators,
NULvs/dev/null, skip unsuitable integration tests on Windows) and replace emoji/status glyphs with ASCII-safe markers.
Reviewed changes
Copilot reviewed 83 out of 83 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_version_checker.py | Adds platform-specific cache-path tests. |
| tests/unit/test_update_command.py | Adds tests ensuring apm update selects PS1 vs SH installer. |
| tests/unit/test_script_runner.py | Normalizes path separators in assertion for Windows. |
| tests/unit/test_runtime_windows.py | Adds Windows-specific RuntimeManager/ScriptRunner behavior tests. |
| tests/unit/test_runtime_factory.py | Makes assertions more defensive via .get() (dict shape variability). |
| tests/unit/test_mcp_client_factory.py | Updates expected warning output to ASCII-safe marker. |
| tests/unit/test_install_command.py | Adds CWD restoration around temp dirs (but currently restores to test dir). |
| tests/unit/test_init_command.py | Adds CWD restoration around temp dirs (but currently restores to test dir). |
| tests/unit/test_copilot_runtime.py | Uses as_posix() for cross-platform path assertions. |
| tests/unit/test_auth_scoping.py | Uses NUL on Windows for git env hardening test. |
| tests/unit/test_ado_path_structure.py | Uses as_posix() to avoid Windows path separator issues. |
| tests/unit/integration/test_deployed_files_manifest.py | Uses as_posix() for deployed file path assertions. |
| tests/test_runnable_prompts.py | Normalizes \ to / for Windows-compatible assertions. |
| tests/test_github_downloader.py | Updates progress/output expectations + adds Windows GIT_CONFIG_GLOBAL tests. |
| tests/test_apm_resolver.py | Updates validity marker to ASCII-safe output. |
| tests/test_apm_package_models.py | Updates validation summary markers to ASCII-safe output. |
| tests/integration/test_runtime_smoke.py | Skips bash-based tests on Windows; uses os.pathsep. |
| tests/integration/test_plugin_e2e.py | Skips symlink test on Windows (admin/privilege constraints). |
| tests/integration/test_multi_runtime_integration.py | Uses .get() in runtime dict assertions. |
| tests/integration/test_compile_permission_denied.py | Skips permission-denied behavior test on Windows. |
| src/apm_cli/utils/github_host.py | Switches bullet formatting to ASCII *. |
| src/apm_cli/utils/console.py | Replaces emoji STATUS_SYMBOLS and removes emoji from table/spinner titles. |
| src/apm_cli/runtime/manager.py | Adds Windows script selection + PowerShell execution path. |
| src/apm_cli/registry/operations.py | Updates env prompt output markers to ASCII-safe. |
| src/apm_cli/primitives/models.py | Normalizes doc text arrows to ASCII. |
| src/apm_cli/output/script_formatters.py | Replaces unicode tree/markers with ASCII-safe equivalents. |
| src/apm_cli/output/formatters.py | Replaces unicode glyphs/arrows with ASCII-safe equivalents throughout output. |
| src/apm_cli/models/validation.py | Updates summary strings + normalizes punctuation in docs/comments. |
| src/apm_cli/models/dependency.py | Normalizes doc/comment arrows/punctuation to ASCII. |
| src/apm_cli/integration/skill_transformer.py | Normalizes doc arrows to ASCII. |
| src/apm_cli/integration/skill_integrator.py | Normalizes doc arrows/punctuation to ASCII. |
| src/apm_cli/integration/prompt_integrator.py | Normalizes comment punctuation to ASCII. |
| src/apm_cli/integration/mcp_integrator.py | Normalizes output markers/tree formatting to ASCII-safe. |
| src/apm_cli/integration/hook_integrator.py | Normalizes comments + improves managed-file path normalization for Windows. |
| src/apm_cli/integration/base_integrator.py | Normalizes comments + path normalization notes. |
| src/apm_cli/integration/agent_integrator.py | Normalizes comment punctuation to ASCII. |
| src/apm_cli/deps/plugin_parser.py | Normalizes docs/comments arrows to ASCII. |
| src/apm_cli/deps/lockfile.py | Normalizes comments arrows to ASCII. |
| src/apm_cli/deps/github_downloader.py | Uses NUL on Windows; adds Windows temp clone cleanup; removes emoji from progress. |
| src/apm_cli/deps/apm_resolver.py | Updates resolution summary markers to ASCII-safe. |
| src/apm_cli/core/token_manager.py | Normalizes docs arrows to ASCII. |
| src/apm_cli/core/target_detection.py | Normalizes docs arrows to ASCII. |
| src/apm_cli/core/script_runner.py | Adds Windows command splitting; normalizes output markers to ASCII-safe. |
| src/apm_cli/core/safe_installer.py | Updates success/warn/error markers to ASCII-safe. |
| src/apm_cli/compilation/link_resolver.py | Normalizes markdown link paths to forward slashes. |
| src/apm_cli/compilation/distributed_compiler.py | Normalizes cleanup output markers to ASCII-safe. |
| src/apm_cli/compilation/context_optimizer.py | Normalizes timing labels + path resolution for Windows. |
| src/apm_cli/compilation/agents_compiler.py | Normalizes preview output glyphs to ASCII-safe. |
| src/apm_cli/commands/update.py | Adds platform-aware installer URL/suffix and PowerShell execution path. |
| src/apm_cli/commands/uninstall.py | Normalizes uninstall output markers to ASCII-safe. |
| src/apm_cli/commands/runtime.py | Normalizes table titles/status output to ASCII-safe; updates arrows. |
| src/apm_cli/commands/run.py | Normalizes panel titles/output glyphs to ASCII-safe. |
| src/apm_cli/commands/prune.py | Normalizes prune output markers to ASCII-safe. |
| src/apm_cli/commands/pack.py | Normalizes pack/unpack output glyphs to ASCII-safe. |
| src/apm_cli/commands/mcp.py | Normalizes MCP command output glyphs to ASCII-safe. |
| src/apm_cli/commands/list_cmd.py | Normalizes list output/table titles to ASCII-safe. |
| src/apm_cli/commands/init.py | Normalizes init output/table content to ASCII-safe. |
| src/apm_cli/commands/deps.py | Normalizes deps output/tree drawing to ASCII-safe; warning markers updated. |
| src/apm_cli/commands/config.py | Normalizes config table title to ASCII-safe. |
| src/apm_cli/commands/compile.py | Normalizes compile help/output bullets and error markers to ASCII-safe. |
| src/apm_cli/commands/_helpers.py | Uses as_posix() for expected install path tracking. |
| src/apm_cli/cli.py | Normalizes module doc punctuation to ASCII. |
| src/apm_cli/bundle/unpacker.py | Tightens unsafe path checks when unpacking. |
| src/apm_cli/bundle/packer.py | Normalizes error message punctuation to ASCII. |
| src/apm_cli/bundle/lockfile_enrichment.py | Normalizes doc punctuation to ASCII. |
| src/apm_cli/adapters/client/vscode.py | Normalizes comment punctuation to ASCII. |
| src/apm_cli/adapters/client/copilot.py | Normalizes comment/output punctuation to ASCII. |
| src/apm_cli/adapters/client/codex.py | Normalizes warning output; improves npm args to use versioned package name. |
| scripts/runtime/setup-llm.ps1 | Adds Windows runtime setup for llm. |
| scripts/runtime/setup-copilot.ps1 | Adds Windows runtime setup for Copilot CLI. |
| scripts/runtime/setup-common.ps1 | Adds shared PowerShell helpers for runtime setup scripts. |
| scripts/runtime/setup-codex.ps1 | Adds Windows runtime setup for Codex binary. |
| scripts/github-token-helper.ps1 | Adds standalone PowerShell token helper with precedence rules. |
| install.ps1 | Adds Windows installer that downloads/installs the Windows zip release (with pip fallback). |
| docs/getting-started.md | Documents Windows install/update and runtime setup. |
| docs/cli-reference.md | Updates install/update docs for Windows installer + manual download steps. |
| README.md | Adds Windows PowerShell install snippet and notes Windows binaries. |
| .github/workflows/ci.yml | Adds Windows PR test job; build waits on both Linux+Windows tests. |
| .github/workflows/build-release.yml | Adds Windows build/test/release-validation + Windows zip packaging. |
| .github/instructions/cicd.instructions.md | Updates CI/CD docs to reflect Windows coverage. |
You can also share your feedback on Copilot code review. Take the survey.
|
@copilot open a new pull request to apply changes based on the comments in this thread |
- Fix CWD restoration in test_init_command.py and test_install_command.py to use self.original_dir instead of __file__ directory (19 occurrences) - Remove unsupported UseBasicParsing from Invoke-RestMethod in setup-codex.ps1 - Fix uv PATH in build-release.yml build job: .cargo\bin -> .local\bin to match other Windows jobs in the workflow
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 87 out of 87 changed files in this pull request and generated 4 comments.
You can also share your feedback on Copilot code review. Take the survey.
- Fix CWD restoration in test_init_command.py and test_install_command.py to use self.original_dir instead of __file__ directory (19 occurrences) - Remove unsupported UseBasicParsing from Invoke-RestMethod in setup-codex.ps1 - Fix uv PATH in build-release.yml build job: .cargo\bin -> .local\bin to match other Windows jobs in the workflow
ef6de06 to
155fd8f
Compare
- Fix CWD restoration in test_init_command.py and test_install_command.py to use self.original_dir instead of __file__ directory (19 occurrences) - Remove unsupported UseBasicParsing from Invoke-RestMethod in setup-codex.ps1 - Fix uv PATH in build-release.yml build job: .cargo\bin -> .local\bin to match other Windows jobs in the workflow
398b3be to
8645de8
Compare
danielmeppiel
left a comment
There was a problem hiding this comment.
Review: Windows Native Support (Phase 1)
Thanks for the solid work on Windows support, Sergio. The architectural approach is right — PyInstaller binary, PowerShell installer at %LOCALAPPDATA%\Programs\apm, platform-aware update routing, .ps1/.sh script selection in the runtime manager. These are good design decisions.
After a thorough review across CI/CD, PowerShell scripts, core source, and tests, there are a few items that need to be addressed before this can merge.
1. Windows must NOT be in PR CI — follow the macOS precedent
Files: ci.yml, ci-integration.yml
Our established philosophy is Linux-only for PR CI (fast, cheap feedback) with full platform coverage only in build-release.yml on release. macOS has zero presence in ci.yml or ci-integration.yml — Windows must follow the same pattern.
Currently, this PR adds test-windows and build-windows jobs to ci.yml, and 3 Windows jobs to ci-integration.yml. This doubles PR CI time and cost, and creates a coupling where a Windows test failure blocks the Linux binary build (needs: [test, test-windows]).
What to do:
- Remove all Windows jobs from
ci.ymlandci-integration.yml - Add
windows-latestas a matrix entry inbuild-release.ymlalongside the existing 4 platforms (ubuntu-24.04,ubuntu-24.04-arm,macos-15-intel,macos-latest) - Follow the exact pattern macOS uses: Windows builds and tests run post-merge on push to main and on tag releases
This keeps PR feedback fast (~3 min, Linux-only) while ensuring Windows is validated on every push to main and every release.
2. STATUS_SYMBOLS inconsistency after Unicode replacement
File: src/apm_cli/utils/console.py
The Unicode→ASCII replacement is accepted as in-scope, but it creates an inconsistency: STATUS_SYMBOLS still contains emojis (✨, 🚀, ⚙️, 💡, ⚠️, ❌, etc.) which are used by _rich_echo(), _rich_info(), _rich_warning(), and other helpers. Meanwhile, inline emojis in ~40 other files have been replaced with ASCII brackets ([+], [!], etc.) or removed entirely.
Post-merge, _rich_info("message", symbol="info") would print 💡 message while a direct click.echo() next to it would print [i] message. This is visually incoherent.
What to do: Update STATUS_SYMBOLS to use the same ASCII bracket notation used everywhere else, or better yet, make the symbols conditional on terminal capability (Rich already does this). The key requirement is internal consistency.
3. for_allinstruction typo in context_optimizer.py
File: src/apm_cli/compilation/context_optimizer.py
The ∀ (for-all) symbol was replaced with for_all but the space between it and the next word was lost:
# Current (broken):
"subject to: for_allinstruction -> existsplacement"
# Should be:
"subject to: for_all instruction -> exists placement"Same issue with ∃ → exists. Small fix but ships a visible bug in compile verbose output.
4. Checksum verification missing from install.ps1
File: install.ps1
The build pipeline generates .sha256 sidecar files for every release artifact, but the installer downloads and extracts the zip without verifying the checksum. The Bash installer has the same gap, but since we are writing a new installer from scratch, we should do it right.
What to do: After downloading apm-windows-x86_64.zip, also download apm-windows-x86_64.zip.sha256, verify with Get-FileHash, and fail with a clear error if the hashes don't match. This is ~5 lines of PowerShell:
$expectedHash = (Invoke-RestMethod -Uri $sha256Url).Split(" ")[0]
$actualHash = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash
if ($actualHash -ne $expectedHash) {
Write-ErrorText "Checksum verification failed. Download may be corrupted."
exit 1
}5. $LASTEXITCODE lost after pipe in install.ps1
File: install.ps1, around the pip fallback logic
& $pipCmd install --user apm-cli 2>&1 | Write-Host
if ($LASTEXITCODE -ne 0) { ... }Piping to Write-Host overwrites $LASTEXITCODE with the pipeline's result, masking pip failures. Fix:
$output = & $pipCmd install --user apm-cli 2>&1
$pipExit = $LASTEXITCODE
$output | Write-Host
if ($pipExit -ne 0) { ... }6. Unrelated functional change in codex.py
File: src/apm_cli/adapters/client/codex.py
This change adds NPM versioned package name support (package@version):
version = package.get("version", "")
versioned_name = f"{package_name}@{version}" if version else package_nameThis is a functional change unrelated to Windows support. It should either be extracted to its own PR with tests, or at minimum called out in the PR description so reviewers can evaluate it independently.
Non-blocking observations (for follow-up)
These are fine to address in subsequent PRs:
- Old release versions accumulate —
install.ps1createsreleases\v1.0.0\,releases\v1.1.0\etc. but never cleans up previous versions. Consider adding cleanup after successful install. - No UPX compression for Windows — Linux/macOS install UPX; Windows binaries will be ~50% larger. Add
choco install upxor download from GitHub in the build step if desired. - ARM64 inconsistency — Installer hardcodes
x86_64(fine, emulation works), butsetup-codex.ps1maps ARM64 toaarch64-pc-windows-msvc. Align the story. - Temp file collision in test scripts —
test-release-validation.ps1uses fixed temp file names without PID/GUID suffix. Low risk but worth fixing. - Chocolatey/Scoop/winget stubs — The
if: falsegated stubs follow the Homebrew dispatch pattern correctly. Recommend shipping Scoop first (lowest friction), then winget, then Chocolatey. Note that winget's standard flow is PRs tomicrosoft/winget-pkgs— clarify if the dispatch approach targets a custom manifest repo or automates those PRs.
Summary
The Windows platform architecture is sound. The six items above are the delta between a good PR and a great one. Items 1–5 are required changes; item 6 is a strong recommendation. Looking forward to the next iteration.
|
One more thing on the package manager stubs: Please remove the Chocolatey and winget jobs entirely. We will only ship Scoop for Windows package management. Reasoning: Chocolatey requires community moderation (days for approval on each release — we do not control our own release cadence). winget requires PRs to Scoop is self-hosted (we own What to do:
We can revisit Chocolatey/winget later if there is community demand, but we are not taking on that operational overhead now. |
- Remove Windows jobs from PR CI (ci.yml): drop test-windows and build-windows - Remove Windows jobs from ci-integration.yml: drop smoke-test-windows, integration-tests-windows, release-validation-windows; update report-status - Remove Chocolatey and winget dispatch jobs from build-release.yml (keep Scoop) - Remove Chocolatey/winget from installation docs, simplify to Scoop-only - Fix STATUS_SYMBOLS in console.py: use bracket notation to match codebase - Fix typo in context_optimizer.py: for_allinstruction -> for_all instruction - Add SHA256 checksum verification to install.ps1 after zip download - Fix $LASTEXITCODE lost in pipe to Write-Host in install.ps1 pip fallback - Revert unrelated NPM versioned package name change in codex.py
|
Thanks for the thorough review, @danielmeppiel! Really appreciate the detailed feedback — all items addressed in ea13e63:
All 1,457 tests passing. Ready for another look when you get a chance! |
…ft#88) - RuntimeManager: platform-aware script selection, PowerShell execution - ScriptRunner: platform-aware command parsing for Windows - PowerShell runtime scripts with PSScriptAnalyzer-clean verb naming - install.ps1: hardened with download fallback, pip fallback, binary test - Release validation: full test-release-validation.ps1 port - CI: Windows added to test, integration, release-validation matrices - Tests: 19 new unit tests for Windows platform support - Docs: updated getting-started and cli-reference for Windows
…, winget) - Add disabled CI dispatch jobs for Scoop, Chocolatey, and winget - Remove Windows checksums from Homebrew update job - Add Windows package manager install commands to getting-started.md Part of microsoft#88
Use [INFO], [OK], [WARN], [ERROR] instead of emoji characters for cross-platform terminal compatibility.
Windows builds use PowerShell in CI; bash script is not needed for Windows.
- Remove all non-ASCII characters from source (fixes cp1252 charmap encoding errors) - Fix path separator issues: normalize to forward slashes in link_resolver, context_optimizer, hook_integrator, _helpers, unpacker - Fix GIT_CONFIG_GLOBAL to use NUL on Windows instead of /dev/null - Fix git repo handle cleanup on Windows (explicit close + gc.collect before temp dir removal) - Fix TemporaryDirectory cleanup in tests (try/finally for os.chdir patterns) - Fix PATH separator in test_runtime_smoke.py (os.pathsep instead of hardcoded ':') - Fix path assertions in tests to be cross-platform (.as_posix(), .endswith(), .replace) - Add platform skip decorators for bash-only, symlink, and chmod tests - Add Windows-specific tests for git env, version checker cache path - All 1828 tests pass on Windows (102 skipped)
- Add test-windows job running on windows-latest in parallel with Linux - Uses PowerShell uv installer and Windows-appropriate cache paths - Build job now depends on both test and test-windows passing - Catches path separator, encoding, and platform-specific issues before merge
- Fix CWD restoration in test_init_command.py and test_install_command.py to use self.original_dir instead of __file__ directory (19 occurrences) - Remove unsupported UseBasicParsing from Invoke-RestMethod in setup-codex.ps1 - Fix uv PATH in build-release.yml build job: .cargo\bin -> .local\bin to match other Windows jobs in the workflow
- ci.yml: add build-windows job (binary build + artifact upload) - ci-integration.yml: add smoke-test-windows, integration-tests-windows, release-validation-windows jobs mirroring Linux pipeline - build-release.yml: include dependency integration scripts in artifacts - New: scripts/test-integration.ps1 (PowerShell integration test runner) - New: scripts/test-dependency-integration.ps1 (dependency integration tests) - Update report-status to depend on all 6 jobs (Linux + Windows)
- New: scripts/build-binary.ps1 (PowerShell equivalent of build-binary.sh) - ci.yml: build-windows job now uses build-binary.ps1 instead of bash - build-release.yml: split build step into Unix/Windows variants Fixes CI failure from shell: bash on Windows runners.
- build-binary.ps1: Rename-Item expects just the new name, not a full path - ci.yml: add shell: pwsh and PATH export for uv in test-windows job
- script_runner.py: use shlex.split(posix=False) on Windows to handle quoted arguments (e.g., paths with spaces, --model "gpt-4o mini") - manager.py: translate --vanilla to -Vanilla and positional version to -Version when calling PowerShell setup scripts - build-binary.ps1: show actual binary output on test failure instead of swallowing it with 2>&1; relax ErrorActionPreference for native command - apm.spec: bundle github-token-helper.ps1 alongside .sh version - tests: add quoted-arg test, platform-aware arg translation tests
GNU strip corrupts Windows PE/COFF binaries (LoadLibrary: Invalid access to memory location). Conditionally disable strip when sys.platform == win32. Strip still runs on Linux/macOS for smaller binaries.
- Remove Windows jobs from PR CI (ci.yml): drop test-windows and build-windows - Remove Windows jobs from ci-integration.yml: drop smoke-test-windows, integration-tests-windows, release-validation-windows; update report-status - Remove Chocolatey and winget dispatch jobs from build-release.yml (keep Scoop) - Remove Chocolatey/winget from installation docs, simplify to Scoop-only - Fix STATUS_SYMBOLS in console.py: use bracket notation to match codebase - Fix typo in context_optimizer.py: for_allinstruction -> for_all instruction - Add SHA256 checksum verification to install.ps1 after zip download - Fix $LASTEXITCODE lost in pipe to Write-Host in install.ps1 pip fallback - Revert unrelated NPM versioned package name change in codex.py
… README update - installation.md: Replace duplicated Windows Quick Install with actual manual download steps (Invoke-WebRequest, Expand-Archive, PATH setup) - installation.md: Remove experimental runtime setup section (belongs on dedicated runtime page) - cli-commands.md: Add Linux/macOS and Windows platform labels in Quick Install, Manual Download, and apm update Manual Update sections - scripts/: Move all .ps1 files to scripts/windows/ for clear platform separation - build-release.yml: Update all .ps1 references to scripts/windows/ - README.md: Add platform labels in Get Started section and split Other install methods into Linux/macOS (Homebrew, pip) and Windows (Scoop, pip)
0f35a80 to
5016794
Compare
The PyInstaller spec was unconditionally bundling github-token-helper.ps1, which fails on Linux/macOS since the file was moved to scripts/windows/. Now .ps1 is bundled only on Windows and .sh only on Unix.
|
as a follow up we should probably add to copilot-instructions that unicode characters not handled by Windows are banned. Great work @sergio-sisternes-epam ! |
…ring) Main branch (PR #227, Windows native support) standardized docstring arrows from '→' to '->' across the codebase. Our branch had added a local path line using '→'. Resolution: adopt main's '->' style for all arrows and keep our local path documentation line. The only conflict was in to_canonical() docstring in dependency.py. All other files auto-merged cleanly. Co-authored-by: danielmeppiel <51440732+danielmeppiel@users.noreply.github.com>
PR microsoft#227 replaced emoji characters with ASCII equivalents for Windows console compatibility. The integration test was still checking for '✨ Package installed and ready to run' which no longer matches the actual output '* Package installed and ready to run', causing the early-termination sentinel and execution_started assertion to fail. Replace all 4 emoji-dependent checks with the stable substring 'Package installed and ready to run' (no emoji dependency).
…ix emoji assertions - Extract duplicated try/finally CWD-restore into _chdir_tmp() context manager (addresses Copilot review feedback) - Fix integration test emoji assertions: replace emoji-dependent '✨ Package installed and ready to run' checks with the stable 'Package installed and ready to run' substring after PR microsoft#227 replaced emoji with ASCII in CLI output Fixes Windows PermissionError [WinError 32] when TemporaryDirectory cleanup runs while the process CWD is inside the temp dir.
Description
Add Phase 1 support for issue #88 by shipping a native Windows binary path without taking on runtime setup work yet.
This PR:
apm updatechoose the correct installer per platformFixes #88
Type of change
Testing
Local validation run:
uv sync --python 3.12 --extra dev --extra builduv run --python 3.12 pytest tests/unit/test_update_command.py -q