patchwarden 0.6.0 → 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 (134) 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 +4 -4
  12. package/dist/logging.d.ts +52 -0
  13. package/dist/logging.js +123 -0
  14. package/dist/runner/changeCapture.d.ts +41 -0
  15. package/dist/runner/changeCapture.js +171 -1
  16. package/dist/runner/runTask.js +204 -24
  17. package/dist/smoke-test.js +8 -8
  18. package/dist/test/unit/android-doctor.test.d.ts +1 -0
  19. package/dist/test/unit/android-doctor.test.js +118 -0
  20. package/dist/test/unit/chinese-path.test.d.ts +1 -0
  21. package/dist/test/unit/chinese-path.test.js +91 -0
  22. package/dist/test/unit/command-guard.test.d.ts +1 -0
  23. package/dist/test/unit/command-guard.test.js +160 -0
  24. package/dist/test/unit/direct-guards.test.d.ts +1 -0
  25. package/dist/test/unit/direct-guards.test.js +213 -0
  26. package/dist/test/unit/logging.test.d.ts +1 -0
  27. package/dist/test/unit/logging.test.js +275 -0
  28. package/dist/test/unit/path-guard.test.d.ts +1 -0
  29. package/dist/test/unit/path-guard.test.js +109 -0
  30. package/dist/test/unit/safe-status.test.d.ts +1 -0
  31. package/dist/test/unit/safe-status.test.js +165 -0
  32. package/dist/test/unit/sensitive-guard.test.d.ts +1 -0
  33. package/dist/test/unit/sensitive-guard.test.js +104 -0
  34. package/dist/test/unit/sync-file.test.d.ts +1 -0
  35. package/dist/test/unit/sync-file.test.js +154 -0
  36. package/dist/test/unit/watcher-status.test.d.ts +1 -0
  37. package/dist/test/unit/watcher-status.test.js +169 -0
  38. package/dist/tools/androidDoctor.d.ts +38 -0
  39. package/dist/tools/androidDoctor.js +391 -0
  40. package/dist/tools/auditTask.js +11 -5
  41. package/dist/tools/getTaskSummary.d.ts +3 -0
  42. package/dist/tools/getTaskSummary.js +15 -1
  43. package/dist/tools/healthCheck.d.ts +5 -0
  44. package/dist/tools/healthCheck.js +21 -0
  45. package/dist/tools/registry.js +53 -0
  46. package/dist/tools/safeStatus.d.ts +19 -0
  47. package/dist/tools/safeStatus.js +72 -0
  48. package/dist/tools/syncFile.d.ts +18 -0
  49. package/dist/tools/syncFile.js +65 -0
  50. package/dist/tools/taskOutputs.d.ts +2 -2
  51. package/dist/tools/toolCatalog.d.ts +2 -2
  52. package/dist/tools/toolCatalog.js +2 -0
  53. package/dist/version.d.ts +2 -2
  54. package/dist/version.js +2 -2
  55. package/dist/watcherStatus.d.ts +1 -0
  56. package/dist/watcherStatus.js +96 -4
  57. package/docs/control-center/README.md +33 -0
  58. package/docs/control-center/control-center-daily-driver.md +211 -0
  59. package/docs/control-center/control-center-mvp.md +205 -0
  60. package/docs/control-center/control-center-phase2.md +159 -0
  61. package/docs/demo.md +3 -0
  62. package/docs/performance-notes.md +55 -0
  63. package/docs/release-v0.6.1.md +75 -0
  64. package/docs/release-v0.6.4.md +45 -0
  65. package/examples/openai-tunnel/README.md +5 -5
  66. package/examples/openai-tunnel/tunnel-client.example.yaml +3 -3
  67. package/package.json +23 -14
  68. package/scripts/README.md +47 -0
  69. package/scripts/{brand-check.js → checks/brand-check.js} +2 -2
  70. package/scripts/checks/control-center-smoke.js +1098 -0
  71. package/scripts/{control-smoke.js → checks/control-smoke.js} +17 -3
  72. package/scripts/{doctor-smoke.js → checks/doctor-smoke.js} +1 -1
  73. package/scripts/{http-mcp-smoke.js → checks/http-mcp-smoke.js} +12 -4
  74. package/scripts/{lifecycle-smoke.js → checks/lifecycle-smoke.js} +347 -13
  75. package/scripts/{mcp-manifest-check.js → checks/mcp-manifest-check.js} +32 -9
  76. package/scripts/{mcp-smoke.js → checks/mcp-smoke.js} +14 -9
  77. package/scripts/{package-manifest-check.js → checks/package-manifest-check.js} +2 -1
  78. package/scripts/{tunnel-supervisor-smoke.js → checks/tunnel-supervisor-smoke.js} +2 -2
  79. package/scripts/checks/unit-tests.js +36 -0
  80. package/scripts/{watcher-supervisor-smoke.js → checks/watcher-supervisor-smoke.js} +11 -5
  81. package/scripts/control/control-center-tray.ps1 +281 -0
  82. package/scripts/{get-patchwarden-health.ps1 → control/get-patchwarden-health.ps1} +3 -3
  83. package/scripts/{manage-patchwarden.ps1 → control/manage-patchwarden.ps1} +30 -4
  84. package/scripts/control/restart-control-center.ps1 +173 -0
  85. package/scripts/{restart-patchwarden.ps1 → control/restart-patchwarden.ps1} +1 -1
  86. package/scripts/control/start-control-center.ps1 +263 -0
  87. package/scripts/{start-patchwarden-tunnel.ps1 → control/start-patchwarden-tunnel.ps1} +48 -6
  88. package/scripts/control/stop-patchwarden.ps1 +114 -0
  89. package/scripts/launchers/Check-PatchWarden-Health.cmd +1 -1
  90. package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +1 -1
  91. package/scripts/launchers/Restart-PatchWarden.cmd +1 -1
  92. package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +1 -1
  93. package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +1 -1
  94. package/scripts/{patchwarden-mcp-direct.cmd → mcp/patchwarden-mcp-direct.cmd} +1 -1
  95. package/scripts/{patchwarden-mcp-stdio.cmd → mcp/patchwarden-mcp-stdio.cmd} +1 -1
  96. package/scripts/release/pack-clean.js +305 -0
  97. package/src/controlCenter.ts +2152 -0
  98. package/src/doctor.ts +5 -5
  99. package/src/logging.ts +152 -0
  100. package/src/runner/changeCapture.ts +212 -1
  101. package/src/runner/runTask.ts +220 -22
  102. package/src/smoke-test.ts +5 -5
  103. package/src/test/unit/android-doctor.test.ts +158 -0
  104. package/src/test/unit/chinese-path.test.ts +106 -0
  105. package/src/test/unit/command-guard.test.ts +221 -0
  106. package/src/test/unit/direct-guards.test.ts +297 -0
  107. package/src/test/unit/logging.test.ts +325 -0
  108. package/src/test/unit/path-guard.test.ts +150 -0
  109. package/src/test/unit/safe-status.test.ts +187 -0
  110. package/src/test/unit/sensitive-guard.test.ts +124 -0
  111. package/src/test/unit/sync-file.test.ts +231 -0
  112. package/src/test/unit/watcher-status.test.ts +190 -0
  113. package/src/tools/androidDoctor.ts +424 -0
  114. package/src/tools/auditTask.ts +11 -5
  115. package/src/tools/getTaskSummary.ts +22 -1
  116. package/src/tools/healthCheck.ts +22 -0
  117. package/src/tools/registry.ts +63 -0
  118. package/src/tools/safeStatus.ts +96 -0
  119. package/src/tools/syncFile.ts +122 -0
  120. package/src/tools/toolCatalog.ts +2 -0
  121. package/src/version.ts +2 -2
  122. package/src/watcherStatus.ts +101 -4
  123. package/ui/colors_and_type.css +141 -0
  124. package/ui/pages/audit.html +743 -0
  125. package/ui/pages/dashboard.html +1154 -0
  126. package/ui/pages/direct-sessions.html +652 -0
  127. package/ui/pages/logs.html +502 -0
  128. package/ui/pages/task-detail.html +1229 -0
  129. package/ui/pages/tasks.html +702 -0
  130. package/ui/pages/workspace.html +947 -0
  131. package/ui/partials/project-shell.html +362 -0
  132. package/ui/vendor/lucide.js +12 -0
  133. package/ui/vendor/tailwindcss-browser.js +947 -0
  134. package/scripts/pack-clean.js +0 -141
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readdirSync } from "node:fs";
3
+ import { spawnSync } from "node:child_process";
4
+ import { resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const scriptDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
8
+ const root = resolve(scriptDir, "..", "..");
9
+ const unitDir = resolve(root, "dist", "test", "unit");
10
+
11
+ if (!existsSync(unitDir)) {
12
+ console.error(`[unit-tests] Missing compiled test directory: ${unitDir}`);
13
+ console.error("[unit-tests] Run npm run build before npm run test:unit.");
14
+ process.exit(1);
15
+ }
16
+
17
+ const testFiles = readdirSync(unitDir)
18
+ .filter((name) => name.endsWith(".test.js"))
19
+ .sort()
20
+ .map((name) => resolve(unitDir, name));
21
+
22
+ if (testFiles.length === 0) {
23
+ console.error(`[unit-tests] No compiled unit tests found in ${unitDir}`);
24
+ process.exit(1);
25
+ }
26
+
27
+ const result = spawnSync(process.execPath, ["--test", ...testFiles], {
28
+ stdio: "inherit",
29
+ });
30
+
31
+ if (result.error) {
32
+ console.error(`[unit-tests] Failed to run unit tests: ${result.error.message}`);
33
+ process.exit(1);
34
+ }
35
+
36
+ process.exit(typeof result.status === "number" ? result.status : 1);
@@ -19,7 +19,7 @@ if (process.platform !== "win32") {
19
19
  process.exit(0);
20
20
  }
21
21
 
22
- const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
22
+ const root = resolve(fileURLToPath(new URL("../..", import.meta.url)));
23
23
  const temp = mkdtempSync(join(tmpdir(), "patchwarden-watcher-supervisor-"));
24
24
  const children = [];
25
25
 
@@ -87,13 +87,19 @@ async function runExternalScenario() {
87
87
  function createFixture(name) {
88
88
  const project = join(temp, name, "patchwarden-fixture");
89
89
  const scripts = join(project, "scripts");
90
+ const controlScripts = join(scripts, "control");
91
+ const mcpScripts = join(scripts, "mcp");
92
+ const checkScripts = join(scripts, "checks");
90
93
  const runner = join(project, "dist", "runner");
91
94
  const workspace = join(project, "workspace");
92
95
  mkdirSync(scripts, { recursive: true });
96
+ mkdirSync(controlScripts, { recursive: true });
97
+ mkdirSync(mcpScripts, { recursive: true });
98
+ mkdirSync(checkScripts, { recursive: true });
93
99
  mkdirSync(runner, { recursive: true });
94
100
  mkdirSync(workspace, { recursive: true });
95
- cpSync(join(root, "scripts", "start-patchwarden-tunnel.ps1"), join(scripts, "start-patchwarden-tunnel.ps1"));
96
- writeFileSync(join(scripts, "patchwarden-mcp-stdio.cmd"), "@echo off\r\nexit /b 0\r\n", "utf-8");
101
+ cpSync(join(root, "scripts", "control", "start-patchwarden-tunnel.ps1"), join(controlScripts, "start-patchwarden-tunnel.ps1"));
102
+ writeFileSync(join(mcpScripts, "patchwarden-mcp-stdio.cmd"), "@echo off\r\nexit /b 0\r\n", "utf-8");
97
103
  const manifestFixture = JSON.stringify({
98
104
  ok: true,
99
105
  server_version: "0.6.0",
@@ -103,7 +109,7 @@ function createFixture(name) {
103
109
  tool_names: [],
104
110
  tool_manifest_sha256: "a".repeat(64),
105
111
  });
106
- writeFileSync(join(scripts, "mcp-manifest-check.js"), `process.stdout.write(${JSON.stringify(manifestFixture)});\n`, "utf-8");
112
+ writeFileSync(join(checkScripts, "mcp-manifest-check.js"), `process.stdout.write(${JSON.stringify(manifestFixture)});\n`, "utf-8");
107
113
  writeFileSync(join(project, "dist", "index.js"), "", "utf-8");
108
114
  const watcherAttemptPath = join(project, "watcher-attempt.txt");
109
115
  const watcherPath = join(runner, "watch.js");
@@ -140,7 +146,7 @@ function createFixture(name) {
140
146
 
141
147
  function runLauncher(fixture, mode, maxRestarts, lifetimeMs) {
142
148
  return spawnSync("powershell.exe", [
143
- "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", join(fixture.project, "scripts", "start-patchwarden-tunnel.ps1"),
149
+ "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", join(fixture.project, "scripts", "control", "start-patchwarden-tunnel.ps1"),
144
150
  "-TunnelId", "tunnel_watcher_fixture",
145
151
  "-TunnelClientExe", fixture.mockCmd,
146
152
  "-ProxyUrl", "http://127.0.0.1:1",
@@ -0,0 +1,281 @@
1
+ #Requires -Version 5.1
2
+ <#
3
+ .SYNOPSIS
4
+ Lightweight PatchWarden Control Center system tray entry.
5
+ .DESCRIPTION
6
+ Builds a Windows NotifyIcon (no Electron, no extra dependencies) whose menu
7
+ drives the Control Center via its HTTP API with the control token. If the
8
+ Control Center is not running when the tray starts, it is launched in the
9
+ background first.
10
+
11
+ Menu:
12
+ - Open Dashboard (opens the dashboard in the default browser)
13
+ - Status (shows a compact Core/Direct/Watcher summary)
14
+ - Start All (POST /api/start-all)
15
+ - Stop All (POST /api/stop-all)
16
+ - Restart All (POST /api/restart-all)
17
+ - Open Logs (POST /api/open-logs-folder)
18
+ - Quit Tray (disposes the tray and exits)
19
+ #>
20
+ param(
21
+ [int]$Port = 8090,
22
+ [switch]$NoStartupBalloon
23
+ )
24
+
25
+ $ErrorActionPreference = "Stop"
26
+ $ProjectRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
27
+ $BaseUrl = "http://127.0.0.1:$Port/"
28
+ $RuntimeDirectory = Join-Path $env:LOCALAPPDATA "patchwarden\control-center"
29
+ $StatusFile = Join-Path $RuntimeDirectory "control-center-status.json"
30
+ $MutexName = "Local\PatchWarden-Control-Tray-" + ([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($ProjectRoot)) -replace "[^A-Za-z0-9]", "").Substring(0, 16)
31
+
32
+ # ── Load WinForms + Drawing ──────────────────────────────────────
33
+ Add-Type -AssemblyName System.Windows.Forms
34
+ Add-Type -AssemblyName System.Drawing
35
+
36
+ $createdMutex = $false
37
+ $trayMutex = New-Object System.Threading.Mutex($true, $MutexName, [ref]$createdMutex)
38
+ if (-not $createdMutex) {
39
+ $trayMutex.Dispose()
40
+ exit 0
41
+ }
42
+
43
+ # ── Helpers ──────────────────────────────────────────────────────
44
+
45
+ function Invoke-ControlApi {
46
+ param([Parameter(Mandatory = $true)][string]$Path, [string]$Method = "POST")
47
+ $url = $BaseUrl.TrimEnd("/") + $Path
48
+ # Fetch the control token (in-memory, per server run).
49
+ $token = $null
50
+ try {
51
+ $tokenResp = Invoke-WebRequest -Uri ($BaseUrl.TrimEnd("/") + "/control-token.json") -UseBasicParsing -TimeoutSec 3 -ErrorAction Stop
52
+ $token = ($tokenResp.Content | ConvertFrom-Json).token
53
+ } catch {
54
+ [System.Windows.Forms.MessageBox]::Show(
55
+ "Could not reach the Control Center at $BaseUrl.`n`nError: $($_.Exception.Message)",
56
+ "PatchWarden Tray",
57
+ [System.Windows.Forms.MessageBoxButtons]::OK,
58
+ [System.Windows.Forms.MessageBoxIcon]::Warning
59
+ ) | Out-Null
60
+ return $null
61
+ }
62
+ $headers = @{ "X-PatchWarden-Control-Token" = $token }
63
+ try {
64
+ $resp = Invoke-WebRequest -Uri $url -Method $Method -Headers $headers -UseBasicParsing -TimeoutSec 60 -ErrorAction Stop
65
+ return $resp.Content | ConvertFrom-Json
66
+ } catch {
67
+ [System.Windows.Forms.MessageBox]::Show(
68
+ "API call failed: $Path`n`nError: $($_.Exception.Message)",
69
+ "PatchWarden Tray",
70
+ [System.Windows.Forms.MessageBoxButtons]::OK,
71
+ [System.Windows.Forms.MessageBoxIcon]::Warning
72
+ ) | Out-Null
73
+ return $null
74
+ }
75
+ }
76
+
77
+ function New-PatchWardenTrayIcon {
78
+ $bitmap = New-Object System.Drawing.Bitmap 32, 32
79
+ $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
80
+ $graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
81
+ $graphics.Clear([System.Drawing.Color]::Transparent)
82
+
83
+ $background = New-Object System.Drawing.SolidBrush -ArgumentList ([System.Drawing.Color]::FromArgb(255, 10, 14, 20))
84
+ $accent = New-Object System.Drawing.SolidBrush -ArgumentList ([System.Drawing.Color]::FromArgb(255, 45, 212, 168))
85
+ $darkPen = New-Object System.Drawing.Pen -ArgumentList ([System.Drawing.Color]::FromArgb(255, 5, 9, 14)), 1.5
86
+
87
+ $graphics.FillEllipse($background, 1, 1, 30, 30)
88
+ $points = [System.Drawing.Point[]]@(
89
+ [System.Drawing.Point]::new(16, 5),
90
+ [System.Drawing.Point]::new(25, 9),
91
+ [System.Drawing.Point]::new(24, 17),
92
+ [System.Drawing.Point]::new(16, 27),
93
+ [System.Drawing.Point]::new(8, 17),
94
+ [System.Drawing.Point]::new(7, 9)
95
+ )
96
+ $graphics.FillPolygon($accent, $points)
97
+ $graphics.DrawPolygon($darkPen, $points)
98
+ $graphics.DrawLine($darkPen, 16, 8, 16, 23)
99
+ $graphics.DrawLine($darkPen, 11, 15, 21, 15)
100
+
101
+ $handle = $bitmap.GetHicon()
102
+ try {
103
+ return [System.Drawing.Icon]::FromHandle($handle)
104
+ } finally {
105
+ $graphics.Dispose()
106
+ $background.Dispose()
107
+ $accent.Dispose()
108
+ $darkPen.Dispose()
109
+ }
110
+ }
111
+
112
+ function Show-TrayBalloon {
113
+ param([string]$Title, [string]$Text, [System.Windows.Forms.ToolTipIcon]$Icon = [System.Windows.Forms.ToolTipIcon]::Info)
114
+ if ($null -eq $script:notifyIcon) { return }
115
+ $script:notifyIcon.BalloonTipTitle = $Title
116
+ $script:notifyIcon.BalloonTipText = $Text
117
+ $script:notifyIcon.BalloonTipIcon = $Icon
118
+ $script:notifyIcon.ShowBalloonTip(2200)
119
+ }
120
+
121
+ function Invoke-TrayControlAction {
122
+ param([string]$Path, [string]$SuccessText)
123
+ $result = Invoke-ControlApi -Path $Path
124
+ if ($null -eq $result) { return }
125
+ if ($result.ok -eq $false) {
126
+ Show-TrayBalloon -Title "PatchWarden needs attention" -Text "Open Dashboard for details." -Icon ([System.Windows.Forms.ToolTipIcon]::Warning)
127
+ return
128
+ }
129
+ Show-TrayBalloon -Title "PatchWarden" -Text $SuccessText
130
+ }
131
+
132
+ function Test-ControlCenterRunning {
133
+ # Probe the status endpoint to confirm the server is live and is ours.
134
+ try {
135
+ $resp = Invoke-WebRequest -Uri ($BaseUrl.TrimEnd("/") + "/api/control-center-status") -UseBasicParsing -TimeoutSec 2 -ErrorAction Stop
136
+ if ($resp.StatusCode -eq 200) {
137
+ $body = $resp.Content | ConvertFrom-Json
138
+ return [bool]$body.running
139
+ }
140
+ } catch {}
141
+ return $false
142
+ }
143
+
144
+ function Ensure-ControlCenterRunning {
145
+ if (Test-ControlCenterRunning) { return $true }
146
+ Write-Host "[tray] Preparing PatchWarden Control Center..."
147
+ $startScript = Join-Path $PSScriptRoot "start-control-center.ps1"
148
+ if (-not (Test-Path -LiteralPath $startScript)) {
149
+ [System.Windows.Forms.MessageBox]::Show(
150
+ "start-control-center.ps1 not found at:`n$startScript",
151
+ "PatchWarden Tray",
152
+ [System.Windows.Forms.MessageBoxButtons]::OK,
153
+ [System.Windows.Forms.MessageBoxIcon]::Error
154
+ ) | Out-Null
155
+ return $false
156
+ }
157
+ try {
158
+ Start-Process -FilePath "powershell.exe" `
159
+ -ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $startScript, "-NoBrowser") `
160
+ -WindowStyle Hidden -Wait -ErrorAction Stop
161
+ } catch {
162
+ [System.Windows.Forms.MessageBox]::Show(
163
+ "Failed to start the Control Center:`n$($_.Exception.Message)",
164
+ "PatchWarden Tray",
165
+ [System.Windows.Forms.MessageBoxButtons]::OK,
166
+ [System.Windows.Forms.MessageBoxIcon]::Error
167
+ ) | Out-Null
168
+ return $false
169
+ }
170
+ # Wait up to 20s for readiness.
171
+ for ($i = 0; $i -lt 20; $i++) {
172
+ Start-Sleep -Seconds 1
173
+ if (Test-ControlCenterRunning) { return $true }
174
+ }
175
+ return $false
176
+ }
177
+
178
+ function Show-CompactStatus {
179
+ $data = Invoke-ControlApi -Path "/api/status" -Method "GET"
180
+ if ($null -eq $data) { return }
181
+
182
+ $core = if ($data.core.available) { "available" } else { "stopped" }
183
+ $direct = if ($data.direct.available) { "available" } else { "stopped" }
184
+ $watcher = if ($data.watcher.status) { [string]$data.watcher.status } else { "unknown" }
185
+ $workspace = if ($data.workspace_root) { [string]$data.workspace_root } else { "not configured" }
186
+
187
+ $message = @(
188
+ "Core: $core"
189
+ "Direct: $direct"
190
+ "Watcher: $watcher"
191
+ ""
192
+ "Workspace:"
193
+ $workspace
194
+ ""
195
+ "Open Dashboard for tasks, audit logs, Direct sessions, and setup details."
196
+ ) -join "`n"
197
+
198
+ [System.Windows.Forms.MessageBox]::Show(
199
+ $message,
200
+ "PatchWarden Status",
201
+ [System.Windows.Forms.MessageBoxButtons]::OK,
202
+ [System.Windows.Forms.MessageBoxIcon]::Information
203
+ ) | Out-Null
204
+ }
205
+
206
+ # ── Ensure the control center is up before showing the tray ──────
207
+ [void](Ensure-ControlCenterRunning)
208
+
209
+ # ── Build the tray icon ──────────────────────────────────────────
210
+ $script:notifyIcon = New-Object System.Windows.Forms.NotifyIcon
211
+ $notifyIcon = $script:notifyIcon
212
+ try {
213
+ $notifyIcon.Icon = New-PatchWardenTrayIcon
214
+ } catch {
215
+ $notifyIcon.Icon = [System.Drawing.SystemIcons]::Shield
216
+ }
217
+ $notifyIcon.Text = "PatchWarden: local control"
218
+ $notifyIcon.Visible = $true
219
+
220
+ # Double-click opens the dashboard.
221
+ $notifyIcon.Add_DoubleClick({
222
+ Start-Process -FilePath $BaseUrl
223
+ })
224
+
225
+ # ── Context menu ─────────────────────────────────────────────────
226
+ $menu = New-Object System.Windows.Forms.ContextMenuStrip
227
+
228
+ [void]$menu.Items.Add("Open Dashboard", $null, {
229
+ Start-Process -FilePath $BaseUrl
230
+ })
231
+
232
+ [void]$menu.Items.Add("Status", $null, {
233
+ Show-CompactStatus
234
+ })
235
+
236
+ [void]$menu.Items.Add("-") # separator
237
+
238
+ [void]$menu.Items.Add("Start All", $null, {
239
+ Invoke-TrayControlAction -Path "/api/start-all" -SuccessText "Core and Direct are starting in the background."
240
+ })
241
+
242
+ [void]$menu.Items.Add("Stop All", $null, {
243
+ Invoke-TrayControlAction -Path "/api/stop-all" -SuccessText "Core and Direct were asked to stop. Tray and dashboard stay available."
244
+ })
245
+
246
+ [void]$menu.Items.Add("Restart All", $null, {
247
+ Invoke-TrayControlAction -Path "/api/restart-all" -SuccessText "Core and Direct are restarting in the background."
248
+ })
249
+
250
+ [void]$menu.Items.Add("-") # separator
251
+
252
+ [void]$menu.Items.Add("Open Logs", $null, {
253
+ Invoke-TrayControlAction -Path "/api/open-logs-folder" -SuccessText "Opening PatchWarden runtime logs."
254
+ })
255
+
256
+ [void]$menu.Items.Add("-") # separator
257
+
258
+ [void]$menu.Items.Add("Quit Tray", $null, {
259
+ $notifyIcon.Visible = $false
260
+ [System.Windows.Forms.Application]::Exit()
261
+ })
262
+
263
+ $notifyIcon.ContextMenuStrip = $menu
264
+
265
+ # Show a brief balloon on launch.
266
+ if (-not $NoStartupBalloon) {
267
+ Show-TrayBalloon -Title "PatchWarden is ready" -Text "Right-click for quick controls. Open Dashboard for the full console."
268
+ }
269
+
270
+ Write-Host "[tray] PatchWarden Control Center tray is active. Right-click the icon for actions."
271
+
272
+ # Run the application loop. Without this the script exits immediately and the
273
+ # tray disappears. The loop exits when Application::Exit() is called from the
274
+ # Exit menu item.
275
+ [System.Windows.Forms.Application]::Run()
276
+
277
+ # Cleanup on exit.
278
+ $notifyIcon.Visible = $false
279
+ $notifyIcon.Dispose()
280
+ if ($trayMutex) { $trayMutex.ReleaseMutex(); $trayMutex.Dispose() }
281
+ Write-Host "[tray] Exited."
@@ -1,6 +1,6 @@
1
1
  $ErrorActionPreference = "Stop"
2
2
 
3
- $ProjectRoot = Split-Path -Parent $PSScriptRoot
3
+ $ProjectRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
4
4
  $ConfigPath = Join-Path $ProjectRoot "patchwarden.config.json"
5
5
  $RuntimeDirectory = Join-Path $env:LOCALAPPDATA "patchwarden\runtime"
6
6
  $HealthUrlFile = Join-Path $RuntimeDirectory "tunnel-health-url.txt"
@@ -15,7 +15,7 @@ $packageVersion = [string](Get-Content -LiteralPath (Join-Path $ProjectRoot "pac
15
15
 
16
16
  $configuredTunnelManifest = $null
17
17
  try {
18
- $manifestJson = node (Join-Path $ProjectRoot "scripts\mcp-manifest-check.js") 2>$null
18
+ $manifestJson = node (Join-Path $ProjectRoot "scripts\checks\mcp-manifest-check.js") 2>$null
19
19
  if ($LASTEXITCODE -eq 0 -and $manifestJson) {
20
20
  $configuredTunnelManifest = $manifestJson | ConvertFrom-Json
21
21
  }
@@ -70,7 +70,7 @@ if (Test-Path -LiteralPath $HealthUrlFile) {
70
70
 
71
71
  $tunnelProcesses = @($safeProcesses | Where-Object { $_.name -eq "tunnel-client.exe" })
72
72
  $mcpChildProcesses = @($safeProcesses | Where-Object {
73
- $_.command_line -match '(?i)patchwarden-mcp-stdio\.cmd|patchwarden\\dist\\index\.js|patchwarden/scripts/\.\./dist/index\.js'
73
+ $_.command_line -match '(?i)patchwarden-mcp-stdio\.cmd|patchwarden\\dist\\index\.js|patchwarden/scripts/mcp/../../dist/index.js'
74
74
  })
75
75
  $deploymentConsistent = [bool](
76
76
  $configuredTunnelManifest -and
@@ -1,4 +1,4 @@
1
- [CmdletBinding()]
1
+ [CmdletBinding()]
2
2
  param(
3
3
  [Parameter(Position = 0)]
4
4
  [ValidateSet("menu", "start", "stop", "restart", "status", "health", "reset-key", "kill")]
@@ -11,13 +11,14 @@ param(
11
11
  [switch]$SkipBuild,
12
12
  [switch]$WhatIf,
13
13
  [switch]$Json,
14
+ [switch]$Background,
14
15
  [int]$KillTimeoutSeconds = 10
15
16
  )
16
17
 
17
18
  $ErrorActionPreference = "Stop"
18
19
  $ProgressPreference = "SilentlyContinue"
19
- $ProjectRoot = Split-Path -Parent $PSScriptRoot
20
- $LauncherDirectory = Join-Path $PSScriptRoot "launchers"
20
+ $ProjectRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
21
+ $LauncherDirectory = Join-Path $ProjectRoot "scripts\launchers"
21
22
  $LocalLauncherDirectory = Join-Path $ProjectRoot ".local\launchers"
22
23
  $PatchWardenRuntimeRoot = Join-Path $env:LOCALAPPDATA "patchwarden"
23
24
 
@@ -518,7 +519,32 @@ function Start-Mode {
518
519
  }
519
520
  Assert-NoUnsafePortConflicts -Definitions @($Definition)
520
521
 
521
- $launcher = if (Test-Path -LiteralPath $Definition.LocalLauncher) { $Definition.LocalLauncher } else { $Definition.GenericLauncher }
522
+ if ($Background) {
523
+ $launcherScript = Join-Path $PSScriptRoot "start-patchwarden-tunnel.ps1"
524
+ if (-not (Test-Path -LiteralPath $launcherScript)) { throw "Launcher script not found: $launcherScript" }
525
+ $healthListenAddr = ([Uri]$Definition.HealthBaseUrl).Authority
526
+ $arguments = @(
527
+ "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $launcherScript,
528
+ "-ToolProfile", $Definition.ToolProfile,
529
+ "-Profile", $Definition.Profile,
530
+ "-HealthListenAddr", $healthListenAddr,
531
+ "-NoTunnelWebUi"
532
+ )
533
+ if (-not $Definition.HasWatcher) { $arguments += "-SkipWatcher" }
534
+ if ($WhatIf) {
535
+ Write-Host "[start:$($Definition.Key)] WHAT-IF: start hidden supervisor for $($Definition.Profile)" -ForegroundColor Magenta
536
+ return
537
+ }
538
+ $process = Start-Process -FilePath "powershell.exe" -ArgumentList $arguments -WorkingDirectory $ProjectRoot -PassThru -WindowStyle Hidden
539
+ Write-Host "[start:$($Definition.Key)] Started hidden supervisor PID $($process.Id). Logs: $($Definition.RuntimeDirectory)" -ForegroundColor Green
540
+ return
541
+ }
542
+
543
+ # Prefer the project's built-in launcher (scripts\launchers\*.cmd) over
544
+ # .local\launchers\*.local.cmd. A .local launcher, if present, MUST call
545
+ # the project root scripts\launchers\*.cmd via relative path (..\..\scripts\launchers\*),
546
+ # NOT assume its own directory contains the target script.
547
+ $launcher = if (Test-Path -LiteralPath $Definition.GenericLauncher) { $Definition.GenericLauncher } else { $Definition.LocalLauncher }
522
548
  if (-not (Test-Path -LiteralPath $launcher)) { throw "Launcher not found: $launcher" }
523
549
  $launcherName = Split-Path -Leaf $launcher
524
550
  if ($WhatIf) {
@@ -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