# agent-standups installer for Windows. # # irm https://git.rethinkstudios.io/rethink-software/agent-standups/raw/tag/v0.1.2/install.ps1 | iex # # Flags (when invoked as a file): -SkillOnly -NoNode -NoCodex -Registry -Yes # Through `irm | iex` the defaults apply; download it to pass flags. # # Idempotent: on an installed box it prints status and changes nothing. # Elevation is requested only by winget, and only when Node must be installed. [CmdletBinding()] param( [switch]$SkillOnly, [switch]$NoNode, [switch]$NoCodex, [string]$Registry = 'https://git.rethinkstudios.io/api/packages/rethink-software/npm/', [switch]$Yes ) # The body runs in a scriptblock of its own (R196c). Under `irm | iex` the # script runs IN the user's session: an `exit` closed their PowerShell window, # and `$ErrorActionPreference = 'Stop'` and the helpers below stayed defined in # it afterwards. Inside `& { }` they are the block's own and go with it. So # nothing in the body may `exit`: the already-installed path `return`s from the # block, and Die throws -- an error at the prompt under iex, exit 1 when the # script is run as a file. & { $ErrorActionPreference = 'Stop' $MinNode = 20 $DaemonFloor = '22.13' # node:sqlite without a flag; see Test-DaemonNode $Pkg = 'agent-standups' $Problems = New-Object 'System.Collections.Generic.List[string]' function Say { param($m) Write-Host " $m" } function Step { param($m) Write-Host "`n== $m" } function Warn { param($m) Write-Warning $m; $Problems.Add($m) } function Die { param($m) throw $m } function Have { param($n) [bool](Get-Command $n -ErrorAction SilentlyContinue) } function Get-NodeMajor { param($exe = 'node') try { [int](& $exe -p 'Number(process.versions.node.split(".")[0])' 2>$null) } catch { 0 } } # Can this node run the daemon? Asked of the node itself, not read off its # major: node:sqlite loads WITHOUT --experimental-sqlite only from 22.13.0 and # 23.4.0, and nothing passes that flag, so a major-only test accepted # 22.0-22.12 and 23.0-23.3, where the daemon dies on import (CF-62). # `--no-warnings` and a local 'Continue': the ExperimentalWarning a good node # prints on stderr would otherwise be a terminating NativeCommandError under # this script's 'Stop', and read as a node that cannot run the daemon. function Test-DaemonNode { param($exe = 'node') $ErrorActionPreference = 'Continue' try { & $exe --no-warnings -e 'require("node:sqlite")' 2>$null | Out-Null return ($LASTEXITCODE -eq 0) } catch { return $false } } # Refresh PATH from the registry so a winget install is visible without a new # shell -- the whole point of doing it in-session. function Update-PathFromRegistry { $machine = [Environment]::GetEnvironmentVariable('Path', 'Machine') $user = [Environment]::GetEnvironmentVariable('Path', 'User') $env:Path = (@($machine, $user) | Where-Object { $_ }) -join ';' } # ------------------------------------------------------------- 1. detect ---- Step 'detect' Say "os : Windows $([Environment]::OSVersion.Version)" $inRepo = $false $scriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { '' } if ($scriptDir -and (Test-Path (Join-Path $scriptDir 'package.json'))) { if ((Get-Content (Join-Path $scriptDir 'package.json') -Raw) -match "`"name`"\s*:\s*`"$Pkg`"") { $inRepo = $true } } Say "source : $(if ($inRepo) { "repo checkout ($scriptDir)" } else { "standalone (registry $Registry)" })" $nodeMajor = if (Have node) { Get-NodeMajor } else { 0 } Say "node : $(if (Have node) { (& node --version) } else { 'not installed' })" Say "winget : $(if (Have winget) { 'present' } else { 'not available' })" $standupBin = if (Have standup) { (Get-Command standup).Source } else { '' } Say "standup : $(if ($standupBin) { $standupBin } else { 'not installed' })" $codexBin = (Get-Command codex -ErrorAction SilentlyContinue).Source Say "codex : $(if ($codexBin) { "$codexBin$(if ($NoCodex) { ' (-NoCodex)' })" } else { 'not installed' })" $standupHome = if ($env:STANDUP_HOME) { $env:STANDUP_HOME } else { Join-Path $HOME '.standup' } Say "config : $(if (Test-Path (Join-Path $standupHome 'config.toml')) { 'exists' } else { 'absent' })" # ------------------------------------------------- 2. choose the target path - $wantDaemon = -not $SkillOnly $daemonOk = $wantDaemon -and (Have node) -and (Test-DaemonNode) # ------------------------------------------------------- 3. provision Node --- function Install-Node { Step "installing Node LTS via winget" if (-not (Have winget)) { Die "winget is not available and Node $DaemonFloor+ is required. Install Node manually from https://nodejs.org/ and re-run, or pass -SkillOnly." } # Elevation may be prompted by winget itself; we never elevate the script. $args = @('install', '--id', 'OpenJS.NodeJS.LTS', '--source', 'winget', '--accept-package-agreements', '--accept-source-agreements') if ($Yes) { $args += '--silent' } & winget @args if ($LASTEXITCODE -ne 0) { Die "winget install failed (exit $LASTEXITCODE)" } Update-PathFromRegistry if (-not (Have node)) { Die 'Node was installed but is still not on PATH. Open a new terminal and re-run.' } Say "installed : $(& node --version) at $((Get-Command node).Source)" } if ($wantDaemon -and -not $daemonOk) { if ($NoNode) { Warn "no Node >= $DaemonFloor and -NoNode was given: installing skill-only (no daemon)." $wantDaemon = $false } else { Install-Node $nodeMajor = Get-NodeMajor $daemonOk = Test-DaemonNode if (-not $daemonOk) { Warn "Node $(& node --version) cannot load node:sqlite (needs $DaemonFloor+); continuing skill-only."; $wantDaemon = $false } } } if ($nodeMajor -lt $MinNode) { if ($NoNode) { Die "Node is below the floor of $MinNode and -NoNode was given." } Install-Node $nodeMajor = Get-NodeMajor } if (-not (Have npm)) { Die 'npm is not installed.' } Step 'plan' Say "running node: $(& node --version) ($((Get-Command node).Source))" Say "daemon : $(if ($wantDaemon) { 'scheduled task at logon' } else { 'skill-only -- no daemon, search or managed agents' })" # --------------------------------------------------- 4. already installed? --- # With a daemon the task is part of the install too (C82-34, as install.sh): # Ctrl-C at step 7 left skill and crossSessionInbound green and the task never # started, and the re-run printed "Nothing changed". A box whose task is not # running now goes straight to step 7; steps 5-6 are done and are not redone. $serviceOnly = $false if ($standupBin) { $doctorJson = (& standup doctor --json 2>$null) | Out-String if ($LASTEXITCODE -eq 0 -or $doctorJson.Trim().StartsWith('[')) { try { $checks = $doctorJson | ConvertFrom-Json $skill = $checks | Where-Object { $_.check -eq 'skill' } $cross = $checks | Where-Object { $_.check -eq 'crossSessionInbound' } $serviceUp = $true if ($skill.ok -and $cross.ok -and $wantDaemon) { & standup service status | Out-Null $serviceUp = ($LASTEXITCODE -eq 0) } if ($skill.ok -and $cross.ok -and -not $serviceUp) { Step 'already installed, but the daemon task is not running -- finishing the service step' $serviceOnly = $true } elseif ($skill.ok -and $cross.ok) { Step 'already installed -- status only' Say "standup $(& standup version 2>$null) at $standupBin" & standup doctor if ($wantDaemon) { Write-Host ''; & standup service status } Write-Host "`nNothing changed. Individual steps:" # `service install` is operator-only once a code exists (R204#12). Say 'standup install --runtime claude # skill, hooks' Say 'standup service install --code # daemon task' Say 'standup doctor # verify' return } } catch { } } } # ------------------------------------------------------- 5. install the CLI -- # Steps 5 and 6 are skipped when step 4 found them done and only the task left. if (-not $serviceOnly) { if ($inRepo) { Step 'building from source' Push-Location $scriptDir try { if (Test-Path 'package-lock.json') { & npm ci --silent } else { & npm install --silent } if ($LASTEXITCODE -ne 0) { Die 'npm install failed' } & npm run build --silent if ($LASTEXITCODE -ne 0) { Die 'build failed' } & npm link | Out-Null if ($LASTEXITCODE -ne 0) { Die 'npm link failed' } } finally { Pop-Location } } else { Step "installing $Pkg" # No `latest` dist-tag while only release candidates are published, so a bare # name resolves nothing; ask for `next` in that case. $spec = $Pkg if ($env:STANDUP_VERSION) { $spec = "$Pkg@$($env:STANDUP_VERSION)" } else { $latest = (& npm view $Pkg --registry $Registry version 2>$null | Out-String).Trim() if (-not $latest) { $spec = "$Pkg@next"; Say "installing $spec (no stable release yet)" } } & npm install -g $spec --registry $Registry | Out-Null if ($LASTEXITCODE -ne 0) { Die 'npm install -g failed' } } Update-PathFromRegistry if (-not (Have standup)) { $npmBin = (& npm prefix -g 2>$null) Die "standup is not on PATH after install. Add $npmBin to PATH and re-run." } Say "standup $(& standup version 2>$null) at $((Get-Command standup).Source)" } # ------------------------------------------------- 6. runtime: skill + hooks - # As on Unix, the runtime install's last step posts a synthetic Stop, which # needs a server answering -- start a throwaway one so the check is real. if (-not $serviceOnly) { Step 'installing the Claude runtime' $probeDir = Join-Path ([IO.Path]::GetTempPath()) ("standup-probe-" + [guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $probeDir -Force | Out-Null $probe = $null try { # `--no-git`, as install.sh has it (R06): the probe dir is a bare temp # directory and `create` refuses outside a git work tree, so without it # nothing was created, `serve` had no project, and the synthetic-Stop check # failed on every Windows install (CF-56). Never `--yes`, which would # `git init` a temp directory. And `serve` only when `create` succeeded. # `--no-register`, as install.sh has it (C71-49, C72-57): the directory is # deleted below, and with a daemon already up a registered throwaway stayed # in its registry and dashboard as a detached `install-probe` project after # every (re)install. Push-Location $probeDir & standup create --no-git --no-register --name install-probe 2>$null | Out-Null $created = ($LASTEXITCODE -eq 0) Pop-Location if ($created) { $probe = Start-Process -FilePath 'standup' -ArgumentList 'serve' -WorkingDirectory $probeDir ` -WindowStyle Hidden -PassThru for ($i = 0; $i -lt 12; $i++) { if (((& standup doctor --json 2>$null) | Out-String) -match '"answering on') { break } Start-Sleep -Milliseconds 300 } } } catch { } & standup install --runtime claude if ($LASTEXITCODE -ne 0) { Warn 'runtime install reported a problem; see doctor below' } # Codex where its binary exists. Machine-wide: the skill and the hooks. The # AGENTS.md step needs a project and is skipped here -- named in the closing # lines instead. Not run at all without codex, so no marked blocks are left # for a tool that is not installed; -NoCodex opts out where it is. if (-not $NoCodex -and $codexBin) { & standup install --runtime codex if ($LASTEXITCODE -ne 0) { Warn 'codex runtime install reported a problem; see doctor below' } } if ($probe -and -not $probe.HasExited) { Stop-Process -Id $probe.Id -Force -ErrorAction SilentlyContinue } Remove-Item -Recurse -Force $probeDir -ErrorAction SilentlyContinue } # ---------------------------------------------------------- 7. the service --- if ($wantDaemon) { Step 'installing the standupd scheduled task' # Once an operator code exists, `service install` and `service start` are # operator-only (R203#14, R203#15) -- and a plain `uninstall` keeps the home, # code included, so a reinstall over it was refused here. This is the # operator's own installer: it reads the code the way the CLI lets the # operator read it locally and hands it to these two calls only, in their # environment (never argv), restoring the session's variable afterwards. # Whether one is SET is asked first, because `standup code` mints one when # there is none; a first install has none and passes nothing. As install.sh. $opCode = $null try { $codeStatus = (& standup expose --status --json 2>$null | Out-String) | ConvertFrom-Json if ($codeStatus.operatorCode -eq 'set') { $opCode = ((& standup code --json 2>$null | Out-String) | ConvertFrom-Json).code } } catch { $opCode = $null } $hadCode = Test-Path Env:STANDUP_OPERATOR_CODE $prevCode = $env:STANDUP_OPERATOR_CODE if ($opCode) { $env:STANDUP_OPERATOR_CODE = $opCode } try { & standup service install if ($LASTEXITCODE -eq 0) { & standup service start | Out-Null if ($LASTEXITCODE -ne 0) { Warn "task registered but did not start; see 'standup service status'" } } else { Warn "service install failed; single mode still works. See 'standup service status'." } } finally { if ($opCode) { if ($hadCode) { $env:STANDUP_OPERATOR_CODE = $prevCode } else { Remove-Item Env:STANDUP_OPERATOR_CODE -ErrorAction SilentlyContinue } } } } # ------------------------------------------------------------ 8. hand over --- Step 'verifying' & standup doctor if ($LASTEXITCODE -ne 0) { Warn 'some checks need attention (above)' } if ($Problems.Count -eq 0) { Step 'done' } else { Step 'done, with things to know' foreach ($p in $Problems) { Say "- $p" } Write-Host '' } $port = if ($env:STANDUP_PORT) { $env:STANDUP_PORT } else { '7335' } Say "dashboard : http://127.0.0.1:$port" Write-Host '' & standup code Write-Host '' Write-Host 'Next: cd ; standup create' if (-not $NoCodex -and $codexBin) { Write-Host '' Write-Host 'Codex: the skill and hooks are wired machine-wide. Codex must TRUST them:' Write-Host ' run /hooks inside Codex and approve.' Write-Host 'Per project, for the room rules in AGENTS.md:' Write-Host ' standup install --runtime codex' } }