@ksw8954/git-ai-commit 1.0.5 → 1.0.7

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.
@@ -67,6 +67,70 @@ export class TagCommand {
67
67
  return;
68
68
  }
69
69
 
70
+ // Check if tag already exists locally
71
+ const localTagExists = await GitService.tagExists(trimmedName);
72
+ let remoteTagExists = false;
73
+ let wasTagReplaced = false;
74
+
75
+ if (localTagExists) {
76
+ console.log(`⚠️ Tag ${trimmedName} already exists locally.`);
77
+ const shouldDelete = await this.confirmTagDelete(trimmedName);
78
+
79
+ if (!shouldDelete) {
80
+ console.log('Tag creation cancelled by user.');
81
+ await LogService.append({
82
+ command: 'tag',
83
+ args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
84
+ status: 'cancelled',
85
+ details: 'user declined to replace existing tag'
86
+ });
87
+ return;
88
+ }
89
+
90
+ // Check if tag exists on remote
91
+ remoteTagExists = await GitService.remoteTagExists(trimmedName);
92
+
93
+ if (remoteTagExists) {
94
+ console.log(`⚠️ Tag ${trimmedName} also exists on remote.`);
95
+ const shouldDeleteRemote = await this.confirmRemoteTagDelete(trimmedName);
96
+
97
+ if (shouldDeleteRemote) {
98
+ console.log(`Deleting remote tag ${trimmedName}...`);
99
+ const remoteDeleted = await GitService.deleteRemoteTag(trimmedName);
100
+ if (!remoteDeleted) {
101
+ console.error('❌ Failed to delete remote tag');
102
+ await LogService.append({
103
+ command: 'tag',
104
+ args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
105
+ status: 'failure',
106
+ details: 'remote tag deletion failed'
107
+ });
108
+ process.exit(1);
109
+ return;
110
+ }
111
+ console.log(`✅ Remote tag ${trimmedName} deleted`);
112
+ remoteTagExists = false;
113
+ }
114
+ }
115
+
116
+ // Delete local tag
117
+ console.log(`Deleting local tag ${trimmedName}...`);
118
+ const localDeleted = await GitService.deleteLocalTag(trimmedName);
119
+ if (!localDeleted) {
120
+ console.error('❌ Failed to delete local tag');
121
+ await LogService.append({
122
+ command: 'tag',
123
+ args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
124
+ status: 'failure',
125
+ details: 'local tag deletion failed'
126
+ });
127
+ process.exit(1);
128
+ return;
129
+ }
130
+ console.log(`✅ Local tag ${trimmedName} deleted`);
131
+ wasTagReplaced = true;
132
+ }
133
+
70
134
  let tagMessage = options.message?.trim();
71
135
 
72
136
  if (!tagMessage) {
@@ -168,20 +232,55 @@ export class TagCommand {
168
232
  const shouldPush = await this.confirmTagPush(trimmedName);
169
233
 
170
234
  if (shouldPush) {
171
- console.log(`Pushing tag ${trimmedName} to remote...`);
172
- const pushSuccess = await GitService.pushTag(trimmedName);
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
+ }
173
252
 
174
- if (pushSuccess) {
175
- console.log(`✅ Tag ${trimmedName} pushed successfully!`);
253
+ console.log(`Force pushing tag ${trimmedName} to remote...`);
254
+ const pushSuccess = await GitService.forcePushTag(trimmedName);
255
+
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
+ }
176
268
  } else {
177
- console.error('❌ Failed to push tag to remote');
178
- await LogService.append({
179
- command: 'tag',
180
- args: { name: trimmedName, ...options, apiKey: options.apiKey ? '***' : undefined },
181
- status: 'failure',
182
- details: 'tag push failed'
183
- });
184
- process.exit(1);
269
+ console.log(`Pushing tag ${trimmedName} to remote...`);
270
+ const pushSuccess = await GitService.pushTag(trimmedName);
271
+
272
+ if (pushSuccess) {
273
+ console.log(`✅ Tag ${trimmedName} pushed successfully!`);
274
+ } 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);
283
+ }
185
284
  }
186
285
  }
187
286
 
@@ -224,6 +323,54 @@ export class TagCommand {
224
323
  return normalized === 'y' || normalized === 'yes';
225
324
  }
226
325
 
326
+ private async confirmTagDelete(tagName: string): Promise<boolean> {
327
+ const rl = readline.createInterface({
328
+ input: process.stdin,
329
+ output: process.stdout
330
+ });
331
+
332
+ const answer: string = await new Promise(resolve => {
333
+ rl.question(`Delete existing tag ${tagName} and create a new one? (y/n): `, resolve);
334
+ });
335
+
336
+ rl.close();
337
+
338
+ const normalized = answer.trim().toLowerCase();
339
+ return normalized === 'y' || normalized === 'yes';
340
+ }
341
+
342
+ private async confirmRemoteTagDelete(tagName: string): Promise<boolean> {
343
+ const rl = readline.createInterface({
344
+ input: process.stdin,
345
+ output: process.stdout
346
+ });
347
+
348
+ const answer: string = await new Promise(resolve => {
349
+ rl.question(`Also delete remote tag ${tagName}? (y/n): `, resolve);
350
+ });
351
+
352
+ rl.close();
353
+
354
+ const normalized = answer.trim().toLowerCase();
355
+ return normalized === 'y' || normalized === 'yes';
356
+ }
357
+
358
+ private async confirmForcePush(tagName: string): Promise<boolean> {
359
+ const rl = readline.createInterface({
360
+ input: process.stdin,
361
+ output: process.stdout
362
+ });
363
+
364
+ const answer: string = await new Promise(resolve => {
365
+ rl.question(`Force push tag ${tagName} to remote? (y/n): `, resolve);
366
+ });
367
+
368
+ rl.close();
369
+
370
+ const normalized = answer.trim().toLowerCase();
371
+ return normalized === 'y' || normalized === 'yes';
372
+ }
373
+
227
374
  getCommand(): Command {
228
375
  return this.program;
229
376
  }
@@ -46,7 +46,9 @@ ${customInstructions}
46
46
  ## CRITICAL: Commit Message Output Rules
47
47
  - DO NOT include any memory bank status indicators like "[Memory Bank: Active]" or "[Memory Bank: Missing]"
48
48
  - DO NOT include any task-specific formatting or artifacts from other rules
49
+ - DO NOT use any Markdown styling (no **bold**, __underline__, \`code\`, links, or emojis) in the commit header, body, or footer
49
50
  - ONLY Generate a clean conventional commit message as specified below
51
+ - Output exactly ONE conventional commit message. Choose a single primary type; never return multiple headers or multiple type prefixes.
50
52
 
51
53
  ${gitContext}
52
54
 
@@ -92,7 +94,7 @@ ${footerGuidelines}
92
94
 
93
95
  ## Analysis Instructions
94
96
  When analyzing staged changes:
95
- 1. Determine Primary Type based on the nature of changes
97
+ 1. Determine a single Primary Type based on the dominant nature of the changes (if multiple types apply, pick the most impactful one and stick to it)
96
98
  2. Identify Scope from modified directories or modules
97
99
  3. Craft Description focusing on the most significant change
98
100
  4. Determine if there are Breaking Changes