#!/usr/bin/env bash
# Entry point Claude Code and Codex call from `hooks/hooks.json`.
#
# Resilience contract (see docs/updating.md):
#   1. FAIL-SOFT. This wrapper must NEVER exit 2 (the only hook exit code
#      that blocks the agent in Claude Code) and never abort the runtime.
#      Any internal problem -> warn on stderr and exit 0 (no-op).
#   2. VERSION-CURRENT RESOLUTION. The plugin checkout root is resolved in
#      priority order, so a stale ~/.local/bin symlink can never strand the
#      hook on an old plugin version:
#        a. $CLAUDE_PLUGIN_ROOT   (Claude Code; always the active version)
#        b. this wrapper's own realpath dir/.. (Codex / stable dir install)
#        c. $OSB_PLUGIN_ROOT      (explicit override)
#      First candidate that actually contains hooks/<name>.ts wins.
#
# Because Claude Code reads hooks.json fresh from the active version and
# substitutes $CLAUDE_PLUGIN_ROOT, shipping (1)+(2) means the NEXT update
# repairs an already-broken install with no user action.
#
# Usage: o2b-hook <hook-basename> [args...]

# NOTE: deliberately no `-e`. We handle every failure explicitly so we can
# always exit 0; `set -e` could abort before our soft-exit runs.
set -uo pipefail

warn() { printf 'o2b-hook: %s\n' "$1" >&2; }

HOOK_NAME=${1:-}
if [[ -z $HOOK_NAME ]]; then
  warn "missing hook name; skipping"
  exit 0
fi
shift

# Resolve the checkout root that actually contains this hook.
resolve_root() {
  local cand
  # a. Claude Code's active plugin dir.
  if [[ -n ${CLAUDE_PLUGIN_ROOT:-} && -f "${CLAUDE_PLUGIN_ROOT}/hooks/${HOOK_NAME}.ts" ]]; then
    printf '%s\n' "$CLAUDE_PLUGIN_ROOT"
    return 0
  fi
  # b. This wrapper's own location (follows the symlink to the real checkout).
  local self
  if self=$(realpath -- "${BASH_SOURCE[0]}" 2>/dev/null); then
    cand=$(cd -- "$(dirname -- "$self")/.." 2>/dev/null && pwd || true)
    if [[ -n $cand && -f "${cand}/hooks/${HOOK_NAME}.ts" ]]; then
      printf '%s\n' "$cand"
      return 0
    fi
  fi
  # c. Explicit override.
  if [[ -n ${OSB_PLUGIN_ROOT:-} && -f "${OSB_PLUGIN_ROOT}/hooks/${HOOK_NAME}.ts" ]]; then
    printf '%s\n' "$OSB_PLUGIN_ROOT"
    return 0
  fi
  return 1
}

ROOT=$(resolve_root) || {
  warn "could not locate hooks/${HOOK_NAME}.ts (plugin root unresolved); skipping"
  exit 0
}

if ! command -v bun >/dev/null 2>&1; then
  warn "bun not on PATH; skipping ${HOOK_NAME}"
  exit 0
fi

# Do NOT exec: capture the hook's exit so a hook that itself exits non-zero
# (e.g. 2) can never bubble out and block the agent. Stdout still passes
# through (hooks that emit JSON decisions on stdout keep working). The
# wrapper always exits 0 - the fail-soft contract.
bun run "${ROOT}/hooks/${HOOK_NAME}.ts" "$@"
status=$?
if [[ $status -ne 0 ]]; then
  warn "hook ${HOOK_NAME} exited ${status}; suppressed to keep the runtime unblocked"
fi
exit 0
