@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.
Files changed (3) hide show
  1. package/README.md +27 -4
  2. package/dist/index.mjs +104 -24
  3. 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
- - Codex 失败时自动回退为本地摘要
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/codex.ts
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 stagedDiff;
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 workingDiff;
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 (/* @__PURE__ */ new RegExp(`^(revert: )?(${[
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}$`)).test(msg);
108
+ ].join("|")})(\\(.+\\))?: .{1,50}$`).test(msg);
80
109
  }
81
- function tryCodex(prompt) {
82
- const bin = process.env.CODEX_BIN || "codex";
83
- const args = process.env.CODEX_ARGS?.split(/\s+/).filter(Boolean) ?? ["exec"];
84
- const result = args.includes("-") ? runCommand(bin, args, { input: prompt }) : runCommand(bin, [...args, prompt]);
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) console.warn(`[commit-msg] codex execution failed: ${result.error.message}`);
87
- else if (result.stderr.trim()) console.warn(`[commit-msg] codex stderr: ${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
- return sanitizeMessage(result.stdout) || null;
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 runCodexCommand() {
136
+ function runProviderCommand(config, options) {
98
137
  try {
99
- const candidate = sanitizeMessage(tryCodex(buildPrompt(ensureNonEmptyDiff())) ?? "");
100
- const message = isConventional(candidate) ? candidate : fallbackMessage();
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$1) {
112
- codex(program$1);
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.1.0";
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.1.0",
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.2",
38
+ "commander": "^14.0.3",
39
39
  "consola": "^3.4.2"
40
40
  },
41
41
  "devDependencies": {
42
- "@antfu/eslint-config": "^6.7.3",
42
+ "@antfu/eslint-config": "^7.6.0",
43
43
  "@commander-js/extra-typings": "^14.0.0",
44
- "@types/node": "^24",
45
- "eslint": "^9.39.2",
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.18.3",
48
+ "tsdown": "^0.20.3",
49
49
  "typescript": "^5.9.3"
50
50
  },
51
51
  "simple-git-hooks": {