port the agent CLIs, using Anthropic's installer rather than npm

Claude Code goes in through https://claude.ai/install.sh, matching what the
platform already does for members in os-user-claude.ts and chosen there for the
auto-update npm does not give. The comment at os-user-claude.ts:28 claiming
setup.sh already did this was simply wrong — setup-ubuntu.sh used
`npm install -g @anthropic-ai/claude-code`.

Two things that installer insists on, both of which a naive port gets wrong and
both of which os-user-claude.ts had already found:

  It REFUSES to run under sudo from a regular user's shell — it checks for uid 0
  with SUDO_USER set, because everything it writes goes under $HOME and under
  sudo that is root's. This script runs as root, so the install has to be done AS
  the account.

  It declares #!/bin/bash and uses [[ … =~ … ]], so it must be piped to bash. On
  Ubuntu /bin/sh is dash and `| sh` fails.

Two bugs found by running it rather than reading it:

  opencode does not install to ~/.local/bin. It goes to ~/.opencode/bin, which is
  what sidecar/opencode/index.ts:22 hardcodes. The first version looked in the
  wrong place, reported a working install as missing, and installed it again —
  the run said "did not complete" while the installer had plainly succeeded.

  claude on this machine came from npm, so `command -v claude` found it and the
  section would have left a copy that never updates. It now detects an npm
  install by resolving the binary into node_modules, says so, and offers to
  reinstall through the official installer — naming the npm copy and how to
  remove it rather than deleting something it did not put there.

~/.local/bin and ~/.opencode/bin are both added to the account's PATH. The
sidecars do not need it — they check the exact paths — but a user who cannot run
`claude` in their own terminal reasonably concludes it was never installed.

PI stays optional and says outright that nothing in the platform spawns it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 21:18:10 +00:00
co-authored by Claude Opus 5
parent cb3e7062b9
commit 607a962115
2 changed files with 193 additions and 1 deletions
+78
View File
@@ -269,3 +269,81 @@ install_deno() {
(cd / && sudo -H -u "$USERNAME" bash -c 'curl -fsSL https://deno.land/install.sh | sh') >/dev/null 2>&1 (cd / && sudo -H -u "$USERNAME" bash -c 'curl -fsSL https://deno.land/install.sh | sh') >/dev/null 2>&1
[[ -x "${USER_HOME}/.deno/bin/deno" ]] [[ -x "${USER_HOME}/.deno/bin/deno" ]]
} }
# -----------------------------------------------------------------------------
# Agent CLIs
# -----------------------------------------------------------------------------
#
# Claude Code goes in through Anthropic's own installer rather than npm, matching
# what the platform does for members (os-user-claude.ts) and chosen there for the
# auto-update the npm package does not do.
#
# Two things that installer insists on, both of which a naive port gets wrong:
#
# It REFUSES to run under sudo from a regular user's shell — it checks for uid 0
# with SUDO_USER set, because everything it installs goes under $HOME and under
# sudo that is root's home. So it must run AS the account, not as root.
#
# It declares #!/bin/bash and uses [[ … =~ … ]], so it must be piped to bash.
# `| sh` fails on a dash-based /bin/sh, which is Ubuntu's.
#
# Both are recorded in os-user-claude.ts too, which found them first.
CLAUDE_INSTALL_URL="https://claude.ai/install.sh"
OPENCODE_INSTALL_URL="https://opencode.ai/install"
# Where each installer actually puts its binary. They disagree, and the platform
# depends on the difference:
#
# claude ~/.local/bin/claude — claude-manager.ts tries Bun.which then
# that exact path
# opencode ~/.opencode/bin/opencode — sidecar/opencode/index.ts:22 hardcodes
# join(homedir(), '.opencode', 'bin', …)
#
# Looking for opencode in ~/.local/bin, as an earlier version of this did,
# reports a perfectly good install as missing and then installs it again.
agent_bin() {
case "$1" in
claude) echo "${USER_HOME}/.local/bin/claude" ;;
opencode) echo "${USER_HOME}/.opencode/bin/opencode" ;;
*) echo "${USER_HOME}/.local/bin/$1" ;;
esac
}
# The directories those live in, for the account's PATH.
agent_bin_dirs() { echo "${USER_HOME}/.local/bin" "${USER_HOME}/.opencode/bin"; }
agent_installed() { [[ -x "$(agent_bin "$1")" ]] || command -v "$1" &>/dev/null; }
# Which copy answers, so the run can say where it came from. Claude installed
# from npm sits in /usr/local/lib/node_modules and does NOT auto-update, which is
# the whole reason the platform prefers Anthropic's installer.
agent_path() {
local name="$1" bin
bin="$(agent_bin "$name")"
[[ -x "$bin" ]] && {
echo "$bin"
return
}
command -v "$name" 2>/dev/null
}
agent_is_npm_install() { [[ "$(readlink -f "$(agent_path "$1")" 2>/dev/null)" == */node_modules/* ]]; }
agent_version() {
local bin
bin="$(agent_path "$1")"
[[ -n "$bin" ]] && (cd / && sudo -H -u "$USERNAME" "$bin" --version 2>/dev/null | head -1)
}
install_claude_code() {
(cd / && sudo -H -u "$USERNAME" bash -c "set -e; curl -fsSL ${CLAUDE_INSTALL_URL} | bash") >/dev/null 2>&1
[[ -x "$(agent_bin claude)" ]]
}
install_opencode() {
(cd / && sudo -H -u "$USERNAME" bash -c "set -e; curl -fsSL ${OPENCODE_INSTALL_URL} | bash") >/dev/null 2>&1
[[ -x "$(agent_bin opencode)" ]]
}
install_pi() { npm install -g @mariozechner/pi-coding-agent >/dev/null 2>&1; }
+115 -1
View File
@@ -2062,13 +2062,127 @@ if ! skip; then
step_ok step_ok
fi fi
# =============================================================================
# 26. Agent CLIs
# =============================================================================
#
# claude and opencode are what the chat sidecars spawn, so they are installed
# rather than offered — for the same reason node and bun are. PI is nobody's
# dependency and is asked for.
step "Agent CLIs"
if ! skip; then
echo ""
info "Agent CLIs — the programs Officer's chat actually runs"
echo " claude $(agent_version claude || echo 'not installed')"
echo " spawned by officer-agent; chat does not work without it."
echo " opencode $(agent_version opencode || echo 'not installed')"
echo " the alternative agent, run by officer-opencode."
echo ""
echo " Installed as ${USERNAME} rather than as root: Anthropic's installer"
echo " refuses to run under sudo, because everything it writes goes under"
echo " \$HOME and under sudo that is root's. They land in different places —"
echo " claude in ~/.local/bin, opencode in ~/.opencode/bin — and the"
echo " sidecars look in exactly those two."
if agent_installed claude && agent_is_npm_install claude; then
# The npm package does not auto-update; Anthropic's installer does, which is
# why the platform uses it for members. Worth offering to switch rather than
# leaving a copy that quietly goes stale.
echo ""
warn "claude here came from npm ($(agent_path claude)) and does not auto-update"
echo " Anthropic's installer puts it in ${USER_HOME}/.local/bin and keeps it"
echo " current. That is what the platform installs for members."
if confirm "Reinstall it with the official installer?"; then
if install_claude_code; then
ok "claude $(agent_version claude) from $(agent_path claude)"
echo " the npm copy is still at /usr/local/bin/claude — remove it with:"
echo " sudo npm uninstall -g @anthropic-ai/claude-code"
SUMMARY+=("Claude Code: reinstalled via the official installer")
else
warn "the Claude Code install did not complete"
ERRORS+=("Claude Code: install failed")
fi
else
echo " left as the npm install"
SUMMARY+=("Claude Code: npm install kept, does not auto-update")
fi
elif agent_installed claude; then
ok "claude $(agent_version claude) already installed"
else
info " installing Claude Code..."
if install_claude_code; then
ok "claude $(agent_version claude)"
SUMMARY+=("Claude Code: $(agent_version claude)")
else
warn "the Claude Code install did not complete"
ERRORS+=("Claude Code: install failed")
fi
fi
if agent_installed opencode; then
ok "opencode $(agent_version opencode) already installed"
else
info " installing opencode..."
if install_opencode; then
ok "opencode $(agent_version opencode)"
SUMMARY+=("opencode: $(agent_version opencode)")
else
warn "the opencode install did not complete"
ERRORS+=("opencode: install failed")
fi
fi
# ~/.local/bin has to be on PATH for the account's own shell. The sidecars find
# claude without it — claude-manager.ts checks that directory explicitly — but
# a user who cannot run `claude` in their own terminal reasonably concludes it
# was never installed.
if id "$USERNAME" &>/dev/null; then
ZSHRC="${USER_HOME}/.zshrc"
touch "$ZSHRC"
chown "$USERNAME:$USERNAME" "$ZSHRC"
# Both directories, because the two installers disagree about where they put
# things. The sidecars find their binaries without this — they check the
# exact paths — but a user who cannot run `claude` in their own terminal
# reasonably concludes it was never installed.
if append_once "$ZSHRC" agent-clis <<'EOF'
export PATH="$HOME/.local/bin:$HOME/.opencode/bin:$PATH"
EOF
then
ok "~/.local/bin and ~/.opencode/bin added to ${USERNAME}'s PATH"
fi
fi
# ── PI, which nothing here uses ──
echo ""
if command -v pi &>/dev/null; then
echo " pi is already installed — left alone"
else
info "PI?"
echo " Another coding agent. Officer does not use it — nothing in the"
echo " platform spawns it — and it is offered because it was in the"
echo " original script and you may want it for your own work."
if confirm "Install PI?" n; then
if install_pi && command -v pi &>/dev/null; then
ok "pi installed"
SUMMARY+=("PI: installed (not used by Officer)")
else
warn "the PI install did not complete"
fi
else
echo " skipped"
fi
fi
step_ok
fi
# ============================================================================= # =============================================================================
# NOT PORTED YET # NOT PORTED YET
# ============================================================================= # =============================================================================
# #
# Sections still to move across from scripts/setup-old/setup-ubuntu.sh, in order: # Sections still to move across from scripts/setup-old/setup-ubuntu.sh, in order:
# #
# dev tools · ufw · zshrc # ufw · zshrc
# #
# And one that is new rather than ported, to come last of all: # And one that is new rather than ported, to come last of all:
# #