@firstpick/pi-package-webui 0.4.3 → 0.4.5

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 DELETED
@@ -1,472 +0,0 @@
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
- print_manual_url() {
187
- local url="$1"
188
-
189
- echo "Open manually: $url"
190
- }
191
-
192
- http_ok() {
193
- local url="$1"
194
-
195
- if command -v curl >/dev/null 2>&1; then
196
- curl -fsS --max-time 2 "$url" >/dev/null 2>&1
197
- elif command -v wget >/dev/null 2>&1; then
198
- wget -q --timeout=2 --tries=1 --spider "$url" >/dev/null 2>&1
199
- elif command -v node >/dev/null 2>&1; then
200
- node -e 'fetch(process.argv[1], { signal: AbortSignal.timeout(2000) }).then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1));' "$url" >/dev/null 2>&1
201
- else
202
- return 1
203
- fi
204
- }
205
-
206
- webui_is_running() {
207
- local base_url="${1%/}"
208
-
209
- http_ok "$base_url/api/webui-status" || http_ok "$base_url/api/webui-status?detailed=1"
210
- }
211
-
212
- http_get() {
213
- local url="$1"
214
-
215
- if command -v curl >/dev/null 2>&1; then
216
- curl -fsS --max-time 5 "$url"
217
- elif command -v wget >/dev/null 2>&1; then
218
- wget -q --timeout=5 --tries=1 -O - "$url"
219
- elif command -v node >/dev/null 2>&1; then
220
- node -e 'fetch(process.argv[1], { signal: AbortSignal.timeout(5000) }).then(async (r) => { if (!r.ok) process.exit(1); process.stdout.write(await r.text()); }, () => process.exit(1));' "$url"
221
- else
222
- return 1
223
- fi
224
- }
225
-
226
- http_post_json() {
227
- local url="$1"
228
- local body="$2"
229
-
230
- if command -v curl >/dev/null 2>&1; then
231
- curl -fsS --max-time 10 -X POST "$url" -H "Content-Type: application/json" --data "$body"
232
- elif command -v node >/dev/null 2>&1; then
233
- node -e 'fetch(process.argv[1], { method: "POST", headers: { "Content-Type": "application/json" }, body: process.argv[2], signal: AbortSignal.timeout(10000) }).then(async (r) => { if (!r.ok) process.exit(1); process.stdout.write(await r.text()); }, () => process.exit(1));' "$url" "$body"
234
- else
235
- return 1
236
- fi
237
- }
238
-
239
- json_quote() {
240
- local value="$1"
241
-
242
- if command -v node >/dev/null 2>&1; then
243
- node -e 'process.stdout.write(JSON.stringify(process.argv[1] ?? ""))' "$value"
244
- elif command -v python3 >/dev/null 2>&1; then
245
- python3 -c 'import json,sys; print(json.dumps(sys.argv[1]), end="")' "$value"
246
- else
247
- return 1
248
- fi
249
- }
250
-
251
- extract_tab_id_for_cwd() {
252
- local cwd="$1"
253
-
254
- if command -v node >/dev/null 2>&1; then
255
- node -e '
256
- const fs = require("fs");
257
- const data = JSON.parse(fs.readFileSync(0, "utf8"));
258
- const target = normalize(process.argv[1]);
259
- const tabs = data?.data?.tabs || [];
260
- const tab = tabs.find((item) => normalize(item?.cwd) === target);
261
- if (tab?.id) process.stdout.write(String(tab.id));
262
- function normalize(value) {
263
- let text = String(value || "").replace(/\\/g, "/");
264
- if (/^\/[a-zA-Z]\//.test(text)) text = `${text[1]}:${text.slice(2)}`;
265
- return process.platform === "win32" ? text.toLowerCase() : text;
266
- }
267
- ' "$cwd"
268
- else
269
- return 1
270
- fi
271
- }
272
-
273
- extract_created_tab_id() {
274
- if command -v node >/dev/null 2>&1; then
275
- node -e '
276
- const fs = require("fs");
277
- const data = JSON.parse(fs.readFileSync(0, "utf8"));
278
- const id = data?.data?.tab?.id;
279
- if (id) process.stdout.write(String(id));
280
- '
281
- else
282
- return 1
283
- fi
284
- }
285
-
286
- webui_url_for_cwd() {
287
- local base_url cwd tabs_json tab_id json_cwd body created_json
288
- base_url="${1%/}"
289
- cwd="$2"
290
-
291
- if tabs_json="$(http_get "$base_url/api/tabs" 2>/dev/null)"; then
292
- tab_id="$(printf '%s' "$tabs_json" | extract_tab_id_for_cwd "$cwd" 2>/dev/null || true)"
293
- if [[ -n "$tab_id" ]]; then
294
- printf '%s/?tab=%s\n' "$base_url" "$tab_id"
295
- return 0
296
- fi
297
- fi
298
-
299
- if json_cwd="$(json_quote "$cwd" 2>/dev/null)"; then
300
- body="{\"cwd\":$json_cwd}"
301
- if created_json="$(http_post_json "$base_url/api/tabs" "$body" 2>/dev/null)"; then
302
- tab_id="$(printf '%s' "$created_json" | extract_created_tab_id 2>/dev/null || true)"
303
- if [[ -n "$tab_id" ]]; then
304
- printf '%s/?tab=%s\n' "$base_url" "$tab_id"
305
- return 0
306
- fi
307
- fi
308
- fi
309
-
310
- printf '%s/\n' "$base_url"
311
- }
312
-
313
- port_is_in_use() {
314
- local host port
315
- host="$(connect_host_for_port "$1")"
316
- port="$2"
317
-
318
- if command -v nc >/dev/null 2>&1 && nc -z "$host" "$port" >/dev/null 2>&1; then
319
- return 0
320
- fi
321
-
322
- if command -v lsof >/dev/null 2>&1 && lsof -iTCP:"$port" -sTCP:LISTEN -Pn >/dev/null 2>&1; then
323
- return 0
324
- fi
325
-
326
- 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
327
- return 0
328
- fi
329
-
330
- if [[ "$host" != *:* ]] && (echo >"/dev/tcp/$host/$port") >/dev/null 2>&1; then
331
- return 0
332
- fi
333
-
334
- if command -v netstat >/dev/null 2>&1 && netstat -an 2>/dev/null | grep -E "[.:]${port}[[:space:]]" >/dev/null 2>&1; then
335
- return 0
336
- fi
337
-
338
- return 1
339
- }
340
-
341
- wait_until_ready() {
342
- local url="$1"
343
- local pid="$2"
344
-
345
- for _ in {1..50}; do
346
- if ! kill -0 "$pid" 2>/dev/null; then
347
- return 2
348
- fi
349
-
350
- http_ok "$url" && return 0
351
- sleep 0.2
352
- done
353
-
354
- return 1
355
- }
356
-
357
- main() {
358
- local cwd host port browser_host connect_host url target_url i ready_status dev_mode local_webui_bin
359
- local args=("$@")
360
- local pass_args=()
361
- local webui_cmd=()
362
- cwd="$(choose_cwd)"
363
- host="${PI_WEBUI_HOST:-$DEFAULT_HOST}"
364
- port="${PI_WEBUI_PORT:-$DEFAULT_PORT}"
365
- dev_mode=0
366
-
367
- for ((i = 0; i < ${#args[@]}; i++)); do
368
- case "${args[$i]}" in
369
- --)
370
- pass_args+=("${args[@]:$i}")
371
- break
372
- ;;
373
- --dev)
374
- dev_mode=1
375
- ;;
376
- --cwd)
377
- if ((i + 1 < ${#args[@]})); then
378
- cwd="${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
- --host)
386
- if ((i + 1 < ${#args[@]})); then
387
- host="${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
- --port)
395
- if ((i + 1 < ${#args[@]})); then
396
- port="${args[$((i + 1))]}"
397
- pass_args+=("${args[$i]}" "${args[$((i + 1))]}")
398
- ((i += 1))
399
- else
400
- pass_args+=("${args[$i]}")
401
- fi
402
- ;;
403
- *)
404
- pass_args+=("${args[$i]}")
405
- ;;
406
- esac
407
- done
408
-
409
- browser_host="$(browser_host_for_url "$host")"
410
- connect_host="$(connect_host_for_port "$host")"
411
- url="http://$browser_host:$port/"
412
-
413
- if webui_is_running "$url"; then
414
- target_url="$(webui_url_for_cwd "$url" "$cwd")"
415
- echo "Pi Web UI already appears to be running at: $url"
416
- if [[ "$dev_mode" -eq 1 ]]; then
417
- echo "--dev only affects newly started servers; stop the existing server first to run this checkout."
418
- fi
419
- print_manual_url "$target_url"
420
- exit 0
421
- fi
422
-
423
- if port_is_in_use "$host" "$port"; then
424
- echo "Port $port is already in use on $connect_host; not starting Pi Web UI." >&2
425
- if http_ok "$url"; then
426
- echo "An HTTP server responded at $url, but it did not expose Pi Web UI status." >&2
427
- else
428
- echo "No Pi Web UI status endpoint responded at $url." >&2
429
- fi
430
- exit 1
431
- fi
432
-
433
- if [[ "$dev_mode" -eq 1 ]]; then
434
- local_webui_bin="$(local_pi_webui_bin)"
435
- webui_cmd=(node "$local_webui_bin")
436
- export PI_WEBUI_DEV=1
437
- echo "Dev mode: using local Pi Web UI server: $local_webui_bin"
438
- else
439
- ensure_pi_webui
440
- webui_cmd=("$PI_WEBUI_COMMAND")
441
- unset PI_WEBUI_DEV
442
- fi
443
-
444
- echo "Starting Pi Web UI in: $cwd"
445
- echo "Web UI URL: $url"
446
-
447
- "${webui_cmd[@]}" --cwd "$cwd" --host "$host" --port "$port" "${pass_args[@]}" &
448
- SERVER_PID="$!"
449
-
450
- trap cleanup EXIT
451
- trap 'cleanup; exit 130' INT
452
- trap 'cleanup; exit 143' TERM
453
-
454
- if wait_until_ready "$url" "$SERVER_PID"; then
455
- echo "Pi Web UI is ready."
456
- print_manual_url "$url"
457
- else
458
- ready_status="$?"
459
- if [[ "$ready_status" -eq 2 ]]; then
460
- echo "Pi Web UI exited before it became ready." >&2
461
- wait "$SERVER_PID"
462
- exit $?
463
- fi
464
-
465
- echo "Server did not respond yet; not opening a browser automatically." >&2
466
- print_manual_url "$url"
467
- fi
468
-
469
- wait "$SERVER_PID"
470
- }
471
-
472
- main "$@"