ai-git-tool 1.2.0 → 1.4.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/LICENSE +21 -0
- package/README.md +74 -23
- package/dist/commands/checkout.js +30 -0
- package/dist/commands/commit.js +55 -0
- package/dist/commands/pr.js +76 -0
- package/dist/commands/push.js +78 -0
- package/dist/index.js +30 -994
- package/dist/services/ai.js +520 -0
- package/dist/services/branch.js +37 -0
- package/dist/services/github.js +479 -0
- package/dist/types.js +2 -0
- package/dist/utils/config.js +98 -0
- package/dist/utils/errors.js +81 -0
- package/dist/utils/git.js +157 -0
- package/dist/utils/text.js +23 -0
- package/dist/utils/ui.js +73 -0
- package/package.json +5 -2
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getGroqClient = getGroqClient;
|
|
7
|
+
exports.generateText = generateText;
|
|
8
|
+
exports.generateCommitMessage = generateCommitMessage;
|
|
9
|
+
exports.extractPRTitle = extractPRTitle;
|
|
10
|
+
exports.stripTitleLine = stripTitleLine;
|
|
11
|
+
exports.generatePRDescription = generatePRDescription;
|
|
12
|
+
exports.suggestBranchNameWithAI = suggestBranchNameWithAI;
|
|
13
|
+
exports.suggestBranchName = suggestBranchName;
|
|
14
|
+
const openai_1 = __importDefault(require("openai"));
|
|
15
|
+
const errors_js_1 = require("../utils/errors.js");
|
|
16
|
+
const text_js_1 = require("../utils/text.js");
|
|
17
|
+
const git_js_1 = require("../utils/git.js");
|
|
18
|
+
const defaultGroqModel = "llama-3.3-70b-versatile";
|
|
19
|
+
const COMMIT_DIFF_COMPACT_CHARS = 3500;
|
|
20
|
+
const PR_COMMITS_COMPACT_CHARS = 1800;
|
|
21
|
+
const PR_DIFF_COMPACT_CHARS = 3500;
|
|
22
|
+
/**
|
|
23
|
+
* Groq クライアントを取得
|
|
24
|
+
*/
|
|
25
|
+
function getGroqClient() {
|
|
26
|
+
const apiKey = process.env.GROQ_API_KEY;
|
|
27
|
+
if (!apiKey) {
|
|
28
|
+
(0, errors_js_1.showFriendlyError)("GROQ_API_KEY が未設定です", "AI 機能を使用するには Groq API キーが必要です", [
|
|
29
|
+
"Groq にサインアップ(無料): https://console.groq.com/",
|
|
30
|
+
"API キーを取得: https://console.groq.com/keys",
|
|
31
|
+
"環境変数に設定:",
|
|
32
|
+
' - 一時的: export GROQ_API_KEY="gsk_..."',
|
|
33
|
+
" - 永続的: ~/.bashrc や ~/.zshrc に上記を追加",
|
|
34
|
+
], [
|
|
35
|
+
"API キー設定後に ai-git commit を実行",
|
|
36
|
+
"API キー設定後に ai-git pr を実行",
|
|
37
|
+
]);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
return new openai_1.default({
|
|
41
|
+
apiKey,
|
|
42
|
+
baseURL: "https://api.groq.com/openai/v1",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* AI でテキストを生成
|
|
47
|
+
*/
|
|
48
|
+
async function generateText(prompt) {
|
|
49
|
+
const client = getGroqClient();
|
|
50
|
+
const model = process.env.GROQ_MODEL || defaultGroqModel;
|
|
51
|
+
const res = await client.chat.completions.create({
|
|
52
|
+
model,
|
|
53
|
+
temperature: 0.2,
|
|
54
|
+
messages: [{ role: "user", content: prompt }],
|
|
55
|
+
});
|
|
56
|
+
return res.choices[0]?.message?.content?.trim() || "";
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* コミットメッセージ生成用プロンプトを構築
|
|
60
|
+
*/
|
|
61
|
+
function buildCommitPrompt(diff, lang) {
|
|
62
|
+
if (lang === "ja") {
|
|
63
|
+
return `あなたは git のコミットメッセージ作成の専門家です。
|
|
64
|
+
次の git diff を注意深く分析し、Conventional Commits 形式の詳細コミットメッセージを生成してください。
|
|
65
|
+
|
|
66
|
+
重要: diff の内容を正確に読み取り、実際に何が変更されたかを理解してください。
|
|
67
|
+
変更されたファイル名、追加/削除された関数名、変更の目的を特定してください。
|
|
68
|
+
|
|
69
|
+
ルール:
|
|
70
|
+
- 1行目: <type>(<optional scope>): <short description>(72文字以内)
|
|
71
|
+
* type は実際の変更内容に基づいて選択(feat, fix, docs, refactor, test, chore など)
|
|
72
|
+
* scope は変更の影響範囲(例: error-handling, ui, api)
|
|
73
|
+
- 2行目は空行
|
|
74
|
+
- 本文は "- " で始まる箇条書き
|
|
75
|
+
- 箇条書きは 3-5 行で、具体的な変更内容を記述
|
|
76
|
+
- WHAT(何を変更したか)と WHY(なぜ変更したか)を書く
|
|
77
|
+
- HOW(どうやって実装したか)は書かない
|
|
78
|
+
- 命令形を使う(「追加する」「修正する」など)
|
|
79
|
+
- 出力はコミットメッセージ本文のみ(説明文は不要)
|
|
80
|
+
- 説明文は日本語にする
|
|
81
|
+
|
|
82
|
+
出力例:
|
|
83
|
+
feat(auth): Google ログインを追加する
|
|
84
|
+
|
|
85
|
+
- auth.ts に GoogleAuthProvider の設定を追加する
|
|
86
|
+
- トークン期限切れ時の更新処理を追加する
|
|
87
|
+
- ネットワーク障害時のエラーハンドリングを追加する
|
|
88
|
+
|
|
89
|
+
Git diff:
|
|
90
|
+
${diff}`;
|
|
91
|
+
}
|
|
92
|
+
return `You are an expert at writing git commit messages.
|
|
93
|
+
Carefully analyze the following git diff and generate a detailed commit message in Conventional Commits format.
|
|
94
|
+
|
|
95
|
+
IMPORTANT: Read the diff content accurately and understand what was actually changed.
|
|
96
|
+
Identify changed file names, added/removed function names, and the purpose of the changes.
|
|
97
|
+
|
|
98
|
+
Rules:
|
|
99
|
+
- First line: <type>(<optional scope>): <short description> (max 72 chars)
|
|
100
|
+
* Choose type based on actual changes (feat, fix, docs, refactor, test, chore, etc.)
|
|
101
|
+
* Scope should reflect the area of impact (e.g., error-handling, ui, api)
|
|
102
|
+
- Blank line after the first line
|
|
103
|
+
- Body: bullet points starting with "- " describing specific changes
|
|
104
|
+
- 3-5 bullet points explaining WHAT was changed and WHY
|
|
105
|
+
- Do NOT explain HOW it was implemented
|
|
106
|
+
- Use imperative mood ("add" not "added")
|
|
107
|
+
- Output ONLY the commit message, nothing else
|
|
108
|
+
|
|
109
|
+
Example:
|
|
110
|
+
feat(auth): add Google login with Firebase Auth
|
|
111
|
+
|
|
112
|
+
- Add GoogleAuthProvider setup in auth.ts
|
|
113
|
+
- Handle token refresh on expiry
|
|
114
|
+
- Add error handling for network failures
|
|
115
|
+
|
|
116
|
+
Git diff:
|
|
117
|
+
${diff}`;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* ファイルパスだけのような内容かチェック
|
|
121
|
+
*/
|
|
122
|
+
function looksLikePathOnly(value) {
|
|
123
|
+
const normalized = value.trim().toLowerCase();
|
|
124
|
+
if (!normalized) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const hasPathToken = normalized.includes("/") || normalized.includes(".");
|
|
128
|
+
return hasPathToken && /^[a-z0-9/_\-.]+$/.test(normalized);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* フォールバックのサブジェクト行を生成
|
|
132
|
+
*/
|
|
133
|
+
function buildFallbackSubjectLine(diff, lang) {
|
|
134
|
+
const files = (0, git_js_1.getChangedFiles)();
|
|
135
|
+
const type = inferBranchType(files, diff);
|
|
136
|
+
const rawTopic = inferTopic(files, diff);
|
|
137
|
+
const topic = rawTopic ? rawTopic.replace(/-/g, " ") : "";
|
|
138
|
+
if (lang === "ja") {
|
|
139
|
+
const description = topic
|
|
140
|
+
? `${topic} の変更意図を分かりやすく整理する`
|
|
141
|
+
: "変更意図が伝わるように更新内容を整理する";
|
|
142
|
+
return `${type}: ${description}`;
|
|
143
|
+
}
|
|
144
|
+
const description = topic
|
|
145
|
+
? `clarify the intent behind ${topic} changes`
|
|
146
|
+
: "clarify update intent in one clear sentence";
|
|
147
|
+
return `${type}: ${description}`;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* フォールバックのコミットメッセージを生成
|
|
151
|
+
*/
|
|
152
|
+
function buildFallbackCommitMessage(diff, lang) {
|
|
153
|
+
const subject = buildFallbackSubjectLine(diff, lang);
|
|
154
|
+
if (lang === "ja") {
|
|
155
|
+
return `${subject}\n\n- 差分の主目的を明確にし、変更理由が伝わる形に整える`;
|
|
156
|
+
}
|
|
157
|
+
return `${subject}\n\n- Clarify the main change intent so the reason is easy to understand`;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* AI が生成したコミットメッセージを正規化
|
|
161
|
+
*/
|
|
162
|
+
function normalizeGeneratedCommitMessage(raw, diff, lang) {
|
|
163
|
+
const lines = raw
|
|
164
|
+
.split("\n")
|
|
165
|
+
.map((line) => line.replace(/\s+$/g, ""))
|
|
166
|
+
.filter((_, idx, arr) => !(idx > 0 && arr[idx - 1] === "" && arr[idx] === ""));
|
|
167
|
+
if (lines.length === 0) {
|
|
168
|
+
return buildFallbackCommitMessage(diff, lang);
|
|
169
|
+
}
|
|
170
|
+
const subjectLine = lines[0]?.trim() || "";
|
|
171
|
+
const conventionalMatch = /^[a-z]+(\([^)]+\))?:\s+(.+)$/.exec(subjectLine);
|
|
172
|
+
const hasConventionalPrefix = Boolean(conventionalMatch);
|
|
173
|
+
const shortDescription = conventionalMatch?.[2]?.trim() || subjectLine;
|
|
174
|
+
if (!hasConventionalPrefix ||
|
|
175
|
+
looksLikePathOnly(shortDescription) ||
|
|
176
|
+
shortDescription.length < 8) {
|
|
177
|
+
lines[0] = buildFallbackSubjectLine(diff, lang);
|
|
178
|
+
}
|
|
179
|
+
return lines.join("\n").trim();
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* git diff から AI を使ってコミットメッセージを生成
|
|
183
|
+
*/
|
|
184
|
+
async function generateCommitMessage(diff, language) {
|
|
185
|
+
const inputDiff = (0, text_js_1.truncateByChars)(diff, COMMIT_DIFF_COMPACT_CHARS);
|
|
186
|
+
const prompt = buildCommitPrompt(inputDiff, language);
|
|
187
|
+
try {
|
|
188
|
+
const raw = await generateText(prompt);
|
|
189
|
+
return normalizeGeneratedCommitMessage(raw, diff, language);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if ((0, errors_js_1.isRequestTooLargeError)(error)) {
|
|
193
|
+
const smallerDiff = (0, text_js_1.truncateByChars)(diff, 1800);
|
|
194
|
+
const retryRaw = await generateText(buildCommitPrompt(smallerDiff, language));
|
|
195
|
+
return normalizeGeneratedCommitMessage(retryRaw, diff, language);
|
|
196
|
+
}
|
|
197
|
+
(0, errors_js_1.handleGroqError)(error);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* PR 説明文生成用プロンプトを構築
|
|
203
|
+
*/
|
|
204
|
+
function buildPRPrompt(commits, diff, lang) {
|
|
205
|
+
if (lang === "ja") {
|
|
206
|
+
return `あなたは GitHub の Pull Request 作成の専門家です。
|
|
207
|
+
次のコミット履歴と差分から、PR のタイトルと説明文を生成してください。
|
|
208
|
+
|
|
209
|
+
出力フォーマット(必ずこの順番で出力してください):
|
|
210
|
+
1行目: Title: <Conventional Commits 形式のタイトル(72文字以内、日本語で)>
|
|
211
|
+
2行目: 空行
|
|
212
|
+
3行目以降: PR 説明文(以下の形式)
|
|
213
|
+
|
|
214
|
+
説明文のルール:
|
|
215
|
+
- ## Summary の後は改行し、次の行から概要を 1-2 文で説明
|
|
216
|
+
- ## Changes の後は改行し、次の行から "- " で始まる箇条書き(3-7個)で具体的な変更内容
|
|
217
|
+
- ## Test plan の後は改行し、次の行から "- " で始まるテスト方法の箇条書き(2-4個)
|
|
218
|
+
- 命令形を使う
|
|
219
|
+
- WHATとWHYを重視し、HOWは最小限に
|
|
220
|
+
- 出力はタイトルとPR説明文のみ(余計な説明は不要)
|
|
221
|
+
|
|
222
|
+
出力例:
|
|
223
|
+
Title: feat: push サブコマンドを追加する
|
|
224
|
+
|
|
225
|
+
## Summary
|
|
226
|
+
コミット後に自動でリモートにプッシュする機能を追加しました。
|
|
227
|
+
|
|
228
|
+
## Changes
|
|
229
|
+
- push サブコマンドを追加し、コミットとプッシュを一括実行できるようにする
|
|
230
|
+
- upstream が未設定の場合は自動で設定する機能を追加する
|
|
231
|
+
- エラーハンドリングを強化する
|
|
232
|
+
|
|
233
|
+
## Test plan
|
|
234
|
+
- ai-git push を実行してコミットとプッシュが正常に動作するか確認する
|
|
235
|
+
- upstream が未設定の状態でも正しくプッシュされるか確認する
|
|
236
|
+
|
|
237
|
+
コミット履歴:
|
|
238
|
+
${commits}
|
|
239
|
+
|
|
240
|
+
変更差分:
|
|
241
|
+
${diff}`;
|
|
242
|
+
}
|
|
243
|
+
return `You are an expert at writing GitHub Pull Request descriptions.
|
|
244
|
+
Generate a PR title and description from the following commit history and diff.
|
|
245
|
+
|
|
246
|
+
Output format (in this exact order):
|
|
247
|
+
Line 1: Title: <Conventional Commits title, max 72 chars>
|
|
248
|
+
Line 2: empty line
|
|
249
|
+
Line 3+: PR description in the following format
|
|
250
|
+
|
|
251
|
+
Description rules:
|
|
252
|
+
- ## Summary should be followed by a line break, then 1-2 sentences explaining the overall change
|
|
253
|
+
- ## Changes should be followed by a line break, then bullet points (3-7 items) with "- " prefix detailing specific changes
|
|
254
|
+
- ## Test plan should be followed by a line break, then bullet points (2-4 items) describing how to test
|
|
255
|
+
- Use imperative mood
|
|
256
|
+
- Focus on WHAT and WHY, minimize HOW
|
|
257
|
+
- Output ONLY the title line and PR description, no extra explanation
|
|
258
|
+
|
|
259
|
+
Output example:
|
|
260
|
+
Title: feat: add push subcommand
|
|
261
|
+
|
|
262
|
+
## Summary
|
|
263
|
+
Added a new push subcommand that automatically commits and pushes changes to remote.
|
|
264
|
+
|
|
265
|
+
## Changes
|
|
266
|
+
- Add push subcommand to execute commit and push in one command
|
|
267
|
+
- Add automatic upstream setup when not configured
|
|
268
|
+
- Improve error handling for push operations
|
|
269
|
+
|
|
270
|
+
## Test plan
|
|
271
|
+
- Run ai-git push and verify commit and push work correctly
|
|
272
|
+
- Verify it works correctly when upstream is not set
|
|
273
|
+
|
|
274
|
+
Commit history:
|
|
275
|
+
${commits}
|
|
276
|
+
|
|
277
|
+
Diff:
|
|
278
|
+
${diff}`;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* PR のタイトルを抽出
|
|
282
|
+
*/
|
|
283
|
+
function extractPRTitle(raw) {
|
|
284
|
+
const firstLine = raw.split("\n")[0]?.trim() ?? "";
|
|
285
|
+
const titleMatch = /^Title:\s*(.+)$/i.exec(firstLine);
|
|
286
|
+
if (titleMatch?.[1]) {
|
|
287
|
+
return titleMatch[1].trim();
|
|
288
|
+
}
|
|
289
|
+
const lines = raw
|
|
290
|
+
.split("\n")
|
|
291
|
+
.map((v) => v.trim())
|
|
292
|
+
.filter(Boolean);
|
|
293
|
+
const summaryIdx = lines.findIndex((l) => l.toLowerCase().startsWith("## summary"));
|
|
294
|
+
let candidate = summaryIdx >= 0
|
|
295
|
+
? (lines.slice(summaryIdx + 1).find((l) => !l.startsWith("##")) ?? "")
|
|
296
|
+
: (lines.find((l) => !l.startsWith("#") && !l.startsWith("-")) ?? "");
|
|
297
|
+
candidate = candidate
|
|
298
|
+
.replace(/^this pull request\s+(is|does)\s*/i, "")
|
|
299
|
+
.replace(/^この\s*pull request\s*は、?/i, "")
|
|
300
|
+
.replace(/^このprは、?/i, "")
|
|
301
|
+
.replace(/\s+/g, " ")
|
|
302
|
+
.trim();
|
|
303
|
+
const sentenceCut = candidate.search(/[。.!?]/);
|
|
304
|
+
if (sentenceCut > 0) {
|
|
305
|
+
candidate = candidate.slice(0, sentenceCut);
|
|
306
|
+
}
|
|
307
|
+
return candidate || "Update project changes";
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Title 行を除去
|
|
311
|
+
*/
|
|
312
|
+
function stripTitleLine(raw) {
|
|
313
|
+
const lines = raw.split("\n");
|
|
314
|
+
if (/^Title:\s*/i.test(lines[0]?.trim() ?? "")) {
|
|
315
|
+
return lines
|
|
316
|
+
.slice(lines[1]?.trim() === "" ? 2 : 1)
|
|
317
|
+
.join("\n")
|
|
318
|
+
.trimStart();
|
|
319
|
+
}
|
|
320
|
+
return raw;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* PR 説明文を生成
|
|
324
|
+
*/
|
|
325
|
+
async function generatePRDescription(baseBranch, language, getBranchDiff) {
|
|
326
|
+
const { commits, diff } = getBranchDiff(baseBranch, language);
|
|
327
|
+
const inputCommits = (0, text_js_1.truncateByChars)(commits, PR_COMMITS_COMPACT_CHARS);
|
|
328
|
+
const inputDiff = (0, text_js_1.truncateByChars)(diff, PR_DIFF_COMPACT_CHARS);
|
|
329
|
+
const prompt = buildPRPrompt(inputCommits, inputDiff, language);
|
|
330
|
+
try {
|
|
331
|
+
return await generateText(prompt);
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
if ((0, errors_js_1.isRequestTooLargeError)(error)) {
|
|
335
|
+
const compactCommits = (0, text_js_1.truncateByChars)(commits, 1200);
|
|
336
|
+
const compactDiff = (0, text_js_1.truncateByChars)(diff, 2200);
|
|
337
|
+
const compactPrompt = buildPRPrompt(compactCommits, compactDiff, language);
|
|
338
|
+
if (language === "ja") {
|
|
339
|
+
console.log("ℹ️ 入力サイズが大きいため、差分を要約モードにして再試行します...");
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
console.log("ℹ️ Input is too large. Retrying with compact diff summary...");
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
return await generateText(compactPrompt);
|
|
346
|
+
}
|
|
347
|
+
catch (retryError) {
|
|
348
|
+
(0, errors_js_1.handleGroqError)(retryError);
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
(0, errors_js_1.handleGroqError)(error);
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// ブランチ名推測用のヘルパー関数
|
|
357
|
+
function inferBranchType(files, diff) {
|
|
358
|
+
const lowerFiles = files.map((f) => f.toLowerCase());
|
|
359
|
+
const lowerDiff = diff.toLowerCase();
|
|
360
|
+
if (lowerFiles.length > 0 && lowerFiles.every((f) => f.endsWith(".md"))) {
|
|
361
|
+
return "docs";
|
|
362
|
+
}
|
|
363
|
+
if (lowerFiles.some((f) => f.includes("readme") || f.endsWith(".md")) &&
|
|
364
|
+
lowerFiles.length <= 2) {
|
|
365
|
+
return "docs";
|
|
366
|
+
}
|
|
367
|
+
if (lowerFiles.some((f) => f.includes("package.json") || f.includes("lock")) &&
|
|
368
|
+
lowerFiles.length <= 2) {
|
|
369
|
+
return "chore";
|
|
370
|
+
}
|
|
371
|
+
if (lowerDiff.includes("fix") ||
|
|
372
|
+
lowerDiff.includes("bug") ||
|
|
373
|
+
lowerDiff.includes("error")) {
|
|
374
|
+
return "fix";
|
|
375
|
+
}
|
|
376
|
+
return "feat";
|
|
377
|
+
}
|
|
378
|
+
function isGoodTopicToken(token, stopWords) {
|
|
379
|
+
if (token.length < 3) {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
if (stopWords.has(token)) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
if (/^[a-f0-9]{6,}$/.test(token)) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
if (/^\d+$/.test(token)) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
function sanitizeBranchPart(value) {
|
|
394
|
+
return value
|
|
395
|
+
.toLowerCase()
|
|
396
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
397
|
+
.replace(/-+/g, "-")
|
|
398
|
+
.replace(/^-|-$/g, "");
|
|
399
|
+
}
|
|
400
|
+
function inferTopic(files, diff) {
|
|
401
|
+
const stopWords = new Set([
|
|
402
|
+
"src",
|
|
403
|
+
"dist",
|
|
404
|
+
"index",
|
|
405
|
+
"readme",
|
|
406
|
+
"package",
|
|
407
|
+
"lock",
|
|
408
|
+
"json",
|
|
409
|
+
"ts",
|
|
410
|
+
"js",
|
|
411
|
+
"md",
|
|
412
|
+
"diff",
|
|
413
|
+
"added",
|
|
414
|
+
"removed",
|
|
415
|
+
"file",
|
|
416
|
+
"files",
|
|
417
|
+
"change",
|
|
418
|
+
"changes",
|
|
419
|
+
"update",
|
|
420
|
+
"updated",
|
|
421
|
+
"line",
|
|
422
|
+
"lines",
|
|
423
|
+
]);
|
|
424
|
+
const fileTokens = files
|
|
425
|
+
.flatMap((f) => f.split(/[\/._-]+/))
|
|
426
|
+
.map((v) => v.toLowerCase())
|
|
427
|
+
.filter((v) => isGoodTopicToken(v, stopWords));
|
|
428
|
+
const diffTokens = diff
|
|
429
|
+
.toLowerCase()
|
|
430
|
+
.split(/[^a-z0-9]+/)
|
|
431
|
+
.filter((v) => isGoodTopicToken(v, stopWords))
|
|
432
|
+
.slice(0, 20);
|
|
433
|
+
const tokens = [...fileTokens, ...diffTokens]
|
|
434
|
+
.map((v) => sanitizeBranchPart(v))
|
|
435
|
+
.filter(Boolean);
|
|
436
|
+
if (tokens.length === 0) {
|
|
437
|
+
return "";
|
|
438
|
+
}
|
|
439
|
+
const unique = Array.from(new Set(tokens));
|
|
440
|
+
return unique.slice(0, 3).join("-");
|
|
441
|
+
}
|
|
442
|
+
function randomSuffix() {
|
|
443
|
+
return Math.random().toString(36).slice(2, 8);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* AI でブランチ名を提案
|
|
447
|
+
*/
|
|
448
|
+
async function suggestBranchNameWithAI(files, diff) {
|
|
449
|
+
if (!process.env.GROQ_API_KEY) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
const compactFiles = files.slice(0, 30).join("\n");
|
|
453
|
+
const compactDiff = (0, text_js_1.truncateByChars)(diff, 2800);
|
|
454
|
+
const prompt = `You are an expert at naming git branches.
|
|
455
|
+
Generate exactly one branch name from the following changes.
|
|
456
|
+
|
|
457
|
+
Rules:
|
|
458
|
+
- Output only branch name, nothing else
|
|
459
|
+
- Format: <type>/<slug>
|
|
460
|
+
- type must be one of: feat, fix, docs, chore, refactor, test, style
|
|
461
|
+
- slug must use kebab-case
|
|
462
|
+
- Prefer meaningful feature intent over noisy token names
|
|
463
|
+
- If changes look like new feature or command addition, prefer feat
|
|
464
|
+
|
|
465
|
+
Changed files:
|
|
466
|
+
${compactFiles}
|
|
467
|
+
|
|
468
|
+
Diff:
|
|
469
|
+
${compactDiff}`;
|
|
470
|
+
try {
|
|
471
|
+
const raw = await generateText(prompt);
|
|
472
|
+
return normalizeBranchCandidate(raw);
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function normalizeBranchCandidate(raw) {
|
|
479
|
+
const firstLine = raw.split("\n")[0]?.trim().toLowerCase() || "";
|
|
480
|
+
const cleaned = firstLine.replace(/[`"' ]/g, "");
|
|
481
|
+
const normalized = cleaned.replace(/^feature\//, "feat/");
|
|
482
|
+
const [head, ...tail] = normalized.split("/");
|
|
483
|
+
if (!head || tail.length === 0) {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
const validTypes = new Set([
|
|
487
|
+
"feat",
|
|
488
|
+
"fix",
|
|
489
|
+
"docs",
|
|
490
|
+
"chore",
|
|
491
|
+
"refactor",
|
|
492
|
+
"test",
|
|
493
|
+
"style",
|
|
494
|
+
]);
|
|
495
|
+
if (!validTypes.has(head)) {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
const slug = sanitizeBranchPart(tail.join("-"));
|
|
499
|
+
if (!slug || slug.length < 3) {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
return `${head}/${slug}`;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* ブランチ名を提案(AI またはヒューリスティック)
|
|
506
|
+
*/
|
|
507
|
+
async function suggestBranchName() {
|
|
508
|
+
const files = (0, git_js_1.getChangedFiles)();
|
|
509
|
+
const diff = (0, git_js_1.getCombinedDiff)();
|
|
510
|
+
const fromAI = await suggestBranchNameWithAI(files, diff);
|
|
511
|
+
if (fromAI) {
|
|
512
|
+
return fromAI;
|
|
513
|
+
}
|
|
514
|
+
const type = inferBranchType(files, diff);
|
|
515
|
+
const topic = inferTopic(files, diff);
|
|
516
|
+
if (!topic) {
|
|
517
|
+
return `${type}/update-${randomSuffix()}`;
|
|
518
|
+
}
|
|
519
|
+
return `${type}/${topic}`;
|
|
520
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ensureAvailableBranchName = ensureAvailableBranchName;
|
|
4
|
+
const git_js_1 = require("../utils/git.js");
|
|
5
|
+
/**
|
|
6
|
+
* ブランチパーツをサニタイズ
|
|
7
|
+
*/
|
|
8
|
+
function sanitizeBranchPart(value) {
|
|
9
|
+
return value
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
12
|
+
.replace(/-+/g, "-")
|
|
13
|
+
.replace(/^-|-$/g, "");
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* ランダムサフィックスを生成
|
|
17
|
+
*/
|
|
18
|
+
function randomSuffix() {
|
|
19
|
+
return Math.random().toString(36).slice(2, 8);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 利用可能なブランチ名を確保
|
|
23
|
+
*/
|
|
24
|
+
function ensureAvailableBranchName(baseName) {
|
|
25
|
+
const [headRaw, ...tailRaw] = baseName.split("/");
|
|
26
|
+
const head = sanitizeBranchPart(headRaw || "feat");
|
|
27
|
+
const tail = sanitizeBranchPart(tailRaw.join("-") || `update-${randomSuffix()}`);
|
|
28
|
+
const normalized = `${head || "feat"}/${tail}`;
|
|
29
|
+
if (!(0, git_js_1.branchExists)(normalized)) {
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
let i = 2;
|
|
33
|
+
while ((0, git_js_1.branchExists)(`${normalized}-${i}`)) {
|
|
34
|
+
i += 1;
|
|
35
|
+
}
|
|
36
|
+
return `${normalized}-${i}`;
|
|
37
|
+
}
|