@diegopetrucci/pi-extensions 0.1.73 → 0.1.76
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/LICENSE +2 -0
- package/README.md +9 -4
- package/extensions/agent-workflow-audit/index.ts +1398 -1009
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/README.md +3 -1
- package/extensions/annotate-git-diff/clipboard.ts +109 -100
- package/extensions/annotate-git-diff/git.ts +853 -767
- package/extensions/annotate-git-diff/glimpseui.d.ts +87 -78
- package/extensions/annotate-git-diff/index.ts +712 -671
- package/extensions/annotate-git-diff/package.json +1 -1
- package/extensions/annotate-git-diff/prompt.ts +45 -45
- package/extensions/annotate-git-diff/quiet-glimpse.ts +161 -155
- package/extensions/annotate-git-diff/review-server.ts +337 -311
- package/extensions/annotate-git-diff/types.ts +132 -132
- package/extensions/annotate-git-diff/ui.ts +27 -27
- package/extensions/annotate-git-diff/watch.ts +74 -74
- package/extensions/annotate-git-diff/web/app.js +1947 -1884
- package/extensions/annotate-git-diff/web/index.html +5 -6
- package/extensions/annotate-last-message/README.md +1 -1
- package/extensions/annotate-last-message/glimpseui.d.ts +87 -78
- package/extensions/annotate-last-message/index.ts +210 -206
- package/extensions/annotate-last-message/package.json +1 -1
- package/extensions/annotate-last-message/prompt.ts +87 -66
- package/extensions/annotate-last-message/quiet-glimpse.ts +161 -155
- package/extensions/annotate-last-message/session.ts +94 -89
- package/extensions/annotate-last-message/theme.ts +199 -193
- package/extensions/annotate-last-message/types.ts +23 -23
- package/extensions/annotate-last-message/ui.ts +27 -25
- package/extensions/annotate-last-message/web/app.js +4 -7
- package/extensions/annotate-last-message/web/md-renderer.js +1 -1
- package/extensions/brrr/README.md +1 -1
- package/extensions/brrr/index.ts +213 -191
- package/extensions/brrr/package.json +1 -1
- package/extensions/claude-fast/README.md +5 -79
- package/extensions/claude-fast/index.ts +31 -289
- package/extensions/claude-fast/package.json +2 -3
- package/extensions/code-reviewer/README.md +1 -1
- package/extensions/code-reviewer/index.ts +1598 -1350
- package/extensions/code-reviewer/package.json +1 -1
- package/extensions/confirm-destructive/README.md +1 -1
- package/extensions/confirm-destructive/index.ts +55 -51
- package/extensions/confirm-destructive/package.json +1 -1
- package/extensions/context-cap/index.ts +312 -304
- package/extensions/context-cap/package.json +1 -1
- package/extensions/context-inspector/index.ts +1211 -1064
- package/extensions/context-inspector/package.json +1 -1
- package/extensions/contrarian/index.ts +1854 -1735
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dirty-repo-guard/index.ts +41 -41
- package/extensions/dynamic-context-pruning/README.md +6 -38
- package/extensions/dynamic-context-pruning/index.ts +2124 -1980
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/README.md +71 -21
- package/extensions/fast/index.ts +513 -313
- package/extensions/fast/package.json +1 -1
- package/extensions/git-footer/README.md +1 -1
- package/extensions/git-footer/index.ts +458 -411
- package/extensions/git-footer/package.json +2 -3
- package/extensions/gnosis/index.ts +180 -130
- package/extensions/gnosis/package.json +1 -1
- package/extensions/inline-bash/index.ts +88 -88
- package/extensions/inline-bash/package.json +1 -1
- package/extensions/librarian/index.ts +1669 -1419
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/index.ts +994 -1012
- package/extensions/minimal-footer/openai-usage.ts +195 -190
- package/extensions/notify/index.ts +222 -230
- package/extensions/notify/package.json +1 -1
- package/extensions/openai-fast/README.md +5 -73
- package/extensions/openai-fast/index.ts +32 -272
- package/extensions/openai-fast/package.json +2 -3
- package/extensions/oracle/index.ts +1731 -1625
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/index.ts +658 -608
- package/extensions/permission-gate/package.json +1 -1
- package/extensions/permission-gate/powershell-safety.ts +840 -793
- package/extensions/project-mcp-json/.pi-fleet-tested-version +1 -0
- package/extensions/project-mcp-json/README.md +199 -0
- package/extensions/project-mcp-json/config.ts +564 -0
- package/extensions/project-mcp-json/index.ts +119 -0
- package/extensions/project-mcp-json/package.json +38 -0
- package/extensions/quiet-tools/index.ts +282 -264
- package/extensions/review/README.md +2 -2
- package/extensions/review/index.ts +1745 -1872
- package/extensions/review/package.json +1 -1
- package/extensions/todo/index.ts +284 -265
- package/extensions/triage-comments/README.md +2 -2
- package/extensions/triage-comments/index.ts +2294 -1882
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +6 -3
- package/assets/social-preview.png +0 -0
- package/extensions/claude-fast/claude-fast.example.json +0 -4
- package/extensions/openai-fast/openai-fast.example.json +0 -4
|
@@ -10,370 +10,396 @@ const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
|
10
10
|
const loopbackHost = "127.0.0.1";
|
|
11
11
|
|
|
12
12
|
export interface ReviewUiServer {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
readonly failure: Error | null;
|
|
14
|
+
html: string;
|
|
15
|
+
url: string;
|
|
16
|
+
onError(listener: (error: Error) => void): () => void;
|
|
17
|
+
dispose(): void;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export interface ReviewUiAssetPaths {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
tailwindBrowserPath: string;
|
|
22
|
+
monacoLoaderPath: string;
|
|
23
|
+
monacoVsDirectory: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
interface StartReviewUiServerOptions {
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
extensionDir?: string;
|
|
28
|
+
token?: string;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function parentDirectories(startPath: string): string[] {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
const directories: string[] = [];
|
|
33
|
+
let current = resolve(startPath);
|
|
34
|
+
|
|
35
|
+
while (true) {
|
|
36
|
+
directories.push(current);
|
|
37
|
+
const parent = dirname(current);
|
|
38
|
+
if (parent === current) return directories;
|
|
39
|
+
current = parent;
|
|
40
|
+
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function findInstalledAsset(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
43
|
+
function findInstalledAsset(
|
|
44
|
+
extensionDir: string,
|
|
45
|
+
dependencyPath: string,
|
|
46
|
+
label: string,
|
|
47
|
+
expected: "file" | "directory",
|
|
48
|
+
): string {
|
|
49
|
+
const searched: string[] = [];
|
|
50
|
+
const starts = [extensionDir];
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const realExtensionDir = realpathSync(extensionDir);
|
|
54
|
+
if (realExtensionDir !== resolve(extensionDir)) starts.push(realExtensionDir);
|
|
55
|
+
} catch {
|
|
56
|
+
// The direct path below will produce the useful not-found diagnostic.
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const start of starts) {
|
|
60
|
+
for (const ancestor of parentDirectories(start)) {
|
|
61
|
+
const candidate = join(ancestor, "node_modules", dependencyPath);
|
|
62
|
+
if (searched.includes(candidate)) continue;
|
|
63
|
+
searched.push(candidate);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const stats = statSync(candidate);
|
|
67
|
+
const matches = expected === "file" ? stats.isFile() : stats.isDirectory();
|
|
68
|
+
if (matches) return realpathSync(candidate);
|
|
69
|
+
} catch {
|
|
70
|
+
// Keep walking toward the package-manager installation root.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw new Error(`Unable to locate packaged ${label}. Searched: ${searched.join(", ")}`);
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
function readDependencyVersion(packageRoot: string, packageName: string): string {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
try {
|
|
80
|
+
const manifest = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")) as {
|
|
81
|
+
name?: unknown;
|
|
82
|
+
version?: unknown;
|
|
83
|
+
};
|
|
84
|
+
if (manifest.name !== packageName || typeof manifest.version !== "string")
|
|
85
|
+
throw new Error("invalid manifest");
|
|
86
|
+
return manifest.version;
|
|
87
|
+
} catch {
|
|
88
|
+
throw new Error(`Unable to validate packaged ${packageName} runtime at ${packageRoot}.`);
|
|
89
|
+
}
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
function isCompatibleTailwindBrowserVersion(version: string): boolean {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
|
|
94
|
+
if (match == null) return false;
|
|
95
|
+
const [, major, minor, patch] = match.map(Number);
|
|
96
|
+
return major === 4 && (minor > 3 || (minor === 3 && patch >= 2));
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
export function resolveTailwindBrowserPath(extensionDir = moduleDir): string {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
const browserPath = findInstalledAsset(
|
|
101
|
+
extensionDir,
|
|
102
|
+
join("@tailwindcss", "browser", "dist", "index.global.js"),
|
|
103
|
+
"@tailwindcss/browser runtime",
|
|
104
|
+
"file",
|
|
105
|
+
);
|
|
106
|
+
const version = readDependencyVersion(dirname(dirname(browserPath)), "@tailwindcss/browser");
|
|
107
|
+
if (!isCompatibleTailwindBrowserVersion(version)) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Incompatible packaged @tailwindcss/browser runtime ${version}; expected ^4.3.2.`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return browserPath;
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
export function resolveMonacoVsDirectory(extensionDir = moduleDir): string {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
const vsDirectory = findInstalledAsset(
|
|
117
|
+
extensionDir,
|
|
118
|
+
join("monaco-editor", "min", "vs"),
|
|
119
|
+
"monaco-editor runtime",
|
|
120
|
+
"directory",
|
|
121
|
+
);
|
|
122
|
+
const version = readDependencyVersion(dirname(dirname(vsDirectory)), "monaco-editor");
|
|
123
|
+
if (version !== "0.52.2") {
|
|
124
|
+
throw new Error(`Incompatible packaged monaco-editor runtime ${version}; expected 0.52.2.`);
|
|
125
|
+
}
|
|
126
|
+
return vsDirectory;
|
|
114
127
|
}
|
|
115
128
|
|
|
116
129
|
export function resolveReviewUiAssetPaths(extensionDir = moduleDir): ReviewUiAssetPaths {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
const tailwindBrowserPath = resolveTailwindBrowserPath(extensionDir);
|
|
131
|
+
const monacoVsDirectory = resolveMonacoVsDirectory(extensionDir);
|
|
132
|
+
const monacoLoaderPath = join(monacoVsDirectory, "loader.js");
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
if (!statSync(monacoLoaderPath).isFile()) throw new Error("not a file");
|
|
136
|
+
} catch {
|
|
137
|
+
throw new Error(`Unable to locate packaged Monaco loader: ${monacoLoaderPath}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
tailwindBrowserPath,
|
|
142
|
+
monacoLoaderPath: realpathSync(monacoLoaderPath),
|
|
143
|
+
monacoVsDirectory,
|
|
144
|
+
};
|
|
132
145
|
}
|
|
133
146
|
|
|
134
147
|
function contentType(filePath: string): string {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
148
|
+
switch (extname(filePath).toLowerCase()) {
|
|
149
|
+
case ".css":
|
|
150
|
+
return "text/css; charset=utf-8";
|
|
151
|
+
case ".html":
|
|
152
|
+
return "text/html; charset=utf-8";
|
|
153
|
+
case ".js":
|
|
154
|
+
case ".mjs":
|
|
155
|
+
return "text/javascript; charset=utf-8";
|
|
156
|
+
case ".json":
|
|
157
|
+
return "application/json; charset=utf-8";
|
|
158
|
+
case ".svg":
|
|
159
|
+
return "image/svg+xml";
|
|
160
|
+
case ".ttf":
|
|
161
|
+
return "font/ttf";
|
|
162
|
+
case ".wasm":
|
|
163
|
+
return "application/wasm";
|
|
164
|
+
default:
|
|
165
|
+
return "application/octet-stream";
|
|
166
|
+
}
|
|
154
167
|
}
|
|
155
168
|
|
|
156
169
|
function setSecurityHeaders(response: ServerResponse): void {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
170
|
+
response.setHeader("Cache-Control", "no-store");
|
|
171
|
+
response.setHeader(
|
|
172
|
+
"Content-Security-Policy",
|
|
173
|
+
"default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; object-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; worker-src 'self' blob:; connect-src 'self'",
|
|
174
|
+
);
|
|
175
|
+
response.setHeader("Cross-Origin-Resource-Policy", "same-origin");
|
|
176
|
+
response.setHeader("Referrer-Policy", "no-referrer");
|
|
177
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
165
178
|
}
|
|
166
179
|
|
|
167
180
|
function sendText(response: ServerResponse, statusCode: number, body: string): void {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
181
|
+
response.statusCode = statusCode;
|
|
182
|
+
response.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
183
|
+
response.setHeader("Content-Length", Buffer.byteLength(body));
|
|
184
|
+
response.end(body);
|
|
172
185
|
}
|
|
173
186
|
|
|
174
187
|
function isContainedPath(rootPath: string, candidatePath: string): boolean {
|
|
175
|
-
|
|
176
|
-
|
|
188
|
+
const relativePath = relative(rootPath, candidatePath);
|
|
189
|
+
return (
|
|
190
|
+
relativePath.length > 0 &&
|
|
191
|
+
!relativePath.startsWith(`..${sep}`) &&
|
|
192
|
+
relativePath !== ".." &&
|
|
193
|
+
!isAbsolute(relativePath)
|
|
194
|
+
);
|
|
177
195
|
}
|
|
178
196
|
|
|
179
197
|
function decodeAssetPath(encodedPath: string): string | null {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
198
|
+
try {
|
|
199
|
+
const decoded = decodeURIComponent(encodedPath);
|
|
200
|
+
if (!decoded || decoded.includes("\0") || decoded.includes("\\")) return null;
|
|
201
|
+
const segments = decoded.split("/");
|
|
202
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === ".."))
|
|
203
|
+
return null;
|
|
204
|
+
return segments.join(sep);
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
189
208
|
}
|
|
190
209
|
|
|
191
210
|
function serveMonacoAsset(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
211
|
+
request: IncomingMessage,
|
|
212
|
+
response: ServerResponse,
|
|
213
|
+
monacoVsDirectory: string,
|
|
214
|
+
encodedPath: string,
|
|
196
215
|
): void {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
216
|
+
const relativePath = decodeAssetPath(encodedPath);
|
|
217
|
+
if (relativePath == null) {
|
|
218
|
+
sendText(response, 404, "Not found");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let assetPath: string;
|
|
223
|
+
let assetSize: number;
|
|
224
|
+
try {
|
|
225
|
+
const candidate = resolve(monacoVsDirectory, relativePath);
|
|
226
|
+
if (!isContainedPath(monacoVsDirectory, candidate)) throw new Error("outside asset root");
|
|
227
|
+
assetPath = realpathSync(candidate);
|
|
228
|
+
const stats = statSync(assetPath);
|
|
229
|
+
if (!isContainedPath(monacoVsDirectory, assetPath) || !stats.isFile())
|
|
230
|
+
throw new Error("not a file");
|
|
231
|
+
assetSize = stats.size;
|
|
232
|
+
} catch {
|
|
233
|
+
sendText(response, 404, "Not found");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
response.statusCode = 200;
|
|
238
|
+
response.setHeader("Content-Type", contentType(assetPath));
|
|
239
|
+
response.setHeader("Content-Length", assetSize);
|
|
240
|
+
if (request.method === "HEAD") {
|
|
241
|
+
response.end();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const stream = createReadStream(assetPath);
|
|
246
|
+
stream.on("error", () => {
|
|
247
|
+
if (!response.headersSent) sendText(response, 500, "Unable to read asset");
|
|
248
|
+
else response.destroy();
|
|
249
|
+
});
|
|
250
|
+
response.on("close", () => stream.destroy());
|
|
251
|
+
stream.pipe(response);
|
|
232
252
|
}
|
|
233
253
|
|
|
234
254
|
function redirectHtml(url: string): string {
|
|
235
|
-
|
|
236
|
-
|
|
255
|
+
const safeUrl = JSON.stringify(url).replace(/</g, "\\u003c");
|
|
256
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>annotate-git-diff</title></head><body><p>Loading review…</p><script>location.replace(${safeUrl});</script></body></html>`;
|
|
237
257
|
}
|
|
238
258
|
|
|
239
259
|
function disposeServer(server: Server): void {
|
|
240
|
-
|
|
241
|
-
|
|
260
|
+
server.close();
|
|
261
|
+
server.closeAllConnections();
|
|
242
262
|
}
|
|
243
263
|
|
|
244
264
|
export async function startReviewUiServer(
|
|
245
|
-
|
|
246
|
-
|
|
265
|
+
data: ReviewWindowData,
|
|
266
|
+
options: StartReviewUiServerOptions = {},
|
|
247
267
|
): Promise<ReviewUiServer> {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
268
|
+
const assetPaths = resolveReviewUiAssetPaths(options.extensionDir ?? moduleDir);
|
|
269
|
+
const token = options.token ?? randomBytes(24).toString("hex");
|
|
270
|
+
if (!/^[A-Za-z0-9_-]{24,128}$/.test(token)) throw new Error("Invalid review UI server token.");
|
|
271
|
+
|
|
272
|
+
const routeRoot = `/${token}/`;
|
|
273
|
+
const monacoRoutePrefix = `${routeRoot}vs/`;
|
|
274
|
+
let expectedHost = "";
|
|
275
|
+
let reviewHtml = "";
|
|
276
|
+
|
|
277
|
+
const server = createServer((request, response) => {
|
|
278
|
+
setSecurityHeaders(response);
|
|
279
|
+
if (request.headers.host !== expectedHost) {
|
|
280
|
+
sendText(response, 403, "Forbidden");
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
284
|
+
response.setHeader("Allow", "GET, HEAD");
|
|
285
|
+
sendText(response, 405, "Method not allowed");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let pathname: string;
|
|
290
|
+
try {
|
|
291
|
+
pathname = new URL(request.url ?? "/", `http://${expectedHost}`).pathname;
|
|
292
|
+
} catch {
|
|
293
|
+
sendText(response, 400, "Bad request");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (pathname === routeRoot || pathname === `${routeRoot}index.html`) {
|
|
298
|
+
response.statusCode = 200;
|
|
299
|
+
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
300
|
+
response.setHeader("Content-Length", Buffer.byteLength(reviewHtml));
|
|
301
|
+
response.end(request.method === "HEAD" ? undefined : reviewHtml);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (pathname.startsWith(monacoRoutePrefix)) {
|
|
306
|
+
serveMonacoAsset(
|
|
307
|
+
request,
|
|
308
|
+
response,
|
|
309
|
+
assetPaths.monacoVsDirectory,
|
|
310
|
+
pathname.slice(monacoRoutePrefix.length),
|
|
311
|
+
);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
sendText(response, 404, "Not found");
|
|
316
|
+
});
|
|
317
|
+
// Keep Node's special EventEmitter `error` event non-fatal across disposal
|
|
318
|
+
// races; the scoped listener below still records and reports live failures.
|
|
319
|
+
server.on("error", () => {});
|
|
320
|
+
server.maxHeadersCount = 50;
|
|
321
|
+
server.headersTimeout = 5_000;
|
|
322
|
+
server.keepAliveTimeout = 1_000;
|
|
323
|
+
server.requestTimeout = 15_000;
|
|
324
|
+
server.on("clientError", (_error, socket) => socket.destroy());
|
|
325
|
+
const errorListeners = new Set<(error: Error) => void>();
|
|
326
|
+
let disposed = false;
|
|
327
|
+
let runtimeFailure: Error | null = null;
|
|
328
|
+
const onRuntimeServerError = (error: Error): void => {
|
|
329
|
+
if (disposed || runtimeFailure != null) return;
|
|
330
|
+
runtimeFailure = error;
|
|
331
|
+
disposeServer(server);
|
|
332
|
+
for (const listener of [...errorListeners]) {
|
|
333
|
+
try {
|
|
334
|
+
listener(error);
|
|
335
|
+
} catch {
|
|
336
|
+
// A lifecycle observer must not turn a server failure into an
|
|
337
|
+
// uncaught process-level exception.
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
await new Promise<void>((resolveListen, rejectListen) => {
|
|
344
|
+
const onError = (error: Error): void => {
|
|
345
|
+
server.off("listening", onListening);
|
|
346
|
+
rejectListen(error);
|
|
347
|
+
};
|
|
348
|
+
const onListening = (): void => {
|
|
349
|
+
server.off("error", onError);
|
|
350
|
+
resolveListen();
|
|
351
|
+
};
|
|
352
|
+
server.once("error", onError);
|
|
353
|
+
server.once("listening", onListening);
|
|
354
|
+
server.listen({ host: loopbackHost, port: 0, exclusive: true });
|
|
355
|
+
});
|
|
356
|
+
server.on("error", onRuntimeServerError);
|
|
357
|
+
|
|
358
|
+
const address = server.address();
|
|
359
|
+
if (address == null || typeof address === "string")
|
|
360
|
+
throw new Error("Review UI server did not expose a TCP address.");
|
|
361
|
+
expectedHost = `${loopbackHost}:${address.port}`;
|
|
362
|
+
const url = `http://${expectedHost}${routeRoot}`;
|
|
363
|
+
reviewHtml = buildReviewHtml(data, {
|
|
364
|
+
tailwindBrowserJs: readFileSync(assetPaths.tailwindBrowserPath, "utf8"),
|
|
365
|
+
monacoLoaderJs: readFileSync(assetPaths.monacoLoaderPath, "utf8"),
|
|
366
|
+
monacoVsBaseUrl: `${url}vs`,
|
|
367
|
+
});
|
|
368
|
+
server.unref();
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
get failure() {
|
|
372
|
+
return runtimeFailure;
|
|
373
|
+
},
|
|
374
|
+
html: redirectHtml(url),
|
|
375
|
+
url,
|
|
376
|
+
onError(listener) {
|
|
377
|
+
if (runtimeFailure != null) {
|
|
378
|
+
let active = true;
|
|
379
|
+
queueMicrotask(() => {
|
|
380
|
+
if (active) listener(runtimeFailure as Error);
|
|
381
|
+
});
|
|
382
|
+
return () => {
|
|
383
|
+
active = false;
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
if (disposed) return () => {};
|
|
387
|
+
errorListeners.add(listener);
|
|
388
|
+
return () => errorListeners.delete(listener);
|
|
389
|
+
},
|
|
390
|
+
dispose() {
|
|
391
|
+
if (disposed) return;
|
|
392
|
+
disposed = true;
|
|
393
|
+
errorListeners.clear();
|
|
394
|
+
server.off("error", onRuntimeServerError);
|
|
395
|
+
disposeServer(server);
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
} catch (error) {
|
|
399
|
+
disposed = true;
|
|
400
|
+
errorListeners.clear();
|
|
401
|
+
server.off("error", onRuntimeServerError);
|
|
402
|
+
disposeServer(server);
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
379
405
|
}
|