patchwarden 1.1.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +83 -8
- package/README.md +78 -10
- package/dist/assessments/agentAssessor.d.ts +1 -1
- package/dist/assessments/agentAssessor.js +3 -3
- package/dist/assessments/assessmentStore.d.ts +1 -1
- package/dist/assessments/assessmentStore.js +2 -2
- package/dist/assessments/confirmCli.js +5 -4
- package/dist/config.d.ts +7 -0
- package/dist/config.js +34 -0
- package/dist/control/middleware/auth.d.ts +10 -0
- package/dist/control/middleware/auth.js +8 -0
- package/dist/control/middleware/static.d.ts +3 -0
- package/dist/control/middleware/static.js +65 -0
- package/dist/control/routes/audit.d.ts +15 -0
- package/dist/control/routes/audit.js +277 -0
- package/dist/control/routes/evidence.d.ts +4 -0
- package/dist/control/routes/evidence.js +96 -0
- package/dist/control/routes/lineage.d.ts +3 -0
- package/dist/control/routes/lineage.js +71 -0
- package/dist/control/routes/policy.d.ts +3 -0
- package/dist/control/routes/policy.js +81 -0
- package/dist/control/routes/process.d.ts +5 -0
- package/dist/control/routes/process.js +200 -0
- package/dist/control/routes/sessions.d.ts +22 -0
- package/dist/control/routes/sessions.js +224 -0
- package/dist/control/routes/status.d.ts +6 -0
- package/dist/control/routes/status.js +250 -0
- package/dist/control/routes/taskActions.d.ts +21 -0
- package/dist/control/routes/taskActions.js +233 -0
- package/dist/control/routes/tasks.d.ts +20 -0
- package/dist/control/routes/tasks.js +310 -0
- package/dist/control/routes/workspace.d.ts +15 -0
- package/dist/control/routes/workspace.js +193 -0
- package/dist/control/runtime.d.ts +91 -0
- package/dist/control/runtime.js +392 -0
- package/dist/control/server.d.ts +13 -0
- package/dist/control/server.js +479 -0
- package/dist/control/shared.d.ts +35 -0
- package/dist/control/shared.js +288 -0
- package/dist/controlCenter.d.ts +6 -0
- package/dist/controlCenter.js +7 -2001
- package/dist/direct/directGuards.js +30 -8
- package/dist/direct/directSessionStore.d.ts +2 -0
- package/dist/direct/directVerification.js +7 -0
- package/dist/doctor.d.ts +18 -1
- package/dist/doctor.js +579 -348
- package/dist/goal/goalReport.d.ts +54 -0
- package/dist/goal/goalReport.js +204 -0
- package/dist/goal/goalStatus.d.ts +6 -0
- package/dist/goal/specKitImport.d.ts +63 -0
- package/dist/goal/specKitImport.js +220 -0
- package/dist/goal/subgoalSync.js +2 -1
- package/dist/httpServer.js +15 -12
- package/dist/index.js +7 -4
- package/dist/logging.d.ts +7 -1
- package/dist/logging.js +8 -0
- package/dist/policy/projectPolicy.d.ts +55 -0
- package/dist/policy/projectPolicy.js +286 -0
- package/dist/runner/changeCapture.d.ts +3 -3
- package/dist/runner/changeCapture.js +63 -39
- package/dist/runner/cli.js +7 -6
- package/dist/runner/postTaskCleanup.js +26 -2
- package/dist/runner/runTask.js +245 -221
- package/dist/runner/simpleProcess.js +4 -4
- package/dist/runner/watch.js +17 -14
- package/dist/security/contentRedaction.d.ts +6 -0
- package/dist/security/contentRedaction.js +22 -0
- package/dist/smoke-test.js +261 -255
- package/dist/test/unit/apply-patch.test.d.ts +1 -0
- package/dist/test/unit/apply-patch.test.js +225 -0
- package/dist/test/unit/create-task.test.d.ts +1 -0
- package/dist/test/unit/create-task.test.js +197 -0
- package/dist/test/unit/direct-guards.test.js +124 -9
- package/dist/test/unit/evidence-pack.test.d.ts +1 -0
- package/dist/test/unit/evidence-pack.test.js +224 -0
- package/dist/test/unit/get-task-status.test.d.ts +1 -0
- package/dist/test/unit/get-task-status.test.js +174 -0
- package/dist/test/unit/get-task-summary.test.d.ts +1 -0
- package/dist/test/unit/get-task-summary.test.js +146 -0
- package/dist/test/unit/goal-report.test.d.ts +1 -0
- package/dist/test/unit/goal-report.test.js +159 -0
- package/dist/test/unit/goal-subgoal-task.test.js +6 -6
- package/dist/test/unit/goal-tools-registry.test.js +6 -4
- package/dist/test/unit/path-guard.test.js +24 -0
- package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
- package/dist/test/unit/project-policy-release-mode.test.js +125 -0
- package/dist/test/unit/run-task-loop.test.d.ts +1 -0
- package/dist/test/unit/run-task-loop.test.js +380 -0
- package/dist/test/unit/schema-drift-check.test.js +10 -9
- package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
- package/dist/test/unit/spec-kit-import.test.js +341 -0
- package/dist/test/unit/wait-for-task.test.d.ts +1 -0
- package/dist/test/unit/wait-for-task.test.js +144 -0
- package/dist/tools/auditTask.d.ts +8 -63
- package/dist/tools/auditTask.js +12 -8
- package/dist/tools/createDirectSession.d.ts +1 -1
- package/dist/tools/createDirectSession.js +2 -2
- package/dist/tools/createTask.d.ts +2 -2
- package/dist/tools/createTask.js +4 -4
- package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
- package/dist/tools/dispatch/coreDispatch.js +282 -0
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
- package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
- package/dist/tools/dispatch/directDispatch.d.ts +8 -0
- package/dist/tools/dispatch/directDispatch.js +115 -0
- package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
- package/dist/tools/dispatch/goalDispatch.js +91 -0
- package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
- package/dist/tools/dispatch/releaseDispatch.js +45 -0
- package/dist/tools/dispatch/types.d.ts +23 -0
- package/dist/tools/dispatch/types.js +15 -0
- package/dist/tools/evidencePack.d.ts +45 -0
- package/dist/tools/evidencePack.js +375 -0
- package/dist/tools/finalizeDirectSession.d.ts +1 -1
- package/dist/tools/finalizeDirectSession.js +3 -3
- package/dist/tools/goalSubgoalTask.d.ts +1 -1
- package/dist/tools/goalSubgoalTask.js +2 -2
- package/dist/tools/healthCheck.js +3 -3
- package/dist/tools/recommendAgentForTask.d.ts +19 -0
- package/dist/tools/recommendAgentForTask.js +56 -0
- package/dist/tools/registry.d.ts +3 -3
- package/dist/tools/registry.js +333 -402
- package/dist/tools/releaseMode.d.ts +50 -0
- package/dist/tools/releaseMode.js +370 -0
- package/dist/tools/retryTask.d.ts +2 -2
- package/dist/tools/retryTask.js +2 -2
- package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
- package/dist/tools/runDirectVerificationBundle.js +64 -0
- package/dist/tools/runTaskLoop.d.ts +57 -0
- package/dist/tools/runTaskLoop.js +417 -0
- package/dist/tools/runVerification.d.ts +4 -0
- package/dist/tools/runVerification.js +4 -0
- package/dist/tools/safeViews.d.ts +8 -2
- package/dist/tools/safeViews.js +4 -2
- package/dist/tools/taskLineage.d.ts +91 -0
- package/dist/tools/taskLineage.js +175 -0
- package/dist/tools/toolCatalog.d.ts +2 -2
- package/dist/tools/toolCatalog.js +6 -0
- package/dist/tools/toolRegistry.js +132 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/docs/CODE_WIKI.md +983 -0
- package/docs/agentseal-integration.md +150 -0
- package/docs/architecture.md +63 -0
- package/docs/assets/patchwarden-oss-demo.gif +0 -0
- package/docs/chatgpt-usage.md +31 -0
- package/docs/control-center/README.md +9 -0
- package/docs/dashboard-overview.md +86 -0
- package/docs/demo.md +8 -0
- package/docs/direct-session-workflow.md +98 -0
- package/docs/evidence-pack-schema.md +215 -0
- package/docs/execution-plan-2026-07-09.md +315 -0
- package/docs/lineage-evidence-pack-workflow.md +127 -0
- package/docs/mcp-inspector-testing.md +200 -0
- package/docs/open-source-application.md +162 -0
- package/docs/opencode-worker.md +151 -0
- package/docs/openhands-worker.md +181 -0
- package/docs/release-evidence.md +72 -0
- package/docs/roadmap-execution-and-acceptance.md +365 -0
- package/docs/spec-kit-integration.md +131 -0
- package/docs/task-safe-review-workflow.md +98 -0
- package/docs/threat-model.md +79 -0
- package/docs/user-feedback.md +40 -0
- package/docs/why-patchwarden.md +110 -0
- package/package.json +3 -3
- package/scripts/checks/control-center-smoke.js +443 -0
- package/scripts/checks/control-smoke.js +2 -2
- package/scripts/checks/lifecycle-smoke.js +29 -23
- package/scripts/checks/mcp-manifest-check.js +12 -0
- package/scripts/checks/mcp-smoke.js +33 -7
- package/scripts/checks/watcher-supervisor-smoke.js +1 -1
- package/scripts/generate-demo-gif.py +320 -0
- package/src/assessments/agentAssessor.ts +3 -3
- package/src/assessments/assessmentStore.ts +2 -2
- package/src/assessments/confirmCli.ts +5 -4
- package/src/config.ts +37 -0
- package/src/control/middleware/auth.ts +17 -0
- package/src/control/middleware/static.ts +71 -0
- package/src/control/routes/audit.ts +321 -0
- package/src/control/routes/evidence.ts +107 -0
- package/src/control/routes/lineage.ts +92 -0
- package/src/control/routes/policy.ts +81 -0
- package/src/control/routes/process.ts +204 -0
- package/src/control/routes/sessions.ts +251 -0
- package/src/control/routes/status.ts +325 -0
- package/src/control/routes/taskActions.ts +248 -0
- package/src/control/routes/tasks.ts +323 -0
- package/src/control/routes/workspace.ts +203 -0
- package/src/control/runtime.ts +472 -0
- package/src/control/server.ts +471 -0
- package/src/control/shared.ts +294 -0
- package/src/controlCenter.ts +7 -2150
- package/src/direct/directGuards.ts +28 -7
- package/src/direct/directSessionStore.ts +2 -0
- package/src/direct/directVerification.ts +7 -0
- package/src/doctor.ts +741 -481
- package/src/goal/goalReport.ts +271 -0
- package/src/goal/goalStatus.ts +6 -0
- package/src/goal/specKitImport.ts +355 -0
- package/src/goal/subgoalSync.ts +4 -2
- package/src/httpServer.ts +17 -14
- package/src/index.ts +7 -4
- package/src/logging.ts +10 -1
- package/src/policy/projectPolicy.ts +344 -0
- package/src/runner/changeCapture.ts +70 -42
- package/src/runner/cli.ts +7 -6
- package/src/runner/postTaskCleanup.ts +26 -2
- package/src/runner/runTask.ts +325 -223
- package/src/runner/simpleProcess.ts +4 -4
- package/src/runner/watch.ts +17 -14
- package/src/security/contentRedaction.ts +29 -0
- package/src/smoke-test.ts +254 -252
- package/src/test/unit/apply-patch.test.ts +293 -0
- package/src/test/unit/create-task.test.ts +255 -0
- package/src/test/unit/direct-guards.test.ts +178 -8
- package/src/test/unit/evidence-pack.test.ts +251 -0
- package/src/test/unit/get-task-status.test.ts +203 -0
- package/src/test/unit/get-task-summary.test.ts +173 -0
- package/src/test/unit/goal-report.test.ts +189 -0
- package/src/test/unit/goal-subgoal-task.test.ts +6 -6
- package/src/test/unit/goal-tools-registry.test.ts +7 -5
- package/src/test/unit/path-guard.test.ts +30 -0
- package/src/test/unit/project-policy-release-mode.test.ts +156 -0
- package/src/test/unit/run-task-loop.test.ts +425 -0
- package/src/test/unit/schema-drift-check.test.ts +11 -9
- package/src/test/unit/spec-kit-import.test.ts +429 -0
- package/src/test/unit/wait-for-task.test.ts +176 -0
- package/src/tools/auditTask.ts +99 -59
- package/src/tools/createDirectSession.ts +3 -3
- package/src/tools/createTask.ts +7 -7
- package/src/tools/dispatch/coreDispatch.ts +374 -0
- package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
- package/src/tools/dispatch/directDispatch.ts +167 -0
- package/src/tools/dispatch/goalDispatch.ts +127 -0
- package/src/tools/dispatch/releaseDispatch.ts +65 -0
- package/src/tools/dispatch/types.ts +24 -0
- package/src/tools/evidencePack.ts +490 -0
- package/src/tools/finalizeDirectSession.ts +4 -4
- package/src/tools/goalSubgoalTask.ts +2 -2
- package/src/tools/healthCheck.ts +3 -3
- package/src/tools/listWorkspace.ts +71 -71
- package/src/tools/recommendAgentForTask.ts +79 -0
- package/src/tools/registry.ts +352 -509
- package/src/tools/releaseMode.ts +450 -0
- package/src/tools/retryTask.ts +2 -2
- package/src/tools/runDirectVerificationBundle.ts +98 -0
- package/src/tools/runTaskLoop.ts +526 -0
- package/src/tools/runVerification.ts +8 -0
- package/src/tools/safeViews.ts +4 -2
- package/src/tools/taskLineage.ts +300 -0
- package/src/tools/toolCatalog.ts +6 -0
- package/src/tools/toolRegistry.ts +132 -0
- package/src/version.ts +2 -2
- package/ui/pages/audit.html +192 -3
- package/ui/pages/dashboard.html +1070 -14
- package/ui/pages/direct-sessions.html +505 -53
- package/ui/pages/task-detail.html +456 -438
- package/ui/pages/tasks.html +598 -61
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PatchWarden Control Center — shared infrastructure.
|
|
3
|
+
*
|
|
4
|
+
* Holds the fault-tolerant config bootstrap, path constants, the in-memory
|
|
5
|
+
* control token, port resolution, and the generic HTTP/file helpers reused by
|
|
6
|
+
* every route module. Importing this module performs the same bootstrap side
|
|
7
|
+
* effects the original monolithic controlCenter.ts did (config load, token
|
|
8
|
+
* generation, port resolution) so that a single import wires up the runtime.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
11
|
+
import { delimiter, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
16
|
+
import { loadConfig, type PatchWardenConfig } from "../config.js";
|
|
17
|
+
import { logger } from "../logging.js";
|
|
18
|
+
|
|
19
|
+
// ── Paths ─────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
22
|
+
export const uiRoot = join(projectRoot, "ui");
|
|
23
|
+
export const manageScriptPath = join(projectRoot, "scripts", "control", "manage-patchwarden.ps1");
|
|
24
|
+
|
|
25
|
+
export const PAGE_ALIASES: Record<string, string> = {
|
|
26
|
+
"/dashboard.html": "pages/dashboard.html",
|
|
27
|
+
"/tasks.html": "pages/tasks.html",
|
|
28
|
+
"/workspace.html": "pages/workspace.html",
|
|
29
|
+
"/audit.html": "pages/audit.html",
|
|
30
|
+
"/task-detail.html": "pages/task-detail.html",
|
|
31
|
+
"/direct-sessions.html": "pages/direct-sessions.html",
|
|
32
|
+
"/logs.html": "pages/logs.html",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ── Config (fault-tolerant bootstrap) ─────────────────────────────
|
|
36
|
+
|
|
37
|
+
export function createFallbackConfig(): PatchWardenConfig {
|
|
38
|
+
return {
|
|
39
|
+
workspaceRoot: process.cwd(),
|
|
40
|
+
plansDir: ".patchwarden/plans",
|
|
41
|
+
tasksDir: ".patchwarden/tasks",
|
|
42
|
+
assessmentsDir: ".patchwarden/assessments",
|
|
43
|
+
assessmentTtlSeconds: 3600,
|
|
44
|
+
agents: {},
|
|
45
|
+
allowedTestCommands: [],
|
|
46
|
+
repoAllowedTestCommands: {},
|
|
47
|
+
maxReadFileBytes: 200_000,
|
|
48
|
+
defaultTaskTimeoutSeconds: 900,
|
|
49
|
+
maxTaskTimeoutSeconds: 3600,
|
|
50
|
+
watcherStaleSeconds: 30,
|
|
51
|
+
toolProfile: "full",
|
|
52
|
+
enableDirectProfile: false,
|
|
53
|
+
directSessionsDir: ".patchwarden/direct-sessions",
|
|
54
|
+
directSessionTtlSeconds: 3600,
|
|
55
|
+
directMaxPatchBytes: 200_000,
|
|
56
|
+
directMaxFileBytes: 500_000,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export let config: PatchWardenConfig;
|
|
61
|
+
try {
|
|
62
|
+
config = loadConfig();
|
|
63
|
+
} catch (err) {
|
|
64
|
+
logger.warn(`[control-center] WARNING: Failed to load config. Using fallback defaults.`, {
|
|
65
|
+
error: errorMessage(err),
|
|
66
|
+
});
|
|
67
|
+
config = createFallbackConfig();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Control token (in-memory only) ────────────────────────────────
|
|
71
|
+
|
|
72
|
+
export const controlToken = randomUUID();
|
|
73
|
+
|
|
74
|
+
// ── Port resolution ───────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
export function resolvePort(): number {
|
|
77
|
+
const argv = process.argv.slice(2);
|
|
78
|
+
for (let i = 0; i < argv.length; i++) {
|
|
79
|
+
const arg = argv[i];
|
|
80
|
+
if (arg === "--port" && i + 1 < argv.length) {
|
|
81
|
+
const n = parseInt(argv[i + 1], 10);
|
|
82
|
+
if (Number.isFinite(n) && n >= 0) return n;
|
|
83
|
+
}
|
|
84
|
+
const m = arg.match(/^--port=(\d+)$/);
|
|
85
|
+
if (m) {
|
|
86
|
+
const n = parseInt(m[1], 10);
|
|
87
|
+
if (Number.isFinite(n) && n >= 0) return n;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const envPort = parseInt(process.env.PATCHWARDEN_CONTROL_PORT || "", 10);
|
|
91
|
+
if (Number.isFinite(envPort) && envPort >= 0) return envPort;
|
|
92
|
+
return 8090;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const port = resolvePort();
|
|
96
|
+
export const host = "127.0.0.1";
|
|
97
|
+
|
|
98
|
+
// Core/Direct probe base URLs — overridable for tests so the smoke test does
|
|
99
|
+
// not depend on the real 8080/8081 ports being free on the host.
|
|
100
|
+
export const CORE_BASE_URL = process.env.PATCHWARDEN_CORE_URL || "http://127.0.0.1:8080";
|
|
101
|
+
export const DIRECT_BASE_URL = process.env.PATCHWARDEN_DIRECT_URL || "http://127.0.0.1:8081";
|
|
102
|
+
export const DEFAULT_TUNNEL_CLIENT_EXE = "D:\\ai_agent\\tunnel-client-v0.0.9--context-conduit-topaz-windows-amd64\\tunnel-client.exe";
|
|
103
|
+
|
|
104
|
+
export const CONTROL_CENTER_FAVICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
105
|
+
<rect width="64" height="64" rx="14" fill="#0a0e14"/>
|
|
106
|
+
<path d="M32 52s17-8 17-27V14L32 8 15 14v11c0 19 17 27 17 27z" fill="#111820" stroke="#2dd4a8" stroke-width="4" stroke-linejoin="round"/>
|
|
107
|
+
<path d="M24 32l6 6 12-14" fill="none" stroke="#2dd4a8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
108
|
+
</svg>`;
|
|
109
|
+
|
|
110
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
export function errorMessage(err: unknown): string {
|
|
113
|
+
return err instanceof Error ? err.message : String(err);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getRuntimeRoot(direct: boolean): string {
|
|
117
|
+
const base =
|
|
118
|
+
process.platform === "win32" && process.env.LOCALAPPDATA
|
|
119
|
+
? join(process.env.LOCALAPPDATA, "patchwarden")
|
|
120
|
+
: join(homedir(), ".patchwarden");
|
|
121
|
+
return join(base, direct ? "runtime-direct" : "runtime");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function getControlCenterLogDir(): string {
|
|
125
|
+
// Test/local override: when set to an absolute path, use it directly so the
|
|
126
|
+
// smoke test can redirect status/events/log files into a sandbox-writable
|
|
127
|
+
// directory under the project root instead of LOCALAPPDATA.
|
|
128
|
+
const override = process.env.PATCHWARDEN_CONTROL_LOG_DIR;
|
|
129
|
+
if (override && isAbsolute(override)) return override;
|
|
130
|
+
const base =
|
|
131
|
+
process.platform === "win32" && process.env.LOCALAPPDATA
|
|
132
|
+
? join(process.env.LOCALAPPDATA, "patchwarden")
|
|
133
|
+
: join(homedir(), ".patchwarden");
|
|
134
|
+
return join(base, "control-center");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Status + events files live alongside the control-center logs so the launcher
|
|
138
|
+
// can discover a running instance without probing the port blindly.
|
|
139
|
+
export const controlCenterStatusPath = join(getControlCenterLogDir(), "control-center-status.json");
|
|
140
|
+
export const controlCenterEventsPath = join(getControlCenterLogDir(), "control-center-events.jsonl");
|
|
141
|
+
export const MAX_EVENT_LINES = 2000;
|
|
142
|
+
|
|
143
|
+
export const ALLOWED_LOG_TAILS = new Set([100, 300, 1000]);
|
|
144
|
+
|
|
145
|
+
export function resolveTailParam(value: string | null): number {
|
|
146
|
+
if (value === null) return 100;
|
|
147
|
+
const n = parseInt(value, 10);
|
|
148
|
+
if (Number.isFinite(n) && ALLOWED_LOG_TAILS.has(n)) return n;
|
|
149
|
+
return 100;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const CONTENT_TYPES: Record<string, string> = {
|
|
153
|
+
".html": "text/html; charset=utf-8",
|
|
154
|
+
".css": "text/css; charset=utf-8",
|
|
155
|
+
".js": "application/javascript; charset=utf-8",
|
|
156
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
157
|
+
".json": "application/json; charset=utf-8",
|
|
158
|
+
".svg": "image/svg+xml",
|
|
159
|
+
".png": "image/png",
|
|
160
|
+
".jpg": "image/jpeg",
|
|
161
|
+
".jpeg": "image/jpeg",
|
|
162
|
+
".gif": "image/gif",
|
|
163
|
+
".ico": "image/x-icon",
|
|
164
|
+
".woff": "font/woff",
|
|
165
|
+
".woff2": "font/woff2",
|
|
166
|
+
".map": "application/json; charset=utf-8",
|
|
167
|
+
".txt": "text/plain; charset=utf-8",
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export function sendJson(res: ServerResponse, status: number, body: unknown): void {
|
|
171
|
+
let payload: string;
|
|
172
|
+
try {
|
|
173
|
+
payload = JSON.stringify(body);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
payload = JSON.stringify({ error: `serialization failed: ${errorMessage(err)}` });
|
|
176
|
+
status = 500;
|
|
177
|
+
}
|
|
178
|
+
res.writeHead(status, {
|
|
179
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
180
|
+
"Cache-Control": "no-store",
|
|
181
|
+
"X-Content-Type-Options": "nosniff",
|
|
182
|
+
});
|
|
183
|
+
res.end(payload);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function readBody(req: IncomingMessage): Promise<unknown | null> {
|
|
187
|
+
return new Promise((resolve) => {
|
|
188
|
+
let total = 0;
|
|
189
|
+
const chunks: Buffer[] = [];
|
|
190
|
+
let aborted = false;
|
|
191
|
+
req.on("data", (chunk: Buffer) => {
|
|
192
|
+
if (aborted) return;
|
|
193
|
+
total += chunk.length;
|
|
194
|
+
if (total > 1024 * 1024) {
|
|
195
|
+
aborted = true;
|
|
196
|
+
try { req.destroy(); } catch { /* ignore */ }
|
|
197
|
+
resolve(null);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
chunks.push(chunk);
|
|
201
|
+
});
|
|
202
|
+
req.on("end", () => {
|
|
203
|
+
if (aborted) return;
|
|
204
|
+
if (chunks.length === 0) {
|
|
205
|
+
resolve(null);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const text = Buffer.concat(chunks).toString("utf-8");
|
|
209
|
+
try {
|
|
210
|
+
resolve(JSON.parse(text));
|
|
211
|
+
} catch {
|
|
212
|
+
resolve(null);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
req.on("error", () => resolve(null));
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function readJsonFileSafe<T = unknown>(filePath: string): T | null {
|
|
220
|
+
if (!existsSync(filePath)) return null;
|
|
221
|
+
try {
|
|
222
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
223
|
+
return JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw) as T;
|
|
224
|
+
} catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function isPathInside(root: string, target: string): boolean {
|
|
230
|
+
const rel = relative(root, target);
|
|
231
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function readJsonFileSafeUnder<T = unknown>(root: string, relPath: string): T | null {
|
|
235
|
+
const base = resolve(root);
|
|
236
|
+
const target = resolve(base, relPath);
|
|
237
|
+
if (!isPathInside(base, target) || !existsSync(target)) return null;
|
|
238
|
+
try {
|
|
239
|
+
const realBase = realpathSync(base);
|
|
240
|
+
const realTarget = realpathSync(target);
|
|
241
|
+
if (!isPathInside(realBase, realTarget)) return null;
|
|
242
|
+
return readJsonFileSafe<T>(realTarget);
|
|
243
|
+
} catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function readTextFileSafe(filePath: string): string | null {
|
|
249
|
+
if (!existsSync(filePath)) return null;
|
|
250
|
+
try {
|
|
251
|
+
return readFileSync(filePath, "utf-8");
|
|
252
|
+
} catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function readFileTail(filePath: string, lines = 100): string {
|
|
258
|
+
if (!existsSync(filePath)) return "";
|
|
259
|
+
const content = readFileSync(filePath, "utf-8");
|
|
260
|
+
const allLines = content.split(/\r?\n/);
|
|
261
|
+
if (allLines.length > 0 && allLines[allLines.length - 1] === "") allLines.pop();
|
|
262
|
+
return allLines.slice(-lines).join("\n");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function findLatestLog(dir: string, pattern: RegExp): string | null {
|
|
266
|
+
if (!existsSync(dir)) return null;
|
|
267
|
+
let entries;
|
|
268
|
+
try {
|
|
269
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
270
|
+
} catch {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
const files = entries.filter((e) => e.isFile() && pattern.test(e.name));
|
|
274
|
+
if (files.length === 0) return null;
|
|
275
|
+
let latestName = files[0].name;
|
|
276
|
+
let latestMtime = -1;
|
|
277
|
+
for (const f of files) {
|
|
278
|
+
try {
|
|
279
|
+
const m = statSync(join(dir, f.name)).mtimeMs;
|
|
280
|
+
if (m > latestMtime) {
|
|
281
|
+
latestMtime = m;
|
|
282
|
+
latestName = f.name;
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
/* keep current */
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return join(dir, latestName);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Re-export widely-used node primitives so route modules have a single import
|
|
292
|
+
// surface for path/delimiter helpers that are also needed alongside the shared
|
|
293
|
+
// helpers above.
|
|
294
|
+
export { delimiter, extname, join, resolve, isAbsolute, relative };
|