@firstpick/pi-package-webui 0.1.9 → 0.2.0

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/start-webui.sh ADDED
@@ -0,0 +1,461 @@
1
+ #!/usr/bin/env bash
2
+ # shellcheck disable=SC2016
3
+ set -euo pipefail
4
+
5
+ PACKAGE_NAME="@firstpick/pi-package-webui"
6
+ DEFAULT_HOST="127.0.0.1"
7
+ DEFAULT_PORT="31415"
8
+ SERVER_PID=""
9
+
10
+ script_dir() {
11
+ local source dir
12
+ source="${BASH_SOURCE[0]}"
13
+
14
+ while [[ -L "$source" ]]; do
15
+ dir="$(cd -P "$(dirname "$source")" >/dev/null 2>&1 && pwd)"
16
+ source="$(readlink "$source")"
17
+ [[ "$source" != /* ]] && source="$dir/$source"
18
+ done
19
+
20
+ cd -P "$(dirname "$source")" >/dev/null 2>&1 && pwd
21
+ }
22
+
23
+ local_pi_webui_bin() {
24
+ local candidate
25
+ candidate="$(script_dir)/bin/pi-webui.mjs"
26
+
27
+ if [[ ! -f "$candidate" ]]; then
28
+ echo "--dev expected the local Pi Web UI server at: $candidate" >&2
29
+ return 1
30
+ fi
31
+
32
+ if ! command -v node >/dev/null 2>&1; then
33
+ echo "node is required to run the local Pi Web UI server in --dev mode." >&2
34
+ return 1
35
+ fi
36
+
37
+ printf '%s\n' "$candidate"
38
+ }
39
+
40
+ cleanup() {
41
+ if [[ -n "${SERVER_PID:-}" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then
42
+ kill "$SERVER_PID" 2>/dev/null || true
43
+ fi
44
+ }
45
+
46
+ choose_cwd() {
47
+ local cwd="${PI_WEBUI_CWD:-${PWD:-}}"
48
+
49
+ if [[ -z "$cwd" || ! -d "$cwd" ]]; then
50
+ cwd="${HOME:-}"
51
+ fi
52
+
53
+ if [[ -z "$cwd" || ! -d "$cwd" ]]; then
54
+ cwd="$(pwd -P)"
55
+ fi
56
+
57
+ case "$(uname -s 2>/dev/null || true)" in
58
+ MINGW*|MSYS*|CYGWIN*)
59
+ if command -v cygpath >/dev/null 2>&1; then
60
+ cwd="$(cygpath -m "$cwd")"
61
+ fi
62
+ ;;
63
+ esac
64
+
65
+ printf '%s\n' "$cwd"
66
+ }
67
+
68
+ ensure_pi_webui() {
69
+ if command -v pi-webui >/dev/null 2>&1; then
70
+ return 0
71
+ fi
72
+
73
+ echo "pi-webui is not installed or not available on PATH."
74
+
75
+ if ! command -v npm >/dev/null 2>&1; then
76
+ echo "npm is required to install it globally. Install Node.js/npm, then run:" >&2
77
+ echo " npm install -g $PACKAGE_NAME" >&2
78
+ return 1
79
+ fi
80
+
81
+ if [[ ! -t 0 ]]; then
82
+ echo "Non-interactive shell; refusing to install without confirmation." >&2
83
+ echo "Run manually:" >&2
84
+ echo " npm install -g $PACKAGE_NAME" >&2
85
+ return 1
86
+ fi
87
+
88
+ local answer=""
89
+ if ! read -r -p "Install $PACKAGE_NAME globally now? [y/N] " answer; then
90
+ answer=""
91
+ fi
92
+
93
+ case "$answer" in
94
+ y|Y|yes|YES|Yes)
95
+ npm install -g "$PACKAGE_NAME"
96
+ ;;
97
+ *)
98
+ echo "Aborted. Install later with:" >&2
99
+ echo " npm install -g $PACKAGE_NAME" >&2
100
+ return 1
101
+ ;;
102
+ esac
103
+
104
+ if ! command -v pi-webui >/dev/null 2>&1; then
105
+ echo "Installed, but pi-webui is still not on PATH. Check your npm global bin directory." >&2
106
+ return 1
107
+ fi
108
+ }
109
+
110
+ browser_host_for_url() {
111
+ local host="$1"
112
+
113
+ case "$host" in
114
+ ""|"0.0.0.0") printf '%s\n' "127.0.0.1" ;;
115
+ "::") printf '%s\n' "[::1]" ;;
116
+ \[*\]) printf '%s\n' "$host" ;;
117
+ *:*) printf '[%s]\n' "$host" ;;
118
+ *) printf '%s\n' "$host" ;;
119
+ esac
120
+ }
121
+
122
+ connect_host_for_port() {
123
+ local host="$1"
124
+
125
+ case "$host" in
126
+ ""|"0.0.0.0") printf '%s\n' "127.0.0.1" ;;
127
+ "::") printf '%s\n' "::1" ;;
128
+ \[*\])
129
+ host="${host#\[}"
130
+ host="${host%\]}"
131
+ printf '%s\n' "$host"
132
+ ;;
133
+ *) printf '%s\n' "$host" ;;
134
+ esac
135
+ }
136
+
137
+ open_url() {
138
+ local url="$1"
139
+ local platform=""
140
+ platform="$(uname -s 2>/dev/null || true)"
141
+
142
+ case "$platform" in
143
+ MINGW*|MSYS*|CYGWIN*)
144
+ if command -v cmd.exe >/dev/null 2>&1; then
145
+ cmd.exe /c start "" "$url" </dev/null >/dev/null 2>&1 &
146
+ return 0
147
+ fi
148
+ if command -v powershell.exe >/dev/null 2>&1; then
149
+ powershell.exe -NoProfile -Command 'Start-Process -FilePath $args[0]' "$url" </dev/null >/dev/null 2>&1 &
150
+ return 0
151
+ fi
152
+ ;;
153
+ Linux*)
154
+ if grep -qi microsoft /proc/version 2>/dev/null; then
155
+ if command -v wslview >/dev/null 2>&1; then
156
+ wslview "$url" </dev/null >/dev/null 2>&1 &
157
+ return 0
158
+ fi
159
+ if command -v cmd.exe >/dev/null 2>&1; then
160
+ cmd.exe /c start "" "$url" </dev/null >/dev/null 2>&1 &
161
+ return 0
162
+ fi
163
+ fi
164
+ ;;
165
+ esac
166
+
167
+ if command -v xdg-open >/dev/null 2>&1; then
168
+ xdg-open "$url" </dev/null >/dev/null 2>&1 &
169
+ elif command -v open >/dev/null 2>&1; then
170
+ open "$url" </dev/null >/dev/null 2>&1 &
171
+ elif command -v wslview >/dev/null 2>&1; then
172
+ wslview "$url" </dev/null >/dev/null 2>&1 &
173
+ elif command -v cmd.exe >/dev/null 2>&1; then
174
+ cmd.exe /c start "" "$url" </dev/null >/dev/null 2>&1 &
175
+ elif command -v powershell.exe >/dev/null 2>&1; then
176
+ powershell.exe -NoProfile -Command 'Start-Process -FilePath $args[0]' "$url" </dev/null >/dev/null 2>&1 &
177
+ else
178
+ echo "Could not find a browser opener. Open manually:" >&2
179
+ echo " $url" >&2
180
+ return 1
181
+ fi
182
+ }
183
+
184
+ http_ok() {
185
+ local url="$1"
186
+
187
+ if command -v curl >/dev/null 2>&1; then
188
+ curl -fsS --max-time 2 "$url" >/dev/null 2>&1
189
+ elif command -v wget >/dev/null 2>&1; then
190
+ wget -q --timeout=2 --tries=1 --spider "$url" >/dev/null 2>&1
191
+ else
192
+ return 1
193
+ fi
194
+ }
195
+
196
+ webui_is_running() {
197
+ local base_url="${1%/}"
198
+
199
+ http_ok "$base_url/api/webui-status" || http_ok "$base_url/api/webui-status?detailed=1"
200
+ }
201
+
202
+ http_get() {
203
+ local url="$1"
204
+
205
+ if command -v curl >/dev/null 2>&1; then
206
+ curl -fsS --max-time 5 "$url"
207
+ elif command -v wget >/dev/null 2>&1; then
208
+ wget -q --timeout=5 --tries=1 -O - "$url"
209
+ else
210
+ return 1
211
+ fi
212
+ }
213
+
214
+ http_post_json() {
215
+ local url="$1"
216
+ local body="$2"
217
+
218
+ if command -v curl >/dev/null 2>&1; then
219
+ curl -fsS --max-time 10 -X POST "$url" -H "Content-Type: application/json" --data "$body"
220
+ else
221
+ return 1
222
+ fi
223
+ }
224
+
225
+ json_quote() {
226
+ local value="$1"
227
+
228
+ if command -v node >/dev/null 2>&1; then
229
+ node -e 'process.stdout.write(JSON.stringify(process.argv[1] ?? ""))' "$value"
230
+ elif command -v python3 >/dev/null 2>&1; then
231
+ python3 -c 'import json,sys; print(json.dumps(sys.argv[1]), end="")' "$value"
232
+ else
233
+ return 1
234
+ fi
235
+ }
236
+
237
+ extract_tab_id_for_cwd() {
238
+ local cwd="$1"
239
+
240
+ if command -v node >/dev/null 2>&1; then
241
+ node -e '
242
+ const fs = require("fs");
243
+ const data = JSON.parse(fs.readFileSync(0, "utf8"));
244
+ const target = normalize(process.argv[1]);
245
+ const tabs = data?.data?.tabs || [];
246
+ const tab = tabs.find((item) => normalize(item?.cwd) === target);
247
+ if (tab?.id) process.stdout.write(String(tab.id));
248
+ function normalize(value) {
249
+ let text = String(value || "").replace(/\\/g, "/");
250
+ if (/^\/[a-zA-Z]\//.test(text)) text = `${text[1]}:${text.slice(2)}`;
251
+ return process.platform === "win32" ? text.toLowerCase() : text;
252
+ }
253
+ ' "$cwd"
254
+ else
255
+ return 1
256
+ fi
257
+ }
258
+
259
+ extract_created_tab_id() {
260
+ if command -v node >/dev/null 2>&1; then
261
+ node -e '
262
+ const fs = require("fs");
263
+ const data = JSON.parse(fs.readFileSync(0, "utf8"));
264
+ const id = data?.data?.tab?.id;
265
+ if (id) process.stdout.write(String(id));
266
+ '
267
+ else
268
+ return 1
269
+ fi
270
+ }
271
+
272
+ webui_url_for_cwd() {
273
+ local base_url cwd tabs_json tab_id json_cwd body created_json
274
+ base_url="${1%/}"
275
+ cwd="$2"
276
+
277
+ if tabs_json="$(http_get "$base_url/api/tabs" 2>/dev/null)"; then
278
+ tab_id="$(printf '%s' "$tabs_json" | extract_tab_id_for_cwd "$cwd" 2>/dev/null || true)"
279
+ if [[ -n "$tab_id" ]]; then
280
+ printf '%s/?tab=%s\n' "$base_url" "$tab_id"
281
+ return 0
282
+ fi
283
+ fi
284
+
285
+ if json_cwd="$(json_quote "$cwd" 2>/dev/null)"; then
286
+ body="{\"cwd\":$json_cwd}"
287
+ if created_json="$(http_post_json "$base_url/api/tabs" "$body" 2>/dev/null)"; then
288
+ tab_id="$(printf '%s' "$created_json" | extract_created_tab_id 2>/dev/null || true)"
289
+ if [[ -n "$tab_id" ]]; then
290
+ printf '%s/?tab=%s\n' "$base_url" "$tab_id"
291
+ return 0
292
+ fi
293
+ fi
294
+ fi
295
+
296
+ printf '%s/\n' "$base_url"
297
+ }
298
+
299
+ port_is_in_use() {
300
+ local host port
301
+ host="$(connect_host_for_port "$1")"
302
+ port="$2"
303
+
304
+ if command -v nc >/dev/null 2>&1 && nc -z "$host" "$port" >/dev/null 2>&1; then
305
+ return 0
306
+ fi
307
+
308
+ if command -v lsof >/dev/null 2>&1 && lsof -iTCP:"$port" -sTCP:LISTEN -Pn >/dev/null 2>&1; then
309
+ return 0
310
+ fi
311
+
312
+ if command -v ss >/dev/null 2>&1 && ss -ltn 2>/dev/null | awk -v port="$port" 'NR > 1 { split($4, parts, ":"); if (parts[length(parts)] == port) found = 1 } END { exit(found ? 0 : 1) }'; then
313
+ return 0
314
+ fi
315
+
316
+ if [[ "$host" != *:* ]] && (echo >"/dev/tcp/$host/$port") >/dev/null 2>&1; then
317
+ return 0
318
+ fi
319
+
320
+ if command -v netstat >/dev/null 2>&1 && netstat -an 2>/dev/null | grep -E "[.:]${port}[[:space:]]" >/dev/null 2>&1; then
321
+ return 0
322
+ fi
323
+
324
+ return 1
325
+ }
326
+
327
+ wait_until_ready() {
328
+ local url="$1"
329
+ local pid="$2"
330
+
331
+ if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then
332
+ sleep 1
333
+ return 0
334
+ fi
335
+
336
+ for _ in {1..50}; do
337
+ if ! kill -0 "$pid" 2>/dev/null; then
338
+ return 2
339
+ fi
340
+
341
+ http_ok "$url" && return 0
342
+ sleep 0.2
343
+ done
344
+
345
+ return 1
346
+ }
347
+
348
+ main() {
349
+ local cwd host port browser_host connect_host url target_url i ready_status dev_mode local_webui_bin
350
+ local args=("$@")
351
+ local pass_args=()
352
+ local webui_cmd=()
353
+ cwd="$(choose_cwd)"
354
+ host="${PI_WEBUI_HOST:-$DEFAULT_HOST}"
355
+ port="${PI_WEBUI_PORT:-$DEFAULT_PORT}"
356
+ dev_mode=0
357
+
358
+ for ((i = 0; i < ${#args[@]}; i++)); do
359
+ case "${args[$i]}" in
360
+ --)
361
+ pass_args+=("${args[@]:$i}")
362
+ break
363
+ ;;
364
+ --dev)
365
+ dev_mode=1
366
+ ;;
367
+ --cwd)
368
+ if ((i + 1 < ${#args[@]})); then
369
+ cwd="${args[$((i + 1))]}"
370
+ pass_args+=("${args[$i]}" "${args[$((i + 1))]}")
371
+ ((i += 1))
372
+ else
373
+ pass_args+=("${args[$i]}")
374
+ fi
375
+ ;;
376
+ --host)
377
+ if ((i + 1 < ${#args[@]})); then
378
+ host="${args[$((i + 1))]}"
379
+ pass_args+=("${args[$i]}" "${args[$((i + 1))]}")
380
+ ((i += 1))
381
+ else
382
+ pass_args+=("${args[$i]}")
383
+ fi
384
+ ;;
385
+ --port)
386
+ if ((i + 1 < ${#args[@]})); then
387
+ port="${args[$((i + 1))]}"
388
+ pass_args+=("${args[$i]}" "${args[$((i + 1))]}")
389
+ ((i += 1))
390
+ else
391
+ pass_args+=("${args[$i]}")
392
+ fi
393
+ ;;
394
+ *)
395
+ pass_args+=("${args[$i]}")
396
+ ;;
397
+ esac
398
+ done
399
+
400
+ browser_host="$(browser_host_for_url "$host")"
401
+ connect_host="$(connect_host_for_port "$host")"
402
+ url="http://$browser_host:$port/"
403
+
404
+ if webui_is_running "$url"; then
405
+ target_url="$(webui_url_for_cwd "$url" "$cwd")"
406
+ echo "Pi Web UI already appears to be running at: $url"
407
+ if [[ "$dev_mode" -eq 1 ]]; then
408
+ echo "--dev only affects newly started servers; stop the existing server first to run this checkout."
409
+ fi
410
+ echo "Opening: $target_url"
411
+ open_url "$target_url" || true
412
+ exit 0
413
+ fi
414
+
415
+ if port_is_in_use "$host" "$port"; then
416
+ echo "Port $port is already in use on $connect_host; not starting Pi Web UI." >&2
417
+ if http_ok "$url"; then
418
+ echo "An HTTP server responded at $url, but it did not expose Pi Web UI status." >&2
419
+ else
420
+ echo "No Pi Web UI status endpoint responded at $url." >&2
421
+ fi
422
+ exit 1
423
+ fi
424
+
425
+ if [[ "$dev_mode" -eq 1 ]]; then
426
+ local_webui_bin="$(local_pi_webui_bin)"
427
+ webui_cmd=(node "$local_webui_bin")
428
+ echo "Dev mode: using local Pi Web UI server: $local_webui_bin"
429
+ else
430
+ ensure_pi_webui
431
+ webui_cmd=(pi-webui)
432
+ fi
433
+
434
+ echo "Starting Pi Web UI in: $cwd"
435
+ echo "Web UI URL: $url"
436
+
437
+ "${webui_cmd[@]}" --cwd "$cwd" --host "$host" --port "$port" "${pass_args[@]}" &
438
+ SERVER_PID="$!"
439
+
440
+ trap cleanup EXIT
441
+ trap 'cleanup; exit 130' INT
442
+ trap 'cleanup; exit 143' TERM
443
+
444
+ if wait_until_ready "$url" "$SERVER_PID"; then
445
+ open_url "$url" || true
446
+ else
447
+ ready_status="$?"
448
+ if [[ "$ready_status" -eq 2 ]]; then
449
+ echo "Pi Web UI exited before it became ready." >&2
450
+ wait "$SERVER_PID"
451
+ exit $?
452
+ fi
453
+
454
+ echo "Server did not respond yet; opening the URL anyway." >&2
455
+ open_url "$url" || true
456
+ fi
457
+
458
+ wait "$SERVER_PID"
459
+ }
460
+
461
+ main "$@"