@trim21/personal-pi-extensions 0.1.685 → 0.1.688
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 +2 -2
- package/package.json +1 -1
- package/src/gh/base.ts +115 -60
- package/src/gh/render.ts +65 -65
- package/src/gh/schemas.ts +102 -129
- package/src/gh/tools/download-release-assets.ts +58 -80
- package/src/gh/tools/list-issues.ts +10 -6
- package/src/gh/tools/list-prs.ts +10 -6
- package/src/gh/tools/list-releases.ts +14 -13
- package/src/gh/tools/list-workflow-runs.ts +7 -19
- package/src/gh/tools/read-ci-logs.ts +6 -19
- package/src/gh/tools/read-issue-comments.ts +10 -7
- package/src/gh/tools/read-issue.ts +10 -14
- package/src/gh/tools/read-pr-comments.ts +19 -38
- package/src/gh/tools/read-pr-diff.ts +9 -8
- package/src/gh/tools/read-pr.ts +7 -15
- package/src/gh/tools/read-release.ts +7 -23
- package/src/gh/tools/read-repo.ts +7 -32
- package/src/gh/tools/wait-pr-checks.ts +6 -7
- package/src/gh/tools/watch-run.ts +35 -25
- package/src/lib/github-reads.ts +405 -0
- package/src/lib/github.ts +74 -57
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
| [写保护(内置)](#写保护内置) | 写工具内置:限制文件写入在 workspace 内,外部写入需审批 |
|
|
17
17
|
| [LSP(内置)](#lsp内置) | 文件工具内置 LSP 诊断 + `lsp-rename`/`lsp-inspect`/`lsp-find-definition`/`lsp-find-reference` |
|
|
18
18
|
| [aft](#aft) | AFT 只读代码感知:`aft_outline`/`aft_zoom`/`aft_callgraph`/`aft_search` |
|
|
19
|
-
| [gh-readonly](#gh-readonly) | GitHub 只读工具集(issue / PR / CI / release
|
|
19
|
+
| [gh-readonly](#gh-readonly) | GitHub 只读工具集(issue / PR / CI / release),数据走 octokit REST |
|
|
20
20
|
| [spawn-agent](#spawn-agent) | 把任务委派给独立上下文窗口的子代理 |
|
|
21
21
|
| [system-prompt](#system-prompt) | 完全替换 pi 默认 system prompt |
|
|
22
22
|
| [vision-agent](#vision-agent) | 视觉代理:主模型不支持视觉时提供 `describe_image`,调用视觉模型识别图片 |
|
|
@@ -461,7 +461,7 @@ pi -e ./src/aft/index.ts
|
|
|
461
461
|
|
|
462
462
|
## gh-readonly
|
|
463
463
|
|
|
464
|
-
GitHub
|
|
464
|
+
GitHub 只读工具集。取数走 octokit REST(`src/lib/github-reads.ts`);只有「当前仓库」解析(`gh repo view`)与 token 获取(`gh auth token`)还用系统 [`gh`](https://cli.github.com/),因此 `gh` 不在 PATH 时整组不注册并在 session_start 报错;Windows 禁用。
|
|
465
465
|
|
|
466
466
|
- **Issue / PR**:`read-github-issue`、`list-github-issues`、`read-github-issue-comments`、`read-github-pr`、`list-github-prs`、`read-github-pr-diff`、`read-github-pr-status`、`read-github-pr-comments`
|
|
467
467
|
- **CI**:`read-github-ci-logs`、`list-github-workflow-runs`、`get-github-workflow-jobs`、`wait-github-pr-checks`、`wait-github-commit-checks`、`watch-github-run`
|
package/package.json
CHANGED
package/src/gh/base.ts
CHANGED
|
@@ -20,14 +20,14 @@ import {
|
|
|
20
20
|
type CommitStatus,
|
|
21
21
|
createGithubChecks,
|
|
22
22
|
createGithubSearch,
|
|
23
|
-
GH_LIST_FIELDS,
|
|
24
23
|
type GithubChecksClient,
|
|
25
24
|
type GithubClientOptions,
|
|
26
25
|
type GithubSearch,
|
|
27
|
-
|
|
26
|
+
normalizeRestList,
|
|
28
27
|
renderHits,
|
|
29
28
|
type SearchHit,
|
|
30
29
|
} from "../lib/github.js";
|
|
30
|
+
import { createGithubReads, type GithubReads } from "../lib/github-reads.js";
|
|
31
31
|
import { parseWithSchema } from "../lib/parse-with-schema.js";
|
|
32
32
|
import { type ToolPendant } from "../lib/pendant.js";
|
|
33
33
|
import { type StructuredResult } from "../lib/tool-bus.js";
|
|
@@ -143,24 +143,6 @@ export async function ghExec(
|
|
|
143
143
|
return result.stdout;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
export function repoArgs(repo?: string): string[] {
|
|
147
|
-
return repo ? ["--repo", repo] : [];
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* `gh api` for a JSON-array endpoint, following pagination. The REST API pages
|
|
152
|
-
* these lists at 30 items by default, so a single page silently drops the rest;
|
|
153
|
-
* `--slurp` is required because `--paginate` alone prints the pages back to back
|
|
154
|
-
* (not valid JSON), and the page arrays are flattened back into one list.
|
|
155
|
-
*/
|
|
156
|
-
export async function ghApiList(
|
|
157
|
-
path: string,
|
|
158
|
-
ctx: { cwd?: string; signal?: AbortSignal; input?: unknown },
|
|
159
|
-
): Promise<unknown[]> {
|
|
160
|
-
const out = await ghExec(["api", "--paginate", "--slurp", path], ctx);
|
|
161
|
-
return Value.Parse(Type.Array(Type.Array(Type.Unknown())), JSON.parse(out)).flat();
|
|
162
|
-
}
|
|
163
|
-
|
|
164
146
|
/** Split `OWNER/REPO`; throws when the name doesn't have exactly one slash. */
|
|
165
147
|
export function splitRepo(nameWithOwner: string): { owner: string; repo: string } {
|
|
166
148
|
const slash = nameWithOwner.indexOf("/");
|
|
@@ -326,53 +308,30 @@ export interface ListFilters {
|
|
|
326
308
|
}
|
|
327
309
|
|
|
328
310
|
/**
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
* Keyword searches no longer go through the `gh` CLI — the octokit-based client
|
|
332
|
-
* in `../lib/github.ts` handles them with state values (`all`, and `merged` for
|
|
333
|
-
* PRs) that `gh search` cannot express. Browse calls keep `gh issue list` /
|
|
334
|
-
* `gh pr list` semantics: `state` is passed through verbatim, since `gh issue
|
|
335
|
-
* list` accepts open/closed/all and `gh pr list` additionally accepts merged.
|
|
336
|
-
*/
|
|
337
|
-
export function listGithubArgs(kind: "issue" | "pr", params: ListFilters): string[] {
|
|
338
|
-
const { repo, state, label, author, assignee, milestone, limit } = params;
|
|
339
|
-
|
|
340
|
-
const args = [kind, "list", ...repoArgs(repo)];
|
|
341
|
-
if (state) {
|
|
342
|
-
args.push("--state", state);
|
|
343
|
-
}
|
|
344
|
-
if (label) {
|
|
345
|
-
args.push("--label", label);
|
|
346
|
-
}
|
|
347
|
-
if (author) {
|
|
348
|
-
args.push("--author", author);
|
|
349
|
-
}
|
|
350
|
-
if (assignee) {
|
|
351
|
-
args.push("--assignee", assignee);
|
|
352
|
-
}
|
|
353
|
-
if (milestone) {
|
|
354
|
-
args.push("--milestone", milestone);
|
|
355
|
-
}
|
|
356
|
-
if (limit) {
|
|
357
|
-
args.push("--limit", String(limit));
|
|
358
|
-
}
|
|
359
|
-
return args;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* 浏览分支(`gh issue list` / `gh pr list`,不带 keywords):直接要 `--json`,再归一到与
|
|
364
|
-
* 搜索分支同一套 `SearchHit`。repo 缺省时先解析当前仓库——文本要据此决定是否带 repo 列,
|
|
365
|
-
* 载荷里也必须有值。
|
|
311
|
+
* 浏览分支(不带 keywords):走 octokit 的 REST 列表端点,再归一到与搜索分支同一套
|
|
312
|
+
* `SearchHit`。repo 缺省时先解析当前仓库——文本要据此决定是否带 repo 列,载荷里也必须有值。
|
|
366
313
|
*/
|
|
367
314
|
export async function browseList(
|
|
315
|
+
gh: GhClient,
|
|
368
316
|
kind: "issue" | "pr",
|
|
369
317
|
params: ListFilters,
|
|
370
318
|
ctx: { cwd?: string; signal?: AbortSignal; input?: unknown },
|
|
371
319
|
): Promise<{ repo: string; hits: SearchHit[] }> {
|
|
372
320
|
const repo = params.repo ?? (await resolveRepo(undefined, ctx.signal, ctx.cwd, ctx.input));
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
|
|
321
|
+
const { owner, repo: repoName } = splitRepo(repo);
|
|
322
|
+
const query = {
|
|
323
|
+
state: params.state,
|
|
324
|
+
label: params.label,
|
|
325
|
+
author: params.author,
|
|
326
|
+
assignee: params.assignee,
|
|
327
|
+
milestone: params.milestone,
|
|
328
|
+
limit: params.limit,
|
|
329
|
+
};
|
|
330
|
+
const items =
|
|
331
|
+
kind === "issue"
|
|
332
|
+
? await gh.reads.listIssues(owner, repoName, query, ctx.signal)
|
|
333
|
+
: await gh.reads.listPulls(owner, repoName, query, ctx.signal);
|
|
334
|
+
return { repo, hits: normalizeRestList(items, repo) };
|
|
376
335
|
}
|
|
377
336
|
|
|
378
337
|
/** 行列表的文本:两条分支共用一套 TSV 渲染;空结果是成功结果。 */
|
|
@@ -400,6 +359,8 @@ export class GhClient {
|
|
|
400
359
|
readonly fetch: typeof globalThis.fetch;
|
|
401
360
|
readonly search: GithubSearch;
|
|
402
361
|
readonly checks: GithubChecksClient;
|
|
362
|
+
/** REST 读取层:gh 工具的取数都走它(不再 spawn `gh` 取数据)。 */
|
|
363
|
+
readonly reads: GithubReads;
|
|
403
364
|
|
|
404
365
|
constructor(
|
|
405
366
|
fetchImpl: typeof globalThis.fetch = egress.fetch,
|
|
@@ -408,12 +369,31 @@ export class GhClient {
|
|
|
408
369
|
this.fetch = fetchImpl;
|
|
409
370
|
this.search = createGithubSearch({ fetch: fetchImpl, ...options });
|
|
410
371
|
this.checks = createGithubChecks({ fetch: fetchImpl, ...options });
|
|
372
|
+
this.reads = createGithubReads({ fetch: fetchImpl, ...options });
|
|
411
373
|
}
|
|
412
374
|
}
|
|
413
375
|
|
|
414
376
|
// ── checks watch (pure rendering + poll loop) ────────────────────────────────
|
|
415
377
|
|
|
416
378
|
const CHECKS_POLL_INTERVAL_MS = 30_000;
|
|
379
|
+
/** `watch-github-run` 的轮询间隔与上限。 */
|
|
380
|
+
const RUN_WATCH_INTERVAL_MS = 30_000;
|
|
381
|
+
const RUN_WATCH_DEADLINE_MS = 600_000;
|
|
382
|
+
|
|
383
|
+
/** 运行快照:只声明我们渲染与载荷用到的字段,其余由 REST 原样带过。 */
|
|
384
|
+
const runSnapshotSchema = Type.Object({
|
|
385
|
+
id: Type.Number(),
|
|
386
|
+
name: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
387
|
+
display_title: Type.Optional(Type.String()),
|
|
388
|
+
status: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
389
|
+
conclusion: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
390
|
+
head_branch: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
391
|
+
event: Type.Optional(Type.String()),
|
|
392
|
+
created_at: Type.Optional(Type.String()),
|
|
393
|
+
updated_at: Type.Optional(Type.String()),
|
|
394
|
+
run_started_at: Type.Optional(Type.String()),
|
|
395
|
+
html_url: Type.Optional(Type.String()),
|
|
396
|
+
});
|
|
417
397
|
const CHECKS_WATCH_DEADLINE_MS = 600_000;
|
|
418
398
|
|
|
419
399
|
export type CheckBucket = "pass" | "skipped" | "fail" | "pending";
|
|
@@ -654,6 +634,81 @@ export async function pollPrChecks(options: PollPrChecksOptions): Promise<Checks
|
|
|
654
634
|
}
|
|
655
635
|
}
|
|
656
636
|
|
|
637
|
+
/** One polled workflow run: the REST fields the watch loop reports and renders. */
|
|
638
|
+
export interface RunSnapshot {
|
|
639
|
+
id: number;
|
|
640
|
+
name?: string | null;
|
|
641
|
+
display_title?: string;
|
|
642
|
+
status?: string | null;
|
|
643
|
+
conclusion?: string | null;
|
|
644
|
+
head_branch?: string | null;
|
|
645
|
+
event?: string;
|
|
646
|
+
created_at?: string;
|
|
647
|
+
updated_at?: string;
|
|
648
|
+
run_started_at?: string;
|
|
649
|
+
html_url?: string;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** 一行运行状态:`工作流 · 事件 · 分支 — 状态`(附耗时与链接)。 */
|
|
653
|
+
export function renderRunStatus(run: RunSnapshot, elapsedMs?: number): string {
|
|
654
|
+
const state = run.conclusion ?? run.status ?? "unknown";
|
|
655
|
+
const facts = [run.name, run.event, run.head_branch].filter(Boolean).join(" · ");
|
|
656
|
+
const elapsed = elapsedMs === undefined ? "" : ` (${Math.round(elapsedMs / 1000)}s)`;
|
|
657
|
+
return `${facts} — ${state}${elapsed}${run.html_url === undefined ? "" : `\n${run.html_url}`}`;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export interface WatchRunOptions {
|
|
661
|
+
owner: string;
|
|
662
|
+
repo: string;
|
|
663
|
+
runId: number;
|
|
664
|
+
reads: GithubReads;
|
|
665
|
+
/** Owned by the caller; the poll loop observes it but never aborts it. */
|
|
666
|
+
signal: AbortSignal;
|
|
667
|
+
/** Test overrides. */
|
|
668
|
+
intervalMs?: number;
|
|
669
|
+
deadlineMs?: number;
|
|
670
|
+
onUpdate?: (msg: ToolResult) => void;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export interface RunWatchResult {
|
|
674
|
+
outcome: "completed" | "timeout";
|
|
675
|
+
run: RunSnapshot;
|
|
676
|
+
elapsedMs: number;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* 轮询 `getWorkflowRun` 直到运行结束(`gh run watch` 的替代):固定间隔轮询,每轮通过
|
|
681
|
+
* onUpdate 报一次当前状态,超过上限以 timeout 结束(不挂住)。
|
|
682
|
+
*/
|
|
683
|
+
export async function watchRun(options: WatchRunOptions): Promise<RunWatchResult> {
|
|
684
|
+
const { owner, repo, runId, reads, signal, onUpdate } = options;
|
|
685
|
+
const intervalMs = options.intervalMs ?? RUN_WATCH_INTERVAL_MS;
|
|
686
|
+
const deadlineMs = options.deadlineMs ?? RUN_WATCH_DEADLINE_MS;
|
|
687
|
+
|
|
688
|
+
const watchStart = Date.now();
|
|
689
|
+
for (;;) {
|
|
690
|
+
signal.throwIfAborted();
|
|
691
|
+
const run = parseWithSchema(runSnapshotSchema, await reads.run(owner, repo, runId, signal));
|
|
692
|
+
const elapsedMs = Date.now() - watchStart;
|
|
693
|
+
onUpdate?.({
|
|
694
|
+
content: [
|
|
695
|
+
{
|
|
696
|
+
type: "text",
|
|
697
|
+
text: `Watching workflow run ${runId}...\n${renderRunStatus(run, elapsedMs)}`,
|
|
698
|
+
},
|
|
699
|
+
],
|
|
700
|
+
details: {},
|
|
701
|
+
});
|
|
702
|
+
if (run.status === "completed") {
|
|
703
|
+
return { outcome: "completed", run, elapsedMs };
|
|
704
|
+
}
|
|
705
|
+
if (elapsedMs >= deadlineMs) {
|
|
706
|
+
return { outcome: "timeout", run, elapsedMs };
|
|
707
|
+
}
|
|
708
|
+
await sleepInterruptibly(intervalMs, signal);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
657
712
|
/** Job conclusions that count as "did not succeed" for CI result reporting. */
|
|
658
713
|
const FAILED_JOB_CONCLUSIONS = new Set([
|
|
659
714
|
"failure",
|
package/src/gh/render.ts
CHANGED
|
@@ -6,15 +6,20 @@
|
|
|
6
6
|
|
|
7
7
|
import { dateOnly } from "../lib/github.js";
|
|
8
8
|
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* 一行一个 release:`tag 标记 发布日期 标题`。
|
|
11
|
+
*
|
|
12
|
+
* 标记有 `latest`(该仓库最新发布)/ `prerelease` / `draft`;REST 没有「isLatest」字段
|
|
13
|
+
* (那是 gh 按列表顺序算的),所以由调用方按第一个非 draft/prerelease 的条目传进来。
|
|
14
|
+
*/
|
|
10
15
|
export function renderReleaseList(
|
|
11
16
|
releases: {
|
|
12
|
-
|
|
13
|
-
name?: string;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
tag_name: string;
|
|
18
|
+
name?: string | null;
|
|
19
|
+
latest?: boolean;
|
|
20
|
+
prerelease?: boolean;
|
|
21
|
+
draft?: boolean;
|
|
22
|
+
published_at?: string | null;
|
|
18
23
|
}[],
|
|
19
24
|
): string {
|
|
20
25
|
if (releases.length === 0) {
|
|
@@ -23,14 +28,14 @@ export function renderReleaseList(
|
|
|
23
28
|
return releases
|
|
24
29
|
.map((release) => {
|
|
25
30
|
const flags = [
|
|
26
|
-
release.
|
|
27
|
-
release.
|
|
28
|
-
release.
|
|
31
|
+
release.latest ? "latest" : "",
|
|
32
|
+
release.prerelease ? "prerelease" : "",
|
|
33
|
+
release.draft ? "draft" : "",
|
|
29
34
|
].filter(Boolean);
|
|
30
35
|
return [
|
|
31
|
-
release.
|
|
36
|
+
release.tag_name,
|
|
32
37
|
flags.join(","),
|
|
33
|
-
dateOnly(release.
|
|
38
|
+
dateOnly(release.published_at),
|
|
34
39
|
release.name ?? "",
|
|
35
40
|
].join("\t");
|
|
36
41
|
})
|
|
@@ -40,14 +45,14 @@ export function renderReleaseList(
|
|
|
40
45
|
/** 一行一个运行:`id 状态 结论 工作流 分支 事件 创建时间 链接`。 */
|
|
41
46
|
export function renderRunList(
|
|
42
47
|
runs: {
|
|
43
|
-
|
|
44
|
-
status?: string;
|
|
48
|
+
id: number;
|
|
49
|
+
status?: string | null;
|
|
45
50
|
conclusion?: string | null;
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
name?: string | null;
|
|
52
|
+
head_branch?: string | null;
|
|
48
53
|
event?: string;
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
created_at?: string;
|
|
55
|
+
html_url?: string;
|
|
51
56
|
}[],
|
|
52
57
|
): string {
|
|
53
58
|
if (runs.length === 0) {
|
|
@@ -56,14 +61,14 @@ export function renderRunList(
|
|
|
56
61
|
return runs
|
|
57
62
|
.map((run) =>
|
|
58
63
|
[
|
|
59
|
-
String(run.
|
|
64
|
+
String(run.id),
|
|
60
65
|
run.status ?? "",
|
|
61
66
|
run.conclusion ?? "",
|
|
62
|
-
run.
|
|
63
|
-
run.
|
|
67
|
+
run.name ?? "",
|
|
68
|
+
run.head_branch ?? "",
|
|
64
69
|
run.event ?? "",
|
|
65
|
-
dateOnly(run.
|
|
66
|
-
run.
|
|
70
|
+
dateOnly(run.created_at),
|
|
71
|
+
run.html_url ?? "",
|
|
67
72
|
].join("\t"),
|
|
68
73
|
)
|
|
69
74
|
.join("\n");
|
|
@@ -71,37 +76,35 @@ export function renderRunList(
|
|
|
71
76
|
|
|
72
77
|
/** 仓库概览:标题行 + 一行事实 + 一行链接 + 一行日期/许可。 */
|
|
73
78
|
export function renderRepoView(repo: {
|
|
74
|
-
|
|
75
|
-
description?: string;
|
|
76
|
-
|
|
79
|
+
full_name?: string;
|
|
80
|
+
description?: string | null;
|
|
81
|
+
html_url?: string;
|
|
77
82
|
visibility?: string;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
createdAt?: string;
|
|
83
|
+
language?: string | null;
|
|
84
|
+
default_branch?: string;
|
|
85
|
+
stargazers_count?: number;
|
|
86
|
+
forks_count?: number;
|
|
87
|
+
open_issues_count?: number;
|
|
88
|
+
license?: { name?: string } | null;
|
|
89
|
+
pushed_at?: string;
|
|
90
|
+
created_at?: string;
|
|
87
91
|
}): string {
|
|
88
92
|
const facts = [
|
|
89
93
|
repo.visibility?.toLowerCase(),
|
|
90
|
-
repo.
|
|
91
|
-
repo.
|
|
92
|
-
repo.
|
|
93
|
-
repo.
|
|
94
|
-
repo.
|
|
95
|
-
repo.pullRequests && `open PRs ${repo.pullRequests.totalCount}`,
|
|
94
|
+
repo.language,
|
|
95
|
+
repo.default_branch && `default branch ${repo.default_branch}`,
|
|
96
|
+
repo.stargazers_count !== undefined && `stars ${repo.stargazers_count}`,
|
|
97
|
+
repo.forks_count !== undefined && `forks ${repo.forks_count}`,
|
|
98
|
+
repo.open_issues_count !== undefined && `open issues ${repo.open_issues_count}`,
|
|
96
99
|
].filter(Boolean);
|
|
97
100
|
return [
|
|
98
|
-
[repo.
|
|
101
|
+
[repo.full_name, repo.description].filter(Boolean).join(" — "),
|
|
99
102
|
facts.join(" · "),
|
|
100
|
-
repo.
|
|
103
|
+
repo.html_url ?? "",
|
|
101
104
|
[
|
|
102
|
-
repo.
|
|
103
|
-
repo.
|
|
104
|
-
repo.
|
|
105
|
+
repo.pushed_at && `pushed ${dateOnly(repo.pushed_at)}`,
|
|
106
|
+
repo.created_at && `created ${dateOnly(repo.created_at)}`,
|
|
107
|
+
repo.license?.name,
|
|
105
108
|
]
|
|
106
109
|
.filter(Boolean)
|
|
107
110
|
.join(" · "),
|
|
@@ -112,33 +115,30 @@ export function renderRepoView(repo: {
|
|
|
112
115
|
|
|
113
116
|
/** release 详情:标题、事实、资产清单、正文。 */
|
|
114
117
|
export function renderReleaseView(release: {
|
|
115
|
-
|
|
116
|
-
name?: string;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
tag_name?: string;
|
|
119
|
+
name?: string | null;
|
|
120
|
+
html_url?: string;
|
|
121
|
+
/** 由调用方按「列表里第一个非 draft / prerelease」推断(REST 没有这个字段)。 */
|
|
122
|
+
latest?: boolean;
|
|
123
|
+
prerelease?: boolean;
|
|
124
|
+
draft?: boolean;
|
|
125
|
+
published_at?: string | null;
|
|
126
|
+
created_at?: string;
|
|
123
127
|
author?: { login?: string } | null;
|
|
124
|
-
assets?: { name?: string; size?: number;
|
|
125
|
-
body?: string;
|
|
128
|
+
assets?: { name?: string; size?: number; download_count?: number }[];
|
|
129
|
+
body?: string | null;
|
|
126
130
|
}): string {
|
|
127
|
-
const flags = [
|
|
128
|
-
release.isLatest && "latest",
|
|
129
|
-
release.isPrerelease && "prerelease",
|
|
130
|
-
release.isDraft && "draft",
|
|
131
|
-
]
|
|
131
|
+
const flags = [release.prerelease && "prerelease", release.draft && "draft"]
|
|
132
132
|
.filter(Boolean)
|
|
133
133
|
.join(",");
|
|
134
134
|
const assets = release.assets ?? [];
|
|
135
135
|
const lines = [
|
|
136
|
-
[release.
|
|
136
|
+
[release.tag_name, release.name].filter(Boolean).join(" — "),
|
|
137
137
|
[
|
|
138
|
-
`published ${dateOnly(release.
|
|
138
|
+
`published ${dateOnly(release.published_at ?? release.created_at)}`,
|
|
139
139
|
flags,
|
|
140
140
|
release.author?.login && `by ${release.author.login}`,
|
|
141
|
-
release.
|
|
141
|
+
release.html_url,
|
|
142
142
|
]
|
|
143
143
|
.filter(Boolean)
|
|
144
144
|
.join(" · "),
|
|
@@ -147,7 +147,7 @@ export function renderReleaseView(release: {
|
|
|
147
147
|
lines.push(`assets (${assets.length}):`);
|
|
148
148
|
for (const asset of assets) {
|
|
149
149
|
lines.push(
|
|
150
|
-
`- ${asset.name ?? ""} ${asset.size ?? 0} bytes, ${asset.
|
|
150
|
+
`- ${asset.name ?? ""} ${asset.size ?? 0} bytes, ${asset.download_count ?? 0} downloads`,
|
|
151
151
|
);
|
|
152
152
|
}
|
|
153
153
|
}
|