@ksw8954/git-ai-commit 1.1.4 → 1.1.6

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.
@@ -14,6 +14,13 @@ export interface TagOptions {
14
14
  prompt?: string;
15
15
  }
16
16
 
17
+ interface TagStyleMismatch {
18
+ newTag: string;
19
+ newPattern: string;
20
+ dominantPattern: string;
21
+ examples: string[];
22
+ }
23
+
17
24
  export class TagCommand {
18
25
  private program: Command;
19
26
 
@@ -106,9 +113,24 @@ export class TagCommand {
106
113
  trimmedName = newVersion;
107
114
  }
108
115
 
109
- // Check if tag already exists locally
116
+ const styleMismatch = await this.checkTagStyleMismatch(trimmedName);
117
+ if (styleMismatch) {
118
+ const shouldProceed = await this.confirmStyleMismatch(styleMismatch);
119
+ if (!shouldProceed) {
120
+ console.log('Tag creation cancelled by user.');
121
+ await LogService.append({
122
+ command: 'tag',
123
+ args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
124
+ status: 'cancelled',
125
+ details: `tag style mismatch: "${styleMismatch.newPattern}" vs "${styleMismatch.dominantPattern}"`,
126
+ model: mergedModel
127
+ });
128
+ return;
129
+ }
130
+ }
131
+
110
132
  const localTagExists = await GitService.tagExists(trimmedName);
111
- let remoteTagExists = false;
133
+ let remoteTagExists = await GitService.remoteTagExists(trimmedName);
112
134
  let previousTagMessage: string | null = null;
113
135
 
114
136
  if (localTagExists) {
@@ -130,9 +152,6 @@ export class TagCommand {
130
152
  return;
131
153
  }
132
154
 
133
- // Check if tag exists on remote
134
- remoteTagExists = await GitService.remoteTagExists(trimmedName);
135
-
136
155
  if (remoteTagExists) {
137
156
  console.log(`⚠️ Tag ${trimmedName} also exists on remote.`);
138
157
  const shouldDeleteRemote = await this.confirmRemoteTagDelete(trimmedName);
@@ -173,6 +192,28 @@ export class TagCommand {
173
192
  return;
174
193
  }
175
194
  console.log(`✅ Local tag ${trimmedName} deleted`);
195
+ } else if (remoteTagExists) {
196
+ console.log(`⚠️ Tag ${trimmedName} exists on remote but not locally.`);
197
+ const shouldDeleteRemote = await this.confirmRemoteTagDelete(trimmedName);
198
+
199
+ if (shouldDeleteRemote) {
200
+ console.log(`Deleting remote tag ${trimmedName}...`);
201
+ const remoteDeleted = await GitService.deleteRemoteTag(trimmedName);
202
+ if (!remoteDeleted) {
203
+ console.error('❌ Failed to delete remote tag');
204
+ await LogService.append({
205
+ command: 'tag',
206
+ args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
207
+ status: 'failure',
208
+ details: 'remote tag deletion failed',
209
+ model: mergedModel
210
+ });
211
+ process.exit(1);
212
+ return;
213
+ }
214
+ console.log(`✅ Remote tag ${trimmedName} deleted`);
215
+ remoteTagExists = false;
216
+ }
176
217
  }
177
218
 
178
219
  let tagMessage = options.message?.trim();
@@ -200,7 +241,59 @@ export class TagCommand {
200
241
  }
201
242
  }
202
243
 
203
- const historyResult = await GitService.getCommitSummariesSince(baseTag);
244
+ let historyResult = await GitService.getCommitSummariesSince(baseTag);
245
+
246
+ if (!historyResult.success && baseTag) {
247
+ console.log(`\n⚠️ No commits found since tag ${baseTag}.`);
248
+
249
+ const shouldDeleteBase = await this.confirmBaseTagDelete(baseTag);
250
+ if (shouldDeleteBase) {
251
+ const olderTagResult = await GitService.getTagBefore(baseTag);
252
+ const olderBase = olderTagResult.success ? olderTagResult.tag : undefined;
253
+
254
+ const localExists = await GitService.tagExists(baseTag);
255
+ if (localExists) {
256
+ const localDeleted = await GitService.deleteLocalTag(baseTag);
257
+ if (localDeleted) {
258
+ console.log(`✅ Local tag ${baseTag} deleted`);
259
+ } else {
260
+ console.error(`❌ Failed to delete local tag ${baseTag}`);
261
+ }
262
+ }
263
+
264
+ const remoteExists = await GitService.remoteTagExists(baseTag);
265
+ if (remoteExists) {
266
+ const shouldDeleteRemote = await this.confirmRemoteTagDelete(baseTag);
267
+ if (shouldDeleteRemote) {
268
+ const remoteDeleted = await GitService.deleteRemoteTag(baseTag);
269
+ if (remoteDeleted) {
270
+ console.log(`✅ Remote tag ${baseTag} deleted`);
271
+ } else {
272
+ console.error(`❌ Failed to delete remote tag ${baseTag}`);
273
+ }
274
+ }
275
+ }
276
+
277
+ if (olderBase) {
278
+ console.log(`Using ${olderBase} as new base tag.`);
279
+ baseTag = olderBase;
280
+ } else {
281
+ console.log('No earlier tag found; using entire commit history.');
282
+ baseTag = undefined;
283
+ }
284
+
285
+ styleReferenceMessage = null;
286
+ if (baseTag && baseTag !== trimmedName) {
287
+ styleReferenceMessage = await GitService.getTagMessage(baseTag);
288
+ if (styleReferenceMessage) {
289
+ console.log(`Using ${baseTag} message as style reference.`);
290
+ }
291
+ }
292
+
293
+ historyResult = await GitService.getCommitSummariesSince(baseTag);
294
+ }
295
+ }
296
+
204
297
  if (!historyResult.success || !historyResult.log) {
205
298
  console.error('Error:', historyResult.error ?? 'Unable to read commit history.');
206
299
  await LogService.append({
@@ -368,6 +461,69 @@ export class TagCommand {
368
461
  });
369
462
  }
370
463
 
464
+ private extractTagPattern(tagName: string): string {
465
+ return tagName.replace(/[a-zA-Z]+/g, '{word}').replace(/\d+/g, '{n}');
466
+ }
467
+
468
+ private async checkTagStyleMismatch(newTagName: string): Promise<TagStyleMismatch | null> {
469
+ const recentTags = await GitService.getRecentTags(10);
470
+ if (recentTags.length === 0) {
471
+ return null;
472
+ }
473
+
474
+ const newPattern = this.extractTagPattern(newTagName);
475
+
476
+ const patternCounts = new Map<string, string[]>();
477
+ for (const tag of recentTags) {
478
+ const pattern = this.extractTagPattern(tag);
479
+ const existing = patternCounts.get(pattern) || [];
480
+ existing.push(tag);
481
+ patternCounts.set(pattern, existing);
482
+ }
483
+
484
+ let dominantPattern = '';
485
+ let maxCount = 0;
486
+ let dominantExamples: string[] = [];
487
+ for (const [pattern, tags] of patternCounts) {
488
+ if (tags.length > maxCount) {
489
+ maxCount = tags.length;
490
+ dominantPattern = pattern;
491
+ dominantExamples = tags;
492
+ }
493
+ }
494
+
495
+ if (dominantPattern && dominantPattern !== newPattern && maxCount >= 2) {
496
+ return {
497
+ newTag: newTagName,
498
+ newPattern,
499
+ dominantPattern,
500
+ examples: dominantExamples.slice(0, 3)
501
+ };
502
+ }
503
+
504
+ return null;
505
+ }
506
+
507
+ private async confirmStyleMismatch(mismatch: TagStyleMismatch): Promise<boolean> {
508
+ const rl = readline.createInterface({
509
+ input: process.stdin,
510
+ output: process.stdout
511
+ });
512
+
513
+ console.log(`\n⚠️ Tag name style mismatch detected.`);
514
+ console.log(` New tag: ${mismatch.newTag} (pattern: ${mismatch.newPattern})`);
515
+ console.log(` Recent tags: ${mismatch.examples.join(', ')} (pattern: ${mismatch.dominantPattern})`);
516
+
517
+ const answer: string = await new Promise(resolve => {
518
+ rl.question(`Proceed with "${mismatch.newTag}" anyway? (y/n): `, resolve);
519
+ });
520
+
521
+ rl.close();
522
+
523
+ const normalized = answer.trim().toLowerCase();
524
+ return normalized === 'y' || normalized === 'yes';
525
+ }
526
+
371
527
  private async selectRemotesForPush(tagName: string, remotes: string[]): Promise<string[] | null> {
372
528
  const rl = readline.createInterface({
373
529
  input: process.stdin,
@@ -463,6 +619,22 @@ export class TagCommand {
463
619
  return normalized === 'y' || normalized === 'yes';
464
620
  }
465
621
 
622
+ private async confirmBaseTagDelete(tagName: string): Promise<boolean> {
623
+ const rl = readline.createInterface({
624
+ input: process.stdin,
625
+ output: process.stdout
626
+ });
627
+
628
+ const answer: string = await new Promise(resolve => {
629
+ rl.question(`Delete tag ${tagName} and use an older base? (y/n): `, resolve);
630
+ });
631
+
632
+ rl.close();
633
+
634
+ const normalized = answer.trim().toLowerCase();
635
+ return normalized === 'y' || normalized === 'yes';
636
+ }
637
+
466
638
  private async confirmForcePush(tagName: string): Promise<boolean> {
467
639
  const rl = readline.createInterface({
468
640
  input: process.stdin,