Skip to content

feat(install.sh, commands): cross-platform installer + !agent alias#12

Merged
bglusman merged 2 commits intomainfrom
feat/install-cross-platform-acpx-alias
Apr 23, 2026
Merged

feat(install.sh, commands): cross-platform installer + !agent alias#12
bglusman merged 2 commits intomainfrom
feat/install-cross-platform-acpx-alias

Conversation

@bglusman
Copy link
Copy Markdown
Owner

Summary

Two related changes grouped for ease of review:

install.sh — now works on Linux too

Was Mac-only (hardcoded LaunchAgents path, brew install everywhere, no sudo/root path). Now:

macOS Linux (root) Linux (non-root)
BIN_DIR ~/.local/bin /usr/local/bin ~/.local/bin
Service manager launchd systemd (system) systemd --user
Units in ~/Library/LaunchAgents/ /etc/systemd/system/ ~/.config/systemd/user/
Logs ~/Library/Logs/ /var/log/zeroclawed/ ~/.local/state/zeroclawed/logs/
Package mgr brew → npm fallback npm → apt fallback same

Plus: handles "Text file busy" on Linux running-binary replacement via install -m 755, auto-populates XDG_RUNTIME_DIR for sudo/su contexts where pam_systemd hasn't run, builds zeroclawed with --features channel-matrix by default (was silently off in deployed binaries), and installs acpx when claude/opencode agents are enabled (fixes "Failed to spawn acpx" on fresh installs — this was biting actively).

commands.rs — !agent alias for !switch

Parallel to the existing !commands!help alias. Types more naturally after !agents lists 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:

  • macOS (Darwin, user): moved acpx out of PATH, reinstall script recreated it, launchd agents (clashd + security-proxy) loaded, services healthy on :9001/:8888.
  • Linux (root, .210 Debian 12): built clashd + security-proxy + zeroclawed (w/ channel-matrix) from scratch, installed to /usr/local/bin, systemd units at /etc/systemd/system, systemctl enable --now worked, health checks passed. Surfaced and fixed the running-binary-replace issue during this run.
  • Linux (non-root, claude user on .210): --configure-only invocation 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.
  • Rebuilt Mac bot with !agent alias; !help output shows !switch, !agent <agent> as expected.

Related

  • feat: gitleaks secret-leak guardrails #11 (feat/secret-guardrails) — opened separately, independent, can merge in either order.
  • Follow-up to the openclaw-native "(no response)" behavior and the onecli/fnox spike — both intentionally out of scope here.

🤖 Generated with Claude Code

## 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>
Copilot AI review requested due to automatic review settings April 22, 2026 03:57
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.sh to support macOS (launchd) and Linux (systemd system/user), with new tool-install helpers and Linux-safe binary replacement.
  • Build zeroclawed with --features channel-matrix by default, and install acpx when relevant agents are enabled.
  • Accept !agent as 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.

Comment thread scripts/install.sh Outdated
Comment on lines +289 to +291

[Install]
WantedBy=default.target
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread scripts/install.sh
Comment on lines +348 to +350
[Install]
WantedBy=default.target
EOF
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +197 to 199
// !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.
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread crates/zeroclawed/src/commands.rs Outdated

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();
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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();

Copilot uses AI. Check for mistakes.
Comment thread scripts/install.sh Outdated
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)')"
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
ok "$bin $(${bin} --version 2>/dev/null | head -1 || echo '(installed)')"
ok "$bin $("$bin" --version 2>/dev/null | head -1 || echo '(installed)')"

Copilot uses AI. Check for mistakes.
Comment thread scripts/install.sh
Comment on lines +172 to +176
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 $?
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread scripts/install.sh
Comment on lines +172 to +180
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
;;
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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.
@bglusman bglusman marked this pull request as ready for review April 23, 2026 21:55
Copilot AI review requested due to automatic review settings April 23, 2026 21:55
@bglusman bglusman merged commit ba5de14 into main Apr 23, 2026
17 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread scripts/install.sh
Comment on lines +567 to +568
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"
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
" !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)",
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
" !switch, !agent <agent> [session] — switch active agent (requires auth)",
" !switch <agent> [session] (alias: !agent) — switch active agent (requires auth)",

Copilot uses AI. Check for mistakes.
bglusman added a commit that referenced this pull request Apr 25, 2026
)

* 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>
@bglusman bglusman deleted the feat/install-cross-platform-acpx-alias branch May 1, 2026 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants