Skip to content
246 changes: 234 additions & 12 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -254,13 +254,17 @@ Usage: $0 [options]
Options:
--prebuilt Download and install a pre-built binary (default when asked)
--source Build from source (skips the pre-built prompt)
--minimal Build kernel only — source only (config + providers + memory, ~6.6MB)
--preset NAME Named feature preset: 'minimal' (kernel only, ~6.6MB) or
'full' (default features). Source builds only.
--minimal Alias for --preset minimal
--features X,Y Select specific features — source only (comma-separated)
--with-gateway Force the gateway feature on (overrides preset/feature default)
--without-gateway Force the gateway feature off (overrides preset/feature default)
--list-features Print all available features and exit
--prefix PATH Install everything under PATH (default: \$HOME)
Sets CARGO_HOME, RUSTUP_HOME, source checkout, config
--dry-run Show what would happen without building or installing
--skip-onboard Skip the setup wizard after install
--skip-onboard Skip the post-install onboarding prompt
--uninstall Remove ZeroClaw binary and optionally config/data
-h, --help Show this help
-V, --version Show version from Cargo.toml
Expand Down Expand Up @@ -304,7 +308,7 @@ do_uninstall() {
if [ -d "$config_dir" ]; then
if [ -t 0 ]; then
printf " Remove config and data (%s)? [y/N] " "$config_dir"
read confirm
read -r confirm
case "$confirm" in
[Yy]*) rm -rf "$config_dir"; info "Removed $config_dir" ;;
*) info "Config preserved at $config_dir" ;;
Expand All @@ -330,6 +334,118 @@ do_uninstall() {
exit 0
}

# ── Onboarding-needed status check ───────────────────────────────
#
# Detect whether the operator already has a completed onboarding so the
# 3-way "how would you like to onboard?" prompt can skip silently on a
# re-install. We treat onboarding as complete when a config file exists at
# the expected path AND it contains at least one `[providers.models.*]` or
# `[providers.fallback]` line — i.e. some provider is configured. Empty or
# default config files still trigger the prompt.
onboarding_needed() {
cfg="$PREFIX/.zeroclaw/config.toml"
[ -f "$cfg" ] || return 0 # no config → onboard
# Already-configured signal: any of these patterns means a provider was set.
if grep -qE '^\[providers\.models\.|^fallback *=|^default_provider *=' "$cfg" 2>/dev/null; then
return 1 # configured → skip
fi
return 0 # config exists but empty → onboard
}

# ── Interactive feature picker ───────────────────────────────────
#
# POSIX-sh number-toggle picker over the OPTIONAL feature set (channel-*,
# observability-*, hardware/peripheral/sandbox/browser flavours). Default
# features are always on; this only surfaces the opt-in extras. The output
# is a comma-separated list of selected features written to stdout.
#
# Invoked from the interactive flow when the operator runs install.sh in a
# TTY without `--minimal`, `--preset`, or `--features`. Skipped in
# non-interactive runs (curl | bash) and in CI.
interactive_feature_picker() {
toml="$1"
parse_cargo_toml "$toml"

picker_features=""
for feat in $ALL_FEATURES; do
case "$feat" in
default|ci-all|fantoccini|landlock|metrics) continue ;;
channel-*|observability-*|hardware|peripheral-*|sandbox-*|browser-*|probe|rag-pdf|webauthn)
picker_features="${picker_features:+$picker_features }$feat" ;;
esac
done

selected=""
echo
printf " %s\n" "$(bold "Optional features (off by default):")"
printf " %s\n" "Type the numbers to toggle, blank line to confirm."
printf " %s\n" "Default features (agent runtime, gateway, …) are always on."
echo

while :; do
i=1
for feat in $picker_features; do
mark=" "
case " $selected " in *" $feat "*) mark="✓" ;; esac
printf " [%2d] %s %s\n" "$i" "$mark" "$feat"
i=$((i + 1))
done
echo
printf " toggle (e.g. \"1 3 5\"), %s confirm: " "$(bold "Enter to")"
read -r choices
[ -z "$choices" ] && break
for n in $choices; do
case "$n" in
''|*[!0-9]*) continue ;;
esac
idx=1
for feat in $picker_features; do
if [ "$idx" -eq "$n" ]; then
case " $selected " in
*" $feat "*) selected=$(printf '%s' "$selected" | tr ' ' '\n' | grep -vx "$feat" | paste -sd' ' -) ;;
*) selected="${selected:+$selected }$feat" ;;
esac
break
fi
idx=$((idx + 1))
done
done
done

printf '%s' "$selected" | tr ' ' ','
}

# ── Web dashboard build for source installs ──────────────────────
#
# When a source build includes the `gateway` feature, the dashboard
# (`web/dist`) needs to be built so the gateway can serve it. If Node.js
# is on PATH we run `npm install && npm run build` in `web/`. Without
# Node.js we warn — the gateway still starts but the dashboard route
# returns 404 until `web/dist` is populated.
build_web_dashboard() {
src_dir="$1"
if [ ! -d "$src_dir/web" ]; then
warn "Source has no web/ directory; skipping dashboard build."
return 0
fi
if [ -f "$src_dir/web/dist/index.html" ]; then
info "Web dashboard already built at $src_dir/web/dist"
return 0
fi
if ! command -v npm >/dev/null 2>&1; then
warn "npm not found — skipping dashboard build. The gateway will run"
warn " in API-only mode until you build the dashboard:"
warn " cd $src_dir/web && npm install && npm run build"
return 0
fi
info "Building web dashboard (npm install + npm run build)..."
(cd "$src_dir/web" && npm install --silent && npm run build --silent) || {
warn "Dashboard build failed — gateway will run in API-only mode."
return 0
}
info "Web dashboard built at $src_dir/web/dist"
}

# ── Parse arguments ───────────────────────────────────────────────

MINIMAL=false
Expand All @@ -340,6 +456,8 @@ UNINSTALL=false
DRY_RUN=false
PREFIX="$HOME"
INSTALL_MODE="" # ""=ask, "prebuilt"=force prebuilt, "source"=force source
PRESET="" # ""=unset, "minimal"=alias for --minimal, "full"=default-features
WITH_GATEWAY="" # ""=unset (preset/feature default applies), "true"/"false"=explicit toggle

# Support legacy env var
if [ -n "${ZEROCLAW_CARGO_FEATURES:-}" ]; then
Expand All @@ -349,11 +467,23 @@ fi
while [ $# -gt 0 ]; do
case "$1" in
--minimal) MINIMAL=true ;;
--preset)
if [ $# -lt 2 ]; then
die "Missing value for --preset. Expected: --preset minimal|full"
fi
shift
case "$1" in
minimal) PRESET="minimal"; MINIMAL=true ;;
full) PRESET="full" ;;
*) die "Unknown preset '$1'. Expected: minimal or full" ;;
esac ;;
--features)
if [ $# -lt 2 ]; then
die "Missing value for --features. Expected: --features X,Y"
fi
shift; USER_FEATURES="${USER_FEATURES:+$USER_FEATURES,}$1" ;;
--with-gateway) WITH_GATEWAY="true" ;;
--without-gateway) WITH_GATEWAY="false" ;;
--list-features) LIST_FEATURES=true ;;
--prefix)
if [ $# -lt 2 ]; then
Expand Down Expand Up @@ -405,8 +535,11 @@ fi

# ── Decide: pre-built or source ───────────────────────────────────

# --minimal or --features imply source
if [ "$MINIMAL" = true ] || [ -n "$USER_FEATURES" ]; then
# --minimal, --features, --without-gateway, or --preset full imply source.
# Prebuilt binaries always ship with default features, so any flag that
# changes the feature set must force a source build.
if [ "$MINIMAL" = true ] || [ -n "$USER_FEATURES" ] \
|| [ "$WITH_GATEWAY" = "false" ] || [ "$PRESET" = "full" ]; then
INSTALL_MODE="source"
fi

Expand All @@ -419,7 +552,7 @@ if [ "$INSTALL_MODE" = "" ]; then
printf " [P] Pre-built binary — fast, no Rust required %s\n" "$(bold "(default)")"
printf " [s] Build from source — custom features, latest code\n"
printf "\n Choice [P/s]: "
read install_choice
read -r install_choice
case "$install_choice" in
[Ss]*) INSTALL_MODE="source" ;;
*) INSTALL_MODE="prebuilt" ;;
Expand Down Expand Up @@ -537,13 +670,51 @@ See all available features:
esac

# ── Build feature flags ──────────────────────────────────────────
#
# Cargo cannot remove individual entries from `default`, so toggling
# `gateway` off requires `--no-default-features` plus an explicit list
# of the rest. Derive that list from $DEFAULT_FEATURES (parsed from
# Cargo.toml above) so it stays in sync automatically.

CARGO_FLAGS=""

if [ "$MINIMAL" = true ]; then
CARGO_FLAGS="--no-default-features"
fi

# `--without-gateway` overrides the default-features set: switch to
# --no-default-features and re-add everything in `default` except gateway.
if [ "$WITH_GATEWAY" = "false" ] && [ "$MINIMAL" != true ]; then
CARGO_FLAGS="--no-default-features"
defaults_no_gateway=$(printf '%s' "$DEFAULT_FEATURES" | tr ',' '\n' | grep -vx gateway | paste -sd, -)
USER_FEATURES="${USER_FEATURES:+$USER_FEATURES,}$defaults_no_gateway"
fi

# `--with-gateway` is a no-op when default features are on (gateway is
# already there), and additive when --no-default-features is in play.
if [ "$WITH_GATEWAY" = "true" ]; then
case "$CARGO_FLAGS" in
*--no-default-features*) USER_FEATURES="${USER_FEATURES:+$USER_FEATURES,}gateway" ;;
esac
fi

# Interactive feature picker — only when the operator did not pin
# features via the CLI and is running under a TTY. Skipped on
# `--minimal`, `--preset`, `--features`, `--with-gateway` /
# `--without-gateway`, and any non-interactive run (curl | bash).
if [ -t 0 ] \
&& [ "$MINIMAL" != true ] \
&& [ -z "$USER_FEATURES" ] \
&& [ -z "$PRESET" ] \
&& [ -z "$WITH_GATEWAY" ] \
&& [ "$DRY_RUN" != true ]; then
PICKED=$(interactive_feature_picker "Cargo.toml")
if [ -n "$PICKED" ]; then
USER_FEATURES="$PICKED"
info "Picked features: $USER_FEATURES"
fi
fi

if [ -n "$USER_FEATURES" ]; then
# Normalize: treat commas, spaces, tabs as delimiters; deduplicate; trim empty
USER_FEATURES=$(printf '%s' "$USER_FEATURES" | tr ',[:space:]' '\n' | grep -v '^$' | sort -u | paste -sd, - || true)
Expand Down Expand Up @@ -576,12 +747,15 @@ if [ -n "$PATH_BIN" ]; then
if [ "$MINIMAL" = true ] && [ "$DRY_RUN" != true ]; then
if [ -t 0 ]; then
printf " --minimal will produce a reduced binary (no agent runtime by default). Continue? [Y/n] "
read confirm
read -r confirm
case "$confirm" in
[Nn]*) echo "Aborted."; exit 0 ;;
esac
fi
fi
if [ "$PRESET" = "full" ] && [ "$DRY_RUN" != true ] && [ -t 1 ]; then
info "--preset full: building from source with the full default feature set."
fi
fi

# ── Dry run ───────────────────────────────────────────────────────
Expand Down Expand Up @@ -624,6 +798,23 @@ echo
# shellcheck disable=SC2086
cargo install --path . --locked --force $CARGO_FLAGS

# ── Web dashboard (gateway feature only) ──────────────────────────
# When the install includes the `gateway` feature, build `web/dist` so
# the dashboard route serves something. Skips silently when the build
# excluded gateway (`--without-gateway`, `--minimal` without explicit
# gateway in --features, etc).
WANT_GATEWAY=true
case "$CARGO_FLAGS" in
*--no-default-features*)
case ",$USER_FEATURES," in
*,gateway,*) ;;
*) WANT_GATEWAY=false ;;
esac ;;
esac
if [ "$WANT_GATEWAY" = true ]; then
build_web_dashboard "$INSTALL_DIR"
fi

# ── Summary ───────────────────────────────────────────────────────

BIN="$CARGO_HOME/bin/zeroclaw"
Expand Down Expand Up @@ -678,13 +869,44 @@ fi
# ── Onboard ───────────────────────────────────────────────────────

if [ "$SKIP_ONBOARD" = false ] && [ "$DRY_RUN" != true ] && [ -f "$BIN" ]; then
if [ -t 0 ]; then
echo
printf "%s\n" "$(bold "Running setup wizard...")"
# Skip the prompt entirely when the operator already has a configured
# ZeroClaw — re-installs should not re-prompt.
if ! onboarding_needed; then
info "Existing ZeroClaw config detected at $PREFIX/.zeroclaw/config.toml — skipping onboard prompt."
info "Run 'zeroclaw onboard' to reconfigure."
elif [ -t 0 ]; then
# 3-way onboarding choice. Bare Enter accepts the [1] CLI default;
# option [2] foregrounds the daemon so the operator can finish in the
# browser and Ctrl+C to return; [3] skips and prints a follow-up hint.
# Non-TTY runs fall through to the silent skip in the else branch.
echo
"$BIN" onboard || warn "Onboard wizard exited with an error — run 'zeroclaw onboard' manually"
printf "%s\n" "$(bold "ZeroClaw installed. How would you like to complete onboarding?")"
printf " [1] CLI/TUI (zeroclaw onboard)\n"
printf " [2] Open gateway in browser (zeroclaw daemon + dashboard)\n"
printf " [3] Skip for now\n"
printf " Choice [1-3, default 1]: "
read -r onboard_choice
case "${onboard_choice:-1}" in
1|"")
echo
"$BIN" onboard || warn "Onboard wizard exited with an error — run 'zeroclaw onboard' manually"
;;
2)
echo
info "Starting gateway daemon for browser-based onboarding..."
info "Open the dashboard in your browser; pair with the code shown in logs."
info "Stop the daemon with Ctrl+C when done; then run 'zeroclaw service install' for always-on."
"$BIN" daemon || warn "Daemon exited with an error — run 'zeroclaw daemon' manually"
;;
3)
info "Skipped onboarding. Run 'zeroclaw onboard' (CLI) or 'zeroclaw daemon' (browser) when ready."
;;
*)
warn "Unknown choice '$onboard_choice' — skipping. Run 'zeroclaw onboard' to configure."
;;
esac
else
info "Non-interactive — skipping onboard wizard. Run 'zeroclaw onboard' to configure."
info "Non-interactive — skipping onboard prompt. Run 'zeroclaw onboard' to configure."
fi
fi

Expand Down
Loading