#!/usr/bin/env bash
# fibers plugin hook launcher — called by hooks/hooks.json with the hook
# event name as $1 and the Claude Code hook JSON on stdin.
#
# Fail-open by contract (ADR-0011): this script ALWAYS exits 0 — the service
# (or a missing runtime, or a missing file) can never break a session. Every
# degraded path leaves exactly one stderr line so losses are findable.
#
# Zero-config: the launcher resolves no URL. The CLI carries the whole chain
# (ADR-0065) — CONVEX_CLOUD_URL env > ~/.fibers/deployment.json > the
# built-in default baked into config.ts.
#
# Runtime (ADR-0110, amending ADR-0084): node + dist/fibers-node.mjs when
# node is on PATH, else the self-contained binary dist/bin/fibers-<target>
# (FIBERS_RUNTIME=binary prefers the binary, FIBERS_RUNTIME=node pins node).
# A released plugin names the binary for each platform in dist/binaries.txt;
# the hook only ever fetches it in the background, so a session never waits
# on a download. lib/runtime.sh holds the choice.
set -u
# extglob for the PreToolUse sieve below; it must be on before bash parses
# the compound command that uses it.
shopt -s extglob

PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
if [ ! -f "$PLUGIN_ROOT/lib/runtime.sh" ]; then
  echo "fibers hook: $PLUGIN_ROOT/lib/runtime.sh missing — presence disabled (reinstall the plugin; in the fibers repo: bun run --cwd packages/plugin build)" >&2
  exit 0
fi
# shellcheck source=../lib/runtime.sh
. "$PLUGIN_ROOT/lib/runtime.sh"

# Pick the runtime, or say why there is none and leave. With no runtime yet
# but a binary this plugin names, the fetch starts now and the next hook
# finds it. With node here, nothing is fetched unless FIBERS_RUNTIME=binary
# opts in; then a node-run session-start starts the fetch, once.
fibers_hook_runtime() {
  if fibers_resolve; then
    if [ "${FIBERS_CMD[0]}" = node ] && [ "${1:-}" = session-start ] &&
      [ "${FIBERS_RUNTIME:-}" = binary ]; then
      fibers_fetch_background
    fi
    return 0
  fi
  if fibers_can_fetch; then
    fibers_fetch_background
    echo "fibers hook: fetching the self-contained CLI for $FIBERS_TARGET in the background — presence starts with the next hook" >&2
  elif [ ! -f "$FIBERS_NODE_CLI" ]; then
    echo "fibers hook: CLI missing at $FIBERS_NODE_CLI — presence disabled (reinstall the plugin; in the fibers repo: bun run --cwd packages/plugin build)" >&2
  else
    echo "fibers hook: node not found on PATH and no self-contained CLI for this platform — presence disabled (install Node >= 20)" >&2
  fi
  return 1
}

# PreToolUse also matches Bash (W-21), and that fires on every shell call.
# Only a few cases need the CLI: the AskUserQuestion question, an
# Edit|Write|MultiEdit call (ENG-365 claim nudge), a Bash call
# that may bypass a git hook (ENG-211 friction: --no-verify, HUSKY=0, SKIP=,
# `commit -n` or a short-flag group holding n before any m: in `-am "msg"`
# everything after the m is the message, so only `-an`, `-nm` and the like
# can mean --no-verify), and a Bash call made INSIDE a
# sub-agent (agent_id on stdin) that runs a fibers command. Everything else
# returns here, without starting the CLI. The CLI re-checks precisely
# (detectBypass); this is only a cheap sieve and may let extra calls through.
#
# ENG-235 bake-in adds the rewrite shapes: `timeout `, ` -C `, `--cwd`, a
# literal `*` and a `/[…]` path part (unquoted globs, ENG-222: Next.js
# route folders like `app/[id]/route.ts` are character classes to zsh; a
# lone `?` is not sieved, it is in too many commit messages and URLs). The
# CLI decides precisely (friction/rewrite.ts); the sieve only pays for a
# CLI start when one may apply.
if [ "${1:-}" = "pre-tool-use" ]; then
  IN="$(cat)"
  case "$IN" in
    *AskUserQuestion*) ;;
    # ENG-365: the first-edit claim nudge. The CLI returns in one stat once
    # the session has been nudged; the sieve only has to let the three
    # editing tools reach it (both JSON spacings).
    *'"tool_name":"Edit"'*|*'"tool_name": "Edit"'*) ;;
    *'"tool_name":"Write"'*|*'"tool_name": "Write"'*) ;;
    *'"tool_name":"MultiEdit"'*|*'"tool_name": "MultiEdit"'*) ;;
    *--no-verify*|*HUSKY=0*|*SKIP=*|*'commit'*' -'*([a-ln-zA-Z])n*) ;;
    *'timeout '*|*' -C '*|*--cwd*|*\**|*'/['*']'*) ;;
    *'"agent_id"'*)
      case "$IN" in
        *fibers*) ;;
        *) exit 0 ;;
      esac
      ;;
    *) exit 0 ;;
  esac
  fibers_hook_runtime "$1" || exit 0
  printf '%s' "$IN" | "${FIBERS_CMD[@]}" hook "$@" || true
  exit 0
fi

# UserPromptSubmit fires on every prompt. The CLI records the person's
# message as a local update row on every install (ENG-578 slice 1) and, when
# this machine opted into the asides spool, an aside too (ENG-619), so the
# CLI is started for every prompt that is not an envelope (`<…`, a
# system-reminder, a pasted block, a task notification) or a slash command.
# The CLI re-checks that; this sieve only spares a process start.
if [ "${1:-}" = "user-prompt-submit" ]; then
  IN="$(cat)"
  case "$IN" in
    *'"prompt":"<'*|*'"prompt": "<'*|*'"prompt":"/'*|*'"prompt": "/'*) exit 0 ;;
  esac
  fibers_hook_runtime "$1" || exit 0
  printf '%s' "$IN" | "${FIBERS_CMD[@]}" hook "$@" || true
  exit 0
fi

# stdin (the hook JSON) passes through untouched. `|| true` guards the
# always-exit-0 contract even if the CLI itself blows up.
fibers_hook_runtime "${1:-}" || exit 0
"${FIBERS_CMD[@]}" hook "$@" || true
exit 0
