#!/usr/bin/env bash
#
# screenless installer.
#
#   curl -fsSL https://screenless.sh/install | bash
#
# Drops the CLI in ~/.screenless, puts a launcher on PATH, and — when there is
# a terminal to talk to — goes straight into `screenless setup`, which verifies
# your phone number and starts the trial. One command, from nothing to a
# working install.
#
# Deliberately does not install Node. A tool that silently mutates your
# toolchain is a tool you cannot reason about later; if Node is missing we say
# so and stop.

set -euo pipefail

readonly BASE_URL="${SCREENLESS_BASE_URL:-https://screenless.sh}"
readonly INSTALL_DIR="${SCREENLESS_INSTALL_DIR:-$HOME/.screenless}"
readonly LIB_DIR="$INSTALL_DIR/lib"
readonly BIN_DIR="$INSTALL_DIR/bin"
readonly SKILL_DIR="${SCREENLESS_SKILL_DIR:-$HOME/.claude/skills/screenless}"
readonly AGENT_LABEL="com.screenless.nightly"
readonly AGENT_PLIST="$HOME/Library/LaunchAgents/$AGENT_LABEL.plist"
readonly MIN_NODE=20

# Colour only when a terminal is actually watching. Piped into a log, the
# escape codes are noise.
if [ -t 1 ]; then
  bold=$'\033[1m'; dim=$'\033[2m'; red=$'\033[31m'; green=$'\033[32m'; reset=$'\033[0m'
else
  bold=''; dim=''; red=''; green=''; reset=''
fi

say()  { printf '%s\n' "$*"; }
ok()   { printf '%s✓%s %s\n' "$green" "$reset" "$*"; }
die()  { printf '%s✗%s %s\n' "$red" "$reset" "$*" >&2; exit 1; }

# ---------------------------------------------------------------- preflight

case "$(uname -s)" in
  Darwin|Linux) ;;
  *) die "unsupported platform: $(uname -s). On Windows, use WSL." ;;
esac

command -v node >/dev/null 2>&1 || die "Node ${MIN_NODE}+ is required but not installed.
  macOS:  brew install node
  Linux:  https://nodejs.org/en/download
  Then run this again."

node_major="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)"
if [ "$node_major" -lt "$MIN_NODE" ]; then
  die "Node ${MIN_NODE}+ is required, found $(node -v). Upgrade Node and run this again."
fi

command -v tar >/dev/null 2>&1 || die "tar is required but not installed."

# Not fatal. The CLI is useful on its own — it places calls and parks editions —
# but the loop that writes them is a Claude Code skill, so say plainly what will
# not work rather than installing a skill nothing can run.
have_claude=1
command -v claude >/dev/null 2>&1 || have_claude=0

if command -v curl >/dev/null 2>&1; then
  fetch() { curl -fsSL "$1" -o "$2"; }
elif command -v wget >/dev/null 2>&1; then
  fetch() { wget -qO "$2" "$1"; }
else
  die "curl or wget is required."
fi

# ------------------------------------------------------------------ install

say ""
say "${bold}screenless${reset} ${dim}— review your agents' overnight work by phone${reset}"
say ""

tmp="$(mktemp -d)"
# Runs on failure too, so a half-downloaded tarball never survives the script.
trap 'rm -rf "$tmp"' EXIT

fetch "$BASE_URL/screenless.tar.gz" "$tmp/screenless.tar.gz" \
  || die "could not download the CLI from $BASE_URL"

# Best-effort: an older server without the skill bundle should still install a
# working CLI rather than fail the whole run.
mkdir -p "$tmp/skill"
fetch "$BASE_URL/SKILL.md" "$tmp/skill/SKILL.md" 2>/dev/null || rm -f "$tmp/skill/SKILL.md"
fetch "$BASE_URL/nightly.sh" "$tmp/nightly.sh" 2>/dev/null || rm -f "$tmp/nightly.sh"

# Only lib/ is replaced. The config file next to it holds a session token and
# a verified phone number, and reinstalling is not a reason to lose either.
rm -rf "$LIB_DIR"
mkdir -p "$LIB_DIR" "$BIN_DIR"
tar -xzf "$tmp/screenless.tar.gz" -C "$LIB_DIR" || die "the download was corrupt — try again"
[ -f "$LIB_DIR/index.js" ] || die "the download was incomplete — try again"

# A launcher rather than a symlink into lib/: this way `screenless` works even
# where the shebang's `env node` resolves differently, and the path it execs is
# fixed at install time.
cat > "$BIN_DIR/screenless" <<EOF
#!/bin/sh
exec node "$LIB_DIR/index.js" "\$@"
EOF
chmod +x "$BIN_DIR/screenless"

ok "installed to ${dim}$INSTALL_DIR${reset}"

# ------------------------------------------------------------------- skill

# Installed globally rather than per-repo: the instructions are the same for
# every project, and the per-project settings live in each repo's own
# .screenless.json, written by `screenless init`.
if [ -f "$tmp/skill/SKILL.md" ]; then
  rm -rf "$SKILL_DIR"
  mkdir -p "$SKILL_DIR"
  cp "$tmp/skill/SKILL.md" "$SKILL_DIR/SKILL.md"
  if [ "$have_claude" -eq 1 ]; then
    ok "nightly loop skill installed for Claude Code"
  else
    say "  ${dim}nightly loop skill installed, but Claude Code was not found.${reset}"
    say "  ${dim}The paper and the call brief are written by it: https://claude.com/claude-code${reset}"
  fi
fi

# ------------------------------------------------------------ nightly job

# The runner is installed next to the CLI, not run from a checkout: the launchd
# job must survive the repo being moved, renamed or deleted, and a scheduled
# task that depends on someone's Desktop layout is a task that silently stops.
if [ -f "$tmp/nightly.sh" ]; then
  cp "$tmp/nightly.sh" "$BIN_DIR/nightly.sh"
  chmod +x "$BIN_DIR/nightly.sh"
fi

install_agent() {
  [ "$(uname -s)" = "Darwin" ] || return 1
  [ -f "$BIN_DIR/nightly.sh" ] || return 1
  command -v launchctl >/dev/null 2>&1 || return 1

  mkdir -p "$HOME/Library/LaunchAgents" "$INSTALL_DIR/logs"

  # StartCalendarInterval, not an interval timer: launchd re-runs a calendar job
  # it missed because the machine was asleep, as soon as it wakes. That is the
  # "03:00, or first thing when the laptop opens" behaviour, for free.
  #
  # PATH is spelled out because a launchd job inherits none of a login shell.
  cat > "$AGENT_PLIST" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>$AGENT_LABEL</string>
  <key>ProgramArguments</key><array><string>$BIN_DIR/nightly.sh</string></array>
  <key>StartCalendarInterval</key><dict>
    <key>Hour</key><integer>3</integer>
    <key>Minute</key><integer>0</integer>
  </dict>
  <key>RunAtLoad</key><false/>
  <key>EnvironmentVariables</key><dict>
    <key>PATH</key><string>$BIN_DIR:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$HOME/.local/bin</string>
  </dict>
  <key>StandardOutPath</key><string>$INSTALL_DIR/logs/launchd.out.log</string>
  <key>StandardErrorPath</key><string>$INSTALL_DIR/logs/launchd.err.log</string>
</dict>
</plist>
PLIST

  # Reload rather than load, so reinstalling picks up a changed path.
  launchctl unload "$AGENT_PLIST" >/dev/null 2>&1 || true
  launchctl load "$AGENT_PLIST" >/dev/null 2>&1 || return 1
  return 0
}

if [ "${SCREENLESS_NO_AGENT:-}" = "1" ]; then
  say "  ${dim}skipping the nightly job (SCREENLESS_NO_AGENT=1)${reset}"
elif install_agent; then
  ok "nightly run scheduled for 03:00 ${dim}(and on wake, if the laptop was shut)${reset}"
elif [ "$(uname -s)" = "Darwin" ]; then
  say "  ${dim}could not schedule the nightly run — see loop/README.md${reset}"
else
  say "  ${dim}on Linux, schedule $BIN_DIR/nightly.sh with a systemd timer (Persistent=true)${reset}"
fi

# --------------------------------------------------------------------- PATH

on_path=0
case ":$PATH:" in *":$BIN_DIR:"*) on_path=1 ;; esac

if [ "$on_path" -eq 0 ]; then
  # Written to the rc file for future shells; the current one is handled by the
  # explicit export below, since a sourced rc cannot reach back into it.
  line="export PATH=\"$BIN_DIR:\$PATH\""
  case "${SHELL##*/}" in
    zsh)  rc="$HOME/.zshrc" ;;
    bash) if [ "$(uname -s)" = "Darwin" ]; then rc="$HOME/.bash_profile"; else rc="$HOME/.bashrc"; fi ;;
    fish) rc="$HOME/.config/fish/config.fish"; line="fish_add_path $BIN_DIR" ;;
    *)    rc="" ;;
  esac

  if [ -n "$rc" ]; then
    mkdir -p "$(dirname "$rc")"
    if ! grep -qF "$BIN_DIR" "$rc" 2>/dev/null; then
      printf '\n# screenless\n%s\n' "$line" >> "$rc"
      ok "added to PATH in ${dim}$rc${reset}"
    fi
  else
    say "  ${dim}add this to your shell profile:${reset} export PATH=\"$BIN_DIR:\$PATH\""
  fi
  export PATH="$BIN_DIR:$PATH"
fi

# -------------------------------------------------------------------- setup

# Piped through bash, stdin is the script itself, so setup's prompts have to
# read from the terminal directly. When there isn't one — CI, a Dockerfile,
# someone's provisioning script — we stop here and say what comes next.
if [ "${SCREENLESS_NO_SETUP:-}" = "1" ] || [ ! -t 1 ] || [ ! -r /dev/tty ]; then
  say ""
  say "Next: ${bold}screenless setup${reset} ${dim}— verify your number${reset}"
  say "Then: ${bold}screenless init${reset} ${dim}— point the nightly loop at a repo${reset}"
  say ""
  exit 0
fi

say ""
say "${bold}Setting up.${reset} ${dim}A code by SMS, then a card for the 7-day free trial.${reset}"
say ""

exec "$BIN_DIR/screenless" setup < /dev/tty
