#!/bin/sh
# Installs Annotate for Claude Code: curl -fsSL https://annotate.fly.dev | sh
# Add -s stage for the stage version (the next one, before it is released), which asks you to sign in with GitHub first.
set -u
NAME='Annotate'
F='https://annotate.fly.dev'
LINE='curl -fsSL https://annotate.fly.dev | sh'
APP='annotate'
WORD='annotate'
say() { printf '%s\n' "$*"; }
# The stage install's second line names the GitHub login it was made for.
# Read by the shell itself, so no command's arguments (Terminal.app's title) name the marker (B26).
signed() { L=''; _s2=''; { read -r _s1; read -r _s2; } < "$T" 2>/dev/null; case "$_s2" in '# clawed-signed-in-as '*) L=$(printf '%s' "${_s2#'# clawed-signed-in-as '}" | tr -cd 'A-Za-z0-9-') ;; esac; [ -z "$L" ] || say "Signed in as $L"; }
# The person's yes, once, in this terminal, before anything else: 0 to go on, 1 when it stops (it says why).
# - An install already here (its current.json) is an update: the person said yes when it was installed, so it is not
#   asked again (the same rule as install.sh's own ask).
# - ANNOTATE_YES=1 (this app's own; CLAWED_YES=1, its old name) is a yes given beforehand, never printed.
# - Otherwise it asks on /dev/tty, which is the person's terminal even when stdin is curl's pipe; Enter, EOF or anything
#   but y is a no. With no terminal at all (Claude Code's Bash or "!", cron) it asks nobody, prints the one line to run in
#   a terminal, relayable as it is (ops, after the flip: a friend asks Claude Code to "install Chippy"), and stops.
ask() {
  [ ! -f "${CLAUDE_APPS_DATA:-$HOME/.claude-apps/$APP}/current.json" ] || return 0
  [ "${ANNOTATE_YES:-${CLAWED_YES:-}}" != 1 ] || return 0
  if ! ( : </dev/tty ) 2>/dev/null; then
    say "Run this in a terminal to install $NAME: $LINE$1"
    return 1
  fi
  printf 'Install %s? This puts the %s app and its Claude Code plugin on this computer%s. [y/N] ' "$NAME" "$NAME" "${2:-}" >/dev/tty
  read -r reply </dev/tty || { reply=''; printf '
' >/dev/tty; }
  # A terminal that wraps pasted text in bracketed-paste marks (ESC[200~ ... ESC[201~) still gives a plain y.
  reply=$(printf '%s' "$reply" | tr -d '\033' | sed 's/\[20[01]~//g' | tr -d '[:space:]')
  case "$reply" in y|Y|yes|Yes|YES) return 0 ;; esac
  say "You didn't say yes, so $NAME isn't installed, and nothing was changed. When you want it, paste this line into a terminal again."
  STOP=0; return 1
}
# B31: Terminal.app titles its window with the foreground command and its arguments, so the sign-in's held poll (its stdin
# the poll's secret, M its seconds) runs as curl in a session of its own (perl's setsid), and this sh is the title while
# it waits. Its stdin goes in by fd 3 (an & command's own stdin is /dev/null). Called in a command substitution: when that
# shell ends (a Ctrl-C, or the line killed), perl ends the curl too, as a Ctrl-C ended it in the foreground.
apart() {
  export F T M
  if command -v perl >/dev/null 2>&1; then
    { perl -e 'use POSIX; POSIX::setsid(); my $p = getppid(); my $k = fork; defined $k or exit 1;
        if (!$k) { exec "curl", "-sS", "-m", $ENV{M}, "-o", $ENV{T}, "-w", "%{http_code}", "-X", "POST", "-H", "content-type: application/json", "--data-binary", "\@-", "$ENV{F}/stage/poll"; exit 1 }
        close STDIN; while (1) { if (waitpid($k, WNOHANG) > 0) { exit($? & 127 ? 1 : $? >> 8) } if (getppid() != $p) { kill "TERM", $k; exit 1 } select(undef, undef, undef, 0.1) }' <&3 2>/dev/null & } 3<&0
  else { curl -sS -m "$M" -o "$T" -w '%{http_code}' -X POST -H 'content-type: application/json' --data-binary @- "$F/stage/poll" <&3 2>/dev/null & } 3<&0
  fi
  wait $!
}
# The browser sign-in: 0 or the install's own status once answered, 1 with the front's line, 3 to sign in with a code.
web() {
  code=$(curl -sS -o "$T" -w '%{http_code}' -X POST -H 'content-type: application/json' -d '{"browser":true}' "$F/stage/start") || { say "Couldn't reach $F (are you online?). Try again in a minute."; return 1; }
  if [ "$code" != 200 ]; then cat "$T"; return 1; fi
  [ "$(sed -n 1p "$T")" = browser ] || { HAVE=1; return 3; }
  links
  say "Opening GitHub to sign you in…"
  # The opener runs on its own; one that ends with an error within 3 s means no browser here.
  "$1" "$O" </dev/null >/dev/null 2>&1 &
  P=$!; i=0
  while [ "$i" -lt 30 ] && kill -0 "$P" 2>/dev/null; do sleep 0.1; i=$((i + 1)); done
  if ! kill -0 "$P" 2>/dev/null && ! wait "$P"; then say "No browser opened here, so sign in with a code instead."; return 3; fi
  say "If nothing opened: $UC"
  T0=$(date +%s); STILL=''; OFF=0
  while :; do
    # The secret goes on stdin, never on a command line.
    M=40; code=$(printf '{"poll":"%s"}' "$K" | apart) || code=0
    case "$code" in
      200) signed; CLAWED_YES=1 sh "$T" </dev/null; return $? ;;
      202) OFF=0 ;;
      0|000|502|503|504) OFF=$((OFF + 1)); [ "$OFF" != 2 ] || say "Can't reach $F, trying again…"; sleep 2 ;;
      *) cat "$T"; return 1 ;;
    esac
    E=$(($(date +%s) - T0))
    [ "$E" -lt 1800 ] || { say "We never heard back from GitHub. $NAME isn't installed, and nothing was changed. When you want it, paste this line into a terminal again."; return 1; }
    [ -n "$STILL" ] || [ "$E" -lt 60 ] || { STILL=1; say "Still waiting for GitHub. If nothing opened: $UC"; }
  done
}
# The browser form's answer (ops row 580): K the terminal's secret; U the short link to print, any browser, any device; O
# the URL this terminal opens itself, which holds a second secret and is never printed (an older service has none: U);
# UC the short link with the code its confirm page shows.
links() {
  K=$(sed -n 2p "$T" | tr -cd 'A-Za-z0-9_-'); U=$(sed -n 3p "$T" | tr -cd 'A-Za-z0-9:/.?=&_%-')
  O=$(sed -n 4p "$T" | tr -cd 'A-Za-z0-9:/.?=&_%-'); [ -n "$O" ] || O=$U
  # The app's own sign-in page on this front (ops row 584), with a choice of how to sign in; else GitHub's page as before.
  case "$O" in "$F/s/"*) PAGE="the sign-in page"; AT='' ;; *) PAGE="GitHub's sign-in"; AT=' at GitHub' ;; esac
  C=$(sed -n 5p "$T" | tr -cd 'A-Z0-9' | cut -c1-8); UC=$U; [ -z "$C" ] || UC="$U (code $C)"
}
# Opens $1 in this computer's default browser (ops row 578; Z, 2026-10-15: "it should auto open the default browser like
# chrome or firefox"): open on a Mac; on Linux xdg-open, then gio open, then the desktop's OpenURI portal over D-Bus, each
# with this terminal's own session (its bus from XDG_RUNTIME_DIR when the terminal lost it). 0 when one of them took it.
openurl() {
  if [ "$OS" = Darwin ]; then open "$1"; return; fi
  if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then R=${XDG_RUNTIME_DIR:-/run/user/$(id -u 2>/dev/null)}; [ ! -S "$R/bus" ] || export DBUS_SESSION_BUS_ADDRESS="unix:path=$R/bus"; fi
  if command -v xdg-open >/dev/null 2>&1 && xdg-open "$1"; then return 0; fi
  if command -v gio >/dev/null 2>&1 && gio open "$1"; then return 0; fi
  command -v gdbus >/dev/null 2>&1 && gdbus call --session --dest org.freedesktop.portal.Desktop --object-path /org/freedesktop/portal/desktop --method org.freedesktop.portal.OpenURI.OpenURI '' "$1" '{}'
}
# The browser opener here, in B: CLAWED_OPEN names one (the tests' own); CLAUDE_APPS_NO_OPEN=1 opens nothing; on Linux
# only in a desktop session's terminal (a display), never from an ssh shell into someone's seat.
opener() {
  B=${CLAWED_OPEN:-}; OS=${CLAWED_OS:-$(uname -s 2>/dev/null)}
  if [ -n "$B" ] || [ "${CLAUDE_APPS_NO_OPEN:-}" = 1 ]; then :
  elif [ "$OS" = Darwin ]; then command -v open >/dev/null 2>&1 && B=openurl
  elif [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ]; then
    for c in xdg-open gio gdbus; do if command -v "$c" >/dev/null 2>&1; then B=openurl; break; fi; done
  fi
}
# Sign-in after the install (ops row 574): who this computer's install is, by GitHub. It never blocks the install, which
# is already done, and never changes its exit. Skipped for CLAWED_TEST=1 (a test box's tester grant) and on a computer
# that signed in before; with a browser, GitHub's page and the held poll; else GitHub's code to type; either waits
# CLAWED_SIGNIN_WAIT seconds ($1 is the default: 120 with a browser and 30 without after an install; 900 for -s sign-in),
# then says how to sign in later. The install's token goes on stdin, never on a command line.
signin() {
  [ "${CLAWED_TEST:-}" != 1 ] || return 0
  D=${CLAUDE_APPS_DATA:-$HOME/.claude-apps/$APP}
  # B40: a file in the app's folder is read through a redirection (the shell opens it), so no command here names it (B29).
  if [ -s "$D/signed-in" ]; then [ "$1" != later ] || say "$NAME on this computer is already signed in as $({ head -c 40 <"$D/signed-in"; } 2>/dev/null | tr -cd 'A-Za-z0-9-')."; return 0; fi
  TOK=$({ head -c 200 <"$D/token"; } 2>/dev/null | tr -cd 'A-Za-z0-9_-'); IID=$({ head -c 64 <"$D/install_id"; } 2>/dev/null | tr -cd 'A-Za-z0-9-')
  if [ -z "$TOK$IID" ]; then [ "$1" != later ] || say "$NAME isn't installed on this computer, so there is nothing to sign in. Install it first: $LINE"; return 0; fi
  LATER="To sign in later, paste this into a terminal:
$LINE -s sign-in"
  [ "${CLAWED_SIGNIN_WAIT:-}" != 0 ] || { say "$NAME is installed. $LATER"; return 0; }
  # Always the browser form (ops row 578): its short link works in any browser on any device, so GitHub's typed code is
  # never the default; a service that answers with a code anyway still gets one.
  # B50: ready when the app is here and no download of it is still going (its fetch.json, read by the shell, B29), so the
  # sign-in page may say the step left in Claude Code; else it says only to go back to the terminal (chippy-team's word).
  RDY=''; R0=''
  if [ -f "$D/current.json" ]; then { IFS= read -r R0 || [ -n "$R0" ]; } 2>/dev/null <"$D/fetch.json"; case "$R0" in ''|*'"done":true'*) RDY=',"ready":true' ;; esac; fi
  opener; BODY="{\"bind\":{\"token\":\"$TOK\",\"install_id\":\"$IID\"}$RDY,\"browser\":true"
  code=$(printf '%s}' "$BODY" | curl -sS -o "$T" -w '%{http_code}' -X POST -H 'content-type: application/json' --data-binary @- "$F/stage/start" 2>/dev/null) || code=0
  if [ "$code" != 200 ]; then say "Signing in isn't available right now. $LATER"; return 0; fi
  T0=$(date +%s)
  # Someone at a terminal gets the long wait; a run with no terminal (an agent, CI) the short one.
  if ( : </dev/tty ) 2>/dev/null; then LONG=${2:-120}; else LONG=${3:-30}; fi
  if [ "$(sed -n 1p "$T")" = browser ]; then
    links; OPENED=''
    if [ -n "$B" ]; then
      "$B" "$O" </dev/null >/dev/null 2>&1 &
      # An opener that ends with an error within 3 s means no browser here.
      P=$!; i=0; OPENED=1
      while [ "$i" -lt 30 ] && kill -0 "$P" 2>/dev/null; do sleep 0.1; i=$((i + 1)); done
      if ! kill -0 "$P" 2>/dev/null && ! wait "$P"; then OPENED=''; fi
    fi
    W=${CLAWED_SIGNIN_WAIT:-${2:-120}}
    # A browser already running may take the page behind its own window, and nothing here can raise it on GNOME (no
    # activation token from a terminal), so the line says where it is (ops row 580, walk 1).
    if [ -n "$OPENED" ]; then say "$(printf '%s' "$PAGE" | sed 's/^t/T/') is open in your browser. Switch to it to sign in."; say "If nothing opened: $UC"
    else [ -n "$B" ] || W=${CLAWED_SIGNIN_WAIT:-$LONG}; say "To sign in, open $U on any device$([ -n "$AT" ] && printf ' signed in to GitHub')$([ -z "$C" ] || printf ' (code %s)' "$C")."; fi
  else
    W=${CLAWED_SIGNIN_WAIT:-$LONG}; AT=' at GitHub'
    K=$(sed -n 1p "$T"); EVERY=$(sed -n 4p "$T" | tr -cd 0-9)
    say "To sign in, open $(sed -n 3p "$T") and type $(sed -n 2p "$T")"
  fi
  # One line of life while it waits (ops row 580, walk 1): what it waits for, how long, and how to skip it. Ctrl-C skips
  # it here, and the install still ends with the app open.
  if [ "$W" -ge 120 ]; then HOW="$((W / 60)) minutes"; else HOW="$W seconds"; fi
  say "Waiting for you to sign in$AT (up to $HOW; Ctrl-C skips it)."
  trap 'stopwait; say ""; say "$NAME is installed, not signed in. $LATER"; later; exit 0' INT TERM HUP
  while :; do
    R=$((W - ($(date +%s) - T0))); [ "$R" -gt 0 ] || break
    [ -n "$U" ] || { sleep "${CLAWED_POLL_SECONDS:-${EVERY:-5}}"; }
    M=$R; [ "$M" -le 40 ] || M=40
    code=$(printf '{"poll":"%s"}' "$K" | apart) || code=0
    case "$code" in
      # Continue as guest (ops row 584): the install goes on as it is, unsigned, and can sign in later.
      200) trap - INT TERM HUP; if [ "$(sed -n 1p "$T")" = guest ]; then say "Continuing as a guest. $LATER"; return 0; fi
        L=$(sed -n 2p "$T" | tr -cd "A-Za-z0-9 .'-" | cut -c1-60); say "Signed in as $L."; printf '%s
' "$L" >"$D/signed-in" 2>/dev/null; return 0 ;;
      202|0|000|502|503|504) W2=$(sed -n 2p "$T" 2>/dev/null | tr -cd 0-9 | cut -c1-3); [ -z "$W2" ] || [ "$W2" = 0 ] || EVERY=$W2 ;;
      *) trap - INT TERM HUP; cat "$T"; say "$LATER"; return 0 ;;
    esac
  done
  trap - INT TERM HUP; stopwait
  say "$NAME is installed, not signed in. $LATER"
}
# Tells the service this terminal stopped waiting: the link dies, and a page opened from it says so (ops row 580).
stopwait() {
  [ -z "$U" ] || printf '{"poll":"%s","stop":true}' "$K" | curl -sS -m 5 -o /dev/null -X POST -H 'content-type: application/json' --data-binary @- "$F/stage/poll" >/dev/null 2>&1 || :
}
main() {
  # -s sign-in: only the sign-in, for a computer that has the app (ops row 574); nothing is asked or installed.
  if [ "${1:-}" = sign-in ]; then
    T=$(mktemp 2>/dev/null || printf '%s' "/tmp/app-short.$$"); trap 'rm -f "$T"' EXIT
    U=''; signin later 900 900; return 0
  fi
  # The ask comes first: before any sign-in, browser, fetch or file, even this script's own temporary one.
  STOP=1
  if [ "${1:-}" = stage ]; then ask ' -s stage' ', at its stage version, once you sign in with GitHub' || return "$STOP"
  else ask '' '' || return "$STOP"; fi
  T=$(mktemp 2>/dev/null || printf '%s' "/tmp/app-short.$$")
  trap 'rm -f "$T" "$T.h"' EXIT
  # A rig's pass (ops row 600): with ~/.config/<app>/rig-token, the install is the pass's track (stage, or candidate for a
  # walk tester's, a tester's grant either way) with no tester check, and it goes on to the sign-in page as a friend's
  # would. The public line takes it too (annotate-team, 2026-10-17), so a walk pastes the very line a friend does and
  # sees nothing else. The token goes on stdin, never on a command line.
  RIG="${XDG_CONFIG_HOME:-$HOME/.config}/$WORD/rig-token"
  if [ "${1:-}" = stage ] || { [ -z "${1:-}" ] && [ -n "$WORD" ] && [ -s "$RIG" ]; }; then
    if [ -n "$WORD" ] && [ -s "$RIG" ]; then
      code=$(printf '{"token":"%s"}' "$(head -c 100 "$RIG" | tr -cd 'A-Za-z0-9_-')" | curl -sS -o "$T" -w '%{http_code}' -X POST -H 'content-type: application/json' --data-binary @- "$F/stage/rig") || { say "Couldn't reach $F (are you online?). Try again in a minute."; return 1; }
      if [ "$code" != 200 ]; then cat "$T"; return 1; fi
      CLAWED_YES=1 CLAWED_OPEN_LATER=1 sh "$T" </dev/null || { rc=$?; later; return $rc; }
      U=''; signin '' 300 30
      later
      return 0
    fi
    # CLAWED_OPEN names the opener (the tests' own); CLAUDE_APPS_NO_OPEN=1 opens nothing.
    opener; HAVE=''
    if [ -n "$B" ]; then web "$B"; rc=$?; [ "$rc" = 3 ] || return "$rc"; fi
    if [ -z "$HAVE" ]; then
      code=$(curl -sS -o "$T" -w '%{http_code}' -X POST "$F/stage/start") || { say "Couldn't reach $F (are you online?). Try again in a minute."; return 1; }
      if [ "$code" != 200 ]; then cat "$T"; return 1; fi
    fi
    POLL=$(sed -n 1p "$T"); USER_CODE=$(sed -n 2p "$T"); URI=$(sed -n 3p "$T"); EVERY=$(sed -n 4p "$T")
    say "To get $NAME's stage version, sign in with GitHub: open $URI and type $USER_CODE"
    say "Waiting for GitHub (checking every $EVERY s)…"
    # The service answers 202 with the seconds to wait on its second line (GitHub's interval, which its slow_down
    # raises); 30 minutes with no sign-in ends it. CLAWED_POLL_SECONDS is the tests' own pace.
    WAIT=$EVERY; T0=$(date +%s); STILL=''; OFF=0
    while :; do
      sleep "${CLAWED_POLL_SECONDS:-$WAIT}"
      code=$(curl -sS -o "$T" -w '%{http_code}' -X POST -H 'content-type: application/json' -d "{\"poll\":\"$POLL\"}" "$F/stage/poll" 2>/dev/null) || code=0
      case "$code" in
        200) signed; CLAWED_YES=1 sh "$T" </dev/null; return $? ;;
        202) OFF=0; W=$(sed -n 2p "$T" | tr -cd 0-9 | cut -c1-3); [ -n "$W" ] && WAIT=$W
          [ "$WAIT" -ge 1 ] 2>/dev/null || WAIT=1; [ "$WAIT" -le 60 ] || WAIT=60 ;;
        0|000) OFF=$((OFF + 1)); [ "$OFF" != 2 ] || say "Can't reach $F, trying again…" ;;
        *) cat "$T"; return 1 ;;
      esac
      S=$(($(date +%s) - T0))
      [ "$S" -lt 1800 ] || { say "We never heard back from GitHub. $NAME isn't installed, and nothing was changed. When you want it, paste this line into a terminal again."; return 1; }
      [ -n "$STILL" ] || [ "$S" -lt 60 ] || { STILL=1; say "Still waiting for GitHub. If GitHub already said you're connected, this finishes within a minute."; }
    done
  fi
  # A computer that has it already sends its install's id, so it gets its own grant again rather than a new one.
  # Read through a redirection, so no command names the app's folder (B29, B40).
  ID=$({ head -c 64 <"${CLAUDE_APPS_DATA:-$HOME/.claude-apps/$APP}/install_id"; } 2>/dev/null | tr -cd 'A-Za-z0-9-')
  # CLAWED_TEST=1 is our own test boxes' install (ops, 2026-10-15): its grant is a tester's, never counted as a person.
  TEST=''; [ "${CLAWED_TEST:-}" != 1 ] || TEST=',"test":true'
  code=$(curl -sS -o "$T" -w '%{http_code}' -X POST -H 'content-type: application/json' -d "{\"install_id\":\"$ID\"$TEST}" "$F/release") || { say "Couldn't reach $F (are you online?). Try again in a minute."; return 1; }
  if [ "$code" != 200 ]; then cat "$T"; return 1; fi
  # Sign-in goes first and alone (ops row 580): the install leaves the app closed (CLAWED_OPEN_LATER=1), so no app window
  # or dialog covers GitHub's page; the app opens once the sign-in ends, however it ends.
  CLAWED_YES=1 CLAWED_OPEN_LATER=1 sh "$T" </dev/null || { rc=$?; later; return $rc; }
  U=''; signin '' 300 30
  later
  return 0
}
# After the sign-in, however it ended: opens the app the install left closed for it (its launcher's own open, as
# /<app>:open runs it), once, then says the install's last line, which waited for this (ops row 580).
# B40 (story E1, after B29): Terminal.app titles its window with the foreground command and its arguments, and a Mac's
# public line showed the launcher's Node (~/.claude-apps/.node/bin/node …/session-start.mjs --open --front) there while
# the app opened. So the open runs in a session of its own (alone), its folders going in by the environment, never as an
# argument, and is waited for as before; the marks in the app's folder are read through a redirection and removed from
# inside it, so no command here names it.
later() {
  M=${CLAUDE_APPS_DATA:-$HOME/.claude-apps/$APP}; OPN=''
  if [ -f "$M/open-later" ]; then
    ( cd "$M" 2>/dev/null && rm -f open-later )
    O_CACHE=${CLAUDE_CODE_PLUGIN_CACHE_DIR:-${CLAUDE_CONFIG_DIR:-$HOME/.claude}/plugins/cache}; O_APP=$APP; export O_CACHE O_APP
    case "$(alone 'for d in $(ls -dt "$O_CACHE"/*/"$O_APP"/*/ 2>/dev/null); do if [ ! -f "$d/.orphaned_at" ] && [ -f "$d/hooks/run.sh" ]; then CLAWED_FRONT=1 CLAUDE_PLUGIN_ROOT="${d%/}" exec sh "${d%/}/hooks/run.sh" --open --front </dev/null 2>/dev/null; fi; done' 2>/dev/null)" in Opened*) OPN=' and open' ;; esac
  fi
  if [ -f "$M/last-line" ]; then say "$(sed "s/{open}/$OPN/" <"$M/last-line" | head -n 2)"; ( cd "$M" 2>/dev/null && rm -f last-line ); fi
}
# The sh script $1 in a session of its own (perl's setsid, on macOS and Linux alike, as B29's launch runs the installers'
# launcher), stdin at /dev/null; its output is this function's. It is waited for here, and a Ctrl-C here stops it too.
alone() {
  Q_SH=$1; export Q_SH
  if command -v perl >/dev/null 2>&1; then perl -e 'use POSIX; POSIX::setsid(); exec "sh", "-c", $ENV{Q_SH}' </dev/null &
  else sh -c "$Q_SH" </dev/null &
  fi
  Q_KID=$!
  trap 'kill -INT "-$Q_KID" 2>/dev/null || kill -INT "$Q_KID" 2>/dev/null; exit 130' INT TERM HUP
  wait "$Q_KID"; Q_CODE=$?
  trap - INT TERM HUP
  return "$Q_CODE"
}
main "$@"
