From 3e7314e99aafd8042f79ff490a1336e09ddf21ad Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Mon, 27 Apr 2026 22:29:57 -0500 Subject: [PATCH 1/4] fix(install.sh): honor AIRC_CHANNEL on fresh install (vhsm's catch) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caught by vhsm-d1f4 2026-04-28 during the #191 release-gate fresh- install verification: \`AIRC_CHANNEL=canary curl|bash\` silently landed on main, requiring a follow-up \`airc canary && airc update\` dance. The fresh-install branch (line 495 pre-fix) was \`git clone\` without specifying a branch, so it defaulted to the repo's default (main) regardless of the env var. The update-existing branch already honored \$SAVED_CHANNEL via \$CLONE_DIR/.channel; only the cold-start path was broken. Fix: 1. \$CHANNEL_TARGET = \${AIRC_CHANNEL:-main}, validated against the known list (main, canary) — unknown values fall back to main with a warning rather than failing later with an obscure git error. 2. \`git clone --branch \$CHANNEL_TARGET\` lands directly on the requested branch. 3. Write \$CLONE_DIR/.channel after clone so future \`airc update\` stays on the same channel (matches what \`airc canary\` / \`airc main\` would write). Verified locally: AIRC_CHANNEL=canary lands on canary HEAD; default lands on main; bogus value falls back to main with the warning. --- install.sh | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 301be38..c6495e7 100755 --- a/install.sh +++ b/install.sh @@ -493,8 +493,25 @@ EOF exit 1 fi else - info "Installing AIRC" - git clone --quiet "$REPO_URL" "$CLONE_DIR" + # First install. Honor AIRC_CHANNEL if set so users can land on canary + # directly via `AIRC_CHANNEL=canary curl|bash` without a follow-up + # `airc canary && airc update` dance. Default to main (the release + # branch) when AIRC_CHANNEL is unset. Caught by vhsm-d1f4 2026-04-28 + # during the #191 release-gate fresh-install verification: env var was + # silently ignored, install landed on main. + CHANNEL_TARGET="${AIRC_CHANNEL:-main}" + case "$CHANNEL_TARGET" in + main|canary) ;; + *) + warn "AIRC_CHANNEL='$CHANNEL_TARGET' is not a known channel (main, canary). Defaulting to main." + CHANNEL_TARGET="main" + ;; + esac + info "Installing AIRC (channel: $CHANNEL_TARGET)" + git clone --quiet --branch "$CHANNEL_TARGET" "$REPO_URL" "$CLONE_DIR" + # Persist the channel choice so future `airc update` follows the same + # branch. Mirrors what `airc canary` / `airc main` write. + echo "$CHANNEL_TARGET" > "$CLONE_DIR/.channel" fi # ── airc on PATH ─────────────────────────────────────────────────────── From 51bc1e85929d2822e0e9f28719c077eedf84b84e Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Mon, 27 Apr 2026 22:43:47 -0500 Subject: [PATCH 2/4] fix(install.sh): make Windows-from-bash work end-to-end (no PowerShell ask) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Joel 2026-04-28: "is anyone running claude or codex from inside powershell?" — basically nobody. Real users are in Git Bash via Claude Code / Codex on Windows, and we were forcing them to switch shells just to install. Bad onboarding. install.sh on MSYS already covered most of the Windows setup (winget prereqs, OpenSSH.Server capability, HNS port-22, firewall, sshd start). Two gaps closed here: 1. **DefaultShell registry write** (#98). The elevated PowerShell payload now also writes HKLM:\SOFTWARE\OpenSSH\DefaultShell to Git for Windows bash.exe. Without this, every Windows airc HOST silently fails inbound `airc msg` because OpenSSH's default shell is cmd.exe, which lacks `cat`, POSIX redirects, and the rest of the vocabulary airc remote commands assume. Bash candidates + PATH lookup + idempotent registry write. 2. **Tailscale via winget** (#94). install_tailscale's case statement now has an MSYS branch using `winget install --id Tailscale.Tailscale` (proper case — winget --exact is case-sensitive). Previously install.sh on Git Bash skipped Tailscale entirely. Result: a Windows user pasting AIRC_CHANNEL=canary bash -c "$(curl -fsSL https://raw.githubusercontent.com/CambrianTech/airc/canary/install.sh)" into their Git Bash terminal gets the FULL Windows host setup in one shot — winget prereqs + Tailscale + sshd + DefaultShell — without ever opening a PowerShell window. One UAC prompt for the elevated sshd payload, that's it. install.ps1 stays for the edge case where someone wants airc.ps1 (PowerShell-native) — that path still installs pwsh + wires airc.cmd / airc.ps1 to %USERPROFILE%\AppData\Local\Programs\airc, which bash install.sh deliberately does not (Git Bash users use the bash airc via ~/.local/bin). --- install.sh | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index c6495e7..6310c93 100755 --- a/install.sh +++ b/install.sh @@ -231,6 +231,11 @@ _ensure_sshd_running() { # rule + start + persist. Idempotent — the inner commands check # state before writing, so re-running install on a healthy box # doesn't re-prompt or duplicate state. + # DefaultShell = Git for Windows bash (#98). Without this, every + # Windows airc HOST silently fails inbound `airc msg` from peers + # because the OpenSSH default shell is cmd.exe, which lacks `cat`, + # `>>`, and the rest of the POSIX vocabulary airc remote commands + # rely on. Locate bash.exe; idempotent registry write. local _elevated_payload=' $ErrorActionPreference = "Stop"; try { @@ -245,7 +250,18 @@ try { } Start-Service sshd; Set-Service -Name sshd -StartupType Automatic; - Write-Host "airc: sshd ready (capability + HNS + firewall + service auto-start)"; + $bashCandidates = @("C:\Program Files\Git\bin\bash.exe", "C:\Program Files (x86)\Git\bin\bash.exe", "$env:USERPROFILE\AppData\Local\Programs\Git\bin\bash.exe"); + $bashPath = $null; + foreach ($c in $bashCandidates) { if (Test-Path $c) { $bashPath = $c; break } } + if (-not $bashPath) { $cmd = Get-Command bash.exe -ErrorAction SilentlyContinue; if ($cmd) { $bashPath = $cmd.Source } } + if ($bashPath) { + $cur = (Get-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -ErrorAction SilentlyContinue).DefaultShell; + if ($cur -ne $bashPath) { + if (-not (Test-Path "HKLM:\SOFTWARE\OpenSSH")) { New-Item -Path "HKLM:\SOFTWARE\OpenSSH" -Force | Out-Null } + New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value $bashPath -PropertyType String -Force | Out-Null + } + } + Write-Host "airc: sshd ready (capability + HNS + firewall + service auto-start + DefaultShell=bash)"; } catch { Write-Host "airc-elevated-error: $_" } ' case "$_state" in @@ -289,6 +305,7 @@ tailscale_present() { install_tailscale() { # Optional. macOS: brew cask. Linux: tailscale's official installer. + # Windows Git Bash: winget (case-sensitive id, see #94). tailscale_present && return 0 case "$(uname -s)" in Darwin) @@ -304,6 +321,17 @@ install_tailscale() { else warn "curl missing; install Tailscale manually: https://tailscale.com/download/linux" fi ;; + MINGW*|MSYS*|CYGWIN*) + # Windows Git Bash: winget. Package id is case-sensitive (#94 — + # 'tailscale.tailscale' lowercase silently fails; 'Tailscale.Tailscale' + # is the actual id). Mirrors install.ps1's Install-IfMissing line. + local wbin; wbin=$(command -v winget.exe 2>/dev/null || command -v winget 2>/dev/null || true) + if [ -n "$wbin" ]; then + "$wbin" install --id Tailscale.Tailscale --silent --accept-source-agreements --accept-package-agreements 2>&1 \ + || warn "Tailscale install via winget failed; install manually: https://tailscale.com/download/windows" + else + warn "winget not present; install Tailscale manually: https://tailscale.com/download/windows" + fi ;; *) warn "Don't know how to install Tailscale on $(uname -s); see https://tailscale.com/download" ;; esac From e4e04a7280a713312114eaa2273d3796f1e561ce Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Mon, 27 Apr 2026 22:45:59 -0500 Subject: [PATCH 3/4] docs(README): bash install.sh is canonical for everyone (incl. Windows Git Bash) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Joel 2026-04-28: \"is anyone running claude or codex from inside powershell?\" — basically nobody. Real users on Windows are in Git Bash via Claude Code / Codex / Cursor / opencode / Windsurf / openclaw. Pointing them at install.ps1 and 'open PowerShell' was bad onboarding that we have to get perfect or we get no users. Demote install.ps1 to a side note for the rare native-PowerShell user who specifically wants airc.ps1. Lead with bash install.sh as the universal entry point. The companion install.sh changes (in this same PR) make MSYS path bulletproof: winget prereqs + Tailscale + sshd capability + HNS port-22 + firewall + DefaultShell=bash.exe, all behind one UAC prompt. Two install sections updated (top, and the Setup block at line 120). Skills already used the bash form everywhere so no skill changes needed. --- README.md | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b03eac3..827af3a 100644 --- a/README.md +++ b/README.md @@ -14,17 +14,15 @@ ## Install -**macOS / Linux / WSL** (bash): +**Any platform** (bash — works from macOS / Linux / WSL / Windows Git Bash): ```bash curl -fsSL https://raw.githubusercontent.com/CambrianTech/airc/main/install.sh | bash ``` -**Windows** (PowerShell — works from the default Windows PowerShell 5.1; bootstraps pwsh 7 + every other prereq via winget): +This is the install command for everyone running Claude Code, Codex, Cursor, opencode, Windsurf, or openclaw — all of which use bash on every platform including Windows. On Windows, install.sh detects Git Bash, installs prereqs via winget, and self-elevates once for OpenSSH server + DefaultShell setup. You stay in your terminal — no PowerShell switch. -```powershell -iwr https://raw.githubusercontent.com/CambrianTech/airc/main/install.ps1 | iex -``` +> **Native-PowerShell users (rare):** if you specifically want `airc.ps1` (the PowerShell port, not the bash one), use `iwr https://raw.githubusercontent.com/CambrianTech/airc/main/install.ps1 | iex` instead. Most users don't need this — Claude Code / Codex / etc. on Windows run in Git Bash, where `install.sh` is the right entry. One command. Puts `airc` on your `PATH` and installs the Claude Code skills automatically. Other agents (Codex, Cursor, opencode, Windsurf, openclaw) get their integration files at [`integrations/`](integrations/). @@ -120,19 +118,15 @@ This isn't a knock on the federation protocols — they solve real enterprise fe ## Install -**macOS / Linux / WSL**: +**Every platform** (macOS / Linux / WSL / Windows Git Bash): ```bash curl -fsSL https://raw.githubusercontent.com/CambrianTech/airc/main/install.sh | bash ``` -**Windows** (PowerShell): - -```powershell -iwr https://raw.githubusercontent.com/CambrianTech/airc/main/install.ps1 | iex -``` +Puts `airc` on your `PATH` and installs Claude Code skills automatically. Auto-installs every prereq (gh, openssl, python3, openssh-client, optional tailscale) via the platform's package manager (brew / apt / dnf / pacman / apk / winget). On Windows it self-elevates once for OpenSSH Server + DefaultShell setup; you stay in your terminal. -Puts `airc` on your `PATH` and installs Claude Code skills automatically. Both installers auto-install every prereq (gh, openssl, python3, openssh-client, optional tailscale) via the platform's package manager (brew / apt / dnf / pacman / apk / winget). +> **Native-PowerShell users:** rare, but if you specifically want the PowerShell port `airc.ps1` instead of the bash binary, use `iwr https://raw.githubusercontent.com/CambrianTech/airc/main/install.ps1 | iex`. The bash install.sh is the right entry for everyone running Claude Code / Codex / Cursor on Windows (which all default to Git Bash). ## 30-Second Setup From cf8cb76362b73b1681575e9cf368e683df27893a Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Mon, 27 Apr 2026 22:52:10 -0500 Subject: [PATCH 4/4] fix(install.sh): post-install message stops claiming Tailscale isn't there when it just got installed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Joel 2026-04-28: 'Cross lan mesh? tailscale is optional but recommended. Well guess fucking what it is installed sooooo. fail?' The end-of-install banner unconditionally printed 'Tailscale is optional but recommended: https://tailscale.com' even after winget had just installed it 30 seconds earlier. Reads as 'install failed' to the user. Three states now handled: - Not installed → show the optional/URL line (was always shown) - Installed, logged out → ts_post_check warns + shows sign-in path - Installed, logged in → silent (best UX) Plus extend ts_post_check + tailscale_present to find Tailscale on Windows Git Bash (`/c/Program Files/Tailscale/tailscale.exe`) — winget installs there, PATH may not include it in the current shell yet, so the bare `command -v tailscale` would have returned false and the post install would have nagged users to install something already installed. --- install.sh | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/install.sh b/install.sh index 6310c93..9eb886e 100755 --- a/install.sh +++ b/install.sh @@ -297,9 +297,13 @@ tailscale_present() { # `tailscale` on PATH — `command -v tailscale` then lies about a missing # install and we'd brew-cask over the user's working Tailscale (sudo # prompt + kernel extension churn). Check the GUI bundle path too. + # Windows Git Bash: winget installs to Program Files; PATH may not + # include it in the current shell yet. Same trap. command -v tailscale >/dev/null 2>&1 && return 0 [ -d /Applications/Tailscale.app ] && return 0 [ -x /Applications/Tailscale.app/Contents/MacOS/Tailscale ] && return 0 + [ -x "/c/Program Files/Tailscale/tailscale.exe" ] && return 0 + [ -x "/c/Program Files (x86)/Tailscale/tailscale.exe" ] && return 0 return 1 } @@ -604,6 +608,13 @@ ts_post_check() { ts_bin="tailscale" elif [ -x /Applications/Tailscale.app/Contents/MacOS/Tailscale ]; then ts_bin="/Applications/Tailscale.app/Contents/MacOS/Tailscale" + elif [ -x "/c/Program Files/Tailscale/tailscale.exe" ]; then + # Windows Git Bash: winget installs Tailscale to Program Files; + # PATH may not yet include it in the current shell. Mirror + # airc.ps1's resolve_tailscale_bin candidates. + ts_bin="/c/Program Files/Tailscale/tailscale.exe" + elif [ -x "/c/Program Files (x86)/Tailscale/tailscale.exe" ]; then + ts_bin="/c/Program Files (x86)/Tailscale/tailscale.exe" fi [ -z "$ts_bin" ] && return 0 # not installed, nothing to nag about @@ -621,10 +632,16 @@ ts_post_check() { else info "Sign in: tailscale up" fi ;; + MINGW*|MSYS*|CYGWIN*) + info "Click the Tailscale tray icon to sign in, or run: tailscale up" + info "Do this BEFORE 'airc join', or cross-machine joins will hang." ;; *) info "Sign in: tailscale up (follow the printed URL)" ;; esac ;; + *) + # Logged in / running normally — silent (good UX, nothing to nag). + ;; esac } @@ -635,10 +652,20 @@ ts_post_check echo "" ok "Installed." echo "" -echo " Cross-LAN mesh? Tailscale is optional but recommended:" -echo " https://tailscale.com (then: tailscale up)" -echo " Same-LAN mesh works without it; gist orchestration handles either." -echo "" +# Tailscale post-install message — be honest about installed state. The +# pre-fix text always read "Tailscale is optional but recommended: +# https://tailscale.com" even when winget had just installed it 30s ago, +# which (per Joel 2026-04-28) reads as a fail. ts_post_check above +# already nudges sign-in if installed-but-logged-out, so here we only +# print the "go install it" line when tailscale really isn't present. +if tailscale_present; then + : # ts_post_check handled the messaging if relevant +else + echo " Cross-LAN mesh? Tailscale is optional (not installed):" + echo " https://tailscale.com (then: tailscale up)" + echo " Same-LAN mesh works without it; gist orchestration handles either." + echo "" +fi echo " Next:" echo " 1. gh auth login -s gist # one-time, browser flow" echo " 2. airc join # auto-#general (joins existing or hosts)"