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