← Files NightshiftARCHIVED FILE

runtime/start-watchman.sh

4.5 KB · Oct 3, 2026 · 06:31 UTC

↓ Download file

#!/usr/bin/env bash
# start-watchman.sh — launch this host's watchman in the background and confirm it armed.
#
#   start-watchman.sh --project DIR --host claude|codex|cursor [-- WATCHMAN-ARGS…]
#
# The watchman is given the workspace explicitly, never the working directory, and runs in a
# session of its own so the host's process cleanup cannot end it. Its own output is appended to
# run/watchman.log. Success means the watchman's pid file names the launched process
# and the shift log gained its `armed` line; anything short of that is reported with the
# watchman's own words, and a launched process that did not arm is stopped.
#
# Exit: 0 armed, or a live watchman already watches the site · 1 usage/resolve · 2 did not arm
set -u

_here="${BASH_SOURCE[0]%/*}"; [ "$_here" != "${BASH_SOURCE[0]}" ] || _here=.
# shellcheck source=plugins/nightshift/lib/lib.sh
. "$_here/../lib/lib.sh"

PROJECT=""
HOST_NAME=""
while [ $# -gt 0 ]; do
  case "$1" in
    --project)
      [ $# -ge 2 ] || { printf 'start-watchman: --project needs a value\n' >&2; exit 1; }
      PROJECT="$2"
      shift 2
      ;;
    --host)
      [ $# -ge 2 ] || { printf 'start-watchman: --host needs a value\n' >&2; exit 1; }
      HOST_NAME="$2"
      shift 2
      ;;
    --)
      shift
      break
      ;;
    -h | --help)
      awk 'NR == 1 { next } !/^#/ { exit } { sub(/^# ?/, ""); print }' "$0"
      exit 1
      ;;
    *) printf 'start-watchman: unknown argument: %s\n' "$1" >&2; exit 1 ;;
  esac
done
[ -n "$PROJECT" ] || { printf 'start-watchman: --project is required\n' >&2; exit 1; }
case "$HOST_NAME" in
  claude | codex | cursor) ;;
  *) printf 'start-watchman: --host must be claude, codex or cursor\n' >&2; exit 1 ;;
esac

WORKSPACE="$(ns_workspace_root "$(ns_state_dir_owner "$PROJECT")" 2>/dev/null)" || {
  printf 'start-watchman: invalid .nightshift-link at %s\n' "$PROJECT" >&2
  exit 1
}
NS="$WORKSPACE/.nightshift"
[ -d "$NS" ] || { printf 'start-watchman: no .nightshift at %s\n' "$WORKSPACE" >&2; exit 1; }
WATCHMAN="$_here/$HOST_NAME/watchman.sh"
[ -f "$WATCHMAN" ] || { printf 'start-watchman: no %s watchman in this plugin\n' "$HOST_NAME" >&2; exit 1; }

declare PIDFILE LOG OUTPUT
ns_layout_set PIDFILE "$NS" watchman
ns_layout_set LOG "$NS" shift-log
ns_layout_set OUTPUT "$NS" watchman-log

pidfile_pid() {
  [ -f "$PIDFILE" ] && [ ! -L "$PIDFILE" ] || return 1
  sed -n 1p "$PIDFILE" 2>/dev/null | tr -d '[:space:]'
}

# One watchman per site. A live one is already doing this job, and a second would refuse anyway.
held="$(pidfile_pid)" || held=""
case "$held" in
  '' | *[!0-9]*) ;;
  *)
    if kill -0 "$held" 2>/dev/null; then
      printf 'watchman already watching (pid %s)\n' "$held"
      exit 0
    fi
    ;;
esac

mkdir -p "${OUTPUT%/*}" "${LOG%/*}" 2>/dev/null
log_from=0
[ -f "$LOG" ] && log_from="$(wc -c <"$LOG" | tr -d '[:space:]')"
printf '%s · start-watchman: launching the %s watchman for %s\n' \
  "$(date '+%Y-%m-%d %H:%M:%S')" "$HOST_NAME" "$WORKSPACE" >>"$OUTPUT"
output_from="$(wc -l <"$OUTPUT" | tr -d '[:space:]')"

# The watchman runs in a session of its own. A host that tears down the process group of the
# session that started it (Codex does, when its app restarts or reloads) must not take the
# watchman with it; nohup alone only survives a hangup. setsid where the system has it, perl's
# setsid on macOS, which ships perl and no setsid; both exec, so the pid stays the watchman's.
if command -v setsid >/dev/null 2>&1; then
  set -- setsid "$WATCHMAN" --project "$WORKSPACE" "$@"
elif command -v perl >/dev/null 2>&1; then
  set -- perl -MPOSIX -e 'POSIX::setsid(); exec @ARGV or die "exec: $!\n"' "$WATCHMAN" --project "$WORKSPACE" "$@"
else
  set -- "$WATCHMAN" --project "$WORKSPACE" "$@"
fi
nohup "$@" >>"$OUTPUT" 2>&1 </dev/null &
child=$!

# Everything the watchman said since this launch, for a refusal to quote.
did_not_arm() {
  printf 'start-watchman: %s\n' "$1" >&2
  sed -n "$((output_from + 1)),\$p" "$OUTPUT" >&2
  printf 'start-watchman: the full output is in %s\n' "$OUTPUT" >&2
  exit 2
}

armed_line() {
  tail -c "+$((log_from + 1))" "$LOG" 2>/dev/null | grep -qE 'watchman( \([a-z]+\))? armed'
}

attempt=0
while [ "$attempt" -lt 150 ]; do
  if [ "$(pidfile_pid)" = "$child" ] && armed_line; then
    printf 'watchman started (pid %s)\n' "$child"
    exit 0
  fi
  if ! kill -0 "$child" 2>/dev/null; then
    wait "$child"
    did_not_arm "the watchman exited before it armed (status $?)"
  fi
  sleep 0.1
  attempt=$((attempt + 1))
done
kill "$child" 2>/dev/null
did_not_arm "the watchman did not arm within 15 seconds and was stopped"

SHA-256: 01d0023ed86299e4d33f3ee47ab822619e38b188706b191002988dd071333ee5