@unifan/pi-commit-zh 1.0.7

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/index.ts ADDED
@@ -0,0 +1,216 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ commitWithMsg,
4
+ getChangedFiles,
5
+ getStagedDiff,
6
+ getUnstagedDiff,
7
+ isGitRepo,
8
+ pushCurrentBranch,
9
+ stageAll,
10
+ } from "./src/git.js";
11
+ import { COMMIT_SYSTEM_PROMPT } from "./src/prompt.ts";
12
+
13
+ interface ParsedCommitArgs {
14
+ yes: boolean;
15
+ stageAll: boolean;
16
+ hint?: string;
17
+ }
18
+
19
+ function parseArgs(raw: string): ParsedCommitArgs {
20
+ const tokens = raw.trim().split(/\s+/).filter(Boolean);
21
+ let yes = false;
22
+ let shouldStageAll = false;
23
+ const hintParts: string[] = [];
24
+
25
+ for (const t of tokens) {
26
+ if (t === "-y" || t === "--yes") {
27
+ yes = true;
28
+ } else if (t === "-a" || t === "--all") {
29
+ shouldStageAll = true;
30
+ } else {
31
+ hintParts.push(t);
32
+ }
33
+ }
34
+
35
+ return {
36
+ yes,
37
+ stageAll: shouldStageAll,
38
+ hint: hintParts.join(" ").trim() || undefined,
39
+ };
40
+ }
41
+
42
+ async function generateCommitMessage(
43
+ ctx: ExtensionCommandContext,
44
+ diff: string,
45
+ changedFiles: string[],
46
+ userHint?: string,
47
+ ): Promise<string> {
48
+ const truncatedDiff = diff.length > 30000 ? diff.slice(0, 30000) + "\n\n... (diff已截断)" : diff;
49
+ const userPrompt = [
50
+ `## 修改的文件清单 (${changedFiles.length} 个文件):`,
51
+ changedFiles.map((f) => `- ${f}`).join("\n"),
52
+ "",
53
+ userHint ? `## 用户额外补充说明:\n${userHint}\n` : "",
54
+ "## Git Diff 变动详情:",
55
+ "```diff",
56
+ truncatedDiff,
57
+ "```",
58
+ ]
59
+ .filter(Boolean)
60
+ .join("\n");
61
+
62
+ if (ctx.model && ctx.modelRegistry) {
63
+ try {
64
+ const provider = ctx.modelRegistry.getProvider(ctx.model.provider);
65
+ if (provider) {
66
+ const response = await provider
67
+ .streamSimple(
68
+ ctx.model,
69
+ {
70
+ systemPrompt: COMMIT_SYSTEM_PROMPT,
71
+ messages: [
72
+ {
73
+ role: "user",
74
+ content: [{ type: "text", text: userPrompt }],
75
+ timestamp: Date.now(),
76
+ },
77
+ ],
78
+ },
79
+ { maxTokens: 800 },
80
+ )
81
+ .result();
82
+
83
+ const text = response.content
84
+ ?.map((c) => (c.type === "text" ? c.text : ""))
85
+ .join("")
86
+ .trim();
87
+
88
+ if (text) {
89
+ // Clean any unexpected code blocks
90
+ return text.replace(/^```[a-zA-Z]*\n?/, "").replace(/\n?```$/, "").trim();
91
+ }
92
+ }
93
+ } catch {
94
+ /* fallback below */
95
+ }
96
+ }
97
+
98
+ // Simple heuristic fallback
99
+ const firstFile = changedFiles[0] ?? "core";
100
+ const scope = firstFile.split(/[/\\]/)[0] || "core";
101
+ return `chore(${scope}): 更新代码与相关配置\n\n- 更新了 ${changedFiles.length} 个文件`;
102
+ }
103
+
104
+ export default function (pi: ExtensionAPI) {
105
+ const handleCommitCommand = async (args: string, ctx: ExtensionCommandContext, andPush = false) => {
106
+ const notify = (msg: string, level: "info" | "warning" | "error" = "info") => {
107
+ if (ctx.hasUI) ctx.ui.notify(msg, level);
108
+ else console.log(`pi-commit: ${msg}`);
109
+ };
110
+
111
+ if (!(await isGitRepo(ctx.cwd))) {
112
+ notify("当前目录不是 Git 仓库,无法执行 commit。", "error");
113
+ return;
114
+ }
115
+
116
+ const parsed = parseArgs(args);
117
+ let stagedDiff = await getStagedDiff(ctx.cwd);
118
+ const unstagedDiff = await getUnstagedDiff(ctx.cwd);
119
+ const changedFiles = await getChangedFiles(ctx.cwd);
120
+
121
+ if (!stagedDiff && !unstagedDiff) {
122
+ notify("当前工作区没有任何修改,无需提交。", "info");
123
+ return;
124
+ }
125
+
126
+ if (!stagedDiff && unstagedDiff) {
127
+ notify("未发现暂存区文件,已自动暂存全部修改 (git add -A)...", "info");
128
+ await stageAll(ctx.cwd);
129
+ stagedDiff = await getStagedDiff(ctx.cwd);
130
+ } else if (parsed.stageAll) {
131
+ await stageAll(ctx.cwd);
132
+ stagedDiff = await getStagedDiff(ctx.cwd);
133
+ }
134
+
135
+ notify("正在深度分析代码改动并生成中文 Commit Message...", "info");
136
+ const commitMessage = await generateCommitMessage(ctx, stagedDiff || unstagedDiff, changedFiles, parsed.hint);
137
+
138
+ let finalMessage = commitMessage;
139
+
140
+ if (!parsed.yes && ctx.hasUI) {
141
+ const choice = await ctx.ui.select(
142
+ `✨ AI 生成的提交信息:\n\n${commitMessage}\n\n请选择操作:`,
143
+ [
144
+ { label: "✅ 立即以此信息提交 (Commit)", value: "commit" },
145
+ { label: "📝 编辑后再提交 (Edit & Commit)", value: "edit" },
146
+ { label: "❌ 取消提交 (Cancel)", value: "cancel" },
147
+ ],
148
+ );
149
+
150
+ if (!choice || choice === "cancel") {
151
+ notify("已取消本次提交。", "info");
152
+ return;
153
+ }
154
+
155
+ if (choice === "edit") {
156
+ const edited = await ctx.ui.editor("编辑 Commit Message", commitMessage);
157
+ if (!edited || !edited.trim()) {
158
+ notify("提交信息为空,已取消提交。", "warning");
159
+ return;
160
+ }
161
+ finalMessage = edited.trim();
162
+ }
163
+ }
164
+
165
+ const commitRes = await commitWithMsg(ctx.cwd, finalMessage);
166
+ if (!commitRes.ok) {
167
+ notify(`Git 提交失败: ${commitRes.output}`, "error");
168
+ return;
169
+ }
170
+
171
+ const firstLine = finalMessage.split("\n")[0];
172
+ notify(`✅ 成功提交: ${firstLine}`, "info");
173
+
174
+ let pushText = "";
175
+ if (andPush) {
176
+ notify("正在推送到远端仓库 (git push)...", "info");
177
+ const pushRes = await pushCurrentBranch(ctx.cwd);
178
+ if (pushRes.ok) {
179
+ notify("🚀 成功推送到远端仓库!", "info");
180
+ pushText = "\n\n🚀 **已自动推送到远端分支**";
181
+ } else {
182
+ notify(`⚠️ 推送失败: ${pushRes.output}`, "warning");
183
+ pushText = `\n\n⚠️ **推送到远端失败**: ${pushRes.output}`;
184
+ }
185
+ }
186
+
187
+ pi.sendMessage({
188
+ customType: "pi-commit-result",
189
+ content: `### 📦 Git 提交完成\n\n\`\`\`text\n${finalMessage}\n\`\`\`${pushText}`,
190
+ display: true,
191
+ });
192
+ };
193
+
194
+ pi.registerCommand("commit", {
195
+ description: "智能 Git 提交助手:自动分析 diff 生成标准中文 Conventional Commit 并提交 (-y 直接提交, -a 暂存全部)",
196
+ getArgumentCompletions: (prefix: string) => {
197
+ const options = [
198
+ { value: "-y", label: "-y / --yes", description: "直接提交无需二次确认" },
199
+ { value: "-a", label: "-a / --all", description: "自动暂存全部修改 (git add -A)" },
200
+ ];
201
+ const trimmed = prefix.trimStart();
202
+ if (!trimmed) return options;
203
+ return options.filter((o) => o.value.startsWith(trimmed));
204
+ },
205
+ handler: async (args, ctx) => {
206
+ await handleCommitCommand(args, ctx, false);
207
+ },
208
+ });
209
+
210
+ pi.registerCommand("commit-push", {
211
+ description: "智能 Git 提交并推流:生成标准中文 Commit 后自动执行 git push",
212
+ handler: async (args, ctx) => {
213
+ await handleCommitCommand(args, ctx, true);
214
+ },
215
+ });
216
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@unifan/pi-commit-zh",
3
+ "version": "1.0.7",
4
+ "description": "Pi 智能 Git 提交助手(规范化 Conventional Commits 纯中文版,支持一键提审与推流)",
5
+ "type": "module",
6
+ "main": "index.ts",
7
+ "author": "821869798",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/821869798/pi-unifan-zh.git"
12
+ },
13
+ "keywords": [
14
+ "pi-package",
15
+ "pi-extension",
16
+ "git",
17
+ "commit",
18
+ "conventional-commits",
19
+ "chinese"
20
+ ],
21
+ "peerDependencies": {
22
+ "@earendil-works/pi-ai": ">=0.74.0 <1.0.0",
23
+ "@earendil-works/pi-coding-agent": ">=0.74.0 <1.0.0"
24
+ },
25
+ "pi": {
26
+ "extensions": [
27
+ "./index.ts"
28
+ ]
29
+ }
30
+ }
package/src/git.ts ADDED
@@ -0,0 +1,61 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+
4
+ const execFileAsync = promisify(execFile);
5
+
6
+ export async function runGit(cwd: string, args: string[]): Promise<{ stdout: string; stderr: string; ok: boolean }> {
7
+ try {
8
+ const res = await execFileAsync("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 });
9
+ return { stdout: res.stdout, stderr: res.stderr, ok: true };
10
+ } catch (err: any) {
11
+ return {
12
+ stdout: err?.stdout ?? "",
13
+ stderr: err?.stderr ?? err?.message ?? String(err),
14
+ ok: false,
15
+ };
16
+ }
17
+ }
18
+
19
+ export async function isGitRepo(cwd: string): Promise<boolean> {
20
+ const res = await runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
21
+ return res.ok && res.stdout.trim() === "true";
22
+ }
23
+
24
+ export async function getGitStatus(cwd: string): Promise<string> {
25
+ const res = await runGit(cwd, ["status", "--porcelain"]);
26
+ return res.ok ? res.stdout.trim() : "";
27
+ }
28
+
29
+ export async function getStagedDiff(cwd: string): Promise<string> {
30
+ const res = await runGit(cwd, ["diff", "--cached"]);
31
+ return res.ok ? res.stdout : "";
32
+ }
33
+
34
+ export async function getUnstagedDiff(cwd: string): Promise<string> {
35
+ const res = await runGit(cwd, ["diff"]);
36
+ return res.ok ? res.stdout : "";
37
+ }
38
+
39
+ export async function getChangedFiles(cwd: string): Promise<string[]> {
40
+ const res = await runGit(cwd, ["status", "--porcelain"]);
41
+ if (!res.ok || !res.stdout.trim()) return [];
42
+ return res.stdout
43
+ .split("\n")
44
+ .map((line) => line.trim().slice(3).trim())
45
+ .filter(Boolean);
46
+ }
47
+
48
+ export async function stageAll(cwd: string): Promise<boolean> {
49
+ const res = await runGit(cwd, ["add", "-A"]);
50
+ return res.ok;
51
+ }
52
+
53
+ export async function commitWithMsg(cwd: string, message: string): Promise<{ ok: boolean; output: string }> {
54
+ const res = await runGit(cwd, ["commit", "-m", message]);
55
+ return { ok: res.ok, output: res.ok ? res.stdout : res.stderr };
56
+ }
57
+
58
+ export async function pushCurrentBranch(cwd: string): Promise<{ ok: boolean; output: string }> {
59
+ const res = await runGit(cwd, ["push"]);
60
+ return { ok: res.ok, output: res.ok ? res.stdout : res.stderr };
61
+ }
package/src/prompt.ts ADDED
@@ -0,0 +1,24 @@
1
+ export const COMMIT_SYSTEM_PROMPT = `你是专业的 Git 提交信息生成专家(Conventional Commits 中文版)。
2
+ 你的唯一任务是根据提供的 Git Diff 变动和修改文件列表,生成规范、准确、地道的中文 Commit Message。
3
+
4
+ ## 严格遵循的标准(Conventional Commits 1.0.0):
5
+ 1. 格式:<type>(<scope>): <中文描述>
6
+ - type 必须为标准英文类型之一:
7
+ • feat: 新增功能/新模块/新特性
8
+ • fix: 修复Bug/缺陷
9
+ • perf: 性能优化/降低GC/提高帧率
10
+ • refactor: 代码重构/结构优化
11
+ • docs: 文档/注释更新
12
+ • style: 格式调整/无逻辑影响
13
+ • test: 单元测试/基准测试
14
+ • chore: 构建/依赖/配置杂项
15
+ - scope 为具体修改的业务模块名(如:combat/战斗、session/会话、review/审查、ui/界面、network/网络、core/核心)
16
+ - 中文描述:50个汉字以内,动词开头,言简意赅,末尾严禁加句号。
17
+
18
+ 2. 若修改较为复杂,可附带简要中文正文列表(Body):
19
+ - 核心改动点 1
20
+ - 核心改动点 2
21
+
22
+ 3. 语言约束:所有标题描述与正文说明必须且只能使用纯正中文,严禁长句英文。
23
+
24
+ 4. 输出格式:直接输出生成的 Commit 信息文本(首行标题,空行后跟正文),不要附加任何额外的 markdown 代码块或寒暄。`;