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/README.md +172 -67
- package/dist/commands/checkout.js +30 -0
- package/dist/commands/commit.js +55 -0
- package/dist/commands/pr.js +74 -0
- package/dist/commands/push.js +78 -0
- package/dist/index.js +37 -933
- package/dist/services/ai.js +520 -0
- package/dist/services/branch.js +37 -0
- package/dist/services/github.js +332 -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 +1 -1
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
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
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:
|
|
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" &&
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
81
|
+
await (0, checkout_js_1.runCheckoutCommand)();
|
|
496
82
|
return;
|
|
497
83
|
}
|
|
498
84
|
if (subcommand === "pr") {
|
|
499
|
-
await
|
|
85
|
+
await (0, pr_js_1.runPRCommand)(language);
|
|
500
86
|
return;
|
|
501
87
|
}
|
|
502
|
-
if (
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
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
|
-
}
|