@trim21/personal-pi-extensions 0.1.550 → 0.1.552
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 +283 -201
- package/src/lib/gh-proxy.ts +64 -70
package/package.json
CHANGED
package/src/gh-readonly.ts
CHANGED
|
@@ -95,7 +95,7 @@ export function isGhAvailable(): boolean {
|
|
|
95
95
|
return false;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
export
|
|
98
|
+
export function runGh(
|
|
99
99
|
args: string[],
|
|
100
100
|
ctx: {
|
|
101
101
|
cwd?: string;
|
|
@@ -105,15 +105,13 @@ export async function runGh(
|
|
|
105
105
|
env?: NodeJS.ProcessEnv;
|
|
106
106
|
},
|
|
107
107
|
): Promise<GhResult> {
|
|
108
|
-
const proxyEnv = await ghProxy.env();
|
|
109
|
-
|
|
110
108
|
return new Promise((resolve) => {
|
|
111
109
|
const proc = spawn("gh", args, {
|
|
112
110
|
cwd: ctx.cwd,
|
|
113
111
|
shell: false,
|
|
114
112
|
stdio: ["ignore", "pipe", "pipe"],
|
|
115
113
|
// gh 是 Go 程序,只认环境变量形式的代理配置;ctx.env 最后合并,调用方可覆盖。
|
|
116
|
-
env: { ...process.env, ...
|
|
114
|
+
env: { ...process.env, ...ghProxy.env, ...ctx.env, GH_PAGER: "cat" },
|
|
117
115
|
});
|
|
118
116
|
|
|
119
117
|
let stdout = "";
|
|
@@ -349,16 +347,14 @@ function toToolResult(
|
|
|
349
347
|
* tool's id parameter, e.g. `repo=x/y number=123`. Returns undefined when
|
|
350
348
|
* neither is available, so the pendant is omitted rather than shown empty.
|
|
351
349
|
*/
|
|
352
|
-
function subtitlePendant(
|
|
353
|
-
params: { repo?: string } & Record<
|
|
354
|
-
idKey?:
|
|
350
|
+
function subtitlePendant<IdKey extends string = never>(
|
|
351
|
+
params: { repo?: string } & Partial<Record<IdKey, string | number>>,
|
|
352
|
+
idKey?: IdKey,
|
|
355
353
|
): ToolPendant | undefined {
|
|
356
354
|
const parts: string[] = [];
|
|
357
355
|
if (params.repo) parts.push(`repo=${params.repo}`);
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (typeof id === "string" || typeof id === "number") parts.push(`${idKey}=${id}`);
|
|
361
|
-
}
|
|
356
|
+
const id = idKey === undefined ? undefined : params[idKey];
|
|
357
|
+
if (typeof id === "string" || typeof id === "number") parts.push(`${idKey}=${id}`);
|
|
362
358
|
if (parts.length === 0) return undefined;
|
|
363
359
|
return { subtitle: parts.join(" ") };
|
|
364
360
|
}
|
|
@@ -756,7 +752,7 @@ export function extractStepFromLog(
|
|
|
756
752
|
|
|
757
753
|
// ── ci-logs rendering (pure, testable) ──────────────────────────────────────
|
|
758
754
|
|
|
759
|
-
export interface
|
|
755
|
+
export interface ToolResult {
|
|
760
756
|
content: { type: "text"; text: string }[];
|
|
761
757
|
details: Record<string, unknown>;
|
|
762
758
|
}
|
|
@@ -978,7 +974,7 @@ export interface PollPrChecksOptions {
|
|
|
978
974
|
/** Test overrides. */
|
|
979
975
|
intervalMs?: number;
|
|
980
976
|
deadlineMs?: number;
|
|
981
|
-
onUpdate?: (msg:
|
|
977
|
+
onUpdate?: (msg: ToolResult) => void;
|
|
982
978
|
}
|
|
983
979
|
|
|
984
980
|
/**
|
|
@@ -1136,45 +1132,243 @@ export function renderChecksVerdict(options: {
|
|
|
1136
1132
|
};
|
|
1137
1133
|
}
|
|
1138
1134
|
|
|
1139
|
-
// ──
|
|
1135
|
+
// ── GitHub REST client ───────────────────────────────────────────────────────
|
|
1140
1136
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1137
|
+
/** Toolcall input for the handlers that read through the REST API. */
|
|
1138
|
+
interface PrStatusParams {
|
|
1139
|
+
number: number | string;
|
|
1140
|
+
repo?: string;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
interface RunIdParams {
|
|
1144
|
+
run_id: number | string;
|
|
1145
|
+
repo?: string;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
interface JobIdParams {
|
|
1149
|
+
job_id: number | string;
|
|
1150
|
+
repo?: string;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
interface PrChecksWaitParams {
|
|
1154
|
+
number: number | string;
|
|
1155
|
+
repo?: string;
|
|
1156
|
+
fail_fast?: boolean;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
interface CommitChecksWaitParams {
|
|
1160
|
+
commit: number | string;
|
|
1161
|
+
repo?: string;
|
|
1162
|
+
event?: string;
|
|
1163
|
+
fail_fast?: boolean;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/** What a toolcall handler receives from the framework. */
|
|
1167
|
+
export interface ToolCall<Params> {
|
|
1168
|
+
params: Params;
|
|
1169
|
+
ctx: { cwd?: string };
|
|
1170
|
+
signal?: AbortSignal;
|
|
1171
|
+
/** Streaming progress updates, passed through as-is. */
|
|
1172
|
+
onUpdate?: (update: ToolResult) => void;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/**
|
|
1176
|
+
* The GitHub reads that go through the REST API, with the HTTP layer injected:
|
|
1177
|
+
* production hands in the proxy-aware fetch, tests hand in a stub and never
|
|
1178
|
+
* touch the network. Handlers that only shell out to `gh` stay plain functions.
|
|
1179
|
+
*
|
|
1180
|
+
* `fetch` is a property (not module state) so a caller that needs different HTTP
|
|
1181
|
+
* behavior — a test, another host — constructs its own instance.
|
|
1182
|
+
*/
|
|
1183
|
+
export class GhClient {
|
|
1184
|
+
readonly fetch: typeof globalThis.fetch;
|
|
1185
|
+
private readonly search: GithubSearch;
|
|
1186
|
+
private readonly checks: GithubChecksClient;
|
|
1187
|
+
|
|
1188
|
+
constructor(fetchImpl: typeof globalThis.fetch = ghProxy.fetch) {
|
|
1189
|
+
this.fetch = fetchImpl;
|
|
1190
|
+
this.search = createGithubSearch({ fetch: fetchImpl });
|
|
1191
|
+
this.checks = createGithubChecks({ fetch: fetchImpl });
|
|
1149
1192
|
}
|
|
1150
1193
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
return;
|
|
1194
|
+
/** `list-github-issues` / `list-github-prs`: browse through `gh`, search through the API. */
|
|
1195
|
+
private async list(kind: "issue" | "pr", call: ToolCall<ListFilters>): Promise<ToolResult> {
|
|
1196
|
+
const { params, ctx, signal } = call;
|
|
1197
|
+
const result = toToolResult(
|
|
1198
|
+
params.keywords
|
|
1199
|
+
? await searchList(kind, params, this.search)
|
|
1200
|
+
: await listGithub(kind, params, { cwd: ctx.cwd, signal, input: params }),
|
|
1201
|
+
params,
|
|
1202
|
+
);
|
|
1203
|
+
result.details.pendant = subtitlePendant(params);
|
|
1204
|
+
return result;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
listIssues(call: ToolCall<ListFilters>): Promise<ToolResult> {
|
|
1208
|
+
return this.list("issue", call);
|
|
1162
1209
|
}
|
|
1163
1210
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1211
|
+
listPrs(call: ToolCall<ListFilters>): Promise<ToolResult> {
|
|
1212
|
+
return this.list("pr", call);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* `read-github-pr-status`: the PR head commit's checks as a snapshot. Same read
|
|
1217
|
+
* path as the wait tools (octokit), but it never polls — pending checks come
|
|
1218
|
+
* back as-is.
|
|
1219
|
+
*/
|
|
1220
|
+
async prStatus(call: ToolCall<PrStatusParams>): Promise<ToolResult> {
|
|
1221
|
+
const { params, ctx, signal } = call;
|
|
1222
|
+
const { number, repo } = params;
|
|
1223
|
+
const pullNumber = toPositiveId(number, "number");
|
|
1224
|
+
const pendant = subtitlePendant(params, "number");
|
|
1225
|
+
const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
|
|
1226
|
+
const { owner, repo: name } = splitRepo(effectiveRepo);
|
|
1227
|
+
|
|
1228
|
+
const pollSignal = signal ?? new AbortController().signal;
|
|
1229
|
+
const headSha = await this.checks.pullHead(owner, name, pullNumber, pollSignal);
|
|
1230
|
+
const [statuses, checkRuns] = await Promise.all([
|
|
1231
|
+
this.checks.statuses(owner, name, headSha, pollSignal),
|
|
1232
|
+
this.checks.checkRuns(owner, name, headSha, pollSignal),
|
|
1233
|
+
]);
|
|
1234
|
+
const checks = mergeChecks(statuses, checkRuns).map((check) => ({
|
|
1235
|
+
name: check.name,
|
|
1236
|
+
bucket: check.bucket,
|
|
1237
|
+
event: check.event,
|
|
1238
|
+
run_id: check.runId,
|
|
1239
|
+
job_id: check.jobId,
|
|
1240
|
+
url: check.link,
|
|
1241
|
+
}));
|
|
1242
|
+
|
|
1243
|
+
const payload = { pr: pullNumber, repo: effectiveRepo, head_sha: headSha, checks };
|
|
1244
|
+
return {
|
|
1245
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
1246
|
+
details: { ...payload, input: params, ...(pendant && { pendant }) },
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/** `get-github-workflow-jobs`: every job of a run, all pages. */
|
|
1251
|
+
async workflowJobs(call: ToolCall<RunIdParams>): Promise<ToolResult> {
|
|
1252
|
+
const { params, ctx, signal } = call;
|
|
1253
|
+
const runId = toPositiveId(params.run_id, "run_id");
|
|
1254
|
+
const effectiveRepo = await resolveRepo(params.repo, signal, ctx.cwd, params);
|
|
1255
|
+
const { owner, repo: name } = splitRepo(effectiveRepo);
|
|
1256
|
+
|
|
1257
|
+
const jobs = await this.checks.runJobs(owner, name, runId, signal);
|
|
1258
|
+
const result = toToolResult(JSON.stringify({ total_count: jobs.length, jobs }), params);
|
|
1259
|
+
result.details.pendant = subtitlePendant(params, "run_id");
|
|
1260
|
+
return result;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/** `read-github-ci-logs`: one job's raw log on disk plus its step line ranges. */
|
|
1264
|
+
async ciLogs(call: ToolCall<JobIdParams>): Promise<ToolResult> {
|
|
1265
|
+
const { params, ctx, signal, onUpdate } = call;
|
|
1266
|
+
const { job_id, repo } = params;
|
|
1267
|
+
const jobId = toPositiveId(job_id, "job_id");
|
|
1268
|
+
|
|
1269
|
+
const pendant = subtitlePendant(params, "job_id");
|
|
1270
|
+
const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
|
|
1271
|
+
const { owner, repo: name } = splitRepo(effectiveRepo);
|
|
1272
|
+
|
|
1273
|
+
const failure = (text: string): ToolResult => ({
|
|
1274
|
+
content: [{ type: "text", text }],
|
|
1275
|
+
details: { input: params, ...(pendant && { pendant }) },
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
let target: RunJob;
|
|
1169
1279
|
try {
|
|
1170
|
-
|
|
1171
|
-
} catch {
|
|
1172
|
-
|
|
1280
|
+
target = await this.checks.job(owner, name, jobId, signal);
|
|
1281
|
+
} catch (error) {
|
|
1282
|
+
const status = (error as { status?: number }).status;
|
|
1283
|
+
if (status !== 404) throw error;
|
|
1284
|
+
return failure(
|
|
1285
|
+
`Job ${jobId} not found in ${effectiveRepo} — job IDs come from \`get-github-workflow-jobs\`.`,
|
|
1286
|
+
);
|
|
1173
1287
|
}
|
|
1174
|
-
});
|
|
1175
1288
|
|
|
1176
|
-
|
|
1177
|
-
|
|
1289
|
+
if (target.status === "queued") {
|
|
1290
|
+
return failure(
|
|
1291
|
+
`Job "${target.name}" is still queued — no logs available yet. Use \`watch-github-run\` to wait for it to start, then retry.`,
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
onUpdate?.({
|
|
1296
|
+
content: [{ type: "text", text: `Fetching log of job "${target.name}"...` }],
|
|
1297
|
+
details: {},
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
const rawLog = await getJobLog(target, signal, ctx.cwd, params);
|
|
1301
|
+
const index = jobLogIndex(target, rawLog);
|
|
1302
|
+
|
|
1303
|
+
return {
|
|
1304
|
+
content: [{ type: "text", text: JSON.stringify(index, null, 2) }],
|
|
1305
|
+
details: { ...index, input: params, ...(pendant && { pendant }) },
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
/** `wait-github-pr-checks`: poll the PR's head commit checks until they settle. */
|
|
1310
|
+
async waitPrChecks(call: ToolCall<PrChecksWaitParams>): Promise<ToolResult> {
|
|
1311
|
+
const { params, ctx, signal, onUpdate } = call;
|
|
1312
|
+
const { number, repo, fail_fast } = params;
|
|
1313
|
+
|
|
1314
|
+
const pendant = subtitlePendant(params, "number");
|
|
1315
|
+
onUpdate?.({
|
|
1316
|
+
content: [{ type: "text", text: `Watching CI checks for PR #${number}...` }],
|
|
1317
|
+
details: {},
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
|
|
1321
|
+
const { owner, repo: repoName } = splitRepo(effectiveRepo);
|
|
1322
|
+
|
|
1323
|
+
const prOut = await ghExec(
|
|
1324
|
+
["pr", "view", String(number), "--repo", effectiveRepo, "--json", "headRefOid"],
|
|
1325
|
+
{ cwd: ctx.cwd, signal, input: params },
|
|
1326
|
+
);
|
|
1327
|
+
const { headRefOid } = Value.Parse(prHeadSchema, JSON.parse(prOut));
|
|
1328
|
+
|
|
1329
|
+
return this.waitChecksReport({
|
|
1330
|
+
subject: `PR #${number}`,
|
|
1331
|
+
owner,
|
|
1332
|
+
repo: repoName,
|
|
1333
|
+
headSha: headRefOid,
|
|
1334
|
+
failFast: fail_fast === true,
|
|
1335
|
+
signal,
|
|
1336
|
+
onUpdate,
|
|
1337
|
+
params,
|
|
1338
|
+
pendant,
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/** `wait-github-commit-checks`: same, addressed by commit/branch/tag instead of a PR. */
|
|
1343
|
+
async waitCommitChecks(call: ToolCall<CommitChecksWaitParams>): Promise<ToolResult> {
|
|
1344
|
+
const { params, ctx, signal, onUpdate } = call;
|
|
1345
|
+
const { commit, repo, event, fail_fast } = params;
|
|
1346
|
+
|
|
1347
|
+
const pendant = subtitlePendant(params, "commit");
|
|
1348
|
+
onUpdate?.({
|
|
1349
|
+
content: [{ type: "text", text: `Watching CI checks for commit ${commit}...` }],
|
|
1350
|
+
details: {},
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
|
|
1354
|
+
const { owner, repo: repoName } = splitRepo(effectiveRepo);
|
|
1355
|
+
|
|
1356
|
+
const pollSignal = signal ?? new AbortController().signal;
|
|
1357
|
+
const sha = await this.checks.headSha(owner, repoName, String(commit), pollSignal);
|
|
1358
|
+
|
|
1359
|
+
return this.waitChecksReport({
|
|
1360
|
+
subject: `commit ${sha.slice(0, 7)}`,
|
|
1361
|
+
owner,
|
|
1362
|
+
repo: repoName,
|
|
1363
|
+
headSha: sha,
|
|
1364
|
+
failFast: fail_fast === true,
|
|
1365
|
+
event,
|
|
1366
|
+
signal,
|
|
1367
|
+
onUpdate,
|
|
1368
|
+
params,
|
|
1369
|
+
pendant,
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1178
1372
|
|
|
1179
1373
|
/**
|
|
1180
1374
|
* Shared wait core of `wait-github-pr-checks` and
|
|
@@ -1182,7 +1376,7 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1182
1376
|
* reports with Actions job details (display-only), render the verdict.
|
|
1183
1377
|
* The caller resolves repo/headSha; `subject` formats the report header.
|
|
1184
1378
|
*/
|
|
1185
|
-
async
|
|
1379
|
+
private async waitChecksReport(options: {
|
|
1186
1380
|
subject: string;
|
|
1187
1381
|
owner: string;
|
|
1188
1382
|
repo: string;
|
|
@@ -1190,10 +1384,10 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1190
1384
|
failFast: boolean;
|
|
1191
1385
|
event?: string;
|
|
1192
1386
|
signal: AbortSignal | undefined;
|
|
1193
|
-
onUpdate: ((msg:
|
|
1387
|
+
onUpdate: ((msg: ToolResult) => void) | undefined;
|
|
1194
1388
|
params: unknown;
|
|
1195
1389
|
pendant?: ToolPendant;
|
|
1196
|
-
}) {
|
|
1390
|
+
}): Promise<ToolResult> {
|
|
1197
1391
|
const { subject, owner, repo, headSha, failFast, event, signal, onUpdate, params, pendant } =
|
|
1198
1392
|
options;
|
|
1199
1393
|
|
|
@@ -1207,7 +1401,7 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1207
1401
|
headSha,
|
|
1208
1402
|
failFast,
|
|
1209
1403
|
event,
|
|
1210
|
-
checks:
|
|
1404
|
+
checks: this.checks,
|
|
1211
1405
|
signal: pollSignal,
|
|
1212
1406
|
onUpdate,
|
|
1213
1407
|
});
|
|
@@ -1218,7 +1412,7 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1218
1412
|
let enrichmentError: string | undefined;
|
|
1219
1413
|
if (poll.checks.some((c) => c.bucket === "fail")) {
|
|
1220
1414
|
try {
|
|
1221
|
-
actionJobs = await
|
|
1415
|
+
actionJobs = await this.checks.actionJobs(owner, repo, headSha, pollSignal);
|
|
1222
1416
|
} catch (error) {
|
|
1223
1417
|
enrichmentError =
|
|
1224
1418
|
error instanceof Error ? error.message : "Actions job details unavailable";
|
|
@@ -1227,7 +1421,7 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1227
1421
|
|
|
1228
1422
|
const verdict = renderChecksVerdict({ subject, poll, actionJobs, enrichmentError });
|
|
1229
1423
|
return {
|
|
1230
|
-
content: [{ type: "text"
|
|
1424
|
+
content: [{ type: "text", text: verdict.text }],
|
|
1231
1425
|
details: {
|
|
1232
1426
|
status: verdict.status,
|
|
1233
1427
|
totalChecks: poll.checks.length,
|
|
@@ -1238,6 +1432,34 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1238
1432
|
},
|
|
1239
1433
|
};
|
|
1240
1434
|
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// ── tools ────────────────────────────────────────────────────────────────────
|
|
1438
|
+
|
|
1439
|
+
export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
1440
|
+
// Windows 上禁用:gh 可执行文件的探测(无扩展名 + POSIX 路径)与进程
|
|
1441
|
+
// 管理(SIGTERM 信号语义)都是 POSIX 假设,不做 Windows 适配。
|
|
1442
|
+
if (process.platform === "win32") {
|
|
1443
|
+
pi.on("session_start", (_event, ctx) => {
|
|
1444
|
+
ctx.ui.notify("gh-readonly tools are disabled on Windows.", "warning");
|
|
1445
|
+
});
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// Fail fast: the `gh` CLI is the only backend for these tools. Without it the
|
|
1450
|
+
// extension registers nothing and reports the problem at session start, so
|
|
1451
|
+
// the user gets one clear error instead of a dozen failing tool calls.
|
|
1452
|
+
if (!isGhAvailable()) {
|
|
1453
|
+
pi.on("session_start", (_event, ctx) => {
|
|
1454
|
+
ctx.ui.notify(
|
|
1455
|
+
"gh CLI not found in PATH: GitHub read-only tools are disabled. Install GitHub CLI (https://cli.github.com/) and reload the session.",
|
|
1456
|
+
"error",
|
|
1457
|
+
);
|
|
1458
|
+
});
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
const client = new GhClient();
|
|
1241
1463
|
|
|
1242
1464
|
// ── read-github-issue ──────────────────────────────────────────────────────
|
|
1243
1465
|
pi.registerTool({
|
|
@@ -1300,15 +1522,8 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1300
1522
|
}),
|
|
1301
1523
|
),
|
|
1302
1524
|
}),
|
|
1303
|
-
async execute(_id, params, signal,
|
|
1304
|
-
|
|
1305
|
-
params.keywords
|
|
1306
|
-
? await searchList("issue", params, githubSearch)
|
|
1307
|
-
: await listGithub("issue", params, { cwd: ctx.cwd, signal, input: params }),
|
|
1308
|
-
params,
|
|
1309
|
-
);
|
|
1310
|
-
result.details.pendant = subtitlePendant(params);
|
|
1311
|
-
return result;
|
|
1525
|
+
async execute(_id, params, signal, onUpdate, ctx) {
|
|
1526
|
+
return client.listIssues({ params, ctx, signal, onUpdate });
|
|
1312
1527
|
},
|
|
1313
1528
|
});
|
|
1314
1529
|
|
|
@@ -1373,15 +1588,8 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1373
1588
|
}),
|
|
1374
1589
|
),
|
|
1375
1590
|
}),
|
|
1376
|
-
async execute(_id, params, signal,
|
|
1377
|
-
|
|
1378
|
-
params.keywords
|
|
1379
|
-
? await searchList("pr", params, githubSearch)
|
|
1380
|
-
: await listGithub("pr", params, { cwd: ctx.cwd, signal, input: params }),
|
|
1381
|
-
params,
|
|
1382
|
-
);
|
|
1383
|
-
result.details.pendant = subtitlePendant(params);
|
|
1384
|
-
return result;
|
|
1591
|
+
async execute(_id, params, signal, onUpdate, ctx) {
|
|
1592
|
+
return client.listPrs({ params, ctx, signal, onUpdate });
|
|
1385
1593
|
},
|
|
1386
1594
|
});
|
|
1387
1595
|
|
|
@@ -1418,35 +1626,8 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1418
1626
|
number: Type.Union([Type.Number(), Type.String()], { description: "PR number" }),
|
|
1419
1627
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO (defaults to current repo)" })),
|
|
1420
1628
|
}),
|
|
1421
|
-
async execute(_id, params, signal,
|
|
1422
|
-
|
|
1423
|
-
const pullNumber = toPositiveId(number, "number");
|
|
1424
|
-
const pendant = subtitlePendant(params, "number");
|
|
1425
|
-
const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
|
|
1426
|
-
const { owner, repo: name } = splitRepo(effectiveRepo);
|
|
1427
|
-
|
|
1428
|
-
// 与 wait 工具同一条读取路径(octokit),只是这里不轮询:pending 的 check
|
|
1429
|
-
// 原样返回,等结果走 wait-github-pr-checks。
|
|
1430
|
-
const pollSignal = signal ?? new AbortController().signal;
|
|
1431
|
-
const headSha = await githubChecks.pullHead(owner, name, pullNumber, pollSignal);
|
|
1432
|
-
const [statuses, checkRuns] = await Promise.all([
|
|
1433
|
-
githubChecks.statuses(owner, name, headSha, pollSignal),
|
|
1434
|
-
githubChecks.checkRuns(owner, name, headSha, pollSignal),
|
|
1435
|
-
]);
|
|
1436
|
-
const checks = mergeChecks(statuses, checkRuns).map((check) => ({
|
|
1437
|
-
name: check.name,
|
|
1438
|
-
bucket: check.bucket,
|
|
1439
|
-
event: check.event,
|
|
1440
|
-
run_id: check.runId,
|
|
1441
|
-
job_id: check.jobId,
|
|
1442
|
-
url: check.link,
|
|
1443
|
-
}));
|
|
1444
|
-
|
|
1445
|
-
const payload = { pr: pullNumber, repo: effectiveRepo, head_sha: headSha, checks };
|
|
1446
|
-
return {
|
|
1447
|
-
content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }],
|
|
1448
|
-
details: { ...payload, input: params, ...(pendant && { pendant }) },
|
|
1449
|
-
};
|
|
1629
|
+
async execute(_id, params, signal, onUpdate, ctx) {
|
|
1630
|
+
return client.prStatus({ params, ctx, signal, onUpdate });
|
|
1450
1631
|
},
|
|
1451
1632
|
});
|
|
1452
1633
|
|
|
@@ -1582,47 +1763,7 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1582
1763
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
1583
1764
|
}),
|
|
1584
1765
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
1585
|
-
|
|
1586
|
-
const jobId = toPositiveId(job_id, "job_id");
|
|
1587
|
-
|
|
1588
|
-
const pendant = subtitlePendant(params, "job_id");
|
|
1589
|
-
const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
|
|
1590
|
-
const { owner, repo: name } = splitRepo(effectiveRepo);
|
|
1591
|
-
|
|
1592
|
-
const failure = (text: string): CiLogsResult => ({
|
|
1593
|
-
content: [{ type: "text", text }],
|
|
1594
|
-
details: { input: params, ...(pendant && { pendant }) },
|
|
1595
|
-
});
|
|
1596
|
-
|
|
1597
|
-
let target: RunJob;
|
|
1598
|
-
try {
|
|
1599
|
-
target = await githubChecks.job(owner, name, jobId, signal);
|
|
1600
|
-
} catch (error) {
|
|
1601
|
-
const status = (error as { status?: number }).status;
|
|
1602
|
-
if (status !== 404) throw error;
|
|
1603
|
-
return failure(
|
|
1604
|
-
`Job ${jobId} not found in ${effectiveRepo} — job IDs come from \`get-github-workflow-jobs\`.`,
|
|
1605
|
-
);
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
if (target.status === "queued") {
|
|
1609
|
-
return failure(
|
|
1610
|
-
`Job "${target.name}" is still queued — no logs available yet. Use \`watch-github-run\` to wait for it to start, then retry.`,
|
|
1611
|
-
);
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
onUpdate?.({
|
|
1615
|
-
content: [{ type: "text", text: `Fetching log of job "${target.name}"...` }],
|
|
1616
|
-
details: {},
|
|
1617
|
-
});
|
|
1618
|
-
|
|
1619
|
-
const rawLog = await getJobLog(target, signal, ctx.cwd, params);
|
|
1620
|
-
const index = jobLogIndex(target, rawLog);
|
|
1621
|
-
|
|
1622
|
-
return {
|
|
1623
|
-
content: [{ type: "text", text: JSON.stringify(index, null, 2) }],
|
|
1624
|
-
details: { ...index, input: params, ...(pendant && { pendant }) },
|
|
1625
|
-
};
|
|
1766
|
+
return client.ciLogs({ params, ctx, signal, onUpdate });
|
|
1626
1767
|
},
|
|
1627
1768
|
});
|
|
1628
1769
|
|
|
@@ -1637,15 +1778,8 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1637
1778
|
run_id: Type.Union([Type.Number(), Type.String()], { description: "Workflow run ID" }),
|
|
1638
1779
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
1639
1780
|
}),
|
|
1640
|
-
async execute(_id, params, signal,
|
|
1641
|
-
|
|
1642
|
-
const runId = toPositiveId(run_id, "run_id");
|
|
1643
|
-
const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
|
|
1644
|
-
const { owner, repo: name } = splitRepo(effectiveRepo);
|
|
1645
|
-
const jobs = await githubChecks.runJobs(owner, name, runId, signal);
|
|
1646
|
-
const result = toToolResult(JSON.stringify({ total_count: jobs.length, jobs }), params);
|
|
1647
|
-
result.details.pendant = subtitlePendant(params, "run_id");
|
|
1648
|
-
return result;
|
|
1781
|
+
async execute(_id, params, signal, onUpdate, ctx) {
|
|
1782
|
+
return client.workflowJobs({ params, ctx, signal, onUpdate });
|
|
1649
1783
|
},
|
|
1650
1784
|
});
|
|
1651
1785
|
|
|
@@ -1738,34 +1872,7 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1738
1872
|
),
|
|
1739
1873
|
}),
|
|
1740
1874
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
const pendant = subtitlePendant(params, "number");
|
|
1744
|
-
onUpdate?.({
|
|
1745
|
-
content: [{ type: "text", text: `Watching CI checks for PR #${number}...` }],
|
|
1746
|
-
details: {},
|
|
1747
|
-
});
|
|
1748
|
-
|
|
1749
|
-
const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
|
|
1750
|
-
const { owner, repo: repoName } = splitRepo(effectiveRepo);
|
|
1751
|
-
|
|
1752
|
-
const prOut = await ghExec(
|
|
1753
|
-
["pr", "view", String(number), "--repo", effectiveRepo, "--json", "headRefOid"],
|
|
1754
|
-
{ cwd: ctx.cwd, signal, input: params },
|
|
1755
|
-
);
|
|
1756
|
-
const { headRefOid } = Value.Parse(prHeadSchema, JSON.parse(prOut));
|
|
1757
|
-
|
|
1758
|
-
return waitChecksReport({
|
|
1759
|
-
subject: `PR #${number}`,
|
|
1760
|
-
owner,
|
|
1761
|
-
repo: repoName,
|
|
1762
|
-
headSha: headRefOid,
|
|
1763
|
-
failFast: fail_fast === true,
|
|
1764
|
-
signal,
|
|
1765
|
-
onUpdate,
|
|
1766
|
-
params,
|
|
1767
|
-
pendant,
|
|
1768
|
-
});
|
|
1875
|
+
return client.waitPrChecks({ params, ctx, signal, onUpdate });
|
|
1769
1876
|
},
|
|
1770
1877
|
});
|
|
1771
1878
|
|
|
@@ -1798,32 +1905,7 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1798
1905
|
),
|
|
1799
1906
|
}),
|
|
1800
1907
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
const pendant = subtitlePendant(params, "commit");
|
|
1804
|
-
onUpdate?.({
|
|
1805
|
-
content: [{ type: "text", text: `Watching CI checks for commit ${commit}...` }],
|
|
1806
|
-
details: {},
|
|
1807
|
-
});
|
|
1808
|
-
|
|
1809
|
-
const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
|
|
1810
|
-
const { owner, repo: repoName } = splitRepo(effectiveRepo);
|
|
1811
|
-
|
|
1812
|
-
const pollSignal = signal ?? new AbortController().signal;
|
|
1813
|
-
const sha = await githubChecks.headSha(owner, repoName, String(commit), pollSignal);
|
|
1814
|
-
|
|
1815
|
-
return waitChecksReport({
|
|
1816
|
-
subject: `commit ${sha.slice(0, 7)}`,
|
|
1817
|
-
owner,
|
|
1818
|
-
repo: repoName,
|
|
1819
|
-
headSha: sha,
|
|
1820
|
-
failFast: fail_fast === true,
|
|
1821
|
-
event,
|
|
1822
|
-
signal,
|
|
1823
|
-
onUpdate,
|
|
1824
|
-
params,
|
|
1825
|
-
pendant,
|
|
1826
|
-
});
|
|
1908
|
+
return client.waitCommitChecks({ params, ctx, signal, onUpdate });
|
|
1827
1909
|
},
|
|
1828
1910
|
});
|
|
1829
1911
|
|
package/src/lib/gh-proxy.ts
CHANGED
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
* - ~/.pi/agent/gh.json: { "proxy": "http://127.0.0.1:7890", "noProxy": "localhost,.corp" }
|
|
6
6
|
* - HTTPS_PROXY / HTTP_PROXY / ALL_PROXY(小写变体同样接受)、NO_PROXY
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* - gh CLI 子进程:env
|
|
8
|
+
* 两条出口共用同一份配置,且在扩展加载时一次性读完:
|
|
9
|
+
* - gh CLI 子进程:env 给出要注入子进程的 HTTP(S)_PROXY / NO_PROXY 等变量
|
|
10
10
|
* - octokit 请求:fetch 是挂了代理 dispatcher 的 fetch;未配置代理时就是全局 fetch
|
|
11
|
+
*
|
|
12
|
+
* 配置有错(JSON 语法错、字段类型不符、proxy 不是 http(s) URL)直接抛错——扩展
|
|
13
|
+
* 加载即失败,而不是带着一份被忽略的配置静默直连。
|
|
11
14
|
*/
|
|
12
15
|
|
|
13
|
-
import {
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
14
17
|
import { homedir } from "node:os";
|
|
15
18
|
import { join } from "node:path";
|
|
16
19
|
|
|
@@ -63,54 +66,55 @@ function firstEnv(names: readonly string[], env: NodeJS.ProcessEnv): string | un
|
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
/** 代理必须是 http(s) URL:undici 的 ProxyAgent 只支持 HTTP CONNECT 代理。 */
|
|
66
|
-
function normalizeProxy(value: string
|
|
67
|
-
if (!value) return {};
|
|
69
|
+
function normalizeProxy(value: string): string {
|
|
68
70
|
let protocol: string;
|
|
69
71
|
try {
|
|
70
72
|
protocol = new URL(value).protocol;
|
|
71
73
|
} catch {
|
|
72
|
-
|
|
74
|
+
throw new Error(`invalid proxy URL: ${value}`);
|
|
73
75
|
}
|
|
74
76
|
if (protocol !== "http:" && protocol !== "https:") {
|
|
75
|
-
|
|
77
|
+
throw new Error(`unsupported proxy protocol: ${value} (expected http:// or https://)`);
|
|
76
78
|
}
|
|
77
|
-
return
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export interface GhProxyLoad {
|
|
81
|
-
/** 生效的代理设置(配置文件与环境变量合并后的结果)。 */
|
|
82
|
-
settings: GhProxySettings;
|
|
83
|
-
/** 配置读取/解析失败的原因;未失败时为 undefined。 */
|
|
84
|
-
error?: string;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function describeError(error: unknown): string {
|
|
88
|
-
return error instanceof Error ? error.message : String(error);
|
|
79
|
+
return value;
|
|
89
80
|
}
|
|
90
81
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
82
|
+
/**
|
|
83
|
+
* 同步读配置:扩展加载时调用一次(node:fs/promises 在同步的初始化路径上用不了,
|
|
84
|
+
* 这里是仓库里允许的同步例外)。文件不存在 = 未配置;文件读不了、JSON 非法或
|
|
85
|
+
* 字段不符都直接抛。
|
|
86
|
+
*/
|
|
87
|
+
export function readGhProxySettings(
|
|
88
|
+
configPath: string = ghProxyConfigPath(),
|
|
89
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
90
|
+
): GhProxySettings {
|
|
91
|
+
let raw: string | undefined;
|
|
94
92
|
try {
|
|
95
|
-
raw =
|
|
93
|
+
raw = readFileSync(configPath, "utf8");
|
|
96
94
|
} catch (error) {
|
|
97
|
-
if ((error as NodeJS.ErrnoException).code
|
|
98
|
-
|
|
95
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
96
|
+
throw new Error(`${configPath}: ${error instanceof Error ? error.message : String(error)}`, {
|
|
97
|
+
cause: error,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
99
100
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
|
|
102
|
+
let file: GhProxySettings = {};
|
|
103
|
+
if (raw !== undefined) {
|
|
104
|
+
try {
|
|
105
|
+
file = parseGhProxyConfig(JSON.parse(raw));
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new Error(`${configPath}: ${error instanceof Error ? error.message : String(error)}`, {
|
|
108
|
+
cause: error,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
104
111
|
}
|
|
105
|
-
}
|
|
106
112
|
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
const normalized = normalizeProxy(file.settings.proxy ?? firstEnv(PROXY_ENV_NAMES, env));
|
|
110
|
-
const noProxy = file.settings.noProxy ?? firstEnv(NO_PROXY_ENV_NAMES, env);
|
|
113
|
+
const proxy = file.proxy ?? firstEnv(PROXY_ENV_NAMES, env);
|
|
114
|
+
const noProxy = file.noProxy ?? firstEnv(NO_PROXY_ENV_NAMES, env);
|
|
111
115
|
return {
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
...(proxy && { proxy: normalizeProxy(proxy) }),
|
|
117
|
+
...(noProxy && { noProxy }),
|
|
114
118
|
};
|
|
115
119
|
}
|
|
116
120
|
|
|
@@ -132,61 +136,51 @@ export function proxyEnvVars(settings: GhProxySettings): NodeJS.ProcessEnv {
|
|
|
132
136
|
}
|
|
133
137
|
|
|
134
138
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* README 的 Proxy Servers 一节就是挂 undici 的代理 dispatcher。NO_PROXY 匹配由
|
|
140
|
-
* EnvHttpProxyAgent 负责;未配置代理时原样返回全局 fetch,默认路径行为不变。
|
|
139
|
+
* 代理 dispatcher:NO_PROXY 匹配、CONNECT 隧道都交给 undici 的
|
|
140
|
+
* EnvHttpProxyAgent,octokit 侧只认自定义 fetch(v5 丢掉了 node-fetch 时代的
|
|
141
|
+
* `agent` 选项,@octokit/request 的选项里没有这个字段,也没有任何地方把它转交给
|
|
142
|
+
* fetch),所以代理只能从 fetch 挂进去。
|
|
141
143
|
*/
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
function createProxyDispatcher(
|
|
145
|
+
proxy: string,
|
|
146
|
+
noProxy: string | undefined,
|
|
147
|
+
): NonNullable<RequestInit["dispatcher"]> {
|
|
145
148
|
// undici 包与 Node 全局 fetch 各带一份 Dispatcher 类型声明(@types/node 走
|
|
146
149
|
// undici-types),结构一致但 compose 重载对不上,这里只做类型层面的转换。
|
|
147
|
-
|
|
150
|
+
return new EnvHttpProxyAgent({
|
|
148
151
|
httpProxy: proxy,
|
|
149
152
|
httpsProxy: proxy,
|
|
150
153
|
...(noProxy && { noProxy }),
|
|
151
154
|
}) as unknown as NonNullable<RequestInit["dispatcher"]>;
|
|
152
|
-
return (input, init) => globalThis.fetch(input, { ...init, dispatcher });
|
|
153
155
|
}
|
|
154
156
|
|
|
155
157
|
export interface GhProxy {
|
|
156
|
-
/**
|
|
157
|
-
|
|
158
|
+
/** 生效的代理设置(配置文件与环境变量合并后的结果)。 */
|
|
159
|
+
readonly settings: GhProxySettings;
|
|
158
160
|
/** 要注入 gh 子进程的代理环境变量;未配置代理时为空对象。 */
|
|
159
|
-
env
|
|
161
|
+
readonly env: NodeJS.ProcessEnv;
|
|
160
162
|
/** 走代理的 fetch;未配置代理时就是全局 fetch。 */
|
|
161
163
|
readonly fetch: typeof globalThis.fetch;
|
|
162
164
|
}
|
|
163
165
|
|
|
164
166
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
+
* 读一次配置并组装代理层。缓存的是 dispatcher(连接池复用),**不是**
|
|
168
|
+
* `globalThis.fetch` 本身:每次调用都取当前的全局 fetch,否则首个请求之后替换
|
|
169
|
+
* `globalThis.fetch`(插桩、测试替身)就不再生效。
|
|
167
170
|
*/
|
|
168
171
|
export function createGhProxy(
|
|
169
172
|
configPath: string = ghProxyConfigPath(),
|
|
170
173
|
env: NodeJS.ProcessEnv = process.env,
|
|
171
174
|
): GhProxy {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
function load(): Promise<GhProxyLoad> {
|
|
176
|
-
loading ??= resolveSettings(configPath, env);
|
|
177
|
-
return loading;
|
|
178
|
-
}
|
|
175
|
+
const settings = readGhProxySettings(configPath, env);
|
|
176
|
+
const { proxy, noProxy } = settings;
|
|
177
|
+
let dispatcher: NonNullable<RequestInit["dispatcher"]> | undefined;
|
|
179
178
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return proxyEnvVars(settings);
|
|
185
|
-
},
|
|
186
|
-
fetch: async (input, init) => {
|
|
187
|
-
const { settings } = await load();
|
|
188
|
-
proxied ??= createFetch(settings);
|
|
189
|
-
return proxied(input, init);
|
|
190
|
-
},
|
|
179
|
+
const fetch: typeof globalThis.fetch = (input, init) => {
|
|
180
|
+
if (!proxy) return globalThis.fetch(input, init);
|
|
181
|
+
dispatcher ??= createProxyDispatcher(proxy, noProxy);
|
|
182
|
+
return globalThis.fetch(input, { ...init, dispatcher });
|
|
191
183
|
};
|
|
184
|
+
|
|
185
|
+
return { settings, env: proxyEnvVars(settings), fetch };
|
|
192
186
|
}
|