@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.
- package/LICENSE +21 -0
- package/README.md +146 -2
- package/THIRD_PARTY_NOTICES.md +93 -0
- package/dist/account-alias-DOHN1RSH.js +973 -0
- package/dist/agent-cli-ChOq0ZuO.js +117 -0
- package/dist/agent-cli-DzWfZr6G.js +2 -0
- package/dist/agent-client-CPQo7iTI.js +305 -0
- package/dist/authoring-tools-Cr6Kpjsx.js +234 -0
- package/dist/build-info.json +1 -0
- package/dist/cli-hVgHcdsv.js +47 -0
- package/dist/cli-runner-D8y2_luU.js +5095 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +128 -0
- package/dist/client-DFoh50W3.js +950 -0
- package/dist/desktop-discovery-DBO-PM2V.js +164 -0
- package/dist/error-message-OtiDonty.js +6 -0
- package/dist/fs-utils-BMPLt0cr.js +22 -0
- package/dist/fuzzy-match-BoAmcyak.js +30 -0
- package/dist/headless-host-BJxf4dUU.js +94 -0
- package/dist/input-error-OLgB_h31.js +11 -0
- package/dist/local-data-CxLk75rx.js +4491 -0
- package/dist/mcp-QeF8vkdf.js +140 -0
- package/dist/package-paths-B6-zxvIO.js +47 -0
- package/dist/process-error-telemetry-v9e5h6D7.js +3952 -0
- package/dist/profile-4ry2f3AM.js +76 -0
- package/dist/profile-Bh5FIrmf.js +2 -0
- package/dist/request-origin-D_4QIdoN.js +811 -0
- package/dist/review-agent-traces-DNJFYzc8.js +3866 -0
- package/dist/review-home-paths-6zZH-1c9.js +2560 -0
- package/dist/review-telemetry-DEFnQsQ2.js +1195 -0
- package/dist/runtime-CKiWhGrf.js +52 -0
- package/dist/runtime.d.ts +9 -0
- package/dist/runtime.js +2 -0
- package/dist/s3-SzLpeCG6.js +345 -0
- package/dist/s3-config-BgxkSoOy.js +2 -0
- package/dist/s3-config-CAxhO9u_.js +676 -0
- package/dist/server/desktop-host.d.ts +4 -0
- package/dist/server/desktop-host.js +1702 -0
- package/dist/server-discovery-Dbzn3w6Z.js +66 -0
- package/dist/sharing/index.d.ts +1977 -0
- package/dist/sharing/index.js +2 -0
- package/dist/src-CGP5ytbV.js +283 -0
- package/dist/src-CmdiBL20.js +3676 -0
- package/dist/src-DnwdaQ2r.js +865 -0
- package/dist/src-X9phtB2j.js +68 -0
- package/dist/stored-document-migration-DHeMpLMb.js +2671 -0
- package/dist/tool-failure-C8zB73HV.js +98 -0
- package/dist/tutorial-trace-DrBvkaeL.js +43 -0
- package/instructions/authoring.md +36 -0
- package/instructions/file-lenses.md +13 -0
- package/instructions/scratchpad.md +23 -0
- package/instructions/trace-archaeology.md +113 -0
- package/onboarding.md +8 -0
- package/package.json +106 -3
- package/src/agent-selection.ts +100 -0
- package/src/agent-session-ref.ts +80 -0
- package/src/ask/agents.ts +351 -0
- package/src/ask/checkout-files.ts +47 -0
- package/src/ask/file-refs.ts +111 -0
- package/src/ask/pi-mcp.ts +154 -0
- package/src/ask/protocol.ts +244 -0
- package/src/ask/thread-state.ts +350 -0
- package/src/ask/thread.ts +1400 -0
- package/src/ask/threads.ts +219 -0
- package/src/ask/watch.ts +79 -0
- package/src/cli-install.ts +1041 -0
- package/src/cli-runner.ts +1461 -0
- package/src/cli-runtime-info.ts +35 -0
- package/src/cli.ts +222 -0
- package/src/connect-prompts.ts +248 -0
- package/src/cursor-deeplink.ts +11 -0
- package/src/desktop-discovery.ts +355 -0
- package/src/diff-selection-migration.ts +63 -0
- package/src/embedded-posthog-key.ts +6 -0
- package/src/error-telemetry.ts +247 -0
- package/src/evidence.ts +14 -0
- package/src/exception-telemetry.ts +126 -0
- package/src/fixtures/blocks/call_stack_diff.json +27 -0
- package/src/fixtures/blocks/callout.json +11 -0
- package/src/fixtures/blocks/code.json +9 -0
- package/src/fixtures/blocks/code_peek.json +8 -0
- package/src/fixtures/blocks/database_lens.json +51 -0
- package/src/fixtures/blocks/divider.json +1 -0
- package/src/fixtures/blocks/fixtures.ts +33 -0
- package/src/fixtures/blocks/flow_diagram.json +36 -0
- package/src/fixtures/blocks/ids.ts +8 -0
- package/src/fixtures/blocks/image.json +9 -0
- package/src/fixtures/blocks/markdown.json +7 -0
- package/src/fixtures/blocks/section.json +11 -0
- package/src/fixtures/blocks/sequence.json +31 -0
- package/src/fixtures/blocks/software_map.json +7 -0
- package/src/fixtures/blocks/trace_quote.json +9 -0
- package/src/fixtures/blocks/tutorial.json +51 -0
- package/src/fs-utils.ts +32 -0
- package/src/fuzzy-match.ts +94 -0
- package/src/install.ts +29 -0
- package/src/legacy-skills.ts +133 -0
- package/src/lens-selection.ts +230 -0
- package/src/markdown-latex-math.ts +230 -0
- package/src/markdown.ts +81 -0
- package/src/package-paths.ts +53 -0
- package/src/posthog-capture-client.ts +610 -0
- package/src/review-api/README.md +295 -0
- package/src/review-api/activity.ts +340 -0
- package/src/review-api/agent-cli.ts +219 -0
- package/src/review-api/agent-client.ts +188 -0
- package/src/review-api/anchor-quotes.ts +88 -0
- package/src/review-api/ask-history.ts +242 -0
- package/src/review-api/authoring-tools.ts +211 -0
- package/src/review-api/blocks/call_stack_diff.ts +75 -0
- package/src/review-api/blocks/callout.ts +24 -0
- package/src/review-api/blocks/code.ts +9 -0
- package/src/review-api/blocks/code_peek.ts +13 -0
- package/src/review-api/blocks/database_lens.ts +150 -0
- package/src/review-api/blocks/definition.ts +40 -0
- package/src/review-api/blocks/divider.ts +6 -0
- package/src/review-api/blocks/flow_diagram.ts +111 -0
- package/src/review-api/blocks/image.ts +10 -0
- package/src/review-api/blocks/index.ts +90 -0
- package/src/review-api/blocks/markdown.ts +17 -0
- package/src/review-api/blocks/section.ts +24 -0
- package/src/review-api/blocks/sequence.ts +56 -0
- package/src/review-api/blocks/software_map.ts +9 -0
- package/src/review-api/blocks/trace_quote.ts +10 -0
- package/src/review-api/blocks/tutorial.ts +39 -0
- package/src/review-api/checkout-fs.ts +14 -0
- package/src/review-api/client.ts +1 -0
- package/src/review-api/comparison-coverage.ts +304 -0
- package/src/review-api/component-reference.ts +30 -0
- package/src/review-api/diff-lenses.ts +175 -0
- package/src/review-api/document-headings.ts +51 -0
- package/src/review-api/document-text.ts +200 -0
- package/src/review-api/document.ts +906 -0
- package/src/review-api/file-lenses.ts +100 -0
- package/src/review-api/http.ts +1877 -0
- package/src/review-api/image-decode.ts +29 -0
- package/src/review-api/input-error.ts +9 -0
- package/src/review-api/instructions.ts +89 -0
- package/src/review-api/lens-alignment.ts +52 -0
- package/src/review-api/local-data.ts +1837 -0
- package/src/review-api/map-input.ts +154 -0
- package/src/review-api/mcp-client-agent.ts +32 -0
- package/src/review-api/mcp.ts +240 -0
- package/src/review-api/origin.ts +43 -0
- package/src/review-api/profile.ts +146 -0
- package/src/review-api/public-tools.ts +102 -0
- package/src/review-api/pull-request.ts +389 -0
- package/src/review-api/read-schemas.ts +85 -0
- package/src/review-api/recovery.ts +2 -0
- package/src/review-api/request-origin.ts +33 -0
- package/src/review-api/review-progress.ts +382 -0
- package/src/review-api/status-tool.ts +10 -0
- package/src/review-api/store-schema.ts +22 -0
- package/src/review-api/store.ts +1647 -0
- package/src/review-api/tool-failure.ts +64 -0
- package/src/review-api/trace-schema.ts +13 -0
- package/src/review-api/traces.ts +119 -0
- package/src/review-api/unsupported-files.integration.ts +91 -0
- package/src/review-api/workspaces.ts +692 -0
- package/src/review-api/worktree-source.ts +170 -0
- package/src/review-api/worktree-structural.integration.ts +320 -0
- package/src/review-app-launcher.ts +432 -0
- package/src/review-app-picker.ts +168 -0
- package/src/review-app.ts +134 -0
- package/src/review-bundled-tools.ts +249 -0
- package/src/review-checkout-paths.ts +37 -0
- package/src/review-diff-files.ts +89 -0
- package/src/review-head-checkout.ts +300 -0
- package/src/review-home-paths.ts +73 -0
- package/src/review-info.ts +59 -0
- package/src/review-instances.ts +113 -0
- package/src/review-logger.ts +183 -0
- package/src/review-preferences.ts +88 -0
- package/src/review-prepare.ts +291 -0
- package/src/review-stack.ts +112 -0
- package/src/review-telemetry.ts +1066 -0
- package/src/runtime.ts +74 -0
- package/src/server/account-alias.ts +30 -0
- package/src/server/bounded-stream.ts +34 -0
- package/src/server/bug-report.ts +301 -0
- package/src/server/client-error-budget.ts +45 -0
- package/src/server/crash-report.ts +273 -0
- package/src/server/desktop-host-shutdown.ts +64 -0
- package/src/server/desktop-host.ts +190 -0
- package/src/server/desktop-server.ts +703 -0
- package/src/server/diffr-config.ts +509 -0
- package/src/server/diffr-languages.ts +97 -0
- package/src/server/global-verb-relay.ts +190 -0
- package/src/server/headless-host.ts +149 -0
- package/src/server/hono-http.ts +162 -0
- package/src/server/http-json.ts +24 -0
- package/src/server/json-review-reporting.ts +174 -0
- package/src/server/process-error-telemetry.ts +174 -0
- package/src/server/review-api-parsers.ts +90 -0
- package/src/server/review-lifecycle-telemetry.ts +73 -0
- package/src/server/review-open-watchdog.ts +46 -0
- package/src/server/review-server-core.ts +290 -0
- package/src/server/structural-comparisons.ts +153 -0
- package/src/server/structural-diff.ts +186 -0
- package/src/server/tutorial-service.ts +241 -0
- package/src/server/ui-telemetry.ts +198 -0
- package/src/server-discovery.ts +95 -0
- package/src/session-markers.ts +132 -0
- package/src/sharing/auth.ts +41 -0
- package/src/sharing/cli.ts +83 -0
- package/src/sharing/client.ts +334 -0
- package/src/sharing/export.ts +195 -0
- package/src/sharing/host.ts +316 -0
- package/src/sharing/import.ts +787 -0
- package/src/sharing/index.ts +16 -0
- package/src/sharing/repository.ts +145 -0
- package/src/sharing/routes.ts +34 -0
- package/src/slug.ts +23 -0
- package/src/software-map-diff-counts.ts +517 -0
- package/src/software-map-model.ts +1147 -0
- package/src/software-map-topology-diff.ts +260 -0
- package/src/source.ts +92 -0
- package/src/startup-trace.ts +232 -0
- package/src/stored-document-migration.ts +151 -0
- package/src/telemetry-clean-text.ts +257 -0
- package/src/telemetry-config.ts +298 -0
- package/src/telemetry-debug-sink.ts +38 -0
- package/src/telemetry.ts +13 -0
- package/src/trace-cli.ts +156 -0
- package/src/trace-storage-cli.ts +509 -0
- package/src/tutorial-conversation.ts +18 -0
- package/src/ui-telemetry-events.ts +765 -0
- package/src/unified-diff.ts +71 -0
- package/src/viewed-coverage.ts +259 -0
- package/src/windows-cli.ts +125 -0
- package/tutorial/document.json +275 -0
- package/tutorial/runtime-manifest.json +12 -0
- package/tutorial/sample-service/package.json +9 -0
- package/tutorial/sample-service/src/api/checkout-api.ts +17 -0
- package/tutorial/sample-service/src/app.ts +26 -0
- package/tutorial/sample-service/src/database/schema.ts +10 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +15 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +24 -0
- package/tutorial/sample-service/src/inventory/inventory-service.ts +11 -0
- package/tutorial/sample-service/src/orders/order-service.ts +34 -0
- package/tutorial/sample-service/src/orders/order.ts +21 -0
- package/tutorial/sample-service/src/orders/orders-repository.ts +24 -0
- package/tutorial/sample-service/src/payments/payment-gateway.ts +12 -0
- package/tutorial/sample-service/src/shipping/shipping-gateway.ts +15 -0
- package/tutorial/sample-service/tsconfig.json +12 -0
- package/tutorial/software-map.json +139 -0
- package/tutorial/trace.json +20 -0
|
@@ -0,0 +1,1461 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import type { Writable } from "node:stream";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
StoreApiError,
|
|
10
|
+
processIsAlive,
|
|
11
|
+
readStoreAuth,
|
|
12
|
+
withStoreAuthorization,
|
|
13
|
+
} from "@dev.fast/trace-core";
|
|
14
|
+
import {
|
|
15
|
+
type CliInputStream,
|
|
16
|
+
DEFAULT_STORE_ORIGIN,
|
|
17
|
+
DEV_REVIEW_HOME_ENV,
|
|
18
|
+
emitJsonEvent,
|
|
19
|
+
humanStream,
|
|
20
|
+
jsonRequestedInArgv,
|
|
21
|
+
registerTraceCommands,
|
|
22
|
+
resolveTraceCommand,
|
|
23
|
+
runStoreLogin,
|
|
24
|
+
runStoreLogout,
|
|
25
|
+
runStoreWhoami,
|
|
26
|
+
runTraceAllow,
|
|
27
|
+
runTraceDeny,
|
|
28
|
+
runTraceInstallMachine,
|
|
29
|
+
runTraceOnboard,
|
|
30
|
+
runTraceSessions,
|
|
31
|
+
runTraceStoreDelete,
|
|
32
|
+
runTraceStoreInfo,
|
|
33
|
+
traceHomeDir,
|
|
34
|
+
traceMachineEnabled,
|
|
35
|
+
traceScope,
|
|
36
|
+
} from "@dev.fast/trace-core";
|
|
37
|
+
import { Argument, Command, CommanderError, Option } from "commander";
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
isOwnedShim,
|
|
41
|
+
pathShimPath,
|
|
42
|
+
windowsInstallerCommand,
|
|
43
|
+
} from "./cli-install";
|
|
44
|
+
import { cliRuntimeInfo, describeCliRuntime } from "./cli-runtime-info";
|
|
45
|
+
import { connectPrompts } from "./connect-prompts";
|
|
46
|
+
import {
|
|
47
|
+
ReviewInstanceUnavailableError,
|
|
48
|
+
readReviewInstances,
|
|
49
|
+
selectReviewInstance,
|
|
50
|
+
} from "./desktop-discovery";
|
|
51
|
+
import {
|
|
52
|
+
ALL_INSTALL_TARGETS,
|
|
53
|
+
type InstallTarget,
|
|
54
|
+
isInstallTarget,
|
|
55
|
+
} from "./install";
|
|
56
|
+
import { scanLegacySkills } from "./legacy-skills";
|
|
57
|
+
import {
|
|
58
|
+
findReviewPackageRoot,
|
|
59
|
+
readReviewPackageVersion,
|
|
60
|
+
} from "./package-paths";
|
|
61
|
+
import { reviewAgentCliHelp } from "./review-api/agent-cli";
|
|
62
|
+
import { ReviewApiError } from "./review-api/client";
|
|
63
|
+
import {
|
|
64
|
+
type ReviewAppEvent,
|
|
65
|
+
ReviewAppStateError,
|
|
66
|
+
ReviewAppUsageError,
|
|
67
|
+
runReviewAppPick,
|
|
68
|
+
} from "./review-app";
|
|
69
|
+
import {
|
|
70
|
+
type ReviewAppLaunchEvent,
|
|
71
|
+
runReviewAppLaunch,
|
|
72
|
+
} from "./review-app-launcher";
|
|
73
|
+
import { runReviewInfo } from "./review-info";
|
|
74
|
+
import {
|
|
75
|
+
clearReviewInstance,
|
|
76
|
+
listReviewInstancesCommand,
|
|
77
|
+
useReviewInstance,
|
|
78
|
+
} from "./review-instances";
|
|
79
|
+
import { emitReviewEvent, serializeReviewError } from "./review-logger";
|
|
80
|
+
import {
|
|
81
|
+
type ReviewCliCommand,
|
|
82
|
+
type ReviewCliCommandPath,
|
|
83
|
+
type ReviewCommandTelemetry,
|
|
84
|
+
ReviewTelemetry,
|
|
85
|
+
type ReviewTelemetryErrorCategory,
|
|
86
|
+
type ReviewTelemetryErrorName,
|
|
87
|
+
} from "./review-telemetry";
|
|
88
|
+
import {
|
|
89
|
+
readReviewServerDiscovery,
|
|
90
|
+
readReviewServerHealth,
|
|
91
|
+
reviewServerIsHealthy,
|
|
92
|
+
reviewServerStateDir,
|
|
93
|
+
serverNotReady,
|
|
94
|
+
} from "./server-discovery";
|
|
95
|
+
import { aliasInstallationToAccount } from "./server/account-alias";
|
|
96
|
+
import { setTraceAttribute, span } from "./startup-trace";
|
|
97
|
+
import type { ReviewTelemetrySurface } from "./telemetry-config";
|
|
98
|
+
import {
|
|
99
|
+
runTraceBlame,
|
|
100
|
+
runTraceDisable,
|
|
101
|
+
runTraceEnable,
|
|
102
|
+
runTraceGitHook,
|
|
103
|
+
runTraceHook,
|
|
104
|
+
runTraceList,
|
|
105
|
+
runTracePull,
|
|
106
|
+
runTraceRepair,
|
|
107
|
+
runTraceShow,
|
|
108
|
+
runTraceStatus,
|
|
109
|
+
runTraceSync,
|
|
110
|
+
} from "./trace-cli";
|
|
111
|
+
import { runTraceConfigMigrate, runTraceStorageUse } from "./trace-storage-cli";
|
|
112
|
+
|
|
113
|
+
class ReviewCliUsageError extends Error {
|
|
114
|
+
readonly name = "ReviewCliUsageError";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface ReviewCliRuntime {
|
|
118
|
+
runReviewAppLaunch: typeof runReviewAppLaunch;
|
|
119
|
+
runReviewAppPick: typeof runReviewAppPick;
|
|
120
|
+
runReviewInfo: typeof runReviewInfo;
|
|
121
|
+
runTraceStatus: typeof runTraceStatus;
|
|
122
|
+
runTraceEnable: typeof runTraceEnable;
|
|
123
|
+
runTraceDisable: typeof runTraceDisable;
|
|
124
|
+
runTraceRepair: typeof runTraceRepair;
|
|
125
|
+
runTraceList: typeof runTraceList;
|
|
126
|
+
runTraceShow: typeof runTraceShow;
|
|
127
|
+
runTracePull: typeof runTracePull;
|
|
128
|
+
runTraceBlame: typeof runTraceBlame;
|
|
129
|
+
runTraceHook: typeof runTraceHook;
|
|
130
|
+
runTraceGitHook: typeof runTraceGitHook;
|
|
131
|
+
runTraceSync: typeof runTraceSync;
|
|
132
|
+
runTraceStorageUse: typeof runTraceStorageUse;
|
|
133
|
+
runTraceConfigMigrate: typeof runTraceConfigMigrate;
|
|
134
|
+
runTraceInstallMachine: typeof runTraceInstallMachine;
|
|
135
|
+
runTraceOnboard: typeof runTraceOnboard;
|
|
136
|
+
runTraceStoreDelete: typeof runTraceStoreDelete;
|
|
137
|
+
runTraceStoreInfo: typeof runTraceStoreInfo;
|
|
138
|
+
runTraceSessions: typeof runTraceSessions;
|
|
139
|
+
runTraceAllow: typeof runTraceAllow;
|
|
140
|
+
runTraceDeny: typeof runTraceDeny;
|
|
141
|
+
runStoreLogin: typeof runStoreLogin;
|
|
142
|
+
runStoreLogout: typeof runStoreLogout;
|
|
143
|
+
runStoreWhoami: typeof runStoreWhoami;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface ReviewCliInput {
|
|
147
|
+
argv: string[];
|
|
148
|
+
cliVersion?: string;
|
|
149
|
+
cliPaths?: { requestedPath: string; effectivePath: string };
|
|
150
|
+
cwd?: string;
|
|
151
|
+
env?: NodeJS.ProcessEnv;
|
|
152
|
+
stdin?: CliInputStream;
|
|
153
|
+
stdout: Writable;
|
|
154
|
+
stderr: Writable;
|
|
155
|
+
telemetry?: ReviewCommandTelemetry;
|
|
156
|
+
runtime?: Partial<ReviewCliRuntime>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
interface ReviewInfoOptions {
|
|
160
|
+
all?: boolean;
|
|
161
|
+
session?: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
type OutputSurface = ReviewCliCommand | "plain";
|
|
165
|
+
|
|
166
|
+
interface CliRunState {
|
|
167
|
+
exitCode: number;
|
|
168
|
+
parseSurface: OutputSurface;
|
|
169
|
+
parserErrorOutput: string;
|
|
170
|
+
json: boolean;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function runReviewCli(input: ReviewCliInput): Promise<number> {
|
|
174
|
+
const env = input.env ?? process.env;
|
|
175
|
+
const cwd = input.cwd ?? env.INIT_CWD ?? process.cwd();
|
|
176
|
+
|
|
177
|
+
// The command every installed hook re-enters. The Review CLI resolves it
|
|
178
|
+
// the same way the hooks did on their own, so `review` behaves as before.
|
|
179
|
+
const traceCommand = resolveTraceCommand({
|
|
180
|
+
env,
|
|
181
|
+
homeDir: traceHomeDir(env),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const scope = traceScope({ env, homeDir: traceHomeDir(env) });
|
|
185
|
+
|
|
186
|
+
const cliVersion =
|
|
187
|
+
input.cliVersion ?? readReviewPackageVersion(import.meta.url);
|
|
188
|
+
|
|
189
|
+
const runtime = reviewCliRuntime(input.runtime);
|
|
190
|
+
const telemetry = input.telemetry ?? ReviewTelemetry.fromEnv(env);
|
|
191
|
+
|
|
192
|
+
const state: CliRunState = {
|
|
193
|
+
exitCode: 0,
|
|
194
|
+
parseSurface: "review",
|
|
195
|
+
parserErrorOutput: "",
|
|
196
|
+
json: jsonRequestedInArgv(input.argv),
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
let telemetryProperties: Record<
|
|
200
|
+
string,
|
|
201
|
+
boolean | number | string | null | undefined
|
|
202
|
+
> = {};
|
|
203
|
+
|
|
204
|
+
let activeTelemetry:
|
|
205
|
+
| {
|
|
206
|
+
command: ReviewCliCommandPath;
|
|
207
|
+
commandRunId: string;
|
|
208
|
+
startedAt: number;
|
|
209
|
+
finished: boolean;
|
|
210
|
+
}
|
|
211
|
+
| undefined;
|
|
212
|
+
|
|
213
|
+
let activeCause: unknown;
|
|
214
|
+
|
|
215
|
+
const configureOutput = <T extends Command>(
|
|
216
|
+
command: T,
|
|
217
|
+
surface: OutputSurface,
|
|
218
|
+
): T => {
|
|
219
|
+
command.configureOutput({
|
|
220
|
+
writeOut: (message) => input.stdout.write(message),
|
|
221
|
+
writeErr: (message) => {
|
|
222
|
+
state.parseSurface = surface;
|
|
223
|
+
state.parserErrorOutput += message;
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
command.showHelpAfterError();
|
|
227
|
+
|
|
228
|
+
return command;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Every command accepts --json so an agent can pass it without first knowing
|
|
232
|
+
// which commands support it. Commands that already write only JSON to stdout
|
|
233
|
+
// treat it as a no-op; the rest switch stdout to events and push human
|
|
234
|
+
// progress to stderr. A factory, not one shared Option: a shared instance
|
|
235
|
+
// would propagate any later .default()/.conflicts() to every command.
|
|
236
|
+
const configureJsonOutput = <T extends Command>(
|
|
237
|
+
command: T,
|
|
238
|
+
surface: OutputSurface,
|
|
239
|
+
): T =>
|
|
240
|
+
configureOutput(command, surface).addOption(
|
|
241
|
+
new Option("--json", "print machine-readable JSON events on stdout"),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const program = configureOutput(new Command(), "review")
|
|
245
|
+
.name("whiteboard")
|
|
246
|
+
.enablePositionalOptions()
|
|
247
|
+
.version(cliVersion)
|
|
248
|
+
.description("Create and open dev.fast reviews.")
|
|
249
|
+
.addHelpText("after", reviewTopLevelHelp());
|
|
250
|
+
|
|
251
|
+
// Tolerate the leading form (`whiteboard --json scaffold`) as well as the usual
|
|
252
|
+
// trailing one. Never give this a .default(): optsWithGlobals merges globals
|
|
253
|
+
// over locals, so a default would clobber a subcommand's own true.
|
|
254
|
+
program.addOption(new Option("--json").hideHelp());
|
|
255
|
+
program.option(
|
|
256
|
+
"--state-dir <path>",
|
|
257
|
+
"select headless Whiteboard state for server, api, and mcp",
|
|
258
|
+
);
|
|
259
|
+
program.exitOverride();
|
|
260
|
+
|
|
261
|
+
const authoringEnv = (
|
|
262
|
+
stateDir = program.opts<{ stateDir?: string }>().stateDir,
|
|
263
|
+
) =>
|
|
264
|
+
stateDir
|
|
265
|
+
? { ...env, DEV_REVIEW_SERVER_DIR: path.resolve(cwd, stateDir) }
|
|
266
|
+
: env;
|
|
267
|
+
|
|
268
|
+
const serverCommand = configureOutput(
|
|
269
|
+
program
|
|
270
|
+
.command("server")
|
|
271
|
+
.description("Run review authoring without Desktop"),
|
|
272
|
+
"plain",
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
configureJsonOutput(
|
|
276
|
+
serverCommand
|
|
277
|
+
.command("start")
|
|
278
|
+
.description(
|
|
279
|
+
"Start the foreground authoring server; stop with Ctrl-C or SIGTERM",
|
|
280
|
+
)
|
|
281
|
+
.option(
|
|
282
|
+
"--state-dir <path>",
|
|
283
|
+
"directory for saved reviews and server discovery",
|
|
284
|
+
)
|
|
285
|
+
.option(
|
|
286
|
+
"--port <port>",
|
|
287
|
+
"loopback port (0 chooses an available port)",
|
|
288
|
+
"0",
|
|
289
|
+
)
|
|
290
|
+
.option(
|
|
291
|
+
"--software-maps",
|
|
292
|
+
"allow the authoring skill to generate optional software maps",
|
|
293
|
+
)
|
|
294
|
+
// Batch authoring was removed; name that instead of "unknown option".
|
|
295
|
+
.addOption(new Option("--authoring-mode <mode>").hideHelp()),
|
|
296
|
+
"plain",
|
|
297
|
+
).action(async (_options, command: Command) => {
|
|
298
|
+
const options = command.optsWithGlobals<{
|
|
299
|
+
stateDir?: string;
|
|
300
|
+
port: string;
|
|
301
|
+
softwareMaps?: boolean;
|
|
302
|
+
authoringMode?: string;
|
|
303
|
+
json?: boolean;
|
|
304
|
+
}>();
|
|
305
|
+
|
|
306
|
+
if (options.authoringMode !== undefined)
|
|
307
|
+
throw new ReviewCliUsageError(
|
|
308
|
+
"--authoring-mode was removed with batch authoring; the server always authors interactively. Drop the option.",
|
|
309
|
+
);
|
|
310
|
+
const port = Number(options.port);
|
|
311
|
+
|
|
312
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535)
|
|
313
|
+
throw new ReviewCliUsageError(
|
|
314
|
+
"--port must be an integer between 0 and 65535.",
|
|
315
|
+
);
|
|
316
|
+
const stateDir = reviewServerStateDir(authoringEnv(options.stateDir));
|
|
317
|
+
const controller = new AbortController();
|
|
318
|
+
const stop = () => controller.abort();
|
|
319
|
+
process.once("SIGINT", stop);
|
|
320
|
+
process.once("SIGTERM", stop);
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const { runHeadlessServer } = await import("./server/headless-host.js");
|
|
324
|
+
await runHeadlessServer({
|
|
325
|
+
stateDir,
|
|
326
|
+
port,
|
|
327
|
+
softwareMapEnabled: options.softwareMaps,
|
|
328
|
+
signal: controller.signal,
|
|
329
|
+
telemetry,
|
|
330
|
+
onReady: ({ url, serverPid }) => {
|
|
331
|
+
input.stdout.write(
|
|
332
|
+
options.json
|
|
333
|
+
? `${JSON.stringify({ event: "server.ready", url, serverPid, stateDir })}\n`
|
|
334
|
+
: `Whiteboard server ready at ${url}\nSaved reviews: ${stateDir}\n`,
|
|
335
|
+
);
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
} finally {
|
|
339
|
+
process.off("SIGINT", stop);
|
|
340
|
+
process.off("SIGTERM", stop);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
configureJsonOutput(
|
|
345
|
+
serverCommand
|
|
346
|
+
.command("status")
|
|
347
|
+
.description("Check whether the headless server is ready")
|
|
348
|
+
.option(
|
|
349
|
+
"--state-dir <path>",
|
|
350
|
+
"directory selected when starting the server",
|
|
351
|
+
),
|
|
352
|
+
"plain",
|
|
353
|
+
).action(async (_options, command: Command) => {
|
|
354
|
+
const options = command.optsWithGlobals<{
|
|
355
|
+
stateDir?: string;
|
|
356
|
+
json?: boolean;
|
|
357
|
+
}>();
|
|
358
|
+
|
|
359
|
+
const stateDir = reviewServerStateDir(authoringEnv(options.stateDir));
|
|
360
|
+
const discovery = await readReviewServerDiscovery(stateDir);
|
|
361
|
+
const health = discovery && (await readReviewServerHealth(discovery));
|
|
362
|
+
|
|
363
|
+
if (!discovery || !health) throw serverNotReady(stateDir);
|
|
364
|
+
const { url, serverPid } = discovery;
|
|
365
|
+
const { version, serverId } = health;
|
|
366
|
+
input.stdout.write(
|
|
367
|
+
options.json
|
|
368
|
+
? `${JSON.stringify({ event: "server.status", ready: true, url, serverPid, stateDir, version, serverId })}\n`
|
|
369
|
+
: `Whiteboard server ready at ${url}\nSaved reviews: ${stateDir}\n`,
|
|
370
|
+
);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
configureJsonOutput(
|
|
374
|
+
serverCommand
|
|
375
|
+
.command("reset-id")
|
|
376
|
+
.description(
|
|
377
|
+
"Give this machine's saved reviews a new server id; stop the server first",
|
|
378
|
+
)
|
|
379
|
+
.option(
|
|
380
|
+
"--state-dir <path>",
|
|
381
|
+
"directory selected when starting the server",
|
|
382
|
+
),
|
|
383
|
+
"plain",
|
|
384
|
+
).action(async (_options, command: Command) => {
|
|
385
|
+
const options = command.optsWithGlobals<{
|
|
386
|
+
stateDir?: string;
|
|
387
|
+
json?: boolean;
|
|
388
|
+
}>();
|
|
389
|
+
|
|
390
|
+
const stateDir = reviewServerStateDir(authoringEnv(options.stateDir));
|
|
391
|
+
|
|
392
|
+
if (!existsSync(path.join(stateDir, "review-api.db")))
|
|
393
|
+
throw new Error(
|
|
394
|
+
`No saved reviews in ${stateDir}, so there is no server id to reset. Check --state-dir.`,
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
// Any live Desktop holds the store, attached window or not. Only a gone
|
|
398
|
+
// process counts as stopped: a paused or busy one may not answer /health.
|
|
399
|
+
const desktops = await readReviewInstances({
|
|
400
|
+
env: { ...env, [DEV_REVIEW_HOME_ENV]: stateDir },
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const [problem] = desktops.broken.values();
|
|
404
|
+
|
|
405
|
+
if (problem)
|
|
406
|
+
throw new Error(
|
|
407
|
+
`Cannot tell whether a Whiteboard Desktop is using ${stateDir}: ${problem.message}`,
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
const inUse = () =>
|
|
411
|
+
new Error(
|
|
412
|
+
`A Whiteboard server is using ${stateDir}. Stop it first, then run whiteboard server reset-id again.`,
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
if (
|
|
416
|
+
desktops.instances.some(({ discovery }) =>
|
|
417
|
+
[discovery.appPid, discovery.serverPid].some((pid) =>
|
|
418
|
+
processIsAlive(pid),
|
|
419
|
+
),
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
throw inUse();
|
|
423
|
+
|
|
424
|
+
const [{ openReviewProfile }, { withHeadlessServerLock }] =
|
|
425
|
+
await Promise.all([
|
|
426
|
+
import("./review-api/profile.js"),
|
|
427
|
+
import("./server/headless-host.js"),
|
|
428
|
+
]);
|
|
429
|
+
|
|
430
|
+
// The headless server's own lock (at the path it resolves): refused while
|
|
431
|
+
// a live server holds it, and no server can start during the reset.
|
|
432
|
+
const reset = await withHeadlessServerLock(
|
|
433
|
+
await realpath(stateDir),
|
|
434
|
+
async () => {
|
|
435
|
+
const local = await openReviewProfile(stateDir, {
|
|
436
|
+
manageWorkspaces: false,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
return local.store.resetServerId();
|
|
441
|
+
} finally {
|
|
442
|
+
await local.data.close();
|
|
443
|
+
await local.store.close();
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
if (!reset.acquired) throw inUse();
|
|
449
|
+
const serverId = reset.result;
|
|
450
|
+
|
|
451
|
+
input.stdout.write(
|
|
452
|
+
options.json
|
|
453
|
+
? `${JSON.stringify({ event: "server.reset-id", serverId, stateDir })}\n`
|
|
454
|
+
: `New server id ${serverId}\nSaved reviews: ${stateDir}\n`,
|
|
455
|
+
);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
configureJsonOutput(
|
|
459
|
+
program
|
|
460
|
+
.command("version")
|
|
461
|
+
.description("Print Whiteboard package version")
|
|
462
|
+
.option("--verbose", "Show executing CLI paths and build identity"),
|
|
463
|
+
"plain",
|
|
464
|
+
).action(async (options: { verbose?: boolean }, command: Command) => {
|
|
465
|
+
const { json } = command.optsWithGlobals<{ json?: boolean }>();
|
|
466
|
+
|
|
467
|
+
if (options.verbose) {
|
|
468
|
+
const selection = await selectReviewInstance({ env });
|
|
469
|
+
|
|
470
|
+
const info = {
|
|
471
|
+
...cliRuntimeInfo(
|
|
472
|
+
input.cliPaths?.requestedPath ??
|
|
473
|
+
path.resolve(process.argv[1] ?? fileURLToPath(import.meta.url)),
|
|
474
|
+
input.cliPaths?.effectivePath,
|
|
475
|
+
),
|
|
476
|
+
instance: selection.key,
|
|
477
|
+
instanceRecord: selection.instance?.filePath ?? null,
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
input.stdout.write(
|
|
481
|
+
json
|
|
482
|
+
? `${JSON.stringify(info)}\n`
|
|
483
|
+
: `${describeCliRuntime(info)}Instance: ${info.instance} (${info.instanceRecord ?? "not running"})\n`,
|
|
484
|
+
);
|
|
485
|
+
state.exitCode = 0;
|
|
486
|
+
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
input.stdout.write(
|
|
491
|
+
json
|
|
492
|
+
? `${JSON.stringify({ event: "version", version: cliVersion })}\n`
|
|
493
|
+
: `${cliVersion}\n`,
|
|
494
|
+
);
|
|
495
|
+
state.exitCode = 0;
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const writeAppEvent = (
|
|
499
|
+
event: ReviewAppLaunchEvent | ReviewAppEvent,
|
|
500
|
+
options: { json?: boolean; focus?: boolean },
|
|
501
|
+
) => {
|
|
502
|
+
if (options.json) {
|
|
503
|
+
input.stdout.write(`${JSON.stringify(event)}\n`);
|
|
504
|
+
} else if (event.action === "launch") {
|
|
505
|
+
input.stdout.write(
|
|
506
|
+
event.state === "running"
|
|
507
|
+
? "Whiteboard Desktop is already running.\n"
|
|
508
|
+
: options.focus
|
|
509
|
+
? "Whiteboard Desktop is ready.\n"
|
|
510
|
+
: "Whiteboard Desktop is ready in the background. Pass --focus to bring it forward.\n",
|
|
511
|
+
);
|
|
512
|
+
} else {
|
|
513
|
+
input.stdout.write(`Whiteboard Desktop is showing "${event.title}".\n`);
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const pickReview = async (options: {
|
|
518
|
+
session?: string;
|
|
519
|
+
focus?: boolean;
|
|
520
|
+
json?: boolean;
|
|
521
|
+
}) => {
|
|
522
|
+
// SAFETY: the picker reads keypresses only after checking isTTY, and only
|
|
523
|
+
// a tty.ReadStream reports isTTY; any other stream fails that check first.
|
|
524
|
+
const event = await runtime.runReviewAppPick({
|
|
525
|
+
cwd,
|
|
526
|
+
reviewUuid: options.session,
|
|
527
|
+
focus: options.focus,
|
|
528
|
+
stdin: (input.stdin ?? process.stdin) as NodeJS.ReadStream,
|
|
529
|
+
// This stream carries only the interactive picker. Under --json it must
|
|
530
|
+
// not be stdout: the picker's ANSI frames would corrupt the event line.
|
|
531
|
+
stdout: humanStream({ ...input, json: options.json }),
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
if (!event) {
|
|
535
|
+
state.exitCode = 1;
|
|
536
|
+
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
writeAppEvent(event, options);
|
|
541
|
+
state.exitCode = 0;
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const launchApp = async (options: {
|
|
545
|
+
focus?: boolean;
|
|
546
|
+
json?: boolean;
|
|
547
|
+
sandbox?: boolean;
|
|
548
|
+
}) => {
|
|
549
|
+
const event = await runtime.runReviewAppLaunch({
|
|
550
|
+
focus: options.focus,
|
|
551
|
+
noSandbox: options.sandbox === false,
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
writeAppEvent(event, options);
|
|
555
|
+
state.exitCode = 0;
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const app = configureJsonOutput(
|
|
559
|
+
program
|
|
560
|
+
.command("app")
|
|
561
|
+
.description("Start Whiteboard Desktop in the background")
|
|
562
|
+
.option("--focus", "bring Whiteboard Desktop to the foreground")
|
|
563
|
+
.option(
|
|
564
|
+
"--no-sandbox",
|
|
565
|
+
"disable the Chromium sandbox for a Linux container",
|
|
566
|
+
),
|
|
567
|
+
"plain",
|
|
568
|
+
).action(launchApp);
|
|
569
|
+
|
|
570
|
+
configureJsonOutput(
|
|
571
|
+
app
|
|
572
|
+
.command("launch")
|
|
573
|
+
.description("Start Whiteboard Desktop in the background")
|
|
574
|
+
.option("--focus", "bring Whiteboard Desktop to the foreground")
|
|
575
|
+
.option(
|
|
576
|
+
"--no-sandbox",
|
|
577
|
+
"disable the Chromium sandbox for a Linux container",
|
|
578
|
+
),
|
|
579
|
+
"plain",
|
|
580
|
+
).action(launchApp);
|
|
581
|
+
configureJsonOutput(
|
|
582
|
+
app
|
|
583
|
+
.command("pick")
|
|
584
|
+
.description("Select a review (interactive picker without --session)")
|
|
585
|
+
.option("--session <uuid>", "review UUID")
|
|
586
|
+
.option("--focus", "bring Whiteboard Desktop to the foreground"),
|
|
587
|
+
"plain",
|
|
588
|
+
).action(pickReview);
|
|
589
|
+
|
|
590
|
+
const instanceOutput = (command: Command) => ({
|
|
591
|
+
env,
|
|
592
|
+
stdout: input.stdout,
|
|
593
|
+
stderr: input.stderr,
|
|
594
|
+
json: command.optsWithGlobals<{ json?: boolean }>().json,
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
const instances = configureJsonOutput(
|
|
598
|
+
program
|
|
599
|
+
.command("instances")
|
|
600
|
+
.description("List running reviews and the one commands use"),
|
|
601
|
+
"plain",
|
|
602
|
+
).action(async (_options: { json?: boolean }, command: Command) => {
|
|
603
|
+
await listReviewInstancesCommand(instanceOutput(command));
|
|
604
|
+
state.exitCode = 0;
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
configureJsonOutput(
|
|
608
|
+
instances
|
|
609
|
+
.command("use")
|
|
610
|
+
.description("Make an instance this machine's default")
|
|
611
|
+
.argument("<key>", "stable, preview, or a dev-… key"),
|
|
612
|
+
"plain",
|
|
613
|
+
).action(
|
|
614
|
+
async (key: string, _options: { json?: boolean }, command: Command) => {
|
|
615
|
+
await useReviewInstance(key, instanceOutput(command));
|
|
616
|
+
state.exitCode = 0;
|
|
617
|
+
},
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
configureJsonOutput(
|
|
621
|
+
instances.command("clear").description("Remove the machine default"),
|
|
622
|
+
"plain",
|
|
623
|
+
).action(async (_options: { json?: boolean }, command: Command) => {
|
|
624
|
+
await clearReviewInstance(instanceOutput(command));
|
|
625
|
+
state.exitCode = 0;
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
configureJsonOutput(
|
|
629
|
+
program.command("info").description("Print review information"),
|
|
630
|
+
"plain",
|
|
631
|
+
)
|
|
632
|
+
.option("--all", "list active reviews for every worktree in this repo")
|
|
633
|
+
.addOption(
|
|
634
|
+
new Option("--session <uuid>", "select a review").conflicts("all"),
|
|
635
|
+
)
|
|
636
|
+
.action(async (options: ReviewInfoOptions) => {
|
|
637
|
+
const event = await runtime.runReviewInfo({
|
|
638
|
+
cwd,
|
|
639
|
+
all: options.all,
|
|
640
|
+
reviewUuid: options.session,
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
input.stdout.write(`${JSON.stringify(event)}\n`);
|
|
644
|
+
state.exitCode = 0;
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
const connect = configureJsonOutput(
|
|
648
|
+
program
|
|
649
|
+
.command("connect")
|
|
650
|
+
.description(
|
|
651
|
+
"Print the prompt that connects a coding agent to Whiteboard",
|
|
652
|
+
)
|
|
653
|
+
.addArgument(
|
|
654
|
+
new Argument("[target...]", "coding agent").choices([
|
|
655
|
+
"claude",
|
|
656
|
+
"claude-code",
|
|
657
|
+
"codex",
|
|
658
|
+
"cursor",
|
|
659
|
+
"opencode",
|
|
660
|
+
"pi",
|
|
661
|
+
"omp",
|
|
662
|
+
"copilot",
|
|
663
|
+
"all",
|
|
664
|
+
]),
|
|
665
|
+
),
|
|
666
|
+
"plain",
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
connect.action(async (targets: string[], options: { json?: boolean }) => {
|
|
670
|
+
const selected = parseTargets(targets);
|
|
671
|
+
|
|
672
|
+
const { homeDir, devHome } = scope;
|
|
673
|
+
|
|
674
|
+
const prompts = connectPrompts({
|
|
675
|
+
legacyPaths: await scanLegacySkills(homeDir),
|
|
676
|
+
hasShim:
|
|
677
|
+
(await isOwnedShim(pathShimPath(homeDir))) ||
|
|
678
|
+
(await windowsInstallerCommand(
|
|
679
|
+
findReviewPackageRoot(import.meta.url),
|
|
680
|
+
env,
|
|
681
|
+
)) !== undefined,
|
|
682
|
+
traceEnabled: await traceMachineEnabled({ homeDir, env }),
|
|
683
|
+
fffBinaryPath: path.join(homeDir, ".local", "bin", "fff-mcp"),
|
|
684
|
+
fffCorpusRoot: path.join(devHome, "trace-search"),
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
const output = {
|
|
688
|
+
json: options.json,
|
|
689
|
+
stdout: input.stdout,
|
|
690
|
+
stderr: input.stderr,
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
if (options.json) {
|
|
694
|
+
emitJsonEvent(output, {
|
|
695
|
+
event: "connect",
|
|
696
|
+
prompts: Object.fromEntries(
|
|
697
|
+
selected.map((target) => [target, prompts[target]]),
|
|
698
|
+
),
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const sections = selected.map((target) =>
|
|
705
|
+
selected.length > 1
|
|
706
|
+
? `## ${TARGET_LABELS[target]}\n\n${prompts[target]}`
|
|
707
|
+
: prompts[target],
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
humanStream(output).write(`${sections.join("\n\n")}\n`);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
const migrate = configureOutput(
|
|
714
|
+
program.command("migrate", { hidden: true }),
|
|
715
|
+
"plain",
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
configureJsonOutput(
|
|
719
|
+
migrate.command("apply").option("--force"),
|
|
720
|
+
"plain",
|
|
721
|
+
).action(
|
|
722
|
+
(_options: { force?: boolean; json?: boolean }, command: Command) => {
|
|
723
|
+
command
|
|
724
|
+
.showHelpAfterError(false)
|
|
725
|
+
.error(
|
|
726
|
+
"Markdown review migration is no longer supported. Ask your agent to migrate your old Whiteboard reviews.",
|
|
727
|
+
);
|
|
728
|
+
},
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
const share = configureJsonOutput(
|
|
732
|
+
program
|
|
733
|
+
.command("share")
|
|
734
|
+
.description(
|
|
735
|
+
"Upload an immutable review snapshot and return its share link",
|
|
736
|
+
),
|
|
737
|
+
"plain",
|
|
738
|
+
)
|
|
739
|
+
.option("--review <id>", "review ID")
|
|
740
|
+
.option("--version <number>", "Saved version to share")
|
|
741
|
+
.option("--preview", "Open the share link in Whiteboard Preview by default")
|
|
742
|
+
.option(
|
|
743
|
+
"--request-id <uuid>",
|
|
744
|
+
"Reuse this ID when retrying the same immutable upload",
|
|
745
|
+
)
|
|
746
|
+
.action(
|
|
747
|
+
async (options: {
|
|
748
|
+
session?: string;
|
|
749
|
+
version?: string;
|
|
750
|
+
requestId?: string;
|
|
751
|
+
preview?: boolean;
|
|
752
|
+
json?: boolean;
|
|
753
|
+
}) => {
|
|
754
|
+
const { runShareCli } = await import("./sharing/cli.js");
|
|
755
|
+
state.exitCode = await runShareCli({
|
|
756
|
+
...input,
|
|
757
|
+
...options,
|
|
758
|
+
env: authoringEnv(),
|
|
759
|
+
});
|
|
760
|
+
},
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
configureJsonOutput(
|
|
764
|
+
share
|
|
765
|
+
.command("revoke <share-id>")
|
|
766
|
+
.description("Revoke future downloads of a share"),
|
|
767
|
+
"plain",
|
|
768
|
+
).action(async (shareId: string, options: { json?: boolean }) => {
|
|
769
|
+
const { runShareCli } = await import("./sharing/cli.js");
|
|
770
|
+
state.exitCode = await runShareCli({
|
|
771
|
+
...input,
|
|
772
|
+
...options,
|
|
773
|
+
revoke: shareId,
|
|
774
|
+
env: authoringEnv(),
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
// Hosted trace store login. Logging in authenticates a user; it selects
|
|
779
|
+
// no storage by itself.
|
|
780
|
+
configureJsonOutput(
|
|
781
|
+
program.command("login").description("Log in to Whiteboard with GitHub"),
|
|
782
|
+
"plain",
|
|
783
|
+
)
|
|
784
|
+
.option("--origin <url>", "Whiteboard service origin", DEFAULT_STORE_ORIGIN)
|
|
785
|
+
.option("--traces", "Also authorize GitHub repositories for hosted traces")
|
|
786
|
+
.option("--no-browser", "Print the URL instead of opening a browser")
|
|
787
|
+
.action(
|
|
788
|
+
async (options: {
|
|
789
|
+
origin?: string;
|
|
790
|
+
browser?: boolean;
|
|
791
|
+
traces?: boolean;
|
|
792
|
+
json?: boolean;
|
|
793
|
+
}) => {
|
|
794
|
+
state.exitCode = await runtime.runStoreLogin({
|
|
795
|
+
origin: options.origin,
|
|
796
|
+
noBrowser: options.browser === false,
|
|
797
|
+
traces: options.traces === true,
|
|
798
|
+
json: options.json,
|
|
799
|
+
stdout: input.stdout,
|
|
800
|
+
stderr: input.stderr,
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
if (state.exitCode === 0)
|
|
804
|
+
await attemptTelemetry(() =>
|
|
805
|
+
aliasInstallationToAccount(telemetry, env),
|
|
806
|
+
);
|
|
807
|
+
},
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
program
|
|
811
|
+
.command("logout")
|
|
812
|
+
.description("Forget the hosted trace store login")
|
|
813
|
+
.action(async () => {
|
|
814
|
+
state.exitCode = await runtime.runStoreLogout({ stdout: input.stdout });
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
configureJsonOutput(
|
|
818
|
+
program.command("whoami").description("Show the hosted trace store login"),
|
|
819
|
+
"plain",
|
|
820
|
+
).action(async (options: { json?: boolean }) => {
|
|
821
|
+
state.exitCode = await runtime.runStoreWhoami({
|
|
822
|
+
json: options.json,
|
|
823
|
+
stdout: input.stdout,
|
|
824
|
+
stderr: input.stderr,
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
// The trace surface: inspect storage, manage one repository, or read events.
|
|
829
|
+
const trace = configureOutput(
|
|
830
|
+
program.command("trace").description("Manage agent traces"),
|
|
831
|
+
"plain",
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
registerTraceCommands(trace, {
|
|
835
|
+
runtime,
|
|
836
|
+
traceCommand,
|
|
837
|
+
scope,
|
|
838
|
+
cwd,
|
|
839
|
+
stdin: input.stdin,
|
|
840
|
+
stdout: input.stdout,
|
|
841
|
+
stderr: input.stderr,
|
|
842
|
+
configureOutput: (command) => configureOutput(command, "plain"),
|
|
843
|
+
configureJsonOutput: (command) => configureJsonOutput(command, "plain"),
|
|
844
|
+
verifyCommand: "whiteboard trace status",
|
|
845
|
+
setExitCode: (code) => {
|
|
846
|
+
state.exitCode = code;
|
|
847
|
+
},
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
// Storage selection and configuration migration write only the shared
|
|
851
|
+
// trace config; legacy files and remote objects are never touched.
|
|
852
|
+
const traceStorage = configureOutput(
|
|
853
|
+
trace.command("storage").description("Select the trace store"),
|
|
854
|
+
"plain",
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
configureJsonOutput(
|
|
858
|
+
traceStorage
|
|
859
|
+
.command("use <mode>")
|
|
860
|
+
.description("Select the s3 (S3/R2 bucket) or hosted trace store")
|
|
861
|
+
.option("--origin <url>", "hosted store origin")
|
|
862
|
+
.option("--endpoint <url>", "S3/R2 endpoint URL (s3)")
|
|
863
|
+
.option("--bucket <name>", "S3/R2 bucket name (s3)")
|
|
864
|
+
.option("--key <id>", "S3/R2 access key ID (s3)")
|
|
865
|
+
.option("--secret <key>", "S3/R2 secret access key (s3)")
|
|
866
|
+
.option("--region <region>", "S3/R2 signing region (s3)"),
|
|
867
|
+
"plain",
|
|
868
|
+
).action(
|
|
869
|
+
async (
|
|
870
|
+
mode: string,
|
|
871
|
+
options: {
|
|
872
|
+
origin?: string;
|
|
873
|
+
endpoint?: string;
|
|
874
|
+
bucket?: string;
|
|
875
|
+
key?: string;
|
|
876
|
+
secret?: string;
|
|
877
|
+
region?: string;
|
|
878
|
+
json?: boolean;
|
|
879
|
+
},
|
|
880
|
+
) => {
|
|
881
|
+
state.exitCode = await runtime.runTraceStorageUse({
|
|
882
|
+
cwd,
|
|
883
|
+
mode,
|
|
884
|
+
origin: options.origin,
|
|
885
|
+
endpoint: options.endpoint,
|
|
886
|
+
bucket: options.bucket,
|
|
887
|
+
key: options.key,
|
|
888
|
+
secret: options.secret,
|
|
889
|
+
region: options.region,
|
|
890
|
+
json: options.json,
|
|
891
|
+
stdout: input.stdout,
|
|
892
|
+
stderr: input.stderr,
|
|
893
|
+
});
|
|
894
|
+
},
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
const traceConfig = configureOutput(
|
|
898
|
+
trace.command("config").description("Manage trace storage configuration"),
|
|
899
|
+
"plain",
|
|
900
|
+
);
|
|
901
|
+
|
|
902
|
+
configureJsonOutput(
|
|
903
|
+
traceConfig
|
|
904
|
+
.command("migrate")
|
|
905
|
+
.description(
|
|
906
|
+
"Copy the legacy S3/R2 setup into $DEV_REVIEW_HOME/trace/config.json",
|
|
907
|
+
)
|
|
908
|
+
.option("--dry-run", "preview without writing")
|
|
909
|
+
.option(
|
|
910
|
+
"--keep-legacy",
|
|
911
|
+
"leave the legacy env and settings files in place instead of renaming them to legacy_*",
|
|
912
|
+
),
|
|
913
|
+
"plain",
|
|
914
|
+
).action(
|
|
915
|
+
async (options: {
|
|
916
|
+
dryRun?: boolean;
|
|
917
|
+
keepLegacy?: boolean;
|
|
918
|
+
json?: boolean;
|
|
919
|
+
}) => {
|
|
920
|
+
state.exitCode = await runtime.runTraceConfigMigrate({
|
|
921
|
+
dryRun: options.dryRun,
|
|
922
|
+
keepLegacy: options.keepLegacy,
|
|
923
|
+
json: options.json,
|
|
924
|
+
stdout: input.stdout,
|
|
925
|
+
stderr: input.stderr,
|
|
926
|
+
});
|
|
927
|
+
},
|
|
928
|
+
);
|
|
929
|
+
|
|
930
|
+
// Keep the established Review help order while app-only commands stay here.
|
|
931
|
+
const traceHelp = trace.createHelp();
|
|
932
|
+
|
|
933
|
+
trace.configureHelp({
|
|
934
|
+
visibleCommands: (command) => {
|
|
935
|
+
const commands = traceHelp.visibleCommands(command);
|
|
936
|
+
commands.splice(commands.indexOf(traceStorage), 1);
|
|
937
|
+
commands.splice(1, 0, traceStorage);
|
|
938
|
+
commands.splice(commands.indexOf(traceConfig), 1);
|
|
939
|
+
commands.splice(6, 0, traceConfig);
|
|
940
|
+
|
|
941
|
+
return commands;
|
|
942
|
+
},
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
// The JSON authoring surface is owned by review-api/agent-cli.ts. Commander
|
|
946
|
+
// passes the tool name and JSON payload through untouched, so these commands
|
|
947
|
+
// share the top-level help, the leading `--json` form, and the telemetry
|
|
948
|
+
// hooks without a separate parser.
|
|
949
|
+
for (const [name, description] of [
|
|
950
|
+
["api", "Call a JSON review authoring tool on the running server"],
|
|
951
|
+
["mcp", "Serve the JSON review authoring tools over stdio MCP"],
|
|
952
|
+
] as const) {
|
|
953
|
+
configureOutput(
|
|
954
|
+
program
|
|
955
|
+
.command(name)
|
|
956
|
+
.description(description)
|
|
957
|
+
.argument("[args...]", "tool name and JSON input")
|
|
958
|
+
.allowUnknownOption()
|
|
959
|
+
.allowExcessArguments()
|
|
960
|
+
.helpOption(false)
|
|
961
|
+
.passThroughOptions()
|
|
962
|
+
.addHelpText("after", `\n${reviewAgentCliHelp}`),
|
|
963
|
+
"plain",
|
|
964
|
+
).action(async (args: string[]) => {
|
|
965
|
+
const { runReviewAgentCli } = await import("./review-api/agent-cli.js");
|
|
966
|
+
state.exitCode = await runReviewAgentCli({
|
|
967
|
+
...input,
|
|
968
|
+
env: authoringEnv(),
|
|
969
|
+
argv: [name, ...args],
|
|
970
|
+
onFailure: (error) => {
|
|
971
|
+
activeCause = error;
|
|
972
|
+
},
|
|
973
|
+
onDesktop: (appVersion) => telemetry.setDesktopVersion(appVersion),
|
|
974
|
+
onToolCall: (call) =>
|
|
975
|
+
attemptTelemetry(() => telemetry.captureToolCalled(call)),
|
|
976
|
+
});
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
program.hook("preAction", async (_command, actionCommand) => {
|
|
981
|
+
// The parsed option is authoritative once parsing succeeds. The argv scan
|
|
982
|
+
// that seeded state.json only has to cover parse failures.
|
|
983
|
+
if (actionCommand.optsWithGlobals().json === true) {
|
|
984
|
+
state.json = true;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const command = telemetryCommandPath(actionCommand);
|
|
988
|
+
|
|
989
|
+
if (!command) return;
|
|
990
|
+
const commandRunId = telemetry.createCommandRunId();
|
|
991
|
+
setTraceAttribute("command", command);
|
|
992
|
+
setTraceAttribute("commandRunId", commandRunId);
|
|
993
|
+
telemetry.setSurface(commandSurface(command));
|
|
994
|
+
activeTelemetry = {
|
|
995
|
+
command,
|
|
996
|
+
commandRunId,
|
|
997
|
+
startedAt: Date.now(),
|
|
998
|
+
finished: false,
|
|
999
|
+
};
|
|
1000
|
+
await attemptTelemetry(() => telemetry.captureInstallationCreated());
|
|
1001
|
+
await attemptTelemetry(() =>
|
|
1002
|
+
telemetry.captureCommandStarted({ command, commandRunId }),
|
|
1003
|
+
);
|
|
1004
|
+
});
|
|
1005
|
+
program.hook("postAction", async () => {
|
|
1006
|
+
await finishActiveTelemetry(
|
|
1007
|
+
telemetry,
|
|
1008
|
+
activeTelemetry,
|
|
1009
|
+
state.exitCode,
|
|
1010
|
+
activeCause,
|
|
1011
|
+
telemetryProperties,
|
|
1012
|
+
);
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
try {
|
|
1016
|
+
await withStoreAuthorization(
|
|
1017
|
+
async (origin) => {
|
|
1018
|
+
if (
|
|
1019
|
+
state.json ||
|
|
1020
|
+
input.stdin?.isTTY !== true ||
|
|
1021
|
+
input.argv[0] !== "trace" ||
|
|
1022
|
+
input.argv.some((arg) => /hook/i.test(arg))
|
|
1023
|
+
)
|
|
1024
|
+
return undefined;
|
|
1025
|
+
|
|
1026
|
+
const prompt = createInterface({
|
|
1027
|
+
input: input.stdin,
|
|
1028
|
+
output: input.stderr,
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
let answer: string;
|
|
1032
|
+
|
|
1033
|
+
try {
|
|
1034
|
+
answer = await prompt.question(
|
|
1035
|
+
"Authorize GitHub repositories for hosted traces? [y/N] ",
|
|
1036
|
+
);
|
|
1037
|
+
} finally {
|
|
1038
|
+
prompt.close();
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
if (!/^y(es)?$/i.test(answer.trim())) return undefined;
|
|
1042
|
+
|
|
1043
|
+
const code = await runtime.runStoreLogin({
|
|
1044
|
+
origin,
|
|
1045
|
+
traces: true,
|
|
1046
|
+
stdout: input.stdout,
|
|
1047
|
+
stderr: input.stderr,
|
|
1048
|
+
env: input.env,
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
return code === 0 ? (await readStoreAuth(input.env))?.token : undefined;
|
|
1052
|
+
},
|
|
1053
|
+
() => program.parseAsync(input.argv, { from: "user" }),
|
|
1054
|
+
);
|
|
1055
|
+
|
|
1056
|
+
return state.exitCode;
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
if (error instanceof CommanderError) {
|
|
1059
|
+
if (error.exitCode === 0) {
|
|
1060
|
+
await captureOneOffCommand(
|
|
1061
|
+
telemetry,
|
|
1062
|
+
input.argv.includes("--version") || input.argv.includes("-V")
|
|
1063
|
+
? "version"
|
|
1064
|
+
: "help",
|
|
1065
|
+
0,
|
|
1066
|
+
);
|
|
1067
|
+
|
|
1068
|
+
return 0;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const surface = state.parseSurface;
|
|
1072
|
+
|
|
1073
|
+
// stdout carries the parseable failure whenever the caller asked for
|
|
1074
|
+
// JSON; stderr keeps the commander message and the help that
|
|
1075
|
+
// showHelpAfterError() produced, because a human may be reading too.
|
|
1076
|
+
if (state.json || surface === "review") {
|
|
1077
|
+
emitReviewEvent(input.stdout, {
|
|
1078
|
+
event: "error",
|
|
1079
|
+
error: {
|
|
1080
|
+
name: "ReviewCliUsageError",
|
|
1081
|
+
message: commanderErrorMessage(error),
|
|
1082
|
+
},
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (surface !== "review") {
|
|
1087
|
+
input.stderr.write(
|
|
1088
|
+
state.parserErrorOutput || ensureTrailingNewline(error.message),
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
await finishActiveTelemetry(
|
|
1093
|
+
telemetry,
|
|
1094
|
+
activeTelemetry,
|
|
1095
|
+
1,
|
|
1096
|
+
error,
|
|
1097
|
+
telemetryProperties,
|
|
1098
|
+
);
|
|
1099
|
+
|
|
1100
|
+
if (!activeTelemetry) {
|
|
1101
|
+
await captureOneOffCommand(telemetry, "invalid", 1, error);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
return 1;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (state.json) {
|
|
1108
|
+
const serialized: ReturnType<typeof serializeReviewError> & {
|
|
1109
|
+
code?: string;
|
|
1110
|
+
remedy?: string;
|
|
1111
|
+
} = serializeReviewError(error);
|
|
1112
|
+
|
|
1113
|
+
if (
|
|
1114
|
+
error instanceof StoreApiError &&
|
|
1115
|
+
error.code === "repository_authorization_required"
|
|
1116
|
+
) {
|
|
1117
|
+
serialized.code = error.code;
|
|
1118
|
+
serialized.remedy = "whiteboard login --traces";
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
emitReviewEvent(input.stdout, { event: "error", error: serialized });
|
|
1122
|
+
} else {
|
|
1123
|
+
input.stderr.write(ensureTrailingNewline(formatCliError(error)));
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
await finishActiveTelemetry(
|
|
1127
|
+
telemetry,
|
|
1128
|
+
activeTelemetry,
|
|
1129
|
+
1,
|
|
1130
|
+
error,
|
|
1131
|
+
telemetryProperties,
|
|
1132
|
+
);
|
|
1133
|
+
|
|
1134
|
+
return 1;
|
|
1135
|
+
} finally {
|
|
1136
|
+
await attemptTelemetry(() => telemetry.shutdown(1_000));
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const TARGET_LABELS: Record<InstallTarget, string> = {
|
|
1141
|
+
claude: "Claude Code",
|
|
1142
|
+
codex: "Codex",
|
|
1143
|
+
cursor: "Cursor",
|
|
1144
|
+
opencode: "OpenCode",
|
|
1145
|
+
pi: "Pi",
|
|
1146
|
+
omp: "oh-my-pi",
|
|
1147
|
+
copilot: "Copilot CLI",
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
function parseTargets(targets: readonly string[]): InstallTarget[] {
|
|
1151
|
+
if (targets.length === 0 || targets.includes("all")) {
|
|
1152
|
+
return [...ALL_INSTALL_TARGETS];
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
return [
|
|
1156
|
+
...new Set(
|
|
1157
|
+
targets
|
|
1158
|
+
.values()
|
|
1159
|
+
.map((target) => (target === "claude-code" ? "claude" : target))
|
|
1160
|
+
.filter(isInstallTarget),
|
|
1161
|
+
),
|
|
1162
|
+
];
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function reviewCliRuntime(
|
|
1166
|
+
overrides: Partial<ReviewCliRuntime> | undefined,
|
|
1167
|
+
): ReviewCliRuntime {
|
|
1168
|
+
return {
|
|
1169
|
+
runReviewAppLaunch,
|
|
1170
|
+
runReviewAppPick,
|
|
1171
|
+
runReviewInfo,
|
|
1172
|
+
runTraceStatus,
|
|
1173
|
+
runTraceEnable,
|
|
1174
|
+
runTraceDisable,
|
|
1175
|
+
runTraceRepair,
|
|
1176
|
+
runTraceList,
|
|
1177
|
+
runTraceShow,
|
|
1178
|
+
runTracePull,
|
|
1179
|
+
runTraceBlame,
|
|
1180
|
+
runTraceHook,
|
|
1181
|
+
runTraceGitHook,
|
|
1182
|
+
runTraceSync,
|
|
1183
|
+
runTraceStorageUse,
|
|
1184
|
+
runTraceConfigMigrate,
|
|
1185
|
+
runTraceInstallMachine,
|
|
1186
|
+
runTraceOnboard,
|
|
1187
|
+
runTraceStoreDelete,
|
|
1188
|
+
runTraceStoreInfo,
|
|
1189
|
+
runTraceSessions,
|
|
1190
|
+
runTraceAllow,
|
|
1191
|
+
runTraceDeny,
|
|
1192
|
+
runStoreLogin,
|
|
1193
|
+
runStoreLogout,
|
|
1194
|
+
runStoreWhoami,
|
|
1195
|
+
...overrides,
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function reviewTopLevelHelp(): string {
|
|
1200
|
+
return [
|
|
1201
|
+
"",
|
|
1202
|
+
"Use `whiteboard info` to discover review documents for this checkout.",
|
|
1203
|
+
"Reviews are authored through the JSON API: `whiteboard api tools` lists the tools, and `whiteboard mcp` serves the same catalog to an agent.",
|
|
1204
|
+
"Use `whiteboard app launch` to start Whiteboard Desktop. Use `whiteboard app pick --session <uuid>` to open one.",
|
|
1205
|
+
"Use `whiteboard server start` for headless authoring, and `whiteboard server status --json` to check readiness.",
|
|
1206
|
+
"",
|
|
1207
|
+
"Every command accepts --json. Stdout then carries only JSON events, one per line,",
|
|
1208
|
+
"human progress moves to stderr, and a failure prints a JSON error event too.",
|
|
1209
|
+
"",
|
|
1210
|
+
"Example agent prompt (for a repository that provides a CI/CD system):",
|
|
1211
|
+
"",
|
|
1212
|
+
" Can you use Whiteboard to explain this repository's CI/CD system to me?",
|
|
1213
|
+
"",
|
|
1214
|
+
" My current understanding:",
|
|
1215
|
+
"",
|
|
1216
|
+
" 1. CI/CD can be configured entirely in JavaScript. There is no YAML;",
|
|
1217
|
+
" everything is code.",
|
|
1218
|
+
" 2. I expect it to look somewhat like Dagger, where user-authored code",
|
|
1219
|
+
" makes RPC-style calls into a build system.",
|
|
1220
|
+
"",
|
|
1221
|
+
" I have a lot of questions, so please start at a high level:",
|
|
1222
|
+
"",
|
|
1223
|
+
" 1. Give me a two-sentence introduction, followed by two or three goals",
|
|
1224
|
+
" and non-goals for the repository.",
|
|
1225
|
+
" 2. Explain the CI/CD APIs it exposes and how a user would use them.",
|
|
1226
|
+
" 3. Show sequence diagrams for the main user flows, including setting up",
|
|
1227
|
+
" a pipeline and pushing a change.",
|
|
1228
|
+
" 4. Show database views for the Cloudflare D1 and Worker access patterns,",
|
|
1229
|
+
" with walkthroughs linked to the relevant code.",
|
|
1230
|
+
"",
|
|
1231
|
+
" Start concise and let me dig deeper through the canvas.",
|
|
1232
|
+
].join("\n");
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
async function finishActiveTelemetry(
|
|
1236
|
+
telemetry: ReviewCommandTelemetry,
|
|
1237
|
+
active:
|
|
1238
|
+
| {
|
|
1239
|
+
command: ReviewCliCommandPath;
|
|
1240
|
+
commandRunId: string;
|
|
1241
|
+
startedAt: number;
|
|
1242
|
+
finished: boolean;
|
|
1243
|
+
}
|
|
1244
|
+
| undefined,
|
|
1245
|
+
exitCode: number,
|
|
1246
|
+
cause?: unknown,
|
|
1247
|
+
properties: Record<string, boolean | number | string | null | undefined> = {},
|
|
1248
|
+
): Promise<void> {
|
|
1249
|
+
if (!active || active.finished) return;
|
|
1250
|
+
active.finished = true;
|
|
1251
|
+
const classification = errorClassification(active.command, cause);
|
|
1252
|
+
await attemptTelemetry(() =>
|
|
1253
|
+
exitCode === 0
|
|
1254
|
+
? telemetry.captureCommandSucceeded({
|
|
1255
|
+
command: active.command,
|
|
1256
|
+
commandRunId: active.commandRunId,
|
|
1257
|
+
exitCode,
|
|
1258
|
+
durationMs: Date.now() - active.startedAt,
|
|
1259
|
+
properties,
|
|
1260
|
+
})
|
|
1261
|
+
: telemetry.captureCommandFailed({
|
|
1262
|
+
command: active.command,
|
|
1263
|
+
commandRunId: active.commandRunId,
|
|
1264
|
+
exitCode,
|
|
1265
|
+
durationMs: Date.now() - active.startedAt,
|
|
1266
|
+
properties,
|
|
1267
|
+
...classification,
|
|
1268
|
+
}),
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
async function captureOneOffCommand(
|
|
1273
|
+
telemetry: ReviewCommandTelemetry,
|
|
1274
|
+
command: ReviewCliCommandPath,
|
|
1275
|
+
exitCode: number,
|
|
1276
|
+
cause?: unknown,
|
|
1277
|
+
): Promise<void> {
|
|
1278
|
+
const commandRunId = telemetry.createCommandRunId();
|
|
1279
|
+
await attemptTelemetry(() => telemetry.captureInstallationCreated());
|
|
1280
|
+
await attemptTelemetry(() =>
|
|
1281
|
+
telemetry.captureCommandStarted({ command, commandRunId }),
|
|
1282
|
+
);
|
|
1283
|
+
const classification = errorClassification(command, cause);
|
|
1284
|
+
await attemptTelemetry(() =>
|
|
1285
|
+
exitCode === 0
|
|
1286
|
+
? telemetry.captureCommandSucceeded({
|
|
1287
|
+
command,
|
|
1288
|
+
commandRunId,
|
|
1289
|
+
exitCode,
|
|
1290
|
+
durationMs: 0,
|
|
1291
|
+
})
|
|
1292
|
+
: telemetry.captureCommandFailed({
|
|
1293
|
+
command,
|
|
1294
|
+
commandRunId,
|
|
1295
|
+
exitCode,
|
|
1296
|
+
durationMs: 0,
|
|
1297
|
+
...classification,
|
|
1298
|
+
}),
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function telemetryCommandPath(
|
|
1303
|
+
command: Command,
|
|
1304
|
+
): ReviewCliCommandPath | undefined {
|
|
1305
|
+
const name = command.name();
|
|
1306
|
+
const parent = command.parent?.name();
|
|
1307
|
+
|
|
1308
|
+
if (parent === "migrate" && name === "apply") return "migrate.apply";
|
|
1309
|
+
|
|
1310
|
+
if (parent === "trace") {
|
|
1311
|
+
if (name === "allow" || name === "deny" || name === "install") {
|
|
1312
|
+
return `trace.${name}`;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (parent === "store" && command.parent?.parent?.name() === "trace") {
|
|
1317
|
+
if (name === "create" || name === "delete" || name === "info") {
|
|
1318
|
+
return `trace.store.${name}`;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
if (parent === "storage" && name === "use") return "trace.storage.use";
|
|
1323
|
+
|
|
1324
|
+
if (parent === "config" && name === "migrate") return "trace.config.migrate";
|
|
1325
|
+
|
|
1326
|
+
if (parent === "server" && name === "start") return "server.start";
|
|
1327
|
+
|
|
1328
|
+
if (name === "login" || name === "logout" || name === "whoami") return name;
|
|
1329
|
+
|
|
1330
|
+
if (name === "api" || name === "mcp" || name === "connect") return name;
|
|
1331
|
+
|
|
1332
|
+
if (parent === "app" && (name === "launch" || name === "pick")) {
|
|
1333
|
+
return `app.${name}`;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
if (parent === "instances" && (name === "use" || name === "clear"))
|
|
1337
|
+
return `instances.${name}`;
|
|
1338
|
+
|
|
1339
|
+
if (name === "version" || name === "info" || name === "instances") {
|
|
1340
|
+
return name;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// Bare `app` takes no --session; picking is only `app pick`.
|
|
1344
|
+
if (name === "app") return "app.launch";
|
|
1345
|
+
|
|
1346
|
+
return undefined;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function commandSurface(command: ReviewCliCommandPath): ReviewTelemetrySurface {
|
|
1350
|
+
if (command === "server.start") return "headless";
|
|
1351
|
+
|
|
1352
|
+
if (command === "mcp") return "mcp";
|
|
1353
|
+
|
|
1354
|
+
if (command === "api") return "api";
|
|
1355
|
+
|
|
1356
|
+
return "cli";
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
interface ErrorClassification {
|
|
1360
|
+
errorName: ReviewTelemetryErrorName;
|
|
1361
|
+
errorCategory: ReviewTelemetryErrorCategory;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
function errorClassification(
|
|
1365
|
+
command: ReviewCliCommandPath,
|
|
1366
|
+
cause: unknown,
|
|
1367
|
+
): ErrorClassification {
|
|
1368
|
+
if (cause instanceof CommanderError || command === "invalid") {
|
|
1369
|
+
return { errorName: "usage_error", errorCategory: "user_input" };
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
if (
|
|
1373
|
+
cause instanceof ReviewCliUsageError ||
|
|
1374
|
+
cause instanceof ReviewAppUsageError
|
|
1375
|
+
) {
|
|
1376
|
+
return { errorName: "usage_error", errorCategory: "user_input" };
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
if (cause instanceof ReviewAppStateError) {
|
|
1380
|
+
return {
|
|
1381
|
+
errorName: "review_state_error",
|
|
1382
|
+
errorCategory: "local_state",
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
if (cause instanceof ReviewInstanceUnavailableError) {
|
|
1387
|
+
return {
|
|
1388
|
+
errorName: "desktop_connection_error",
|
|
1389
|
+
errorCategory: "dependency",
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
if (cause instanceof ReviewApiError) {
|
|
1394
|
+
if (cause.status === 404)
|
|
1395
|
+
return { errorName: "review_not_found", errorCategory: "local_state" };
|
|
1396
|
+
|
|
1397
|
+
if (cause.status >= 500)
|
|
1398
|
+
return { errorName: "unexpected_error", errorCategory: "internal" };
|
|
1399
|
+
|
|
1400
|
+
return { errorName: "usage_error", errorCategory: "user_input" };
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
if (cause instanceof Error && "code" in cause) {
|
|
1404
|
+
const code = cause.code;
|
|
1405
|
+
|
|
1406
|
+
if (code === "EADDRINUSE" || code === "ECONNREFUSED") {
|
|
1407
|
+
return { errorName: "network_error", errorCategory: "transport" };
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
if (command === "api" && cause instanceof TypeError) {
|
|
1412
|
+
return { errorName: "network_error", errorCategory: "transport" };
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
const name = cause instanceof Error ? cause.name.toLowerCase() : "";
|
|
1416
|
+
|
|
1417
|
+
if (name.includes("notfound")) {
|
|
1418
|
+
return { errorName: "review_not_found", errorCategory: "local_state" };
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
if (command.startsWith("app.")) {
|
|
1422
|
+
return {
|
|
1423
|
+
errorName: "desktop_connection_error",
|
|
1424
|
+
errorCategory: "dependency",
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
if (command === "info") {
|
|
1429
|
+
return { errorName: "review_state_error", errorCategory: "local_state" };
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
if (cause) {
|
|
1433
|
+
return { errorName: "unexpected_error", errorCategory: "internal" };
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
return { errorName: "process_error", errorCategory: "dependency" };
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
async function attemptTelemetry(fn: () => Promise<void>): Promise<void> {
|
|
1440
|
+
try {
|
|
1441
|
+
await span("telemetry capture", fn);
|
|
1442
|
+
} catch {
|
|
1443
|
+
// Telemetry must never affect CLI behavior.
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
function commanderErrorMessage(error: CommanderError): string {
|
|
1448
|
+
return error.message.replace(/^error:\s*/i, "");
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
function formatCliError(cause: unknown): string {
|
|
1452
|
+
if (cause instanceof Error) {
|
|
1453
|
+
return cause.stack || `${cause.name}: ${cause.message}`;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
return String(cause);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function ensureTrailingNewline(value: string): string {
|
|
1460
|
+
return value.endsWith("\n") ? value : `${value}\n`;
|
|
1461
|
+
}
|