The release rewrote the pinned tag in both files. Their header comments now name the private source repo at v0.1.1, which users cannot fetch; the README's own install commands point at this repo's raw/branch/main and are the ones people actually run. Filed as a defect rather than patched here: the pin rewriter and C24 both require a tag URL in these two files, so changing them to point at this repo is a design decision, not a sync fix. Signed-off-by: disqualifier <dev@disqualifier.me>
514 lines
23 KiB
Bash
Executable File
514 lines
23 KiB
Bash
Executable File
#!/bin/sh
|
|
# agent-standups installer: detect, provision Node, install, start.
|
|
#
|
|
# curl -fsSL https://git.rethinkstudios.io/rethink-software/agent-standups/raw/tag/v0.1.1/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
|
|
NO_CODEX=0
|
|
ASSUME_YES=0
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--skill-only) SKILL_ONLY=1 ;;
|
|
--no-node) NO_NODE=1 ;;
|
|
--no-codex) NO_CODEX=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
|
|
--no-codex never wire the Codex runtime, even where codex is installed
|
|
--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.1/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)"
|
|
CODEX_BIN="$(command -v codex 2>/dev/null || true)"
|
|
say " standup : ${STANDUP_BIN:-not installed}"
|
|
say " codex : ${CODEX_BIN:-not installed}$([ -n "$CODEX_BIN" ] && [ "$NO_CODEX" = 1 ] && echo ' (--no-codex)')"
|
|
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 agent runtimes"
|
|
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"
|
|
# 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; --no-codex opts out where it is.
|
|
if [ "$NO_CODEX" = 0 ] && [ -n "$CODEX_BIN" ]; then
|
|
standup install --runtime codex || warn "codex runtime install reported a problem; see doctor below"
|
|
fi
|
|
[ -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
|
|
# By hand on the unit's home, `standup daemon` is operator-only once a
|
|
# code exists (P8-06), so the hint carries the flag (R213 C1).
|
|
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 --code <operator code>' yourself ('standup code'
|
|
prints it); 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"
|
|
if [ "$NO_CODEX" = 0 ] && [ -n "$CODEX_BIN" ]; then
|
|
say ""
|
|
say "Codex: the skill and hooks are wired machine-wide. Codex must TRUST them:"
|
|
say " run /hooks inside Codex and approve."
|
|
say "Per project, for the room rules in AGENTS.md:"
|
|
say " standup install --runtime codex"
|
|
fi
|