@ksw8954/git-ai-commit 1.0.11 → 1.1.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/CHANGELOG.md +9 -0
- package/Makefile +50 -1
- package/dist/commands/completion.d.ts +10 -0
- package/dist/commands/completion.d.ts.map +1 -0
- package/dist/commands/completion.js +250 -0
- package/dist/commands/completion.js.map +1 -0
- package/dist/commands/git.d.ts +5 -1
- package/dist/commands/git.d.ts.map +1 -1
- package/dist/commands/git.js +45 -3
- package/dist/commands/git.js.map +1 -1
- package/dist/commands/tag.d.ts +1 -1
- package/dist/commands/tag.d.ts.map +1 -1
- package/dist/commands/tag.js +83 -49
- package/dist/commands/tag.js.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/prompts/tag.d.ts.map +1 -1
- package/dist/prompts/tag.js +48 -21
- package/dist/prompts/tag.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/tagCommand.test.ts +43 -18
- package/src/commands/completion.ts +251 -0
- package/src/commands/git.ts +45 -3
- package/src/commands/tag.ts +90 -50
- package/src/index.ts +3 -0
- package/src/prompts/tag.ts +50 -21
package/src/commands/git.ts
CHANGED
|
@@ -293,12 +293,54 @@ export class GitService {
|
|
|
293
293
|
}
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
-
static async forcePushTag(tagName: string): Promise<boolean> {
|
|
296
|
+
static async forcePushTag(tagName: string, remote = 'origin'): Promise<boolean> {
|
|
297
297
|
try {
|
|
298
|
-
await execFileAsync('git', ['push',
|
|
298
|
+
await execFileAsync('git', ['push', remote, tagName, '--force']);
|
|
299
299
|
return true;
|
|
300
300
|
} catch (error) {
|
|
301
|
-
console.error(
|
|
301
|
+
console.error(`Failed to force push tag to ${remote}:`, error instanceof Error ? error.message : error);
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
static async getRemotes(): Promise<string[]> {
|
|
307
|
+
try {
|
|
308
|
+
const { stdout } = await execAsync('git remote');
|
|
309
|
+
const remotes = stdout
|
|
310
|
+
.split('\n')
|
|
311
|
+
.map(line => line.trim())
|
|
312
|
+
.filter(line => line.length > 0);
|
|
313
|
+
return remotes;
|
|
314
|
+
} catch {
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
static async pushTagToRemote(tagName: string, remote: string): Promise<boolean> {
|
|
320
|
+
try {
|
|
321
|
+
await execFileAsync('git', ['push', remote, tagName]);
|
|
322
|
+
return true;
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.error(`Failed to push tag to ${remote}:`, error instanceof Error ? error.message : error);
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
static async deleteRemoteTagFrom(tagName: string, remote: string): Promise<boolean> {
|
|
330
|
+
try {
|
|
331
|
+
await execFileAsync('git', ['push', remote, '--delete', tagName]);
|
|
332
|
+
return true;
|
|
333
|
+
} catch (error) {
|
|
334
|
+
console.error(`Failed to delete tag from ${remote}:`, error instanceof Error ? error.message : error);
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
static async remoteTagExistsOn(tagName: string, remote: string): Promise<boolean> {
|
|
340
|
+
try {
|
|
341
|
+
const { stdout } = await execAsync(`git ls-remote --tags ${remote} refs/tags/${tagName}`);
|
|
342
|
+
return stdout.trim().length > 0;
|
|
343
|
+
} catch {
|
|
302
344
|
return false;
|
|
303
345
|
}
|
|
304
346
|
}
|
package/src/commands/tag.ts
CHANGED
|
@@ -229,57 +229,66 @@ export class TagCommand {
|
|
|
229
229
|
|
|
230
230
|
console.log(`✅ Tag ${trimmedName} created successfully!`);
|
|
231
231
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (shouldPush) {
|
|
235
|
-
// If tag was replaced or remote tag still exists, use force push
|
|
236
|
-
const needsForcePush = wasTagReplaced || remoteTagExists;
|
|
237
|
-
|
|
238
|
-
if (needsForcePush) {
|
|
239
|
-
console.log(`⚠️ Tag ${trimmedName} exists on remote. Force push is required.`);
|
|
240
|
-
const shouldForcePush = await this.confirmForcePush(trimmedName);
|
|
241
|
-
|
|
242
|
-
if (!shouldForcePush) {
|
|
243
|
-
console.log('Tag push cancelled by user.');
|
|
244
|
-
await LogService.append({
|
|
245
|
-
command: 'tag',
|
|
246
|
-
args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
247
|
-
status: 'cancelled',
|
|
248
|
-
details: 'user declined force push'
|
|
249
|
-
});
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
232
|
+
// Get available remotes
|
|
233
|
+
const remotes = await GitService.getRemotes();
|
|
252
234
|
|
|
253
|
-
|
|
254
|
-
|
|
235
|
+
if (remotes.length === 0) {
|
|
236
|
+
console.log('No remotes configured. Skipping push.');
|
|
237
|
+
} else {
|
|
238
|
+
const selectedRemotes = await this.selectRemotesForPush(trimmedName, remotes);
|
|
255
239
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
status: 'failure',
|
|
264
|
-
details: 'tag force push failed'
|
|
265
|
-
});
|
|
266
|
-
process.exit(1);
|
|
267
|
-
}
|
|
268
|
-
} else {
|
|
269
|
-
console.log(`Pushing tag ${trimmedName} to remote...`);
|
|
270
|
-
const pushSuccess = await GitService.pushTag(trimmedName);
|
|
240
|
+
if (selectedRemotes && selectedRemotes.length > 0) {
|
|
241
|
+
// If tag was replaced or remote tag still exists, use force push
|
|
242
|
+
const needsForcePush = wasTagReplaced || remoteTagExists;
|
|
243
|
+
|
|
244
|
+
if (needsForcePush) {
|
|
245
|
+
console.log(`\n⚠️ Tag ${trimmedName} may exist on remote. Force push is required.`);
|
|
246
|
+
const shouldForcePush = await this.confirmForcePush(trimmedName);
|
|
271
247
|
|
|
272
|
-
|
|
273
|
-
|
|
248
|
+
if (!shouldForcePush) {
|
|
249
|
+
console.log('Tag push cancelled by user.');
|
|
250
|
+
await LogService.append({
|
|
251
|
+
command: 'tag',
|
|
252
|
+
args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
253
|
+
status: 'cancelled',
|
|
254
|
+
details: 'user declined force push'
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const remote of selectedRemotes) {
|
|
260
|
+
console.log(`Force pushing tag ${trimmedName} to ${remote}...`);
|
|
261
|
+
const pushSuccess = await GitService.forcePushTag(trimmedName, remote);
|
|
262
|
+
|
|
263
|
+
if (pushSuccess) {
|
|
264
|
+
console.log(`✅ Tag ${trimmedName} force pushed to ${remote} successfully!`);
|
|
265
|
+
} else {
|
|
266
|
+
console.error(`❌ Failed to force push tag to ${remote}`);
|
|
267
|
+
await LogService.append({
|
|
268
|
+
command: 'tag',
|
|
269
|
+
args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
270
|
+
status: 'failure',
|
|
271
|
+
details: `tag force push to ${remote} failed`
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
274
275
|
} else {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
276
|
+
for (const remote of selectedRemotes) {
|
|
277
|
+
console.log(`Pushing tag ${trimmedName} to ${remote}...`);
|
|
278
|
+
const pushSuccess = await GitService.pushTagToRemote(trimmedName, remote);
|
|
279
|
+
|
|
280
|
+
if (pushSuccess) {
|
|
281
|
+
console.log(`✅ Tag ${trimmedName} pushed to ${remote} successfully!`);
|
|
282
|
+
} else {
|
|
283
|
+
console.error(`❌ Failed to push tag to ${remote}`);
|
|
284
|
+
await LogService.append({
|
|
285
|
+
command: 'tag',
|
|
286
|
+
args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
|
|
287
|
+
status: 'failure',
|
|
288
|
+
details: `tag push to ${remote} failed`
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
283
292
|
}
|
|
284
293
|
}
|
|
285
294
|
}
|
|
@@ -291,20 +300,51 @@ export class TagCommand {
|
|
|
291
300
|
});
|
|
292
301
|
}
|
|
293
302
|
|
|
294
|
-
private async
|
|
303
|
+
private async selectRemotesForPush(tagName: string, remotes: string[]): Promise<string[] | null> {
|
|
295
304
|
const rl = readline.createInterface({
|
|
296
305
|
input: process.stdin,
|
|
297
306
|
output: process.stdout
|
|
298
307
|
});
|
|
299
308
|
|
|
309
|
+
console.log('\nAvailable remotes:');
|
|
310
|
+
remotes.forEach((remote, index) => {
|
|
311
|
+
console.log(` ${index + 1}. ${remote}`);
|
|
312
|
+
});
|
|
313
|
+
console.log(` all. Push to all remotes`);
|
|
314
|
+
console.log(` n. Skip push`);
|
|
315
|
+
|
|
300
316
|
const answer: string = await new Promise(resolve => {
|
|
301
|
-
rl.question(
|
|
317
|
+
rl.question(`\nSelect remote(s) to push tag ${tagName} (e.g., 1 or 1,2 or all or n): `, resolve);
|
|
302
318
|
});
|
|
303
319
|
|
|
304
320
|
rl.close();
|
|
305
321
|
|
|
306
322
|
const normalized = answer.trim().toLowerCase();
|
|
307
|
-
|
|
323
|
+
|
|
324
|
+
if (normalized === 'n' || normalized === 'no' || normalized === '') {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (normalized === 'all') {
|
|
329
|
+
return remotes;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const selections = normalized.split(',').map(s => s.trim());
|
|
333
|
+
const selectedRemotes: string[] = [];
|
|
334
|
+
|
|
335
|
+
for (const sel of selections) {
|
|
336
|
+
const index = parseInt(sel, 10);
|
|
337
|
+
if (isNaN(index) || index < 1 || index > remotes.length) {
|
|
338
|
+
console.log(`Invalid selection: ${sel}`);
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
const remote = remotes[index - 1];
|
|
342
|
+
if (!selectedRemotes.includes(remote)) {
|
|
343
|
+
selectedRemotes.push(remote);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return selectedRemotes.length > 0 ? selectedRemotes : null;
|
|
308
348
|
}
|
|
309
349
|
|
|
310
350
|
private async confirmTagCreate(tagName: string): Promise<boolean> {
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { ConfigCommand } from './commands/configCommand';
|
|
|
8
8
|
import { PullRequestCommand } from './commands/prCommand';
|
|
9
9
|
import { TagCommand } from './commands/tag';
|
|
10
10
|
import { HistoryCommand } from './commands/history';
|
|
11
|
+
import { CompletionCommand } from './commands/completion';
|
|
11
12
|
|
|
12
13
|
function getPackageVersion(): string {
|
|
13
14
|
try {
|
|
@@ -34,11 +35,13 @@ const configCommand = new ConfigCommand();
|
|
|
34
35
|
const pullRequestCommand = new PullRequestCommand();
|
|
35
36
|
const tagCommand = new TagCommand();
|
|
36
37
|
const historyCommand = new HistoryCommand();
|
|
38
|
+
const completionCommand = new CompletionCommand();
|
|
37
39
|
|
|
38
40
|
program.addCommand(commitCommand.getCommand());
|
|
39
41
|
program.addCommand(configCommand.getCommand());
|
|
40
42
|
program.addCommand(pullRequestCommand.getCommand());
|
|
41
43
|
program.addCommand(tagCommand.getCommand());
|
|
42
44
|
program.addCommand(historyCommand.getCommand());
|
|
45
|
+
program.addCommand(completionCommand.getCommand());
|
|
43
46
|
|
|
44
47
|
program.parse();
|
package/src/prompts/tag.ts
CHANGED
|
@@ -5,46 +5,75 @@ export const generateTagPrompt = (
|
|
|
5
5
|
customInstructions = '',
|
|
6
6
|
language: TagPromptLanguage = 'ko'
|
|
7
7
|
): string => {
|
|
8
|
+
const titleInstruction = language === 'ko'
|
|
9
|
+
? `첫 줄에 버전 "${tagName}"을 제목으로 작성하세요.`
|
|
10
|
+
: `Write the version "${tagName}" as the title on the first line.`;
|
|
11
|
+
|
|
8
12
|
const summaryInstruction = language === 'ko'
|
|
9
|
-
? '
|
|
10
|
-
: '
|
|
13
|
+
? '제목 다음 줄에 이번 릴리즈의 전체적인 영향을 요약하는 한 문장을 작성하세요.'
|
|
14
|
+
: 'On the line after the title, write a one-sentence summary capturing the overall impact of this release.';
|
|
11
15
|
|
|
12
16
|
const listInstruction = language === 'ko'
|
|
13
|
-
? '
|
|
14
|
-
: 'After the summary,
|
|
17
|
+
? '요약 후 빈 줄을 두고, 각 카테고리별로 변경사항을 나열하세요: 새로운 기능, 버그 수정, 개선사항. 각 항목은 "- " 로 시작합니다.'
|
|
18
|
+
: 'After the summary, leave a blank line, then list changes by category: New Features, Bug Fixes, Improvements. Each item starts with "- ".';
|
|
19
|
+
|
|
20
|
+
const categoryFormat = language === 'ko'
|
|
21
|
+
? `카테고리 형식:
|
|
22
|
+
### 새로운 기능
|
|
23
|
+
- 변경사항 1
|
|
24
|
+
- 변경사항 2
|
|
25
|
+
|
|
26
|
+
### 버그 수정
|
|
27
|
+
- 수정사항 1
|
|
28
|
+
|
|
29
|
+
### 개선사항
|
|
30
|
+
- 개선사항 1`
|
|
31
|
+
: `Category format:
|
|
32
|
+
### New Features
|
|
33
|
+
- Change 1
|
|
34
|
+
- Change 2
|
|
35
|
+
|
|
36
|
+
### Bug Fixes
|
|
37
|
+
- Fix 1
|
|
38
|
+
|
|
39
|
+
### Improvements
|
|
40
|
+
- Improvement 1`;
|
|
15
41
|
|
|
16
42
|
const emptyCategoryInstruction = language === 'ko'
|
|
17
|
-
? '
|
|
18
|
-
: '
|
|
43
|
+
? '변경사항이 없는 카테고리는 생략하세요.'
|
|
44
|
+
: 'Omit categories with no changes.';
|
|
19
45
|
|
|
20
46
|
const noChangesInstruction = language === 'ko'
|
|
21
|
-
? '
|
|
22
|
-
: 'If no changes exist at all, state "No changes to report."
|
|
47
|
+
? '변경사항이 전혀 없으면 "변경 사항 없음"이라고 작성하세요.'
|
|
48
|
+
: 'If no changes exist at all, state "No changes to report."';
|
|
23
49
|
|
|
24
50
|
const outputLanguageLine = language === 'ko'
|
|
25
|
-
? '
|
|
26
|
-
: 'Write the release notes in English
|
|
51
|
+
? '릴리즈 노트를 한국어로 작성하세요.'
|
|
52
|
+
: 'Write the release notes in English.';
|
|
27
53
|
|
|
28
|
-
return `You are an experienced release manager. Produce clear, user-facing release notes
|
|
54
|
+
return `You are an experienced release manager. Produce clear, user-facing release notes in GitHub Release style.
|
|
29
55
|
|
|
30
56
|
## Objective
|
|
31
|
-
|
|
57
|
+
Create release notes for ${tagName} that describe the meaningful changes since the previous release.
|
|
32
58
|
|
|
33
59
|
## Input Context
|
|
34
60
|
- Target tag to publish: ${tagName}
|
|
35
|
-
- Commit history between the previous tag and ${tagName} will be supplied in the user message
|
|
61
|
+
- Commit history between the previous tag and ${tagName} will be supplied in the user message.
|
|
62
|
+
|
|
63
|
+
${customInstructions ? `## Additional Instructions\n${customInstructions}\n` : ''}
|
|
64
|
+
## Output Format (GitHub Release Style)
|
|
65
|
+
${titleInstruction}
|
|
66
|
+
${summaryInstruction}
|
|
67
|
+
${listInstruction}
|
|
36
68
|
|
|
37
|
-
${
|
|
69
|
+
${categoryFormat}
|
|
38
70
|
|
|
39
|
-
##
|
|
71
|
+
## Rules
|
|
40
72
|
- ${outputLanguageLine}
|
|
41
|
-
- ${summaryInstruction}
|
|
42
|
-
- ${listInstruction}
|
|
43
|
-
- Use short phrases for each change and include scope/component names when helpful, without copying commit messages verbatim.
|
|
44
73
|
- ${emptyCategoryInstruction}
|
|
45
74
|
- ${noChangesInstruction}
|
|
46
|
-
-
|
|
47
|
-
- Do not
|
|
48
|
-
-
|
|
75
|
+
- Use concise descriptions; do not copy commit messages verbatim.
|
|
76
|
+
- Do not invent changes beyond what appears in the commit log.
|
|
77
|
+
- Use markdown formatting (###, -, etc.) as shown in the category format.
|
|
49
78
|
- Return only the release notes content with no surrounding commentary.`;
|
|
50
79
|
};
|