@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.
- package/dist/.env.development +89 -0
- package/dist/emulator/cloud-init/emulator/meta-data +2 -0
- package/dist/emulator/cloud-init/emulator/user-data +615 -0
- package/dist/emulator/common.sh +70 -0
- package/dist/emulator/run-emulator.sh +402 -0
- package/dist/index.js +242 -28
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
|
@@ -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
|