feat: add native Windows support#74
Merged
Merged
Conversation
Claude Code, Goose, Gemini CLI, and Codex CLI all run natively on Windows. Amp is WSL-only so it remains Linux-only. This change adds the plumbing to make all four agents work on native Windows without requiring WSL. Key design decisions: - Centralize all platform branching in a new `platform.py` module (single monkeypatch target for tests; no scattered sys.platform checks). - Goose and Statusline use different config dirs on Windows (%APPDATA%/Block and %APPDATA%/claude-statusline respectively); helpers return tilde paths on Unix for backward compatibility. - Gemini CLI actively destroys symlinks when rewriting settings.json (GitHub issue #10960, closed "not planned"). Windows Gemini gets file copies instead of symlinks via a new copy_mode_targets mechanism; preserved fields are read from the live target before each cache rebuild to survive Gemini rewrites. - Windows symlinks require Developer Mode; PermissionError surfaces a step-by-step enable guide instead of a generic message. - PowerShell completions use Register-ArgumentCompleter with bash_source mode, setting COMP_WORDS/COMP_CWORD so Click returns contextual completions. - uv path resolution respects UV_TOOL_DIR override on all platforms. - CI matrix extended to windows-latest.
Replace is_windows() boolean with Platform enum (MACOS, LINUX, WINDOWS, WSL) and detect_platform() using platform.uname().release for WSL detection. Adopt shellingham for shell detection on all platforms -- PSModulePath was inherited by every Windows process, falsely identifying all shells as PowerShell. Query PowerShell for its profile path via subprocess instead of hardcoding ~/Documents (OneDrive silently redirects it). Drop click-pwsh (broken under uv).
Fix Python 3 SyntaxError from bare-comma except clauses. Rewrite PowerShell completion to use Click's bash_complete mode with correct COMP_CWORD cursor-position logic. Add backup and confirmation to create_file_copy when target has user modifications. Wire copy-mode awareness into global pending-change and first-run checks. Add APPDATA fallback for stripped Windows environments, filter Amp skills on Windows, and add shellingham upper-bound pin.
On Windows, Path.home() reads USERPROFILE (not HOME), and str(Path) uses backslashes. Tests that only set HOME caused 75 failures on Windows CI because Path.home() resolved to the real C:\Users\runneradmin. - Add autouse _patch_home_from_env fixture in conftest.py that makes Path.home() follow the HOME env var at call time via a lazy closure, so all tests that set HOME get correct isolation without any per-test changes - Update mock_home fixture to also set APPDATA for Windows parity - Fix test_agents.py: use Path.as_posix() when comparing symlink targets so ~\.claude\CLAUDE.md comparisons work cross-platform - Fix test_bootstrap_installer.py: use UV_TOOL_DIR override instead of XDG_DATA_HOME in tool-source tests (UV_TOOL_DIR is the canonical env var and takes precedence on all platforms); use as_posix() for path assertions; clear UV_TOOL_DIR in tests that check default path structure - Fix test_bootstrap_updater.py: same UV_TOOL_DIR approach for pyvenv.cfg tests - Fix test_session_search_core.py: use Path.as_posix() when comparing session_to_json path output; fix matches_term path test to not rely on forward-slash path separators in str(path) - Fix test_session_search_readers.py: also set USERPROFILE in _patch_home helper (belt-and-suspenders alongside the autouse fixture)
Four root causes: (1) Click CliRunner uses cp1252 on Windows, causing UnicodeDecodeError for skill markdown with emoji — fixed via PYTHONUTF8=1 in autouse fixture. (2) Path assertions using forward slashes fail when Path() stringifies with backslashes — normalized with .replace/as_posix(). (3) repo_score() converts POSIX session paths to backslash form, breaking all string comparisons — fixed with .as_posix() in production code.
a8e6f56 to
34f44e5
Compare
Path.read_text() without encoding uses the system default (cp1252 on Windows). SKILL.md files contain UTF-8 emoji characters that can't be decoded by cp1252, causing UnicodeDecodeError in all skill CLI commands on Windows.
Cross-platform testing belongs in the E2E workflow (e2e.yml) which runs on all 3 OSes. The matrix in ci.yml caused the job name to change from bare "checks" to "checks (ubuntu-latest)" / "checks (windows-latest)", which no longer matched the ruleset's required status check context.
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.
Adds native Windows support for Claude Code, Goose, Gemini CLI, and Codex CLI. Amp is WSL-only and remains excluded on Windows. All lifecycle operations (
install,status,diff,uninstall, completions) now work on native Windows.Stack: this PR → #78
Platform detection
platform.pyexports a 4-valuePlatformenum (MACOS,LINUX,WINDOWS,WSL) withdetect_platform()(@lru_cache),is_platform(), andis_unix_likeproperty — single monkeypatch target for testsplatform.uname().release(contains"microsoft") withWSL_DISTRO_NAMEenv var as secondary signalget_goose_config_dir,get_statusline_config_dir,get_uv_tools_dir, etc.) useis_platform(Platform.WINDOWS)internallyget_appdata_dir()falls back toPath.home() / "AppData" / "Roaming"whenAPPDATAis unset (Docker, CI)Shell detection and completions
shellinghamadopted for shell detection on all platforms — inspects the process tree via parent PID instead ofPSModulePathenv varpwsh -NoProfile -Command "Write-Output $PROFILE"instead of hardcoded path — OneDrive Folder Backup silently redirects~/Documentson modern WindowsRegister-ArgumentCompleter -NativePowerShell completion script using Click'sbash_completeprotocol with correctCOMP_CWORDcursor-position logicGemini copy-mode
settings.json— file-copy mode viacopy_mode_targetsonConfigTargetcreate_file_copyincludes backup and confirmation when target has user modificationsWindows symlink handling
PermissionErrorsurfaces Developer Mode enable guide;target_is_directorypassed tosymlink_to()(required on Windows)_is_specialized_pathuses.as_posix()instead ofstr()for backslash-safe substring matchingCI
windows-latestCross-platform test suite fixes
_patch_home_from_envfixture makesPath.home()followHOMEenv var via lazy closure — isolates tests from the real user home on Windows CImock_homefixture setsUSERPROFILEandAPPDATAalongsideHOME— Windows code paths that read these env vars resolve to the temp dirPYTHONUTF8=1set in autouse fixture — preventscp1252UnicodeDecodeErrorwhen Click'sCliRunnercaptures Rich output containing emoji.as_posix()throughout —str(WindowsPath)produces backslashes that break forward-slash string comparisonsUV_TOOL_DIRcleared/overridden in bootstrap tests — Windows CI runners set this env var, overriding the default XDG-based path structurerepo_score()in session-search normalizes session paths via.as_posix()— session data contains POSIX paths from macOS/Linux hostsRelated: shell-configs#64, shell-configs#65