@dev.fast/whiteboard 0.2.5-preview.20261013.113 → 0.2.5-preview.20261015.115

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 (127) hide show
  1. package/THIRD_PARTY_NOTICES.md +29 -0
  2. package/dist/{account-alias-D0hF0sKk.js → account-alias-ChZR3RIU.js} +94 -8
  3. package/dist/{agent-cli-04xbDj23.js → agent-cli-CR2PnA9G.js} +9 -12
  4. package/dist/agent-cli-CRPPmS3l.js +2 -0
  5. package/dist/{agent-client-P3J4y8Zn.js → agent-client-DVd-prfp.js} +31 -14
  6. package/dist/{authoring-tools-fdA0RPIT.js → authoring-tools-BXKEwhfi.js} +5 -5
  7. package/dist/background-server-Cw7CcfaZ.js +156 -0
  8. package/dist/background-server-D2Rrr_e2.js +2 -0
  9. package/dist/build-info.json +1 -1
  10. package/dist/{cli-C0ziP6hg.js → cli-CSrz_wGk.js} +1 -1
  11. package/dist/{cli-runner-C3men22A.js → cli-runner-PlGqBeMw.js} +368 -252
  12. package/dist/cli.js +3 -2
  13. package/dist/{client-DzcSt6pV.js → client-DhZzPePs.js} +5 -3
  14. package/dist/{process-error-telemetry-BCBYp2t4.js → headless-host-CVXTSIlx.js} +3285 -2585
  15. package/dist/headless-host-Cp37C-v0.js +2 -0
  16. package/dist/input-error-OLgB_h31.js +11 -0
  17. package/dist/{local-data-Dy1pNtLF.js → local-data-yfLp5PiJ.js} +113 -62
  18. package/dist/{mcp-B8kuRSXM.js → mcp-DBbDwG4y.js} +4 -4
  19. package/dist/{package-paths-B9PDoAl6.js → package-paths-BsuI4WUf.js} +1 -1
  20. package/dist/profile-BSOkA6A-.js +2 -0
  21. package/dist/{profile-tbGWuWHu.js → profile-pAzg7fxC.js} +3 -3
  22. package/dist/remote-attach-BAt2eZx3.js +137 -0
  23. package/dist/remote-extensions-2Cxf3hMo.js +2 -0
  24. package/dist/remote-extensions-BiNx6NQK.js +323 -0
  25. package/dist/remote-language-server-DQgWUNeV.js +252 -0
  26. package/dist/remote-uninstall-DLtDG0o7.js +153 -0
  27. package/dist/{request-origin-DA4K1qWY.js → request-origin-DaRIV3cB.js} +5 -2
  28. package/dist/review-app-launcher-D7I4DGoH.js +222 -0
  29. package/dist/{review-home-paths-Y9mYWkCI.js → review-home-paths-zwevIq4I.js} +7 -7
  30. package/dist/{review-telemetry-CmM6tCbj.js → review-telemetry-BqB4CiTD.js} +5 -5
  31. package/dist/{s3-SzLpeCG6.js → s3-77-qkEhs.js} +1 -1
  32. package/dist/{s3-config-BgxkSoOy.js → s3-config-0xuCh-F_.js} +1 -1
  33. package/dist/{s3-config-CAxhO9u_.js → s3-config-DDWWMh5x.js} +5 -1
  34. package/dist/server/desktop-host.js +1663 -266
  35. package/dist/{desktop-discovery-Ba3dD5Cn.js → server-discovery-CifQ3yQS.js} +88 -3
  36. package/dist/sharing/index.d.ts +88 -32
  37. package/dist/sharing/index.js +1 -1
  38. package/dist/{src-DiHP11l3.js → src-BMPuxWv4.js} +6 -11
  39. package/dist/{src-WCbabvdS.js → src-Bgvf1sEN.js} +128 -8
  40. package/dist/{stored-document-migration-u4L_1VZa.js → stored-document-migration-CFOvjRwW.js} +5 -18
  41. package/dist/{tool-failure-CJHBCHMh.js → tool-failure-BXWzQG8T.js} +2 -2
  42. package/instructions/authoring.md +16 -12
  43. package/instructions/file-lenses.md +4 -2
  44. package/package.json +9 -14
  45. package/src/agent-connect.ts +507 -0
  46. package/src/agent-selection.ts +4 -0
  47. package/src/ask/agents.ts +59 -13
  48. package/src/ask/pi-mcp.ts +7 -3
  49. package/src/ask/thread-state.ts +9 -4
  50. package/src/ask/thread.ts +88 -50
  51. package/src/ask/threads.ts +53 -7
  52. package/src/cli-install.ts +3 -1
  53. package/src/cli-runner.ts +472 -81
  54. package/src/cli.ts +1 -1
  55. package/src/connect-agents.ts +285 -0
  56. package/src/connect-prompts.ts +60 -6
  57. package/src/desktop-discovery.ts +24 -0
  58. package/src/pinned-download.ts +118 -0
  59. package/src/remote-attach.ts +130 -0
  60. package/src/remote-extensions.ts +359 -0
  61. package/src/remote-language-server.ts +428 -0
  62. package/src/remote-toolchains.ts +107 -0
  63. package/src/remote-uninstall.ts +280 -0
  64. package/src/review-api/README.md +1 -3
  65. package/src/review-api/agent-cli.ts +4 -4
  66. package/src/review-api/agent-client.ts +23 -0
  67. package/src/review-api/ask-history.ts +31 -16
  68. package/src/review-api/authoring-tools.ts +1 -1
  69. package/src/review-api/blocks/index.ts +2 -4
  70. package/src/review-api/document.ts +1 -4
  71. package/src/review-api/http.ts +265 -237
  72. package/src/review-api/local-data.ts +18 -3
  73. package/src/review-api/onboarding.ts +478 -0
  74. package/src/review-api/origin.ts +7 -0
  75. package/src/review-api/recent-folders.ts +173 -0
  76. package/src/review-api/store.ts +56 -10
  77. package/src/review-api/workspaces.ts +56 -11
  78. package/src/review-app-launcher.ts +61 -3
  79. package/src/review-stack.ts +162 -36
  80. package/src/server/background-server.ts +250 -0
  81. package/src/server/desktop-host-shutdown.ts +24 -0
  82. package/src/server/desktop-host.ts +39 -24
  83. package/src/server/desktop-server.ts +68 -118
  84. package/src/server/global-verb-relay.ts +16 -0
  85. package/src/server/headless-host.ts +128 -3
  86. package/src/server/hono-http.ts +6 -1
  87. package/src/server/legacy-tutorial-cleanup.ts +38 -0
  88. package/src/server/review-gateway-hosts.ts +379 -0
  89. package/src/server/review-gateway-list.ts +109 -0
  90. package/src/server/review-gateway-memory.ts +130 -0
  91. package/src/server/review-gateway-pushes.ts +142 -0
  92. package/src/server/review-gateway-streams.ts +415 -0
  93. package/src/server/review-gateway-transport.ts +204 -0
  94. package/src/server/review-gateway.ts +433 -0
  95. package/src/server/review-server-core.ts +36 -6
  96. package/src/server-discovery.ts +11 -0
  97. package/src/sharing/import.ts +1 -0
  98. package/src/ui-telemetry-events.ts +5 -1
  99. package/src/vsix.ts +122 -0
  100. package/dist/agent-cli-BR_Jw5BL.js +0 -2
  101. package/dist/headless-host-BLLJ7Dyu.js +0 -95
  102. package/dist/profile-aMSw1oCM.js +0 -2
  103. package/dist/server-discovery-ZVi8Ay8J.js +0 -66
  104. package/src/fixtures/blocks/tutorial.json +0 -14
  105. package/src/review-api/blocks/tutorial.ts +0 -23
  106. package/src/server/tutorial-service.ts +0 -217
  107. package/tutorial/document.json +0 -216
  108. package/tutorial/lenses.json +0 -10
  109. package/tutorial/runtime-manifest.json +0 -11
  110. package/tutorial/sample-service/package.json +0 -9
  111. package/tutorial/sample-service/src/api/checkout-api.ts +0 -17
  112. package/tutorial/sample-service/src/app.ts +0 -26
  113. package/tutorial/sample-service/src/database/drizzle-orm.d.ts +0 -16
  114. package/tutorial/sample-service/src/database/schema.ts +0 -9
  115. package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +0 -15
  116. package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +0 -24
  117. package/tutorial/sample-service/src/inventory/forklift-mood.ts +0 -4
  118. package/tutorial/sample-service/src/inventory/inventory-service.ts +0 -28
  119. package/tutorial/sample-service/src/inventory/warehouse-lore.ts +0 -11
  120. package/tutorial/sample-service/src/orders/order-service.ts +0 -35
  121. package/tutorial/sample-service/src/orders/order.ts +0 -43
  122. package/tutorial/sample-service/src/orders/orders-repository.ts +0 -24
  123. package/tutorial/sample-service/src/payments/fraud-vibes.ts +0 -4
  124. package/tutorial/sample-service/src/payments/payment-gateway.ts +0 -29
  125. package/tutorial/sample-service/src/payments/raccoon-ledger.ts +0 -9
  126. package/tutorial/sample-service/src/shipping/shipping-gateway.ts +0 -15
  127. package/tutorial/sample-service/tsconfig.json +0 -12
@@ -0,0 +1,323 @@
1
+ import { u as parseJsonText } from "./src-X9phtB2j.js";
2
+ import "./review-home-paths-zwevIq4I.js";
3
+ import { M as devReviewHome } from "./s3-config-DDWWMh5x.js";
4
+ import { t as findReviewPackageRoot } from "./package-paths-BsuI4WUf.js";
5
+ import { createHash } from "node:crypto";
6
+ import { z } from "zod";
7
+ import { createReadStream, createWriteStream } from "node:fs";
8
+ import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
9
+ import path from "node:path";
10
+ import { execFile } from "node:child_process";
11
+ import { promisify } from "node:util";
12
+ import { Readable } from "node:stream";
13
+ import { pipeline } from "node:stream/promises";
14
+ import yauzl from "yauzl";
15
+ //#region src/pinned-download.ts
16
+ const DOWNLOAD_TIMEOUT_MS = 12e4;
17
+ /** Streams `url` into `file` through a temporary file and returns its sha256. */
18
+ async function downloadFile(url, file, { size, timeoutMs = DOWNLOAD_TIMEOUT_MS, signal } = {}) {
19
+ const { host } = new URL(url);
20
+ const partial = `${file}.${process.pid}.part`;
21
+ let response;
22
+ await mkdir(path.dirname(file), { recursive: true });
23
+ try {
24
+ response = await fetch(url, { signal: AbortSignal.any([AbortSignal.timeout(timeoutMs), ...signal ? [signal] : []]) });
25
+ } catch (error) {
26
+ throw new Error(`Network error reaching ${host}: ${error instanceof Error ? networkCause(error) : String(error)}`);
27
+ }
28
+ if (!response.ok || !response.body) throw new Error(`${host} answered ${response.status} for ${url}`);
29
+ const hash = createHash("sha256");
30
+ let received = 0;
31
+ const tooLarge = /* @__PURE__ */ new Error(`${url} is larger than its pinned ${size} bytes`);
32
+ try {
33
+ await pipeline(Readable.fromWeb(response.body), async function* (chunks) {
34
+ for await (const chunk of chunks) {
35
+ received += chunk.length;
36
+ if (size !== void 0 && received > size) throw tooLarge;
37
+ hash.update(chunk);
38
+ yield chunk;
39
+ }
40
+ }, createWriteStream(partial));
41
+ await rename(partial, file);
42
+ } catch (error) {
43
+ await rm(partial, { force: true });
44
+ if (error === tooLarge) throw error;
45
+ throw new Error(`Network error downloading from ${host}: ${error instanceof Error ? networkCause(error) : String(error)}`);
46
+ }
47
+ return hash.digest("hex");
48
+ }
49
+ /** Leaves a matching `file` in place; otherwise downloads it and checks the pin. */
50
+ async function downloadPinned(pin, file, options = {}) {
51
+ if (await sha256File(file).catch(() => void 0) === pin.sha256) return;
52
+ const actual = await downloadFile(pin.url, file, {
53
+ ...options,
54
+ size: pin.size
55
+ });
56
+ if (actual !== pin.sha256) {
57
+ await rm(file, { force: true });
58
+ throw new Error(`Checksum mismatch for ${pin.url}: expected ${pin.sha256}, got ${actual}. The download was deleted.`);
59
+ }
60
+ }
61
+ async function sha256File(file) {
62
+ const hash = createHash("sha256");
63
+ for await (const chunk of createReadStream(file)) hash.update(chunk);
64
+ return hash.digest("hex");
65
+ }
66
+ const networkCauseSchema = z.object({ code: z.string() });
67
+ const networkCause = (error) => error.name === "TimeoutError" ? "timed out" : networkCauseSchema.safeParse(error.cause).data?.code ?? error.message;
68
+ //#endregion
69
+ //#region src/vsix.ts
70
+ const manifestSchema = z.looseObject({
71
+ publisher: z.string(),
72
+ name: z.string(),
73
+ engines: z.looseObject({ vscode: z.string().optional() }).optional()
74
+ });
75
+ /** Unpacks a VSIX's `extension/` payload into `destination`. */
76
+ async function extractVsix(vsix, destination, maxBytes = Number.POSITIVE_INFINITY) {
77
+ let unpacked = 0;
78
+ const zip = await new Promise((resolve, reject) => yauzl.open(vsix, { lazyEntries: true }, (error, opened) => error ? reject(error) : resolve(opened)));
79
+ const openReadStream = (entry) => new Promise((resolve, reject) => zip.openReadStream(entry, (error, stream) => error ? reject(error) : resolve(stream)));
80
+ try {
81
+ await new Promise((resolve, reject) => {
82
+ zip.on("error", reject);
83
+ zip.on("end", () => resolve());
84
+ zip.on("entry", (entry) => {
85
+ (async () => {
86
+ const name = entry.fileName.replaceAll("\\", "/");
87
+ if (name.endsWith("/") || !name.startsWith("extension/")) return;
88
+ const relative = name.slice(10);
89
+ const parts = relative.split("/");
90
+ const mode = entry.externalFileAttributes >>> 16 & 65535;
91
+ if (parts.some((part) => part === ".." || part === "." || part === "" || part.includes(":")) || path.posix.isAbsolute(relative)) throw new Error(`VSIX contains an unsafe path: ${name}`);
92
+ if ((mode & 61440) === 40960) throw new Error(`VSIX contains a symlink: ${name}`);
93
+ unpacked += entry.uncompressedSize;
94
+ if (unpacked > maxBytes) throw new Error(`VSIX unpacks to more than ${maxBytes} bytes`);
95
+ const output = path.join(destination, ...parts);
96
+ await mkdir(path.dirname(output), { recursive: true });
97
+ await pipeline(await openReadStream(entry), createWriteStream(output));
98
+ if (process.platform !== "win32" && mode & 73) await chmod(output, mode & 511);
99
+ })().then(() => zip.readEntry(), reject);
100
+ });
101
+ zip.readEntry();
102
+ });
103
+ } finally {
104
+ zip.close();
105
+ }
106
+ }
107
+ /**
108
+ * Drops the `scripts` and `dependencies` a payload ships prebundled, and
109
+ * `extensionPack` when Review does not ship the pack's members.
110
+ */
111
+ async function sanitizeVsixManifest(directory, extension) {
112
+ const file = path.join(directory, "package.json");
113
+ const manifest = manifestSchema.parse(JSON.parse(await readFile(file, "utf8")));
114
+ const declared = `${manifest.publisher}.${manifest.name}`;
115
+ if (declared.toLowerCase() !== extension.id.toLowerCase()) throw new Error(`${extension.id}: the VSIX declares ${declared}`);
116
+ const { scripts, dependencies, devDependencies, extensionPack, ...kept } = manifest;
117
+ await writeFile(file, `${JSON.stringify(extension.stripExtensionPack ? kept : {
118
+ ...kept,
119
+ extensionPack
120
+ }, void 0, 2)}\n`);
121
+ return manifest.engines?.vscode;
122
+ }
123
+ //#endregion
124
+ //#region src/remote-extensions.ts
125
+ const EXECUTABLE_TIMEOUT_MS = 15e3;
126
+ const STAMP_FILE = ".curated.json";
127
+ const curatedDownloadSchema = z.object({
128
+ universal: z.boolean(),
129
+ url: z.string(),
130
+ sha256: z.string(),
131
+ size: z.number()
132
+ });
133
+ const curatedExtensionSchema = z.object({
134
+ id: z.string(),
135
+ version: z.string(),
136
+ tier: z.enum(["bundled", "optional"]),
137
+ group: z.string(),
138
+ executables: z.array(z.string()),
139
+ stripExtensionPack: z.boolean(),
140
+ targets: z.object({
141
+ "linux-x64": curatedDownloadSchema,
142
+ "linux-arm64": curatedDownloadSchema
143
+ })
144
+ });
145
+ const stampSchema = z.object({
146
+ id: z.string(),
147
+ version: z.string(),
148
+ target: z.string(),
149
+ sha256: z.string(),
150
+ installedTimestamp: z.number()
151
+ });
152
+ function whiteboardRemoteHome(env = process.env) {
153
+ return path.join(devReviewHome(env), "whiteboard-remote");
154
+ }
155
+ function remoteServerPaths(env = process.env) {
156
+ const home = whiteboardRemoteHome(env);
157
+ return {
158
+ extensionsDir: path.join(home, "extensions"),
159
+ serverDataDir: path.join(home, "server")
160
+ };
161
+ }
162
+ function remoteExtensionTarget(platform = process.platform, arch = process.arch) {
163
+ if (platform === "linux" && (arch === "x64" || arch === "arm64")) return `linux-${arch}`;
164
+ throw new Error(`Language features on a remote need Linux on x64 or arm64; this machine is ${platform}-${arch}.`);
165
+ }
166
+ async function ensureRemoteExtensions(input = {}) {
167
+ const target = input.target ?? remoteExtensionTarget();
168
+ const curated = input.curated ?? await readPackagedCurated();
169
+ const { extensionsDir, serverDataDir } = remoteServerPaths(input.env);
170
+ const installed = [];
171
+ const skipped = [];
172
+ const failed = [];
173
+ const listed = [];
174
+ await mkdir(extensionsDir, { recursive: true });
175
+ for (const extension of curated) {
176
+ if (extension.tier === "optional" && !input.groups?.includes(extension.group)) {
177
+ skipped.push({
178
+ id: extension.id,
179
+ reason: `optional group "${extension.group}" not requested`
180
+ });
181
+ continue;
182
+ }
183
+ const download = extension.targets[target];
184
+ const folder = `${extension.id}-${extension.version}`;
185
+ const directory = path.join(extensionsDir, folder);
186
+ const stamp = {
187
+ id: extension.id,
188
+ version: extension.version,
189
+ target,
190
+ sha256: download.sha256
191
+ };
192
+ try {
193
+ const current = await readStamp(directory);
194
+ if (current && sameStamp(current, stamp) && await checkExecutables(directory, extension).then(() => true, () => false)) {
195
+ skipped.push({
196
+ id: extension.id,
197
+ reason: "up to date"
198
+ });
199
+ listed.push(storedExtension(extension, directory, folder, target, current.installedTimestamp));
200
+ continue;
201
+ }
202
+ const installedTimestamp = Date.now();
203
+ await install(extension, download, directory, {
204
+ ...stamp,
205
+ installedTimestamp
206
+ }, input.timeoutMs, input.signal);
207
+ installed.push(extension.id);
208
+ listed.push(storedExtension(extension, directory, folder, target, installedTimestamp));
209
+ } catch (error) {
210
+ failed.push({
211
+ id: extension.id,
212
+ error: error instanceof Error ? error.message : String(error)
213
+ });
214
+ }
215
+ }
216
+ const list = `${JSON.stringify(listed)}\n`;
217
+ const listFile = path.join(extensionsDir, "extensions.json");
218
+ if (await readFile(listFile, "utf8").catch(() => void 0) !== list) {
219
+ const temporary = `${listFile}.${process.pid}.tmp`;
220
+ await writeFile(temporary, list);
221
+ await rename(temporary, listFile);
222
+ await rm(path.join(serverDataDir, "data", "CachedProfilesData"), {
223
+ recursive: true,
224
+ force: true
225
+ });
226
+ }
227
+ const kept = new Set(listed.map((entry) => entry.relativeLocation));
228
+ for (const folder of await readdir(extensionsDir)) if (!kept.has(folder) && curated.some(({ id }) => folder.startsWith(`${id}-`) && /^\d/.test(folder.slice(id.length + 1)))) await rm(path.join(extensionsDir, folder), {
229
+ recursive: true,
230
+ force: true
231
+ });
232
+ return {
233
+ event: "remote.extensions",
234
+ target,
235
+ installed,
236
+ skipped,
237
+ failed,
238
+ groups: requestedGroups(curated, input.groups ?? [], failed)
239
+ };
240
+ }
241
+ function requestedGroups(curated, groups, failed) {
242
+ return groups.flatMap((group) => {
243
+ const members = curated.filter((extension) => extension.tier === "optional" && extension.group === group);
244
+ if (members.length === 0) return [];
245
+ const errors = failed.filter(({ id }) => members.some((extension) => extension.id === id));
246
+ return [errors.length === 0 ? {
247
+ group,
248
+ installed: true
249
+ } : {
250
+ group,
251
+ installed: false,
252
+ detail: errors.map(({ id, error }) => `${id}: ${error}`).join("; ")
253
+ }];
254
+ });
255
+ }
256
+ async function readPackagedCurated() {
257
+ const file = path.join(findReviewPackageRoot(import.meta.url), "vscode-server", "curated.json");
258
+ const text = await readFile(file, "utf8").catch(() => {
259
+ throw new Error(`This package has no VS Code server (${file} is missing).`);
260
+ });
261
+ return z.object({ extensions: z.array(curatedExtensionSchema) }).parse(JSON.parse(text)).extensions;
262
+ }
263
+ async function readStamp(directory) {
264
+ return readFile(path.join(directory, STAMP_FILE), "utf8").then((text) => stampSchema.safeParse(parseJsonText(text)).data).catch(() => void 0);
265
+ }
266
+ function sameStamp(current, expected) {
267
+ return current.id === expected.id && current.version === expected.version && current.target === expected.target && current.sha256 === expected.sha256;
268
+ }
269
+ async function install(extension, download, directory, stamp, timeoutMs, signal) {
270
+ const part = `${directory}.${process.pid}.vsix`;
271
+ const staging = `${directory}.${process.pid}.staging`;
272
+ try {
273
+ await downloadPinned(download, part, {
274
+ timeoutMs,
275
+ signal
276
+ });
277
+ await rm(staging, {
278
+ recursive: true,
279
+ force: true
280
+ });
281
+ await extractVsix(part, staging, download.size * 8);
282
+ await sanitizeVsixManifest(staging, extension);
283
+ await checkExecutables(staging, extension);
284
+ await writeFile(path.join(staging, STAMP_FILE), `${JSON.stringify(stamp)}\n`);
285
+ await rm(directory, {
286
+ recursive: true,
287
+ force: true
288
+ });
289
+ await rename(staging, directory);
290
+ } finally {
291
+ await rm(part, { force: true });
292
+ await rm(staging, {
293
+ recursive: true,
294
+ force: true
295
+ });
296
+ }
297
+ }
298
+ async function checkExecutables(directory, extension) {
299
+ for (const relative of extension.executables) await promisify(execFile)(path.join(directory, relative), ["--version"], {
300
+ timeout: EXECUTABLE_TIMEOUT_MS,
301
+ killSignal: "SIGKILL"
302
+ }).catch((error) => {
303
+ throw new Error(`${extension.id}: ${relative} does not run here: ${error.message}`);
304
+ });
305
+ }
306
+ function storedExtension(extension, directory, folder, target, installedTimestamp) {
307
+ return {
308
+ identifier: { id: extension.id },
309
+ version: extension.version,
310
+ location: {
311
+ $mid: 1,
312
+ scheme: "file",
313
+ path: directory
314
+ },
315
+ relativeLocation: folder,
316
+ metadata: {
317
+ installedTimestamp,
318
+ targetPlatform: extension.targets[target].universal ? "universal" : target
319
+ }
320
+ };
321
+ }
322
+ //#endregion
323
+ export { whiteboardRemoteHome as i, remoteExtensionTarget as n, remoteServerPaths as r, ensureRemoteExtensions as t };
@@ -0,0 +1,252 @@
1
+ import "./review-home-paths-zwevIq4I.js";
2
+ import { A as withFileLock, k as processStartIdentity } from "./s3-config-DDWWMh5x.js";
3
+ import { t as findReviewPackageRoot } from "./package-paths-BsuI4WUf.js";
4
+ import { n as cliSpawn, s as spawnDetached } from "./background-server-Cw7CcfaZ.js";
5
+ import { r as remoteServerPaths, t as ensureRemoteExtensions } from "./remote-extensions-BiNx6NQK.js";
6
+ import { randomBytes } from "node:crypto";
7
+ import { z } from "zod";
8
+ import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
9
+ import path from "node:path";
10
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
11
+ //#region src/remote-language-server.ts
12
+ const START_TIMEOUT_MS = 15e3;
13
+ const VERSION_TIMEOUT_MS = 3e3;
14
+ const INSTALL_TIMEOUT_MS = 35e3;
15
+ const SHUTDOWN_WITHOUT_DELAY_ENV = "DEV_FAST_REVIEW_REMOTE_SHUTDOWN_WITHOUT_DELAY";
16
+ const GRACE_ENV = "DEV_FAST_REVIEW_REMOTE_RECONNECTION_GRACE_SECONDS";
17
+ const PENDING = {
18
+ languageServer: null,
19
+ languageServerDetail: "Installing the language extensions on this host; they will be available on the next connection.",
20
+ languageServerPending: true
21
+ };
22
+ function idleArgs(env) {
23
+ const grace = env[GRACE_ENV] ?? "";
24
+ return [
25
+ "--enable-remote-auto-shutdown",
26
+ "--reconnection-grace-time",
27
+ /^[1-9]\d{0,3}$/.test(grace) ? grace : "600"
28
+ ];
29
+ }
30
+ const LISTENING = /Extension host agent listening on (\d+)/;
31
+ const LOCK = {
32
+ retryMs: 100,
33
+ staleMs: 6e4,
34
+ unownedGraceMs: 5e3,
35
+ identifyOwner: true
36
+ };
37
+ const installingSchema = z.object({
38
+ pid: z.number(),
39
+ started: z.string().nullable()
40
+ });
41
+ const runningSchema = z.object({
42
+ pid: z.number(),
43
+ started: z.string().nullable(),
44
+ port: z.number()
45
+ });
46
+ function remoteLanguageServerFiles(env) {
47
+ const { serverDataDir } = remoteServerPaths(env);
48
+ return {
49
+ serverDataDir,
50
+ tokenFile: path.join(serverDataDir, "connection-token"),
51
+ runningFile: path.join(serverDataDir, "server.json"),
52
+ logFile: path.join(serverDataDir, "server.log"),
53
+ lock: path.join(serverDataDir, "start.lock"),
54
+ installingFile: path.join(serverDataDir, "install.json"),
55
+ installLog: path.join(serverDataDir, "install.log"),
56
+ installLock: path.join(serverDataDir, "install.lock")
57
+ };
58
+ }
59
+ async function ensureRemoteLanguageServer(input) {
60
+ const root = path.join(input.packageRoot ?? findReviewPackageRoot(import.meta.url), "vscode-server");
61
+ const files = remoteLanguageServerFiles(input.env);
62
+ const capped = new AbortController();
63
+ let languageGroups = (input.groups ?? []).map((group) => ({
64
+ group,
65
+ installed: false
66
+ }));
67
+ const cap = setTimeout(() => capped.abort(), input.installTimeoutMs ?? INSTALL_TIMEOUT_MS);
68
+ try {
69
+ const commit = await readCommit(root);
70
+ const ensure = input.ensure ?? ensureRemoteExtensions;
71
+ await mkdir(files.serverDataDir, {
72
+ recursive: true,
73
+ mode: 448
74
+ });
75
+ if (await installing(files)) return {
76
+ ...PENDING,
77
+ languageGroups
78
+ };
79
+ const { failed, groups } = await ensure({
80
+ env: input.env,
81
+ groups: input.groups,
82
+ signal: AbortSignal.any([capped.signal, ...input.signal ? [input.signal] : []])
83
+ });
84
+ if (failed.length > 0 && capped.signal.aborted && !input.signal?.aborted && await installDetached(files, input)) return {
85
+ ...PENDING,
86
+ languageGroups
87
+ };
88
+ if (groups) languageGroups = groups;
89
+ if (failed.length > 0) throw new Error(`Could not install the language extensions: ${failed.map(({ id, error }) => `${id}: ${error}`).join("; ")}`);
90
+ const outcome = await withFileLock(files.lock, {
91
+ ...LOCK,
92
+ timeoutMs: (input.timeoutMs ?? START_TIMEOUT_MS) * 2
93
+ }, async () => {
94
+ return {
95
+ port: (await healthy(files.runningFile, commit))?.port ?? await startServer(root, files, input.env, input.timeoutMs),
96
+ connectionToken: (await readFile(files.tokenFile, "utf8")).trim(),
97
+ commit
98
+ };
99
+ });
100
+ if (!outcome.acquired) throw new Error("Another start of the VS Code server did not finish.");
101
+ return {
102
+ languageServer: outcome.result,
103
+ languageGroups
104
+ };
105
+ } catch (error) {
106
+ return {
107
+ languageServer: null,
108
+ languageServerDetail: error instanceof Error ? error.message : String(error),
109
+ languageGroups
110
+ };
111
+ } finally {
112
+ clearTimeout(cap);
113
+ }
114
+ }
115
+ async function readInstalling(files) {
116
+ return readFile(files.installingFile, "utf8").then((text) => installingSchema.safeParse(JSON.parse(text)).data).catch(() => void 0);
117
+ }
118
+ async function installing(files) {
119
+ const running = await readInstalling(files);
120
+ return running !== void 0 && ours(running);
121
+ }
122
+ async function remoteLanguageServerGroups(env) {
123
+ const files = remoteLanguageServerFiles(env);
124
+ return (await Promise.all([readRunning(files.runningFile), readInstalling(files)])).flatMap((entry) => entry && ours(entry) ? [entry.pid] : []);
125
+ }
126
+ async function stopProcessGroup(leader) {
127
+ for (const signal of ["SIGTERM", "SIGKILL"]) {
128
+ try {
129
+ process.kill(-leader, signal);
130
+ } catch {
131
+ return;
132
+ }
133
+ for (let i = 0; i < 50; i++) {
134
+ if (!groupAlive(leader)) return;
135
+ await setTimeout$1(100);
136
+ }
137
+ }
138
+ }
139
+ function groupAlive(leader) {
140
+ try {
141
+ process.kill(-leader, 0);
142
+ return true;
143
+ } catch (error) {
144
+ return error instanceof Error && "code" in error && error.code === "EPERM";
145
+ }
146
+ }
147
+ async function installDetached(files, input) {
148
+ return (await withFileLock(files.installLock, {
149
+ ...LOCK,
150
+ timeoutMs: 1e4
151
+ }, async () => {
152
+ if (await installing(files)) return;
153
+ const { command, args, env } = cliSpawn(input.cli, input.env);
154
+ const child = await spawnDetached({
155
+ command,
156
+ args: [
157
+ ...args,
158
+ "remote",
159
+ "extensions",
160
+ "ensure",
161
+ "--json",
162
+ ...input.groups?.length ? ["--groups", input.groups.join(",")] : []
163
+ ],
164
+ cwd: files.serverDataDir,
165
+ env,
166
+ log: files.installLog
167
+ });
168
+ if (child.pid !== void 0) await writeFile(files.installingFile, `${JSON.stringify({
169
+ pid: child.pid,
170
+ started: processStartIdentity(child.pid)
171
+ })}\n`, { mode: 384 });
172
+ })).acquired;
173
+ }
174
+ async function readCommit(root) {
175
+ const file = path.join(root, "product.json");
176
+ const text = await readFile(file, "utf8").catch(() => {
177
+ throw new Error(`This Whiteboard install has no VS Code server (${file} is missing).`);
178
+ });
179
+ const { commit } = z.object({ commit: z.string().regex(/^[0-9a-f]{40}$/) }).parse(JSON.parse(text));
180
+ return commit;
181
+ }
182
+ async function readRunning(file) {
183
+ return readFile(file, "utf8").then((text) => runningSchema.safeParse(JSON.parse(text)).data).catch(() => void 0);
184
+ }
185
+ const ours = (running) => running.started !== null && processStartIdentity(running.pid) === running.started;
186
+ async function healthy(file, commit) {
187
+ const running = await readRunning(file);
188
+ if (!running) return void 0;
189
+ if (ours(running) && await version(running.port) === commit) return running;
190
+ await stop(running);
191
+ }
192
+ async function version(port) {
193
+ return fetch(`http://127.0.0.1:${port}/version`, { signal: AbortSignal.timeout(VERSION_TIMEOUT_MS) }).then((response) => response.ok ? response.text() : void 0).catch(() => void 0);
194
+ }
195
+ async function stop(running) {
196
+ if (!ours(running)) return;
197
+ try {
198
+ process.kill(running.pid, "SIGTERM");
199
+ } catch {
200
+ return;
201
+ }
202
+ for (let i = 0; i < 50 && processStartIdentity(running.pid) !== null; i++) await setTimeout$1(100);
203
+ }
204
+ async function startServer(root, files, env, timeoutMs = START_TIMEOUT_MS) {
205
+ const { extensionsDir, serverDataDir } = remoteServerPaths(env);
206
+ const temporary = `${files.tokenFile}.${process.pid}.tmp`;
207
+ await writeFile(temporary, randomBytes(32).toString("hex"), { mode: 384 });
208
+ await rename(temporary, files.tokenFile);
209
+ const logStart = (await stat(files.logFile).catch(() => null))?.size ?? 0;
210
+ const child = await spawnDetached({
211
+ command: process.execPath,
212
+ args: [
213
+ path.join(root, "out", "server-main.js"),
214
+ "--host",
215
+ "127.0.0.1",
216
+ "--port",
217
+ "0",
218
+ "--connection-token-file",
219
+ files.tokenFile,
220
+ "--server-data-dir",
221
+ serverDataDir,
222
+ "--extensions-dir",
223
+ extensionsDir,
224
+ "--accept-server-license-terms",
225
+ ...idleArgs(env),
226
+ ...env[SHUTDOWN_WITHOUT_DELAY_ENV] === "1" ? ["--remote-auto-shutdown-without-delay"] : []
227
+ ],
228
+ cwd: serverDataDir,
229
+ env,
230
+ log: files.logFile
231
+ });
232
+ const deadline = Date.now() + timeoutMs;
233
+ while (Date.now() < deadline && !child.exited) {
234
+ const output = (await readFile(files.logFile)).subarray(logStart).toString("utf8");
235
+ const port = Number(LISTENING.exec(output)?.[1]);
236
+ if (port && child.pid !== void 0) {
237
+ const running = {
238
+ pid: child.pid,
239
+ started: processStartIdentity(child.pid),
240
+ port
241
+ };
242
+ await writeFile(files.runningFile, `${JSON.stringify(running)}\n`, { mode: 384 });
243
+ return port;
244
+ }
245
+ await setTimeout$1(100);
246
+ }
247
+ if (!child.exited && child.pid !== void 0) process.kill(child.pid, "SIGTERM");
248
+ const tail = (await readFile(files.logFile)).subarray(logStart).toString("utf8").trimEnd().split("\n").slice(-10).join("\n");
249
+ throw new Error(`The VS Code server did not start${child.error ? `: ${child.error.message}` : child.exited ? "" : ` within ${timeoutMs / 1e3} s`}. The end of ${files.logFile}:\n${tail}`);
250
+ }
251
+ //#endregion
252
+ export { remoteLanguageServerGroups as n, stopProcessGroup as r, ensureRemoteLanguageServer as t };