Files
agent-standups-install/install.sh
T
dsql 93473dd789 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>
2026-10-15 11:28:57 -04:00

492 lines
22 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.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"