@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.
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/{account-alias-D0hF0sKk.js → account-alias-ChZR3RIU.js} +94 -8
- package/dist/{agent-cli-04xbDj23.js → agent-cli-CR2PnA9G.js} +9 -12
- package/dist/agent-cli-CRPPmS3l.js +2 -0
- package/dist/{agent-client-P3J4y8Zn.js → agent-client-DVd-prfp.js} +31 -14
- package/dist/{authoring-tools-fdA0RPIT.js → authoring-tools-BXKEwhfi.js} +5 -5
- package/dist/background-server-Cw7CcfaZ.js +156 -0
- package/dist/background-server-D2Rrr_e2.js +2 -0
- package/dist/build-info.json +1 -1
- package/dist/{cli-C0ziP6hg.js → cli-CSrz_wGk.js} +1 -1
- package/dist/{cli-runner-C3men22A.js → cli-runner-PlGqBeMw.js} +368 -252
- package/dist/cli.js +3 -2
- package/dist/{client-DzcSt6pV.js → client-DhZzPePs.js} +5 -3
- package/dist/{process-error-telemetry-BCBYp2t4.js → headless-host-CVXTSIlx.js} +3285 -2585
- package/dist/headless-host-Cp37C-v0.js +2 -0
- package/dist/input-error-OLgB_h31.js +11 -0
- package/dist/{local-data-Dy1pNtLF.js → local-data-yfLp5PiJ.js} +113 -62
- package/dist/{mcp-B8kuRSXM.js → mcp-DBbDwG4y.js} +4 -4
- package/dist/{package-paths-B9PDoAl6.js → package-paths-BsuI4WUf.js} +1 -1
- package/dist/profile-BSOkA6A-.js +2 -0
- package/dist/{profile-tbGWuWHu.js → profile-pAzg7fxC.js} +3 -3
- package/dist/remote-attach-BAt2eZx3.js +137 -0
- package/dist/remote-extensions-2Cxf3hMo.js +2 -0
- package/dist/remote-extensions-BiNx6NQK.js +323 -0
- package/dist/remote-language-server-DQgWUNeV.js +252 -0
- package/dist/remote-uninstall-DLtDG0o7.js +153 -0
- package/dist/{request-origin-DA4K1qWY.js → request-origin-DaRIV3cB.js} +5 -2
- package/dist/review-app-launcher-D7I4DGoH.js +222 -0
- package/dist/{review-home-paths-Y9mYWkCI.js → review-home-paths-zwevIq4I.js} +7 -7
- package/dist/{review-telemetry-CmM6tCbj.js → review-telemetry-BqB4CiTD.js} +5 -5
- package/dist/{s3-SzLpeCG6.js → s3-77-qkEhs.js} +1 -1
- package/dist/{s3-config-BgxkSoOy.js → s3-config-0xuCh-F_.js} +1 -1
- package/dist/{s3-config-CAxhO9u_.js → s3-config-DDWWMh5x.js} +5 -1
- package/dist/server/desktop-host.js +1663 -266
- package/dist/{desktop-discovery-Ba3dD5Cn.js → server-discovery-CifQ3yQS.js} +88 -3
- package/dist/sharing/index.d.ts +88 -32
- package/dist/sharing/index.js +1 -1
- package/dist/{src-DiHP11l3.js → src-BMPuxWv4.js} +6 -11
- package/dist/{src-WCbabvdS.js → src-Bgvf1sEN.js} +128 -8
- package/dist/{stored-document-migration-u4L_1VZa.js → stored-document-migration-CFOvjRwW.js} +5 -18
- package/dist/{tool-failure-CJHBCHMh.js → tool-failure-BXWzQG8T.js} +2 -2
- package/instructions/authoring.md +16 -12
- package/instructions/file-lenses.md +4 -2
- package/package.json +9 -14
- package/src/agent-connect.ts +507 -0
- package/src/agent-selection.ts +4 -0
- package/src/ask/agents.ts +59 -13
- package/src/ask/pi-mcp.ts +7 -3
- package/src/ask/thread-state.ts +9 -4
- package/src/ask/thread.ts +88 -50
- package/src/ask/threads.ts +53 -7
- package/src/cli-install.ts +3 -1
- package/src/cli-runner.ts +472 -81
- package/src/cli.ts +1 -1
- package/src/connect-agents.ts +285 -0
- package/src/connect-prompts.ts +60 -6
- package/src/desktop-discovery.ts +24 -0
- package/src/pinned-download.ts +118 -0
- package/src/remote-attach.ts +130 -0
- package/src/remote-extensions.ts +359 -0
- package/src/remote-language-server.ts +428 -0
- package/src/remote-toolchains.ts +107 -0
- package/src/remote-uninstall.ts +280 -0
- package/src/review-api/README.md +1 -3
- package/src/review-api/agent-cli.ts +4 -4
- package/src/review-api/agent-client.ts +23 -0
- package/src/review-api/ask-history.ts +31 -16
- package/src/review-api/authoring-tools.ts +1 -1
- package/src/review-api/blocks/index.ts +2 -4
- package/src/review-api/document.ts +1 -4
- package/src/review-api/http.ts +265 -237
- package/src/review-api/local-data.ts +18 -3
- package/src/review-api/onboarding.ts +478 -0
- package/src/review-api/origin.ts +7 -0
- package/src/review-api/recent-folders.ts +173 -0
- package/src/review-api/store.ts +56 -10
- package/src/review-api/workspaces.ts +56 -11
- package/src/review-app-launcher.ts +61 -3
- package/src/review-stack.ts +162 -36
- package/src/server/background-server.ts +250 -0
- package/src/server/desktop-host-shutdown.ts +24 -0
- package/src/server/desktop-host.ts +39 -24
- package/src/server/desktop-server.ts +68 -118
- package/src/server/global-verb-relay.ts +16 -0
- package/src/server/headless-host.ts +128 -3
- package/src/server/hono-http.ts +6 -1
- package/src/server/legacy-tutorial-cleanup.ts +38 -0
- package/src/server/review-gateway-hosts.ts +379 -0
- package/src/server/review-gateway-list.ts +109 -0
- package/src/server/review-gateway-memory.ts +130 -0
- package/src/server/review-gateway-pushes.ts +142 -0
- package/src/server/review-gateway-streams.ts +415 -0
- package/src/server/review-gateway-transport.ts +204 -0
- package/src/server/review-gateway.ts +433 -0
- package/src/server/review-server-core.ts +36 -6
- package/src/server-discovery.ts +11 -0
- package/src/sharing/import.ts +1 -0
- package/src/ui-telemetry-events.ts +5 -1
- package/src/vsix.ts +122 -0
- package/dist/agent-cli-BR_Jw5BL.js +0 -2
- package/dist/headless-host-BLLJ7Dyu.js +0 -95
- package/dist/profile-aMSw1oCM.js +0 -2
- package/dist/server-discovery-ZVi8Ay8J.js +0 -66
- package/src/fixtures/blocks/tutorial.json +0 -14
- package/src/review-api/blocks/tutorial.ts +0 -23
- package/src/server/tutorial-service.ts +0 -217
- package/tutorial/document.json +0 -216
- package/tutorial/lenses.json +0 -10
- package/tutorial/runtime-manifest.json +0 -11
- package/tutorial/sample-service/package.json +0 -9
- package/tutorial/sample-service/src/api/checkout-api.ts +0 -17
- package/tutorial/sample-service/src/app.ts +0 -26
- package/tutorial/sample-service/src/database/drizzle-orm.d.ts +0 -16
- package/tutorial/sample-service/src/database/schema.ts +0 -9
- package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +0 -15
- package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +0 -24
- package/tutorial/sample-service/src/inventory/forklift-mood.ts +0 -4
- package/tutorial/sample-service/src/inventory/inventory-service.ts +0 -28
- package/tutorial/sample-service/src/inventory/warehouse-lore.ts +0 -11
- package/tutorial/sample-service/src/orders/order-service.ts +0 -35
- package/tutorial/sample-service/src/orders/order.ts +0 -43
- package/tutorial/sample-service/src/orders/orders-repository.ts +0 -24
- package/tutorial/sample-service/src/payments/fraud-vibes.ts +0 -4
- package/tutorial/sample-service/src/payments/payment-gateway.ts +0 -29
- package/tutorial/sample-service/src/payments/raccoon-ledger.ts +0 -9
- package/tutorial/sample-service/src/shipping/shipping-gateway.ts +0 -15
- 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 };
|