@a9n-shoji/rvw 0.2.0 → 0.2.2
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/CHANGELOG.md +32 -0
- package/README.md +28 -7
- package/dist/cli.mjs +492 -307
- package/dist/cli.mjs.map +3 -3
- package/dist/web/assets/{DocumentViewer-CNgHpCuL.js → DocumentViewer-ADzspLC4.js} +32 -32
- package/dist/web/assets/{markdown-source-map-_dS1q5Dg.js → MarkdownTable-DVJ0_MIC.js} +2 -2
- package/dist/web/assets/WalkthroughViewer-yQibj2tK.js +1 -0
- package/dist/web/assets/{abnfDiagram-N423BO3Z-0v-ndtqQ.js → abnfDiagram-N423BO3Z-6dQzEuPV.js} +1 -1
- package/dist/web/assets/architecture-TIHT7OUA-DEoBuEnc.js +1 -0
- package/dist/web/assets/{architectureDiagram-T3A2C74G-WSNJ-tZE.js → architectureDiagram-T3A2C74G-DP7hHV7T.js} +1 -1
- package/dist/web/assets/{blockDiagram-VBNYF7ZC-lmMbEUwI.js → blockDiagram-VBNYF7ZC-ZbwzVKWP.js} +1 -1
- package/dist/web/assets/{c4Diagram-5PPSVZJV-BrN5YVV8.js → c4Diagram-5PPSVZJV-HVD9_uV4.js} +1 -1
- package/dist/web/assets/channel-DjgdV-zW.js +1 -0
- package/dist/web/assets/{chunk-2GRJ4B5K-gt2JPyrH.js → chunk-2GRJ4B5K-Bkxs2d4J.js} +1 -1
- package/dist/web/assets/{chunk-4I5QYGJK-Cg5GZ_-7.js → chunk-4I5QYGJK-BJHE6zC_.js} +1 -1
- package/dist/web/assets/{chunk-5RXB4S5H-BdhRGvxW.js → chunk-5RXB4S5H-B-52dfCQ.js} +1 -1
- package/dist/web/assets/{chunk-6Q2QTUOP-B1DYWNVJ.js → chunk-6Q2QTUOP-BpD0vQ66.js} +1 -1
- package/dist/web/assets/{chunk-7Z6QIM7H-D2YImamZ.js → chunk-7Z6QIM7H-BTI1qgAO.js} +1 -1
- package/dist/web/assets/{chunk-GF5L2VYU-CWtwL_CM.js → chunk-GF5L2VYU-B8BxoCCx.js} +1 -1
- package/dist/web/assets/{chunk-I66GZJ75-DdoGlxMt.js → chunk-I66GZJ75-zwi-Xf01.js} +1 -1
- package/dist/web/assets/{chunk-JQJVKLGR-CVWAp3XP.js → chunk-JQJVKLGR-D9mKBWUz.js} +1 -1
- package/dist/web/assets/{chunk-KBJHAD2P-BBacwg3J.js → chunk-KBJHAD2P-Qmtir3qc.js} +1 -1
- package/dist/web/assets/{chunk-NSK5VX7P-1AsXiBV3.js → chunk-NSK5VX7P-SP2vZdAo.js} +1 -1
- package/dist/web/assets/{chunk-QR6OTTB3-DL2s5pNp.js → chunk-QR6OTTB3-uVDCHurs.js} +1 -1
- package/dist/web/assets/{chunk-UBXNYLIW-mu3fkNQI.js → chunk-UBXNYLIW-CtNzBWOU.js} +1 -1
- package/dist/web/assets/{chunk-W5SLKNZC-BAwhMBBb.js → chunk-W5SLKNZC-BPfDqbmK.js} +1 -1
- package/dist/web/assets/{chunk-WRU74C26-C7IeXZaP.js → chunk-WRU74C26-BZkEOexq.js} +1 -1
- package/dist/web/assets/classDiagram-JCYQIIEL-B6Qjam7Y.js +1 -0
- package/dist/web/assets/classDiagram-v2-OCEON4UE-B6Qjam7Y.js +1 -0
- package/dist/web/assets/{cynefin-VYW2F7L2-DNisGvNv.js → cynefin-VYW2F7L2-BRisCyqC.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-MW4NZA55-BA27gOTz.js → cynefinDiagram-MW4NZA55-ClIrmCkB.js} +1 -1
- package/dist/web/assets/{dagre-VZM6K2ZE-vo_1YAuM.js → dagre-VZM6K2ZE-X31uC_JB.js} +1 -1
- package/dist/web/assets/{diagram-7IWD3JNH-DLLdUj0_.js → diagram-7IWD3JNH-DBQFjl0i.js} +1 -1
- package/dist/web/assets/{diagram-B4RE2ZJO-BV0WyNA-.js → diagram-B4RE2ZJO-BZ7934Wg.js} +1 -1
- package/dist/web/assets/{diagram-LBJQPF4R-DXxKSx2U.js → diagram-LBJQPF4R-CcJqH9aY.js} +1 -1
- package/dist/web/assets/{diagram-Q27KOJAE-DsQ2GzIK.js → diagram-Q27KOJAE-DzM7eiil.js} +1 -1
- package/dist/web/assets/{diagram-UB23O5K3-DfjElFP1.js → diagram-UB23O5K3-CVCV-d3m.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-BXEA7PRR-cYu0XV1a.js → ebnfDiagram-BXEA7PRR-D8xb9FBp.js} +1 -1
- package/dist/web/assets/{erDiagram-JOGREHBK-DnsLBHaV.js → erDiagram-JOGREHBK-1l-G_PMG.js} +1 -1
- package/dist/web/assets/eventmodeling-45OFAUF4-ndNeUBOZ.js +1 -0
- package/dist/web/assets/flowDiagram-UKHOOZJN-BVxK_6Tu.js +1 -0
- package/dist/web/assets/{ganttDiagram-PKOTCBZU-CVD5gq3E.js → ganttDiagram-PKOTCBZU-CDJ9a1tp.js} +1 -1
- package/dist/web/assets/{gitGraph-TEB2WS4Q-C_j0n-Kw.js → gitGraph-TEB2WS4Q-DOeTh5wB.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-DS77QQ5N-qACevJmW.js → gitGraphDiagram-DS77QQ5N-DYLxc8Nz.js} +1 -1
- package/dist/web/assets/index-9cOaPesh.js +384 -0
- package/dist/web/assets/index-kK3hngnz.css +1 -0
- package/dist/web/assets/{info-DKCQHKI2-Cmkbz3Yi.js → info-DKCQHKI2-B5u97QuQ.js} +1 -1
- package/dist/web/assets/{infoDiagram-6WML65LV-CDrAem7M.js → infoDiagram-6WML65LV-zosKW2OD.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-WSZJBQD7-DXgCnFqW.js → ishikawaDiagram-WSZJBQD7-VYcc_au8.js} +1 -1
- package/dist/web/assets/{journeyDiagram-NVQOT4AX-1KW-I7DV.js → journeyDiagram-NVQOT4AX-C_nU8kP4.js} +1 -1
- package/dist/web/assets/{kanban-definition-27J2QSJJ-CCt75OuF.js → kanban-definition-27J2QSJJ-DybKaWfx.js} +1 -1
- package/dist/web/assets/{line-DQfkDi_r.js → line-DWsGplGj.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-B7TMMtY0.js → mermaid-parser.core-D_T_3oIQ.js} +3 -3
- package/dist/web/assets/{mermaid.core-B93x-7Mk.js → mermaid.core-BoLGzxB5.js} +4 -4
- package/dist/web/assets/{mindmap-definition-FAOFIHXS-Dyt28V6S.js → mindmap-definition-FAOFIHXS-CsdpqPIa.js} +1 -1
- package/dist/web/assets/{packet-7NZHBO7P-zQ0u35s3.js → packet-7NZHBO7P-CReAEF1h.js} +1 -1
- package/dist/web/assets/{pegDiagram-VL7TDLO6-et0KHGIE.js → pegDiagram-VL7TDLO6-CE9-NPMu.js} +1 -1
- package/dist/web/assets/{pie-RZYD4A2V-uur86LkQ.js → pie-RZYD4A2V-GYixXUka.js} +1 -1
- package/dist/web/assets/{pieDiagram-7S7Q4E2Y-c027WPTB.js → pieDiagram-7S7Q4E2Y-CqaAKNg2.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-CIZ2JOQS-BtSV4E-0.js → quadrantDiagram-CIZ2JOQS-eRMHJqfj.js} +1 -1
- package/dist/web/assets/{radar-I7S5WNFK-B_g0VZMF.js → radar-I7S5WNFK-BwHxP0iK.js} +1 -1
- package/dist/web/assets/{railroad-3IZDKUUU-VFLsM_Em.js → railroad-3IZDKUUU-AHc3O4Oz.js} +1 -1
- package/dist/web/assets/railroad-abnf-AHOZXSZD-ZKY5olmV.js +1 -0
- package/dist/web/assets/railroad-ebnf-EBAXGLYW-CrG1UCMo.js +1 -0
- package/dist/web/assets/railroad-peg-LSFZ7HO6-CoY6MIbj.js +1 -0
- package/dist/web/assets/{railroadDiagram-AXF67PYL-uOtnxRbQ.js → railroadDiagram-AXF67PYL-BhAxCokG.js} +1 -1
- package/dist/web/assets/{requirementDiagram-LRYGKXZP-DGSZAsj4.js → requirementDiagram-LRYGKXZP-BRW-kYhg.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-W5VNT64P-CCDk8Vxt.js → sankeyDiagram-W5VNT64P-DfnYLbDJ.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-SI44F4Z6-Ce-Jzdk5.js → sequenceDiagram-SI44F4Z6-WEkIPSg8.js} +1 -1
- package/dist/web/assets/{stateDiagram-OKZ733FA-BYpYzyEa.js → stateDiagram-OKZ733FA-CdN7veIN.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-UEYNNEHI-0xn0gbpn.js +1 -0
- package/dist/web/assets/{swimlanes-SLNWSIFB-BL-CUc6d.js → swimlanes-SLNWSIFB-g9wWLf5Y.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-YJ3yh_d3.js +8 -0
- package/dist/web/assets/{timeline-definition-Z64GVDOM-p9pkof_K.js → timeline-definition-Z64GVDOM-DjkKZIPE.js} +1 -1
- package/dist/web/assets/{treeView-QDETBFTQ-dRhXTU9p.js → treeView-QDETBFTQ-WARa9r09.js} +1 -1
- package/dist/web/assets/{treemap-6X3UGDF4-CZHe3dIh.js → treemap-6X3UGDF4-CTFjdb6k.js} +1 -1
- package/dist/web/assets/{vennDiagram-T6HMQDX7-ipODJegx.js → vennDiagram-T6HMQDX7-CI9QrBwu.js} +1 -1
- package/dist/web/assets/{wardley-OPB4EBWU-CxHlVjkF.js → wardley-OPB4EBWU-Cfo_Nnla.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-T6FBY63Y-YarkIncH.js → wardleyDiagram-T6FBY63Y-CzGJ4fzZ.js} +1 -1
- package/dist/web/assets/{xychartDiagram-ELKLHX3M-Cw1M-7a4.js → xychartDiagram-ELKLHX3M-BXCiD7gJ.js} +1 -1
- package/dist/web/index.html +2 -2
- package/migrations/010_comment_post_references.sql +17 -0
- package/package.json +2 -1
- package/skills/rvw/SKILL.md +79 -6
- package/skills/rvw-walkthrough/SKILL.md +1 -1
- package/skills/rvw-watch-comments/SKILL.md +217 -67
- package/skills/rvw-watch-comments/scripts/auto-ack.mjs +211 -0
- package/skills/rvw-watch-comments/scripts/preflight.mjs +108 -0
- package/skills/rvw-watch-comments/scripts/rvw-command.mjs +80 -0
- package/skills/rvw-watch-comments/scripts/watch-driver.mjs +272 -0
- package/skills/rvw-watch-comments/scripts/watch-state.mjs +223 -70
- package/dist/web/assets/WalkthroughViewer-BVB3ETHG.js +0 -1
- package/dist/web/assets/architecture-TIHT7OUA-BjGWtjPU.js +0 -1
- package/dist/web/assets/channel-DkH22XO0.js +0 -1
- package/dist/web/assets/classDiagram-JCYQIIEL-C6Z4rmZR.js +0 -1
- package/dist/web/assets/classDiagram-v2-OCEON4UE-C6Z4rmZR.js +0 -1
- package/dist/web/assets/eventmodeling-45OFAUF4-BUAcQ5NU.js +0 -1
- package/dist/web/assets/flowDiagram-UKHOOZJN-hAe9ojjP.js +0 -1
- package/dist/web/assets/index-Coye1BY5.js +0 -384
- package/dist/web/assets/index-DFdYNMZE.css +0 -1
- package/dist/web/assets/railroad-abnf-AHOZXSZD-TVRi9_2L.js +0 -1
- package/dist/web/assets/railroad-ebnf-EBAXGLYW-B5B-JrS4.js +0 -1
- package/dist/web/assets/railroad-peg-LSFZ7HO6-CfIpa6ba.js +0 -1
- package/dist/web/assets/stateDiagram-v2-UEYNNEHI-BJFG9cb2.js +0 -1
- package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-BS8L8fb1.js +0 -8
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runRvw } from "./rvw-command.mjs";
|
|
4
|
+
|
|
5
|
+
const REQUIRED_NODE = [24, 15, 0];
|
|
6
|
+
const REQUIRED_PROTOCOL = 3;
|
|
7
|
+
const REQUIRED_CAPABILITIES = [
|
|
8
|
+
"agent.transport",
|
|
9
|
+
"comment.watch",
|
|
10
|
+
"comment.read",
|
|
11
|
+
"comment.reply",
|
|
12
|
+
"comment.edit",
|
|
13
|
+
"comment.codeReferences",
|
|
14
|
+
"pullRequest.sync",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function nodeIsSupported(version) {
|
|
18
|
+
const actual = version.split(".").map(Number);
|
|
19
|
+
for (let index = 0; index < REQUIRED_NODE.length; index += 1) {
|
|
20
|
+
if ((actual[index] ?? 0) > REQUIRED_NODE[index]) return true;
|
|
21
|
+
if ((actual[index] ?? 0) < REQUIRED_NODE[index]) return false;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function commandSummary(result) {
|
|
27
|
+
return {
|
|
28
|
+
exitCode: result.code,
|
|
29
|
+
signal: result.signal,
|
|
30
|
+
output: result.json,
|
|
31
|
+
...(result.stderr.trim() ? { stderr: result.stderr.trim() } : {}),
|
|
32
|
+
...(!result.json && result.stdout.trim() ? { stdout: result.stdout.trim() } : {}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function main() {
|
|
37
|
+
const settled = await Promise.allSettled([
|
|
38
|
+
runRvw(["protocol", "--json"]),
|
|
39
|
+
runRvw(["agent", "status", "--json"]),
|
|
40
|
+
runRvw(["agent", "ping", "--json"]),
|
|
41
|
+
]);
|
|
42
|
+
const [protocolResult, statusResult, pingResult] = settled.map((result) =>
|
|
43
|
+
result.status === "fulfilled" ? result.value : null,
|
|
44
|
+
);
|
|
45
|
+
const protocol = protocolResult?.json;
|
|
46
|
+
const agentStatus = statusResult?.json;
|
|
47
|
+
const agentPing = pingResult?.json;
|
|
48
|
+
const capabilities = Array.isArray(protocol?.capabilities) ? protocol.capabilities : [];
|
|
49
|
+
const missingCapabilities = REQUIRED_CAPABILITIES.filter(
|
|
50
|
+
(capability) => !capabilities.includes(capability),
|
|
51
|
+
);
|
|
52
|
+
const checks = {
|
|
53
|
+
nodeVersion: nodeIsSupported(process.versions.node),
|
|
54
|
+
rvwBinary: protocolResult !== null,
|
|
55
|
+
appVersion: typeof protocol?.appVersion === "string" && protocol.appVersion.length > 0,
|
|
56
|
+
protocol: protocol?.protocolVersion === REQUIRED_PROTOCOL,
|
|
57
|
+
capabilities: missingCapabilities.length === 0,
|
|
58
|
+
agentStatus: agentStatus?.ok === true && agentStatus.selectedTransport !== "unavailable",
|
|
59
|
+
agentPingInspected: agentPing !== null && typeof agentPing === "object",
|
|
60
|
+
agentPingConnected: agentPing?.ok === true && agentPing.connected === true,
|
|
61
|
+
};
|
|
62
|
+
const ok =
|
|
63
|
+
checks.nodeVersion &&
|
|
64
|
+
checks.rvwBinary &&
|
|
65
|
+
checks.appVersion &&
|
|
66
|
+
checks.protocol &&
|
|
67
|
+
checks.capabilities &&
|
|
68
|
+
checks.agentStatus &&
|
|
69
|
+
checks.agentPingInspected;
|
|
70
|
+
const output = {
|
|
71
|
+
ok,
|
|
72
|
+
node: {
|
|
73
|
+
version: process.versions.node,
|
|
74
|
+
required: ">=24.15.0",
|
|
75
|
+
ok: checks.nodeVersion,
|
|
76
|
+
},
|
|
77
|
+
rvw: {
|
|
78
|
+
binary: protocolResult?.binary ?? process.env.RVW_BIN ?? "rvw",
|
|
79
|
+
appVersion: typeof protocol?.appVersion === "string" ? protocol.appVersion : null,
|
|
80
|
+
protocolVersion:
|
|
81
|
+
typeof protocol?.protocolVersion === "number" ? protocol.protocolVersion : null,
|
|
82
|
+
requiredProtocolVersion: REQUIRED_PROTOCOL,
|
|
83
|
+
capabilities,
|
|
84
|
+
requiredCapabilities: REQUIRED_CAPABILITIES,
|
|
85
|
+
missingCapabilities,
|
|
86
|
+
},
|
|
87
|
+
agent: {
|
|
88
|
+
status: statusResult ? commandSummary(statusResult) : null,
|
|
89
|
+
ping: pingResult ? commandSummary(pingResult) : null,
|
|
90
|
+
pingRequired: false,
|
|
91
|
+
},
|
|
92
|
+
checks,
|
|
93
|
+
errors: settled.flatMap((result, index) =>
|
|
94
|
+
result.status === "rejected"
|
|
95
|
+
? [
|
|
96
|
+
{
|
|
97
|
+
command: ["protocol", "agent status", "agent ping"][index],
|
|
98
|
+
error: String(result.reason),
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
: [],
|
|
102
|
+
),
|
|
103
|
+
};
|
|
104
|
+
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
105
|
+
if (!ok) process.exitCode = 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await main();
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const MAX_CAPTURE_BYTES = 40 * 1024 * 1024;
|
|
4
|
+
|
|
5
|
+
function commandPrefix() {
|
|
6
|
+
const binary = process.env.RVW_BIN?.trim() || "rvw";
|
|
7
|
+
const rawPrefix = process.env.RVW_BIN_ARGS_JSON;
|
|
8
|
+
if (!rawPrefix) return { binary, prefix: [] };
|
|
9
|
+
let parsed;
|
|
10
|
+
try {
|
|
11
|
+
parsed = JSON.parse(rawPrefix);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
throw new Error(`RVW_BIN_ARGS_JSON must be a JSON string array: ${String(error)}`, {
|
|
14
|
+
cause: error,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
if (!Array.isArray(parsed) || parsed.some((value) => typeof value !== "string")) {
|
|
18
|
+
throw new Error("RVW_BIN_ARGS_JSON must be a JSON string array");
|
|
19
|
+
}
|
|
20
|
+
return { binary, prefix: parsed };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function spawnRvw(args, options = {}) {
|
|
24
|
+
const { binary, prefix } = commandPrefix();
|
|
25
|
+
return spawn(binary, [...prefix, ...args], {
|
|
26
|
+
stdio: options.stdio ?? ["pipe", "pipe", "pipe"],
|
|
27
|
+
env: process.env,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function runRvw(args, options = {}) {
|
|
32
|
+
const child = spawnRvw(args);
|
|
33
|
+
const stdout = [];
|
|
34
|
+
const stderr = [];
|
|
35
|
+
let stdoutBytes = 0;
|
|
36
|
+
let stderrBytes = 0;
|
|
37
|
+
child.stdout.on("data", (chunk) => {
|
|
38
|
+
stdoutBytes += chunk.length;
|
|
39
|
+
if (stdoutBytes <= MAX_CAPTURE_BYTES) stdout.push(chunk);
|
|
40
|
+
});
|
|
41
|
+
child.stderr.on("data", (chunk) => {
|
|
42
|
+
stderrBytes += chunk.length;
|
|
43
|
+
if (stderrBytes <= MAX_CAPTURE_BYTES) stderr.push(chunk);
|
|
44
|
+
});
|
|
45
|
+
if (options.input === undefined) child.stdin.end();
|
|
46
|
+
else
|
|
47
|
+
child.stdin.end(
|
|
48
|
+
typeof options.input === "string" ? options.input : JSON.stringify(options.input),
|
|
49
|
+
);
|
|
50
|
+
const status = await new Promise((resolve, reject) => {
|
|
51
|
+
child.once("error", reject);
|
|
52
|
+
child.once("close", (code, signal) => resolve({ code, signal }));
|
|
53
|
+
});
|
|
54
|
+
if (stdoutBytes > MAX_CAPTURE_BYTES || stderrBytes > MAX_CAPTURE_BYTES) {
|
|
55
|
+
throw new Error("rvw command output exceeded 40 MiB");
|
|
56
|
+
}
|
|
57
|
+
const stdoutText = Buffer.concat(stdout).toString("utf8");
|
|
58
|
+
const stderrText = Buffer.concat(stderr).toString("utf8");
|
|
59
|
+
let json = null;
|
|
60
|
+
if (stdoutText.trim()) {
|
|
61
|
+
try {
|
|
62
|
+
json = JSON.parse(stdoutText);
|
|
63
|
+
} catch {
|
|
64
|
+
// The caller reports malformed output with the original stdout attached.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
args,
|
|
69
|
+
binary: commandPrefix().binary,
|
|
70
|
+
code: status.code,
|
|
71
|
+
signal: status.signal,
|
|
72
|
+
stdout: stdoutText,
|
|
73
|
+
stderr: stderrText,
|
|
74
|
+
json,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function successfulJson(result) {
|
|
79
|
+
return result.code === 0 && result.json && result.json.ok !== false;
|
|
80
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { StringDecoder } from "node:string_decoder";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { spawnRvw } from "./rvw-command.mjs";
|
|
8
|
+
|
|
9
|
+
const EXIT_WATCH = 20;
|
|
10
|
+
const EXIT_SEQUENCE = 21;
|
|
11
|
+
const EXIT_INGEST = 22;
|
|
12
|
+
const EXIT_AUTO_ACK = 23;
|
|
13
|
+
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const stateScript = path.join(scriptDirectory, "watch-state.mjs");
|
|
15
|
+
const autoAckScript = path.join(scriptDirectory, "auto-ack.mjs");
|
|
16
|
+
|
|
17
|
+
class DriverError extends Error {
|
|
18
|
+
constructor(message, exitCode, details = null) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.exitCode = exitCode;
|
|
21
|
+
this.details = details;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseArguments(values) {
|
|
26
|
+
let state = null;
|
|
27
|
+
let autoAck = false;
|
|
28
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
29
|
+
const value = values[index];
|
|
30
|
+
if (value === "--auto-ack") {
|
|
31
|
+
autoAck = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (value === "--state") {
|
|
35
|
+
const next = values[index + 1];
|
|
36
|
+
if (!next || next.startsWith("--")) {
|
|
37
|
+
throw new DriverError("--state requires a path", EXIT_WATCH);
|
|
38
|
+
}
|
|
39
|
+
state = next;
|
|
40
|
+
index += 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!value.startsWith("--") && state === null) {
|
|
44
|
+
state = value;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
throw new DriverError(`Unexpected argument: ${value}`, EXIT_WATCH);
|
|
48
|
+
}
|
|
49
|
+
if (!state) throw new DriverError("Pass the task state database path", EXIT_WATCH);
|
|
50
|
+
return { state: path.resolve(state), autoAck };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function runNodeScript(script, args, input) {
|
|
54
|
+
const child = spawn(process.execPath, [script, ...args], {
|
|
55
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
56
|
+
});
|
|
57
|
+
const stdout = [];
|
|
58
|
+
const stderr = [];
|
|
59
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
60
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
61
|
+
child.stdin.end(input === undefined ? undefined : JSON.stringify(input));
|
|
62
|
+
const status = await new Promise((resolve, reject) => {
|
|
63
|
+
child.once("error", reject);
|
|
64
|
+
child.once("close", (code, signal) => resolve({ code, signal }));
|
|
65
|
+
});
|
|
66
|
+
const stdoutText = Buffer.concat(stdout).toString("utf8");
|
|
67
|
+
const stderrText = Buffer.concat(stderr).toString("utf8");
|
|
68
|
+
let json = null;
|
|
69
|
+
try {
|
|
70
|
+
json = JSON.parse(stdoutText);
|
|
71
|
+
} catch {
|
|
72
|
+
// The caller selects the appropriate driver exit contract.
|
|
73
|
+
}
|
|
74
|
+
return { ...status, stdout: stdoutText, stderr: stderrText, json };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function stateCommand(state, command, args = [], input) {
|
|
78
|
+
const result = await runNodeScript(stateScript, [command, "--state", state, ...args], input);
|
|
79
|
+
if (result.code !== 0 || !result.json?.ok) {
|
|
80
|
+
throw new DriverError(`watch-state ${command} failed`, EXIT_INGEST, result);
|
|
81
|
+
}
|
|
82
|
+
return result.json;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function write(value) {
|
|
86
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function dispatchAutoAck(state, pullRequest) {
|
|
90
|
+
const pending = await stateCommand(state, "list");
|
|
91
|
+
if (!pending.pending.some((batch) => batch.pullRequest === pullRequest)) {
|
|
92
|
+
write({ ok: true, type: "queued", pullRequest, reason: "batch-not-yet-eligible" });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const result = await runNodeScript(autoAckScript, [
|
|
96
|
+
"--state",
|
|
97
|
+
state,
|
|
98
|
+
"--pull-request",
|
|
99
|
+
pullRequest,
|
|
100
|
+
]);
|
|
101
|
+
if (result.code !== 0 || !result.json?.ok) {
|
|
102
|
+
throw new DriverError(`auto-ack failed for ${pullRequest}`, EXIT_AUTO_ACK, result);
|
|
103
|
+
}
|
|
104
|
+
write({ ...result.json, type: "batch-acknowledged" });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function dispatchPendingAutoAcks(state) {
|
|
108
|
+
const pending = await stateCommand(state, "list");
|
|
109
|
+
for (const batch of pending.pending) {
|
|
110
|
+
await dispatchAutoAck(state, batch.pullRequest);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function processFrame(state, frame, autoAck, pullRequests) {
|
|
115
|
+
if (!frame || typeof frame !== "object" || typeof frame.type !== "string") {
|
|
116
|
+
throw new DriverError("Invalid RFC 7464 frame", EXIT_SEQUENCE, frame);
|
|
117
|
+
}
|
|
118
|
+
if (frame.type === "error") {
|
|
119
|
+
throw new DriverError("rvw comment watch returned an error frame", EXIT_WATCH, frame);
|
|
120
|
+
}
|
|
121
|
+
const ingested = await stateCommand(state, "ingest", [], frame);
|
|
122
|
+
if (frame.type === "ready") {
|
|
123
|
+
write({
|
|
124
|
+
ok: true,
|
|
125
|
+
type: "watch-ready",
|
|
126
|
+
cursor: ingested.cursor,
|
|
127
|
+
databaseId: frame.databaseId,
|
|
128
|
+
anchoredAtCurrent: frame.anchoredAtCurrent,
|
|
129
|
+
autoAck,
|
|
130
|
+
});
|
|
131
|
+
} else if (frame.type === "comment-posted" && ingested.status === "queued") {
|
|
132
|
+
if (autoAck) pullRequests.add(frame.event.pullRequestUrl);
|
|
133
|
+
else {
|
|
134
|
+
write({
|
|
135
|
+
ok: true,
|
|
136
|
+
type: "pending",
|
|
137
|
+
pullRequest: frame.event.pullRequestUrl,
|
|
138
|
+
commentRef: frame.event.commentRef,
|
|
139
|
+
cursor: ingested.cursor,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function runWatchOnce(state, autoAck, stopping) {
|
|
146
|
+
const current = await stateCommand(state, "status");
|
|
147
|
+
const args = ["comment", "watch"];
|
|
148
|
+
if (current.cursor) args.push("--after", current.cursor);
|
|
149
|
+
args.push("--interval", "1", "--json-seq");
|
|
150
|
+
const child = spawnRvw(args);
|
|
151
|
+
stopping.child = child;
|
|
152
|
+
let stderr = "";
|
|
153
|
+
child.stderr.setEncoding("utf8");
|
|
154
|
+
child.stderr.on("data", (chunk) => {
|
|
155
|
+
stderr += chunk;
|
|
156
|
+
process.stderr.write(chunk);
|
|
157
|
+
});
|
|
158
|
+
const statusPromise = new Promise((resolve, reject) => {
|
|
159
|
+
child.once("error", reject);
|
|
160
|
+
child.once("close", (code, signal) => resolve({ code, signal }));
|
|
161
|
+
});
|
|
162
|
+
child.stdin.end();
|
|
163
|
+
const decoder = new StringDecoder("utf8");
|
|
164
|
+
let buffered = "";
|
|
165
|
+
let readySeen = false;
|
|
166
|
+
let stoppedSeen = false;
|
|
167
|
+
try {
|
|
168
|
+
for await (const chunk of child.stdout) {
|
|
169
|
+
buffered += decoder.write(chunk);
|
|
170
|
+
const lines = buffered.split("\n");
|
|
171
|
+
buffered = lines.pop() ?? "";
|
|
172
|
+
const pullRequests = new Set();
|
|
173
|
+
for (const rawLine of lines) {
|
|
174
|
+
if (!rawLine) continue;
|
|
175
|
+
if (!rawLine.startsWith("\u001e")) {
|
|
176
|
+
throw new DriverError("RFC 7464 frame does not start with RS", EXIT_SEQUENCE, rawLine);
|
|
177
|
+
}
|
|
178
|
+
let frame;
|
|
179
|
+
try {
|
|
180
|
+
frame = JSON.parse(rawLine.slice(1));
|
|
181
|
+
} catch (error) {
|
|
182
|
+
throw new DriverError("RFC 7464 frame is not valid JSON", EXIT_SEQUENCE, String(error));
|
|
183
|
+
}
|
|
184
|
+
readySeen ||= frame.type === "ready";
|
|
185
|
+
stoppedSeen ||= frame.type === "stopped";
|
|
186
|
+
await processFrame(state, frame, autoAck, pullRequests);
|
|
187
|
+
}
|
|
188
|
+
for (const pullRequest of pullRequests) await dispatchAutoAck(state, pullRequest);
|
|
189
|
+
}
|
|
190
|
+
buffered += decoder.end();
|
|
191
|
+
if (buffered.trim()) {
|
|
192
|
+
throw new DriverError(
|
|
193
|
+
"rvw comment watch ended with a truncated frame",
|
|
194
|
+
EXIT_SEQUENCE,
|
|
195
|
+
buffered,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
const status = await statusPromise;
|
|
199
|
+
stopping.child = null;
|
|
200
|
+
return { ...status, stderr, readySeen, stoppedSeen };
|
|
201
|
+
} catch (error) {
|
|
202
|
+
child.kill("SIGTERM");
|
|
203
|
+
await statusPromise.catch(() => undefined);
|
|
204
|
+
stopping.child = null;
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function retryDelay(attempt) {
|
|
210
|
+
const base = Number(process.env.RVW_WATCH_DRIVER_RETRY_MS ?? "1000");
|
|
211
|
+
const safeBase = Number.isFinite(base) && base >= 0 ? base : 1000;
|
|
212
|
+
return Math.min(safeBase * 2 ** Math.max(0, attempt - 1), 30_000);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function delay(milliseconds) {
|
|
216
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function main() {
|
|
220
|
+
const { state, autoAck } = parseArguments(process.argv.slice(2));
|
|
221
|
+
const stopping = { requested: false, child: null };
|
|
222
|
+
const stop = () => {
|
|
223
|
+
stopping.requested = true;
|
|
224
|
+
stopping.child?.kill("SIGTERM");
|
|
225
|
+
};
|
|
226
|
+
process.once("SIGINT", stop);
|
|
227
|
+
process.once("SIGTERM", stop);
|
|
228
|
+
const maxRestarts = Number(process.env.RVW_WATCH_DRIVER_MAX_RESTARTS ?? "5");
|
|
229
|
+
let restarts = 0;
|
|
230
|
+
try {
|
|
231
|
+
while (!stopping.requested) {
|
|
232
|
+
const startedAt = Date.now();
|
|
233
|
+
if (autoAck) await dispatchPendingAutoAcks(state);
|
|
234
|
+
const result = await runWatchOnce(state, autoAck, stopping);
|
|
235
|
+
if (stopping.requested) return;
|
|
236
|
+
if (Date.now() - startedAt >= 30_000) restarts = 0;
|
|
237
|
+
restarts += 1;
|
|
238
|
+
if (restarts > maxRestarts) {
|
|
239
|
+
throw new DriverError("rvw comment watch exceeded its reconnect limit", EXIT_WATCH, result);
|
|
240
|
+
}
|
|
241
|
+
const delayMs = retryDelay(restarts);
|
|
242
|
+
write({
|
|
243
|
+
ok: true,
|
|
244
|
+
type: "reconnecting",
|
|
245
|
+
attempt: restarts,
|
|
246
|
+
delayMs,
|
|
247
|
+
lastExitCode: result.code,
|
|
248
|
+
lastSignal: result.signal,
|
|
249
|
+
readySeen: result.readySeen,
|
|
250
|
+
});
|
|
251
|
+
await delay(delayMs);
|
|
252
|
+
}
|
|
253
|
+
} finally {
|
|
254
|
+
process.off("SIGINT", stop);
|
|
255
|
+
process.off("SIGTERM", stop);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
await main();
|
|
261
|
+
} catch (error) {
|
|
262
|
+
const exitCode = error instanceof DriverError ? error.exitCode : EXIT_WATCH;
|
|
263
|
+
process.stderr.write(
|
|
264
|
+
`${JSON.stringify({
|
|
265
|
+
ok: false,
|
|
266
|
+
error: error instanceof Error ? error.message : String(error),
|
|
267
|
+
details: error?.details ?? null,
|
|
268
|
+
exitCode,
|
|
269
|
+
})}\n`,
|
|
270
|
+
);
|
|
271
|
+
process.exitCode = exitCode;
|
|
272
|
+
}
|