@trim21/personal-pi-extensions 0.1.684 → 0.1.685
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 +8 -6
- package/package.json +1 -1
- package/src/codemode/declarations.ts +3 -3
- package/src/codemode/tool.ts +13 -26
- package/src/gh/base.ts +20 -10
- package/src/gh/render.ts +259 -0
- package/src/gh/schemas.ts +213 -0
- package/src/gh/tools/list-issues.ts +62 -47
- package/src/gh/tools/list-prs.ts +62 -47
- package/src/gh/tools/list-releases.ts +41 -24
- package/src/gh/tools/list-workflow-runs.ts +52 -34
- package/src/gh/tools/read-pr-diff.ts +32 -21
- package/src/gh/tools/read-release.ts +52 -24
- package/src/gh/tools/read-repo.ts +59 -23
- package/src/gh/tools/watch-run.ts +55 -39
- package/src/lib/github.ts +75 -0
- package/src/lib/lsp/inspect-schemas.ts +44 -0
- package/src/lib/lsp/inspect-tool.ts +188 -137
package/README.md
CHANGED
|
@@ -36,12 +36,14 @@
|
|
|
36
36
|
- **嵌套调用经工具总线**:`call("Bash", { command })` 最终执行的是 `Bash` 工具自己的
|
|
37
37
|
`execute`,所以工具的审批照常生效(Bash 沙箱外执行会弹自己的提权确认)。codemode
|
|
38
38
|
不额外加确认层:脚本里连发十次调用就是十次工具自己的审批(需要审批的那些)。
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
- **可调用集合 = 声明了结构化输出的工具**(有 `structuredSchema` 的才进),执行时再与当前
|
|
40
|
+
active 列表求交,所以 `personalExtensions.disabledTools`、pi 的 `defaultTools` /
|
|
41
|
+
`--tools`、子代理的工具白名单都同样约束脚本。脚本拿到的返回值必须有确定的形状,因此没给
|
|
42
|
+
结构化结果的工具不进集合——这条规则自己维持一致,不需要维护一份会漂移的黑名单。由此天然
|
|
43
|
+
不在集合里的:`codemode` 自身与 `spawn-agent`、文件读写工具(脚本用 `fs.read` /
|
|
44
|
+
`fs.write`)、搜索工具(脚本用 `call("Bash", { command: "rg …" })`,走同一个沙箱、拿得到
|
|
45
|
+
退出码,还能拼管道)、`lsp-rename`(写工具)以及 talk / 会话工具(会把执行时间交给外部
|
|
46
|
+
输入)。
|
|
45
47
|
- **文件读写只有 `fs` 一条路**:`fs.read(path)` 返回文件全文的原始 UTF-8 文本(不加行号、
|
|
46
48
|
不截断、不设大小上限,只有内容不是合法 UTF-8 时报错),`fs.write(path, content)` 整体写入并自动创建
|
|
47
49
|
父目录,相对路径相对当前 cwd。`fs.write` 与写类工具共用同一套保护:写前要求「已读且读后
|
package/package.json
CHANGED
|
@@ -12,7 +12,8 @@ interface ToolLike {
|
|
|
12
12
|
name: string;
|
|
13
13
|
description?: string;
|
|
14
14
|
parameters?: unknown;
|
|
15
|
-
|
|
15
|
+
/** 只有声明了它的工具才可调用(见 `tool.ts` 的准入条件),因此这里是必需的。 */
|
|
16
|
+
structuredSchema: unknown;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
type JsonSchema = Record<string, unknown>;
|
|
@@ -88,8 +89,7 @@ function renderType(schema: unknown, indent: string): string {
|
|
|
88
89
|
function renderOverload(tool: ToolLike): string {
|
|
89
90
|
const summary = tool.description?.split("\n", 1)[0]?.trim();
|
|
90
91
|
const doc = summary ? `/** ${summary} */\n` : "";
|
|
91
|
-
const output =
|
|
92
|
-
tool.structuredSchema === undefined ? "string" : renderType(tool.structuredSchema, "");
|
|
92
|
+
const output = renderType(tool.structuredSchema, "");
|
|
93
93
|
return `${doc}declare function call(name: ${JSON.stringify(tool.name)}, args: ${renderType(tool.parameters, "")}): Promise<${output}>;`;
|
|
94
94
|
}
|
|
95
95
|
|
package/src/codemode/tool.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 自己的确认层),以及 `fs.read` / `fs.write` 两个文件原语(同样由主线程执行,与文件工具
|
|
6
6
|
* 共用写审批与已读记账,见 fs.ts)。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* 可调用集合:总线上实际注册的、**声明了 `structuredSchema`** 的工具,执行时再与 active
|
|
9
9
|
* 列表求交——pi 自己的 `defaultTools` / `--tools` / 子代理白名单的排除因此同样生效。
|
|
10
10
|
*
|
|
11
11
|
* wasm 在注册这个工具时编译一次(`createCodemodeSandbox`),worker 复用编译结果。
|
|
@@ -32,30 +32,16 @@ import { CODEMODE_SOURCE_GRAMMAR, DEFAULT_OUTPUT_TOKENS, parseCodemodeSource } f
|
|
|
32
32
|
export const CODEMODE_TOOL_NAME = "codemode";
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
35
|
+
* 准入条件:工具**声明了 `structuredSchema`** 才进 codemode。
|
|
36
|
+
*
|
|
37
|
+
* 脚本拿到的返回值必须有确定的形状——只给文本的工具在脚本里既没法当数据用(得解析文本),
|
|
38
|
+
* 也没有返回类型能写进声明。用「有没有结构化输出」当门槛,这条规则自己会维持一致:不给工具
|
|
39
|
+
* 加 schema 就不进集合,不需要维护一份会随工具增减而漂移的黑名单。因此下列工具天然不在集合
|
|
40
|
+
* 里,理由各自成立:`codemode` 自身与 `spawn-agent`(没有 schema)、文件读写工具(脚本用
|
|
41
|
+
* `fs.read` / `fs.write`,不重复一套为 LLM 上下文设计的行号/锚点语义)、搜索工具(脚本用
|
|
42
|
+
* `call("Bash", { command })` 跑 `rg`,退出码可用、能拼管道)、`lsp-rename`(写工具)以及
|
|
43
|
+
* talk / 会话工具(会把执行时间交给外部输入)。
|
|
43
44
|
*/
|
|
44
|
-
const EXCLUDED_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
45
|
-
CODEMODE_TOOL_NAME,
|
|
46
|
-
"spawn-agent",
|
|
47
|
-
"Read",
|
|
48
|
-
"Edit",
|
|
49
|
-
"Write",
|
|
50
|
-
"read",
|
|
51
|
-
"edit",
|
|
52
|
-
"write",
|
|
53
|
-
"Grep",
|
|
54
|
-
"Glob",
|
|
55
|
-
"grep",
|
|
56
|
-
"glob",
|
|
57
|
-
]);
|
|
58
|
-
|
|
59
45
|
/** 估计 token 用的字符数(与 pi 一致)。 */
|
|
60
46
|
const CHARS_PER_TOKEN = 4;
|
|
61
47
|
|
|
@@ -78,7 +64,8 @@ interface CallableTool {
|
|
|
78
64
|
name: string;
|
|
79
65
|
description?: string;
|
|
80
66
|
parameters?: unknown;
|
|
81
|
-
|
|
67
|
+
/** 准入条件:进集合的工具一定带着它(见上面的注释)。 */
|
|
68
|
+
structuredSchema: unknown;
|
|
82
69
|
}
|
|
83
70
|
|
|
84
71
|
/** 只在 pi 有 active 工具概念时才求交(子代理、`--tools` 等场景)。 */
|
|
@@ -94,7 +81,7 @@ function allowedToolNames(pi: ExtensionAPI): Set<string> | undefined {
|
|
|
94
81
|
function collectTools(bus: ToolBus, allowed: Set<string> | undefined): CallableTool[] {
|
|
95
82
|
return bus
|
|
96
83
|
.list()
|
|
97
|
-
.filter((definition) =>
|
|
84
|
+
.filter((definition) => definition.structuredSchema !== undefined)
|
|
98
85
|
.filter((definition) => allowed === undefined || allowed.has(definition.name))
|
|
99
86
|
.map((definition) => ({
|
|
100
87
|
name: definition.name,
|
package/src/gh/base.ts
CHANGED
|
@@ -20,10 +20,13 @@ import {
|
|
|
20
20
|
type CommitStatus,
|
|
21
21
|
createGithubChecks,
|
|
22
22
|
createGithubSearch,
|
|
23
|
+
GH_LIST_FIELDS,
|
|
23
24
|
type GithubChecksClient,
|
|
24
25
|
type GithubClientOptions,
|
|
25
26
|
type GithubSearch,
|
|
27
|
+
normalizeGhList,
|
|
26
28
|
renderHits,
|
|
29
|
+
type SearchHit,
|
|
27
30
|
} from "../lib/github.js";
|
|
28
31
|
import { parseWithSchema } from "../lib/parse-with-schema.js";
|
|
29
32
|
import { type ToolPendant } from "../lib/pendant.js";
|
|
@@ -356,25 +359,32 @@ export function listGithubArgs(kind: "issue" | "pr", params: ListFilters): strin
|
|
|
356
359
|
return args;
|
|
357
360
|
}
|
|
358
361
|
|
|
359
|
-
|
|
362
|
+
/**
|
|
363
|
+
* 浏览分支(`gh issue list` / `gh pr list`,不带 keywords):直接要 `--json`,再归一到与
|
|
364
|
+
* 搜索分支同一套 `SearchHit`。repo 缺省时先解析当前仓库——文本要据此决定是否带 repo 列,
|
|
365
|
+
* 载荷里也必须有值。
|
|
366
|
+
*/
|
|
367
|
+
export async function browseList(
|
|
360
368
|
kind: "issue" | "pr",
|
|
361
369
|
params: ListFilters,
|
|
362
370
|
ctx: { cwd?: string; signal?: AbortSignal; input?: unknown },
|
|
363
|
-
): Promise<string> {
|
|
364
|
-
|
|
371
|
+
): Promise<{ repo: string; hits: SearchHit[] }> {
|
|
372
|
+
const repo = params.repo ?? (await resolveRepo(undefined, ctx.signal, ctx.cwd, ctx.input));
|
|
373
|
+
const args = [...listGithubArgs(kind, params), "--json", GH_LIST_FIELDS];
|
|
374
|
+
const stdout = await ghExec(args, ctx);
|
|
375
|
+
return { repo, hits: normalizeGhList(stdout, repo) };
|
|
365
376
|
}
|
|
366
377
|
|
|
367
|
-
/**
|
|
368
|
-
export
|
|
378
|
+
/** 行列表的文本:两条分支共用一套 TSV 渲染;空结果是成功结果。 */
|
|
379
|
+
export function renderHitList(
|
|
369
380
|
kind: "issue" | "pr",
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
):
|
|
373
|
-
const hits = await githubSearch.search(kind, params);
|
|
381
|
+
hits: SearchHit[],
|
|
382
|
+
options: { repo?: string; fields?: string },
|
|
383
|
+
): string {
|
|
374
384
|
if (hits.length === 0) {
|
|
375
385
|
return `(no matching ${kind === "issue" ? "issues" : "pull requests"})`;
|
|
376
386
|
}
|
|
377
|
-
return renderHits(hits,
|
|
387
|
+
return renderHits(hits, options);
|
|
378
388
|
}
|
|
379
389
|
|
|
380
390
|
/**
|
package/src/gh/render.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文本类 gh 工具改走 `--json` 之后的渲染器:同一份 JSON 既渲染成给模型看的文本,也原样
|
|
3
|
+
* 进结构化载荷。文本格式是我们自己的(不再是 gh 的表格),因此这些函数要稳定、好读,
|
|
4
|
+
* 且只依赖 schema 里声明过的字段。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { dateOnly } from "../lib/github.js";
|
|
8
|
+
|
|
9
|
+
/** 一行一个 release:`tag 标记 发布日期 标题`。 */
|
|
10
|
+
export function renderReleaseList(
|
|
11
|
+
releases: {
|
|
12
|
+
tagName: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
isLatest?: boolean;
|
|
15
|
+
isPrerelease?: boolean;
|
|
16
|
+
isDraft?: boolean;
|
|
17
|
+
publishedAt?: string | null;
|
|
18
|
+
}[],
|
|
19
|
+
): string {
|
|
20
|
+
if (releases.length === 0) {
|
|
21
|
+
return "(no releases)";
|
|
22
|
+
}
|
|
23
|
+
return releases
|
|
24
|
+
.map((release) => {
|
|
25
|
+
const flags = [
|
|
26
|
+
release.isLatest ? "latest" : "",
|
|
27
|
+
release.isPrerelease ? "prerelease" : "",
|
|
28
|
+
release.isDraft ? "draft" : "",
|
|
29
|
+
].filter(Boolean);
|
|
30
|
+
return [
|
|
31
|
+
release.tagName,
|
|
32
|
+
flags.join(","),
|
|
33
|
+
dateOnly(release.publishedAt),
|
|
34
|
+
release.name ?? "",
|
|
35
|
+
].join("\t");
|
|
36
|
+
})
|
|
37
|
+
.join("\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 一行一个运行:`id 状态 结论 工作流 分支 事件 创建时间 链接`。 */
|
|
41
|
+
export function renderRunList(
|
|
42
|
+
runs: {
|
|
43
|
+
databaseId: number;
|
|
44
|
+
status?: string;
|
|
45
|
+
conclusion?: string | null;
|
|
46
|
+
workflowName?: string;
|
|
47
|
+
headBranch?: string;
|
|
48
|
+
event?: string;
|
|
49
|
+
createdAt?: string;
|
|
50
|
+
url?: string;
|
|
51
|
+
}[],
|
|
52
|
+
): string {
|
|
53
|
+
if (runs.length === 0) {
|
|
54
|
+
return "(no workflow runs)";
|
|
55
|
+
}
|
|
56
|
+
return runs
|
|
57
|
+
.map((run) =>
|
|
58
|
+
[
|
|
59
|
+
String(run.databaseId),
|
|
60
|
+
run.status ?? "",
|
|
61
|
+
run.conclusion ?? "",
|
|
62
|
+
run.workflowName ?? "",
|
|
63
|
+
run.headBranch ?? "",
|
|
64
|
+
run.event ?? "",
|
|
65
|
+
dateOnly(run.createdAt),
|
|
66
|
+
run.url ?? "",
|
|
67
|
+
].join("\t"),
|
|
68
|
+
)
|
|
69
|
+
.join("\n");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 仓库概览:标题行 + 一行事实 + 一行链接 + 一行日期/许可。 */
|
|
73
|
+
export function renderRepoView(repo: {
|
|
74
|
+
nameWithOwner?: string;
|
|
75
|
+
description?: string;
|
|
76
|
+
url?: string;
|
|
77
|
+
visibility?: string;
|
|
78
|
+
primaryLanguage?: { name: string } | null;
|
|
79
|
+
defaultBranchRef?: { name: string } | null;
|
|
80
|
+
stargazerCount?: number;
|
|
81
|
+
forkCount?: number;
|
|
82
|
+
issues?: { totalCount: number } | null;
|
|
83
|
+
pullRequests?: { totalCount: number } | null;
|
|
84
|
+
licenseInfo?: { name: string } | null;
|
|
85
|
+
pushedAt?: string;
|
|
86
|
+
createdAt?: string;
|
|
87
|
+
}): string {
|
|
88
|
+
const facts = [
|
|
89
|
+
repo.visibility?.toLowerCase(),
|
|
90
|
+
repo.primaryLanguage?.name,
|
|
91
|
+
repo.defaultBranchRef && `default branch ${repo.defaultBranchRef.name}`,
|
|
92
|
+
repo.stargazerCount !== undefined && `stars ${repo.stargazerCount}`,
|
|
93
|
+
repo.forkCount !== undefined && `forks ${repo.forkCount}`,
|
|
94
|
+
repo.issues && `open issues ${repo.issues.totalCount}`,
|
|
95
|
+
repo.pullRequests && `open PRs ${repo.pullRequests.totalCount}`,
|
|
96
|
+
].filter(Boolean);
|
|
97
|
+
return [
|
|
98
|
+
[repo.nameWithOwner, repo.description].filter(Boolean).join(" — "),
|
|
99
|
+
facts.join(" · "),
|
|
100
|
+
repo.url ?? "",
|
|
101
|
+
[
|
|
102
|
+
repo.pushedAt && `pushed ${dateOnly(repo.pushedAt)}`,
|
|
103
|
+
repo.createdAt && `created ${dateOnly(repo.createdAt)}`,
|
|
104
|
+
repo.licenseInfo?.name,
|
|
105
|
+
]
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
.join(" · "),
|
|
108
|
+
]
|
|
109
|
+
.filter((line) => line !== "")
|
|
110
|
+
.join("\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** release 详情:标题、事实、资产清单、正文。 */
|
|
114
|
+
export function renderReleaseView(release: {
|
|
115
|
+
tagName?: string;
|
|
116
|
+
name?: string;
|
|
117
|
+
url?: string;
|
|
118
|
+
isLatest?: boolean;
|
|
119
|
+
isPrerelease?: boolean;
|
|
120
|
+
isDraft?: boolean;
|
|
121
|
+
publishedAt?: string | null;
|
|
122
|
+
createdAt?: string;
|
|
123
|
+
author?: { login?: string } | null;
|
|
124
|
+
assets?: { name?: string; size?: number; downloadCount?: number }[];
|
|
125
|
+
body?: string;
|
|
126
|
+
}): string {
|
|
127
|
+
const flags = [
|
|
128
|
+
release.isLatest && "latest",
|
|
129
|
+
release.isPrerelease && "prerelease",
|
|
130
|
+
release.isDraft && "draft",
|
|
131
|
+
]
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.join(",");
|
|
134
|
+
const assets = release.assets ?? [];
|
|
135
|
+
const lines = [
|
|
136
|
+
[release.tagName, release.name].filter(Boolean).join(" — "),
|
|
137
|
+
[
|
|
138
|
+
`published ${dateOnly(release.publishedAt ?? release.createdAt)}`,
|
|
139
|
+
flags,
|
|
140
|
+
release.author?.login && `by ${release.author.login}`,
|
|
141
|
+
release.url,
|
|
142
|
+
]
|
|
143
|
+
.filter(Boolean)
|
|
144
|
+
.join(" · "),
|
|
145
|
+
];
|
|
146
|
+
if (assets.length > 0) {
|
|
147
|
+
lines.push(`assets (${assets.length}):`);
|
|
148
|
+
for (const asset of assets) {
|
|
149
|
+
lines.push(
|
|
150
|
+
`- ${asset.name ?? ""} ${asset.size ?? 0} bytes, ${asset.downloadCount ?? 0} downloads`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const body = release.body?.trim() ?? "";
|
|
155
|
+
if (body !== "") {
|
|
156
|
+
lines.push("", body);
|
|
157
|
+
}
|
|
158
|
+
return lines.join("\n");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface DiffFileStat {
|
|
162
|
+
path: string;
|
|
163
|
+
additions: number;
|
|
164
|
+
deletions: number;
|
|
165
|
+
oldPath?: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface DiffStats {
|
|
169
|
+
files: DiffFileStat[];
|
|
170
|
+
additions: number;
|
|
171
|
+
deletions: number;
|
|
172
|
+
changedFiles: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** diff 里的路径:去掉 `a/` / `b/` 前缀与 git 给带空格路径加的双引号;`/dev/null` 给空串。 */
|
|
176
|
+
function stripDiffPrefix(raw: string): string {
|
|
177
|
+
const trimmed = raw.trimEnd();
|
|
178
|
+
if (trimmed === "/dev/null") {
|
|
179
|
+
return "";
|
|
180
|
+
}
|
|
181
|
+
return trimmed
|
|
182
|
+
.replace(/^[ab]\//, "")
|
|
183
|
+
.replace(/^"/, "")
|
|
184
|
+
.replace(/"$/, "");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** `diff --git a/x b/y` 的两个路径(路径带空格时 git 会加双引号)。 */
|
|
188
|
+
function parseDiffHeader(rest: string): { oldPath: string; newPath: string } | undefined {
|
|
189
|
+
const match = /^(?:"a\/(.+)"|a\/(\S+)) (?:"b\/(.+)"|b\/(\S+))$/.exec(rest.trim());
|
|
190
|
+
if (!match) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
// 两组可选路径各有一个未参与匹配的捕获组(运行期是 undefined,类型上是 string),用 `||`
|
|
194
|
+
return { oldPath: match[1] || match[2], newPath: match[3] || match[4] };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 从 unified diff 文本里解析变更统计。
|
|
199
|
+
*
|
|
200
|
+
* `gh pr diff` 没有 `--json`,因此载荷只能从这里来。解析的边界都是 diff 的结构标记
|
|
201
|
+
* (`diff --git` / `---` / `+++` / `@@`),不依赖具体语言或文件内容:二进制文件与纯
|
|
202
|
+
* 模式变更(没有 `---` / `+++`)按 0 行变更计,路径取自 `diff --git` 头。
|
|
203
|
+
*/
|
|
204
|
+
export function parseDiffStats(diff: string): DiffStats {
|
|
205
|
+
const files: DiffFileStat[] = [];
|
|
206
|
+
let current: DiffFileStat | undefined;
|
|
207
|
+
/** `--- ` 给出的旧路径,等 `+++ ` 到了再决定它是不是重命名。 */
|
|
208
|
+
let oldPathFromHunk = "";
|
|
209
|
+
let inHunk = false;
|
|
210
|
+
|
|
211
|
+
for (const line of diff.split("\n")) {
|
|
212
|
+
if (line.startsWith("diff --git ")) {
|
|
213
|
+
if (current) {
|
|
214
|
+
files.push(current);
|
|
215
|
+
}
|
|
216
|
+
const header = parseDiffHeader(line.slice("diff --git ".length));
|
|
217
|
+
current = { path: header?.newPath ?? "", additions: 0, deletions: 0 };
|
|
218
|
+
oldPathFromHunk = header?.oldPath ?? "";
|
|
219
|
+
inHunk = false;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (current === undefined) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (line.startsWith("--- ")) {
|
|
226
|
+
oldPathFromHunk = stripDiffPrefix(line.slice(4));
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (line.startsWith("+++ ")) {
|
|
230
|
+
const newPath = stripDiffPrefix(line.slice(4));
|
|
231
|
+
// 删除的文件新路径是 /dev/null,此时用旧路径当 path
|
|
232
|
+
current.path = newPath === "" ? oldPathFromHunk : newPath;
|
|
233
|
+
if (oldPathFromHunk !== "" && oldPathFromHunk !== current.path) {
|
|
234
|
+
current.oldPath = oldPathFromHunk;
|
|
235
|
+
}
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (line.startsWith("@@")) {
|
|
239
|
+
inHunk = true;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (!inHunk) {
|
|
243
|
+
// 文件头(index / mode / rename / Binary files …)不算变更行
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (line.startsWith("+")) {
|
|
247
|
+
current.additions += 1;
|
|
248
|
+
} else if (line.startsWith("-")) {
|
|
249
|
+
current.deletions += 1;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (current) {
|
|
253
|
+
files.push(current);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const additions = files.reduce((sum, file) => sum + file.additions, 0);
|
|
257
|
+
const deletions = files.reduce((sum, file) => sum + file.deletions, 0);
|
|
258
|
+
return { files, additions, deletions, changedFiles: files.length };
|
|
259
|
+
}
|
package/src/gh/schemas.ts
CHANGED
|
@@ -228,3 +228,216 @@ export const checksVerdictSchema = Type.Object({
|
|
|
228
228
|
}),
|
|
229
229
|
),
|
|
230
230
|
});
|
|
231
|
+
|
|
232
|
+
// ── 文本类工具改成 --json 之后的载荷 ─────────────────────────────────────────
|
|
233
|
+
//
|
|
234
|
+
// 这批工具的文本由我们自己从 JSON 渲染,因此载荷是 gh 的 JSON(字段名就是 gh 的)加一个
|
|
235
|
+
// `text`(与工具输出一致)。与上面十个透传工具一样,gh 的 JSON 允许额外字段。
|
|
236
|
+
|
|
237
|
+
/** `list-github-issues` / `list-github-prs` 的一行:浏览与搜索两条分支归一后的记录。 */
|
|
238
|
+
export const ghHitSchema = Type.Object(
|
|
239
|
+
{
|
|
240
|
+
number: Type.Number(),
|
|
241
|
+
state: Type.Union([Type.Literal("open"), Type.Literal("closed"), Type.Literal("merged")]),
|
|
242
|
+
title: Type.String(),
|
|
243
|
+
url: Type.String(),
|
|
244
|
+
repo: Type.String({ description: "OWNER/REPO;浏览分支取参数或当前仓库" }),
|
|
245
|
+
author: Type.String(),
|
|
246
|
+
labels: Type.Array(Type.String()),
|
|
247
|
+
milestone: Type.String(),
|
|
248
|
+
assignees: Type.Array(Type.String()),
|
|
249
|
+
comments: Type.Number({ description: "评论数(浏览分支是评论数组的长度)" }),
|
|
250
|
+
createdAt: Type.String({ description: "YYYY-MM-DD" }),
|
|
251
|
+
updatedAt: Type.String({ description: "YYYY-MM-DD" }),
|
|
252
|
+
closedAt: Type.String(),
|
|
253
|
+
mergedAt: Type.String(),
|
|
254
|
+
},
|
|
255
|
+
{ additionalProperties: false },
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
export const ghHitListPayloadSchema = Type.Object(
|
|
259
|
+
{
|
|
260
|
+
text: Type.String({ description: "与工具输出一致的行列表(可能被行数/字节预算截断)" }),
|
|
261
|
+
items: Type.Array(ghHitSchema, { description: "全部命中,不受文本截断影响" }),
|
|
262
|
+
},
|
|
263
|
+
{ additionalProperties: false },
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
/** `gh release list --json` 的一行。 */
|
|
267
|
+
export const ghReleaseSummarySchema = Type.Object(
|
|
268
|
+
{
|
|
269
|
+
tagName: Type.String(),
|
|
270
|
+
name: Type.Optional(Type.String()),
|
|
271
|
+
isLatest: Type.Optional(Type.Boolean()),
|
|
272
|
+
isPrerelease: Type.Optional(Type.Boolean()),
|
|
273
|
+
isDraft: Type.Optional(Type.Boolean()),
|
|
274
|
+
publishedAt: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
275
|
+
createdAt: Type.Optional(Type.String()),
|
|
276
|
+
},
|
|
277
|
+
{ additionalProperties: true },
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
export const ghReleaseListPayloadSchema = Type.Object(
|
|
281
|
+
{
|
|
282
|
+
text: Type.String(),
|
|
283
|
+
releases: Type.Array(ghReleaseSummarySchema),
|
|
284
|
+
},
|
|
285
|
+
{ additionalProperties: false },
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
/** `gh run list` / `gh run view --json` 的共同字段。 */
|
|
289
|
+
export const ghRunSummarySchema = Type.Object(
|
|
290
|
+
{
|
|
291
|
+
databaseId: Type.Number(),
|
|
292
|
+
displayTitle: Type.Optional(Type.String()),
|
|
293
|
+
status: Type.Optional(Type.String()),
|
|
294
|
+
conclusion: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
295
|
+
workflowName: Type.Optional(Type.String()),
|
|
296
|
+
headBranch: Type.Optional(Type.String()),
|
|
297
|
+
event: Type.Optional(Type.String()),
|
|
298
|
+
createdAt: Type.Optional(Type.String()),
|
|
299
|
+
updatedAt: Type.Optional(Type.String()),
|
|
300
|
+
startedAt: Type.Optional(Type.String()),
|
|
301
|
+
url: Type.Optional(Type.String()),
|
|
302
|
+
},
|
|
303
|
+
{ additionalProperties: true },
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
export const ghRunListPayloadSchema = Type.Object(
|
|
307
|
+
{
|
|
308
|
+
text: Type.String(),
|
|
309
|
+
runs: Type.Array(ghRunSummarySchema),
|
|
310
|
+
},
|
|
311
|
+
{ additionalProperties: false },
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
/** `watch-github-run`:文本是 `gh run watch` 的输出,载荷是收尾时查到的运行状态。 */
|
|
315
|
+
export const ghRunPayloadSchema = Type.Object(
|
|
316
|
+
{
|
|
317
|
+
text: Type.String(),
|
|
318
|
+
run: ghRunSummarySchema,
|
|
319
|
+
},
|
|
320
|
+
{ additionalProperties: false },
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
/** `gh repo view --json` 里我们渲染与脚本可能用到的字段。 */
|
|
324
|
+
export const ghRepoViewSchema = Type.Object(
|
|
325
|
+
{
|
|
326
|
+
name: Type.Optional(Type.String()),
|
|
327
|
+
nameWithOwner: Type.Optional(Type.String()),
|
|
328
|
+
description: Type.Optional(Type.String()),
|
|
329
|
+
url: Type.Optional(Type.String()),
|
|
330
|
+
homepageUrl: Type.Optional(Type.String()),
|
|
331
|
+
visibility: Type.Optional(Type.String()),
|
|
332
|
+
isPrivate: Type.Optional(Type.Boolean()),
|
|
333
|
+
isFork: Type.Optional(Type.Boolean()),
|
|
334
|
+
isArchived: Type.Optional(Type.Boolean()),
|
|
335
|
+
stargazerCount: Type.Optional(Type.Number()),
|
|
336
|
+
forkCount: Type.Optional(Type.Number()),
|
|
337
|
+
primaryLanguage: Type.Optional(
|
|
338
|
+
Type.Union([
|
|
339
|
+
Type.Object({ name: Type.String() }, { additionalProperties: true }),
|
|
340
|
+
Type.Null(),
|
|
341
|
+
]),
|
|
342
|
+
),
|
|
343
|
+
defaultBranchRef: Type.Optional(
|
|
344
|
+
Type.Union([
|
|
345
|
+
Type.Object({ name: Type.String() }, { additionalProperties: true }),
|
|
346
|
+
Type.Null(),
|
|
347
|
+
]),
|
|
348
|
+
),
|
|
349
|
+
licenseInfo: Type.Optional(
|
|
350
|
+
Type.Union([
|
|
351
|
+
Type.Object({ name: Type.String() }, { additionalProperties: true }),
|
|
352
|
+
Type.Null(),
|
|
353
|
+
]),
|
|
354
|
+
),
|
|
355
|
+
issues: Type.Optional(
|
|
356
|
+
Type.Union([
|
|
357
|
+
Type.Object({ totalCount: Type.Number() }, { additionalProperties: true }),
|
|
358
|
+
Type.Null(),
|
|
359
|
+
]),
|
|
360
|
+
),
|
|
361
|
+
pullRequests: Type.Optional(
|
|
362
|
+
Type.Union([
|
|
363
|
+
Type.Object({ totalCount: Type.Number() }, { additionalProperties: true }),
|
|
364
|
+
Type.Null(),
|
|
365
|
+
]),
|
|
366
|
+
),
|
|
367
|
+
pushedAt: Type.Optional(Type.String()),
|
|
368
|
+
createdAt: Type.Optional(Type.String()),
|
|
369
|
+
updatedAt: Type.Optional(Type.String()),
|
|
370
|
+
},
|
|
371
|
+
{ additionalProperties: true },
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
export const ghRepoPayloadSchema = Type.Object(
|
|
375
|
+
{
|
|
376
|
+
text: Type.String(),
|
|
377
|
+
repo: ghRepoViewSchema,
|
|
378
|
+
},
|
|
379
|
+
{ additionalProperties: false },
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
/** `gh release view --json` 里我们渲染与脚本可能用到的字段。 */
|
|
383
|
+
export const ghReleaseViewSchema = Type.Object(
|
|
384
|
+
{
|
|
385
|
+
tagName: Type.Optional(Type.String()),
|
|
386
|
+
name: Type.Optional(Type.String()),
|
|
387
|
+
body: Type.Optional(Type.String()),
|
|
388
|
+
url: Type.Optional(Type.String()),
|
|
389
|
+
isDraft: Type.Optional(Type.Boolean()),
|
|
390
|
+
isPrerelease: Type.Optional(Type.Boolean()),
|
|
391
|
+
createdAt: Type.Optional(Type.String()),
|
|
392
|
+
publishedAt: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
393
|
+
targetCommitish: Type.Optional(Type.String()),
|
|
394
|
+
author: Type.Optional(Type.Union([ghActorSchema, Type.Null()])),
|
|
395
|
+
assets: Type.Optional(
|
|
396
|
+
Type.Array(
|
|
397
|
+
Type.Object(
|
|
398
|
+
{
|
|
399
|
+
name: Type.Optional(Type.String()),
|
|
400
|
+
size: Type.Optional(Type.Number()),
|
|
401
|
+
downloadCount: Type.Optional(Type.Number()),
|
|
402
|
+
contentType: Type.Optional(Type.String()),
|
|
403
|
+
state: Type.Optional(Type.String()),
|
|
404
|
+
url: Type.Optional(Type.String()),
|
|
405
|
+
},
|
|
406
|
+
{ additionalProperties: true },
|
|
407
|
+
),
|
|
408
|
+
),
|
|
409
|
+
),
|
|
410
|
+
},
|
|
411
|
+
{ additionalProperties: true },
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
export const ghReleaseViewPayloadSchema = Type.Object(
|
|
415
|
+
{
|
|
416
|
+
text: Type.String(),
|
|
417
|
+
release: ghReleaseViewSchema,
|
|
418
|
+
},
|
|
419
|
+
{ additionalProperties: false },
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
/** `read-github-pr-diff`:文本是原始 diff,载荷是从它解析出的变更统计。 */
|
|
423
|
+
export const ghDiffPayloadSchema = Type.Object(
|
|
424
|
+
{
|
|
425
|
+
text: Type.String(),
|
|
426
|
+
files: Type.Array(
|
|
427
|
+
Type.Object(
|
|
428
|
+
{
|
|
429
|
+
path: Type.String(),
|
|
430
|
+
additions: Type.Number(),
|
|
431
|
+
deletions: Type.Number(),
|
|
432
|
+
/** diff 里的旧路径,重命名/复制时与 `path` 不同。 */
|
|
433
|
+
oldPath: Type.Optional(Type.String()),
|
|
434
|
+
},
|
|
435
|
+
{ additionalProperties: false },
|
|
436
|
+
),
|
|
437
|
+
),
|
|
438
|
+
additions: Type.Number(),
|
|
439
|
+
deletions: Type.Number(),
|
|
440
|
+
changedFiles: Type.Number(),
|
|
441
|
+
},
|
|
442
|
+
{ additionalProperties: false },
|
|
443
|
+
);
|