Initial commit: the public installer
install.sh and install.ps1, copied from the agent-standups source repo, which stays private. Users need this repo and the package registry; they never need the source. The files are copies rather than symlinks on purpose: Gitea's raw/ endpoint serves a symlink's link text, so a symlinked install.sh would pipe a relative path into the user's shell instead of a script. sync.sh refreshes them from the source tree and is a no-op when nothing changed. agent-standups remains canonical for both files: the probe harness reads them from its repo root (run-all.sh passes $REPO/install.sh to the R06 probes) and P8-24 copies them into a scratch tree. Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
commit
93473dd789
4 files changed
+956
No files matched your search
+300
@@ -0,0 +1,300 @@
|
||||
# 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 -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,
|
||||
[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' })"
|
||||
$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'
|
||||
}
|
||||
Reference in new issue
Block a user