@dev.fast/whiteboard 0.0.0-stage → 0.2.1-preview.20261005.98
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,134 @@
|
|
|
1
|
+
import type { Writable } from "node:stream";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ReviewApiClient,
|
|
5
|
+
type ReviewApiSummary,
|
|
6
|
+
} from "@dev.fast/review-protocol";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type ReviewInstanceSelection,
|
|
10
|
+
healthyReviewInstance,
|
|
11
|
+
reviewInstanceUnavailable,
|
|
12
|
+
selectReviewInstance,
|
|
13
|
+
} from "./desktop-discovery";
|
|
14
|
+
import { focusReviewDesktop, runReviewAppLaunch } from "./review-app-launcher";
|
|
15
|
+
import { pickReview } from "./review-app-picker";
|
|
16
|
+
import { resolveReviewRoot } from "./runtime";
|
|
17
|
+
|
|
18
|
+
interface ReviewAppRuntime {
|
|
19
|
+
launch: typeof runReviewAppLaunch;
|
|
20
|
+
selectInstance: () => Promise<ReviewInstanceSelection>;
|
|
21
|
+
resolveReviewRoot: typeof resolveReviewRoot;
|
|
22
|
+
pickReview: typeof pickReview;
|
|
23
|
+
fetch: typeof globalThis.fetch;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class ReviewAppUsageError extends Error {
|
|
27
|
+
readonly name = "ReviewAppUsageError";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class ReviewAppStateError extends Error {
|
|
31
|
+
readonly name = "ReviewAppStateError";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RunReviewAppInput {
|
|
35
|
+
cwd: string;
|
|
36
|
+
reviewUuid?: string;
|
|
37
|
+
/** Bring Whiteboard Desktop forward. */
|
|
38
|
+
focus?: boolean;
|
|
39
|
+
stdin: NodeJS.ReadStream;
|
|
40
|
+
stdout: Writable;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ReviewAppEvent {
|
|
44
|
+
event: "app";
|
|
45
|
+
action: "pick";
|
|
46
|
+
reviewUuid: string;
|
|
47
|
+
title: string;
|
|
48
|
+
cancelled?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function runReviewAppPick(
|
|
52
|
+
input: RunReviewAppInput,
|
|
53
|
+
overrides: Partial<ReviewAppRuntime> = {},
|
|
54
|
+
): Promise<ReviewAppEvent | null> {
|
|
55
|
+
const fetch = overrides.fetch ?? globalThis.fetch;
|
|
56
|
+
|
|
57
|
+
const runtime = {
|
|
58
|
+
launch: runReviewAppLaunch,
|
|
59
|
+
selectInstance: () => selectReviewInstance({ fetch }),
|
|
60
|
+
resolveReviewRoot,
|
|
61
|
+
pickReview,
|
|
62
|
+
fetch,
|
|
63
|
+
...overrides,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Only `whiteboard app launch` may recover a stale or incompatible record; the
|
|
67
|
+
// other verbs report the diagnosis rather than start a second Desktop. No
|
|
68
|
+
// record at all means nothing is running, which launching does fix.
|
|
69
|
+
let selection = await runtime.selectInstance();
|
|
70
|
+
let launched = false;
|
|
71
|
+
|
|
72
|
+
if (!selection.instance && !selection.problem) {
|
|
73
|
+
await runtime.launch({ focus: input.focus });
|
|
74
|
+
launched = true;
|
|
75
|
+
selection = await runtime.selectInstance();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const discovery = healthyReviewInstance(selection);
|
|
79
|
+
|
|
80
|
+
if (!discovery) throw reviewInstanceUnavailable(selection);
|
|
81
|
+
|
|
82
|
+
const client = new ReviewApiClient(
|
|
83
|
+
{ serverUrl: discovery.url, token: discovery.token },
|
|
84
|
+
runtime.fetch,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
let review: Pick<ReviewApiSummary, "reviewId" | "title">;
|
|
88
|
+
|
|
89
|
+
if (input.reviewUuid) {
|
|
90
|
+
// Without `full`, GET /reviews-api/:id answers inspectSnapshot(): block
|
|
91
|
+
// descriptors with no reviewId or title.
|
|
92
|
+
review = await client.read(
|
|
93
|
+
`/${encodeURIComponent(input.reviewUuid)}?full=true`,
|
|
94
|
+
);
|
|
95
|
+
} else {
|
|
96
|
+
if (!input.stdin.isTTY)
|
|
97
|
+
throw new ReviewAppUsageError(
|
|
98
|
+
"whiteboard app pick needs a terminal without --session. Pass --session <uuid> or run it in a terminal.",
|
|
99
|
+
);
|
|
100
|
+
const root = await runtime.resolveReviewRoot(input.cwd);
|
|
101
|
+
|
|
102
|
+
const reviews = (await client.read<ReviewApiSummary[]>(""))
|
|
103
|
+
.filter((review) => review.repositoryPath === root && !review.dismissedAt)
|
|
104
|
+
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
105
|
+
|
|
106
|
+
if (!reviews.length) throw new ReviewAppStateError("No review to show.");
|
|
107
|
+
|
|
108
|
+
const picked = await runtime.pickReview(
|
|
109
|
+
reviews.map((review) => ({
|
|
110
|
+
uuid: review.reviewId,
|
|
111
|
+
title: review.title,
|
|
112
|
+
status: review.viewedAt ? "viewed" : "new",
|
|
113
|
+
lastPublishedAt: review.createdAt,
|
|
114
|
+
})),
|
|
115
|
+
input,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (!picked) return null;
|
|
119
|
+
review = { reviewId: picked.uuid, title: picked.title };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await client.post(`/${encodeURIComponent(review.reviewId)}/open`, {});
|
|
123
|
+
|
|
124
|
+
// A focused fresh launch already came forward; a running one must be asked.
|
|
125
|
+
if (input.focus && !launched)
|
|
126
|
+
await focusReviewDesktop(discovery, runtime.fetch);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
event: "app",
|
|
130
|
+
action: "pick",
|
|
131
|
+
reviewUuid: review.reviewId,
|
|
132
|
+
title: review.title,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
chmod,
|
|
5
|
+
copyFile,
|
|
6
|
+
mkdir,
|
|
7
|
+
rename,
|
|
8
|
+
rm,
|
|
9
|
+
stat,
|
|
10
|
+
writeFile,
|
|
11
|
+
} from "node:fs/promises";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { Writable } from "node:stream";
|
|
14
|
+
import { pipeline } from "node:stream/promises";
|
|
15
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
16
|
+
|
|
17
|
+
import { isJsonObject, parseJsonText } from "@dev.fast/review-protocol";
|
|
18
|
+
|
|
19
|
+
import { devReviewHome } from "./review-home-paths";
|
|
20
|
+
|
|
21
|
+
interface EnsureBundledToolInput {
|
|
22
|
+
tool: string;
|
|
23
|
+
sourcePath?: string;
|
|
24
|
+
env?: NodeJS.ProcessEnv;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type EnsureBundledToolResult = "staged" | "fresh" | "no-source";
|
|
28
|
+
|
|
29
|
+
interface ReviewToolIdentity {
|
|
30
|
+
tool: string;
|
|
31
|
+
platform: string;
|
|
32
|
+
sha256: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function reviewToolsRoot(env: NodeJS.ProcessEnv = process.env): string {
|
|
36
|
+
return path.join(devReviewHome(env), "review-tools");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function ensureBundledTool(
|
|
40
|
+
input: EnsureBundledToolInput,
|
|
41
|
+
): Promise<EnsureBundledToolResult> {
|
|
42
|
+
if (!/^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/.test(input.tool)) {
|
|
43
|
+
throw new Error(`Invalid bundled tool name: ${input.tool}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const sourcePath = input.sourcePath?.trim();
|
|
47
|
+
|
|
48
|
+
if (!sourcePath) return "no-source";
|
|
49
|
+
const source = path.resolve(sourcePath);
|
|
50
|
+
|
|
51
|
+
if (!(await isExecutableFile(source))) return "no-source";
|
|
52
|
+
|
|
53
|
+
const env = input.env ?? process.env;
|
|
54
|
+
const platform = reviewToolPlatform();
|
|
55
|
+
const executable = stagedToolPath(env, input.tool, platform);
|
|
56
|
+
const destination = path.dirname(executable);
|
|
57
|
+
const sha256 = await hashFile(source);
|
|
58
|
+
|
|
59
|
+
const identity: ReviewToolIdentity = {
|
|
60
|
+
tool: input.tool,
|
|
61
|
+
platform,
|
|
62
|
+
sha256,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (installedToolMatches(destination, executable, identity)) return "fresh";
|
|
66
|
+
|
|
67
|
+
await mkdir(path.dirname(destination), { recursive: true, mode: 0o700 });
|
|
68
|
+
|
|
69
|
+
return await withInstallLock(destination, async () => {
|
|
70
|
+
if (installedToolMatches(destination, executable, identity)) return "fresh";
|
|
71
|
+
const staging = `${destination}.staging-${process.pid}-${randomBytes(6).toString("hex")}`;
|
|
72
|
+
await rm(staging, { recursive: true, force: true });
|
|
73
|
+
await mkdir(staging, { recursive: true, mode: 0o700 });
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const stagedExecutable = path.join(
|
|
77
|
+
staging,
|
|
78
|
+
executableName(input.tool, platform),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
await copyFile(source, stagedExecutable);
|
|
82
|
+
|
|
83
|
+
if ((await hashFile(stagedExecutable)) !== sha256) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`The bundled ${input.tool} source changed while Whiteboard staged it.`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await chmod(stagedExecutable, 0o755);
|
|
90
|
+
await writeToolStamp(staging, identity);
|
|
91
|
+
await rm(destination, { recursive: true, force: true });
|
|
92
|
+
await rename(staging, destination);
|
|
93
|
+
|
|
94
|
+
return "staged";
|
|
95
|
+
} finally {
|
|
96
|
+
await rm(staging, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function ensureBundledRustAnalyzer(
|
|
102
|
+
input: { env?: NodeJS.ProcessEnv } = {},
|
|
103
|
+
): Promise<EnsureBundledToolResult> {
|
|
104
|
+
const env = input.env ?? process.env;
|
|
105
|
+
|
|
106
|
+
return await ensureBundledTool({
|
|
107
|
+
tool: "rust-analyzer",
|
|
108
|
+
sourcePath: env.DEV_FAST_REVIEW_RUST_ANALYZER,
|
|
109
|
+
env,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function stagedToolPath(
|
|
114
|
+
env: NodeJS.ProcessEnv,
|
|
115
|
+
tool: string,
|
|
116
|
+
platform = reviewToolPlatform(),
|
|
117
|
+
): string {
|
|
118
|
+
return path.join(
|
|
119
|
+
reviewToolsRoot(env),
|
|
120
|
+
tool,
|
|
121
|
+
"bundled",
|
|
122
|
+
platform,
|
|
123
|
+
executableName(tool, platform),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function withInstallLock<T>(
|
|
128
|
+
destination: string,
|
|
129
|
+
operation: () => Promise<T>,
|
|
130
|
+
): Promise<T> {
|
|
131
|
+
const lockPath = `${destination}.install-lock`;
|
|
132
|
+
const deadline = Date.now() + 120_000;
|
|
133
|
+
|
|
134
|
+
while (true) {
|
|
135
|
+
try {
|
|
136
|
+
await mkdir(lockPath, { mode: 0o700 });
|
|
137
|
+
break;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (
|
|
140
|
+
!(error instanceof Error && "code" in error && error.code === "EEXIST")
|
|
141
|
+
) {
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const lockAge = await stat(lockPath)
|
|
146
|
+
.then((value) => Date.now() - value.mtimeMs)
|
|
147
|
+
.catch(() => 0);
|
|
148
|
+
|
|
149
|
+
if (lockAge > 10 * 60_000) {
|
|
150
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (Date.now() >= deadline) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`Timed out waiting for another Whiteboard process to install ${path.basename(destination)}.`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
await delay(100);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
return await operation();
|
|
166
|
+
} finally {
|
|
167
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function installedToolMatches(
|
|
172
|
+
destination: string,
|
|
173
|
+
executable: string,
|
|
174
|
+
identity: ReviewToolIdentity,
|
|
175
|
+
): boolean {
|
|
176
|
+
if (!isExecutable(executable)) return false;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const stamp = parseJsonText(
|
|
180
|
+
fs.readFileSync(path.join(destination, "review-tool.json"), "utf8"),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
isJsonObject(stamp) &&
|
|
185
|
+
stamp.tool === identity.tool &&
|
|
186
|
+
stamp.platform === identity.platform &&
|
|
187
|
+
stamp.sha256 === identity.sha256
|
|
188
|
+
);
|
|
189
|
+
} catch {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function writeToolStamp(
|
|
195
|
+
destination: string,
|
|
196
|
+
identity: ReviewToolIdentity,
|
|
197
|
+
): Promise<void> {
|
|
198
|
+
await writeFile(
|
|
199
|
+
path.join(destination, "review-tool.json"),
|
|
200
|
+
`${JSON.stringify(identity, null, 2)}\n`,
|
|
201
|
+
{ encoding: "utf8", mode: 0o600 },
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function reviewToolPlatform(): string {
|
|
206
|
+
const platform =
|
|
207
|
+
process.platform === "darwin"
|
|
208
|
+
? "darwin"
|
|
209
|
+
: process.platform === "linux"
|
|
210
|
+
? "linux"
|
|
211
|
+
: process.platform;
|
|
212
|
+
|
|
213
|
+
return `${platform}-${process.arch}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function executableName(name: string, platform: string): string {
|
|
217
|
+
return platform.startsWith("win32-") ? `${name}.exe` : name;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isExecutable(filePath: string): boolean {
|
|
221
|
+
try {
|
|
222
|
+
fs.accessSync(filePath, fs.constants.X_OK);
|
|
223
|
+
|
|
224
|
+
return true;
|
|
225
|
+
} catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function isExecutableFile(filePath: string): Promise<boolean> {
|
|
231
|
+
const metadata = await stat(filePath).catch(() => undefined);
|
|
232
|
+
|
|
233
|
+
return Boolean(metadata?.isFile() && isExecutable(filePath));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function hashFile(filePath: string): Promise<string> {
|
|
237
|
+
const hash = createHash("sha256");
|
|
238
|
+
await pipeline(
|
|
239
|
+
fs.createReadStream(filePath),
|
|
240
|
+
new Writable({
|
|
241
|
+
write(chunk: Buffer, _encoding, callback) {
|
|
242
|
+
hash.update(chunk);
|
|
243
|
+
callback();
|
|
244
|
+
},
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return hash.digest("hex");
|
|
249
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { safeStorageSegment } from "./review-home-paths";
|
|
4
|
+
|
|
5
|
+
export type ReviewCheckoutRole = "head" | "base";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Root for Review-owned checkouts. A Review UUID identifies one subtree.
|
|
9
|
+
*/
|
|
10
|
+
export function reviewManagedCheckoutsDir(gitCommonDir: string): string {
|
|
11
|
+
return path.join(gitCommonDir, "dev-fast", "reviews");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Return the checkout subtree owned by one Review UUID. */
|
|
15
|
+
export function reviewManagedCheckoutRoot(
|
|
16
|
+
gitCommonDir: string,
|
|
17
|
+
reviewUuid: string,
|
|
18
|
+
): string {
|
|
19
|
+
return path.join(
|
|
20
|
+
reviewManagedCheckoutsDir(gitCommonDir),
|
|
21
|
+
safeStorageSegment(reviewUuid),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A detached checkout owned by one Review revision and source role. */
|
|
26
|
+
export function reviewManagedCheckoutDir(
|
|
27
|
+
gitCommonDir: string,
|
|
28
|
+
reviewUuid: string,
|
|
29
|
+
role: ReviewCheckoutRole,
|
|
30
|
+
commit: string,
|
|
31
|
+
): string {
|
|
32
|
+
return path.join(
|
|
33
|
+
reviewManagedCheckoutRoot(gitCommonDir, reviewUuid),
|
|
34
|
+
role,
|
|
35
|
+
safeStorageSegment(commit),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
diff as readLocalVcsDiff,
|
|
3
|
+
diffFileSummaries as readLocalVcsDiffFileSummaries,
|
|
4
|
+
splitGitPatchFiles,
|
|
5
|
+
} from "@dev.fast/local-vcs";
|
|
6
|
+
|
|
7
|
+
export interface ReviewDiffFile {
|
|
8
|
+
path: string;
|
|
9
|
+
previousPath?: string;
|
|
10
|
+
status: "added" | "modified" | "deleted" | "renamed";
|
|
11
|
+
additions: number;
|
|
12
|
+
deletions: number;
|
|
13
|
+
patch?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ReviewDiffFilesResult {
|
|
17
|
+
baseRef?: string;
|
|
18
|
+
headRef?: string;
|
|
19
|
+
files: ReviewDiffFile[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function resolveReviewDiffFiles(input: {
|
|
23
|
+
rootPath: string;
|
|
24
|
+
baseRef?: string;
|
|
25
|
+
headRef?: string;
|
|
26
|
+
contextLines?: number;
|
|
27
|
+
includePatch?: boolean;
|
|
28
|
+
paths?: string[];
|
|
29
|
+
}): Promise<ReviewDiffFilesResult> {
|
|
30
|
+
const baseRef = input.baseRef?.trim();
|
|
31
|
+
|
|
32
|
+
if (!baseRef) return { files: [] };
|
|
33
|
+
|
|
34
|
+
const headRef = input.headRef?.trim() || undefined;
|
|
35
|
+
const paths = normalizeDiffPaths(input.paths);
|
|
36
|
+
|
|
37
|
+
if (input.includePatch === false) {
|
|
38
|
+
return {
|
|
39
|
+
baseRef,
|
|
40
|
+
headRef,
|
|
41
|
+
files: await readLocalVcsDiffFileSummaries({
|
|
42
|
+
rootPath: input.rootPath,
|
|
43
|
+
baseRef,
|
|
44
|
+
headRef,
|
|
45
|
+
paths,
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const stdout = await readLocalVcsDiff({
|
|
51
|
+
rootPath: input.rootPath,
|
|
52
|
+
baseRef,
|
|
53
|
+
headRef,
|
|
54
|
+
paths,
|
|
55
|
+
contextLines: input.contextLines,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
baseRef,
|
|
60
|
+
headRef,
|
|
61
|
+
files: splitGitPatchFiles(stdout)
|
|
62
|
+
.map(({ file, patch }) => ({
|
|
63
|
+
path: file.path,
|
|
64
|
+
previousPath: file.previousPath,
|
|
65
|
+
status: file.status,
|
|
66
|
+
additions: file.additions,
|
|
67
|
+
deletions: file.deletions,
|
|
68
|
+
patch,
|
|
69
|
+
}))
|
|
70
|
+
.filter((file) => matchesDiffPath(file, paths)),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeDiffPaths(paths: string[] | undefined): string[] {
|
|
75
|
+
return [
|
|
76
|
+
...new Set(
|
|
77
|
+
(paths ?? [])
|
|
78
|
+
.map((path) => path.trim())
|
|
79
|
+
.filter((path) => path.length > 0),
|
|
80
|
+
),
|
|
81
|
+
].sort();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function matchesDiffPath(file: ReviewDiffFile, paths: string[]): boolean {
|
|
85
|
+
if (paths.length === 0) return true;
|
|
86
|
+
const candidates = new Set([file.path, file.previousPath].filter(Boolean));
|
|
87
|
+
|
|
88
|
+
return paths.some((path) => candidates.has(path));
|
|
89
|
+
}
|