# 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