patchwarden 0.6.1 → 0.6.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.
Files changed (65) hide show
  1. package/PatchWarden-Control-Tray.cmd +11 -0
  2. package/PatchWarden-Control.cmd +6 -0
  3. package/PatchWarden-Desktop.cmd +5 -0
  4. package/PatchWarden.cmd +1 -1
  5. package/README.en.md +106 -18
  6. package/README.md +30 -19
  7. package/Restart-PatchWarden-Control.cmd +6 -0
  8. package/Stop-PatchWarden.cmd +11 -0
  9. package/dist/controlCenter.d.ts +14 -0
  10. package/dist/controlCenter.js +2002 -0
  11. package/dist/doctor.js +3 -3
  12. package/dist/version.d.ts +1 -1
  13. package/dist/version.js +1 -1
  14. package/docs/control-center/README.md +33 -0
  15. package/docs/control-center/control-center-daily-driver.md +211 -0
  16. package/docs/control-center/control-center-mvp.md +205 -0
  17. package/docs/control-center/control-center-phase2.md +159 -0
  18. package/docs/demo.md +3 -0
  19. package/docs/release-v0.6.4.md +45 -0
  20. package/examples/openai-tunnel/README.md +5 -5
  21. package/examples/openai-tunnel/tunnel-client.example.yaml +3 -3
  22. package/package.json +23 -15
  23. package/scripts/README.md +47 -0
  24. package/scripts/{brand-check.js → checks/brand-check.js} +2 -2
  25. package/scripts/checks/control-center-smoke.js +1098 -0
  26. package/scripts/{control-smoke.js → checks/control-smoke.js} +17 -3
  27. package/scripts/{doctor-smoke.js → checks/doctor-smoke.js} +1 -1
  28. package/scripts/{http-mcp-smoke.js → checks/http-mcp-smoke.js} +2 -2
  29. package/scripts/{lifecycle-smoke.js → checks/lifecycle-smoke.js} +12 -12
  30. package/scripts/{mcp-manifest-check.js → checks/mcp-manifest-check.js} +2 -2
  31. package/scripts/{mcp-smoke.js → checks/mcp-smoke.js} +4 -2
  32. package/scripts/{package-manifest-check.js → checks/package-manifest-check.js} +2 -1
  33. package/scripts/{tunnel-supervisor-smoke.js → checks/tunnel-supervisor-smoke.js} +2 -2
  34. package/scripts/{unit-tests.js → checks/unit-tests.js} +1 -1
  35. package/scripts/{watcher-supervisor-smoke.js → checks/watcher-supervisor-smoke.js} +11 -5
  36. package/scripts/control/control-center-tray.ps1 +281 -0
  37. package/scripts/{get-patchwarden-health.ps1 → control/get-patchwarden-health.ps1} +3 -3
  38. package/scripts/{manage-patchwarden.ps1 → control/manage-patchwarden.ps1} +30 -4
  39. package/scripts/control/restart-control-center.ps1 +173 -0
  40. package/scripts/{restart-patchwarden.ps1 → control/restart-patchwarden.ps1} +1 -1
  41. package/scripts/control/start-control-center.ps1 +263 -0
  42. package/scripts/{start-patchwarden-tunnel.ps1 → control/start-patchwarden-tunnel.ps1} +48 -6
  43. package/scripts/control/stop-patchwarden.ps1 +114 -0
  44. package/scripts/launchers/Check-PatchWarden-Health.cmd +1 -1
  45. package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +1 -1
  46. package/scripts/launchers/Restart-PatchWarden.cmd +1 -1
  47. package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +1 -1
  48. package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +1 -1
  49. package/scripts/{patchwarden-mcp-direct.cmd → mcp/patchwarden-mcp-direct.cmd} +1 -1
  50. package/scripts/{patchwarden-mcp-stdio.cmd → mcp/patchwarden-mcp-stdio.cmd} +1 -1
  51. package/scripts/{pack-clean.js → release/pack-clean.js} +9 -1
  52. package/src/controlCenter.ts +2152 -0
  53. package/src/doctor.ts +4 -4
  54. package/src/version.ts +1 -1
  55. package/ui/colors_and_type.css +141 -0
  56. package/ui/pages/audit.html +743 -0
  57. package/ui/pages/dashboard.html +1154 -0
  58. package/ui/pages/direct-sessions.html +652 -0
  59. package/ui/pages/logs.html +502 -0
  60. package/ui/pages/task-detail.html +1229 -0
  61. package/ui/pages/tasks.html +702 -0
  62. package/ui/pages/workspace.html +947 -0
  63. package/ui/partials/project-shell.html +362 -0
  64. package/ui/vendor/lucide.js +12 -0
  65. package/ui/vendor/tailwindcss-browser.js +947 -0
@@ -0,0 +1,173 @@
1
+ #Requires -Version 5.1
2
+ param(
3
+ [switch]$NoBrowser,
4
+ [switch]$NoBuild
5
+ )
6
+
7
+ $ErrorActionPreference = "Stop"
8
+
9
+ $ProjectRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
10
+ $ControlCenterEntry = Join-Path $ProjectRoot "dist\controlCenter.js"
11
+ $ControlCenterEntryFull = [System.IO.Path]::GetFullPath($ControlCenterEntry)
12
+ $ProjectRootFull = [System.IO.Path]::GetFullPath($ProjectRoot).TrimEnd("\")
13
+ $ListenPort = 8090
14
+ $ListenUrl = "http://127.0.0.1:$ListenPort/"
15
+ $RuntimeDirectory = Join-Path $env:LOCALAPPDATA "patchwarden\control-center"
16
+ $StdoutLog = Join-Path $RuntimeDirectory "control-center.stdout.log"
17
+ $StderrLog = Join-Path $RuntimeDirectory "control-center.stderr.log"
18
+ $MaxWaitSeconds = 15
19
+
20
+ Set-Location -LiteralPath $ProjectRoot
21
+
22
+ function Test-ControlCenterProcess {
23
+ param([Parameter(Mandatory = $true)]$ProcessInfo)
24
+
25
+ $commandLine = [string]$ProcessInfo.CommandLine
26
+ if (-not $commandLine) { return $false }
27
+
28
+ $normalizedCommand = $commandLine.Replace("/", "\").ToLowerInvariant()
29
+ $normalizedEntry = $ControlCenterEntryFull.ToLowerInvariant()
30
+ $normalizedRoot = $ProjectRootFull.ToLowerInvariant()
31
+
32
+ return $normalizedCommand.Contains($normalizedEntry) -or (
33
+ $normalizedCommand.Contains("dist\controlcenter.js") -and
34
+ $normalizedCommand.Contains($normalizedRoot)
35
+ )
36
+ }
37
+
38
+ function Get-ControlCenterProcesses {
39
+ Get-CimInstance Win32_Process -Filter "Name = 'node.exe'" |
40
+ Where-Object { Test-ControlCenterProcess $_ }
41
+ }
42
+
43
+ function Assert-PortIsNotForeign {
44
+ try {
45
+ $listeners = Get-NetTCPConnection -LocalPort $ListenPort -State Listen -ErrorAction Stop
46
+ } catch {
47
+ return
48
+ }
49
+
50
+ foreach ($listener in $listeners) {
51
+ $owner = Get-CimInstance Win32_Process -Filter "ProcessId = $($listener.OwningProcess)" -ErrorAction SilentlyContinue
52
+ if (-not $owner) { continue }
53
+ if (-not (Test-ControlCenterProcess $owner)) {
54
+ Write-Host "[error] Port $ListenPort is used by a non-PatchWarden Control Center process." -ForegroundColor Red
55
+ Write-Host "[error] PID $($owner.ProcessId): $($owner.CommandLine)" -ForegroundColor DarkRed
56
+ Write-Host "[error] Close that process manually, then rerun this script." -ForegroundColor Yellow
57
+ exit 1
58
+ }
59
+ }
60
+ }
61
+
62
+ function Stop-ControlCenterProcesses {
63
+ $processes = @(Get-ControlCenterProcesses)
64
+ if ($processes.Count -eq 0) {
65
+ Write-Host "[stop] No existing PatchWarden Control Center process found." -ForegroundColor DarkGray
66
+ return
67
+ }
68
+
69
+ foreach ($proc in $processes) {
70
+ Write-Host "[stop] Stopping Control Center PID $($proc.ProcessId)." -ForegroundColor Yellow
71
+ Stop-Process -Id $proc.ProcessId -ErrorAction SilentlyContinue
72
+ }
73
+
74
+ Start-Sleep -Milliseconds 800
75
+
76
+ foreach ($proc in $processes) {
77
+ $stillRunning = Get-Process -Id $proc.ProcessId -ErrorAction SilentlyContinue
78
+ if ($stillRunning) {
79
+ Write-Host "[stop] PID $($proc.ProcessId) did not exit cleanly; forcing stop." -ForegroundColor Yellow
80
+ Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue
81
+ }
82
+ }
83
+ }
84
+
85
+ function Invoke-BuildIfNeeded {
86
+ if ($NoBuild) {
87
+ Write-Host "[build] Skipped by -NoBuild." -ForegroundColor DarkGray
88
+ return
89
+ }
90
+
91
+ Write-Host "[build] Running npm.cmd run build..." -ForegroundColor Cyan
92
+ npm.cmd run build
93
+ if ($LASTEXITCODE -ne 0) {
94
+ Write-Host "[error] npm.cmd run build exited with code $LASTEXITCODE." -ForegroundColor Red
95
+ exit $LASTEXITCODE
96
+ }
97
+ if (-not (Test-Path -LiteralPath $ControlCenterEntryFull)) {
98
+ Write-Host "[error] Build completed but dist\controlCenter.js is missing." -ForegroundColor Red
99
+ exit 1
100
+ }
101
+ }
102
+
103
+ function Start-ControlCenter {
104
+ $nodeCommand = Get-Command node.exe -ErrorAction SilentlyContinue
105
+ if (-not $nodeCommand) {
106
+ Write-Host "[error] node.exe was not found on PATH." -ForegroundColor Red
107
+ exit 1
108
+ }
109
+
110
+ New-Item -ItemType Directory -Force -Path $RuntimeDirectory | Out-Null
111
+ Remove-Item -LiteralPath $StdoutLog, $StderrLog -Force -ErrorAction SilentlyContinue
112
+
113
+ Write-Host "[run] Starting PatchWarden Control Center on $ListenUrl" -ForegroundColor Cyan
114
+ $process = Start-Process -FilePath $nodeCommand.Source `
115
+ -ArgumentList @($ControlCenterEntryFull) `
116
+ -WorkingDirectory $ProjectRoot `
117
+ -RedirectStandardOutput $StdoutLog `
118
+ -RedirectStandardError $StderrLog `
119
+ -PassThru -WindowStyle Hidden
120
+
121
+ Start-Sleep -Milliseconds 500
122
+ if ($process.HasExited) {
123
+ Write-Host "[error] Control Center exited immediately with code $($process.ExitCode)." -ForegroundColor Red
124
+ if (Test-Path -LiteralPath $StderrLog) {
125
+ Get-Content -LiteralPath $StderrLog -Tail 20 -ErrorAction SilentlyContinue
126
+ }
127
+ exit 1
128
+ }
129
+
130
+ $ready = $false
131
+ for ($attempt = 1; $attempt -le $MaxWaitSeconds; $attempt++) {
132
+ try {
133
+ $process.Refresh()
134
+ if ($process.HasExited) {
135
+ Write-Host "[error] Control Center exited before readiness with code $($process.ExitCode)." -ForegroundColor Red
136
+ if (Test-Path -LiteralPath $StderrLog) {
137
+ Get-Content -LiteralPath $StderrLog -Tail 20 -ErrorAction SilentlyContinue
138
+ }
139
+ exit 1
140
+ }
141
+
142
+ $response = Invoke-WebRequest -Uri $ListenUrl -UseBasicParsing -TimeoutSec 2 -ErrorAction Stop
143
+ if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500) {
144
+ $ready = $true
145
+ break
146
+ }
147
+ } catch {
148
+ Start-Sleep -Seconds 1
149
+ }
150
+ }
151
+
152
+ if (-not $ready) {
153
+ Write-Host "[error] Control Center did not become ready on $ListenUrl within $MaxWaitSeconds seconds." -ForegroundColor Red
154
+ Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
155
+ exit 1
156
+ }
157
+
158
+ Write-Host "[ok] PatchWarden Control Center restarted at $ListenUrl" -ForegroundColor Green
159
+ Write-Host "[info] node PID $($process.Id)" -ForegroundColor DarkGray
160
+ Write-Host "[info] stdout log: $StdoutLog" -ForegroundColor DarkGray
161
+ Write-Host "[info] stderr log: $StderrLog" -ForegroundColor DarkGray
162
+
163
+ if (-not $NoBrowser) {
164
+ $cacheBuster = Get-Date -Format "yyyyMMddHHmmss"
165
+ Start-Process "$ListenUrl`?restarted=$cacheBuster"
166
+ }
167
+ }
168
+
169
+ Assert-PortIsNotForeign
170
+ Stop-ControlCenterProcesses
171
+ Invoke-BuildIfNeeded
172
+ Assert-PortIsNotForeign
173
+ Start-ControlCenter
@@ -8,7 +8,7 @@ param(
8
8
  $ErrorActionPreference = "Stop"
9
9
  $ProgressPreference = "SilentlyContinue"
10
10
 
11
- $ProjectRoot = Split-Path -Parent $PSScriptRoot
11
+ $ProjectRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
12
12
  $ConfigPath = Join-Path $ProjectRoot "patchwarden.config.json"
13
13
  $RuntimeDirectory = Join-Path $env:LOCALAPPDATA "patchwarden\runtime"
14
14
 
@@ -0,0 +1,263 @@
1
+ #Requires -Version 5.1
2
+ param(
3
+ [switch]$NoBrowser,
4
+ [switch]$Foreground,
5
+ [switch]$Background
6
+ )
7
+
8
+ $ErrorActionPreference = "Stop"
9
+
10
+ # ── Argument reconciliation ──────────────────────────────────────
11
+ # -Foreground and -Background are mutually exclusive. When neither is
12
+ # supplied, default to -Background (detach node, exit after readiness).
13
+ if ($Foreground -and $Background) {
14
+ Write-Host "[error] -Foreground and -Background are mutually exclusive." -ForegroundColor Red
15
+ exit 1
16
+ }
17
+ if (-not $Foreground -and -not $Background) {
18
+ $Background = $true
19
+ }
20
+
21
+ $ProjectRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
22
+ $ControlCenterEntry = Join-Path $ProjectRoot "dist\controlCenter.js"
23
+ $ControlCenterEntryFull = [System.IO.Path]::GetFullPath($ControlCenterEntry)
24
+ $ProjectRootFull = [System.IO.Path]::GetFullPath($ProjectRoot).TrimEnd("\")
25
+ $ListenPort = 8090
26
+ $ListenUrl = "http://127.0.0.1:$ListenPort/"
27
+ $RuntimeDirectory = Join-Path $env:LOCALAPPDATA "patchwarden\control-center"
28
+ $StatusFile = Join-Path $RuntimeDirectory "control-center-status.json"
29
+ $StdoutLog = Join-Path $RuntimeDirectory "control-center.stdout.log"
30
+ $StderrLog = Join-Path $RuntimeDirectory "control-center.stderr.log"
31
+ $MaxWaitSeconds = 15
32
+ $PollIntervalSeconds = 1
33
+
34
+ Set-Location -LiteralPath $ProjectRoot
35
+
36
+ # ── Helpers ──────────────────────────────────────────────────────
37
+
38
+ function Test-ControlCenterCommandLine {
39
+ param([string]$CommandLine)
40
+ if (-not $CommandLine) { return $false }
41
+ $normalizedCommand = $CommandLine.Replace("/", "\").ToLowerInvariant()
42
+ $normalizedEntry = $ControlCenterEntryFull.ToLowerInvariant()
43
+ $normalizedRoot = $ProjectRootFull.ToLowerInvariant()
44
+ return $normalizedCommand.Contains($normalizedEntry) -or (
45
+ $normalizedCommand.Contains("dist\controlcenter.js") -and
46
+ $normalizedCommand.Contains($normalizedRoot)
47
+ )
48
+ }
49
+
50
+ function Get-ControlCenterProcesses {
51
+ Get-CimInstance Win32_Process -Filter "Name = 'node.exe'" -ErrorAction SilentlyContinue |
52
+ Where-Object { Test-ControlCenterCommandLine ([string]$_.CommandLine) }
53
+ }
54
+
55
+ function Read-StatusFile {
56
+ if (-not (Test-Path -LiteralPath $StatusFile)) { return $null }
57
+ try {
58
+ return Get-Content -LiteralPath $StatusFile -Raw -Encoding UTF8 | ConvertFrom-Json
59
+ } catch {
60
+ return $null
61
+ }
62
+ }
63
+
64
+ function Test-PortOwnedByControlCenter {
65
+ # Returns $true if a process is listening on $ListenPort AND it is our control
66
+ # center node process. Returns $false if nothing is listening. Throws a
67
+ # descriptive message if a foreign process owns the port.
68
+ $listeners = @()
69
+ try {
70
+ $listeners = Get-NetTCPConnection -LocalPort $ListenPort -State Listen -ErrorAction Stop |
71
+ Sort-Object OwningProcess -Unique
72
+ } catch {
73
+ return $false # nothing listening
74
+ }
75
+ foreach ($listener in $listeners) {
76
+ $owner = Get-CimInstance Win32_Process -Filter "ProcessId = $($listener.OwningProcess)" -ErrorAction SilentlyContinue
77
+ if (-not $owner) { continue }
78
+ if (Test-ControlCenterCommandLine ([string]$owner.CommandLine)) {
79
+ return $true
80
+ } else {
81
+ Write-Host "[error] Port $ListenPort is already used by a non-PatchWarden process." -ForegroundColor Red
82
+ Write-Host "[error] PID $($owner.ProcessId): $($owner.CommandLine)" -ForegroundColor DarkRed
83
+ Write-Host "[error] Close that process manually, then rerun this script." -ForegroundColor Yellow
84
+ exit 1
85
+ }
86
+ }
87
+ return $false
88
+ }
89
+
90
+ function Get-StderrTail {
91
+ if (-not (Test-Path -LiteralPath $StderrLog)) { return "" }
92
+ try {
93
+ $content = Get-Content -LiteralPath $StderrLog -Raw -Encoding UTF8 -ErrorAction Stop
94
+ if (-not $content) { return "" }
95
+ $trimmed = ($content -replace '[\r\n]+', ' ').Trim()
96
+ if ($trimmed.Length -gt 500) { $trimmed = $trimmed.Substring(0, 500) }
97
+ return $trimmed
98
+ } catch {
99
+ return ""
100
+ }
101
+ }
102
+
103
+ # ── Single-instance detection ────────────────────────────────────
104
+ # 1. Read the status file. If it points at a live control center (port
105
+ # responds AND pid is alive AND command line matches), just open the
106
+ # browser and exit — do NOT spawn a second server.
107
+ # 2. If the port is occupied by a foreign process, error out.
108
+ # 3. Otherwise start a fresh instance.
109
+
110
+ $existing = Read-StatusFile
111
+ if ($existing) {
112
+ $existingPid = 0
113
+ [int]::TryParse([string]$existing.pid, [ref]$existingPid) | Out-Null
114
+ $existingPort = 0
115
+ [int]::TryParse([string]$existing.port, [ref]$existingPort) | Out-Null
116
+
117
+ $pidAlive = $false
118
+ if ($existingPid -gt 0) {
119
+ $proc = Get-CimInstance Win32_Process -Filter "ProcessId = $existingPid" -ErrorAction SilentlyContinue
120
+ if ($proc -and (Test-ControlCenterCommandLine ([string]$proc.CommandLine))) {
121
+ $pidAlive = $true
122
+ }
123
+ }
124
+
125
+ if ($pidAlive -and $existingPort -gt 0) {
126
+ $probeUrl = "http://127.0.0.1:$existingPort/api/control-center-status"
127
+ try {
128
+ $resp = Invoke-WebRequest -Uri $probeUrl -UseBasicParsing -TimeoutSec 2 -ErrorAction Stop
129
+ if ($resp.StatusCode -eq 200) {
130
+ $body = $resp.Content | ConvertFrom-Json
131
+ if ($body.running -eq $true) {
132
+ $existingUrl = "http://127.0.0.1:$existingPort/"
133
+ Write-Host "[ok] PatchWarden Control Center is already running (PID $existingPid)." -ForegroundColor Green
134
+ Write-Host "[ok] URL: $existingUrl" -ForegroundColor Green
135
+ if (-not $NoBrowser) {
136
+ Write-Host "[open] Launching browser at $existingUrl"
137
+ Start-Process $existingUrl
138
+ }
139
+ exit 0
140
+ }
141
+ }
142
+ } catch {
143
+ # Status file exists but the probe failed — stale entry. Fall through
144
+ # to a fresh start after cleaning up.
145
+ Write-Host "[warn] Status file exists but the server did not respond; starting a new instance." -ForegroundColor DarkYellow
146
+ }
147
+ } else {
148
+ Write-Host "[warn] Status file references a dead PID; starting a new instance." -ForegroundColor DarkYellow
149
+ }
150
+ }
151
+
152
+ # Detect foreign port occupation before spawning.
153
+ Test-PortOwnedByControlCenter | Out-Null
154
+ # If we reach here, either nothing is listening or our own (now-dead) process
155
+ # was the listener. Clean up any orphaned node processes for this entry point.
156
+ $orphans = @(Get-ControlCenterProcesses)
157
+ if ($orphans.Count -gt 0) {
158
+ Write-Host "[cleanup] Stopping $($orphans.Count) orphaned Control Center node process(es)." -ForegroundColor DarkYellow
159
+ foreach ($p in $orphans) {
160
+ try { Stop-Process -Id $p.ProcessId -Force -ErrorAction Stop } catch {}
161
+ }
162
+ Start-Sleep -Milliseconds 500
163
+ }
164
+
165
+ # ── Build if needed ──────────────────────────────────────────────
166
+
167
+ $nodeCommand = Get-Command node.exe -ErrorAction SilentlyContinue
168
+ if (-not $nodeCommand) {
169
+ Write-Host "[error] node.exe was not found on PATH." -ForegroundColor Red
170
+ exit 1
171
+ }
172
+
173
+ if (-not (Test-Path -LiteralPath $ControlCenterEntryFull)) {
174
+ Write-Host "[build] dist/controlCenter.js not found. Running npm.cmd run build..." -ForegroundColor Yellow
175
+ npm.cmd run build
176
+ if ($LASTEXITCODE -ne 0) {
177
+ Write-Host "[error] npm.cmd run build exited with code $LASTEXITCODE." -ForegroundColor Red
178
+ exit 1
179
+ }
180
+ if (-not (Test-Path -LiteralPath $ControlCenterEntryFull)) {
181
+ Write-Host "[error] Build completed but dist/controlCenter.js is still missing." -ForegroundColor Red
182
+ exit 1
183
+ }
184
+ Write-Host "[build] Build complete." -ForegroundColor Green
185
+ }
186
+
187
+ New-Item -ItemType Directory -Force -Path $RuntimeDirectory | Out-Null
188
+ Remove-Item -LiteralPath $StdoutLog, $StderrLog -Force -ErrorAction SilentlyContinue
189
+
190
+ # ── Foreground mode: node attached to this console, logs stream live ──
191
+ if ($Foreground) {
192
+ Write-Host "[run] Starting PatchWarden Control Center on $ListenUrl (foreground)" -ForegroundColor Cyan
193
+ Write-Host "[run] Press Ctrl+C to stop. Logs stream below."
194
+ # In foreground mode the node process inherits this console; the status file
195
+ # is still written by the server itself on listen().
196
+ & $nodeCommand.Source $ControlCenterEntryFull
197
+ exit $LASTEXITCODE
198
+ }
199
+
200
+ # ── Background mode (default): detached node, this script exits after ready ──
201
+ Write-Host "[run] Starting PatchWarden Control Center on $ListenUrl (background)" -ForegroundColor Cyan
202
+ $process = Start-Process -FilePath $nodeCommand.Source `
203
+ -ArgumentList @($ControlCenterEntryFull) `
204
+ -WorkingDirectory $ProjectRoot `
205
+ -RedirectStandardOutput $StdoutLog `
206
+ -RedirectStandardError $StderrLog `
207
+ -PassThru -WindowStyle Hidden
208
+
209
+ Write-Host "[run] node PID $($process.Id)."
210
+
211
+ # Brief wait to detect truly immediate exits.
212
+ Start-Sleep -Milliseconds 500
213
+ if ($process.HasExited) {
214
+ $exitCode = $process.ExitCode
215
+ $stderrTail = Get-StderrTail
216
+ Write-Host "[error] node process exited immediately with code $exitCode." -ForegroundColor Red
217
+ if ($stderrTail) {
218
+ Write-Host "[error] stderr: $stderrTail" -ForegroundColor DarkRed
219
+ }
220
+ exit 1
221
+ }
222
+
223
+ # Poll for port readiness.
224
+ $ready = $false
225
+ for ($attempt = 1; $attempt -le $MaxWaitSeconds; $attempt++) {
226
+ try { $process.Refresh() } catch {}
227
+ if ($process.HasExited) {
228
+ $exitCode = $process.ExitCode
229
+ $stderrTail = Get-StderrTail
230
+ Write-Host "[error] node process exited with code $exitCode before the port became ready." -ForegroundColor Red
231
+ if ($stderrTail) {
232
+ Write-Host "[error] stderr: $stderrTail" -ForegroundColor DarkRed
233
+ }
234
+ exit 1
235
+ }
236
+ try {
237
+ $response = Invoke-WebRequest -Uri $ListenUrl -UseBasicParsing -TimeoutSec 2 -ErrorAction Stop
238
+ if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500) {
239
+ $ready = $true
240
+ break
241
+ }
242
+ } catch {
243
+ # Not ready yet; keep polling.
244
+ }
245
+ Start-Sleep -Seconds $PollIntervalSeconds
246
+ }
247
+
248
+ if (-not $ready) {
249
+ Write-Host "[error] Control Center did not become ready on $ListenUrl within $MaxWaitSeconds seconds." -ForegroundColor Red
250
+ try { Stop-Process -Id $process.Id -Force -ErrorAction Stop } catch {}
251
+ exit 1
252
+ }
253
+
254
+ Write-Host "[ok] PatchWarden Control Center is ready at $ListenUrl" -ForegroundColor Green
255
+
256
+ if (-not $NoBrowser) {
257
+ Write-Host "[open] Launching default browser at $ListenUrl"
258
+ Start-Process $ListenUrl
259
+ }
260
+
261
+ Write-Host "[info] node PID $($process.Id); stdout log: $StdoutLog; stderr log: $StderrLog"
262
+ Write-Host "[info] Re-run this script to open the existing instance instead of starting a new one."
263
+ Write-Host "[info] Stop with: Stop-Process -Id $($process.Id)"
@@ -1,10 +1,10 @@
1
- param(
1
+ param(
2
2
  [string]$TunnelId = $env:PATCHWARDEN_TUNNEL_ID,
3
3
  [string]$Profile = "patchwarden",
4
4
  [ValidateSet("chatgpt_core", "chatgpt_direct")]
5
5
  [string]$ToolProfile = "chatgpt_core",
6
6
  [string]$ProxyUrl = $(if ($env:HTTPS_PROXY) { $env:HTTPS_PROXY } else { "http://127.0.0.1:7892" }),
7
- [string]$TunnelClientExe = $env:TUNNEL_CLIENT_EXE,
7
+ [string]$TunnelClientExe = $(if ($env:TUNNEL_CLIENT_EXE) { $env:TUNNEL_CLIENT_EXE } else { $env:PATCHWARDEN_TUNNEL_CLIENT_EXE }),
8
8
  [string]$OpencodeBin = $env:OPENCODE_BIN_DIR,
9
9
  [string]$ConfigPath = $env:PATCHWARDEN_CONFIG,
10
10
  [string]$CredentialPath = $(if ($env:PATCHWARDEN_CREDENTIAL_PATH) { $env:PATCHWARDEN_CREDENTIAL_PATH } else { Join-Path $env:APPDATA "patchwarden\control-plane-api-key.dpapi" }),
@@ -16,6 +16,7 @@ param(
16
16
  [int]$WatcherHealthyResetSeconds = 60,
17
17
  [switch]$SkipWatcher,
18
18
  [switch]$ForgetSavedApiKey,
19
+ [switch]$NoTunnelWebUi,
19
20
  [string]$HealthListenAddr = ""
20
21
  )
21
22
 
@@ -29,12 +30,12 @@ if ([string]::IsNullOrWhiteSpace($HealthListenAddr)) {
29
30
  }
30
31
  }
31
32
 
32
- $ProjectRoot = Split-Path -Parent $PSScriptRoot
33
+ $ProjectRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
33
34
  if (-not $ConfigPath) {
34
35
  $ConfigPath = Join-Path $ProjectRoot "patchwarden.config.json"
35
36
  }
36
37
  $McpLauncherName = if ($ToolProfile -eq "chatgpt_direct") { "patchwarden-mcp-direct.cmd" } else { "patchwarden-mcp-stdio.cmd" }
37
- $McpStdioLauncher = Join-Path $ProjectRoot "scripts\$McpLauncherName"
38
+ $McpStdioLauncher = Join-Path $ProjectRoot "scripts\mcp\$McpLauncherName"
38
39
  $McpStdioLauncherForTunnel = $McpStdioLauncher -replace "\\", "/"
39
40
  $OpencodeConfigHome = Join-Path $env:LOCALAPPDATA "patchwarden\opencode-config"
40
41
  $ProfilePath = Join-Path $env:APPDATA "tunnel-client\$Profile.yaml"
@@ -57,6 +58,7 @@ $script:WatcherManaged = $false
57
58
  $script:WatcherRestartAttempts = 0
58
59
  $script:WatcherHealthySince = $null
59
60
  $script:WatcherRestartExhausted = $false
61
+ $DefaultTunnelClientPath = "D:\ai_agent\tunnel-client-v0.0.9--context-conduit-topaz-windows-amd64\tunnel-client.exe"
60
62
 
61
63
  if ($ToolProfile -eq "chatgpt_direct") {
62
64
  $SkipWatcher = $true
@@ -501,7 +503,34 @@ if (-not $TunnelClientExe) {
501
503
  }
502
504
  }
503
505
 
506
+ # Search common installation locations
507
+ $commonPaths = @(
508
+ $DefaultTunnelClientPath
509
+ Join-Path $env:LOCALAPPDATA "patchwarden\tunnel-client.exe"
510
+ Join-Path $env:APPDATA "tunnel-client\tunnel-client.exe"
511
+ Join-Path $env:USERPROFILE "tunnel-client\tunnel-client.exe"
512
+ )
513
+ if (-not $TunnelClientExe) {
514
+ foreach ($candidate in $commonPaths) {
515
+ if (Test-Path -LiteralPath $candidate) {
516
+ $TunnelClientExe = $candidate
517
+ Write-Host "[detect] Found tunnel-client.exe: $TunnelClientExe" -ForegroundColor Green
518
+ break
519
+ }
520
+ }
521
+ }
522
+
504
523
  if (-not $TunnelClientExe) {
524
+ Write-Host ""
525
+ Write-Host "[input] tunnel-client.exe not found on PATH or common locations." -ForegroundColor Yellow
526
+ Write-Host " Please provide the full path."
527
+ Write-Host ""
528
+ Write-Host " To search for it manually, try one of these commands:" -ForegroundColor Cyan
529
+ Write-Host " Get-ChildItem -Path `$env:LOCALAPPDATA -Recurse -Filter tunnel-client.exe -ErrorAction SilentlyContinue" -ForegroundColor Gray
530
+ Write-Host " Get-ChildItem -Path `$env:APPDATA -Recurse -Filter tunnel-client.exe -ErrorAction SilentlyContinue" -ForegroundColor Gray
531
+ Write-Host " Get-ChildItem -Path C:\ -Recurse -Filter tunnel-client.exe -ErrorAction SilentlyContinue" -ForegroundColor Gray
532
+ Write-Host " Or download it from: https://platform.openai.com/docs/guides/mcp-connector" -ForegroundColor Cyan
533
+ Write-Host ""
505
534
  $TunnelClientExe = Read-Host "Path to tunnel-client.exe"
506
535
  }
507
536
 
@@ -521,6 +550,19 @@ Assert-File -Path $TunnelClientExe -Name "tunnel-client.exe"
521
550
  Assert-File -Path $ConfigPath -Name "patchwarden.config.json"
522
551
  Assert-File -Path $McpStdioLauncher -Name $McpLauncherName
523
552
 
553
+ # Auto-read Tunnel ID from existing profile YAML if not already set
554
+ if (-not $TunnelId -and (Test-Path -LiteralPath $ProfilePath)) {
555
+ try {
556
+ $profileContent = Get-Content -LiteralPath $ProfilePath -Raw -Encoding UTF8
557
+ if ($profileContent -match 'tunnel_id:\s*"([^"]+)"') {
558
+ $TunnelId = $matches[1]
559
+ Write-Host "[detect] Tunnel ID read from profile $Profile`: $TunnelId" -ForegroundColor Green
560
+ }
561
+ } catch {
562
+ Write-Host "[warn] Could not read Tunnel ID from $ProfilePath`: $($_.Exception.Message)" -ForegroundColor Yellow
563
+ }
564
+ }
565
+
524
566
  if (-not $TunnelId) {
525
567
  $TunnelId = Read-Host "Tunnel ID"
526
568
  }
@@ -538,7 +580,7 @@ if (-not (Test-Path -LiteralPath (Join-Path $ProjectRoot "dist\index.js"))) {
538
580
  New-Item -ItemType Directory -Force -Path $RuntimeDirectory | Out-Null
539
581
  $env:PATCHWARDEN_CONFIG = $ConfigPath
540
582
  Write-Host "[manifest] Verifying the exact $ToolProfile stdio MCP tool catalog..."
541
- $manifestOutput = (& node (Join-Path $ProjectRoot "scripts\mcp-manifest-check.js") --profile $ToolProfile --json 2>&1 | Out-String).Trim()
583
+ $manifestOutput = (& node (Join-Path $ProjectRoot "scripts\checks\mcp-manifest-check.js") --profile $ToolProfile --json 2>&1 | Out-String).Trim()
542
584
  if ($LASTEXITCODE -ne 0) {
543
585
  Write-TunnelStatus -Status "stopped" -ReasonCode "tool_manifest_check_failed" -LastError "The tunnel MCP tool manifest preflight failed."
544
586
  throw "Tool manifest preflight failed: $manifestOutput"
@@ -630,7 +672,7 @@ Write-Host "[run-config] cwd: $ProjectRoot"
630
672
  Write-Host ""
631
673
 
632
674
  $attempt = 0
633
- $openUi = $true
675
+ $openUi = -not $NoTunnelWebUi
634
676
  try {
635
677
  while ($true) {
636
678
  $attempt++
@@ -0,0 +1,114 @@
1
+ #Requires -Version 5.1
2
+ <#
3
+ .SYNOPSIS
4
+ One-click shutdown for PatchWarden desktop use.
5
+ .DESCRIPTION
6
+ Stops Core/Direct through the consolidated manager, then closes the local
7
+ Control Center and tray processes that belong to this project path.
8
+ #>
9
+ param(
10
+ [switch]$KeepControlCenter,
11
+ [switch]$KeepTray
12
+ )
13
+
14
+ $ErrorActionPreference = "Stop"
15
+ $ProjectRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
16
+ $ProjectRootFull = [System.IO.Path]::GetFullPath($ProjectRoot).TrimEnd("\")
17
+ $Manager = Join-Path $PSScriptRoot "manage-patchwarden.ps1"
18
+ $ControlCenterEntry = Join-Path $ProjectRoot "dist\controlCenter.js"
19
+ $ControlCenterEntryFull = [System.IO.Path]::GetFullPath($ControlCenterEntry)
20
+ $TrayScript = Join-Path $PSScriptRoot "control-center-tray.ps1"
21
+ $TrayScriptFull = [System.IO.Path]::GetFullPath($TrayScript)
22
+ $RuntimeDirectory = Join-Path $env:LOCALAPPDATA "patchwarden\control-center"
23
+ $StatusFile = Join-Path $RuntimeDirectory "control-center-status.json"
24
+
25
+ Set-Location -LiteralPath $ProjectRoot
26
+
27
+ function Normalize-CommandLine {
28
+ param([string]$CommandLine)
29
+ if (-not $CommandLine) { return "" }
30
+ return $CommandLine.Replace("/", "\").ToLowerInvariant()
31
+ }
32
+
33
+ function Test-ControlCenterProcess {
34
+ param($ProcessInfo)
35
+ $normalizedCommand = Normalize-CommandLine ([string]$ProcessInfo.CommandLine)
36
+ $normalizedEntry = $ControlCenterEntryFull.ToLowerInvariant()
37
+ $normalizedRoot = $ProjectRootFull.ToLowerInvariant()
38
+ return $normalizedCommand.Contains($normalizedEntry) -or (
39
+ $normalizedCommand.Contains("dist\controlcenter.js") -and
40
+ $normalizedCommand.Contains($normalizedRoot)
41
+ )
42
+ }
43
+
44
+ function Test-TrayProcess {
45
+ param($ProcessInfo)
46
+ $normalizedCommand = Normalize-CommandLine ([string]$ProcessInfo.CommandLine)
47
+ $normalizedTray = $TrayScriptFull.ToLowerInvariant()
48
+ $normalizedRoot = $ProjectRootFull.ToLowerInvariant()
49
+ return $normalizedCommand.Contains($normalizedTray) -or (
50
+ $normalizedCommand.Contains("control-center-tray.ps1") -and
51
+ $normalizedCommand.Contains($normalizedRoot)
52
+ )
53
+ }
54
+
55
+ function Stop-MatchingProcesses {
56
+ param(
57
+ [Parameter(Mandatory = $true)][string]$Label,
58
+ [Parameter(Mandatory = $true)][scriptblock]$Predicate
59
+ )
60
+
61
+ $processes = @(
62
+ Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
63
+ Where-Object {
64
+ [int]$_.ProcessId -ne $PID -and (& $Predicate $_)
65
+ }
66
+ )
67
+
68
+ if ($processes.Count -eq 0) {
69
+ Write-Host "[stop] No $Label process found." -ForegroundColor DarkGray
70
+ return
71
+ }
72
+
73
+ foreach ($proc in $processes) {
74
+ Write-Host "[stop] Closing $Label PID $($proc.ProcessId)." -ForegroundColor Yellow
75
+ try {
76
+ Stop-Process -Id $proc.ProcessId -ErrorAction SilentlyContinue
77
+ } catch {
78
+ Write-Host "[stop] PID $($proc.ProcessId) already exited or is inaccessible." -ForegroundColor DarkGray
79
+ }
80
+ }
81
+
82
+ Start-Sleep -Milliseconds 800
83
+
84
+ foreach ($proc in $processes) {
85
+ $stillRunning = Get-Process -Id $proc.ProcessId -ErrorAction SilentlyContinue
86
+ if ($stillRunning) {
87
+ Write-Host "[stop] PID $($proc.ProcessId) did not exit cleanly; forcing stop." -ForegroundColor Yellow
88
+ Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue
89
+ }
90
+ }
91
+ }
92
+
93
+ if (-not (Test-Path -LiteralPath $Manager)) {
94
+ throw "Manager script not found: $Manager"
95
+ }
96
+
97
+ Write-Host "[stop] Stopping PatchWarden Core/Direct services..." -ForegroundColor Cyan
98
+ & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $Manager stop all
99
+ if ($LASTEXITCODE -ne 0) {
100
+ exit $LASTEXITCODE
101
+ }
102
+
103
+ if (-not $KeepControlCenter) {
104
+ Stop-MatchingProcesses -Label "Control Center" -Predicate ${function:Test-ControlCenterProcess}
105
+ if (Test-Path -LiteralPath $StatusFile) {
106
+ Remove-Item -LiteralPath $StatusFile -Force -ErrorAction SilentlyContinue
107
+ }
108
+ }
109
+
110
+ if (-not $KeepTray) {
111
+ Stop-MatchingProcesses -Label "tray" -Predicate ${function:Test-TrayProcess}
112
+ }
113
+
114
+ Write-Host "[ok] PatchWarden shutdown complete." -ForegroundColor Green
@@ -1,6 +1,6 @@
1
1
  @echo off
2
2
  setlocal
3
3
  cd /d "%~dp0..\.."
4
- powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\manage-patchwarden.ps1" status all
4
+ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\control\manage-patchwarden.ps1" status all
5
5
  echo.
6
6
  pause
@@ -1,6 +1,6 @@
1
1
  @echo off
2
2
  setlocal
3
3
  cd /d "%~dp0..\.."
4
- powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\start-patchwarden-tunnel.ps1" -ForgetSavedApiKey
4
+ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\control\start-patchwarden-tunnel.ps1" -ForgetSavedApiKey
5
5
  echo.
6
6
  pause