@firstpick/pi-package-webui 0.4.2 → 0.4.4
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/README.md +10 -6
- package/bin/pi-webui.mjs +100 -15
- package/{WEBUI_TUI_NATIVE_PARITY.json → dev/docs/WEBUI_TUI_NATIVE_PARITY.json} +2 -2
- package/package.json +10 -6
- package/public/app.js +1000 -50
- package/public/index.html +20 -5
- package/public/styles.css +322 -56
- package/tests/http-endpoints-harness.test.mjs +2 -0
- package/tests/mobile-static.test.mjs +90 -19
- package/tests/native-parity-harness.test.mjs +1 -1
- package/tests/native-parity.test.mjs +2 -2
- package/tests/remote-auth-settings-harness.test.mjs +81 -0
- package/start-webui.ps1 +0 -368
- package/start-webui.sh +0 -472
package/start-webui.ps1
DELETED
|
@@ -1,368 +0,0 @@
|
|
|
1
|
-
$ErrorActionPreference = "Stop"
|
|
2
|
-
|
|
3
|
-
$PackageName = "@firstpick/pi-package-webui"
|
|
4
|
-
$DefaultHost = "127.0.0.1"
|
|
5
|
-
$DefaultPort = "31415"
|
|
6
|
-
$script:ServerProcess = $null
|
|
7
|
-
|
|
8
|
-
function Write-Stderr {
|
|
9
|
-
param([string]$Message)
|
|
10
|
-
[Console]::Error.WriteLine($Message)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function Get-LaunchCwd {
|
|
14
|
-
$cwd = $env:PI_WEBUI_CWD
|
|
15
|
-
|
|
16
|
-
if ([string]::IsNullOrWhiteSpace($cwd) -or -not (Test-Path -LiteralPath $cwd -PathType Container)) {
|
|
17
|
-
try {
|
|
18
|
-
$cwd = (Get-Location).ProviderPath
|
|
19
|
-
} catch {
|
|
20
|
-
$cwd = $HOME
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if ([string]::IsNullOrWhiteSpace($cwd) -or -not (Test-Path -LiteralPath $cwd -PathType Container)) {
|
|
25
|
-
$cwd = $HOME
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if ([string]::IsNullOrWhiteSpace($cwd) -or -not (Test-Path -LiteralPath $cwd -PathType Container)) {
|
|
29
|
-
throw "Could not determine a valid working directory."
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return $cwd
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function Get-PiManagedPiWebui {
|
|
36
|
-
$node = Get-Command "node" -ErrorAction SilentlyContinue
|
|
37
|
-
if (-not $node) {
|
|
38
|
-
return $null
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
$script = @'
|
|
42
|
-
const { homedir } = require("node:os");
|
|
43
|
-
const { join } = require("node:path");
|
|
44
|
-
|
|
45
|
-
let agentDir = process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
|
|
46
|
-
if (agentDir === "~") {
|
|
47
|
-
agentDir = homedir();
|
|
48
|
-
} else if (agentDir.startsWith("~/") || (process.platform === "win32" && agentDir.startsWith("~\\"))) {
|
|
49
|
-
agentDir = join(homedir(), agentDir.slice(2));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const binName = process.platform === "win32" ? "pi-webui.cmd" : "pi-webui";
|
|
53
|
-
for (const candidate of [
|
|
54
|
-
join(agentDir, "npm", "node_modules", ".bin", binName),
|
|
55
|
-
join(agentDir, "npm", "node_modules", ".bin", "pi-webui"),
|
|
56
|
-
]) {
|
|
57
|
-
process.stdout.write(`${candidate}\n`);
|
|
58
|
-
}
|
|
59
|
-
'@
|
|
60
|
-
|
|
61
|
-
$candidates = @(& $node.Source -e $script 2>$null)
|
|
62
|
-
if ($LASTEXITCODE -ne 0) {
|
|
63
|
-
return $null
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
foreach ($candidate in $candidates) {
|
|
67
|
-
if (-not [string]::IsNullOrWhiteSpace($candidate) -and (Test-Path -LiteralPath $candidate -PathType Leaf)) {
|
|
68
|
-
return $candidate
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return $null
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function Ensure-PiWebui {
|
|
76
|
-
$managed = Get-PiManagedPiWebui
|
|
77
|
-
if ($managed) {
|
|
78
|
-
return $managed
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
$command = Get-Command "pi-webui" -ErrorAction SilentlyContinue
|
|
82
|
-
if ($command) {
|
|
83
|
-
return $command.Source
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
Write-Host "pi-webui is not installed or not available on PATH."
|
|
87
|
-
|
|
88
|
-
$npm = Get-Command "npm" -ErrorAction SilentlyContinue
|
|
89
|
-
if (-not $npm) {
|
|
90
|
-
Write-Stderr "npm is required to install it globally. Install Node.js/npm, then run:`n npm install -g $PackageName"
|
|
91
|
-
exit 1
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (-not [Environment]::UserInteractive) {
|
|
95
|
-
Write-Stderr "Non-interactive shell; refusing to install without confirmation. Run manually:`n npm install -g $PackageName"
|
|
96
|
-
exit 1
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
$answer = Read-Host "Install $PackageName globally now? [y/N]"
|
|
100
|
-
if ($answer -match '^(?i:y|yes)$') {
|
|
101
|
-
& $npm.Source install -g $PackageName
|
|
102
|
-
if ($LASTEXITCODE -ne 0) {
|
|
103
|
-
exit $LASTEXITCODE
|
|
104
|
-
}
|
|
105
|
-
} else {
|
|
106
|
-
Write-Stderr "Aborted. Install later with:`n npm install -g $PackageName"
|
|
107
|
-
exit 1
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
$command = Get-Command "pi-webui" -ErrorAction SilentlyContinue
|
|
111
|
-
if (-not $command) {
|
|
112
|
-
Write-Stderr "Installed, but pi-webui is still not on PATH. Check your npm global bin directory."
|
|
113
|
-
exit 1
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return $command.Source
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function Get-BrowserHostForUrl {
|
|
120
|
-
param([string]$HostName)
|
|
121
|
-
|
|
122
|
-
if ([string]::IsNullOrWhiteSpace($HostName) -or $HostName -eq "0.0.0.0") {
|
|
123
|
-
return "127.0.0.1"
|
|
124
|
-
}
|
|
125
|
-
if ($HostName -eq "::") {
|
|
126
|
-
return "[::1]"
|
|
127
|
-
}
|
|
128
|
-
if ($HostName.StartsWith("[")) {
|
|
129
|
-
return $HostName
|
|
130
|
-
}
|
|
131
|
-
if ($HostName.Contains(":")) {
|
|
132
|
-
return "[$HostName]"
|
|
133
|
-
}
|
|
134
|
-
return $HostName
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function Get-ConnectHostForPort {
|
|
138
|
-
param([string]$HostName)
|
|
139
|
-
|
|
140
|
-
if ([string]::IsNullOrWhiteSpace($HostName) -or $HostName -eq "0.0.0.0") {
|
|
141
|
-
return "127.0.0.1"
|
|
142
|
-
}
|
|
143
|
-
if ($HostName -eq "::") {
|
|
144
|
-
return "::1"
|
|
145
|
-
}
|
|
146
|
-
if ($HostName.StartsWith("[") -and $HostName.EndsWith("]")) {
|
|
147
|
-
return $HostName.Substring(1, $HostName.Length - 2)
|
|
148
|
-
}
|
|
149
|
-
return $HostName
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function Open-WebUrl {
|
|
153
|
-
param([string]$Url)
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
Start-Process $Url | Out-Null
|
|
157
|
-
} catch {
|
|
158
|
-
Write-Warning "Could not open the default browser. Open manually: $Url"
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function Test-HttpOk {
|
|
163
|
-
param([string]$Url)
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
$response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 2 -Method Get
|
|
167
|
-
return ([int]$response.StatusCode -ge 200 -and [int]$response.StatusCode -lt 400)
|
|
168
|
-
} catch {
|
|
169
|
-
return $false
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function Test-WebuiRunning {
|
|
174
|
-
param([string]$Url)
|
|
175
|
-
|
|
176
|
-
$baseUrl = $Url.TrimEnd("/")
|
|
177
|
-
return (Test-HttpOk "${baseUrl}/api/webui-status") -or (Test-HttpOk "${baseUrl}/api/webui-status?detailed=1")
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function Normalize-CwdComparable {
|
|
181
|
-
param([string]$Path)
|
|
182
|
-
|
|
183
|
-
$text = ([string]$Path) -replace '\\', '/'
|
|
184
|
-
if ($text -match '^/[A-Za-z]/') {
|
|
185
|
-
$text = "$($text[1]):$($text.Substring(2))"
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if ($env:OS -eq "Windows_NT") {
|
|
189
|
-
return $text.ToLowerInvariant()
|
|
190
|
-
}
|
|
191
|
-
return $text
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function Get-WebuiUrlForCwd {
|
|
195
|
-
param(
|
|
196
|
-
[string]$Url,
|
|
197
|
-
[string]$Cwd
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
$baseUrl = $Url.TrimEnd("/")
|
|
201
|
-
$targetCwd = Normalize-CwdComparable $Cwd
|
|
202
|
-
|
|
203
|
-
try {
|
|
204
|
-
$tabsResponse = Invoke-RestMethod -Uri "${baseUrl}/api/tabs" -UseBasicParsing -TimeoutSec 5 -Method Get
|
|
205
|
-
$tab = @($tabsResponse.data.tabs) | Where-Object { (Normalize-CwdComparable $_.cwd) -eq $targetCwd } | Select-Object -First 1
|
|
206
|
-
if ($tab -and $tab.id) {
|
|
207
|
-
return "$baseUrl/?tab=$([System.Uri]::EscapeDataString([string]$tab.id))"
|
|
208
|
-
}
|
|
209
|
-
} catch {
|
|
210
|
-
# Fall back to creating a tab below, then to the root URL.
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
try {
|
|
214
|
-
$body = @{ cwd = $Cwd } | ConvertTo-Json -Compress
|
|
215
|
-
$created = Invoke-RestMethod -Uri "${baseUrl}/api/tabs" -UseBasicParsing -TimeoutSec 10 -Method Post -ContentType "application/json" -Body $body
|
|
216
|
-
$id = $created.data.tab.id
|
|
217
|
-
if ($id) {
|
|
218
|
-
return "$baseUrl/?tab=$([System.Uri]::EscapeDataString([string]$id))"
|
|
219
|
-
}
|
|
220
|
-
} catch {
|
|
221
|
-
# Fall back to the root URL.
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return "$baseUrl/"
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function Test-PortInUse {
|
|
228
|
-
param(
|
|
229
|
-
[string]$HostName,
|
|
230
|
-
[string]$Port
|
|
231
|
-
)
|
|
232
|
-
|
|
233
|
-
$portNumber = 0
|
|
234
|
-
if (-not [int]::TryParse($Port, [ref]$portNumber)) {
|
|
235
|
-
return $false
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
$connectHost = Get-ConnectHostForPort $HostName
|
|
239
|
-
$client = $null
|
|
240
|
-
$async = $null
|
|
241
|
-
|
|
242
|
-
try {
|
|
243
|
-
$client = [System.Net.Sockets.TcpClient]::new()
|
|
244
|
-
$async = $client.BeginConnect($connectHost, $portNumber, $null, $null)
|
|
245
|
-
if (-not $async.AsyncWaitHandle.WaitOne(500, $false)) {
|
|
246
|
-
return $false
|
|
247
|
-
}
|
|
248
|
-
$client.EndConnect($async)
|
|
249
|
-
return $client.Connected
|
|
250
|
-
} catch {
|
|
251
|
-
return $false
|
|
252
|
-
} finally {
|
|
253
|
-
if ($async -and $async.AsyncWaitHandle) {
|
|
254
|
-
$async.AsyncWaitHandle.Close()
|
|
255
|
-
}
|
|
256
|
-
if ($client) {
|
|
257
|
-
$client.Close()
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function Wait-UntilReady {
|
|
263
|
-
param(
|
|
264
|
-
[string]$Url,
|
|
265
|
-
[System.Diagnostics.Process]$Process
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
for ($i = 0; $i -lt 50; $i++) {
|
|
269
|
-
$Process.Refresh()
|
|
270
|
-
if ($Process.HasExited) {
|
|
271
|
-
return 2
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (Test-HttpOk $Url) {
|
|
275
|
-
return 0
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
Start-Sleep -Milliseconds 200
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return 1
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function Stop-ServerProcess {
|
|
285
|
-
if ($script:ServerProcess) {
|
|
286
|
-
$script:ServerProcess.Refresh()
|
|
287
|
-
if (-not $script:ServerProcess.HasExited) {
|
|
288
|
-
try {
|
|
289
|
-
Stop-Process -Id $script:ServerProcess.Id -Force -ErrorAction SilentlyContinue
|
|
290
|
-
} catch {
|
|
291
|
-
# Best-effort cleanup only.
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
$cwd = Get-LaunchCwd
|
|
298
|
-
$hostName = if ([string]::IsNullOrWhiteSpace($env:PI_WEBUI_HOST)) { $DefaultHost } else { $env:PI_WEBUI_HOST }
|
|
299
|
-
$port = if ([string]::IsNullOrWhiteSpace($env:PI_WEBUI_PORT)) { $DefaultPort } else { $env:PI_WEBUI_PORT }
|
|
300
|
-
$passThroughArgs = @($args)
|
|
301
|
-
|
|
302
|
-
for ($i = 0; $i -lt $passThroughArgs.Count; $i++) {
|
|
303
|
-
if ($passThroughArgs[$i] -eq "--") {
|
|
304
|
-
break
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
switch ($passThroughArgs[$i]) {
|
|
308
|
-
"--cwd" {
|
|
309
|
-
if ($i + 1 -lt $passThroughArgs.Count) { $cwd = $passThroughArgs[$i + 1] }
|
|
310
|
-
}
|
|
311
|
-
"--host" {
|
|
312
|
-
if ($i + 1 -lt $passThroughArgs.Count) { $hostName = $passThroughArgs[$i + 1] }
|
|
313
|
-
}
|
|
314
|
-
"--port" {
|
|
315
|
-
if ($i + 1 -lt $passThroughArgs.Count) { $port = $passThroughArgs[$i + 1] }
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
$browserHost = Get-BrowserHostForUrl $hostName
|
|
321
|
-
$connectHost = Get-ConnectHostForPort $hostName
|
|
322
|
-
$url = "http://${browserHost}:${port}/"
|
|
323
|
-
|
|
324
|
-
if (Test-WebuiRunning $url) {
|
|
325
|
-
$targetUrl = Get-WebuiUrlForCwd $url $cwd
|
|
326
|
-
Write-Host "Pi Web UI already appears to be running at: $url"
|
|
327
|
-
Write-Host "Opening: $targetUrl"
|
|
328
|
-
Open-WebUrl $targetUrl
|
|
329
|
-
exit 0
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (Test-PortInUse $hostName $port) {
|
|
333
|
-
Write-Stderr "Port $port is already in use on $connectHost; not starting Pi Web UI."
|
|
334
|
-
if (Test-HttpOk $url) {
|
|
335
|
-
Write-Stderr "An HTTP server responded at $url, but it did not expose Pi Web UI status."
|
|
336
|
-
} else {
|
|
337
|
-
Write-Stderr "No Pi Web UI status endpoint responded at $url."
|
|
338
|
-
}
|
|
339
|
-
exit 1
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
$piWebuiCommand = Ensure-PiWebui
|
|
343
|
-
$webuiArgs = @("--cwd", $cwd, "--host", $hostName, "--port", [string]$port) + $passThroughArgs
|
|
344
|
-
|
|
345
|
-
Write-Host "Starting Pi Web UI in: $cwd"
|
|
346
|
-
Write-Host "Web UI URL: $url"
|
|
347
|
-
|
|
348
|
-
try {
|
|
349
|
-
$script:ServerProcess = Start-Process -FilePath $piWebuiCommand -ArgumentList $webuiArgs -NoNewWindow -PassThru
|
|
350
|
-
$readyStatus = Wait-UntilReady $url $script:ServerProcess
|
|
351
|
-
|
|
352
|
-
if ($readyStatus -eq 0) {
|
|
353
|
-
Open-WebUrl $url
|
|
354
|
-
} elseif ($readyStatus -eq 2) {
|
|
355
|
-
Write-Stderr "Pi Web UI exited before it became ready."
|
|
356
|
-
$script:ServerProcess.Refresh()
|
|
357
|
-
exit $script:ServerProcess.ExitCode
|
|
358
|
-
} else {
|
|
359
|
-
Write-Warning "Server did not respond yet; opening the URL anyway."
|
|
360
|
-
Open-WebUrl $url
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
Wait-Process -Id $script:ServerProcess.Id
|
|
364
|
-
$script:ServerProcess.Refresh()
|
|
365
|
-
exit $script:ServerProcess.ExitCode
|
|
366
|
-
} finally {
|
|
367
|
-
Stop-ServerProcess
|
|
368
|
-
}
|