UNPKG

gchcg-cli

Version:
1 lines 8.21 kB
const Command=require("./command"),{execSync:execSync}=require("child_process"),axios=require("axios"),log=require("./log"),inquirer=require("inquirer").default,SimpleGit=require("simple-git"),path=require("path"),fs=require("fs"),fse=require("fs-extra"),{readFile:readFile,writeFile:writeFile,spinnerStart:spinnerStart,sleep:sleep}=require("./utils"),CONFIG_NAME="commit.json",BASE_URL="https://openrouter.ai/api/v1",TOKEN="",MODEL="google/gemini-2.0-flash-thinking-exp:free";class CommitCommand extends Command{async init(){this.baseUrl=this._cmd.baseUrl,this.setToken=this._cmd.setToken,this.setModel=this._cmd.setModel,this.files=this._cmd.files,this.context=this._cmd.context,this.skip=this._cmd.skip,this.config={},this.configPath=path.resolve(process.env.CLI_HOME_PATH,CONFIG_NAME)}async exec(){const e=(new Date).getTime();try{if(this.prepare(),this.baseUrl)return await this.setBaseUrlAction(),void log.verbose("本次执行指令耗时:"+((new Date).getTime()-e)/1e3+"秒");if(this.setToken)return await this.setTokenAction(),void log.verbose("本次执行指令耗时:"+((new Date).getTime()-e)/1e3+"秒");if(this.setModel)return await this.setModelAction(),void log.verbose("本次执行指令耗时:"+((new Date).getTime()-e)/1e3+"秒");if(!this.config.baseUrl)throw new Error("baseUrl 未设置");if(!this.config.token){if(!/^https?:\/\/localhost/.test(this.config.baseUrl))throw new Error("token 必须设置, 请执行:cg commit --setToken");log.warn("token 为空,请执行:cg commit --setToken")}if(!this.config.model)throw new Error("model 未设置, 请执行:cg commit --setModel");log.verbose(`当前使用模型:${this.config.model}`);const t=require("child_process").execSync("git config user.name").toString().trim();if(!t)throw new Error('git用户名不存在,请先执行git config --global user.name "Your Name"');if(!this.skip){const e=spinnerStart("正在 git add ...");await sleep();try{execSync(`git add ${Array.isArray(this.files)&&this.files.length?this.files.join(" "):"."}`),e.stop(!0),log.success("git add 成功")}catch(t){e.stop(!0)}}const i=this.getDiff();await this.commitAction(t,i),await this.pushAction(t)}catch(e){log.error(e.message)}log.verbose("本次执行指令耗时:"+((new Date).getTime()-e)/1e3+"秒")}async setBaseUrlAction(){const e=this.configPath,t={type:"text",name:"baseUrl",message:`请输入baseUrl(支持http,https,localhost, 默认:${BASE_URL}):`,default:this.config.baseUrl===BASE_URL?"":this.config.baseUrl,validate:e=>!(!e.startsWith("http")&&!e.startsWith("https"))||"baseUrl 格式错误,请输入正确的 url"};let i="";for(;!i;){i=(await inquirer.prompt(t)).baseUrl}this.config.baseUrl=i,writeFile(e,JSON.stringify(this.config,null,2))}async setTokenAction(){const e=this.configPath,t={type:"text",name:"token",message:"请输入Token(本地localhost时,token不需要设置):",default:""};let i="";for(;!i;){i=(await inquirer.prompt(t)).token}let s=spinnerStart("检查 token 是否正确...");await sleep();try{await axios.get(`${this.config.baseUrl}/models`,{headers:{Authorization:`Bearer ${i}`}}),s.stop(!0),this.config.token=i,log.success("token 设置成功")}catch(e){s.stop(!0),log.error("token 设置失败")}if(!this.config.token)throw new Error(`${this.config.baseUrl}的 token 无效`);writeFile(e,JSON.stringify(this.config,null,2))}async setModelAction(){const e=this.configPath,t={type:"text",name:"model",message:`请输入模型名称(默认${MODEL}):`,default:this.config.model};let i="";for(;!i;){i=(await inquirer.prompt(t)).model}let s=spinnerStart("检查模型是否存在...");await sleep();try{await axios.get(`${this.config.baseUrl}/models/${i}`,{headers:{Authorization:`Bearer ${this.config.token}`}}),s.stop(!0),this.config.model=i,log.success("模型设置成功")}catch(e){s.stop(!0),log.error(`${i}模型不存在`)}if(writeFile(e,JSON.stringify(this.config,null,2)),!this.config.model)throw new Error(`${i}模型不存在`)}getDiff(){try{const e=execSync("git diff --staged").toString();return e.length>2e3?(log.warn("更新内容太多,简要统计信息。"),execSync("git diff --staged --stat").toString()):e}catch(e){return log.warn("更新内容太多,简要统计信息。"),execSync("git diff --staged --stat").toString()}}formatMessage(e,t){let i="";if(i=e.startsWith("<think>")&&e.includes("</think>")?e.split("</think>")[1].trim():e.trim(),this.config.model.includes("deepseek")&&i.includes("```")){const e=i.split("```").filter((e=>""!==e));i="```"+e.slice(0,1)+"```"}i.startsWith("```")&&i.endsWith("```")&&(i=i.split(/\r\n |\r|\n/g).slice(1).join("\n"),i=i.slice(0,-4)),(i.startsWith("---")||i.endsWith("---"))&&(i=i.split(/\r\n |\r|\n/g).filter((e=>!e.startsWith("---"))).join("\n"));const s=i.split("\n");let o=0;return s.forEach(((e,i)=>{i&&!o&&(t.test(e)?(s[i]=e.replace(t,""),o=i):e.startsWith("/^((feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)")&&(s[i]="",o=i))})),o&&(i=s.slice(0,o).join("\n")),i}async commitAction(e,t){const i=SimpleGit(this.projectInfo.dir),s=await i.status();if(!(s.not_added.length>0||s.created.length>0||s.deleted.length>0||s.modified.length>0||s.renamed.length>0))return log.warn("没有需要commit的文件变更"),Promise.resolve();const o=spinnerStart("正在生成commit message...");await sleep();try{const s=["feat","fix","docs","style","refactor","perf","test","build","ci","chore","revert"];let n="";this.config.model.includes("deepseek")&&(n="禁止生成如:该提交信息遵循以下规范...、该提交信息遵循以下格式...、等字样");const r=new RegExp(`^((${s.join("|")})\\(${e}\\)):[\\s\\S]*$`),a=[{role:"system",content:`您是一个中文提交消息生成器,通过diff字符串创建一个提交消息,而不添加不必要的信息!以下是https://karma-runner.github.io/6.4/dev/git-commit-msg.html指南中一个好的中文提交消息的格式:\n \n ---\n <type>(${e}): <subject>\n <BLANK LINE>\n <body>\n ---\n \n 允许的< type >值有${s.join("、")}。生成格式正则校验:${r}。这里有一个很好的中文提交消息的例子:\n \n ---sh\n fix(${e}): 确保范围标题更加符合RFC 2616\n \n 添加一个新的依赖项,使用 range-parser ( Express dependency)计算范围。它在野外更经得起考验。\n ---${n?`\n \n 按照这个指示 "${n}"!`:""}`},{role:"user",content:t}],c=await axios.post(`${this.config.baseUrl}/chat/completions`,{model:this.config.model,messages:a},{headers:{Authorization:`Bearer ${this.config.token}`,"Content-Type":"application/json"}});o.stop(!0);const l=this.formatMessage(c.data.choices[0].message.content,r);log.success(`AI 生成的内容:\n${l}`);const h=spinnerStart("正在提交 commit message ...");await sleep();try{await i.commit(l),h.stop(!0),log.success("commit 提交成功")}catch(e){throw h.stop(!0),new Error(e?.message)}}catch(e){throw o.stop(!0),this.skip||execSync("git reset"),new Error(`commit message失败:\n${e.message}`)}}async pushAction(){const e=SimpleGit(this.projectInfo.dir),t=await e.status();if(t.behind)return log.warn("远程分支领先本地分支:"+t.behind+"。请拉取代码:cg pull"),Promise.resolve();let i=spinnerStart(`正在 push 到${t.current}分支...`);await sleep();try{await e.push("origin",t.current),i.stop(!0),log.success("代码推送成功")}catch(e){throw i.stop(!0),new Error(`代码推送失败: \n${e.message}`)}}prepare(){const e=process.cwd(),t=path.resolve(e,"package.json");if(!fs.existsSync(t))throw new Error("package.json不存在!");const i=fse.readJsonSync(t),{name:s,version:o}=i;if(log.verbose("package.json 检查必填信息 name, version"),!s||!o)throw new Error("package.json信息不全,请检查是否存在name、version!");const n=this.configPath;try{const e=JSON.parse(readFile(n));Object.keys(e).length>0?this.config=e:this.config={}}catch(e){this.config={}}this.config.baseUrl||(this.config.baseUrl=BASE_URL),this.config.token||(this.config.token=""),this.config.model||(this.config.model=MODEL),writeFile(n,JSON.stringify(this.config,null,2)),this.projectInfo={name:s,version:o,dir:e}}}function init(e){return new CommitCommand(e)}module.exports=init,module.exports.CommitCommand=CommitCommand;