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:
@@ -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; }
|
||||||
|
|||||||
@@ -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:
|
||||||
#
|
#
|
||||||
|
|||||||
Reference in New Issue
Block a user