feat(install.sh, commands): cross-platform installer + !agent alias#12
feat(install.sh, commands): cross-platform installer + !agent alias#12
Conversation
## install.sh - Platform detection: Darwin / Linux (root vs non-root) determines BIN_DIR, service manager (launchd vs systemd), log paths. - Builds zeroclawed with --features channel-matrix so Matrix channel works in deployed binaries (was silently off before). - Replaces `ensure_brew opencode` etc. with `ensure_tool` that falls back to npm (opencode-ai) when brew is absent. - `zeroclaw` CLI install path gated to Darwin; Linux emits a warning with the source-build hint (no Linux package available). - Installs `acpx` via npm when claude or opencode agents are enabled (fixes "Failed to spawn acpx: No such file or directory" on fresh installs of ACPX-kind agents). - Overwrites running Linux binaries safely via `install -m 755` (falls back to rm+cp if coreutils `install` not present). Fixes "Text file busy" when `/usr/local/bin/zeroclawed` is in use. - Auto-sets XDG_RUNTIME_DIR / DBUS_SESSION_BUS_ADDRESS when running under `su`/`sudo` without a PAM session so `systemctl --user` works in CI, scripted installs, and claude-user tests. - brew-specific `brew services start zeroclaw` gated on Darwin. ## commands.rs - Adds `!agent` as an alias for `!switch`: reads naturally after `!agents` lists available agents. Parallel to the existing `!commands` ↔ `!help` alias pattern. - Updates help text and usage-error message to show both forms. ## Tested - Mac: bash scripts/install.sh --yes --agents claude re-installed acpx after removal, all launchd agents loaded, services healthy. - .210 Linux root: same invocation built from scratch, installed to /usr/local/bin, set up systemd services at /etc/systemd/system, fetched acpx, updated Claude Code hooks. - .210 claude user (non-root): --configure-only run exercised ~/.config/systemd/user, XDG_RUNTIME_DIR auto-population, user bus connection. Services created + enabled (port conflict with root services expected, not an install.sh bug). - cargo check -p zeroclawed --features channel-matrix: clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds Linux support to the project’s unified installer and introduces a !agent alias for the existing !switch command in zeroclawed, making agent switching more discoverable after !agents.
Changes:
- Update
scripts/install.shto support macOS (launchd) and Linux (systemd system/user), with new tool-install helpers and Linux-safe binary replacement. - Build
zeroclawedwith--features channel-matrixby default, and installacpxwhen relevant agents are enabled. - Accept
!agentas an alias for!switch, updating help/usage text accordingly.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
scripts/install.sh |
Adds cross-platform install + service wiring (launchd on macOS, systemd on Linux), plus new tool-install logic and ACPX handling. |
crates/zeroclawed/src/commands.rs |
Adds !agent alias for !switch and updates user-facing help/usage strings. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| [Install] | ||
| WantedBy=default.target |
There was a problem hiding this comment.
The generated systemd unit uses WantedBy=default.target. That’s appropriate for systemctl --user, but for system-wide installs (IS_ROOT=true / systemctl) the conventional target is multi-user.target (and your own systemd_unit() helper later in this script uses multi-user.target). Consider switching WantedBy based on root vs user installs to avoid surprising enable behavior on system units.
| [Install] | ||
| WantedBy=default.target | ||
| EOF |
There was a problem hiding this comment.
Same as above: WantedBy=default.target is fine for user units but is nonstandard for system units. Consider multi-user.target when running as root/system service to match typical systemd behavior and the script’s own systemd_unit() generator.
| // !agent is an alias: reads as "pick an agent" since !agents lists them. | ||
| "!switch" | "!agent" => None, | ||
| // !default needs auth — switches back to the configured default agent. |
There was a problem hiding this comment.
New !agent alias behavior isn’t covered by the existing command-handler tests in this file. Adding a couple assertions (e.g. handle("!agent foo") returns None pre-auth, and is_switch_command("!agent foo") is true) would prevent regressions when command parsing is refactored.
|
|
||
| if args.is_empty() { | ||
| return "Usage: !switch <agent> [session]\n\nUse !agents to see available agents.\nUse !sessions <agent> to list available sessions for acpx agents.".to_string(); | ||
| return "Usage: !switch <agent> [session] (alias: !agent)\n\nUse !agents to see available agents.\nUse !sessions <agent> to list available sessions for acpx agents.".to_string(); |
There was a problem hiding this comment.
The updated usage string has a double space before (alias: !agent) ("[session] (alias...") which looks unintentional. Consider tightening the spacing to keep help/usage output consistent.
| return "Usage: !switch <agent> [session] (alias: !agent)\n\nUse !agents to see available agents.\nUse !sessions <agent> to list available sessions for acpx agents.".to_string(); | |
| return "Usage: !switch <agent> [session] (alias: !agent)\n\nUse !agents to see available agents.\nUse !sessions <agent> to list available sessions for acpx agents.".to_string(); |
| local brew_pkg="${2:-$1}" | ||
| local npm_pkg="${3:-$1}" | ||
| if command -v "$bin" &>/dev/null; then | ||
| ok "$bin $(${bin} --version 2>/dev/null | head -1 || echo '(installed)')" |
There was a problem hiding this comment.
In ensure_tool, the success-path version logging uses command substitution with ${bin} (e.g. $(${bin} --version ...)). This relies on word-splitting and makes it harder to safely handle unusual binary names; prefer invoking "$bin" directly (e.g. $("$bin" --version ...)) like ensure_npm does.
| ok "$bin $(${bin} --version 2>/dev/null | head -1 || echo '(installed)')" | |
| ok "$bin $("$bin" --version 2>/dev/null | head -1 || echo '(installed)')" |
| Linux) | ||
| # Try npm first (most universal); fall through to apt if available. | ||
| if command -v npm &>/dev/null || [[ "$YES" == true ]]; then | ||
| ensure_npm "$npm_pkg" "$bin" | ||
| return $? |
There was a problem hiding this comment.
Linux branch of ensure_tool will call ensure_npm when --yes is set even if npm is not installed. In that case ensure_npm will hard-fail (due to require_npm) with a macOS-specific hint ("brew install node"). Consider gating on command -v npm, or adding a Linux-specific Node/npm install path (apt/dnf) so --yes behaves as expected on fresh Linux hosts.
| Linux) | ||
| # Try npm first (most universal); fall through to apt if available. | ||
| if command -v npm &>/dev/null || [[ "$YES" == true ]]; then | ||
| ensure_npm "$npm_pkg" "$bin" | ||
| return $? | ||
| fi | ||
| warn "$bin not found and no package manager available — install manually" | ||
| return 1 | ||
| ;; |
There was a problem hiding this comment.
ensure_tool comments (and the PR description) mention an apt/dnf fallback on Linux, but the current Linux branch only warns and returns 1 when npm is unavailable. Either implement the advertised distro package-manager fallback here, or adjust the comments/README so the installer behavior matches expectations.
install.sh: - Systemd WantedBy is now driven by IS_ROOT: multi-user.target for system units (root install), default.target for user units (systemctl --user). Both inline unit generators and the systemd_unit() helper now use the same shared variable. - ensure_tool: quote the binary path when invoking --version so unusual names / paths are handled safely. - Linux --yes path actually installs node+npm via apt-get or dnf now, rather than hard-failing ensure_npm with a macOS-only brew hint. commands.rs: - Added explicit tests for the !agent alias: handle() returns None pre-auth (same as !switch), is_switch_command recognizes !agent. - Fixed double-space in !switch usage string.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| printf '[Unit]\nDescription=ZeroClawed %s\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=%s/%s\n%sRestart=always\nRestartSec=5\n\n[Install]\nWantedBy=%s\n' \ | ||
| "$bin" "$install_dir" "$bin" "$(printf '%b' "$env_lines")" "$WANTED_BY_TARGET" |
There was a problem hiding this comment.
In multi-node deployment, systemd_unit() uses WantedBy=${WANTED_BY_TARGET}, but WANTED_BY_TARGET is derived from the local EUID. The remote Linux deployment path always writes to /etc/systemd/system and runs systemctl (system instance), so it should always use multi-user.target (or determine the wanted-by target per remote install mode) rather than inheriting the local value; otherwise running the installer as non-root locally can generate units that don’t enable correctly on the remote host.
| " !metrics — messages routed, average latency", | ||
| " !ping — connectivity check (replies: pong)", | ||
| " !switch <agent> [session] — switch active agent (requires auth)", | ||
| " !switch, !agent <agent> [session] — switch active agent (requires auth)", |
There was a problem hiding this comment.
The help line !switch, !agent <agent> [session] reads like !switch takes no args and only !agent takes <agent>. Consider aligning this with the usage text (e.g., !switch <agent> [session] (alias: !agent)) to avoid ambiguity in the command syntax presented to users.
| " !switch, !agent <agent> [session] — switch active agent (requires auth)", | |
| " !switch <agent> [session] (alias: !agent) — switch active agent (requires auth)", |
) * feat(install.sh, commands): cross-platform installer + !agent alias ## install.sh - Platform detection: Darwin / Linux (root vs non-root) determines BIN_DIR, service manager (launchd vs systemd), log paths. - Builds zeroclawed with --features channel-matrix so Matrix channel works in deployed binaries (was silently off before). - Replaces `ensure_brew opencode` etc. with `ensure_tool` that falls back to npm (opencode-ai) when brew is absent. - `zeroclaw` CLI install path gated to Darwin; Linux emits a warning with the source-build hint (no Linux package available). - Installs `acpx` via npm when claude or opencode agents are enabled (fixes "Failed to spawn acpx: No such file or directory" on fresh installs of ACPX-kind agents). - Overwrites running Linux binaries safely via `install -m 755` (falls back to rm+cp if coreutils `install` not present). Fixes "Text file busy" when `/usr/local/bin/zeroclawed` is in use. - Auto-sets XDG_RUNTIME_DIR / DBUS_SESSION_BUS_ADDRESS when running under `su`/`sudo` without a PAM session so `systemctl --user` works in CI, scripted installs, and claude-user tests. - brew-specific `brew services start zeroclaw` gated on Darwin. ## commands.rs - Adds `!agent` as an alias for `!switch`: reads naturally after `!agents` lists available agents. Parallel to the existing `!commands` ↔ `!help` alias pattern. - Updates help text and usage-error message to show both forms. ## Tested - Mac: bash scripts/install.sh --yes --agents claude re-installed acpx after removal, all launchd agents loaded, services healthy. - .210 Linux root: same invocation built from scratch, installed to /usr/local/bin, set up systemd services at /etc/systemd/system, fetched acpx, updated Claude Code hooks. - .210 claude user (non-root): --configure-only run exercised ~/.config/systemd/user, XDG_RUNTIME_DIR auto-population, user bus connection. Services created + enabled (port conflict with root services expected, not an install.sh bug). - cargo check -p zeroclawed --features channel-matrix: clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(install.sh,commands): address Copilot review polish on #12 install.sh: - Systemd WantedBy is now driven by IS_ROOT: multi-user.target for system units (root install), default.target for user units (systemctl --user). Both inline unit generators and the systemd_unit() helper now use the same shared variable. - ensure_tool: quote the binary path when invoking --version so unusual names / paths are handled safely. - Linux --yes path actually installs node+npm via apt-get or dnf now, rather than hard-failing ensure_npm with a macOS-only brew hint. commands.rs: - Added explicit tests for the !agent alias: handle() returns None pre-auth (same as !switch), is_switch_command recognizes !agent. - Fixed double-space in !switch usage string. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Two related changes grouped for ease of review:
install.sh — now works on Linux too
Was Mac-only (hardcoded LaunchAgents path,
brew installeverywhere, no sudo/root path). Now:BIN_DIR~/.local/bin/usr/local/bin~/.local/bin~/Library/LaunchAgents//etc/systemd/system/~/.config/systemd/user/~/Library/Logs//var/log/zeroclawed/~/.local/state/zeroclawed/logs/Plus: handles "Text file busy" on Linux running-binary replacement via
install -m 755, auto-populatesXDG_RUNTIME_DIRfor sudo/su contexts where pam_systemd hasn't run, builds zeroclawed with--features channel-matrixby default (was silently off in deployed binaries), and installsacpxwhen claude/opencode agents are enabled (fixes "Failed to spawn acpx" on fresh installs — this was biting actively).commands.rs —
!agentalias for!switchParallel to the existing
!commands↔!helpalias. Types more naturally after!agentslists options. Help text and usage error updated to show both forms. No behavior change beyond the second accepted command name.Test plan
End-to-end verified on three platforms during development:
acpxout of PATH, reinstall script recreated it, launchd agents (clashd + security-proxy) loaded, services healthy on :9001/:8888..210Debian 12): built clashd + security-proxy + zeroclawed (w/ channel-matrix) from scratch, installed to/usr/local/bin, systemd units at/etc/systemd/system,systemctl enable --nowworked, health checks passed. Surfaced and fixed the running-binary-replace issue during this run.claudeuser on.210):--configure-onlyinvocation exercised~/.config/systemd/user, the XDG_RUNTIME_DIR auto-population kicked in correctly, services registered (port conflict with root services expected and ignored by fail2ban's ignoreip — not a script bug).cargo check -p zeroclawed --features channel-matrix: clean.!agentalias;!helpoutput shows!switch, !agent <agent>as expected.Related
feat/secret-guardrails) — opened separately, independent, can merge in either order.🤖 Generated with Claude Code