git-repo-comitter 0.2.5 → 0.3.1

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 CHANGED
@@ -5,14 +5,22 @@
5
5
 
6
6
  ## 使用
7
7
 
8
- 1. 配置环境变量:`LLM_API_KEY`、`LLM_ENDPOINT`、`LLM_MODEL`
8
+ 1. 配置环境变量:`LLM_API_KEY`、`LLM_ENDPOINT`、`LLM_MODEL`,或运行交互式初始化命令:
9
+
10
+ ```bash
11
+ npx git-repo-comitter init
12
+ ```
13
+
14
+ 交互式命令会将配置保存到 `~/.grc/config.json`。
15
+
9
16
  2. 运行:
10
- ```bash
11
- # 发布后直接使用
12
- npx git-repo-comitter
13
17
 
14
- # 本地开发
15
- pnpm build && node dist/index.cjs
16
- ```
18
+ ```bash
19
+ # 发布后直接使用
20
+ npx git-repo-comitter
21
+
22
+ # 本地开发
23
+ pnpm build && node dist/index.cjs
24
+ ```
17
25
 
18
26
  也可通过 `config.yaml` 自定义模型参数和生成风格。
package/dist/index.cjs CHANGED
@@ -1,14 +1,16 @@
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,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(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},c=(n,r,a)=>(a=n==null?{}:e(i(n)),s(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let l=require("yaml");l=c(l,1);let u=require("dotenv");u=c(u,1);let d=require("openai");d=c(d,1);var f=o(((e,t)=>{t.exports={}}))();u.default.config();var p={llm:{model:`deepseek-v4-flash`,temperature:.7,max_tokens:262144},style:{language:`zh-CN`,template:`conventional`},git:{repoPath:`.`},apiKey:``,endpoint:`https://api.openai.com/v1`};function m(e){let t=e||[`.git-repo-committer.yaml`,`.git-repo-committer.yml`,`config.yaml`].find(e=>(0,f.existsSync)((0,f.resolve)(process.cwd(),e))),n={};if(t){let e=(0,f.readFileSync)((0,f.resolve)(process.cwd(),t),`utf-8`);n=l.default.parse(e)}return{...p,...n,llm:{...p.llm,...n.llm||{}},style:{...p.style,...n.style||{}},git:{...p.git,...n.git||{}},apiKey:process.env.LLM_API_KEY||n.apiKey||``,endpoint:process.env.LLM_ENDPOINT||n.endpoint||p.endpoint}}function h(e){let t=e||process.cwd();try{return(0,f.execSync)(`git diff --cached`,{cwd:t,encoding:`utf-8`})}catch{return``}}function g(e){let t=e||process.cwd();try{return(0,f.execSync)(`git diff`,{cwd:t,encoding:`utf-8`})}catch{return``}}function _(e){let t=e||process.cwd(),n=h(t),r=g(t),i=[];return n.trim()&&i.push(`=== Staged Changes ===
1
+ 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("fs"),l=require("path"),u=require("os"),d=require("yaml");d=s(d,1);let f=require("dotenv");f=s(f,1);let p=require("child_process"),m=require("openai");m=s(m,1);let h=require("readline/promises");var g=(0,l.resolve)((0,u.homedir)(),`.grc`,`config.json`);f.default.config();var _={llm:{model:`deepseek-v4-flash`,temperature:.7,max_input_tokens:128e3,max_output_tokens:16384},style:{language:`zh-CN`,template:`conventional`},git:{repoPath:`.`},apiKey:``,endpoint:`https://api.openai.com/v1`};function v(){try{if(!(0,c.existsSync)(g))return{};let e=(0,c.readFileSync)(g,`utf-8`),t=JSON.parse(e);return{...t,llm:t.llm?{...t.llm}:void 0,style:t.style?{...t.style}:void 0,git:t.git?{...t.git}:void 0}}catch{return{}}}function y(e){let t=(0,l.resolve)((0,u.homedir)(),`.grc`);(0,c.existsSync)(t)||(0,c.mkdirSync)(t,{recursive:!0}),(0,c.writeFileSync)(g,JSON.stringify(e,null,2),`utf-8`)}function b(e){let t=e||[`.git-repo-committer.yaml`,`.git-repo-committer.yml`,`config.yaml`].find(e=>(0,c.existsSync)((0,l.resolve)(process.cwd(),e))),n={};if(t){let e=(0,c.readFileSync)((0,l.resolve)(process.cwd(),t),`utf-8`);n=d.default.parse(e)}let r=v(),i=process.env.LLM_MODEL,a=process.env.LLM_API_KEY,o=process.env.LLM_ENDPOINT;return{..._,...n,...r,llm:{..._.llm,...n.llm||{},...r.llm||{},...i?{model:i}:{}},style:{..._.style,...n.style||{},...r.style||{}},git:{..._.git,...n.git||{},...r.git||{}},apiKey:a||r.apiKey||n.apiKey||``,endpoint:o||r.endpoint||n.endpoint||_.endpoint}}function x(e){let t=e||process.cwd();try{return(0,p.execSync)(`git diff --cached`,{cwd:t,encoding:`utf-8`})}catch{return``}}function S(e){let t=e||process.cwd();try{return(0,p.execSync)(`git diff`,{cwd:t,encoding:`utf-8`})}catch{return``}}function C(e){let t=e||process.cwd(),n=x(t),r=S(t),i=[];return n.trim()&&i.push(`=== Staged Changes ===
3
2
  `+n),r.trim()&&i.push(`=== Unstaged Changes ===
4
3
  `+r),i.join(`
5
4
 
6
- `)}function v(e){let t=e||process.cwd();try{return(0,f.execSync)(`git diff --cached --name-only`,{cwd:t,encoding:`utf-8`}).trim().length>0}catch{return!1}}var y=`你是一位擅长编写 Git 提交信息的专家。
5
+ `)}function w(e){let t=e||process.cwd();try{return(0,p.execSync)(`git diff --cached --name-only`,{cwd:t,encoding:`utf-8`}).trim().length>0}catch{return!1}}var T=`你是一位擅长编写 Git 提交信息的专家。
7
6
  根据提供的 Git diff,生成简洁且描述准确的提交信息。
8
7
  遵循 Conventional Commits 格式:<type>(<scope>): <description>
9
8
  类型包括:feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
10
9
  标题行控制在 72 个字符以内。
11
10
  如果有重要变更,在空行后添加更详细的正文说明。
12
- 只回复提交信息本身,不要添加任何解释。`;async function b(e,t){if(!t.apiKey)throw Error(`LLM_API_KEY is not set. Please set it in .env or environment variables.`);let n=(await new d.default({apiKey:t.apiKey,baseURL:t.endpoint}).chat.completions.create({model:t.llm.model,temperature:t.llm.temperature,max_tokens:t.llm.max_tokens,messages:[{role:`system`,content:y},{role:`user`,content:`以下是 Git diff 内容:\n\n${e}`}]})).choices[0]?.message?.content?.trim();if(!n)throw Error(`LLM returned an empty commit message.`);return n}function x(e,t){let n=t||process.cwd();(0,f.execSync)(`git commit -m "${e.replace(/"/g,`\\"`)}"`,{cwd:n,encoding:`utf-8`})}function S(e){(0,f.execSync)(`git add -A`,{cwd:e||process.cwd(),encoding:`utf-8`})}async function C(){let e=m();e.apiKey||(console.error(`Error: LLM_API_KEY is not set. Please set it in .env or environment variables.`),process.exit(1)),v(e.git.repoPath)||(console.log(`No staged changes found. Staging all changes...`),S(e.git.repoPath));let t=_(e.git.repoPath);t.trim()||(console.log(`No changes to commit.`),process.exit(0)),console.log(`Generating commit message...
13
- `);let n=await b(t,e);console.log(`Commit message:\n ${n}\n`),x(n,e.git.repoPath),console.log(`Commit successful!`)}C().catch(e=>{console.error(`Error:`,e.message),process.exit(1)});
11
+ 只回复提交信息本身,不要添加任何解释。`;function E(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():``}async function D(e,t){if(!t.apiKey)throw Error(`LLM_API_KEY is not set. Please set it in .env or environment variables.`);let n=await new m.default({apiKey:t.apiKey,baseURL:t.endpoint}).chat.completions.create({model:t.llm.model,temperature:t.llm.temperature,max_tokens:t.llm.max_output_tokens,messages:[{role:`system`,content:T},{role:`user`,content:`以下是 Git diff 内容:\n\n${e}`}]}),r=E(n);if(!r)throw Error(`LLM returned an empty commit message. Response: ${JSON.stringify(n)}`);return r}function O(e,t){let n=t||process.cwd();(0,p.execSync)(`git commit -m "${e.replace(/"/g,`\\"`)}"`,{cwd:n,encoding:`utf-8`})}function k(e){(0,p.execSync)(`git add -A`,{cwd:e||process.cwd(),encoding:`utf-8`})}function A(e){let t=(0,h.createInterface)({input:process.stdin,output:process.stdout});return t.question(e).finally(()=>t.close())}async function j(){console.log(`LLM 配置初始化
12
+ `);let e=v(),t=(await A(`大模型链接 (API 地址) [${e.endpoint||`https://api.openai.com/v1`}]: `)).trim()||e.endpoint||`https://api.openai.com/v1`,n=(await A(`模型名称 [${e.llm?.model||`deepseek-v4-flash`}]: `)).trim()||e.llm?.model||`deepseek-v4-flash`,r=(await A(`API Key [${e.apiKey?`***`:`(必填)`}]: `)).trim()||e.apiKey;r||(console.error(`
13
+ 错误:API Key 不能为空。`),process.exit(1)),y({apiKey:r,endpoint:t,llm:{model:n,temperature:.7,max_input_tokens:262144,max_output_tokens:16384}}),console.log(`
14
+ 配置已保存到 ~/.grc/config.json`)}async function M(){let e=b();e.apiKey||(console.error(`Error: LLM_API_KEY is not set. Please set it in .env or environment variables.`),process.exit(1)),w(e.git.repoPath)||(console.log(`No staged changes found. Staging all changes...`),k(e.git.repoPath));let t=C(e.git.repoPath);t.trim()||(console.log(`No changes to commit.`),process.exit(0)),console.log(`Generating commit message...
15
+ `);let n=await D(t,e);console.log(`Commit message:\n ${n}\n`),O(n,e.git.repoPath),console.log(`Commit successful!`)}async function N(){process.argv[2]===`init`?await j():await M()}N().catch(e=>{console.error(`Error:`,e.message),process.exit(1)});
14
16
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../__vite-browser-external","../src/config.ts","../src/git.ts","../src/llm.ts","../src/commit.ts","../src/index.ts"],"sourcesContent":["module.exports = {}","import { readFileSync, existsSync } from \"fs\";\nimport { resolve } from \"path\";\nimport YAML from \"yaml\";\nimport dotenv from \"dotenv\";\n\ndotenv.config();\n\nexport interface LLMConfig {\n model: string;\n temperature: number;\n max_tokens: number;\n}\n\nexport interface StyleConfig {\n language: string;\n template: string;\n}\n\nexport interface GitConfig {\n repoPath: string;\n}\n\nexport interface AppConfig {\n llm: LLMConfig;\n style: StyleConfig;\n git: GitConfig;\n apiKey: string;\n endpoint: string;\n}\n\nconst DEFAULT_CONFIG: AppConfig = {\n llm: {\n model: \"deepseek-v4-flash\",\n temperature: 0.7,\n max_tokens: 262144,\n },\n style: {\n language: \"zh-CN\",\n template: \"conventional\",\n },\n git: {\n repoPath: \".\",\n },\n apiKey: \"\",\n endpoint: \"https://api.openai.com/v1\",\n};\n\nexport function loadConfig(configPath?: string): AppConfig {\n const configFile =\n configPath ||\n [\".git-repo-committer.yaml\", \".git-repo-committer.yml\", \"config.yaml\"].find(\n (f) => existsSync(resolve(process.cwd(), f)),\n );\n\n let fileConfig: Partial<AppConfig> = {};\n\n if (configFile) {\n const content = readFileSync(resolve(process.cwd(), configFile), \"utf-8\");\n fileConfig = YAML.parse(content);\n }\n\n return {\n ...DEFAULT_CONFIG,\n ...fileConfig,\n llm: { ...DEFAULT_CONFIG.llm, ...(fileConfig.llm || {}) },\n style: { ...DEFAULT_CONFIG.style, ...(fileConfig.style || {}) },\n git: { ...DEFAULT_CONFIG.git, ...(fileConfig.git || {}) },\n apiKey: process.env.LLM_API_KEY || fileConfig.apiKey || \"\",\n endpoint:\n process.env.LLM_ENDPOINT ||\n fileConfig.endpoint ||\n DEFAULT_CONFIG.endpoint,\n };\n}\n","import { execSync } from \"child_process\";\n\nexport function getStagedDiff(repoPath?: string): string {\n const cwd = repoPath || process.cwd();\n try {\n return execSync(\"git diff --cached\", { cwd, encoding: \"utf-8\" });\n } catch {\n return \"\";\n }\n}\n\nexport function getUnstagedDiff(repoPath?: string): string {\n const cwd = repoPath || process.cwd();\n try {\n return execSync(\"git diff\", { cwd, encoding: \"utf-8\" });\n } catch {\n return \"\";\n }\n}\n\nexport function getAllDiff(repoPath?: string): string {\n const cwd = repoPath || process.cwd();\n const staged = getStagedDiff(cwd);\n const unstaged = getUnstagedDiff(cwd);\n\n const parts: string[] = [];\n if (staged.trim()) {\n parts.push(\"=== Staged Changes ===\\n\" + staged);\n }\n if (unstaged.trim()) {\n parts.push(\"=== Unstaged Changes ===\\n\" + unstaged);\n }\n return parts.join(\"\\n\\n\");\n}\n\nexport function hasStagedChanges(repoPath?: string): boolean {\n const cwd = repoPath || process.cwd();\n try {\n const result = execSync(\"git diff --cached --name-only\", {\n cwd,\n encoding: \"utf-8\",\n });\n return result.trim().length > 0;\n } catch {\n return false;\n }\n}\n","import OpenAI from \"openai\";\nimport type { AppConfig } from \"./config\";\n\nconst SYSTEM_PROMPT = `你是一位擅长编写 Git 提交信息的专家。\n根据提供的 Git diff,生成简洁且描述准确的提交信息。\n遵循 Conventional Commits 格式:<type>(<scope>): <description>\n类型包括:feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert\n标题行控制在 72 个字符以内。\n如果有重要变更,在空行后添加更详细的正文说明。\n只回复提交信息本身,不要添加任何解释。`;\n\nexport async function generateCommitMessage(\n diff: string,\n config: AppConfig,\n): Promise<string> {\n if (!config.apiKey) {\n throw new Error(\n \"LLM_API_KEY is not set. Please set it in .env or environment variables.\",\n );\n }\n\n const client = new OpenAI({\n apiKey: config.apiKey,\n baseURL: config.endpoint,\n });\n\n const response = await client.chat.completions.create({\n model: config.llm.model,\n temperature: config.llm.temperature,\n max_tokens: config.llm.max_tokens,\n messages: [\n { role: \"system\", content: SYSTEM_PROMPT },\n {\n role: \"user\",\n content: `以下是 Git diff 内容:\\n\\n${diff}`,\n },\n ],\n });\n\n const message = response.choices[0]?.message?.content?.trim();\n if (!message) {\n throw new Error(\"LLM returned an empty commit message.\");\n }\n return message;\n}\n","import { execSync } from \"child_process\";\n\nexport function gitCommit(message: string, repoPath?: string): void {\n const cwd = repoPath || process.cwd();\n const escapedMessage = message.replace(/\"/g, '\\\\\"');\n execSync(`git commit -m \"${escapedMessage}\"`, { cwd, encoding: \"utf-8\" });\n}\n\nexport function gitAddAll(repoPath?: string): void {\n const cwd = repoPath || process.cwd();\n execSync(\"git add -A\", { cwd, encoding: \"utf-8\" });\n}\n","#!/usr/bin/env node\n\nimport { loadConfig } from \"./config\";\nimport { getAllDiff, hasStagedChanges } from \"./git\";\nimport { generateCommitMessage } from \"./llm\";\nimport { gitCommit, gitAddAll } from \"./commit\";\n\nasync function main() {\n const config = loadConfig();\n\n if (!config.apiKey) {\n console.error(\n \"Error: LLM_API_KEY is not set. Please set it in .env or environment variables.\",\n );\n process.exit(1);\n }\n\n if (!hasStagedChanges(config.git.repoPath)) {\n console.log(\"No staged changes found. Staging all changes...\");\n gitAddAll(config.git.repoPath);\n }\n\n const diff = getAllDiff(config.git.repoPath);\n if (!diff.trim()) {\n console.log(\"No changes to commit.\");\n process.exit(0);\n }\n\n console.log(\"Generating commit message...\\n\");\n const message = await generateCommitMessage(diff, config);\n console.log(`Commit message:\\n ${message}\\n`);\n\n gitCommit(message, config.git.repoPath);\n console.log(\"Commit successful!\");\n}\n\nmain().catch((err) => {\n console.error(\"Error:\", err.message);\n process.exit(1);\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/config.ts","../src/git.ts","../src/llm.ts","../src/commit.ts","../src/init.ts","../src/index.ts"],"sourcesContent":["import { readFileSync, writeFileSync, existsSync, mkdirSync } from \"fs\";\nimport { resolve } from \"path\";\nimport { homedir } from \"os\";\nimport YAML from \"yaml\";\nimport dotenv from \"dotenv\";\n\n\nconst USER_CONFIG_PATH = resolve(homedir(), \".grc\", \"config.json\");\ndotenv.config();\n\nexport interface LLMConfig {\n model: string;\n temperature: number;\n max_input_tokens: number;\n max_output_tokens: number;\n}\n\nexport interface StyleConfig {\n language: string;\n template: string;\n}\n\nexport interface GitConfig {\n repoPath: string;\n}\n\nexport interface AppConfig {\n llm: LLMConfig;\n style: StyleConfig;\n git: GitConfig;\n apiKey: string;\n endpoint: string;\n}\nconst DEFAULT_CONFIG: AppConfig = {\n llm: {\n model: \"deepseek-v4-flash\",\n temperature: 0.7,\n max_input_tokens: 128000,\n max_output_tokens: 16384,\n },\n style: {\n language: \"zh-CN\",\n template: \"conventional\",\n },\n git: {\n repoPath: \".\",\n },\n apiKey: \"\",\n endpoint: \"https://api.openai.com/v1\",\n};\nexport function loadUserConfig(): Partial<AppConfig> {\n try {\n if (!existsSync(USER_CONFIG_PATH)) return {};\n const raw = readFileSync(USER_CONFIG_PATH, \"utf-8\");\n const parsed = JSON.parse(raw);\n return {\n ...parsed,\n llm: parsed.llm ? { ...parsed.llm } : undefined,\n style: parsed.style ? { ...parsed.style } : undefined,\n git: parsed.git ? { ...parsed.git } : undefined,\n };\n } catch {\n return {};\n }\n}\n\nexport function saveUserConfig(config: Partial<AppConfig>): void {\n const dir = resolve(homedir(), \".grc\");\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n writeFileSync(USER_CONFIG_PATH, JSON.stringify(config, null, 2), \"utf-8\");\n}\n\nexport function loadConfig(configPath?: string): AppConfig {\n const configFile =\n configPath ||\n [\".git-repo-committer.yaml\", \".git-repo-committer.yml\", \"config.yaml\"].find(\n (f) => existsSync(resolve(process.cwd(), f)),\n );\n\n let fileConfig: Partial<AppConfig> = {};\n\n if (configFile) {\n const content = readFileSync(resolve(process.cwd(), configFile), \"utf-8\");\n fileConfig = YAML.parse(content);\n }\n\n const userConfig = loadUserConfig();\n\n const envModel = process.env.LLM_MODEL;\n const envApiKey = process.env.LLM_API_KEY;\n const envEndpoint = process.env.LLM_ENDPOINT;\n\n return {\n ...DEFAULT_CONFIG,\n ...fileConfig,\n ...userConfig,\n llm: {\n ...DEFAULT_CONFIG.llm,\n ...(fileConfig.llm || {}),\n ...(userConfig.llm || {}),\n ...(envModel ? { model: envModel } : {}),\n },\n style: { ...DEFAULT_CONFIG.style, ...(fileConfig.style || {}), ...(userConfig.style || {}) },\n git: { ...DEFAULT_CONFIG.git, ...(fileConfig.git || {}), ...(userConfig.git || {}) },\n apiKey: envApiKey || userConfig.apiKey || fileConfig.apiKey || \"\",\n endpoint: envEndpoint || userConfig.endpoint || fileConfig.endpoint || DEFAULT_CONFIG.endpoint,\n };\n}\n","import { execSync } from \"child_process\";\n\nexport function getStagedDiff(repoPath?: string): string {\n const cwd = repoPath || process.cwd();\n try {\n return execSync(\"git diff --cached\", { cwd, encoding: \"utf-8\" });\n } catch {\n return \"\";\n }\n}\n\nexport function getUnstagedDiff(repoPath?: string): string {\n const cwd = repoPath || process.cwd();\n try {\n return execSync(\"git diff\", { cwd, encoding: \"utf-8\" });\n } catch {\n return \"\";\n }\n}\n\nexport function getAllDiff(repoPath?: string): string {\n const cwd = repoPath || process.cwd();\n const staged = getStagedDiff(cwd);\n const unstaged = getUnstagedDiff(cwd);\n\n const parts: string[] = [];\n if (staged.trim()) {\n parts.push(\"=== Staged Changes ===\\n\" + staged);\n }\n if (unstaged.trim()) {\n parts.push(\"=== Unstaged Changes ===\\n\" + unstaged);\n }\n return parts.join(\"\\n\\n\");\n}\n\nexport function hasStagedChanges(repoPath?: string): boolean {\n const cwd = repoPath || process.cwd();\n try {\n const result = execSync(\"git diff --cached --name-only\", {\n cwd,\n encoding: \"utf-8\",\n });\n return result.trim().length > 0;\n } catch {\n return false;\n }\n}\n","import OpenAI from \"openai\";\nimport type { AppConfig } from \"./config\";\n\nconst SYSTEM_PROMPT = `你是一位擅长编写 Git 提交信息的专家。\n根据提供的 Git diff,生成简洁且描述准确的提交信息。\n遵循 Conventional Commits 格式:<type>(<scope>): <description>\n类型包括:feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert\n标题行控制在 72 个字符以内。\n如果有重要变更,在空行后添加更详细的正文说明。\n只回复提交信息本身,不要添加任何解释。`;\n\nfunction extractContent(response: any): string {\n const data = typeof response === \"string\" ? JSON.parse(response) : response;\n const content = data.choices?.[0]?.message?.content;\n if (typeof content === \"string\") return content.trim();\n if (Array.isArray(content)) {\n return content\n .filter((c: any) => c.type === \"text\")\n .map((c: any) => c.text)\n .join(\"\")\n .trim();\n }\n return \"\";\n}\n\nexport async function generateCommitMessage(\n diff: string,\n config: AppConfig,\n): Promise<string> {\n if (!config.apiKey) {\n throw new Error(\n \"LLM_API_KEY is not set. Please set it in .env or environment variables.\",\n );\n }\n\n const client = new OpenAI({\n apiKey: config.apiKey,\n baseURL: config.endpoint,\n });\n\n const response = await client.chat.completions.create({\n model: config.llm.model,\n temperature: config.llm.temperature,\n max_tokens: config.llm.max_output_tokens,\n messages: [\n { role: \"system\", content: SYSTEM_PROMPT },\n {\n role: \"user\",\n content: `以下是 Git diff 内容:\\n\\n${diff}`,\n },\n ],\n });\n\n const message = extractContent(response);\n if (!message) {\n throw new Error(\n `LLM returned an empty commit message. Response: ${JSON.stringify(response)}`,\n );\n }\n return message;\n}\n","import { execSync } from \"child_process\";\n\nexport function gitCommit(message: string, repoPath?: string): void {\n const cwd = repoPath || process.cwd();\n const escapedMessage = message.replace(/\"/g, '\\\\\"');\n execSync(`git commit -m \"${escapedMessage}\"`, { cwd, encoding: \"utf-8\" });\n}\n\nexport function gitAddAll(repoPath?: string): void {\n const cwd = repoPath || process.cwd();\n execSync(\"git add -A\", { cwd, encoding: \"utf-8\" });\n}\n","import { createInterface } from \"readline/promises\";\nimport { loadUserConfig, saveUserConfig } from \"./config\";\n\nfunction question(query: string): Promise<string> {\n const rl = createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n return rl.question(query).finally(() => rl.close());\n}\n\nexport async function runInit(): Promise<void> {\n console.log(\"LLM 配置初始化\\n\");\n\n const existing = loadUserConfig();\n\n const endpoint =\n (await question(\n `大模型链接 (API 地址) [${existing.endpoint || \"https://api.openai.com/v1\"}]: `,\n )).trim() || existing.endpoint || \"https://api.openai.com/v1\";\n\n const model =\n (await question(\n `模型名称 [${existing.llm?.model || \"deepseek-v4-flash\"}]: `,\n )).trim() || existing.llm?.model || \"deepseek-v4-flash\";\n\n const apiKey =\n (await question(\n `API Key [${existing.apiKey ? \"***\" : \"(必填)\"}]: `,\n )).trim() || existing.apiKey;\n\n if (!apiKey) {\n console.error(\"\\n错误:API Key 不能为空。\");\n process.exit(1);\n }\n\n saveUserConfig({ apiKey, endpoint, llm: { model, temperature: 0.7, max_input_tokens: 262144, max_output_tokens: 16384 } });\n console.log(\"\\n配置已保存到 ~/.grc/config.json\");\n}\n","import { loadConfig } from \"./config\";\nimport { getAllDiff, hasStagedChanges } from \"./git\";\nimport { generateCommitMessage } from \"./llm\";\nimport { gitCommit, gitAddAll } from \"./commit\";\nimport { runInit } from \"./init\";\n\nasync function runCommit() {\n const config = loadConfig();\n\n if (!config.apiKey) {\n console.error(\n \"Error: LLM_API_KEY is not set. Please set it in .env or environment variables.\",\n );\n process.exit(1);\n }\n\n if (!hasStagedChanges(config.git.repoPath)) {\n console.log(\"No staged changes found. Staging all changes...\");\n gitAddAll(config.git.repoPath);\n }\n\n const diff = getAllDiff(config.git.repoPath);\n if (!diff.trim()) {\n console.log(\"No changes to commit.\");\n process.exit(0);\n }\n\n console.log(\"Generating commit message...\\n\");\n const message = await generateCommitMessage(diff, config);\n console.log(`Commit message:\\n ${message}\\n`);\n\n gitCommit(message, config.git.repoPath);\n console.log(\"Commit successful!\");\n}\n\nasync function main() {\n const subcommand = process.argv[2];\n\n if (subcommand === \"init\") {\n await runInit();\n } else {\n await runCommit();\n }\n}\n\nmain().catch((err) => {\n console.error(\"Error:\", err.message);\n process.exit(1);\n});\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "git-repo-comitter",
3
- "version": "0.2.5",
3
+ "version": "0.3.1",
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",
7
7
  "bin": {
8
8
  "git-repo-comitter": "dist/index.cjs"
9
9
  },
10
+ "files": [
11
+ "dist"
12
+ ],
10
13
  "scripts": {
11
14
  "dev": "vite",
12
15
  "build": "tsc && vite build",
package/.env.example DELETED
@@ -1,3 +0,0 @@
1
- LLM_API_KEY=your-api-key-here
2
- LLM_ENDPOINT=https://api.openai.com/v1
3
- LLM_MODEL=deepseek-v4-flash
package/.eslintrc.cjs DELETED
@@ -1,19 +0,0 @@
1
- module.exports = {
2
- root: true,
3
- parser: "@typescript-eslint/parser",
4
- plugins: ["@typescript-eslint"],
5
- extends: [
6
- "eslint:recommended",
7
- "plugin:@typescript-eslint/recommended",
8
- "prettier",
9
- ],
10
- env: {
11
- node: true,
12
- es2020: true,
13
- },
14
- parserOptions: {
15
- ecmaVersion: 2020,
16
- sourceType: "module",
17
- },
18
- rules: {},
19
- };
@@ -1,34 +0,0 @@
1
- name: Publish Package
2
-
3
- on:
4
- push:
5
- tags:
6
- - "v*"
7
-
8
- permissions:
9
- id-token: write
10
- contents: read
11
-
12
- jobs:
13
- publish:
14
- runs-on: ubuntu-latest
15
- steps:
16
- - uses: actions/checkout@v6
17
-
18
- - uses: pnpm/action-setup@v6
19
- with:
20
- version: "10"
21
-
22
- - uses: actions/setup-node@v6
23
- with:
24
- node-version: "24"
25
- registry-url: "https://registry.npmjs.org"
26
- package-manager-cache: false
27
-
28
- - run: npm install -g npm@latest
29
-
30
- - run: pnpm install --frozen-lockfile
31
-
32
- - run: pnpm build
33
-
34
- - run: npm publish --provenance --access public
package/.prettierrc DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "semi": true,
3
- "singleQuote": false,
4
- "tabWidth": 2,
5
- "trailingComma": "all",
6
- "printWidth": 80
7
- }
package/config.yaml DELETED
@@ -1,16 +0,0 @@
1
- # Git Repo Committer 配置文件
2
-
3
- # 大模型配置
4
- llm:
5
- model: "gpt-4"
6
- temperature: 0.7
7
- max_tokens: 256
8
-
9
- # 生成风格模板
10
- style:
11
- language: "zh-CN"
12
- template: "conventional" # conventional | conventional-commit | custom
13
-
14
- # Git 目录路径(默认为当前目录)
15
- git:
16
- repoPath: "."
@@ -1,143 +0,0 @@
1
- # 项目初始化
2
-
3
- ## 项目概述
4
-
5
- 本项目是一个基于 TypeScript 的前端工具,用于调用大语言模型(采用 OpenAI 兼容格式),管理 Git 差异(diff),自动生成 Git 提交信息(commit message),并执行提交操作。
6
-
7
- ## 技术栈
8
-
9
- - 编程语言:TypeScript
10
- - 构建工具:Vite
11
- - 包管理器:npm
12
- - 代码格式化:Prettier + ESLint
13
- - Git 钩子:husky + lint-staged
14
-
15
- ## 功能需求
16
-
17
- ### 核心功能
18
-
19
- 1. **Git 差异管理**
20
- - 读取当前 Git 仓库的变更内容(diff)
21
- - 支持暂存区和工作区的差异提取
22
-
23
- 2. **大模型调用**
24
- - 调用外部大语言模型 API 生成提交信息(OpenAI 兼容格式)
25
- - 支持从环境变量读取模型配置:
26
- - `LLM_API_KEY`:API 密钥
27
- - `LLM_ENDPOINT`:服务地址(例如 `https://api.openai.com/v1`)
28
- - `LLM_MODEL`:模型名称(例如 `gpt-4`、`gpt-3.5-turbo`)
29
-
30
- 3. **配置系统**
31
- - 支持 YAML 配置文件(如 `.git-repo-committer.yaml`)
32
- - 可配置项包括:
33
- - 模型选择与参数(model、temperature、max_tokens 等)
34
- - 生成风格或模板
35
- - Git 目录路径
36
-
37
- 4. **Git 提交**
38
- - 自动执行 `git commit` 操作
39
- - 使用生成的提交信息
40
-
41
- ### 环境变量
42
-
43
- 项目将依赖以下环境变量:
44
-
45
- - `LLM_API_KEY`:大模型 API 密钥
46
- - `LLM_ENDPOINT`:大模型服务地址(OpenAI 兼容端点)
47
- - `LLM_MODEL`:模型名称
48
-
49
- ## 项目结构
50
-
51
- ```
52
- git-repo-committer/
53
- ├── src/
54
- │ ├── index.ts # 入口文件
55
- │ ├── git.ts # Git 差异处理
56
- │ ├── llm.ts # 大模型调用(OpenAI 兼容)
57
- │ ├── config.ts # 配置读取
58
- │ └── commit.ts # 提交管理
59
- ├── config.yaml # YAML 配置文件
60
- ├── .env # 环境变量文件(不提交)
61
- ├── .env.example # 环境变量示例
62
- ├── .eslintrc.cjs # ESLint 配置
63
- ├── .prettierrc # Prettier 配置
64
- ├── tsconfig.json # TypeScript 配置
65
- ├── vite.config.ts # Vite 配置
66
- ├── package.json # 包描述文件
67
- ├── README.md # 项目说明
68
- └── .gitignore # Git 忽略规则
69
- ```
70
-
71
- ## 初始化步骤
72
-
73
- 1. **创建项目目录并初始化 npm**
74
-
75
- ```bash
76
- mkdir git-repo-committer
77
- cd git-repo-committer
78
- npm init -y
79
- ```
80
-
81
- 2. **初始化 TypeScript 配置**
82
-
83
- ```bash
84
- npm install --save-dev typescript
85
- npx tsc --init
86
- ```
87
-
88
- 3. **安装 Vite 及相关依赖**
89
-
90
- ```bash
91
- npm install --save-dev vite @types/node
92
- ```
93
-
94
- 4. **安装代码格式化工具**
95
-
96
- ```bash
97
- npm install --save-dev prettier eslint eslint-config-prettier @typescript-eslint/parser @typescript-eslint/eslint-plugin
98
- ```
99
-
100
- 5. **安装 Git 钩子工具**
101
-
102
- ```bash
103
- npm install --save-dev husky lint-staged
104
- npx husky init
105
- ```
106
-
107
- 6. **安装运行时依赖**
108
-
109
- ```bash
110
- npm install yaml dotenv openai
111
- ```
112
-
113
- 7. **创建配置文件**
114
-
115
- - 创建 `.env.example` 和 `.env`(写入环境变量)
116
- - 创建 `config.yaml`(定义模型参数和模板)
117
- - 创建 `.eslintrc.cjs`、`.prettierrc` 配置规则
118
- - 创建 `vite.config.ts` 定义构建目标
119
-
120
- 8. **实现核心模块**
121
-
122
- - `src/config.ts`:读取 YAML 配置和 `.env` 环境变量
123
- - `src/git.ts`:执行 `git diff` 命令并解析结果
124
- - `src/llm.ts`:使用 OpenAI SDK 调用兼容 API
125
- - `src/commit.ts`:组装并执行 `git commit -m "..."`
126
-
127
- 9. **完善 package.json 元数据**
128
-
129
-
130
- ## 发布计划
131
-
132
- 项目完成后将发布到 npm registry,作为命令行工具(CLI)供开发者使用。发布步骤:
133
-
134
- 1. 确保 `package.json` 中已填入正确的 GitHub 仓库地址和元信息
135
- 2. 运行 `npm run build` 构建产物
136
- 3. 执行 `npm publish` 发布
137
-
138
- ## 注意事项
139
-
140
- - 确保 `.gitignore` 包含 `node_modules`、`dist`、`.env` 等目录
141
- - 环境变量文件 `.env` 不应提交到版本控制系统
142
- - 使用 `openai` 包时需配置 `baseURL` 为 `LLM_ENDPOINT` 的值
143
- - 代码提交前会通过 husky + lint-staged 自动执行 ESLint 和 Prettier 格式化
package/src/commit.ts DELETED
@@ -1,12 +0,0 @@
1
- import { execSync } from "child_process";
2
-
3
- export function gitCommit(message: string, repoPath?: string): void {
4
- const cwd = repoPath || process.cwd();
5
- const escapedMessage = message.replace(/"/g, '\\"');
6
- execSync(`git commit -m "${escapedMessage}"`, { cwd, encoding: "utf-8" });
7
- }
8
-
9
- export function gitAddAll(repoPath?: string): void {
10
- const cwd = repoPath || process.cwd();
11
- execSync("git add -A", { cwd, encoding: "utf-8" });
12
- }
package/src/config.ts DELETED
@@ -1,74 +0,0 @@
1
- import { readFileSync, existsSync } from "fs";
2
- import { resolve } from "path";
3
- import YAML from "yaml";
4
- import dotenv from "dotenv";
5
-
6
- dotenv.config();
7
-
8
- export interface LLMConfig {
9
- model: string;
10
- temperature: number;
11
- max_tokens: number;
12
- }
13
-
14
- export interface StyleConfig {
15
- language: string;
16
- template: string;
17
- }
18
-
19
- export interface GitConfig {
20
- repoPath: string;
21
- }
22
-
23
- export interface AppConfig {
24
- llm: LLMConfig;
25
- style: StyleConfig;
26
- git: GitConfig;
27
- apiKey: string;
28
- endpoint: string;
29
- }
30
-
31
- const DEFAULT_CONFIG: AppConfig = {
32
- llm: {
33
- model: "deepseek-v4-flash",
34
- temperature: 0.7,
35
- max_tokens: 262144,
36
- },
37
- style: {
38
- language: "zh-CN",
39
- template: "conventional",
40
- },
41
- git: {
42
- repoPath: ".",
43
- },
44
- apiKey: "",
45
- endpoint: "https://api.openai.com/v1",
46
- };
47
-
48
- export function loadConfig(configPath?: string): AppConfig {
49
- const configFile =
50
- configPath ||
51
- [".git-repo-committer.yaml", ".git-repo-committer.yml", "config.yaml"].find(
52
- (f) => existsSync(resolve(process.cwd(), f)),
53
- );
54
-
55
- let fileConfig: Partial<AppConfig> = {};
56
-
57
- if (configFile) {
58
- const content = readFileSync(resolve(process.cwd(), configFile), "utf-8");
59
- fileConfig = YAML.parse(content);
60
- }
61
-
62
- return {
63
- ...DEFAULT_CONFIG,
64
- ...fileConfig,
65
- llm: { ...DEFAULT_CONFIG.llm, ...(fileConfig.llm || {}) },
66
- style: { ...DEFAULT_CONFIG.style, ...(fileConfig.style || {}) },
67
- git: { ...DEFAULT_CONFIG.git, ...(fileConfig.git || {}) },
68
- apiKey: process.env.LLM_API_KEY || fileConfig.apiKey || "",
69
- endpoint:
70
- process.env.LLM_ENDPOINT ||
71
- fileConfig.endpoint ||
72
- DEFAULT_CONFIG.endpoint,
73
- };
74
- }
package/src/git.ts DELETED
@@ -1,47 +0,0 @@
1
- import { execSync } from "child_process";
2
-
3
- export function getStagedDiff(repoPath?: string): string {
4
- const cwd = repoPath || process.cwd();
5
- try {
6
- return execSync("git diff --cached", { cwd, encoding: "utf-8" });
7
- } catch {
8
- return "";
9
- }
10
- }
11
-
12
- export function getUnstagedDiff(repoPath?: string): string {
13
- const cwd = repoPath || process.cwd();
14
- try {
15
- return execSync("git diff", { cwd, encoding: "utf-8" });
16
- } catch {
17
- return "";
18
- }
19
- }
20
-
21
- export function getAllDiff(repoPath?: string): string {
22
- const cwd = repoPath || process.cwd();
23
- const staged = getStagedDiff(cwd);
24
- const unstaged = getUnstagedDiff(cwd);
25
-
26
- const parts: string[] = [];
27
- if (staged.trim()) {
28
- parts.push("=== Staged Changes ===\n" + staged);
29
- }
30
- if (unstaged.trim()) {
31
- parts.push("=== Unstaged Changes ===\n" + unstaged);
32
- }
33
- return parts.join("\n\n");
34
- }
35
-
36
- export function hasStagedChanges(repoPath?: string): boolean {
37
- const cwd = repoPath || process.cwd();
38
- try {
39
- const result = execSync("git diff --cached --name-only", {
40
- cwd,
41
- encoding: "utf-8",
42
- });
43
- return result.trim().length > 0;
44
- } catch {
45
- return false;
46
- }
47
- }
package/src/index.ts DELETED
@@ -1,40 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { loadConfig } from "./config";
4
- import { getAllDiff, hasStagedChanges } from "./git";
5
- import { generateCommitMessage } from "./llm";
6
- import { gitCommit, gitAddAll } from "./commit";
7
-
8
- async function main() {
9
- const config = loadConfig();
10
-
11
- if (!config.apiKey) {
12
- console.error(
13
- "Error: LLM_API_KEY is not set. Please set it in .env or environment variables.",
14
- );
15
- process.exit(1);
16
- }
17
-
18
- if (!hasStagedChanges(config.git.repoPath)) {
19
- console.log("No staged changes found. Staging all changes...");
20
- gitAddAll(config.git.repoPath);
21
- }
22
-
23
- const diff = getAllDiff(config.git.repoPath);
24
- if (!diff.trim()) {
25
- console.log("No changes to commit.");
26
- process.exit(0);
27
- }
28
-
29
- console.log("Generating commit message...\n");
30
- const message = await generateCommitMessage(diff, config);
31
- console.log(`Commit message:\n ${message}\n`);
32
-
33
- gitCommit(message, config.git.repoPath);
34
- console.log("Commit successful!");
35
- }
36
-
37
- main().catch((err) => {
38
- console.error("Error:", err.message);
39
- process.exit(1);
40
- });
package/src/llm.ts DELETED
@@ -1,45 +0,0 @@
1
- import OpenAI from "openai";
2
- import type { AppConfig } from "./config";
3
-
4
- const SYSTEM_PROMPT = `你是一位擅长编写 Git 提交信息的专家。
5
- 根据提供的 Git diff,生成简洁且描述准确的提交信息。
6
- 遵循 Conventional Commits 格式:<type>(<scope>): <description>
7
- 类型包括:feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
8
- 标题行控制在 72 个字符以内。
9
- 如果有重要变更,在空行后添加更详细的正文说明。
10
- 只回复提交信息本身,不要添加任何解释。`;
11
-
12
- export async function generateCommitMessage(
13
- diff: string,
14
- config: AppConfig,
15
- ): Promise<string> {
16
- if (!config.apiKey) {
17
- throw new Error(
18
- "LLM_API_KEY is not set. Please set it in .env or environment variables.",
19
- );
20
- }
21
-
22
- const client = new OpenAI({
23
- apiKey: config.apiKey,
24
- baseURL: config.endpoint,
25
- });
26
-
27
- const response = await client.chat.completions.create({
28
- model: config.llm.model,
29
- temperature: config.llm.temperature,
30
- max_tokens: config.llm.max_tokens,
31
- messages: [
32
- { role: "system", content: SYSTEM_PROMPT },
33
- {
34
- role: "user",
35
- content: `以下是 Git diff 内容:\n\n${diff}`,
36
- },
37
- ],
38
- });
39
-
40
- const message = response.choices[0]?.message?.content?.trim();
41
- if (!message) {
42
- throw new Error("LLM returned an empty commit message.");
43
- }
44
- return message;
45
- }
package/tsconfig.json DELETED
@@ -1,18 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "strict": true,
7
- "esModuleInterop": true,
8
- "skipLibCheck": true,
9
- "forceConsistentCasingInFileNames": true,
10
- "resolveJsonModule": true,
11
- "declaration": true,
12
- "outDir": "./dist",
13
- "rootDir": "./src",
14
- "sourceMap": true
15
- },
16
- "include": ["src/**/*"],
17
- "exclude": ["node_modules", "dist"]
18
- }
package/vite.config.ts DELETED
@@ -1,18 +0,0 @@
1
- import { defineConfig } from "vite";
2
- import { resolve } from "path";
3
-
4
- export default defineConfig({
5
- build: {
6
- lib: {
7
- entry: resolve(__dirname, "src/index.ts"),
8
- name: "git-repo-comitter",
9
- fileName: "index",
10
- formats: ["cjs"],
11
- },
12
- outDir: "dist",
13
- sourcemap: true,
14
- rollupOptions: {
15
- external: ["openai", "yaml", "dotenv"],
16
- },
17
- },
18
- });