From 607a962115045a868664622110e88e1a19a762d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Wed, 12 Aug 2026 21:18:10 +0000 Subject: [PATCH] port the agent CLIs, using Anthropic's installer rather than npm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- scripts/setup/machine-setup/lib/dev.sh | 78 +++++++++++++ scripts/setup/machine-setup/machine-setup.sh | 116 ++++++++++++++++++- 2 files changed, 193 insertions(+), 1 deletion(-) diff --git a/scripts/setup/machine-setup/lib/dev.sh b/scripts/setup/machine-setup/lib/dev.sh index b69278e5..5aa790d8 100644 --- a/scripts/setup/machine-setup/lib/dev.sh +++ b/scripts/setup/machine-setup/lib/dev.sh @@ -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; } diff --git a/scripts/setup/machine-setup/machine-setup.sh b/scripts/setup/machine-setup/machine-setup.sh index 61db28ca..6ac4674b 100755 --- a/scripts/setup/machine-setup/machine-setup.sh +++ b/scripts/setup/machine-setup/machine-setup.sh @@ -2062,13 +2062,127 @@ if ! skip; then step_ok 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 # ============================================================================= # # 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: #