Monorepo for Tangled
tangled.org
1#!/bin/sh
2# dev bootstrap:
3# - create accounts (alice, bob)
4# - write OWNER_DID to /shared/owner-did (for knot/spindle)
5# - create system label definitions under SYSTEM_DID
6set -eu
7
8: "${PDS_URL:?PDS_URL must be set}"
9PASSWORD="password"
10
11USERS="alice bob"
12OWNER_USER="${OWNER_USER:-alice}"
13SYSTEM_USER="${SYSTEM_USER:-alice}"
14SHARED_DIR="${SHARED_DIR:-/shared}"
15
16# --- helpers ---
17
18# resolve_handle HANDLE → DID on stdout
19resolve_handle() {
20 resp=$(curl -sS -w '\n%{http_code}' \
21 "${PDS_URL}/xrpc/com.atproto.identity.resolveHandle?handle=$1")
22 body=$(printf '%s\n' "$resp" | sed '$d')
23 status=$(printf '%s\n' "$resp" | tail -n1)
24 case "$status" in
25 200) printf '%s\n' "$body" | jq -er '.did' ;;
26 400) : ;; # not found — expected
27 *) printf 'resolveHandle %s: HTTP %s: %s\n' "$1" "$status" "$body" >&2; return 1 ;;
28 esac
29}
30
31# ensure_account USERNAME → DID on stdout. Creates account if missing.
32ensure_account() {
33 username="$1"
34 handle="${username}.${PDS_HOSTNAME}"
35 email="${username}@${PDS_HOSTNAME}"
36
37 did=$(resolve_handle "$handle")
38 if [ -n "$did" ]; then
39 printf '[skip] %s = %s\n' "$handle" "$did" >&2
40 printf '%s\n' "$did"
41 return 0
42 fi
43
44 invite=$(curl -fsS -u "admin:${PDS_ADMIN_PASSWORD}" \
45 -H "Content-Type: application/json" \
46 -d '{"useCount":1}' \
47 "${PDS_URL}/xrpc/com.atproto.server.createInviteCode" | jq -er '.code')
48
49 result=$(curl -fsS \
50 -H "Content-Type: application/json" \
51 -d "{\"email\":\"${email}\",\"handle\":\"${handle}\",\"password\":\"${PASSWORD}\",\"inviteCode\":\"${invite}\"}" \
52 "${PDS_URL}/xrpc/com.atproto.server.createAccount")
53
54 did=$(printf '%s\n' "$result" | jq -er '.did')
55 printf '[create] %s = %s (password: %s)\n' "$handle" "$did" "$PASSWORD" >&2
56 printf '%s\n' "$did"
57}
58
59# login DID/Handle → access JWT on stdout
60login() {
61 curl -fsS -H "Content-Type: application/json" \
62 -d "{\"identifier\":\"$1\",\"password\":\"${PASSWORD}\"}" \
63 "${PDS_URL}/xrpc/com.atproto.server.createSession" \
64 | jq -er '.accessJwt'
65}
66
67# put_record JWT DID COLLECTION RKEY RECORD_JSON
68put_record() {
69 jwt="$1"; did="$2"; collection="$3"; rkey="$4"; record="$5"
70
71 payload=$(jq -nc \
72 --arg repo "$did" \
73 --arg collection "$collection" \
74 --arg rkey "$rkey" \
75 --argjson record "$record" \
76 '{repo:$repo, collection:$collection, rkey:$rkey, record:$record}')
77
78 curl -fsS \
79 -H "Content-Type: application/json" \
80 -H "Authorization: Bearer ${jwt}" \
81 -d "$payload" \
82 "${PDS_URL}/xrpc/com.atproto.repo.putRecord" >/dev/null
83
84 printf '[record] at://%s/%s/%s\n' "$did" "$collection" "$rkey" >&2
85}
86
87# ensure accounts
88OWNER_DID=""
89SYSTEM_DID=""
90for u in $USERS; do
91 did=$(ensure_account "$u")
92 if [ "$u" = "$OWNER_USER" ]; then
93 OWNER_DID="$did"
94 fi
95 if [ "$u" = "$SYSTEM_USER" ]; then
96 SYSTEM_DID="$did"
97 fi
98done
99
100[ -n "$OWNER_DID" ] || { printf 'OWNER_USER %s not in USERS list\n' "$OWNER_USER" >&2; exit 1; }
101[ -n "$SYSTEM_DID" ] || { printf 'SYSTEM_USER %s not in USERS list\n' "$SYSTEM_USER" >&2; exit 1; }
102
103mkdir -p "$SHARED_DIR"
104printf '%s' "$OWNER_DID" > "${SHARED_DIR}/owner-did"
105printf '[owner] %s → %s/owner-did\n' "$OWNER_USER" "$SHARED_DIR" >&2
106printf '%s' "$SYSTEM_DID" > "${SHARED_DIR}/system-did"
107printf '[system] %s → %s/system-did\n' "$SYSTEM_USER" "$SHARED_DIR" >&2
108
109# label definitions (under SYSTEM_DID)
110JWT=$(login "$SYSTEM_DID")
111
112CREATED_AT="2025-09-22T11:14:35+01:00"
113
114put_record "$JWT" "$SYSTEM_DID" "sh.tangled.label.definition" "wontfix" "$(cat <<JSON
115{
116 "name": "wontfix",
117 "color": "#64748b",
118 "scope": ["sh.tangled.repo.issue"],
119 "multiple": false,
120 "createdAt": "${CREATED_AT}",
121 "valueType": {"type": "null", "format": "any"}
122}
123JSON
124)"
125
126put_record "$JWT" "$SYSTEM_DID" "sh.tangled.label.definition" "good-first-issue" "$(cat <<JSON
127{
128 "name": "good-first-issue",
129 "color": "#8B5CF6",
130 "scope": ["sh.tangled.repo.issue"],
131 "multiple": false,
132 "createdAt": "${CREATED_AT}",
133 "valueType": {"type": "null", "format": "any"}
134}
135JSON
136)"
137
138put_record "$JWT" "$SYSTEM_DID" "sh.tangled.label.definition" "duplicate" "$(cat <<JSON
139{
140 "name": "duplicate",
141 "color": "#ef4444",
142 "scope": ["sh.tangled.repo.issue"],
143 "multiple": false,
144 "createdAt": "${CREATED_AT}",
145 "valueType": {"type": "null", "format": "any"}
146}
147JSON
148)"
149
150put_record "$JWT" "$SYSTEM_DID" "sh.tangled.label.definition" "documentation" "$(cat <<JSON
151{
152 "name": "documentation",
153 "color": "#06b6d4",
154 "scope": ["sh.tangled.repo.issue"],
155 "multiple": false,
156 "createdAt": "${CREATED_AT}",
157 "valueType": {"type": "null", "format": "any"}
158}
159JSON
160)"
161
162put_record "$JWT" "$SYSTEM_DID" "sh.tangled.label.definition" "assignee" "$(cat <<JSON
163{
164 "name": "assignee",
165 "color": "#10B981",
166 "scope": ["sh.tangled.repo.issue", "sh.tangled.repo.pull"],
167 "multiple": false,
168 "createdAt": "${CREATED_AT}",
169 "valueType": {"type": "string", "format": "did"}
170}
171JSON
172)"
173
174# shared env values for appview
175LABEL_GFI=at://${SYSTEM_DID}/sh.tangled.label.definition/good-first-issue
176LABEL_DEFAULTS=$LABEL_GFI
177LABEL_DEFAULTS=$LABEL_DEFAULTS,at://${SYSTEM_DID}/sh.tangled.label.definition/assignee
178LABEL_DEFAULTS=$LABEL_DEFAULTS,at://${SYSTEM_DID}/sh.tangled.label.definition/documentation
179LABEL_DEFAULTS=$LABEL_DEFAULTS,at://${SYSTEM_DID}/sh.tangled.label.definition/duplicate
180LABEL_DEFAULTS=$LABEL_DEFAULTS,at://${SYSTEM_DID}/sh.tangled.label.definition/wontfix
181
182printf '%s' "$LABEL_GFI" > "${SHARED_DIR}/label-gfi"
183printf '%s' "$LABEL_DEFAULTS" > "${SHARED_DIR}/label-defaults"
184printf '[env] wrote label-defaults, label-gfi\n' >&2
185
186# service definitions (under OWNER_DID)
187JWT=$(login "$OWNER_DID")
188
189put_record "$JWT" "$OWNER_DID" "sh.tangled.knot" $KNOT_HOSTNAME "{\"createdAt\": \"${CREATED_AT}\"}"
190
191printf 'done.\n' >&2