The installer cannot run `--runtime codex` for you: the verb requires a project, since one of its targets is the project's own AGENTS.md. It now detects codex and prints the command to run in each project instead. Signed-off-by: disqualifier <dev@disqualifier.me>
313 lines
14 KiB
PowerShell
313 lines
14 KiB
PowerShell
# agent-standups installer for Windows.
|
|
#
|
|
# irm https://git.rethinkstudios.io/rethink-software/agent-standups/raw/tag/v0.1.0/install.ps1 | iex
|
|
#
|
|
# Flags (when invoked as a file): -SkillOnly -NoNode -NoCodex -Registry <url> -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 <operator 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' }
|
|
|
|
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 <your project>; standup create'
|
|
# Codex's runtime is per-project -- one of its three targets is the project's
|
|
# own AGENTS.md -- so the installer cannot wire it from here: there is no
|
|
# project yet, and the runtime step's throwaway is deleted. Name the command
|
|
# instead, only where codex exists and the user has not opted out.
|
|
if (-not $NoCodex -and $codexBin) {
|
|
Write-Host ''
|
|
Write-Host 'Codex is installed here. In each project you want it in:'
|
|
Write-Host ' standup install --runtime codex # hooks, skill, a block in AGENTS.md'
|
|
}
|
|
}
|