@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.
@@ -0,0 +1,323 @@
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 Ensure-PiWebui {
36
+ $command = Get-Command "pi-webui" -ErrorAction SilentlyContinue
37
+ if ($command) {
38
+ return $command.Source
39
+ }
40
+
41
+ Write-Host "pi-webui is not installed or not available on PATH."
42
+
43
+ $npm = Get-Command "npm" -ErrorAction SilentlyContinue
44
+ if (-not $npm) {
45
+ Write-Stderr "npm is required to install it globally. Install Node.js/npm, then run:`n npm install -g $PackageName"
46
+ exit 1
47
+ }
48
+
49
+ if (-not [Environment]::UserInteractive) {
50
+ Write-Stderr "Non-interactive shell; refusing to install without confirmation. Run manually:`n npm install -g $PackageName"
51
+ exit 1
52
+ }
53
+
54
+ $answer = Read-Host "Install $PackageName globally now? [y/N]"
55
+ if ($answer -match '^(?i:y|yes)$') {
56
+ & $npm.Source install -g $PackageName
57
+ if ($LASTEXITCODE -ne 0) {
58
+ exit $LASTEXITCODE
59
+ }
60
+ } else {
61
+ Write-Stderr "Aborted. Install later with:`n npm install -g $PackageName"
62
+ exit 1
63
+ }
64
+
65
+ $command = Get-Command "pi-webui" -ErrorAction SilentlyContinue
66
+ if (-not $command) {
67
+ Write-Stderr "Installed, but pi-webui is still not on PATH. Check your npm global bin directory."
68
+ exit 1
69
+ }
70
+
71
+ return $command.Source
72
+ }
73
+
74
+ function Get-BrowserHostForUrl {
75
+ param([string]$HostName)
76
+
77
+ if ([string]::IsNullOrWhiteSpace($HostName) -or $HostName -eq "0.0.0.0") {
78
+ return "127.0.0.1"
79
+ }
80
+ if ($HostName -eq "::") {
81
+ return "[::1]"
82
+ }
83
+ if ($HostName.StartsWith("[")) {
84
+ return $HostName
85
+ }
86
+ if ($HostName.Contains(":")) {
87
+ return "[$HostName]"
88
+ }
89
+ return $HostName
90
+ }
91
+
92
+ function Get-ConnectHostForPort {
93
+ param([string]$HostName)
94
+
95
+ if ([string]::IsNullOrWhiteSpace($HostName) -or $HostName -eq "0.0.0.0") {
96
+ return "127.0.0.1"
97
+ }
98
+ if ($HostName -eq "::") {
99
+ return "::1"
100
+ }
101
+ if ($HostName.StartsWith("[") -and $HostName.EndsWith("]")) {
102
+ return $HostName.Substring(1, $HostName.Length - 2)
103
+ }
104
+ return $HostName
105
+ }
106
+
107
+ function Open-WebUrl {
108
+ param([string]$Url)
109
+
110
+ try {
111
+ Start-Process $Url | Out-Null
112
+ } catch {
113
+ Write-Warning "Could not open the default browser. Open manually: $Url"
114
+ }
115
+ }
116
+
117
+ function Test-HttpOk {
118
+ param([string]$Url)
119
+
120
+ try {
121
+ $response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 2 -Method Get
122
+ return ([int]$response.StatusCode -ge 200 -and [int]$response.StatusCode -lt 400)
123
+ } catch {
124
+ return $false
125
+ }
126
+ }
127
+
128
+ function Test-WebuiRunning {
129
+ param([string]$Url)
130
+
131
+ $baseUrl = $Url.TrimEnd("/")
132
+ return (Test-HttpOk "${baseUrl}/api/webui-status") -or (Test-HttpOk "${baseUrl}/api/webui-status?detailed=1")
133
+ }
134
+
135
+ function Normalize-CwdComparable {
136
+ param([string]$Path)
137
+
138
+ $text = ([string]$Path) -replace '\\', '/'
139
+ if ($text -match '^/[A-Za-z]/') {
140
+ $text = "$($text[1]):$($text.Substring(2))"
141
+ }
142
+
143
+ if ($env:OS -eq "Windows_NT") {
144
+ return $text.ToLowerInvariant()
145
+ }
146
+ return $text
147
+ }
148
+
149
+ function Get-WebuiUrlForCwd {
150
+ param(
151
+ [string]$Url,
152
+ [string]$Cwd
153
+ )
154
+
155
+ $baseUrl = $Url.TrimEnd("/")
156
+ $targetCwd = Normalize-CwdComparable $Cwd
157
+
158
+ try {
159
+ $tabsResponse = Invoke-RestMethod -Uri "${baseUrl}/api/tabs" -UseBasicParsing -TimeoutSec 5 -Method Get
160
+ $tab = @($tabsResponse.data.tabs) | Where-Object { (Normalize-CwdComparable $_.cwd) -eq $targetCwd } | Select-Object -First 1
161
+ if ($tab -and $tab.id) {
162
+ return "$baseUrl/?tab=$([System.Uri]::EscapeDataString([string]$tab.id))"
163
+ }
164
+ } catch {
165
+ # Fall back to creating a tab below, then to the root URL.
166
+ }
167
+
168
+ try {
169
+ $body = @{ cwd = $Cwd } | ConvertTo-Json -Compress
170
+ $created = Invoke-RestMethod -Uri "${baseUrl}/api/tabs" -UseBasicParsing -TimeoutSec 10 -Method Post -ContentType "application/json" -Body $body
171
+ $id = $created.data.tab.id
172
+ if ($id) {
173
+ return "$baseUrl/?tab=$([System.Uri]::EscapeDataString([string]$id))"
174
+ }
175
+ } catch {
176
+ # Fall back to the root URL.
177
+ }
178
+
179
+ return "$baseUrl/"
180
+ }
181
+
182
+ function Test-PortInUse {
183
+ param(
184
+ [string]$HostName,
185
+ [string]$Port
186
+ )
187
+
188
+ $portNumber = 0
189
+ if (-not [int]::TryParse($Port, [ref]$portNumber)) {
190
+ return $false
191
+ }
192
+
193
+ $connectHost = Get-ConnectHostForPort $HostName
194
+ $client = $null
195
+ $async = $null
196
+
197
+ try {
198
+ $client = [System.Net.Sockets.TcpClient]::new()
199
+ $async = $client.BeginConnect($connectHost, $portNumber, $null, $null)
200
+ if (-not $async.AsyncWaitHandle.WaitOne(500, $false)) {
201
+ return $false
202
+ }
203
+ $client.EndConnect($async)
204
+ return $client.Connected
205
+ } catch {
206
+ return $false
207
+ } finally {
208
+ if ($async -and $async.AsyncWaitHandle) {
209
+ $async.AsyncWaitHandle.Close()
210
+ }
211
+ if ($client) {
212
+ $client.Close()
213
+ }
214
+ }
215
+ }
216
+
217
+ function Wait-UntilReady {
218
+ param(
219
+ [string]$Url,
220
+ [System.Diagnostics.Process]$Process
221
+ )
222
+
223
+ for ($i = 0; $i -lt 50; $i++) {
224
+ $Process.Refresh()
225
+ if ($Process.HasExited) {
226
+ return 2
227
+ }
228
+
229
+ if (Test-HttpOk $Url) {
230
+ return 0
231
+ }
232
+
233
+ Start-Sleep -Milliseconds 200
234
+ }
235
+
236
+ return 1
237
+ }
238
+
239
+ function Stop-ServerProcess {
240
+ if ($script:ServerProcess) {
241
+ $script:ServerProcess.Refresh()
242
+ if (-not $script:ServerProcess.HasExited) {
243
+ try {
244
+ Stop-Process -Id $script:ServerProcess.Id -Force -ErrorAction SilentlyContinue
245
+ } catch {
246
+ # Best-effort cleanup only.
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+ $cwd = Get-LaunchCwd
253
+ $hostName = if ([string]::IsNullOrWhiteSpace($env:PI_WEBUI_HOST)) { $DefaultHost } else { $env:PI_WEBUI_HOST }
254
+ $port = if ([string]::IsNullOrWhiteSpace($env:PI_WEBUI_PORT)) { $DefaultPort } else { $env:PI_WEBUI_PORT }
255
+ $passThroughArgs = @($args)
256
+
257
+ for ($i = 0; $i -lt $passThroughArgs.Count; $i++) {
258
+ if ($passThroughArgs[$i] -eq "--") {
259
+ break
260
+ }
261
+
262
+ switch ($passThroughArgs[$i]) {
263
+ "--cwd" {
264
+ if ($i + 1 -lt $passThroughArgs.Count) { $cwd = $passThroughArgs[$i + 1] }
265
+ }
266
+ "--host" {
267
+ if ($i + 1 -lt $passThroughArgs.Count) { $hostName = $passThroughArgs[$i + 1] }
268
+ }
269
+ "--port" {
270
+ if ($i + 1 -lt $passThroughArgs.Count) { $port = $passThroughArgs[$i + 1] }
271
+ }
272
+ }
273
+ }
274
+
275
+ $browserHost = Get-BrowserHostForUrl $hostName
276
+ $connectHost = Get-ConnectHostForPort $hostName
277
+ $url = "http://${browserHost}:${port}/"
278
+
279
+ if (Test-WebuiRunning $url) {
280
+ $targetUrl = Get-WebuiUrlForCwd $url $cwd
281
+ Write-Host "Pi Web UI already appears to be running at: $url"
282
+ Write-Host "Opening: $targetUrl"
283
+ Open-WebUrl $targetUrl
284
+ exit 0
285
+ }
286
+
287
+ if (Test-PortInUse $hostName $port) {
288
+ Write-Stderr "Port $port is already in use on $connectHost; not starting Pi Web UI."
289
+ if (Test-HttpOk $url) {
290
+ Write-Stderr "An HTTP server responded at $url, but it did not expose Pi Web UI status."
291
+ } else {
292
+ Write-Stderr "No Pi Web UI status endpoint responded at $url."
293
+ }
294
+ exit 1
295
+ }
296
+
297
+ $piWebuiCommand = Ensure-PiWebui
298
+ $webuiArgs = @("--cwd", $cwd, "--host", $hostName, "--port", [string]$port) + $passThroughArgs
299
+
300
+ Write-Host "Starting Pi Web UI in: $cwd"
301
+ Write-Host "Web UI URL: $url"
302
+
303
+ try {
304
+ $script:ServerProcess = Start-Process -FilePath $piWebuiCommand -ArgumentList $webuiArgs -NoNewWindow -PassThru
305
+ $readyStatus = Wait-UntilReady $url $script:ServerProcess
306
+
307
+ if ($readyStatus -eq 0) {
308
+ Open-WebUrl $url
309
+ } elseif ($readyStatus -eq 2) {
310
+ Write-Stderr "Pi Web UI exited before it became ready."
311
+ $script:ServerProcess.Refresh()
312
+ exit $script:ServerProcess.ExitCode
313
+ } else {
314
+ Write-Warning "Server did not respond yet; opening the URL anyway."
315
+ Open-WebUrl $url
316
+ }
317
+
318
+ Wait-Process -Id $script:ServerProcess.Id
319
+ $script:ServerProcess.Refresh()
320
+ exit $script:ServerProcess.ExitCode
321
+ } finally {
322
+ Stop-ServerProcess
323
+ }