@dev.fast/whiteboard 0.4.1-preview.20261031.144 → 0.5.0

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 (42) hide show
  1. package/dist/{account-alias-DbtsNr_c.js → account-alias-Bw3PT3Qo.js} +3 -3
  2. package/dist/agent-cli-B8KBHuh_.js +2 -0
  3. package/dist/{agent-cli-DPt9w5-8.js → agent-cli-kHRrwO4n.js} +6 -6
  4. package/dist/{agent-client-CXXjD60Q.js → agent-client-CMacGbMp.js} +3 -3
  5. package/dist/{authoring-tools-BEGW2s0A.js → authoring-tools-CiLy46kh.js} +1 -1
  6. package/dist/{background-server-Cw7lddwb.js → background-server-BLNHbksB.js} +1 -1
  7. package/dist/{background-server-D_4Xt3fF.js → background-server-eZWXoVcI.js} +2 -2
  8. package/dist/build-info.json +1 -1
  9. package/dist/{cli-SDWXOZPY.js → cli-Bxmiqutp.js} +35 -2
  10. package/dist/{cli-runner-aO7Bw1Bn.js → cli-runner-DQPuu7kJ.js} +26 -17
  11. package/dist/cli.js +2 -2
  12. package/dist/{fuzzy-match-DRhI0DDn.js → fuzzy-match-DZjeOaPY.js} +2 -2
  13. package/dist/{headless-host-CgxnmRir.js → headless-host-B4r0s1ks.js} +1 -1
  14. package/dist/{headless-host-s4A3qcsl.js → headless-host-Dl2KZ9IT.js} +88 -26
  15. package/dist/{import-Dnbujw7d.js → import-YAIU6rB0.js} +3 -3
  16. package/dist/{local-data-G2I-77FX.js → local-data-C58vzLZR.js} +3 -3
  17. package/dist/{mcp-tqD6Fwvy.js → mcp-COYWiGVn.js} +3 -3
  18. package/dist/{profile-i2n1HdIg.js → profile--BYIjPkc.js} +1 -1
  19. package/dist/profile-BjcXi-oH.js +2 -0
  20. package/dist/{remote-attach-BsOO_ER4.js → remote-attach-C1prLjmj.js} +3 -3
  21. package/dist/{remote-language-server-AamAHhw_.js → remote-language-server-z3rdHcLL.js} +1 -1
  22. package/dist/{remote-uninstall-BH2ovwP1.js → remote-uninstall-B3Slwor1.js} +3 -3
  23. package/dist/{request-origin-Cr1tW4Vl.js → request-origin-B3_XI2VA.js} +7 -2
  24. package/dist/{review-app-launcher-1tcH84u6.js → review-app-launcher-BZEbDKuK.js} +2 -2
  25. package/dist/{review-telemetry-CzYSJMMy.js → review-telemetry-x8u-9EZb.js} +2 -2
  26. package/dist/server/desktop-host.js +10 -10
  27. package/dist/{server-discovery-nLX2SH_v.js → server-discovery-WmuFl2S3.js} +1 -1
  28. package/dist/sharing/index.d.ts +2 -0
  29. package/dist/sharing/index.js +1 -1
  30. package/dist/{src-Coec7jLU.js → src-DLWol7GZ.js} +7 -0
  31. package/dist/{stored-document-migration-Ca9BWqEF.js → stored-document-migration-iwIQ4l5G.js} +1 -1
  32. package/dist/{thread-state-DosGpp3S.js → thread-state-B7L1mpN_.js} +1 -1
  33. package/dist/{tool-failure-CwTgI76j.js → tool-failure-RLR8m9hq.js} +3 -3
  34. package/package.json +9 -9
  35. package/src/cli-runner.ts +29 -0
  36. package/src/sharing/cli.ts +59 -0
  37. package/src/sharing/file.ts +52 -0
  38. package/src/sharing/host.ts +131 -37
  39. package/src/sharing/import.ts +1 -1
  40. package/src/ui-telemetry-events.ts +5 -0
  41. package/dist/agent-cli-BL1HiH5g.js +0 -2
  42. package/dist/profile-Bw0hDDNd.js +0 -2
@@ -1,7 +1,7 @@
1
1
  import { a as jsonNumber, c as jsonString, i as jsonBoolean, n as isJsonObject } from "./src-X9phtB2j.js";
2
2
  import { X as SELECTION_UNAVAILABLE_REASONS } from "./src-B2b8cCoI.js";
3
- import { _ as REVIEW_DIFF_LAYOUTS, g as REVIEW_COLOR_THEME_KINDS, v as REVIEW_DIFF_LAYOUT_SCOPES } from "./src-Coec7jLU.js";
4
- import { n as askAgentIds } from "./thread-state-DosGpp3S.js";
3
+ import { _ as REVIEW_DIFF_LAYOUTS, g as REVIEW_COLOR_THEME_KINDS, v as REVIEW_DIFF_LAYOUT_SCOPES } from "./src-DLWol7GZ.js";
4
+ import { n as askAgentIds } from "./thread-state-B7L1mpN_.js";
5
5
  import { z } from "zod";
6
6
  //#region src/telemetry-clean-text.ts
7
7
  const USER_DATA_REGEXES = [
@@ -232,6 +232,7 @@ const SETTING_NAME = [
232
232
  "keymap",
233
233
  "software_map_enabled",
234
234
  "show_release_notes",
235
+ "update_pill",
235
236
  "diffr_config",
236
237
  "theme",
237
238
  "diff_theme",
@@ -499,6 +500,10 @@ const UI_TELEMETRY_EVENTS = {
499
500
  event: "review_source_tree_opened",
500
501
  properties: { via: SOURCE_TREE_OPENED_VIA }
501
502
  },
503
+ whiteboard_file_exported: {
504
+ event: "review_whiteboard_file_exported",
505
+ properties: {}
506
+ },
502
507
  diff_layout_changed: {
503
508
  event: "review_diff_layout_changed",
504
509
  properties: {
@@ -1,5 +1,5 @@
1
- import { o as parseReviewVerbResponse } from "./src-Coec7jLU.js";
2
- import { b as selectReviewInstance, m as healthyReviewInstance, v as reviewInstanceStartHint, y as reviewInstanceUnavailable } from "./server-discovery-nLX2SH_v.js";
1
+ import { o as parseReviewVerbResponse } from "./src-DLWol7GZ.js";
2
+ import { b as selectReviewInstance, m as healthyReviewInstance, v as reviewInstanceStartHint, y as reviewInstanceUnavailable } from "./server-discovery-WmuFl2S3.js";
3
3
  import { closeSync, existsSync, fstatSync, mkdtempSync, openSync, readSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { homedir, tmpdir } from "node:os";
@@ -1,9 +1,9 @@
1
1
  import { c as jsonString, l as jsonValueSchema, o as jsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
2
- import "./src-Coec7jLU.js";
2
+ import "./src-DLWol7GZ.js";
3
3
  import "./review-home-paths-LdGS4SpR.js";
4
4
  import { F as writeFileAtomic, N as devReviewHome, j as withFileLock, k as processIsAlive } from "./s3-config-CjcLyFV5.js";
5
5
  import { i as readReviewPackageVersion, t as findReviewPackageRoot } from "./package-paths-Cg6fnaGp.js";
6
- import "./request-origin-Cr1tW4Vl.js";
6
+ import "./request-origin-B3_XI2VA.js";
7
7
  import { createHmac, randomUUID } from "node:crypto";
8
8
  import { z } from "zod";
9
9
  import { existsSync, readFileSync, rmSync } from "node:fs";
@@ -1,21 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import { S as splitGitPatchFiles, a as diff, o as diffFileSummaries } from "../src-Y30gweiK.js";
3
3
  import { c as jsonString, i as jsonBoolean, n as isJsonObject, p as isObjectValue, s as jsonProperty, u as parseJsonText } from "../src-X9phtB2j.js";
4
- import { g as readBoundedStream, h as StreamLimitError, u as legacyModeSchema } from "../import-Dnbujw7d.js";
4
+ import { _ as readBoundedStream, d as legacyModeSchema, g as StreamLimitError } from "../import-YAIU6rB0.js";
5
5
  import { ot as ReviewInputError, y as resourceReferences } from "../src-B2b8cCoI.js";
6
- import { A as ReviewGatewayHostSchema, D as ReviewConnectRequestSchema, P as reviewDiffrSummarizerInputSchema, d as ReviewBugReportRequestSchema, f as parseReviewBugReportResponse, m as REVIEW_CLIENT_REMOTE, p as REVIEW_CLIENT_HEADER, r as parseReviewDesktopVerbFrame, s as parseZod, t as parseReviewCliInstallApplyRequest, u as ReviewBugReportMetaV2Schema, y as REVIEW_HOST_HEADER } from "../src-Coec7jLU.js";
6
+ import { A as ReviewGatewayHostSchema, D as ReviewConnectRequestSchema, P as reviewDiffrSummarizerInputSchema, d as ReviewBugReportRequestSchema, f as parseReviewBugReportResponse, m as REVIEW_CLIENT_REMOTE, p as REVIEW_CLIENT_HEADER, r as parseReviewDesktopVerbFrame, s as parseZod, t as parseReviewCliInstallApplyRequest, u as ReviewBugReportMetaV2Schema, y as REVIEW_HOST_HEADER } from "../src-DLWol7GZ.js";
7
7
  import { i as reviewInstanceDiscoveryPath, o as reviewLegacyDiscoveryPath } from "../review-home-paths-LdGS4SpR.js";
8
- import { p as pullRequestProvider } from "../stored-document-migration-Ca9BWqEF.js";
8
+ import { p as pullRequestProvider } from "../stored-document-migration-iwIQ4l5G.js";
9
9
  import { L as writePrivateJsonAtomic, N as devReviewHome } from "../s3-config-CjcLyFV5.js";
10
10
  import { t as errorMessage } from "../error-message-OtiDonty.js";
11
11
  import { i as readReviewPackageVersion, t as findReviewPackageRoot } from "../package-paths-Cg6fnaGp.js";
12
- import { _ as reviewInstanceIdentity, u as REVIEW_INSTANCE_ENV } from "../server-discovery-nLX2SH_v.js";
13
- import { t as openReviewProfile } from "../profile-i2n1HdIg.js";
14
- import { r as reviewTelemetryChannel, t as ReviewTelemetry } from "../review-telemetry-CzYSJMMy.js";
15
- import { l as REVIEW_TELEMETRY_TABS } from "../request-origin-Cr1tW4Vl.js";
16
- import { c as removeLegacyReviewSkills, d as skipCliInstall, h as detectConnectAgents, i as finishCliInstallUpdate, l as resetCliInstall, m as connectAgent, n as applyCliInstall, o as pathShimPath, r as declineCliInstall, s as removeCliInstall, t as aliasInstallationToAccount, u as resolveCliInstallStatus } from "../account-alias-DbtsNr_c.js";
17
- import { C as setDiffrConfigValue, S as saveDiffrSummarizer, T as cliAskTools, _ as HttpJsonError, a as drainServerCrashReport, b as migrateDiffrConfig, c as answerErrorsAsJson, d as captureSanitizedUiTelemetry, f as clientErrorsForSession, g as DEFAULT_MAX_REQUEST_BYTES, h as readBoundedRequestJson, l as createWhiteboardCore, m as createNodeRequestListener, o as installProcessErrorTelemetry, p as recordClientError, r as takeOverServerHome, s as serverCrashReportPath, u as serverJson, v as ReviewServerError, w as testDiffrSummarizer, x as readDiffrConfig, y as GlobalReviewDesktopVerbRelay } from "../headless-host-s4A3qcsl.js";
18
- import { s as readQuerySchemas } from "../authoring-tools-BEGW2s0A.js";
12
+ import { _ as reviewInstanceIdentity, u as REVIEW_INSTANCE_ENV } from "../server-discovery-WmuFl2S3.js";
13
+ import { t as openReviewProfile } from "../profile--BYIjPkc.js";
14
+ import { r as reviewTelemetryChannel, t as ReviewTelemetry } from "../review-telemetry-x8u-9EZb.js";
15
+ import { l as REVIEW_TELEMETRY_TABS } from "../request-origin-B3_XI2VA.js";
16
+ import { c as removeLegacyReviewSkills, d as skipCliInstall, h as detectConnectAgents, i as finishCliInstallUpdate, l as resetCliInstall, m as connectAgent, n as applyCliInstall, o as pathShimPath, r as declineCliInstall, s as removeCliInstall, t as aliasInstallationToAccount, u as resolveCliInstallStatus } from "../account-alias-Bw3PT3Qo.js";
17
+ import { C as setDiffrConfigValue, S as saveDiffrSummarizer, T as cliAskTools, _ as HttpJsonError, a as drainServerCrashReport, b as migrateDiffrConfig, c as answerErrorsAsJson, d as captureSanitizedUiTelemetry, f as clientErrorsForSession, g as DEFAULT_MAX_REQUEST_BYTES, h as readBoundedRequestJson, l as createWhiteboardCore, m as createNodeRequestListener, o as installProcessErrorTelemetry, p as recordClientError, r as takeOverServerHome, s as serverCrashReportPath, u as serverJson, v as ReviewServerError, w as testDiffrSummarizer, x as readDiffrConfig, y as GlobalReviewDesktopVerbRelay } from "../headless-host-Dl2KZ9IT.js";
18
+ import { s as readQuerySchemas } from "../authoring-tools-CiLy46kh.js";
19
19
  import crypto, { createHash, randomBytes } from "node:crypto";
20
20
  import { z } from "zod";
21
21
  import { Agent, request } from "undici";
@@ -1,5 +1,5 @@
1
1
  import { a as jsonNumber, n as isJsonObject, s as jsonProperty, u as parseJsonText } from "./src-X9phtB2j.js";
2
- import { n as parseReviewDesktopDiscovery } from "./src-Coec7jLU.js";
2
+ import { n as parseReviewDesktopDiscovery } from "./src-DLWol7GZ.js";
3
3
  import { a as reviewInstancesDir, o as reviewLegacyDiscoveryPath, r as reviewDevInstanceKey, t as reviewDefaultInstancePath } from "./review-home-paths-LdGS4SpR.js";
4
4
  import { M as DEV_REVIEW_HOME_ENV, N as devReviewHome, k as processIsAlive } from "./s3-config-CjcLyFV5.js";
5
5
  import { z } from "zod";
@@ -635,6 +635,8 @@ interface ActivityPresence {
635
635
  focus?: ActivityFocus;
636
636
  /** Where it last wrote; absent until its first attributed write. */
637
637
  surface?: ActivitySurface;
638
+ /** Blocks reserved for it; others' edits on them or inside them are refused. */
639
+ hold?: string[];
638
640
  }
639
641
  interface ActivitySnapshot {
640
642
  /** Live presences. */
@@ -1,2 +1,2 @@
1
- import { c as exportShare, n as shareKey, r as validateShareBundle, s as ShareClient, t as importShare } from "../import-Dnbujw7d.js";
1
+ import { c as ShareClient, i as validateShareBundle, l as exportShare, r as shareKey, t as importShare } from "../import-YAIU6rB0.js";
2
2
  export { ShareClient, exportShare, importShare, shareKey, validateShareBundle };
@@ -827,6 +827,13 @@ const ReviewVerbRequestSchema = z.discriminatedUnion("name", [
827
827
  reviewId: apiReviewIdSchema,
828
828
  title: requiredString$1
829
829
  })
830
+ }),
831
+ z.strictObject({
832
+ name: z.literal("exportReviewFile"),
833
+ args: z.strictObject({
834
+ reviewId: apiReviewIdSchema,
835
+ version: z.number().int().nonnegative().optional()
836
+ })
830
837
  })
831
838
  ]);
832
839
  const ReviewVerbResponseSchema = z.discriminatedUnion("ok", [z.strictObject({
@@ -1,7 +1,7 @@
1
1
  import { C as renderVcsArgs, v as parseGitRemote, w as runVcs } from "./src-Y30gweiK.js";
2
2
  import { d as isBooleanValue, f as isNumberValue, l as jsonValueSchema, m as isStringValue, n as isJsonObject } from "./src-X9phtB2j.js";
3
3
  import { C as respellLinks, H as lensTargetsSchema, W as label, at as unionIntervals, et as formatAnchor, ot as ReviewInputError } from "./src-B2b8cCoI.js";
4
- import "./src-Coec7jLU.js";
4
+ import "./src-DLWol7GZ.js";
5
5
  import "./review-home-paths-LdGS4SpR.js";
6
6
  import { t as errorMessage } from "./error-message-OtiDonty.js";
7
7
  import { randomUUID } from "node:crypto";
@@ -1,4 +1,4 @@
1
- import { M as ReviewSelectedDiffSchema, T as ReviewApiSelectionSourceSchema } from "./src-Coec7jLU.js";
1
+ import { M as ReviewSelectedDiffSchema, T as ReviewApiSelectionSourceSchema } from "./src-DLWol7GZ.js";
2
2
  import { z } from "zod";
3
3
  import { stat } from "node:fs/promises";
4
4
  //#region src/fs-utils.ts
@@ -1,11 +1,11 @@
1
1
  import { m as isStringValue, n as isJsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
2
2
  import { M as checkReferences, j as blocks, ot as ReviewInputError } from "./src-B2b8cCoI.js";
3
- import { l as boardTitle } from "./src-Coec7jLU.js";
3
+ import { l as boardTitle } from "./src-DLWol7GZ.js";
4
4
  import "./review-home-paths-LdGS4SpR.js";
5
- import { n as callAuthoringTool, s as ReviewApiError, t as ToolText } from "./agent-client-CXXjD60Q.js";
5
+ import { n as callAuthoringTool, s as ReviewApiError, t as ToolText } from "./agent-client-CMacGbMp.js";
6
6
  import { N as devReviewHome } from "./s3-config-CjcLyFV5.js";
7
7
  import { t as findReviewPackageRoot } from "./package-paths-Cg6fnaGp.js";
8
- import { m as isMissingFileError } from "./thread-state-DosGpp3S.js";
8
+ import { m as isMissingFileError } from "./thread-state-B7L1mpN_.js";
9
9
  import { createHash } from "node:crypto";
10
10
  import { z } from "zod";
11
11
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dev.fast/whiteboard",
3
- "version": "0.4.1-preview.20261031.144",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -42,7 +42,7 @@
42
42
  "@agentclientprotocol/claude-agent-acp": "0.84.0",
43
43
  "@agentclientprotocol/codex-acp": "2.0.0",
44
44
  "@agentclientprotocol/sdk": "1.5.1",
45
- "@hono/node-server": "2.0.12",
45
+ "@hono/node-server": "2.1.5",
46
46
  "@img/sharp-wasm32": "0.35.5",
47
47
  "@modelcontextprotocol/server": "2.3.1",
48
48
  "@parcel/watcher": "2.6.0",
@@ -59,7 +59,7 @@
59
59
  "semver": "7.8.5",
60
60
  "sharp": "0.35.5",
61
61
  "smol-toml": "1.9.0",
62
- "undici": "7.29.1",
62
+ "undici": "7.30.1",
63
63
  "yauzl": "3.4.0",
64
64
  "zod": "4.6.6",
65
65
  "@dev.fast/diffr": "0.1.23"
@@ -75,13 +75,13 @@
75
75
  "tsdown": "0.22.14",
76
76
  "tsx": "4.21.0",
77
77
  "typescript": "6.0.3",
78
- "vitest": "4.1.11",
78
+ "vitest": "4.1.12",
79
79
  "@dev.fast/json": "0.0.0",
80
- "@dev.fast/review-share-protocol": "0.3.0",
81
- "@dev.fast/review-protocol": "0.0.0",
82
80
  "@dev.fast/trace-core": "0.0.0",
83
- "@dev.fast/trace-protocol": "0.5.0",
84
- "@dev.fast/whiteboard-document": "0.2.0"
81
+ "@dev.fast/review-share-protocol": "0.3.1",
82
+ "@dev.fast/whiteboard-document": "0.2.1",
83
+ "@dev.fast/review-protocol": "0.0.0",
84
+ "@dev.fast/trace-protocol": "0.5.0"
85
85
  },
86
86
  "engines": {
87
87
  "node": ">=24 <25"
@@ -91,7 +91,7 @@
91
91
  "repo": "devdotfast/review",
92
92
  "docs": "packages/review/instructions/authoring.md"
93
93
  },
94
- "gitHead": "92dca469c9d8aa180aad5e48653cefca86079785",
94
+ "gitHead": "b3f475d81a1ec90ba0b8c597a19b5bf9221c158a",
95
95
  "scripts": {
96
96
  "prebuild": "pnpm run build:workspace-deps",
97
97
  "build": "node --max-old-space-size=8192 node_modules/tsdown/dist/run.mjs",
package/src/cli-runner.ts CHANGED
@@ -1191,6 +1191,35 @@ export async function runReviewCli(input: ReviewCliInput): Promise<number> {
1191
1191
  });
1192
1192
  });
1193
1193
 
1194
+ configureJsonOutput(
1195
+ program
1196
+ .command("export")
1197
+ .description("Write a review snapshot as a .whiteboard file"),
1198
+ "plain",
1199
+ )
1200
+ .option("--review <id>", "review ID")
1201
+ .option("--version <number>", "Saved version to export")
1202
+ .option(
1203
+ "--out <path>",
1204
+ "File to write, or - for stdout; defaults to <title>.whiteboard here",
1205
+ )
1206
+ .action(
1207
+ async (options: {
1208
+ review?: string;
1209
+ version?: string;
1210
+ out?: string;
1211
+ json?: boolean;
1212
+ }) => {
1213
+ const { runExportCli } = await import("./sharing/cli.js");
1214
+ state.exitCode = await runExportCli({
1215
+ ...input,
1216
+ ...options,
1217
+ cwd,
1218
+ env: authoringEnv(),
1219
+ });
1220
+ },
1221
+ );
1222
+
1194
1223
  // Hosted trace store login. Logging in authenticates a user; it selects
1195
1224
  // no storage by itself.
1196
1225
  configureJsonOutput(
@@ -1,4 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
2
4
  import type { Writable } from "node:stream";
3
5
 
4
6
  import { connectReviewApi } from "@review/review-api/agent-client.js";
@@ -81,3 +83,60 @@ export async function runShareCli(input: {
81
83
  return 1;
82
84
  }
83
85
  }
86
+
87
+ const exportSchema = z.strictObject({
88
+ filename: z.string().min(1),
89
+ text: z.string(),
90
+ });
91
+
92
+ export async function runExportCli(input: {
93
+ review?: string;
94
+ version?: string;
95
+ out?: string;
96
+ json?: boolean;
97
+ cwd: string;
98
+ env?: NodeJS.ProcessEnv;
99
+ stdout: Writable;
100
+ stderr: Writable;
101
+ }) {
102
+ try {
103
+ if (!input.review) throw new Error("Use whiteboard export --review <id>.");
104
+
105
+ const client = await connectReviewApi(input.env);
106
+
107
+ const result = exportSchema.parse(
108
+ await client.post("/sharing/export", {
109
+ reviewId: input.review,
110
+ version:
111
+ input.version === undefined
112
+ ? undefined
113
+ : z.number().int().nonnegative().parse(Number(input.version)),
114
+ }),
115
+ );
116
+
117
+ if (input.out === "-") {
118
+ input.stdout.write(result.text);
119
+
120
+ return 0;
121
+ }
122
+
123
+ const file = path.resolve(input.cwd, input.out ?? result.filename);
124
+ await mkdir(path.dirname(file), { recursive: true });
125
+ await writeFile(file, result.text);
126
+ input.stdout.write(
127
+ input.json ? JSON.stringify({ path: file }) + "\n" : file + "\n",
128
+ );
129
+
130
+ return 0;
131
+ } catch (error) {
132
+ const message = error instanceof Error ? error.message : "Export failed.";
133
+
134
+ if (input.json)
135
+ input.stdout.write(
136
+ JSON.stringify({ error: { code: "export_failed", message } }) + "\n",
137
+ );
138
+ input.stderr.write(message + "\n");
139
+
140
+ return 1;
141
+ }
142
+ }
@@ -0,0 +1,52 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import {
4
+ SHARE_FORMAT,
5
+ type ShareBundle,
6
+ shareBundleSchema,
7
+ } from "@dev.fast/review-share-protocol";
8
+ import { ReviewInputError } from "@dev.fast/whiteboard-document";
9
+ import { slugify } from "@review/slug.js";
10
+ import { z } from "zod";
11
+
12
+ export const WHITEBOARD_FILE_EXTENSION = ".whiteboard";
13
+
14
+ const headerSchema = z.looseObject({ format: z.string() });
15
+
16
+ export function whiteboardFileName(title: string): string {
17
+ return `${slugify(title) || "untitled"}${WHITEBOARD_FILE_EXTENSION}`;
18
+ }
19
+
20
+ export function whiteboardFileText(bundle: ShareBundle): string {
21
+ return JSON.stringify(bundle, null, 2) + "\n";
22
+ }
23
+
24
+ export function parseWhiteboardFile(text: string): ShareBundle {
25
+ let parsed: unknown;
26
+
27
+ try {
28
+ parsed = JSON.parse(text);
29
+ } catch {
30
+ parsed = undefined;
31
+ }
32
+
33
+ const header = headerSchema.safeParse(parsed);
34
+
35
+ if (!header.success)
36
+ throw new ReviewInputError("This is not a Whiteboard file.", 400);
37
+
38
+ if (header.data.format !== SHARE_FORMAT)
39
+ throw new ReviewInputError(
40
+ "This file needs a different Whiteboard version.",
41
+ 400,
42
+ );
43
+
44
+ return shareBundleSchema.parse(parsed);
45
+ }
46
+
47
+ export function whiteboardFileKey(text: string): string {
48
+ return createHash("sha256")
49
+ .update("whiteboard-file\0")
50
+ .update(text)
51
+ .digest("hex");
52
+ }
@@ -1,7 +1,13 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
2
4
  import { Writable } from "node:stream";
3
5
 
4
- import { parseShareLink, shareIdSchema } from "@dev.fast/review-share-protocol";
6
+ import {
7
+ type ShareBundle,
8
+ parseShareLink,
9
+ shareIdSchema,
10
+ } from "@dev.fast/review-share-protocol";
5
11
  import {
6
12
  DEFAULT_STORE_ORIGIN,
7
13
  clearStoreAuth,
@@ -11,7 +17,7 @@ import {
11
17
  } from "@dev.fast/trace-core";
12
18
  import { ReviewInputError } from "@dev.fast/whiteboard-document";
13
19
  import type { LocalReviewData } from "@review/review-api/local-data.js";
14
- import type { ReviewStore } from "@review/review-api/store.js";
20
+ import type { ReviewStore, Snapshot } from "@review/review-api/store.js";
15
21
  import { readBoundedRequestJson } from "@review/server/hono-http.js";
16
22
  import type { Hono } from "hono";
17
23
  import { z } from "zod";
@@ -19,7 +25,18 @@ import { z } from "zod";
19
25
  import { readSharingAuth } from "./auth.js";
20
26
  import { ShareAuthError, ShareClient } from "./client.js";
21
27
  import { exportShare, validateShareSources } from "./export.js";
22
- import { importShare, shareKey } from "./import.js";
28
+ import {
29
+ parseWhiteboardFile,
30
+ whiteboardFileKey,
31
+ whiteboardFileName,
32
+ whiteboardFileText,
33
+ } from "./file.js";
34
+ import {
35
+ type ShareAttribution,
36
+ importShare,
37
+ needsRepository,
38
+ shareKey,
39
+ } from "./import.js";
23
40
  import {
24
41
  type cloneSharedRepository,
25
42
  type fetchPinnedRepository,
@@ -58,8 +75,19 @@ interface SharingHostOptions extends SharingHostEvents {
58
75
  fetch?: typeof fetch;
59
76
  }
60
77
 
78
+ const importSchema = z.union([
79
+ z.strictObject({ url: z.string().max(2048) }),
80
+ z.strictObject({ path: z.string().min(1).max(4096) }),
81
+ ]);
82
+
61
83
  interface ImportStatus {
62
- stage: "downloading" | "fetching" | "validating" | "ready" | "error";
84
+ stage:
85
+ | "downloading"
86
+ | "reading"
87
+ | "fetching"
88
+ | "validating"
89
+ | "ready"
90
+ | "error";
63
91
  /** The copy, once it is made. */
64
92
  reviewId?: string;
65
93
  error?: string;
@@ -170,23 +198,15 @@ export function mountSharingHost(
170
198
  ...(status.stage === "ready" && { title: store.read(reviewId).title }),
171
199
  });
172
200
  });
173
- app.post("/sharing/import", async (context) => {
174
- const { url } = z
175
- .strictObject({ url: z.string().max(2048) })
176
- .parse(await readBoundedRequestJson(context.req.raw));
177
-
178
- const parsed = parseShareLink(url);
179
- const account = await readStoreAuth();
180
201
 
181
- if (
182
- parsed.origin !== DEFAULT_STORE_ORIGIN &&
183
- parsed.origin !== account?.origin
184
- )
185
- throw new ReviewInputError(
186
- "This share uses an untrusted service. Sign in to that service before opening its links.",
187
- 400,
188
- );
189
- const id = shareKey(parsed.origin, parsed.capability);
202
+ const startImport = (
203
+ id: string,
204
+ initial: "downloading" | "reading",
205
+ load: () => Promise<{
206
+ bundle: ShareBundle;
207
+ attribution?: ShareAttribution;
208
+ }>,
209
+ ) => {
190
210
  const known = imports.get(id)?.stage;
191
211
 
192
212
  // A failed import retries, and a deleted copy imports again.
@@ -195,16 +215,14 @@ export function mountSharingHost(
195
215
 
196
216
  const status: ImportStatus = copy
197
217
  ? { stage: "ready", reviewId: copy }
198
- : { stage: "downloading" };
218
+ : { stage: initial };
199
219
 
200
220
  imports.set(id, status);
201
221
 
202
222
  if (!copy)
203
223
  importing = importing
204
224
  .then(async () => {
205
- const { bundle, attribution } = await new ShareClient(
206
- parsed.origin,
207
- ).download(parsed.capability);
225
+ const { bundle, attribution } = await load();
208
226
 
209
227
  status.reviewId = await importShare(
210
228
  {
@@ -233,7 +251,53 @@ export function mountSharingHost(
233
251
 
234
252
  const { reviewId: _reviewId, ...status } = imports.get(id)!;
235
253
 
236
- return context.json({ reviewId: id, ...status }, 202);
254
+ return { reviewId: id, ...status };
255
+ };
256
+
257
+ app.post("/sharing/import", async (context) => {
258
+ const input = importSchema.parse(
259
+ await readBoundedRequestJson(context.req.raw),
260
+ );
261
+
262
+ if ("path" in input) {
263
+ if (!path.isAbsolute(input.path))
264
+ throw new ReviewInputError("Use an absolute file path.", 400);
265
+
266
+ const text = await readFile(input.path, "utf8").catch(() => {
267
+ throw new ReviewInputError(
268
+ `Could not read ${path.basename(input.path)}. Check that the file still exists and Whiteboard can open it.`,
269
+ 400,
270
+ );
271
+ });
272
+
273
+ return context.json(
274
+ startImport(whiteboardFileKey(text), "reading", async () => ({
275
+ bundle: parseWhiteboardFile(text),
276
+ })),
277
+ 202,
278
+ );
279
+ }
280
+
281
+ const parsed = parseShareLink(input.url);
282
+ const account = await readStoreAuth();
283
+
284
+ if (
285
+ parsed.origin !== DEFAULT_STORE_ORIGIN &&
286
+ parsed.origin !== account?.origin
287
+ )
288
+ throw new ReviewInputError(
289
+ "This share uses an untrusted service. Sign in to that service before opening its links.",
290
+ 400,
291
+ );
292
+
293
+ return context.json(
294
+ startImport(
295
+ shareKey(parsed.origin, parsed.capability),
296
+ "downloading",
297
+ () => new ShareClient(parsed.origin).download(parsed.capability),
298
+ ),
299
+ 202,
300
+ );
237
301
  });
238
302
  }
239
303
 
@@ -247,11 +311,7 @@ export function mountSharingPublisher(
247
311
  "readRepository" | "fetch" | "onPublished" | "onRevoked"
248
312
  > = {},
249
313
  ) {
250
- app.post("/sharing/publish", async (context) => {
251
- const input = publishSchema.parse(
252
- await readBoundedRequestJson(context.req.raw),
253
- );
254
-
314
+ const shareable = (input: z.infer<typeof publishSchema>) => {
255
315
  const snapshot = store.read(input.reviewId, input.version);
256
316
 
257
317
  if (snapshot.target?.kind === "worktree")
@@ -260,14 +320,11 @@ export function mountSharingPublisher(
260
320
  );
261
321
 
262
322
  validateShareSources(snapshot);
263
- const account = await readSharingAuth();
264
323
 
265
- if (!account)
266
- throw new ReviewInputError(
267
- "Set DEV_REVIEW_SHARE_TOKEN for CI, or run whiteboard login before sharing.",
268
- 409,
269
- );
324
+ return snapshot;
325
+ };
270
326
 
327
+ const shareBundle = async (snapshot: Snapshot) => {
271
328
  const repositoryId =
272
329
  snapshot.pins?.repositoryId ?? snapshot.target?.repositoryId;
273
330
 
@@ -283,7 +340,7 @@ export function mountSharingPublisher(
283
340
  const bundle = await exportShare({
284
341
  store,
285
342
  data,
286
- reviewId: input.reviewId,
343
+ reviewId: snapshot.reviewId,
287
344
  version: snapshot.version,
288
345
  });
289
346
 
@@ -291,6 +348,43 @@ export function mountSharingPublisher(
291
348
 
292
349
  if (verified) bundle.repository = verified;
293
350
 
351
+ return bundle;
352
+ };
353
+
354
+ app.post("/sharing/export", async (context) => {
355
+ const snapshot = shareable(
356
+ publishSchema.parse(await readBoundedRequestJson(context.req.raw)),
357
+ );
358
+
359
+ const bundle = await shareBundle(snapshot);
360
+
361
+ // A link falls back to the browser viewer; a file has no such reader.
362
+ if (!bundle.repository && needsRepository(bundle, snapshot))
363
+ throw new ReviewInputError(
364
+ "Exporting pinned code needs a GitHub remote the file's reader can fetch from.",
365
+ );
366
+
367
+ return context.json({
368
+ filename: whiteboardFileName(snapshot.title),
369
+ text: whiteboardFileText(bundle),
370
+ });
371
+ });
372
+ app.post("/sharing/publish", async (context) => {
373
+ const input = publishSchema.parse(
374
+ await readBoundedRequestJson(context.req.raw),
375
+ );
376
+
377
+ const snapshot = shareable(input);
378
+ const account = await readSharingAuth();
379
+
380
+ if (!account)
381
+ throw new ReviewInputError(
382
+ "Set DEV_REVIEW_SHARE_TOKEN for CI, or run whiteboard login before sharing.",
383
+ 409,
384
+ );
385
+
386
+ const bundle = await shareBundle(snapshot);
387
+
294
388
  try {
295
389
  const result = await new ShareClient(
296
390
  account.origin,
@@ -116,7 +116,7 @@ export interface ShareAttribution {
116
116
  }
117
117
 
118
118
  /** Commits and traces live in a checkout; a document alone needs none. */
119
- function needsRepository(
119
+ export function needsRepository(
120
120
  bundle: ShareBundle,
121
121
  snapshot: Parameters<typeof referenceCommits>[0],
122
122
  ) {
@@ -146,6 +146,7 @@ export const SETTING_NAME = [
146
146
  "keymap",
147
147
  "software_map_enabled",
148
148
  "show_release_notes",
149
+ "update_pill",
149
150
  "diffr_config",
150
151
  "theme",
151
152
  "diff_theme",
@@ -430,6 +431,10 @@ export const UI_TELEMETRY_EVENTS = {
430
431
  event: "review_source_tree_opened",
431
432
  properties: { via: SOURCE_TREE_OPENED_VIA },
432
433
  },
434
+ whiteboard_file_exported: {
435
+ event: "review_whiteboard_file_exported",
436
+ properties: {},
437
+ },
433
438
  diff_layout_changed: {
434
439
  event: "review_diff_layout_changed",
435
440
  properties: {
@@ -1,2 +0,0 @@
1
- import { n as runReviewAgentCli } from "./agent-cli-DPt9w5-8.js";
2
- export { runReviewAgentCli };
@@ -1,2 +0,0 @@
1
- import { t as openReviewProfile } from "./profile-i2n1HdIg.js";
2
- export { openReviewProfile };