@zjex/git-workflow 0.4.7 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +44 -6
  3. package/dist/index.js +614 -11
  4. package/docs/.vitepress/cache/deps/_metadata.json +10 -10
  5. package/docs/.vitepress/config.ts +2 -0
  6. package/docs/commands/index.md +4 -0
  7. package/docs/commands/review.md +142 -0
  8. package/docs/guide/ai-review.md +159 -0
  9. package/docs/guide/index.md +2 -0
  10. package/docs/index.md +26 -3
  11. package/package.json +1 -1
  12. package/src/commands/review.ts +759 -0
  13. package/src/index.ts +29 -1
  14. package/tests/review.test.ts +1058 -0
  15. package/docs/.vitepress/cache/deps_temp_44e2fb0f/chunk-2CLQ7TTZ.js +0 -9719
  16. package/docs/.vitepress/cache/deps_temp_44e2fb0f/chunk-2CLQ7TTZ.js.map +0 -7
  17. package/docs/.vitepress/cache/deps_temp_44e2fb0f/chunk-LE5NDSFD.js +0 -12824
  18. package/docs/.vitepress/cache/deps_temp_44e2fb0f/chunk-LE5NDSFD.js.map +0 -7
  19. package/docs/.vitepress/cache/deps_temp_44e2fb0f/package.json +0 -3
  20. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vue_devtools-api.js +0 -4505
  21. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vue_devtools-api.js.map +0 -7
  22. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vueuse_core.js +0 -583
  23. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vueuse_core.js.map +0 -7
  24. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vueuse_integrations_useFocusTrap.js +0 -1352
  25. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___@vueuse_integrations_useFocusTrap.js.map +0 -7
  26. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___mark__js_src_vanilla__js.js +0 -1665
  27. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___mark__js_src_vanilla__js.js.map +0 -7
  28. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___minisearch.js +0 -1813
  29. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vitepress___minisearch.js.map +0 -7
  30. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vue.js +0 -347
  31. package/docs/.vitepress/cache/deps_temp_44e2fb0f/vue.js.map +0 -7
package/dist/index.js CHANGED
@@ -407,7 +407,7 @@ var init_update_notifier = __esm({
407
407
  // src/index.ts
408
408
  init_utils();
409
409
  import { cac } from "cac";
410
- import { select as select10 } from "@inquirer/prompts";
410
+ import { select as select11 } from "@inquirer/prompts";
411
411
  import { ExitPromptError } from "@inquirer/core";
412
412
 
413
413
  // src/commands/branch.ts
@@ -3602,6 +3602,589 @@ fi
3602
3602
  }
3603
3603
  }
3604
3604
 
3605
+ // src/commands/review.ts
3606
+ init_utils();
3607
+ import { select as select10, checkbox as checkbox2 } from "@inquirer/prompts";
3608
+ import ora7 from "ora";
3609
+ import { writeFileSync as writeFileSync5, existsSync as existsSync5, mkdirSync } from "fs";
3610
+ import { join as join6 } from "path";
3611
+ var AI_PROVIDERS2 = {
3612
+ github: {
3613
+ name: "GitHub Models",
3614
+ endpoint: "https://models.github.ai/inference/chat/completions",
3615
+ defaultModel: "gpt-4o"
3616
+ },
3617
+ openai: {
3618
+ name: "OpenAI",
3619
+ endpoint: "https://api.openai.com/v1/chat/completions",
3620
+ defaultModel: "gpt-4o"
3621
+ },
3622
+ claude: {
3623
+ name: "Claude",
3624
+ endpoint: "https://api.anthropic.com/v1/messages",
3625
+ defaultModel: "claude-3-5-sonnet-20241022"
3626
+ },
3627
+ ollama: {
3628
+ name: "Ollama",
3629
+ endpoint: "http://localhost:11434/api/generate",
3630
+ defaultModel: "qwen2.5-coder:14b"
3631
+ }
3632
+ };
3633
+ function getRecentCommits3(limit = 20) {
3634
+ try {
3635
+ const output = execOutput(
3636
+ `git log -${limit} --pretty=format:"%H|%h|%s|%an|%ad" --date=short`
3637
+ );
3638
+ if (!output) return [];
3639
+ return output.split("\n").filter(Boolean).map((line) => {
3640
+ const [hash, shortHash, subject, author, date] = line.split("|");
3641
+ return { hash, shortHash, subject, author, date };
3642
+ });
3643
+ } catch {
3644
+ return [];
3645
+ }
3646
+ }
3647
+ function getStagedDiff() {
3648
+ try {
3649
+ const diff = execOutput("git diff --cached");
3650
+ if (diff) return diff;
3651
+ return execOutput("git diff") || "";
3652
+ } catch {
3653
+ return "";
3654
+ }
3655
+ }
3656
+ function getCommitDiff(hash) {
3657
+ try {
3658
+ return execOutput(`git show ${hash} --format="" --patch`) || "";
3659
+ } catch {
3660
+ return "";
3661
+ }
3662
+ }
3663
+ function getMultipleCommitsDiff(hashes) {
3664
+ if (hashes.length === 0) return "";
3665
+ if (hashes.length === 1) return getCommitDiff(hashes[0]);
3666
+ const oldest = hashes[hashes.length - 1];
3667
+ const newest = hashes[0];
3668
+ try {
3669
+ return execOutput(`git diff ${oldest}^..${newest}`) || "";
3670
+ } catch {
3671
+ return hashes.map((h) => getCommitDiff(h)).join("\n\n");
3672
+ }
3673
+ }
3674
+ function parseDiff(diff) {
3675
+ const files = [];
3676
+ const fileDiffs = diff.split(/^diff --git /m).filter(Boolean);
3677
+ for (const fileDiff of fileDiffs) {
3678
+ const lines = fileDiff.split("\n");
3679
+ const headerMatch = lines[0]?.match(/a\/(.+) b\/(.+)/);
3680
+ if (!headerMatch) continue;
3681
+ const oldPath = headerMatch[1];
3682
+ const newPath = headerMatch[2];
3683
+ let status = "M";
3684
+ if (fileDiff.includes("new file mode")) status = "A";
3685
+ else if (fileDiff.includes("deleted file mode")) status = "D";
3686
+ else if (fileDiff.includes("rename from")) status = "R";
3687
+ files.push({
3688
+ oldPath,
3689
+ newPath,
3690
+ status,
3691
+ diff: "diff --git " + fileDiff
3692
+ });
3693
+ }
3694
+ return files;
3695
+ }
3696
+ function getDiffStats(diff) {
3697
+ const lines = diff.split("\n");
3698
+ let additions = 0;
3699
+ let deletions = 0;
3700
+ for (const line of lines) {
3701
+ if (line.startsWith("+") && !line.startsWith("+++")) {
3702
+ additions++;
3703
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
3704
+ deletions++;
3705
+ }
3706
+ }
3707
+ const files = parseDiff(diff).length;
3708
+ return { additions, deletions, files };
3709
+ }
3710
+ function buildSystemPrompt(language) {
3711
+ const isZh = language === "zh-CN";
3712
+ if (isZh) {
3713
+ return `\u4F60\u662F\u4E00\u4E2A\u8D44\u6DF1\u7684\u4EE3\u7801\u5BA1\u67E5\u4E13\u5BB6\uFF0C\u62E5\u6709\u4E30\u5BCC\u7684\u8F6F\u4EF6\u5F00\u53D1\u7ECF\u9A8C\u3002\u4F60\u7684\u4EFB\u52A1\u662F\u5BA1\u67E5 Git \u63D0\u4EA4\u4E2D\u7684\u4EE3\u7801\u53D8\u66F4\uFF0C\u63D0\u4F9B\u4E13\u4E1A\u3001\u6709\u4EF7\u503C\u3001\u6709\u5EFA\u8BBE\u6027\u7684\u5BA1\u67E5\u610F\u89C1\u3002
3714
+
3715
+ ## \u5BA1\u67E5\u539F\u5219
3716
+
3717
+ 1. **\u91CD\u70B9\u5173\u6CE8\u53D8\u66F4\u4EE3\u7801**\uFF1A\u53EA\u5BA1\u67E5 diff \u4E2D\u5E26 \`+\` \u6216 \`-\` \u7684\u4EE3\u7801\u884C\uFF0C\u8FD9\u4E9B\u662F\u5B9E\u9645\u7684\u53D8\u66F4\u5185\u5BB9
3718
+ 2. **\u63D0\u4F9B\u5177\u4F53\u5EFA\u8BAE**\uFF1A\u4E0D\u8981\u6CDB\u6CDB\u800C\u8C08\uFF0C\u8981\u9488\u5BF9\u5177\u4F53\u4EE3\u7801\u884C\u7ED9\u51FA\u6539\u8FDB\u5EFA\u8BAE
3719
+ 3. **\u533A\u5206\u95EE\u9898\u4E25\u91CD\u7A0B\u5EA6**\uFF1A\u4F7F\u7528 \u{1F534} \u4E25\u91CD\u3001\u{1F7E1} \u8B66\u544A\u3001\u{1F535} \u5EFA\u8BAE \u4E09\u4E2A\u7EA7\u522B
3720
+ 4. **\u4EE3\u7801\u793A\u4F8B**\uFF1A\u5728\u5EFA\u8BAE\u4FEE\u6539\u65F6\uFF0C\u5C3D\u53EF\u80FD\u63D0\u4F9B\u4FEE\u6539\u540E\u7684\u4EE3\u7801\u793A\u4F8B
3721
+ 5. **\u6B63\u9762\u53CD\u9988**\uFF1A\u5BF9\u4E8E\u5199\u5F97\u597D\u7684\u4EE3\u7801\uFF0C\u4E5F\u8981\u7ED9\u4E88\u80AF\u5B9A
3722
+
3723
+ ## \u5BA1\u67E5\u7EF4\u5EA6
3724
+
3725
+ 1. **\u4EE3\u7801\u8D28\u91CF**\uFF1A\u53EF\u8BFB\u6027\u3001\u53EF\u7EF4\u62A4\u6027\u3001\u4EE3\u7801\u98CE\u683C
3726
+ 2. **\u6F5C\u5728 Bug**\uFF1A\u7A7A\u6307\u9488\u3001\u8FB9\u754C\u6761\u4EF6\u3001\u5F02\u5E38\u5904\u7406
3727
+ 3. **\u5B89\u5168\u95EE\u9898**\uFF1ASQL \u6CE8\u5165\u3001XSS\u3001\u654F\u611F\u4FE1\u606F\u6CC4\u9732
3728
+ 4. **\u6027\u80FD\u95EE\u9898**\uFF1A\u4E0D\u5FC5\u8981\u7684\u5FAA\u73AF\u3001\u5185\u5B58\u6CC4\u6F0F\u3001\u91CD\u590D\u8BA1\u7B97
3729
+ 5. **\u6700\u4F73\u5B9E\u8DF5**\uFF1A\u8BBE\u8BA1\u6A21\u5F0F\u3001SOLID \u539F\u5219\u3001DRY \u539F\u5219
3730
+
3731
+ ## Diff \u683C\u5F0F\u8BF4\u660E
3732
+
3733
+ - \u4EE5 \`+\` \u5F00\u5934\u7684\u884C\u662F\u65B0\u589E\u7684\u4EE3\u7801
3734
+ - \u4EE5 \`-\` \u5F00\u5934\u7684\u884C\u662F\u5220\u9664\u7684\u4EE3\u7801
3735
+ - \`@@\` \u884C\u8868\u793A\u4EE3\u7801\u4F4D\u7F6E\u4FE1\u606F\uFF0C\u683C\u5F0F\u4E3A \`@@ -\u65E7\u6587\u4EF6\u8D77\u59CB\u884C,\u884C\u6570 +\u65B0\u6587\u4EF6\u8D77\u59CB\u884C,\u884C\u6570 @@\`
3736
+ - \u6CA1\u6709 \`+\` \u6216 \`-\` \u524D\u7F00\u7684\u884C\u662F\u4E0A\u4E0B\u6587\u4EE3\u7801\uFF0C\u7528\u4E8E\u5E2E\u52A9\u7406\u89E3\u53D8\u66F4
3737
+
3738
+ ## \u8F93\u51FA\u683C\u5F0F
3739
+
3740
+ \u8BF7\u4F7F\u7528 Markdown \u683C\u5F0F\u8F93\u51FA\u5BA1\u67E5\u62A5\u544A\uFF0C\u5305\u542B\u4EE5\u4E0B\u90E8\u5206\uFF1A
3741
+
3742
+ 1. **\u6982\u8FF0**\uFF1A\u7B80\u8981\u603B\u7ED3\u672C\u6B21\u53D8\u66F4\u7684\u5185\u5BB9\u548C\u6574\u4F53\u8BC4\u4EF7
3743
+ 2. **\u95EE\u9898\u5217\u8868**\uFF1A\u6309\u4E25\u91CD\u7A0B\u5EA6\u5217\u51FA\u53D1\u73B0\u7684\u95EE\u9898
3744
+ 3. **\u6539\u8FDB\u5EFA\u8BAE**\uFF1A\u63D0\u4F9B\u5177\u4F53\u7684\u4EE3\u7801\u6539\u8FDB\u5EFA\u8BAE
3745
+ 4. **\u4EAE\u70B9**\uFF1A\u6307\u51FA\u4EE3\u7801\u4E2D\u5199\u5F97\u597D\u7684\u5730\u65B9\uFF08\u5982\u679C\u6709\uFF09
3746
+
3747
+ \u6CE8\u610F\uFF1A
3748
+ - \u6BCF\u4E2A\u95EE\u9898\u90FD\u8981\u6307\u660E\u6587\u4EF6\u8DEF\u5F84\u548C\u884C\u53F7
3749
+ - \u63D0\u4F9B\u4FEE\u6539\u5EFA\u8BAE\u65F6\u8981\u7ED9\u51FA\u4EE3\u7801\u793A\u4F8B
3750
+ - \u5982\u679C\u4EE3\u7801\u6CA1\u6709\u660E\u663E\u95EE\u9898\uFF0C\u4E5F\u8981\u8BF4\u660E\u5BA1\u67E5\u7ED3\u8BBA`;
3751
+ }
3752
+ return `You are a senior code review expert with extensive software development experience. Your task is to review code changes in Git commits and provide professional, valuable, and constructive review feedback.
3753
+
3754
+ ## Review Principles
3755
+
3756
+ 1. **Focus on Changed Code**: Only review lines with \`+\` or \`-\` prefixes in the diff - these are the actual changes
3757
+ 2. **Provide Specific Suggestions**: Don't be vague, give improvement suggestions for specific code lines
3758
+ 3. **Categorize Issue Severity**: Use \u{1F534} Critical, \u{1F7E1} Warning, \u{1F535} Suggestion levels
3759
+ 4. **Code Examples**: When suggesting changes, provide modified code examples whenever possible
3760
+ 5. **Positive Feedback**: Also acknowledge well-written code
3761
+
3762
+ ## Review Dimensions
3763
+
3764
+ 1. **Code Quality**: Readability, maintainability, code style
3765
+ 2. **Potential Bugs**: Null pointers, boundary conditions, exception handling
3766
+ 3. **Security Issues**: SQL injection, XSS, sensitive data exposure
3767
+ 4. **Performance Issues**: Unnecessary loops, memory leaks, redundant calculations
3768
+ 5. **Best Practices**: Design patterns, SOLID principles, DRY principle
3769
+
3770
+ ## Diff Format Explanation
3771
+
3772
+ - Lines starting with \`+\` are added code
3773
+ - Lines starting with \`-\` are deleted code
3774
+ - \`@@\` lines indicate code location, format: \`@@ -old_start,count +new_start,count @@\`
3775
+ - Lines without \`+\` or \`-\` prefix are context code to help understand changes
3776
+
3777
+ ## Output Format
3778
+
3779
+ Please output the review report in Markdown format, including:
3780
+
3781
+ 1. **Overview**: Brief summary of changes and overall assessment
3782
+ 2. **Issues**: List issues by severity
3783
+ 3. **Suggestions**: Provide specific code improvement suggestions
3784
+ 4. **Highlights**: Point out well-written code (if any)
3785
+
3786
+ Note:
3787
+ - Each issue should specify file path and line number
3788
+ - Provide code examples when suggesting modifications
3789
+ - If no obvious issues, state the review conclusion`;
3790
+ }
3791
+ function buildUserPrompt(diff, commits, language) {
3792
+ const isZh = language === "zh-CN";
3793
+ const stats = getDiffStats(diff);
3794
+ const files = parseDiff(diff);
3795
+ let prompt = "";
3796
+ if (isZh) {
3797
+ prompt += `## \u53D8\u66F4\u6982\u89C8
3798
+
3799
+ `;
3800
+ prompt += `- \u6D89\u53CA\u6587\u4EF6: ${stats.files} \u4E2A
3801
+ `;
3802
+ prompt += `- \u65B0\u589E\u884C\u6570: +${stats.additions}
3803
+ `;
3804
+ prompt += `- \u5220\u9664\u884C\u6570: -${stats.deletions}
3805
+
3806
+ `;
3807
+ if (commits.length > 0) {
3808
+ prompt += `## \u76F8\u5173\u63D0\u4EA4
3809
+
3810
+ `;
3811
+ for (const commit2 of commits) {
3812
+ prompt += `- \`${commit2.shortHash}\` ${commit2.subject} (${commit2.author}, ${commit2.date})
3813
+ `;
3814
+ }
3815
+ prompt += `
3816
+ `;
3817
+ }
3818
+ prompt += `## \u53D8\u66F4\u6587\u4EF6\u5217\u8868
3819
+
3820
+ `;
3821
+ for (const file of files) {
3822
+ const statusIcon = file.status === "A" ? "\u{1F195}" : file.status === "D" ? "\u{1F5D1}\uFE0F" : file.status === "R" ? "\u{1F4DD}" : "\u270F\uFE0F";
3823
+ prompt += `- ${statusIcon} \`${file.newPath}\`
3824
+ `;
3825
+ }
3826
+ prompt += `
3827
+ `;
3828
+ prompt += `## Diff \u5185\u5BB9
3829
+
3830
+ \u8BF7\u4ED4\u7EC6\u5BA1\u67E5\u4EE5\u4E0B\u4EE3\u7801\u53D8\u66F4\uFF1A
3831
+
3832
+ `;
3833
+ } else {
3834
+ prompt += `## Change Overview
3835
+
3836
+ `;
3837
+ prompt += `- Files changed: ${stats.files}
3838
+ `;
3839
+ prompt += `- Lines added: +${stats.additions}
3840
+ `;
3841
+ prompt += `- Lines deleted: -${stats.deletions}
3842
+
3843
+ `;
3844
+ if (commits.length > 0) {
3845
+ prompt += `## Related Commits
3846
+
3847
+ `;
3848
+ for (const commit2 of commits) {
3849
+ prompt += `- \`${commit2.shortHash}\` ${commit2.subject} (${commit2.author}, ${commit2.date})
3850
+ `;
3851
+ }
3852
+ prompt += `
3853
+ `;
3854
+ }
3855
+ prompt += `## Changed Files
3856
+
3857
+ `;
3858
+ for (const file of files) {
3859
+ const statusIcon = file.status === "A" ? "\u{1F195}" : file.status === "D" ? "\u{1F5D1}\uFE0F" : file.status === "R" ? "\u{1F4DD}" : "\u270F\uFE0F";
3860
+ prompt += `- ${statusIcon} \`${file.newPath}\`
3861
+ `;
3862
+ }
3863
+ prompt += `
3864
+ `;
3865
+ prompt += `## Diff Content
3866
+
3867
+ Please carefully review the following code changes:
3868
+
3869
+ `;
3870
+ }
3871
+ for (const file of files) {
3872
+ prompt += `### ${file.newPath}
3873
+
3874
+ `;
3875
+ prompt += "```diff\n";
3876
+ prompt += file.diff;
3877
+ prompt += "\n```\n\n";
3878
+ }
3879
+ return prompt;
3880
+ }
3881
+ async function callGitHubAPI2(systemPrompt, userPrompt, apiKey, model) {
3882
+ const response = await fetch(AI_PROVIDERS2.github.endpoint, {
3883
+ method: "POST",
3884
+ headers: {
3885
+ Authorization: `Bearer ${apiKey}`,
3886
+ "Content-Type": "application/json"
3887
+ },
3888
+ body: JSON.stringify({
3889
+ model,
3890
+ messages: [
3891
+ { role: "system", content: systemPrompt },
3892
+ { role: "user", content: userPrompt }
3893
+ ],
3894
+ max_tokens: 4e3,
3895
+ temperature: 0.3
3896
+ })
3897
+ });
3898
+ if (!response.ok) {
3899
+ const error = await response.text();
3900
+ throw new Error(`GitHub Models API \u9519\u8BEF: ${response.status} ${error}`);
3901
+ }
3902
+ const data = await response.json();
3903
+ return data.choices[0]?.message?.content?.trim() || "";
3904
+ }
3905
+ async function callOpenAIAPI2(systemPrompt, userPrompt, apiKey, model) {
3906
+ const response = await fetch(AI_PROVIDERS2.openai.endpoint, {
3907
+ method: "POST",
3908
+ headers: {
3909
+ Authorization: `Bearer ${apiKey}`,
3910
+ "Content-Type": "application/json"
3911
+ },
3912
+ body: JSON.stringify({
3913
+ model,
3914
+ messages: [
3915
+ { role: "system", content: systemPrompt },
3916
+ { role: "user", content: userPrompt }
3917
+ ],
3918
+ max_tokens: 4e3,
3919
+ temperature: 0.3
3920
+ })
3921
+ });
3922
+ if (!response.ok) {
3923
+ const error = await response.text();
3924
+ throw new Error(`OpenAI API \u9519\u8BEF: ${response.status} ${error}`);
3925
+ }
3926
+ const data = await response.json();
3927
+ return data.choices[0]?.message?.content?.trim() || "";
3928
+ }
3929
+ async function callClaudeAPI2(systemPrompt, userPrompt, apiKey, model) {
3930
+ const response = await fetch(AI_PROVIDERS2.claude.endpoint, {
3931
+ method: "POST",
3932
+ headers: {
3933
+ "x-api-key": apiKey,
3934
+ "anthropic-version": "2023-06-01",
3935
+ "Content-Type": "application/json"
3936
+ },
3937
+ body: JSON.stringify({
3938
+ model,
3939
+ system: systemPrompt,
3940
+ messages: [{ role: "user", content: userPrompt }],
3941
+ max_tokens: 4e3,
3942
+ temperature: 0.3
3943
+ })
3944
+ });
3945
+ if (!response.ok) {
3946
+ const error = await response.text();
3947
+ throw new Error(`Claude API \u9519\u8BEF: ${response.status} ${error}`);
3948
+ }
3949
+ const data = await response.json();
3950
+ return data.content[0]?.text?.trim() || "";
3951
+ }
3952
+ async function callOllamaAPI2(systemPrompt, userPrompt, model) {
3953
+ try {
3954
+ const response = await fetch(AI_PROVIDERS2.ollama.endpoint, {
3955
+ method: "POST",
3956
+ headers: { "Content-Type": "application/json" },
3957
+ body: JSON.stringify({
3958
+ model,
3959
+ prompt: `${systemPrompt}
3960
+
3961
+ ${userPrompt}`,
3962
+ stream: false,
3963
+ options: {
3964
+ num_predict: 4e3,
3965
+ temperature: 0.3
3966
+ }
3967
+ })
3968
+ });
3969
+ if (!response.ok) {
3970
+ throw new Error(`Ollama \u672A\u8FD0\u884C\u6216\u6A21\u578B\u672A\u5B89\u88C5`);
3971
+ }
3972
+ const data = await response.json();
3973
+ return data.response?.trim() || "";
3974
+ } catch (error) {
3975
+ throw new Error(
3976
+ `Ollama \u8FDE\u63A5\u5931\u8D25\u3002\u8BF7\u786E\u4FDD\uFF1A
3977
+ 1. \u5DF2\u5B89\u88C5 Ollama (https://ollama.com)
3978
+ 2. \u8FD0\u884C 'ollama serve'
3979
+ 3. \u4E0B\u8F7D\u6A21\u578B 'ollama pull ${model}'`
3980
+ );
3981
+ }
3982
+ }
3983
+ async function callAIReview(diff, commits, config2) {
3984
+ const aiConfig = config2.aiCommit || {};
3985
+ const provider = aiConfig.provider || "github";
3986
+ const language = aiConfig.language || "zh-CN";
3987
+ const apiKey = aiConfig.apiKey || "";
3988
+ const providerInfo = AI_PROVIDERS2[provider];
3989
+ if (!providerInfo) {
3990
+ throw new Error(`\u4E0D\u652F\u6301\u7684 AI \u63D0\u4F9B\u5546: ${provider}`);
3991
+ }
3992
+ const model = aiConfig.model || providerInfo.defaultModel;
3993
+ if (provider !== "ollama" && !apiKey) {
3994
+ throw new Error(
3995
+ `${providerInfo.name} \u9700\u8981 API key\u3002\u8BF7\u8FD0\u884C 'gw init' \u914D\u7F6E\uFF0C\u6216\u5728 .gwrc.json \u4E2D\u8BBE\u7F6E aiCommit.apiKey`
3996
+ );
3997
+ }
3998
+ const systemPrompt = buildSystemPrompt(language);
3999
+ const userPrompt = buildUserPrompt(diff, commits, language);
4000
+ const maxLength = 3e4;
4001
+ const truncatedUserPrompt = userPrompt.length > maxLength ? userPrompt.slice(0, maxLength) + "\n\n[... diff \u5185\u5BB9\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD ...]" : userPrompt;
4002
+ switch (provider) {
4003
+ case "github":
4004
+ return callGitHubAPI2(systemPrompt, truncatedUserPrompt, apiKey, model);
4005
+ case "openai":
4006
+ return callOpenAIAPI2(systemPrompt, truncatedUserPrompt, apiKey, model);
4007
+ case "claude":
4008
+ return callClaudeAPI2(systemPrompt, truncatedUserPrompt, apiKey, model);
4009
+ case "ollama":
4010
+ return callOllamaAPI2(systemPrompt, truncatedUserPrompt, model);
4011
+ default:
4012
+ throw new Error(`\u4E0D\u652F\u6301\u7684 AI \u63D0\u4F9B\u5546: ${provider}`);
4013
+ }
4014
+ }
4015
+ function generateReportFile(reviewContent, commits, stats, outputPath) {
4016
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
4017
+ const commitInfo = commits.length > 0 ? commits.map((c) => c.shortHash).join("-") : "staged";
4018
+ const reviewDir = ".gw-reviews";
4019
+ if (!existsSync5(reviewDir)) {
4020
+ mkdirSync(reviewDir, { recursive: true });
4021
+ }
4022
+ const filename = outputPath || join6(reviewDir, `review-${commitInfo}-${timestamp}.md`);
4023
+ let report = `# \u{1F50D} \u4EE3\u7801\u5BA1\u67E5\u62A5\u544A
4024
+
4025
+ `;
4026
+ report += `> \u751F\u6210\u65F6\u95F4: ${(/* @__PURE__ */ new Date()).toLocaleString("zh-CN")}
4027
+
4028
+ `;
4029
+ report += `## \u{1F4CA} \u53D8\u66F4\u7EDF\u8BA1
4030
+
4031
+ `;
4032
+ report += `| \u6307\u6807 | \u6570\u503C |
4033
+ `;
4034
+ report += `|------|------|
4035
+ `;
4036
+ report += `| \u6587\u4EF6\u6570 | ${stats.files} |
4037
+ `;
4038
+ report += `| \u65B0\u589E\u884C | +${stats.additions} |
4039
+ `;
4040
+ report += `| \u5220\u9664\u884C | -${stats.deletions} |
4041
+
4042
+ `;
4043
+ if (commits.length > 0) {
4044
+ report += `## \u{1F4DD} \u5BA1\u67E5\u7684\u63D0\u4EA4
4045
+
4046
+ `;
4047
+ for (const commit2 of commits) {
4048
+ report += `- \`${commit2.shortHash}\` ${commit2.subject} - ${commit2.author} (${commit2.date})
4049
+ `;
4050
+ }
4051
+ report += `
4052
+ `;
4053
+ }
4054
+ report += `## \u{1F916} AI \u5BA1\u67E5\u7ED3\u679C
4055
+
4056
+ `;
4057
+ report += reviewContent;
4058
+ report += `
4059
+
4060
+ ---
4061
+
4062
+ `;
4063
+ report += `*\u672C\u62A5\u544A\u7531 [git-workflow](https://github.com/iamzjt-front-end/git-workflow) \u7684 AI Review \u529F\u80FD\u751F\u6210*
4064
+ `;
4065
+ writeFileSync5(filename, report, "utf-8");
4066
+ return filename;
4067
+ }
4068
+ async function review(hashes, options = {}) {
4069
+ const config2 = loadConfig();
4070
+ const aiConfig = config2.aiCommit;
4071
+ if (!aiConfig?.apiKey && aiConfig?.provider !== "ollama") {
4072
+ console.log(colors.red("\u274C \u672A\u914D\u7F6E AI API Key"));
4073
+ console.log("");
4074
+ console.log(colors.dim(" \u8BF7\u5148\u8FD0\u884C\u4EE5\u4E0B\u547D\u4EE4\u914D\u7F6E AI:"));
4075
+ console.log(colors.cyan(" gw init"));
4076
+ console.log("");
4077
+ return;
4078
+ }
4079
+ let diff = "";
4080
+ let commits = [];
4081
+ if (hashes && hashes.length > 0) {
4082
+ commits = hashes.map((hash) => {
4083
+ const info = execOutput(
4084
+ `git log -1 --pretty=format:"%H|%h|%s|%an|%ad" --date=short ${hash}`
4085
+ );
4086
+ if (!info) {
4087
+ console.log(colors.red(`\u274C \u627E\u4E0D\u5230 commit: ${hash}`));
4088
+ process.exit(1);
4089
+ }
4090
+ const [fullHash, shortHash, subject, author, date] = info.split("|");
4091
+ return { hash: fullHash, shortHash, subject, author, date };
4092
+ });
4093
+ diff = getMultipleCommitsDiff(hashes);
4094
+ } else if (options.last) {
4095
+ commits = getRecentCommits3(options.last);
4096
+ diff = getMultipleCommitsDiff(commits.map((c) => c.hash));
4097
+ } else if (options.staged) {
4098
+ diff = getStagedDiff();
4099
+ } else {
4100
+ const recentCommits = getRecentCommits3(20);
4101
+ const stagedDiff = getStagedDiff();
4102
+ const choices = [];
4103
+ if (stagedDiff) {
4104
+ choices.push({
4105
+ name: `\u{1F4E6} \u6682\u5B58\u533A\u7684\u66F4\u6539 (staged changes)`,
4106
+ value: "staged"
4107
+ });
4108
+ }
4109
+ choices.push(
4110
+ ...recentCommits.map((c) => ({
4111
+ name: `${colors.yellow(c.shortHash)} ${c.subject} ${colors.dim(`- ${c.author} (${c.date})`)}`,
4112
+ value: c.hash
4113
+ }))
4114
+ );
4115
+ if (choices.length === 0) {
4116
+ console.log(colors.yellow("\u26A0\uFE0F \u6CA1\u6709\u53EF\u5BA1\u67E5\u7684\u5185\u5BB9"));
4117
+ return;
4118
+ }
4119
+ divider();
4120
+ const selected = await checkbox2({
4121
+ message: "\u9009\u62E9\u8981\u5BA1\u67E5\u7684\u5185\u5BB9 (\u7A7A\u683C\u9009\u62E9\uFF0C\u56DE\u8F66\u786E\u8BA4):",
4122
+ choices,
4123
+ pageSize: choices.length,
4124
+ // 显示所有选项,不滚动
4125
+ loop: false,
4126
+ // 到达边界时不循环
4127
+ theme
4128
+ });
4129
+ if (selected.length === 0) {
4130
+ console.log(colors.yellow("\u26A0\uFE0F \u672A\u9009\u62E9\u4EFB\u4F55\u5185\u5BB9"));
4131
+ return;
4132
+ }
4133
+ if (selected.includes("staged")) {
4134
+ diff = stagedDiff;
4135
+ } else {
4136
+ commits = recentCommits.filter((c) => selected.includes(c.hash));
4137
+ diff = getMultipleCommitsDiff(selected);
4138
+ }
4139
+ }
4140
+ if (!diff) {
4141
+ console.log(colors.yellow("\u26A0\uFE0F \u6CA1\u6709\u68C0\u6D4B\u5230\u4EE3\u7801\u53D8\u66F4"));
4142
+ return;
4143
+ }
4144
+ const stats = getDiffStats(diff);
4145
+ divider();
4146
+ console.log(colors.cyan("\u{1F4CA} \u53D8\u66F4\u7EDF\u8BA1:"));
4147
+ console.log(colors.dim(` \u6587\u4EF6: ${stats.files} \u4E2A`));
4148
+ console.log(colors.dim(` \u65B0\u589E: +${stats.additions} \u884C`));
4149
+ console.log(colors.dim(` \u5220\u9664: -${stats.deletions} \u884C`));
4150
+ divider();
4151
+ const spinner = ora7("\u{1F916} AI \u6B63\u5728\u5BA1\u67E5\u4EE3\u7801...").start();
4152
+ try {
4153
+ const reviewContent = await callAIReview(diff, commits, config2);
4154
+ spinner.succeed("AI \u5BA1\u67E5\u5B8C\u6210");
4155
+ const reportPath = generateReportFile(
4156
+ reviewContent,
4157
+ commits,
4158
+ stats,
4159
+ options.output
4160
+ );
4161
+ console.log("");
4162
+ console.log(colors.green(`\u2705 \u5BA1\u67E5\u62A5\u544A\u5DF2\u751F\u6210: ${colors.cyan(reportPath)}`));
4163
+ console.log("");
4164
+ const shouldOpen = await select10({
4165
+ message: "\u662F\u5426\u6253\u5F00\u5BA1\u67E5\u62A5\u544A?",
4166
+ choices: [
4167
+ { name: "\u662F\uFF0C\u5728\u7F16\u8F91\u5668\u4E2D\u6253\u5F00", value: true },
4168
+ { name: "\u5426\uFF0C\u7A0D\u540E\u67E5\u770B", value: false }
4169
+ ],
4170
+ theme
4171
+ });
4172
+ if (shouldOpen) {
4173
+ try {
4174
+ const { exec: exec2 } = await import("child_process");
4175
+ exec2(`open "${reportPath}"`);
4176
+ } catch {
4177
+ console.log(colors.dim(` \u8BF7\u624B\u52A8\u6253\u5F00: ${reportPath}`));
4178
+ }
4179
+ }
4180
+ } catch (error) {
4181
+ spinner.fail("AI \u5BA1\u67E5\u5931\u8D25");
4182
+ console.log("");
4183
+ console.log(colors.red(`\u274C ${error.message}`));
4184
+ console.log("");
4185
+ }
4186
+ }
4187
+
3605
4188
  // src/index.ts
3606
4189
  process.on("uncaughtException", (err) => {
3607
4190
  if (err instanceof ExitPromptError) {
@@ -3627,7 +4210,7 @@ process.on("SIGTERM", () => {
3627
4210
  console.log("");
3628
4211
  process.exit(0);
3629
4212
  });
3630
- var version = true ? "0.4.7" : "0.0.0-dev";
4213
+ var version = true ? "0.5.0" : "0.0.0-dev";
3631
4214
  async function mainMenu() {
3632
4215
  console.log(
3633
4216
  colors.green(`
@@ -3641,7 +4224,7 @@ async function mainMenu() {
3641
4224
  );
3642
4225
  console.log(colors.dim(` git-workflow v${colors.yellow(version)}
3643
4226
  `));
3644
- const action = await select10({
4227
+ const action = await select11({
3645
4228
  message: "\u9009\u62E9\u64CD\u4F5C:",
3646
4229
  choices: [
3647
4230
  {
@@ -3697,7 +4280,11 @@ async function mainMenu() {
3697
4280
  value: "amend"
3698
4281
  },
3699
4282
  {
3700
- name: `[e] \u2699\uFE0F \u521D\u59CB\u5316\u914D\u7F6E ${colors.dim("gw init")}`,
4283
+ name: `[e] \u{1F50D} AI \u4EE3\u7801\u5BA1\u67E5 ${colors.dim("gw review")}`,
4284
+ value: "review"
4285
+ },
4286
+ {
4287
+ name: `[f] \u2699\uFE0F \u521D\u59CB\u5316\u914D\u7F6E ${colors.dim("gw init")}`,
3701
4288
  value: "init"
3702
4289
  },
3703
4290
  { name: "[0] \u2753 \u5E2E\u52A9", value: "help" },
@@ -3758,6 +4345,10 @@ async function mainMenu() {
3758
4345
  checkGitRepo();
3759
4346
  await amend();
3760
4347
  break;
4348
+ case "review":
4349
+ checkGitRepo();
4350
+ await review();
4351
+ break;
3761
4352
  case "init":
3762
4353
  await init();
3763
4354
  break;
@@ -3851,18 +4442,30 @@ cli.command("amend [hash]", "\u4FEE\u6539\u6307\u5B9A commit \u7684\u63D0\u4EA4\
3851
4442
  checkGitRepo();
3852
4443
  return amend(hash);
3853
4444
  });
4445
+ cli.command("review [...hashes]", "AI \u4EE3\u7801\u5BA1\u67E5").alias("rw").option("-n, --last <number>", "\u5BA1\u67E5\u6700\u8FD1 N \u4E2A commits").option("-s, --staged", "\u5BA1\u67E5\u6682\u5B58\u533A\u7684\u66F4\u6539").option("-o, --output <path>", "\u6307\u5B9A\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84").action(async (hashes, options) => {
4446
+ await checkForUpdates(version, "@zjex/git-workflow");
4447
+ checkGitRepo();
4448
+ return review(
4449
+ hashes.length > 0 ? hashes : void 0,
4450
+ {
4451
+ last: options.last ? parseInt(options.last) : void 0,
4452
+ staged: options.staged,
4453
+ output: options.output
4454
+ }
4455
+ );
4456
+ });
3854
4457
  cli.command("clean", "\u6E05\u7406\u7F13\u5B58\u548C\u4E34\u65F6\u6587\u4EF6").alias("cc").action(async () => {
3855
4458
  const { clearUpdateCache: clearUpdateCache3 } = await Promise.resolve().then(() => (init_update_notifier(), update_notifier_exports));
3856
- const { existsSync: existsSync5, unlinkSync: unlinkSync4, readdirSync } = await import("fs");
4459
+ const { existsSync: existsSync6, unlinkSync: unlinkSync4, readdirSync } = await import("fs");
3857
4460
  const { homedir: homedir5, tmpdir: tmpdir2 } = await import("os");
3858
- const { join: join6 } = await import("path");
3859
- const { select: select11 } = await import("@inquirer/prompts");
4461
+ const { join: join7 } = await import("path");
4462
+ const { select: select12 } = await import("@inquirer/prompts");
3860
4463
  let cleanedCount = 0;
3861
4464
  let deletedGlobalConfig = false;
3862
- const globalConfig = join6(homedir5(), ".gwrc.json");
3863
- const hasGlobalConfig = existsSync5(globalConfig);
4465
+ const globalConfig = join7(homedir5(), ".gwrc.json");
4466
+ const hasGlobalConfig = existsSync6(globalConfig);
3864
4467
  if (hasGlobalConfig) {
3865
- const shouldDeleteConfig = await select11({
4468
+ const shouldDeleteConfig = await select12({
3866
4469
  message: "\u68C0\u6D4B\u5230\u5168\u5C40\u914D\u7F6E\u6587\u4EF6\uFF0C\u662F\u5426\u5220\u9664\uFF1F",
3867
4470
  choices: [
3868
4471
  { name: "\u5426\uFF0C\u4FDD\u7559\u914D\u7F6E\u6587\u4EF6", value: false },
@@ -3887,7 +4490,7 @@ cli.command("clean", "\u6E05\u7406\u7F13\u5B58\u548C\u4E34\u65F6\u6587\u4EF6").a
3887
4490
  const gwTmpFiles = files.filter((f) => f.startsWith(".gw-commit-msg-"));
3888
4491
  for (const file of gwTmpFiles) {
3889
4492
  try {
3890
- unlinkSync4(join6(tmpDir, file));
4493
+ unlinkSync4(join7(tmpDir, file));
3891
4494
  cleanedCount++;
3892
4495
  } catch {
3893
4496
  }