@ksw8954/git-ai-commit 1.1.1 → 1.1.4

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.
Files changed (40) hide show
  1. package/AGENTS.md +8 -0
  2. package/CHANGELOG.md +22 -0
  3. package/dist/commands/ai.d.ts +1 -1
  4. package/dist/commands/ai.d.ts.map +1 -1
  5. package/dist/commands/ai.js +10 -3
  6. package/dist/commands/ai.js.map +1 -1
  7. package/dist/commands/commit.d.ts.map +1 -1
  8. package/dist/commands/commit.js +8 -0
  9. package/dist/commands/commit.js.map +1 -1
  10. package/dist/commands/git.d.ts +1 -0
  11. package/dist/commands/git.d.ts.map +1 -1
  12. package/dist/commands/git.js +20 -0
  13. package/dist/commands/git.js.map +1 -1
  14. package/dist/commands/history.d.ts.map +1 -1
  15. package/dist/commands/history.js +5 -5
  16. package/dist/commands/history.js.map +1 -1
  17. package/dist/commands/log.d.ts +9 -1
  18. package/dist/commands/log.d.ts.map +1 -1
  19. package/dist/commands/log.js +19 -1
  20. package/dist/commands/log.js.map +1 -1
  21. package/dist/commands/prCommand.d.ts.map +1 -1
  22. package/dist/commands/prCommand.js +8 -4
  23. package/dist/commands/prCommand.js.map +1 -1
  24. package/dist/commands/tag.d.ts +1 -0
  25. package/dist/commands/tag.d.ts.map +1 -1
  26. package/dist/commands/tag.js +82 -28
  27. package/dist/commands/tag.js.map +1 -1
  28. package/dist/prompts/tag.d.ts +1 -1
  29. package/dist/prompts/tag.d.ts.map +1 -1
  30. package/dist/prompts/tag.js +35 -3
  31. package/dist/prompts/tag.js.map +1 -1
  32. package/package.json +12 -12
  33. package/src/commands/ai.ts +19 -3
  34. package/src/commands/commit.ts +8 -0
  35. package/src/commands/git.ts +20 -0
  36. package/src/commands/history.ts +6 -5
  37. package/src/commands/log.ts +30 -2
  38. package/src/commands/prCommand.ts +8 -4
  39. package/src/commands/tag.ts +98 -30
  40. package/src/prompts/tag.ts +39 -3
@@ -50,7 +50,8 @@ export class PullRequestCommand {
50
50
  command: 'pr',
51
51
  args: { ...options, apiKey: options.apiKey ? '***' : undefined },
52
52
  status: 'failure',
53
- details: err
53
+ details: err,
54
+ model: mergedModel
54
55
  });
55
56
  process.exit(1);
56
57
  return;
@@ -77,7 +78,8 @@ export class PullRequestCommand {
77
78
  command: 'pr',
78
79
  args: { ...options, apiKey: options.apiKey ? '***' : undefined },
79
80
  status: 'failure',
80
- details: err
81
+ details: err,
82
+ model: mergedModel
81
83
  });
82
84
  process.exit(1);
83
85
  return;
@@ -87,7 +89,8 @@ export class PullRequestCommand {
87
89
  await LogService.append({
88
90
  command: 'pr',
89
91
  args: { ...options, apiKey: options.apiKey ? '***' : undefined },
90
- status: 'success'
92
+ status: 'success',
93
+ model: mergedModel
91
94
  });
92
95
  } catch (error) {
93
96
  const message = error instanceof Error ? error.message : String(error);
@@ -96,7 +99,8 @@ export class PullRequestCommand {
96
99
  command: 'pr',
97
100
  args: { ...options, apiKey: options.apiKey ? '***' : undefined },
98
101
  status: 'failure',
99
- details: message
102
+ details: message,
103
+ model: options.model
100
104
  });
101
105
  process.exit(1);
102
106
  }
@@ -20,18 +20,29 @@ export class TagCommand {
20
20
  constructor() {
21
21
  this.program = new Command('tag')
22
22
  .description('Create an annotated git tag with optional AI-generated notes')
23
- .argument('<name>', 'Tag name to create')
23
+ .argument('[name]', 'Tag name to create (auto-increments patch version if omitted)')
24
24
  .option('-k, --api-key <key>', 'OpenAI API key (overrides env var)')
25
25
  .option('--base-url <url>', 'Custom API base URL (overrides env var)')
26
26
  .option('-m, --model <model>', 'Model to use (overrides env var)')
27
27
  .option('--message <message>', 'Tag message to use directly (skips AI generation)')
28
28
  .option('-t, --base-tag <tag>', 'Existing tag to diff against when generating notes')
29
29
  .option('--prompt <text>', 'Additional instructions to append to the AI prompt for this tag')
30
- .action(async (tagName: string, options: TagOptions) => {
30
+ .action(async (tagName: string | undefined, options: TagOptions) => {
31
31
  await this.handleTag(tagName, options);
32
32
  });
33
33
  }
34
34
 
35
+ private incrementPatchVersion(version: string): string | null {
36
+ // Match semver patterns: v1.2.3, 1.2.3, prefix-v1.2.3, prefix-1.2.3, etc.
37
+ const match = version.match(/^(.*?-?)?(v?)(\d+)\.(\d+)\.(\d+)(.*)$/);
38
+ if (!match) {
39
+ return null;
40
+ }
41
+ const [, prefix = '', v = '', major, minor, patch, suffix = ''] = match;
42
+ const newPatch = parseInt(patch, 10) + 1;
43
+ return `${prefix}${v}${major}.${minor}.${newPatch}${suffix}`;
44
+ }
45
+
35
46
  private resolveAIConfig(options: TagOptions): AIServiceConfig {
36
47
  const storedConfig = ConfigService.getConfig();
37
48
 
@@ -52,27 +63,58 @@ export class TagCommand {
52
63
  };
53
64
  }
54
65
 
55
- private async handleTag(tagName: string, options: TagOptions): Promise<void> {
56
- const trimmedName = tagName?.trim();
66
+ private async handleTag(tagName: string | undefined, options: TagOptions): Promise<void> {
67
+ const storedConfig = ConfigService.getConfig();
68
+ const mergedModel = options.model || storedConfig.model;
57
69
 
70
+ let trimmedName = tagName?.trim();
71
+
72
+ // If no tag name provided, auto-increment from latest tag
58
73
  if (!trimmedName) {
59
- console.error('Tag name is required.');
60
- await LogService.append({
61
- command: 'tag',
62
- args: { name: tagName, ...options, apiKey: options.apiKey ? '***' : undefined },
63
- status: 'failure',
64
- details: 'missing tag name'
65
- });
66
- process.exit(1);
67
- return;
74
+ const latestTagResult = await GitService.getLatestTag();
75
+
76
+ if (!latestTagResult.success || !latestTagResult.tag) {
77
+ console.error('No existing tags found. Please provide a tag name explicitly.');
78
+ await LogService.append({
79
+ command: 'tag',
80
+ args: { ...options, apiKey: options.apiKey ? '***' : undefined },
81
+ status: 'failure',
82
+ details: 'no existing tags found for auto-increment',
83
+ model: mergedModel
84
+ });
85
+ process.exit(1);
86
+ return;
87
+ }
88
+
89
+ const newVersion = this.incrementPatchVersion(latestTagResult.tag);
90
+
91
+ if (!newVersion) {
92
+ console.error(`Cannot parse version from tag "${latestTagResult.tag}". Please provide a tag name explicitly.`);
93
+ await LogService.append({
94
+ command: 'tag',
95
+ args: { ...options, apiKey: options.apiKey ? '***' : undefined },
96
+ status: 'failure',
97
+ details: `cannot parse version from tag: ${latestTagResult.tag}`,
98
+ model: mergedModel
99
+ });
100
+ process.exit(1);
101
+ return;
102
+ }
103
+
104
+ console.log(`Latest tag: ${latestTagResult.tag}`);
105
+ console.log(`New tag: ${newVersion}`);
106
+ trimmedName = newVersion;
68
107
  }
69
108
 
70
109
  // Check if tag already exists locally
71
110
  const localTagExists = await GitService.tagExists(trimmedName);
72
111
  let remoteTagExists = false;
73
- let wasTagReplaced = false;
112
+ let previousTagMessage: string | null = null;
74
113
 
75
114
  if (localTagExists) {
115
+ // Get existing tag message before deletion for reference
116
+ previousTagMessage = await GitService.getTagMessage(trimmedName);
117
+
76
118
  console.log(`⚠️ Tag ${trimmedName} already exists locally.`);
77
119
  const shouldDelete = await this.confirmTagDelete(trimmedName);
78
120
 
@@ -82,7 +124,8 @@ export class TagCommand {
82
124
  command: 'tag',
83
125
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
84
126
  status: 'cancelled',
85
- details: 'user declined to replace existing tag'
127
+ details: 'user declined to replace existing tag',
128
+ model: mergedModel
86
129
  });
87
130
  return;
88
131
  }
@@ -103,7 +146,8 @@ export class TagCommand {
103
146
  command: 'tag',
104
147
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
105
148
  status: 'failure',
106
- details: 'remote tag deletion failed'
149
+ details: 'remote tag deletion failed',
150
+ model: mergedModel
107
151
  });
108
152
  process.exit(1);
109
153
  return;
@@ -122,13 +166,13 @@ export class TagCommand {
122
166
  command: 'tag',
123
167
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
124
168
  status: 'failure',
125
- details: 'local tag deletion failed'
169
+ details: 'local tag deletion failed',
170
+ model: mergedModel
126
171
  });
127
172
  process.exit(1);
128
173
  return;
129
174
  }
130
175
  console.log(`✅ Local tag ${trimmedName} deleted`);
131
- wasTagReplaced = true;
132
176
  }
133
177
 
134
178
  let tagMessage = options.message?.trim();
@@ -147,6 +191,15 @@ export class TagCommand {
147
191
  }
148
192
  }
149
193
 
194
+ // Get style reference from base tag (if different from current tag being replaced)
195
+ let styleReferenceMessage: string | null = null;
196
+ if (baseTag && baseTag !== trimmedName) {
197
+ styleReferenceMessage = await GitService.getTagMessage(baseTag);
198
+ if (styleReferenceMessage) {
199
+ console.log(`Using ${baseTag} message as style reference.`);
200
+ }
201
+ }
202
+
150
203
  const historyResult = await GitService.getCommitSummariesSince(baseTag);
151
204
  if (!historyResult.success || !historyResult.log) {
152
205
  console.error('Error:', historyResult.error ?? 'Unable to read commit history.');
@@ -154,7 +207,8 @@ export class TagCommand {
154
207
  command: 'tag',
155
208
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
156
209
  status: 'failure',
157
- details: historyResult.error ?? 'Unable to read commit history.'
210
+ details: historyResult.error ?? 'Unable to read commit history.',
211
+ model: mergedModel
158
212
  });
159
213
  process.exit(1);
160
214
  return;
@@ -171,14 +225,21 @@ export class TagCommand {
171
225
  command: 'tag',
172
226
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
173
227
  status: 'failure',
174
- details: message
228
+ details: message,
229
+ model: mergedModel
175
230
  });
176
231
  process.exit(1);
177
232
  return;
178
233
  }
179
234
 
180
235
  const aiService = new AIService(aiConfig);
181
- const aiResult = await aiService.generateTagNotes(trimmedName, historyResult.log, options.prompt);
236
+ const aiResult = await aiService.generateTagNotes(
237
+ trimmedName,
238
+ historyResult.log,
239
+ options.prompt,
240
+ previousTagMessage,
241
+ styleReferenceMessage
242
+ );
182
243
 
183
244
  if (!aiResult.success || !aiResult.notes) {
184
245
  console.error('Error:', aiResult.error ?? 'Failed to generate tag notes.');
@@ -186,7 +247,8 @@ export class TagCommand {
186
247
  command: 'tag',
187
248
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
188
249
  status: 'failure',
189
- details: aiResult.error ?? 'Failed to generate tag notes.'
250
+ details: aiResult.error ?? 'Failed to generate tag notes.',
251
+ model: mergedModel
190
252
  });
191
253
  process.exit(1);
192
254
  return;
@@ -207,7 +269,8 @@ export class TagCommand {
207
269
  command: 'tag',
208
270
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
209
271
  status: 'cancelled',
210
- details: 'user declined tag creation'
272
+ details: 'user declined tag creation',
273
+ model: mergedModel
211
274
  });
212
275
  return;
213
276
  }
@@ -221,7 +284,8 @@ export class TagCommand {
221
284
  command: 'tag',
222
285
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
223
286
  status: 'failure',
224
- details: 'git tag creation failed'
287
+ details: 'git tag creation failed',
288
+ model: mergedModel
225
289
  });
226
290
  process.exit(1);
227
291
  return;
@@ -238,8 +302,8 @@ export class TagCommand {
238
302
  const selectedRemotes = await this.selectRemotesForPush(trimmedName, remotes);
239
303
 
240
304
  if (selectedRemotes && selectedRemotes.length > 0) {
241
- // If tag was replaced or remote tag still exists, use force push
242
- const needsForcePush = wasTagReplaced || remoteTagExists;
305
+ // If remote tag still exists (user declined to delete), use force push
306
+ const needsForcePush = remoteTagExists;
243
307
 
244
308
  if (needsForcePush) {
245
309
  console.log(`\n⚠️ Tag ${trimmedName} may exist on remote. Force push is required.`);
@@ -251,7 +315,8 @@ export class TagCommand {
251
315
  command: 'tag',
252
316
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
253
317
  status: 'cancelled',
254
- details: 'user declined force push'
318
+ details: 'user declined force push',
319
+ model: mergedModel
255
320
  });
256
321
  return;
257
322
  }
@@ -268,7 +333,8 @@ export class TagCommand {
268
333
  command: 'tag',
269
334
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
270
335
  status: 'failure',
271
- details: `tag force push to ${remote} failed`
336
+ details: `tag force push to ${remote} failed`,
337
+ model: mergedModel
272
338
  });
273
339
  }
274
340
  }
@@ -285,7 +351,8 @@ export class TagCommand {
285
351
  command: 'tag',
286
352
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
287
353
  status: 'failure',
288
- details: `tag push to ${remote} failed`
354
+ details: `tag push to ${remote} failed`,
355
+ model: mergedModel
289
356
  });
290
357
  }
291
358
  }
@@ -296,7 +363,8 @@ export class TagCommand {
296
363
  await LogService.append({
297
364
  command: 'tag',
298
365
  args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
299
- status: 'success'
366
+ status: 'success',
367
+ model: mergedModel
300
368
  });
301
369
  }
302
370
 
@@ -3,7 +3,9 @@ export type TagPromptLanguage = 'ko' | 'en';
3
3
  export const generateTagPrompt = (
4
4
  tagName: string,
5
5
  customInstructions = '',
6
- language: TagPromptLanguage = 'ko'
6
+ language: TagPromptLanguage = 'ko',
7
+ isImprovement = false,
8
+ hasStyleReference = false
7
9
  ): string => {
8
10
  const titleInstruction = language === 'ko'
9
11
  ? `첫 줄에 버전 "${tagName}"을 제목으로 작성하세요.`
@@ -51,15 +53,49 @@ export const generateTagPrompt = (
51
53
  ? '릴리즈 노트를 한국어로 작성하세요.'
52
54
  : 'Write the release notes in English.';
53
55
 
56
+ const improvementInstruction = isImprovement
57
+ ? language === 'ko'
58
+ ? `\n## 개선 모드
59
+ 이전 릴리즈 노트가 제공됩니다. 이를 참고하여:
60
+ - 기존 내용의 구조와 스타일을 유지하면서 개선하세요.
61
+ - 누락된 변경사항이 있으면 추가하세요.
62
+ - 불필요하거나 중복된 내용은 제거하세요.
63
+ - 더 명확하고 사용자 친화적인 표현으로 다듬으세요.\n`
64
+ : `\n## Improvement Mode
65
+ Previous release notes are provided. Use them as reference to:
66
+ - Maintain the structure and style while improving the content.
67
+ - Add any missing changes from the commit log.
68
+ - Remove unnecessary or redundant content.
69
+ - Refine the language to be clearer and more user-friendly.\n`
70
+ : '';
71
+
72
+ const styleReferenceInstruction = hasStyleReference && !isImprovement
73
+ ? language === 'ko'
74
+ ? `\n## 스타일 참조
75
+ 이전 태그의 릴리즈 노트가 스타일 참조용으로 제공됩니다.
76
+ - 제공된 형식과 구조를 따르세요.
77
+ - 제목, 카테고리, 항목 스타일을 일관되게 유지하세요.
78
+ - 언어 톤과 상세도 수준을 맞추세요.\n`
79
+ : `\n## Style Reference
80
+ A previous tag's release notes are provided as style reference.
81
+ - Follow the provided format and structure.
82
+ - Maintain consistency in title, categories, and item styles.
83
+ - Match the language tone and level of detail.\n`
84
+ : '';
85
+
54
86
  return `You are an experienced release manager. Produce clear, user-facing release notes in GitHub Release style.
55
87
 
56
88
  ## Objective
57
- Create release notes for ${tagName} that describe the meaningful changes since the previous release.
89
+ ${isImprovement
90
+ ? `Improve the existing release notes for ${tagName} based on the commit history and previous notes.`
91
+ : `Create release notes for ${tagName} that describe the meaningful changes since the previous release.`}
58
92
 
59
93
  ## Input Context
60
94
  - Target tag to publish: ${tagName}
61
95
  - Commit history between the previous tag and ${tagName} will be supplied in the user message.
62
-
96
+ ${isImprovement ? '- Previous release notes are provided for improvement.' : ''}
97
+ ${hasStyleReference && !isImprovement ? '- Style reference from previous tag is provided.' : ''}
98
+ ${improvementInstruction}${styleReferenceInstruction}
63
99
  ${customInstructions ? `## Additional Instructions\n${customInstructions}\n` : ''}
64
100
  ## Output Format (GitHub Release Style)
65
101
  ${titleInstruction}