code_review_llm_gpp_no_tool
Version:
Your AI code reviewer. Improve code quality and catch bugs before you break production
132 lines (94 loc) • 41.1 kB
JavaScript
var to=Object.create;var{getPrototypeOf:co,defineProperty:C,getOwnPropertyNames:so}=Object;var fo=Object.prototype.hasOwnProperty;var d=(e,r,o)=>{o=e!=null?to(co(e)):{};let i=r||!e||!e.__esModule?C(o,"default",{value:e,enumerable:!0}):o;for(let n of so(e))if(!fo.call(i,n))C(i,n,{get:()=>e[n],enumerable:!0});return i};var _=(e,r)=>{for(var o in r)C(e,o,{get:r[o],enumerable:!0,configurable:!0,set:(i)=>r[o]=()=>i})};var u=(e,r)=>()=>(e&&(r=e(e=0)),r);var Ee,c;var g=u(()=>{Ee=require("tslog"),c=new Ee.Logger({prettyLogTemplate:"{{logLevelName}}\t"})});var Ae=()=>{let e=process.env.OPENAI_API_KEY,r=process.env.AZURE_OPENAI_API_KEY;if(!e&&!r)c.error("Ни OPENAI_API_KEY, ни AZURE_OPENAI_API_KEY не установлены");return e??r??""},Ie=()=>{if(!process.env.GITHUB_TOKEN)c.error("Переменная GITHUB_TOKEN не установлена");return process.env.GITHUB_TOKEN??""},k=()=>{let e=["GITHUB_SHA","BASE_SHA","GITHUB_TOKEN"],r=[];for(let o of e)if(!process.env[o])r.push(o);if(r.length>0)throw c.error(`Отсутствуют переменные окружения: ${r.join(", ")}`),new Error("Одна или несколько переменных окружения GitHub не установлены");return{githubSha:process.env.GITHUB_SHA??"",baseSha:process.env.BASE_SHA??"",githubToken:process.env.GITHUB_TOKEN??""}},F=()=>{let e=["CI_MERGE_REQUEST_DIFF_BASE_SHA","CI_PROJECT_ID","CI_MERGE_REQUEST_IID","CI_COMMIT_SHA","GITLAB_TOKEN","GITLAB_HOST"].filter((r)=>!process.env[r]);if(e.length>0)throw c.error(`Отсутствуют переменные окружения: ${e.join(", ")}`),new Error("Одна или несколько переменных окружения GitLab не установлены. Установите токен доступа GitLab? См. README (раздел GitLab CI) для инструкций.");return{mergeRequestBaseSha:process.env.CI_MERGE_REQUEST_DIFF_BASE_SHA??"",gitlabSha:process.env.CI_COMMIT_SHA??"",gitlabToken:process.env.GITLAB_TOKEN??"",projectId:process.env.CI_PROJECT_ID??"",mergeRequestIIdString:process.env.CI_MERGE_REQUEST_IID??"",gitlabHost:process.env.GITLAB_HOST??"https://gitlab.com"}},T=()=>{let e=["SYSTEM_PULLREQUEST_SOURCECOMMITID","BASE_SHA","API_TOKEN"],r=[];for(let o of e)if(!process.env[o])r.push(o);if(r.length>0)throw c.error(`Отсутствуют переменные окружения: ${r.join(", ")}`),new Error("Одна или несколько переменных окружения Azure DevOps не установлены");return{azdevSha:process.env.SYSTEM_PULLREQUEST_SOURCECOMMITID??"",baseSha:process.env.BASE_SHA??"",azdevToken:process.env.API_TOKEN??""}};var I=u(()=>{g()});var R=()=>{};var ye,U=async(e)=>{let r=await ye.glob(e,{onlyFiles:!0});if(r.length===0)throw new Error(`Не найдены файлы шаблона по шаблону: ${e}`);return r[0]};var _e=u(()=>{ye=require("tinyglobby")});var Re={};_(Re,{configure:()=>uo});var G,E,N,ke,uo=async(e)=>{if(e.setupTarget==="github")await po();if(e.setupTarget==="gitlab")await lo();if(e.setupTarget==="azdev")await go()},Fe=async()=>{return await ke.password({message:"Пожалуйста, введите ваш ключ API OpenAI:"})},po=async()=>{let e=await U("**/templates/github-pr.yml"),r=N.default.join(process.cwd(),".github","workflows");E.default.mkdirSync(r,{recursive:!0});let o=N.default.join(r,"code-review-gpt.yml");E.default.writeFileSync(o,E.default.readFileSync(e,"utf8"),"utf8"),c.info(`Создан workflow GitHub Actions по пути: ${o}`);let i=await Fe();if(!i){c.error("Ключ API не предоставлен. Пожалуйста, вручную добавьте секрет OPENAI_API_KEY в репозиторий GitHub.");return}try{G.execSync("gh auth status || gh auth login",{stdio:"inherit"}),G.execSync(`gh secret set OPENAI_API_KEY --body=${String(i)}`),c.info("Секрет OPENAI_API_KEY успешно добавлен в репозиторий GitHub.")}catch(n){c.error("Похоже, GitHub CLI не установлен или произошла ошибка аутентификации. Не забудьте вручную добавить OPENAI_API_KEY в настройки репозитория/Environment/Actions/Repository Secrets.")}},lo=async()=>{let e=await U("**/templates/gitlab-pr.yml"),r=process.cwd(),o=N.default.join(r,".gitlab-ci.yml");E.default.writeFileSync(o,E.default.readFileSync(e,"utf8"),"utf8"),c.info(`Создан GitLab CI по пути: ${o}`);let i=await Fe();if(!i){c.error("Ключ API не предоставлен. Пожалуйста, вручную добавьте секрет OPENAI_API_KEY в переменные CI/CD репозитория GitLab.");return}try{G.execSync("glab auth login",{stdio:"inherit"}),G.execSync(`glab variable set OPENAI_API_KEY ${String(i)}`),c.info(`Секрет OPENAI_API_KEY успешно добавлен в репозиторий GitLab.
Пожалуйста, убедитесь, что токен доступа GitLab настроен перед использованием. См. README (раздел GitLab CI) для инструкций.`)}catch(n){c.error("Похоже, GitLab CLI не установлен или произошла ошибка аутентификации. Не забудьте вручную добавить OPENAI_API_KEY и GITLAB_TOKEN в переменные CI/CD репозитория. См. README (раздел GitLab CI) для настройки токена.")}},go=async()=>{let e=await U("**/templates/azdev-pr.yml"),r=process.cwd(),o=N.default.join(r,"code-review-gpt.yaml");E.default.writeFileSync(o,E.default.readFileSync(e,"utf8"),"utf8"),c.info(`Создан пайплайн Azure DevOps по пути: ${o}`),c.info("Пожалуйста, вручную добавьте секреты OPENAI_API_KEY и API_TOKEN как зашифрованные переменные в UI.")};var $e=u(()=>{G=require("child_process"),E=d(require("fs")),N=d(require("path")),ke=require("@inquirer/prompts");R();g();_e()});var M,mo=()=>{let e=["SYSTEM_TEAMFOUNDATIONCOLLECTIONURI","API_TOKEN","SYSTEM_PULLREQUEST_PULLREQUESTID","BUILD_REPOSITORY_ID","SYSTEM_TEAMPROJECTID"],r=[];for(let o of e)if(!process.env[o])r.push(o);if(r.length>0)throw c.error(`Отсутствуют переменные окружения: ${r.join(", ")}`),new Error("Одна или несколько переменных окружения Azure DevOps не установлены");return{serverUrl:process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI??"",azdevToken:process.env.API_TOKEN??"",pullRequestId:process.env.SYSTEM_PULLREQUEST_PULLREQUESTID??"",project:process.env.SYSTEM_TEAMPROJECTID??"",repositoryId:process.env.BUILD_REPOSITORY_ID??""}},Ge=async(e,r)=>{try{let{serverUrl:o,azdevToken:i,pullRequestId:n,repositoryId:t,project:s}=mo(),f=Number(n),p=M.getPersonalAccessTokenHandler(i),l=await new M.WebApi(o,p).getGitApi(),h={comments:[{content:`${e}
---
${r}`}]};await l.createThread(h,t,f,s)}catch(o){throw c.error(`Не удалось оставить комментарий в PR: ${JSON.stringify(o)}`),o}};var Ne=u(()=>{M=d(require("azure-devops-node-api"));g()});var ee=(e)=>{return e.map((r)=>`
${r.reasoning}
${r.suggestedChanges?`Предлагаемые изменения:
\`\`\`suggestion
${r.suggestedChanges}
\`\`\`
`:""}
<details>
<summary>Просмотреть оригинальный код</summary>
\`\`\`code
${r.targetCodeBlock}
\`\`\`
</details>
`).join(`
`)},ho=(e)=>`
**Уровень риска ${e.riskScore} - ${e.fileName}**
${ee(e.review)}
`,Oe=(e)=>`
${e.map(ho).join(`
---
`)}
`;var P,bo=(e,r)=>{let o=e.lastIndexOf(r);if(o!==-1)return e.slice(o+r.length+1);return e},re=()=>{let{githubToken:e}=k();if(!e)throw new Error("GITHUB_TOKEN не установлен");return e},Se=()=>{let e=re(),{payload:r,issue:o}=P.context;if(!r.pull_request){c.warn("Это не pull request. Пропускаем комментарий в PR...");return}let i=P.getOctokit(e),{owner:n,repo:t,number:s}=o;return{octokit:i,owner:n,repo:t,pull_number:s}},Te=async(e,r)=>{try{let o=`${ee(r.feedback.review)}
---
${r.signOff}`,{data:i}=await e.rest.pulls.listReviewComments({owner:r.owner,repo:r.repo,pull_number:r.pull_number}),n=bo(r.feedback.fileName,r.repo),t=i.find((s)=>s.path===n&&s.body.includes(r.signOff));if(t)await e.rest.pulls.updateReviewComment({owner:r.owner,repo:r.repo,comment_id:t.id,body:o});else await e.rest.pulls.createReviewComment({owner:r.owner,repo:r.repo,pull_number:r.pull_number,body:o,commit_id:r.commit_id,path:n,subject_type:"FILE"})}catch(o){c.error(`Не удалось оставить комментарий в PR для отзыва: ${r.feedback.review}. Ошибка: ${JSON.stringify(o)}`)}};var oe=u(()=>{P=require("@actions/github");I();g()});var K,B=async(e,r)=>{try{let o=re(),{payload:i,issue:n}=K.context;if(!i.pull_request){c.warn("Это не pull request. Пропускаем комментарий в PR...");return}let t=K.getOctokit(o),{owner:s,repo:f,number:p}=n,{data:a}=await t.rest.issues.listComments({owner:s,repo:f,issue_number:p}),l=a.find((w)=>w.body?.includes(r)),h=`${e}
---
${r}`;if(l)await t.rest.issues.updateComment({owner:s,repo:f,comment_id:l.id,body:h});else await t.rest.issues.createComment({owner:s,repo:f,issue_number:p,body:h})}catch(o){throw c.error(`Не удалось оставить комментарий в PR: ${JSON.stringify(o)}`),o}};var ie=u(()=>{K=require("@actions/github");g();oe()});var Ue=async(e,r)=>{let o=Se();if(o){let{octokit:i,owner:n,repo:t,pull_number:s}=o,p=(await i.rest.pulls.get({owner:n,repo:t,pull_number:s})).data.head.sha;for(let a of e)await Te(i,{feedback:a,signOff:r,owner:n,repo:t,pull_number:s,commit_id:p})}};var He=u(()=>{oe()});var Me,Y=async(e,r)=>{try{let{gitlabToken:o,projectId:i,mergeRequestIIdString:n,gitlabHost:t}=F(),s=Number.parseInt(n,10),f=new Me.Gitlab({token:o,host:t}),a=(await f.MergeRequestNotes.all(i,s)).find((h)=>h.body.includes(r)),l=`${e}
---
${r}`;if(a)await f.MergeRequestNotes.edit(i,s,a.id,{body:l});else await f.MergeRequestNotes.create(i,s,l)}catch(o){throw c.error(`Не удалось оставить комментарий в PR: ${JSON.stringify(o)}`),o}};var ne=u(()=>{Me=require("@gitbeaker/rest");I();g()});var O="#### Работает на [Code Review GPT](https://github.com/mattzcarey/code-review-gpt)",Pe,te,D,V,Ke=5;var y=u(()=>{Pe=[{model:"gpt-4o-mini",maxPromptLength:300000},{model:"gpt-4o",maxPromptLength:300000},{model:"gpt-4-turbo",maxPromptLength:300000},{model:"gpt-4-turbo-preview",maxPromptLength:300000},{model:"gpt-4",maxPromptLength:21000},{model:"gpt-4-32k",maxPromptLength:90000},{model:"gpt-3.5-turbo",maxPromptLength:9000},{model:"gpt-3.5-turbo-16k",maxPromptLength:45000}],te={".js":"JavaScript",".ts":"TypeScript",".py":"Python",".sh":"Shell",".go":"Go",".rs":"Rust",".tsx":"TypeScript",".jsx":"JavaScript",".dart":"Dart",".php":"PHP",".cpp":"C++",".h":"C++",".cxx":"C++",".hpp":"C++",".hxx":"C++",".cs":"C#",".rb":"Ruby",".kt":"Kotlin",".kts":"Kotlin",".java":"Java",".vue":"Vue",".tf":"Terraform",".hcl":"Terraform",".swift":"Swift"},D=new Set(Object.keys(te)),V=new Set(["types"])});var Be=65536,j=(e)=>{let r=Pe.find((o)=>o.model===e)?.maxPromptLength;if(!r)return c.warn(`Модель ${e} не найдена в предопределенном списке. Используется длина промпта по умолчанию (${Be} символов).`),Be;return r};var ce=u(()=>{y();g()});var se=`Вы - эксперт-разработчик на языке {ProgrammingLanguage}. Ваша задача - ревью набора pull request.
Вам даны список имен файлов и их частичное содержимое, но учтите, что у вас может не быть полного контекста кода.
Ревью только строки кода, которые были изменены (добавлены или удалены) в pull request. Код выглядит как вывод команды git diff. Удаленные строки начинаются с минуса (-), добавленные - с плюса (+). Другие строки добавлены для контекста, но их следует игнорировать в ревью.
Не хвалите и не комплиментируйте ничего. Фокусируйтесь только на негативных аспектах кода.
Начните ревью с оценки измененного кода по шкале риска, похожей на LOGAF, но от 1 до 5, где 1 - наименьший риск для кодовой базы при слиянии, а 5 - наивысший риск, который вероятно сломает что-то или будет небезопасным.
В отзыве фокусируйтесь на выявлении потенциальных багов, улучшении читаемости (если это проблема), очистке кода и максимизации производительности языка программирования. Немедленно отметьте как наивысший риск любые API-ключи или секреты в коде в открытом виде. Оценивайте изменения по принципам SOLID, если применимо.
Не комментируйте разбиение функций на меньшие, если это не огромная проблема. Также учтите, что используются библиотеки и техники, с которыми вы не знакомы, так что не комментируйте их, если не уверены в проблеме.
Используйте markdown-форматирование для деталей отзыва. Не включайте имя файла или уровень риска в детали отзыва.
Делайте детали отзыва краткими, точными и на {ReviewLanguage}. Если несколько похожих проблем, комментируйте только самые критичные.
Включайте краткие примеры кода в детали отзыва для предлагаемых изменений, если уверены в улучшениях. Используйте тот же язык программирования, что и в файле.
Если несколько улучшений, используйте нумерованный список для приоритета.
Отвечайте в валидном JSON, убедившись, что все специальные символы экранированы:
- Блоки кода экранируйте так: \`\`\`typescript\\nкод здесь\\n\`\`\`
- Обычные обратные кавычки экранируйте как \`
- Новые строки как \\n
- Двойные кавычки как \\\"
Убедитесь, что ответ можно разобрать с помощью JSON.parse().`,Ye=`Вы - эксперт-разработчик. Ваша задача - ревью кода.
Не хвалите и не комплиментируйте ничего. Фокусируйтесь только на негативных аспектах кода.
В отзыве фокусируйтесь на выявлении потенциальных багов, улучшении читаемости (если это проблема), очистке кода и максимизации производительности языка программирования. Немедленно отметьте как наивысший риск любые API-ключи или секреты в коде в открытом виде. Оценивайте изменения по принципам SOLID, если применимо.
Не комментируйте разбиение функций на меньшие, если это не огромная проблема. Также учтите, что используются библиотеки и техники, с которыми вы не знакомы, так что не комментируйте их, если не уверены в проблеме.
Используйте markdown-форматирование для деталей отзыва. Не включайте имя файла или уровень риска в детали отзыва.
Делайте детали отзыва краткими, точными. Если несколько похожих проблем, комментируйте только самые критичные.
Включайте краткие примеры кода в детали отзыва для предлагаемых изменений, если уверены в улучшениях. Используйте тот же язык программирования, что и в файле.
Если несколько улучшений, используйте нумерованный список для приоритета.
`;class S{constructor(e){switch(e.provider){case"openai":this.model=new J.ChatOpenAI({apiKey:e.apiKey,...(e.organization&&{organization:e.organization}),temperature:e.temperature,modelName:e.modelName,configuration:{baseURL:process.env.OPENAI_BASE_URL},modelKwargs:{tool_choice:"none"},toolChoice:"none"});break;case"azureai":this.model=new J.AzureChatOpenAI({temperature:e.temperature,modelKwargs:{tool_choice:"none"},toolChoice:"none"});break;case"bedrock":throw new Error("Провайдер Bedrock не реализован");default:throw new Error("Провайдер не поддерживается")}}async callModel(e){const res=await this.model.invoke(e,{timeout:20000});const chunk=Array.isArray(res.content)?res.content[0]:res.content;return typeof chunk==="string"?chunk:(chunk?.text??String(chunk??""))}async callStructuredModel(e,r){try{let i=await this.model.invoke(e+"\n\nОтветьте ТОЛЬКО валидным JSON, соответствующим схеме.",{timeout:20000});let parsedContent=wo(i.content);return parsedContent}catch(i){c.error("Ошибка разбора JSON, использование сырого вызова",i);let n=e.split("JSON.parse()."),t=JSON.parse(n[1]),s=[];for(let f of t){let a=(await this.model.invoke(Ye+`
Важно: В этом случае просто верните отзыв о коде как строку. Только отзыв, без дополнительного форматирования JSON и т.д. Код:
`+f.promptContent,{timeout:20000})).content;c.warn("Запасной ответ как строка",a);const reasoning=Array.isArray(a)?(a[0]?.text??String(a[0])):String(a);s.push({fileName:f.fileName,riskScore:3,confidence:5,review:[{targetCodeBlock:f.promptContent,reasoning}]})}return s}}}var J,wo=(e)=>{c.debug("Неразобранный JSON",e);let r=e.replace(/\\/g,"\\\\").replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/\t/g,"\\t").replace(/```/g,"\\`\\`\\`").replace(/`/g,"\\`").replace(/"/g,"\\\"").replace(/\f/g,"\\f").replace(/\b/g,"\\b").replace(/\u2028/g,"\\u2028").replace(/\u2029/g,"\\u2029");return c.debug("Экранированный JSON",r),JSON.parse(r)};var fe=u(()=>{J=require("@langchain/openai");g()});var v,vo,De;var Ve=u(()=>{v=require("zod"),vo=v.z.array(v.z.object({targetCodeBlock:v.z.string().describe("Точный блок кода, о котором отзыв, дословно. Не включайте другой текст или комментарии в блок кода. Не включайте ``` или другое форматирование."),suggestedChanges:v.z.string().describe("Полный блок кода с предлагаемыми изменениями, если это имеет смысл. Не включайте другой текст или комментарии в блок кода. Не включайте ``` или другое форматирование. Не включайте + или - для обозначения изменений.").optional(),reasoning:v.z.string().describe("Ревью блока кода и обоснование предлагаемых изменений (если есть). Обоснуйте изменения как ревьюер кода. Если изменений нет, просто напишите ревью блока кода.")})),De=v.z.object({fileName:v.z.string().describe("Имя файла с изменениями кода"),riskScore:v.z.number().describe("Оценка риска от 1 до 5, где 1 - наименьший риск для кодовой базы при слиянии, а 5 - наивысший риск, который вероятно сломает что-то или будет небезопасным"),review:vo,confidence:v.z.number().describe("Оценка уверенности от 1 до 5, где 1 - наименьшая уверенность, 5 - наивысшая. Низкая уверенность для случаев, когда вы не уверены в библиотеке или безопасности кода. Высокая - для объективно плохих практик.")})});class je{items;constructor(e=[]){this.items=e}enqueue(e,r){let o={priority:r,item:e};this.items.push(o),this.items.sort((i,n)=>i.priority-n.priority)}dequeue(){return this.items.shift()?.item}size(){return this.items.length}peek(){return this.items[0]}getItems(){return this.items.map((e)=>e.item)}}var Je;var ze=u(()=>{Je=je});var Eo=async(e)=>{try{return await e}catch(r){throw c.error("Ошибка обработки промпта",r),r}},Ao=(e)=>{let r=new Je,o=e.filter((i)=>i.riskScore>1&&i.confidence>3);for(let i of o)r.enqueue(i,i.riskScore*i.confidence);return r.getItems()},Io=(e)=>{return e.reduce((r,o)=>{if(o.status==="fulfilled")return r.concat(o.value);return r},[])},qe=async(e,r)=>{let o=r.map((t)=>e.callStructuredModel(t,De)),i=await Promise.allSettled(o.map(Eo)),n=Io(i);return Ao(n)};var Ze=u(()=>{g();Ve();ze()});var z=async(e,r,o,i,n)=>{c.info("Спрашиваем экспертов...");let t=new S({modelName:r,temperature:0,apiKey:o,organization:i,provider:n}),s=await qe(t,e);if(s.length===0)return{markdownReport:"Проблем не найдено в PR \uD83C\uDF89",feedbacks:[]};return{markdownReport:Oe(s),feedbacks:s}};var ae=u(()=>{fe();g();Ze()});var Qe,We=(e)=>{let r=Qe.extname(e);return te[r]||"Неизвестный язык"};var xe=u(()=>{Qe=require("path");y()});var yo=(e,r)=>{let o={},i=0,n=Number.POSITIVE_INFINITY,t=Number.NEGATIVE_INFINITY,s=new Map;for(let[f,p]of e.entries()){let a=p.trim(),l=s.get(a)||[];l.push(f),s.set(a,l)}for(let f of r){let p=f.substring(1).trim(),a=s.get(p);if(a?.length){let l=a.shift();if(l!==void 0)o[l]=f,i+=f.length+1,n=Math.min(n,l),t=Math.max(t,l)}}return{changedIndices:o,totalChangedLinesLength:i,minIndex:n,maxIndex:t}},_o=(e,r,o,i)=>{let n=Math.max(e-(i||0),0),t=Math.min(r+(i||0),o-1);return{start:n,end:t}},ko=(e,r,o,i)=>{let n=i,t=!0,s=!0,f=e,p=r;while(n>0&&(t||s)){if(t&&f>0){let a=o[f-1].length+1;if(a<=n)f--,n-=a;else t=!1}if(s&&p<o.length-1){let a=o[p+1].length+1;if(a<=n)p++,n-=a;else s=!1}if((f===0||!t)&&(p===o.length-1||!s))break;if(f===0)t=!1;if(p===o.length-1)s=!1}return{start:f,end:p}},Fo=(e,r,o,i)=>{let n=e>0?`...
`:"";for(let t=e;t<=r;t++)n+=`${o[t]||i[t]}
`;if(r<i.length-1)n+=`...
`;return n.trim()},q=(e,r,o)=>{return e.reduce((i,n)=>{let t=n.fileContent.split(`
`),s=n.changedLines.split(`
`),{changedIndices:f,totalChangedLinesLength:p,minIndex:a,maxIndex:l}=yo(t,s);if(p===0)return i;let h=r-p-n.fileName.length,{start:w,end:b}=_o(a,l,t.length,o);if(!o)({start:w,end:b}=ko(w,b,t,h));let L=Fo(w,b,f,t);return i.push({fileName:n.fileName,promptContent:L}),i},[])};var Xe=(e)=>e.fileName.length+e.promptContent.length;var $=(e,r)=>{let o=[],i=[],n=0;for(let t of e){let s=Xe(t);if(s>r)c.error(`Изменения в файле ${t.fileName} превышают максимальную длину промпта, рассмотрите модель с большим окном контекста. Пропускаем изменения файла...`);else if(n+s>r)o.push(i),i=[t],n=s;else i.push(t),n+=s}if(i.length>0)o.push(i);return o};var Z=u(()=>{g()});var Le=(e,r)=>{let o=q(e,r);return $(o,r)};var Ce=u(()=>{Z()});var er=(e,r)=>{let o=q(e,r,Ke);return $(o,r)};var rr=u(()=>{y();Z()});var or=(e,r)=>{let o=e.map((i)=>({fileName:i.fileName,promptContent:i.fileContent.split(`
`).map((n)=>`+${n}`).join(`
`)}));return $(o,r)};var ir=u(()=>{Z()});var Q=(e,r,o,i="Russian")=>{let n=r-se.length,t;switch(o){case"full":t=or(e,n);break;case"changed":t=Le(e,n);break;case"costOptimized":t=er(e,n);break;default:throw new Error(`Тип ревью ${o} не поддерживается. Используйте один из: full, changed, costOptimized.`)}let s=se.replace("{ProgrammingLanguage}",We(e[0].fileName)).replace("{ReviewLanguage}",i);return t.map((p)=>{return s+JSON.stringify(p)})};var ue=u(()=>{xe();Ce();rr();ir()});var nr,pe=(e)=>{return e.filter((o)=>{let i=nr.extname(o.fileName);return D.has(i)&&![...V].some((n)=>o.fileName.includes(n))&&o.changedLines.trim()!==""})};var tr=u(()=>{nr=require("path");y()});var cr=u(()=>{tr()});var sr={};_(sr,{review:()=>Ro});var Ro=async(e,r,o)=>{c.debug("Ревью начато."),c.debug(`Используемая модель: ${e.model}`),c.debug(`CI включено: ${e.ci??"ci не определено"}`),c.debug(`Комментарий на файл включен: ${String(e.commentPerFile)}`),c.debug(`Выбранный тип ревью: ${e.reviewType}`),c.debug(`Выбранная организация: ${e.org??"организация не определена"}`),c.debug(`Удаленный Pull Request: ${e.remote??"удаленный pull request не определен"}`);let{ci:i,commentPerFile:n,model:t,reviewType:s,org:f,provider:p,reviewLanguage:a}=e,l=pe(r);if(l.length===0){c.info("Нет файлов для ревью, завершаем ревью.");return}c.debug(`Файлы для ревью после фильтрации: ${l.map((no)=>no.fileName).toString()}`);let h=j(t),w=Q(l,h,s,a);c.debug(`Используемые промпты:
${w.toString()}`);let{markdownReport:b,feedbacks:L}=await z(w,t,o,f,p);if(c.debug(`Отчет в markdown:
${b}`),i==="github"){if(!n)await B(b,O);if(n)await Ue(L,O)}if(i==="gitlab")await Y(b,O);if(i==="azdev")await Ge(b,O);return b};var fr=u(()=>{Ne();ie();He();ne();ce();R();g();y();ae();ue();cr()});var ar,$o,Go=(e)=>{return!$o.includes(e)},No=(e)=>{let r=ar.extname(e);return D.has(r)&&![...V].some((o)=>e.includes(o))},ur=(e,r)=>{return Go(r)&&No(e)};var pr=u(()=>{ar=require("path");y();$o=["removed","unchanged"]});class le{client=new lr.Octokit({auth:Ie()});async fetchReviewFiles(e){let r=await this.client.paginate(this.client.rest.pulls.listFiles,{owner:e.owner,repo:e.repo,pull_number:e.prNumber});return await this.fetchPullRequestFiles(r)}async fetchPullRequestFiles(e){let r=[];for(let o of e){if(!ur(o.filename,o.status))continue;let i=await this.fetchPullRequestFile(o);r.push(i)}return r}async fetchPullRequestFile(e){let r=await this.fetchPullRequestFileContent(e.contents_url);return{fileName:e.filename,fileContent:r,changedLines:e.patch??""}}async fetchPullRequestFileContent(e){let r=await this.client.request(`GET ${e}`);if(Oo(r))return this.decodeBase64(r.data.content);throw new Error(`Неожиданный ответ от Octokit. Ответ: ${JSON.stringify(r)}.`)}decodeBase64(e){return Buffer.from(e,"base64").toString("utf-8")}}var lr,Oo=(e)=>typeof e==="object"&&e!==null&&("data"in e)&&typeof e.data==="object"&&e.data!==null&&("content"in e.data)&&typeof e.data.content==="string";var gr=u(()=>{lr=require("octokit");I();pr()});var dr=(e)=>{let[r,o,i,n,t]=e.split(/(\/|#)/),s=Number.parseInt(t);return{owner:r,repo:i,prNumber:s}};var mr={};_(mr,{getRemotePullRequestFiles:()=>So});var So=async(e)=>{let r=dr(e),o=new le;try{return await o.fetchReviewFiles(r)}catch(i){throw new Error(`Не удалось получить файлы удаленного Pull Request: ${JSON.stringify(i)}`)}};var hr=u(()=>{gr()});var br,To=(e)=>`"${e.replace(/(["$`\\])/g,"\\$1")}"`,Uo=(e,r)=>{let o=To(r);if(e==="github"){let{githubSha:i,baseSha:n}=k();return`git diff -U0 --diff-filter=AMRT ${n} ${i} ${o}`}if(e==="gitlab"){let{gitlabSha:i,mergeRequestBaseSha:n}=F();return`git diff -U0 --diff-filter=AMRT ${n} ${i} ${o}`}if(e==="azdev"){let{azdevSha:i,baseSha:n}=T();return`git diff -U0 --diff-filter=AMRT ${n} ${i} ${o}`}return`git diff -U0 --diff-filter=AMRT --cached ${o}`},wr=async(e,r)=>{let o=Uo(e,r);return new Promise((i,n)=>{br.exec(o,(t,s,f)=>{if(t)n(new Error(`Не удалось выполнить команду. Ошибка: ${t.message}`));else if(f)n(new Error(`Ошибка выполнения команды: ${f}`));else{let p=s.split(`
`).filter((a)=>a.startsWith("+")||a.startsWith("-")).filter((a)=>!(a.startsWith("---")||a.startsWith("+++"))).join(`
`);i(p)}})})};var vr=u(()=>{br=require("child_process");I();R()});var ge,Er,Ho=(e)=>{if(e==="github"){let r= k().githubSha, o= k().baseSha;return`git diff --name-only --diff-filter=AMRT ${o} ${r}`}if(e==="gitlab"){let r= F().gitlabSha, o= F().mergeRequestBaseSha;return`git diff --name-only --diff-filter=AMRT ${o} ${r}`}if(e==="azdev"){let r= T().azdevSha, o= T().baseSha;return`git diff --name-only --diff-filter=AMRT ${o} ${r}`}if(e===void 0)return"git diff --name-only --diff-filter=AMRT --cached";throw new Error("Недопустимая платформа CI")},Mo=()=>{return new Promise((e,r)=>{ge.exec("git rev-parse --show-toplevel",(o,i)=>{if(o)r(new Error(`Не удалось найти корень git. Ошибка: ${o.message}`));else e(i.trim())})})},Ar=async(e)=>{let r=await Mo();c.debug("gitRoot",r);let o=Ho(e);return c.debug("commandString",o),new Promise((i,n)=>{ge.exec(o,{cwd:r},(t,s,f)=>{if(t)n(new Error(`Не удалось выполнить команду. Ошибка: ${t.message}`));else if(f)n(new Error(`Ошибка выполнения команды: ${f}`));else{let p=s.split(`
`).filter((a)=>a.trim()!=="").map((a)=>Er.join(r,a.trim()));i(p)}})})};var Ir=u(()=>{ge=require("child_process"),Er=require("path");I();R();g()});var kr={};_(kr,{getFilesWithChanges:()=>Po});var yr,_r,Po=async(e)=>{try{let r=await Ar(e);if(c.debug("fileNames",r),r.length===0)c.warn("Не найдены файлы с изменениями, возможно, нужно заステージить изменения."),_r.exit(0);let o=await Promise.all(r.map(async(i)=>{let n=await yr.readFile(i,"utf8"),t=await wr(e,i);return c.debug("changedLines",t),{fileName:i,fileContent:n,changedLines:t}}));return c.debug("files",o),o}catch(r){throw new Error(`Не удалось получить файлы с изменениями: ${r.message}
${r.stack}`)}};var Fr=u(()=>{yr=require("fs/promises"),_r=require("process");g();vr();Ir()});var Rr={};_(Rr,{getReviewFiles:()=>Ko});var Ko=async(e,r)=>{if(r!==void 0){let{getRemotePullRequestFiles:i}=await Promise.resolve().then(() => (hr(),mr));return await i(r)}let{getFilesWithChanges:o}=await Promise.resolve().then(() => (Fr(),kr));return await o(e)};var $r=`
Ваша роль - помочь в тестировании приложения GPT для ревью изменений кода. Вы получаете тест-кейс и должны сгенерировать код на TypeScript, соответствующий этому тест-кейсу, даже если он следует плохим практикам или имеет проблемы безопасности.
Тест-кейс отформатирован как строка JSON-объекта со свойствами:
- name: имя тест-кейса
- description: описание тест-кейса
Входные данные:
{testCase}
Верните содержимое валидного файла TypeScript, который прошел бы тест-кейс.
`,de=0.1,me="#### Тесты работают на [Code Review GPT](https://github.com/mattzcarey/code-review-gpt)";var Gr,Nr,Or=async(e)=>{let r=new Gr.OpenAIEmbeddings;return await Nr.MemoryVectorStore.fromDocuments(e,r)};var Sr=u(()=>{Gr=require("@langchain/openai"),Nr=require("langchain/vectorstores/memory")});var Tr,Ur,Hr,Bo=async(e)=>{return await new Hr.TextLoader(e).load()},Mr=async(e)=>{let r=Tr.readdirSync(e),o=await Promise.all(r.map(async(i)=>{return Bo(Ur.default.join(e,i))}));return await Or(o.flat())};var Pr=u(()=>{Tr=require("fs"),Ur=d(require("path")),Hr=require("langchain/document_loaders/fs/text");Sr()});var Kr,W,Yo=(e)=>typeof e==="object"&&e!==null&&("name"in e)&&typeof e.name==="string"&&("description"in e)&&typeof e.description==="string",Do=async(e)=>{try{let r=await W.readFile(e,"utf8"),o=JSON.parse(r);if(!Yo(o))throw new Error("Данные файла в неожиданном формате.");return o}catch(r){throw c.error(`Ошибка загрузки тест-кейса: ${e}`),r}},Br=async(e)=>{try{let r=(await W.readdir(e)).filter((o)=>o.endsWith(".json"));return Promise.all(r.map(async(o)=>await Do(Kr.default.join(e,o))))}catch(r){throw c.error(`Ошибка загрузки тест-кейсов из: ${e}`),r}};var Yr=u(()=>{Kr=d(require("path")),W=require("fs/promises");g()});var Dr,Vo="sha256",Vr=(e)=>{return Dr.default.createHash(Vo).update(e).digest("hex")};var jr=u(()=>{Dr=d(require("crypto"))});var x,Jr,jo=async(e,r)=>{let o=$r.replace("{testCase}",JSON.stringify(e));return(await r.callModel(o)).replace("```typescript","").replace("```","")},Jo=async(e,r,o)=>{if(e.snippet)return e;let i=Vr(e.description),n=Jr.default.join(r,`${i}.ts`);try{let t=x.readFileSync(n,"utf8");return{...e,snippet:{fileName:n,fileContent:t,changedLines:t}}}catch(t){c.info(`Сниппет не найден в кэше: ${e.name}. Генерируем...`);let s=await jo(e,o);return x.writeFileSync(n,s,"utf8"),{...e,snippet:{fileName:n,fileContent:s,changedLines:s}}}},zr=async(e,r,o)=>{return Promise.all(e.map((i)=>Jo(i,r,o)))};var qr=u(()=>{x=require("fs"),Jr=d(require("path"));g();jr()});var A,Zr,zo=(e)=>{if(e>1-de)return"PASS";if(e>1-2*de)return"WARN";return"FAIL"},Qr=(e,r)=>{switch(e){case"PASS":return A.default.green(`✅ [PASS] - ${r}`);case"WARN":return A.default.yellow(`⚠️ [WARN] - ${r}`);case"FAIL":return A.default.red(`❌ [FAIL] - ${r}`)}},Wr=(e,r,o,i)=>{let n=zo(i),t=n!=="PASS",s=Qr(n,`Тест-кейс: ${e.name} - Оценка схожести: ${i}
`)+(t?qo(e,r,o):"");return{result:n,report:s}},qo=(e,r,o)=>`
> Сниппет тест-кейса: ${JSON.stringify(e.snippet)}
===============================================================================
> Ревью:
${r}
===============================================================================
> Похожее ревью:
${o}
`,xr=(e)=>{let r=Object.entries(e).reduce((i,[n,t])=>{return`${i+Qr(t,`Тест-кейс: ${n}`)}
`},A.default.blue(`
### Сводка результатов тестов:
`)),o=Object.values(e).reduce((i,n)=>{return i[n]++,i},Object.fromEntries(Object.values(Zr).map((i)=>[i,0])));return`${r}
**СВОДКА: ${A.default.green(`✅ ПРОЙДЕНО: ${o.PASS}`)} - ${A.default.yellow(`⚠️ ПРЕДУПРЕЖДЕНИЕ: ${o.WARN}`)} - ${A.default.red(`❌ НЕУДАЧА: ${o.FAIL}`)}**
`};var Xr=u(()=>{A=d(require("picocolors"));((i)=>{i.PASS="PASS";i.WARN="WARN";i.FAIL="FAIL"})(Zr||={})});var Lr,Zo=async(e,r,o,i,n,t,s)=>{if(!r.snippet)throw new Error(`Тестовый случай ${r.name} не имеет фрагмента.`);c.info(Lr.default.blue(`Запуск тестового случая ${r.name}...`));let f=Q([r.snippet],i,t,s),{markdownReport:p}=await z(f,o,e,void 0,"openai"),a=await n.similaritySearchWithScore(p,1);if(a.length===0)throw new Error(`Не найдено похожих обзоров для тестового случая ${r.name}.`);let[l,h]=a[0],{result:w,report:b}=Wr(r,p,l.pageContent,h);return c.info(b),w},Cr=async(e,r,o,i,n,t,s)=>{if(r.length===0)return"Тестовые случаи не найдены.";c.info(`Запуск ${r.length} тестовых случаев...
`);let f={};for(let a of r)try{let l=await Zo(e,a,o,i,n,t,s);f[a.name]=l}catch(l){c.error(`Ошибка при запуске тестового случая ${a.name}:`,l)}let p=xr(f);return c.info(p),p};var eo=u(()=>{Lr=d(require("picocolors"));g();ae();ue();Xr()});var ro={};_(ro,{test:()=>Qo});var X,__dirname="/home/alex/playground/code-review-gpt/src/test",Qo=async({ci:e,model:r,reviewType:o,reviewLanguage:i},n)=>{let t=j(r),s=await Br(X.default.join(__dirname,"cases")),f=await zr(s,X.default.join(__dirname,"cases/.cache"),new S({modelName:r,temperature:0,apiKey:n,organization:void 0,provider:"openai"})),p=await Mr(X.default.join(__dirname,"cases/snapshots")),a=await Cr(n,f,r,t,p,o,i);if(e==="github")await B(a,me);if(e==="gitlab")await Y(a,me)};var oo=u(()=>{X=d(require("path"));ie();ne();fe();ce();R();Pr();Yr();qr();eo()});var io=d(require("dotenv"));var ao=d(require("@inquirer/rawlist")),he=d(require("dotenv")),be=d(require("yargs")),we=require("yargs/helpers");he.default.config();var ve=async()=>{return be.default(we.hideBin(process.argv)).command("configure","Настроить инструмент").command("review","Просмотреть изменения кода").command("test","Запустить тесты").demandCommand(1,"Пожалуйста, укажите команду: configure, review или test").option("ci",{description:"Тип среды CI",choices:["github","gitlab","azdev"],type:"string",coerce:(e)=>e||"github"}).option("setupTarget",{description:"Указывает, для какой платформы ('github', 'gitlab' или 'azdev') следует настроить проект. По умолчанию 'github'.",choices:["github","gitlab","azdev"],type:"string",default:"github"}).option("commentPerFile",{description:"Включает обратную связь по файлам по отдельности. Работает только при запуске скрипта на GitHub.",type:"boolean",default:!1}).option("model",{description:"Модель, которую следует использовать для генерации обзора.",type:"string",default:"gpt-4o-mini"}).option("reviewType",{description:"Тип обзора для выполнения. 'full' просмотрит весь файл, 'changed' просмотрит только измененные строки, но предоставит весь файл как контекст, если возможно. 'costOptimized' просмотрит только измененные строки, используя минимальное количество токенов для снижения затрат на API. По умолчанию 'changed'.",choices:["full","changed","costOptimized"],type:"string",default:"changed"}).option("reviewLanguage",{description:"Указывает целевой естественный язык для перевода",type:"string"}).option("remote",{description:"Идентификатор удаленного Pull Request для обзора",type:"string",coerce:(e)=>{return e||""}}).option("debug",{description:"Включает отладочное логирование.",type:"boolean",default:!1}).option("org",{description:"Идентификатор организации для использования в openAI",type:"string",default:void 0}).option("provider",{description:"Провайдер для использования в AI",choices:["openai","azureai","bedrock"],type:"string",default:"openai"}).help().parse()};g();I();io.default.config();var Wo=async()=>{let e=await ve(),r=Ae();switch(c.settings.minLevel=e.debug?2:e.ci?4:3,c.debug(`Аргументы: ${JSON.stringify(e)}`),e._[0]){case"configure":{let{configure:o}=await Promise.resolve().then(() => ($e(),Re));await o(e);break}case"review":{let{review:o}=await Promise.resolve().then(() => (fr(),sr)),{getReviewFiles:i}=await Promise.resolve().then(() => Rr),n=await i(e.ci,e.remote);await o(e,n,r);break}case"test":{let{test:o}=await Promise.resolve().then(() => (oo(),ro));await o(e,r);break}default:c.error("Неизвестная команда"),process.exit(1)}};Wo().catch((e)=>{let r=e instanceof Error?e.message:"Произошла неизвестная ошибка",o=e instanceof Error?e.stack:"Трассировка стека недоступна";if(c.error(`Ошибка: ${r}`),o)c.debug(`Трассировка стека: ${o}`);process.exit(1)});