ai-git-tool 1.1.0 → 1.3.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/dist/index.js CHANGED
@@ -1,70 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
- if (k2 === undefined) k2 = k;
5
- var desc = Object.getOwnPropertyDescriptor(m, k);
6
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
- desc = { enumerable: true, get: function() { return m[k]; } };
8
- }
9
- Object.defineProperty(o, k2, desc);
10
- }) : (function(o, m, k, k2) {
11
- if (k2 === undefined) k2 = k;
12
- o[k2] = m[k];
13
- }));
14
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
- Object.defineProperty(o, "default", { enumerable: true, value: v });
16
- }) : function(o, v) {
17
- o["default"] = v;
18
- });
19
- var __importStar = (this && this.__importStar) || (function () {
20
- var ownKeys = function(o) {
21
- ownKeys = Object.getOwnPropertyNames || function (o) {
22
- var ar = [];
23
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
- return ar;
25
- };
26
- return ownKeys(o);
27
- };
28
- return function (mod) {
29
- if (mod && mod.__esModule) return mod;
30
- var result = {};
31
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
- __setModuleDefault(result, mod);
33
- return result;
34
- };
35
- })();
36
- var __importDefault = (this && this.__importDefault) || function (mod) {
37
- return (mod && mod.__esModule) ? mod : { "default": mod };
38
- };
39
3
  Object.defineProperty(exports, "__esModule", { value: true });
40
- const child_process_1 = require("child_process");
41
- const readline = __importStar(require("readline"));
42
- const fs = __importStar(require("fs"));
43
- const os = __importStar(require("os"));
44
- const path = __importStar(require("path"));
45
- const openai_1 = __importDefault(require("openai"));
4
+ const text_js_1 = require("./utils/text.js");
5
+ const config_js_1 = require("./utils/config.js");
6
+ const commit_js_1 = require("./commands/commit.js");
7
+ const push_js_1 = require("./commands/push.js");
8
+ const pr_js_1 = require("./commands/pr.js");
9
+ const checkout_js_1 = require("./commands/checkout.js");
46
10
  // ── フラグ解析 ──────────────────────────────────────────
47
11
  const args = process.argv.slice(2);
48
12
  // サブコマンドの抽出
49
13
  const subcommand = args[0];
50
14
  const subcommandArgs = args.slice(1);
51
15
  const showHelp = args.includes("--help") || args.includes("-h") || subcommand === "help";
52
- const setLangArg = getOptionValue(args, "--set-lang");
53
- const langArg = getOptionValue(subcommandArgs, "--lang");
16
+ const setLangArg = (0, text_js_1.getOptionValue)(args, "--set-lang");
17
+ const langArg = (0, text_js_1.getOptionValue)(subcommandArgs, "--lang");
54
18
  const noAdd = subcommandArgs.includes("--no-add");
55
- const CONFIG_DIR = path.join(os.homedir(), ".ai-commit");
56
- const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
57
- const defaultGroqModel = "llama-3.1-8b-instant";
58
- const COMMIT_DIFF_COMPACT_CHARS = 3500;
59
- const PR_COMMITS_COMPACT_CHARS = 1800;
60
- const PR_DIFF_COMPACT_CHARS = 3500;
61
19
  if (setLangArg) {
62
- const nextLang = parseLanguage(setLangArg);
20
+ const nextLang = (0, config_js_1.parseLanguage)(setLangArg);
63
21
  if (!nextLang) {
64
22
  console.error("Error: --set-lang は ja または en を指定してください。");
65
23
  process.exit(1);
66
24
  }
67
- saveConfig({ language: nextLang });
25
+ (0, config_js_1.saveConfig)({ language: nextLang });
68
26
  console.log(`✅ デフォルト言語を '${nextLang}' に保存しました。`);
69
27
  process.exit(0);
70
28
  }
@@ -74,6 +32,7 @@ Usage: ai-git <command> [options]
74
32
 
75
33
  Commands:
76
34
  commit Generate commit message from staged changes
35
+ push Commit with AI message and push to remote (git add . + commit + push)
77
36
  pr Generate PR description and create pull request
78
37
  checkout Create branch from current changes
79
38
 
@@ -90,904 +49,49 @@ Global Options:
90
49
 
91
50
  Environment:
92
51
  GROQ_API_KEY Your Groq API key (required)
93
- GROQ_MODEL Optional model name (default: llama-3.1-8b-instant)
52
+ GROQ_MODEL Optional model name (default: Llama 3.3 70B Versatile)
94
53
  `);
95
54
  process.exit(0);
96
55
  }
97
56
  if (!subcommand ||
98
- (subcommand !== "commit" && subcommand !== "pr" && subcommand !== "checkout")) {
99
- console.error("Error: Please specify a command: 'commit', 'pr' or 'checkout'");
100
- console.error("Run 'ai-git --help' for usage information");
57
+ (subcommand !== "commit" &&
58
+ subcommand !== "push" &&
59
+ subcommand !== "pr" &&
60
+ subcommand !== "checkout")) {
61
+ console.error("❌ コマンドが指定されていないか、無効なコマンドです");
62
+ console.error("");
63
+ console.error("📚 利用可能なコマンド:");
64
+ console.error(" ai-git commit - AI でコミットメッセージを生成してコミット");
65
+ console.error(" ai-git push - コミット後、リモートにプッシュ");
66
+ console.error(" ai-git pr - PR の説明を生成して Pull Request を作成");
67
+ console.error(" ai-git checkout - 変更内容から新しいブランチを作成");
68
+ console.error("");
69
+ console.error("💡 詳しい使い方:");
70
+ console.error(" ai-git --help");
71
+ console.error("");
72
+ console.error("🎯 最初に試すなら:");
73
+ console.error(" ai-git commit (変更をコミット)");
74
+ console.error(" ai-git push (コミット&プッシュを一度に実行)");
101
75
  process.exit(1);
102
76
  }
103
- const language = resolveLanguage(langArg);
104
- // ── git diff 取得 ────────────────────────────────────────
105
- function getStagedDiff() {
106
- try {
107
- return (0, child_process_1.execSync)("git diff --cached", { encoding: "utf-8" });
108
- }
109
- catch {
110
- console.error("Error: Failed to run git diff --cached");
111
- process.exit(1);
112
- }
113
- }
114
- // ── Groq APIでコミットメッセージ生成 ───────────────────
115
- async function generateCommitMessage(diff) {
116
- const inputDiff = truncateByChars(diff, COMMIT_DIFF_COMPACT_CHARS);
117
- const prompt = buildPrompt(inputDiff, language);
118
- try {
119
- const raw = await generateText(prompt);
120
- return normalizeGeneratedCommitMessage(raw, diff, language);
121
- }
122
- catch (error) {
123
- if (isRequestTooLargeError(error)) {
124
- const smallerDiff = truncateByChars(diff, 1800);
125
- const retryRaw = await generateText(buildPrompt(smallerDiff, language));
126
- return normalizeGeneratedCommitMessage(retryRaw, diff, language);
127
- }
128
- handleGroqError(error);
129
- process.exit(1);
130
- }
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
- }
182
- function buildPrompt(diff, lang) {
183
- if (lang === "ja") {
184
- return `あなたは git のコミットメッセージ作成の専門家です。
185
- 次の git diff から、Conventional Commits 形式の詳細コミットメッセージを生成してください。
186
-
187
- ルール:
188
- - 1行目: <type>(<optional scope>): <short description>(72文字以内)
189
- - 2行目は空行
190
- - 本文は "- " で始まる箇条書き
191
- - 箇条書きは 3-5 行
192
- - WHAT と WHY を書き、HOW は書かない
193
- - 命令形を使う
194
- - 出力はコミットメッセージ本文のみ(説明文は不要)
195
- - 説明文は日本語にする
196
-
197
- 出力例:
198
- feat(auth): Google ログインを追加する
199
-
200
- - auth.ts に GoogleAuthProvider の設定を追加する
201
- - トークン期限切れ時の更新処理を追加する
202
- - ネットワーク障害時のエラーハンドリングを追加する
203
-
204
- Git diff:
205
- ${diff}`;
206
- }
207
- return `You are an expert at writing git commit messages.
208
- Generate a detailed commit message in Conventional Commits format for the following git diff.
209
-
210
- Rules:
211
- - First line: <type>(<optional scope>): <short description> (max 72 chars)
212
- - Blank line after the first line
213
- - Body: bullet points starting with "- " explaining WHAT and WHY
214
- - 3-5 bullet points max
215
- - Use imperative mood ("add" not "added")
216
- - Output ONLY the commit message, nothing else
217
-
218
- Example:
219
- feat(auth): add Google login with Firebase Auth
220
-
221
- - Add GoogleAuthProvider setup in auth.ts
222
- - Handle token refresh on expiry
223
- - Add error handling for network failures
224
-
225
- Git diff:
226
- ${diff}`;
227
- }
228
- // ── ユーザー確認 ─────────────────────────────────────────
229
- function askUser(question) {
230
- const rl = readline.createInterface({
231
- input: process.stdin,
232
- output: process.stdout,
233
- });
234
- return new Promise((resolve) => {
235
- rl.question(question, (answer) => {
236
- rl.close();
237
- resolve(answer.trim().toLowerCase());
238
- });
239
- });
240
- }
241
- // ── エディタで編集 ───────────────────────────────────────
242
- function editInEditor(message) {
243
- const tmpFile = path.join(os.tmpdir(), `commit-msg-${Date.now()}.txt`);
244
- fs.writeFileSync(tmpFile, message, "utf-8");
245
- const editor = process.env.EDITOR || "vi";
246
- const result = (0, child_process_1.spawnSync)(editor, [tmpFile], { stdio: "inherit" });
247
- if (result.error) {
248
- console.error(`Error: Failed to open editor: ${result.error.message}`);
249
- process.exit(1);
250
- }
251
- const edited = fs.readFileSync(tmpFile, "utf-8").trim();
252
- fs.unlinkSync(tmpFile);
253
- return edited;
254
- }
255
- // ── git commit 実行 ──────────────────────────────────────
256
- function doCommit(message) {
257
- const result = (0, child_process_1.spawnSync)("git", ["commit", "-m", message], {
258
- stdio: "inherit",
259
- });
260
- if (result.status !== 0) {
261
- console.error("Error: git commit failed");
262
- process.exit(1);
263
- }
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
- }
272
- async function mainCheckout() {
273
- const suggested = await suggestBranchName();
274
- const branchName = ensureAvailableBranchName(suggested);
275
- const result = (0, child_process_1.spawnSync)("git", ["checkout", "-b", branchName], {
276
- stdio: "inherit",
277
- });
278
- if (result.status !== 0) {
279
- console.error("❌ ブランチ作成に失敗しました。");
280
- process.exit(1);
281
- }
282
- console.log(`✅ ブランチを作成しました: ${branchName}`);
283
- }
284
- async function suggestBranchName() {
285
- const files = getChangedFiles();
286
- const diff = getCombinedDiff();
287
- const fromAI = await suggestBranchNameWithAI(files, diff);
288
- if (fromAI) {
289
- return fromAI;
290
- }
291
- const type = inferBranchType(files, diff);
292
- const topic = inferTopic(files, diff);
293
- if (!topic) {
294
- return `${type}/update-${randomSuffix()}`;
295
- }
296
- return `${type}/${topic}`;
297
- }
298
- async function suggestBranchNameWithAI(files, diff) {
299
- if (!process.env.GROQ_API_KEY) {
300
- return null;
301
- }
302
- const compactFiles = files.slice(0, 30).join("\n");
303
- const compactDiff = truncateByChars(diff, 2800);
304
- const prompt = `You are an expert at naming git branches.
305
- Generate exactly one branch name from the following changes.
306
-
307
- Rules:
308
- - Output only branch name, nothing else
309
- - Format: <type>/<slug>
310
- - type must be one of: feat, fix, docs, chore, refactor, test, style
311
- - slug must use kebab-case
312
- - Prefer meaningful feature intent over noisy token names
313
- - If changes look like new feature or command addition, prefer feat
314
-
315
- Changed files:
316
- ${compactFiles}
317
-
318
- Diff:
319
- ${compactDiff}`;
320
- try {
321
- const raw = await generateText(prompt);
322
- return normalizeBranchCandidate(raw);
323
- }
324
- catch {
325
- return null;
326
- }
327
- }
328
- function normalizeBranchCandidate(raw) {
329
- const firstLine = raw.split("\n")[0]?.trim().toLowerCase() || "";
330
- const cleaned = firstLine.replace(/[`"' ]/g, "");
331
- const normalized = cleaned.replace(/^feature\//, "feat/");
332
- const [head, ...tail] = normalized.split("/");
333
- if (!head || tail.length === 0) {
334
- return null;
335
- }
336
- const validTypes = new Set([
337
- "feat",
338
- "fix",
339
- "docs",
340
- "chore",
341
- "refactor",
342
- "test",
343
- "style",
344
- ]);
345
- if (!validTypes.has(head)) {
346
- return null;
347
- }
348
- const slug = sanitizeBranchPart(tail.join("-"));
349
- if (!slug || slug.length < 3) {
350
- return null;
351
- }
352
- return `${head}/${slug}`;
353
- }
354
- function getChangedFiles() {
355
- const staged = getCommandOutput("git diff --cached --name-only");
356
- const unstaged = getCommandOutput("git diff --name-only");
357
- const merged = `${staged}\n${unstaged}`
358
- .split("\n")
359
- .map((v) => v.trim())
360
- .filter(Boolean);
361
- return Array.from(new Set(merged));
362
- }
363
- function getCombinedDiff() {
364
- const staged = getCommandOutput("git diff --cached");
365
- const unstaged = getCommandOutput("git diff");
366
- return `${staged}\n${unstaged}`;
367
- }
368
- function getCommandOutput(command) {
369
- try {
370
- return (0, child_process_1.execSync)(command, { encoding: "utf-8", stdio: "pipe" });
371
- }
372
- catch {
373
- return "";
374
- }
375
- }
376
- function inferBranchType(files, diff) {
377
- const lowerFiles = files.map((f) => f.toLowerCase());
378
- const lowerDiff = diff.toLowerCase();
379
- if (lowerFiles.length > 0 && lowerFiles.every((f) => f.endsWith(".md"))) {
380
- return "docs";
381
- }
382
- if (lowerFiles.some((f) => f.includes("readme") || f.endsWith(".md")) &&
383
- lowerFiles.length <= 2) {
384
- return "docs";
385
- }
386
- if (lowerFiles.some((f) => f.includes("package.json") || f.includes("lock")) &&
387
- lowerFiles.length <= 2) {
388
- return "chore";
389
- }
390
- if (lowerDiff.includes("fix") ||
391
- lowerDiff.includes("bug") ||
392
- lowerDiff.includes("error")) {
393
- return "fix";
394
- }
395
- return "feat";
396
- }
397
- function inferTopic(files, diff) {
398
- const stopWords = new Set([
399
- "src",
400
- "dist",
401
- "index",
402
- "readme",
403
- "package",
404
- "lock",
405
- "json",
406
- "ts",
407
- "js",
408
- "md",
409
- "diff",
410
- "added",
411
- "removed",
412
- "file",
413
- "files",
414
- "change",
415
- "changes",
416
- "update",
417
- "updated",
418
- "line",
419
- "lines",
420
- ]);
421
- // ファイル名ベースを優先し、diff本文は補助的に使う
422
- const fileTokens = files
423
- .flatMap((f) => f.split(/[\/._-]+/))
424
- .map((v) => v.toLowerCase())
425
- .filter((v) => isGoodTopicToken(v, stopWords));
426
- const diffTokens = diff
427
- .toLowerCase()
428
- .split(/[^a-z0-9]+/)
429
- .filter((v) => isGoodTopicToken(v, stopWords))
430
- .slice(0, 20);
431
- const tokens = [...fileTokens, ...diffTokens]
432
- .map((v) => sanitizeBranchPart(v))
433
- .filter(Boolean);
434
- if (tokens.length === 0) {
435
- return "";
436
- }
437
- const unique = Array.from(new Set(tokens));
438
- return unique.slice(0, 3).join("-");
439
- }
440
- function isGoodTopicToken(token, stopWords) {
441
- if (token.length < 3) {
442
- return false;
443
- }
444
- if (stopWords.has(token)) {
445
- return false;
446
- }
447
- // 16進ハッシュ断片(例: 22e133f, cd9353f)を除外
448
- if (/^[a-f0-9]{6,}$/.test(token)) {
449
- return false;
450
- }
451
- // 数字主体トークンを除外
452
- if (/^\d+$/.test(token)) {
453
- return false;
454
- }
455
- return true;
456
- }
457
- function sanitizeBranchPart(value) {
458
- return value
459
- .toLowerCase()
460
- .replace(/[^a-z0-9-]/g, "-")
461
- .replace(/-+/g, "-")
462
- .replace(/^-|-$/g, "");
463
- }
464
- function randomSuffix() {
465
- return Math.random().toString(36).slice(2, 8);
466
- }
467
- function ensureAvailableBranchName(baseName) {
468
- const [headRaw, ...tailRaw] = baseName.split("/");
469
- const head = sanitizeBranchPart(headRaw || "feat");
470
- const tail = sanitizeBranchPart(tailRaw.join("-") || `update-${randomSuffix()}`);
471
- const normalized = `${head || "feat"}/${tail}`;
472
- if (!branchExists(normalized)) {
473
- return normalized;
474
- }
475
- let i = 2;
476
- while (branchExists(`${normalized}-${i}`)) {
477
- i += 1;
478
- }
479
- return `${normalized}-${i}`;
480
- }
481
- function branchExists(name) {
482
- try {
483
- (0, child_process_1.execSync)(`git show-ref --verify --quiet refs/heads/${name}`, {
484
- stdio: "pipe",
485
- });
486
- return true;
487
- }
488
- catch {
489
- return false;
490
- }
491
- }
77
+ const language = (0, config_js_1.resolveLanguage)(langArg);
492
78
  // ── メイン ───────────────────────────────────────────────
493
79
  async function main() {
494
80
  if (subcommand === "checkout") {
495
- await mainCheckout();
81
+ await (0, checkout_js_1.runCheckoutCommand)();
496
82
  return;
497
83
  }
498
84
  if (subcommand === "pr") {
499
- await mainPR();
85
+ await (0, pr_js_1.runPRCommand)(language);
500
86
  return;
501
87
  }
502
- if (!noAdd) {
503
- console.log("📦 変更をステージしています... (git add .)");
504
- stageAllChanges();
505
- }
506
- const diff = getStagedDiff();
507
- if (!diff.trim()) {
508
- console.log("No staged changes found. Run `git add` first.");
509
- process.exit(1);
510
- }
511
- console.log("🤖 コミットメッセージを生成中... (compact summary input)");
512
- const message = await generateCommitMessage(diff);
513
- console.log(`\n📝 Generated commit message:\n`);
514
- // 詳細モードは複数行なのでインデントして表示
515
- message.split("\n").forEach((line) => {
516
- console.log(` ${line}`);
517
- });
518
- console.log();
519
- const answer = await askUser("Use this message? [y/n/e(edit)]: ");
520
- let finalMessage = message;
521
- if (answer === "e" || answer === "edit") {
522
- finalMessage = editInEditor(message);
523
- if (!finalMessage) {
524
- console.log("Aborted: empty message.");
525
- process.exit(0);
526
- }
527
- console.log(`\n✏️ Edited message:\n`);
528
- finalMessage.split("\n").forEach((line) => {
529
- console.log(` ${line}`);
530
- });
531
- console.log();
532
- }
533
- else if (answer !== "y" && answer !== "yes") {
534
- console.log("Aborted.");
535
- process.exit(0);
88
+ if (subcommand === "push") {
89
+ await (0, push_js_1.runPushCommand)(language, noAdd);
90
+ return;
536
91
  }
537
- doCommit(finalMessage);
538
- console.log(`\n✅ Committed successfully!`);
92
+ await (0, commit_js_1.runCommitCommand)(language, noAdd);
539
93
  }
540
94
  main().catch((err) => {
541
95
  console.error("❌ 予期しないエラー:", err.message);
542
96
  process.exit(1);
543
97
  });
544
- function getOptionValue(argv, name) {
545
- const idx = argv.indexOf(name);
546
- if (idx === -1) {
547
- return undefined;
548
- }
549
- return argv[idx + 1];
550
- }
551
- function parseLanguage(value) {
552
- if (!value) {
553
- return null;
554
- }
555
- if (value === "ja" || value === "en") {
556
- return value;
557
- }
558
- return null;
559
- }
560
- function resolveLanguage(langValue) {
561
- const fromFlag = parseLanguage(langValue);
562
- if (langValue && !fromFlag) {
563
- console.error("Error: --lang は ja または en を指定してください。");
564
- process.exit(1);
565
- }
566
- if (fromFlag) {
567
- return fromFlag;
568
- }
569
- const config = loadConfig();
570
- return config?.language ?? "ja";
571
- }
572
- function loadConfig() {
573
- try {
574
- if (!fs.existsSync(CONFIG_PATH)) {
575
- return null;
576
- }
577
- const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
578
- const parsed = JSON.parse(raw);
579
- const lang = parseLanguage(parsed.language);
580
- if (!lang) {
581
- return null;
582
- }
583
- return { language: lang };
584
- }
585
- catch {
586
- return null;
587
- }
588
- }
589
- function saveConfig(config) {
590
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
591
- fs.writeFileSync(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
592
- }
593
- function getGroqClient() {
594
- const apiKey = process.env.GROQ_API_KEY;
595
- if (!apiKey) {
596
- console.error("❌ GROQ_API_KEY が未設定です。");
597
- console.error(' 取得先: https://console.groq.com/keys\n 例: export GROQ_API_KEY="your_api_key"');
598
- process.exit(1);
599
- }
600
- return new openai_1.default({
601
- apiKey,
602
- baseURL: "https://api.groq.com/openai/v1",
603
- });
604
- }
605
- async function generateText(prompt) {
606
- const client = getGroqClient();
607
- const model = process.env.GROQ_MODEL || defaultGroqModel;
608
- const res = await client.chat.completions.create({
609
- model,
610
- temperature: 0.2,
611
- messages: [{ role: "user", content: prompt }],
612
- });
613
- return res.choices[0]?.message?.content?.trim() || "";
614
- }
615
- function handleGroqError(error) {
616
- const message = error instanceof Error ? error.message : String(error);
617
- const lower = message.toLowerCase();
618
- if (lower.includes("429") ||
619
- lower.includes("quota") ||
620
- lower.includes("rate")) {
621
- console.error("❌ Groq API の利用上限に達しました(429: quota/rate limit)。");
622
- console.error(" - 少し待って再実行してください");
623
- console.error(" - https://console.groq.com で利用枠を確認してください");
624
- console.error(" - 必要なら別プロジェクトの API キーを利用してください");
625
- return;
626
- }
627
- if (lower.includes("401") ||
628
- lower.includes("403") ||
629
- lower.includes("api key")) {
630
- console.error("❌ Groq API 認証エラーです。GROQ_API_KEY を確認してください。");
631
- console.error(" 取得先: https://console.groq.com/keys");
632
- return;
633
- }
634
- console.error("❌ Groq API 呼び出しに失敗しました。");
635
- console.error(` 詳細: ${message}`);
636
- }
637
- // ── PR生成用の関数群 ─────────────────────────────────────
638
- function checkGHCLI() {
639
- try {
640
- (0, child_process_1.execSync)("gh --version", { encoding: "utf-8", stdio: "pipe" });
641
- }
642
- catch {
643
- if (language === "ja") {
644
- console.error("❌ GitHub CLI (gh) がインストールされていません。");
645
- console.error(" インストール: https://cli.github.com/");
646
- }
647
- else {
648
- console.error("❌ GitHub CLI (gh) is not installed.");
649
- console.error(" Install from: https://cli.github.com/");
650
- }
651
- process.exit(1);
652
- }
653
- }
654
- function checkGHAuth() {
655
- if (process.env.GH_TOKEN) {
656
- return;
657
- }
658
- try {
659
- (0, child_process_1.execSync)("gh auth status", { encoding: "utf-8", stdio: "pipe" });
660
- }
661
- catch {
662
- if (language === "ja") {
663
- console.error("❌ GitHub CLI の認証が必要です。");
664
- console.error(" 次を実行してください: gh auth login");
665
- console.error(" もしくは GH_TOKEN を環境変数に設定してください。");
666
- }
667
- else {
668
- console.error("❌ GitHub CLI authentication is required.");
669
- console.error(" Run: gh auth login");
670
- console.error(" Or set GH_TOKEN environment variable.");
671
- }
672
- process.exit(1);
673
- }
674
- }
675
- function getPullRequestURL(branch) {
676
- try {
677
- const remoteUrl = (0, child_process_1.execSync)("git remote get-url origin", {
678
- encoding: "utf-8",
679
- stdio: "pipe",
680
- }).trim();
681
- const repoPath = parseGitHubRepoPath(remoteUrl);
682
- if (!repoPath) {
683
- return null;
684
- }
685
- return `https://github.com/${repoPath}/pull/new/${branch}`;
686
- }
687
- catch {
688
- return null;
689
- }
690
- }
691
- function parseGitHubRepoPath(remoteUrl) {
692
- const normalized = remoteUrl.replace(/\.git$/, "");
693
- const sshMatch = normalized.match(/^git@github\.com:(.+\/.+)$/);
694
- if (sshMatch) {
695
- return sshMatch[1];
696
- }
697
- const httpsMatch = normalized.match(/^https:\/\/github\.com\/(.+\/.+)$/);
698
- if (httpsMatch) {
699
- return httpsMatch[1];
700
- }
701
- return null;
702
- }
703
- function detectBaseBranch() {
704
- // Try 1: Get default branch from remote
705
- try {
706
- const result = (0, child_process_1.execSync)("git symbolic-ref refs/remotes/origin/HEAD", {
707
- encoding: "utf-8",
708
- stdio: "pipe",
709
- }).trim();
710
- // Parse "refs/remotes/origin/main" -> "main"
711
- return result.replace("refs/remotes/origin/", "");
712
- }
713
- catch {
714
- // Fallback: Try common branch names
715
- }
716
- // Try 2: Check for common base branches
717
- for (const branch of ["main", "master", "develop"]) {
718
- try {
719
- (0, child_process_1.execSync)(`git rev-parse --verify ${branch}`, { stdio: "pipe" });
720
- return branch;
721
- }
722
- catch {
723
- continue;
724
- }
725
- }
726
- // Error: No base branch found
727
- if (language === "ja") {
728
- console.error("❌ ベースブランチを検出できませんでした。");
729
- console.error(" main, master, develop のいずれも存在しません。");
730
- }
731
- else {
732
- console.error("❌ Could not detect base branch.");
733
- console.error(" None of main, master, develop exist.");
734
- }
735
- process.exit(1);
736
- }
737
- function getBranchDiff(baseBranch) {
738
- try {
739
- const commits = (0, child_process_1.execSync)(`git log ${baseBranch}..HEAD --format="%h %s%n%b%n---"`, { encoding: "utf-8" });
740
- const diff = (0, child_process_1.execSync)(`git diff ${baseBranch}...HEAD`, {
741
- encoding: "utf-8",
742
- });
743
- return { commits, diff };
744
- }
745
- catch {
746
- if (language === "ja") {
747
- console.error("❌ ブランチの差分取得に失敗しました。");
748
- }
749
- else {
750
- console.error("❌ Failed to get branch diff.");
751
- }
752
- process.exit(1);
753
- }
754
- }
755
- function buildPRPrompt(commits, diff, lang) {
756
- if (lang === "ja") {
757
- return `あなたは GitHub の Pull Request 作成の専門家です。
758
- 次のコミット履歴と差分から、PR の説明文を生成してください。
759
-
760
- ルール:
761
- - GitHub の標準フォーマットを使用する
762
- - ## Summary: 2-3文で変更の概要を説明
763
- - ## Changes: "- " で始まる箇条書き(3-7個)で具体的な変更内容
764
- - ## Test plan: テスト方法の箇条書き(2-4個)
765
- - 命令形を使う
766
- - WHATとWHYを重視し、HOWは最小限に
767
- - 出力はPR説明文のみ(余計な説明は不要)
768
-
769
- コミット履歴:
770
- ${commits}
771
-
772
- 変更差分:
773
- ${diff}`;
774
- }
775
- return `You are an expert at writing GitHub Pull Request descriptions.
776
- Generate a PR description from the following commit history and diff.
777
-
778
- Rules:
779
- - Use standard GitHub format
780
- - ## Summary: 2-3 sentences explaining the overall change
781
- - ## Changes: bullet points (3-7 items) with "- " prefix detailing specific changes
782
- - ## Test plan: bullet points (2-4 items) describing how to test
783
- - Use imperative mood
784
- - Focus on WHAT and WHY, minimize HOW
785
- - Output ONLY the PR description, no extra explanation
786
-
787
- Commit history:
788
- ${commits}
789
-
790
- Diff:
791
- ${diff}`;
792
- }
793
- async function generatePRDescription(baseBranch) {
794
- const { commits, diff } = getBranchDiff(baseBranch);
795
- const inputCommits = truncateByChars(commits, PR_COMMITS_COMPACT_CHARS);
796
- const inputDiff = truncateByChars(diff, PR_DIFF_COMPACT_CHARS);
797
- const prompt = buildPRPrompt(inputCommits, inputDiff, language);
798
- try {
799
- return await generateText(prompt);
800
- }
801
- catch (error) {
802
- if (isRequestTooLargeError(error)) {
803
- const compactCommits = truncateByChars(commits, 1200);
804
- const compactDiff = truncateByChars(diff, 2200);
805
- const compactPrompt = buildPRPrompt(compactCommits, compactDiff, language);
806
- if (language === "ja") {
807
- console.log("ℹ️ 入力サイズが大きいため、差分を要約モードにして再試行します...");
808
- }
809
- else {
810
- console.log("ℹ️ Input is too large. Retrying with compact diff summary...");
811
- }
812
- try {
813
- return await generateText(compactPrompt);
814
- }
815
- catch (retryError) {
816
- handleGroqError(retryError);
817
- process.exit(1);
818
- }
819
- }
820
- handleGroqError(error);
821
- process.exit(1);
822
- }
823
- }
824
- function truncateByChars(input, maxChars) {
825
- if (input.length <= maxChars) {
826
- return input;
827
- }
828
- return `${input.slice(0, maxChars)}\n\n... (truncated)`;
829
- }
830
- function isRequestTooLargeError(error) {
831
- const message = error instanceof Error ? error.message : String(error);
832
- const lower = message.toLowerCase();
833
- return (lower.includes("413") ||
834
- lower.includes("request too large") ||
835
- lower.includes("tokens per minute") ||
836
- lower.includes("tpm"));
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
- }
875
- function createPR(description, baseBranch, fallbackURL) {
876
- const titleLine = makePRTitle(description);
877
- const result = (0, child_process_1.spawnSync)("gh", [
878
- "pr",
879
- "create",
880
- "--base",
881
- baseBranch,
882
- "--title",
883
- titleLine.trim(),
884
- "--body",
885
- description,
886
- ], { encoding: "utf-8", stdio: "pipe" });
887
- if (result.stdout) {
888
- process.stdout.write(result.stdout);
889
- }
890
- if (result.stderr) {
891
- process.stderr.write(result.stderr);
892
- }
893
- if (result.status !== 0) {
894
- if (language === "ja") {
895
- console.error("❌ PR の作成に失敗しました。");
896
- if (fallbackURL) {
897
- console.error(` 手動で作成する場合: ${fallbackURL}`);
898
- }
899
- }
900
- else {
901
- console.error("❌ Failed to create PR.");
902
- if (fallbackURL) {
903
- console.error(` Create manually: ${fallbackURL}`);
904
- }
905
- }
906
- process.exit(1);
907
- }
908
- }
909
- async function mainPR() {
910
- checkGHCLI();
911
- checkGHAuth();
912
- const baseBranch = detectBaseBranch();
913
- const currentBranch = (0, child_process_1.execSync)("git branch --show-current", {
914
- encoding: "utf-8",
915
- }).trim();
916
- if (currentBranch === baseBranch) {
917
- if (language === "ja") {
918
- console.error(`❌ ベースブランチ (${baseBranch}) からPRを作成できません。`);
919
- }
920
- else {
921
- console.error(`❌ Cannot create PR from base branch (${baseBranch}).`);
922
- }
923
- process.exit(1);
924
- }
925
- // ブランチが push されているかチェック、されていなければ自動 push
926
- try {
927
- // upstream が設定されているかチェック
928
- (0, child_process_1.execSync)(`git rev-parse --abbrev-ref ${currentBranch}@{upstream}`, {
929
- encoding: "utf-8",
930
- stdio: "pipe",
931
- });
932
- // upstream が存在する場合、リモートと同期しているかチェック
933
- const localCommit = (0, child_process_1.execSync)(`git rev-parse ${currentBranch}`, {
934
- encoding: "utf-8",
935
- stdio: "pipe",
936
- }).trim();
937
- const remoteCommit = (0, child_process_1.execSync)(`git rev-parse ${currentBranch}@{upstream}`, {
938
- encoding: "utf-8",
939
- stdio: "pipe",
940
- }).trim();
941
- if (localCommit !== remoteCommit) {
942
- // ローカルに新しいコミットがある場合は push
943
- console.log(`📤 ブランチを push 中... (origin ${currentBranch})`);
944
- const pushResult = (0, child_process_1.spawnSync)("git", ["push"], { stdio: "inherit" });
945
- if (pushResult.status !== 0) {
946
- console.error("❌ ブランチの push に失敗しました。");
947
- process.exit(1);
948
- }
949
- }
950
- }
951
- catch {
952
- // upstream が存在しない場合、新規 push
953
- console.log(`📤 ブランチを push 中... (origin ${currentBranch} を新規作成)`);
954
- const pushResult = (0, child_process_1.spawnSync)("git", ["push", "-u", "origin", currentBranch], { stdio: "inherit" });
955
- if (pushResult.status !== 0) {
956
- if (language === "ja") {
957
- console.error("❌ ブランチの push に失敗しました。");
958
- }
959
- else {
960
- console.error("❌ Failed to push branch.");
961
- }
962
- process.exit(1);
963
- }
964
- }
965
- console.log(`🤖 PR説明文を生成中... (${baseBranch}...${currentBranch}) [compact summary input]`);
966
- const description = await generatePRDescription(baseBranch);
967
- console.log(`\n📝 Generated PR description:\n`);
968
- description.split("\n").forEach((line) => {
969
- console.log(` ${line}`);
970
- });
971
- console.log();
972
- const answer = await askUser("Create PR with this description? [y/n/e(edit)]: ");
973
- let finalDescription = description;
974
- if (answer === "e" || answer === "edit") {
975
- finalDescription = editInEditor(description);
976
- if (!finalDescription) {
977
- console.log("Aborted: empty description.");
978
- process.exit(0);
979
- }
980
- console.log(`\n✏️ Edited description:\n`);
981
- finalDescription.split("\n").forEach((line) => {
982
- console.log(` ${line}`);
983
- });
984
- console.log();
985
- }
986
- else if (answer !== "y" && answer !== "yes") {
987
- console.log("Aborted.");
988
- process.exit(0);
989
- }
990
- const fallbackURL = getPullRequestURL(currentBranch);
991
- createPR(finalDescription, baseBranch, fallbackURL);
992
- console.log(`\n✅ PR created successfully!`);
993
- }