@ckpack/ai-commit 1.1.0 → 1.2.0
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 +27 -4
- package/dist/index.mjs +104 -24
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# ai-commit
|
|
2
2
|
|
|
3
|
-
使用 Codex 根据 git diff 生成符合 Conventional Commits 的提交信息。
|
|
3
|
+
使用 Codex 或 Gemini CLI 根据 git diff 生成符合 Conventional Commits 的提交信息。
|
|
4
4
|
|
|
5
5
|
## 功能
|
|
6
6
|
|
|
7
7
|
- 读取暂存区或工作区 diff(优先暂存区)
|
|
8
8
|
- 自动忽略常见锁文件与日志文件
|
|
9
9
|
- 生成单行 commit message,长度不超过 50 个字符
|
|
10
|
-
-
|
|
10
|
+
- 支持 `codex`(默认)与 `gemini` 子命令
|
|
11
|
+
- AI 执行失败时自动回退为本地摘要
|
|
12
|
+
- 支持 debug 模式输出关键诊断信息(输出到 stderr)
|
|
11
13
|
|
|
12
14
|
## 安装
|
|
13
15
|
|
|
@@ -25,13 +27,28 @@ pnpm add -g @ckpack/ai-commit
|
|
|
25
27
|
|
|
26
28
|
在包含变更的 git 仓库中运行:
|
|
27
29
|
|
|
30
|
+
```bash
|
|
31
|
+
ai-commit
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`ai-commit` 默认等同于 `ai-commit codex`,你也可以显式指定:
|
|
35
|
+
|
|
28
36
|
```bash
|
|
29
37
|
ai-commit codex
|
|
38
|
+
ai-commit gemini
|
|
30
39
|
```
|
|
31
40
|
|
|
32
|
-
|
|
41
|
+
调试模式:
|
|
33
42
|
|
|
43
|
+
```bash
|
|
44
|
+
ai-commit --debug
|
|
45
|
+
ai-commit codex --debug
|
|
46
|
+
ai-commit gemini --debug
|
|
34
47
|
```
|
|
48
|
+
|
|
49
|
+
输出示例:
|
|
50
|
+
|
|
51
|
+
```text
|
|
35
52
|
feat: add commit message generator
|
|
36
53
|
```
|
|
37
54
|
|
|
@@ -39,17 +56,23 @@ feat: add commit message generator
|
|
|
39
56
|
|
|
40
57
|
```bash
|
|
41
58
|
pnpm dev -- codex
|
|
59
|
+
pnpm dev -- gemini
|
|
42
60
|
```
|
|
43
61
|
|
|
44
62
|
## 环境变量
|
|
45
63
|
|
|
46
64
|
- `CODEX_BIN`:Codex 可执行文件名,默认 `codex`
|
|
47
65
|
- `CODEX_ARGS`:传给 Codex 的参数,默认 `exec`
|
|
66
|
+
- `GEMINI_BIN`:Gemini 可执行文件名,默认 `gemini`
|
|
67
|
+
- `GEMINI_ARGS`:传给 Gemini 的参数,默认 `-p`
|
|
68
|
+
- `AI_COMMIT_DEBUG`:开启调试日志(`1/true/on` 开启,`0/false/off/no` 关闭)
|
|
48
69
|
|
|
49
70
|
示例:
|
|
50
71
|
|
|
51
72
|
```bash
|
|
52
73
|
CODEX_BIN=codex CODEX_ARGS="exec" ai-commit codex
|
|
74
|
+
GEMINI_BIN=gemini GEMINI_ARGS="-p" ai-commit gemini
|
|
75
|
+
AI_COMMIT_DEBUG=1 ai-commit codex
|
|
53
76
|
```
|
|
54
77
|
|
|
55
78
|
## 生成逻辑
|
|
@@ -58,7 +81,7 @@ CODEX_BIN=codex CODEX_ARGS="exec" ai-commit codex
|
|
|
58
81
|
- 默认忽略:`**/*.log`、`**/pnpm-lock.yaml`、`**/package-lock.json`、`**/yarn.lock`
|
|
59
82
|
- 生成的消息需匹配 Conventional Commits:
|
|
60
83
|
`type(scope?): description`,描述不超过 50 个字符
|
|
61
|
-
- Codex 返回异常或不符合规范时,回退为 `chore: update <files>`
|
|
84
|
+
- Codex / Gemini 返回异常或不符合规范时,回退为 `chore: update <files>`
|
|
62
85
|
|
|
63
86
|
## 开发
|
|
64
87
|
|
package/dist/index.mjs
CHANGED
|
@@ -5,12 +5,7 @@ import process from "node:process";
|
|
|
5
5
|
import { consola } from "consola";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
|
-
//#region src/command/
|
|
9
|
-
function codex(program$1) {
|
|
10
|
-
program$1.command("codex", { isDefault: true }).action(() => {
|
|
11
|
-
runCodexCommand();
|
|
12
|
-
});
|
|
13
|
-
}
|
|
8
|
+
//#region src/command/provider.ts
|
|
14
9
|
function runCommand(command, args, options = {}) {
|
|
15
10
|
const result = spawnSync(command, args, {
|
|
16
11
|
encoding: "utf-8",
|
|
@@ -23,6 +18,34 @@ function runCommand(command, args, options = {}) {
|
|
|
23
18
|
error: result.error
|
|
24
19
|
};
|
|
25
20
|
}
|
|
21
|
+
function debugLog(enabled, message) {
|
|
22
|
+
if (!enabled) return;
|
|
23
|
+
consola.info(`[commit-msg][debug] ${message}`);
|
|
24
|
+
}
|
|
25
|
+
function formatShellArg(value) {
|
|
26
|
+
if (!value) return "\"\"";
|
|
27
|
+
return /[^\w@%+=:,./-]/.test(value) ? JSON.stringify(value) : value;
|
|
28
|
+
}
|
|
29
|
+
function formatCommandPreview(bin, args, options) {
|
|
30
|
+
const base = [bin, ...args].map(formatShellArg).join(" ");
|
|
31
|
+
if (options.useStdin) return `${base} <stdin:${options.promptChars} chars>`;
|
|
32
|
+
return `${base} <prompt:${options.promptChars} chars>`;
|
|
33
|
+
}
|
|
34
|
+
function parseEnvFlag(value) {
|
|
35
|
+
if (!value) return false;
|
|
36
|
+
return ![
|
|
37
|
+
"0",
|
|
38
|
+
"false",
|
|
39
|
+
"off",
|
|
40
|
+
"no"
|
|
41
|
+
].includes(value.trim().toLowerCase());
|
|
42
|
+
}
|
|
43
|
+
function hasDebugArgv(argv = process.argv.slice(2)) {
|
|
44
|
+
return argv.includes("--debug") || argv.includes("-d");
|
|
45
|
+
}
|
|
46
|
+
function resolveDebugMode(value) {
|
|
47
|
+
return Boolean(value) || hasDebugArgv() || parseEnvFlag(process.env.AI_COMMIT_DEBUG);
|
|
48
|
+
}
|
|
26
49
|
function ensureNonEmptyDiff(options) {
|
|
27
50
|
const ignores = (options?.ignores ?? [
|
|
28
51
|
"**/*.log",
|
|
@@ -41,9 +64,15 @@ function ensureNonEmptyDiff(options) {
|
|
|
41
64
|
"--staged",
|
|
42
65
|
...ignores
|
|
43
66
|
]).stdout.trim();
|
|
44
|
-
if (stagedDiff) return
|
|
67
|
+
if (stagedDiff) return {
|
|
68
|
+
diff: stagedDiff,
|
|
69
|
+
source: "staged"
|
|
70
|
+
};
|
|
45
71
|
const workingDiff = runCommand("git", [...baseArgs, ...ignores]).stdout.trim();
|
|
46
|
-
if (workingDiff) return
|
|
72
|
+
if (workingDiff) return {
|
|
73
|
+
diff: workingDiff,
|
|
74
|
+
source: "working"
|
|
75
|
+
};
|
|
47
76
|
throw new Error("No changes detected. Stage changes or modify files before generating a commit message.");
|
|
48
77
|
}
|
|
49
78
|
function buildPrompt(diff) {
|
|
@@ -60,7 +89,7 @@ function sanitizeMessage(raw) {
|
|
|
60
89
|
return (raw.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "").replace(/\s+/g, " ").trim();
|
|
61
90
|
}
|
|
62
91
|
function isConventional(msg) {
|
|
63
|
-
return
|
|
92
|
+
return new RegExp(`^(revert: )?(${[
|
|
64
93
|
"feat",
|
|
65
94
|
"fix",
|
|
66
95
|
"docs",
|
|
@@ -76,52 +105,103 @@ function isConventional(msg) {
|
|
|
76
105
|
"types",
|
|
77
106
|
"wip",
|
|
78
107
|
"release"
|
|
79
|
-
].join("|")})(\\(.+\\))?: .{1,50}$`)
|
|
108
|
+
].join("|")})(\\(.+\\))?: .{1,50}$`).test(msg);
|
|
80
109
|
}
|
|
81
|
-
function
|
|
82
|
-
const bin = process.env.
|
|
83
|
-
const args = process.env.
|
|
84
|
-
const
|
|
110
|
+
function tryGenerateMessage(prompt, config, options) {
|
|
111
|
+
const bin = process.env[config.binEnv] || config.defaultBin;
|
|
112
|
+
const args = process.env[config.argsEnv]?.split(/\s+/).filter(Boolean) ?? config.defaultArgs;
|
|
113
|
+
const useStdin = args.includes("-");
|
|
114
|
+
debugLog(options.debug, `provider=${config.providerName} bin=${bin}`);
|
|
115
|
+
debugLog(options.debug, `args=${JSON.stringify(args)} stdin=${useStdin ? "yes" : "no"} promptChars=${prompt.length}`);
|
|
116
|
+
debugLog(options.debug, `command=${formatCommandPreview(bin, args, {
|
|
117
|
+
useStdin,
|
|
118
|
+
promptChars: prompt.length
|
|
119
|
+
})}`);
|
|
120
|
+
const result = useStdin ? runCommand(bin, args, { input: prompt }) : runCommand(bin, [...args, prompt]);
|
|
121
|
+
debugLog(options.debug, `exitStatus=${String(result.status)} stdoutChars=${result.stdout.length} stderrChars=${result.stderr.length}`);
|
|
85
122
|
if (result.status !== 0) {
|
|
86
|
-
if (result.error)
|
|
87
|
-
else if (result.stderr.trim())
|
|
123
|
+
if (result.error) consola.warn(`[commit-msg] ${config.providerName} execution failed: ${result.error.message}`);
|
|
124
|
+
else if (result.stderr.trim()) consola.warn(`[commit-msg] ${config.providerName} stderr: ${result.stderr.trim()}`);
|
|
88
125
|
return null;
|
|
89
126
|
}
|
|
90
|
-
|
|
127
|
+
const msg = sanitizeMessage(result.stdout);
|
|
128
|
+
debugLog(options.debug, `modelMessage=${msg || "<empty>"}`);
|
|
129
|
+
return msg || null;
|
|
91
130
|
}
|
|
92
131
|
function fallbackMessage() {
|
|
93
132
|
const files = runCommand("git", ["status", "--short"]).stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
94
133
|
if (files.length === 0) return "chore: update changes";
|
|
95
134
|
return `chore: update ${files.slice(0, 3).map((line) => line.replace(/^\S+\s+/, "")).join(", ")}${files.length > 3 ? ` and ${files.length - 3} more` : ""}`;
|
|
96
135
|
}
|
|
97
|
-
function
|
|
136
|
+
function runProviderCommand(config, options) {
|
|
98
137
|
try {
|
|
99
|
-
const
|
|
100
|
-
|
|
138
|
+
const { diff, source } = ensureNonEmptyDiff();
|
|
139
|
+
debugLog(options.debug, `diffSource=${source} diffChars=${diff.length}`);
|
|
140
|
+
const candidate = sanitizeMessage(tryGenerateMessage(buildPrompt(diff), config, options) ?? "");
|
|
141
|
+
const valid = isConventional(candidate);
|
|
142
|
+
const message = valid ? candidate : fallbackMessage();
|
|
143
|
+
debugLog(options.debug, `candidate=${candidate || "<empty>"}`);
|
|
144
|
+
debugLog(options.debug, `isConventional=${valid ? "yes" : "no"}`);
|
|
145
|
+
debugLog(options.debug, `finalMessage=${message}`);
|
|
101
146
|
consola.log(message);
|
|
102
147
|
} catch (error) {
|
|
103
148
|
const fallback = fallbackMessage();
|
|
149
|
+
debugLog(options.debug, `error=${error instanceof Error ? error.message : String(error)}`);
|
|
150
|
+
debugLog(options.debug, `finalMessage=${fallback}`);
|
|
104
151
|
consola.warn(`[commit-msg] ${error instanceof Error ? error.message : String(error)}. Using fallback message.`);
|
|
105
152
|
consola.log(fallback);
|
|
106
153
|
}
|
|
107
154
|
}
|
|
155
|
+
function addProviderCommand(program, config) {
|
|
156
|
+
program.command(config.name, { isDefault: config.isDefault }).option("-d, --debug", "print debug information").action(function() {
|
|
157
|
+
runProviderCommand(config, { debug: resolveDebugMode(this.opts().debug) });
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/command/codex.ts
|
|
163
|
+
function codex(program) {
|
|
164
|
+
addProviderCommand(program, {
|
|
165
|
+
name: "codex",
|
|
166
|
+
isDefault: true,
|
|
167
|
+
providerName: "codex",
|
|
168
|
+
binEnv: "CODEX_BIN",
|
|
169
|
+
argsEnv: "CODEX_ARGS",
|
|
170
|
+
defaultBin: "codex",
|
|
171
|
+
defaultArgs: ["exec"]
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/command/gemini.ts
|
|
177
|
+
function gemini(program) {
|
|
178
|
+
addProviderCommand(program, {
|
|
179
|
+
name: "gemini",
|
|
180
|
+
providerName: "gemini",
|
|
181
|
+
binEnv: "GEMINI_BIN",
|
|
182
|
+
argsEnv: "GEMINI_ARGS",
|
|
183
|
+
defaultBin: "gemini",
|
|
184
|
+
defaultArgs: ["-p"]
|
|
185
|
+
});
|
|
186
|
+
}
|
|
108
187
|
|
|
109
188
|
//#endregion
|
|
110
189
|
//#region src/command/index.ts
|
|
111
|
-
function initCommand(program
|
|
112
|
-
codex(program
|
|
190
|
+
function initCommand(program) {
|
|
191
|
+
codex(program);
|
|
192
|
+
gemini(program);
|
|
113
193
|
}
|
|
114
194
|
|
|
115
195
|
//#endregion
|
|
116
196
|
//#region package.json
|
|
117
197
|
var name = "@ckpack/ai-commit";
|
|
118
|
-
var version = "1.
|
|
198
|
+
var version = "1.2.0";
|
|
119
199
|
var description = "根据 git diff 信息 生成符合 Conventional Commits 的提交信息.";
|
|
120
200
|
|
|
121
201
|
//#endregion
|
|
122
202
|
//#region src/index.ts
|
|
123
203
|
const program = new Command();
|
|
124
|
-
program.name(name).description(description).version(version);
|
|
204
|
+
program.name(name).description(description).version(version).option("-d, --debug", "print debug information");
|
|
125
205
|
initCommand(program);
|
|
126
206
|
program.parse();
|
|
127
207
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ckpack/ai-commit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac",
|
|
7
7
|
"description": "根据 git diff 信息 生成符合 Conventional Commits 的提交信息.",
|
|
@@ -35,17 +35,17 @@
|
|
|
35
35
|
"release": "npx changelogen --release --push"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"commander": "^14.0.
|
|
38
|
+
"commander": "^14.0.3",
|
|
39
39
|
"consola": "^3.4.2"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@antfu/eslint-config": "^6.
|
|
42
|
+
"@antfu/eslint-config": "^7.6.0",
|
|
43
43
|
"@commander-js/extra-typings": "^14.0.0",
|
|
44
|
-
"@types/node": "^
|
|
45
|
-
"eslint": "^
|
|
44
|
+
"@types/node": "^25.3.0",
|
|
45
|
+
"eslint": "^10.0.2",
|
|
46
46
|
"lint-staged": "^16.2.7",
|
|
47
47
|
"simple-git-hooks": "^2.13.1",
|
|
48
|
-
"tsdown": "^0.
|
|
48
|
+
"tsdown": "^0.20.3",
|
|
49
49
|
"typescript": "^5.9.3"
|
|
50
50
|
},
|
|
51
51
|
"simple-git-hooks": {
|