@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
@@ -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 };
@@ -0,0 +1,153 @@
1
+ import { b as REVIEW_REMOTE_INSTALL_LOCK, x as REVIEW_REMOTE_WRAPPER_MARK } from "./src-Bgvf1sEN.js";
2
+ import "./review-home-paths-zwevIq4I.js";
3
+ import { j as DEV_REVIEW_HOME_ENV } from "./s3-config-DDWWMh5x.js";
4
+ import { c as stopBackgroundServer, o as recordedBackgroundServer } from "./background-server-Cw7CcfaZ.js";
5
+ import { i as whiteboardRemoteHome } from "./remote-extensions-BiNx6NQK.js";
6
+ import { n as remoteLanguageServerGroups, r as stopProcessGroup } from "./remote-language-server-DQgWUNeV.js";
7
+ import { existsSync } from "node:fs";
8
+ import { lstat, mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
9
+ import path from "node:path";
10
+ import { execFile } from "node:child_process";
11
+ import { promisify } from "node:util";
12
+ //#region src/remote-uninstall.ts
13
+ const LOCK_STALE_MS = 9e5;
14
+ const REVIEW_STORE = [
15
+ "review-api.db",
16
+ "review-api.db-wal",
17
+ "review-api.db-shm",
18
+ "review-api.db.workspaces",
19
+ "review-api.db.workspaces-wal",
20
+ "review-api.db.workspaces-shm"
21
+ ];
22
+ async function remoteUninstall(input) {
23
+ const refuse = (reason) => ({
24
+ event: "remote.uninstall",
25
+ ok: false,
26
+ reason
27
+ });
28
+ if (!path.isAbsolute(input.home)) return refuse(`HOME is ${input.home ? JSON.stringify(input.home) : "not set"}; Whiteboard removes nothing without an absolute home.`);
29
+ if (!path.isAbsolute(input.stateDir)) return refuse(`The review home ${JSON.stringify(input.stateDir)} is not an absolute path.`);
30
+ const override = input.env[DEV_REVIEW_HOME_ENV]?.trim();
31
+ if (override && (/[\x00-\x1f\x7f-\x9f]/.test(override) || override.includes("//") || path.resolve(override) !== (override.replace(/\/$/, "") || "/"))) return refuse(`DEV_REVIEW_HOME is ${JSON.stringify(override)}; Whiteboard removes nothing under a review home that is not an absolute, normalised path.`);
32
+ const install = whiteboardRemoteHome(input.env);
33
+ const wrapper = path.join(input.home, ".local", "bin", "whiteboard");
34
+ const lock = path.join(install, REVIEW_REMOTE_INSTALL_LOCK);
35
+ const busy = async () => refuse(`Desktop on ${await readText(path.join(lock, "owner")) || "another computer"} is installing Whiteboard here. Try again when it is done.`);
36
+ if (existsSync(lock)) {
37
+ if (!await stale(lock)) return busy();
38
+ await rm(lock, {
39
+ recursive: true,
40
+ force: true
41
+ });
42
+ }
43
+ const held = existsSync(install);
44
+ if (held && !await mkdir(lock).then(() => true, () => false)) return busy();
45
+ try {
46
+ return await removeInstall(input, install, wrapper, refuse);
47
+ } finally {
48
+ if (held) await rm(lock, {
49
+ recursive: true,
50
+ force: true
51
+ });
52
+ }
53
+ }
54
+ async function removeInstall(input, install, wrapper, refuse) {
55
+ const server = await recordedBackgroundServer(input.stateDir);
56
+ const recorded = server && {
57
+ ...server.discovery,
58
+ version: server.health.version ?? null
59
+ };
60
+ const stoppable = recorded && recorded.startedBy !== "user" ? recorded.serverPid : void 0;
61
+ const groups = await remoteLanguageServerGroups(input.env);
62
+ const running = await processesFrom(`${install}/`);
63
+ if (running === void 0) return refuse(`Cannot list this host's processes to check that none runs from ${install}.`);
64
+ const others = running.filter(({ pid, pgid }) => pid !== stoppable && !groups.includes(pgid)).map(({ pid }) => pid);
65
+ if (others.length) return refuse(`${others.map((pid) => pid === recorded?.serverPid ? `A Whiteboard server you started (process ${pid})` : `Process ${pid}`).join(", ")} ${others.length > 1 ? "run" : "runs"} from ${install}. Stop ${others.length > 1 ? "them" : "it"}, then run whiteboard remote uninstall again.`);
66
+ if (recorded?.startedBy === "user" && input.deleteReviews) return refuse(`A Whiteboard server you started (process ${recorded.serverPid}) uses the reviews in ${input.stateDir}. Stop it, then run whiteboard remote uninstall again.`);
67
+ await Promise.all(groups.map(stopProcessGroup));
68
+ let stoppedServer;
69
+ if (recorded && stoppable !== void 0) {
70
+ try {
71
+ await stopBackgroundServer({ serverPid: stoppable });
72
+ } catch (error) {
73
+ return refuse(error instanceof Error ? error.message : String(error));
74
+ }
75
+ stoppedServer = {
76
+ pid: stoppable,
77
+ version: recorded.version
78
+ };
79
+ }
80
+ const left = await processesFrom(`${install}/`);
81
+ if (left?.length !== 0) return refuse(`${left ? `Process ${left.map(({ pid }) => pid).join(", ")}` : "A process"} still runs from ${install}. Stop it, then run whiteboard remote uninstall again.`);
82
+ const removed = [];
83
+ if (existsSync(install)) {
84
+ await rm(install, {
85
+ recursive: true,
86
+ force: true
87
+ });
88
+ removed.push(install);
89
+ }
90
+ if (await desktopWrote(wrapper)) {
91
+ await rm(wrapper, { force: true });
92
+ removed.push(wrapper);
93
+ }
94
+ if (input.deleteReviews) for (const name of REVIEW_STORE) {
95
+ const file = path.join(input.stateDir, name);
96
+ if (existsSync(file)) {
97
+ await rm(file, { force: true });
98
+ removed.push(file);
99
+ }
100
+ }
101
+ const result = {
102
+ event: "remote.uninstall",
103
+ ok: true,
104
+ removed,
105
+ keptReviews: !input.deleteReviews
106
+ };
107
+ if (stoppedServer) result.stoppedServer = stoppedServer;
108
+ return result;
109
+ }
110
+ async function stale(lock) {
111
+ const started = await readText(path.join(lock, "started"));
112
+ if (started !== "") return /^\d+$/.test(started) && Date.now() - Number(started) * 1e3 >= LOCK_STALE_MS;
113
+ if (existsSync(path.join(lock, "started"))) return false;
114
+ const since = (await stat(lock).catch(() => void 0))?.mtimeMs;
115
+ return since !== void 0 && Date.now() - since >= LOCK_STALE_MS;
116
+ }
117
+ const readText = (file) => readFile(file, "utf8").then((text) => text.trim(), () => "");
118
+ async function processesFrom(prefix) {
119
+ let lines;
120
+ try {
121
+ const pids = (await readdir("/proc")).filter((name) => /^\d+$/.test(name));
122
+ lines = await Promise.all(pids.map(async (pid) => {
123
+ const [args = "", stat = ""] = await Promise.all([`/proc/${pid}/cmdline`, `/proc/${pid}/stat`].map((file) => readFile(file, "utf8").catch(() => "")));
124
+ const pgid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[2]);
125
+ return {
126
+ pid: Number(pid),
127
+ pgid,
128
+ args: args.replaceAll("\0", " ")
129
+ };
130
+ }));
131
+ } catch {
132
+ try {
133
+ const { stdout } = await promisify(execFile)("ps", ["-eo", "pid=,pgid=,args="], { maxBuffer: 16 << 20 });
134
+ lines = stdout.split("\n").map((line) => {
135
+ const [, pid = "", pgid = "", args = ""] = /^\s*(\d+)\s+(\d+)\s(.*)$/.exec(line) ?? [];
136
+ return {
137
+ pid: Number(pid),
138
+ pgid: Number(pgid),
139
+ args
140
+ };
141
+ });
142
+ } catch {
143
+ return;
144
+ }
145
+ }
146
+ return lines.filter(({ pid, args }) => pid && pid !== process.pid && args.includes(prefix));
147
+ }
148
+ async function desktopWrote(file) {
149
+ if (!(await lstat(file).catch(() => void 0))?.isFile()) return false;
150
+ return (await readFile(file, "utf8")).split("\n").includes(REVIEW_REMOTE_WRAPPER_MARK);
151
+ }
152
+ //#endregion
153
+ export { remoteUninstall };
@@ -1,5 +1,5 @@
1
1
  import { a as jsonNumber, c as jsonString, i as jsonBoolean, n as isJsonObject } from "./src-X9phtB2j.js";
2
- import { f as REVIEW_DIFF_LAYOUTS, p as REVIEW_THEME_CHOICES } from "./src-WCbabvdS.js";
2
+ import { S as REVIEW_THEME_CHOICES, h as REVIEW_DIFF_LAYOUT_SCOPES, m as REVIEW_DIFF_LAYOUTS } from "./src-Bgvf1sEN.js";
3
3
  import { z } from "zod";
4
4
  //#region src/telemetry-clean-text.ts
5
5
  const USER_DATA_REGEXES = [
@@ -442,7 +442,10 @@ const UI_TELEMETRY_EVENTS = {
442
442
  },
443
443
  diff_layout_changed: {
444
444
  event: "review_diff_layout_changed",
445
- properties: { layout: REVIEW_DIFF_LAYOUTS }
445
+ properties: {
446
+ layout: REVIEW_DIFF_LAYOUTS,
447
+ scope: REVIEW_DIFF_LAYOUT_SCOPES
448
+ }
446
449
  },
447
450
  review_dismissed: {
448
451
  event: "review_review_dismissed",
@@ -0,0 +1,222 @@
1
+ import { o as parseReviewVerbResponse } from "./src-Bgvf1sEN.js";
2
+ import { b as selectReviewInstance, m as healthyReviewInstance, v as reviewInstanceStartHint, y as reviewInstanceUnavailable } from "./server-discovery-CifQ3yQS.js";
3
+ import { closeSync, existsSync, fstatSync, mkdtempSync, openSync, readSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { homedir, tmpdir } from "node:os";
6
+ import { execFileSync, spawn } from "node:child_process";
7
+ //#region src/review-app-launcher.ts
8
+ const RELEASE_APPS = {
9
+ stable: {
10
+ bundleId: "dev.fast.review",
11
+ macApp: "Whiteboard.app",
12
+ linuxLauncher: "/usr/bin/review-desktop"
13
+ },
14
+ preview: {
15
+ bundleId: "dev.fast.review.preview",
16
+ macApp: "Whiteboard Preview.app",
17
+ linuxLauncher: "/usr/bin/review-preview-desktop"
18
+ }
19
+ };
20
+ /** "1" on launches without --focus; Desktop opens inactive. */
21
+ const REVIEW_DESKTOP_BACKGROUND_ENV = "DEV_FAST_REVIEW_DESKTOP_BACKGROUND";
22
+ const DEFAULT_LAUNCH_TIMEOUT_MS = 9e4;
23
+ const POLL_INTERVAL_MS = 250;
24
+ const EARLY_EXIT_GRACE_MS = 5e3;
25
+ async function runReviewAppLaunch(input = {}, overrides = {}) {
26
+ const fetch = overrides.fetch ?? globalThis.fetch;
27
+ const runtime = {
28
+ selectInstance: () => selectReviewInstance({ fetch }),
29
+ fetch,
30
+ focusDesktop: (discovery) => focusReviewDesktop(discovery, fetch),
31
+ launchDesktop: launchDesktopApplication,
32
+ now: Date.now,
33
+ wait: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
34
+ ...overrides
35
+ };
36
+ const selection = await runtime.selectInstance();
37
+ const current = healthyReviewInstance(selection);
38
+ if (current) {
39
+ if (input.focus) await runtime.focusDesktop(current);
40
+ return launchEvent("running", current.instanceId);
41
+ }
42
+ const running = selection.instances.filter((instance) => instance.healthy);
43
+ if (selection.key !== "stable" && selection.key !== "preview") throw new Error(`${reviewInstanceStartHint(selection)}; \`whiteboard app launch\` starts only installed apps.`);
44
+ if (selection.source === "fallback" && running.length > 1) throw reviewInstanceUnavailable(selection);
45
+ const launch = {
46
+ focus: input.focus,
47
+ noSandbox: input.noSandbox
48
+ };
49
+ if (selection.source !== "fallback") launch.instance = {
50
+ key: selection.key,
51
+ appPath: selection.instance?.discovery.appPath
52
+ };
53
+ const attempt = runtime.launchDesktop(launch);
54
+ let completion = observedCompletion(attempt);
55
+ completion.catch(() => void 0);
56
+ const deadline = runtime.now() + (input.timeoutMs ?? DEFAULT_LAUNCH_TIMEOUT_MS);
57
+ let unexpectedSuccessfulExitAt;
58
+ while (runtime.now() < deadline) {
59
+ const ready = healthyReviewInstance(await runtime.selectInstance());
60
+ if (ready) return launchEvent("launched", ready.instanceId);
61
+ if (unexpectedSuccessfulExitAt !== void 0 && runtime.now() - unexpectedSuccessfulExitAt >= EARLY_EXIT_GRACE_MS) throw launchFailure(attempt, /* @__PURE__ */ new Error("the launch process exited before Desktop became ready"));
62
+ const remaining = Math.max(0, deadline - runtime.now());
63
+ const outcome = completion ? await Promise.race([completion.then((result) => ({ completion: result })), runtime.wait(Math.min(POLL_INTERVAL_MS, remaining)).then(() => ({ completion: null }))]) : await runtime.wait(Math.min(POLL_INTERVAL_MS, remaining)).then(() => ({ completion: null }));
64
+ if (outcome.completion) {
65
+ assertSuccessfulLaunchCompletion(attempt, outcome.completion);
66
+ unexpectedSuccessfulExitAt = runtime.now();
67
+ completion = void 0;
68
+ }
69
+ }
70
+ if (unexpectedSuccessfulExitAt !== void 0) throw launchFailure(attempt, /* @__PURE__ */ new Error("the launch process exited before Desktop became ready"));
71
+ throw new Error(`Whiteboard Desktop did not become ready within ${Math.ceil((input.timeoutMs ?? DEFAULT_LAUNCH_TIMEOUT_MS) / 1e3)} seconds after ${attempt.method}. Open Whiteboard Desktop once, then run \`whiteboard app launch\` again.${launchDiagnostics(attempt.logPath)}`);
72
+ }
73
+ async function focusReviewDesktop(discovery, fetch) {
74
+ const response = await fetch(`${discovery.url}/app/focus`, {
75
+ method: "POST",
76
+ headers: { "x-review-token": discovery.token },
77
+ signal: AbortSignal.timeout(5e3)
78
+ });
79
+ const result = parseReviewVerbResponse(await response.json());
80
+ if (!response.ok || !result.ok) throw new Error(result.ok ? `Whiteboard Desktop focus returned ${response.status}.` : result.error);
81
+ }
82
+ function desktopApplicationInstalled(input = {}) {
83
+ const env = input.env ?? process.env;
84
+ const platform = input.platform ?? process.platform;
85
+ const exists = input.exists ?? existsSync;
86
+ const apps = Object.values(RELEASE_APPS);
87
+ if (platform !== "linux" && platform !== "darwin" || (input.electron ?? Boolean(process.versions.electron)) || env.DEV_FAST_REVIEW_DESKTOP_COMMAND?.trim()) return true;
88
+ if (platform === "linux") return apps.some(({ linuxLauncher }) => exists(linuxLauncher));
89
+ return apps.some(({ macApp }) => ["/Applications", path.join(homedir(), "Applications")].some((dir) => exists(path.join(dir, macApp)))) || apps.some(({ bundleId }) => (input.spotlight ?? spotlightFinds)(bundleId));
90
+ }
91
+ function spotlightFinds(bundleId) {
92
+ try {
93
+ return execFileSync("/usr/bin/mdfind", [`kMDItemCFBundleIdentifier == '${bundleId}'`], {
94
+ encoding: "utf8",
95
+ timeout: 2e3
96
+ }).trim() !== "";
97
+ } catch {
98
+ return false;
99
+ }
100
+ }
101
+ function launchDesktopApplication(input = {}) {
102
+ const platform = input.platform ?? process.platform;
103
+ if (platform !== "darwin" && platform !== "linux") return {
104
+ method: `the ${platform} application launcher`,
105
+ completion: Promise.reject(/* @__PURE__ */ new Error("automatic launch is available only on macOS and Linux"))
106
+ };
107
+ if (input.noSandbox && platform !== "linux") return {
108
+ method: `the ${platform} application launcher`,
109
+ completion: Promise.reject(/* @__PURE__ */ new Error("--no-sandbox is only supported for Linux Desktop launches."))
110
+ };
111
+ const electron = input.electron ?? Boolean(process.versions.electron);
112
+ const env = { ...input.env ?? process.env };
113
+ const execPath = input.execPath ?? process.execPath;
114
+ const focus = input.focus === true;
115
+ if (focus) delete env[REVIEW_DESKTOP_BACKGROUND_ENV];
116
+ else env[REVIEW_DESKTOP_BACKGROUND_ENV] = "1";
117
+ delete env.ELECTRON_RUN_AS_NODE;
118
+ delete env.DEV_FAST_REVIEW_CHECKOUT;
119
+ const release = RELEASE_APPS[input.instance?.key ?? "stable"];
120
+ const stateRoot = env.DEV_FAST_REVIEW_DESKTOP_STATE_ROOT?.trim();
121
+ const profileArgs = stateRoot ? [`--user-data-dir=${path.resolve(stateRoot, "user-data")}`, `--extensions-dir=${path.resolve(stateRoot, "extensions")}`] : [];
122
+ let command = "/usr/bin/open";
123
+ let method;
124
+ let args;
125
+ if (platform === "linux") {
126
+ delete env.VSCODE_DEV;
127
+ delete env.VSCODE_CLI;
128
+ command = (input.instance ? "" : env.DEV_FAST_REVIEW_DESKTOP_COMMAND?.trim()) || release.linuxLauncher;
129
+ method = `the installed Linux launcher at "${command}"`;
130
+ if (electron && !env.DEV_FAST_REVIEW_DESKTOP_COMMAND?.trim()) {
131
+ command = execPath;
132
+ method = `the Desktop-managed bundle at "${command}"`;
133
+ }
134
+ args = profileArgs;
135
+ if (input.noSandbox) args.push("--no-sandbox");
136
+ } else {
137
+ const appPath = input.instance ? input.instance.appPath : electron ? execPath.match(/^(.*?\.app)\/Contents\/MacOS\//)?.[1] : void 0;
138
+ const target = appPath?.endsWith(".app") ? ["-a", appPath] : ["-b", release.bundleId];
139
+ method = appPath?.endsWith(".app") ? `the macOS application at "${appPath}"` : `the macOS bundle identifier "${release.bundleId}"`;
140
+ args = [
141
+ "-n",
142
+ "-W",
143
+ ...focus ? [] : ["-g"],
144
+ ...target
145
+ ];
146
+ for (const [key, value] of Object.entries(env)) if (value !== void 0 && /^DEV_(REVIEW|FAST)_/.test(key)) args.push("--env", `${key}=${value}`);
147
+ if (profileArgs.length > 0) args.push("--args", ...profileArgs);
148
+ }
149
+ const { promise: completion, resolve, reject } = Promise.withResolvers();
150
+ let logPath;
151
+ let logFd;
152
+ try {
153
+ if (platform === "linux") {
154
+ logPath = path.join(mkdtempSync(path.join(tmpdir(), "whiteboard-launch-")), "stderr.log");
155
+ logFd = openSync(logPath, "wx", 384);
156
+ }
157
+ const child = (input.spawn ?? spawn)(command, args, {
158
+ detached: true,
159
+ env,
160
+ stdio: logFd === void 0 ? "ignore" : [
161
+ "ignore",
162
+ "ignore",
163
+ logFd
164
+ ]
165
+ });
166
+ child.once("error", (error) => reject(error));
167
+ child.once("exit", (code, signal) => {
168
+ resolve({
169
+ code,
170
+ signal
171
+ });
172
+ });
173
+ child.unref();
174
+ } catch (error) {
175
+ reject(error instanceof Error ? error : new Error(String(error)));
176
+ } finally {
177
+ if (logFd !== void 0) closeSync(logFd);
178
+ }
179
+ return {
180
+ method,
181
+ logPath,
182
+ completion
183
+ };
184
+ }
185
+ function launchEvent(state, instanceId) {
186
+ return {
187
+ event: "app",
188
+ action: "launch",
189
+ state,
190
+ instanceId
191
+ };
192
+ }
193
+ function launchFailure(attempt, error) {
194
+ return /* @__PURE__ */ new Error(`Could not launch Whiteboard with ${attempt.method}: ${error.message}. Open Whiteboard once, then run \`whiteboard app launch\` again. A sandboxed agent must run it outside the sandbox.${launchDiagnostics(attempt.logPath)}`);
195
+ }
196
+ function assertSuccessfulLaunchCompletion(attempt, completion) {
197
+ if (completion.code === 0 && !completion.signal) return;
198
+ throw launchFailure(attempt, /* @__PURE__ */ new Error(completion.signal ? `the launch process exited on ${completion.signal}` : `the launch process exited with code ${completion.code ?? "unknown"}`));
199
+ }
200
+ function observedCompletion(attempt) {
201
+ return attempt.completion.catch((error) => {
202
+ throw launchFailure(attempt, error instanceof Error ? error : new Error(String(error)));
203
+ });
204
+ }
205
+ function launchDiagnostics(logPath) {
206
+ if (!logPath) return "";
207
+ let tail = "";
208
+ try {
209
+ const fd = openSync(logPath, "r");
210
+ try {
211
+ const size = fstatSync(fd).size;
212
+ const buffer = Buffer.alloc(Math.min(size, 16384));
213
+ const read = readSync(fd, buffer, 0, buffer.length, size - buffer.length);
214
+ tail = buffer.subarray(0, read).toString("utf8").trim().split("\n").slice(-20).join("\n");
215
+ } finally {
216
+ closeSync(fd);
217
+ }
218
+ } catch {}
219
+ return `\nDesktop log: ${logPath}${tail ? `\n${tail}` : ""}`;
220
+ }
221
+ //#endregion
222
+ export { focusReviewDesktop as n, runReviewAppLaunch as r, desktopApplicationInstalled as t };