@dev.fast/whiteboard 0.0.0-stage → 0.2.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 (247) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -2
  3. package/THIRD_PARTY_NOTICES.md +93 -0
  4. package/dist/account-alias-DOHN1RSH.js +973 -0
  5. package/dist/agent-cli-ChOq0ZuO.js +117 -0
  6. package/dist/agent-cli-DzWfZr6G.js +2 -0
  7. package/dist/agent-client-CPQo7iTI.js +305 -0
  8. package/dist/authoring-tools-Cr6Kpjsx.js +234 -0
  9. package/dist/build-info.json +1 -0
  10. package/dist/cli-hVgHcdsv.js +47 -0
  11. package/dist/cli-runner-D8y2_luU.js +5095 -0
  12. package/dist/cli.d.ts +1 -0
  13. package/dist/cli.js +128 -0
  14. package/dist/client-DFoh50W3.js +950 -0
  15. package/dist/desktop-discovery-DBO-PM2V.js +164 -0
  16. package/dist/error-message-OtiDonty.js +6 -0
  17. package/dist/fs-utils-BMPLt0cr.js +22 -0
  18. package/dist/fuzzy-match-BoAmcyak.js +30 -0
  19. package/dist/headless-host-BJxf4dUU.js +94 -0
  20. package/dist/input-error-OLgB_h31.js +11 -0
  21. package/dist/local-data-CxLk75rx.js +4491 -0
  22. package/dist/mcp-QeF8vkdf.js +140 -0
  23. package/dist/package-paths-B6-zxvIO.js +47 -0
  24. package/dist/process-error-telemetry-v9e5h6D7.js +3952 -0
  25. package/dist/profile-4ry2f3AM.js +76 -0
  26. package/dist/profile-Bh5FIrmf.js +2 -0
  27. package/dist/request-origin-D_4QIdoN.js +811 -0
  28. package/dist/review-agent-traces-DNJFYzc8.js +3866 -0
  29. package/dist/review-home-paths-6zZH-1c9.js +2560 -0
  30. package/dist/review-telemetry-DEFnQsQ2.js +1195 -0
  31. package/dist/runtime-CKiWhGrf.js +52 -0
  32. package/dist/runtime.d.ts +9 -0
  33. package/dist/runtime.js +2 -0
  34. package/dist/s3-SzLpeCG6.js +345 -0
  35. package/dist/s3-config-BgxkSoOy.js +2 -0
  36. package/dist/s3-config-CAxhO9u_.js +676 -0
  37. package/dist/server/desktop-host.d.ts +4 -0
  38. package/dist/server/desktop-host.js +1702 -0
  39. package/dist/server-discovery-Dbzn3w6Z.js +66 -0
  40. package/dist/sharing/index.d.ts +1977 -0
  41. package/dist/sharing/index.js +2 -0
  42. package/dist/src-CGP5ytbV.js +283 -0
  43. package/dist/src-CmdiBL20.js +3676 -0
  44. package/dist/src-DnwdaQ2r.js +865 -0
  45. package/dist/src-X9phtB2j.js +68 -0
  46. package/dist/stored-document-migration-DHeMpLMb.js +2671 -0
  47. package/dist/tool-failure-C8zB73HV.js +98 -0
  48. package/dist/tutorial-trace-DrBvkaeL.js +43 -0
  49. package/instructions/authoring.md +36 -0
  50. package/instructions/file-lenses.md +13 -0
  51. package/instructions/scratchpad.md +23 -0
  52. package/instructions/trace-archaeology.md +113 -0
  53. package/onboarding.md +8 -0
  54. package/package.json +106 -3
  55. package/src/agent-selection.ts +100 -0
  56. package/src/agent-session-ref.ts +80 -0
  57. package/src/ask/agents.ts +351 -0
  58. package/src/ask/checkout-files.ts +47 -0
  59. package/src/ask/file-refs.ts +111 -0
  60. package/src/ask/pi-mcp.ts +154 -0
  61. package/src/ask/protocol.ts +244 -0
  62. package/src/ask/thread-state.ts +350 -0
  63. package/src/ask/thread.ts +1400 -0
  64. package/src/ask/threads.ts +219 -0
  65. package/src/ask/watch.ts +79 -0
  66. package/src/cli-install.ts +1041 -0
  67. package/src/cli-runner.ts +1461 -0
  68. package/src/cli-runtime-info.ts +35 -0
  69. package/src/cli.ts +222 -0
  70. package/src/connect-prompts.ts +248 -0
  71. package/src/cursor-deeplink.ts +11 -0
  72. package/src/desktop-discovery.ts +355 -0
  73. package/src/diff-selection-migration.ts +63 -0
  74. package/src/embedded-posthog-key.ts +6 -0
  75. package/src/error-telemetry.ts +247 -0
  76. package/src/evidence.ts +14 -0
  77. package/src/exception-telemetry.ts +126 -0
  78. package/src/fixtures/blocks/call_stack_diff.json +27 -0
  79. package/src/fixtures/blocks/callout.json +11 -0
  80. package/src/fixtures/blocks/code.json +9 -0
  81. package/src/fixtures/blocks/code_peek.json +8 -0
  82. package/src/fixtures/blocks/database_lens.json +51 -0
  83. package/src/fixtures/blocks/divider.json +1 -0
  84. package/src/fixtures/blocks/fixtures.ts +33 -0
  85. package/src/fixtures/blocks/flow_diagram.json +36 -0
  86. package/src/fixtures/blocks/ids.ts +8 -0
  87. package/src/fixtures/blocks/image.json +9 -0
  88. package/src/fixtures/blocks/markdown.json +7 -0
  89. package/src/fixtures/blocks/section.json +11 -0
  90. package/src/fixtures/blocks/sequence.json +31 -0
  91. package/src/fixtures/blocks/software_map.json +7 -0
  92. package/src/fixtures/blocks/trace_quote.json +9 -0
  93. package/src/fixtures/blocks/tutorial.json +51 -0
  94. package/src/fs-utils.ts +32 -0
  95. package/src/fuzzy-match.ts +94 -0
  96. package/src/install.ts +29 -0
  97. package/src/legacy-skills.ts +133 -0
  98. package/src/lens-selection.ts +230 -0
  99. package/src/markdown-latex-math.ts +230 -0
  100. package/src/markdown.ts +81 -0
  101. package/src/package-paths.ts +53 -0
  102. package/src/posthog-capture-client.ts +610 -0
  103. package/src/review-api/README.md +295 -0
  104. package/src/review-api/activity.ts +340 -0
  105. package/src/review-api/agent-cli.ts +219 -0
  106. package/src/review-api/agent-client.ts +188 -0
  107. package/src/review-api/anchor-quotes.ts +88 -0
  108. package/src/review-api/ask-history.ts +242 -0
  109. package/src/review-api/authoring-tools.ts +211 -0
  110. package/src/review-api/blocks/call_stack_diff.ts +75 -0
  111. package/src/review-api/blocks/callout.ts +24 -0
  112. package/src/review-api/blocks/code.ts +9 -0
  113. package/src/review-api/blocks/code_peek.ts +13 -0
  114. package/src/review-api/blocks/database_lens.ts +150 -0
  115. package/src/review-api/blocks/definition.ts +40 -0
  116. package/src/review-api/blocks/divider.ts +6 -0
  117. package/src/review-api/blocks/flow_diagram.ts +111 -0
  118. package/src/review-api/blocks/image.ts +10 -0
  119. package/src/review-api/blocks/index.ts +90 -0
  120. package/src/review-api/blocks/markdown.ts +17 -0
  121. package/src/review-api/blocks/section.ts +24 -0
  122. package/src/review-api/blocks/sequence.ts +56 -0
  123. package/src/review-api/blocks/software_map.ts +9 -0
  124. package/src/review-api/blocks/trace_quote.ts +10 -0
  125. package/src/review-api/blocks/tutorial.ts +39 -0
  126. package/src/review-api/checkout-fs.ts +14 -0
  127. package/src/review-api/client.ts +1 -0
  128. package/src/review-api/comparison-coverage.ts +304 -0
  129. package/src/review-api/component-reference.ts +30 -0
  130. package/src/review-api/diff-lenses.ts +175 -0
  131. package/src/review-api/document-headings.ts +51 -0
  132. package/src/review-api/document-text.ts +200 -0
  133. package/src/review-api/document.ts +906 -0
  134. package/src/review-api/file-lenses.ts +100 -0
  135. package/src/review-api/http.ts +1877 -0
  136. package/src/review-api/image-decode.ts +29 -0
  137. package/src/review-api/input-error.ts +9 -0
  138. package/src/review-api/instructions.ts +89 -0
  139. package/src/review-api/lens-alignment.ts +52 -0
  140. package/src/review-api/local-data.ts +1837 -0
  141. package/src/review-api/map-input.ts +154 -0
  142. package/src/review-api/mcp-client-agent.ts +32 -0
  143. package/src/review-api/mcp.ts +240 -0
  144. package/src/review-api/origin.ts +43 -0
  145. package/src/review-api/profile.ts +146 -0
  146. package/src/review-api/public-tools.ts +102 -0
  147. package/src/review-api/pull-request.ts +389 -0
  148. package/src/review-api/read-schemas.ts +85 -0
  149. package/src/review-api/recovery.ts +2 -0
  150. package/src/review-api/request-origin.ts +33 -0
  151. package/src/review-api/review-progress.ts +382 -0
  152. package/src/review-api/status-tool.ts +10 -0
  153. package/src/review-api/store-schema.ts +22 -0
  154. package/src/review-api/store.ts +1647 -0
  155. package/src/review-api/tool-failure.ts +64 -0
  156. package/src/review-api/trace-schema.ts +13 -0
  157. package/src/review-api/traces.ts +119 -0
  158. package/src/review-api/unsupported-files.integration.ts +91 -0
  159. package/src/review-api/workspaces.ts +692 -0
  160. package/src/review-api/worktree-source.ts +170 -0
  161. package/src/review-api/worktree-structural.integration.ts +320 -0
  162. package/src/review-app-launcher.ts +432 -0
  163. package/src/review-app-picker.ts +168 -0
  164. package/src/review-app.ts +134 -0
  165. package/src/review-bundled-tools.ts +249 -0
  166. package/src/review-checkout-paths.ts +37 -0
  167. package/src/review-diff-files.ts +89 -0
  168. package/src/review-head-checkout.ts +300 -0
  169. package/src/review-home-paths.ts +73 -0
  170. package/src/review-info.ts +59 -0
  171. package/src/review-instances.ts +113 -0
  172. package/src/review-logger.ts +183 -0
  173. package/src/review-preferences.ts +88 -0
  174. package/src/review-prepare.ts +291 -0
  175. package/src/review-stack.ts +112 -0
  176. package/src/review-telemetry.ts +1066 -0
  177. package/src/runtime.ts +74 -0
  178. package/src/server/account-alias.ts +30 -0
  179. package/src/server/bounded-stream.ts +34 -0
  180. package/src/server/bug-report.ts +301 -0
  181. package/src/server/client-error-budget.ts +45 -0
  182. package/src/server/crash-report.ts +273 -0
  183. package/src/server/desktop-host-shutdown.ts +64 -0
  184. package/src/server/desktop-host.ts +190 -0
  185. package/src/server/desktop-server.ts +703 -0
  186. package/src/server/diffr-config.ts +509 -0
  187. package/src/server/diffr-languages.ts +97 -0
  188. package/src/server/global-verb-relay.ts +190 -0
  189. package/src/server/headless-host.ts +149 -0
  190. package/src/server/hono-http.ts +162 -0
  191. package/src/server/http-json.ts +24 -0
  192. package/src/server/json-review-reporting.ts +174 -0
  193. package/src/server/process-error-telemetry.ts +174 -0
  194. package/src/server/review-api-parsers.ts +90 -0
  195. package/src/server/review-lifecycle-telemetry.ts +73 -0
  196. package/src/server/review-open-watchdog.ts +46 -0
  197. package/src/server/review-server-core.ts +290 -0
  198. package/src/server/structural-comparisons.ts +153 -0
  199. package/src/server/structural-diff.ts +186 -0
  200. package/src/server/tutorial-service.ts +241 -0
  201. package/src/server/ui-telemetry.ts +198 -0
  202. package/src/server-discovery.ts +95 -0
  203. package/src/session-markers.ts +132 -0
  204. package/src/sharing/auth.ts +41 -0
  205. package/src/sharing/cli.ts +83 -0
  206. package/src/sharing/client.ts +334 -0
  207. package/src/sharing/export.ts +195 -0
  208. package/src/sharing/host.ts +316 -0
  209. package/src/sharing/import.ts +787 -0
  210. package/src/sharing/index.ts +16 -0
  211. package/src/sharing/repository.ts +145 -0
  212. package/src/sharing/routes.ts +34 -0
  213. package/src/slug.ts +23 -0
  214. package/src/software-map-diff-counts.ts +517 -0
  215. package/src/software-map-model.ts +1147 -0
  216. package/src/software-map-topology-diff.ts +260 -0
  217. package/src/source.ts +92 -0
  218. package/src/startup-trace.ts +232 -0
  219. package/src/stored-document-migration.ts +151 -0
  220. package/src/telemetry-clean-text.ts +257 -0
  221. package/src/telemetry-config.ts +298 -0
  222. package/src/telemetry-debug-sink.ts +38 -0
  223. package/src/telemetry.ts +13 -0
  224. package/src/trace-cli.ts +156 -0
  225. package/src/trace-storage-cli.ts +509 -0
  226. package/src/tutorial-conversation.ts +18 -0
  227. package/src/ui-telemetry-events.ts +765 -0
  228. package/src/unified-diff.ts +71 -0
  229. package/src/viewed-coverage.ts +259 -0
  230. package/src/windows-cli.ts +125 -0
  231. package/tutorial/document.json +275 -0
  232. package/tutorial/runtime-manifest.json +12 -0
  233. package/tutorial/sample-service/package.json +9 -0
  234. package/tutorial/sample-service/src/api/checkout-api.ts +17 -0
  235. package/tutorial/sample-service/src/app.ts +26 -0
  236. package/tutorial/sample-service/src/database/schema.ts +10 -0
  237. package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +15 -0
  238. package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +24 -0
  239. package/tutorial/sample-service/src/inventory/inventory-service.ts +11 -0
  240. package/tutorial/sample-service/src/orders/order-service.ts +34 -0
  241. package/tutorial/sample-service/src/orders/order.ts +21 -0
  242. package/tutorial/sample-service/src/orders/orders-repository.ts +24 -0
  243. package/tutorial/sample-service/src/payments/payment-gateway.ts +12 -0
  244. package/tutorial/sample-service/src/shipping/shipping-gateway.ts +15 -0
  245. package/tutorial/sample-service/tsconfig.json +12 -0
  246. package/tutorial/software-map.json +139 -0
  247. package/tutorial/trace.json +20 -0
@@ -0,0 +1,38 @@
1
+ // Developer-only telemetry sink. When DEV_FAST_REVIEW_TELEMETRY_DEBUG is `1`
2
+ // or `true`, Review prints each event to stderr and sends nothing to PostHog.
3
+ // The switch is an environment variable, not a CLI flag, because the CLI, the
4
+ // local server, and the Desktop server host each build their own telemetry
5
+ // instance, and most events reach the server process, not the CLI.
6
+ import type { PostHogCaptureInput } from "./posthog-capture-client";
7
+ import type { ReviewTelemetryCaptureClient } from "./review-telemetry";
8
+
9
+ /**
10
+ * The debug sink, or `undefined` when the switch is off. The sink replaces the
11
+ * PostHog client: a developer run never reaches the production project.
12
+ */
13
+ export function createTelemetryDebugSink(
14
+ env: NodeJS.ProcessEnv,
15
+ output: NodeJS.WritableStream = process.stderr,
16
+ ): ReviewTelemetryCaptureClient | undefined {
17
+ if (!isEnabledEnvValue(env.DEV_FAST_REVIEW_TELEMETRY_DEBUG)) return undefined;
18
+
19
+ return {
20
+ enabled: true,
21
+ // Printing is not sending, so the sink prints events that the opt-out
22
+ // would otherwise drop.
23
+ ignoresOptOut: true,
24
+ capture(input: PostHogCaptureInput): Promise<void> {
25
+ try {
26
+ output.write(`[review-telemetry] ${JSON.stringify(input)}\n`);
27
+ } catch {
28
+ // The sink is a developer aid and must never affect Review behavior.
29
+ }
30
+
31
+ return Promise.resolve();
32
+ },
33
+ };
34
+ }
35
+
36
+ function isEnabledEnvValue(value: string | undefined): boolean {
37
+ return value === "1" || value?.toLowerCase() === "true";
38
+ }
@@ -0,0 +1,13 @@
1
+ // Keep this compatibility module because the review server and app import it.
2
+ // The implementation lives in one place so CLI and UI telemetry share the
3
+ // same install state, privacy checks, queue, and transport.
4
+ export {
5
+ ReviewTelemetry,
6
+ createLogger,
7
+ isTelemetryOptedOut,
8
+ type Logger,
9
+ type ReviewTabTelemetryEvent,
10
+ type ReviewTabTelemetryReason,
11
+ type ReviewTelemetryTab,
12
+ type ReviewTelemetryContext,
13
+ } from "./review-telemetry";
@@ -0,0 +1,156 @@
1
+ import type { Writable } from "node:stream";
2
+
3
+ import type { ReviewApiSummary } from "@dev.fast/review-protocol";
4
+ import {
5
+ type TraceListScope,
6
+ type TracePullScope,
7
+ type TraceReviewScope,
8
+ errorMessage,
9
+ runTraceList as listWithScope,
10
+ runTracePull as pullWithScope,
11
+ resolveTraceReadStorage,
12
+ } from "@dev.fast/trace-core";
13
+ import type { TraceStorageKind } from "@dev.fast/trace-core";
14
+
15
+ import { runReviewInfo } from "./review-info";
16
+
17
+ /**
18
+ * The Review app's trace commands. It resolves `--review <uuid>` (or the
19
+ * Review that owns the current checkout) against the Review store and hands
20
+ * the change range to the store-free read commands as a value.
21
+ */
22
+
23
+ export {
24
+ runTraceDisable,
25
+ runTraceEnable,
26
+ runTraceGitHook,
27
+ runTraceHook,
28
+ runTraceRepair,
29
+ runTraceStatus,
30
+ runTraceSync,
31
+ type TraceListScope,
32
+ type TracePullScope,
33
+ type TraceReviewScope,
34
+ runTraceBlame,
35
+ runTraceLookupCommit,
36
+ runTraceLookupSession,
37
+ runTraceShow,
38
+ } from "@dev.fast/trace-core";
39
+
40
+ export async function resolveTraceReviewScope(
41
+ cwd: string,
42
+ reviewUuid: string | undefined,
43
+ ): Promise<TraceReviewScope> {
44
+ const review = await resolveTraceReview(cwd, reviewUuid);
45
+
46
+ if (!review.repositoryPath || !review.pins)
47
+ throw new Error("Review repository is unavailable.");
48
+
49
+ return {
50
+ uuid: review.reviewId,
51
+ repoRoot: review.repositoryPath,
52
+ baseCommit: review.pins.base,
53
+ headCommit: review.pins.head,
54
+ };
55
+ }
56
+
57
+ export async function runTraceList(input: {
58
+ cwd: string;
59
+ reviewUuid?: string;
60
+ commitSha?: string;
61
+ storage?: TraceStorageKind;
62
+ json?: boolean;
63
+ stdout: Writable;
64
+ }): Promise<number> {
65
+ const resolvedStorage = await resolveTraceReadStorage(
66
+ input.storage,
67
+ input.cwd,
68
+ );
69
+ // Without --commit the command lists the Review's range, so a missing
70
+ // --review still resolves the Review that owns this checkout.
71
+
72
+ const scope: TraceListScope = input.commitSha
73
+ ? { commit: input.commitSha }
74
+ : { review: await resolveTraceReviewScope(input.cwd, input.reviewUuid) };
75
+
76
+ return listWithScope({
77
+ cwd: input.cwd,
78
+ scope,
79
+ resolvedStorage,
80
+ json: input.json,
81
+ stdout: input.stdout,
82
+ });
83
+ }
84
+
85
+ export async function runTracePull(input: {
86
+ cwd: string;
87
+ repo?: string;
88
+ reviewUuid?: string;
89
+ commitSha?: string;
90
+ session?: string;
91
+ mainOnly?: boolean;
92
+ storage?: TraceStorageKind;
93
+ json?: boolean;
94
+ stdout: Writable;
95
+ stderr: Writable;
96
+ }): Promise<number> {
97
+ try {
98
+ const resolvedStorage = await resolveTraceReadStorage(
99
+ input.storage,
100
+ input.cwd,
101
+ );
102
+
103
+ const scope = await resolveTracePullScope(input);
104
+
105
+ return pullWithScope({
106
+ cwd: input.cwd,
107
+ scope,
108
+ repo: input.repo,
109
+ mainOnly: input.mainOnly,
110
+ resolvedStorage,
111
+ json: input.json,
112
+ stdout: input.stdout,
113
+ stderr: input.stderr,
114
+ });
115
+ } catch (error) {
116
+ input.stderr.write(`trace pull error: ${errorMessage(error)}\n`);
117
+
118
+ return 1;
119
+ }
120
+ }
121
+
122
+ async function resolveTracePullScope(input: {
123
+ cwd: string;
124
+ reviewUuid?: string;
125
+ commitSha?: string;
126
+ session?: string;
127
+ }): Promise<TracePullScope> {
128
+ if (input.reviewUuid) {
129
+ return {
130
+ review: await resolveTraceReviewScope(input.cwd, input.reviewUuid),
131
+ };
132
+ }
133
+
134
+ if (input.commitSha) return { commit: input.commitSha };
135
+
136
+ if (input.session) return { session: input.session };
137
+
138
+ return { repository: true };
139
+ }
140
+
141
+ async function resolveTraceReview(
142
+ cwd: string,
143
+ reviewUuid: string | undefined,
144
+ ): Promise<ReviewApiSummary> {
145
+ const candidates = (await runReviewInfo({ cwd, reviewUuid })).reviews;
146
+
147
+ if (candidates.length === 0) {
148
+ throw new Error("No review found for this worktree.");
149
+ }
150
+
151
+ if (candidates.length > 1) {
152
+ throw new Error("Multiple Whiteboards require --session <uuid>.");
153
+ }
154
+
155
+ return candidates[0];
156
+ }
@@ -0,0 +1,509 @@
1
+ import { existsSync, renameSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ import {
6
+ type CliJsonOutput,
7
+ DEFAULT_HOSTED_ORIGIN,
8
+ HOSTED_CAPTURE_SCOPE_DESCRIPTION,
9
+ type S3CaptureSettings,
10
+ type S3Credentials,
11
+ type S3Profile,
12
+ S3_DEFAULT_REGION,
13
+ StoreApiError,
14
+ StoreClient,
15
+ type TraceConfig,
16
+ TraceConfigurationError,
17
+ type TraceRepositoryTarget,
18
+ clearTraceEnvCache,
19
+ currentStore,
20
+ describeSelection,
21
+ emitJsonEvent,
22
+ emptyTraceConfig,
23
+ errorMessage,
24
+ failWithJsonError,
25
+ humanStream,
26
+ isS3MockMode,
27
+ loadS3TraceStorage,
28
+ normalizeStoreOrigin,
29
+ readLegacyCaptureSettings,
30
+ readStoreAuth,
31
+ readTraceConfigFile,
32
+ readTraceEnvFile,
33
+ readTraceUserConfig,
34
+ requireTraceConsent,
35
+ resolveS3Setup,
36
+ resolveTraceRepositoryTarget,
37
+ s3ProfileSchema,
38
+ s3Store,
39
+ sameS3Profile,
40
+ selectTraceStorage,
41
+ traceMachineStatus,
42
+ traceRepositoryStatus,
43
+ traceSettingsPath,
44
+ writeTraceConfigFile,
45
+ } from "@dev.fast/trace-core";
46
+
47
+ import { devReviewHome } from "./review-home-paths";
48
+
49
+ /**
50
+ * `whiteboard trace storage use` and `whiteboard trace config migrate`: the explicit
51
+ * selection and configuration commands. Both write only
52
+ * `$DEV_REVIEW_HOME/trace/config.json`; the legacy files, environment, and
53
+ * every remote object stay as they are.
54
+ */
55
+
56
+ interface TraceStorageCommandScope {
57
+ homeDir?: string;
58
+ env?: NodeJS.ProcessEnv;
59
+ }
60
+
61
+ export interface RunReviewTraceStorageUseInput
62
+ extends CliJsonOutput, TraceStorageCommandScope {
63
+ cwd: string;
64
+ mode: string;
65
+ origin?: string;
66
+ /** The hosted API client; tests inject one that answers locally. */
67
+ client?: StoreClient;
68
+ endpoint?: string;
69
+ bucket?: string;
70
+ key?: string;
71
+ secret?: string;
72
+ region?: string;
73
+ }
74
+
75
+ export async function runTraceStorageUse(
76
+ input: RunReviewTraceStorageUseInput,
77
+ ): Promise<number> {
78
+ const stage = "trace.storage.use";
79
+ const scope = commandScope(input);
80
+
81
+ if (input.mode === "hosted") return useHosted(input, scope, stage);
82
+
83
+ if (input.mode !== "s3") {
84
+ return failWithJsonError(
85
+ input,
86
+ stage,
87
+ `Unknown storage mode "${input.mode}". Use "s3" or "hosted".`,
88
+ );
89
+ }
90
+
91
+ try {
92
+ const configFile = readTraceConfigFile(scope);
93
+
94
+ if (configFile.error) throw new TraceConfigurationError(configFile.error);
95
+ const current = configFile.config ?? emptyTraceConfig();
96
+ const flags = [input.endpoint, input.bucket, input.key, input.secret];
97
+ let next: TraceConfig;
98
+
99
+ if (flags.some(Boolean)) {
100
+ if (!flags.every(Boolean)) {
101
+ throw new TraceConfigurationError(
102
+ "The s3 store needs --endpoint, --bucket, --key, and --secret together.",
103
+ );
104
+ }
105
+
106
+ const profile = s3ProfileSchema.parse({
107
+ endpoint: input.endpoint,
108
+ bucket: input.bucket,
109
+ accessKeyId: input.key,
110
+ secretAccessKey: input.secret,
111
+ region:
112
+ input.region?.trim() ||
113
+ current.stores?.s3?.region ||
114
+ S3_DEFAULT_REGION,
115
+ capture: current.stores?.s3?.capture ??
116
+ (await readLegacyCaptureSettings(
117
+ traceSettingsPath(scope.homeDir, scope.env),
118
+ )) ?? { enabled: true, autoActivateRepositories: true },
119
+ });
120
+
121
+ await requireReachable(profile, scope);
122
+ next = {
123
+ ...current,
124
+ "current-store": "s3",
125
+ stores: { ...current.stores, s3: profile },
126
+ };
127
+ } else {
128
+ const setup = resolveS3Setup(scope);
129
+
130
+ if (!setup.credentials && !isS3MockMode(scope.env)) {
131
+ throw new TraceConfigurationError(
132
+ "No S3/R2 credentials are configured. Pass --endpoint, --bucket, --key, and --secret, or use Whiteboard Agent Setup.",
133
+ );
134
+ }
135
+
136
+ next = { ...current, "current-store": "s3" };
137
+ }
138
+
139
+ await writeTraceConfigFile(configFile, next);
140
+ clearTraceEnvCache();
141
+
142
+ const selection = selectTraceStorage(scope);
143
+ const machine = await traceMachineStatus(scope);
144
+ const repository = await traceRepositoryStatus(input.cwd);
145
+ const human = humanStream(input);
146
+ human.write(`Storage: ${describeSelection(selection)}\n`);
147
+ human.write(`Capture: ${machine.enabled ? "enabled" : "disabled"}\n`);
148
+ human.write(`Repository: ${repository.message}\n`);
149
+ emitJsonEvent(input, {
150
+ event: stage,
151
+ mode: "s3",
152
+ configPath: configFile.path,
153
+ endpoint: machine.endpoint ?? null,
154
+ bucket: machine.bucket ?? null,
155
+ region: machine.region ?? null,
156
+ captureEnabled: machine.enabled,
157
+ repository: repository.message,
158
+ });
159
+
160
+ return 0;
161
+ } catch (error) {
162
+ return failWithJsonError(input, stage, errorMessage(error));
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Selecting hosted storage validates everything a publication needs: a
168
+ * login for the origin, a store that answers the revised contract, and
169
+ * consent for this checkout's repository at that origin. Only then is the
170
+ * selection persisted. Bucket credentials stay where they are, inert.
171
+ */
172
+ async function useHosted(
173
+ input: RunReviewTraceStorageUseInput,
174
+ scope: { homeDir: string; env: NodeJS.ProcessEnv },
175
+ stage: string,
176
+ ): Promise<number> {
177
+ try {
178
+ const auth = await readStoreAuth(scope.env);
179
+
180
+ const origin = normalizeStoreOrigin(
181
+ input.origin ?? auth?.origin ?? DEFAULT_HOSTED_ORIGIN,
182
+ );
183
+
184
+ if (!auth || auth.origin !== origin) {
185
+ throw new TraceConfigurationError(
186
+ `Log in to ${origin} first: \`whiteboard login --origin ${origin}\`.`,
187
+ );
188
+ }
189
+
190
+ const devHome = devReviewHome(scope.env, scope.homeDir);
191
+
192
+ const client =
193
+ input.client ?? new StoreClient({ origin, token: auth.token });
194
+
195
+ let target: TraceRepositoryTarget;
196
+
197
+ try {
198
+ ({ target } = await resolveTraceRepositoryTarget({
199
+ cwd: input.cwd,
200
+ origin,
201
+ client,
202
+ write: true,
203
+ devHome,
204
+ }));
205
+ } catch (error) {
206
+ if (error instanceof StoreApiError && error.code === "upgrade_required") {
207
+ throw new TraceConfigurationError(
208
+ `${origin} does not serve the trace store contract this version of Whiteboard needs. Hosted storage was not selected.`,
209
+ );
210
+ }
211
+
212
+ throw error;
213
+ }
214
+
215
+ const consent = await requireTraceConsent(target, devHome);
216
+
217
+ const configFile = readTraceConfigFile(scope);
218
+
219
+ if (configFile.error) throw new TraceConfigurationError(configFile.error);
220
+ const current = configFile.config ?? emptyTraceConfig();
221
+ // The default origin needs no entry; any other origin is written down.
222
+ const stores = { ...current.stores };
223
+
224
+ if (origin === DEFAULT_HOSTED_ORIGIN) delete stores.hosted;
225
+ else stores.hosted = { origin };
226
+ await writeTraceConfigFile(configFile, {
227
+ ...current,
228
+ "current-store": "hosted",
229
+ stores,
230
+ });
231
+ clearTraceEnvCache();
232
+
233
+ const config = await readTraceUserConfig(devHome);
234
+ const human = humanStream(input);
235
+ human.write(`Storage: hosted (${origin})\n`);
236
+ human.write(
237
+ `Destination: ${target.name} (repository ${target.repositoryId}, store ${target.storeId})\n`,
238
+ );
239
+ human.write(
240
+ `Publication scope: ${config.repositories
241
+ .filter((entry) => entry.enabledOrigins.includes(origin))
242
+ .map((entry) => entry.name)
243
+ .join(", ")}\n`,
244
+ );
245
+
246
+ if (selectTraceStorage(scope).s3?.credentials) {
247
+ human.write(
248
+ "Bucket credentials stay saved and inactive; `whiteboard trace storage use s3` switches back.\n",
249
+ );
250
+ }
251
+
252
+ human.write(HOSTED_CAPTURE_SCOPE_DESCRIPTION);
253
+
254
+ emitJsonEvent(input, {
255
+ event: stage,
256
+ mode: "hosted",
257
+ configPath: configFile.path,
258
+ origin,
259
+ repositoryId: target.repositoryId,
260
+ storeId: target.storeId,
261
+ name: target.name,
262
+ allowedAt: consent.allowedAt,
263
+ allowedRepositories: config.repositories
264
+ .filter((entry) => entry.enabledOrigins.includes(origin))
265
+ .map((entry) => entry.name),
266
+ });
267
+
268
+ return 0;
269
+ } catch (error) {
270
+ return failWithJsonError(input, stage, errorMessage(error));
271
+ }
272
+ }
273
+
274
+ export interface RunReviewTraceConfigMigrateInput
275
+ extends CliJsonOutput, TraceStorageCommandScope {
276
+ dryRun?: boolean;
277
+ /** Leave the legacy env and settings files in place after migrating. */
278
+ keepLegacy?: boolean;
279
+ }
280
+
281
+ /**
282
+ * Where a migrated legacy file goes: `legacy_<name>` beside the original,
283
+ * with a timestamp when that name is already taken by an earlier backup.
284
+ */
285
+ export function legacyRetiredPath(filePath: string): string {
286
+ const base = path.join(
287
+ path.dirname(filePath),
288
+ `legacy_${path.basename(filePath)}`,
289
+ );
290
+
291
+ if (!existsSync(base)) return base;
292
+
293
+ return `${base}.${new Date().toISOString().replace(/[:.]/g, "-")}`;
294
+ }
295
+
296
+ /**
297
+ * Copies the effective legacy bucket setup into the version-2 config.
298
+ * Configuration moves; bucket objects, paths, and formats do not.
299
+ */
300
+ export async function runTraceConfigMigrate(
301
+ input: RunReviewTraceConfigMigrateInput,
302
+ ): Promise<number> {
303
+ const stage = "trace.config.migrate";
304
+ const scope = commandScope(input);
305
+ const human = humanStream(input);
306
+
307
+ try {
308
+ // 1. The effective legacy inputs, overrides and custom paths included.
309
+ const legacy = resolveS3Setup({ ...scope, ignoreProfile: true });
310
+
311
+ if (!legacy.credentials) {
312
+ throw new TraceConfigurationError(
313
+ `No legacy S3/R2 configuration to migrate (checked ${legacy.envPath} and the environment).`,
314
+ );
315
+ }
316
+
317
+ const settingsPath = traceSettingsPath(scope.homeDir, scope.env);
318
+ const settings = await readLegacyCaptureSettings(settingsPath);
319
+
320
+ // 2. The candidate profile and explicit selection. Disabled or absent
321
+ // capture settings stay disabled; migration never enables capture.
322
+ const capture: S3CaptureSettings = {
323
+ enabled: settings?.enabled === true,
324
+ autoActivateRepositories:
325
+ settings?.enabled === true &&
326
+ settings.autoActivateRepositories === true,
327
+ };
328
+
329
+ if (settings?.verifiedAt) capture.verifiedAt = settings.verifiedAt;
330
+
331
+ const candidate = s3ProfileSchema.parse({
332
+ ...legacy.credentials,
333
+ capture,
334
+ });
335
+
336
+ const configFile = readTraceConfigFile(scope);
337
+
338
+ if (configFile.error) throw new TraceConfigurationError(configFile.error);
339
+ const current = configFile.config ?? emptyTraceConfig();
340
+
341
+ if (currentStore(current) === "hosted") {
342
+ throw new TraceConfigurationError(
343
+ `Hosted storage is selected in ${configFile.path}. Run \`whiteboard trace storage use s3\` first; migration never switches destinations.`,
344
+ );
345
+ }
346
+
347
+ const existingProfile = s3Store(current);
348
+
349
+ const unchanged =
350
+ existingProfile !== null && sameS3Profile(existingProfile, candidate);
351
+
352
+ if (existingProfile && !unchanged) {
353
+ throw new TraceConfigurationError(
354
+ `${configFile.path} already holds a different s3 store. Remove it or update it with \`whiteboard trace storage use s3 --endpoint ...\`; migration does not overwrite it.`,
355
+ );
356
+ }
357
+
358
+ human.write(
359
+ `${input.dryRun ? "Previewing" : "Migrating"} S3 trace configuration into ${configFile.path}\n`,
360
+ );
361
+ human.write(
362
+ ` Credentials: ${legacy.source === "process-env" ? "process environment" : legacy.envPath}${
363
+ legacy.overrides.length > 0 && legacy.source !== "process-env"
364
+ ? ` (environment overrides: ${legacy.overrides.join(", ")})`
365
+ : ""
366
+ }\n`,
367
+ );
368
+ human.write(
369
+ ` Capture: ${candidate.capture?.enabled ? "enabled" : "disabled"} (from ${settingsPath})\n`,
370
+ );
371
+ human.write(
372
+ ` Destination: ${candidate.endpoint} bucket "${candidate.bucket}" region ${candidate.region ?? S3_DEFAULT_REGION}, key ${candidate.accessKeyId.slice(0, 6)}…\n`,
373
+ );
374
+
375
+ // 3. Validate independently of overrides and check reachability.
376
+ await requireReachable(candidate, scope);
377
+ human.write(" Reachability: ok\n");
378
+
379
+ let status: "unchanged" | "written" | "preview";
380
+
381
+ if (input.dryRun) {
382
+ // A dry run touches nothing, whatever the config already holds.
383
+ status = "preview";
384
+ human.write(
385
+ unchanged && currentStore(current) === "s3"
386
+ ? "Dry run: the config already holds this profile; nothing would be written.\n"
387
+ : "Dry run: nothing was written.\n",
388
+ );
389
+ } else if (unchanged && currentStore(current) === "s3") {
390
+ status = "unchanged";
391
+ human.write("Nothing to do: the config already holds this profile.\n");
392
+ } else {
393
+ // 4. Atomic private write; concurrent edits are refused.
394
+ await writeTraceConfigFile(configFile, {
395
+ ...current,
396
+ "current-store": "s3",
397
+ stores: { ...current.stores, s3: candidate },
398
+ });
399
+ clearTraceEnvCache();
400
+ status = "written";
401
+ human.write(`Wrote ${configFile.path} (mode 0600).\n`);
402
+ }
403
+
404
+ // 5. The legacy files are retired beside their originals so the new
405
+ // file is the only active source. Renaming, not deleting, keeps the
406
+ // rollback a rename away. Exported variables are the user's own.
407
+ const retired: Array<{ from: string; to: string }> = [];
408
+ const kept: string[] = [];
409
+
410
+ if (!input.dryRun && !input.keepLegacy) {
411
+ for (const filePath of [legacy.envPath, settingsPath]) {
412
+ if (!existsSync(filePath)) continue;
413
+
414
+ // The env file may also hold session-root settings that only it
415
+ // supplies; those keys are not migrated, so such a file stays.
416
+ const others =
417
+ filePath === legacy.envPath
418
+ ? [...readTraceEnvFile(filePath).keys()].filter(
419
+ (key) => !key.startsWith("TRACE_R2_"),
420
+ )
421
+ : [];
422
+
423
+ if (others.length > 0) {
424
+ kept.push(`${filePath} (still supplies ${others.join(", ")})`);
425
+ continue;
426
+ }
427
+
428
+ const to = legacyRetiredPath(filePath);
429
+ renameSync(filePath, to);
430
+ retired.push({ from: filePath, to });
431
+ }
432
+
433
+ clearTraceEnvCache();
434
+ }
435
+
436
+ for (const line of kept) human.write(`Kept ${line}\n`);
437
+
438
+ if (retired.length > 0) {
439
+ for (const move of retired) {
440
+ human.write(`Retired ${move.from} -> ${move.to}\n`);
441
+ }
442
+
443
+ human.write(
444
+ `To roll back, rename the retired files back and delete ${configFile.path}. Exported TRACE_R2_* variables still take precedence.\n`,
445
+ );
446
+ } else {
447
+ human.write(
448
+ "Legacy env and settings files were left unchanged; exported TRACE_R2_* variables still take precedence.\n",
449
+ );
450
+ }
451
+
452
+ emitJsonEvent(input, {
453
+ event: stage,
454
+ status,
455
+ dryRun: Boolean(input.dryRun),
456
+ retired,
457
+ configPath: configFile.path,
458
+ credentialsSource: legacy.source,
459
+ overrides: legacy.overrides,
460
+ settingsPath,
461
+ endpoint: candidate.endpoint,
462
+ bucket: candidate.bucket,
463
+ region: candidate.region ?? S3_DEFAULT_REGION,
464
+ accessKeyIdPrefix: candidate.accessKeyId.slice(0, 6),
465
+ capture: candidate.capture ?? null,
466
+ });
467
+
468
+ return 0;
469
+ } catch (error) {
470
+ return failWithJsonError(input, stage, errorMessage(error));
471
+ }
472
+ }
473
+
474
+ async function requireReachable(
475
+ profile: S3Profile,
476
+ scope: TraceStorageCommandScope,
477
+ ): Promise<void> {
478
+ const env = scope.env ?? process.env;
479
+
480
+ if (isS3MockMode(env)) return;
481
+
482
+ const credentials: S3Credentials = {
483
+ endpoint: profile.endpoint,
484
+ bucket: profile.bucket,
485
+ accessKeyId: profile.accessKeyId,
486
+ secretAccessKey: profile.secretAccessKey,
487
+ region: profile.region ?? S3_DEFAULT_REGION,
488
+ };
489
+
490
+ const S3TraceStorage = await loadS3TraceStorage();
491
+
492
+ const readiness = await S3TraceStorage.fromCredentials(
493
+ credentials,
494
+ env,
495
+ ).readiness();
496
+
497
+ if (!readiness.ready) {
498
+ throw new TraceConfigurationError(
499
+ `Cannot reach S3/R2 bucket "${profile.bucket}": ${readiness.reason ?? "unknown error"}. Nothing was written; retry when the bucket is reachable.`,
500
+ );
501
+ }
502
+ }
503
+
504
+ function commandScope(scope: TraceStorageCommandScope) {
505
+ return {
506
+ homeDir: scope.homeDir ?? os.homedir(),
507
+ env: scope.env ?? process.env,
508
+ };
509
+ }