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
[[ -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; }