@trim21/personal-pi-extensions 0.1.514 → 0.1.516
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/package.json +1 -1
- package/src/gh-readonly.ts +197 -15
package/package.json
CHANGED
package/src/gh-readonly.ts
CHANGED
|
@@ -35,7 +35,7 @@ import { homedir } from "node:os";
|
|
|
35
35
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
36
36
|
|
|
37
37
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
38
|
-
import { Type } from "typebox";
|
|
38
|
+
import { type Static, Type } from "typebox";
|
|
39
39
|
import { Value } from "typebox/value";
|
|
40
40
|
|
|
41
41
|
import { createGithubSearch, type GithubSearch, renderHits } from "./lib/github.js";
|
|
@@ -266,6 +266,23 @@ const workflowRunSchema = Type.Object({
|
|
|
266
266
|
|
|
267
267
|
const workflowRunsSchema = Type.Object({ workflow_runs: Type.Array(workflowRunSchema) });
|
|
268
268
|
|
|
269
|
+
/**
|
|
270
|
+
* One entry of `gh pr checks --json`. In JSON mode gh exits 0 once it could
|
|
271
|
+
* fetch the checks (real errors still exit non-zero); pass/fail/pending is
|
|
272
|
+
* only conveyed by the `bucket` field, never by the exit code.
|
|
273
|
+
*/
|
|
274
|
+
const prCheckSchema = Type.Object({
|
|
275
|
+
name: Type.String(),
|
|
276
|
+
state: Type.String(),
|
|
277
|
+
bucket: Type.String(),
|
|
278
|
+
startedAt: Type.Union([Type.String(), Type.Null()]),
|
|
279
|
+
completedAt: Type.Union([Type.String(), Type.Null()]),
|
|
280
|
+
link: Type.Union([Type.String(), Type.Null()]),
|
|
281
|
+
workflow: Type.Union([Type.String(), Type.Null()]),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
type PrCheck = Static<typeof prCheckSchema>;
|
|
285
|
+
|
|
269
286
|
function truncate(
|
|
270
287
|
text: string,
|
|
271
288
|
maxLines = 2000,
|
|
@@ -1111,6 +1128,174 @@ export async function writeLogFile(
|
|
|
1111
1128
|
};
|
|
1112
1129
|
}
|
|
1113
1130
|
|
|
1131
|
+
// ── pr checks watch (pure rendering + poll loop) ────────────────────────────
|
|
1132
|
+
|
|
1133
|
+
const PR_CHECKS_JSON_FIELDS = "name,state,bucket,startedAt,completedAt,link,workflow";
|
|
1134
|
+
const CHECKS_POLL_INTERVAL_MS = 30_000;
|
|
1135
|
+
const CHECKS_WATCH_DEADLINE_MS = 600_000;
|
|
1136
|
+
|
|
1137
|
+
function bucketIcon(bucket: string): string {
|
|
1138
|
+
switch (bucket) {
|
|
1139
|
+
case "pass": {
|
|
1140
|
+
return "✅";
|
|
1141
|
+
}
|
|
1142
|
+
case "fail": {
|
|
1143
|
+
return "❌";
|
|
1144
|
+
}
|
|
1145
|
+
case "skipping": {
|
|
1146
|
+
return "⏭️";
|
|
1147
|
+
}
|
|
1148
|
+
case "cancel": {
|
|
1149
|
+
return "🚫";
|
|
1150
|
+
}
|
|
1151
|
+
default: {
|
|
1152
|
+
return "🔄";
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function formatClock(ms: number): string {
|
|
1158
|
+
const date = new Date(ms);
|
|
1159
|
+
return [date.getHours(), date.getMinutes(), date.getSeconds()]
|
|
1160
|
+
.map((n) => String(n).padStart(2, "0"))
|
|
1161
|
+
.join(":");
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function pad2(n: number): string {
|
|
1165
|
+
return String(n).padStart(2, "0");
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function formatDuration(totalSeconds: number): string {
|
|
1169
|
+
const seconds = Math.floor(totalSeconds);
|
|
1170
|
+
const h = Math.floor(seconds / 3600);
|
|
1171
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
1172
|
+
const s = seconds % 60;
|
|
1173
|
+
if (h > 0) return `${h}h${pad2(m)}m${pad2(s)}s`;
|
|
1174
|
+
if (m > 0) return `${m}m${pad2(s)}s`;
|
|
1175
|
+
return `${s}s`;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Render one polling round of `gh pr checks` like the GitHub web UI: check
|
|
1180
|
+
* name, workflow, status, start time and elapsed (total duration when the
|
|
1181
|
+
* check completed, time-so-far while still pending). Pure — no network.
|
|
1182
|
+
*/
|
|
1183
|
+
export function renderPrChecksTable(options: {
|
|
1184
|
+
prNumber: number | string;
|
|
1185
|
+
round: number;
|
|
1186
|
+
checks: readonly PrCheck[];
|
|
1187
|
+
now: number;
|
|
1188
|
+
}): string {
|
|
1189
|
+
const { prNumber, round, checks, now } = options;
|
|
1190
|
+
const completed = checks.filter((c) => c.bucket !== "pending").length;
|
|
1191
|
+
|
|
1192
|
+
const rows = checks.map((check) => {
|
|
1193
|
+
const name = check.link ? `[${check.name}](${check.link})` : check.name;
|
|
1194
|
+
let elapsed = "—";
|
|
1195
|
+
if (check.startedAt) {
|
|
1196
|
+
const startMs = Date.parse(check.startedAt);
|
|
1197
|
+
const endMs = check.completedAt ? Date.parse(check.completedAt) : now;
|
|
1198
|
+
elapsed = formatDuration(Math.max(0, (endMs - startMs) / 1000));
|
|
1199
|
+
}
|
|
1200
|
+
const cells = [
|
|
1201
|
+
`${bucketIcon(check.bucket)} ${name}`,
|
|
1202
|
+
check.workflow ?? "—",
|
|
1203
|
+
check.bucket,
|
|
1204
|
+
check.startedAt ? formatClock(Date.parse(check.startedAt)) : "—",
|
|
1205
|
+
elapsed,
|
|
1206
|
+
];
|
|
1207
|
+
return `| ${cells.join(" | ")} |`;
|
|
1208
|
+
});
|
|
1209
|
+
const body = rows.length > 0 ? rows.join("\n") : "| _no checks reported_ | — | — | — | — |";
|
|
1210
|
+
|
|
1211
|
+
return (
|
|
1212
|
+
`### PR #${prNumber} checks — round ${round}: ${completed}/${checks.length} complete\n\n` +
|
|
1213
|
+
`| Check | Workflow | Status | Started | Elapsed |\n` +
|
|
1214
|
+
`|---|---|---|---|---|\n` +
|
|
1215
|
+
body
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function sleepInterruptibly(ms: number, signal: AbortSignal | undefined): Promise<void> {
|
|
1220
|
+
return new Promise((resolve, reject) => {
|
|
1221
|
+
const onAbort = () => {
|
|
1222
|
+
clearTimeout(timer);
|
|
1223
|
+
reject(new Error("aborted while waiting for the next checks poll"));
|
|
1224
|
+
};
|
|
1225
|
+
const timer = setTimeout(() => {
|
|
1226
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1227
|
+
resolve();
|
|
1228
|
+
}, ms);
|
|
1229
|
+
if (signal?.aborted) {
|
|
1230
|
+
onAbort();
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
export interface PollPrChecksOptions {
|
|
1238
|
+
prNumber: number | string;
|
|
1239
|
+
repo?: string;
|
|
1240
|
+
failFast: boolean;
|
|
1241
|
+
cwd?: string;
|
|
1242
|
+
signal?: AbortSignal;
|
|
1243
|
+
/** Test overrides. */
|
|
1244
|
+
intervalMs?: number;
|
|
1245
|
+
deadlineMs?: number;
|
|
1246
|
+
onUpdate?: (msg: CiLogsResult) => void;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* Poll `gh pr checks --json` until no check is pending (or a fail-fast
|
|
1251
|
+
* failure, or the deadline), emitting a GitHub-UI-like table via `onUpdate`
|
|
1252
|
+
* each round. In JSON mode gh exits 0 whenever it could fetch the checks —
|
|
1253
|
+
* completion is judged from the `bucket` field, not the exit code. A non-zero
|
|
1254
|
+
* exit (no checks reported, auth, network) is not fatal here: the caller
|
|
1255
|
+
* proceeds to the Actions API verification, which either produces the final
|
|
1256
|
+
* report or surfaces the error.
|
|
1257
|
+
*/
|
|
1258
|
+
export async function pollPrChecks(options: PollPrChecksOptions): Promise<void> {
|
|
1259
|
+
const { prNumber, repo, failFast, cwd, signal, onUpdate } = options;
|
|
1260
|
+
const intervalMs = options.intervalMs ?? CHECKS_POLL_INTERVAL_MS;
|
|
1261
|
+
const deadlineMs = options.deadlineMs ?? CHECKS_WATCH_DEADLINE_MS;
|
|
1262
|
+
const args = [
|
|
1263
|
+
"pr",
|
|
1264
|
+
"checks",
|
|
1265
|
+
String(prNumber),
|
|
1266
|
+
...repoArgs(repo),
|
|
1267
|
+
"--json",
|
|
1268
|
+
PR_CHECKS_JSON_FIELDS,
|
|
1269
|
+
];
|
|
1270
|
+
|
|
1271
|
+
const watchStart = Date.now();
|
|
1272
|
+
for (let round = 1; ; round++) {
|
|
1273
|
+
const result = await runGh(args, { cwd, signal });
|
|
1274
|
+
if (result.killed) {
|
|
1275
|
+
throw new Error(
|
|
1276
|
+
result.reason === "timeout" ? "gh pr checks poll timed out" : "gh pr checks was aborted",
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
if (result.code !== 0) {
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
const checks: PrCheck[] = Value.Parse(Type.Array(prCheckSchema), JSON.parse(result.stdout));
|
|
1283
|
+
|
|
1284
|
+
onUpdate?.({
|
|
1285
|
+
content: [
|
|
1286
|
+
{ type: "text", text: renderPrChecksTable({ prNumber, round, checks, now: Date.now() }) },
|
|
1287
|
+
],
|
|
1288
|
+
details: {},
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
const hasPending = checks.some((c) => c.bucket === "pending");
|
|
1292
|
+
if (!hasPending) return;
|
|
1293
|
+
if (failFast && checks.some((c) => c.bucket === "fail")) return;
|
|
1294
|
+
if (Date.now() - watchStart >= deadlineMs) return;
|
|
1295
|
+
await sleepInterruptibly(intervalMs, signal);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1114
1299
|
// ── tools ────────────────────────────────────────────────────────────────────
|
|
1115
1300
|
|
|
1116
1301
|
export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
@@ -1667,6 +1852,7 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1667
1852
|
label: "Watch GitHub PR Checks",
|
|
1668
1853
|
description:
|
|
1669
1854
|
"Watch CI status checks for a PR until they complete. Blocks until all checks finish or one fails. " +
|
|
1855
|
+
"Each polling round streams a GitHub-UI-like check table (name, workflow, status, started, elapsed) via onUpdate. " +
|
|
1670
1856
|
"Use this when you need to wait for CI to complete and see the final result.",
|
|
1671
1857
|
promptSnippet: "Watch and wait for GitHub PR CI checks to complete",
|
|
1672
1858
|
parameters: Type.Object({
|
|
@@ -1685,21 +1871,17 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1685
1871
|
details: {},
|
|
1686
1872
|
});
|
|
1687
1873
|
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
// gh 的退出码语义不可靠:checks 失败时返回 exit 1(SilentError),挂起时
|
|
1692
|
-
// 返回 exit 8(PendingError),且失败详情只在非结构化的 stdout 表格里。
|
|
1693
|
-
// 因此 watch 退出后直接用 Actions API 抓取该 PR head 提交关联的所有
|
|
1874
|
+
// 轮询 `gh pr checks --json`(每轮经 onUpdate 流式输出 GitHub Web UI 风格
|
|
1875
|
+
// 的检查表),结束后再用 Actions API 抓取该 PR head 提交关联的所有
|
|
1694
1876
|
// workflow job,以 job 的真实 conclusion 为准判断成功/失败。
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
}
|
|
1877
|
+
await pollPrChecks({
|
|
1878
|
+
prNumber: number,
|
|
1879
|
+
repo,
|
|
1880
|
+
failFast: fail_fast === true,
|
|
1881
|
+
cwd: ctx.cwd,
|
|
1882
|
+
signal,
|
|
1883
|
+
onUpdate,
|
|
1884
|
+
});
|
|
1703
1885
|
|
|
1704
1886
|
const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
|
|
1705
1887
|
const prOut = await ghExec(
|