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.
- package/PatchWarden-Control-Tray.cmd +11 -0
- package/PatchWarden-Control.cmd +6 -0
- package/PatchWarden-Desktop.cmd +5 -0
- package/PatchWarden.cmd +1 -1
- package/README.en.md +106 -18
- package/README.md +30 -19
- package/Restart-PatchWarden-Control.cmd +6 -0
- package/Stop-PatchWarden.cmd +11 -0
- package/dist/controlCenter.d.ts +14 -0
- package/dist/controlCenter.js +2002 -0
- package/dist/doctor.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/control-center/README.md +33 -0
- package/docs/control-center/control-center-daily-driver.md +211 -0
- package/docs/control-center/control-center-mvp.md +205 -0
- package/docs/control-center/control-center-phase2.md +159 -0
- package/docs/demo.md +3 -0
- package/docs/release-v0.6.4.md +45 -0
- package/examples/openai-tunnel/README.md +5 -5
- package/examples/openai-tunnel/tunnel-client.example.yaml +3 -3
- package/package.json +23 -15
- package/scripts/README.md +47 -0
- package/scripts/{brand-check.js → checks/brand-check.js} +2 -2
- package/scripts/checks/control-center-smoke.js +1098 -0
- package/scripts/{control-smoke.js → checks/control-smoke.js} +17 -3
- package/scripts/{doctor-smoke.js → checks/doctor-smoke.js} +1 -1
- package/scripts/{http-mcp-smoke.js → checks/http-mcp-smoke.js} +2 -2
- package/scripts/{lifecycle-smoke.js → checks/lifecycle-smoke.js} +12 -12
- package/scripts/{mcp-manifest-check.js → checks/mcp-manifest-check.js} +2 -2
- package/scripts/{mcp-smoke.js → checks/mcp-smoke.js} +4 -2
- package/scripts/{package-manifest-check.js → checks/package-manifest-check.js} +2 -1
- package/scripts/{tunnel-supervisor-smoke.js → checks/tunnel-supervisor-smoke.js} +2 -2
- package/scripts/{unit-tests.js → checks/unit-tests.js} +1 -1
- package/scripts/{watcher-supervisor-smoke.js → checks/watcher-supervisor-smoke.js} +11 -5
- package/scripts/control/control-center-tray.ps1 +281 -0
- package/scripts/{get-patchwarden-health.ps1 → control/get-patchwarden-health.ps1} +3 -3
- package/scripts/{manage-patchwarden.ps1 → control/manage-patchwarden.ps1} +30 -4
- package/scripts/control/restart-control-center.ps1 +173 -0
- package/scripts/{restart-patchwarden.ps1 → control/restart-patchwarden.ps1} +1 -1
- package/scripts/control/start-control-center.ps1 +263 -0
- package/scripts/{start-patchwarden-tunnel.ps1 → control/start-patchwarden-tunnel.ps1} +48 -6
- package/scripts/control/stop-patchwarden.ps1 +114 -0
- package/scripts/launchers/Check-PatchWarden-Health.cmd +1 -1
- package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +1 -1
- package/scripts/launchers/Restart-PatchWarden.cmd +1 -1
- package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +1 -1
- package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +1 -1
- package/scripts/{patchwarden-mcp-direct.cmd → mcp/patchwarden-mcp-direct.cmd} +1 -1
- package/scripts/{patchwarden-mcp-stdio.cmd → mcp/patchwarden-mcp-stdio.cmd} +1 -1
- package/scripts/{pack-clean.js → release/pack-clean.js} +9 -1
- package/src/controlCenter.ts +2152 -0
- package/src/doctor.ts +4 -4
- package/src/version.ts +1 -1
- package/ui/colors_and_type.css +141 -0
- package/ui/pages/audit.html +743 -0
- package/ui/pages/dashboard.html +1154 -0
- package/ui/pages/direct-sessions.html +652 -0
- package/ui/pages/logs.html +502 -0
- package/ui/pages/task-detail.html +1229 -0
- package/ui/pages/tasks.html +702 -0
- package/ui/pages/workspace.html +947 -0
- package/ui/partials/project-shell.html +362 -0
- package/ui/vendor/lucide.js +12 -0
- package/ui/vendor/tailwindcss-browser.js +947 -0
|
@@ -11,8 +11,8 @@ if (process.platform !== "win32") {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const scriptDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
|
|
14
|
-
const root = resolve(scriptDir, "..");
|
|
15
|
-
const manager = join(
|
|
14
|
+
const root = resolve(scriptDir, "..", "..");
|
|
15
|
+
const manager = join(root, "scripts", "control", "manage-patchwarden.ps1");
|
|
16
16
|
const temp = mkdtempSync(join(tmpdir(), "patchwarden-control-smoke-"));
|
|
17
17
|
const mockConfig = join(temp, "patchwarden.config.json");
|
|
18
18
|
writeFileSync(mockConfig, JSON.stringify({
|
|
@@ -139,7 +139,13 @@ try {
|
|
|
139
139
|
|
|
140
140
|
const expectedFiles = [
|
|
141
141
|
"PatchWarden.cmd",
|
|
142
|
-
"
|
|
142
|
+
"PatchWarden-Control.cmd",
|
|
143
|
+
"PatchWarden-Control-Tray.cmd",
|
|
144
|
+
"PatchWarden-Desktop.cmd",
|
|
145
|
+
"Restart-PatchWarden-Control.cmd",
|
|
146
|
+
"Stop-PatchWarden.cmd",
|
|
147
|
+
"scripts/control/manage-patchwarden.ps1",
|
|
148
|
+
"scripts/control/stop-patchwarden.ps1",
|
|
143
149
|
"scripts/launchers/Start-PatchWarden-Tunnel.cmd",
|
|
144
150
|
"scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
|
|
145
151
|
];
|
|
@@ -152,6 +158,14 @@ try {
|
|
|
152
158
|
if (!rootEntry.includes("manage-patchwarden.ps1")) {
|
|
153
159
|
throw new Error("PatchWarden.cmd does not invoke the consolidated manager");
|
|
154
160
|
}
|
|
161
|
+
const stopEntry = readFileSync(join(root, "Stop-PatchWarden.cmd"), "utf8");
|
|
162
|
+
if (!stopEntry.includes("scripts\\control\\stop-patchwarden.ps1")) {
|
|
163
|
+
throw new Error("Stop-PatchWarden.cmd does not invoke the one-click shutdown script");
|
|
164
|
+
}
|
|
165
|
+
const desktopEntry = readFileSync(join(root, "PatchWarden-Desktop.cmd"), "utf8");
|
|
166
|
+
if (!desktopEntry.includes("control-center-tray.ps1") || !desktopEntry.includes("WindowStyle Hidden")) {
|
|
167
|
+
throw new Error("PatchWarden-Desktop.cmd must launch the tray hidden as the daily desktop entry");
|
|
168
|
+
}
|
|
155
169
|
console.log("ok - control handles orphan cleanup, scoped kill, port conflicts, health fallback, and Core/Direct lifecycle actions");
|
|
156
170
|
} finally {
|
|
157
171
|
if (fakeTunnel?.exitCode === null) fakeTunnel.kill();
|
|
@@ -5,7 +5,7 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
|
-
const root = resolve(fileURLToPath(new URL("
|
|
8
|
+
const root = resolve(fileURLToPath(new URL("../..", import.meta.url)));
|
|
9
9
|
const tempRoot = mkdtempSync(join(tmpdir(), "patchwarden-doctor-"));
|
|
10
10
|
const workspaceRoot = join(tempRoot, "workspace");
|
|
11
11
|
const configPath = join(tempRoot, "patchwarden.config.json");
|
|
@@ -11,10 +11,10 @@ import { tmpdir } from "node:os";
|
|
|
11
11
|
import { dirname, join, resolve } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
14
|
-
import { CHATGPT_CORE_TOOL_NAMES } from "
|
|
14
|
+
import { CHATGPT_CORE_TOOL_NAMES } from "../../dist/tools/toolCatalog.js";
|
|
15
15
|
|
|
16
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
const root = resolve(__dirname, "..");
|
|
17
|
+
const root = resolve(__dirname, "..", "..");
|
|
18
18
|
const serverPath = resolve(root, "dist", "httpServer.js");
|
|
19
19
|
|
|
20
20
|
const host = "127.0.0.1";
|
|
@@ -148,18 +148,18 @@ try {
|
|
|
148
148
|
);
|
|
149
149
|
process.env.PATCHWARDEN_CONFIG = configPath;
|
|
150
150
|
|
|
151
|
-
const { savePlan } = await import("
|
|
152
|
-
const { createTask } = await import("
|
|
153
|
-
const { getTaskStatus } = await import("
|
|
154
|
-
const { cancelTask } = await import("
|
|
155
|
-
const { killTask } = await import("
|
|
156
|
-
const { getDiff } = await import("
|
|
157
|
-
const { listAgents } = await import("
|
|
158
|
-
const { getTaskSummary } = await import("
|
|
159
|
-
const { auditTask } = await import("
|
|
160
|
-
const { waitForTask } = await import("
|
|
161
|
-
const { runTask } = await import("
|
|
162
|
-
const { reloadConfig } = await import("
|
|
151
|
+
const { savePlan } = await import("../../dist/tools/savePlan.js");
|
|
152
|
+
const { createTask } = await import("../../dist/tools/createTask.js");
|
|
153
|
+
const { getTaskStatus } = await import("../../dist/tools/getTaskStatus.js");
|
|
154
|
+
const { cancelTask } = await import("../../dist/tools/cancelTask.js");
|
|
155
|
+
const { killTask } = await import("../../dist/tools/killTask.js");
|
|
156
|
+
const { getDiff } = await import("../../dist/tools/taskOutputs.js");
|
|
157
|
+
const { listAgents } = await import("../../dist/tools/listAgents.js");
|
|
158
|
+
const { getTaskSummary } = await import("../../dist/tools/getTaskSummary.js");
|
|
159
|
+
const { auditTask } = await import("../../dist/tools/auditTask.js");
|
|
160
|
+
const { waitForTask } = await import("../../dist/tools/waitForTask.js");
|
|
161
|
+
const { runTask } = await import("../../dist/runner/runTask.js");
|
|
162
|
+
const { reloadConfig } = await import("../../dist/config.js");
|
|
163
163
|
|
|
164
164
|
await test("list_agents reports configured executables", async () => {
|
|
165
165
|
const result = listAgents();
|
|
@@ -4,9 +4,9 @@ import { resolve } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
6
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
7
|
-
import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES } from "
|
|
7
|
+
import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES } from "../../dist/tools/toolCatalog.js";
|
|
8
8
|
|
|
9
|
-
const root = resolve(fileURLToPath(new URL("
|
|
9
|
+
const root = resolve(fileURLToPath(new URL("../..", import.meta.url)));
|
|
10
10
|
const jsonOnly = process.argv.includes("--json");
|
|
11
11
|
const profileIndex = process.argv.indexOf("--profile");
|
|
12
12
|
const profile = profileIndex >= 0 ? process.argv[profileIndex + 1] : "chatgpt_core";
|
|
@@ -14,7 +14,9 @@ import { fileURLToPath } from "node:url";
|
|
|
14
14
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
15
15
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
16
16
|
|
|
17
|
-
const root = resolve(fileURLToPath(new URL("
|
|
17
|
+
const root = resolve(fileURLToPath(new URL("../..", import.meta.url)));
|
|
18
|
+
const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
|
|
19
|
+
const expectedServerVersion = packageJson.version;
|
|
18
20
|
const tempRoot = mkdtempSync(join(tmpdir(), "patchwarden-mcp-"));
|
|
19
21
|
const workspaceRoot = join(tempRoot, "workspace");
|
|
20
22
|
const configPath = join(tempRoot, "patchwarden.config.json");
|
|
@@ -212,7 +214,7 @@ try {
|
|
|
212
214
|
test_command: "npm test",
|
|
213
215
|
});
|
|
214
216
|
if (
|
|
215
|
-
task.server_version !==
|
|
217
|
+
task.server_version !== expectedServerVersion ||
|
|
216
218
|
!/^[a-f0-9]{64}$/.test(task.tool_manifest_sha256 || "") ||
|
|
217
219
|
task.next_tool_call?.name !== "wait_for_task" ||
|
|
218
220
|
task.next_tool_call?.arguments?.timeout_seconds !== 25
|
|
@@ -45,7 +45,8 @@ if (leaked.length > 0) {
|
|
|
45
45
|
|
|
46
46
|
const required = [
|
|
47
47
|
"PatchWarden.cmd",
|
|
48
|
-
"
|
|
48
|
+
"PatchWarden-Desktop.cmd",
|
|
49
|
+
"scripts/control/manage-patchwarden.ps1",
|
|
49
50
|
"scripts/launchers/Start-PatchWarden-Tunnel.cmd",
|
|
50
51
|
"scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
|
|
51
52
|
];
|
|
@@ -10,7 +10,7 @@ if (process.platform !== "win32") {
|
|
|
10
10
|
process.exit(0);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const root = resolve(fileURLToPath(new URL("
|
|
13
|
+
const root = resolve(fileURLToPath(new URL("../..", import.meta.url)));
|
|
14
14
|
const temp = mkdtempSync(join(tmpdir(), "patchwarden-tunnel-smoke-"));
|
|
15
15
|
const mockJs = join(temp, "mock-tunnel-client.js");
|
|
16
16
|
const mockCmd = join(temp, "mock-tunnel-client.cmd");
|
|
@@ -57,7 +57,7 @@ if(command==='run'){
|
|
|
57
57
|
MOCK_TUNNEL_STATE: stateFile,
|
|
58
58
|
};
|
|
59
59
|
const result = spawnSync("powershell.exe", [
|
|
60
|
-
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", join(root, "scripts", "start-patchwarden-tunnel.ps1"),
|
|
60
|
+
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", join(root, "scripts", "control", "start-patchwarden-tunnel.ps1"),
|
|
61
61
|
"-TunnelId", "tunnel_smoke_fixture",
|
|
62
62
|
"-TunnelClientExe", mockCmd,
|
|
63
63
|
"-ConfigPath", mockConfig,
|
|
@@ -5,7 +5,7 @@ import { resolve } from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
7
|
const scriptDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
|
|
8
|
-
const root = resolve(scriptDir, "..");
|
|
8
|
+
const root = resolve(scriptDir, "..", "..");
|
|
9
9
|
const unitDir = resolve(root, "dist", "test", "unit");
|
|
10
10
|
|
|
11
11
|
if (!existsSync(unitDir)) {
|
|
@@ -19,7 +19,7 @@ if (process.platform !== "win32") {
|
|
|
19
19
|
process.exit(0);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const root = resolve(fileURLToPath(new 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(
|
|
96
|
-
writeFileSync(join(
|
|
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(
|
|
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
|
|
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 $
|
|
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
|
-
|
|
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) {
|