orcommit 1.0.3 → 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.
Files changed (45) hide show
  1. package/README.md +226 -59
  2. package/dist/cli.d.ts +4 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +36 -0
  5. package/dist/cli.js.map +1 -1
  6. package/dist/data/thinking-phrases.d.ts +11 -0
  7. package/dist/data/thinking-phrases.d.ts.map +1 -0
  8. package/dist/data/thinking-phrases.js +146 -0
  9. package/dist/data/thinking-phrases.js.map +1 -0
  10. package/dist/modules/api.d.ts.map +1 -1
  11. package/dist/modules/api.js +2 -9
  12. package/dist/modules/api.js.map +1 -1
  13. package/dist/modules/core.d.ts +11 -1
  14. package/dist/modules/core.d.ts.map +1 -1
  15. package/dist/modules/core.js +391 -124
  16. package/dist/modules/core.js.map +1 -1
  17. package/dist/modules/diff-filter.d.ts.map +1 -1
  18. package/dist/modules/diff-filter.js +83 -21
  19. package/dist/modules/diff-filter.js.map +1 -1
  20. package/dist/modules/git.d.ts +17 -1
  21. package/dist/modules/git.d.ts.map +1 -1
  22. package/dist/modules/git.js +209 -13
  23. package/dist/modules/git.js.map +1 -1
  24. package/dist/modules/logger.d.ts +12 -0
  25. package/dist/modules/logger.d.ts.map +1 -1
  26. package/dist/modules/logger.js +40 -3
  27. package/dist/modules/logger.js.map +1 -1
  28. package/dist/modules/promo.d.ts +9 -0
  29. package/dist/modules/promo.d.ts.map +1 -0
  30. package/dist/modules/promo.js +66 -0
  31. package/dist/modules/promo.js.map +1 -0
  32. package/dist/modules/spinner.d.ts +90 -0
  33. package/dist/modules/spinner.d.ts.map +1 -0
  34. package/dist/modules/spinner.js +205 -0
  35. package/dist/modules/spinner.js.map +1 -0
  36. package/dist/types/index.d.ts +16 -0
  37. package/dist/types/index.d.ts.map +1 -1
  38. package/dist/types/index.js +10 -0
  39. package/dist/types/index.js.map +1 -1
  40. package/dist/utils/formatting.d.ts +57 -0
  41. package/dist/utils/formatting.d.ts.map +1 -0
  42. package/dist/utils/formatting.js +94 -0
  43. package/dist/utils/formatting.js.map +1 -0
  44. package/package.json +42 -5
  45. package/preview.png +0 -0
@@ -6,8 +6,11 @@ import { logger } from './logger.js';
6
6
  import { tokenManager } from './tokenizer.js';
7
7
  import { cacheManager } from './cache.js';
8
8
  import { diffFilter } from './diff-filter.js';
9
- import { confirm, isCancel } from '@clack/prompts';
9
+ import { maybeShowPromo } from './promo.js';
10
+ import { confirm, isCancel, text } from '@clack/prompts';
10
11
  import chalk from 'chalk';
12
+ import { wrapInstructions, wrapRules, wrapContext, wrapUserFeedback, wrapDiffContent, wrapInBlock, cleanText, parseAIResponse } from '../utils/formatting.js';
13
+ import { createAIThinkingSpinner, createProcessingSpinner } from './spinner.js';
11
14
  export class CoreOrchestrator {
12
15
  config;
13
16
  /**
@@ -43,9 +46,13 @@ export class CoreOrchestrator {
43
46
  await cacheManager.clear();
44
47
  cacheProgress.succeed('Cache cleared');
45
48
  }
46
- // Phase 1: Get staged changes
47
- console.log(chalk.blue('\nšŸ” Analyzing changes...'));
48
- const analyzeProgress = contextualLogger.startProgress('Reading staged changes');
49
+ // Phase 1: Get staged changes (with safety check)
50
+ const analyzeProgress = contextualLogger.startProgress('Analyzing changes');
51
+ // Quick safety check first
52
+ const safetyAnalysis = await gitManager.analyzeStagedFilesSafety();
53
+ analyzeProgress.update('Checking file safety');
54
+ await this.handleSafetyCheck(safetyAnalysis, options, contextualLogger, analyzeProgress);
55
+ analyzeProgress.update('Reading staged changes');
49
56
  const rawDiff = await gitManager.getStagedDiff({
50
57
  maxChunkSize: CHUNK_LIMITS.MAX_CHUNK_SIZE,
51
58
  preserveContext: true,
@@ -57,6 +64,13 @@ export class CoreOrchestrator {
57
64
  return;
58
65
  }
59
66
  analyzeProgress.succeed(`Found ${rawDiff.files.length} staged files`);
67
+ // Show repository statistics
68
+ const totalSize = (rawDiff.totalSize / 1024).toFixed(1);
69
+ contextualLogger.repoStats({
70
+ files: rawDiff.files.length,
71
+ lines: rawDiff.totalLines,
72
+ size: `${totalSize} KB`
73
+ });
60
74
  // Phase 2: Filter and process
61
75
  const filterProgress = contextualLogger.startProgress('Processing and filtering changes');
62
76
  let diff = diffFilter.filterDiff(rawDiff, {
@@ -83,65 +97,86 @@ export class CoreOrchestrator {
83
97
  if (filterSummary.filesRemoved > 0) {
84
98
  contextualLogger.debug(`Filtered out ${filterSummary.filesRemoved} irrelevant files`);
85
99
  }
86
- // Phase 3: Generate commit message
87
- console.log(chalk.blue('\nšŸ¤– Generating commit message...'));
100
+ // Phase 3: Generate commit message
88
101
  const provider = options.provider || this.config.preferences.defaultProvider;
89
- contextualLogger.debug(`Using ${provider} provider`);
90
102
  // Initialize API client
91
103
  apiManager.initializeProvider(provider, this.config);
92
- // Generate commit message
93
- const commitMessage = await this.generateCommitMessage(diff, options, provider);
94
- if (options.dryRun) {
95
- console.log(chalk.blue('\nšŸ“ Generated commit message (dry run):'));
96
- console.log(chalk.gray('——————————————————'));
97
- console.log(commitMessage);
98
- console.log(chalk.gray('——————————————————\n'));
99
- return;
104
+ // Generate commit message with regeneration loop
105
+ let commitMessage;
106
+ let codeAssessment = null;
107
+ let userFeedback;
108
+ let regenerationAttempt = 0;
109
+ const maxRegenerations = 5; // Prevent infinite loops
110
+ // eslint-disable-next-line no-constant-condition
111
+ while (true) {
112
+ // Generate commit message (with optional user feedback)
113
+ const result = await this.generateCommitMessage(diff, options, provider, userFeedback);
114
+ commitMessage = result.commitMessage;
115
+ codeAssessment = result.assessment;
116
+ if (options.dryRun) {
117
+ console.log(chalk.blue('\nšŸ“ Generated commit message (dry run):'));
118
+ console.log(chalk.gray('——————————————————'));
119
+ console.log(commitMessage);
120
+ console.log(chalk.gray('——————————————————\n'));
121
+ if (userFeedback) {
122
+ console.log(chalk.yellow(`Regenerated with feedback: ${userFeedback}\n`));
123
+ }
124
+ return;
125
+ }
126
+ // Confirm or regenerate
127
+ const confirmation = await this.confirmCommit(commitMessage, codeAssessment, options);
128
+ if (confirmation.action === 'confirm') {
129
+ break; // Exit loop and proceed to commit
130
+ }
131
+ else if (confirmation.action === 'cancel') {
132
+ console.log(chalk.yellow('\nāœ– Commit cancelled by user'));
133
+ return;
134
+ }
135
+ else if (confirmation.action === 'regenerate') {
136
+ regenerationAttempt++;
137
+ if (regenerationAttempt >= maxRegenerations) {
138
+ console.log(chalk.yellow(`\n⚠ Maximum regeneration attempts (${maxRegenerations}) reached`));
139
+ const forceCommit = await confirm({
140
+ message: 'Use the last generated message anyway?',
141
+ });
142
+ if (isCancel(forceCommit) || !forceCommit) {
143
+ console.log(chalk.yellow('\nāœ– Commit cancelled'));
144
+ return;
145
+ }
146
+ break;
147
+ }
148
+ userFeedback = confirmation.feedback;
149
+ // Regenerate with user feedback
150
+ continue;
151
+ }
100
152
  }
101
- // Confirm or auto-commit
102
- const shouldCommit = await this.confirmCommit(commitMessage, options);
103
- if (shouldCommit) {
104
- console.log(chalk.blue('\nšŸ’¾ Creating commit...'));
105
- const commitProgress = contextualLogger.startProgress('Committing changes');
153
+ // Create the commit
154
+ if (commitMessage) {
155
+ const commitSpinner = createProcessingSpinner('Creating commit');
156
+ commitSpinner.start();
106
157
  await gitManager.createCommit(commitMessage);
107
- commitProgress.succeed('Commit created');
108
- console.log(chalk.green('āœ“ Commit: ') + chalk.white(commitMessage));
158
+ commitSpinner.succeed('Commit created');
159
+ console.log(chalk.gray('\nšŸ’¬ Message: ') + chalk.white(commitMessage));
160
+ // Maybe show promotional message (1% chance)
161
+ maybeShowPromo();
109
162
  // Phase 4: Handle push
110
- if (options.autoPush) {
111
- console.log(chalk.blue('\nšŸš€ Auto-pushing to remote...'));
112
- await this.performPush(contextualLogger);
113
- }
114
- else if (options.push) {
115
- console.log(chalk.blue('\nšŸš€ Pushing to remote...'));
163
+ if (options.autoPush || options.push) {
116
164
  await this.performPush(contextualLogger);
117
165
  }
118
166
  else if (!options.yes && await gitManager.hasUnpushedCommits()) {
119
167
  // Ask user if they want to push (only if not in auto mode)
120
168
  try {
121
- console.log(''); // Add some space
122
169
  const shouldPush = await confirm({
123
- message: 'Do you want to push to remote?'
170
+ message: 'Push to remote?'
124
171
  });
125
- if (isCancel(shouldPush)) {
126
- console.log(chalk.yellow('ℹ Push cancelled'));
127
- }
128
- else if (shouldPush) {
129
- console.log(chalk.blue('\nšŸš€ Pushing to remote...'));
172
+ if (!isCancel(shouldPush) && shouldPush) {
130
173
  await this.performPush(contextualLogger);
131
174
  }
132
- else {
133
- console.log(chalk.gray('šŸ’” Tip: Use --push to automatically push changes in the future'));
134
- }
135
175
  }
136
176
  catch (error) {
137
- // If interactive prompts fail (e.g., in CI), skip push
138
- console.log(chalk.gray('šŸ’” Tip: Use --push to automatically push changes'));
177
+ // If interactive prompts fail (e.g., in CI), skip push silently
139
178
  }
140
179
  }
141
- else if (await gitManager.hasUnpushedCommits()) {
142
- // Just inform about unpushed commits
143
- console.log(chalk.gray('šŸ’” Tip: Use --push to automatically push changes'));
144
- }
145
180
  }
146
181
  else {
147
182
  contextualLogger.info('Commit cancelled by user');
@@ -149,7 +184,7 @@ export class CoreOrchestrator {
149
184
  }
150
185
  catch (error) {
151
186
  if (error instanceof ConfigError || error instanceof GitError || error instanceof ApiError) {
152
- contextualLogger.error(error.message, options.verbose ? error : undefined);
187
+ contextualLogger.error(error.message, error);
153
188
  }
154
189
  else {
155
190
  contextualLogger.error(`Unexpected error: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
@@ -158,22 +193,31 @@ export class CoreOrchestrator {
158
193
  }
159
194
  }
160
195
  /**
161
- * Generate commit message from diff
196
+ * Generate commit message from diff with optional user feedback
162
197
  */
163
- async generateCommitMessage(diff, options, provider) {
164
- const progress = logger.startProgress('Generating commit message...');
198
+ async generateCommitMessage(diff, options, provider, userFeedback) {
199
+ const spinner = createAIThinkingSpinner(userFeedback ? 'Regenerating commit' : 'Generating commit');
200
+ spinner.start();
165
201
  try {
166
- // Create system prompt
167
- const systemPrompt = this.createSystemPrompt(options);
202
+ // Create system prompt (with optional user feedback for regeneration)
203
+ const systemPrompt = this.createSystemPrompt(options, userFeedback);
204
+ if (userFeedback) {
205
+ logger.debug('Regenerating with user feedback', { feedbackLength: userFeedback.length });
206
+ }
168
207
  // Prepare diff content for processing
169
- const diffContent = this.prepareDiffContent(diff);
208
+ const rawDiffContent = this.prepareDiffContent(diff);
209
+ const diffContent = wrapDiffContent(rawDiffContent); // Wrap in DIFF_CONTENT block
170
210
  const model = this.getModel(provider);
171
- // Check cache first (unless disabled)
172
- if (!options.noCache) {
173
- const cachedMessage = await cacheManager.get(diffContent, model, provider, this.config.preferences.temperature);
211
+ // Check cache first (unless disabled or regenerating with feedback)
212
+ if (!options.noCache && !userFeedback) {
213
+ const cachedMessage = await cacheManager.get(rawDiffContent, // Use raw content for cache key
214
+ model, provider, this.config.preferences.temperature);
174
215
  if (cachedMessage) {
175
- progress.succeed('Commit message retrieved from cache');
176
- return cachedMessage;
216
+ spinner.succeed('Retrieved from cache');
217
+ return {
218
+ commitMessage: cachedMessage,
219
+ assessment: null // Cached messages don't have assessment
220
+ };
177
221
  }
178
222
  }
179
223
  // Get optimal chunk size for the model
@@ -198,22 +242,33 @@ export class CoreOrchestrator {
198
242
  if (!result.success || !result.data) {
199
243
  throw new ApiError(result.error?.message || 'Failed to generate commit message');
200
244
  }
201
- // Cache the result
202
- if (!options.noCache) {
203
- await cacheManager.set(diffContent, model, provider, this.config.preferences.temperature, result.data);
245
+ const rawMessage = result.data;
246
+ // Parse JSON response (with fallback to plain text)
247
+ logger.debug('Raw AI response:', { length: rawMessage.length, preview: rawMessage.substring(0, 200) });
248
+ const parsed = parseAIResponse(rawMessage);
249
+ logger.debug('Parsed response:', { hasAssessment: !!parsed.assessment, messageLength: parsed.commitMessage.length });
250
+ spinner.update('Polishing the message');
251
+ // Stage 2: Finalize and clean the commit message part (with user context)
252
+ const finalMessage = await this.finalizeCommitMessage(parsed.commitMessage, provider, options, userFeedback);
253
+ // Cache the finalized result (skip if regenerating with feedback)
254
+ if (!options.noCache && !userFeedback) {
255
+ await cacheManager.set(rawDiffContent, // Use raw content for cache key
256
+ model, provider, this.config.preferences.temperature, finalMessage);
204
257
  }
205
- progress.succeed('Commit message generated');
206
- return result.data;
258
+ spinner.succeed('Commit message generated');
259
+ return {
260
+ commitMessage: finalMessage,
261
+ assessment: parsed.assessment
262
+ };
207
263
  }
208
264
  else {
209
265
  // Multiple chunks processing with token-based splitting
210
- progress.update('Processing large diff in chunks...');
266
+ spinner.update('Processing large diff in chunks');
211
267
  const chunks = tokenManager.splitIntoTokenChunks(diffContent, {
212
268
  model,
213
269
  maxTokens: optimalChunkSize,
214
270
  reservedTokens: systemTokens,
215
271
  });
216
- logger.debug(`Split into ${chunks.length} token-based chunks`);
217
272
  const baseRequest = {
218
273
  provider,
219
274
  model,
@@ -226,77 +281,190 @@ export class CoreOrchestrator {
226
281
  throw new ApiError(result.error?.message || 'Failed to process chunks');
227
282
  }
228
283
  // Combine chunk results into a single commit message
229
- const finalMessage = this.combineChunkResults(result.data, options);
230
- progress.succeed('Commit message generated from chunks');
231
- return finalMessage;
284
+ const rawMessage = this.combineChunkResults(result.data, options);
285
+ // Parse JSON response (with fallback to plain text)
286
+ logger.debug('Raw AI response (chunks):', { length: rawMessage.length, preview: rawMessage.substring(0, 200) });
287
+ const parsed = parseAIResponse(rawMessage);
288
+ logger.debug('Parsed response (chunks):', { hasAssessment: !!parsed.assessment, messageLength: parsed.commitMessage.length });
289
+ spinner.update('Polishing the message');
290
+ // Stage 2: Finalize and clean the commit message part (with user context)
291
+ const finalMessage = await this.finalizeCommitMessage(parsed.commitMessage, provider, options, userFeedback);
292
+ spinner.succeed('Commit message generated');
293
+ return {
294
+ commitMessage: finalMessage,
295
+ assessment: parsed.assessment
296
+ };
232
297
  }
233
298
  }
234
299
  catch (error) {
235
- progress.fail('Failed to generate commit message');
300
+ spinner.fail('Failed to generate commit message');
236
301
  throw error;
237
302
  }
238
303
  }
239
304
  /**
240
305
  * Create system prompt based on options and preferences
241
306
  */
242
- createSystemPrompt(options) {
307
+ createSystemPrompt(options, userFeedback) {
308
+ // Use custom prompt from CLI option first, then from config, then default
309
+ if (options.prompt) {
310
+ return options.prompt;
311
+ }
312
+ if (this.config.preferences.customPrompt) {
313
+ return this.config.preferences.customPrompt;
314
+ }
315
+ // Build default prompt with structured blocks
243
316
  const format = this.config.preferences.commitFormat;
244
317
  const language = this.config.preferences.language;
245
- let prompt = `You are an expert Git commit message generator. Generate a concise, meaningful commit message based on the provided git diff.
318
+ const sections = [];
319
+ // Main instructions
320
+ const mainInstructions = `You are a senior software engineer and Git commit message expert with deep understanding of software architecture and code quality.
321
+
322
+ YOUR MISSION: Analyze the git diff carefully and generate a professional, comprehensive commit message that accurately captures ALL significant changes.
246
323
 
247
- Requirements:
324
+ ANALYSIS REQUIREMENTS:
325
+ 1. THINK DEEPLY about what the code changes actually do
326
+ 2. Identify the PRIMARY purpose of the changes (feature, fix, refactor, etc.)
327
+ 3. Notice ALL important modifications - don't miss secondary changes
328
+ 4. Understand the INTENT behind the changes, not just the syntax
329
+ 5. Consider the IMPACT on the codebase (breaking changes, new features, bug fixes)
330
+ 6. Recognize patterns: new files, deletions, refactoring, configuration changes`;
331
+ sections.push(wrapInstructions(mainInstructions));
332
+ // Quality standards and rules
333
+ let rules = `- Be SPECIFIC about what changed (mention key functions, components, files when relevant)
334
+ - Be ACCURATE - every word should reflect the actual changes
335
+ - Be COMPLETE - include all important changes, don't omit significant details
336
+ - Be CONCISE but INFORMATIVE - no fluff, but don't skip important info
337
+ - Use technical terminology appropriately
248
338
  - Write in ${language === 'en' ? 'English' : language}
249
- - Use ${format === 'conventional' ? 'Conventional Commits format' : 'simple descriptive format'}
250
- - Focus on the most significant changes
251
- - Be specific and actionable
339
+ - Follow ${format === 'conventional' ? 'Conventional Commits format strictly' : 'simple descriptive format'}
252
340
 
253
- `;
254
- // Formatting constraints
341
+ THINK STEP BY STEP:
342
+ 1. What is the main change? (new feature, bug fix, refactor, etc.)
343
+ 2. What files/components are affected?
344
+ 3. Are there any breaking changes?
345
+ 4. Are there secondary important changes?
346
+ 5. What's the overall impact?`;
347
+ // Add formatting constraints to rules
255
348
  if (options.oneLine) {
256
- prompt += `- Generate a single-line commit message only\n`;
349
+ rules += `\n- Generate a single-line commit message only`;
257
350
  }
258
351
  else {
259
- prompt += `- Keep subject line under 72 characters\n- Add body if needed for complex changes\n`;
352
+ rules += `\n- Keep subject line under 72 characters\n- Add body if needed for complex changes`;
260
353
  }
261
354
  if (options.descriptionLength) {
262
- prompt += `- Limit description to ${options.descriptionLength} characters\n`;
355
+ rules += `\n- Limit description to ${options.descriptionLength} characters`;
263
356
  }
264
357
  if (options.emoji) {
265
- prompt += `- Include appropriate emoji at the start of the commit message\n`;
358
+ rules += `\n- Include appropriate emoji at the start of the commit message`;
266
359
  }
267
360
  if (format === 'conventional') {
268
- prompt += `\nConventional Commits format:
269
- <type>[optional scope]: <description>
270
-
271
- Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert
272
- `;
361
+ rules += `\n\nConventional Commits format:\n<type>[optional scope]: <description>\n\nTypes: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert`;
273
362
  if (options.emoji) {
274
- prompt += `Emoji mapping:
275
- - feat: ✨
276
- - fix: šŸ›
277
- - docs: šŸ“
278
- - style: šŸ’„
279
- - refactor: ā™»ļø
280
- - test: āœ…
281
- - chore: šŸ”§
282
- - perf: ⚔
283
- - ci: šŸ‘·
284
- - build: šŸ“¦
285
- - revert: āŖ
286
- `;
363
+ rules += `\n\nEmoji mapping:\n- feat: ✨\n- fix: šŸ›\n- docs: šŸ“\n- style: šŸ’„\n- refactor: ā™»ļø\n- test: āœ…\n- chore: šŸ”§\n- perf: ⚔\n- ci: šŸ‘·\n- build: šŸ“¦\n- revert: āŖ`;
287
364
  }
288
365
  }
289
366
  if (options.type) {
290
- prompt += `\nRequired type: ${options.type}`;
367
+ rules += `\n\nRequired type: ${options.type}`;
291
368
  }
292
369
  if (options.scope) {
293
- prompt += `\nRequired scope: ${options.scope}`;
370
+ rules += `\nRequired scope: ${options.scope}`;
294
371
  }
295
372
  if (options.breaking) {
296
- prompt += `\nThis is a BREAKING CHANGE - include "BREAKING CHANGE:" in the commit message.`;
373
+ rules += `\n\nāš ļø CRITICAL: This is a BREAKING CHANGE - MUST include "BREAKING CHANGE:" in the commit message footer with explanation.`;
374
+ }
375
+ sections.push(wrapRules(rules));
376
+ // Add context if provided
377
+ if (options.context) {
378
+ sections.push(wrapContext(options.context));
379
+ }
380
+ // Add user feedback if provided (from regeneration request)
381
+ if (userFeedback) {
382
+ sections.push(wrapUserFeedback(userFeedback));
383
+ }
384
+ // JSON schema for response
385
+ const jsonSchema = `{
386
+ "codeAssessment": "Brief (1-2 sentences) sarcastic, darkly humorous assessment of the code changes. Be witty and technically insightful. Channel maximum developer cynicism. ${userFeedback ? 'IMPORTANT: Follow user feedback requirements for language and style!' : `Write in ${language}.`}",
387
+ "commitMessage": "Professional ${format === 'conventional' ? 'conventional commits format' : 'descriptive'} commit message. ${userFeedback ? 'CRITICAL: Follow ALL user feedback instructions!' : `Write in ${language}.`}"
388
+ }`;
389
+ sections.push(wrapInBlock('RESPONSE_SCHEMA', jsonSchema, false));
390
+ // Final generation instructions
391
+ const finalInstructions = `GENERATE YOUR RESPONSE AS A VALID JSON OBJECT:
392
+
393
+ ${userFeedback ? 'āš ļø CRITICAL: The [IMPORTANT_USER_FEEDBACK] block above contains explicit user requirements. Follow ALL instructions from the user feedback - they override ALL other rules (including language, format, style, etc.).\n' : ''}
394
+ 1. CODE ASSESSMENT - Provide a brief (1-2 sentences), brutally honest, darkly humorous take on the code changes
395
+ Examples: "Someone discovered copy-paste today", "WIP commits everywhere, as expected", "Finally fixing that TODO from 2019"
396
+
397
+ 2. COMMIT MESSAGE - Generate a professional commit message:
398
+ - ${format === 'conventional' ? 'Use conventional commits format: type(scope): description' : 'Use clear descriptive format'}
399
+ - Subject line under 72 characters
400
+ - Add detailed body if changes are complex
401
+ - Include BREAKING CHANGE footer if applicable
402
+
403
+ CRITICAL: Return ONLY a valid JSON object matching the RESPONSE_SCHEMA above. No markdown, no explanations, no code blocks - just pure JSON.
404
+
405
+ Example output:
406
+ {
407
+ "codeAssessment": "Ah yes, another 'quick fix' that touches 47 files",
408
+ "commitMessage": "refactor: restructure authentication flow\\n\\nMigrate from session-based to JWT authentication"
409
+ }`;
410
+ sections.push(wrapInstructions(finalInstructions));
411
+ return sections.join('\n\n');
412
+ }
413
+ /**
414
+ * Finalize and clean up the commit message (Stage 2)
415
+ * Takes the raw AI-generated message and ensures it's perfectly formatted
416
+ */
417
+ async finalizeCommitMessage(rawMessage, provider, options, userFeedback) {
418
+ const format = this.config.preferences.commitFormat;
419
+ // Create finalization prompt with structured blocks
420
+ const instructions = `You are a commit message quality control expert.
421
+
422
+ YOUR TASK: Clean and perfect the commit message below. Remove ANY explanatory text, prefixes, or formatting artifacts.
423
+ ${userFeedback ? '\nāš ļø CRITICAL: User provided feedback. You MUST preserve the language and style they requested!' : ''}`;
424
+ const rules = `1. Output ONLY the final commit message - nothing else
425
+ 2. Remove prefixes like "commit message:", "here is", "this is", etc.
426
+ 3. Remove surrounding quotes, backticks, or markdown
427
+ 4. Remove any explanations or comments
428
+ 5. Keep the message structure intact (subject + body + footer if present)
429
+ 6. Start directly with the commit type or message
430
+ 7. Preserve ${format === 'conventional' ? 'conventional commits format (type(scope): description)' : 'simple format'}
431
+ 8. Preserve line breaks for multi-line messages
432
+ 9. Ensure subject line is under 72 characters
433
+ 10. NO additional text, NO commentary, NO explanations
434
+ ${userFeedback ? `11. CRITICAL: Preserve the EXACT language used in the message below (user requested specific changes)` : ''}`;
435
+ const finalizationPrompt = `${wrapInstructions(instructions)}
436
+
437
+ ${wrapRules(rules)}
438
+
439
+ [RAW_MESSAGE_TO_CLEAN]
440
+ ${cleanText(rawMessage)}
441
+ [/RAW_MESSAGE_TO_CLEAN]
442
+
443
+ OUTPUT ONLY THE CLEANED COMMIT MESSAGE:`;
444
+ try {
445
+ const model = this.getModel(provider);
446
+ const result = await apiManager.generateCommitMessage({
447
+ provider,
448
+ model,
449
+ maxTokens: this.config.preferences.maxTokens,
450
+ temperature: 0.3, // Lower temperature for more consistent cleaning
451
+ messages: [
452
+ { role: 'system', content: finalizationPrompt },
453
+ { role: 'user', content: 'Clean this message now.' }
454
+ ],
455
+ }, provider);
456
+ if (!result.success || !result.data) {
457
+ // If finalization fails, return original message
458
+ logger.warn('Finalization failed, using original message');
459
+ return rawMessage;
460
+ }
461
+ return result.data.trim();
462
+ }
463
+ catch (error) {
464
+ // If finalization fails, return original message
465
+ logger.warn('Finalization error, using original message');
466
+ return rawMessage;
297
467
  }
298
- prompt += `\nGenerate only the commit message, no additional text or explanation.`;
299
- return prompt;
300
468
  }
301
469
  /**
302
470
  * Prepare diff content for API consumption
@@ -383,26 +551,71 @@ Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert
383
551
  }
384
552
  /**
385
553
  * Confirm commit with user (unless auto-confirm is enabled)
554
+ * Returns: 'confirm' | 'regenerate' | 'cancel'
386
555
  */
387
- async confirmCommit(message, options) {
556
+ async confirmCommit(message, assessment, options) {
388
557
  if (options.yes || this.config.preferences.autoConfirm) {
389
- return true;
558
+ return { action: 'confirm' };
390
559
  }
391
- // For now, we'll implement a simple console confirmation
392
- // In a full implementation, you might want to use a library like inquirer
393
- console.log(`\nProposed commit message:\n${message}\n`);
394
- // Simple readline implementation for confirmation
395
- const readline = await import('readline');
396
- const rl = readline.createInterface({
397
- input: process.stdin,
398
- output: process.stdout,
399
- });
400
- return new Promise((resolve) => {
401
- rl.question('Create this commit? (y/N): ', (answer) => {
402
- rl.close();
403
- resolve(answer.toLowerCase().startsWith('y'));
560
+ // Show sarcastic code assessment if available
561
+ if (assessment) {
562
+ console.log(chalk.dim('\nšŸ’­ AI thinks: ') + chalk.yellow.italic(assessment));
563
+ }
564
+ console.log(chalk.cyan('\nšŸ“ Generated commit message:'));
565
+ console.log(chalk.gray('——————————————————'));
566
+ console.log(chalk.white(message));
567
+ console.log(chalk.gray('——————————————————\n'));
568
+ try {
569
+ const action = await confirm({
570
+ message: 'Accept this commit message?',
571
+ initialValue: true,
404
572
  });
405
- });
573
+ if (isCancel(action)) {
574
+ return { action: 'cancel' };
575
+ }
576
+ if (action) {
577
+ return { action: 'confirm' };
578
+ }
579
+ // User rejected - ask if they want to regenerate with feedback
580
+ console.log('');
581
+ const shouldRegenerate = await confirm({
582
+ message: 'Would you like to regenerate with additional instructions?',
583
+ initialValue: true,
584
+ });
585
+ if (isCancel(shouldRegenerate) || !shouldRegenerate) {
586
+ return { action: 'cancel' };
587
+ }
588
+ // Get user feedback for regeneration
589
+ const feedback = await text({
590
+ message: 'What should be changed or improved?',
591
+ placeholder: 'e.g., "Be more specific about the bug fix" or "Mention the new API endpoint"',
592
+ validate: (value) => {
593
+ if (!value || value.trim().length < 3) {
594
+ return 'Please provide at least 3 characters of feedback';
595
+ }
596
+ return undefined; // Valid input
597
+ },
598
+ });
599
+ if (isCancel(feedback)) {
600
+ return { action: 'cancel' };
601
+ }
602
+ return { action: 'regenerate', feedback: feedback };
603
+ }
604
+ catch (error) {
605
+ // Fallback to simple confirmation if prompts fail
606
+ console.log(chalk.yellow('⚠ Interactive prompts unavailable, using simple mode'));
607
+ const readline = await import('readline');
608
+ const rl = readline.createInterface({
609
+ input: process.stdin,
610
+ output: process.stdout,
611
+ });
612
+ return new Promise((resolve) => {
613
+ rl.question('Accept this commit? (y/N): ', (answer) => {
614
+ rl.close();
615
+ resolve({ action: answer.toLowerCase().startsWith('y') ? 'confirm' : 'cancel' });
616
+ });
617
+ });
618
+ }
406
619
  }
407
620
  /**
408
621
  * Validate environment before processing
@@ -438,20 +651,74 @@ Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert
438
651
  const pushMessage = hasUpstream
439
652
  ? `Pushing to ${currentBranch}`
440
653
  : `Setting upstream and pushing to ${currentBranch}`;
441
- const pushProgress = contextualLogger.startProgress(pushMessage);
654
+ const pushSpinner = createProcessingSpinner(pushMessage);
655
+ pushSpinner.start();
442
656
  try {
443
657
  await gitManager.pushToRemote(true); // Set upstream if needed
444
658
  const successMessage = hasUpstream
445
659
  ? `Pushed to ${currentBranch}`
446
660
  : `Upstream set and pushed to ${currentBranch}`;
447
- pushProgress.succeed(successMessage);
448
- console.log(chalk.green('āœ“ Changes pushed successfully'));
661
+ pushSpinner.succeed(successMessage);
449
662
  }
450
663
  catch (error) {
451
- pushProgress.fail(`Push failed`);
452
- console.log(chalk.red(`āœ— Error: ${error.message}`));
453
- console.log(chalk.gray('šŸ’” You can try pushing manually: git push'));
664
+ pushSpinner.fail(`Push failed: ${error.message}`);
665
+ }
666
+ }
667
+ /**
668
+ * Handle safety check for staged files
669
+ */
670
+ async handleSafetyCheck(analysis, options, contextualLogger, progress) {
671
+ const { riskLevel, totalFiles, recommendations } = analysis;
672
+ // Only show messages for non-safe commits
673
+ if (riskLevel === 'safe') {
674
+ return; // Silent for safe commits
675
+ }
676
+ // Handle dangerous commits - block immediately
677
+ if (riskLevel === 'dangerous') {
678
+ if (options.yes) {
679
+ return; // Silent proceed with --yes
680
+ }
681
+ // Stop progress before showing error
682
+ if (progress) {
683
+ progress.fail(`Dangerous commit detected (${totalFiles} files)`);
684
+ }
685
+ // Show only the most important recommendations
686
+ const criticalRecs = recommendations.filter(rec => rec.includes('node_modules') || rec.includes('vendor') || rec.includes('STOP'));
687
+ if (criticalRecs.length > 0) {
688
+ criticalRecs.slice(0, 2).forEach(rec => {
689
+ console.log(chalk.yellow(` ${rec}`));
690
+ });
691
+ }
692
+ console.log(chalk.gray('Use --yes to override or fix staging area first.\n'));
693
+ throw new GitError('Dangerous commit blocked for safety');
694
+ }
695
+ // Handle critical commits - ask for confirmation
696
+ if (riskLevel === 'critical') {
697
+ if (options.yes) {
698
+ return; // Silent proceed with --yes
699
+ }
700
+ // Stop progress before showing dialog
701
+ if (progress) {
702
+ progress.stop();
703
+ }
704
+ try {
705
+ const { confirm, isCancel } = await import('@clack/prompts');
706
+ const shouldProceed = await confirm({
707
+ message: `Large commit detected (${totalFiles} files). Continue?`,
708
+ initialValue: false,
709
+ });
710
+ if (isCancel(shouldProceed) || !shouldProceed) {
711
+ throw new GitError('Large commit cancelled by user');
712
+ }
713
+ }
714
+ catch (error) {
715
+ if (error instanceof GitError) {
716
+ throw error;
717
+ }
718
+ // If interactive prompts fail, proceed silently
719
+ }
454
720
  }
721
+ // For warnings, proceed silently
455
722
  }
456
723
  /**
457
724
  * Determine if we should push changes