@diffohq/diffo 0.8.0 → 0.10.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/README.md +8 -6
- package/dist/cli.mjs +625 -43
- package/dist/client/assets/{abnfDiagram-O67JEVCF-BUIFF3Lv.js → abnfDiagram-O67JEVCF-DBlSJW53.js} +1 -1
- package/dist/client/assets/{arc-x6Ulf5MI.js → arc-BZZ_ET2N.js} +1 -1
- package/dist/client/assets/architecture-7GRP2DOG-Cds-sOVr.js +1 -0
- package/dist/client/assets/{architectureDiagram-NJMV4G6O-D25O1A2B.js → architectureDiagram-NJMV4G6O-CmS7e2K4.js} +1 -1
- package/dist/client/assets/{blockDiagram-BEXU5L5S-Dy7P3IZw.js → blockDiagram-BEXU5L5S-Kj8H2ODF.js} +1 -1
- package/dist/client/assets/{c4Diagram-YGBWAQC7-BnE5HTw4.js → c4Diagram-YGBWAQC7-CixbFJxb.js} +1 -1
- package/dist/client/assets/channel-BI4dhOPo.js +1 -0
- package/dist/client/assets/{chunk-3FUC2YCW-6C72_i5n.js → chunk-3FUC2YCW-CRdo0Gw5.js} +1 -1
- package/dist/client/assets/{chunk-5DYCD2WN-B0IgEc5I.js → chunk-5DYCD2WN-Bw9n6qPh.js} +1 -1
- package/dist/client/assets/{chunk-742MDFTN-BNLZzbxp.js → chunk-742MDFTN-BsvOSyDz.js} +1 -1
- package/dist/client/assets/{chunk-7INBJB4K-K1DkKka3.js → chunk-7INBJB4K-ie7EoCoW.js} +1 -1
- package/dist/client/assets/{chunk-7M6MHVWA-B-KIWC1T.js → chunk-7M6MHVWA-DJlbYDLH.js} +1 -1
- package/dist/client/assets/{chunk-7PRAP22T-DR6CzDYx.js → chunk-7PRAP22T-Hs5y39V8.js} +1 -1
- package/dist/client/assets/{chunk-GTNCS2PH-BWkvnYRd.js → chunk-GTNCS2PH-DNq6Ojc7.js} +1 -1
- package/dist/client/assets/{chunk-GWA4HPMP-l8NLPoZc.js → chunk-GWA4HPMP-vgn0kxbp.js} +1 -1
- package/dist/client/assets/{chunk-MBY4JIJT-k1mwME9y.js → chunk-MBY4JIJT-C7fMuqSl.js} +1 -1
- package/dist/client/assets/{chunk-NETBCI7D-DV1F8c_R.js → chunk-NETBCI7D-ZJtjvGwt.js} +1 -1
- package/dist/client/assets/{chunk-O7XYJQB3-Bb8gIxaB.js → chunk-O7XYJQB3-DESjnZZT.js} +1 -1
- package/dist/client/assets/{chunk-UA2S7LBM-BL7m6fd_.js → chunk-UA2S7LBM-Cmf087s8.js} +1 -1
- package/dist/client/assets/{chunk-WEXAMYUT-CAnQv1qY.js → chunk-WEXAMYUT-U1TW5ZAF.js} +1 -1
- package/dist/client/assets/{chunk-XXDRQBXY-DooI1miH.js → chunk-XXDRQBXY-D1B1IYm1.js} +1 -1
- package/dist/client/assets/{chunk-Z7XXMR3K-DPVaCcdM.js → chunk-Z7XXMR3K-D3BUH0qv.js} +1 -1
- package/dist/client/assets/{chunk-ZIGJFQKS-Bz4IWpFy.js → chunk-ZIGJFQKS-Bx6yZhwg.js} +1 -1
- package/dist/client/assets/{classDiagram-v2-NBCMYWYE-Dso7ogb5.js → classDiagram-v2-NBCMYWYE-sjiBGJdD.js} +1 -1
- package/dist/client/assets/{core-Dy-hJrbg.js → core-6IJUkucC.js} +1 -1
- package/dist/client/assets/{cose-bilkent-JH36ORCC-Baj8S6bg.js → cose-bilkent-JH36ORCC-xaMCCs4V.js} +1 -1
- package/dist/client/assets/{cynefin-OW5HDTMX-BkA1ccYX.js → cynefin-OW5HDTMX-C5k9L5NF.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-VND7K2PF-nyK9OZGN.js → cynefinDiagram-VND7K2PF-BqiILl4G.js} +1 -1
- package/dist/client/assets/{dagre-6A5THRUB-BP_dyQLS.js → dagre-6A5THRUB-BtgX5KvU.js} +1 -1
- package/dist/client/assets/{diagram-22UHCM2B-BIt_aIJx.js → diagram-22UHCM2B-BcDIKY8C.js} +1 -1
- package/dist/client/assets/{diagram-3UASUU5V-UX2eUwyi.js → diagram-3UASUU5V-C-DrBJ6K.js} +1 -1
- package/dist/client/assets/{diagram-ATOU4E4O-CEYK5oZO.js → diagram-ATOU4E4O-B0W6FX6y.js} +1 -1
- package/dist/client/assets/{diagram-CDSNMT55-4uIYrmGp.js → diagram-CDSNMT55-D1O9NYQ3.js} +1 -1
- package/dist/client/assets/{diagram-MLGK6HIB-5BGrc92k.js → diagram-MLGK6HIB-WC8izmk2.js} +1 -1
- package/dist/client/assets/{diagram-MPIPVDR6-0BG375Nf.js → diagram-MPIPVDR6-D5ko9V-k.js} +1 -1
- package/dist/client/assets/{dist-uIEEXDIm.js → dist-BBmACywa.js} +1 -1
- package/dist/client/assets/{dist-hfzhbdYS.js → dist-Bss8uvIz.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-ZINNZB2B-CnAveRvy.js → ebnfDiagram-ZINNZB2B-BFIYDkrv.js} +1 -1
- package/dist/client/assets/{elk-276RUBZZ-CejJ0q5w.js → elk-276RUBZZ-WsSc0CyB.js} +1 -1
- package/dist/client/assets/{engine-oniguruma-BpOI5GVJ.js → engine-oniguruma-DmL6leO7.js} +1 -1
- package/dist/client/assets/{erDiagram-OPXOYQCR-Lg3_iVkX.js → erDiagram-OPXOYQCR-CjaKeGKh.js} +1 -1
- package/dist/client/assets/eventmodeling-NTZA5JFV-DE5ql6lo.js +1 -0
- package/dist/client/assets/flowDiagram-KWPJA3E3-CObKVtKJ.js +1 -0
- package/dist/client/assets/{ganttDiagram-FUAMR5RP-D5nJpDg8.js → ganttDiagram-FUAMR5RP-22OFMoat.js} +1 -1
- package/dist/client/assets/{gitGraph-4MIJSDKK-BLNIFBUu.js → gitGraph-4MIJSDKK-BP0idayz.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-X574FWY7-YHwgA3VE.js → gitGraphDiagram-X574FWY7-Cx5rPzjc.js} +1 -1
- package/dist/client/assets/{index-CE0LasmM.css → index-CL4K6Y07.css} +1 -1
- package/dist/client/assets/index-DXMhk4ok.js +93 -0
- package/dist/client/assets/{info-A6RAGUB7-Bfa8ErwC.js → info-A6RAGUB7-BvZZu8IY.js} +1 -1
- package/dist/client/assets/{infoDiagram-VRGFBTTK-Dzp_xQpV.js → infoDiagram-VRGFBTTK-Bbk1aZco.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-OU5B5YK6-Hun-xe-t.js → ishikawaDiagram-OU5B5YK6-DbyWrUBY.js} +1 -1
- package/dist/client/assets/{journeyDiagram-ZHPQQLJL-CT5fXqj_.js → journeyDiagram-ZHPQQLJL-Ka7zlagO.js} +1 -1
- package/dist/client/assets/{kanban-definition-PNTS6WVX-kFYcpsMx.js → kanban-definition-PNTS6WVX-Q_NAEdRt.js} +1 -1
- package/dist/client/assets/{line-Cda_OIfO.js → line-SDAA9fEr.js} +1 -1
- package/dist/client/assets/{linear-CDQGk4q-.js → linear-BPf2OGUv.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-CCoRJPAh.js → mermaid-parser.core-BjLzFSoy.js} +3 -3
- package/dist/client/assets/{mermaid.core-SnjyC0ZV.js → mermaid.core-vvUsQbpX.js} +4 -4
- package/dist/client/assets/{mindmap-definition-NLK3R4M7-C3oLUr5b.js → mindmap-definition-NLK3R4M7-DuVzu_4M.js} +1 -1
- package/dist/client/assets/{packet-AYTQ26CC-BT6BgV6a.js → packet-AYTQ26CC-A66za-wc.js} +1 -1
- package/dist/client/assets/{pegDiagram-GJSIUBJH-OT5BsfLg.js → pegDiagram-GJSIUBJH-ClH92MDY.js} +1 -1
- package/dist/client/assets/{pie-WAS4IAKB-Bou89QXR.js → pie-WAS4IAKB-sJ2H2jF4.js} +1 -1
- package/dist/client/assets/{pieDiagram-5QR66LMP-AzEtJfJu.js → pieDiagram-5QR66LMP-BmlcB4fD.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-O4NWA36T-BUKCkyd7.js → quadrantDiagram-O4NWA36T-BUdNHhtc.js} +1 -1
- package/dist/client/assets/{radar-RG4KPBEZ-D77V4y6h.js → radar-RG4KPBEZ-B3MRkz7l.js} +1 -1
- package/dist/client/assets/{railroad-74A4TZTK-CIYb8bnG.js → railroad-74A4TZTK-CPQddZw9.js} +1 -1
- package/dist/client/assets/railroad-abnf-HS5TGJTU-CijG3JBn.js +1 -0
- package/dist/client/assets/railroad-ebnf-LZEXJU2U-CNgXgJgc.js +1 -0
- package/dist/client/assets/railroad-peg-WCYAUIDC-BkwGmMtj.js +1 -0
- package/dist/client/assets/{railroadDiagram-XR7U4H2S-DhUQyIW9.js → railroadDiagram-XR7U4H2S-B2wicmNk.js} +1 -1
- package/dist/client/assets/{requirementDiagram-PLB6GJNP-5HRstoAa.js → requirementDiagram-PLB6GJNP-4fGmNry2.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-IPEJSGJF-sNbgZbEH.js → sankeyDiagram-IPEJSGJF-ByuhL9nO.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-PO4LG4MO-B_vIrQ3k.js → sequenceDiagram-PO4LG4MO-CHeFXNrS.js} +1 -1
- package/dist/client/assets/{src-BWz11yEV.js → src-DuEx6MBc.js} +1 -1
- package/dist/client/assets/{stateDiagram-v2-GCMORJYK-BnRt6hQF.js → stateDiagram-v2-GCMORJYK-Dsfly8rU.js} +1 -1
- package/dist/client/assets/{swimlanes-2SLR337P-CLlJdLDU.js → swimlanes-2SLR337P-DDb1xXAV.js} +1 -1
- package/dist/client/assets/swimlanesDiagram-TC7HE7FX-DXpYpXP-.js +8 -0
- package/dist/client/assets/{timeline-definition-EJHVYXUP-sXwAwUmH.js → timeline-definition-EJHVYXUP-iC4lxMiK.js} +1 -1
- package/dist/client/assets/{treeView-Q6P3EWNA-BmhLEMPr.js → treeView-Q6P3EWNA-zXF3RWVa.js} +1 -1
- package/dist/client/assets/{treemap-WGGIJYW6-D_PGkPFQ.js → treemap-WGGIJYW6-BvA719M0.js} +1 -1
- package/dist/client/assets/{usecaseDiagram-POWQR4AR-GARihMJS.js → usecaseDiagram-POWQR4AR-CXzh5cdW.js} +1 -1
- package/dist/client/assets/{vennDiagram-UO4OBE2U-DXSYoTtG.js → vennDiagram-UO4OBE2U-BuMQeNKp.js} +1 -1
- package/dist/client/assets/{wardley-WFR3VGLG-Caebv6bl.js → wardley-WFR3VGLG-m1zZI6-6.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-VNRHLVJA-BnFa4EKC.js → wardleyDiagram-VNRHLVJA-DiB2QtGU.js} +1 -1
- package/dist/client/assets/{xychartDiagram-PMCCYNJV-XS2BbOyB.js → xychartDiagram-PMCCYNJV-CKRPoaV8.js} +1 -1
- package/dist/client/index.html +2 -2
- package/package.json +8 -8
- package/plugin.json +1 -1
- package/dist/client/assets/architecture-7GRP2DOG-Cx2ezWq8.js +0 -1
- package/dist/client/assets/channel-IK-3bv-L.js +0 -1
- package/dist/client/assets/eventmodeling-NTZA5JFV-Dh34QIVY.js +0 -1
- package/dist/client/assets/flowDiagram-KWPJA3E3-Bcos9jHd.js +0 -1
- package/dist/client/assets/index-DYt1d3uP.js +0 -93
- package/dist/client/assets/railroad-abnf-HS5TGJTU-OVikttJY.js +0 -1
- package/dist/client/assets/railroad-ebnf-LZEXJU2U-DPAI1fGw.js +0 -1
- package/dist/client/assets/railroad-peg-WCYAUIDC-CcsZpvwt.js +0 -1
- package/dist/client/assets/swimlanesDiagram-TC7HE7FX-D6iEiuTl.js +0 -8
package/dist/cli.mjs
CHANGED
|
@@ -17,9 +17,24 @@ import { stream, streamSSE } from "hono/streaming";
|
|
|
17
17
|
* token only: under Claude Code the transient `zsh -c` carries
|
|
18
18
|
* `~/.claude/shell-snapshots/…` in its arguments, which would match HARNESS. */
|
|
19
19
|
const SHELL = /^-?(?:\/[^ ]*\/)?(?:sh|bash|zsh|fish|dash|ksh|csh|tcsh)(?:\s|$)/;
|
|
20
|
+
/** The coding agents whose processes the ancestor walk recognises. Also the
|
|
21
|
+
* vocabulary of the `agent` field in usage data, so a name never travels unless
|
|
22
|
+
* it is on this list. */
|
|
23
|
+
const HARNESS_NAMES = [
|
|
24
|
+
"claude",
|
|
25
|
+
"cursor",
|
|
26
|
+
"codex",
|
|
27
|
+
"copilot",
|
|
28
|
+
"windsurf",
|
|
29
|
+
"zed",
|
|
30
|
+
"aider",
|
|
31
|
+
"goose",
|
|
32
|
+
"cline",
|
|
33
|
+
"amp"
|
|
34
|
+
];
|
|
20
35
|
/** A harness name at the start of a path segment or token, never inside a word:
|
|
21
36
|
* `/Users/sam/example/` must not read as `amp`. */
|
|
22
|
-
const HARNESS =
|
|
37
|
+
const HARNESS = new RegExp(`(?:^|[/\\s@_.-])(${HARNESS_NAMES.join("|")})`, "i");
|
|
23
38
|
function ownInvocation() {
|
|
24
39
|
return {
|
|
25
40
|
script: process.argv[1] ?? "",
|
|
@@ -34,15 +49,26 @@ function isOwnLauncher(command, own) {
|
|
|
34
49
|
const tail = own.args.length > 0 ? `${base} ${own.args.join(" ")}` : own.script;
|
|
35
50
|
return command === tail || command.endsWith(` ${tail}`) || command.endsWith(`/${tail}`);
|
|
36
51
|
}
|
|
37
|
-
function
|
|
52
|
+
function pickSession(ancestors, own) {
|
|
38
53
|
for (const row of ancestors) {
|
|
39
54
|
const command = row.command.trim();
|
|
40
55
|
if (SHELL.test(command)) continue;
|
|
41
56
|
if (isOwnLauncher(command, own)) continue;
|
|
42
|
-
|
|
57
|
+
const match = HARNESS.exec(command);
|
|
58
|
+
if (match) return {
|
|
59
|
+
pid: row.pid,
|
|
60
|
+
harness: match[1].toLowerCase()
|
|
61
|
+
};
|
|
43
62
|
}
|
|
44
63
|
return null;
|
|
45
64
|
}
|
|
65
|
+
function pickSessionAncestor(ancestors, own = ownInvocation()) {
|
|
66
|
+
return pickSession(ancestors, own)?.pid ?? null;
|
|
67
|
+
}
|
|
68
|
+
/** Which agent the session is, by the same walk that finds it. */
|
|
69
|
+
function pickSessionHarness(ancestors, own = ownInvocation()) {
|
|
70
|
+
return pickSession(ancestors, own)?.harness ?? null;
|
|
71
|
+
}
|
|
46
72
|
function readRow(pid) {
|
|
47
73
|
try {
|
|
48
74
|
const ppid = Number.parseInt(execFileSync("ps", [
|
|
@@ -87,6 +113,16 @@ function detectSessionPid() {
|
|
|
87
113
|
return null;
|
|
88
114
|
}
|
|
89
115
|
}
|
|
116
|
+
/** The agent this CLI runs under, or null from a plain terminal (and on Windows,
|
|
117
|
+
* where the walk is not available). Only ever a name from HARNESS_NAMES. */
|
|
118
|
+
function detectHarness() {
|
|
119
|
+
if (process.platform === "win32") return null;
|
|
120
|
+
try {
|
|
121
|
+
return pickSessionHarness(readAncestors(process.ppid));
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
90
126
|
//#endregion
|
|
91
127
|
//#region src/shared/review.ts
|
|
92
128
|
const THREAD_INTENTS = ["fix", "question"];
|
|
@@ -1019,6 +1055,8 @@ For the reviewer:
|
|
|
1019
1055
|
setup Register diffo with the coding agents on this machine
|
|
1020
1056
|
(Claude Code, Cursor, VS Code, Copilot, and the shared
|
|
1021
1057
|
~/.agents skills dir read by Codex, Gemini, Amp, Goose, …)
|
|
1058
|
+
telemetry Show whether anonymous usage data is on, or turn it
|
|
1059
|
+
off / on (telemetry off · telemetry on)
|
|
1022
1060
|
|
|
1023
1061
|
For the agent (the AI that wrote the change):
|
|
1024
1062
|
poll Wait for the reviewer's feedback (blocking long-poll;
|
|
@@ -1358,7 +1396,33 @@ are left as they are, and opening a review keeps installed skills fresh
|
|
|
1358
1396
|
automatically after upgrades.
|
|
1359
1397
|
|
|
1360
1398
|
Example:
|
|
1361
|
-
diffo setup
|
|
1399
|
+
diffo setup`,
|
|
1400
|
+
telemetry: `diffo telemetry: anonymous usage data — see it, turn it off, turn it on
|
|
1401
|
+
|
|
1402
|
+
Usage: diffo telemetry [status|on|off]
|
|
1403
|
+
|
|
1404
|
+
Diffo reports two small anonymous events per review, "opened" and "finished":
|
|
1405
|
+
its version, your OS and Node major, the kind of review (working tree, branch,
|
|
1406
|
+
or pull request), which agent opened it, counts (threads, comments, whether
|
|
1407
|
+
layers were used, a coarse duration), and whether it runs from a source
|
|
1408
|
+
checkout (dev: true, so the maintainers' own use is filtered out). Never code,
|
|
1409
|
+
paths, repository or branch names, comment text, or anything about the pull
|
|
1410
|
+
request. One random id per machine tells returning machines from new ones;
|
|
1411
|
+
turning reporting off deletes it. Nothing is sent until the review page has
|
|
1412
|
+
shown you the notice once, and nothing from the review that showed it.
|
|
1413
|
+
|
|
1414
|
+
status what is on, why it is off, and where the data goes (the default)
|
|
1415
|
+
off stop reporting on this machine, for good (a machine that has
|
|
1416
|
+
reported sends one last event saying so, then nothing)
|
|
1417
|
+
on start again (a fresh id, not the old history)
|
|
1418
|
+
|
|
1419
|
+
DIFFO_TELEMETRY_DISABLED=1 or DO_NOT_TRACK=1 turns it off for one process or
|
|
1420
|
+
for a fleet; CI does the same. DIFFO_TELEMETRY_DEBUG=1 writes every payload to
|
|
1421
|
+
the server log before it is sent. The full list, and where it goes:
|
|
1422
|
+
https://diffohq.github.io/diffo/telemetry
|
|
1423
|
+
|
|
1424
|
+
Example:
|
|
1425
|
+
diffo telemetry off`
|
|
1362
1426
|
};
|
|
1363
1427
|
function helpFor(topic) {
|
|
1364
1428
|
return topic && VERB_HELP[topic] || HELP_TEXT;
|
|
@@ -1370,6 +1434,7 @@ const VERBS = /* @__PURE__ */ new Set([
|
|
|
1370
1434
|
"layers",
|
|
1371
1435
|
"end",
|
|
1372
1436
|
"setup",
|
|
1437
|
+
"telemetry",
|
|
1373
1438
|
"status",
|
|
1374
1439
|
"stop",
|
|
1375
1440
|
"clean",
|
|
@@ -1653,9 +1718,38 @@ function parseCleanVerb(rest) {
|
|
|
1653
1718
|
all: parsed.value.values.all === true
|
|
1654
1719
|
};
|
|
1655
1720
|
}
|
|
1721
|
+
function parseTelemetryVerb(rest) {
|
|
1722
|
+
const parsed = tryParse(() => parseArgs({
|
|
1723
|
+
args: rest,
|
|
1724
|
+
allowPositionals: true,
|
|
1725
|
+
options: { help: {
|
|
1726
|
+
type: "boolean",
|
|
1727
|
+
short: "h"
|
|
1728
|
+
} }
|
|
1729
|
+
}));
|
|
1730
|
+
if (!parsed.ok) return {
|
|
1731
|
+
kind: "error",
|
|
1732
|
+
message: parsed.message
|
|
1733
|
+
};
|
|
1734
|
+
const { values, positionals } = parsed.value;
|
|
1735
|
+
if (values.help) return {
|
|
1736
|
+
kind: "help",
|
|
1737
|
+
topic: "telemetry"
|
|
1738
|
+
};
|
|
1739
|
+
const action = positionals[0] ?? "status";
|
|
1740
|
+
if (positionals.length > 1 || action !== "status" && action !== "on" && action !== "off") return {
|
|
1741
|
+
kind: "error",
|
|
1742
|
+
message: "telemetry takes one of: status, on, off"
|
|
1743
|
+
};
|
|
1744
|
+
return {
|
|
1745
|
+
kind: "telemetry",
|
|
1746
|
+
action
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1656
1749
|
function parseVerb(verb, rest) {
|
|
1657
1750
|
if (verb === "layers") return parseLayersVerb(rest);
|
|
1658
1751
|
if (verb === "clean") return parseCleanVerb(rest);
|
|
1752
|
+
if (verb === "telemetry") return parseTelemetryVerb(rest);
|
|
1659
1753
|
const parsed = tryParse(() => parseArgs({
|
|
1660
1754
|
args: rest,
|
|
1661
1755
|
allowPositionals: true,
|
|
@@ -1865,7 +1959,6 @@ query($owner: String!, $name: String!, $number: Int!) {
|
|
|
1865
1959
|
messageHeadline
|
|
1866
1960
|
committedDate
|
|
1867
1961
|
author { name user { login avatarUrl } }
|
|
1868
|
-
statusCheckRollup { state }
|
|
1869
1962
|
}
|
|
1870
1963
|
}
|
|
1871
1964
|
}
|
|
@@ -1880,6 +1973,24 @@ query($owner: String!, $name: String!, $number: Int!) {
|
|
|
1880
1973
|
}
|
|
1881
1974
|
}
|
|
1882
1975
|
}`;
|
|
1976
|
+
/**
|
|
1977
|
+
* CI status, asked for on its own. Reading it needs the checks and statuses
|
|
1978
|
+
* scopes, which a fine-grained or SAML-limited token may lack while reading
|
|
1979
|
+
* everything else about the pull request; GraphQL answers that with a
|
|
1980
|
+
* FORBIDDEN error on this one field, and `gh` turns any error into a failed
|
|
1981
|
+
* call. Kept out of the PR query so a token that cannot see CI still opens
|
|
1982
|
+
* the review.
|
|
1983
|
+
*/
|
|
1984
|
+
const CHECKS_QUERY = `
|
|
1985
|
+
query($owner: String!, $name: String!, $number: Int!) {
|
|
1986
|
+
repository(owner: $owner, name: $name) {
|
|
1987
|
+
pullRequest(number: $number) {
|
|
1988
|
+
commits(last: 1) {
|
|
1989
|
+
nodes { commit { statusCheckRollup { state } } }
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}`;
|
|
1883
1994
|
/** The pages after the first of each connection the PR query opens. */
|
|
1884
1995
|
const REVIEWS_QUERY = `
|
|
1885
1996
|
query($owner: String!, $name: String!, $number: Int!, $cursor: String) {
|
|
@@ -2126,18 +2237,32 @@ var GhClient = class {
|
|
|
2126
2237
|
reviewNodes
|
|
2127
2238
|
};
|
|
2128
2239
|
}
|
|
2240
|
+
/** CI on the head commit, best effort: a token without the checks scope
|
|
2241
|
+
* gets `unknown` and the review opens anyway. Never throws. */
|
|
2242
|
+
async checks(ref) {
|
|
2243
|
+
try {
|
|
2244
|
+
const rollup = ((await this.graphql(ref.host, CHECKS_QUERY, {
|
|
2245
|
+
owner: ref.owner,
|
|
2246
|
+
name: ref.repo,
|
|
2247
|
+
number: ref.number
|
|
2248
|
+
})).repository?.pullRequest?.commits.nodes.at(-1)?.commit)?.statusCheckRollup;
|
|
2249
|
+
return checkState(rollup?.state);
|
|
2250
|
+
} catch {
|
|
2251
|
+
return "unknown";
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2129
2254
|
async getPr(ref) {
|
|
2130
|
-
const { viewer, pr, reviewNodes } = await this.head(ref);
|
|
2131
|
-
return prInfo(ref, viewer, pr, reviewNodes);
|
|
2255
|
+
const [{ viewer, pr, reviewNodes }, checks] = await Promise.all([this.head(ref), this.checks(ref)]);
|
|
2256
|
+
return prInfo(ref, viewer, pr, reviewNodes, checks);
|
|
2132
2257
|
}
|
|
2133
2258
|
async listThreads(ref) {
|
|
2134
2259
|
const { pr, reviewNodes } = await this.head(ref);
|
|
2135
2260
|
return this.conversation(ref, pr, reviewNodes);
|
|
2136
2261
|
}
|
|
2137
2262
|
async fetchPr(ref) {
|
|
2138
|
-
const { viewer, pr, reviewNodes } = await this.head(ref);
|
|
2263
|
+
const [{ viewer, pr, reviewNodes }, checks] = await Promise.all([this.head(ref), this.checks(ref)]);
|
|
2139
2264
|
return {
|
|
2140
|
-
pr: prInfo(ref, viewer, pr, reviewNodes),
|
|
2265
|
+
pr: prInfo(ref, viewer, pr, reviewNodes, checks),
|
|
2141
2266
|
threads: await this.conversation(ref, pr, reviewNodes)
|
|
2142
2267
|
};
|
|
2143
2268
|
}
|
|
@@ -2251,9 +2376,8 @@ var GhClient = class {
|
|
|
2251
2376
|
}
|
|
2252
2377
|
};
|
|
2253
2378
|
/** The PR query's answer as a PrInfo, with every review page in hand. */
|
|
2254
|
-
function prInfo(ref, viewer, pr, reviewNodes) {
|
|
2255
|
-
const
|
|
2256
|
-
const commits = commitNodes.map((n) => {
|
|
2379
|
+
function prInfo(ref, viewer, pr, reviewNodes, checks) {
|
|
2380
|
+
const commits = (pr.commits.nodes ?? []).map((n) => {
|
|
2257
2381
|
const c = n.commit;
|
|
2258
2382
|
const author = c.author;
|
|
2259
2383
|
return {
|
|
@@ -2263,7 +2387,6 @@ function prInfo(ref, viewer, pr, reviewNodes) {
|
|
|
2263
2387
|
at: String(c.committedDate ?? "")
|
|
2264
2388
|
};
|
|
2265
2389
|
});
|
|
2266
|
-
const rollup = (commitNodes.at(-1)?.commit)?.statusCheckRollup;
|
|
2267
2390
|
const reviews = reviewNodes.map((r) => ({
|
|
2268
2391
|
id: String(r.id),
|
|
2269
2392
|
author: user(r.author),
|
|
@@ -2296,7 +2419,7 @@ function prInfo(ref, viewer, pr, reviewNodes) {
|
|
|
2296
2419
|
sha: String(pr.headRefOid)
|
|
2297
2420
|
},
|
|
2298
2421
|
commits,
|
|
2299
|
-
checks: { state:
|
|
2422
|
+
checks: { state: checks },
|
|
2300
2423
|
reviews: reviews.filter((r) => r.state !== "PENDING"),
|
|
2301
2424
|
approvals: [...latest.values()].filter((s) => s === "APPROVED").length,
|
|
2302
2425
|
changesRequested: [...latest.values()].filter((s) => s === "CHANGES_REQUESTED").length,
|
|
@@ -2514,6 +2637,25 @@ function branchExists(root, branch) {
|
|
|
2514
2637
|
return err.status !== 1;
|
|
2515
2638
|
}
|
|
2516
2639
|
}
|
|
2640
|
+
/** {@link branchExists}, off the event loop, for the daemon's background sweep
|
|
2641
|
+
* over every repo it has ever reviewed. Same lean: true unless git said no. */
|
|
2642
|
+
async function branchExistsAsync(root, branch) {
|
|
2643
|
+
try {
|
|
2644
|
+
await execFileAsync("git", [
|
|
2645
|
+
"show-ref",
|
|
2646
|
+
"--verify",
|
|
2647
|
+
"--quiet",
|
|
2648
|
+
`refs/heads/${branch}`
|
|
2649
|
+
], {
|
|
2650
|
+
cwd: root,
|
|
2651
|
+
encoding: "utf-8",
|
|
2652
|
+
maxBuffer: MAX_BUFFER
|
|
2653
|
+
});
|
|
2654
|
+
return true;
|
|
2655
|
+
} catch (err) {
|
|
2656
|
+
return err.code !== 1;
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2517
2659
|
/**
|
|
2518
2660
|
* The name of the linked worktree we're in, or null in the main one. A linked
|
|
2519
2661
|
* worktree's git dir is `<main>/.git/worktrees/<name>`, while the main worktree's
|
|
@@ -3148,7 +3290,17 @@ const SCHEMA_VERSION = 3;
|
|
|
3148
3290
|
const REVIEWS_RESHAPED_AT = 2;
|
|
3149
3291
|
var DiffoDb = class {
|
|
3150
3292
|
db;
|
|
3293
|
+
closed = false;
|
|
3151
3294
|
path;
|
|
3295
|
+
/**
|
|
3296
|
+
* Opening is cheap on purpose: the schema, and one SQL statement for the TTL.
|
|
3297
|
+
* Nothing here touches the filesystem beyond the database file, and nothing
|
|
3298
|
+
* asks git. It used to: every open checked every review's branch with a
|
|
3299
|
+
* `git show-ref`, across every repo this machine ever reviewed — 73 git
|
|
3300
|
+
* processes, 1.5s, on a database a few weeks old, and an open constructs
|
|
3301
|
+
* this five times. The checks live in {@link pruneRepo} (one repo, before its
|
|
3302
|
+
* review loads) and {@link maintain} (everything, in the daemon's background).
|
|
3303
|
+
*/
|
|
3152
3304
|
constructor(path = defaultDbPath()) {
|
|
3153
3305
|
this.path = path;
|
|
3154
3306
|
mkdirSync(dirname(path), {
|
|
@@ -3160,7 +3312,7 @@ var DiffoDb = class {
|
|
|
3160
3312
|
chmodSync(path, 384);
|
|
3161
3313
|
this.db.exec("PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 3000; PRAGMA journal_size_limit = 4194304;");
|
|
3162
3314
|
this.ensureSchema();
|
|
3163
|
-
this.
|
|
3315
|
+
this.pruneExpiredReviews();
|
|
3164
3316
|
}
|
|
3165
3317
|
/**
|
|
3166
3318
|
* Creates what is missing and retires what is unknown. Runs at open, and
|
|
@@ -3229,38 +3381,69 @@ var DiffoDb = class {
|
|
|
3229
3381
|
}
|
|
3230
3382
|
}
|
|
3231
3383
|
/**
|
|
3232
|
-
*
|
|
3233
|
-
*
|
|
3234
|
-
*
|
|
3235
|
-
|
|
3384
|
+
* Reviews untouched for `REVIEW_TTL_DAYS` go on every open: one statement,
|
|
3385
|
+
* no git, and the backstop for everything the two targeted sweeps below leave
|
|
3386
|
+
* for later.
|
|
3387
|
+
*/
|
|
3388
|
+
pruneExpiredReviews() {
|
|
3389
|
+
const cutoff = (/* @__PURE__ */ new Date(Date.now() - 5184e6)).toISOString();
|
|
3390
|
+
const stale = this.db.prepare("DELETE FROM reviews WHERE updated_at < ?").run(cutoff);
|
|
3391
|
+
if (Number(stale.changes) > 0) this.checkpoint();
|
|
3392
|
+
}
|
|
3393
|
+
/**
|
|
3394
|
+
* Retire this one repo's reviews for branches it no longer has. The server
|
|
3395
|
+
* runs it before loading the review it is about to serve, so a branch
|
|
3396
|
+
* deleted since the last server start is forgotten before a namesake could
|
|
3397
|
+
* inherit its threads. Only this repo's branches are asked about, so the
|
|
3398
|
+
* cost is a handful of `git show-ref`s, once per server start, never per CLI
|
|
3399
|
+
* call. Branch existence is asked of git rather than assumed — when git can't
|
|
3400
|
+
* answer the row is kept, because guessing here deletes threads.
|
|
3401
|
+
*/
|
|
3402
|
+
pruneRepo(repoPath) {
|
|
3403
|
+
const rows = this.db.prepare("SELECT DISTINCT branch FROM reviews WHERE repo_path = ?").all(repoPath);
|
|
3404
|
+
let dropped = 0;
|
|
3405
|
+
for (const { branch } of rows) {
|
|
3406
|
+
if (branch === "" || branchExists(repoPath, branch)) continue;
|
|
3407
|
+
dropped += this.dropReviews(repoPath, branch);
|
|
3408
|
+
}
|
|
3409
|
+
return dropped;
|
|
3410
|
+
}
|
|
3411
|
+
/**
|
|
3412
|
+
* The full sweep, for a long-lived process with time on its hands: rows about
|
|
3413
|
+
* repos that are gone, and reviews for branches that are. The daemon runs it
|
|
3414
|
+
* once, a few seconds after it is serving; `diffo clean` runs it on demand.
|
|
3415
|
+
* Git is asked asynchronously and one repo at a time, so a sweep over every
|
|
3416
|
+
* repo this machine ever reviewed never blocks a request. A `close()` midway
|
|
3417
|
+
* ends it quietly between steps.
|
|
3418
|
+
*
|
|
3419
|
+
* Rows about a worktree that is gone can never be reached again — the path
|
|
3420
|
+
* *is* the key. An unmounted volume looks the same as a deleted worktree, so
|
|
3421
|
+
* its rows go too; the `servers` row needs its process dead as well, or a live
|
|
3422
|
+
* server's claim could be dropped over a moment of unreachability.
|
|
3236
3423
|
*/
|
|
3237
|
-
|
|
3238
|
-
|
|
3424
|
+
async maintain() {
|
|
3425
|
+
const report = { reviews: 0 };
|
|
3426
|
+
if (this.closed) return report;
|
|
3427
|
+
for (const { repo_path } of this.distinctRepoPaths("reviews")) if (!existsSync(repo_path)) report.reviews += this.dropReviews(repo_path);
|
|
3239
3428
|
for (const { repo_path } of this.distinctRepoPaths("repo_ports")) if (!existsSync(repo_path)) this.db.prepare("DELETE FROM repo_ports WHERE repo_path = ?").run(repo_path);
|
|
3240
3429
|
const worktrees = this.db.prepare("SELECT worktree_path, repo_path FROM worktrees").all();
|
|
3241
3430
|
for (const { worktree_path, repo_path } of worktrees) if (!existsSync(repo_path)) this.db.prepare("DELETE FROM worktrees WHERE worktree_path = ?").run(worktree_path);
|
|
3242
3431
|
const servers = this.db.prepare("SELECT repo_path, pid FROM servers").all();
|
|
3243
3432
|
for (const { repo_path, pid } of servers) if (!existsSync(repo_path) && !pidAlive$1(pid)) this.db.prepare("DELETE FROM servers WHERE repo_path = ?").run(repo_path);
|
|
3244
|
-
this.pruneStaleReviews();
|
|
3245
|
-
}
|
|
3246
|
-
/**
|
|
3247
|
-
* Reviews the reviewer can never return to: a branch that no longer exists, or
|
|
3248
|
-
* one untouched for `REVIEW_TTL_DAYS`. Branch existence is asked of git rather
|
|
3249
|
-
* than assumed — if git can't answer the row is kept, because guessing here
|
|
3250
|
-
* deletes threads.
|
|
3251
|
-
*/
|
|
3252
|
-
pruneStaleReviews() {
|
|
3253
|
-
const cutoff = (/* @__PURE__ */ new Date(Date.now() - 5184e6)).toISOString();
|
|
3254
|
-
const stale = this.db.prepare("DELETE FROM reviews WHERE updated_at < ?").run(cutoff);
|
|
3255
|
-
if (Number(stale.changes) > 0) {
|
|
3256
|
-
this.checkpoint();
|
|
3257
|
-
return;
|
|
3258
|
-
}
|
|
3259
3433
|
const rows = this.db.prepare("SELECT DISTINCT repo_path, branch FROM reviews").all();
|
|
3260
3434
|
for (const { repo_path, branch } of rows) {
|
|
3261
|
-
if (branch === ""
|
|
3262
|
-
|
|
3435
|
+
if (branch === "") continue;
|
|
3436
|
+
const exists = await branchExistsAsync(repo_path, branch);
|
|
3437
|
+
if (this.closed) return report;
|
|
3438
|
+
if (!exists) report.reviews += this.dropReviews(repo_path, branch);
|
|
3263
3439
|
}
|
|
3440
|
+
if (report.reviews > 0) this.checkpoint();
|
|
3441
|
+
return report;
|
|
3442
|
+
}
|
|
3443
|
+
/** Every review of a repo, or only one branch's. Returns the rows dropped. */
|
|
3444
|
+
dropReviews(repoPath, branch) {
|
|
3445
|
+
const result = branch === void 0 ? this.db.prepare("DELETE FROM reviews WHERE repo_path = ?").run(repoPath) : this.db.prepare("DELETE FROM reviews WHERE repo_path = ? AND branch = ?").run(repoPath, branch);
|
|
3446
|
+
return Number(result.changes);
|
|
3264
3447
|
}
|
|
3265
3448
|
/** Fold the write-ahead log back into the database file and truncate it. WAL
|
|
3266
3449
|
* only shrinks at a checkpoint, which a long-lived server never reaches. */
|
|
@@ -3358,6 +3541,7 @@ var DiffoDb = class {
|
|
|
3358
3541
|
else this.db.prepare("DELETE FROM servers WHERE repo_path = ? AND port = ?").run(repoPath, port);
|
|
3359
3542
|
}
|
|
3360
3543
|
close() {
|
|
3544
|
+
this.closed = true;
|
|
3361
3545
|
this.db.close();
|
|
3362
3546
|
}
|
|
3363
3547
|
};
|
|
@@ -3423,6 +3607,9 @@ var DeliveryQueue = class DeliveryQueue {
|
|
|
3423
3607
|
stalled = false;
|
|
3424
3608
|
stallTimer = null;
|
|
3425
3609
|
betweenPolls = false;
|
|
3610
|
+
/** Why the agent is parked between polls: a reply just landed, or its poll
|
|
3611
|
+
* ended on its own and the re-poll is expected. */
|
|
3612
|
+
parked = "replied";
|
|
3426
3613
|
graceTimer = null;
|
|
3427
3614
|
lastDetach = null;
|
|
3428
3615
|
since = Date.now();
|
|
@@ -3570,7 +3757,7 @@ var DeliveryQueue = class DeliveryQueue {
|
|
|
3570
3757
|
if (this.awaitingReply) return this.stalled ? "stalled" : "delivered";
|
|
3571
3758
|
if (this.outlining() !== null) return "delivered";
|
|
3572
3759
|
if (this.waiter) return "polling";
|
|
3573
|
-
if (this.betweenPolls) return
|
|
3760
|
+
if (this.betweenPolls) return this.parked;
|
|
3574
3761
|
return this.lastDetach ?? "no-agent";
|
|
3575
3762
|
}
|
|
3576
3763
|
hasListener() {
|
|
@@ -3623,9 +3810,10 @@ var DeliveryQueue = class DeliveryQueue {
|
|
|
3623
3810
|
if (this.stallTimer !== null) clearTimeout(this.stallTimer);
|
|
3624
3811
|
this.stallTimer = null;
|
|
3625
3812
|
}
|
|
3626
|
-
armGrace() {
|
|
3813
|
+
armGrace(why = "replied") {
|
|
3627
3814
|
this.clearGrace();
|
|
3628
3815
|
this.betweenPolls = true;
|
|
3816
|
+
this.parked = why;
|
|
3629
3817
|
const drop = () => {
|
|
3630
3818
|
this.graceTimer = null;
|
|
3631
3819
|
if (!this.betweenPolls) return;
|
|
@@ -3660,6 +3848,24 @@ var DeliveryQueue = class DeliveryQueue {
|
|
|
3660
3848
|
this.graceTimer = null;
|
|
3661
3849
|
this.betweenPolls = false;
|
|
3662
3850
|
}
|
|
3851
|
+
/**
|
|
3852
|
+
* A poll ended without `diffo end`: the poll window closed, or the process
|
|
3853
|
+
* died under its harness. Neither means the agent left — the CLI tells the
|
|
3854
|
+
* session to re-run the poll, and it usually does within moments — so the
|
|
3855
|
+
* reviewer keeps an agent instead of being asked to invite one again. The
|
|
3856
|
+
* agent parks in the same grace a reply uses: a named session is watched
|
|
3857
|
+
* until its process is gone (or the cap), an unnamed one gets the fixed
|
|
3858
|
+
* window. Only a session that is already dead reads as gone at once. When
|
|
3859
|
+
* the grace runs out without a re-poll, the reason it fell to waiting is
|
|
3860
|
+
* still the dead poll.
|
|
3861
|
+
*/
|
|
3862
|
+
pollDropped() {
|
|
3863
|
+
this.lastDetach = "disconnected";
|
|
3864
|
+
const pid = this.owner;
|
|
3865
|
+
const isAlive = this.liveness.isAlive ?? pidAlive;
|
|
3866
|
+
if (!(pid !== null && !isAlive(pid)) && !this.awaitingReply && this.outlining() === null) this.armGrace("repolling");
|
|
3867
|
+
this.notify();
|
|
3868
|
+
}
|
|
3663
3869
|
enqueueThreads(threadIds) {
|
|
3664
3870
|
const bucket = this.bucket();
|
|
3665
3871
|
for (const id of threadIds) bucket.threads.add(id);
|
|
@@ -3747,8 +3953,7 @@ var DeliveryQueue = class DeliveryQueue {
|
|
|
3747
3953
|
onAbort?.(() => {
|
|
3748
3954
|
if (this.waiter === waiter) {
|
|
3749
3955
|
this.releaseWaiter(null);
|
|
3750
|
-
this.
|
|
3751
|
-
this.notify();
|
|
3956
|
+
this.pollDropped();
|
|
3752
3957
|
}
|
|
3753
3958
|
});
|
|
3754
3959
|
});
|
|
@@ -5993,6 +6198,286 @@ var ChangesetStore = class {
|
|
|
5993
6198
|
}
|
|
5994
6199
|
};
|
|
5995
6200
|
//#endregion
|
|
6201
|
+
//#region src/server/telemetry.ts
|
|
6202
|
+
/**
|
|
6203
|
+
* Anonymous usage data, Homebrew-style: on by default, announced in the review
|
|
6204
|
+
* page before the first event leaves, one command or variable to turn off. Two
|
|
6205
|
+
* events per review, `review_opened` and `review_finished`, and one last
|
|
6206
|
+
* `telemetry_disabled` when a machine that has reported turns it off; `send`
|
|
6207
|
+
* below is the authoritative list of what they carry, and the docs page links here. Never
|
|
6208
|
+
* paths, repo or branch names, code, comment text, or anything IP-derived. The
|
|
6209
|
+
* one identifier is a random UUID per machine, in a file of its own
|
|
6210
|
+
* (`~/.diffo/telemetry-id`) rather than the database: `DIFFO_DB` moves the
|
|
6211
|
+
* database, and one machine must count as one machine wherever it keeps state.
|
|
6212
|
+
*/
|
|
6213
|
+
const TELEMETRY_DOCS_URL = "https://diffohq.github.io/diffo/telemetry";
|
|
6214
|
+
/** PostHog Cloud, EU region. */
|
|
6215
|
+
const TELEMETRY_ENDPOINT = "https://eu.i.posthog.com/capture/";
|
|
6216
|
+
/** Public and write-only by design: adds events, reads nothing. */
|
|
6217
|
+
const TELEMETRY_KEY = "phc_nZqoJQbyccraPZwxsHaaAUPjf5z8k2QqdnWYedyhQzCx";
|
|
6218
|
+
/** A blocked host must never be felt. */
|
|
6219
|
+
const SEND_TIMEOUT_MS = 2e3;
|
|
6220
|
+
function defaultMachineIdPath() {
|
|
6221
|
+
return join(homedir(), ".diffo", "telemetry-id");
|
|
6222
|
+
}
|
|
6223
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6224
|
+
/** One UUID and a newline, owner-readable like the database next to it. */
|
|
6225
|
+
function fileMachineIdStore(path = defaultMachineIdPath()) {
|
|
6226
|
+
const read = () => {
|
|
6227
|
+
try {
|
|
6228
|
+
const id = readFileSync(path, "utf8").trim();
|
|
6229
|
+
return UUID.test(id) ? id : null;
|
|
6230
|
+
} catch {
|
|
6231
|
+
return null;
|
|
6232
|
+
}
|
|
6233
|
+
};
|
|
6234
|
+
const write = (id, flag) => {
|
|
6235
|
+
try {
|
|
6236
|
+
mkdirSync(dirname(path), {
|
|
6237
|
+
recursive: true,
|
|
6238
|
+
mode: 448
|
|
6239
|
+
});
|
|
6240
|
+
writeFileSync(path, `${id}\n`, {
|
|
6241
|
+
flag,
|
|
6242
|
+
mode: 384
|
|
6243
|
+
});
|
|
6244
|
+
return id;
|
|
6245
|
+
} catch (err) {
|
|
6246
|
+
if (flag === "wx" && err.code === "EEXIST") return read() ?? write(id, "w");
|
|
6247
|
+
return null;
|
|
6248
|
+
}
|
|
6249
|
+
};
|
|
6250
|
+
return {
|
|
6251
|
+
read,
|
|
6252
|
+
claim: (id) => write(id, "wx"),
|
|
6253
|
+
clear() {
|
|
6254
|
+
try {
|
|
6255
|
+
rmSync(path, { force: true });
|
|
6256
|
+
} catch {}
|
|
6257
|
+
}
|
|
6258
|
+
};
|
|
6259
|
+
}
|
|
6260
|
+
const KEY = {
|
|
6261
|
+
enabled: "telemetry.enabled",
|
|
6262
|
+
notice: "telemetry.notice",
|
|
6263
|
+
noticeAt: "telemetry.notice_at"
|
|
6264
|
+
};
|
|
6265
|
+
const DEBUG_VAR = "DIFFO_TELEMETRY_DEBUG";
|
|
6266
|
+
/** `0`, `false` and the empty string mean unset, as most tools read them. */
|
|
6267
|
+
function isSet(value) {
|
|
6268
|
+
if (value === void 0) return false;
|
|
6269
|
+
const v = value.trim().toLowerCase();
|
|
6270
|
+
return v !== "" && v !== "0" && v !== "false";
|
|
6271
|
+
}
|
|
6272
|
+
/** The variable that turns usage data off, in precedence order, or null. */
|
|
6273
|
+
function envOptOut(env) {
|
|
6274
|
+
if (isSet(env.DO_NOT_TRACK)) return "DO_NOT_TRACK";
|
|
6275
|
+
if (isSet(env.DIFFO_TELEMETRY_DISABLED)) return "DIFFO_TELEMETRY_DISABLED";
|
|
6276
|
+
if (isSet(env.CI)) return "CI";
|
|
6277
|
+
return null;
|
|
6278
|
+
}
|
|
6279
|
+
function reviewKindOf(spec, pr) {
|
|
6280
|
+
if (pr) return "pr";
|
|
6281
|
+
return spec.kind === "branch" ? "branch" : "tree";
|
|
6282
|
+
}
|
|
6283
|
+
const AGENT_NAMES = /* @__PURE__ */ new Set([...HARNESS_NAMES, "unknown"]);
|
|
6284
|
+
/** Whatever the CLI reported, narrowed to a name this build knows. */
|
|
6285
|
+
function agentNameOf(raw) {
|
|
6286
|
+
return typeof raw === "string" && AGENT_NAMES.has(raw) ? raw : "other";
|
|
6287
|
+
}
|
|
6288
|
+
/** Coarse on purpose: low cardinality, and nobody's afternoon timed. */
|
|
6289
|
+
function durationBucket(ms) {
|
|
6290
|
+
const min = ms / 6e4;
|
|
6291
|
+
if (min < 5) return "<5m";
|
|
6292
|
+
if (min < 15) return "5-15m";
|
|
6293
|
+
if (min < 30) return "15-30m";
|
|
6294
|
+
if (min < 60) return "30-60m";
|
|
6295
|
+
return "1h+";
|
|
6296
|
+
}
|
|
6297
|
+
var Telemetry = class {
|
|
6298
|
+
store;
|
|
6299
|
+
ids;
|
|
6300
|
+
env;
|
|
6301
|
+
fetchImpl;
|
|
6302
|
+
now;
|
|
6303
|
+
log;
|
|
6304
|
+
dev;
|
|
6305
|
+
base;
|
|
6306
|
+
/** Decided at open and kept for the finish, so the review that first shows the
|
|
6307
|
+
* notice reports nothing at all. Undefined until the first open. */
|
|
6308
|
+
armedReview;
|
|
6309
|
+
openedAt;
|
|
6310
|
+
inFlight = [];
|
|
6311
|
+
/** The id this process settled on when the disk refused to keep one. */
|
|
6312
|
+
fallbackId;
|
|
6313
|
+
constructor(deps) {
|
|
6314
|
+
this.store = deps.store;
|
|
6315
|
+
this.ids = deps.ids;
|
|
6316
|
+
this.env = deps.env ?? process.env;
|
|
6317
|
+
this.fetchImpl = deps.fetch ?? globalThis.fetch;
|
|
6318
|
+
this.now = deps.now ?? Date.now;
|
|
6319
|
+
this.log = deps.log ?? (() => {});
|
|
6320
|
+
this.dev = deps.dev ?? false;
|
|
6321
|
+
this.base = {
|
|
6322
|
+
version: deps.version ?? VERSION,
|
|
6323
|
+
platform: deps.platform ?? process.platform,
|
|
6324
|
+
arch: deps.arch ?? process.arch,
|
|
6325
|
+
node: deps.nodeMajor ?? Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10),
|
|
6326
|
+
dev: this.dev
|
|
6327
|
+
};
|
|
6328
|
+
}
|
|
6329
|
+
status() {
|
|
6330
|
+
const shared = {
|
|
6331
|
+
notice: this.notice(),
|
|
6332
|
+
machineId: this.readMachineId(),
|
|
6333
|
+
dev: this.dev,
|
|
6334
|
+
docs: TELEMETRY_DOCS_URL
|
|
6335
|
+
};
|
|
6336
|
+
const variable = envOptOut(this.env);
|
|
6337
|
+
if (variable !== null) return {
|
|
6338
|
+
enabled: false,
|
|
6339
|
+
source: "env",
|
|
6340
|
+
variable,
|
|
6341
|
+
...shared
|
|
6342
|
+
};
|
|
6343
|
+
if (this.store.get(KEY.enabled) === "false") return {
|
|
6344
|
+
enabled: false,
|
|
6345
|
+
source: "setting",
|
|
6346
|
+
...shared
|
|
6347
|
+
};
|
|
6348
|
+
return {
|
|
6349
|
+
enabled: true,
|
|
6350
|
+
source: "default",
|
|
6351
|
+
...shared
|
|
6352
|
+
};
|
|
6353
|
+
}
|
|
6354
|
+
/**
|
|
6355
|
+
* Either way the machine id goes: back on means a new history, not the old one.
|
|
6356
|
+
* Turning off a machine that has reported sends one last `telemetry_disabled`,
|
|
6357
|
+
* so opt-outs can be counted; a machine that never reported, or one an
|
|
6358
|
+
* environment variable already silenced, sends nothing. Returns whether it did.
|
|
6359
|
+
*/
|
|
6360
|
+
setEnabled(on, source) {
|
|
6361
|
+
const farewell = !on && this.status().enabled && this.readMachineId() !== null;
|
|
6362
|
+
if (farewell) this.send("telemetry_disabled", { source });
|
|
6363
|
+
this.store.set(KEY.enabled, on ? "true" : "false");
|
|
6364
|
+
this.ids.clear();
|
|
6365
|
+
this.fallbackId = void 0;
|
|
6366
|
+
if (on) this.store.set(KEY.notice, "acknowledged");
|
|
6367
|
+
return farewell;
|
|
6368
|
+
}
|
|
6369
|
+
/** The review page rendered the notice; nothing is sent before this. */
|
|
6370
|
+
noticeShown() {
|
|
6371
|
+
if (this.notice() !== "pending") return;
|
|
6372
|
+
this.store.set(KEY.notice, "shown");
|
|
6373
|
+
this.store.set(KEY.noticeAt, new Date(this.now()).toISOString());
|
|
6374
|
+
}
|
|
6375
|
+
/** The reviewer dismissed the notice; it stays gone. */
|
|
6376
|
+
acknowledge() {
|
|
6377
|
+
if (this.notice() === "pending") this.noticeShown();
|
|
6378
|
+
this.store.set(KEY.notice, "acknowledged");
|
|
6379
|
+
}
|
|
6380
|
+
/** A `diffo` open landed. Decides whether this review reports, and says so. */
|
|
6381
|
+
reviewOpened(props) {
|
|
6382
|
+
const armed = this.armed();
|
|
6383
|
+
this.armedReview = armed;
|
|
6384
|
+
this.openedAt = this.now();
|
|
6385
|
+
if (armed) this.send("review_opened", {
|
|
6386
|
+
kind: props.kind,
|
|
6387
|
+
agent: props.agent
|
|
6388
|
+
});
|
|
6389
|
+
return armed;
|
|
6390
|
+
}
|
|
6391
|
+
/** Only for a review that was opened reporting, and is still on. */
|
|
6392
|
+
reviewFinished(props) {
|
|
6393
|
+
if (this.armedReview !== true || !this.armed()) return;
|
|
6394
|
+
this.send("review_finished", {
|
|
6395
|
+
kind: props.kind,
|
|
6396
|
+
threads: props.threads,
|
|
6397
|
+
comments: props.comments,
|
|
6398
|
+
layers: props.layers,
|
|
6399
|
+
...props.event ? { event: props.event } : {},
|
|
6400
|
+
...this.openedAt !== void 0 ? { duration: durationBucket(this.now() - this.openedAt) } : {}
|
|
6401
|
+
});
|
|
6402
|
+
}
|
|
6403
|
+
/** Every send so far, settled. Tests, and an orderly shutdown. */
|
|
6404
|
+
async flush() {
|
|
6405
|
+
await Promise.all(this.inFlight.splice(0));
|
|
6406
|
+
}
|
|
6407
|
+
notice() {
|
|
6408
|
+
const raw = this.store.get(KEY.notice);
|
|
6409
|
+
return raw === "shown" || raw === "acknowledged" ? raw : "pending";
|
|
6410
|
+
}
|
|
6411
|
+
/** On, and the notice has been on a screen at least once. */
|
|
6412
|
+
armed() {
|
|
6413
|
+
return this.status().enabled && this.notice() !== "pending";
|
|
6414
|
+
}
|
|
6415
|
+
readMachineId() {
|
|
6416
|
+
return this.ids.read() ?? this.fallbackId ?? null;
|
|
6417
|
+
}
|
|
6418
|
+
/** The id on disk, minted on the first send; a disk that refuses it gets one
|
|
6419
|
+
* id per process rather than one per event. */
|
|
6420
|
+
machineId() {
|
|
6421
|
+
const existing = this.readMachineId();
|
|
6422
|
+
if (existing) return existing;
|
|
6423
|
+
const id = randomUUID();
|
|
6424
|
+
const claimed = this.ids.claim(id);
|
|
6425
|
+
if (claimed === null) this.fallbackId = id;
|
|
6426
|
+
return claimed ?? id;
|
|
6427
|
+
}
|
|
6428
|
+
send(event, properties) {
|
|
6429
|
+
const payload = {
|
|
6430
|
+
api_key: TELEMETRY_KEY,
|
|
6431
|
+
event,
|
|
6432
|
+
distinct_id: this.machineId(),
|
|
6433
|
+
timestamp: new Date(this.now()).toISOString(),
|
|
6434
|
+
properties: {
|
|
6435
|
+
$lib: "diffo",
|
|
6436
|
+
$lib_version: this.base.version,
|
|
6437
|
+
$process_person_profile: false,
|
|
6438
|
+
$geoip_disable: true,
|
|
6439
|
+
...this.base,
|
|
6440
|
+
...properties
|
|
6441
|
+
}
|
|
6442
|
+
};
|
|
6443
|
+
if (isSet(this.env[DEBUG_VAR])) this.log(`telemetry: ${JSON.stringify(payload)}`);
|
|
6444
|
+
let request;
|
|
6445
|
+
try {
|
|
6446
|
+
request = this.fetchImpl(TELEMETRY_ENDPOINT, {
|
|
6447
|
+
method: "POST",
|
|
6448
|
+
headers: { "Content-Type": "application/json" },
|
|
6449
|
+
body: JSON.stringify(payload),
|
|
6450
|
+
signal: AbortSignal.timeout(SEND_TIMEOUT_MS)
|
|
6451
|
+
});
|
|
6452
|
+
} catch {
|
|
6453
|
+
request = Promise.resolve();
|
|
6454
|
+
}
|
|
6455
|
+
const debug = isSet(this.env[DEBUG_VAR]);
|
|
6456
|
+
this.inFlight.push(request.then((res) => {
|
|
6457
|
+
if (debug) this.log(`telemetry: ${event} → ${res?.status ?? "sent"}`);
|
|
6458
|
+
}, (err) => {
|
|
6459
|
+
if (debug) this.log(`telemetry: ${event} failed: ${err?.message ?? String(err)}`);
|
|
6460
|
+
}));
|
|
6461
|
+
}
|
|
6462
|
+
};
|
|
6463
|
+
/** `diffo telemetry [status|on|off]`, for a human's terminal. */
|
|
6464
|
+
function describeTelemetry(status, asked, farewell = false) {
|
|
6465
|
+
const lines = [];
|
|
6466
|
+
if (status.enabled) {
|
|
6467
|
+
lines.push(asked === "on" ? "usage data: on (turned on)" : "usage data: on", " what: two anonymous events per review (opened, finished) — version, OS,", " kind of review, agent, counts. Never code, paths, or comment text", " off: diffo telemetry off, or DIFFO_TELEMETRY_DISABLED=1 (DO_NOT_TRACK=1 counts too)");
|
|
6468
|
+
if (status.notice === "pending") lines.push(" note: nothing has been sent yet; the review page shows a notice first");
|
|
6469
|
+
if (status.dev) lines.push(" dev: running from a source checkout; every event carries dev: true");
|
|
6470
|
+
} else {
|
|
6471
|
+
const why = status.source === "env" ? `${status.variable} is set` : asked === "off" ? "turned off" : "turned off with `diffo telemetry off`";
|
|
6472
|
+
lines.push(`usage data: off (${why})`);
|
|
6473
|
+
if (asked === "off" && farewell) lines.push(" sent: one last event saying this machine turned it off; nothing follows");
|
|
6474
|
+
if (asked === "on" && status.source === "env") lines.push(` note: it stays off while ${status.variable} is set`);
|
|
6475
|
+
if (status.source === "setting") lines.push(" on: diffo telemetry on");
|
|
6476
|
+
}
|
|
6477
|
+
lines.push(` more: ${status.docs}`);
|
|
6478
|
+
return lines.join("\n");
|
|
6479
|
+
}
|
|
6480
|
+
//#endregion
|
|
5996
6481
|
//#region src/server/watcher.ts
|
|
5997
6482
|
const DEBOUNCE_MS = 300;
|
|
5998
6483
|
const MAX_COOLDOWN_MS = 5e3;
|
|
@@ -6115,6 +6600,9 @@ const POLL_HEARTBEAT_MS = 15e3;
|
|
|
6115
6600
|
* aborts, and a listening poll blocks the daemon's idle reap.
|
|
6116
6601
|
*/
|
|
6117
6602
|
const POLL_MAX_MS = 18e5;
|
|
6603
|
+
/** How long after listening the daemon starts the machine-wide database sweep:
|
|
6604
|
+
* past the first page load, which is what the sweep must never delay. */
|
|
6605
|
+
const MAINTENANCE_DELAY_MS = 5e3;
|
|
6118
6606
|
function createApp(ctx, store, review, queue) {
|
|
6119
6607
|
const app = new Hono();
|
|
6120
6608
|
if (review && queue) queue.onBatchClosed((closed) => {
|
|
@@ -6185,6 +6673,31 @@ function createApp(ctx, store, review, queue) {
|
|
|
6185
6673
|
ctx.uiSettings.set("theme", theme);
|
|
6186
6674
|
return c.json({ ok: true });
|
|
6187
6675
|
});
|
|
6676
|
+
const reviewKind = () => reviewKindOf(ctx.spec, ctx.pr !== void 0);
|
|
6677
|
+
app.get("/api/telemetry", (c) => {
|
|
6678
|
+
if (!ctx.telemetry) return c.json({ error: "telemetry unavailable" }, 503);
|
|
6679
|
+
return c.json(ctx.telemetry.status());
|
|
6680
|
+
});
|
|
6681
|
+
app.put("/api/telemetry", async (c) => {
|
|
6682
|
+
if (!ctx.telemetry) return c.json({ error: "telemetry unavailable" }, 503);
|
|
6683
|
+
const body = await c.req.json().catch(() => null);
|
|
6684
|
+
if (typeof body?.enabled === "boolean") ctx.telemetry.setEnabled(body.enabled, "ui");
|
|
6685
|
+
else if (body?.notice === "shown") ctx.telemetry.noticeShown();
|
|
6686
|
+
else if (body?.notice === "acknowledged") ctx.telemetry.acknowledge();
|
|
6687
|
+
else return c.json({ error: "need {enabled: boolean} or {notice: \"shown\" | \"acknowledged\"}" }, 400);
|
|
6688
|
+
return c.json(ctx.telemetry.status());
|
|
6689
|
+
});
|
|
6690
|
+
app.post("/api/telemetry/opened", async (c) => {
|
|
6691
|
+
const body = await c.req.json().catch(() => null);
|
|
6692
|
+
const armed = ctx.telemetry?.reviewOpened({
|
|
6693
|
+
kind: reviewKind(),
|
|
6694
|
+
agent: agentNameOf(body?.agent)
|
|
6695
|
+
}) ?? false;
|
|
6696
|
+
return c.json({
|
|
6697
|
+
ok: true,
|
|
6698
|
+
armed
|
|
6699
|
+
});
|
|
6700
|
+
});
|
|
6188
6701
|
const repoInfo = () => store?.get().repo ?? {
|
|
6189
6702
|
path: resolve(ctx.root),
|
|
6190
6703
|
name: "",
|
|
@@ -6543,6 +7056,13 @@ function createApp(ctx, store, review, queue) {
|
|
|
6543
7056
|
queue.enqueueFinish(coverage);
|
|
6544
7057
|
}
|
|
6545
7058
|
review.clearWithheld(batch.map((t) => t.id));
|
|
7059
|
+
ctx.telemetry?.reviewFinished({
|
|
7060
|
+
kind: reviewKind(),
|
|
7061
|
+
threads: batch.length,
|
|
7062
|
+
comments: batch.reduce((n, t) => n + t.messages.filter((m) => m.author === "reviewer").length, 0),
|
|
7063
|
+
layers: (review.get().layers?.items.length ?? 0) > 0,
|
|
7064
|
+
...event !== null ? { event } : {}
|
|
7065
|
+
});
|
|
6546
7066
|
return c.json({
|
|
6547
7067
|
threads: review.get().threads,
|
|
6548
7068
|
prompt,
|
|
@@ -6865,11 +7385,22 @@ function startServer(options) {
|
|
|
6865
7385
|
throw new RepoAlreadyServedError(holder);
|
|
6866
7386
|
}
|
|
6867
7387
|
db.setPreferredPort(resolve(options.root), options.port);
|
|
7388
|
+
const log = (message) => console.log(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB", { hour12: false })}] ${message}`);
|
|
7389
|
+
const retired = db.pruneRepo(resolve(options.root));
|
|
7390
|
+
if (retired > 0) log(`retired ${retired} ${retired === 1 ? "review" : "reviews"} for branches this repo no longer has`);
|
|
6868
7391
|
const review = new ReviewStore(options.root, db, options.spec);
|
|
6869
7392
|
const queue = new DeliveryQueue();
|
|
6870
7393
|
queue.rescope(store.get().repo.branch);
|
|
6871
7394
|
rehydrateQueue(review, queue);
|
|
6872
|
-
const
|
|
7395
|
+
const telemetry = options.telemetry ?? new Telemetry({
|
|
7396
|
+
store: {
|
|
7397
|
+
get: (key) => db.getUiSetting(key),
|
|
7398
|
+
set: (key, value) => db.setUiSetting(key, value)
|
|
7399
|
+
},
|
|
7400
|
+
ids: fileMachineIdStore(),
|
|
7401
|
+
log,
|
|
7402
|
+
dev: SRC_STAMP !== null || IS_DEV
|
|
7403
|
+
});
|
|
6873
7404
|
queue.subscribe((presence) => log(`agent ${presence}`));
|
|
6874
7405
|
const unsubscribeBatch = queue.onBatchClosed((closed) => {
|
|
6875
7406
|
const took = Math.round((closed.closedAt - closed.deliveredAt) / 1e3);
|
|
@@ -6931,6 +7462,7 @@ function startServer(options) {
|
|
|
6931
7462
|
...options,
|
|
6932
7463
|
...prContext ? { pr: prContext } : {},
|
|
6933
7464
|
idle,
|
|
7465
|
+
telemetry,
|
|
6934
7466
|
onShutdownRequest: options.onShutdownRequest ?? (() => process.exit(0)),
|
|
6935
7467
|
uiSettings: {
|
|
6936
7468
|
get: (key) => db.getUiSetting(key),
|
|
@@ -6942,6 +7474,12 @@ function startServer(options) {
|
|
|
6942
7474
|
port: options.port,
|
|
6943
7475
|
hostname: "127.0.0.1"
|
|
6944
7476
|
});
|
|
7477
|
+
const maintenance = setTimeout(() => {
|
|
7478
|
+
db.maintain().then((report) => {
|
|
7479
|
+
if (report.reviews > 0) log(`retired ${report.reviews} ${report.reviews === 1 ? "review" : "reviews"} whose repo or branch is gone`);
|
|
7480
|
+
}).catch((err) => log(`database sweep failed: ${err.message}`));
|
|
7481
|
+
}, MAINTENANCE_DELAY_MS);
|
|
7482
|
+
maintenance.unref();
|
|
6945
7483
|
const deregister = () => {
|
|
6946
7484
|
try {
|
|
6947
7485
|
db.removeServer(review.repoPath, options.port);
|
|
@@ -6963,7 +7501,9 @@ function startServer(options) {
|
|
|
6963
7501
|
store,
|
|
6964
7502
|
review,
|
|
6965
7503
|
queue,
|
|
7504
|
+
telemetry,
|
|
6966
7505
|
stopWatching: async () => {
|
|
7506
|
+
clearTimeout(maintenance);
|
|
6967
7507
|
idle?.stop();
|
|
6968
7508
|
puller?.stop();
|
|
6969
7509
|
unsubscribeReconcile();
|
|
@@ -7569,6 +8109,7 @@ if (command.kind === "setup") {
|
|
|
7569
8109
|
if (command.kind === "clean") {
|
|
7570
8110
|
const db = new DiffoDb();
|
|
7571
8111
|
try {
|
|
8112
|
+
await db.maintain();
|
|
7572
8113
|
const entries = sweepWorktrees(db, void 0, {
|
|
7573
8114
|
force: command.force,
|
|
7574
8115
|
all: command.all
|
|
@@ -7686,6 +8227,21 @@ function printBaseHint(spec, files) {
|
|
|
7686
8227
|
if (!hint) return;
|
|
7687
8228
|
console.log(`this branch has ${hint.commits} ${hint.commits === 1 ? "commit" : "commits"} since ${hint.base}. Review them with \`diffo --base ${hint.base}\``);
|
|
7688
8229
|
}
|
|
8230
|
+
/**
|
|
8231
|
+
* Tells the server a review was opened, and by which agent — the server decides
|
|
8232
|
+
* whether that is reported (see server/telemetry.ts). Best effort: an open never
|
|
8233
|
+
* waits on, or fails over, usage data.
|
|
8234
|
+
*/
|
|
8235
|
+
async function announceOpen(port) {
|
|
8236
|
+
try {
|
|
8237
|
+
await fetch(apiUrl(port, "/api/telemetry/opened"), {
|
|
8238
|
+
method: "POST",
|
|
8239
|
+
headers: { "Content-Type": "application/json" },
|
|
8240
|
+
body: JSON.stringify({ agent: detectHarness() ?? "unknown" }),
|
|
8241
|
+
signal: AbortSignal.timeout(2e3)
|
|
8242
|
+
});
|
|
8243
|
+
} catch {}
|
|
8244
|
+
}
|
|
7689
8245
|
async function fetchChangesetInfo(port) {
|
|
7690
8246
|
try {
|
|
7691
8247
|
return await (await fetch(apiUrl(port, "/api/changeset"), { signal: AbortSignal.timeout(2e3) })).json();
|
|
@@ -7829,6 +8385,26 @@ if (command.kind === "stop") {
|
|
|
7829
8385
|
}
|
|
7830
8386
|
process.exit(0);
|
|
7831
8387
|
}
|
|
8388
|
+
if (command.kind === "telemetry") {
|
|
8389
|
+
const db = new DiffoDb();
|
|
8390
|
+
try {
|
|
8391
|
+
const telemetry = new Telemetry({
|
|
8392
|
+
store: {
|
|
8393
|
+
get: (key) => db.getUiSetting(key),
|
|
8394
|
+
set: (key, value) => db.setUiSetting(key, value)
|
|
8395
|
+
},
|
|
8396
|
+
ids: fileMachineIdStore(),
|
|
8397
|
+
log: (line) => process.stderr.write(`diffo: ${line}\n`),
|
|
8398
|
+
dev: SRC_STAMP !== null || process.env.ENV === "development"
|
|
8399
|
+
});
|
|
8400
|
+
const farewell = command.action === "status" ? false : telemetry.setEnabled(command.action === "on", "cli");
|
|
8401
|
+
console.log(describeTelemetry(telemetry.status(), command.action, farewell));
|
|
8402
|
+
await telemetry.flush();
|
|
8403
|
+
} finally {
|
|
8404
|
+
db.close();
|
|
8405
|
+
}
|
|
8406
|
+
process.exit(0);
|
|
8407
|
+
}
|
|
7832
8408
|
if (command.kind === "poll") {
|
|
7833
8409
|
const port = await requireServer();
|
|
7834
8410
|
process.stderr.write("diffo: waiting for the reviewer. Keep this process attended: a tracked\nbackground task or the foreground, never detached. If it dies, just\nre-run it; feedback is held in the review and survives.\n");
|
|
@@ -8047,6 +8623,7 @@ if (existing !== null && takeOverPort === null) {
|
|
|
8047
8623
|
const url = reviewUrl(existing.port);
|
|
8048
8624
|
console.log(prOpen ? "diffo is already watching this pull request" : "diffo is already watching this repo");
|
|
8049
8625
|
await warnSpecMismatch(existing.port, spec);
|
|
8626
|
+
await announceOpen(existing.port);
|
|
8050
8627
|
console.log(`→ ${url}`);
|
|
8051
8628
|
await printAgentNextStep(existing.port, prOpen?.pr);
|
|
8052
8629
|
process.exit(0);
|
|
@@ -8066,6 +8643,7 @@ if (existing !== null && takeOverPort !== null) {
|
|
|
8066
8643
|
}
|
|
8067
8644
|
if (!command.foreground) {
|
|
8068
8645
|
const daemonPort = await requireServer(command.port, spec.kind === "branch" ? spec.base : void 0, SRC_STAMP, prOpen ? command.target : void 0);
|
|
8646
|
+
await announceOpen(daemonPort);
|
|
8069
8647
|
const url = reviewUrl(daemonPort);
|
|
8070
8648
|
await printChangesetSummary(daemonPort);
|
|
8071
8649
|
console.log(`→ ${url}`);
|
|
@@ -8107,6 +8685,10 @@ try {
|
|
|
8107
8685
|
}
|
|
8108
8686
|
throw err;
|
|
8109
8687
|
}
|
|
8688
|
+
started.telemetry.reviewOpened({
|
|
8689
|
+
kind: reviewKindOf(spec, prOpen !== null),
|
|
8690
|
+
agent: detectHarness() ?? "unknown"
|
|
8691
|
+
});
|
|
8110
8692
|
const { stats, repo } = started.store.get();
|
|
8111
8693
|
const url = reviewUrl(port);
|
|
8112
8694
|
const label = prOpen ? `pull request #${prOpen.pr.number} "${prOpen.pr.title}"` : specLabel(spec);
|