@dev.fast/whiteboard 0.3.4 → 0.3.5-preview.20261021.131
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/dist/{account-alias-CprZojXd.js → account-alias-CisiYUZT.js} +13 -8
- package/dist/agent-cli-BglNRDZN.js +2 -0
- package/dist/{agent-cli-CThN7Q59.js → agent-cli-TJ7_5_B3.js} +15 -10
- package/dist/{agent-client-BOOi7EUR.js → agent-client-H53ajvrm.js} +13 -3
- package/dist/{authoring-tools-BNwuMnz6.js → authoring-tools-BCquUsB3.js} +34 -27
- package/dist/{background-server-DLSDogkI.js → background-server-BSYXD9au.js} +1 -1
- package/dist/{background-server-CkvxA-6O.js → background-server-Xom8cA_u.js} +5 -5
- package/dist/build-info.json +1 -1
- package/dist/{cli-R4yfa5DT.js → cli-BOnV4cCh.js} +1 -1
- package/dist/{cli-runner-Cuc9FSzu.js → cli-runner-cRY5NOno.js} +70 -34
- package/dist/cli.js +4 -3
- package/dist/{client-BN_FDxFr.js → client-UH1bEG7Z.js} +260 -49
- package/dist/{fuzzy-match-BO9bK37N.js → fuzzy-match-D_x3wumC.js} +6 -4
- package/dist/{headless-host-CVBOQyRS.js → headless-host-BEe-aFtB.js} +1 -1
- package/dist/{headless-host-Cog6SpS9.js → headless-host-CEb5d-A0.js} +563 -175
- package/dist/lens-selection-BxPk53c8.js +214 -0
- package/dist/{local-data-D_9tvdsp.js → local-data-mbCb1HDH.js} +2448 -347
- package/dist/{mcp-D1hziANa.js → mcp-cBtzXsmN.js} +9 -12
- package/dist/{package-paths-C--__jEv.js → package-paths-BkeQsdaw.js} +1 -1
- package/dist/profile-Dbyidrcr.js +2 -0
- package/dist/{profile-DokFw782.js → profile-duqp5uPa.js} +20 -8
- package/dist/{remote-attach-CEJ-kvDY.js → remote-attach-BeKuMm_x.js} +4 -4
- package/dist/{remote-extensions-GN8Gf_qG.js → remote-extensions-CX2fAec1.js} +1 -1
- package/dist/{remote-extensions-Blcjr9Ze.js → remote-extensions-CYWNTVSf.js} +3 -3
- package/dist/{remote-language-server-BTKBQ7x4.js → remote-language-server-DLWluAeR.js} +5 -5
- package/dist/{remote-uninstall-DW7Gdryh.js → remote-uninstall-Y3S0YmJs.js} +6 -6
- package/dist/{request-origin-Zcn_CjX4.js → request-origin-icXPv6jh.js} +55 -18
- package/dist/{review-app-launcher-CkwLclzR.js → review-app-launcher-DZvG5ZAh.js} +2 -2
- package/dist/{review-home-paths-j2BRAaNv.js → review-home-paths-BwH08ihj.js} +19 -17
- package/dist/{review-telemetry-BLNwYnLV.js → review-telemetry-AZ0imsmJ.js} +6 -6
- package/dist/runtime-CF4JXdb8.js +27 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/{s3-DtJFWC0G.js → s3-CsG2EWED.js} +1 -1
- package/dist/{s3-config-BTO12tCC.js → s3-config-CjcLyFV5.js} +4 -2
- package/dist/{s3-config-CSy3fx8H.js → s3-config-zz4GWh2T.js} +1 -1
- package/dist/server/desktop-host.js +28 -104
- package/dist/{server-discovery-Bey8IqXH.js → server-discovery-DeVTdcqK.js} +3 -3
- package/dist/sharing/index.d.ts +484 -74
- package/dist/sharing/index.js +1 -1
- package/dist/{src-DsM3jO8G.js → src-DYZLCUP_.js} +98 -18
- package/dist/{src-BMPuxWv4.js → src-DtfSndqS.js} +269 -227
- package/dist/{stored-document-migration-CChxtFAy.js → stored-document-migration-B29lve9-.js} +318 -271
- package/dist/{thread-state-5pYr6_zM.js → thread-state-BPpvKFwQ.js} +12 -2
- package/dist/{tool-failure-C9MpbXJJ.js → tool-failure-CQhyLht3.js} +10 -3
- package/instructions/authoring.md +20 -5
- package/package.json +4 -4
- package/src/agent-selection.ts +6 -0
- package/src/ask/agents.ts +13 -1
- package/src/ask/checkout-files.ts +2 -5
- package/src/ask/thread-state.ts +4 -0
- package/src/ask/thread.ts +61 -31
- package/src/ask/threads.ts +24 -3
- package/src/cli-install.ts +13 -2
- package/src/cli-runner.ts +4 -1
- package/src/cli.ts +4 -1
- package/src/diff-selection-migration.ts +48 -34
- package/src/heading-anchors.ts +42 -0
- package/src/lens-selection.ts +38 -2
- package/src/markdown.ts +42 -0
- package/src/posthog-capture-client.ts +1 -1
- package/src/review-api/README.md +52 -13
- package/src/review-api/activity.ts +1 -4
- package/src/review-api/agent-cli.ts +9 -1
- package/src/review-api/anchor-quotes.ts +14 -7
- package/src/review-api/ask-history.ts +118 -51
- package/src/review-api/authoring-tools.ts +45 -30
- package/src/review-api/azure-devops-provider.ts +2 -20
- package/src/review-api/azure-remote.ts +21 -0
- package/src/review-api/blocks/definition.ts +1 -1
- package/src/review-api/blocks/markdown.ts +2 -2
- package/src/review-api/board-order.ts +144 -0
- package/src/review-api/checkout-groups.ts +586 -0
- package/src/review-api/document-headings.ts +58 -27
- package/src/review-api/document-text.ts +31 -2
- package/src/review-api/document.ts +362 -67
- package/src/review-api/foreign-references.ts +328 -0
- package/src/review-api/github-provider.ts +51 -13
- package/src/review-api/http.ts +734 -135
- package/src/review-api/instructions.ts +0 -19
- package/src/review-api/link-preview.ts +113 -0
- package/src/review-api/local-data.ts +310 -46
- package/src/review-api/mcp.ts +7 -13
- package/src/review-api/onboarding.ts +1 -1
- package/src/review-api/profile.ts +37 -6
- package/src/review-api/public-tools.ts +24 -2
- package/src/review-api/pull-request.ts +82 -49
- package/src/review-api/request-origin.ts +10 -0
- package/src/review-api/review-progress.ts +61 -27
- package/src/review-api/scratchpad-plan-upgrade.ts +250 -0
- package/src/review-api/store-migrations.ts +355 -0
- package/src/review-api/store-schema.ts +38 -29
- package/src/review-api/store.ts +2176 -495
- package/src/review-api/worktree-events.ts +3 -5
- package/src/review-api/worktree-source.ts +3 -7
- package/src/review-api/worktree-structural.integration.ts +72 -0
- package/src/review-stack.ts +15 -23
- package/src/runtime.ts +4 -30
- package/src/server/desktop-host.ts +6 -1
- package/src/server/desktop-server.ts +1 -36
- package/src/server/headless-host.ts +0 -2
- package/src/server/hono-http.ts +2 -1
- package/src/server/review-gateway-list.ts +3 -5
- package/src/server/review-gateway-memory.ts +1 -1
- package/src/server/review-lifecycle-telemetry.ts +2 -2
- package/src/server/review-server-core.ts +1 -5
- package/src/server/structural-comparisons.ts +47 -0
- package/src/sharing/export.ts +36 -7
- package/src/sharing/host.ts +34 -14
- package/src/sharing/import.ts +156 -24
- package/src/sharing/repository.ts +143 -24
- package/src/startup-trace.ts +3 -3
- package/src/stored-document-migration.ts +110 -19
- package/src/ui-telemetry-events.ts +44 -18
- package/dist/agent-cli-C6SPSII0.js +0 -2
- package/dist/profile-CYPmvBoA.js +0 -2
- package/dist/runtime-CKiWhGrf.js +0 -52
- package/instructions/scratchpad.md +0 -23
- package/src/review-preferences.ts +0 -88
- package/src/server/legacy-tutorial-cleanup.ts +0 -38
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { type JsonValue, isJsonObject, isStringValue } from "@dev.fast/json";
|
|
4
|
+
import { parseAnchor } from "@review/lens-selection.js";
|
|
5
|
+
import {
|
|
6
|
+
markdownNodes,
|
|
7
|
+
markdownText,
|
|
8
|
+
parseMarkdown,
|
|
9
|
+
} from "@review/markdown.js";
|
|
10
|
+
|
|
11
|
+
/** A file's text at a commit; undefined when it can't be read. */
|
|
12
|
+
export type ReadAtCommit = (
|
|
13
|
+
repositoryId: string,
|
|
14
|
+
commit: string,
|
|
15
|
+
file: string,
|
|
16
|
+
) => string | undefined;
|
|
17
|
+
|
|
18
|
+
const LANGUAGES = new Map([
|
|
19
|
+
[".ts", "typescript"],
|
|
20
|
+
[".tsx", "tsx"],
|
|
21
|
+
[".js", "javascript"],
|
|
22
|
+
[".jsx", "jsx"],
|
|
23
|
+
[".mjs", "javascript"],
|
|
24
|
+
[".rs", "rust"],
|
|
25
|
+
[".go", "go"],
|
|
26
|
+
[".py", "python"],
|
|
27
|
+
[".rb", "ruby"],
|
|
28
|
+
[".java", "java"],
|
|
29
|
+
[".kt", "kotlin"],
|
|
30
|
+
[".swift", "swift"],
|
|
31
|
+
[".c", "c"],
|
|
32
|
+
[".h", "c"],
|
|
33
|
+
[".cc", "cpp"],
|
|
34
|
+
[".cpp", "cpp"],
|
|
35
|
+
[".css", "css"],
|
|
36
|
+
[".html", "html"],
|
|
37
|
+
[".json", "json"],
|
|
38
|
+
[".md", "markdown"],
|
|
39
|
+
[".sh", "bash"],
|
|
40
|
+
[".sql", "sql"],
|
|
41
|
+
[".toml", "toml"],
|
|
42
|
+
[".yaml", "yaml"],
|
|
43
|
+
[".yml", "yaml"],
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/** A source link in Markdown, in either spelling. */
|
|
47
|
+
const SOURCE_LINK = /^(?:whiteboard:source\/|review-source:)/i;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Migrates the old global scratchpad, which quoted several repositories.
|
|
51
|
+
* A board may only quote its own, so each quote of another repository is
|
|
52
|
+
* replaced:
|
|
53
|
+
* - a code peek becomes a `code` block of those lines;
|
|
54
|
+
* - a source link becomes its text, then what it pointed at in code;
|
|
55
|
+
* - a diagram attachment leaves the diagram and its lines follow it as
|
|
56
|
+
* a `code` block;
|
|
57
|
+
* - a step keeps its label and takes the lines as its code;
|
|
58
|
+
* - a call stack or database lens becomes a line saying what it was,
|
|
59
|
+
* then a `code` block for each range it quoted, in any repository; the
|
|
60
|
+
* first takes its id. A lens that quotes only its own repository is
|
|
61
|
+
* kept.
|
|
62
|
+
* New blocks take ids from `allocate`.
|
|
63
|
+
*/
|
|
64
|
+
export function snapshotForeignReferences(
|
|
65
|
+
document: JsonValue[],
|
|
66
|
+
own: string,
|
|
67
|
+
read: ReadAtCommit,
|
|
68
|
+
repositoryName: (repositoryId: string) => string,
|
|
69
|
+
allocate: (prefix: string) => string,
|
|
70
|
+
): JsonValue[] {
|
|
71
|
+
const foreign = (pins: JsonValue | undefined) =>
|
|
72
|
+
isJsonObject(pins) &&
|
|
73
|
+
isStringValue(pins.repositoryId) &&
|
|
74
|
+
pins.repositoryId !== own
|
|
75
|
+
? pins
|
|
76
|
+
: undefined;
|
|
77
|
+
|
|
78
|
+
const caption = (
|
|
79
|
+
pins: { repositoryId: string; head: string; base?: string },
|
|
80
|
+
anchor: string,
|
|
81
|
+
) => {
|
|
82
|
+
const selection = parseAnchor(anchor);
|
|
83
|
+
|
|
84
|
+
const commit =
|
|
85
|
+
selection?.start.side === "base" ? (pins.base ?? pins.head) : pins.head;
|
|
86
|
+
|
|
87
|
+
const range = selection
|
|
88
|
+
? `${selection.file}:${selection.start.line}-${selection.end.line}`
|
|
89
|
+
: anchor;
|
|
90
|
+
|
|
91
|
+
return `${repositoryName(pins.repositoryId)} · ${range} @ ${commit.slice(0, 7)}`;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const snapshot = (
|
|
95
|
+
id: JsonValue | undefined,
|
|
96
|
+
pins: { repositoryId: string; head: string; base?: string },
|
|
97
|
+
anchor: string,
|
|
98
|
+
): JsonValue => {
|
|
99
|
+
const selection = parseAnchor(anchor);
|
|
100
|
+
|
|
101
|
+
const commit =
|
|
102
|
+
selection?.start.side === "base" ? (pins.base ?? pins.head) : pins.head;
|
|
103
|
+
|
|
104
|
+
const file = selection
|
|
105
|
+
? read(pins.repositoryId, commit, selection.file)
|
|
106
|
+
: undefined;
|
|
107
|
+
|
|
108
|
+
const label = caption(pins, anchor);
|
|
109
|
+
|
|
110
|
+
const text =
|
|
111
|
+
file !== undefined && selection
|
|
112
|
+
? file
|
|
113
|
+
.split("\n")
|
|
114
|
+
.slice(selection.start.line - 1, selection.end.line)
|
|
115
|
+
.join("\n")
|
|
116
|
+
: `${label}\n(source unavailable)`;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
type: "code",
|
|
120
|
+
...(isStringValue(id) && { id }),
|
|
121
|
+
language: selection
|
|
122
|
+
? (LANGUAGES.get(path.extname(selection.file).toLowerCase()) ?? "text")
|
|
123
|
+
: "text",
|
|
124
|
+
text,
|
|
125
|
+
caption: label,
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const pinsIn = (pins: JsonValue | undefined) => {
|
|
130
|
+
return isJsonObject(pins) &&
|
|
131
|
+
isStringValue(pins.repositoryId) &&
|
|
132
|
+
isStringValue(pins.head)
|
|
133
|
+
? {
|
|
134
|
+
repositoryId: pins.repositoryId,
|
|
135
|
+
head: pins.head,
|
|
136
|
+
...(isStringValue(pins.base) && { base: pins.base }),
|
|
137
|
+
}
|
|
138
|
+
: undefined;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const pinsOf = (value: JsonValue | undefined) => pinsIn(foreign(value));
|
|
142
|
+
|
|
143
|
+
const peeks = (blocks: JsonValue[]): JsonValue[] =>
|
|
144
|
+
blocks.map((block) => {
|
|
145
|
+
if (!isJsonObject(block)) return block;
|
|
146
|
+
|
|
147
|
+
if (Array.isArray(block.children))
|
|
148
|
+
return { ...block, children: peeks(block.children) };
|
|
149
|
+
|
|
150
|
+
const pins = pinsOf(block.pins);
|
|
151
|
+
|
|
152
|
+
if (block.type === "code_peek" && pins && isStringValue(block.source))
|
|
153
|
+
return snapshot(block.id, pins, block.source);
|
|
154
|
+
|
|
155
|
+
return block;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const rest = (blocks: JsonValue[]): JsonValue[] =>
|
|
159
|
+
blocks.flatMap((block): JsonValue | JsonValue[] => {
|
|
160
|
+
if (!isJsonObject(block)) return block;
|
|
161
|
+
|
|
162
|
+
if (Array.isArray(block.children))
|
|
163
|
+
return { ...block, children: rest(block.children) };
|
|
164
|
+
|
|
165
|
+
const blockPins = pinsOf(block.pins);
|
|
166
|
+
|
|
167
|
+
if (
|
|
168
|
+
block.type === "markdown" &&
|
|
169
|
+
blockPins &&
|
|
170
|
+
isStringValue(block.markdown)
|
|
171
|
+
) {
|
|
172
|
+
const { pins: _pins, ...kept } = block;
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
...kept,
|
|
176
|
+
markdown: unlinkSourceLinks(block.markdown, blockPins),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (block.type === "sequence" && Array.isArray(block.steps))
|
|
181
|
+
return {
|
|
182
|
+
...block,
|
|
183
|
+
steps: block.steps.map((step) => {
|
|
184
|
+
if (!isJsonObject(step) || !isStringValue(step.source)) return step;
|
|
185
|
+
const pins = pinsOf(step.pins ?? block.pins);
|
|
186
|
+
|
|
187
|
+
if (!pins) return step;
|
|
188
|
+
const { source, pins: _pins, ...kept } = step;
|
|
189
|
+
const quoted = snapshot(undefined, pins, source);
|
|
190
|
+
|
|
191
|
+
return isJsonObject(quoted)
|
|
192
|
+
? {
|
|
193
|
+
...kept,
|
|
194
|
+
code: { language: quoted.language, text: quoted.text },
|
|
195
|
+
}
|
|
196
|
+
: kept;
|
|
197
|
+
}),
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if (block.type === "flow_diagram" && Array.isArray(block.nodes)) {
|
|
201
|
+
const moved: JsonValue[] = [];
|
|
202
|
+
|
|
203
|
+
const diagram = {
|
|
204
|
+
...block,
|
|
205
|
+
nodes: block.nodes.map((node) =>
|
|
206
|
+
isJsonObject(node) && Array.isArray(node.attachments)
|
|
207
|
+
? {
|
|
208
|
+
...node,
|
|
209
|
+
attachments: node.attachments.filter((attachment) => {
|
|
210
|
+
if (
|
|
211
|
+
!isJsonObject(attachment) ||
|
|
212
|
+
!pinsOf(attachment.pins ?? block.pins)
|
|
213
|
+
)
|
|
214
|
+
return true;
|
|
215
|
+
|
|
216
|
+
// Its lines stay on the board, after the diagram.
|
|
217
|
+
for (const [pins, anchor] of quotes(
|
|
218
|
+
attachment,
|
|
219
|
+
pinsIn,
|
|
220
|
+
block.pins,
|
|
221
|
+
))
|
|
222
|
+
moved.push(snapshot(allocate("block"), pins, anchor));
|
|
223
|
+
|
|
224
|
+
return false;
|
|
225
|
+
}),
|
|
226
|
+
}
|
|
227
|
+
: node,
|
|
228
|
+
),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return [diagram, ...moved];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (block.type === "call_stack_diff" || block.type === "database_lens") {
|
|
235
|
+
const foreignQuote = quotes(block, pinsOf)[0];
|
|
236
|
+
|
|
237
|
+
if (!foreignQuote) return block;
|
|
238
|
+
|
|
239
|
+
const what =
|
|
240
|
+
block.type === "call_stack_diff" ? "Call stack" : "Database lens";
|
|
241
|
+
|
|
242
|
+
const title = isStringValue(block.title) ? ` “${block.title}”` : "";
|
|
243
|
+
|
|
244
|
+
// Its own repository's ranges too, so none of it is lost.
|
|
245
|
+
return [
|
|
246
|
+
{
|
|
247
|
+
type: "markdown",
|
|
248
|
+
id: allocate("block"),
|
|
249
|
+
markdown: `${what}${title} (from ${repositoryName(foreignQuote[0].repositoryId)}), kept as snapshots below.`,
|
|
250
|
+
},
|
|
251
|
+
...quotes(block, pinsIn).map(([pins, anchor], index) =>
|
|
252
|
+
snapshot(index === 0 ? block.id : allocate("block"), pins, anchor),
|
|
253
|
+
),
|
|
254
|
+
];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return block;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
/** The ranges a block's frames or operations quote at the pins `read`
|
|
261
|
+
* accepts. */
|
|
262
|
+
function quotes(
|
|
263
|
+
block: JsonValue,
|
|
264
|
+
read: typeof pinsIn,
|
|
265
|
+
inherited?: JsonValue,
|
|
266
|
+
): [{ repositoryId: string; head: string; base?: string }, string][] {
|
|
267
|
+
if (Array.isArray(block))
|
|
268
|
+
return block.flatMap((item) => quotes(item, read, inherited));
|
|
269
|
+
|
|
270
|
+
if (!isJsonObject(block)) return [];
|
|
271
|
+
const scope = block.pins ?? inherited;
|
|
272
|
+
const pins = read(scope);
|
|
273
|
+
|
|
274
|
+
const own: [
|
|
275
|
+
{ repositoryId: string; head: string; base?: string },
|
|
276
|
+
string,
|
|
277
|
+
][] = pins
|
|
278
|
+
? [
|
|
279
|
+
block.source,
|
|
280
|
+
block.callSite,
|
|
281
|
+
...(Array.isArray(block.contextSources) ? block.contextSources : []),
|
|
282
|
+
...(Array.isArray(block.sources) ? block.sources : []),
|
|
283
|
+
].flatMap((anchor): [typeof pins, string][] =>
|
|
284
|
+
isStringValue(anchor) ? [[pins, anchor]] : [],
|
|
285
|
+
)
|
|
286
|
+
: [];
|
|
287
|
+
|
|
288
|
+
return [
|
|
289
|
+
...own,
|
|
290
|
+
...Object.entries(block)
|
|
291
|
+
.filter(([key]) => key !== "pins")
|
|
292
|
+
.flatMap(([, value]) =>
|
|
293
|
+
Array.isArray(value) || isJsonObject(value)
|
|
294
|
+
? quotes(value, read, scope)
|
|
295
|
+
: [],
|
|
296
|
+
),
|
|
297
|
+
];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function unlinkSourceLinks(
|
|
301
|
+
markdown: string,
|
|
302
|
+
pins: { repositoryId: string; head: string; base?: string },
|
|
303
|
+
) {
|
|
304
|
+
let result = markdown;
|
|
305
|
+
|
|
306
|
+
// Rewrite from the end so earlier offsets stay valid.
|
|
307
|
+
const links = [...markdownNodes(parseMarkdown(markdown))]
|
|
308
|
+
.filter(
|
|
309
|
+
(node) =>
|
|
310
|
+
node.type === "link" &&
|
|
311
|
+
SOURCE_LINK.test(node.url ?? "") &&
|
|
312
|
+
node.position?.start.offset !== undefined &&
|
|
313
|
+
node.position.end.offset !== undefined,
|
|
314
|
+
)
|
|
315
|
+
.reverse();
|
|
316
|
+
|
|
317
|
+
// Its text, then where it pointed, so neither is lost.
|
|
318
|
+
for (const node of links)
|
|
319
|
+
result =
|
|
320
|
+
result.slice(0, node.position!.start.offset) +
|
|
321
|
+
`${markdownText(node)} (\`${caption(pins, node.url!.replace(SOURCE_LINK, ""))}\`)` +
|
|
322
|
+
result.slice(node.position!.end.offset);
|
|
323
|
+
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return rest(peeks(document));
|
|
328
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { parseGitRemote } from "@dev.fast/local-vcs";
|
|
2
|
+
import type { PullRequestState } from "@dev.fast/review-protocol";
|
|
2
3
|
import { errorMessage } from "@dev.fast/trace-core";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
|
|
5
6
|
import { ReviewInputError } from "./document.js";
|
|
6
7
|
import type { PullRequestProvider } from "./provider.js";
|
|
7
|
-
import { firstLine } from "./pull-request.js";
|
|
8
|
+
import { GitHubRefusedRead, firstLine } from "./pull-request.js";
|
|
8
9
|
|
|
9
10
|
const GH_TIMEOUT_MS = 20_000;
|
|
10
11
|
|
|
@@ -20,8 +21,24 @@ const metadataSchema = z.object({
|
|
|
20
21
|
baseRefOid: z.string().optional(),
|
|
21
22
|
headRefName: z.string().min(1),
|
|
22
23
|
isCrossRepository: z.boolean(),
|
|
24
|
+
state: z.enum(["OPEN", "CLOSED", "MERGED"]).optional(),
|
|
25
|
+
isDraft: z.boolean().optional(),
|
|
23
26
|
});
|
|
24
27
|
|
|
28
|
+
/** gh's state and draft flag as one state. */
|
|
29
|
+
function ghState(
|
|
30
|
+
state: "OPEN" | "CLOSED" | "MERGED" | undefined,
|
|
31
|
+
isDraft: boolean | undefined,
|
|
32
|
+
): PullRequestState | undefined {
|
|
33
|
+
if (state === "MERGED") return "merged";
|
|
34
|
+
|
|
35
|
+
if (state === "CLOSED") return "closed";
|
|
36
|
+
|
|
37
|
+
if (state === "OPEN") return isDraft ? "draft" : "open";
|
|
38
|
+
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
25
42
|
function unreadable(url: string, ghFailure: string, fallback: string) {
|
|
26
43
|
return new ReviewInputError(
|
|
27
44
|
`Could not read ${url}: gh failed (${ghFailure}) and ${fallback}. Run \`gh auth status\` and sign in with an account that can read the repository, then retry.`,
|
|
@@ -54,24 +71,27 @@ export const githubProvider: PullRequestProvider = {
|
|
|
54
71
|
[
|
|
55
72
|
"pr",
|
|
56
73
|
"view",
|
|
57
|
-
String(number),
|
|
74
|
+
{ value: String(number) },
|
|
58
75
|
"--repo",
|
|
59
|
-
`${host}/${slug}
|
|
76
|
+
{ value: `${host}/${slug}` },
|
|
60
77
|
"--json",
|
|
61
|
-
"number,title,baseRefName,baseRefOid,headRefName,isCrossRepository",
|
|
78
|
+
"number,title,baseRefName,baseRefOid,headRefName,isCrossRepository,state,isDraft",
|
|
62
79
|
],
|
|
63
80
|
{ timeoutMs: GH_TIMEOUT_MS },
|
|
64
81
|
);
|
|
65
82
|
|
|
66
|
-
const { headRefName, isCrossRepository, ...metadata } =
|
|
83
|
+
const { headRefName, isCrossRepository, state, isDraft, ...metadata } =
|
|
67
84
|
metadataSchema.parse(JSON.parse(stdout));
|
|
68
85
|
|
|
86
|
+
const pullRequestState = ghState(state, isDraft);
|
|
87
|
+
|
|
69
88
|
return {
|
|
70
89
|
host,
|
|
71
90
|
slug,
|
|
72
91
|
headRef,
|
|
73
92
|
...metadata,
|
|
74
93
|
headBranch: isCrossRepository ? undefined : headRefName,
|
|
94
|
+
...(pullRequestState && { state: pullRequestState }),
|
|
75
95
|
};
|
|
76
96
|
} catch (error) {
|
|
77
97
|
ghFailure = firstLine(errorMessage(error));
|
|
@@ -110,15 +130,21 @@ export const githubProvider: PullRequestProvider = {
|
|
|
110
130
|
404,
|
|
111
131
|
);
|
|
112
132
|
|
|
113
|
-
if (response.status === 403 || response.status === 429)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
133
|
+
if (response.status === 403 || response.status === 429) {
|
|
134
|
+
const reset = Number(response.headers.get("x-ratelimit-reset"));
|
|
135
|
+
|
|
136
|
+
throw new GitHubRefusedRead(
|
|
137
|
+
unreadable(
|
|
138
|
+
url,
|
|
139
|
+
ghFailure,
|
|
140
|
+
response.headers.get("x-ratelimit-remaining") === "0" ||
|
|
141
|
+
response.status === 429
|
|
142
|
+
? "the unauthenticated GitHub API rate limit is exhausted"
|
|
143
|
+
: "the GitHub API refused the request",
|
|
144
|
+
).message,
|
|
145
|
+
reset > 0 ? reset * 1000 : Date.now() + 15 * 60_000,
|
|
121
146
|
);
|
|
147
|
+
}
|
|
122
148
|
|
|
123
149
|
if (!response.ok)
|
|
124
150
|
throw unreadable(
|
|
@@ -140,6 +166,9 @@ export const githubProvider: PullRequestProvider = {
|
|
|
140
166
|
ref: z.string().min(1),
|
|
141
167
|
repo: z.object({ id: z.number() }).nullable(),
|
|
142
168
|
}),
|
|
169
|
+
state: z.enum(["open", "closed"]).optional(),
|
|
170
|
+
draft: z.boolean().optional(),
|
|
171
|
+
merged_at: z.string().nullable().optional(),
|
|
143
172
|
})
|
|
144
173
|
.safeParse(await response.json().catch(() => undefined));
|
|
145
174
|
|
|
@@ -158,6 +187,15 @@ export const githubProvider: PullRequestProvider = {
|
|
|
158
187
|
parsed.data.head.repo?.id === parsed.data.base.repo.id
|
|
159
188
|
? parsed.data.head.ref
|
|
160
189
|
: undefined,
|
|
190
|
+
...(parsed.data.state && {
|
|
191
|
+
state: parsed.data.merged_at
|
|
192
|
+
? "merged"
|
|
193
|
+
: parsed.data.state === "closed"
|
|
194
|
+
? "closed"
|
|
195
|
+
: parsed.data.draft
|
|
196
|
+
? "draft"
|
|
197
|
+
: "open",
|
|
198
|
+
}),
|
|
161
199
|
};
|
|
162
200
|
},
|
|
163
201
|
|