@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.
@@ -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', 'origin', tagName, '--force']);
298
+ await execFileAsync('git', ['push', remote, tagName, '--force']);
299
299
  return true;
300
300
  } catch (error) {
301
- console.error('Failed to force push tag to remote:', error instanceof Error ? error.message : 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
  }
@@ -229,57 +229,66 @@ export class TagCommand {
229
229
 
230
230
  console.log(`✅ Tag ${trimmedName} created successfully!`);
231
231
 
232
- const shouldPush = await this.confirmTagPush(trimmedName);
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
- console.log(`Force pushing tag ${trimmedName} to remote...`);
254
- const pushSuccess = await GitService.forcePushTag(trimmedName);
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
- if (pushSuccess) {
257
- console.log(`✅ Tag ${trimmedName} force pushed successfully!`);
258
- } else {
259
- console.error('❌ Failed to force push tag to remote');
260
- await LogService.append({
261
- command: 'tag',
262
- args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
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
- if (pushSuccess) {
273
- console.log(`✅ Tag ${trimmedName} pushed successfully!`);
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
- console.error('❌ Failed to push tag to remote');
276
- await LogService.append({
277
- command: 'tag',
278
- args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
279
- status: 'failure',
280
- details: 'tag push failed'
281
- });
282
- process.exit(1);
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 confirmTagPush(tagName: string): Promise<boolean> {
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(`Push tag ${tagName} to remote? (y/n): `, resolve);
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
- return normalized === 'y' || normalized === 'yes';
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();
@@ -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
- ? 'Begin with a short summary sentence (in Korean) that captures the overall impact of the release using plain text only.'
10
- : 'Begin with a short summary sentence (in English) that captures the overall impact of the release using plain text only.';
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
- ? 'After the summary, write one line per category 사용자 기능, 버그 수정, 유지 보수 using the format "사용자 기능: 변경1; 변경2" with plain text only (no bullets, numbers, or markdown symbols).'
14
- : 'After the summary, write one line per category User Features, Bug Fixes, Maintenance using the format "User Features: Change 1; Change 2" with plain text only (no bullets, numbers, or markdown symbols).';
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
- ? 'If a category has no changes, write "사용자 기능: 해당 사항 없음." (or the matching category label) using the same plain text format.'
18
- : 'If a category has no changes, write "User Features: None." (or the matching category label) using the same plain text format.';
43
+ ? '변경사항이 없는 카테고리는 생략하세요.'
44
+ : 'Omit categories with no changes.';
19
45
 
20
46
  const noChangesInstruction = language === 'ko'
21
- ? 'If no changes exist at all, state "변경 사항 없음" plainly.'
22
- : 'If no changes exist at all, state "No changes to report." plainly.';
47
+ ? '변경사항이 전혀 없으면 "변경 사항 없음"이라고 작성하세요.'
48
+ : 'If no changes exist at all, state "No changes to report."';
23
49
 
24
50
  const outputLanguageLine = language === 'ko'
25
- ? 'Write the release notes in Korean using concise plain text without markdown syntax.'
26
- : 'Write the release notes in English using concise plain text without markdown syntax.';
51
+ ? '릴리즈 노트를 한국어로 작성하세요.'
52
+ : 'Write the release notes in English.';
27
53
 
28
- return `You are an experienced release manager. Produce clear, user-facing release notes that describe the differences between the previous tag and ${tagName}.
54
+ return `You are an experienced release manager. Produce clear, user-facing release notes in GitHub Release style.
29
55
 
30
56
  ## Objective
31
- Summarize the meaningful changes that occurred between the prior release tag and ${tagName}. Treat the commit log provided by the user message as the complete history of changes since the previous tag.
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 (most recent first).
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
- ${customInstructions}
69
+ ${categoryFormat}
38
70
 
39
- ## Output Requirements
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
- - Do not invent work beyond what appears in the commit log.
47
- - Do not use markdown syntax such as headings (#), bullets (-), emphasis (**), underscores (_), or backticks (\`).
48
- - Separate lines using newline characters only; do not use numbering, bullet prefixes, tables, or code blocks.
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
  };