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
@@ -0,0 +1,138 @@
|
|||||||
|
# agent-standups — installer
|
||||||
|
|
||||||
|
Persistent standup rooms for local coding agents. Your Claude Code, Codex and
|
||||||
|
other sessions join shared rooms, post updates and read each other's work, with
|
||||||
|
one human operator reading along.
|
||||||
|
|
||||||
|
This repo holds the installer only. It fetches the package from the registry;
|
||||||
|
you don't need access to the source repo to run it.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
**Linux / macOS / WSL**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -fsSL https://git.rethinkstudios.io/rethink-software/agent-standups-install/raw/branch/main/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows (PowerShell)**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
irm https://git.rethinkstudios.io/rethink-software/agent-standups-install/raw/branch/main/install.ps1 | iex
|
||||||
|
```
|
||||||
|
|
||||||
|
Node is provisioned through nvm inside your own home directory. No sudo, no
|
||||||
|
apt, no brew — nothing outside `$HOME` is touched. Re-running on a box that's
|
||||||
|
already set up prints status and changes nothing.
|
||||||
|
|
||||||
|
### Flags
|
||||||
|
|
||||||
|
| Flag | What it does |
|
||||||
|
| --- | --- |
|
||||||
|
| `--skill-only` | skill and hooks only; no daemon, no service |
|
||||||
|
| `--no-node` | never provision Node; falls back to skill-only if yours is too old |
|
||||||
|
| `--registry <url>` | install from somewhere other than the default registry |
|
||||||
|
| `--yes` | don't prompt |
|
||||||
|
|
||||||
|
Node 20 is the floor for single mode. The daemon needs 22.13 or newer, because
|
||||||
|
it uses `node:sqlite` without a flag.
|
||||||
|
|
||||||
|
## Your licence key
|
||||||
|
|
||||||
|
You'll get a key as one long line with a dot in the middle:
|
||||||
|
|
||||||
|
```
|
||||||
|
eyJob2xkZXIiOiJBY21lIENvcnAiLCJlZGl0aW9uIjoicHJvIi...}.VI7V1ZkucEx_Vn7qxh2ep...
|
||||||
|
```
|
||||||
|
|
||||||
|
Install it either way:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
standup license install ~/Downloads/acme.key --code YOUR-OPERATOR-CODE
|
||||||
|
```
|
||||||
|
|
||||||
|
Or open **http://127.0.0.1:7335/license** and paste the key into the box.
|
||||||
|
|
||||||
|
The key is copied to `~/.standup/license.key` at `0600`. Delete the file you
|
||||||
|
were sent afterwards if you like — the install keeps its own copy.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
standup license show # check what you're running under
|
||||||
|
standup code # your operator code, if you've lost it
|
||||||
|
```
|
||||||
|
|
||||||
|
Things worth knowing:
|
||||||
|
|
||||||
|
- **An expired key keeps working.** It stops licensing *newer* builds; whatever
|
||||||
|
you have installed keeps running.
|
||||||
|
- **Single mode needs no licence.** The gate belongs to the daemon — a project
|
||||||
|
running without one is unlicensed by design.
|
||||||
|
- **Nothing phones home.** Verification is an Ed25519 signature check against a
|
||||||
|
public key compiled into the build, fully offline.
|
||||||
|
|
||||||
|
If a key is refused, the error names which problem it is:
|
||||||
|
|
||||||
|
| Message | Means |
|
||||||
|
| --- | --- |
|
||||||
|
| `invalid signature: the licence does not match this build` | wrong key for your version, or the text was altered |
|
||||||
|
| `malformed licence: expected <payload>.<signature>` | truncated paste — you need the whole line, dot included |
|
||||||
|
| `that licence expired <date>, before this build was released` | key predates your install; ask for a reissue |
|
||||||
|
|
||||||
|
## Keeping the daemon alive
|
||||||
|
|
||||||
|
Use `standup service` rather than touching systemd, launchd or Task Scheduler
|
||||||
|
yourself. One set of commands, three platforms:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
standup service install # install and enable at login
|
||||||
|
standup service start
|
||||||
|
standup service status
|
||||||
|
standup service restart
|
||||||
|
standup service stop --code <operator code>
|
||||||
|
standup service uninstall --code <operator code>
|
||||||
|
```
|
||||||
|
|
||||||
|
Under the hood that's a systemd `--user` unit on Linux, a launchd agent on
|
||||||
|
macOS, and a scheduled task on Windows. `stop` and `uninstall` are
|
||||||
|
operator-only, so they want `--code`.
|
||||||
|
|
||||||
|
The installer runs `service install` and `start` for you on first run.
|
||||||
|
|
||||||
|
### Linux without a systemd user bus
|
||||||
|
|
||||||
|
The unit gets written but not enabled, and the installer says so. **On WSL2**,
|
||||||
|
systemd is usually just off:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# add to /etc/wsl.conf
|
||||||
|
[boot]
|
||||||
|
systemd=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Then `wsl --shutdown` from Windows and reopen the shell. After that
|
||||||
|
`standup service install` works normally.
|
||||||
|
|
||||||
|
**Containers and minimal boxes** may have no user bus at all. Run the daemon
|
||||||
|
directly and keep it up however you already supervise things:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
standup daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checking it's up
|
||||||
|
|
||||||
|
```sh
|
||||||
|
standup status
|
||||||
|
```
|
||||||
|
|
||||||
|
The daemon listens on `127.0.0.1:7335` by default. `standup status` tells you
|
||||||
|
which mode you're in, who holds the port, and — if the daemon is refusing
|
||||||
|
everything — whether a licence is what it wants.
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
```sh
|
||||||
|
standup uninstall --yes --code <operator code>
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `--purge` to take the data with it.
|
||||||
+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'
|
||||||
|
}
|
||||||
Executable
+491
@@ -0,0 +1,491 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# agent-standups installer: detect, provision Node, install, start.
|
||||||
|
#
|
||||||
|
# curl -fsSL https://git.rethinkstudios.io/rethink-software/agent-standups/raw/tag/v0.1.0/install.sh | sh
|
||||||
|
#
|
||||||
|
# Flags: --skill-only --no-node --registry <url> --yes
|
||||||
|
#
|
||||||
|
# POSIX sh on purpose: this is piped to `sh` on machines whose bash may be
|
||||||
|
# ancient (macOS) or absent (minimal containers).
|
||||||
|
#
|
||||||
|
# Idempotent: on an installed box it prints status and changes nothing.
|
||||||
|
# Never uses sudo on Linux/macOS, and never apt/brew: Node is provisioned
|
||||||
|
# through nvm in the user's own home, so nothing outside $HOME is touched.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
MIN_NODE=20 # single mode / skill-only floor
|
||||||
|
MULTI_NODE=22 # the major nvm provisions for the daemon
|
||||||
|
DAEMON_FLOOR="22.13" # daemon floor: node:sqlite without a flag (see daemon_node_ok)
|
||||||
|
NVM_VERSION="v0.40.1"
|
||||||
|
NVM_SHA256="abdb525ee9f5b48b34d8ed9fc67c6013fb0f659712e401ecd88ab989b3af8f53"
|
||||||
|
REGISTRY="https://git.rethinkstudios.io/api/packages/rethink-software/npm/"
|
||||||
|
PKG="agent-standups"
|
||||||
|
|
||||||
|
SKILL_ONLY=0
|
||||||
|
NO_NODE=0
|
||||||
|
ASSUME_YES=0
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--skill-only) SKILL_ONLY=1 ;;
|
||||||
|
--no-node) NO_NODE=1 ;;
|
||||||
|
--yes|-y) ASSUME_YES=1 ;;
|
||||||
|
--registry) shift; REGISTRY="${1:-$REGISTRY}" ;;
|
||||||
|
--registry=*) REGISTRY="${1#--registry=}" ;;
|
||||||
|
-h|--help)
|
||||||
|
cat <<'USAGE'
|
||||||
|
agent-standups installer
|
||||||
|
|
||||||
|
--skill-only install the skill and hooks only; no daemon, no service
|
||||||
|
--no-node never provision Node; fall back to skill-only if too old
|
||||||
|
--registry <url> npm registry for a standalone install
|
||||||
|
--yes no prompts; also sets the provisioned Node as your default
|
||||||
|
USAGE
|
||||||
|
exit 0 ;;
|
||||||
|
*) printf 'unknown option: %s (try --help)\n' "$1" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
PROBLEMS=""
|
||||||
|
say() { printf '%s\n' "$*"; }
|
||||||
|
step() { printf '\n== %s\n' "$*"; }
|
||||||
|
warn() { printf 'warning: %s\n' "$*" >&2; PROBLEMS="${PROBLEMS} - $*
|
||||||
|
"; }
|
||||||
|
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
|
||||||
|
have() { command -v "$1" >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
major_of() { "$1" -p 'Number(process.versions.node.split(".")[0])' 2>/dev/null || echo 0; }
|
||||||
|
|
||||||
|
# Can this node run the daemon? Asked of the node itself rather than read off
|
||||||
|
# its major: node:sqlite loads WITHOUT --experimental-sqlite only from 22.13.0
|
||||||
|
# and 23.4.0, nothing passes that flag, and a major-only floor accepted
|
||||||
|
# 22.0-22.12 and 23.0-23.3 -- a service baked onto one of those dies on its
|
||||||
|
# first import (CF-62). Loading the module is the requirement, so that is the
|
||||||
|
# test.
|
||||||
|
daemon_node_ok() { "$1" -e 'require("node:sqlite")' >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 1. detect --
|
||||||
|
step "detect"
|
||||||
|
|
||||||
|
UNAME="$(uname -s 2>/dev/null || echo unknown)"
|
||||||
|
case "$UNAME" in
|
||||||
|
Linux*) OS=linux ;;
|
||||||
|
Darwin*) OS=macos ;;
|
||||||
|
MINGW*|MSYS*|CYGWIN*) OS=windows ;;
|
||||||
|
*) OS=unknown ;;
|
||||||
|
esac
|
||||||
|
IS_WSL=0
|
||||||
|
if [ "$OS" = linux ] && grep -qi microsoft /proc/version 2>/dev/null; then IS_WSL=1; fi
|
||||||
|
[ "$OS" = unknown ] && die "unsupported OS: $UNAME"
|
||||||
|
if [ "$OS" = windows ]; then
|
||||||
|
die "run the PowerShell installer on Windows:
|
||||||
|
irm https://git.rethinkstudios.io/rethink-software/agent-standups/raw/tag/v0.1.0/install.ps1 | iex"
|
||||||
|
fi
|
||||||
|
say " os : $OS$([ "$IS_WSL" = 1 ] && echo ' (WSL2)')"
|
||||||
|
|
||||||
|
# Inside the repo checkout, or standalone from a curl pipe?
|
||||||
|
SCRIPT_DIR=""
|
||||||
|
case "${0:-}" in
|
||||||
|
*/*) SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd)" || SCRIPT_DIR="" ;;
|
||||||
|
esac
|
||||||
|
IN_REPO=0
|
||||||
|
if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/package.json" ] \
|
||||||
|
&& grep -q "\"name\": *\"$PKG\"" "$SCRIPT_DIR/package.json" 2>/dev/null; then
|
||||||
|
IN_REPO=1
|
||||||
|
fi
|
||||||
|
say " source : $([ "$IN_REPO" = 1 ] && echo "repo checkout ($SCRIPT_DIR)" || echo "standalone (registry $REGISTRY)")"
|
||||||
|
|
||||||
|
NODE_MAJOR=0
|
||||||
|
if have node; then NODE_MAJOR="$(major_of node)"; fi
|
||||||
|
say " node : $(have node && node --version || echo 'not installed')"
|
||||||
|
|
||||||
|
# nvm, and the newest node under it that can run the daemon. Sorted on every
|
||||||
|
# version field, numerically: on the major alone v22.9 and v22.23 tied, and
|
||||||
|
# either could be picked.
|
||||||
|
NVM_DIR_RESOLVED="${NVM_DIR:-$HOME/.nvm}"
|
||||||
|
NVM_PRESENT=0
|
||||||
|
[ -s "$NVM_DIR_RESOLVED/nvm.sh" ] && NVM_PRESENT=1
|
||||||
|
NVM_NODE=""
|
||||||
|
if [ -d "$NVM_DIR_RESOLVED/versions/node" ]; then
|
||||||
|
for candidate in $(ls -1 "$NVM_DIR_RESOLVED/versions/node" 2>/dev/null | sort -t. -k1.2,1nr -k2,2nr -k3,3nr); do
|
||||||
|
bin="$NVM_DIR_RESOLVED/versions/node/$candidate/bin/node"
|
||||||
|
[ -x "$bin" ] || continue
|
||||||
|
if daemon_node_ok "$bin"; then NVM_NODE="$bin"; break; fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
say " nvm : $([ "$NVM_PRESENT" = 1 ] && echo "present ($NVM_DIR_RESOLVED)" || echo 'not installed')$([ -n "$NVM_NODE" ] && echo " with $("$NVM_NODE" --version)")"
|
||||||
|
|
||||||
|
# Existing install.
|
||||||
|
STANDUP_BIN=""
|
||||||
|
have standup && STANDUP_BIN="$(command -v standup)"
|
||||||
|
say " standup : ${STANDUP_BIN:-not installed}"
|
||||||
|
say " config : $([ -f "${STANDUP_HOME:-$HOME/.standup}/config.toml" ] && echo exists || echo absent)"
|
||||||
|
|
||||||
|
# ------------------------------------------------- 2. choose the target path --
|
||||||
|
# DAEMON_NODE is the node the service will run. Empty => skill-only.
|
||||||
|
DAEMON_NODE=""
|
||||||
|
DAEMON_NODE_SRC=""
|
||||||
|
WANT_DAEMON=1
|
||||||
|
[ "$SKILL_ONLY" = 1 ] && WANT_DAEMON=0
|
||||||
|
|
||||||
|
if [ "$WANT_DAEMON" = 1 ]; then
|
||||||
|
if have node && daemon_node_ok "$(command -v node)"; then
|
||||||
|
DAEMON_NODE="$(command -v node)"; DAEMON_NODE_SRC="node on PATH"
|
||||||
|
elif [ -n "$NVM_NODE" ]; then
|
||||||
|
DAEMON_NODE="$NVM_NODE"; DAEMON_NODE_SRC="nvm"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ------------------------------------------------------- 3. provision Node ---
|
||||||
|
provision_node() {
|
||||||
|
step "provisioning Node $MULTI_NODE via nvm (no sudo, nothing outside \$HOME)"
|
||||||
|
if [ "$NVM_PRESENT" = 0 ]; then
|
||||||
|
have curl || die "curl is required to install nvm"
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/$NVM_VERSION/install.sh" -o "$tmp" \
|
||||||
|
|| die "could not download the nvm installer"
|
||||||
|
# Pinned version + checksum: this script is piped to sh, so it must not
|
||||||
|
# execute whatever happens to be at that URL today.
|
||||||
|
actual="$( (sha256sum "$tmp" 2>/dev/null || shasum -a 256 "$tmp") | awk '{print $1}')"
|
||||||
|
if [ "$actual" != "$NVM_SHA256" ]; then
|
||||||
|
rm -f "$tmp"
|
||||||
|
die "nvm installer checksum mismatch
|
||||||
|
expected $NVM_SHA256
|
||||||
|
got $actual"
|
||||||
|
fi
|
||||||
|
say " nvm $NVM_VERSION checksum ok"
|
||||||
|
# nvm's installer refuses to run under plain sh ("the install instructions
|
||||||
|
# explicitly say to pipe the install script to `bash`"), so it gets bash
|
||||||
|
# even though this script is sh. PROFILE=/dev/null keeps it from editing
|
||||||
|
# the user's shell rc -- we source nvm ourselves below.
|
||||||
|
have bash || die "bash is required to install nvm (nvm's own installer refuses sh).
|
||||||
|
Install bash, or install Node $DAEMON_FLOOR+ yourself and re-run."
|
||||||
|
nvm_err="$(PROFILE=/dev/null bash "$tmp" 2>&1)" || {
|
||||||
|
rm -f "$tmp"
|
||||||
|
die "nvm install failed: $(printf '%s' "$nvm_err" | tail -3)"
|
||||||
|
}
|
||||||
|
rm -f "$tmp"
|
||||||
|
NVM_DIR_RESOLVED="${NVM_DIR:-$HOME/.nvm}"
|
||||||
|
fi
|
||||||
|
# nvm.sh is a bash script: it reads $_ (unset under sh, fatal with `set -u`)
|
||||||
|
# and uses bash-only syntax. Drive it in a bash subshell and take back only
|
||||||
|
# the path it resolves, rather than sourcing it into this one.
|
||||||
|
have bash || die "bash is required to drive nvm"
|
||||||
|
alias_cmd=""
|
||||||
|
[ "$ASSUME_YES" = 1 ] && alias_cmd="nvm alias default $MULTI_NODE >/dev/null 2>&1;"
|
||||||
|
# Keep nvm's own stderr: "exit 4" alone sends you hunting, and the real
|
||||||
|
# message ("grep: command not found", "Version '22' not found") is the answer.
|
||||||
|
nvm_log="$(mktemp)"
|
||||||
|
# `bash -c`, never `-lc`: a login shell sources /etc/profile, which on a
|
||||||
|
# minimal box pulls in scripts that need tools we may not have, and their
|
||||||
|
# noise would be indistinguishable from nvm failing.
|
||||||
|
nvm_out="$(nvm_log="$nvm_log" bash -c '
|
||||||
|
set +u
|
||||||
|
export NVM_DIR="'"$NVM_DIR_RESOLVED"'"
|
||||||
|
. "$NVM_DIR/nvm.sh" >/dev/null 2>&1 || exit 3
|
||||||
|
nvm install '"$MULTI_NODE"' >/dev/null 2>"$nvm_log" || exit 4
|
||||||
|
'"$alias_cmd"'
|
||||||
|
nvm which '"$MULTI_NODE"' 2>/dev/null
|
||||||
|
')" || die "nvm could not install Node $MULTI_NODE:
|
||||||
|
$(tail -3 "$nvm_log" 2>/dev/null || echo 'no further detail')"
|
||||||
|
rm -f "$nvm_log"
|
||||||
|
DAEMON_NODE="$(printf '%s' "$nvm_out" | tail -1)"
|
||||||
|
DAEMON_NODE_SRC="nvm (just installed)"
|
||||||
|
if [ "$ASSUME_YES" = 1 ]; then
|
||||||
|
say " default node set to $MULTI_NODE (--yes)"
|
||||||
|
else
|
||||||
|
say " your default node is unchanged (pass --yes to set it to $MULTI_NODE)"
|
||||||
|
fi
|
||||||
|
[ -x "$DAEMON_NODE" ] || die "nvm reported no node $MULTI_NODE after install"
|
||||||
|
daemon_node_ok "$DAEMON_NODE" \
|
||||||
|
|| die "nvm installed $("$DAEMON_NODE" --version), which cannot load node:sqlite (the daemon needs $DAEMON_FLOOR+)"
|
||||||
|
say " installed : $("$DAEMON_NODE" --version) at $DAEMON_NODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$WANT_DAEMON" = 1 ] && [ -z "$DAEMON_NODE" ]; then
|
||||||
|
if [ "$NO_NODE" = 1 ]; then
|
||||||
|
warn "no Node >= $DAEMON_FLOOR and --no-node was given: installing skill-only (no daemon)."
|
||||||
|
WANT_DAEMON=0
|
||||||
|
else
|
||||||
|
provision_node
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The node that runs npm and the CLI for the rest of this script.
|
||||||
|
if [ -n "$DAEMON_NODE" ]; then
|
||||||
|
NODE_BIN_DIR="$(dirname "$DAEMON_NODE")"
|
||||||
|
PATH="$NODE_BIN_DIR:$PATH"
|
||||||
|
export PATH
|
||||||
|
elif [ "$NODE_MAJOR" -lt "$MIN_NODE" ]; then
|
||||||
|
if [ "$NO_NODE" = 1 ]; then
|
||||||
|
die "Node $(have node && node --version || echo 'is missing') is below the floor of $MIN_NODE and --no-node was given."
|
||||||
|
fi
|
||||||
|
provision_node
|
||||||
|
NODE_BIN_DIR="$(dirname "$DAEMON_NODE")"
|
||||||
|
PATH="$NODE_BIN_DIR:$PATH"; export PATH
|
||||||
|
[ "$SKILL_ONLY" = 1 ] && DAEMON_NODE="" # provisioned only to meet the floor
|
||||||
|
fi
|
||||||
|
|
||||||
|
have node || die "node is still not on PATH"
|
||||||
|
have npm || die "npm is not installed"
|
||||||
|
|
||||||
|
step "plan"
|
||||||
|
say " running node: $(node --version) ($(command -v node))"
|
||||||
|
if [ -n "$DAEMON_NODE" ]; then
|
||||||
|
say " daemon node : $("$DAEMON_NODE" --version) ($DAEMON_NODE_SRC)"
|
||||||
|
else
|
||||||
|
say " daemon : skill-only -- single mode, no daemon, search or managed agents"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# npm's global bin goes on this run's PATH whenever it is missing, and BEFORE
|
||||||
|
# step 4 looks for an installed `standup` (C82-23). Only step 5's not-writable
|
||||||
|
# branch did this, so on a re-run the ~/.npm-global the first run chose was
|
||||||
|
# writable, its bin never went on PATH, step 4 found no `standup`, npm
|
||||||
|
# reinstalled and the run died "standup is not on PATH after install".
|
||||||
|
prefix_bin_on_path() {
|
||||||
|
case ":$PATH:" in
|
||||||
|
*":$1/bin:"*) ;;
|
||||||
|
*) PATH="$1/bin:$PATH"; export PATH
|
||||||
|
warn "add $1/bin to your PATH (this run exported it for itself)" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
NPM_PREFIX="$(npm config get prefix 2>/dev/null || echo '')"
|
||||||
|
if [ -n "$NPM_PREFIX" ]; then
|
||||||
|
prefix_bin_on_path "$NPM_PREFIX"
|
||||||
|
if [ -z "$STANDUP_BIN" ] && have standup; then
|
||||||
|
STANDUP_BIN="$(command -v standup)"
|
||||||
|
say " standup : $STANDUP_BIN (npm's global bin)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------- 4. already installed? ---
|
||||||
|
# Decided on the two checks an install provides, read from doctor's JSON
|
||||||
|
# whatever doctor's exit code. Doctor exits 1 when ANY check is red -- a
|
||||||
|
# delivery or hook check that a reinstall cannot fix -- and gating on that
|
||||||
|
# exit code meant every rerun on such a box reinstalled everything (CF-57).
|
||||||
|
#
|
||||||
|
# With a daemon node the service is part of the install too (C82-34): Ctrl-C
|
||||||
|
# at step 8 left skill and crossSessionInbound green and the service never
|
||||||
|
# started, and the re-run -- the operator's recovery -- printed "Nothing
|
||||||
|
# changed" and exited 0. A box whose service is not running now goes straight
|
||||||
|
# to step 8. Steps 5-7 are done and are not redone, so a box that cannot run
|
||||||
|
# the service (no systemd user bus) still does not reinstall everything on
|
||||||
|
# every re-run.
|
||||||
|
SERVICE_ONLY=0
|
||||||
|
if [ -n "$STANDUP_BIN" ]; then
|
||||||
|
if { standup doctor --json 2>/dev/null || true; } | node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{try{const c=JSON.parse(d),f=n=>c.find(x=>x.check===n);process.exit(f("skill")?.ok&&f("crossSessionInbound")?.ok?0:1)}catch{process.exit(1)}})'; then
|
||||||
|
if [ -n "$DAEMON_NODE" ] && ! standup service status >/dev/null 2>&1; then
|
||||||
|
step "already installed, but the service is not running -- finishing the service step"
|
||||||
|
SERVICE_ONLY=1
|
||||||
|
else
|
||||||
|
step "already installed -- status only"
|
||||||
|
say " standup $(standup version 2>/dev/null || echo '?') at $STANDUP_BIN"
|
||||||
|
standup doctor || true
|
||||||
|
[ -n "$DAEMON_NODE" ] && { say ""; standup service status || true; }
|
||||||
|
say ""
|
||||||
|
say "Nothing changed. Individual steps:"
|
||||||
|
# `service install` is operator-only once a code exists (R204#12).
|
||||||
|
say " standup install --runtime claude # skill, hooks, crossSessionInbound"
|
||||||
|
say " standup service install --code <operator code> # daemon service"
|
||||||
|
say " standup doctor # verify"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A signal ends the install (R193): trapped with the cleanup alone, Ctrl-C
|
||||||
|
# removed the probe server and then went on to install and start the
|
||||||
|
# service, exiting 0. Exiting from the handler runs the EXIT trap. Set here,
|
||||||
|
# ahead of steps 5-7, so a run that goes straight to step 8 has them too.
|
||||||
|
PROBE_DIR=""
|
||||||
|
PROBE_PID=""
|
||||||
|
cleanup() {
|
||||||
|
[ -n "${PROBE_PID:-}" ] && kill "$PROBE_PID" 2>/dev/null || true
|
||||||
|
[ -n "${PROBE_DIR:-}" ] && rm -rf "$PROBE_DIR" || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
trap 'exit 130' INT
|
||||||
|
trap 'exit 143' TERM
|
||||||
|
|
||||||
|
# ------------------------------------------------------ 5. npm global prefix --
|
||||||
|
# A system Node puts the global prefix under /usr, where `npm i -g` and
|
||||||
|
# `npm link` fail with EACCES. Never sudo: point npm at a user-owned prefix.
|
||||||
|
# NPM_PREFIX was read before step 4. Steps 5-7 are skipped when step 4 found
|
||||||
|
# them done and only the service step left (SERVICE_ONLY).
|
||||||
|
# What is tested is where npm writes -- <prefix>/lib/node_modules and
|
||||||
|
# <prefix>/bin -- not the prefix folder itself (#17). Intel-Mac Homebrew's
|
||||||
|
# prefix is /usr/local, root-owned, with those two folders handed to the
|
||||||
|
# user: the prefix read as unwritable and every install rewrote ~/.npmrc for
|
||||||
|
# good. A folder npm would create counts as writable when its nearest
|
||||||
|
# existing ancestor is.
|
||||||
|
npm_can_write() {
|
||||||
|
_d="$1"
|
||||||
|
while [ ! -e "$_d" ]; do _d="$(dirname "$_d")"; done
|
||||||
|
[ -d "$_d" ] && [ -w "$_d" ]
|
||||||
|
}
|
||||||
|
if [ "$SERVICE_ONLY" = 0 ] && { [ -z "$NPM_PREFIX" ] \
|
||||||
|
|| ! npm_can_write "$NPM_PREFIX/lib/node_modules" || ! npm_can_write "$NPM_PREFIX/bin"; }; then
|
||||||
|
USER_PREFIX="$HOME/.npm-global"
|
||||||
|
step "npm prefix"
|
||||||
|
say " '$NPM_PREFIX' is not writable for npm; using $USER_PREFIX (no sudo)"
|
||||||
|
mkdir -p "$USER_PREFIX"
|
||||||
|
npm config set prefix "$USER_PREFIX" >/dev/null
|
||||||
|
prefix_bin_on_path "$USER_PREFIX"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ------------------------------------------------------- 6. install the CLI --
|
||||||
|
npm_err="$(mktemp)"
|
||||||
|
if [ "$SERVICE_ONLY" = 1 ]; then
|
||||||
|
: # step 4 found the CLI installed
|
||||||
|
elif [ "$IN_REPO" = 1 ]; then
|
||||||
|
step "building from source"
|
||||||
|
# npm's own error is what says WHY. `--silent` plus a bare "build failed"
|
||||||
|
# left an operator with three words and nothing to act on -- a missing
|
||||||
|
# toolchain, a TypeScript error and a full disk all looked identical. The
|
||||||
|
# output is captured and the tail printed, the way the `npm install -g`
|
||||||
|
# branch below already does it.
|
||||||
|
if ! ( cd "$SCRIPT_DIR" && { [ -f package-lock.json ] && npm ci || npm install; } \
|
||||||
|
&& npm run build ) >"$npm_err" 2>&1; then
|
||||||
|
die "build failed:
|
||||||
|
$(tail -15 "$npm_err" 2>/dev/null)
|
||||||
|
Full output: $npm_err"
|
||||||
|
fi
|
||||||
|
if ! ( cd "$SCRIPT_DIR" && npm link ) >"$npm_err" 2>&1; then
|
||||||
|
die "npm link failed:
|
||||||
|
$(tail -10 "$npm_err" 2>/dev/null)
|
||||||
|
Full output: $npm_err"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
step "installing $PKG"
|
||||||
|
# The spec, in the order install.ps1 and `standup update` use (R30): a pinned
|
||||||
|
# STANDUP_VERSION; else the stable build when a `latest` exists -- the plain
|
||||||
|
# name resolves it; else `next` while every published build is a release
|
||||||
|
# candidate (no `latest` tag, so the plain name resolves nothing); else the
|
||||||
|
# plain name, which fails below with the registry's own answer. This asked
|
||||||
|
# for `next` whenever that tag existed, so a registry with 0.6.0 stable and a
|
||||||
|
# lingering rc installed the rc and said "no stable release yet".
|
||||||
|
SPEC="$PKG"
|
||||||
|
if [ -n "${STANDUP_VERSION:-}" ]; then
|
||||||
|
SPEC="$PKG@$STANDUP_VERSION"
|
||||||
|
elif LATEST_VERSION="$(npm view "$PKG" --registry "$REGISTRY" version 2>/dev/null)" \
|
||||||
|
&& [ -n "$LATEST_VERSION" ]; then
|
||||||
|
SPEC="$PKG"
|
||||||
|
elif NEXT_VERSION="$(npm view "$PKG@next" --registry "$REGISTRY" version 2>/dev/null)" \
|
||||||
|
&& [ -n "$NEXT_VERSION" ]; then
|
||||||
|
SPEC="$PKG@next"
|
||||||
|
fi
|
||||||
|
if [ "$SPEC" = "$PKG@next" ]; then say " installing $SPEC (no stable release yet)"
|
||||||
|
elif [ "$SPEC" != "$PKG" ]; then say " installing $SPEC"; fi
|
||||||
|
if ! npm install -g "$SPEC" --registry "$REGISTRY" >/dev/null 2>"$npm_err"; then
|
||||||
|
if grep -q 'E404' "$npm_err" 2>/dev/null; then
|
||||||
|
die "$PKG is not published to $REGISTRY yet (404).
|
||||||
|
Until it is, install from a checkout:
|
||||||
|
git clone ssh://git@git.rethinkstudios.io/rethink-software/agent-standups.git
|
||||||
|
cd agent-standups && sh install.sh
|
||||||
|
Or point --registry at one that has it."
|
||||||
|
fi
|
||||||
|
die "npm install -g failed:
|
||||||
|
$(tail -3 "$npm_err" 2>/dev/null)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
rm -f "$npm_err"
|
||||||
|
have standup || die "standup is not on PATH after install (check your npm global bin)"
|
||||||
|
say " standup $(standup version 2>/dev/null || echo '?') at $(command -v standup)"
|
||||||
|
|
||||||
|
# ------------------------------------------------- 7. runtime: skill + hooks --
|
||||||
|
# The runtime install ends with a synthetic Stop proving the hook is REACHABLE,
|
||||||
|
# not merely present. That needs a server answering, so start a throwaway one.
|
||||||
|
if [ "$SERVICE_ONLY" = 0 ]; then
|
||||||
|
step "installing the Claude runtime"
|
||||||
|
PROBE_DIR="$(mktemp -d)"
|
||||||
|
# `--no-git`: the probe project is a throwaway in a bare mktemp dir, and
|
||||||
|
# `create` requires a git work tree (spec 06 §3). Without this flag it exits
|
||||||
|
# non-zero on every fresh install, the whole `if` is skipped, and the
|
||||||
|
# installer reports `FAIL hook connect ECONNREFUSED` -- so its own
|
||||||
|
# "verifies the hook with a synthetic Stop" step could never pass (R06).
|
||||||
|
# Never `--yes` here: that would `git init` a repository in a temp directory,
|
||||||
|
# which is a worse answer than not having one.
|
||||||
|
# `--no-register`: 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-run (C71-49).
|
||||||
|
if ( cd "$PROBE_DIR" && standup create --no-git --no-register --name install-probe >/dev/null 2>&1 ); then
|
||||||
|
# exec: $! must be the server, not a subshell that merely spawned it, or the
|
||||||
|
# kill below misses and `standup serve` keeps holding the port.
|
||||||
|
( cd "$PROBE_DIR" && exec standup serve >/dev/null 2>&1 ) &
|
||||||
|
PROBE_PID=$!
|
||||||
|
i=0
|
||||||
|
while [ "$i" -lt 12 ]; do
|
||||||
|
standup doctor --json 2>/dev/null | grep -q '"answering on' && break
|
||||||
|
i=$((i + 1)); sleep 0.3
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
standup install --runtime claude || warn "runtime install reported a problem; see doctor below"
|
||||||
|
[ -n "$PROBE_PID" ] && { kill "$PROBE_PID" 2>/dev/null || true; wait "$PROBE_PID" 2>/dev/null || true; PROBE_PID=""; }
|
||||||
|
rm -rf "$PROBE_DIR"; PROBE_DIR=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- 8. the service ---
|
||||||
|
if [ -n "$DAEMON_NODE" ]; then
|
||||||
|
step "installing the standupd service"
|
||||||
|
# The unit resolves node absolutely from whichever node runs `service
|
||||||
|
# install`, so run it under the DAEMON node -- otherwise a Node 20 default
|
||||||
|
# would bake a Node 20 ExecStart into the unit and the daemon could not start.
|
||||||
|
#
|
||||||
|
# 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, never exported to this shell). Whether one is SET
|
||||||
|
# is asked first, because `standup code` mints one when there is none; a
|
||||||
|
# first install has none and passes nothing.
|
||||||
|
OP_CODE=""
|
||||||
|
if standup expose --status --json 2>/dev/null \
|
||||||
|
| node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{try{process.exit(JSON.parse(d).operatorCode==="set"?0:1)}catch{process.exit(1)}})'; then
|
||||||
|
OP_CODE="$(standup code --json 2>/dev/null \
|
||||||
|
| node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{try{process.stdout.write(String(JSON.parse(d).code??""))}catch{}})')" \
|
||||||
|
|| OP_CODE=""
|
||||||
|
fi
|
||||||
|
# Which `standup` this runs is resolved HERE, under the installer's own
|
||||||
|
# PATH, and not inside the subshell below: `PATH=...` on its own line is in
|
||||||
|
# effect for the next line's `command -v`, so the daemon node's bin
|
||||||
|
# directory won -- and a global install left there by an earlier version
|
||||||
|
# (nvm v22's bin held 0.6.0-rc2 while the fresh install was 0.7.0-rc1) was
|
||||||
|
# what `service install` then baked into ExecStart via `binaryPath()`. An
|
||||||
|
# upgrade brought the service up on the OLD package (audit 5 finding 2).
|
||||||
|
STANDUP_JS="$(command -v standup)"
|
||||||
|
service_as_operator() {
|
||||||
|
( if [ -n "$OP_CODE" ]; then STANDUP_OPERATOR_CODE="$OP_CODE"; export STANDUP_OPERATOR_CODE; fi
|
||||||
|
PATH="$(dirname "$DAEMON_NODE"):$PATH"
|
||||||
|
exec "$DAEMON_NODE" "$STANDUP_JS" service "$@" )
|
||||||
|
}
|
||||||
|
SVC_OUT="$(service_as_operator install 2>&1)" && SVC_OK=1 || SVC_OK=0
|
||||||
|
say " $SVC_OUT"
|
||||||
|
if [ "$SVC_OK" = 1 ]; then
|
||||||
|
service_as_operator start >/dev/null 2>&1 \
|
||||||
|
|| warn "service installed but did not start; see 'standup service status'"
|
||||||
|
elif printf '%s' "$SVC_OUT" | grep -qi 'connect to bus'; then
|
||||||
|
warn "no systemd --user bus here, so the unit was written but not enabled.
|
||||||
|
If this is WSL2: add [boot] systemd=true to /etc/wsl.conf, then 'wsl --shutdown'.
|
||||||
|
Until then run 'standup daemon' yourself; single mode needs nothing."
|
||||||
|
else
|
||||||
|
warn "service install failed; single mode still works. See 'standup service status'."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- 9. hand over --
|
||||||
|
step "verifying"
|
||||||
|
standup doctor || warn "some checks need attention (above)"
|
||||||
|
|
||||||
|
if [ -z "$PROBLEMS" ]; then step "done"; else
|
||||||
|
step "done, with things to know"
|
||||||
|
printf '%s' "$PROBLEMS"
|
||||||
|
say ""
|
||||||
|
fi
|
||||||
|
PORT="${STANDUP_PORT:-7335}"
|
||||||
|
say " dashboard : http://127.0.0.1:$PORT"
|
||||||
|
say ""
|
||||||
|
standup code 2>/dev/null || say " (run 'standup code' for the operator code)"
|
||||||
|
say ""
|
||||||
|
say "Next: cd <your project> && standup create"
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Copy the installers from the source repo into this one.
|
||||||
|
#
|
||||||
|
# agent-standups is canonical: install.sh and install.ps1 are tracked there,
|
||||||
|
# the probe harness reads them from the repo root (run-all.sh passes
|
||||||
|
# $REPO/install.sh to the R06 probes), and P8-24 copies them. This repo is the
|
||||||
|
# public face, so the files are copied rather than symlinked -- Gitea's raw/
|
||||||
|
# endpoint serves a symlink's link text, which would pipe a path into the
|
||||||
|
# user's shell instead of a script.
|
||||||
|
#
|
||||||
|
# Run this before committing here, whenever the installers change.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SRC="${1:-../agent-standups}"
|
||||||
|
[ -f "$SRC/install.sh" ] || { echo "error: no install.sh under $SRC" >&2; exit 1; }
|
||||||
|
|
||||||
|
changed=0
|
||||||
|
for f in install.sh install.ps1; do
|
||||||
|
if ! cmp -s "$SRC/$f" "$f" 2>/dev/null; then
|
||||||
|
cp "$SRC/$f" "$f"
|
||||||
|
echo " updated $f"
|
||||||
|
changed=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
chmod +x install.sh
|
||||||
|
|
||||||
|
[ "$changed" = 1 ] || echo " already in sync with $SRC"
|
||||||
Reference in new issue
Block a user