@firstpick/pi-package-webui 0.1.9 → 0.2.1

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