-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathenv-agent.sh
More file actions
1252 lines (1120 loc) · 48.1 KB
/
Copy pathenv-agent.sh
File metadata and controls
1252 lines (1120 loc) · 48.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Lightweight env init for non-interactive agent shells (Claude Code, Cursor agent, etc.).
# Sourced automatically via BASH_ENV — keep it silent and fast.
# Do NOT source ~/.bashrc here; agents don't need interactive shell config.
if [[ -n "${GLAZE_ROOT:-}" ]]; then
_GLAZE_SCRIPT_DIR="$GLAZE_ROOT"
else
_GLAZE_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
fi
_gz_detect_git_root() {
local cwd="${PWD:-$_GLAZE_SCRIPT_DIR}"
local root
root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null) || return 1
# Bazel places a .git symlink inside execroot/_main pointing back to the
# real workspace, causing git to report execroot/_main as the toplevel when
# BASH_ENV sources this script from inside a bazel subprocess. Reject any
# path that lives inside a Bazel output directory so we don't corrupt GLAZE_ROOT.
[[ "$root" == *"/execroot/"* ]] && return 1
echo "$root"
}
_gz_detect_shared_root() {
local git_root="$1"
local common_dir
common_dir="$(git -C "$git_root" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)" || return 1
cd "$common_dir/.." 2>/dev/null && pwd
}
_detected_root="$(_gz_detect_git_root)"
_detected_root="${_detected_root:-$_GLAZE_SCRIPT_DIR}"
# Guard against double-sourcing, but re-initialize if we're in a different
# git root than what was previously detected (e.g. switched into a worktree).
if [[ -z "$_GLAZE_AGENT_ENV_LOADED" || "${GLAZE_ROOT:-}" != "$_detected_root" ]]; then
export _GLAZE_AGENT_ENV_LOADED=1
export GLAZE_AGENT=1
GLAZE_ROOT="$_detected_root"
unset _detected_root
GLAZE_SHARED_ROOT="$(_gz_detect_shared_root "$GLAZE_ROOT")"
GLAZE_SHARED_ROOT="${GLAZE_SHARED_ROOT:-$GLAZE_ROOT}"
export GLAZE_ROOT GLAZE_SHARED_ROOT
_GLAZE_PIDS="$GLAZE_ROOT/.dev-pids"
_GLAZE_LOGS="$GLAZE_ROOT/.dev-logs"
mkdir -p "$_GLAZE_PIDS" "$_GLAZE_LOGS"
_gz_prepend_path_once() {
local dir="$1"
[[ -d "$dir" ]] || return 0
case ":${PATH:-}:" in
*":$dir:"*) return 0 ;;
esac
PATH="$dir${PATH:+:$PATH}"
export PATH
}
_gz_preferred_root_for() {
printf '%s\n' "$GLAZE_ROOT"
}
# Activate Bazel-managed venv if present (built by `bazel run //:manage.venv`)
_GLAZE_VENV_ROOT="$(_gz_preferred_root_for ".manage.venv/bin/activate")"
[[ -f "$_GLAZE_VENV_ROOT/.manage.venv/bin/activate" ]] && source "$_GLAZE_VENV_ROOT/.manage.venv/bin/activate"
# Load local env vars
_gz_load_env_file() {
local path="$1"
[[ -f "$path" ]] || return 0
set -a
# shellcheck disable=SC1090
source "$path"
set +a
}
_gz_load_preferred_env_file() {
local rel="$1"
local preferred_root
preferred_root="$(_gz_preferred_root_for "$rel")"
_gz_load_env_file "$preferred_root/$rel"
}
_gz_load_preferred_env_file ".env.local"
_gz_load_preferred_env_file "web/.env.local"
_gz_bazel() {
local -a cmd
if command -v rtk &>/dev/null; then
cmd=(rtk bazel)
else
cmd=(bazel)
fi
# When running inside a worktree that lives beneath the shared root,
# bazel refuses to start ("called from a bazel output directory") because
# it finds the parent workspace. A per-worktree --output_base sidesteps
# this by giving each worktree its own isolated output directory.
if [[ -n "${GLAZE_SHARED_ROOT:-}" && "$GLAZE_ROOT" != "$GLAZE_SHARED_ROOT" ]]; then
local _wt_slug
_wt_slug="$(basename "$GLAZE_ROOT")"
"${cmd[@]}" --output_base="$HOME/.cache/bazel-worktrees/$_wt_slug" "$@"
else
"${cmd[@]}" "$@"
fi
}
_gz_manage_venv_looks_ready() {
[[ -f "$GLAZE_ROOT/.manage.venv/bin/activate" ]] || return 1
[[ -L "$GLAZE_ROOT/.manage.venv" ]] || return 0
local link_target
link_target="$(readlink -f "$GLAZE_ROOT/.manage.venv" 2>/dev/null)" || return 1
[[ -n "$link_target" && -d "$link_target" ]]
}
_gz_resolved_db_path() {
local db_url="${DATABASE_URL:-}"
if [[ -z "$db_url" ]]; then
printf '%s\n' "$GLAZE_ROOT/db.sqlite3"
return 0
fi
if [[ "$db_url" != sqlite:* ]]; then
return 1
fi
python3 - "$db_url" "$GLAZE_ROOT" <<'PY'
import os
import sys
from urllib.parse import unquote, urlparse
db_url = sys.argv[1]
root = sys.argv[2]
parsed = urlparse(db_url)
if parsed.scheme != "sqlite":
raise SystemExit(1)
path = unquote(parsed.path or "")
if path.startswith("//"):
path = path[1:]
if not path:
raise SystemExit(1)
if path.startswith("/"):
print(path)
else:
print(os.path.abspath(os.path.join(root, path)))
PY
}
_gz_ensure_bootstrap() {
if [[ -n "${BASH_ENV:-}" ]] && ! command -v rtk &>/dev/null; then
echo "--- Installing RTK (for test optimizations and type generation)..."
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
mkdir -p ~/.claude
rtk init -g --auto-patch
fi
if ! _gz_manage_venv_looks_ready; then
if [[ -e "$GLAZE_ROOT/.manage.venv" || -L "$GLAZE_ROOT/.manage.venv" ]]; then
rm -rf "$GLAZE_ROOT/.manage.venv"
fi
echo "--- Building Python venv with Bazel (//:manage.venv)..."
(cd "$GLAZE_ROOT" && _gz_bazel run //:manage.venv)
fi
local db_path=""
if db_path="$(_gz_resolved_db_path 2>/dev/null)"; then
if [[ ! -e "$db_path" ]]; then
echo "--- Running migrations..."
(cd "$GLAZE_ROOT" && _gz_bazel run //:manage -- migrate --run-syncdb)
fi
fi
[[ -f "$GLAZE_ROOT/.manage.venv/bin/activate" ]] && source "$GLAZE_ROOT/.manage.venv/bin/activate"
}
_gz_ensure_bootstrap
_gz_prepend_path_once "$GLAZE_ROOT/web/node_modules/.bin"
_gz_prepend_path_once "$GLAZE_ROOT/bin"
# Prevent Rust/rtk stack overflows from crashing the WSL2 VM
ulimit -s unlimited 2>/dev/null || true
# Propagate to child processes so agents spawned from an interactive shell
# (Codex, etc.) also get this bootstrap without per-tool config.
export BASH_ENV="$_GLAZE_SCRIPT_DIR/env-agent.sh"
fi
unset _detected_root
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
_gz_venv_root() {
_gz_preferred_root_for ".manage.venv/bin/activate"
}
_gz_port_is_free() { # _gz_port_is_free <port>
local port="$1"
python3 - "$port" <<'EOF'
import socket, sys
port = int(sys.argv[1])
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(0.2)
raise SystemExit(1 if s.connect_ex(("127.0.0.1", port)) == 0 else 0)
EOF
}
_gz_find_free_port() { # _gz_find_free_port [start_port] — returns first unbound port >= start
local port="${1:-8080}"
[[ "$port" =~ ^[0-9]+$ ]] || return 1
(( port >= 0 && port <= 65535 )) || return 1
while [[ "$port" -le 65535 ]]; do
if _gz_port_is_free "$port"; then
printf '%s\n' "$port"
return 0
fi
(( port++ ))
done
return 1
}
_gz_is_running() { # _gz_is_running <name>
local pidfile="$_GLAZE_PIDS/$1.pid"
[[ -f "$pidfile" ]] || return 1
local pid
pid=$(cat "$pidfile")
[[ -n "$pid" ]] || return 1
# Check if PID exists and is not a zombie
ps -p "$pid" -o state= 2>/dev/null | grep -qv "Z"
}
_gz_wait_for_health() {
local port="$1"
local url="http://127.0.0.1:$port/api/health/ready/"
local i=0
echo -n "Waiting for backend to be ready..."
until curl -s "$url" | python3 -c 'import json, sys; print("ready" if json.load(sys.stdin).get("status") == "ready" else "not_ready")' 2>/dev/null | grep -q '^ready$'; do
echo -n "."
sleep 0.5
(( i++ ))
if (( i >= 40 )); then
echo " timed out!"
return 1
fi
done
echo " ready."
}
_gz_start() { # _gz_start <name> <logfile> <cmd...>
local name="$1" logfile="$2"; shift 2
if _gz_is_running "$name"; then
echo "$name: already running (PID $(cat "$_GLAZE_PIDS/$name.pid"))"
return 0
fi
# Use direct backgrounding. monitor mode ensures a new process group.
# We use a subshell to avoid affecting the main shell state.
(
set -m
"$@" >> "$logfile" 2>&1 &
echo $! > "$_GLAZE_PIDS/$name.pid"
)
# Wait a moment to ensure the PID file is written
sleep 0.1
local pid=$(cat "$_GLAZE_PIDS/$name.pid" 2>/dev/null)
echo "$name: started (PID ${pid:-unknown}) — logs: $logfile"
}
_gz_stop() { # _gz_stop <name>
local pidfile="$_GLAZE_PIDS/$1.pid"
if [[ -f "$pidfile" ]]; then
local pid i
pid=$(cat "$pidfile")
echo "Stopping $1 (PID $pid)..."
# Kill the entire process group so child processes don't get orphaned when
# the bash wrapper shell exits (e.g. on Ctrl+C from gz_start)
kill -- -"$pid" 2>/dev/null || kill "$pid" 2>/dev/null || true
for i in {1..10}; do
kill -0 -- -"$pid" 2>/dev/null || break
sleep 0.1
done
if kill -0 -- -"$pid" 2>/dev/null; then
kill -KILL -- -"$pid" 2>/dev/null || true
fi
echo "$1: stopped"
else
echo "$1: not running"
fi
rm -f "$pidfile" "${pidfile%.pid}.port"
}
_gz_rotate_log() { # _gz_rotate_log <name>
local logfile="$_GLAZE_LOGS/$1.log"
[[ -f "$logfile" ]] && mv "$logfile" "${logfile%.log}.$(date +%Y%m%dT%H%M%S).log"
}
_gz_remove_symlink_if_present() { # _gz_remove_symlink_if_present <path>
local path="$1"
[[ -L "$path" ]] || return 1
rm "$path"
return 0
}
_gz_link_shared_dir_if_missing() { # _gz_link_shared_dir_if_missing <relative_path>
local rel="$1"
local shared_path="$GLAZE_SHARED_ROOT/$rel"
local local_path="$GLAZE_ROOT/$rel"
[[ "$GLAZE_SHARED_ROOT" == "$GLAZE_ROOT" ]] && return 1
[[ -e "$local_path" || ! -e "$shared_path" ]] && return 1
mkdir -p "$(dirname "$local_path")"
ln -s "$shared_path" "$local_path"
}
_gz_ensure_node() {
if command -v node &>/dev/null; then
return 0
fi
# Try loading nvm if installed but not active in this shell
local nvm_dir="${NVM_DIR:-$HOME/.nvm}"
if [[ -f "$nvm_dir/nvm.sh" ]]; then
source "$nvm_dir/nvm.sh"
command -v node &>/dev/null && return 0
fi
# Install nvm then Node 20
echo "Node not found — installing nvm and Node 20 (this may take a minute)..."
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
export NVM_DIR="$HOME/.nvm"
source "$NVM_DIR/nvm.sh"
${GLAZE_AGENT:+rtk }nvm install 20
${GLAZE_AGENT:+rtk }nvm use 20
}
gz_install_mcp_tools() {
echo "=== Installing MCP tool dependencies ==="
local tools=(jq curl git gh)
local missing=()
for tool in "${tools[@]}"; do
command -v "$tool" &>/dev/null || missing+=("$tool")
done
if [[ ${#missing[@]} -eq 0 ]]; then
echo "All MCP tools already installed: ${tools[*]}"
return 0
fi
echo "Missing: ${missing[*]}"
if command -v apt-get &>/dev/null; then
sudo apt-get update -q && sudo apt-get install -y "${missing[@]}"
elif command -v brew &>/dev/null; then
brew install "${missing[@]}"
elif command -v dnf &>/dev/null; then
sudo dnf install -y "${missing[@]}"
else
echo "No supported package manager found (apt-get, brew, dnf). Install manually: ${missing[*]}"
return 1
fi
echo "=== MCP tools installed ==="
}
gz_sync() {
echo "=== Glaze: syncing package manager state ==="
echo "--- Syncing Python dependencies via uv..."
uv sync
echo "--- Syncing web dependencies via pnpm import..."
(cd "$GLAZE_ROOT/web" && pnpm import)
echo "--- Reloading shell bootstrap..."
gz_reload
echo "=== Sync complete ==="
}
# ---------------------------------------------------------------------------
# Django manage.py
# ---------------------------------------------------------------------------
gz_manage() { # gz_manage <subcommand> [args…]
(
cd "$GLAZE_ROOT"
${GLAZE_AGENT:+rtk }bazel run //:manage -- "$@"
)
}
gz_migrate() { gz_manage migrate "$@"; }
gz_makemigrations() { gz_manage makemigrations "$@"; }
gz_shell() { gz_manage shell "$@"; }
gz_dbshell() { gz_manage dbshell "$@"; }
gz_showmigrations() { gz_manage showmigrations "$@"; }
gz_dump_public_library() { gz_manage dump_public_library "$@"; }
gz_load_public_library() { gz_manage load_public_library "$@"; }
_gz_prod_kube() { # internal: "KUBECONFIG=... kubectl exec deployment/glaze-web -- ..."
local host="${GLAZE_PROD_HOST:?Set GLAZE_PROD_HOST=user@host in .env.local}"
local KC="KUBECONFIG=/etc/rancher/k3s/k3s.yaml"
ssh "$host" "$KC kubectl exec deployment/glaze-web -- $*"
}
gz_prod() { # gz_prod <manage.py subcommand> [args…]
_gz_prod_kube "python manage.py $*"
}
gz_prod_shell() { # gz_prod_shell [-c "cmd"] — piping avoids SSH quoting issues
local host="${GLAZE_PROD_HOST:?Set GLAZE_PROD_HOST=user@host in .env.local}"
local KC="KUBECONFIG=/etc/rancher/k3s/k3s.yaml"
if [[ "$1" == "-c" ]]; then
echo "${2:?gz_prod_shell -c requires a command string}" \
| ssh "$host" "$KC kubectl exec -i deployment/glaze-web -- python manage.py shell"
else
ssh "$host" "$KC kubectl exec -it deployment/glaze-web -- python manage.py shell $*"
fi
}
gz_prod_dbshell() { gz_prod dbshell "$@"; }
gz_backup() {
# Stream a production Postgres dump locally, then restore it into a
# disposable postgres:17 container to verify the dump is readable and
# contains application data.
local host="${GLAZE_PROD_HOST:?Set GLAZE_PROD_HOST=user@host in .env.local}"
local KC="KUBECONFIG=/etc/rancher/k3s/k3s.yaml"
local dump_path="${1:-}"
if [[ -z "$dump_path" ]]; then
dump_path="$(mktemp /tmp/glaze-prod-postgres-XXXXXX.dump)"
elif [[ -e "$dump_path" ]]; then
echo "gz_backup: refusing to overwrite existing file: $dump_path" >&2
return 1
fi
command -v docker >/dev/null || {
echo "gz_backup: docker is required locally" >&2
return 1
}
docker info >/dev/null 2>&1 || {
echo "gz_backup: docker daemon is not reachable locally" >&2
return 1
}
echo "--- backing up production postgres from $host ---"
ssh "$host" "$KC kubectl exec glaze-postgres-0 -- bash -c 'PGPASSWORD=\"\$POSTGRES_PASSWORD\" pg_dump -U \"\$POSTGRES_USER\" -d \"\$POSTGRES_DB\" -Fc'" > "$dump_path"
sha256sum "$dump_path"
ls -lh "$dump_path"
echo "--- verifying dump in disposable postgres:17 container ---"
(
set -euo pipefail
local cid
cid=$(docker run -d --rm -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=glaze postgres:17)
cleanup() {
docker stop "$cid" >/dev/null 2>&1 || true
}
trap cleanup EXIT
for _ in $(seq 1 30); do
if docker exec "$cid" pg_isready -U postgres >/dev/null 2>&1; then
break
fi
sleep 2
done
docker exec "$cid" pg_restore --version
docker exec -i "$cid" pg_restore -U postgres -d glaze --no-owner --no-privileges < "$dump_path"
local table_count piece_count user_count
table_count=$(docker exec "$cid" psql -U postgres -d glaze -Atqc \
"SELECT COUNT(*) FROM pg_catalog.pg_tables WHERE schemaname = 'public';")
piece_count=$(docker exec "$cid" psql -U postgres -d glaze -Atqc \
"SELECT COUNT(*) FROM api_piece;")
user_count=$(docker exec "$cid" psql -U postgres -d glaze -Atqc \
"SELECT COUNT(*) FROM auth_user;")
echo "Backed up $piece_count pieces across $user_count users."
echo "Verified restore: $table_count public tables, api_piece has $piece_count rows."
)
echo "Backup saved to $dump_path"
}
gz_restore() {
# Restore a pg_dump (-Fc) file into Postgres.
#
# Without --prod: restores into the local dev Postgres. If DATABASE_URL is
# already a postgres:// URL, restores directly into it. Otherwise, starts
# (or reuses) a persistent Docker container named glaze-dev-db on
# localhost:5433 and prints the DATABASE_URL to add to .env.local.
#
# With --prod: restores into the PRODUCTION Postgres database via
# SSH + kubectl exec. Requires typing a confirmation string.
# THIS IS DESTRUCTIVE AND IRREVERSIBLE.
#
# Usage: gz_restore [--prod] <dump_file>
# dump_file: path produced by gz_backup (custom format, -Fc)
local prod=false
local dump_path=""
while [[ $# -gt 0 ]]; do
case "$1" in
--prod) prod=true; shift ;;
*) dump_path="$1"; shift ;;
esac
done
if [[ -z "$dump_path" ]]; then
echo "Usage: gz_restore [--prod] <dump_file>" >&2
echo " Tip: run 'gz_backup' first to get a dump file." >&2
return 1
fi
if [[ ! -f "$dump_path" ]]; then
echo "gz_restore: file not found: $dump_path" >&2
return 1
fi
if [[ "$prod" == true ]]; then
local host="${GLAZE_PROD_HOST:?Set GLAZE_PROD_HOST=user@host in .env.local}"
local KC="KUBECONFIG=/etc/rancher/k3s/k3s.yaml"
echo ""
echo "╔═════════════════════════════════════════════════════════════════════════════════╗"
echo "║ WARNING: PRODUCTION DATABASE OVERWRITE ║"
echo "╠═════════════════════════════════════════════════════════════════════════════════╣"
echo "║ This will DROP AND RECREATE the production Postgres database. ║"
echo "║ All current production data will be replaced by the dump. ║"
echo "║ This action is IRREVERSIBLE. There is no undo. ║"
echo "║ ║"
printf "║ Dump: %-73s║\n" "$(basename "$dump_path")"
printf "║ Host: %-73s║\n" "$host"
echo "╚═════════════════════════════════════════════════════════════════════════════════╝"
echo ""
local confirm_str
confirm_str="$(openssl rand -hex 3)"
echo "Type the following string to confirm, or anything else to abort:"
echo ""
echo " $confirm_str"
echo ""
local input
read -r input
if [[ "$input" != "$confirm_str" ]]; then
echo "Aborted — confirmation did not match." >&2
return 1
fi
local start_ts
start_ts="$(date +%s)"
echo "--- restoring dump into production postgres ---"
local spinner_pid
(while true; do for c in '|' '/' '-' '\'; do printf '\r %s restoring...' "$c" >&2; sleep 0.2; done; done) &
spinner_pid=$!
ssh "$host" "$KC kubectl exec -i glaze-postgres-0 -- bash -c 'PGPASSWORD=\"\$POSTGRES_PASSWORD\" pg_restore -U \"\$POSTGRES_USER\" -d \"\$POSTGRES_DB\" --no-owner --no-privileges --clean --if-exists'" < "$dump_path"
kill "$spinner_pid" 2>/dev/null; wait "$spinner_pid" 2>/dev/null
printf '\r \r' >&2
echo "--- verifying restore ---"
local verify_out user_count piece_count orphan_pieces orphan_states
verify_out="$(printf '%s\n' \
'SELECT COUNT(*) FROM auth_user;' \
'SELECT COUNT(*) FROM api_piece;' \
'SELECT COUNT(*) FROM api_piece p WHERE NOT EXISTS (SELECT 1 FROM api_piecestate s WHERE s.piece_id = p.id);' \
'SELECT COUNT(*) FROM api_piecestate s WHERE NOT EXISTS (SELECT 1 FROM api_piece p WHERE p.id = s.piece_id);' \
| ssh "$host" "$KC kubectl exec -i glaze-postgres-0 -- bash -c 'PGPASSWORD=\"\$POSTGRES_PASSWORD\" psql -U \"\$POSTGRES_USER\" -d \"\$POSTGRES_DB\" -Atq'")"
user_count="$( sed -n '1p' <<< "$verify_out")"
piece_count="$( sed -n '2p' <<< "$verify_out")"
orphan_pieces="$( sed -n '3p' <<< "$verify_out")"
orphan_states="$( sed -n '4p' <<< "$verify_out")"
local elapsed=$(( $(date +%s) - start_ts ))
echo ""
echo "Restore complete in ${elapsed}s."
echo " users: $user_count"
echo " pieces: $piece_count"
echo " orphan pieces: $orphan_pieces (expected 0)"
echo " orphan states: $orphan_states (expected 0)"
echo ""
[[ "$user_count" -gt 0 ]] || { echo "FAIL: no users found after restore" >&2; return 1; }
[[ "$piece_count" -gt 0 ]] || { echo "FAIL: no pieces found after restore" >&2; return 1; }
[[ "$orphan_pieces" -eq 0 ]] || { echo "FAIL: $orphan_pieces pieces have no state history" >&2; return 1; }
[[ "$orphan_states" -eq 0 ]] || { echo "FAIL: $orphan_states orphaned piece states found" >&2; return 1; }
echo "All verification checks passed."
echo "Record this drill in docs/ops/restore-drill-log.md."
return 0
fi
command -v docker >/dev/null || {
echo "gz_restore: docker is required" >&2
return 1
}
docker info >/dev/null 2>&1 || {
echo "gz_restore: docker daemon is not reachable" >&2
return 1
}
local db_url="${DATABASE_URL:-}"
local using_container=false
local dev_db_url="postgres://postgres:postgres@localhost:5433/glaze"
if [[ "$db_url" == postgres://* || "$db_url" == postgresql://* ]]; then
echo "--- restoring into existing Postgres: $db_url ---"
else
using_container=true
echo "--- DATABASE_URL is not Postgres; using Docker container glaze-dev-db ---"
# Start container if not already running
if ! docker inspect glaze-dev-db >/dev/null 2>&1; then
echo "Starting glaze-dev-db (postgres:17 on localhost:5433)..."
docker run -d \
--name glaze-dev-db \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=glaze \
-p 5433:5432 \
postgres:17 >/dev/null
elif [[ "$(docker inspect -f '{{.State.Running}}' glaze-dev-db 2>/dev/null)" != "true" ]]; then
echo "Restarting stopped glaze-dev-db container..."
docker start glaze-dev-db >/dev/null
else
echo "Reusing existing glaze-dev-db container."
fi
echo "Waiting for Postgres to be ready..."
for _ in $(seq 1 30); do
if docker exec glaze-dev-db pg_isready -U postgres >/dev/null 2>&1; then
break
fi
sleep 1
done
docker exec glaze-dev-db pg_isready -U postgres >/dev/null 2>&1 || {
echo "gz_restore: glaze-dev-db never became ready" >&2
return 1
}
db_url="$dev_db_url"
fi
echo "--- restoring dump (this may take a moment) ---"
local piece_count user_count
if [[ "$using_container" == true ]]; then
# Use pg_restore inside the container to match the server's pg version,
# avoiding "unsupported version" errors when the local client is older.
docker exec glaze-dev-db psql -U postgres \
-c "DROP DATABASE IF EXISTS glaze;" \
-c "CREATE DATABASE glaze;" >/dev/null
docker exec -i glaze-dev-db pg_restore \
-U postgres -d glaze --no-owner --no-privileges \
< "$dump_path"
piece_count=$(docker exec glaze-dev-db psql -U postgres -d glaze \
-Atqc "SELECT COUNT(*) FROM api_piece;")
user_count=$(docker exec glaze-dev-db psql -U postgres -d glaze \
-Atqc "SELECT COUNT(*) FROM auth_user;")
else
# Parse connection details from the URL: postgres://user:pass@host:port/dbname
local userpass hostport dbname user pass host port
userpass="${db_url#postgres*://}"
dbname="${userpass##*/}"
hostport="${userpass%%/*}"
hostport="${hostport##*@}"
user="${userpass%%:*}"
pass="${userpass#*:}"; pass="${pass%%@*}"
host="${hostport%%:*}"
port="${hostport##*:}"; [[ "$port" == "$host" ]] && port=5432
PGPASSWORD="$pass" psql -h "$host" -p "$port" -U "$user" -d postgres \
-c "DROP DATABASE IF EXISTS $dbname;" \
-c "CREATE DATABASE $dbname;" 2>/dev/null || {
echo "(could not drop/recreate db — restoring with --clean instead)"
}
PGPASSWORD="$pass" pg_restore \
-h "$host" -p "$port" -U "$user" -d "$dbname" \
--no-owner --no-privileges --clean --if-exists \
< "$dump_path"
piece_count=$(PGPASSWORD="$pass" psql -h "$host" -p "$port" -U "$user" -d "$dbname" \
-Atqc "SELECT COUNT(*) FROM api_piece;")
user_count=$(PGPASSWORD="$pass" psql -h "$host" -p "$port" -U "$user" -d "$dbname" \
-Atqc "SELECT COUNT(*) FROM auth_user;")
fi
echo "Restored $piece_count pieces across $user_count users."
echo "--- running migrations (to apply any branch-local changes) ---"
DATABASE_URL="$db_url" gz_migrate
if [[ "$using_container" == true ]]; then
echo ""
echo "Add this to your .env.local to use the restored database:"
echo " DATABASE_URL=$dev_db_url"
echo ""
echo "Then restart your dev server: gz_stop && gz_start"
fi
}
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
gz_test_common() {
${GLAZE_AGENT:+rtk }bazel test //tests:common_test "$@"
}
gz_test_backend() {
${GLAZE_AGENT:+rtk }bazel test //api:api_test "$@"
}
gz_test_web() {
${GLAZE_AGENT:+rtk }bazel test //web:web_test "$@"
}
gz_test() {
local target="//..."
local mode="auto"
local coverage=false
local usage="Usage: gz_test [--all|--affected|--coverage] [bazel args...]"
# Parse our custom flags first
while [[ $# -gt 0 ]]; do
case "$1" in
--all)
mode="all"
shift
;;
--affected)
mode="affected"
shift
;;
--coverage)
coverage=true
shift
;;
--help)
echo "$usage"
return 0
;;
-*)
# Stop parsing at first bazel flag
break
;;
*)
# Treat as target override if no flag matched
target="$1"
shift
mode="manual"
break
;;
esac
done
if [[ "$mode" == "auto" ]]; then
# Default to affected if on a branch, otherwise all
local current_branch
current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [[ "$current_branch" != "main" && "$current_branch" != "HEAD" ]]; then
mode="affected"
else
mode="all"
fi
fi
if [[ "$mode" == "affected" ]]; then
local diff_base="main"
# Handle cases where main is not available or we are on main
if ! git rev-parse --verify "$diff_base" &>/dev/null; then
diff_base="origin/main"
fi
if git rev-parse --verify "$diff_base" &>/dev/null; then
local FILES
FILES=$(git diff --name-only "$diff_base...HEAD" 2>/dev/null)
if [[ -n "$FILES" ]]; then
local EXISTING=""
for f in $FILES; do [[ -f "$f" ]] && EXISTING="$EXISTING $f"; done
if [[ -n "$EXISTING" ]]; then
echo "Determining affected tests (comparing with $diff_base)..."
# Filter existing files to only those Bazel knows about to avoid query errors (exit code 7)
local ALL_SOURCES
ALL_SOURCES=$(_gz_get_all_sources)
local BAZEL_FILES
BAZEL_FILES=$(echo "$EXISTING" | tr ' ' '\n' | grep -Fxf <(echo "$ALL_SOURCES"))
if [[ -n "$BAZEL_FILES" ]]; then
target=$(_gz_get_affected_targets 'kind(test, //...)' "$BAZEL_FILES")
if [[ -n "$target" ]]; then
target=$(echo "$target" | tr '\n' ' ')
echo "Testing $(echo "$target" | wc -w) affected target(s)."
else
echo "No tests affected by these changes. Use 'gz_test --all' if you want to run everything."
return 0
fi
else
echo "No Bazel-tracked code changes detected. Use 'gz_test --all' if you want to run everything."
return 0
fi
else
echo "No code changes detected. Use 'gz_test --all' if you want to run everything."
return 0
fi
else
echo "No differences from $diff_base. Running all tests."
target="//..."
fi
else
echo "Warning: Could not find base branch '$diff_base'. Running all tests."
target="//..."
fi
fi
if [ "$coverage" = true ]; then
# Exclude lint targets from coverage (they don't produce coverage data and slow down the pass)
local coverage_target
echo "Determining coverage targets (excluding 'lint' tagged targets)..."
coverage_target=$(${GLAZE_AGENT:+rtk }bazel query "tests(set($target)) except attr(tags, lint, //...)" 2>/dev/null | tr '\n' ' ')
if [[ -z "$coverage_target" ]]; then
echo "No coverage targets found."
return 0
fi
echo "Running: ${GLAZE_AGENT:+rtk }bazel coverage --config=ci --combined_report=lcov $coverage_target"
(cd "$GLAZE_ROOT" && ${GLAZE_AGENT:+rtk }bazel coverage --config=ci --combined_report=lcov $coverage_target "$@")
else
echo "Running: ${GLAZE_AGENT:+rtk }bazel test $target"
(cd "$GLAZE_ROOT" && ${GLAZE_AGENT:+rtk }bazel test --test_output=errors $target "$@")
fi
}
# CI-aligned: run ruff, eslint, tsc, and mypy via Bazel (same as CI).
gz_lint() {
echo "Running: ${GLAZE_AGENT:+rtk }bazel build --config=lint //..."
(cd "$GLAZE_ROOT" && ${GLAZE_AGENT:+rtk }bazel build --config=ci --config=lint //... "$@") || return 1
echo "Running: ${GLAZE_AGENT:+rtk }bazel test --config=lint --test_tag_filters=lint //..."
(cd "$GLAZE_ROOT" && ${GLAZE_AGENT:+rtk }bazel test --config=ci --config=lint --test_tag_filters=lint //... "$@")
}
# Auto-fix: reformat Python files and apply ruff auto-fixes in one step.
gz_format() {
(
source "$(_gz_venv_root)/.manage.venv/bin/activate"
cd "$GLAZE_ROOT"
ruff format .
ruff check --fix .
)
}
_gz_build() {
# Full production build via Bazel (CI-aligned).
# Symlinks web/dist → bazel-bin/web/dist for easy inspection.
${GLAZE_AGENT:+rtk }bazel build //... || return $?
local dist_src="$GLAZE_ROOT/bazel-bin/web/dist"
local dist_link="$GLAZE_ROOT/web/dist"
if [[ ! -L "$dist_link" && -e "$dist_link" ]]; then
echo "gz_build: warning: $dist_link exists and is not a symlink; skipping link"
else
ln -sfn "$dist_src" "$dist_link"
echo "gz_build: web/dist → $dist_src"
fi
}
gz_push() {
# Build and push the OCI image to ghcr.io/shaoster/glaze.
# Usage: gz_push [--latest]
# Always tags with the current commit SHA; pass --latest to also tag :latest.
local sha
sha=$(git -C "$GLAZE_ROOT" rev-parse HEAD) || return 1
local tag_args=(--tag "$sha")
[[ "${1:-}" == "--latest" ]] && tag_args+=(--tag latest)
${GLAZE_AGENT:+rtk }bazel run --stamp //:push -- "${tag_args[@]}"
}
gz_deploy() {
# Push the current image and trigger CD via helm upgrade on the droplet.
# Usage: gz_deploy [--no-push]
# Reads GLAZE_PROD_HOST from .env.local (e.g. GLAZE_PROD_HOST=user@host).
local host="${GLAZE_PROD_HOST:?Set GLAZE_PROD_HOST=user@host in .env.local}"
local sha
sha=$(git -C "$GLAZE_ROOT" rev-parse HEAD) || return 1
if [[ "${1:-}" != "--no-push" ]]; then
gz_push || return $?
fi
echo "Shipping chart and upgrading on $host for image tag $sha..."
scp -r "$GLAZE_ROOT/chart/glaze" "$host":~/glaze-chart-deploy/
ssh "$host" "
KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm upgrade --install glaze ~/glaze-chart-deploy/glaze/ \
-f ~/glaze-values-override.yaml \
--set image.tag=$sha \
--timeout 5m \
--wait
"
}
# ---------------------------------------------------------------------------
# Servers
# ---------------------------------------------------------------------------
gz_story() {
local port=${1:-6006}
echo "Starting Storybook dev server on http://localhost:$port ..."
(cd "$GLAZE_ROOT" && ${GLAZE_AGENT:+rtk }bazel run //web:storybook_dev -- dev --port "$port")
}
gz_open() {
(
# Give servers a moment to settle
sleep 1
local port
port=$(cat "$_GLAZE_PIDS/web.port" 2>/dev/null || grep 'Local:' "$_GLAZE_LOGS/web.log" 2>/dev/null | tail -1 | grep -oE ':[0-9]+/' | tr -d ':/')
local url="http://localhost:${port:-5173}"
echo "Opening $url"
if command -v wslview &>/dev/null; then
wslview "$url"
else
xdg-open "$url" 2>/dev/null || true
fi
)
}
gz_start() {
(
cd "$GLAZE_ROOT"
${GLAZE_AGENT:+rtk }bazel run //tools:gz_start_launcher
) || return $?
# Best-effort cleanup when this terminal tab closes normally (Ctrl-D / exit / tab X).
# Does not fire on SIGKILL. Intentionally registered only after gz_start so that
# terminals that never started servers are unaffected.
trap 'gz_stop' EXIT
}
gz_stop() {
_gz_stop backend
_gz_stop web
}
gz_status() {
for name in backend web; do
if _gz_is_running "$name"; then
local pid port
pid=$(cat "$_GLAZE_PIDS/$name.pid")
port=$(cat "$_GLAZE_PIDS/$name.port" 2>/dev/null)
if [[ -n "$port" ]]; then
echo "$name: running on :$port (PID $pid)"
else
echo "$name: running (PID $pid)"
fi
elif [[ -f "$_GLAZE_PIDS/$name.pid" ]]; then
local pid=$(cat "$_GLAZE_PIDS/$name.pid" 2>/dev/null)
echo "$name: died (stale PID file: ${pid:-empty})"
else
echo "$name: not running"
fi
done
}
gz_clean() { # purge all PID and port files in current worktree
echo "Cleaning server state in $GLAZE_ROOT..."
rm -f "$_GLAZE_PIDS"/*.pid "$_GLAZE_PIDS"/*.port
}
_gz_worktrees_purge() {
echo "=== Purging agent worktrees ==="
local wt_path="" wt_branch=""
while IFS= read -r line; do
if [[ "$line" == worktree\ * ]]; then
wt_path="${line#worktree }"
elif [[ "$line" == branch\ * ]]; then
wt_branch="${line#branch refs/heads/}"
elif [[ -z "$line" && -n "$wt_path" ]]; then
# Only target agent-managed worktrees
if [[ "$wt_path" =~ \.agent-worktrees/ || "$wt_path" =~ \.claude/worktrees/ ]]; then
local skip=0
echo "Checking $wt_branch ($wt_path)..."
# 1. Check for open PRs
if command -v gh &>/dev/null; then
if gh pr list --head "$wt_branch" --json number --jq '.[].number' | grep -q .; then
echo " [SKIP] Branch '$wt_branch' has an open PR."
skip=1
fi
fi
# 2. Check for unpushed changes
if [[ $skip -eq 0 ]]; then
local upstream
upstream=$(git -C "$GLAZE_SHARED_ROOT" rev-parse --abbrev-ref "$wt_branch@{u}" 2>/dev/null) || upstream=""
if [[ -n "$upstream" ]]; then
if [[ -n $(git -C "$GLAZE_SHARED_ROOT" rev-list "$upstream..$wt_branch") ]]; then
echo " [SKIP] Branch '$wt_branch' has unpushed commits."
skip=1
fi
else
# No upstream, check if it's merged into main
if ! git -C "$GLAZE_SHARED_ROOT" merge-base --is-ancestor "$wt_branch" main 2>/dev/null; then
echo " [SKIP] Branch '$wt_branch' has no upstream and is not merged into main."
skip=1
fi
fi
fi
if [[ $skip -eq 0 ]]; then
echo " Removing..."
# Stop servers if running
(_GLAZE_PIDS="$wt_path/.dev-pids"; gz_stop >/dev/null 2>&1)
git -C "$GLAZE_SHARED_ROOT" worktree remove --force "$wt_path"
fi
fi
wt_path="" wt_branch=""
fi
done < <(git -C "$GLAZE_SHARED_ROOT" worktree list --porcelain; echo)