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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/THIRD_PARTY_NOTICES.md +29 -0
  2. package/dist/{account-alias-D0hF0sKk.js → account-alias-ChZR3RIU.js} +94 -8
  3. package/dist/{agent-cli-04xbDj23.js → agent-cli-CR2PnA9G.js} +9 -12
  4. package/dist/agent-cli-CRPPmS3l.js +2 -0
  5. package/dist/{agent-client-P3J4y8Zn.js → agent-client-DVd-prfp.js} +31 -14
  6. package/dist/{authoring-tools-fdA0RPIT.js → authoring-tools-BXKEwhfi.js} +5 -5
  7. package/dist/background-server-Cw7CcfaZ.js +156 -0
  8. package/dist/background-server-D2Rrr_e2.js +2 -0
  9. package/dist/build-info.json +1 -1
  10. package/dist/{cli-C0ziP6hg.js → cli-CSrz_wGk.js} +1 -1
  11. package/dist/{cli-runner-C3men22A.js → cli-runner-PlGqBeMw.js} +368 -252
  12. package/dist/cli.js +3 -2
  13. package/dist/{client-DzcSt6pV.js → client-DhZzPePs.js} +5 -3
  14. package/dist/{process-error-telemetry-BCBYp2t4.js → headless-host-CVXTSIlx.js} +3285 -2585
  15. package/dist/headless-host-Cp37C-v0.js +2 -0
  16. package/dist/input-error-OLgB_h31.js +11 -0
  17. package/dist/{local-data-Dy1pNtLF.js → local-data-yfLp5PiJ.js} +113 -62
  18. package/dist/{mcp-B8kuRSXM.js → mcp-DBbDwG4y.js} +4 -4
  19. package/dist/{package-paths-B9PDoAl6.js → package-paths-BsuI4WUf.js} +1 -1
  20. package/dist/profile-BSOkA6A-.js +2 -0
  21. package/dist/{profile-tbGWuWHu.js → profile-pAzg7fxC.js} +3 -3
  22. package/dist/remote-attach-BAt2eZx3.js +137 -0
  23. package/dist/remote-extensions-2Cxf3hMo.js +2 -0
  24. package/dist/remote-extensions-BiNx6NQK.js +323 -0
  25. package/dist/remote-language-server-DQgWUNeV.js +252 -0
  26. package/dist/remote-uninstall-DLtDG0o7.js +153 -0
  27. package/dist/{request-origin-DA4K1qWY.js → request-origin-DaRIV3cB.js} +5 -2
  28. package/dist/review-app-launcher-D7I4DGoH.js +222 -0
  29. package/dist/{review-home-paths-Y9mYWkCI.js → review-home-paths-zwevIq4I.js} +7 -7
  30. package/dist/{review-telemetry-CmM6tCbj.js → review-telemetry-BqB4CiTD.js} +5 -5
  31. package/dist/{s3-SzLpeCG6.js → s3-77-qkEhs.js} +1 -1
  32. package/dist/{s3-config-BgxkSoOy.js → s3-config-0xuCh-F_.js} +1 -1
  33. package/dist/{s3-config-CAxhO9u_.js → s3-config-DDWWMh5x.js} +5 -1
  34. package/dist/server/desktop-host.js +1663 -266
  35. package/dist/{desktop-discovery-Ba3dD5Cn.js → server-discovery-CifQ3yQS.js} +88 -3
  36. package/dist/sharing/index.d.ts +88 -32
  37. package/dist/sharing/index.js +1 -1
  38. package/dist/{src-DiHP11l3.js → src-BMPuxWv4.js} +6 -11
  39. package/dist/{src-WCbabvdS.js → src-Bgvf1sEN.js} +128 -8
  40. package/dist/{stored-document-migration-u4L_1VZa.js → stored-document-migration-CFOvjRwW.js} +5 -18
  41. package/dist/{tool-failure-CJHBCHMh.js → tool-failure-BXWzQG8T.js} +2 -2
  42. package/instructions/authoring.md +16 -12
  43. package/instructions/file-lenses.md +4 -2
  44. package/package.json +9 -14
  45. package/src/agent-connect.ts +507 -0
  46. package/src/agent-selection.ts +4 -0
  47. package/src/ask/agents.ts +59 -13
  48. package/src/ask/pi-mcp.ts +7 -3
  49. package/src/ask/thread-state.ts +9 -4
  50. package/src/ask/thread.ts +88 -50
  51. package/src/ask/threads.ts +53 -7
  52. package/src/cli-install.ts +3 -1
  53. package/src/cli-runner.ts +472 -81
  54. package/src/cli.ts +1 -1
  55. package/src/connect-agents.ts +285 -0
  56. package/src/connect-prompts.ts +60 -6
  57. package/src/desktop-discovery.ts +24 -0
  58. package/src/pinned-download.ts +118 -0
  59. package/src/remote-attach.ts +130 -0
  60. package/src/remote-extensions.ts +359 -0
  61. package/src/remote-language-server.ts +428 -0
  62. package/src/remote-toolchains.ts +107 -0
  63. package/src/remote-uninstall.ts +280 -0
  64. package/src/review-api/README.md +1 -3
  65. package/src/review-api/agent-cli.ts +4 -4
  66. package/src/review-api/agent-client.ts +23 -0
  67. package/src/review-api/ask-history.ts +31 -16
  68. package/src/review-api/authoring-tools.ts +1 -1
  69. package/src/review-api/blocks/index.ts +2 -4
  70. package/src/review-api/document.ts +1 -4
  71. package/src/review-api/http.ts +265 -237
  72. package/src/review-api/local-data.ts +18 -3
  73. package/src/review-api/onboarding.ts +478 -0
  74. package/src/review-api/origin.ts +7 -0
  75. package/src/review-api/recent-folders.ts +173 -0
  76. package/src/review-api/store.ts +56 -10
  77. package/src/review-api/workspaces.ts +56 -11
  78. package/src/review-app-launcher.ts +61 -3
  79. package/src/review-stack.ts +162 -36
  80. package/src/server/background-server.ts +250 -0
  81. package/src/server/desktop-host-shutdown.ts +24 -0
  82. package/src/server/desktop-host.ts +39 -24
  83. package/src/server/desktop-server.ts +68 -118
  84. package/src/server/global-verb-relay.ts +16 -0
  85. package/src/server/headless-host.ts +128 -3
  86. package/src/server/hono-http.ts +6 -1
  87. package/src/server/legacy-tutorial-cleanup.ts +38 -0
  88. package/src/server/review-gateway-hosts.ts +379 -0
  89. package/src/server/review-gateway-list.ts +109 -0
  90. package/src/server/review-gateway-memory.ts +130 -0
  91. package/src/server/review-gateway-pushes.ts +142 -0
  92. package/src/server/review-gateway-streams.ts +415 -0
  93. package/src/server/review-gateway-transport.ts +204 -0
  94. package/src/server/review-gateway.ts +433 -0
  95. package/src/server/review-server-core.ts +36 -6
  96. package/src/server-discovery.ts +11 -0
  97. package/src/sharing/import.ts +1 -0
  98. package/src/ui-telemetry-events.ts +5 -1
  99. package/src/vsix.ts +122 -0
  100. package/dist/agent-cli-BR_Jw5BL.js +0 -2
  101. package/dist/headless-host-BLLJ7Dyu.js +0 -95
  102. package/dist/profile-aMSw1oCM.js +0 -2
  103. package/dist/server-discovery-ZVi8Ay8J.js +0 -66
  104. package/src/fixtures/blocks/tutorial.json +0 -14
  105. package/src/review-api/blocks/tutorial.ts +0 -23
  106. package/src/server/tutorial-service.ts +0 -217
  107. package/tutorial/document.json +0 -216
  108. package/tutorial/lenses.json +0 -10
  109. package/tutorial/runtime-manifest.json +0 -11
  110. package/tutorial/sample-service/package.json +0 -9
  111. package/tutorial/sample-service/src/api/checkout-api.ts +0 -17
  112. package/tutorial/sample-service/src/app.ts +0 -26
  113. package/tutorial/sample-service/src/database/drizzle-orm.d.ts +0 -16
  114. package/tutorial/sample-service/src/database/schema.ts +0 -9
  115. package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +0 -15
  116. package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +0 -24
  117. package/tutorial/sample-service/src/inventory/forklift-mood.ts +0 -4
  118. package/tutorial/sample-service/src/inventory/inventory-service.ts +0 -28
  119. package/tutorial/sample-service/src/inventory/warehouse-lore.ts +0 -11
  120. package/tutorial/sample-service/src/orders/order-service.ts +0 -35
  121. package/tutorial/sample-service/src/orders/order.ts +0 -43
  122. package/tutorial/sample-service/src/orders/orders-repository.ts +0 -24
  123. package/tutorial/sample-service/src/payments/fraud-vibes.ts +0 -4
  124. package/tutorial/sample-service/src/payments/payment-gateway.ts +0 -29
  125. package/tutorial/sample-service/src/payments/raccoon-ledger.ts +0 -9
  126. package/tutorial/sample-service/src/shipping/shipping-gateway.ts +0 -15
  127. package/tutorial/sample-service/tsconfig.json +0 -12
@@ -15,6 +15,8 @@ const discoverySchema = z.object({
15
15
  }),
16
16
  serverPid: z.number().int().positive(),
17
17
  token: z.string().min(1),
18
+ // Older servers wrote no startedBy; only a person ran those.
19
+ startedBy: z.enum(["user", "cli", "desktop"]).default("user"),
18
20
  });
19
21
 
20
22
  export type ReviewServerDiscovery = z.infer<typeof discoverySchema>;
@@ -29,6 +31,9 @@ export function reviewServerDiscoveryPath(stateDir: string) {
29
31
  return path.join(stateDir, "review-server", "server.json");
30
32
  }
31
33
 
34
+ /** Sent by a Desktop taking over the server's state directory. */
35
+ export const DESKTOP_TAKEOVER_SIGNAL = "SIGUSR2";
36
+
32
37
  export function headlessServerLockPath(stateDir: string) {
33
38
  return path.join(stateDir, "headless-server.lock");
34
39
  }
@@ -76,7 +81,9 @@ export async function readReviewServerHealth(
76
81
  ok: z.literal(true),
77
82
  instanceId: z.string(),
78
83
  serverId: z.string().optional(),
84
+ serverPid: z.number().optional(),
79
85
  version: z.string().optional(),
86
+ commit: z.string().nullable().optional(),
80
87
  })
81
88
  .safeParse(await response.json());
82
89
 
@@ -88,6 +95,10 @@ export async function readReviewServerHealth(
88
95
  }
89
96
  }
90
97
 
98
+ export function desktopOwnsHome(stateDir: string) {
99
+ return `Whiteboard Desktop is serving ${stateDir}, so no other Whiteboard server can start there. Quit Desktop, or choose another --state-dir.`;
100
+ }
101
+
91
102
  export function serverNotReady(stateDir: string) {
92
103
  return new Error(
93
104
  `Whiteboard server is not ready in ${stateDir}. Run whiteboard server start --state-dir ${JSON.stringify(stateDir)}, then retry.`,
@@ -79,6 +79,7 @@ export const sharedSnapshotSchema = z.strictObject({
79
79
  baseRef: z.string().optional(),
80
80
  pullRequestNumber: z.number().int().positive().optional(),
81
81
  pullRequestUrl: z.string().optional(),
82
+ pullRequestTitle: z.string().optional(),
82
83
  revision: z.string().optional(),
83
84
  })
84
85
  .optional(),
@@ -19,6 +19,7 @@
19
19
  import {
20
20
  type JsonObject,
21
21
  REVIEW_DIFF_LAYOUTS,
22
+ REVIEW_DIFF_LAYOUT_SCOPES,
22
23
  REVIEW_THEME_CHOICES,
23
24
  isJsonObject,
24
25
  jsonBoolean,
@@ -369,7 +370,10 @@ export const UI_TELEMETRY_EVENTS = {
369
370
  },
370
371
  diff_layout_changed: {
371
372
  event: "review_diff_layout_changed",
372
- properties: { layout: REVIEW_DIFF_LAYOUTS },
373
+ properties: {
374
+ layout: REVIEW_DIFF_LAYOUTS,
375
+ scope: REVIEW_DIFF_LAYOUT_SCOPES,
376
+ },
373
377
  },
374
378
  // Dismissal replaced the old reject decision: the reader is finished with the
375
379
  // review, whether or not they liked the change.
package/src/vsix.ts ADDED
@@ -0,0 +1,122 @@
1
+ // Plain Node and yauzl only: apps/review-desktop/scripts imports this file directly.
2
+ import { createWriteStream } from "node:fs";
3
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import type { Readable } from "node:stream";
6
+ import { pipeline } from "node:stream/promises";
7
+
8
+ import yauzl from "yauzl";
9
+ import { z } from "zod";
10
+
11
+ const manifestSchema = z.looseObject({
12
+ publisher: z.string(),
13
+ name: z.string(),
14
+ engines: z.looseObject({ vscode: z.string().optional() }).optional(),
15
+ });
16
+
17
+ /** Unpacks a VSIX's `extension/` payload into `destination`. */
18
+ export async function extractVsix(
19
+ vsix: string,
20
+ destination: string,
21
+ maxBytes = Number.POSITIVE_INFINITY,
22
+ ) {
23
+ let unpacked = 0;
24
+
25
+ const zip = await new Promise<yauzl.ZipFile>((resolve, reject) =>
26
+ yauzl.open(vsix, { lazyEntries: true }, (error, opened) =>
27
+ error ? reject(error) : resolve(opened),
28
+ ),
29
+ );
30
+
31
+ const openReadStream = (entry: yauzl.Entry) =>
32
+ new Promise<Readable>((resolve, reject) =>
33
+ zip.openReadStream(entry, (error, stream) =>
34
+ error ? reject(error) : resolve(stream),
35
+ ),
36
+ );
37
+
38
+ try {
39
+ await new Promise<void>((resolve, reject) => {
40
+ zip.on("error", reject);
41
+ zip.on("end", () => resolve());
42
+ zip.on("entry", (entry: yauzl.Entry) => {
43
+ void (async () => {
44
+ const name = entry.fileName.replaceAll("\\", "/");
45
+
46
+ if (name.endsWith("/") || !name.startsWith("extension/")) return;
47
+
48
+ const relative = name.slice("extension/".length);
49
+ const parts = relative.split("/");
50
+ const mode = (entry.externalFileAttributes >>> 16) & 0o177777;
51
+
52
+ if (
53
+ parts.some(
54
+ (part) =>
55
+ part === ".." ||
56
+ part === "." ||
57
+ part === "" ||
58
+ part.includes(":"),
59
+ ) ||
60
+ path.posix.isAbsolute(relative)
61
+ )
62
+ throw new Error(`VSIX contains an unsafe path: ${name}`);
63
+
64
+ if ((mode & 0o170000) === 0o120000)
65
+ throw new Error(`VSIX contains a symlink: ${name}`);
66
+
67
+ unpacked += entry.uncompressedSize;
68
+
69
+ if (unpacked > maxBytes)
70
+ throw new Error(`VSIX unpacks to more than ${maxBytes} bytes`);
71
+
72
+ const output = path.join(destination, ...parts);
73
+ await mkdir(path.dirname(output), { recursive: true });
74
+ await pipeline(
75
+ await openReadStream(entry),
76
+ createWriteStream(output),
77
+ );
78
+
79
+ if (process.platform !== "win32" && mode & 0o111)
80
+ await chmod(output, mode & 0o777);
81
+ })().then(() => zip.readEntry(), reject);
82
+ });
83
+ zip.readEntry();
84
+ });
85
+ } finally {
86
+ zip.close();
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Drops the `scripts` and `dependencies` a payload ships prebundled, and
92
+ * `extensionPack` when Review does not ship the pack's members.
93
+ */
94
+ export async function sanitizeVsixManifest(
95
+ directory: string,
96
+ extension: { id: string; stripExtensionPack: boolean },
97
+ ) {
98
+ const file = path.join(directory, "package.json");
99
+
100
+ const manifest = manifestSchema.parse(
101
+ JSON.parse(await readFile(file, "utf8")),
102
+ );
103
+
104
+ const declared = `${manifest.publisher}.${manifest.name}`;
105
+
106
+ if (declared.toLowerCase() !== extension.id.toLowerCase())
107
+ throw new Error(`${extension.id}: the VSIX declares ${declared}`);
108
+
109
+ const { scripts, dependencies, devDependencies, extensionPack, ...kept } =
110
+ manifest;
111
+
112
+ await writeFile(
113
+ file,
114
+ `${JSON.stringify(
115
+ extension.stripExtensionPack ? kept : { ...kept, extensionPack },
116
+ undefined,
117
+ 2,
118
+ )}\n`,
119
+ );
120
+
121
+ return manifest.engines?.vscode;
122
+ }
@@ -1,2 +0,0 @@
1
- import { n as runReviewAgentCli } from "./agent-cli-04xbDj23.js";
2
- export { runReviewAgentCli };
@@ -1,95 +0,0 @@
1
- import { p as isObjectValue } from "./src-X9phtB2j.js";
2
- import "./review-home-paths-Y9mYWkCI.js";
3
- import { O as withFileLock, P as writePrivateJsonAtomic } from "./s3-config-CAxhO9u_.js";
4
- import { t as openReviewProfile } from "./profile-tbGWuWHu.js";
5
- import { i as createWhiteboardCore, l as createNodeRequestListener, m as migrateDiffrConfig, n as installProcessErrorTelemetry, p as GlobalReviewDesktopVerbRelay, t as drainServerCrashReport } from "./process-error-telemetry-BCBYp2t4.js";
6
- import { i as reviewServerDiscoveryPath, t as headlessServerLockPath } from "./server-discovery-ZVi8Ay8J.js";
7
- import { randomBytes, randomUUID } from "node:crypto";
8
- import { mkdir, realpath, rm } from "node:fs/promises";
9
- import { createServer } from "node:http";
10
- import { once } from "node:events";
11
- //#region src/server/headless-host.ts
12
- /** One foreground headless endpoint per profile; Desktop shares its database. */
13
- async function runHeadlessServer(input) {
14
- await mkdir(input.stateDir, {
15
- recursive: true,
16
- mode: 448
17
- });
18
- const stateDir = await realpath(input.stateDir);
19
- const stopErrorTelemetry = input.telemetry && installProcessErrorTelemetry(input.telemetry);
20
- if (!(await withHeadlessServerLock(stateDir, () => serve({
21
- ...input,
22
- stateDir
23
- })).finally(() => stopErrorTelemetry?.())).acquired) throw new Error(`A Whiteboard server already owns ${stateDir}. Stop it first, or choose another --state-dir.`);
24
- }
25
- /** Held by a running server, so also by anything that must not run beside one. */
26
- function withHeadlessServerLock(stateDir, operation) {
27
- return withFileLock(headlessServerLockPath(stateDir), {
28
- timeoutMs: 0,
29
- retryMs: 20,
30
- staleMs: Infinity,
31
- unownedGraceMs: 1e3,
32
- identifyOwner: true
33
- }, operation);
34
- }
35
- async function serve(input) {
36
- if (input.signal.aborted) return;
37
- await migrateDiffrConfig(input.signal);
38
- if (input.telemetry) await drainServerCrashReport(input.telemetry);
39
- const local = await openReviewProfile(input.stateDir, { manageWorkspaces: false });
40
- const discovery = {
41
- version: 1,
42
- instanceId: randomUUID(),
43
- url: "http://127.0.0.1:0",
44
- serverPid: process.pid,
45
- token: randomBytes(32).toString("base64url")
46
- };
47
- const relay = new GlobalReviewDesktopVerbRelay();
48
- const { app, api } = createWhiteboardCore({
49
- profile: local,
50
- relay,
51
- token: discovery.token,
52
- instanceId: discovery.instanceId,
53
- softwareMapEnabled: input.softwareMapEnabled,
54
- scratchpad: () => false,
55
- status: () => ({
56
- key: "headless",
57
- home: input.stateDir
58
- })
59
- });
60
- app.route("/reviews-api", api);
61
- const server = createServer(createNodeRequestListener(app));
62
- let published = false;
63
- try {
64
- const listening = once(server, "listening");
65
- server.listen(input.port ?? 0, "127.0.0.1");
66
- await listening;
67
- const address = server.address();
68
- if (!isObjectValue(address)) throw new Error("Whiteboard server did not bind a TCP port.");
69
- discovery.url = `http://127.0.0.1:${address.port}`;
70
- await writePrivateJsonAtomic(reviewServerDiscoveryPath(input.stateDir), discovery);
71
- published = true;
72
- input.onReady(discovery);
73
- await new Promise((resolve) => {
74
- if (input.signal.aborted) resolve();
75
- else input.signal.addEventListener("abort", () => resolve(), { once: true });
76
- });
77
- } finally {
78
- const forceClose = setTimeout(() => server.closeAllConnections(), 5e3);
79
- forceClose.unref();
80
- try {
81
- if (published) await rm(reviewServerDiscoveryPath(input.stateDir), { force: true });
82
- } finally {
83
- relay.close();
84
- await new Promise((resolve) => server.close(() => resolve()));
85
- clearTimeout(forceClose);
86
- try {
87
- await local.data.close();
88
- } finally {
89
- await local.store.close();
90
- }
91
- }
92
- }
93
- }
94
- //#endregion
95
- export { runHeadlessServer, withHeadlessServerLock };
@@ -1,2 +0,0 @@
1
- import { t as openReviewProfile } from "./profile-tbGWuWHu.js";
2
- export { openReviewProfile };
@@ -1,66 +0,0 @@
1
- import "./review-home-paths-Y9mYWkCI.js";
2
- import { A as devReviewHome } from "./s3-config-CAxhO9u_.js";
3
- import { z } from "zod";
4
- import { readFile } from "node:fs/promises";
5
- import path from "node:path";
6
- //#region src/server-discovery.ts
7
- const discoverySchema = z.object({
8
- version: z.literal(1),
9
- instanceId: z.uuid(),
10
- url: z.url().refine((value) => {
11
- const url = new URL(value);
12
- return url.protocol === "http:" && url.hostname === "127.0.0.1";
13
- }),
14
- serverPid: z.number().int().positive(),
15
- token: z.string().min(1)
16
- });
17
- function reviewServerStateDir(env = process.env) {
18
- return env.DEV_REVIEW_SERVER_DIR?.trim() ? path.resolve(env.DEV_REVIEW_SERVER_DIR.trim()) : devReviewHome(env);
19
- }
20
- function reviewServerDiscoveryPath(stateDir) {
21
- return path.join(stateDir, "review-server", "server.json");
22
- }
23
- function headlessServerLockPath(stateDir) {
24
- return path.join(stateDir, "headless-server.lock");
25
- }
26
- async function readReviewServerDiscovery(stateDir) {
27
- let source;
28
- try {
29
- source = await readFile(reviewServerDiscoveryPath(stateDir), "utf8");
30
- } catch (error) {
31
- if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
32
- throw error;
33
- }
34
- try {
35
- return discoverySchema.parse(JSON.parse(source));
36
- } catch {
37
- throw new Error(`Whiteboard server discovery in ${stateDir} is invalid or incompatible. Restart it with this version of whiteboard server start.`);
38
- }
39
- }
40
- async function reviewServerIsHealthy(discovery) {
41
- return await readReviewServerHealth(discovery) !== null;
42
- }
43
- /** The recorded server's /health, or null when another or none answers. */
44
- async function readReviewServerHealth(discovery) {
45
- try {
46
- const response = await fetch(`${discovery.url}/health`, {
47
- headers: { "x-review-token": discovery.token },
48
- signal: AbortSignal.timeout(1500)
49
- });
50
- if (!response.ok) return null;
51
- const health = z.object({
52
- ok: z.literal(true),
53
- instanceId: z.string(),
54
- serverId: z.string().optional(),
55
- version: z.string().optional()
56
- }).safeParse(await response.json());
57
- return health.success && health.data.instanceId === discovery.instanceId ? health.data : null;
58
- } catch {
59
- return null;
60
- }
61
- }
62
- function serverNotReady(stateDir) {
63
- return /* @__PURE__ */ new Error(`Whiteboard server is not ready in ${stateDir}. Run whiteboard server start --state-dir ${JSON.stringify(stateDir)}, then retry.`);
64
- }
65
- //#endregion
66
- export { reviewServerIsHealthy as a, reviewServerDiscoveryPath as i, readReviewServerDiscovery as n, reviewServerStateDir as o, readReviewServerHealth as r, serverNotReady as s, headlessServerLockPath as t };
@@ -1,14 +0,0 @@
1
- [
2
- {
3
- "type": "tutorial",
4
- "kind": "keymap",
5
- "id": "block-1"
6
- },
7
- {
8
- "type": "tutorial",
9
- "kind": "view",
10
- "view": "diff",
11
- "label": "Open the diff",
12
- "id": "block-2"
13
- }
14
- ]
@@ -1,23 +0,0 @@
1
- import { z } from "zod";
2
-
3
- import { type BlockDefinition, defineBlock } from "./definition.js";
4
-
5
- type TutorialContent =
6
- | { kind: "keymap" }
7
- | { kind: "view"; view: "diff"; label: string };
8
-
9
- export type TutorialBlock = { id?: string; type: "tutorial" } & TutorialContent;
10
-
11
- export const tutorialSchema = z.discriminatedUnion("kind", [
12
- defineBlock("tutorial", { kind: z.literal("keymap") }),
13
- defineBlock("tutorial", {
14
- kind: z.literal("view"),
15
- view: z.literal("diff"),
16
- label: z.string(),
17
- }),
18
- ]);
19
-
20
- export const tutorial: BlockDefinition<TutorialBlock> = {
21
- type: "tutorial",
22
- schema: tutorialSchema,
23
- };
@@ -1,217 +0,0 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import { cp, mkdir, readFile, rename, rm } from "node:fs/promises";
3
- import path from "node:path";
4
-
5
- import { resolveRevision } from "@dev.fast/local-vcs";
6
- import { writePrivateJsonAtomic } from "@dev.fast/trace-core";
7
- import { lensSchema } from "@review/review-api/diff-lenses";
8
- import { documentSchema } from "@review/review-api/document";
9
- import type { LocalReviewData } from "@review/review-api/local-data";
10
- import type { ReviewStore, Snapshot } from "@review/review-api/store";
11
- import { devReviewHome } from "@review/review-home-paths";
12
- import { z } from "zod";
13
-
14
- const authoredSchema = z.strictObject({
15
- title: z.string().min(1),
16
- document: documentSchema,
17
- });
18
-
19
- const lensesSchema = z.array(lensSchema.omit({ id: true }));
20
-
21
- const pinsSchema = z.strictObject({
22
- base: z.string().regex(/^[a-f0-9]{40}$/),
23
- head: z.string().regex(/^[a-f0-9]{40}$/),
24
- });
25
-
26
- const stampSchema = z.object({
27
- version: z.literal(10),
28
- reviewUuid: z.string(),
29
- contentHash: z.string(),
30
- });
31
-
32
- export async function readTutorialAssets(assetsRoot: string) {
33
- const names = ["document.json", "lenses.json", "pins.json"];
34
-
35
- const bytes = await Promise.all(
36
- names.map((name) => readFile(path.join(assetsRoot, name), "utf8")),
37
- );
38
-
39
- return {
40
- authored: authoredSchema.parse(JSON.parse(bytes[0]!)),
41
- lenses: lensesSchema.parse(JSON.parse(bytes[1]!)),
42
- pins: pinsSchema.parse(JSON.parse(bytes[2]!)),
43
- contentHash: createHash("sha256")
44
- .update(JSON.stringify(bytes))
45
- .digest("hex"),
46
- };
47
- }
48
-
49
- /** Create the shipped document through the same strict validation as authored reviews. */
50
- export async function createNativeTutorial(input: {
51
- assetsRoot: string;
52
- sampleRoot: string;
53
- store: ReviewStore;
54
- data: LocalReviewData;
55
- }) {
56
- const assets = await readTutorialAssets(input.assetsRoot);
57
- const repository = await input.data.register(input.sampleRoot);
58
-
59
- const pins = await input.data.resolvePins(
60
- repository.id,
61
- assets.pins.base,
62
- assets.pins.head,
63
- );
64
-
65
- const result = await input.store.execute(
66
- {
67
- operation: {
68
- type: "create",
69
- title: assets.authored.title,
70
- target: { kind: "commits", ...pins },
71
- },
72
- },
73
- { document: assets.authored.document, origin: { tutorial: true } },
74
- );
75
-
76
- for (const { title, targets } of assets.lenses)
77
- await input.store.execute({
78
- operation: {
79
- type: "lens_edit",
80
- reviewId: result.reviewId,
81
- edit: { type: "insert", title, targets },
82
- },
83
- });
84
-
85
- return {
86
- snapshot: input.store.read(result.reviewId),
87
- contentHash: assets.contentHash,
88
- };
89
- }
90
-
91
- export function createTutorialService(input: {
92
- packageRoot: string;
93
- store: ReviewStore;
94
- data: LocalReviewData;
95
- }) {
96
- const tutorialRoot = path.join(devReviewHome(), "tutorial");
97
- const sampleRoot = path.join(tutorialRoot, "sample-service");
98
- const stampPath = path.join(tutorialRoot, "stamp.json");
99
- const assetsRoot = path.join(input.packageRoot, "tutorial");
100
-
101
- const readStamp = async () =>
102
- stampSchema.safeParse(
103
- await readFile(stampPath, "utf8")
104
- .then((value) => JSON.parse(value))
105
- .catch(() => null),
106
- ).data;
107
-
108
- async function find(): Promise<Snapshot | null> {
109
- try {
110
- const stamp = await readStamp();
111
-
112
- if (!stamp || !input.store.has(stamp.reviewUuid)) return null;
113
- const snapshot = input.store.read(stamp.reviewUuid);
114
- const assets = await readTutorialAssets(assetsRoot);
115
-
116
- const [head, base] = await Promise.all([
117
- resolveRevision(sampleRoot, "HEAD"),
118
- resolveRevision(sampleRoot, "HEAD^"),
119
- ]);
120
-
121
- return snapshot.origin?.tutorial &&
122
- stamp.contentHash === assets.contentHash &&
123
- head &&
124
- base &&
125
- snapshot.pins?.head === head.commit &&
126
- snapshot.pins?.base === base.commit &&
127
- head.commit === assets.pins.head &&
128
- base.commit === assets.pins.base
129
- ? snapshot
130
- : null;
131
- } catch {
132
- return null;
133
- }
134
- }
135
-
136
- async function cleanup() {
137
- for (const reviewId of input.store.tutorialIds()) {
138
- if (!input.store.read(reviewId).trashedAt)
139
- await input.store.execute({
140
- operation: { type: "lifecycle", state: "trash", reviewId },
141
- });
142
- await input.store.execute({
143
- operation: { type: "delete", reviewId },
144
- });
145
- }
146
-
147
- // Windows cannot delete a checkout while its Git reader or watchers
148
- // hold it open, and handles can outlive their close briefly.
149
- for (const repository of input.store.repositories()) {
150
- const relative = path.relative(tutorialRoot, repository.path);
151
-
152
- if (relative && !relative.startsWith("..") && !path.isAbsolute(relative))
153
- await input.data.forgetRepository(repository.id);
154
- }
155
-
156
- await rm(tutorialRoot, { recursive: true, force: true, maxRetries: 10 });
157
- }
158
-
159
- return {
160
- async status() {
161
- return {
162
- version: 1 as const,
163
- reviewUuid: (await find())?.reviewId ?? null,
164
- };
165
- },
166
- async referencesReview(id: string) {
167
- return (
168
- input.store.has(id) && input.store.read(id).origin?.tutorial === true
169
- );
170
- },
171
- async prepare(options?: { beforeReset(): Promise<void> }) {
172
- const current = await find();
173
-
174
- if (current) return current;
175
-
176
- await readTutorialAssets(assetsRoot);
177
- await options?.beforeReset();
178
- await cleanup();
179
- await mkdir(tutorialRoot, { recursive: true, mode: 0o700 });
180
-
181
- const temporaryRoot = path.join(
182
- tutorialRoot,
183
- `.sample-service-${randomUUID()}`,
184
- );
185
-
186
- try {
187
- await cp(path.join(assetsRoot, "sample-service"), temporaryRoot, {
188
- recursive: true,
189
- });
190
- await cp(
191
- path.join(assetsRoot, "git-stub"),
192
- path.join(temporaryRoot, ".git"),
193
- { recursive: true },
194
- );
195
- await rename(temporaryRoot, sampleRoot);
196
-
197
- const { snapshot, contentHash } = await createNativeTutorial({
198
- assetsRoot,
199
- sampleRoot,
200
- store: input.store,
201
- data: input.data,
202
- });
203
-
204
- await writePrivateJsonAtomic(stampPath, {
205
- version: 10,
206
- reviewUuid: snapshot.reviewId,
207
- contentHash,
208
- });
209
-
210
- return snapshot;
211
- } finally {
212
- await rm(temporaryRoot, { recursive: true, force: true });
213
- }
214
- },
215
- cleanup,
216
- };
217
- }