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