# Local dev stack for pds.js.
#
# `just dev` brings up the docker infra (local PLC, relay, Caddy), runs the
# example PDS on the host, registers a mock user in the local PLC, and seeds a
# few records. Then sign in at http://localhost:2471/account with the printed
# credentials. `just dev-down` tears it all back down.
#
# `just dev-git` is the repository browser instead: a PDS on localhost with a
# repository, a package and an image already linked, and Vite serving
# @pdsjs/git-ui against it at http://localhost:5173.
#
# `just dev-spaces` is the space browser: two PDSes writing to one space, with
# @pdsjs/spaces-ui at http://localhost:5174.
#
# That account answers on localhost because the page resolves the DID document
# and then reads the endpoint it names. A localhost endpoint is out of the
# docker relay's reach, and this stack has no use for the relay, so its infra
# is the local PLC alone. `VITE_PLC_URL` is what sends the page to that PLC
# rather than the public directory.
#
# `just git-host-for` is the other direction: no local PDS at all. It points
# git-host at some real account's PDS over the public XRPC surface and opens
# the repository browser for that account.

set shell := ["bash", "-uc"]

# Host port the PDS listens on. Caddy proxies :3443 -> :2471; keep in sync with
# test/helpers/test-port.js and docker/Caddyfile.
port := "2471"
data := ".dev-pds"
pidfile := ".dev-pds/pds.pid"
log := ".dev-pds/pds.log"
base := "http://localhost:2471"
handle := "alice"
bob_port := "2472"
carol_port := "2473"
# 2474 to 2481 belong to the test suite, which binds them on the same machine.
nova_port := "2482"
dan_port := "2483"
plc := "http://localhost:2582"
ui_port := "5173"
spaces_ui_port := "5174"
gh_port := "8057"
gh_dir := ".dev-git-host"

# List available recipes.
default:
    @just --list

# One-shot: infra + PDS + mock user, ready to sign in.
dev: build-ui stack-up pds-start seed
    @echo ""
    @echo "Ready. Open {{base}}/account and sign in with the credentials above."
    @echo "This serves a prebuilt UI (no hot reload). For live UI work: just dev-live"

# Live-reload dev: infra + PDS (backend --watch) + Vite HMR for the account UI.
dev-live: stack-up pds-start seed
    @echo ""
    @echo "PDS on {{base}} (backend hot-restarts). Starting Vite HMR for the UI…"
    @echo "Open http://localhost:5173/account and sign in with the credentials above."
    npm run dev --workspace=@pdsjs/account-ui

# Two-account dev for permissioned data and multi-writer git: alice's stack
# plus bob on his own PDS (:{{bob_port}}), both registered in the local PLC
# with host-reachable endpoints so the two servers can notify each other. The
# tradeoff: the docker relay cannot crawl endpoints on localhost, so firehose
# consumers in the compose stack see nothing from these accounts.
dev-multi: stack-up pds-start
    PDS_DEV_PDS_URL={{base}} node scripts/dev-seed.mjs {{handle}}
    just bob-start
    @echo ""
    @echo "alice on {{base}}, bob on http://localhost:{{bob_port}} (DID in {{data}}/bob/credentials-bob.json)."
    @echo "Both use the password test-password. Starting Vite HMR for the UI…"
    npm run dev --workspace=@pdsjs/account-ui

# The repository browser with a repository, a package, an image and a history
# of check runs to read.
dev-git: plc-up (pds-start "localhost:2471") (seed base) git-demo git-checks
    @echo ""
    @echo "Open http://localhost:{{ui_port}} — the repository is demo-app."
    @echo "The account page is {{base}}/account (handle {{handle}}.localhost, password test-password)."
    PDS_URL={{base}} VITE_PLC_URL={{plc}} npm run dev --workspace=@pdsjs/git-ui -- --port {{ui_port}} --strictPort

# The space browser with a space to read: alice hosts it, bob writes into it
# from his own PDS, and the page reaches bob's repo the way the proposal says
# to — a delegation token from alice's server, a credential from the authority,
# then a read addressed straight at bob's.
dev-spaces: plc-up (pds-start "localhost:2471") (seed base) bob-start spaces-demo
    @echo ""
    @echo "Open http://localhost:{{spaces_ui_port}} and sign in with alice's DID above."
    @echo "A .localhost handle resolves nowhere, so the DID is what this stack signs in with."
    PDS_URL={{base}} VITE_PLC_URL={{plc}} npm run dev --workspace=@pdsjs/spaces-ui -- --port {{spaces_ui_port}} --strictPort

# The repository browser with one repository worked on from five accounts, so
# its pull request list has open branches, a rebase with two versions, a
# conflict, reviews and a fork to read. Five servers, because a PDS answers for
# a single DID.
dev-git-collab: plc-up (pds-start "localhost:2471") (seed base) (member-start "bob" bob_port) (member-start "carol" carol_port) (member-start "nova" nova_port) (member-start "dan" dan_port) git-collab-demo
    @echo ""
    @echo "Open http://localhost:{{ui_port}}/demo-app/pulls"
    PDS_URL={{base}} VITE_PLC_URL={{plc}} npm run dev --workspace=@pdsjs/git-ui -- --port {{ui_port}} --strictPort

# Serve one real account's atproto git repositories through git-host: resolve
# the handle's PDS from the directory, build the browser app, and open the
# forge. `git-host-stop` tears it down. The forge reads the account's PDS over
# the public XRPC surface, so any account with dev.pdsjs.git.* records works.
git-host-for handle="grain.social":
    #!/usr/bin/env bash
    set -euo pipefail
    npm run build --workspace=@pdsjs/git-ui
    resolved="$(curl -sS -w '\n%{http_code}' "https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle={{handle}}")"
    code="${resolved##*$'\n'}"
    body="${resolved%$'\n'*}"
    if [[ "$code" != "200" ]] || [[ -z "$body" ]]; then
      echo "could not resolve {{handle}}: $body"; exit 1
    fi
    did="$(node -pe "JSON.parse(process.argv[1]).did" "$body")"
    if [[ "$did" == did:plc:* ]]; then
      doc="$(curl -fsS "https://plc.directory/$did/data")"
      pds="$(node -pe "JSON.parse(process.argv[1]).services.atproto_pds.endpoint" "$doc")"
    else
      host="${did#did:web:}"; host="${host//:/\/}"
      doc="$(curl -fsS "https://$host/.well-known/did.json")"
      pds="$(node -pe "JSON.parse(process.argv[1]).service.find(s => s.id === 'atproto_pds').serviceEndpoint" "$doc")"
    fi
    echo "{{handle}} -> $did @ $pds"
    mkdir -p {{gh_dir}}
    lsof -ti tcp:{{gh_port}} | xargs kill 2>/dev/null || true
    rm -f {{gh_dir}}/forge.sqlite
    nohup node packages/git-host/src/cli.js \
      --pds "$pds" \
      --account "$did" \
      --db {{gh_dir}}/forge.sqlite \
      --port {{gh_port}} \
      --ui packages/git-ui/dist \
      --no-watch \
      > {{gh_dir}}/forge.log 2>&1 &
    echo $! > {{gh_dir}}/forge.pid
    disown || true
    echo -n "Waiting for {{handle}}'s forge on :{{gh_port}}"
    for _ in $(seq 1 30); do
      if curl -fsS "http://localhost:{{gh_port}}/xrpc/dev.pdsjs.git.listRepos" >/dev/null 2>&1; then echo " ready"; break; fi
      echo -n "."; sleep 1
    done
    open "http://localhost:{{gh_port}}/"

# Stop the forge started by git-host-for.
git-host-stop:
    [ -f {{gh_dir}}/forge.pid ] && kill "$(cat {{gh_dir}}/forge.pid)" 2>/dev/null || true
    lsof -ti tcp:{{gh_port}} | xargs kill 2>/dev/null || true
    rm -f {{gh_dir}}/forge.pid

# One repository, four accounts, and the records the pull request list reads.
git-collab-demo:
    PDS_DEV_URL={{base}} PDS_DEV_PLC_URL={{plc}} node scripts/dev-git-collab-demo.mjs

# The same four accounts, working on this repository rather than a toy one:
# changes of tens of files over a dozen commits, which is the size an agent
# writes. Run it after dev-git-collab, against the servers that are already up.
git-scale-demo:
    PDS_DEV_URL={{base}} PDS_DEV_PLC_URL={{plc}} node scripts/dev-git-scale-demo.mjs

# A space, a second member on another PDS, and records from both.
spaces-demo:
    node scripts/dev-spaces-demo.mjs

# A repository, an npm package, an image, and the record that links them.
git-demo:
    PDS_DEV_URL={{base}} PDS_DEV_PLC_URL={{plc}} node scripts/dev-git-demo.mjs

# A few more commits, and the check records a runner would publish about them.
git-checks:
    PDS_DEV_URL={{base}} PDS_DEV_PLC_URL={{plc}} node scripts/dev-git-checks.mjs

# Start the local PLC alone, for a stack that needs no relay.
plc-up:
    #!/usr/bin/env bash
    set -euo pipefail
    docker compose up -d plc
    echo -n "Waiting for local PLC"
    for _ in $(seq 1 30); do
      if curl -fsS {{plc}}/_health >/dev/null 2>&1; then echo " ready"; exit 0; fi
      echo -n "."; sleep 1
    done
    echo " timed out"; exit 1

# Start bob's PDS in the background and register him in the local PLC.
bob-start: (member-start "bob" bob_port)

# Start one more account's PDS in the background, on its own port, and
# register it in the local PLC. Each account is one server: pds.js answers for
# a single DID, so a second contributor means a second process.
member-start name port:
    #!/usr/bin/env bash
    set -euo pipefail
    lsof -ti tcp:{{port}} | xargs kill 2>/dev/null || true
    rm -rf {{data}}/{{name}}
    mkdir -p {{data}}/{{name}}/blobs
    PORT={{port}} \
    HOSTNAME=localhost:{{port}} \
    JWT_SECRET=dev-secret-{{name}} \
    PDS_PASSWORD=test-password \
    PDS_ENABLE_SPACES=true \
    PDS_EXPERIMENTAL_GIT_HTTP=true \
    PLC_URL=http://localhost:2582 \
    RELAY_URL=http://localhost:2470 \
    APPVIEW_URL=https://api.bsky.app \
    APPVIEW_DID=did:web:api.bsky.app \
    PDS_DB_PATH={{data}}/{{name}}/pds.db \
    PDS_BLOBS_DIR={{data}}/{{name}}/blobs \
    nohup node --watch examples/node/index.js > {{data}}/{{name}}/pds.log 2>&1 &
    echo $! > {{data}}/{{name}}/pds.pid
    disown || true
    echo -n "Waiting for {{name}}'s PDS on :{{port}}"
    for _ in $(seq 1 30); do
      if curl -fsS http://localhost:{{port}}/xrpc/_health >/dev/null 2>&1; then echo " ready"; break; fi
      echo -n "."; sleep 1
    done
    (cd {{data}}/{{name}} && PDS_PASSWORD=test-password node ../../scripts/setup.js --pds http://localhost:{{port}} --plc-url http://localhost:2582 --handle {{name}} >/dev/null)
    node -e "console.log('{{name}}:', JSON.parse(require('fs').readFileSync('{{data}}/{{name}}/credentials-{{name}}.json')).did)"

# Build the account UI (the PDS serves its prebuilt assets).
build-ui:
    npm run build:ui

# Start the docker infra with clean volumes and wait for the local PLC.
stack-up:
    #!/usr/bin/env bash
    set -euo pipefail
    docker compose down -v >/dev/null 2>&1 || true
    docker compose up -d
    echo -n "Waiting for local PLC"
    for _ in $(seq 1 30); do
      if curl -fsS http://localhost:2582/_health >/dev/null 2>&1; then echo " ready"; exit 0; fi
      echo -n "."; sleep 1
    done
    echo " timed out"; exit 1

# Start the example PDS in the background (host port {{port}}), on the hostname the account answers to.
pds-start hostname="host.docker.internal:3443":
    #!/usr/bin/env bash
    set -euo pipefail
    lsof -ti tcp:{{port}} | xargs kill 2>/dev/null || true
    rm -rf {{data}}
    mkdir -p {{data}}/blobs
    PORT={{port}} \
    HOSTNAME={{hostname}} \
    JWT_SECRET=dev-secret \
    PDS_PASSWORD=test-password \
    PDS_ENABLE_SPACES=true \
    PDS_EXPERIMENTAL_GIT_HTTP=true \
    PDS_EXPERIMENTAL_OCI=true \
    PDS_EXPERIMENTAL_NPM=true \
    PLC_URL={{plc}} \
    RELAY_URL=http://localhost:2470 \
    APPVIEW_URL=https://api.bsky.app \
    APPVIEW_DID=did:web:api.bsky.app \
    PDS_DB_PATH={{data}}/pds.db \
    PDS_BLOBS_DIR={{data}}/blobs \
    nohup node --watch examples/node/index.js > {{log}} 2>&1 &
    echo $! > {{pidfile}}
    disown || true
    echo -n "Waiting for PDS on :{{port}}"
    for _ in $(seq 1 30); do
      if curl -fsS {{base}}/xrpc/_health >/dev/null 2>&1; then echo " ready"; exit 0; fi
      echo -n "."; sleep 1
    done
    echo " timed out — see {{log}}"; exit 1

# Register a mock user in the local PLC and seed records, at the PDS URL the PLC records.
seed pds_url="https://host.docker.internal:3443":
    PDS_DEV_PDS_URL={{pds_url}} node scripts/dev-seed.mjs {{handle}}

# Tail the PDS log.
logs:
    tail -f {{log}}

# Stop the PDS and every member started beside it (leaves docker infra up).
#
# Each server runs under `node --watch`, so killing the child alone leaves a
# supervisor that restarts it on the next file change, on a port the test
# suite wants. The pid files name the supervisors.
pds-stop:
    #!/usr/bin/env bash
    [ -f {{pidfile}} ] && kill "$(cat {{pidfile}})" 2>/dev/null || true
    lsof -ti tcp:{{port}} | xargs kill 2>/dev/null || true
    rm -f {{pidfile}}
    for member in {{data}}/*/pds.pid; do
      [ -f "$member" ] || continue
      pid="$(cat "$member")"
      pkill -P "$pid" 2>/dev/null || true
      kill "$pid" 2>/dev/null || true
      rm -f "$member"
    done

# Stop the PDS and tear down the docker infra (with volumes).
dev-down: pds-stop
    docker compose down -v
