# Ad Engine installer for Windows (Windows PowerShell 5.1 or PowerShell 7). # # irm https://runargusapp.com/install.ps1 | iex # # With setup options (forwarded to `adengine setup --yes`): # & ([scriptblock]::Create((irm https://runargusapp.com/install.ps1))) --editor resolve # # This file is the single source of the served installer. site/ad-engine/build_deploy.py copies it # into the website with https://runargusapp.com replaced by the site URL. It needs no administrator rights, # is safe to re-run, and never installs the unrelated `adengine` project from PyPI. In order, it: # 1. asks the release channel for the current release, always without a token first. The public channel # answers everyone; only when a private (development) channel says it needs one is the token in # ADENGINE_ACCESS_TOKEN sent, in the Authorization header only; # 2. downloads the wheel and checks it against the published SHA-256 (a mismatch installs nothing); # 3. keeps the verified wheel in ~\.adengine\releases\ and describes it in releases\current.json, # so uv and `adengine upgrade` can find it again later; # 4. installs uv from astral.sh when it is missing (current.json records that, so `adengine uninstall` can # list it), then runs uv tool install on the kept wheel; # 5. provides ffmpeg when no suitable copy is found; # 6. runs `adengine setup --yes`, forwarding any arguments given to this script, and ends with the # next steps setup reports. The first one is always to open your agent and run /adengine-setup. # Setup can finish with a step still left for you (open DaVinci Resolve once, install an agent); # that is a finished install, and the step is printed last. # # Environment: ADENGINE_SITE, ADENGINE_HOME (default ~\.adengine), ADENGINE_PYTHON (default 3.12), # ADENGINE_UV_INSTALLER_URL (default https://astral.sh/uv/install.ps1), and ADENGINE_ACCESS_TOKEN only for a # private (development) release channel. # # Everything runs inside one script block, so `irm | iex` leaves the caller's variables and preferences # as they were. A token is taken out of this session's environment while the installer runs, so the # programs it starts never see it, and it is put back at the end. & { param([object[]]$SetupArguments = @()) Set-StrictMode -Version 2.0 $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' $DefaultSite = 'https://runargusapp.com' $OnWindows = [IO.Path]::DirectorySeparatorChar -eq '\' $Exe = if ($OnWindows) { '.exe' } else { '' } $PathSeparator = [string][IO.Path]::PathSeparator $Utf8 = New-Object Text.UTF8Encoding $false function Say([string]$Message) { Write-Host $Message } function Warn([string]$Message) { Write-Host "Warning: $Message" -ForegroundColor Yellow } # throw, never exit: under `irm | iex`, exit would close the user's PowerShell window. The error record uses # the id PowerShell gives a native program's error line, which every error view (Windows PowerShell 5.1 and # PowerShell 7) prints as the message alone, so the user sees one red line and no pointer into this script. # The command still fails ($? is false, and powershell -Command exits 1). function Fail([string]$Message) { $exception = New-Object Management.Automation.RuntimeException ("Ad Engine install failed: $Message") throw (New-Object Management.Automation.ErrorRecord $exception, 'NativeCommandErrorMessage', ([Management.Automation.ErrorCategory]::NotSpecified), $null) } function Get-HomeDir { if ($env:USERPROFILE) { return $env:USERPROFILE } return $HOME } function Join-Parts([string]$Base, [string[]]$Parts) { $result = $Base foreach ($part in $Parts) { $result = Join-Path $result $part } return $result } # PowerShell decodes what it reads from a native program with [Console]::OutputEncoding, which on # Windows is the OEM code page (437, 850 and so on) by default. uv writes UTF-8 into a pipe, so the # folders of a user whose name has an accented, Cyrillic or CJK letter would come back garbled. # Returns the encoding to restore, or $null when this host has no console and refuses the change # (callers confirm the folders on disk anyway). function Set-OutputEncoding($Encoding) { try { $previous = [Console]::OutputEncoding [Console]::OutputEncoding = $Encoding return $previous } catch { return $null } } # Native programs report failure through $LASTEXITCODE; PowerShell 5.1 would otherwise turn their # progress messages on stderr into errors under 'Stop'. function Invoke-Native([string]$File, [object[]]$Arguments = @()) { $saved = $ErrorActionPreference $ErrorActionPreference = 'Continue' $encoding = Set-OutputEncoding $Utf8 try { & $File @Arguments | Out-Host } finally { $ErrorActionPreference = $saved if ($null -ne $encoding) { [void](Set-OutputEncoding $encoding) } } } # Standard output as text. With -Quiet, standard error is captured with it and never shown: for a program # whose messages the installer replaces with its own. function Invoke-Capture([string]$File, [object[]]$Arguments = @(), [switch]$Quiet) { $saved = $ErrorActionPreference $ErrorActionPreference = 'Continue' $encoding = Set-OutputEncoding $Utf8 try { if ($Quiet) { $lines = @(& $File @Arguments 2>&1) } else { $lines = @(& $File @Arguments) } } finally { $ErrorActionPreference = $saved if ($null -ne $encoding) { [void](Set-OutputEncoding $encoding) } } return (($lines | ForEach-Object { [string]$_ }) -join "`n").Trim() } # The first folder that holds $Leaf: uv's own answer first, then uv's documented defaults, for a # host where that answer could not be read. function Find-Folder([string[]]$Candidates, [string]$Leaf) { foreach ($dir in $Candidates) { if (-not $dir) { continue } try { if (Test-Path -LiteralPath (Join-Path $dir $Leaf) -PathType Leaf) { return $dir } } catch { } } return $null } # The PowerShell that runs this script, started from its install folder. The host process can be an # editor such as powershell_ise.exe, which does not accept -Command. function Get-PowerShellExe { $name = if ([string]$PSVersionTable['PSEdition'] -eq 'Core') { 'pwsh' } else { 'powershell' } $candidate = Join-Path $PSHOME ($name + $Exe) if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate } $found = Get-Command -Name ($name + $Exe), ('pwsh' + $Exe), ('powershell' + $Exe) -CommandType Application ` -ErrorAction SilentlyContinue | Select-Object -First 1 if ($found) { return $found.Source } return $null } function Get-Text([string]$Uri, [hashtable]$Headers) { $response = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing $content = $response.Content if ($content -is [byte[]]) { $content = [Text.Encoding]::UTF8.GetString($content) } return [string]$content } function Get-Sha256([string]$File) { return (Get-FileHash -Algorithm SHA256 -LiteralPath $File).Hash.ToLowerInvariant() } function Get-Field($Object, [string]$Name) { if ($null -eq $Object) { return $null } $property = $Object.PSObject.Properties[$Name] if ($property) { return $property.Value } return $null } function Find-Uv { $found = Get-Command uv -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if ($found) { return $found.Source } $homeDir = Get-HomeDir $dirs = @() if ($env:UV_INSTALL_DIR) { $dirs += $env:UV_INSTALL_DIR; $dirs += (Join-Path $env:UV_INSTALL_DIR 'bin') } if ($env:XDG_BIN_HOME) { $dirs += $env:XDG_BIN_HOME } if ($env:XDG_DATA_HOME) { $dirs += (Join-Path (Split-Path $env:XDG_DATA_HOME -Parent) 'bin') } $dirs += (Join-Parts $homeDir @('.local', 'bin')) $dirs += (Join-Parts $homeDir @('.cargo', 'bin')) if ($env:LOCALAPPDATA) { $dirs += (Join-Parts $env:LOCALAPPDATA @('Microsoft', 'WinGet', 'Links')) } $dirs += (Join-Parts $homeDir @('scoop', 'shims')) foreach ($dir in $dirs) { $candidate = Join-Path $dir ('uv' + $Exe) if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate } } return $null } # Windows cannot replace a program that is running, so a reinstall needs the agent apps closed first. function Get-RunningEngine([string]$ToolRoot) { return @(Get-Process -ErrorAction SilentlyContinue | Where-Object { $processPath = $null try { $processPath = $_.Path } catch { $processPath = $null } $_.ProcessName -like 'adengine*' -or ($ToolRoot -and $processPath -and $processPath.StartsWith($ToolRoot, [StringComparison]::OrdinalIgnoreCase)) }) } function Test-Interactive { try { return [Environment]::UserInteractive -and -not [Console]::IsInputRedirected } catch { return $false } } # Capital first letter and a closing full stop, for setup's hints ("run `adengine doctor --fix`"). function Format-Sentence([string]$Text) { $words = ([string]$Text -replace '\s+', ' ').Trim() if (-not $words) { return '' } $words = $words.Substring(0, 1).ToUpperInvariant() + $words.Substring(1) if ($words -notmatch '[.!?]$') { $words += '.' } return $words } function Show-Steps([string[]]$Items) { $list = @($Items | Where-Object { $_ }) if ($list.Count -eq 1) { Write-Host "Next step: $($list[0])"; return } if ($list.Count -eq 0) { return } Write-Host 'Next steps:' for ($i = 0; $i -lt $list.Count; $i++) { Write-Host " $($i + 1). $($list[$i])" } } # Prints `adengine setup --json` as readable lines and returns what the closing lines need: # Agents is 'setup' when an agent is registered and the adengine-setup skill was put in its skill # folder, 'yes' when an agent is registered without that skill, 'none' when no agent is, and 'unknown' # when the result does not say. Readiness is true when setup (0.5.0 and later) reported its own # readiness (Ready) and next steps (Hints, NextStep, FirstPrompt, OnPath from adengine_command). function Show-SetupReport([string]$Text) { $report = @{ Agents = 'unknown'; Readiness = $false; Ready = $false; Hints = @(); NextStep = $null FirstPrompt = '/adengine-setup'; OnPath = $null } $data = $null try { $data = $Text | ConvertFrom-Json } catch { $data = $null } if ($null -eq $data) { $start = $Text.LastIndexOf("`n{") if ($start -ge 0) { try { $data = $Text.Substring($start + 1) | ConvertFrom-Json; Write-Host $Text.Substring(0, $start) } catch { $data = $null } } } if ($data -isnot [Management.Automation.PSCustomObject]) { if ($Text) { Write-Host $Text } return $report } $ready = Get-Field $data 'ready' $listed = @() if ($ready -is [bool] -and $null -ne $data.PSObject.Properties['next_steps']) { $report.Readiness = $true $report.Ready = $ready $report.NextStep = [string](Get-Field $data 'next_step') $prompt = [string](Get-Field $data 'first_prompt') if ($prompt) { $report.FirstPrompt = $prompt } $onPath = Get-Field (Get-Field $data 'adengine_command') 'on_path' if ($onPath -is [bool]) { $report.OnPath = $onPath } $hints = @() foreach ($item in @(Get-Field $data 'next_steps')) { if ($null -eq $item) { continue } $hint = [string](Get-Field $item 'hint') if (-not $hint) { continue } $listed += $hint $hints += (Format-Sentence $hint) } if ($hints.Count -eq 0 -and $report.NextStep) { $hints = @(Format-Sentence $report.NextStep) } $report.Hints = @($hints | Select-Object -Unique) } $tags = @{ ok = 'ok'; unchanged = 'ok'; registered = 'ok'; restored = 'ok'; removed = 'removed' warn = 'warn'; skipped = 'skip'; next = 'next'; failed = 'FAIL'; refused = 'FAIL' } foreach ($step in @(Get-Field $data 'steps')) { if ($null -eq $step) { continue } $status = [string](Get-Field $step 'status') $tag = if ($tags.ContainsKey($status)) { $tags[$status] } else { $status } $message = [string](Get-Field $step 'message') $line = " [$tag] $([string](Get-Field $step 'step'))" if ($message) { $line += ": $message" } Write-Host $line } $ffmpeg = Get-Field $data 'ffmpeg' if ($ffmpeg -and (Get-Field $ffmpeg 'ok') -eq $false) { foreach ($hint in @(Get-Field $ffmpeg 'hints')) { if ($hint) { Write-Host " $hint" } } } # An editor step that is also one of setup's next steps is printed once, with the next steps. $steps = @(@(Get-Field (Get-Field $data 'editor_mcps') 'instructions') | Where-Object { $_ -and ($listed -notcontains $_) } | Select-Object -Unique) if ($steps.Count -gt 0) { if ($steps.Count -eq 1) { Write-Host 'One more step:' } else { Write-Host 'Remaining manual steps:' } foreach ($item in $steps) { Write-Host " - $item" } } $workspace = Get-Field $data 'workspace' if ($workspace) { Write-Host " Workspace: $workspace" } if ($null -eq $data.PSObject.Properties['clients']) { $report.Readiness = $false; return $report } $labels = @{ 'claude-code' = 'Claude Code'; 'codex' = 'Codex'; 'claude-desktop' = 'Claude Desktop'; 'cursor' = 'Cursor' } $names = @() foreach ($client in @(Get-Field $data 'clients')) { if ($null -eq $client) { continue } if (@('registered', 'unchanged') -contains [string](Get-Field $client 'status')) { $id = [string](Get-Field $client 'client') if ($labels.ContainsKey($id)) { $names += $labels[$id] } else { $names += $id } } } if ($names.Count -eq 0) { Write-Host ' Agents: none registered'; $report.Agents = 'none'; return $report } Write-Host " Agents: $($names -join ', ')" # Releases older than the adengine-setup skill still install; they get a general closing line. $report.Agents = 'yes' foreach ($root in @(Get-Field (Get-Field $data 'skills') 'roots')) { if ($null -eq $root) { continue } if ((@(Get-Field $root 'written') + @(Get-Field $root 'unchanged')) -contains 'adengine-setup') { $report.Agents = 'setup' } } return $report } $Site = if ($env:ADENGINE_SITE) { $env:ADENGINE_SITE } else { $DefaultSite } $Site = $Site.TrimEnd('/') if ($Site.StartsWith('__')) { Fail 'this is the unbuilt installer template. Use the command on the Ad Engine website, or set ADENGINE_SITE.' } if ($Site -notmatch '^(https://[A-Za-z0-9.-]+(:[0-9]+)?|http://(127\.0\.0\.1|localhost):[0-9]+)(/[A-Za-z0-9._~/-]*)?$') { Fail 'ADENGINE_SITE must be a plain https:// URL.' } $PythonVersion = if ($env:ADENGINE_PYTHON) { $env:ADENGINE_PYTHON } else { '3.12' } if ($PythonVersion -notmatch '^[A-Za-z0-9.@+_-]+$') { Fail 'ADENGINE_PYTHON must be a Python version such as 3.12.' } $UvInstallerUrl = if ($env:ADENGINE_UV_INSTALLER_URL) { $env:ADENGINE_UV_INSTALLER_URL } else { 'https://astral.sh/uv/install.ps1' } if ($UvInstallerUrl -notmatch '^https?://[A-Za-z0-9._~:/?&=%+-]+$') { Fail 'ADENGINE_UV_INSTALLER_URL must be a plain URL.' } $SavedToken = $env:ADENGINE_ACCESS_TOKEN $SavedPackage = $env:ADENGINE_PACKAGE $SavedUtf8 = $env:PYTHONUTF8 $SavedIoEncoding = $env:PYTHONIOENCODING $SavedProtocol = [Net.ServicePointManager]::SecurityProtocol $Tmp = $null try { # An access token is optional: only a private (development) channel needs one, and it is sent only when # the channel asks. A leftover token is then never sent to a public channel or recorded as private. $Token = $SavedToken if ($Token) { Remove-Item Env:ADENGINE_ACCESS_TOKEN -ErrorAction SilentlyContinue } $Access = 'public' $Headers = @{} # Windows PowerShell 5.1 on older systems does not offer TLS 1.2 by default. try { [Net.ServicePointManager]::SecurityProtocol = $SavedProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch { } try { $Answer = Get-Text "$Site/api/releases/latest?format=sh" $Headers } catch { Fail "could not reach the release channel at $Site ($($_.Exception.Message)). Check your connection and run the command again." } if (([string]$Answer).Trim().StartsWith('private_beta') -and $Token) { if ($Token.Length -gt 200 -or $Token -notmatch '^[A-Za-z0-9._~+/=-]+$') { Fail 'ADENGINE_ACCESS_TOKEN contains characters an access token never has. Check its value.' } $Access = 'private' $Headers = @{ Authorization = "Bearer $Token" } try { $Answer = Get-Text "$Site/api/releases/latest?format=sh" $Headers } catch { Fail "could not reach the release channel at $Site ($($_.Exception.Message)). Check your connection and run the command again." } } $Parts = @(([string]$Answer).Trim() -split '\s+') $Status = [string]$Parts[0] if ($Status -eq 'unreleased') { Say "No Ad Engine release has been published on $Site yet, so nothing was installed. Try again later." return } if ($Status -eq 'private_beta') { if ($Access -eq 'private') { Fail 'this channel did not accept the access token in ADENGINE_ACCESS_TOKEN. Check it and run the command again.' } Fail 'this channel needs an access token. Set $env:ADENGINE_ACCESS_TOKEN and run the command again.' } if ($Status.StartsWith('{')) { Fail 'the release channel answered in a format this installer cannot read. Try again in a few minutes.' } if ($Status -ne 'available' -or $Parts.Count -ne 4) { Fail 'the release channel returned an unexpected answer.' } $Name = [string]$Parts[1] $Sha = [string]$Parts[2] $Version = [string]$Parts[3] if ($Name -notmatch '^adengine-[A-Za-z0-9._+-]+\.whl$') { Fail "the release channel offered $Name, which is not an Ad Engine wheel." } if ($Sha -cnotmatch '^[0-9a-f]{64}$' -or $Version -notmatch '^[A-Za-z0-9._+-]+$') { Fail 'the release channel returned an unexpected answer.' } $Tmp = Join-Path ([IO.Path]::GetTempPath()) ('adengine-' + [guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $Tmp | Out-Null $Download = Join-Path $Tmp $Name Say "Downloading Ad Engine $Version" try { Invoke-WebRequest -Uri "$Site/releases/$Name" -Headers $Headers -OutFile $Download -UseBasicParsing } catch { Fail "the download failed ($($_.Exception.Message)). Check your connection and run the command again." } $Headers = $null $Token = $null Remove-Variable -Name Token, Headers -ErrorAction SilentlyContinue $Got = Get-Sha256 $Download if ($Got -ne $Sha) { Fail "checksum mismatch (expected $Sha, got $Got). Nothing was installed." } # Keep the verified wheel where uv and `adengine upgrade` can find it again. $AdeHome = if ($env:ADENGINE_HOME) { $env:ADENGINE_HOME } else { Join-Path (Get-HomeDir) '.adengine' } if ($AdeHome.StartsWith('~')) { $AdeHome = (Get-HomeDir) + $AdeHome.Substring(1) } $AdeHome = [IO.Path]::GetFullPath($AdeHome) $Releases = Join-Path $AdeHome 'releases' New-Item -ItemType Directory -Force -Path $Releases | Out-Null $Wheel = Join-Path $Releases $Name $WheelExisted = Test-Path -LiteralPath $Wheel -PathType Leaf $Partial = Join-Path $Releases ".$Name.partial" Copy-Item -LiteralPath $Download -Destination $Partial -Force Move-Item -LiteralPath $Partial -Destination $Wheel -Force if ((Get-Sha256 $Wheel) -ne $Sha) { Fail "the saved copy in $Releases does not match the checksum. Nothing was installed." } # An earlier run that added uv keeps saying so, for `adengine uninstall`. $UvAdded = $false $RecordPath = Join-Path $Releases 'current.json' try { if (Test-Path -LiteralPath $RecordPath -PathType Leaf) { $UvAdded = (Get-Field ([IO.File]::ReadAllText($RecordPath) | ConvertFrom-Json) 'uv_installed_by_installer') -eq $true } } catch { $UvAdded = $false } $Uv = Find-Uv if (-not $Uv) { Say 'Installing uv, the Python package manager from astral.sh' # A separate process keeps this script's strict mode away from the official installer. Settings such # as TLS 1.2 belong to one process, so the child enables it too (astral.sh accepts TLS 1.2 or later). $Shell = Get-PowerShellExe if (-not $Shell) { Fail 'could not find powershell.exe to run the uv installer. Install uv with: winget install --id=astral-sh.uv -e. Then run this command again.' } Invoke-Native $Shell @('-NoProfile', '-ExecutionPolicy', 'ByPass', '-Command', ("try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072 } catch { }; " + "`$ProgressPreference = 'SilentlyContinue'; Invoke-RestMethod -UseBasicParsing '$UvInstallerUrl' | Invoke-Expression")) $BootstrapExit = $LASTEXITCODE $Uv = Find-Uv if (-not $Uv) { $Policy = 'unknown' try { $Policy = [string](Get-ExecutionPolicy) } catch { } Fail ("uv could not be installed (installer exit code $BootstrapExit, execution policy $Policy). " + 'If this computer blocks PowerShell scripts (execution policy AllSigned or Restricted) or cannot reach astral.sh, ' + 'install uv another way, for example: winget install --id=astral-sh.uv -e. Then run this command again.') } $UvAdded = $true } Say "Using uv at $Uv" $ToolsDir = Invoke-Capture $Uv @('tool', 'dir') if ($LASTEXITCODE -ne 0 -or -not $ToolsDir) { Fail "could not find uv's tool folder." } if ($OnWindows) { $Running = @(Get-RunningEngine (Join-Path $ToolsDir 'adengine')) for ($attempt = 0; $Running.Count -gt 0 -and $attempt -lt 3 -and (Test-Interactive); $attempt++) { $list = ($Running | ForEach-Object { "$($_.ProcessName) ($($_.Id))" }) -join ', ' Say "Ad Engine is running right now: $list." Say 'Quit Claude Desktop, Cursor, Claude Code and Codex so Windows can replace its files, then press Enter.' [void](Read-Host) $Running = @(Get-RunningEngine (Join-Path $ToolsDir 'adengine')) } if ($Running.Count -gt 0) { Fail 'Ad Engine is still running inside an agent app. Quit Claude Desktop, Cursor, Claude Code and Codex, then run this command again.' } } $BinAnswer = Invoke-Capture $Uv @('tool', 'dir', '--bin') if ($LASTEXITCODE -ne 0 -or -not $BinAnswer) { Fail "could not find uv's tool folder." } Say "Installing Ad Engine $Version (Python $PythonVersion)" # With its bin folder on PATH, uv does not print its own PATH advice; the closing lines hold the one notice. $SavedPath = $env:PATH $env:PATH = $BinAnswer + $PathSeparator + $env:PATH try { Invoke-Native $Uv @('tool', 'install', '--force', '--reinstall-package', 'adengine', '--python', $PythonVersion, $Wheel) } finally { $env:PATH = $SavedPath } if ($LASTEXITCODE -ne 0) { if (-not $WheelExisted) { Remove-Item -LiteralPath $Wheel -Force -ErrorAction SilentlyContinue } Fail ('uv could not install Ad Engine (see the messages above). Run this command again once they are resolved; ' + 'if they mention a file in use, quit the agent apps that run Ad Engine first.') } $Record = [ordered]@{ schema = 'adengine-release/1' site = $Site channel = "$Site/api/releases/latest" download_url = "$Site/releases/$Name" name = $Name version = $Version sha256 = $Sha wheel = $Wheel python = $PythonVersion installed_at = [DateTime]::UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", [Globalization.CultureInfo]::InvariantCulture) access = $Access uv_installed_by_installer = $UvAdded uv = $Uv installer = 'install.ps1' } $RecordTmp = Join-Path $Releases '.current.json.tmp' [IO.File]::WriteAllText($RecordTmp, ($Record | ConvertTo-Json) + "`n", (New-Object Text.UTF8Encoding $false)) Move-Item -LiteralPath $RecordTmp -Destination (Join-Path $Releases 'current.json') -Force Get-ChildItem -LiteralPath $Releases -Filter 'adengine-*.whl' | Where-Object { $_.Name -ne $Name } | Remove-Item -Force -ErrorAction SilentlyContinue $HomeDir = Get-HomeDir $BinCandidates = @($BinAnswer, $env:UV_TOOL_BIN_DIR, $env:XDG_BIN_HOME) if ($env:XDG_DATA_HOME) { $BinCandidates += (Join-Path (Split-Path $env:XDG_DATA_HOME -Parent) 'bin') } $BinCandidates += (Join-Parts $HomeDir @('.local', 'bin')) $BinDir = Find-Folder $BinCandidates ('adengine' + $Exe) if (-not $BinDir) { Fail "the adengine command is missing from $BinAnswer after the install." } $Adengine = Join-Path $BinDir ('adengine' + $Exe) $ToolLeaf = if ($OnWindows) { Join-Parts 'adengine' @('Scripts', 'python.exe') } else { Join-Parts 'adengine' @('bin', 'python') } $ToolCandidates = @($ToolsDir, $env:UV_TOOL_DIR) if ($env:APPDATA) { $ToolCandidates += (Join-Parts $env:APPDATA @('uv', 'tools')); $ToolCandidates += (Join-Parts $env:APPDATA @('uv', 'data', 'tools')) } if ($env:XDG_DATA_HOME) { $ToolCandidates += (Join-Parts $env:XDG_DATA_HOME @('uv', 'tools')) } $ToolCandidates += (Join-Parts $HomeDir @('.local', 'share', 'uv', 'tools')) $ToolRoot = Find-Folder $ToolCandidates $ToolLeaf $ToolPy = if ($ToolRoot) { Join-Path $ToolRoot $ToolLeaf } else { $null } # Python writes UTF-8 to the pipes read below, so paths under a user name outside the ANSI code page print # (and never stop the step) in the ffmpeg step and in setup. Put back as they were in the finally block. $env:PYTHONUTF8 = '1' $env:PYTHONIOENCODING = 'utf-8' $FfmpegOk = $false if ($ToolPy) { Invoke-Native $ToolPy @('-m', 'adengine.install.ffmpeg_provision', '--if-needed') $FfmpegOk = $LASTEXITCODE -eq 0 } if (-not $FfmpegOk) { Warn 'ffmpeg is not ready yet. Setup checks it again next and says what to do.' } Say 'Configuring Ad Engine for your agents and editor' $env:ADENGINE_PACKAGE = $Wheel $SetupText = Invoke-Capture $Adengine (@('setup', '--yes', '--json') + @($SetupArguments)) $SetupExit = $LASTEXITCODE $Report = Show-SetupReport $SetupText $Agents = $Report.Agents # Exit 0 is a finished install even when a step is left for the user; only a real setup error fails. if ($SetupExit -ne 0) { if ($Report.Readiness) { Show-Steps $Report.Hints } # Exit 2 is a usage error, such as a mistyped option passed on to setup: its one line is shown above. if ($SetupExit -eq 2) { Fail 'setup stopped at the error above (exit 2). Fix it and run the command again.' } # The call operator makes the quoted path a command PowerShell runs when the line is pasted. $Doctor = "& '$($Adengine.Replace("'", "''"))' doctor" Fail "setup did not finish (exit $SetupExit). Fix the problems listed above and run the command again. To see the details, run: $Doctor" } Say '' Say "Ad Engine is installed at $Adengine" $LoginPath = if ($OnWindows) { [string][Environment]::GetEnvironmentVariable('Path', 'User') + ';' + [string][Environment]::GetEnvironmentVariable('Path', 'Machine') } else { [string]$env:PATH } $Entries = @($LoginPath -split [regex]::Escape($PathSeparator) | Where-Object { $_ } | ForEach-Object { $_.TrimEnd('\', '/') }) $SessionLine = " `$env:Path = '$($BinDir.Replace("'", "''"))$PathSeparator' + `$env:Path" if ($Entries -notcontains $BinDir.TrimEnd('\', '/')) { # uv's own messages stay hidden: it reports an error when the setting is already up to date, which # still means a new terminal finds adengine. One notice follows. $UpdateOut = Invoke-Capture $Uv @('tool', 'update-shell') -Quiet if ($LASTEXITCODE -eq 0 -or $UpdateOut -match 'already up.to.date') { Say 'Open a new terminal to use the adengine command, or run this in the current one:' } else { Warn "could not add $BinDir to your PATH automatically. To use the adengine command, run:" } Say $SessionLine } elseif ($Report.OnPath -eq $false) { # Setup's adengine_command describes this window: the saved PATH already has the folder, and a # window opened before an earlier install does not. Say 'To use the adengine command in this window, run:' Say $SessionLine } # Setup's own next steps: "Done." with the first prompt when it is ready, or the steps left, ending # with /adengine-setup when an agent is registered and with installing an agent when none is. if ($Report.Readiness) { if ($Report.Ready) { $Step = if ($Report.NextStep) { $Report.NextStep } else { "Open your agent and run $($Report.FirstPrompt)" } Say ('Done. ' + (Format-Sentence $Step)) } else { if ($Agents -eq 'none') { Say 'No agent is connected to Ad Engine yet.' } $Hints = @($Report.Hints) if ($Hints.Count -eq 0) { $Hints = @('Run adengine doctor to see what is left.') } Show-Steps $Hints } return } # Releases older than 0.5.0 report no readiness of their own. if ($Agents -eq 'none') { Say 'No agent app was found, so Ad Engine is not connected to one yet.' Say 'Install Claude Code, Codex, Claude Desktop or Cursor, then run adengine setup.' return } # /adengine-setup is named only when setup put that skill in an agent's skill folder. $Next = if ($Agents -eq 'setup') { 'run /adengine-setup' } else { 'ask it to use Ad Engine' } if (-not $FfmpegOk) { Say "Ad Engine is installed, and ffmpeg still needs the fix shown above. Then open your agent and $Next." return } Say "Done. Open your agent and $Next." } finally { if ($null -ne $SavedToken) { $env:ADENGINE_ACCESS_TOKEN = $SavedToken } if ($null -ne $SavedPackage) { $env:ADENGINE_PACKAGE = $SavedPackage } else { Remove-Item Env:ADENGINE_PACKAGE -ErrorAction SilentlyContinue } if ($null -ne $SavedUtf8) { $env:PYTHONUTF8 = $SavedUtf8 } else { Remove-Item Env:PYTHONUTF8 -ErrorAction SilentlyContinue } if ($null -ne $SavedIoEncoding) { $env:PYTHONIOENCODING = $SavedIoEncoding } else { Remove-Item Env:PYTHONIOENCODING -ErrorAction SilentlyContinue } try { [Net.ServicePointManager]::SecurityProtocol = $SavedProtocol } catch { } if ($Tmp) { Remove-Item -LiteralPath $Tmp -Recurse -Force -ErrorAction SilentlyContinue } Remove-Variable -Name Token, Headers, SavedToken -ErrorAction SilentlyContinue } } -SetupArguments @($args)