ai-git-tool 1.0.0 → 1.1.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 (3) hide show
  1. package/README.md +8 -7
  2. package/dist/index.js +105 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -13,7 +13,7 @@ Groq API を使って、ステージ済み差分からコミットメッセー
13
13
  ### npm からインストール(推奨)
14
14
 
15
15
  ```bash
16
- npm install -g @imakento/ai-git
16
+ npm install -g ai-git-tool
17
17
  ```
18
18
 
19
19
  ### ローカル開発版をリンク
@@ -44,17 +44,18 @@ API キーの取得先: [Groq Console](https://console.groq.com/keys)
44
44
 
45
45
  ### コミットメッセージ生成
46
46
 
47
- 1. 先に変更をステージ
47
+ 1. コミットメッセージを生成
48
48
 
49
49
  ```bash
50
- git add .
50
+ # 通常(タイトル + 箇条書き本文)
51
+ ai-git commit
51
52
  ```
52
53
 
53
- 2. コミットメッセージを生成
54
+ `ai-git commit` はデフォルトで `git add .` を実行してから生成します。
54
55
 
55
56
  ```bash
56
- # 通常(タイトル + 箇条書き本文)
57
- ai-git commit
57
+ # 自動 add を無効化(手動でステージした差分のみ使う)
58
+ ai-git commit --no-add
58
59
  ```
59
60
 
60
61
  デフォルト言語は日本語です。
@@ -68,7 +69,7 @@ ai-git --set-lang en
68
69
  ai-git --set-lang ja
69
70
  ```
70
71
 
71
- 3. 確認プロンプトで選択
72
+ 2. 確認プロンプトで選択
72
73
 
73
74
  - `y`: そのままコミット
74
75
  - `n`: 中止
package/dist/index.js CHANGED
@@ -51,6 +51,7 @@ const subcommandArgs = args.slice(1);
51
51
  const showHelp = args.includes("--help") || args.includes("-h") || subcommand === "help";
52
52
  const setLangArg = getOptionValue(args, "--set-lang");
53
53
  const langArg = getOptionValue(subcommandArgs, "--lang");
54
+ const noAdd = subcommandArgs.includes("--no-add");
54
55
  const CONFIG_DIR = path.join(os.homedir(), ".ai-commit");
55
56
  const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
56
57
  const defaultGroqModel = "llama-3.1-8b-instant";
@@ -78,6 +79,7 @@ Commands:
78
79
 
79
80
  Commit Options:
80
81
  --lang <ja|en> Set language for this run
82
+ --no-add Skip automatic git add .
81
83
 
82
84
  PR Options:
83
85
  --lang <ja|en> Set language for this run
@@ -114,17 +116,69 @@ async function generateCommitMessage(diff) {
114
116
  const inputDiff = truncateByChars(diff, COMMIT_DIFF_COMPACT_CHARS);
115
117
  const prompt = buildPrompt(inputDiff, language);
116
118
  try {
117
- return await generateText(prompt);
119
+ const raw = await generateText(prompt);
120
+ return normalizeGeneratedCommitMessage(raw, diff, language);
118
121
  }
119
122
  catch (error) {
120
123
  if (isRequestTooLargeError(error)) {
121
124
  const smallerDiff = truncateByChars(diff, 1800);
122
- return generateText(buildPrompt(smallerDiff, language));
125
+ const retryRaw = await generateText(buildPrompt(smallerDiff, language));
126
+ return normalizeGeneratedCommitMessage(retryRaw, diff, language);
123
127
  }
124
128
  handleGroqError(error);
125
129
  process.exit(1);
126
130
  }
127
131
  }
132
+ function normalizeGeneratedCommitMessage(raw, diff, lang) {
133
+ const lines = raw
134
+ .split("\n")
135
+ .map((line) => line.replace(/\s+$/g, ""))
136
+ .filter((_, idx, arr) => !(idx > 0 && arr[idx - 1] === "" && arr[idx] === ""));
137
+ if (lines.length === 0) {
138
+ return buildFallbackCommitMessage(diff, lang);
139
+ }
140
+ const subjectLine = lines[0]?.trim() || "";
141
+ const conventionalMatch = /^[a-z]+(\([^)]+\))?:\s+(.+)$/.exec(subjectLine);
142
+ const hasConventionalPrefix = Boolean(conventionalMatch);
143
+ const shortDescription = conventionalMatch?.[2]?.trim() || subjectLine;
144
+ if (!hasConventionalPrefix ||
145
+ looksLikePathOnly(shortDescription) ||
146
+ shortDescription.length < 8) {
147
+ lines[0] = buildFallbackSubjectLine(diff, lang);
148
+ }
149
+ return lines.join("\n").trim();
150
+ }
151
+ function looksLikePathOnly(value) {
152
+ const normalized = value.trim().toLowerCase();
153
+ if (!normalized) {
154
+ return false;
155
+ }
156
+ const hasPathToken = normalized.includes("/") || normalized.includes(".");
157
+ return hasPathToken && /^[a-z0-9/_\-.]+$/.test(normalized);
158
+ }
159
+ function buildFallbackCommitMessage(diff, lang) {
160
+ const subject = buildFallbackSubjectLine(diff, lang);
161
+ if (lang === "ja") {
162
+ return `${subject}\n\n- 差分の主目的を明確にし、変更理由が伝わる形に整える`;
163
+ }
164
+ return `${subject}\n\n- Clarify the main change intent so the reason is easy to understand`;
165
+ }
166
+ function buildFallbackSubjectLine(diff, lang) {
167
+ const files = getChangedFiles();
168
+ const type = inferBranchType(files, diff);
169
+ const rawTopic = inferTopic(files, diff);
170
+ const topic = rawTopic ? rawTopic.replace(/-/g, " ") : "";
171
+ if (lang === "ja") {
172
+ const description = topic
173
+ ? `${topic} の変更意図を分かりやすく整理する`
174
+ : "変更意図が伝わるように更新内容を整理する";
175
+ return `${type}: ${description}`;
176
+ }
177
+ const description = topic
178
+ ? `clarify the intent behind ${topic} changes`
179
+ : "clarify update intent in one clear sentence";
180
+ return `${type}: ${description}`;
181
+ }
128
182
  function buildPrompt(diff, lang) {
129
183
  if (lang === "ja") {
130
184
  return `あなたは git のコミットメッセージ作成の専門家です。
@@ -208,6 +262,13 @@ function doCommit(message) {
208
262
  process.exit(1);
209
263
  }
210
264
  }
265
+ function stageAllChanges() {
266
+ const result = (0, child_process_1.spawnSync)("git", ["add", "."], { stdio: "inherit" });
267
+ if (result.status !== 0) {
268
+ console.error("Error: git add . failed");
269
+ process.exit(1);
270
+ }
271
+ }
211
272
  async function mainCheckout() {
212
273
  const suggested = await suggestBranchName();
213
274
  const branchName = ensureAvailableBranchName(suggested);
@@ -438,6 +499,10 @@ async function main() {
438
499
  await mainPR();
439
500
  return;
440
501
  }
502
+ if (!noAdd) {
503
+ console.log("📦 変更をステージしています... (git add .)");
504
+ stageAllChanges();
505
+ }
441
506
  const diff = getStagedDiff();
442
507
  if (!diff.trim()) {
443
508
  console.log("No staged changes found. Run `git add` first.");
@@ -770,10 +835,45 @@ function isRequestTooLargeError(error) {
770
835
  lower.includes("tokens per minute") ||
771
836
  lower.includes("tpm"));
772
837
  }
838
+ function makePRTitle(description) {
839
+ const maxLen = 64;
840
+ const lines = description
841
+ .split("\n")
842
+ .map((v) => v.trim())
843
+ .filter(Boolean);
844
+ // Prefer the first sentence under ## Summary
845
+ const summaryIdx = lines.findIndex((l) => l.toLowerCase().startsWith("## summary"));
846
+ let candidate = "";
847
+ if (summaryIdx >= 0) {
848
+ const summaryLine = lines
849
+ .slice(summaryIdx + 1)
850
+ .find((l) => !l.startsWith("##"));
851
+ candidate = summaryLine || "";
852
+ }
853
+ if (!candidate) {
854
+ candidate = lines.find((l) => !l.startsWith("#") && !l.startsWith("-")) || "";
855
+ }
856
+ candidate = candidate
857
+ .replace(/^this pull request\s+(is|does)\s*/i, "")
858
+ .replace(/^この\s*pull request\s*は、?/i, "")
859
+ .replace(/^このprは、?/i, "")
860
+ .replace(/\s+/g, " ")
861
+ .trim();
862
+ // Trim at first sentence boundary when possible.
863
+ const sentenceCut = candidate.search(/[。.!?]/);
864
+ if (sentenceCut > 0) {
865
+ candidate = candidate.slice(0, sentenceCut);
866
+ }
867
+ if (!candidate) {
868
+ candidate = "Update project changes";
869
+ }
870
+ if (candidate.length > maxLen) {
871
+ candidate = `${candidate.slice(0, maxLen - 1).trimEnd()}…`;
872
+ }
873
+ return candidate;
874
+ }
773
875
  function createPR(description, baseBranch, fallbackURL) {
774
- // Extract title from first non-header line
775
- const lines = description.split("\n");
776
- const titleLine = lines.find((l) => l.trim() && !l.startsWith("#")) || "Pull Request";
876
+ const titleLine = makePRTitle(description);
777
877
  const result = (0, child_process_1.spawnSync)("gh", [
778
878
  "pr",
779
879
  "create",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-git-tool",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "AI-powered git commit and PR description generator using Groq API",
5
5
  "main": "dist/index.js",
6
6
  "bin": {