@stackframe/stack-cli 2.8.83 → 2.8.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,402 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ # shellcheck source=common.sh
6
+ source "$SCRIPT_DIR/common.sh"
7
+
8
+ IMAGE_DIR="${EMULATOR_IMAGE_DIR:-$HOME/.stack/emulator/images}"
9
+ RUN_DIR="${EMULATOR_RUN_DIR:-$HOME/.stack/emulator/run}"
10
+
11
+ VM_RAM="${EMULATOR_RAM:-4096}"
12
+ VM_CPUS="${EMULATOR_CPUS:-4}"
13
+ PORT_PREFIX="${PORT_PREFIX:-${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}}"
14
+ READY_TIMEOUT="${EMULATOR_READY_TIMEOUT:-240}"
15
+
16
+ # Fixed host-side ports for the QEMU emulator (267xx range).
17
+ # Only user-facing services are exposed; internal deps stay inside the VM.
18
+ EMULATOR_DASHBOARD_PORT="${EMULATOR_DASHBOARD_PORT:-26700}"
19
+ EMULATOR_BACKEND_PORT="${EMULATOR_BACKEND_PORT:-26701}"
20
+ EMULATOR_MINIO_PORT="${EMULATOR_MINIO_PORT:-26702}"
21
+ EMULATOR_INBUCKET_PORT="${EMULATOR_INBUCKET_PORT:-26703}"
22
+
23
+ RED='\033[0;31m'
24
+ GREEN='\033[0;32m'
25
+ YELLOW='\033[0;33m'
26
+ CYAN='\033[0;36m'
27
+ NC='\033[0m'
28
+
29
+ log() { echo -e "${GREEN}[emulator]${NC} $*"; }
30
+ warn() { echo -e "${YELLOW}[emulator]${NC} $*"; }
31
+ err() { echo -e "${RED}[emulator]${NC} $*" >&2; }
32
+ info() { echo -e "${CYAN}[emulator]${NC} $*"; }
33
+
34
+
35
+ detect_host
36
+ ARCH="${EMULATOR_ARCH:-$HOST_ARCH}"
37
+
38
+ select_accelerator() {
39
+ local accel="tcg"
40
+ if [ "$ARCH" = "$HOST_ARCH" ]; then
41
+ case "$HOST_OS" in
42
+ darwin)
43
+ if "$(qemu_binary_for_arch "$ARCH")" -accel help 2>&1 | grep -q hvf; then
44
+ accel="hvf"
45
+ fi
46
+ ;;
47
+ linux)
48
+ if [ -w /dev/kvm ]; then
49
+ accel="kvm"
50
+ fi
51
+ ;;
52
+ esac
53
+ fi
54
+ ACCEL="$accel"
55
+ }
56
+
57
+ select_accelerator
58
+
59
+ VM_DIR="$RUN_DIR/vm"
60
+
61
+ image_path() {
62
+ echo "$IMAGE_DIR/stack-emulator-$ARCH.qcow2"
63
+ }
64
+
65
+ runtime_iso_path() {
66
+ echo "$VM_DIR/runtime-config.iso"
67
+ }
68
+
69
+ # Returns a fast fingerprint (size:mtime) of the base QEMU image.
70
+ # Used to detect whether the image has changed since the overlay was created.
71
+ base_image_fingerprint() {
72
+ local img="$1"
73
+ case "$HOST_OS" in
74
+ darwin) stat -f "%z:%m" "$img" 2>/dev/null ;;
75
+ linux) stat -c "%s:%Y" "$img" 2>/dev/null ;;
76
+ *) stat -f "%z:%m" "$img" 2>/dev/null || stat -c "%s:%Y" "$img" 2>/dev/null ;;
77
+ esac
78
+ }
79
+
80
+ prepare_runtime_config_iso() {
81
+ local cfg_dir="$VM_DIR/runtime-config"
82
+ local cfg_iso
83
+ cfg_iso="$(runtime_iso_path)"
84
+ rm -rf "$cfg_dir"
85
+ mkdir -p "$cfg_dir"
86
+ {
87
+ printf "STACK_EMULATOR_PORT_PREFIX=%s\n" "$PORT_PREFIX"
88
+ printf "STACK_EMULATOR_DASHBOARD_HOST_PORT=%s\n" "$EMULATOR_DASHBOARD_PORT"
89
+ printf "STACK_EMULATOR_BACKEND_HOST_PORT=%s\n" "$EMULATOR_BACKEND_PORT"
90
+ printf "STACK_EMULATOR_MINIO_HOST_PORT=%s\n" "$EMULATOR_MINIO_PORT"
91
+ printf "STACK_EMULATOR_INBUCKET_HOST_PORT=%s\n" "$EMULATOR_INBUCKET_PORT"
92
+ printf "STACK_EMULATOR_VM_DIR_HOST=%s\n" "$VM_DIR"
93
+ } > "$cfg_dir/runtime.env"
94
+ cp "$SCRIPT_DIR/../.env.development" "$cfg_dir/base.env"
95
+ make_iso_from_dir "$cfg_iso" "STACKCFG" "$cfg_dir"
96
+ }
97
+
98
+ service_is_up() {
99
+ local port="$1"
100
+ local proto="$2"
101
+ local path="${3:-/}"
102
+ local expected_codes="${4:-200}"
103
+
104
+ if [ "$proto" = "tcp" ]; then
105
+ nc -z -w2 127.0.0.1 "$port" 2>/dev/null
106
+ return $?
107
+ fi
108
+
109
+ local code
110
+ code="$(curl -s -o /dev/null -w "%{http_code}" --max-time 2 "http://127.0.0.1:${port}${path}" 2>/dev/null || true)"
111
+ local expected
112
+ for expected in ${expected_codes//,/ }; do
113
+ if [ "$code" = "$expected" ]; then
114
+ return 0
115
+ fi
116
+ done
117
+ return 1
118
+ }
119
+
120
+ deps_ready() {
121
+ service_is_up "$EMULATOR_MINIO_PORT" http /minio/health/live &&
122
+ service_is_up "$EMULATOR_INBUCKET_PORT" http /
123
+ }
124
+
125
+ app_ready() {
126
+ service_is_up "$EMULATOR_BACKEND_PORT" http "/health?db=1" &&
127
+ service_is_up "$EMULATOR_DASHBOARD_PORT" http /handler/sign-in
128
+ }
129
+
130
+ all_ready() {
131
+ deps_ready && app_ready
132
+ }
133
+
134
+ wait_for_condition() {
135
+ local label="$1"
136
+ local timeout="$2"
137
+ local check_fn="$3"
138
+ local started=$SECONDS
139
+ local elapsed=0
140
+
141
+ log "Waiting for ${label}..."
142
+ while [ "$elapsed" -lt "$timeout" ]; do
143
+ if "$check_fn"; then
144
+ echo ""
145
+ log "${label} ready in ${elapsed}s"
146
+ return 0
147
+ fi
148
+ sleep 1
149
+ elapsed=$((SECONDS - started))
150
+ printf "\r [%3ds] %s..." "$elapsed" "$label"
151
+ done
152
+ echo ""
153
+ return 1
154
+ }
155
+
156
+ build_qemu_cmd() {
157
+ local base_img
158
+ base_img="$(image_path)"
159
+
160
+ if [ ! -f "$base_img" ]; then
161
+ err "Missing QEMU image: $base_img"
162
+ err "Run docker/local-emulator/qemu/build-image.sh $ARCH first."
163
+ exit 1
164
+ fi
165
+
166
+ mkdir -p "$VM_DIR"
167
+ local fingerprint_file="$VM_DIR/base-image.fingerprint"
168
+ local current_fp
169
+ current_fp="$(base_image_fingerprint "$base_img")"
170
+ if [ -f "$VM_DIR/disk.qcow2" ]; then
171
+ if [ -f "$fingerprint_file" ] && [ "$(cat "$fingerprint_file")" = "$current_fp" ]; then
172
+ log "Reusing existing overlay disk (changes persist)"
173
+ else
174
+ warn "QEMU base image has changed — recreating overlay."
175
+ rm -f "$VM_DIR/disk.qcow2" "$fingerprint_file"
176
+ fi
177
+ fi
178
+ if [ ! -f "$VM_DIR/disk.qcow2" ]; then
179
+ qemu-img create -f qcow2 -b "$base_img" -F qcow2 "$VM_DIR/disk.qcow2" >/dev/null
180
+ base_image_fingerprint "$base_img" > "$fingerprint_file"
181
+ fi
182
+
183
+ local qemu_bin machine cpu firmware_args=()
184
+ qemu_bin="$(qemu_binary_for_arch "$ARCH")"
185
+ case "$ARCH" in
186
+ arm64)
187
+ machine="virt"
188
+ cpu="max"
189
+ local firmware
190
+ firmware="$(find_aarch64_firmware)"
191
+ firmware_args=(-bios "$firmware")
192
+ ;;
193
+ amd64)
194
+ machine="q35"
195
+ if [ "$ACCEL" = "tcg" ] && [ "$HOST_ARCH" != "amd64" ]; then
196
+ cpu="qemu64"
197
+ else
198
+ cpu="max"
199
+ fi
200
+ ;;
201
+ esac
202
+
203
+ local netdev="user,id=net0"
204
+ # Only expose user-facing services; internal deps stay inside the VM.
205
+ # Bind to 127.0.0.1 so the emulator is not reachable from the LAN.
206
+ netdev+=",hostfwd=tcp:127.0.0.1:${EMULATOR_DASHBOARD_PORT}-:${PORT_PREFIX}01"
207
+ netdev+=",hostfwd=tcp:127.0.0.1:${EMULATOR_BACKEND_PORT}-:${PORT_PREFIX}02"
208
+ netdev+=",hostfwd=tcp:127.0.0.1:${EMULATOR_MINIO_PORT}-:9090"
209
+ netdev+=",hostfwd=tcp:127.0.0.1:${EMULATOR_INBUCKET_PORT}-:9001"
210
+ # Mock OAuth server: browser redirects land on `localhost:${PORT_PREFIX}14`
211
+ # (backend sets STACK_OAUTH_MOCK_URL to that value), so we forward host:port
212
+ # ↔ VM:port on the same number. Collides with pnpm dev, but the two modes
213
+ # are mutually exclusive.
214
+ netdev+=",hostfwd=tcp:127.0.0.1:${PORT_PREFIX}14-:${PORT_PREFIX}14"
215
+
216
+ QEMU_CMD=(
217
+ "$qemu_bin"
218
+ -machine "$machine"
219
+ -accel "$ACCEL"
220
+ -cpu "$cpu"
221
+ "${firmware_args[@]}"
222
+ -boot order=c
223
+ -m "$VM_RAM"
224
+ -smp "$VM_CPUS"
225
+ -drive "file=$VM_DIR/disk.qcow2,format=qcow2,if=virtio"
226
+ -drive "file=$(runtime_iso_path),format=raw,if=virtio,readonly=on"
227
+ -netdev "$netdev"
228
+ -device virtio-net-pci,netdev=net0
229
+ -device virtio-balloon-pci
230
+ -virtfs "local,path=/,mount_tag=hostfs,security_model=none"
231
+ -chardev "socket,id=monitor,path=$VM_DIR/monitor.sock,server=on,wait=off"
232
+ -mon "chardev=monitor,mode=control"
233
+ -serial "file:$VM_DIR/serial.log"
234
+ -display none
235
+ -daemonize
236
+ -pidfile "$VM_DIR/qemu.pid"
237
+ )
238
+
239
+ }
240
+
241
+ is_running() {
242
+ if [ ! -f "$VM_DIR/qemu.pid" ]; then
243
+ return 1
244
+ fi
245
+ local pid
246
+ pid="$(cat "$VM_DIR/qemu.pid")"
247
+ kill -0 "$pid" 2>/dev/null
248
+ }
249
+
250
+ tail_vm_logs() {
251
+ if [ -f "$VM_DIR/serial.log" ]; then
252
+ echo ""
253
+ warn "Last serial log lines:"
254
+ tail -40 "$VM_DIR/serial.log" || true
255
+ fi
256
+ }
257
+
258
+ ensure_ports_free() {
259
+ local ports=("$EMULATOR_DASHBOARD_PORT" "$EMULATOR_BACKEND_PORT" "$EMULATOR_MINIO_PORT" "$EMULATOR_INBUCKET_PORT" "${PORT_PREFIX}14")
260
+ local port
261
+ for port in "${ports[@]}"; do
262
+ if lsof -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1; then
263
+ err "Port $port is already in use. Stop any conflicting services first."
264
+ exit 1
265
+ fi
266
+ done
267
+ }
268
+
269
+ start_vm() {
270
+ mkdir -p "$VM_DIR"
271
+ : > "$VM_DIR/serial.log"
272
+ prepare_runtime_config_iso
273
+ build_qemu_cmd
274
+ "${QEMU_CMD[@]}"
275
+ }
276
+
277
+ stop_vm() {
278
+ if [ ! -f "$VM_DIR/qemu.pid" ]; then
279
+ return 0
280
+ fi
281
+ local pid
282
+ pid="$(cat "$VM_DIR/qemu.pid")"
283
+ if kill -0 "$pid" 2>/dev/null; then
284
+ if [ -S "$VM_DIR/monitor.sock" ]; then
285
+ echo '{"execute":"qmp_capabilities"}' | socat - UNIX-CONNECT:"$VM_DIR/monitor.sock" >/dev/null 2>&1 || true
286
+ echo '{"execute":"system_powerdown"}' | socat - UNIX-CONNECT:"$VM_DIR/monitor.sock" >/dev/null 2>&1 || true
287
+ sleep 3
288
+ fi
289
+ if kill -0 "$pid" 2>/dev/null; then
290
+ kill "$pid" 2>/dev/null || true
291
+ sleep 1
292
+ kill -9 "$pid" 2>/dev/null || true
293
+ fi
294
+ fi
295
+ rm -f "$VM_DIR/qemu.pid" "$VM_DIR/monitor.sock" "$VM_DIR/serial.log"
296
+ rm -rf "$VM_DIR/runtime-config"
297
+ rm -f "$VM_DIR/runtime-config.iso"
298
+ }
299
+
300
+ cmd_start() {
301
+ ensure_ports_free
302
+ mkdir -p "$RUN_DIR"
303
+
304
+ info "Starting QEMU local emulator"
305
+ info "Arch: $ARCH | Accel: $ACCEL"
306
+ info "Ports: Dashboard=$EMULATOR_DASHBOARD_PORT Backend=$EMULATOR_BACKEND_PORT MinIO=$EMULATOR_MINIO_PORT Inbucket=$EMULATOR_INBUCKET_PORT"
307
+
308
+ start_vm
309
+
310
+ info "VM: ${VM_RAM}MB / ${VM_CPUS} CPUs"
311
+
312
+ if ! wait_for_condition "deps services" "$READY_TIMEOUT" deps_ready; then
313
+ tail_vm_logs
314
+ exit 1
315
+ fi
316
+
317
+ if ! wait_for_condition "dashboard/backend" "$READY_TIMEOUT" app_ready; then
318
+ tail_vm_logs
319
+ exit 1
320
+ fi
321
+
322
+ log "All services are green."
323
+ info "Dashboard: http://localhost:${EMULATOR_DASHBOARD_PORT}"
324
+ info "Backend: http://localhost:${EMULATOR_BACKEND_PORT}"
325
+ }
326
+
327
+ cmd_stop() {
328
+ stop_vm
329
+ log "QEMU emulator stopped."
330
+ }
331
+
332
+ cmd_reset() {
333
+ cmd_stop 2>/dev/null || true
334
+ rm -rf "$RUN_DIR"
335
+ log "Emulator state reset. Next start will be a fresh boot."
336
+ }
337
+
338
+ STATUS_FAILED=0
339
+
340
+ print_service_status() {
341
+ local name="$1"
342
+ local port="$2"
343
+ local proto="$3"
344
+ local path="${4:-/}"
345
+ local expected_codes="${5:-200}"
346
+ if service_is_up "$port" "$proto" "$path" "$expected_codes"; then
347
+ echo -e " ${GREEN}●${NC} $name (:$port)"
348
+ else
349
+ echo -e " ${RED}●${NC} $name (:$port)"
350
+ STATUS_FAILED=1
351
+ fi
352
+ }
353
+
354
+ cmd_status() {
355
+ STATUS_FAILED=0
356
+ echo "VM:"
357
+ if is_running; then
358
+ echo -e " ${GREEN}●${NC} emulator"
359
+ else
360
+ echo -e " ${RED}●${NC} emulator"
361
+ STATUS_FAILED=1
362
+ fi
363
+ echo ""
364
+ echo "Services:"
365
+ print_service_status "Dashboard" "$EMULATOR_DASHBOARD_PORT" http /handler/sign-in
366
+ print_service_status "Backend" "$EMULATOR_BACKEND_PORT" http "/health?db=1"
367
+ print_service_status "MinIO" "$EMULATOR_MINIO_PORT" http /minio/health/live
368
+ print_service_status "Inbucket HTTP" "$EMULATOR_INBUCKET_PORT" http /
369
+ exit "$STATUS_FAILED"
370
+ }
371
+
372
+ cmd_bench() {
373
+ local elapsed
374
+ cmd_stop >/dev/null 2>&1 || true
375
+ SECONDS=0
376
+ cmd_start
377
+ elapsed="$SECONDS"
378
+ printf "Startup time: %.1fs\n" "$elapsed"
379
+ }
380
+
381
+ ACTION="start"
382
+
383
+ while [[ $# -gt 0 ]]; do
384
+ case "$1" in
385
+ start|stop|reset|status|bench)
386
+ ACTION="$1"
387
+ shift
388
+ ;;
389
+ *)
390
+ echo "Usage: $0 [start|stop|reset|status|bench]"
391
+ exit 1
392
+ ;;
393
+ esac
394
+ done
395
+
396
+ case "$ACTION" in
397
+ start) cmd_start ;;
398
+ stop) cmd_stop ;;
399
+ reset) cmd_reset ;;
400
+ status) cmd_status ;;
401
+ bench) cmd_bench ;;
402
+ esac