@dev.fast/whiteboard 0.2.5-preview.20261013.112 → 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 (130) 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-BBPxwUOd.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-D8WXsm_g.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-C1gY-HAI.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-C0swByfp.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-BlERtVm2.js → local-data-yfLp5PiJ.js} +237 -157
  18. package/dist/{mcp-t7VN9R3k.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-BsYDBr7t.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/github-provider.ts +154 -0
  72. package/src/review-api/http.ts +265 -237
  73. package/src/review-api/local-data.ts +34 -17
  74. package/src/review-api/onboarding.ts +478 -0
  75. package/src/review-api/origin.ts +10 -6
  76. package/src/review-api/provider.ts +44 -0
  77. package/src/review-api/pull-request.ts +15 -146
  78. package/src/review-api/recent-folders.ts +173 -0
  79. package/src/review-api/store.ts +56 -10
  80. package/src/review-api/workspaces.ts +56 -11
  81. package/src/review-app-launcher.ts +61 -3
  82. package/src/review-stack.ts +162 -36
  83. package/src/server/background-server.ts +250 -0
  84. package/src/server/desktop-host-shutdown.ts +24 -0
  85. package/src/server/desktop-host.ts +39 -24
  86. package/src/server/desktop-server.ts +68 -118
  87. package/src/server/global-verb-relay.ts +16 -0
  88. package/src/server/headless-host.ts +128 -3
  89. package/src/server/hono-http.ts +6 -1
  90. package/src/server/legacy-tutorial-cleanup.ts +38 -0
  91. package/src/server/review-gateway-hosts.ts +379 -0
  92. package/src/server/review-gateway-list.ts +109 -0
  93. package/src/server/review-gateway-memory.ts +130 -0
  94. package/src/server/review-gateway-pushes.ts +142 -0
  95. package/src/server/review-gateway-streams.ts +415 -0
  96. package/src/server/review-gateway-transport.ts +204 -0
  97. package/src/server/review-gateway.ts +433 -0
  98. package/src/server/review-server-core.ts +36 -6
  99. package/src/server-discovery.ts +11 -0
  100. package/src/sharing/import.ts +1 -0
  101. package/src/ui-telemetry-events.ts +5 -1
  102. package/src/vsix.ts +122 -0
  103. package/dist/agent-cli-YSuhE5X8.js +0 -2
  104. package/dist/headless-host-BHD_Vg5F.js +0 -95
  105. package/dist/profile-j97aRTS7.js +0 -2
  106. package/dist/server-discovery-ZVi8Ay8J.js +0 -66
  107. package/src/fixtures/blocks/tutorial.json +0 -14
  108. package/src/review-api/blocks/tutorial.ts +0 -23
  109. package/src/server/tutorial-service.ts +0 -217
  110. package/tutorial/document.json +0 -216
  111. package/tutorial/lenses.json +0 -10
  112. package/tutorial/runtime-manifest.json +0 -11
  113. package/tutorial/sample-service/package.json +0 -9
  114. package/tutorial/sample-service/src/api/checkout-api.ts +0 -17
  115. package/tutorial/sample-service/src/app.ts +0 -26
  116. package/tutorial/sample-service/src/database/drizzle-orm.d.ts +0 -16
  117. package/tutorial/sample-service/src/database/schema.ts +0 -9
  118. package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +0 -15
  119. package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +0 -24
  120. package/tutorial/sample-service/src/inventory/forklift-mood.ts +0 -4
  121. package/tutorial/sample-service/src/inventory/inventory-service.ts +0 -28
  122. package/tutorial/sample-service/src/inventory/warehouse-lore.ts +0 -11
  123. package/tutorial/sample-service/src/orders/order-service.ts +0 -35
  124. package/tutorial/sample-service/src/orders/order.ts +0 -43
  125. package/tutorial/sample-service/src/orders/orders-repository.ts +0 -24
  126. package/tutorial/sample-service/src/payments/fraud-vibes.ts +0 -4
  127. package/tutorial/sample-service/src/payments/payment-gateway.ts +0 -29
  128. package/tutorial/sample-service/src/payments/raccoon-ledger.ts +0 -9
  129. package/tutorial/sample-service/src/shipping/shipping-gateway.ts +0 -15
  130. package/tutorial/sample-service/tsconfig.json +0 -12
@@ -1,7 +1,7 @@
1
- import { C as ReviewInputError } from "./src-DiHP11l3.js";
2
- import { a as toolResultText, s as ReviewApiError } from "./agent-client-P3J4y8Zn.js";
3
- import { n as REVIEW_STATUS_TOOL, t as authoringTools } from "./authoring-tools-D8WXsm_g.js";
4
- import { i as publicTool, n as RECOVERY, r as callPublicTool, t as toolFailure } from "./tool-failure-CJHBCHMh.js";
1
+ import { a as toolResultText, s as ReviewApiError } from "./agent-client-DVd-prfp.js";
2
+ import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
3
+ import { n as REVIEW_STATUS_TOOL, t as authoringTools } from "./authoring-tools-BXKEwhfi.js";
4
+ import { i as publicTool, n as RECOVERY, r as callPublicTool, t as toolFailure } from "./tool-failure-BXWzQG8T.js";
5
5
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
7
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -1,5 +1,5 @@
1
1
  import { c as jsonString, o as jsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
2
- import "./review-home-paths-Y9mYWkCI.js";
2
+ import "./review-home-paths-zwevIq4I.js";
3
3
  import { readFileSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -0,0 +1,2 @@
1
+ import { t as openReviewProfile } from "./profile-pAzg7fxC.js";
2
+ export { openReviewProfile };
@@ -1,6 +1,6 @@
1
- import { o as initializeReviewStoreSchema, t as openLocalReviewStore } from "./local-data-BlERtVm2.js";
2
- import "./review-home-paths-Y9mYWkCI.js";
3
- import { O as withFileLock } from "./s3-config-CAxhO9u_.js";
1
+ import { o as initializeReviewStoreSchema, t as openLocalReviewStore } from "./local-data-yfLp5PiJ.js";
2
+ import "./review-home-paths-zwevIq4I.js";
3
+ import { A as withFileLock } from "./s3-config-DDWWMh5x.js";
4
4
  import { access, mkdir } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { DatabaseSync } from "node:sqlite";
@@ -0,0 +1,137 @@
1
+ import { i as readReviewPackageVersion } from "./package-paths-BsuI4WUf.js";
2
+ import { a as readReviewServerHealth, l as serverNotReady } from "./server-discovery-CifQ3yQS.js";
3
+ import { c as stopBackgroundServer, o as recordedBackgroundServer, r as ensureBackgroundServer } from "./background-server-Cw7CcfaZ.js";
4
+ import { t as ensureRemoteLanguageServer } from "./remote-language-server-DQgWUNeV.js";
5
+ import path from "node:path";
6
+ import { spawn } from "node:child_process";
7
+ import { gt, valid } from "semver";
8
+ //#region src/remote-toolchains.ts
9
+ const PROBE_TIMEOUT_MS = 1e4;
10
+ const TOOLCHAINS = {
11
+ rust: [["cargo", "--version"], ["rustc", "--version"]],
12
+ swift: [["swift", "--version"]],
13
+ csharp: [["dotnet", "--version"]],
14
+ go: [["go", "version"]]
15
+ };
16
+ function probe(command, env, timeoutMs) {
17
+ const shell = env.SHELL || "/bin/sh";
18
+ const flags = ["csh", "tcsh"].includes(path.basename(shell)) ? ["-ic"] : [
19
+ "-i",
20
+ "-l",
21
+ "-c"
22
+ ];
23
+ return new Promise((resolve) => {
24
+ const child = spawn(shell, [...flags, command.join(" ")], {
25
+ env,
26
+ detached: true,
27
+ stdio: "ignore"
28
+ });
29
+ const timer = setTimeout(() => {
30
+ child.removeAllListeners("exit");
31
+ try {
32
+ process.kill(-child.pid, "SIGKILL");
33
+ } catch {}
34
+ resolve(`${command.join(" ")} did not answer within ${timeoutMs / 1e3} s`);
35
+ }, timeoutMs);
36
+ child.once("error", (error) => {
37
+ clearTimeout(timer);
38
+ resolve(`the login shell ${shell} did not run: ${error.message}`);
39
+ });
40
+ child.once("exit", (code) => {
41
+ clearTimeout(timer);
42
+ resolve(code === 0 ? "found" : code === 127 ? "missing" : `${command.join(" ")} failed with exit code ${code ?? "none"}`);
43
+ });
44
+ });
45
+ }
46
+ async function missingToolchains(groups, env, { timeoutMs = PROBE_TIMEOUT_MS, toolchains = TOOLCHAINS } = {}) {
47
+ const entries = await Promise.all(groups.map(async (group) => {
48
+ const commands = Object.hasOwn(toolchains, group) ? toolchains[group] : [];
49
+ const outcomes = await Promise.all(commands.map((command) => probe(command, env, timeoutMs)));
50
+ const missing = commands.filter((_, i) => outcomes[i] === "missing").map(([tool]) => tool);
51
+ const failures = outcomes.filter((outcome) => outcome !== "found" && outcome !== "missing");
52
+ const details = [...missing.length > 0 ? [`${missing.join(" and ")} ${missing.length > 1 ? "were" : "was"} not found on the login shell's PATH`] : [], ...failures];
53
+ return details.length > 0 ? [[group, details.join("; ")]] : [];
54
+ }));
55
+ return new Map(entries.flat());
56
+ }
57
+ //#endregion
58
+ //#region src/remote-attach.ts
59
+ async function remoteAttach(input) {
60
+ const extensions = new AbortController();
61
+ const language = ensureRemoteLanguageServer({
62
+ env: input.env,
63
+ packageRoot: input.packageRoot,
64
+ groups: input.groups,
65
+ signal: extensions.signal,
66
+ ensure: input.ensureExtensions,
67
+ installTimeoutMs: input.installTimeoutMs,
68
+ cli: input.cli
69
+ });
70
+ const toolchains = missingToolchains(input.groups ?? [], input.env);
71
+ let server;
72
+ let previousVersion;
73
+ let incompatibleRunning;
74
+ try {
75
+ const version = input.version ?? readReviewPackageVersion(import.meta.url);
76
+ const other = input.replace ? await otherVersionRunning(input.stateDir, version) : void 0;
77
+ if (other && (other.startedBy === "user" || newer(other.version, version))) incompatibleRunning = other;
78
+ else if (other) {
79
+ await stopBackgroundServer({ serverPid: other.pid });
80
+ previousVersion = other.version;
81
+ }
82
+ server = await ensureBackgroundServer({
83
+ stateDir: input.stateDir,
84
+ env: input.env,
85
+ startedBy: "desktop",
86
+ cli: input.cli
87
+ });
88
+ } catch (error) {
89
+ extensions.abort();
90
+ await Promise.all([language, toolchains]);
91
+ throw error;
92
+ }
93
+ const { discovery, started } = server;
94
+ const { languageServer, languageServerDetail, languageServerPending, languageGroups } = await language;
95
+ const missing = await toolchains;
96
+ const health = await readReviewServerHealth(discovery);
97
+ if (!health) throw serverNotReady(input.stateDir);
98
+ return {
99
+ event: "remote.attach",
100
+ version: health.version ?? null,
101
+ commit: health.commit ?? null,
102
+ serverId: health.serverId ?? null,
103
+ url: discovery.url,
104
+ token: discovery.token,
105
+ startedServer: started,
106
+ languageServer,
107
+ ...languageServerDetail !== void 0 && { languageServerDetail },
108
+ ...languageServerPending && { languageServerPending },
109
+ languageGroups: languageGroups.map(({ group, installed, detail }) => {
110
+ const details = [detail, missing.get(group)].filter(Boolean).join("; ");
111
+ return {
112
+ group,
113
+ installed,
114
+ ...details && { detail: details }
115
+ };
116
+ }),
117
+ ...previousVersion !== void 0 && {
118
+ replaced: true,
119
+ previousVersion
120
+ },
121
+ ...incompatibleRunning && { incompatibleRunning }
122
+ };
123
+ }
124
+ const newer = (running, own) => valid(running) !== null && valid(own) !== null && gt(running, own);
125
+ async function otherVersionRunning(stateDir, version) {
126
+ const recorded = await recordedBackgroundServer(stateDir);
127
+ if (!recorded) return void 0;
128
+ const { discovery, health } = recorded;
129
+ const running = health.version ?? "unknown";
130
+ return running === version ? void 0 : {
131
+ version: running,
132
+ pid: discovery.serverPid,
133
+ startedBy: discovery.startedBy
134
+ };
135
+ }
136
+ //#endregion
137
+ export { remoteAttach };
@@ -0,0 +1,2 @@
1
+ import { t as ensureRemoteExtensions } from "./remote-extensions-BiNx6NQK.js";
2
+ export { ensureRemoteExtensions };
@@ -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 };