feat(setup): add first-class native Windows support - #44
Open
andrew-codes wants to merge 3 commits into
Open
Conversation
Nix has no native Windows support, so setup/windows.sh carries alone what nix-darwin and home-manager carry on macOS: winget packages, dotfile symlinks, activation-equivalent steps and system defaults. setup.sh dispatches to it when it detects Git for Windows' bash, so one command still works on either OS. The target is native Windows with Git Bash, not WSL, which is what the ticket describes: bash as WezTerm's default shell, and the repo already cloned with Git for Windows. home/.gitconfig-windows and home/.ssh/config-windows keep their names and their place in the existing -os include scheme; only their contents moved from WSL to native Windows. setup/windows-parity.md is the inventory the ticket asks for: every macOS package, config and system default mapped to implemented, deliberately skipped with a reason, or deferred. Notable decisions recorded there are PowerToys standing in for Raycast, twg having no Windows build at all (its installer refuses anything but macOS and Linux), and herdr left to a manual install because its only Windows path is an unpinned `irm | iex`. Supporting changes: - home/bin scripts move from zsh to bash so they run on both platforms. aup and kaup gain netstat/taskkill branches for the missing lsof. - starship's settings move out of home.nix into a tracked home/.config/starship.toml that both platforms symlink, so the prompt is genuinely shared rather than macOS only. - session-start.sh reaches through $VOLTA_HOME instead of a hard-coded ~/.volta. Validated on macOS only, since the Windows path cannot execute here: shellcheck and bash -n across every script, nix flake check plus a full darwin eval, a Lua harness proving WezTerm resolves Git Bash (which caught a nil-in-table bug that silently skipped every candidate), unit tests for the linker's backup and idempotency contract, and a stubbed end-to-end dry run whose second and third passes change nothing on disk.
…arsing, and OpenSSH step
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.
Intent
The developer is migrating their personal dev environment from Ansible playbooks to a Nix/nix-darwin/home-manager flake, currently focused on the macOS (Apple Silicon) portion while keeping the legacy Ansible/Windows path as unmigrated reference. Across the session they wanted Nix to install and configure a specific set of tools (1Password and its CLI/SSH agent, Docker, kubectl, Flux, GitOps, Terraform, Ansible, Raycast, Volta for Node, Telepresence, Lens, various AXI npm tools, Go-based CLIs like no-mistakes and treehouse, plannotator, and a Mac App Store app), migrate their custom bin scripts and zsh completions into the repo as symlinks, and convert those scripts and rebuild.sh from bash to zsh. They also asked for OS-specific gitconfig via git includes, split SSH config to keep private host entries out of the public repo, an optional ~/.env secret-stubbing mechanism, pi agent hooks/subagents wiring, disabling Spotlight indexing, making setup.sh auto-dispatch by OS/platform with a clear unsupported message, and a README centered on capabilities, install steps, and supported platforms. Explicit constraints included preferring toolchain-managed/pinned installs over unpinned curl-pipe-sh scripts, never using em dashes, not auto-adding co-author lines, and changing Homebrew cleanup to not remove unreferenced packages (with AGENTS.md updated to record that as the deliberate choice).
What Changed
setup/windows.sh, a full native-Windows setup path (Git Bash, not WSL) covering winget package installs, Nerd Font and kubeseal downloads, dotfile symlinking, registry/system defaults, 1Password commit signing, and the Windows OpenSSH Client capability, plussetup.shdispatch to it and a repo-root guard so an out-of-repo copy or the~/.local/bin/devtools-rebuildsymlink resolves correctly.home/.bash_profile,home/.bashrc(PATH, session vars,~/.envsourcing with unset-secret warnings, aliases, completions, starship), Windows-awarewezterm.luadefault_progandstarship.tomlentries, and reworkedhome/.gitconfig-windows/home/.ssh/config-windowsfor native Windows.home/bin/scripts to portable bash so they run on both platforms, including CRLF stripping inaupandkaupwhen parsing nativenetstat/tasklistoutput.setup/windows-parity.mdmapping every macOS package, config, and system default to its Windows decision, plusreadme.mdandAGENTS.mdupdates andhome.nixadjustments.Note on open items from the pipeline: two low-severity infos remain unaddressed - a
home/.bashrcsecret-name guard that lets an empty or digit-leading key reach${!_var}, and thereadme.mdinstall summary not listing the new OpenSSH Client step. The Windows-only halves ofsetup/windows.sh(winget installs, downloads, symlink step, registry defaults, OpenSSH capability) could not be executed from the macOS machine this ran on and remain unverified; shellcheck andbash -npassed, and the shared shell, bin scripts, wezterm, and setup entrypoints were exercised end to end.Risk Assessment
✅ Low: Every prior finding, including the destructive repo-root resolution bug, is fixed with a matching fail-loud guard, the docs and parity contract were updated in step, shellcheck is clean, and the only remaining items are two cosmetic robustness/documentation nits confined to the additive Windows path.
Testing
I exercised the branch the way a Windows user would meet it, from macOS: a real login shell over the new .bash_profile/.bashrc in a throwaway HOME (PATH, session vars,
/.env secrets warning, aliases, completions, starship prompt), the aup/kaup Windows branch against stub netstat/tasklist/taskkill emitting genuine CRLF, the devtools-rebuild symlink path into setup/windows.sh, ./setup.sh OS dispatch, the bash-converted home/bin commands against a scratch git repo, and the wezterm.lua Windows default-shell branch under lua. Each of the three fixes in the review commit was run at both HEAD1 and HEAD, and the before/after transcripts show the old behavior failing exactly as described (name printed as "?", a "killed" message while taskkill errored, and a repo root resolving to ~/.local) and the new behavior correct. nix eval confirms the macOS configuration still evaluates and the npm/Go/secret lists match between home.nix and setup/windows.sh. Everything passed; the only gap is that the winget installs, registry defaults, symlink step and the new OpenSSH capability step cannot execute outside Git Bash, so they remain untested here./var/folders/mc/mzp6sm4d449b277kc1h4y3kw0000gn/T/no-mistakes-evidence/01KZRJZZ0FS379JNTRKG9G2VKN/windows-bash-startup.png)Evidence: Same startup transcript as rendered HTML
Evidence: aup / kaup CRLF fix, before and after
### home/bin/aup 3000 -- Windows branch (no lsof; netstat/tasklist emit CRLF) $ aup 3000 # (before: HEAD1) ? 7421<CR> LISTENING 0.0.0.0:3000 $ aup 3000 # (after: HEAD) node.exe 7421 LISTENING 0.0.0.0:3000 ### home/bin/kaup 3000 -- Windows branch (taskkill rejects a pid with a stray CR) $ kaup 3000 # (before: HEAD1) ERROR: The process "7421<CR>" not found.<CR> Killed process 7421<CR> listening on port 3000 $ kaup 3000 # (after: HEAD) Killed process 7421 listening on port 3000Evidence: setup.sh dispatch and windows.sh repo-root resolution
$ ./setup.sh # on an unsupported platform (macOS reporting x86_64) ==> Detected: macos / x86_64 Error: Unsupported platform: macos / x86_64 Currently supported: macOS on Apple Silicon (arm64), Windows on x86_64. $/.local/bin/devtools-rebuild # (before: HEAD1) resolved repo root: .../home/.local contains setup/windows.sh: NO -- ~/.dotfiles and every dotfile symlink would point here $ ~/.local/bin/devtools-rebuild # (after: HEAD) resolved repo root: .../HEAD contains setup/windows.sh: yes $ /tmp/loose-copy/windows.sh # after: script copied out of the repo Error: could not locate the repo root (resolved ...).Evidence: wezterm default_prog on Windows and macOS, plus the shared starship prompt
$ wezterm on Windows, machine-wide Git install default_prog = { .../ProgramFiles\Git\bin\bash.exe, -l } $ wezterm on Windows, per-user Git install only (%ProgramFiles% unset) default_prog = { .../LocalAppData\Programs\Git\bin\bash.exe, -l } $ wezterm on Windows, no Git installed anywhere default_prog = (unset -- WezTerm's own default shell) $ wezterm on macOS (the branch must not fire) default_prog = (unset -- WezTerm's own default shell)Evidence: home/bin commands running under bash
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
🔧 **Rebase** - 1 issue found → auto-fixed ✅
readme.md- merge conflict rebasing onto origin/main🔧 Fix applied.
✅ Re-checked - no issues remain.
setup/windows.sh:22- SCRIPT_DIR is computed as dirname "${BASH_SOURCE[0]}"/.. which does not resolve symlinks. Step 8 links this file to ~/.local/bin/devtools-rebuild, and readme.md documentsdevtools-rebuildas the way to apply later changes on Windows. Invoked that way, BASH_SOURCE[0] is ~/.local/bin/devtools-rebuild, so SCRIPT_DIR becomes $HOME/.local. Step 3 then doesrm -f ~/.dotfilesand relinks it to $HOME/.local, and Step 8 recreates every dotfile link pointing at $HOME/.local/home/... (dangling), removing the correct links on the way; thefor script in "$SCRIPT_DIR"/home/bin/*loop matches nothing and creates a symlink literally named*. rebuild.sh already handles this withSCRIPT_DIR="${0:A:h}"and a comment explaining the devtools-rebuild case. Fix: resolve the source path first, e.g. SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)".home/bin/kaup:14- netstat is a native Windows program, so its output lines end in CRLF and the last awk field ($5, the PID) carries a trailing \r.taskkill /F /PID "1234\r"fails, its error goes to stderr (only stdout is redirected) and the script has noset -e, so kaup then prints "Killed process 1234 listening on port N" while the process is still alive. setup/windows.sh already applies| tr -d '\r'to native-program output twice, so the pattern is known; add the same here (e.g. pipe the netstat output throughtr -d '\r').home/bin/aup:14- Same CRLF problem on the read side: the PID field parsed out of netstat retains a trailing \r, sotasklist /FI "PID eq 1234\r"never matches and every row prints the?placeholder instead of a process name, with the stray CR also mangling the printf'd line in the terminal. Strip \r from the netstat output before the awk parse.setup/windows.sh:336-mkdir -p "$FONT_EXTRACT" "$FONT_DIR"is repeated verbatim on lines 335 and 336. Harmless, but one of the two should go.home/.bashrc:128- The required-secret list now exists in three places (secretEnvVars in home.nix, SECRET_ENV_VARS in setup/windows.sh, _secret_vars here), and readme.md's Customizing table instructs bumping all three. The shell copy is avoidable: the warning could iterate the keys already stubbed into ~/.env and report the ones with empty values, which keeps the list at two declarations. This changes what the warning covers slightly (any empty key in ~/.env, not a hardcoded set), so it is a call for the author.home/.gitconfig-windows:9- core.sshCommand is pinned to the absolute C:/Windows/System32/OpenSSH/ssh.exe. The reasoning for not using a baressh.exeis sound, but the OpenSSH Client is an optional Windows feature and setup/windows.sh never checks for it, so on a machine without it every git fetch/push fails with a bare 'cannot run' error and nothing in the setup output hints at the cause. A preflight[ -f /c/Windows/System32/OpenSSH/ssh.exe ] || warn ...in windows.sh would surface it at setup time.setup/windows.sh:469- home/.config/herdr is linked as a directory into both ~/.config/herdr and %APPDATA%\herdr. %APPDATA%<app> is conventionally where a Windows app writes runtime state, not just config, and the tracked directory already contains a herdr-written .plugins.lock - so anything herdr writes there (state, tokens) lands in this public checkout. That is the pattern AGENTS.md warns about for third-party writes through out-of-store symlinks. The parity doc records the double link as deliberate, and herdr is not installed on Windows today, so this is a heads-up rather than a defect.home.nix:288- The comment above the .ssh/config-os entry still says the OS-specific bits are '(the 1Password IdentityAgent path on macOS; nothing needed on WSL)'. This branch retargets the Windows path to native Windows and rewrites home/.ssh/config-windows accordingly, so the WSL reference is now wrong.🔧 Fix: Fix Windows setup script root resolution, CRLF parsing, and OpenSSH step
2 infos still open:
home/.bashrc:144- The derived-secret loop validates the key withcase "$_var" in *[!A-Za-z0-9_]*) continue, which accepts two names that are not valid bash identifiers: the empty string (a line beginning with=) and a digit-leading name (e.g.1PASSWORD_TOKEN=, plausible in this environment).${!_var:-}then aborts that command withbash: : invalid variable name/bash: 1PASSWORD_TOKEN: invalid variable nameon stderr at every shell start, and in the empty case a blank entry is appended to the reported list. Sourcing continues, so the impact is a spurious error line rather than a broken rc. Fix: extend the guard tocase "$_var" in '' | [0-9]* | *[!A-Za-z0-9_]*) continue ;; esac.readme.md:51- The Windows install summary enumerates what setup/windows.sh does, and item 7 still ends at "applies the Windows system defaults, and wires up 1Password commit signing" - it does not mention the new step that installs the Windows OpenSSH Client capability, which is the one that can raise an extra UAC prompt on a fresh machine. setup/windows-parity.md was updated; this list was not.setup/windows.sh:174- The Windows-only half of setup/windows.sh cannot be executed from this macOS machine: the script exits at its Git Bash preflight, so the winget package installs (step 1-4), the Nerd Font and kubeseal downloads, the dotfile symlink step 8, the registry/system defaults step 12, and the new OpenSSH Client capability step 13 are all unverified. Root resolution, the new repo-root guard, and every shared shell/bin/wezterm file were verified; the install and system-mutation steps need a real Windows machine before this can be called proven.bash run-shell-startup.sh <repo>- login shell against home/.bash_profile + home/.bashrc in a throwaway HOME: PATH loop, session vars, ~/.env sourcing, unset-secret warning, aliases, bin-completions, starship promptbash run-crlf-port-tools.sh <repo> 249fb25- home/bin/aup and home/bin/kaup Windows branch (no lsof; stub netstat/tasklist/taskkill emitting CRLF), run at HEAD~1 and HEAD; plus aup on a real listening port to confirm the macOS branch is unchangedbash run-setup-entrypoints.sh <repo> 249fb25- ./setup.sh unsupported-platform and windows dispatch; setup/windows.sh repo-root resolution via a/.local/bin/devtools-rebuild symlink at HEAD1 vs HEAD; new root guard against a copy outside the repobash run-bin-scripts.sh <repo>- st, glg, projs, projs alpha, gnxt, stash, rh, gwta under bash from ~/.local/bin against a scratch git repobash run-wezterm-and-prompt.sh <repo>- wezterm.lua default_prog under lua with a stubbed wezterm module for machine-wide Git, per-user-only Git, no Git, and macOS; starship prompt rendered from home/.config/starship.tomlnix eval .#darwinConfigurations.mac.system.outPath- macOS configuration still evaluates after the home.nix changesgrepcomparison of globalNpmPackages / goPackages / secretEnvVars in home.nix against GLOBAL_NPM_PACKAGES / GO_PACKAGES / SECRET_ENV_VARS in setup/windows.sh✅ **Document** - passed
✅ No issues found.
setup/windows.sh:48- shfmt -d reports diffs in setup/windows.sh (and pre-existing ones in setup/macOS.sh, home/.bashrc, rebuild.sh). The repo has no shfmt config or CI enforcing it, and applying it would collapse the deliberately aligned trailing-comment columns in WINGET_PACKAGES and the reg_* block, hurting readability. Left unformatted on purpose; adopting shfmt repo-wide would be a separate decision.✅ **Push** - passed
✅ No issues found.