# Flieber CLI installer for Windows, in PowerShell (AIDEV-834): the twin of install.sh. # # One line in PowerShell (the organisation id is on the Developer page of the Flieber portal): # # & ([scriptblock]::Create((irm https://build.flieber.com/install.ps1))) # & ([scriptblock]::Create((irm https://build.flieberqa.com/install.ps1))) -Qa # # The scriptblock form is what passes arguments. Without an organisation id, # `irm https://build.flieber.com/install.ps1 | iex` also works: it signs in and lists the organisations. # People who double-click a file use flieber-install.bat instead. # # Installs uv if missing (no admin rights, no Python needed), installs or upgrades flieber, turns on # onboarding writes, adds the flieber skills for Claude Code when it is there, signs in through the browser # and checks the sign-in. Safe to run again: running it again is the fix. Works on Windows PowerShell 5.1 # and PowerShell 7, and stays plain ASCII so 5.1 reads it the same everywhere. It never ends the PowerShell # session (run through iex, that would close the window): a failure throws. tests/test_install_scripts.py # covers it with install.sh and the .bat. # # FLIEBER_INSTALL_SKIP_LOGIN=1 stops after the install (CI). # Distinctive names: run through `irm | iex`, the param block assigns in the caller's own scope, so a plain # `$Org` would overwrite the person's variable. Anything after the organisation id is refused, so the # .bat's ` qa` cannot silently sign in to production. [CmdletBinding()] param( [Parameter(Position = 0)] [string]$FlieberOrgId = '', [Alias('Qa')] [switch]$FlieberQa, [Parameter(ValueFromRemainingArguments = $true)] [string[]]$FlieberUnknownArgs = @() ) # Every native program (uv, flieber, sh, powershell.exe) runs through here. Windows PowerShell 5.1 turns # each line a native program writes to a redirected stderr into an error record, and under # $ErrorActionPreference = 'Stop' the first one ends the script (uv's "... is already in PATH" did, on the # Windows runner). So the call runs with 'Continue', and success is judged only by $LASTEXITCODE, which # the caller reads. Stderr stays on screen; -Quiet discards everything, -NoStderr only stderr. A caller that # assigns the result gets the program's stdout. function Invoke-FlieberNative([string]$Command, [string[]]$Arguments = @(), [switch]$Quiet, [switch]$NoStderr) { $ErrorActionPreference = 'Continue' # A program that is not there would throw CommandNotFoundException at the caller, with PowerShell's own # wording: stop with the setup's plain message instead. if (-not (Get-Command -Name $Command -CommandType Application -ErrorAction SilentlyContinue)) { Stop-FlieberSetup "Could not start $Command." } if ($Quiet) { & $Command @Arguments *> $null } elseif ($NoStderr) { & $Command @Arguments 2> $null } else { & $Command @Arguments } } function Stop-FlieberSetup([string]$Message) { Write-Host '' Write-Host $Message -ForegroundColor Red # Said plainly first: PowerShell wraps and colours the text of a thrown error. Write-Host 'Setup stopped. Nothing is broken: fix the message above and run the same command again.' throw 'Flieber setup stopped.' } # Where uv lives: on PATH, or where astral's installer puts it (not on PATH in this window yet). function Find-FlieberUv([string]$UserHome, [string]$Exe) { $onPath = Get-Command uv -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if ($onPath) { return $onPath.Source } $dirs = @() if ($env:XDG_BIN_HOME) { $dirs += $env:XDG_BIN_HOME } $dirs += (Join-Path (Join-Path $UserHome '.local') 'bin') $dirs += (Join-Path (Join-Path $UserHome '.cargo') 'bin') foreach ($dir in $dirs) { $candidate = Join-Path $dir ('uv' + $Exe) if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate } } return $null } # uv's tool folder, read as UTF-8: Windows PowerShell decodes a native program's output in the console # code page, which garbles a user folder with an accent. Its stderr stays on screen, as in install.sh. function Get-FlieberToolBin([string]$Uv) { $previous = $null try { try { $previous = [Console]::OutputEncoding [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false) } catch { $previous = $null } $answer = Invoke-FlieberNative $Uv @('tool', 'dir', '--bin') } finally { if ($previous) { try { [Console]::OutputEncoding = $previous } catch { } } } if ($LASTEXITCODE -eq 0 -and $answer) { return ([string]($answer | Select-Object -Last 1)).Trim() } return $null } # A new sign-up has no organisation yet (AIDEV-830): `flieber signup` creates one, after the person reads and # accepts Flieber's terms. Its prompts need a console whose input is the keyboard: run from a PowerShell window, # as a file or as `irm ... | iex`. With input redirected (piped in, CI) it is only named. # FLIEBER_INSTALL_INTERACTIVE=1 says the input is a console (tests). A failure or a "no" never stops the # setup. Returns $true only when it created one. function Invoke-FlieberSignupOffer([string]$Flieber, [string[]]$EnvFlag) { $commandLine = (@('flieber') + $EnvFlag + @('signup')) -join ' ' Write-Host "No Flieber organisation yet? Create one with: $commandLine" $console = ($env:FLIEBER_INSTALL_INTERACTIVE -eq '1') -or ([Environment]::UserInteractive -and -not [Console]::IsInputRedirected) if (-not $console) { return $false } # Only `no_organization` means "none yet". A member of several is signed in with none selected (ADR-0005 # D4): they choose one of theirs, they don't need another. Read by its code, which `--format json` writes # to stdout: the exit code alone (3) is shared with a missing or refused sign-in. $answer = (Invoke-FlieberNative $Flieber ($EnvFlag + @('--format', 'json', 'ping')) -NoStderr) -join "`n" if (-not $answer.Contains('"code": "no_organization"')) { return $false } Write-Host '' # To the host, not the pipeline: anything signup prints would otherwise become this function's result. Invoke-FlieberNative $Flieber ($EnvFlag + @('--format', 'table', 'signup')) | Out-Host if ($LASTEXITCODE -eq 0) { return $true } Write-Host "No organisation was created. Run $commandLine when you're ready." Write-Host '' return $false } function Install-Flieber([string]$Org, [switch]$Qa, [string[]]$UnknownArgs = @()) { $ErrorActionPreference = 'Stop' if ($UnknownArgs -and $UnknownArgs.Count -gt 0) { Stop-FlieberSetup ("Unknown argument(s): $($UnknownArgs -join ' ')`n" + 'Usage: & ([scriptblock]::Create((irm https://build.flieber.com/install.ps1))) [-Qa]') } $onWindows = [System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT $exe = if ($onWindows) { '.exe' } else { '' } $userHome = if ($env:USERPROFILE) { $env:USERPROFILE } else { $HOME } # Pasted ids often carry spaces or quotes. $Org = $Org -replace '[\s"'']', '' Write-Host '' Write-Host '=== Flieber setup ===' Write-Host '' $uv = Find-FlieberUv $userHome $exe if (-not $uv) { Write-Host '[1/4] Installing uv, the tool that installs flieber...' if ($onWindows) { # In its own process, so nothing astral's script does can end this session. Invoke-FlieberNative 'powershell.exe' @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', 'irm https://astral.sh/uv/install.ps1 | iex') } else { Invoke-FlieberNative 'sh' @('-c', 'curl -LsSf https://astral.sh/uv/install.sh | sh') } $uv = Find-FlieberUv $userHome $exe if (-not $uv) { Stop-FlieberSetup ("Could not install uv. Your network or a company policy may block it.`n" + 'Ask your IT team to allow astral.sh, releases.astral.sh, github.com, pypi.org and ' + "files.pythonhosted.org,`nor install uv by hand: https://docs.astral.sh/uv/") } } Write-Host '[1/4] uv is ready.' Write-Host '[2/4] Installing flieber...' Invoke-FlieberNative $uv @('tool', 'install', '--upgrade', 'flieber') if ($LASTEXITCODE -ne 0) { Stop-FlieberSetup ("Could not install flieber. Close any window that is running flieber and check your " + "internet connection`n(IT may need to allow astral.sh, releases.astral.sh, github.com, pypi.org " + 'and files.pythonhosted.org).') } # PATH for new windows is a convenience (the full path is used below): a failure is not fatal, as in # install.sh. Invoke-FlieberNative $uv @('tool', 'update-shell') -Quiet $flieber = $null $toolBin = Get-FlieberToolBin $uv if ($toolBin) { $candidate = Join-Path $toolBin ('flieber' + $exe) if (Test-Path -LiteralPath $candidate -PathType Leaf) { $flieber = $candidate } } # Unlike install.sh, which stops ("uv too old") when uv names no folder: here uv's default tool folder # is tried next, because that is where uv put flieber unless UV_TOOL_BIN_DIR or XDG_BIN_HOME says # otherwise, and it also covers uv output this window could not decode. If flieber is not there either, # the setup stops just below. if (-not $flieber) { $candidate = Join-Path (Join-Path (Join-Path $userHome '.local') 'bin') ('flieber' + $exe) if (Test-Path -LiteralPath $candidate -PathType Leaf) { $flieber = $candidate } } if (-not $flieber) { Stop-FlieberSetup 'flieber was installed but could not be found. Open a new window and run this again.' } # Onboarding writes stay on in new windows (user environment) and in this one. $env:FLIEBER_OAUTH_WRITES = '1' if ($onWindows) { [Environment]::SetEnvironmentVariable('FLIEBER_OAUTH_WRITES', '1', 'User') } # The agent skills (AIDEV-833), when Claude Code is on this computer (its config folder exists): they # need no sign-in, and a failure (an older flieber has no `skills` command) never stops the setup. $claudeDir = if ($env:CLAUDE_CONFIG_DIR) { $env:CLAUDE_CONFIG_DIR } else { Join-Path $userHome '.claude' } if (Test-Path -LiteralPath $claudeDir -PathType Container) { Invoke-FlieberNative $flieber @('skills', 'install', '--client', 'claude-code', '--scope', 'user') -Quiet if ($LASTEXITCODE -eq 0) { Write-Host 'Added the flieber skills for Claude Code.' } else { Write-Host 'Skipped the flieber skills for Claude Code; `flieber skills install` says why. flieber itself is ready.' } } # QA (Flieber staff) is for this run only: persisting FLIEBER_ENV would point every later run at QA, # and the previous FLIEBER_PROFILE comes back when the setup ends, even in a window that ran it via iex. $previousProfile = $env:FLIEBER_PROFILE $envFlag = @() if ($Qa) { $env:FLIEBER_PROFILE = 'qa' $envFlag = @('--env', 'qa') } try { if ($env:FLIEBER_INSTALL_SKIP_LOGIN -eq '1') { Invoke-FlieberNative $flieber ($envFlag + @('--version')) if ($LASTEXITCODE -ne 0) { Stop-FlieberSetup "flieber was installed but does not run (exit code $LASTEXITCODE)." } return } Write-Host '[3/4] Opening the Flieber sign-in in your browser. Finish it there, then come back here.' if (-not $Org) { Invoke-FlieberNative $flieber ($envFlag + @('login')) if ($LASTEXITCODE -ne 0) { Stop-FlieberSetup 'The sign-in did not finish.' } Write-Host '' Write-Host 'Your organisations:' Invoke-FlieberNative $flieber ($envFlag + @('tenants')) Write-Host '' if (Invoke-FlieberSignupOffer $flieber $envFlag) { Write-Host '' Write-Host 'Done. flieber is installed and signed in to your new organisation.' Write-Host 'Open a NEW PowerShell window to use the flieber command yourself.' return } Write-Host 'If you have an organisation, run this again with its id at the end (from the Developer page of the Flieber portal).' return } Invoke-FlieberNative $flieber ($envFlag + @('login', '--tenant', $Org)) if ($LASTEXITCODE -ne 0) { Stop-FlieberSetup 'The sign-in did not finish.' } Write-Host '[4/4] Checking the connection...' Invoke-FlieberNative $flieber ($envFlag + @('ping')) if ($LASTEXITCODE -ne 0) { Stop-FlieberSetup 'Signed in, but the check failed. The message above says why.' } Write-Host '' Invoke-FlieberNative $flieber ($envFlag + @('onboard', 'status')) Write-Host '' Write-Host 'Done. flieber is installed and signed in.' Write-Host 'Open a NEW PowerShell window to use the flieber command yourself.' } finally { $env:FLIEBER_PROFILE = $previousProfile } } Install-Flieber -Org $FlieberOrgId -Qa:$FlieberQa -UnknownArgs $FlieberUnknownArgs