$ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest function Start-DaemonIfRequested { if ($env:AUTTER_RESTART_DAEMON_AFTER_INSTALL -ne '1') { return } $daemonExe = Join-Path $HOME '.autter\bin\autter.exe' if (-not (Test-Path $daemonExe)) { Write-Warning 'Warning: Failed to locate autter.exe for daemon restart after install.' return } try { & $daemonExe bg start *> $null } catch { Write-Warning 'Warning: Failed to restart autter background service automatically.' } } function Write-ErrorAndExit { param( [Parameter(Mandatory = $true)][string]$Message ) Write-Host "Error: $Message" -ForegroundColor Red Start-DaemonIfRequested exit 1 } function Write-Success { param( [Parameter(Mandatory = $true)][string]$Message ) Write-Host $Message -ForegroundColor Green } function Write-Warning { param( [Parameter(Mandatory = $true)][string]$Message ) Write-Host $Message -ForegroundColor Yellow } function Normalize-PathString { param( [Parameter(Mandatory = $true)][string]$Path ) try { return ([IO.Path]::GetFullPath($Path.Trim())).TrimEnd('\').ToLowerInvariant() } catch { return ($Path.Trim()).TrimEnd('\').ToLowerInvariant() } } function Test-FileAvailable { param( [Parameter(Mandatory = $true)][string]$Path ) try { $stream = [System.IO.File]::Open($Path, 'Open', 'Write', 'None') $stream.Close() return $true } catch { return $false } } function Stop-AutterBackgroundService { param( [Parameter(Mandatory = $true)][string]$AutterExe, [Parameter(Mandatory = $false)][switch]$Hard ) if (-not (Test-Path -LiteralPath $AutterExe)) { return $false } $args = @('bg', 'shutdown') if ($Hard) { $args += '--hard' } try { & $AutterExe @args *> $null return $LASTEXITCODE -eq 0 } catch { return $false } } function Get-AutterManagedProcesses { param( [Parameter(Mandatory = $true)][string]$InstallDir ) $targetPaths = @( (Normalize-PathString (Join-Path $InstallDir 'autter.exe')), (Normalize-PathString (Join-Path $InstallDir 'git.exe')) ) $processes = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessId -ne $PID -and $_.ExecutablePath -and ($targetPaths -contains (Normalize-PathString $_.ExecutablePath)) }) return $processes } function Stop-AutterManagedProcesses { param( [Parameter(Mandatory = $true)][string]$InstallDir ) $processes = @(Get-AutterManagedProcesses -InstallDir $InstallDir) if ($processes.Count -eq 0) { return $false } $pids = @($processes | Sort-Object ProcessId -Unique | Select-Object -ExpandProperty ProcessId) Write-Warning ("Stopping lingering autter processes: {0}" -f ($pids -join ', ')) foreach ($managedPid in $pids) { try { Stop-Process -Id $managedPid -Force -ErrorAction Stop } catch { } } return $true } function Wait-ForFileAvailable { param( [Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string]$InstallDir, [Parameter(Mandatory = $false)][int]$MaxWaitSeconds = 300, [Parameter(Mandatory = $false)][int]$RetryIntervalSeconds = 5, [Parameter(Mandatory = $false)][int]$ForceKillAfterSeconds = 20 ) $elapsed = 0 $autterExe = Join-Path $InstallDir 'autter.exe' [void](Stop-AutterBackgroundService -AutterExe $autterExe) while ($elapsed -lt $MaxWaitSeconds) { if (Test-FileAvailable -Path $Path) { return $true } if ($elapsed -ge $ForceKillAfterSeconds) { [void](Stop-AutterBackgroundService -AutterExe $autterExe -Hard) [void](Stop-AutterManagedProcesses -InstallDir $InstallDir) } if (-not (Test-FileAvailable -Path $Path)) { if ($elapsed -eq 0) { Write-Host "Waiting for file to be available: $Path" -ForegroundColor Yellow } Start-Sleep -Seconds $RetryIntervalSeconds $elapsed += $RetryIntervalSeconds } } return $false } function Verify-Checksum { param( [Parameter(Mandatory = $true)][string]$File, [Parameter(Mandatory = $true)][string]$BinaryName ) # Local developer installs do not download a release artifact. if (-not [string]::IsNullOrWhiteSpace($env:AUTTER_LOCAL_BINARY)) { return } if ($EmbeddedChecksums -eq $ChecksumsSentinel) { Write-ErrorAndExit "Release checksums were not loaded; refusing to install $BinaryName" } # Extract expected checksum for this binary $expected = $null $entries = $EmbeddedChecksums -split '\|' foreach ($entry in $entries) { if ($entry -match "^([0-9a-fA-F]{64})\s+$([regex]::Escape($BinaryName))$") { $expected = $Matches[1].ToLowerInvariant() break } } if (-not $expected) { Write-ErrorAndExit "No checksum found for $BinaryName" } # Calculate actual checksum $hashCommand = Get-Command Get-FileHash -ErrorAction SilentlyContinue if ($null -ne $hashCommand) { $actual = (Get-FileHash -Path $File -Algorithm SHA256).Hash.ToLower() } else { $stream = [System.IO.File]::OpenRead($File) try { $sha256 = [System.Security.Cryptography.SHA256]::Create() $hashBytes = $sha256.ComputeHash($stream) $actual = ([System.BitConverter]::ToString($hashBytes)).Replace('-', '').ToLower() } finally { $stream.Dispose() if ($sha256) { $sha256.Dispose() } } } if ($expected -ne $actual) { Remove-Item -Force -ErrorAction SilentlyContinue $File Write-ErrorAndExit "Checksum verification failed for $BinaryName`nExpected: $expected`nActual: $actual" } Write-Success "Checksum verified for $BinaryName" } # Release-fill placeholders. The release workflow blindly string-replaces each # placeholder token EVERYWHERE in this file, so the guards below compare # against *Sentinel values built by concatenation — those survive the fill. # Comparing against the literal token would self-destruct on fill: the pinned # copy would ignore its version pin and skip checksum verification. # Repository ("owner/repo"); the sentinel defaults to the canonical repo. $Repo = '__REPO_PLACEHOLDER__' $RepoSentinel = '__REPO_' + 'PLACEHOLDER__' if ($Repo -eq $RepoSentinel) { $Repo = 'autter-dev/autter-cli' } # Version pin (e.g. "v1.6.8") in release copies; the sentinel means "latest". $PinnedVersion = '__VERSION_PLACEHOLDER__' $VersionSentinel = '__VERSION_' + 'PLACEHOLDER__' # Pipe-separated "sha256 filename" entries in release copies. Public installer # copies replace the sentinel by downloading the release's checksums.txt. $EmbeddedChecksums = '__CHECKSUMS_PLACEHOLDER__' $ChecksumsSentinel = '__CHECKSUMS_' + 'PLACEHOLDER__' # Ensure TLS 1.2 for GitHub downloads on older PowerShell versions try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch { } function Get-Architecture { # Environment variables first: they exist on every PowerShell version. # PROCESSOR_ARCHITEW6432 is set when a 32-bit shell runs on a 64-bit OS # (WOW64), where PROCESSOR_ARCHITECTURE misreports 'x86'. foreach ($pa in @($env:PROCESSOR_ARCHITEW6432, $env:PROCESSOR_ARCHITECTURE)) { if ([string]::IsNullOrWhiteSpace($pa)) { continue } if ($pa -match 'ARM64') { return 'arm64' } if ($pa -match '64') { return 'x64' } } # Fallback: RuntimeInformation, which some Windows PowerShell 5.1 hosts # cannot resolve (the type lives in a facade assembly that is not always # loaded), so it may throw rather than return. try { switch ("$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)") { 'X64' { return 'x64' } 'Arm64' { return 'arm64' } } } catch { } return $null } # Ensure $PathToAdd is on the User PATH (appended if absent). No Machine PATH, # no admin required, no positioning logic. function Set-PathEnsureContains { param( [Parameter(Mandatory = $true)][string]$PathToAdd ) $sep = ';' function NormalizePath([string]$p) { try { return ([IO.Path]::GetFullPath($p.Trim())).TrimEnd('\\').ToLowerInvariant() } catch { return ($p.Trim()).TrimEnd('\\').ToLowerInvariant() } } $normalizedAdd = NormalizePath $PathToAdd try { $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') $entries = @() if ($userPath) { $entries = ($userPath -split $sep) | Where-Object { $_ -and $_.Trim() -ne '' } } $alreadyPresent = $false foreach ($e in $entries) { if ((NormalizePath $e) -eq $normalizedAdd) { $alreadyPresent = $true; break } } if ($alreadyPresent) { $userStatus = 'AlreadyPresent' } else { $newUserPath = if ($userPath) { "$userPath$sep$PathToAdd" } else { $PathToAdd } [Environment]::SetEnvironmentVariable('Path', $newUserPath, 'User') $userStatus = 'Updated' } } catch { $userStatus = 'Error' } # Update current process PATH immediately for this session try { $procPath = $env:PATH $procEntries = @() if ($procPath) { $procEntries = ($procPath -split $sep) | Where-Object { $_ -and $_.Trim() -ne '' } } $procHas = $false foreach ($e in $procEntries) { if ((NormalizePath $e) -eq $normalizedAdd) { $procHas = $true; break } } if (-not $procHas) { $env:PATH = if ($procPath) { "$procPath$sep$PathToAdd" } else { $PathToAdd } } } catch { } return [PSCustomObject]@{ UserStatus = $userStatus } } # Detect architecture and OS $arch = Get-Architecture if (-not $arch) { # Do NOT probe RuntimeInformation here: on hosts where it is unavailable # that probe itself throws, replacing this message with an unrelated # PropertyNotFound/TypeNotFound error. $reported = if ([string]::IsNullOrWhiteSpace($env:PROCESSOR_ARCHITECTURE)) { 'unknown' } else { $env:PROCESSOR_ARCHITECTURE } Write-ErrorAndExit "Unsupported architecture (PROCESSOR_ARCHITECTURE='$reported'). autter provides Windows binaries for x64 and arm64." } $os = 'windows' # git is required — autter wraps git and cannot function without it. try { $null = & git --version 2>&1 if ($LASTEXITCODE -ne 0) { Write-ErrorAndExit 'git is required but not found. Install Git for Windows (https://git-scm.com/download/win) and re-run the installer.' } } catch { Write-ErrorAndExit 'git is required but not found. Install Git for Windows (https://git-scm.com/download/win) and re-run the installer.' } # Determine binary name and download URLs $binaryName = "autter-$os-$arch" # Determine release tag # Priority: 1. Local binary override, 2. Pinned version (for release builds), 3. Environment variable, 4. "latest" if (-not [string]::IsNullOrWhiteSpace($env:AUTTER_LOCAL_BINARY)) { $releaseTag = 'local' } elseif ($PinnedVersion -ne $VersionSentinel) { # Version-pinned install script from a release $releaseTag = $PinnedVersion $downloadUrlExe = "https://github.com/$Repo/releases/download/$releaseTag/$binaryName.exe" $downloadUrlNoExt = "https://github.com/$Repo/releases/download/$releaseTag/$binaryName" } elseif (-not [string]::IsNullOrWhiteSpace($env:AUTTER_RELEASE_TAG) -and $env:AUTTER_RELEASE_TAG -ne 'latest') { # Environment variable override $releaseTag = $env:AUTTER_RELEASE_TAG $downloadUrlExe = "https://github.com/$Repo/releases/download/$releaseTag/$binaryName.exe" $downloadUrlNoExt = "https://github.com/$Repo/releases/download/$releaseTag/$binaryName" } else { # Resolve the latest-release redirect to a concrete tag below. $releaseTag = 'latest' } # Resolve a specific release and load the checksum file produced by release.yml # before downloading the executable. A missing/malformed checksum is fatal. if ([string]::IsNullOrWhiteSpace($env:AUTTER_LOCAL_BINARY)) { if ($releaseTag -eq 'latest') { try { $latestResponse = Invoke-WebRequest -Uri "https://github.com/$Repo/releases/latest" -Method Head -UseBasicParsing -ErrorAction Stop $latestResponseUriProperty = $latestResponse.BaseResponse.PSObject.Properties['ResponseUri'] $latestUrl = if ($latestResponseUriProperty -and $latestResponseUriProperty.Value) { $latestResponse.BaseResponse.ResponseUri.AbsoluteUri } else { $latestResponse.BaseResponse.RequestMessage.RequestUri.AbsoluteUri } if ($latestUrl -notmatch '/releases/tag/([^/?]+)') { Write-ErrorAndExit 'Failed to resolve latest release to a specific version' } $releaseTag = $Matches[1] } catch { Write-ErrorAndExit "Failed to resolve the latest release: $($_.Exception.Message)" } } $checksumsUrl = "https://github.com/$Repo/releases/download/$releaseTag/checksums.txt" $checksumsTmp = [IO.Path]::GetTempFileName() try { $oldProgressPreference = $ProgressPreference $ProgressPreference = 'SilentlyContinue' try { $null = Invoke-WebRequest -Uri $checksumsUrl -OutFile $checksumsTmp -UseBasicParsing -ErrorAction Stop } finally { $ProgressPreference = $oldProgressPreference } $EmbeddedChecksums = ((Get-Content -LiteralPath $checksumsTmp) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) -join '|' if ([string]::IsNullOrWhiteSpace($EmbeddedChecksums)) { Write-ErrorAndExit 'Release checksums are empty' } } catch { Write-ErrorAndExit "Failed to download release checksums: $($_.Exception.Message)" } finally { Remove-Item -Force -ErrorAction SilentlyContinue $checksumsTmp } $downloadUrlExe = "https://github.com/$Repo/releases/download/$releaseTag/$binaryName.exe" $downloadUrlNoExt = "https://github.com/$Repo/releases/download/$releaseTag/$binaryName" } # ============================================================ # Anonymous install ping. # One fire-and-forget event so we can count installs. Contains only: # OS, CPU architecture, requested release tag, and whether this run # is a fresh install or a daemon self-upgrade. No hostname, username, # paths, or any personal data. Disable with AUTTER_NO_INSTALL_PING=1. # The API key is a public write-only project token (same one baked # into release builds for opt-in telemetry). # ============================================================ function Send-InstallPing { if ($env:AUTTER_NO_INSTALL_PING -eq '1' -or -not [string]::IsNullOrWhiteSpace($env:AUTTER_LOCAL_BINARY)) { return } $trigger = if (-not [string]::IsNullOrWhiteSpace($env:AUTTER_DAEMON_UPGRADE)) { 'upgrade' } else { 'install' } # Keep daemon self-upgrades quiet; tell interactive installers what is sent if ($trigger -eq 'install') { Write-Host 'Counting this install with an anonymous ping (OS, architecture, and version only).' Write-Host 'Set AUTTER_NO_INSTALL_PING=1 to disable.' } # releaseTag can come from an env var; sanitize before embedding $safeTag = ($releaseTag -replace '[^A-Za-z0-9._-]', '') $payload = @{ api_key = 'phc_aWveMd1bPhuEYtFnCS1G2IHgln3iGQqjfIdkfnuolxI' event = 'install_script_run' distinct_id = [guid]::NewGuid().ToString() properties = @{ os = $os arch = $arch release_tag = $safeTag trigger = $trigger source = 'install.ps1' } } | ConvertTo-Json -Compress try { $null = Invoke-RestMethod -Uri 'https://us.i.posthog.com/capture/' -Method Post -ContentType 'application/json' -Body $payload -TimeoutSec 5 } catch { } } Send-InstallPing # ============================================================ # Warn when installing as Administrator (not recommended). # Running elevated creates files that normal-user processes # cannot access, causing persistent daemon lock failures. # ============================================================ $isElevated = $false try { # Detect explicit UAC elevation ("Run as Administrator") via TokenElevationType. # Type 1 (Default) = no split token (UAC disabled or built-in Admin) -> no warn # Type 2 (Full) = elevated half of a split token -> WARN (this is the danger case) # Type 3 (Limited) = non-elevated half of a split token -> no warn # We only warn on type 2: user explicitly elevated, so files will be admin-owned # but normal processes won't be, causing the daemon.lock mismatch from issue #1287. Add-Type -TypeDefinition @" using System; using System.Runtime.InteropServices; public static class AutterElevation { [DllImport("advapi32.dll", SetLastError=true)] static extern bool OpenProcessToken(IntPtr h, uint access, out IntPtr token); [DllImport("advapi32.dll", SetLastError=true)] static extern bool GetTokenInformation(IntPtr token, int cls, ref int info, int len, out int ret); [DllImport("kernel32.dll")] static extern IntPtr GetCurrentProcess(); [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr h); public static bool IsElevated() { IntPtr tok; if (!OpenProcessToken(GetCurrentProcess(), 0x0008, out tok)) return false; try { int elevType = 0; int sz; // TokenElevationType = class 18; returns 1/2/3 if (!GetTokenInformation(tok, 18, ref elevType, 4, out sz)) return false; return elevType == 2; // TokenElevationTypeFull } finally { CloseHandle(tok); } } } "@ -ErrorAction SilentlyContinue $isElevated = [AutterElevation]::IsElevated() } catch { } if ($isElevated -and $env:AUTTER_ALLOW_SUPERUSER -ne '1') { # Auto-allow in CI environments and daemon-triggered self-updates $isCi = $env:CI -or $env:GITHUB_ACTIONS -or $env:GITLAB_CI -or $env:JENKINS_URL ` -or $env:BUILDKITE -or $env:CIRCLECI -or $env:CODEBUILD_BUILD_ID ` -or $env:AGENT_OS -or $env:KUBERNETES_SERVICE_HOST ` -or $env:AUTTER_DAEMON_UPGRADE -or $env:container if (-not $isCi) { Write-Host '' Write-Host 'Warning: installing autter as Administrator is not recommended.' -ForegroundColor Yellow Write-Host '' Write-Host 'Running with elevated privileges creates files owned by Administrator that' Write-Host 'become inaccessible to your normal user account, causing persistent daemon' Write-Host 'lock failures. A future version may refuse to install in this configuration.' Write-Host '' Write-Host 'To suppress this warning, either:' Write-Host ' - Run this installer from a normal (non-elevated) PowerShell window (recommended), or' Write-Host ' - Set $env:AUTTER_ALLOW_SUPERUSER = "1"' -ForegroundColor Yellow Write-Host '' } # Propagate to child autter invocations (install-hooks, exchange-nonce, login) $env:AUTTER_ALLOW_SUPERUSER = '1' } # Install directory: %USERPROFILE%\.autter\bin $installDir = Join-Path $HOME ".autter\bin" New-Item -ItemType Directory -Force -Path $installDir | Out-Null Write-Host ("Downloading autter (release: {0})..." -f $releaseTag) $tmpFile = Join-Path $installDir "autter.tmp.$PID.exe" # Each failed attempt is recorded as " -> " so the final error # can report exactly what was tried instead of an opaque "HTTP error". $downloadFailures = New-Object System.Collections.Generic.List[string] function Try-Download { param( [Parameter(Mandatory = $true)][string]$Url ) try { # Disable progress bar to avoid extreme slowdown caused by PowerShell's # progress-stream rendering (can make downloads 10-50x slower). $oldProgressPreference = $ProgressPreference $ProgressPreference = 'SilentlyContinue' try { Invoke-WebRequest -Uri $Url -OutFile $tmpFile -UseBasicParsing -ErrorAction Stop } finally { $ProgressPreference = $oldProgressPreference } return $true } catch { $reason = $_.Exception.Message try { # WebException (PowerShell 5.1) and HttpResponseException (7+) both # carry the response; pure network/TLS failures have none, and on # some exception types even probing .Response throws — hence the # inner try/catch keeping the plain exception message. if ($_.Exception.Response -and $_.Exception.Response.StatusCode) { $reason = 'HTTP {0} {1}' -f [int]$_.Exception.Response.StatusCode, $_.Exception.Response.StatusCode } } catch { } [void]$downloadFailures.Add((" {0}`n -> {1}" -f $Url, $reason)) return $false } } # Track which download URL succeeded for checksum verification $downloadedBinaryName = $null if (-not [string]::IsNullOrWhiteSpace($env:AUTTER_LOCAL_BINARY)) { if (-not (Test-Path -LiteralPath $env:AUTTER_LOCAL_BINARY)) { Remove-Item -Force -ErrorAction SilentlyContinue $tmpFile Write-ErrorAndExit "Local binary not found at $($env:AUTTER_LOCAL_BINARY)" } Copy-Item -Force -Path $env:AUTTER_LOCAL_BINARY -Destination $tmpFile $downloadedBinaryName = "$binaryName.exe" } elseif (Try-Download -Url $downloadUrlExe) { $downloadedBinaryName = "$binaryName.exe" } elseif (Try-Download -Url $downloadUrlNoExt) { $downloadedBinaryName = $binaryName } if (-not $downloadedBinaryName) { Remove-Item -Force -ErrorAction SilentlyContinue $tmpFile $details = $downloadFailures -join "`n" $message = "Failed to download $binaryName (release: $releaseTag). Attempted:`n$details" if ($details -match 'HTTP 404') { $message += "`nA 404 means release '$releaseTag' does not include a Windows binary named $binaryName." $message += "`nWindows binaries ship with releases v1.6.8 and later - see https://github.com/$Repo/releases" $message += "`nIf AUTTER_RELEASE_TAG pins an older version, unset it to install the latest release." } else { $message += "`nCheck your network connection, proxy, and TLS settings, then retry." $message += "`nReleases: https://github.com/$Repo/releases" } Write-ErrorAndExit $message } try { if ((Get-Item $tmpFile).Length -le 0) { Remove-Item -Force -ErrorAction SilentlyContinue $tmpFile Write-ErrorAndExit 'Downloaded file is empty' } } catch { Remove-Item -Force -ErrorAction SilentlyContinue $tmpFile Write-ErrorAndExit 'Download failed' } # Verify before the executable is moved into place or run. Verify-Checksum -File $tmpFile -BinaryName $downloadedBinaryName $finalExe = Join-Path $installDir 'autter.exe' # Wait for autter.exe to be available if it exists and is in use if (Test-Path -LiteralPath $finalExe) { if (-not (Wait-ForFileAvailable -Path $finalExe -InstallDir $installDir -MaxWaitSeconds 300 -RetryIntervalSeconds 5)) { Remove-Item -Force -ErrorAction SilentlyContinue $tmpFile Write-ErrorAndExit "Timeout waiting for $finalExe to be available. Please close any running autter processes and try again." } } Move-Item -Force -Path $tmpFile -Destination $finalExe try { Unblock-File -Path $finalExe -ErrorAction SilentlyContinue } catch { } # Verify the binary runs before reporting success. try { $installedVersion = & $finalExe --version 2>&1 | Out-String $installedVersion = $installedVersion.Trim() if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($installedVersion)) { Remove-Item -Force -ErrorAction SilentlyContinue $finalExe Write-ErrorAndExit "The autter binary could not run on this system:`n$installedVersion" } Write-Host "Installed autter $installedVersion" } catch { Remove-Item -Force -ErrorAction SilentlyContinue $finalExe Write-ErrorAndExit "The autter binary could not run on this system: $($_.Exception.Message)" } # Refresh git.exe for existing wrapper users (it's a copy, not a symlink on Windows) $gitShim = Join-Path $installDir 'git.exe' if (Test-Path -LiteralPath $gitShim) { if (-not (Wait-ForFileAvailable -Path $gitShim -InstallDir $installDir -MaxWaitSeconds 300 -RetryIntervalSeconds 5)) { Write-ErrorAndExit "Timeout waiting for $gitShim to be available. Please close any running git processes and try again." } Copy-Item -Force -Path $finalExe -Destination $gitShim try { Unblock-File -Path $gitShim -ErrorAction SilentlyContinue } catch { } } # Login user with install token if provided $needLogin = $false if ($env:INSTALL_NONCE -and $env:API_BASE) { try { & $finalExe exchange-nonce | Out-Host if ($LASTEXITCODE -ne 0) { $needLogin = $true } } catch { $needLogin = $true } } # Install hooks Write-Host 'Setting up IDE/agent hooks...' try { & $finalExe install-hooks | Out-Host Write-Success 'Successfully set up IDE/agent hooks' } catch { Write-Warning "Warning: Failed to set up IDE/agent hooks. Please try running 'autter install-hooks' manually." } # Best-effort restart only for daemon-initiated self-updates. Start-DaemonIfRequested $skipPathUpdate = $env:AUTTER_SKIP_PATH_UPDATE -eq '1' if ($skipPathUpdate) { Write-Warning 'Skipping PATH updates because AUTTER_SKIP_PATH_UPDATE=1' $pathUpdate = [PSCustomObject]@{ UserStatus = 'Skipped' } } else { $pathUpdate = Set-PathEnsureContains -PathToAdd $installDir } if ($pathUpdate.UserStatus -eq 'Updated') { Write-Success 'Successfully added autter to the user PATH.' } elseif ($pathUpdate.UserStatus -eq 'AlreadyPresent') { Write-Success 'autter already present in the user PATH.' } elseif ($pathUpdate.UserStatus -eq 'Error') { Write-Host 'Failed to update the user PATH.' -ForegroundColor Red } Write-Success "Successfully installed autter into $installDir" Write-Success "You can now run 'autter' from your terminal" # Configure Git Bash shell profiles so autter takes precedence over /mingw64/bin/git # Git Bash (MSYS2/MinGW) prepends its own directories to PATH, which shadows # the Windows PATH entry we set above. Writing to ~/.bashrc ensures autter's # bin directory is prepended after Git Bash's own PATH setup. $gitBashConfigured = $false $gitBashAlreadyConfigured = $false try { $bashrcPath = Join-Path $HOME '.bashrc' $bashProfilePath = Join-Path $HOME '.bash_profile' $pathCmd = 'export PATH="$HOME/.autter/bin:$PATH"' $markerString = '.autter/bin' # Detect if Git Bash is installed $gitBashInstalled = $false $gitForWindowsPaths = @() if ($env:ProgramFiles) { $gitForWindowsPaths += Join-Path $env:ProgramFiles 'Git\bin\bash.exe' } if (${env:ProgramFiles(x86)}) { $gitForWindowsPaths += Join-Path ${env:ProgramFiles(x86)} 'Git\bin\bash.exe' } if ($env:LOCALAPPDATA) { $gitForWindowsPaths += Join-Path $env:LOCALAPPDATA 'Programs\Git\bin\bash.exe' } foreach ($p in $gitForWindowsPaths) { if ($p -and (Test-Path -LiteralPath $p)) { $gitBashInstalled = $true break } } if ($gitBashInstalled) { # Determine which config file to update (prefer .bashrc, fall back to .bash_profile) $targetBashConfig = $null if (Test-Path -LiteralPath $bashrcPath) { $targetBashConfig = $bashrcPath } elseif (Test-Path -LiteralPath $bashProfilePath) { $targetBashConfig = $bashProfilePath } else { # No existing config; create .bashrc $targetBashConfig = $bashrcPath } # Check if already configured $alreadyPresent = $false if (Test-Path -LiteralPath $targetBashConfig) { $content = Get-Content -LiteralPath $targetBashConfig -Raw -ErrorAction SilentlyContinue if ($content -and $content.Contains($markerString)) { $alreadyPresent = $true } } if ($alreadyPresent) { $gitBashAlreadyConfigured = $true } else { $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $appendContent = "`n# Added by autter installer on $timestamp`n$pathCmd`n" $utf8NoBom = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::AppendAllText($targetBashConfig, $appendContent, $utf8NoBom) $gitBashConfigured = $true } } } catch { Write-Host "Warning: Failed to configure Git Bash: $($_.Exception.Message)" -ForegroundColor Yellow } if ($gitBashConfigured) { Write-Success "Successfully configured Git Bash ($targetBashConfig)" } elseif ($gitBashAlreadyConfigured) { Write-Success "Git Bash already configured ($targetBashConfig)" } Write-Host 'Close and reopen your terminal and IDE sessions to use autter.' -ForegroundColor Yellow # If nonce exchange failed, run interactive login if ($needLogin) { Write-Host '' Write-Host 'Launching login...' & $finalExe login } # Walk the user through onboarding: choose local-only vs connecting to the # Autter platform, plus telemetry consent. The CLI skips itself gracefully # when the console isn't interactive (CI, scripted installs). Write-Host '' & $finalExe onboard