@trim21/personal-pi-extensions 0.1.517 → 0.1.519
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 +18 -75
- package/src/lib/lsp/client.ts +46 -5
package/package.json
CHANGED
package/src/gh-readonly.ts
CHANGED
|
@@ -1134,86 +1134,30 @@ const PR_CHECKS_JSON_FIELDS = "name,state,bucket,startedAt,completedAt,link,work
|
|
|
1134
1134
|
const CHECKS_POLL_INTERVAL_MS = 30_000;
|
|
1135
1135
|
const CHECKS_WATCH_DEADLINE_MS = 600_000;
|
|
1136
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
1137
|
/**
|
|
1179
|
-
* Render one polling round of `gh pr checks`
|
|
1180
|
-
*
|
|
1181
|
-
*
|
|
1138
|
+
* Render one polling round of `gh pr checks` as a compact bullet list of the
|
|
1139
|
+
* checks still in flight: running ones first (`- [>]`), queued ones after
|
|
1140
|
+
* (`- [ ]`). Completed checks are hidden — the header already reports the
|
|
1141
|
+
* completion count. Pure — no network.
|
|
1182
1142
|
*/
|
|
1183
|
-
export function
|
|
1143
|
+
export function renderPrChecksList(options: {
|
|
1184
1144
|
prNumber: number | string;
|
|
1185
1145
|
round: number;
|
|
1186
1146
|
checks: readonly PrCheck[];
|
|
1187
|
-
now: number;
|
|
1188
1147
|
}): string {
|
|
1189
|
-
const { prNumber, round, checks
|
|
1148
|
+
const { prNumber, round, checks } = options;
|
|
1190
1149
|
const completed = checks.filter((c) => c.bucket !== "pending").length;
|
|
1191
1150
|
|
|
1192
|
-
const
|
|
1151
|
+
const pending = checks.filter((c) => c.bucket === "pending");
|
|
1152
|
+
const ordered = [...pending.filter((c) => c.startedAt), ...pending.filter((c) => !c.startedAt)];
|
|
1153
|
+
const lines = ordered.map((check) => {
|
|
1193
1154
|
const name = check.link ? `[${check.name}](${check.link})` : check.name;
|
|
1194
|
-
|
|
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(" | ")} |`;
|
|
1155
|
+
return `- [${check.startedAt ? ">" : " "}] ${name}`;
|
|
1208
1156
|
});
|
|
1209
|
-
const body =
|
|
1157
|
+
const body =
|
|
1158
|
+
checks.length === 0 ? "- _no checks reported_" : lines.length > 0 ? lines.join("\n") : "";
|
|
1210
1159
|
|
|
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
|
-
);
|
|
1160
|
+
return `### PR #${prNumber} checks — round ${round}: ${completed}/${checks.length} complete${body ? `\n\n${body}` : ""}`;
|
|
1217
1161
|
}
|
|
1218
1162
|
|
|
1219
1163
|
function sleepInterruptibly(ms: number, signal: AbortSignal | undefined): Promise<void> {
|
|
@@ -1248,8 +1192,9 @@ export interface PollPrChecksOptions {
|
|
|
1248
1192
|
|
|
1249
1193
|
/**
|
|
1250
1194
|
* Poll `gh pr checks --json` until no check is pending (or a fail-fast
|
|
1251
|
-
* failure, or the deadline), emitting a
|
|
1252
|
-
* each round. In JSON mode gh exits 0 whenever it could fetch the
|
|
1195
|
+
* failure, or the deadline), emitting a compact list of in-flight checks via
|
|
1196
|
+
* `onUpdate` each round. In JSON mode gh exits 0 whenever it could fetch the
|
|
1197
|
+
* checks —
|
|
1253
1198
|
* completion is judged from the `bucket` field, not the exit code. A non-zero
|
|
1254
1199
|
* exit (no checks reported, auth, network) is not fatal here: the caller
|
|
1255
1200
|
* proceeds to the Actions API verification, which either produces the final
|
|
@@ -1282,9 +1227,7 @@ export async function pollPrChecks(options: PollPrChecksOptions): Promise<void>
|
|
|
1282
1227
|
const checks: PrCheck[] = Value.Parse(Type.Array(prCheckSchema), JSON.parse(result.stdout));
|
|
1283
1228
|
|
|
1284
1229
|
onUpdate?.({
|
|
1285
|
-
content: [
|
|
1286
|
-
{ type: "text", text: renderPrChecksTable({ prNumber, round, checks, now: Date.now() }) },
|
|
1287
|
-
],
|
|
1230
|
+
content: [{ type: "text", text: renderPrChecksList({ prNumber, round, checks }) }],
|
|
1288
1231
|
details: {},
|
|
1289
1232
|
});
|
|
1290
1233
|
|
|
@@ -1852,7 +1795,7 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1852
1795
|
label: "Watch GitHub PR Checks",
|
|
1853
1796
|
description:
|
|
1854
1797
|
"Watch CI status checks for a PR until they complete. Blocks until all checks finish or one fails. " +
|
|
1855
|
-
"Each polling round streams a
|
|
1798
|
+
"Each polling round streams a compact bullet list of the checks still in flight via onUpdate. " +
|
|
1856
1799
|
"Use this when you need to wait for CI to complete and see the final result.",
|
|
1857
1800
|
promptSnippet: "Watch and wait for GitHub PR CI checks to complete",
|
|
1858
1801
|
parameters: Type.Object({
|
package/src/lib/lsp/client.ts
CHANGED
|
@@ -213,10 +213,39 @@ function toInspectLocations(result: DefinitionResult): InspectLocation[] {
|
|
|
213
213
|
return locations;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
/** stderr 尾部保留上限(字符):足够容纳启动失败的最后报错,又不撑爆通知。 */
|
|
217
|
+
const STDERR_TAIL_CHARS = 4_000;
|
|
218
|
+
|
|
219
|
+
/** 展开 Error 的 cause 链为一条 message;流包装错误只包一层,逐层展开即可还原根因。 */
|
|
220
|
+
function errorChainMessage(error: unknown): string {
|
|
221
|
+
if (!(error instanceof Error)) return String(error);
|
|
222
|
+
const parts: string[] = [];
|
|
223
|
+
let current: unknown = error;
|
|
224
|
+
while (current instanceof Error && current.message && !parts.includes(current.message)) {
|
|
225
|
+
parts.push(current.message);
|
|
226
|
+
current = current.cause;
|
|
227
|
+
}
|
|
228
|
+
if (typeof current === "string" || typeof current === "number") parts.push(String(current));
|
|
229
|
+
return parts.join(": ");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** 拼装握手失败的完整原因:错误链 + 进程退出状态 + stderr 尾部。 */
|
|
233
|
+
function describeStartupFailure(
|
|
234
|
+
error: unknown,
|
|
235
|
+
exitDescription: string | undefined,
|
|
236
|
+
stderrTail: string,
|
|
237
|
+
): string {
|
|
238
|
+
const parts = [errorChainMessage(error)];
|
|
239
|
+
if (exitDescription) parts.push(`server ${exitDescription}`);
|
|
240
|
+
const stderr = stderrTail.trim();
|
|
241
|
+
if (stderr) parts.push(`stderr: ${stderr}`);
|
|
242
|
+
return parts.join("; ");
|
|
243
|
+
}
|
|
244
|
+
|
|
216
245
|
export class InitializeError extends Error {
|
|
217
246
|
readonly serverID: string;
|
|
218
|
-
constructor(serverID: string, cause: unknown) {
|
|
219
|
-
super(`Failed to initialize LSP server ${serverID}`, { cause });
|
|
247
|
+
constructor(serverID: string, cause: unknown, detail?: string) {
|
|
248
|
+
super(`Failed to initialize LSP server ${serverID}${detail ? `: ${detail}` : ""}`, { cause });
|
|
220
249
|
this.serverID = serverID;
|
|
221
250
|
}
|
|
222
251
|
}
|
|
@@ -443,11 +472,19 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
443
472
|
new StreamMessageReader(input.server.process.stdout),
|
|
444
473
|
new StreamMessageWriter(input.server.process.stdin),
|
|
445
474
|
);
|
|
446
|
-
|
|
475
|
+
// stderr 平时只在尾部保留少量内容(避免子进程大量输出撑爆内存);
|
|
476
|
+
// 握手失败时随错误输出,服务器 panic / 参数错误等启动原因由此还原。
|
|
477
|
+
let stderrTail = "";
|
|
478
|
+
input.server.process.stderr.setEncoding("utf8");
|
|
479
|
+
input.server.process.stderr.on("data", (chunk: string) => {
|
|
480
|
+
stderrTail = (stderrTail + chunk).slice(-STDERR_TAIL_CHARS);
|
|
481
|
+
});
|
|
447
482
|
/** 连接或服务器进程已关闭;pull 重试循环以此终止,避免无界等待。 */
|
|
448
483
|
let connectionClosed = false;
|
|
449
|
-
|
|
484
|
+
let exitDescription: string | undefined;
|
|
485
|
+
input.server.process.once("exit", (code, signal) => {
|
|
450
486
|
connectionClosed = true;
|
|
487
|
+
exitDescription = code === null ? `killed by signal ${signal}` : `exited with code ${code}`;
|
|
451
488
|
});
|
|
452
489
|
connection.onDispose(() => {
|
|
453
490
|
connectionClosed = true;
|
|
@@ -582,7 +619,11 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
582
619
|
connection.end();
|
|
583
620
|
connection.dispose();
|
|
584
621
|
await stopProcess(input.server.process);
|
|
585
|
-
throw new InitializeError(
|
|
622
|
+
throw new InitializeError(
|
|
623
|
+
input.serverID,
|
|
624
|
+
error,
|
|
625
|
+
describeStartupFailure(error, exitDescription, stderrTail),
|
|
626
|
+
);
|
|
586
627
|
});
|
|
587
628
|
|
|
588
629
|
const syncKind = getSyncKind(initialized.capabilities);
|