@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,509 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type JsonObject,
|
|
9
|
+
type JsonValue,
|
|
10
|
+
isJsonObject,
|
|
11
|
+
isStringValue,
|
|
12
|
+
parseJsonText,
|
|
13
|
+
} from "@dev.fast/json";
|
|
14
|
+
import {
|
|
15
|
+
type ReviewDiffrConfig,
|
|
16
|
+
type ReviewDiffrProvider,
|
|
17
|
+
type ReviewDiffrSummarizerInput,
|
|
18
|
+
decodeStructuralDiffEvent,
|
|
19
|
+
reviewDiffrSummarizerInputSchema,
|
|
20
|
+
} from "@dev.fast/review-protocol";
|
|
21
|
+
import { stringify } from "smol-toml";
|
|
22
|
+
|
|
23
|
+
import { invalidateStructuralComparisons } from "./structural-comparisons.js";
|
|
24
|
+
import { diffrExecutable, diffrMissingError } from "./structural-diff";
|
|
25
|
+
|
|
26
|
+
const execFileAsync = promisify(execFile);
|
|
27
|
+
|
|
28
|
+
let writes: Promise<unknown> = Promise.resolve();
|
|
29
|
+
|
|
30
|
+
let testing = false;
|
|
31
|
+
|
|
32
|
+
function serialized<T>(operation: () => Promise<T>): Promise<T> {
|
|
33
|
+
const next = writes.then(operation, operation);
|
|
34
|
+
writes = next.catch(() => {});
|
|
35
|
+
|
|
36
|
+
return next;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function diffr(
|
|
40
|
+
args: string[],
|
|
41
|
+
rootPath?: string,
|
|
42
|
+
options: { signal?: AbortSignal; env?: NodeJS.ProcessEnv } = {},
|
|
43
|
+
): Promise<string> {
|
|
44
|
+
try {
|
|
45
|
+
const { stdout } = await execFileAsync(diffrExecutable(), args, {
|
|
46
|
+
cwd: rootPath,
|
|
47
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
48
|
+
signal: options.signal ?? AbortSignal.timeout(30_000),
|
|
49
|
+
env: options.env ?? process.env,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return stdout;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
// SAFETY: execFile rejects with an Error carrying its exit or spawn code.
|
|
55
|
+
// Do not forward its message: it includes command arguments.
|
|
56
|
+
const failure = error as NodeJS.ErrnoException;
|
|
57
|
+
|
|
58
|
+
if (failure.code === "ENOENT") throw diffrMissingError();
|
|
59
|
+
|
|
60
|
+
if (failure.name === "AbortError")
|
|
61
|
+
throw new Error("diffr timed out or was cancelled.");
|
|
62
|
+
throw new Error(
|
|
63
|
+
"diffr could not complete the operation. Check its configuration and credentials.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function values(
|
|
69
|
+
rootPath?: string,
|
|
70
|
+
reveal = false,
|
|
71
|
+
signal?: AbortSignal,
|
|
72
|
+
): Promise<JsonObject> {
|
|
73
|
+
return json(
|
|
74
|
+
await diffr(
|
|
75
|
+
["config", "show", "--json", ...(reveal ? ["--reveal"] : [])],
|
|
76
|
+
rootPath,
|
|
77
|
+
{ signal },
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function json(output: string): JsonObject {
|
|
83
|
+
try {
|
|
84
|
+
const parsed = parseJsonText(output);
|
|
85
|
+
|
|
86
|
+
if (isJsonObject(parsed)) return parsed;
|
|
87
|
+
} catch {
|
|
88
|
+
/* Do not echo config contents in parse errors. */
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw new Error("diffr configuration response is malformed.");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function valueAt(object: JsonObject, key: string): JsonValue | undefined {
|
|
95
|
+
let value: JsonValue | undefined = object;
|
|
96
|
+
|
|
97
|
+
for (const segment of key.split("."))
|
|
98
|
+
value = isJsonObject(value) ? value[segment] : undefined;
|
|
99
|
+
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const prefix = "plugins.bundled.summarize";
|
|
104
|
+
|
|
105
|
+
function savedProvider(config: JsonObject): string {
|
|
106
|
+
const value = valueAt(config, `${prefix}.provider`);
|
|
107
|
+
|
|
108
|
+
return isStringValue(value) ? value : "";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function savedEndpoint(config: JsonObject): string {
|
|
112
|
+
const value = valueAt(config, `${prefix}.endpoint`);
|
|
113
|
+
|
|
114
|
+
return isStringValue(value) ? value : "";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Whether a saved key belongs to a different destination than the draft's. */
|
|
118
|
+
function movesKey(
|
|
119
|
+
config: JsonObject,
|
|
120
|
+
draft: Pick<ReviewDiffrSummarizerInput, "provider" | "endpoint">,
|
|
121
|
+
): boolean {
|
|
122
|
+
return (
|
|
123
|
+
draft.provider !== savedProvider(config) ||
|
|
124
|
+
draft.endpoint !== savedEndpoint(config)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function environmentKey(
|
|
129
|
+
providers: ReviewDiffrProvider[],
|
|
130
|
+
id: string,
|
|
131
|
+
): string | undefined {
|
|
132
|
+
return providers
|
|
133
|
+
.find((provider) => provider.id === id)
|
|
134
|
+
?.keyVariables.map((name) => process.env[name])
|
|
135
|
+
.find((key) => !!key);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function keyOptional(
|
|
139
|
+
providers: ReviewDiffrProvider[],
|
|
140
|
+
id: string,
|
|
141
|
+
endpoint: string,
|
|
142
|
+
): boolean {
|
|
143
|
+
return (
|
|
144
|
+
endpoint !== "" &&
|
|
145
|
+
!!providers.find((provider) => provider.id === id)?.keylessCustomEndpoint
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function strings(value: JsonValue | undefined): string[] {
|
|
150
|
+
return Array.isArray(value) ? value.filter(isStringValue) : [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* What diffr's schema says about summaries: each provider's defaults, and
|
|
155
|
+
* the default prompt with the link its description gives. Nothing when
|
|
156
|
+
* diffr cannot describe its schema: the rest of Settings still works.
|
|
157
|
+
*/
|
|
158
|
+
async function summaryDefaults(
|
|
159
|
+
rootPath?: string,
|
|
160
|
+
): Promise<Pick<ReviewDiffrConfig, "providers" | "defaultPrompt">> {
|
|
161
|
+
let schema: JsonObject;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
schema = json(await diffr(["config", "schema"], rootPath));
|
|
165
|
+
} catch {
|
|
166
|
+
return {};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const options = valueAt(
|
|
170
|
+
schema,
|
|
171
|
+
"properties.plugins.properties.bundled.properties.summarize.properties",
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (!isJsonObject(options)) return {};
|
|
175
|
+
|
|
176
|
+
const option = (name: string) =>
|
|
177
|
+
isJsonObject(options[name]) ? options[name] : {};
|
|
178
|
+
|
|
179
|
+
const provider = option("provider");
|
|
180
|
+
const titles = strings(provider["x-enum-titles"]);
|
|
181
|
+
const models = valueAt(option("model"), "x-default-by.values");
|
|
182
|
+
const details = valueAt(option("provider_details"), "x-default-by.values");
|
|
183
|
+
|
|
184
|
+
const providers = strings(provider.enum).map((id, index) => {
|
|
185
|
+
const detail = isJsonObject(details) ? details[id] : undefined;
|
|
186
|
+
const model = isJsonObject(models) ? models[id] : undefined;
|
|
187
|
+
const endpoint = isJsonObject(detail) ? detail.endpoint : undefined;
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
id,
|
|
191
|
+
title: titles[index] ?? id,
|
|
192
|
+
model: isStringValue(model) ? model : "",
|
|
193
|
+
endpoint: isStringValue(endpoint) ? endpoint : "",
|
|
194
|
+
keyVariables: isJsonObject(detail) ? strings(detail.key_variables) : [],
|
|
195
|
+
keylessCustomEndpoint:
|
|
196
|
+
isJsonObject(detail) && detail.keyless_custom_endpoint === true,
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const prompt = option("system_prompt");
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
providers,
|
|
204
|
+
defaultPrompt: isStringValue(prompt.default) ? prompt.default : undefined,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function read(rootPath?: string): Promise<ReviewDiffrConfig> {
|
|
209
|
+
const config = await values(rootPath);
|
|
210
|
+
const defaults = await summaryDefaults(rootPath);
|
|
211
|
+
const saved = valueAt(config, `${prefix}.api_key`);
|
|
212
|
+
|
|
213
|
+
const credentialSource =
|
|
214
|
+
isStringValue(saved) && saved.length > 0
|
|
215
|
+
? "config"
|
|
216
|
+
: environmentKey(defaults.providers ?? [], savedProvider(config))
|
|
217
|
+
? "environment"
|
|
218
|
+
: "missing";
|
|
219
|
+
|
|
220
|
+
const plugins = config.plugins;
|
|
221
|
+
|
|
222
|
+
if (isJsonObject(plugins)) {
|
|
223
|
+
for (const namespace of Object.values(plugins)) {
|
|
224
|
+
if (!isJsonObject(namespace)) continue;
|
|
225
|
+
|
|
226
|
+
for (const plugin of Object.values(namespace)) {
|
|
227
|
+
if (isJsonObject(plugin)) delete plugin.api_key;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { values: config, credentialSource, ...defaults };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function readDiffrConfig(rootPath?: string): Promise<ReviewDiffrConfig> {
|
|
236
|
+
return serialized(() => read(rootPath));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function diffrConfigValueText(value: JsonValue): string {
|
|
240
|
+
return isStringValue(value) ? value : JSON.stringify(value);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function writeSettings(
|
|
244
|
+
entries: [string, JsonValue][],
|
|
245
|
+
rootPath?: string,
|
|
246
|
+
): Promise<ReviewDiffrConfig> {
|
|
247
|
+
let changed = false;
|
|
248
|
+
let error: string | undefined;
|
|
249
|
+
let current = await values(rootPath);
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
for (const [key, value] of entries) {
|
|
253
|
+
if (valueAt(current, key) === value) continue;
|
|
254
|
+
await diffr(
|
|
255
|
+
["config", "set", key, diffrConfigValueText(value)],
|
|
256
|
+
rootPath,
|
|
257
|
+
);
|
|
258
|
+
changed = true;
|
|
259
|
+
// A write can clear others, such as a provider's endpoint and key.
|
|
260
|
+
current = await values(rootPath);
|
|
261
|
+
}
|
|
262
|
+
} catch {
|
|
263
|
+
error = changed
|
|
264
|
+
? "Some settings were saved before diffr rejected a change. Check the current values and try again."
|
|
265
|
+
: "diffr could not save the setting. Check its configuration and the entered value.";
|
|
266
|
+
} finally {
|
|
267
|
+
if (changed) invalidateStructuralComparisons();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { ...(await read(rootPath)), changed, error };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function setDiffrConfigValue(
|
|
274
|
+
key: string,
|
|
275
|
+
value: JsonValue,
|
|
276
|
+
rootPath?: string,
|
|
277
|
+
): Promise<ReviewDiffrConfig> {
|
|
278
|
+
if (
|
|
279
|
+
!/^[A-Za-z0-9_][A-Za-z0-9_-]*(\.[A-Za-z0-9_][A-Za-z0-9_-]*)*$/.test(key)
|
|
280
|
+
) {
|
|
281
|
+
throw new Error("Invalid diffr config key.");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return serialized(() => writeSettings([[key, value]], rootPath));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function saveDiffrSummarizer(
|
|
288
|
+
input: ReviewDiffrSummarizerInput,
|
|
289
|
+
rootPath?: string,
|
|
290
|
+
): Promise<ReviewDiffrConfig> {
|
|
291
|
+
const parsed = reviewDiffrSummarizerInputSchema.safeParse(input);
|
|
292
|
+
|
|
293
|
+
if (!parsed.success) throw new Error("Invalid summary settings.");
|
|
294
|
+
const draft = parsed.data;
|
|
295
|
+
|
|
296
|
+
return serialized(async () => {
|
|
297
|
+
const current = await read(rootPath);
|
|
298
|
+
const providers = current.providers ?? [];
|
|
299
|
+
const moving = movesKey(current.values, draft);
|
|
300
|
+
const savedKey = current.credentialSource === "config";
|
|
301
|
+
|
|
302
|
+
if (
|
|
303
|
+
draft.enabled &&
|
|
304
|
+
!draft.apiKey &&
|
|
305
|
+
!(savedKey && !moving) &&
|
|
306
|
+
!environmentKey(providers, draft.provider) &&
|
|
307
|
+
!keyOptional(providers, draft.provider, draft.endpoint)
|
|
308
|
+
) {
|
|
309
|
+
return {
|
|
310
|
+
...current,
|
|
311
|
+
changed: false,
|
|
312
|
+
error: "Add an API key before enabling summaries.",
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const entries: [string, JsonValue][] = [];
|
|
317
|
+
|
|
318
|
+
if (!draft.enabled) entries.push([`${prefix}.enabled`, false]);
|
|
319
|
+
|
|
320
|
+
// Clear a key before its destination changes and write a new one only
|
|
321
|
+
// after, so a failure part way never pairs a key with another vendor.
|
|
322
|
+
if (moving && savedKey) entries.push([`${prefix}.api_key`, ""]);
|
|
323
|
+
entries.push([`${prefix}.provider`, draft.provider]);
|
|
324
|
+
|
|
325
|
+
// diffr clears a provider's endpoint when the provider changes, so a
|
|
326
|
+
// custom one kept across a switch is written again.
|
|
327
|
+
if (
|
|
328
|
+
draft.endpoint !== savedEndpoint(current.values) ||
|
|
329
|
+
(draft.endpoint && draft.provider !== savedProvider(current.values))
|
|
330
|
+
)
|
|
331
|
+
entries.push([`${prefix}.endpoint`, draft.endpoint]);
|
|
332
|
+
|
|
333
|
+
if (draft.apiKey) entries.push([`${prefix}.api_key`, draft.apiKey]);
|
|
334
|
+
|
|
335
|
+
// A blank prompt leaves diffr's own in place.
|
|
336
|
+
if (
|
|
337
|
+
draft.systemPrompt.trim() &&
|
|
338
|
+
draft.systemPrompt !== valueAt(current.values, `${prefix}.system_prompt`)
|
|
339
|
+
)
|
|
340
|
+
entries.push([`${prefix}.system_prompt`, draft.systemPrompt]);
|
|
341
|
+
|
|
342
|
+
entries.push(
|
|
343
|
+
[`${prefix}.model`, draft.model],
|
|
344
|
+
[`${prefix}.tests`, draft.tests],
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
if (draft.enabled) entries.push([`${prefix}.enabled`, true]);
|
|
348
|
+
|
|
349
|
+
return writeSettings(entries, rootPath);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export async function testDiffrSummarizer(
|
|
354
|
+
input: ReviewDiffrSummarizerInput,
|
|
355
|
+
rootPath?: string,
|
|
356
|
+
signal?: AbortSignal,
|
|
357
|
+
): Promise<string> {
|
|
358
|
+
const parsed = reviewDiffrSummarizerInputSchema.safeParse(input);
|
|
359
|
+
|
|
360
|
+
if (!parsed.success) throw new Error("Invalid summary settings.");
|
|
361
|
+
|
|
362
|
+
if (testing) throw new Error("A summary test is already running.");
|
|
363
|
+
testing = true;
|
|
364
|
+
const timeout = AbortSignal.timeout(60_000);
|
|
365
|
+
const deadline = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
366
|
+
let directory: string | undefined;
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const config = await serialized(() => values(rootPath, true, deadline));
|
|
370
|
+
const saved = valueAt(config, prefix);
|
|
371
|
+
|
|
372
|
+
if (!isJsonObject(saved))
|
|
373
|
+
throw new Error("The summarizer is not available in this configuration.");
|
|
374
|
+
|
|
375
|
+
const { provider, endpoint, model, systemPrompt } = parsed.data;
|
|
376
|
+
const providers = (await summaryDefaults(rootPath)).providers ?? [];
|
|
377
|
+
|
|
378
|
+
const variables =
|
|
379
|
+
providers.find((known) => known.id === provider)?.keyVariables ?? [];
|
|
380
|
+
|
|
381
|
+
const apiKey =
|
|
382
|
+
parsed.data.apiKey ||
|
|
383
|
+
(!movesKey(config, parsed.data) && isStringValue(saved.api_key)
|
|
384
|
+
? saved.api_key
|
|
385
|
+
: "") ||
|
|
386
|
+
environmentKey(providers, provider);
|
|
387
|
+
|
|
388
|
+
if (!apiKey && !keyOptional(providers, provider, endpoint))
|
|
389
|
+
throw new Error("Add an API key to test summaries.");
|
|
390
|
+
directory = await mkdtemp(join(tmpdir(), "review-summary-"));
|
|
391
|
+
|
|
392
|
+
const options: JsonObject = {
|
|
393
|
+
...saved,
|
|
394
|
+
enabled: true,
|
|
395
|
+
provider,
|
|
396
|
+
model,
|
|
397
|
+
...(systemPrompt.trim() && { system_prompt: systemPrompt }),
|
|
398
|
+
min_lines: 1,
|
|
399
|
+
retries: 0,
|
|
400
|
+
request_timeout_ms: 45_000,
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
delete options.api_key;
|
|
404
|
+
// Resolved for the saved provider; diffr derives the draft provider's.
|
|
405
|
+
delete options.provider_details;
|
|
406
|
+
|
|
407
|
+
if (endpoint) options.endpoint = endpoint;
|
|
408
|
+
else delete options.endpoint;
|
|
409
|
+
await mkdir(join(directory, "diffr"));
|
|
410
|
+
await writeFile(
|
|
411
|
+
join(directory, "diffr", "config.toml"),
|
|
412
|
+
stringify({
|
|
413
|
+
version: 1,
|
|
414
|
+
plugins: {
|
|
415
|
+
order: ["bundled.summarize"],
|
|
416
|
+
bundled: { summarize: options },
|
|
417
|
+
},
|
|
418
|
+
}),
|
|
419
|
+
{ mode: 0o600 },
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
const before = join(directory, "before.rs"),
|
|
423
|
+
after = join(directory, "after.rs");
|
|
424
|
+
|
|
425
|
+
await writeFile(before, "");
|
|
426
|
+
await writeFile(after, SUMMARY_SAMPLE);
|
|
427
|
+
|
|
428
|
+
const output = await diffr(
|
|
429
|
+
[
|
|
430
|
+
"--no-index",
|
|
431
|
+
"--format",
|
|
432
|
+
"ndjson",
|
|
433
|
+
"--stream-annotations",
|
|
434
|
+
"--",
|
|
435
|
+
before,
|
|
436
|
+
after,
|
|
437
|
+
],
|
|
438
|
+
directory,
|
|
439
|
+
{
|
|
440
|
+
env: {
|
|
441
|
+
...process.env,
|
|
442
|
+
...Object.fromEntries(
|
|
443
|
+
providers
|
|
444
|
+
.flatMap((known) => known.keyVariables)
|
|
445
|
+
.map((name) => [name, ""]),
|
|
446
|
+
),
|
|
447
|
+
...(apiKey && variables[0] && { [variables[0]]: apiKey }),
|
|
448
|
+
XDG_CONFIG_HOME: directory,
|
|
449
|
+
},
|
|
450
|
+
signal: deadline,
|
|
451
|
+
},
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
const events = output.trim().split("\n").map(decodeStructuralDiffEvent);
|
|
456
|
+
const complete = events.at(-1);
|
|
457
|
+
|
|
458
|
+
const annotation = events
|
|
459
|
+
.flatMap((event) =>
|
|
460
|
+
event.type === "annotations" ? event.annotations : [],
|
|
461
|
+
)
|
|
462
|
+
.find((item) => item.label.trim());
|
|
463
|
+
|
|
464
|
+
if (
|
|
465
|
+
complete?.type === "complete" &&
|
|
466
|
+
complete.failed === 0 &&
|
|
467
|
+
!complete.aborted &&
|
|
468
|
+
!events.some(
|
|
469
|
+
(event) =>
|
|
470
|
+
(event.type === "annotations" || event.type === "file") &&
|
|
471
|
+
event.error,
|
|
472
|
+
) &&
|
|
473
|
+
annotation
|
|
474
|
+
) {
|
|
475
|
+
return apiKey
|
|
476
|
+
? annotation.label.split(apiKey).join("[redacted]")
|
|
477
|
+
: annotation.label;
|
|
478
|
+
}
|
|
479
|
+
} catch {
|
|
480
|
+
/* Never return provider output or parser causes. */
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
throw new Error(
|
|
484
|
+
"No summary was produced. Check the API key, model, and network connection.",
|
|
485
|
+
);
|
|
486
|
+
} finally {
|
|
487
|
+
testing = false;
|
|
488
|
+
|
|
489
|
+
if (directory) await rm(directory, { recursive: true, force: true });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const SUMMARY_SAMPLE = `pub fn count_values(values: &[i32]) -> (i32, i32) {
|
|
494
|
+
let mut total = 0;
|
|
495
|
+
let mut count = 0;
|
|
496
|
+
for value in values {
|
|
497
|
+
if *value > 0 {
|
|
498
|
+
total += value;
|
|
499
|
+
count += 1;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
let average = if count > 0 {
|
|
503
|
+
total / count
|
|
504
|
+
} else {
|
|
505
|
+
0
|
|
506
|
+
};
|
|
507
|
+
(count, average)
|
|
508
|
+
}
|
|
509
|
+
`;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { lstatSync, readFileSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
import { findReviewPackageRoot } from "@review/package-paths";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
const exec = promisify(execFile);
|
|
11
|
+
|
|
12
|
+
const versionSchema = z.object({ version: z.string() });
|
|
13
|
+
|
|
14
|
+
const stampSchema = z.object({ version: z.string(), full: z.boolean() });
|
|
15
|
+
|
|
16
|
+
const pending = new Map<string, Promise<void>>();
|
|
17
|
+
|
|
18
|
+
function configuration(root: string) {
|
|
19
|
+
const installer = path.join(root, "bin/diffr-package/bin/fetch.mjs");
|
|
20
|
+
|
|
21
|
+
const { version } = versionSchema.parse(
|
|
22
|
+
JSON.parse(
|
|
23
|
+
readFileSync(path.join(root, "bin/diffr-package/package.json"), "utf8"),
|
|
24
|
+
),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const directory = path.join(
|
|
28
|
+
process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache"),
|
|
29
|
+
"diffr",
|
|
30
|
+
version,
|
|
31
|
+
`${process.platform}-${process.arch}`,
|
|
32
|
+
"full",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
installer,
|
|
37
|
+
version,
|
|
38
|
+
directory,
|
|
39
|
+
binary: path.join(
|
|
40
|
+
directory,
|
|
41
|
+
process.platform === "win32" ? "diffr.exe" : "diffr",
|
|
42
|
+
),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function installedFullDiffr(root: string): string | undefined {
|
|
47
|
+
try {
|
|
48
|
+
const { binary, directory, version } = configuration(root);
|
|
49
|
+
const stat = lstatSync(binary);
|
|
50
|
+
|
|
51
|
+
const stamp = stampSchema.parse(
|
|
52
|
+
JSON.parse(
|
|
53
|
+
readFileSync(path.join(directory, "diffr.stamp.json"), "utf8"),
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (
|
|
58
|
+
stat.isFile() &&
|
|
59
|
+
(process.platform === "win32" || stat.mode & 0o111) &&
|
|
60
|
+
stamp.full &&
|
|
61
|
+
stamp.version === version
|
|
62
|
+
)
|
|
63
|
+
return binary;
|
|
64
|
+
} catch {}
|
|
65
|
+
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function installFullDiffr(
|
|
70
|
+
root = findReviewPackageRoot(import.meta.url),
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
if (process.env.REVIEW_DIFFR_BINARY)
|
|
73
|
+
return Promise.reject(
|
|
74
|
+
new Error(
|
|
75
|
+
"An explicitly selected diffr executable is in use. Install its full edition separately.",
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
const { installer, directory } = configuration(root);
|
|
79
|
+
const current = pending.get(directory);
|
|
80
|
+
|
|
81
|
+
if (current) return current;
|
|
82
|
+
|
|
83
|
+
const task = exec(
|
|
84
|
+
process.execPath,
|
|
85
|
+
[installer, "--full", "--required", "--into", directory],
|
|
86
|
+
{
|
|
87
|
+
env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
|
|
88
|
+
timeout: 180_000,
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
.then(() => {})
|
|
92
|
+
.finally(() => pending.delete(directory));
|
|
93
|
+
|
|
94
|
+
pending.set(directory, task);
|
|
95
|
+
|
|
96
|
+
return task;
|
|
97
|
+
}
|