ai-git-tool 1.0.0 → 1.2.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.
- package/README.md +125 -68
- package/dist/index.js +198 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,121 +1,151 @@
|
|
|
1
|
-
# ai-git
|
|
1
|
+
# ai-git-tool
|
|
2
2
|
|
|
3
3
|
Groq API を使って、ステージ済み差分からコミットメッセージと PR 説明文を自動生成する TypeScript 製 CLI です。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## セットアップ
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g ai-git-tool
|
|
9
|
+
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
### 環境変数
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
API キーの取得先: [Groq Console](https://console.groq.com/keys)
|
|
14
|
+
|
|
15
|
+
**macOS / Linux (bash/zsh):**
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
|
-
|
|
18
|
+
export GROQ_API_KEY="your_api_key"
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
永続化する場合は `~/.bashrc` や `~/.zshrc` に追記してください。
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
**Windows (コマンドプロンプト):**
|
|
24
|
+
|
|
25
|
+
```cmd
|
|
26
|
+
setx GROQ_API_KEY "your_api_key"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Windows (PowerShell):**
|
|
30
|
+
|
|
31
|
+
```powershell
|
|
32
|
+
[System.Environment]::SetEnvironmentVariable("GROQ_API_KEY", "your_api_key", "User")
|
|
25
33
|
```
|
|
26
34
|
|
|
27
|
-
|
|
35
|
+
> `setx` / `SetEnvironmentVariable` はユーザー環境変数として永続保存されます。設定後は**ターミナルを再起動**してください。
|
|
28
36
|
|
|
29
|
-
|
|
37
|
+
**Windows (Git Bash):**
|
|
30
38
|
|
|
31
39
|
```bash
|
|
32
|
-
export GROQ_API_KEY="your_api_key"
|
|
40
|
+
echo 'export GROQ_API_KEY="your_api_key"' >> ~/.bashrc
|
|
41
|
+
source ~/.bashrc
|
|
33
42
|
```
|
|
34
43
|
|
|
35
|
-
|
|
44
|
+
任意でモデル指定も可能です(デフォルト: `llama-3.1-8b-instant`)。
|
|
36
45
|
|
|
37
46
|
```bash
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
# macOS / Linux / Git Bash
|
|
48
|
+
export GROQ_MODEL="llama-3.3-70b-versatile"
|
|
40
49
|
|
|
41
|
-
|
|
50
|
+
# Windows コマンドプロンプト
|
|
51
|
+
setx GROQ_MODEL "llama-3.3-70b-versatile"
|
|
52
|
+
```
|
|
42
53
|
|
|
43
54
|
## 使い方
|
|
44
55
|
|
|
45
56
|
### コミットメッセージ生成
|
|
46
57
|
|
|
47
|
-
1. 先に変更をステージ
|
|
48
|
-
|
|
49
58
|
```bash
|
|
50
|
-
git
|
|
59
|
+
ai-git commit
|
|
51
60
|
```
|
|
52
61
|
|
|
53
|
-
|
|
62
|
+
デフォルトで `git add .` を実行してからコミットメッセージを生成します。
|
|
54
63
|
|
|
55
64
|
```bash
|
|
56
|
-
#
|
|
57
|
-
ai-git commit
|
|
65
|
+
# 手動でステージした差分のみ使う
|
|
66
|
+
ai-git commit --no-add
|
|
58
67
|
```
|
|
59
68
|
|
|
60
|
-
|
|
69
|
+
確認プロンプトで操作を選択できます。
|
|
61
70
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
71
|
+
| 入力 | 動作 |
|
|
72
|
+
|------|------|
|
|
73
|
+
| `y` | そのままコミット |
|
|
74
|
+
| `n` | 中止 |
|
|
75
|
+
| `e` | エディタで編集してからコミット |
|
|
65
76
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
77
|
+
**生成されるコミットメッセージの形式(Conventional Commits):**
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
feat(auth): Google ログインを追加する
|
|
81
|
+
|
|
82
|
+
- GoogleAuthProvider の設定を auth.ts に追加する
|
|
83
|
+
- トークン期限切れ時の更新処理を追加する
|
|
84
|
+
- ネットワーク障害時のエラーハンドリングを追加する
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### コミット & プッシュ
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
ai-git push
|
|
69
91
|
```
|
|
70
92
|
|
|
71
|
-
|
|
93
|
+
`git add .` → AI によるコミットメッセージ生成 → コミット → `git push` を一括で実行します。
|
|
72
94
|
|
|
73
|
-
- `
|
|
74
|
-
- `
|
|
75
|
-
- `e`: エディタで編集してからコミット
|
|
95
|
+
- upstream が未設定の場合は `git push -u origin <branch>` で自動設定
|
|
96
|
+
- PR は作成しません(PR を作りたい場合は `ai-git pr` を使用)
|
|
76
97
|
|
|
77
|
-
|
|
98
|
+
確認プロンプトで操作を選択できます。
|
|
78
99
|
|
|
79
|
-
|
|
100
|
+
| 入力 | 動作 |
|
|
101
|
+
|------|------|
|
|
102
|
+
| `y` | そのままコミット & プッシュ |
|
|
103
|
+
| `n` | 中止 |
|
|
104
|
+
| `e` | エディタで編集してからコミット & プッシュ |
|
|
80
105
|
|
|
81
106
|
```bash
|
|
82
|
-
|
|
83
|
-
git add
|
|
84
|
-
ai-git commit
|
|
107
|
+
# ステージ済みの差分のみ使う
|
|
108
|
+
ai-git push --no-add
|
|
85
109
|
```
|
|
86
110
|
|
|
87
|
-
|
|
111
|
+
### PR 作成
|
|
88
112
|
|
|
89
113
|
```bash
|
|
90
114
|
ai-git pr
|
|
91
|
-
|
|
92
|
-
# 言語指定も可能
|
|
93
|
-
ai-git pr --lang en
|
|
94
115
|
```
|
|
95
116
|
|
|
96
|
-
|
|
117
|
+
自動的に以下を実行します。
|
|
118
|
+
|
|
119
|
+
- ブランチがまだ push されていない場合: `git push -u origin <branch>`
|
|
120
|
+
- ローカルに新しいコミットがある場合: `git push`
|
|
121
|
+
- PR 説明文を生成して PR を作成
|
|
122
|
+
|
|
123
|
+
確認プロンプトで操作を選択できます。
|
|
97
124
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
125
|
+
| 入力 | 動作 |
|
|
126
|
+
|------|------|
|
|
127
|
+
| `y` | PR を作成 |
|
|
128
|
+
| `n` | 中止 |
|
|
129
|
+
| `e` | エディタで編集してから作成 |
|
|
101
130
|
|
|
102
|
-
|
|
131
|
+
**生成される PR 説明文の形式:**
|
|
103
132
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
133
|
+
```markdown
|
|
134
|
+
## Summary
|
|
135
|
+
変更の全体像を 2〜3 文で説明
|
|
136
|
+
|
|
137
|
+
## Changes
|
|
138
|
+
- 具体的な変更内容(3〜7 項目)
|
|
139
|
+
|
|
140
|
+
## Test plan
|
|
141
|
+
- テスト・確認方法(2〜4 項目)
|
|
142
|
+
```
|
|
107
143
|
|
|
108
144
|
**前提条件:**
|
|
109
145
|
|
|
110
146
|
- GitHub CLI (`gh`) がインストール済み ([インストール方法](https://cli.github.com/))
|
|
111
147
|
- `gh auth login` で認証済み
|
|
112
148
|
|
|
113
|
-
**生成されるPR説明文のフォーマット:**
|
|
114
|
-
|
|
115
|
-
- ## Summary: 変更の概要(2-3文)
|
|
116
|
-
- ## Changes: 具体的な変更内容(箇条書き)
|
|
117
|
-
- ## Test plan: テスト方法(箇条書き)
|
|
118
|
-
|
|
119
149
|
### ブランチ作成(自動命名)
|
|
120
150
|
|
|
121
151
|
変更差分(ステージ済み + 未ステージ)からブランチ名を推定して作成します。
|
|
@@ -126,21 +156,48 @@ ai-git checkout
|
|
|
126
156
|
|
|
127
157
|
命名例:
|
|
128
158
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
159
|
+
```
|
|
160
|
+
feat/google-login
|
|
161
|
+
fix/api-error-handling
|
|
162
|
+
docs/readme-update
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 言語設定
|
|
166
|
+
|
|
167
|
+
デフォルト言語は日本語です。
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# 今回だけ英語で生成
|
|
171
|
+
ai-git commit --lang en
|
|
172
|
+
ai-git pr --lang en
|
|
173
|
+
|
|
174
|
+
# デフォルト言語を永続化
|
|
175
|
+
ai-git --set-lang en
|
|
176
|
+
ai-git --set-lang ja
|
|
177
|
+
```
|
|
132
178
|
|
|
133
|
-
|
|
179
|
+
### ヘルプ
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
ai-git --help
|
|
183
|
+
```
|
|
134
184
|
|
|
135
185
|
## 開発
|
|
136
186
|
|
|
137
187
|
```bash
|
|
138
|
-
npm
|
|
188
|
+
npm install
|
|
139
189
|
npm run build
|
|
190
|
+
npm link # どの Git リポジトリでも ai-git が使えるようになります
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
npm run dev
|
|
140
195
|
```
|
|
141
196
|
|
|
142
197
|
## トラブルシューティング
|
|
143
198
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
199
|
+
| エラー | 対処 |
|
|
200
|
+
|--------|------|
|
|
201
|
+
| `No staged changes found.` | `git add` してから実行 |
|
|
202
|
+
| `GROQ_API_KEY が未設定です` | `GROQ_API_KEY` を設定 |
|
|
203
|
+
| `413 Request too large` / TPM 超過 | `git add -p` でステージを分割 |
|
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";
|
|
@@ -73,11 +74,13 @@ Usage: ai-git <command> [options]
|
|
|
73
74
|
|
|
74
75
|
Commands:
|
|
75
76
|
commit Generate commit message from staged changes
|
|
77
|
+
push Commit with AI message and push to remote (git add . + commit + push)
|
|
76
78
|
pr Generate PR description and create pull request
|
|
77
79
|
checkout Create branch from current changes
|
|
78
80
|
|
|
79
81
|
Commit Options:
|
|
80
82
|
--lang <ja|en> Set language for this run
|
|
83
|
+
--no-add Skip automatic git add .
|
|
81
84
|
|
|
82
85
|
PR Options:
|
|
83
86
|
--lang <ja|en> Set language for this run
|
|
@@ -93,8 +96,11 @@ Environment:
|
|
|
93
96
|
process.exit(0);
|
|
94
97
|
}
|
|
95
98
|
if (!subcommand ||
|
|
96
|
-
(subcommand !== "commit" &&
|
|
97
|
-
|
|
99
|
+
(subcommand !== "commit" &&
|
|
100
|
+
subcommand !== "push" &&
|
|
101
|
+
subcommand !== "pr" &&
|
|
102
|
+
subcommand !== "checkout")) {
|
|
103
|
+
console.error("Error: Please specify a command: 'commit', 'push', 'pr' or 'checkout'");
|
|
98
104
|
console.error("Run 'ai-git --help' for usage information");
|
|
99
105
|
process.exit(1);
|
|
100
106
|
}
|
|
@@ -114,17 +120,69 @@ async function generateCommitMessage(diff) {
|
|
|
114
120
|
const inputDiff = truncateByChars(diff, COMMIT_DIFF_COMPACT_CHARS);
|
|
115
121
|
const prompt = buildPrompt(inputDiff, language);
|
|
116
122
|
try {
|
|
117
|
-
|
|
123
|
+
const raw = await generateText(prompt);
|
|
124
|
+
return normalizeGeneratedCommitMessage(raw, diff, language);
|
|
118
125
|
}
|
|
119
126
|
catch (error) {
|
|
120
127
|
if (isRequestTooLargeError(error)) {
|
|
121
128
|
const smallerDiff = truncateByChars(diff, 1800);
|
|
122
|
-
|
|
129
|
+
const retryRaw = await generateText(buildPrompt(smallerDiff, language));
|
|
130
|
+
return normalizeGeneratedCommitMessage(retryRaw, diff, language);
|
|
123
131
|
}
|
|
124
132
|
handleGroqError(error);
|
|
125
133
|
process.exit(1);
|
|
126
134
|
}
|
|
127
135
|
}
|
|
136
|
+
function normalizeGeneratedCommitMessage(raw, diff, lang) {
|
|
137
|
+
const lines = raw
|
|
138
|
+
.split("\n")
|
|
139
|
+
.map((line) => line.replace(/\s+$/g, ""))
|
|
140
|
+
.filter((_, idx, arr) => !(idx > 0 && arr[idx - 1] === "" && arr[idx] === ""));
|
|
141
|
+
if (lines.length === 0) {
|
|
142
|
+
return buildFallbackCommitMessage(diff, lang);
|
|
143
|
+
}
|
|
144
|
+
const subjectLine = lines[0]?.trim() || "";
|
|
145
|
+
const conventionalMatch = /^[a-z]+(\([^)]+\))?:\s+(.+)$/.exec(subjectLine);
|
|
146
|
+
const hasConventionalPrefix = Boolean(conventionalMatch);
|
|
147
|
+
const shortDescription = conventionalMatch?.[2]?.trim() || subjectLine;
|
|
148
|
+
if (!hasConventionalPrefix ||
|
|
149
|
+
looksLikePathOnly(shortDescription) ||
|
|
150
|
+
shortDescription.length < 8) {
|
|
151
|
+
lines[0] = buildFallbackSubjectLine(diff, lang);
|
|
152
|
+
}
|
|
153
|
+
return lines.join("\n").trim();
|
|
154
|
+
}
|
|
155
|
+
function looksLikePathOnly(value) {
|
|
156
|
+
const normalized = value.trim().toLowerCase();
|
|
157
|
+
if (!normalized) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
const hasPathToken = normalized.includes("/") || normalized.includes(".");
|
|
161
|
+
return hasPathToken && /^[a-z0-9/_\-.]+$/.test(normalized);
|
|
162
|
+
}
|
|
163
|
+
function buildFallbackCommitMessage(diff, lang) {
|
|
164
|
+
const subject = buildFallbackSubjectLine(diff, lang);
|
|
165
|
+
if (lang === "ja") {
|
|
166
|
+
return `${subject}\n\n- 差分の主目的を明確にし、変更理由が伝わる形に整える`;
|
|
167
|
+
}
|
|
168
|
+
return `${subject}\n\n- Clarify the main change intent so the reason is easy to understand`;
|
|
169
|
+
}
|
|
170
|
+
function buildFallbackSubjectLine(diff, lang) {
|
|
171
|
+
const files = getChangedFiles();
|
|
172
|
+
const type = inferBranchType(files, diff);
|
|
173
|
+
const rawTopic = inferTopic(files, diff);
|
|
174
|
+
const topic = rawTopic ? rawTopic.replace(/-/g, " ") : "";
|
|
175
|
+
if (lang === "ja") {
|
|
176
|
+
const description = topic
|
|
177
|
+
? `${topic} の変更意図を分かりやすく整理する`
|
|
178
|
+
: "変更意図が伝わるように更新内容を整理する";
|
|
179
|
+
return `${type}: ${description}`;
|
|
180
|
+
}
|
|
181
|
+
const description = topic
|
|
182
|
+
? `clarify the intent behind ${topic} changes`
|
|
183
|
+
: "clarify update intent in one clear sentence";
|
|
184
|
+
return `${type}: ${description}`;
|
|
185
|
+
}
|
|
128
186
|
function buildPrompt(diff, lang) {
|
|
129
187
|
if (lang === "ja") {
|
|
130
188
|
return `あなたは git のコミットメッセージ作成の専門家です。
|
|
@@ -208,6 +266,13 @@ function doCommit(message) {
|
|
|
208
266
|
process.exit(1);
|
|
209
267
|
}
|
|
210
268
|
}
|
|
269
|
+
function stageAllChanges() {
|
|
270
|
+
const result = (0, child_process_1.spawnSync)("git", ["add", "."], { stdio: "inherit" });
|
|
271
|
+
if (result.status !== 0) {
|
|
272
|
+
console.error("Error: git add . failed");
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
211
276
|
async function mainCheckout() {
|
|
212
277
|
const suggested = await suggestBranchName();
|
|
213
278
|
const branchName = ensureAvailableBranchName(suggested);
|
|
@@ -428,15 +493,11 @@ function branchExists(name) {
|
|
|
428
493
|
return false;
|
|
429
494
|
}
|
|
430
495
|
}
|
|
431
|
-
// ──
|
|
432
|
-
async function
|
|
433
|
-
if (
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
437
|
-
if (subcommand === "pr") {
|
|
438
|
-
await mainPR();
|
|
439
|
-
return;
|
|
496
|
+
// ── コミットフロー共通処理 ────────────────────────────────
|
|
497
|
+
async function runCommitFlow() {
|
|
498
|
+
if (!noAdd) {
|
|
499
|
+
console.log("📦 変更をステージしています... (git add .)");
|
|
500
|
+
stageAllChanges();
|
|
440
501
|
}
|
|
441
502
|
const diff = getStagedDiff();
|
|
442
503
|
if (!diff.trim()) {
|
|
@@ -446,7 +507,6 @@ async function main() {
|
|
|
446
507
|
console.log("🤖 コミットメッセージを生成中... (compact summary input)");
|
|
447
508
|
const message = await generateCommitMessage(diff);
|
|
448
509
|
console.log(`\n📝 Generated commit message:\n`);
|
|
449
|
-
// 詳細モードは複数行なのでインデントして表示
|
|
450
510
|
message.split("\n").forEach((line) => {
|
|
451
511
|
console.log(` ${line}`);
|
|
452
512
|
});
|
|
@@ -472,6 +532,53 @@ async function main() {
|
|
|
472
532
|
doCommit(finalMessage);
|
|
473
533
|
console.log(`\n✅ Committed successfully!`);
|
|
474
534
|
}
|
|
535
|
+
// ── push サブコマンド ─────────────────────────────────────
|
|
536
|
+
async function mainPush() {
|
|
537
|
+
await runCommitFlow();
|
|
538
|
+
const currentBranch = (0, child_process_1.execSync)("git branch --show-current", {
|
|
539
|
+
encoding: "utf-8",
|
|
540
|
+
}).trim();
|
|
541
|
+
// upstream が設定済みか確認し、なければ -u origin で push
|
|
542
|
+
try {
|
|
543
|
+
(0, child_process_1.execSync)(`git rev-parse --abbrev-ref ${currentBranch}@{upstream}`, {
|
|
544
|
+
encoding: "utf-8",
|
|
545
|
+
stdio: "pipe",
|
|
546
|
+
});
|
|
547
|
+
console.log(`📤 push 中... (origin ${currentBranch})`);
|
|
548
|
+
const pushResult = (0, child_process_1.spawnSync)("git", ["push"], { stdio: "inherit" });
|
|
549
|
+
if (pushResult.status !== 0) {
|
|
550
|
+
console.error("❌ git push に失敗しました。");
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
console.log(`📤 push 中... (origin ${currentBranch} を新規作成)`);
|
|
556
|
+
const pushResult = (0, child_process_1.spawnSync)("git", ["push", "-u", "origin", currentBranch], {
|
|
557
|
+
stdio: "inherit",
|
|
558
|
+
});
|
|
559
|
+
if (pushResult.status !== 0) {
|
|
560
|
+
console.error("❌ git push に失敗しました。");
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
console.log(`\n✅ Push 完了!`);
|
|
565
|
+
}
|
|
566
|
+
// ── メイン ───────────────────────────────────────────────
|
|
567
|
+
async function main() {
|
|
568
|
+
if (subcommand === "checkout") {
|
|
569
|
+
await mainCheckout();
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (subcommand === "pr") {
|
|
573
|
+
await mainPR();
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (subcommand === "push") {
|
|
577
|
+
await mainPush();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
await runCommitFlow();
|
|
581
|
+
}
|
|
475
582
|
main().catch((err) => {
|
|
476
583
|
console.error("❌ 予期しないエラー:", err.message);
|
|
477
584
|
process.exit(1);
|
|
@@ -690,16 +797,24 @@ function getBranchDiff(baseBranch) {
|
|
|
690
797
|
function buildPRPrompt(commits, diff, lang) {
|
|
691
798
|
if (lang === "ja") {
|
|
692
799
|
return `あなたは GitHub の Pull Request 作成の専門家です。
|
|
693
|
-
次のコミット履歴と差分から、PR
|
|
800
|
+
次のコミット履歴と差分から、PR のタイトルと説明文を生成してください。
|
|
694
801
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
802
|
+
出力フォーマット(必ずこの順番で出力してください):
|
|
803
|
+
1行目: Title: <Conventional Commits 形式のタイトル(72文字以内、日本語で)>
|
|
804
|
+
2行目: 空行
|
|
805
|
+
3行目以降: PR 説明文(以下の形式)
|
|
806
|
+
|
|
807
|
+
説明文のルール:
|
|
808
|
+
- ## Summary: 1-2文で変更の概要を説明
|
|
698
809
|
- ## Changes: "- " で始まる箇条書き(3-7個)で具体的な変更内容
|
|
699
810
|
- ## Test plan: テスト方法の箇条書き(2-4個)
|
|
700
811
|
- 命令形を使う
|
|
701
812
|
- WHATとWHYを重視し、HOWは最小限に
|
|
702
|
-
-
|
|
813
|
+
- 出力はタイトルとPR説明文のみ(余計な説明は不要)
|
|
814
|
+
|
|
815
|
+
タイトルの例:
|
|
816
|
+
Title: feat: push サブコマンドを追加する
|
|
817
|
+
Title: fix: コミットメッセージ生成のエラーハンドリングを修正する
|
|
703
818
|
|
|
704
819
|
コミット履歴:
|
|
705
820
|
${commits}
|
|
@@ -708,16 +823,24 @@ ${commits}
|
|
|
708
823
|
${diff}`;
|
|
709
824
|
}
|
|
710
825
|
return `You are an expert at writing GitHub Pull Request descriptions.
|
|
711
|
-
Generate a PR description from the following commit history and diff.
|
|
826
|
+
Generate a PR title and description from the following commit history and diff.
|
|
712
827
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
828
|
+
Output format (in this exact order):
|
|
829
|
+
Line 1: Title: <Conventional Commits title, max 72 chars>
|
|
830
|
+
Line 2: empty line
|
|
831
|
+
Line 3+: PR description in the following format
|
|
832
|
+
|
|
833
|
+
Description rules:
|
|
834
|
+
- ## Summary: 1-2 sentences explaining the overall change
|
|
716
835
|
- ## Changes: bullet points (3-7 items) with "- " prefix detailing specific changes
|
|
717
836
|
- ## Test plan: bullet points (2-4 items) describing how to test
|
|
718
837
|
- Use imperative mood
|
|
719
838
|
- Focus on WHAT and WHY, minimize HOW
|
|
720
|
-
- Output ONLY the PR description, no extra explanation
|
|
839
|
+
- Output ONLY the title line and PR description, no extra explanation
|
|
840
|
+
|
|
841
|
+
Title examples:
|
|
842
|
+
Title: feat: add push subcommand
|
|
843
|
+
Title: fix: improve error handling in commit message generation
|
|
721
844
|
|
|
722
845
|
Commit history:
|
|
723
846
|
${commits}
|
|
@@ -770,10 +893,51 @@ function isRequestTooLargeError(error) {
|
|
|
770
893
|
lower.includes("tokens per minute") ||
|
|
771
894
|
lower.includes("tpm"));
|
|
772
895
|
}
|
|
896
|
+
/**
|
|
897
|
+
* AI が出力した "Title: <text>" 行からタイトルを抽出する。
|
|
898
|
+
* 見つからない場合は description の先頭テキストからフォールバック。
|
|
899
|
+
*/
|
|
900
|
+
function extractPRTitle(raw) {
|
|
901
|
+
const firstLine = raw.split("\n")[0]?.trim() ?? "";
|
|
902
|
+
const titleMatch = /^Title:\s*(.+)$/i.exec(firstLine);
|
|
903
|
+
if (titleMatch?.[1]) {
|
|
904
|
+
return titleMatch[1].trim();
|
|
905
|
+
}
|
|
906
|
+
// フォールバック: Summary 直下の最初の文
|
|
907
|
+
const lines = raw
|
|
908
|
+
.split("\n")
|
|
909
|
+
.map((v) => v.trim())
|
|
910
|
+
.filter(Boolean);
|
|
911
|
+
const summaryIdx = lines.findIndex((l) => l.toLowerCase().startsWith("## summary"));
|
|
912
|
+
let candidate = summaryIdx >= 0
|
|
913
|
+
? (lines.slice(summaryIdx + 1).find((l) => !l.startsWith("##")) ?? "")
|
|
914
|
+
: (lines.find((l) => !l.startsWith("#") && !l.startsWith("-")) ?? "");
|
|
915
|
+
candidate = candidate
|
|
916
|
+
.replace(/^this pull request\s+(is|does)\s*/i, "")
|
|
917
|
+
.replace(/^この\s*pull request\s*は、?/i, "")
|
|
918
|
+
.replace(/^このprは、?/i, "")
|
|
919
|
+
.replace(/\s+/g, " ")
|
|
920
|
+
.trim();
|
|
921
|
+
const sentenceCut = candidate.search(/[。.!?]/);
|
|
922
|
+
if (sentenceCut > 0) {
|
|
923
|
+
candidate = candidate.slice(0, sentenceCut);
|
|
924
|
+
}
|
|
925
|
+
return candidate || "Update project changes";
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* "Title: <text>" 行を description 本文から除いて返す。
|
|
929
|
+
*/
|
|
930
|
+
function stripTitleLine(raw) {
|
|
931
|
+
const lines = raw.split("\n");
|
|
932
|
+
if (/^Title:\s*/i.test(lines[0]?.trim() ?? "")) {
|
|
933
|
+
// Title 行と直後の空行を除去
|
|
934
|
+
return lines.slice(lines[1]?.trim() === "" ? 2 : 1).join("\n").trimStart();
|
|
935
|
+
}
|
|
936
|
+
return raw;
|
|
937
|
+
}
|
|
773
938
|
function createPR(description, baseBranch, fallbackURL) {
|
|
774
|
-
|
|
775
|
-
const
|
|
776
|
-
const titleLine = lines.find((l) => l.trim() && !l.startsWith("#")) || "Pull Request";
|
|
939
|
+
const titleLine = extractPRTitle(description);
|
|
940
|
+
const body = stripTitleLine(description);
|
|
777
941
|
const result = (0, child_process_1.spawnSync)("gh", [
|
|
778
942
|
"pr",
|
|
779
943
|
"create",
|
|
@@ -782,7 +946,7 @@ function createPR(description, baseBranch, fallbackURL) {
|
|
|
782
946
|
"--title",
|
|
783
947
|
titleLine.trim(),
|
|
784
948
|
"--body",
|
|
785
|
-
|
|
949
|
+
body,
|
|
786
950
|
], { encoding: "utf-8", stdio: "pipe" });
|
|
787
951
|
if (result.stdout) {
|
|
788
952
|
process.stdout.write(result.stdout);
|
|
@@ -864,8 +1028,12 @@ async function mainPR() {
|
|
|
864
1028
|
}
|
|
865
1029
|
console.log(`🤖 PR説明文を生成中... (${baseBranch}...${currentBranch}) [compact summary input]`);
|
|
866
1030
|
const description = await generatePRDescription(baseBranch);
|
|
867
|
-
console.log(`\n📝 Generated PR
|
|
868
|
-
|
|
1031
|
+
console.log(`\n📝 Generated PR:\n`);
|
|
1032
|
+
console.log(` Title: ${extractPRTitle(description)}`);
|
|
1033
|
+
console.log();
|
|
1034
|
+
stripTitleLine(description)
|
|
1035
|
+
.split("\n")
|
|
1036
|
+
.forEach((line) => {
|
|
869
1037
|
console.log(` ${line}`);
|
|
870
1038
|
});
|
|
871
1039
|
console.log();
|