#!/usr/bin/env bash
# claude-current: the Claude Code executable a launch should start. That is
# the newest version npm publishes, verified with `--version`, and it is named
# by an ABSOLUTE PATH. `claude` is never resolved through PATH.
#
# Installed on PATH by `openRepoTools --install`, beside `lane-start`, which
# calls it.
#
# Why it exists (opensoft/workBenches#119, Brett Heap's rulings of 2026-09-29):
# a lane started on an old Claude Code, and nothing noticed. Two installs
# competed on PATH, the image's copy frozen at the image build and the user's
# own npm copy, and a session that had run 2.1.283 for twenty hours lacked a
# model a fresh `claude` listed. The home ruling puts the resolver here,
# installed by `openRepoTools --install`. Point 2 is verbatim "for 2, we can
# run update on every start, this ensures we have the latest models", so every
# launch runs this check.
#
# USAGE
#   claude-current [--porcelain] [--offline]
#   claude-current --help
#
# WHAT IT DOES
#   1. Reads the published version with `npm view @anthropic-ai/claude-code
#      version`, bounded by $CLAUDE_CURRENT_TIMEOUT seconds (default 10).
#      --offline skips this step and step 3.
#   2. Reads each installed candidate's `--version`, by absolute path, in this
#      order:
#        - the newest native version in $CLAUDE_CURRENT_NATIVE_DIR (default
#          ~/.local/share/claude/versions), by its x.y.z name, whose
#          `--version` answers: a newer file that does not run gives way to
#          the next one that does, and is not read again in the same run
#          unless an update or a wait for the lock may have changed it;
#        - <user npm prefix>/bin/claude. The prefix is $CLAUDE_CURRENT_NPM_PREFIX,
#          else $NPM_CONFIG_PREFIX, else $npm_config_prefix, else the
#          `prefix=` line of ~/.npmrc, else ~/.npm-global. A relative one is
#          taken under $HOME;
#        - ~/.local/bin/claude;
#        - each entry of $CLAUDE_CURRENT_SYSTEM_CANDIDATES (default
#          /usr/local/bin/claude:/usr/bin/claude, the image's copies).
#      A path that resolves to one already read is read once, and the first
#      spelling, the user-writable one, is kept.
#   3. When every candidate is behind the published version, or there is none,
#      it updates the user-writable install under a lock and reads again. A
#      native install gets `claude update`. When that does not reach the
#      published version, or there is no native install, it runs
#      `npm install -g --prefix <user prefix> @anthropic-ai/claude-code@<published>`
#      and then the package's own install.cjs if `--version` still differs,
#      because npm 12 skips the package's native-binary hook. A second launch
#      waits on the lock and reads again before it updates anything itself.
#   4. Picks the first candidate whose version EQUALS the published one
#      (verified). Else it picks the newest candidate AHEAD of it (ahead).
#      Else it refuses (stale), unless CLAUDE_ALLOW_STALE=1, when it picks the
#      newest (stale). When npm could not be read, it picks the newest
#      candidate (unverified).
#
# OUTPUT
#   stdout   the absolute path, one line. With --porcelain, exactly four
#            lines: path=<absolute path>, version=<x.y.z>,
#            published=<x.y.z, or empty>, and
#            status=<verified|ahead|unverified|stale>. A refusal prints nothing
#            on stdout.
#   stderr   one line saying what was chosen, for example
#            "claude-current: claude 2.1.284 (verified against npm 2.1.284) at <path>".
#            An update prints its own progress there too.
#
# EXIT
#   0   resolved: verified, ahead or unverified, and stale only under
#       CLAUDE_ALLOW_STALE=1
#   1   no runnable Claude Code at all
#   2   refused: every candidate is still behind npm after the update attempt
#   64  usage: an argument it does not know, or a CLAUDE_CURRENT_* bound that
#       is not a whole number of seconds. Kept apart from 1, which a launcher
#       reports as "no Claude Code" (opensoft/workBenches#121)
#
# ENVIRONMENT
#   CLAUDE_ALLOW_STALE=1             launch a stale candidate anyway, and say so
#   CLAUDE_CURRENT_NPM               the npm to run (default: npm)
#   CLAUDE_CURRENT_NODE              the node that runs install.cjs (default: node)
#   CLAUDE_CURRENT_TIMEOUT           seconds for `npm view` (default 10)
#   CLAUDE_CURRENT_VERSION_TIMEOUT   seconds for each `--version` (default 10)
#   CLAUDE_CURRENT_UPDATE_TIMEOUT    seconds for each update command (default 300)
#   CLAUDE_CURRENT_LOCK_WAIT         seconds to wait for another launch's update
#                                    (default 330)
#   CLAUDE_CURRENT_CACHE_DIR         where the lock lives (default
#                                    ${XDG_CACHE_HOME:-~/.cache}/openrepotools)
#                                    Relative paths are taken under $HOME.
#   CLAUDE_CURRENT_NPM_PREFIX        the user npm prefix, see step 2
#   CLAUDE_CURRENT_NATIVE_DIR        the native versions directory, see step 2
#   CLAUDE_CURRENT_SYSTEM_CANDIDATES colon-separated image paths, see step 2.
#                                    Set it empty for none.
#
# HANDING THE RESULT ON (docs/README-claude-current.md)
#   A launcher that starts the path exports CLAUDE_BIN=<path>,
#   CLAUDE_RESOLVED_BIN=<the same path> and CLAUDE_VERIFIED_VERSION=<version>.
#   `lane-start` records `claude <version>` on the register's launch line and
#   keeps CLAUDE_VERIFIED_VERSION out of the session's environment. A later
#   launch that finds CLAUDE_BIN equal to CLAUDE_RESOLVED_BIN, with no
#   CLAUDE_VERIFIED_VERSION, knows the value was inherited from a session and
#   is not an operator's pin, and resolves again.
#
# Bash 3.2 safe (macOS): no mapfile, no associative arrays, no readlink -f,
# no GNU-only flags; `timeout`, else `gtimeout`, else a watchdog; ONE symlink
# lock for every launch, never `flock`, so what PATH holds cannot split it.

set -euo pipefail

prog="claude-current"
PACKAGE="@anthropic-ai/claude-code"

usage() {
	cat <<'USAGE'
claude-current [--porcelain] [--offline]
claude-current --help

Print the absolute path of the Claude Code a launch should start: the version
npm publishes, verified with --version, updating the user-writable install
first when every installed copy is behind. Never resolved through PATH.

  --porcelain   four lines on stdout: path=, version=, published=, status=
                (status is verified, ahead, unverified or stale)
  --offline     no npm read and no update: the newest installed copy, unverified

Exit 0 resolved, 1 nothing runnable, 2 refused as stale, 64 usage or a
malformed CLAUDE_CURRENT_* bound.
CLAUDE_ALLOW_STALE=1 launches a stale copy anyway. The CLAUDE_CURRENT_*
variables and the hand-off to lane-start are in docs/README-claude-current.md.
USAGE
}

say() { printf '%s: %s\n' "$prog" "$*" >&2; }
die() { printf '%s: %s\n' "$prog" "$1" >&2; exit "${2:-1}"; }

porcelain=0
offline=0
while [ $# -gt 0 ]; do
	case "$1" in
		--porcelain) porcelain=1 ;;
		--offline) offline=1 ;;
		-h | --help) usage; exit 0 ;;
		*)
			printf '%s: unknown argument: %s\n' "$prog" "$1" >&2
			usage >&2
			exit 64 ;;
	esac
	shift
done

# ------------------------------------------------------------------ settings

posint() {
	case "${1:-}" in
		'' | *[!0-9]*) return 1 ;;
	esac
	[ "$1" -gt 0 ]
}

NPM="${CLAUDE_CURRENT_NPM:-npm}"
NODE="${CLAUDE_CURRENT_NODE:-node}"
TIMEOUT="${CLAUDE_CURRENT_TIMEOUT:-10}"
VERSION_TIMEOUT="${CLAUDE_CURRENT_VERSION_TIMEOUT:-10}"
UPDATE_TIMEOUT="${CLAUDE_CURRENT_UPDATE_TIMEOUT:-300}"
LOCK_WAIT="${CLAUDE_CURRENT_LOCK_WAIT:-330}"
for setting in TIMEOUT VERSION_TIMEOUT UPDATE_TIMEOUT LOCK_WAIT; do
	eval "setting_value=\${$setting}"
	# shellcheck disable=SC2154
	posint "$setting_value" ||
		die "CLAUDE_CURRENT_$setting must be a whole number of seconds above 0, and it is '$setting_value'" 64
done
NATIVE_DIR="${CLAUDE_CURRENT_NATIVE_DIR:-$HOME/.local/share/claude/versions}"
CACHE_DIR="${CLAUDE_CURRENT_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/openrepotools}"
# Every checkout updating this user's install must take the same lock, even
# when the override (or XDG_CACHE_HOME) is relative and their cwd differs.
case "$CACHE_DIR" in
	/*) : ;;
	*) CACHE_DIR="$HOME/$CACHE_DIR" ;;
esac
LOCK="$CACHE_DIR/claude-current.lock.l"

# A version is x.y.z, optionally with a pre-release or build tail.
version_re='^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.+-]+)?$'
core_re='^[0-9]+\.[0-9]+\.[0-9]+$'
is_version() { [[ "${1:-}" =~ $version_re ]]; }

# ------------------------------------------------------------ small helpers

# Seconds a command that ignores TERM is given before it is sent KILL, so a
# bound is a bound whatever the command does with the first signal.
KILL_GRACE=5

# kill_tree <pid> [<signal>]: the process and every process below it, and the
# pids it signalled, one per line on stdout. Each one is stopped before its
# children are listed, so none of them can start another while the tree is
# walked, then sent <signal> (default TERM) and continued so that it can act
# on it. `pgrep -P` is on Linux and macOS alike; without it only <pid> is
# signalled.
kill_tree() {
	local child sig="${2:-TERM}"
	kill -STOP "$1" 2>/dev/null || return 0
	printf '%s\n' "$1"
	for child in $(pgrep -P "$1" 2>/dev/null); do
		kill_tree "$child" "$sig"
	done
	kill -"$sig" "$1" 2>/dev/null || :
	kill -CONT "$1" 2>/dev/null || :
}

# running <pid>: zombies have exited, even before their parent reaps them.
# The installation lock uses this to distinguish a live owner from a dead one.
running() {
	local st
	kill -0 "$1" 2>/dev/null || return 1
	st="$(ps -o stat= -p "$1" 2>/dev/null)" || return 0
	case "$st" in *Z*) return 1 ;; esac
	return 0
}

# run_bounded <seconds> <command> [args...]: the command's own status, or 124
# when it ran out of time. GNU `timeout`, else Homebrew's `gtimeout`, else a
# watchdog, because a stock macOS ships neither. Every branch sends TERM at
# the bound and KILL $KILL_GRACE seconds later to what is still running:
# `-k` for the two commands, and for the watchdog a dedicated process group.
# The watchdog ends the WHOLE tree, as `timeout` ends its process group: a
# command's own children (npm's lifecycle processes, a script's `sleep`)
# would otherwise keep the caller's command substitution open after the
# command itself was killed. The watchdog's own output goes to /dev/null for
# the same reason.
run_bounded() {
	local secs="$1" pid dog rc=0 flag monitor=0
	shift
	if command -v timeout >/dev/null 2>&1; then
		timeout -k "$KILL_GRACE" "$secs" "$@" || rc=$?
	elif command -v gtimeout >/dev/null 2>&1; then
		gtimeout -k "$KILL_GRACE" "$secs" "$@" || rc=$?
	else
		# WHOEVER MAKES $flag FIRST DECIDES: the watchdog, when the time is up,
		# or this function, when the command ended first. So this function
		# never kills a watchdog half way through its TERM and KILL, and a
		# watchdog that wakes after the command ended signals nothing. Where no
		# flag can be made at all, the watchdog acts and this function stops it.
		flag="${TMPDIR:-/tmp}/claude-current.bound.$$.$RANDOM"
		# Bash job control assigns this asynchronous command its own process
		# group on both Linux and stock macOS. Keep the caller's setting, and
		# signal the group even after its leader exits: a TERM handler may
		# create another child that still owns the output pipe.
		case "$-" in *m*) monitor=1 ;; esac
		set -m
		"$@" &
		pid=$!
		[ "$monitor" = 1 ] || set +m
		(
			sleep "$secs"
			if ! mkdir -- "$flag" 2>/dev/null && [ -d "$flag" ]; then exit 0; fi
			kill -TERM -- "-$pid" 2>/dev/null || exit 0
			sleep "$KILL_GRACE"
			kill -KILL -- "-$pid" 2>/dev/null || :
		) >/dev/null 2>&1 &
		dog=$!
		wait "$pid" || rc=$?
		if mkdir -- "$flag" 2>/dev/null || [ ! -d "$flag" ]; then
			kill_tree "$dog" >/dev/null
		else
			# The watchdog won even if TERM was handled as a successful exit.
			rc=124
		fi
		wait "$dog" 2>/dev/null || :
		rmdir -- "$flag" 2>/dev/null || :
		[ "$rc" = 143 ] && rc=124
	fi
	[ "$rc" = 137 ] && rc=124
	return "$rc"
}

# numcmp <a> <b>: -1, 0 or 1 for two digit strings, compared as numbers of any
# length without integer arithmetic (the same rule as workBenches #109).
numcmp() {
	local x="$1" y="$2"
	x="${x#"${x%%[!0]*}"}"
	y="${y#"${y%%[!0]*}"}"
	x="${x:-0}"
	y="${y:-0}"
	if [ "${#x}" -ne "${#y}" ]; then
		if [ "${#x}" -gt "${#y}" ]; then echo 1; else echo -1; fi
		return 0
	fi
	if [ "$x" = "$y" ]; then echo 0; return 0; fi
	if [[ "$x" > "$y" ]]; then echo 1; else echo -1; fi
}

# precmp <a> <b>: -1, 0 or 1 for two pre-release tags, by SemVer 2.0.0 rule 11:
# dot-separated identifiers from the left; digits-only ones compare as numbers
# and sort before the others, which compare in byte order (LC_ALL=C); when
# every shared identifier is equal, the tag with fewer identifiers is lower.
# So beta.9 < beta.10, and alpha < alpha.1 < alpha.beta.
precmp() {
	local a="$1" b="$2" x y xn yn c
	while [ -n "$a" ] || [ -n "$b" ]; do
		[ -n "$a" ] || { echo -1; return 0; }
		[ -n "$b" ] || { echo 1; return 0; }
		x="${a%%.*}"
		y="${b%%.*}"
		case "$a" in *.*) a="${a#*.}" ;; *) a="" ;; esac
		case "$b" in *.*) b="${b#*.}" ;; *) b="" ;; esac
		xn=1
		yn=1
		case "$x" in '' | *[!0-9]*) xn=0 ;; esac
		case "$y" in '' | *[!0-9]*) yn=0 ;; esac
		if [ "$xn$yn" = 11 ]; then
			c="$(numcmp "$x" "$y")"
		elif [ "$xn$yn" = 10 ]; then
			c=-1
		elif [ "$xn$yn" = 01 ]; then
			c=1
		elif [ "$x" = "$y" ]; then
			c=0
		elif (LC_ALL=C; [[ "$x" < "$y" ]]); then
			c=-1
		else
			c=1
		fi
		[ "$c" = 0 ] || { echo "$c"; return 0; }
	done
	echo 0
}

# vercmp <a> <b>: -1, 0 or 1. Numeric on the x.y.z core. Then a version with no
# pre-release tag beats one with a tag, and two tags compare by precmp. Build
# metadata (+...) is ignored.
vercmp() {
	local a="${1%%+*}" b="${2%%+*}" ac bc ap="" bp="" ar br i c
	ac="${a%%-*}"
	bc="${b%%-*}"
	case "$a" in *-*) ap="${a#*-}" ;; esac
	case "$b" in *-*) bp="${b#*-}" ;; esac
	for i in 1 2 3; do
		ar="${ac%%.*}"
		br="${bc%%.*}"
		c="$(numcmp "$ar" "$br")"
		[ "$c" = 0 ] || { echo "$c"; return 0; }
		ac="${ac#*.}"
		bc="${bc#*.}"
	done
	if [ "$ap" = "$bp" ]; then echo 0; return 0; fi
	if [ -z "$ap" ]; then echo 1; return 0; fi
	if [ -z "$bp" ]; then echo -1; return 0; fi
	precmp "$ap" "$bp"
}

# physical <path>: the path with every symlink resolved, without `readlink -f`,
# which BSD `readlink` did not take until macOS 12.3.
physical() {
	local p="$1" link n=0 dir
	while [ -L "$p" ] && [ "$n" -lt 40 ]; do
		link="$(readlink -- "$p")" || break
		case "$link" in
			/*) p="$link" ;;
			*) p="$(dirname -- "$p")/$link" ;;
		esac
		n=$((n + 1))
	done
	dir="$(cd -P -- "$(dirname -- "$p")" 2>/dev/null && pwd)" || {
		printf '%s\n' "$p"
		return 0
	}
	printf '%s/%s\n' "$dir" "$(basename -- "$p")"
}

# read_version <path>: the first version on the first line of `<path> --version`.
read_version() {
	local out="" v
	out="$(run_bounded "$VERSION_TIMEOUT" "$1" --version 2>/dev/null </dev/null)" || return 1
	v="$(printf '%s\n' "$out" | sed -n -e '1s/^[^0-9]*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*[-+0-9A-Za-z.]*\).*$/\1/p')"
	is_version "$v" || return 1
	printf '%s\n' "$v"
}

# ----------------------------------------------------------- the candidates

# The user npm prefix: the one place an update may write.
npm_user_prefix() {
	local p=""
	if [ -n "${CLAUDE_CURRENT_NPM_PREFIX:-}" ]; then
		p="$CLAUDE_CURRENT_NPM_PREFIX"
	elif [ -n "${NPM_CONFIG_PREFIX:-}" ]; then
		p="$NPM_CONFIG_PREFIX"
	elif [ -n "${npm_config_prefix:-}" ]; then
		p="$npm_config_prefix"
	elif [ -r "$HOME/.npmrc" ]; then
		p="$(sed -n -e 's/^[[:space:]]*prefix[[:space:]]*=[[:space:]]*//p' "$HOME/.npmrc" | tail -n 1)"
		p="${p%"${p##*[![:space:]]}"}"
		p="${p%\"}"
		p="${p#\"}"
	fi
	case "$p" in
		'~') p="$HOME" ;;
		'~/'*) p="$HOME/${p#'~/'}" ;;
		'${HOME}'*) p="$HOME${p#'${HOME}'}" ;;
		'$HOME'*) p="$HOME${p#'$HOME'}" ;;
	esac
	p="${p%/}"
	[ -n "$p" ] || p="$HOME/.npm-global"
	# A relative prefix is anchored at $HOME, so the path npm installs into is
	# the path read back, and the same one whichever directory asks.
	case "$p" in
		/*) ;;
		*) p="$HOME/$p" ;;
	esac
	printf '%s\n' "$p"
}
USER_PREFIX="$(npm_user_prefix)"

cands=""       # one "<path><TAB><version>" line per candidate, in order
seen=$'\n'     # the physical paths already read
native_path="" # the native candidate that runs, when there is one
native_broken=$'\n' # the native files whose version read failed, until something may have changed them

add_candidate() {
	local p="$1" real v
	case "$p" in /*) ;; *) return 0 ;; esac
	[ -f "$p" ] && [ -x "$p" ] || return 0
	real="$(physical "$p")"
	case "$seen" in *$'\n'"$real"$'\n'*) return 0 ;; esac
	seen="$seen$real"$'\n'
	v="$(read_version "$p")" || return 0
	cands="$cands$p"$'\t'"$v"$'\n'
}

# native_names: the x.y.z names of the executable regular files in the native
# versions directory, highest first (workBenches #109's ordering), one per
# line. One pass and one `sort`, because ordering by `vercmp` is a process for
# every comparison.
native_names() {
	local f name
	for f in "$NATIVE_DIR"/*; do
		[ -f "$f" ] && [ -x "$f" ] && [ ! -L "$f" ] || continue
		name="${f##*/}"
		[[ "$name" =~ $core_re ]] || continue
		printf '%s\n' "$name"
	done | sort -t. -k1,1nr -k2,2nr -k3,3nr
}

# add_native: the native candidate is the newest native file whose `--version`
# answers. A newer file that does not run (a download cut short, a binary for
# another libc, a first start that hangs) must not hide an older one that
# does, so each is read from the highest down and the first that answers is
# kept. A working newest file costs one read. A file that failed is
# remembered, because collect runs up to four times and a hung file would cost
# its whole bound on each, and forgotten wherever something else may have
# written it: after a wait for the lock, when another launch may have finished
# a file this one read half written, and after `claude update`, which rewrites
# files, the failed one among them.
add_native() {
	local name f before
	native_path=""
	[ -d "$NATIVE_DIR" ] || return 0
	while IFS= read -r name; do
		[ -n "$name" ] || continue
		f="$NATIVE_DIR/$name"
		case "$native_broken" in *$'\n'"$f"$'\n'*) continue ;; esac
		before="$cands"
		add_candidate "$f"
		if [ "$cands" != "$before" ]; then
			native_path="$f"
			return 0
		fi
		native_broken="$native_broken$f"$'\n'
	done <<EOF
$(native_names)
EOF
}

collect() {
	local p old_ifs
	cands=""
	seen=$'\n'
	add_native
	add_candidate "$USER_PREFIX/bin/claude"
	add_candidate "$HOME/.local/bin/claude"
	old_ifs="$IFS"
	set -f
	IFS=:
	for p in ${CLAUDE_CURRENT_SYSTEM_CANDIDATES-/usr/local/bin/claude:/usr/bin/claude}; do
		IFS="$old_ifs"
		add_candidate "$p"
		IFS=:
	done
	IFS="$old_ifs"
	set +f
}

# pick <mode> [<published>]: sets CHOSEN_PATH and CHOSEN_VERSION, or returns 1.
#   equal    the first candidate whose version equals <published>
#   ahead    the newest candidate newer than <published>
#   newest   the newest candidate at all (the first of equal ones)
pick() {
	local mode="$1" ref="${2:-}" p v c best="" bestv=""
	while IFS=$'\t' read -r p v; do
		[ -n "$p" ] || continue
		case "$mode" in
			equal)
				if [ "$(vercmp "$v" "$ref")" = 0 ]; then
					CHOSEN_PATH="$p"
					CHOSEN_VERSION="$v"
					return 0
				fi
				continue ;;
			ahead)
				[ "$(vercmp "$v" "$ref")" = 1 ] || continue ;;
		esac
		if [ -z "$best" ]; then
			best="$p"
			bestv="$v"
			continue
		fi
		c="$(vercmp "$v" "$bestv")"
		if [ "$c" = 1 ]; then
			best="$p"
			bestv="$v"
		fi
	done <<EOF
$cands
EOF
	[ -n "$best" ] || return 1
	CHOSEN_PATH="$best"
	CHOSEN_VERSION="$bestv"
	return 0
}

installed_summary() {
	local p v out=""
	while IFS=$'\t' read -r p v; do
		[ -n "$p" ] || continue
		out="${out:+$out; }$v at $p"
	done <<EOF
$cands
EOF
	printf '%s' "${out:-none}"
}

# ------------------------------------------------------------ the lock
#
# ONE LOCK FOR EVERY LAUNCH, WHATEVER ITS PATH HOLDS (Copilot on #134). The
# lock was `flock` where PATH had one and a symlink where it did not, so two
# launches with different PATHs held two independent locks and both updated
# the same install at once. Every launch now takes the one symlink at $LOCK,
# and `flock` is never consulted.
#
# THE SYMLINK'S TEXT NAMES ITS OWNER, `pid=<pid>@<place>`. `ln -s` makes the
# lock and publishes the owner in one atomic act, so a launch killed at any
# point leaves either no lock or a lock that names it, never an ownerless one
# every later launch would wait on. <place> is where that pid means anything:
# the host's name and, where /proc shows one, the inode of the pid namespace.
# Containers that share one home share this lock, and a pid read in another
# namespace says nothing about whether its owner still runs, which `flock`
# never needed to know. So a lock made ELSEWHERE is waited on and never taken
# over, and the refusal names it and its place; a lock made HERE whose owner
# is no longer running is taken over.
#
# ONE LAUNCH REMOVES A DEAD OWNER'S LOCK, and it holds a second directory, the
# reaper, while it looks again. Two launches that both read the same dead pid
# would otherwise both remove "the stale lock", and the second removal would
# take the lock the first had just made, so both would update at once. Under
# the reaper the lock is removed only while it still names the owner that was
# seen dead. A reaper left by a launch killed in those few lines blocks the
# takeover, and the refusal names it.

OWNER=""
PLACE=""
link_lock=""
lock_why=""
lock_slept=0 # 1 once this launch has waited for the lock

# lock_place: this launch's <place>, `<host>` or `<host>:<pid namespace inode>`.
# No `/`, because `ln -s` onto a directory names the link it puts inside after
# the target's last component.
lock_place() {
	local host ns
	host="$(uname -n 2>/dev/null || :)"
	ns="$(readlink "/proc/$$/ns/pid" 2>/dev/null || :)"
	# `pid:[4026531836]` on Linux, nothing without /proc: its digits or none.
	ns="${ns//[!0-9]/}"
	case "$host" in '' | *[/@:]*) host=unknown ;; esac
	printf '%s' "$host${ns:+:$ns}"
}

# take_lock: 0 with the lock held, or 1 with lock_why saying why not.
take_lock() {
	local waited=0 missing=0 record owner where reaped
	if ! mkdir -p -- "$CACHE_DIR" 2>/dev/null; then
		lock_why="the update lock's directory $CACHE_DIR could not be created"
		return 1
	fi
	PLACE="$(lock_place)"
	OWNER="pid=$$@$PLACE"
	while :; do
		if ln -s "$OWNER" "$LOCK" 2>/dev/null; then
			[ "$(readlink -- "$LOCK" 2>/dev/null || :)" = "$OWNER" ] && break
			# A directory at that path: `ln` put the link inside it.
			rm -f -- "$LOCK/$OWNER" 2>/dev/null || :
			lock_why="$LOCK is a directory and not this command's lock: remove it"
			return 1
		fi
		if [ ! -L "$LOCK" ] && [ ! -e "$LOCK" ]; then
			# Nothing there and still no lock: one that was released between
			# the two acts is taken at the next try, and a directory this
			# launch cannot write in is named now rather than waited out.
			missing=$((missing + 1))
			if [ "$missing" -ge 2 ]; then
				lock_why="the update lock $LOCK could not be made in $CACHE_DIR"
				return 1
			fi
			continue
		fi
		missing=0
		record="$(readlink -- "$LOCK" 2>/dev/null || :)"
		owner=""
		where=""
		case "$record" in
			pid=*@*)
				owner="${record#pid=}"
				owner="${owner%%@*}"
				where="${record#*@}" ;;
		esac
		case "$owner" in '' | *[!0-9]*) owner="" ;; esac
		# A lock made here whose owner has gone, or that names this very pid
		# (a dead launch's, which this one never took), is a lock nobody holds.
		if [ -n "$owner" ] && [ "$where" = "$PLACE" ] &&
			{ [ "$owner" = "$$" ] || ! running "$owner"; } &&
			mkdir -- "$LOCK.reap" 2>/dev/null; then
			reaped=0
			if [ "$(readlink -- "$LOCK" 2>/dev/null || :)" = "$record" ] &&
				rm -f -- "$LOCK" 2>/dev/null && [ ! -L "$LOCK" ]; then
				reaped=1
			fi
			rmdir -- "$LOCK.reap" 2>/dev/null || :
			# Taken over: try for the lock at once. Not taken over (another
			# launch got there first, or the lock would not go): wait as usual.
			[ "$reaped" = 1 ] && continue
		fi
		if [ "$waited" -ge "$LOCK_WAIT" ]; then
			lock_why="the update lock $LOCK was not free within ${LOCK_WAIT}s"
			if [ -n "$owner" ] && [ "$where" != "$PLACE" ]; then
				lock_why="$lock_why. It is held by pid $owner at $where, and this launch runs at $PLACE, where that pid says nothing about whether its owner still runs: remove $LOCK if no launch is running there"
			elif [ -z "$owner" ]; then
				lock_why="$lock_why. It is not a lock this command makes (it reads '${record:-not a symlink}'): remove $LOCK if no launch is running"
			fi
			[ -d "$LOCK.reap" ] &&
				lock_why="$lock_why, and $LOCK.reap, the directory a launch holds while it removes a dead owner's lock, is still there: remove it if no launch is running"
			return 1
		fi
		sleep 1
		waited=$((waited + 1))
		lock_slept=1
	done
	link_lock="$LOCK"
	lock_why=""
	return 0
}

release_lock() {
	if [ -n "$link_lock" ]; then
		if [ "$(readlink -- "$link_lock" 2>/dev/null || :)" = "$OWNER" ]; then
			rm -f -- "$link_lock"
		fi
		link_lock=""
	fi
}
trap release_lock EXIT

# ------------------------------------------------------------ the update

update_why=""

update_native() {
	local rc=0
	say "updating the native install toward $PUBLISHED: $native_path update"
	run_bounded "$UPDATE_TIMEOUT" "$native_path" update </dev/null >&2 9>&- || rc=$?
	[ "$rc" = 0 ] || update_why="\`$native_path update\` exited $rc"
	return "$rc"
}

update_npm() {
	local rc=0 v hook
	if ! command -v "$NPM" >/dev/null 2>&1; then
		update_why="there is no npm ('$NPM') to install $PACKAGE@$PUBLISHED with"
		return 1
	fi
	if ! mkdir -p -- "$USER_PREFIX" 2>/dev/null; then
		update_why="the user npm prefix $USER_PREFIX could not be created"
		return 1
	fi
	say "updating $USER_PREFIX to claude $PUBLISHED: $NPM install -g --prefix $USER_PREFIX $PACKAGE@$PUBLISHED"
	run_bounded "$UPDATE_TIMEOUT" "$NPM" install -g --prefix "$USER_PREFIX" --no-fund --no-audit \
		"$PACKAGE@$PUBLISHED" </dev/null >&2 9>&- || rc=$?
	if [ "$rc" != 0 ]; then
		update_why="\`$NPM install -g --prefix $USER_PREFIX $PACKAGE@$PUBLISHED\` exited $rc"
		return "$rc"
	fi
	# npm 12's install-script policy skips the package's native-binary hook, so
	# the launcher it links can be a stub until that hook runs. The shared image
	# runs the same hook by hand (workBenches base-image/install-ai-clis.sh).
	v="$(read_version "$USER_PREFIX/bin/claude" || :)"
	hook="$USER_PREFIX/lib/node_modules/$PACKAGE/install.cjs"
	if [ "$v" != "$PUBLISHED" ] && [ -f "$hook" ]; then
		say "running the package's own install hook, which npm skipped: $NODE $hook"
		rc=0
		run_bounded "$UPDATE_TIMEOUT" "$NODE" "$hook" </dev/null >&2 9>&- || rc=$?
		[ "$rc" = 0 ] || update_why="the package's install hook ($hook) exited $rc"
	fi
	return 0
}

# ------------------------------------------------------------ the answer

PUBLISHED=""
CHOSEN_PATH=""
CHOSEN_VERSION=""

answer() { # <status> <stderr line>
	say "$2"
	if [ "$porcelain" = 1 ]; then
		printf 'path=%s\nversion=%s\npublished=%s\nstatus=%s\n' \
			"$CHOSEN_PATH" "$CHOSEN_VERSION" "$PUBLISHED" "$1"
	else
		printf '%s\n' "$CHOSEN_PATH"
	fi
	exit 0
}

none_installed() { # <why>
	die "no runnable Claude Code was found by absolute path ($1). Looked in: $NATIVE_DIR, $USER_PREFIX/bin/claude, $HOME/.local/bin/claude and ${CLAUDE_CURRENT_SYSTEM_CANDIDATES-/usr/local/bin/claude:/usr/bin/claude}. Install it with: npm install -g --prefix $USER_PREFIX $PACKAGE" 1
}

collect

if [ "$offline" = 1 ]; then
	pick newest || none_installed "--offline reads no npm and installs nothing"
	answer unverified "claude $CHOSEN_VERSION at $CHOSEN_PATH (--offline: not checked against npm)"
fi

published_why=""
if ! command -v "$NPM" >/dev/null 2>&1; then
	published_why="there is no npm ('$NPM') to ask"
else
	view_rc=0
	view_out="$(run_bounded "$TIMEOUT" "$NPM" view "$PACKAGE" version --prefer-online 2>/dev/null </dev/null)" || view_rc=$?
	if [ "$view_rc" = 124 ]; then
		published_why="npm view took longer than ${TIMEOUT}s"
	elif [ "$view_rc" != 0 ]; then
		published_why="npm view exited $view_rc"
	else
		view_last="$(printf '%s\n' "$view_out" | sed -n -e '$p' | tr -d "'\"[:space:]")"
		if is_version "$view_last"; then
			PUBLISHED="$view_last"
		else
			published_why="npm view answered '$view_out'"
		fi
	fi
fi

if [ -z "$PUBLISHED" ]; then
	pick newest || none_installed "and npm could not be read: $published_why"
	answer unverified "UNVERIFIED: could not reach npm ($published_why); launching claude $CHOSEN_VERSION at $CHOSEN_PATH"
fi

if pick equal "$PUBLISHED"; then
	answer verified "claude $CHOSEN_VERSION (verified against npm $PUBLISHED) at $CHOSEN_PATH"
fi
if pick ahead "$PUBLISHED"; then
	answer ahead "claude $CHOSEN_VERSION (ahead of npm $PUBLISHED) at $CHOSEN_PATH"
fi

# Every candidate is behind, or there is none. Update under the lock, and read
# again after taking it: another launch may have updated while this one waited.
locked=0
if take_lock; then
	locked=1
fi
# Whoever held the lock while this launch waited may have finished or repaired
# a native it read half written.
[ "$lock_slept" = 0 ] || native_broken=$'\n'
collect
if pick equal "$PUBLISHED"; then
	release_lock
	answer verified "claude $CHOSEN_VERSION (verified against npm $PUBLISHED) at $CHOSEN_PATH, updated by another launch"
fi
# The same two answers as before the wait, in the same order: a copy newer
# than npm that appeared while this launch waited is `ahead` here too, and not
# a reason to update again or to refuse (Copilot on #134).
if pick ahead "$PUBLISHED"; then
	release_lock
	answer ahead "claude $CHOSEN_VERSION (ahead of npm $PUBLISHED) at $CHOSEN_PATH, updated by another launch"
fi
if [ "$locked" = 1 ]; then
	if [ -n "$native_path" ]; then
		update_native || :
		native_broken=$'\n' # the update rewrites files, the failed one among them
		collect
	fi
	if ! pick equal "$PUBLISHED" && ! pick ahead "$PUBLISHED"; then
		update_npm || :
		collect
	fi
	release_lock
	if pick equal "$PUBLISHED"; then
		answer verified "claude $CHOSEN_VERSION (verified against npm $PUBLISHED) at $CHOSEN_PATH, updated"
	fi
	if pick ahead "$PUBLISHED"; then
		answer ahead "claude $CHOSEN_VERSION (ahead of npm $PUBLISHED) at $CHOSEN_PATH, updated"
	fi
fi

why="${lock_why:-${update_why:-the update ran and still did not produce $PUBLISHED}}"
if ! pick newest; then
	none_installed "and installing $PUBLISHED did not produce one: $why"
fi
if [ "${CLAUDE_ALLOW_STALE:-}" = 1 ]; then
	answer stale "STALE: claude $CHOSEN_VERSION at $CHOSEN_PATH is behind npm $PUBLISHED ($why); launching it because CLAUDE_ALLOW_STALE=1"
fi
fix="npm install -g --prefix $USER_PREFIX $PACKAGE@$PUBLISHED"
[ -n "$native_path" ] && fix="$native_path update, or $fix"
die "REFUSED: npm publishes claude $PUBLISHED and the newest installed is $CHOSEN_VERSION at $CHOSEN_PATH, and the update did not produce $PUBLISHED ($why). Installed: $(installed_summary). Fix: $fix. CLAUDE_ALLOW_STALE=1 launches $CHOSEN_VERSION anyway." 2
