git-repo-comitter 0.5.3 → 0.5.4

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 +19 -14
  2. package/dist/index.cjs +35 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -18,12 +18,11 @@ D:\path\to\repo> grc
18
18
  正在生成提交信息...
19
19
 
20
20
  提交信息:
21
- docs(readme): 更新 README 文档结构和配置优先级说明
21
+ docs(readme): 更新 README 文档结构和配置说明
22
22
 
23
- - 移除“模型无关”特性描述
24
- - 简化配置优先级为两层:用户配置和系统环境变量
25
- - 调整环境变量表格格式,默认端点改为 localhost
26
- - 将“快速开始”章节提前,移除“技术栈”和“许可”章节
23
+ - 移除环境变量配置方式,统一使用 config.json
24
+ - 简化配置优先级为单一配置源
25
+ - 调整默认端点为 https://api.openai.com/v1
27
26
 
28
27
  提交成功!
29
28
 
@@ -57,18 +56,24 @@ pnpm build && node dist/index.cjs
57
56
 
58
57
  ## 使用
59
58
 
60
- ### 配置优先级
59
+ ### 配置
61
60
 
62
- 1. 用户配置文件 `~/.grc/config.json`
63
- 2. 系统环境变量
61
+ 所有配置通过 `~/.grc/config.json` 管理,不再支持环境变量。
64
62
 
65
- ### 环境变量
63
+ ### 配置文件结构
66
64
 
67
- | 变量 | 说明 | 默认值 |
68
- |----------------|----------|----------------------------|
69
- | `LLM_ENDPOINT` | API 端点 | `https://localhost/api/v1` |
70
- | `LLM_MODEL` | 模型名称 | `deepseek-v4-flash` |
71
- | `LLM_API_KEY` | API 密钥 | |
65
+ ```json
66
+ {
67
+ "endpoint": "https://api.openai.com/v1",
68
+ "apiKey": "sk-xxx",
69
+ "llm": {
70
+ "model": "deepseek-v4-flash",
71
+ "temperature": 0.7,
72
+ "maxInputTokens": 262144,
73
+ "maxOutputTokens": 16384
74
+ }
75
+ }
76
+ ```
72
77
 
73
78
  ### 交互式初始化
74
79
 
package/dist/index.cjs CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require("commander"),l=require("fs"),u=require("path"),d=require("os"),f=require("child_process"),p=require("openai");p=s(p,1);let m=require("readline/promises");var h={llm:{model:`deepseek-v4-flash`,temperature:.7,maxInputTokens:262144,maxOutputTokens:16384},apiKey:``,endpoint:`https://api.openai.com/v1`},g=(0,u.resolve)((0,d.homedir)(),`.grc`,`config.json`);function _(){try{if(!(0,l.existsSync)(g))return{};let e=(0,l.readFileSync)(g,`utf-8`),t=JSON.parse(e);return{...t,llm:t.llm?{...t.llm}:void 0}}catch{return{}}}function v(e){let t=(0,u.resolve)((0,d.homedir)(),`.grc`);(0,l.existsSync)(t)||(0,l.mkdirSync)(t,{recursive:!0}),(0,l.writeFileSync)(g,JSON.stringify(e,null,2),`utf-8`)}function y(){let e=_(),t=process.env.LLM_MODEL,n=process.env.LLM_API_KEY,r=process.env.LLM_ENDPOINT;return{...h,...e,llm:{...h.llm,...e.llm||{},...t?{model:t}:{}},apiKey:n||e.apiKey||``,endpoint:r||e.endpoint||h.endpoint}}function b(){(0,f.execSync)(`git add .`,{cwd:process.cwd(),encoding:`utf-8`})}function x(e){(0,f.execSync)(`git commit -m "${e.replace(/"/g,`\\"`)}"`,{cwd:process.cwd(),encoding:`utf-8`})}function S(e){try{return(0,f.execSync)(`git ${e}`,{cwd:process.cwd(),encoding:`utf-8`})}catch{return``}}function C(){let e=S(`diff --cached`),t=S(`diff`),n=[];return e.trim()&&n.push(`=== Staged Changes ===
2
+ var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require("commander"),l=require("fs"),u=require("path"),d=require("os"),f=require("child_process"),p=require("openai");p=s(p,1);let m=require("readline/promises");var h={llm:{model:`deepseek-v4-flash`,temperature:.7,maxInputTokens:262144,maxOutputTokens:16384},apiKey:``,endpoint:`https://api.openai.com/v1`},g=(0,u.resolve)((0,d.homedir)(),`.grc`,`config.json`);function _(){if(!(0,l.existsSync)(g))return{};try{let e=(0,l.readFileSync)(g,`utf-8`),t=JSON.parse(e);return{...t,llm:t.llm?{...t.llm}:void 0}}catch(e){return console.warn(`警告:解析 ${g} 失败,已忽略用户配置。原因:${e instanceof Error?e.message:String(e)}`),{}}}function v(e){let t=(0,u.resolve)((0,d.homedir)(),`.grc`);(0,l.existsSync)(t)||(0,l.mkdirSync)(t,{recursive:!0}),(0,l.writeFileSync)(g,JSON.stringify(e,null,2),`utf-8`)}function y(){let e=_();return{...h,...e,llm:{...h.llm,...e.llm||{}},apiKey:e.apiKey||``,endpoint:e.endpoint||h.endpoint}}var b=class extends Error{constructor(e,t=1){super(e),this.exitCode=t,this.name=`CliError`}};function x(e,t={}){try{return(0,f.execFileSync)(`git`,e,{cwd:process.cwd(),encoding:`utf-8`})}catch(e){if(t.tolerateError)return``;throw e}}function S(){x([`add`,`.`])}function C(e){x([`commit`,`-m`,e])}function w(){let e=x([`diff`,`--cached`],{tolerateError:!0}),t=x([`diff`],{tolerateError:!0}),n=[];return e.trim()&&n.push(`=== Staged Changes ===
3
3
  `+e),t.trim()&&n.push(`=== Unstaged Changes ===
4
4
  `+t),n.join(`
5
5
 
6
- `)}function w(){return S(`diff --cached --name-only`).trim().length>0}function T(e){let t=(typeof e==`string`?JSON.parse(e):e).choices?.[0]?.message?.content;return typeof t==`string`?t.trim():Array.isArray(t)?t.filter(e=>e.type===`text`).map(e=>e.text).join(``).trim():``}var E=[`feat`,`fix`,`docs`,`style`,`refactor`,`perf`,`test`,`build`,`ci`,`chore`,`revert`];function D(e){let t=e.indexOf(`
6
+ `)}function T(){return x([`diff`,`--cached`,`--name-only`],{tolerateError:!0}).trim().length>0}var E=[`feat`,`fix`,`docs`,`style`,`refactor`,`perf`,`test`,`build`,`ci`,`chore`,`revert`];function D(e){let t=e.indexOf(`
7
7
 
8
- `),n=t===-1?e.trim():e.slice(0,t).trim(),r=t===-1?``:e.slice(t+2).trim(),i=n.match(/^(?<type>[a-zA-Z]+)(?:\((?<scope>[^)]*)\))?(?<breaking>!)?:\s*(?<description>.+)$/);if(!i)return{valid:!1,reason:`标题不符合 Conventional Commits 格式:type(optional scope): description`};let{type:a,description:o}=i.groups;if(!E.includes(a))return{valid:!1,reason:`type 字段的值 "${a}" 不在允许的列表中 (${E.join(`, `)})`};if(n.length>72)return{valid:!1,reason:`标题行超过 72 字符限制 (当前 ${n.length} 字符)`};if(r){if(t===-1)return{valid:!1,reason:`正文前需空一行`};let e=r.split(`
9
- `).filter(e=>e.length>72);if(e.length>0)return{valid:!1,reason:`正文行超出 72 字符限制: ${e.join(`, `)}`}}return{valid:!0}}var O=`你是一位擅长编写 Git 提交信息的专家。根据提供的 Git diff,生成简洁且描述准确的提交信息。
8
+ `),n=t===-1?e.trim():e.slice(0,t).trim(),r=t===-1?``:e.slice(t+2).trim(),i=n.match(/^(?<type>[a-zA-Z]+)(?:\((?<scope>[^)]*)\))?(?<breaking>!)?:\s*(?<description>.+)$/);if(!i)return{valid:!1,reason:`标题不符合 Conventional Commits 格式:type(optional scope): description`};let{type:a}=i.groups;if(!E.includes(a))return{valid:!1,reason:`type 字段的值 "${a}" 不在允许的列表中 (${E.join(`, `)})`};if(n.length>72)return{valid:!1,reason:`标题行超过 72 字符限制 (当前 ${n.length} 字符)`};if(r){if(t===-1)return{valid:!1,reason:`正文前需空一行`};let e=r.split(`
9
+ `).filter(e=>e.length>72);if(e.length>0)return{valid:!1,reason:`正文行超出 72 字符限制: ${e.join(`, `)}`}}return{valid:!0}}function O(e){let t=(typeof e==`string`?JSON.parse(e):e).choices?.[0]?.message?.content;return typeof t==`string`?t.trim():Array.isArray(t)?t.filter(e=>e.type===`text`).map(e=>e.text).join(``).trim():``}var k=`你是一位擅长编写 Git 提交信息的专家。根据提供的 Git diff,生成简洁且描述准确的提交信息。
10
10
 
11
11
  格式规范:
12
12
  - 遵循 Conventional Commits:<type>(<scope>): <description>
@@ -36,9 +36,34 @@ var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=
36
36
  - 迁移 session 和 permission 模块至新接口
37
37
  - 移除已废弃的 getCacheKey 方法
38
38
 
39
- 只回复提交信息本身,不要添加任何解释。`,k=3;async function A(e,t){if(!t.apiKey)throw Error(`LLM_API_KEY 未设置,请在 .env 或环境变量中配置。`);let n=new p.default({apiKey:t.apiKey,baseURL:t.endpoint}),r=[{role:`system`,content:O},{role:`user`,content:`以下是 Git diff 内容:\n\n${e}`}];for(let e=1;e<=k;e++){let i=await n.chat.completions.create({model:t.llm.model,temperature:t.llm.temperature,max_tokens:t.llm.maxOutputTokens,messages:r}),a=T(i);if(!a)throw Error(`LLM 返回了空的提交信息。响应内容:${JSON.stringify(i)}`);let o=await D(a);if(o.valid)return a;if(e<k)console.log(` 提交信息格式校验未通过(第 ${e} 次): ${o.reason}`),console.log(` 正在重新生成...
40
- `),r.push({role:`assistant`,content:a},{role:`user`,content:`生成的提交信息格式不符合规范:${o.reason}。请严格按照 Conventional Commits 格式重新生成。`});else throw Error(`LLM 提交信息格式校验失败(已重试 ${k} 次): ${o.reason}\n最后一次生成的提交信息:\n${a}`)}throw Error(`意外的错误:重试循环结束后仍未能生成有效提交信息`)}function j(){let e=y();return e.apiKey||(console.error("错误:LLM_API_KEY 未设置,请运行 `grc init` 进行配置。"),process.exit(1)),e}function M(){w()?console.log(`检出已有暂存变更,直接提交...`):(console.log(`暂存所有变更...`),b())}function N(){let e=C();return e.trim()||(console.log(`没有可提交的变更。`),process.exit(0)),e}async function P(){let e=j();M();let t=N();console.log(`正在生成提交信息...
41
- `);let n=await A(t,e);x(n),console.log(`提交信息:\n ${n}\n`),console.log(`提交成功!`)}function F(e){let t=(0,m.createInterface)({input:process.stdin,output:process.stdout});return t.question(e).finally(()=>t.close())}async function I(){console.log(`LLM 配置初始化
42
- `);let e=_(),t=(await F(`大模型链接(API 地址)[${e.endpoint||`https://api.openai.com/v1`}]: `)).trim()||e.endpoint||`https://api.openai.com/v1`,n=(await F(`模型名称 [${e.llm?.model||`deepseek-v4-flash`}]: `)).trim()||e.llm?.model||`deepseek-v4-flash`,r=(await F(`API Key [${e.apiKey?`***`:`(必填)`}]: `)).trim()||e.apiKey;r||(console.error(`
43
- 错误:API Key 不能为空。`),process.exit(1)),v({apiKey:r,endpoint:t,llm:{model:n,temperature:.7,maxInputTokens:262144,maxOutputTokens:16384}}),console.log(`
44
- 配置已保存到 ~/.grc/config.json`)}function L(){let e=(0,u.resolve)(__dirname,`..`,`package.json`);return JSON.parse((0,l.readFileSync)(e,`utf-8`)).version}var R=new c.Command;R.name(`grc`).version(L(),`-v, --version`,`显示当前版本`).description(`一款使用 LLM 生成 Git 提交信息并执行提交的命令行工具`).helpOption(`-h, --help`,`显示帮助信息`),R.command(`init`).description(`交互式 LLM 配置初始化`).action(()=>{I().catch(e=>{console.error(`错误:`,e.message),process.exit(1)})});function z(){if(process.argv.slice(2).length===0){P().catch(e=>{console.error(`错误:`,e.message),process.exit(1)});return}R.parse(process.argv)}z();
39
+ 只回复提交信息本身,不要添加任何解释。`;function A(e){if(!e.apiKey)throw Error("apiKey 未设置,请运行 `grc init` 进行配置。");return new p.default({apiKey:e.apiKey,baseURL:e.endpoint})}async function j(e,t){let n=A(t),r=[{role:`system`,content:k},{role:`user`,content:`以下是 Git diff 内容:\n\n${e}`}];for(let e=1;e<=3;e++){let i=await n.chat.completions.create({model:t.llm.model,temperature:t.llm.temperature,max_tokens:t.llm.maxOutputTokens,messages:r}),a=O(i);if(!a)throw Error(`LLM 返回了空的提交信息。响应内容:${JSON.stringify(i)}`);let o=await D(a);if(o.valid)return a;if(e<3)console.log(` 提交信息格式校验未通过(第 ${e} 次): ${o.reason}`),console.log(` 正在重新生成...
40
+ `),r.push({role:`assistant`,content:a},{role:`user`,content:`生成的提交信息格式不符合规范:${o.reason}。请严格按照 Conventional Commits 格式重新生成。`});else throw Error(`LLM 提交信息格式校验失败(已重试 3 次): ${o.reason}\n最后一次生成的提交信息:\n${a}`)}throw Error(`意外的错误:重试循环结束后仍未能生成有效提交信息`)}var M=4;function N(e){return Math.ceil(e.length/M)}function P(e){let t=e.split(`
41
+ `),n=[],r=``,i=[],a=``,o=()=>{if(i.length===0)return;let e=i.join(`
42
+ `);n.push({filePath:a,content:e,estimatedTokens:N(e)}),i=[]};for(let e of t){let t=e.match(/^diff --git a\/(.+?) b\//),n=e.match(/^=== .+ ===$/);t?(o(),a=t[1],r&&=(i.push(r),``),i.push(e)):n?r=e:(r&&i.length===0&&(i.push(r),r=``),i.push(e))}return o(),n}function F(e,t){if(e.length===0)return[];let n=[],r=[],i=0,a=()=>{if(r.length===0)return;let e=r.map(e=>e.content).join(`
43
+ `);n.push({blocks:r,content:e,estimatedTokens:i}),r=[],i=0};for(let n of e)i+n.estimatedTokens<=t?(r.push(n),i+=n.estimatedTokens):(a(),r.push(n),i=n.estimatedTokens);return a(),n}var I=200,L=`你是一位擅长编写 Git 提交信息的专家。你将收到一个大型 Git diff 的**一部分**(包含部分文件的变更)。
44
+
45
+ 请仅根据这部分 diff 生成一个**局部的**提交信息草稿,格式规范同 Conventional Commits,但允许 scope 或 type 不够精确——后续会合并所有部分的结果。
46
+
47
+ 格式规范:
48
+ - 遵循 Conventional Commits:<type>(<scope>): <description>
49
+ - type 可选值:feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
50
+ - 标题行控制在 72 字符以内
51
+ - 正文使用无序列表分条陈述,每条以 "- " 开头
52
+ - 描述语言使用中文,避免使用英文
53
+ - 只回复提交信息本身,不要添加任何解释`,R=`你是一位擅长编写 Git 提交信息的专家。你将收到多个**局部的**提交信息草稿,每个草稿描述了部分文件的变更。
54
+
55
+ 请将它们合并为一个**完整的、连贯的**提交信息,遵循 Conventional Commits 格式:
56
+ - 选择最能概括所有变更的 type 和 scope
57
+ - 标题行控制在 72 字符以内
58
+ - 正文合并所有草稿的要点,去重,保持逻辑分组,使用无序列表
59
+ - 如变更较简单,可省略正文
60
+ - 描述语言使用中文,避免使用英文
61
+ - 只回复最终的提交信息本身,不要添加任何解释`;function z(e,t){return e.llm.maxInputTokens-N(t)-I-e.llm.maxOutputTokens}async function B(e,t){let n=O(await A(t).chat.completions.create({model:t.llm.model,temperature:t.llm.temperature,max_tokens:t.llm.maxOutputTokens,messages:[{role:`system`,content:L},{role:`user`,content:`以下是 Git diff 的一部分:\n\n${e}`}]}));if(!n)throw Error(`LLM 在处理分批 diff 时返回了空内容。`);return n}async function V(e,t){let n=A(t),r=z(t,R),i=[],a=0,o=0;for(let t=0;t<e.length;t++){let n=`--- 部分 ${t+1} ---\n${e[t]}`,s=N(n);if(a+s<=r)i.push(n),a+=s;else{o=e.length-t;break}}let s=i.join(`
62
+
63
+ `)+(o>0?`\n\n[... 前面 ${o} 个批次已省略 ...]`:``),c=O(await n.chat.completions.create({model:t.llm.model,temperature:t.llm.temperature,max_tokens:t.llm.maxOutputTokens,messages:[{role:`system`,content:R},{role:`user`,content:s}]}));if(!c)throw Error(`LLM 在合并提交信息时返回了空内容。`);return c}async function H(e,t,n){let r=[{role:`system`,content:R},{role:`user`,content:t.map((e,t)=>`--- 部分 ${t+1} ---\n${e}`).join(`
64
+
65
+ `)},{role:`assistant`,content:e}],i=e;for(let e=1;e<=3;e++){let t=await D(i);if(t.valid)return i;if(e<3){if(console.log(` 合并信息格式校验未通过(第 ${e} 次): ${t.reason}`),console.log(` 正在重新合并...
66
+ `),r.push({role:`user`,content:`生成的提交信息格式不符合规范:${t.reason}。请严格按照 Conventional Commits 格式重新生成。`}),i=O(await A(n).chat.completions.create({model:n.llm.model,temperature:n.llm.temperature,max_tokens:n.llm.maxOutputTokens,messages:r})),!i)throw Error(`LLM 在重试合并时返回了空内容。`);r.push({role:`assistant`,content:i})}else throw Error(`合并信息格式校验失败(已重试 3 次): ${t.reason}\n最后一次生成的提交信息:\n${i}`)}throw Error(`意外的错误:重试循环结束后仍未能生成有效提交信息`)}async function U(e,t){let n=z(t,k),r=N(e);if(r<=n)return{message:await j(e,t),batchCount:1};let i=F(P(e),n);console.log(` 变更内容较大(约 ${r} tokens),将分为 ${i.length} 批次处理...`);let a=[];for(let e=0;e<i.length;e++){console.log(` 正在处理第 ${e+1}/${i.length} 批次...`);let n=await B(i[e].content,t);a.push(n)}return console.log(` 正在合并 ${i.length} 个批次的提交信息...`),{message:await H(await V(a,t),a,t),batchCount:i.length}}function W(){let e=y();if(!e.apiKey)throw new b("API Key 未设置,请运行 `grc init` 进行配置。");return e}function G(e){if(e){if(!T())throw new b(`没有已暂存的变更,请先使用 git add 暂存文件。`);console.log(`仅提交暂存变更...`);return}T()?console.log(`检出已有暂存变更,直接提交...`):(console.log(`暂存所有变更...`),S())}function K(){let e=w();return e.trim()?e:null}async function q(e={}){let t=W();G(!!e.stagedOnly);let n=K();if(!n){console.log(`没有可提交的变更。`);return}console.log(`正在生成提交信息...
67
+ `);let{message:r,batchCount:i}=await U(n,t);i>1&&console.log(` (已将 diff 分为 ${i} 批次处理并合并)\n`),C(r),console.log(`提交信息:\n ${r}\n`),console.log(`提交成功!`)}function J(e){let t=(0,m.createInterface)({input:process.stdin,output:process.stdout});return t.question(e).finally(()=>t.close())}async function Y(){console.log(`LLM 配置初始化
68
+ `);let e=_(),t=(await J(`大模型链接(API 地址)[${e.endpoint||`https://api.openai.com/v1`}]: `)).trim()||e.endpoint||`https://api.openai.com/v1`,n=(await J(`模型名称 [${e.llm?.model||`deepseek-v4-flash`}]: `)).trim()||e.llm?.model||`deepseek-v4-flash`,r=(await J(`API Key [${e.apiKey?`***`:`(必填)`}]: `)).trim()||e.apiKey;if(!r)throw new b(`API Key 不能为空。`);v({apiKey:r,endpoint:t,llm:{model:n,temperature:.7,maxInputTokens:262144,maxOutputTokens:16384}}),console.log(`
69
+ 配置已保存到 ~/.grc/config.json`)}function X(){let e=(0,u.resolve)(__dirname,`..`,`package.json`);return JSON.parse((0,l.readFileSync)(e,`utf-8`)).version}function Z(e){e instanceof b&&(console.error(`错误:`,e.message),process.exit(e.exitCode)),console.error(`意外错误:`,e instanceof Error?e.message:String(e)),process.exit(1)}var Q=new c.Command;Q.name(`grc`).version(X(),`-v, --version`,`显示当前版本`).description(`一款使用 LLM 生成 Git 提交信息并执行提交的命令行工具`).helpOption(`-h, --help`,`显示帮助信息`).option(`-s, --staged-only`,`仅提交已暂存的变更`),Q.command(`init`).description(`交互式 LLM 配置初始化`).action(()=>{Y().catch(Z)}),Q.action(e=>{q({stagedOnly:e.stagedOnly??!1}).catch(Z)});function $(){Q.parse(process.argv)}$();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-repo-comitter",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "A CLI tool that uses LLM to generate Git commit messages and execute commits",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",