orcommit 1.0.4 ā 1.1.1
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.
- package/README.md +226 -59
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +31 -0
- package/dist/cli.js.map +1 -1
- package/dist/data/thinking-phrases.d.ts +11 -0
- package/dist/data/thinking-phrases.d.ts.map +1 -0
- package/dist/data/thinking-phrases.js +146 -0
- package/dist/data/thinking-phrases.js.map +1 -0
- package/dist/modules/api.d.ts.map +1 -1
- package/dist/modules/api.js +2 -9
- package/dist/modules/api.js.map +1 -1
- package/dist/modules/core.d.ts +7 -1
- package/dist/modules/core.d.ts.map +1 -1
- package/dist/modules/core.js +326 -120
- package/dist/modules/core.js.map +1 -1
- package/dist/modules/diff-filter.d.ts.map +1 -1
- package/dist/modules/diff-filter.js +83 -21
- package/dist/modules/diff-filter.js.map +1 -1
- package/dist/modules/git.d.ts +9 -0
- package/dist/modules/git.d.ts.map +1 -1
- package/dist/modules/git.js +40 -0
- package/dist/modules/git.js.map +1 -1
- package/dist/modules/promo.d.ts +9 -0
- package/dist/modules/promo.d.ts.map +1 -0
- package/dist/modules/promo.js +66 -0
- package/dist/modules/promo.js.map +1 -0
- package/dist/modules/spinner.d.ts +90 -0
- package/dist/modules/spinner.d.ts.map +1 -0
- package/dist/modules/spinner.js +205 -0
- package/dist/modules/spinner.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/formatting.d.ts +58 -0
- package/dist/utils/formatting.d.ts.map +1 -0
- package/dist/utils/formatting.js +120 -0
- package/dist/utils/formatting.js.map +1 -0
- package/package.json +42 -5
- package/preview.png +0 -0
package/dist/modules/core.js
CHANGED
|
@@ -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 {
|
|
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
|
/**
|
|
@@ -94,65 +97,86 @@ export class CoreOrchestrator {
|
|
|
94
97
|
if (filterSummary.filesRemoved > 0) {
|
|
95
98
|
contextualLogger.debug(`Filtered out ${filterSummary.filesRemoved} irrelevant files`);
|
|
96
99
|
}
|
|
97
|
-
// Phase 3: Generate commit message
|
|
98
|
-
console.log(chalk.blue('\nš¤ Generating commit message...'));
|
|
100
|
+
// Phase 3: Generate commit message
|
|
99
101
|
const provider = options.provider || this.config.preferences.defaultProvider;
|
|
100
|
-
contextualLogger.debug(`Using ${provider} provider`);
|
|
101
102
|
// Initialize API client
|
|
102
103
|
apiManager.initializeProvider(provider, this.config);
|
|
103
|
-
// Generate commit message
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
+
}
|
|
111
152
|
}
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const commitProgress = contextualLogger.startProgress('Committing changes');
|
|
153
|
+
// Create the commit
|
|
154
|
+
if (commitMessage) {
|
|
155
|
+
const commitSpinner = createProcessingSpinner('Creating commit');
|
|
156
|
+
commitSpinner.start();
|
|
117
157
|
await gitManager.createCommit(commitMessage);
|
|
118
|
-
|
|
119
|
-
console.log(chalk.
|
|
158
|
+
commitSpinner.succeed('Commit created');
|
|
159
|
+
console.log(chalk.gray('\nš¬ Message: ') + chalk.white(commitMessage));
|
|
160
|
+
// Maybe show promotional message (1% chance)
|
|
161
|
+
maybeShowPromo();
|
|
120
162
|
// Phase 4: Handle push
|
|
121
|
-
if (options.autoPush) {
|
|
122
|
-
console.log(chalk.blue('\nš Auto-pushing to remote...'));
|
|
123
|
-
await this.performPush(contextualLogger);
|
|
124
|
-
}
|
|
125
|
-
else if (options.push) {
|
|
126
|
-
console.log(chalk.blue('\nš Pushing to remote...'));
|
|
163
|
+
if (options.autoPush || options.push) {
|
|
127
164
|
await this.performPush(contextualLogger);
|
|
128
165
|
}
|
|
129
166
|
else if (!options.yes && await gitManager.hasUnpushedCommits()) {
|
|
130
167
|
// Ask user if they want to push (only if not in auto mode)
|
|
131
168
|
try {
|
|
132
|
-
console.log(''); // Add some space
|
|
133
169
|
const shouldPush = await confirm({
|
|
134
|
-
message: '
|
|
170
|
+
message: 'Push to remote?'
|
|
135
171
|
});
|
|
136
|
-
if (isCancel(shouldPush)) {
|
|
137
|
-
console.log(chalk.yellow('ā¹ Push cancelled'));
|
|
138
|
-
}
|
|
139
|
-
else if (shouldPush) {
|
|
140
|
-
console.log(chalk.blue('\nš Pushing to remote...'));
|
|
172
|
+
if (!isCancel(shouldPush) && shouldPush) {
|
|
141
173
|
await this.performPush(contextualLogger);
|
|
142
174
|
}
|
|
143
|
-
else {
|
|
144
|
-
console.log(chalk.gray('š” Tip: Use --push to automatically push changes in the future'));
|
|
145
|
-
}
|
|
146
175
|
}
|
|
147
176
|
catch (error) {
|
|
148
|
-
// If interactive prompts fail (e.g., in CI), skip push
|
|
149
|
-
console.log(chalk.gray('š” Tip: Use --push to automatically push changes'));
|
|
177
|
+
// If interactive prompts fail (e.g., in CI), skip push silently
|
|
150
178
|
}
|
|
151
179
|
}
|
|
152
|
-
else if (await gitManager.hasUnpushedCommits()) {
|
|
153
|
-
// Just inform about unpushed commits
|
|
154
|
-
console.log(chalk.gray('š” Tip: Use --push to automatically push changes'));
|
|
155
|
-
}
|
|
156
180
|
}
|
|
157
181
|
else {
|
|
158
182
|
contextualLogger.info('Commit cancelled by user');
|
|
@@ -169,22 +193,31 @@ export class CoreOrchestrator {
|
|
|
169
193
|
}
|
|
170
194
|
}
|
|
171
195
|
/**
|
|
172
|
-
* Generate commit message from diff
|
|
196
|
+
* Generate commit message from diff with optional user feedback
|
|
173
197
|
*/
|
|
174
|
-
async generateCommitMessage(diff, options, provider) {
|
|
175
|
-
const
|
|
198
|
+
async generateCommitMessage(diff, options, provider, userFeedback) {
|
|
199
|
+
const spinner = createAIThinkingSpinner(userFeedback ? 'Regenerating commit' : 'Generating commit');
|
|
200
|
+
spinner.start();
|
|
176
201
|
try {
|
|
177
|
-
// Create system prompt
|
|
178
|
-
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
|
+
}
|
|
179
207
|
// Prepare diff content for processing
|
|
180
|
-
const
|
|
208
|
+
const rawDiffContent = this.prepareDiffContent(diff);
|
|
209
|
+
const diffContent = wrapDiffContent(rawDiffContent); // Wrap in DIFF_CONTENT block
|
|
181
210
|
const model = this.getModel(provider);
|
|
182
|
-
// Check cache first (unless disabled)
|
|
183
|
-
if (!options.noCache) {
|
|
184
|
-
const cachedMessage = await cacheManager.get(
|
|
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);
|
|
185
215
|
if (cachedMessage) {
|
|
186
|
-
|
|
187
|
-
return
|
|
216
|
+
spinner.succeed('Retrieved from cache');
|
|
217
|
+
return {
|
|
218
|
+
commitMessage: cachedMessage,
|
|
219
|
+
assessment: null // Cached messages don't have assessment
|
|
220
|
+
};
|
|
188
221
|
}
|
|
189
222
|
}
|
|
190
223
|
// Get optimal chunk size for the model
|
|
@@ -209,22 +242,33 @@ export class CoreOrchestrator {
|
|
|
209
242
|
if (!result.success || !result.data) {
|
|
210
243
|
throw new ApiError(result.error?.message || 'Failed to generate commit message');
|
|
211
244
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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);
|
|
215
257
|
}
|
|
216
|
-
|
|
217
|
-
return
|
|
258
|
+
spinner.succeed('Commit message generated');
|
|
259
|
+
return {
|
|
260
|
+
commitMessage: finalMessage,
|
|
261
|
+
assessment: parsed.assessment
|
|
262
|
+
};
|
|
218
263
|
}
|
|
219
264
|
else {
|
|
220
265
|
// Multiple chunks processing with token-based splitting
|
|
221
|
-
|
|
266
|
+
spinner.update('Processing large diff in chunks');
|
|
222
267
|
const chunks = tokenManager.splitIntoTokenChunks(diffContent, {
|
|
223
268
|
model,
|
|
224
269
|
maxTokens: optimalChunkSize,
|
|
225
270
|
reservedTokens: systemTokens,
|
|
226
271
|
});
|
|
227
|
-
logger.debug(`Split into ${chunks.length} token-based chunks`);
|
|
228
272
|
const baseRequest = {
|
|
229
273
|
provider,
|
|
230
274
|
model,
|
|
@@ -237,77 +281,196 @@ export class CoreOrchestrator {
|
|
|
237
281
|
throw new ApiError(result.error?.message || 'Failed to process chunks');
|
|
238
282
|
}
|
|
239
283
|
// Combine chunk results into a single commit message
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
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
|
+
};
|
|
243
297
|
}
|
|
244
298
|
}
|
|
245
299
|
catch (error) {
|
|
246
|
-
|
|
300
|
+
spinner.fail('Failed to generate commit message');
|
|
247
301
|
throw error;
|
|
248
302
|
}
|
|
249
303
|
}
|
|
250
304
|
/**
|
|
251
305
|
* Create system prompt based on options and preferences
|
|
252
306
|
*/
|
|
253
|
-
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
|
|
254
316
|
const format = this.config.preferences.commitFormat;
|
|
255
317
|
const language = this.config.preferences.language;
|
|
256
|
-
|
|
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.
|
|
257
323
|
|
|
258
|
-
|
|
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
|
|
259
338
|
- Write in ${language === 'en' ? 'English' : language}
|
|
260
|
-
-
|
|
261
|
-
- Focus on the most significant changes
|
|
262
|
-
- Be specific and actionable
|
|
339
|
+
- Follow ${format === 'conventional' ? 'Conventional Commits format strictly' : 'simple descriptive format'}
|
|
263
340
|
|
|
264
|
-
|
|
265
|
-
|
|
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
|
|
266
348
|
if (options.oneLine) {
|
|
267
|
-
|
|
349
|
+
rules += `\n- Generate a single-line commit message only`;
|
|
268
350
|
}
|
|
269
351
|
else {
|
|
270
|
-
|
|
352
|
+
rules += `\n- Keep subject line under 72 characters\n- Add body if needed for complex changes`;
|
|
271
353
|
}
|
|
272
354
|
if (options.descriptionLength) {
|
|
273
|
-
|
|
355
|
+
rules += `\n- Limit description to ${options.descriptionLength} characters`;
|
|
274
356
|
}
|
|
275
357
|
if (options.emoji) {
|
|
276
|
-
|
|
358
|
+
rules += `\n- Include appropriate emoji at the start of the commit message`;
|
|
277
359
|
}
|
|
278
360
|
if (format === 'conventional') {
|
|
279
|
-
|
|
280
|
-
<type>[optional scope]: <description>
|
|
281
|
-
|
|
282
|
-
Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert
|
|
283
|
-
`;
|
|
361
|
+
rules += `\n\nConventional Commits format:\n<type>[optional scope]: <description>\n\nTypes: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert`;
|
|
284
362
|
if (options.emoji) {
|
|
285
|
-
|
|
286
|
-
- feat: āØ
|
|
287
|
-
- fix: š
|
|
288
|
-
- docs: š
|
|
289
|
-
- style: š
|
|
290
|
-
- refactor: ā»ļø
|
|
291
|
-
- test: ā
|
|
292
|
-
- chore: š§
|
|
293
|
-
- perf: ā”
|
|
294
|
-
- ci: š·
|
|
295
|
-
- build: š¦
|
|
296
|
-
- revert: āŖ
|
|
297
|
-
`;
|
|
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: āŖ`;
|
|
298
364
|
}
|
|
299
365
|
}
|
|
300
366
|
if (options.type) {
|
|
301
|
-
|
|
367
|
+
rules += `\n\nRequired type: ${options.type}`;
|
|
302
368
|
}
|
|
303
369
|
if (options.scope) {
|
|
304
|
-
|
|
370
|
+
rules += `\nRequired scope: ${options.scope}`;
|
|
305
371
|
}
|
|
306
372
|
if (options.breaking) {
|
|
307
|
-
|
|
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.
|
|
404
|
+
ā ļø DO NOT wrap in markdown code blocks (no \`\`\`json)
|
|
405
|
+
ā ļø DO NOT add explanations before or after
|
|
406
|
+
ā ļø START your response with { and END with }
|
|
407
|
+
ā ļø This is REQUIRED - the response MUST be parseable JSON
|
|
408
|
+
|
|
409
|
+
CORRECT Example:
|
|
410
|
+
{"codeAssessment": "Ah yes, another 'quick fix' that touches 47 files", "commitMessage": "refactor: restructure authentication flow\\n\\nMigrate from session-based to JWT authentication"}
|
|
411
|
+
|
|
412
|
+
WRONG Examples:
|
|
413
|
+
- \`\`\`json {"codeAssessment": "..."} \`\`\` ā
|
|
414
|
+
- Here is the JSON: {...} ā
|
|
415
|
+
- Just plain text without JSON ā`;
|
|
416
|
+
sections.push(wrapInstructions(finalInstructions));
|
|
417
|
+
return sections.join('\n\n');
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Finalize and clean up the commit message (Stage 2)
|
|
421
|
+
* Takes the raw AI-generated message and ensures it's perfectly formatted
|
|
422
|
+
*/
|
|
423
|
+
async finalizeCommitMessage(rawMessage, provider, options, userFeedback) {
|
|
424
|
+
const format = this.config.preferences.commitFormat;
|
|
425
|
+
// Create finalization prompt with structured blocks
|
|
426
|
+
const instructions = `You are a commit message quality control expert.
|
|
427
|
+
|
|
428
|
+
YOUR TASK: Clean and perfect the commit message below. Remove ANY explanatory text, prefixes, or formatting artifacts.
|
|
429
|
+
${userFeedback ? '\nā ļø CRITICAL: User provided feedback. You MUST preserve the language and style they requested!' : ''}`;
|
|
430
|
+
const rules = `1. Output ONLY the final commit message - nothing else
|
|
431
|
+
2. Remove prefixes like "commit message:", "here is", "this is", etc.
|
|
432
|
+
3. Remove surrounding quotes, backticks, or markdown
|
|
433
|
+
4. Remove any explanations or comments
|
|
434
|
+
5. Keep the message structure intact (subject + body + footer if present)
|
|
435
|
+
6. Start directly with the commit type or message
|
|
436
|
+
7. Preserve ${format === 'conventional' ? 'conventional commits format (type(scope): description)' : 'simple format'}
|
|
437
|
+
8. Preserve line breaks for multi-line messages
|
|
438
|
+
9. Ensure subject line is under 72 characters
|
|
439
|
+
10. NO additional text, NO commentary, NO explanations
|
|
440
|
+
${userFeedback ? `11. CRITICAL: Preserve the EXACT language used in the message below (user requested specific changes)` : ''}`;
|
|
441
|
+
const finalizationPrompt = `${wrapInstructions(instructions)}
|
|
442
|
+
|
|
443
|
+
${wrapRules(rules)}
|
|
444
|
+
|
|
445
|
+
[RAW_MESSAGE_TO_CLEAN]
|
|
446
|
+
${cleanText(rawMessage)}
|
|
447
|
+
[/RAW_MESSAGE_TO_CLEAN]
|
|
448
|
+
|
|
449
|
+
OUTPUT ONLY THE CLEANED COMMIT MESSAGE:`;
|
|
450
|
+
try {
|
|
451
|
+
const model = this.getModel(provider);
|
|
452
|
+
const result = await apiManager.generateCommitMessage({
|
|
453
|
+
provider,
|
|
454
|
+
model,
|
|
455
|
+
maxTokens: this.config.preferences.maxTokens,
|
|
456
|
+
temperature: 0.3, // Lower temperature for more consistent cleaning
|
|
457
|
+
messages: [
|
|
458
|
+
{ role: 'system', content: finalizationPrompt },
|
|
459
|
+
{ role: 'user', content: 'Clean this message now.' }
|
|
460
|
+
],
|
|
461
|
+
}, provider);
|
|
462
|
+
if (!result.success || !result.data) {
|
|
463
|
+
// If finalization fails, return original message
|
|
464
|
+
logger.warn('Finalization failed, using original message');
|
|
465
|
+
return rawMessage;
|
|
466
|
+
}
|
|
467
|
+
return result.data.trim();
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
// If finalization fails, return original message
|
|
471
|
+
logger.warn('Finalization error, using original message');
|
|
472
|
+
return rawMessage;
|
|
308
473
|
}
|
|
309
|
-
prompt += `\nGenerate only the commit message, no additional text or explanation.`;
|
|
310
|
-
return prompt;
|
|
311
474
|
}
|
|
312
475
|
/**
|
|
313
476
|
* Prepare diff content for API consumption
|
|
@@ -394,26 +557,71 @@ Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert
|
|
|
394
557
|
}
|
|
395
558
|
/**
|
|
396
559
|
* Confirm commit with user (unless auto-confirm is enabled)
|
|
560
|
+
* Returns: 'confirm' | 'regenerate' | 'cancel'
|
|
397
561
|
*/
|
|
398
|
-
async confirmCommit(message, options) {
|
|
562
|
+
async confirmCommit(message, assessment, options) {
|
|
399
563
|
if (options.yes || this.config.preferences.autoConfirm) {
|
|
400
|
-
return
|
|
564
|
+
return { action: 'confirm' };
|
|
401
565
|
}
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
resolve(answer.toLowerCase().startsWith('y'));
|
|
566
|
+
// Show sarcastic code assessment if available
|
|
567
|
+
if (assessment) {
|
|
568
|
+
console.log(chalk.dim('\nš AI thinks: ') + chalk.yellow.italic(assessment));
|
|
569
|
+
}
|
|
570
|
+
console.log(chalk.cyan('\nš Generated commit message:'));
|
|
571
|
+
console.log(chalk.gray('āāāāāāāāāāāāāāāāāā'));
|
|
572
|
+
console.log(chalk.white(message));
|
|
573
|
+
console.log(chalk.gray('āāāāāāāāāāāāāāāāāā\n'));
|
|
574
|
+
try {
|
|
575
|
+
const action = await confirm({
|
|
576
|
+
message: 'Accept this commit message?',
|
|
577
|
+
initialValue: true,
|
|
415
578
|
});
|
|
416
|
-
|
|
579
|
+
if (isCancel(action)) {
|
|
580
|
+
return { action: 'cancel' };
|
|
581
|
+
}
|
|
582
|
+
if (action) {
|
|
583
|
+
return { action: 'confirm' };
|
|
584
|
+
}
|
|
585
|
+
// User rejected - ask if they want to regenerate with feedback
|
|
586
|
+
console.log('');
|
|
587
|
+
const shouldRegenerate = await confirm({
|
|
588
|
+
message: 'Would you like to regenerate with additional instructions?',
|
|
589
|
+
initialValue: true,
|
|
590
|
+
});
|
|
591
|
+
if (isCancel(shouldRegenerate) || !shouldRegenerate) {
|
|
592
|
+
return { action: 'cancel' };
|
|
593
|
+
}
|
|
594
|
+
// Get user feedback for regeneration
|
|
595
|
+
const feedback = await text({
|
|
596
|
+
message: 'What should be changed or improved?',
|
|
597
|
+
placeholder: 'e.g., "Be more specific about the bug fix" or "Mention the new API endpoint"',
|
|
598
|
+
validate: (value) => {
|
|
599
|
+
if (!value || value.trim().length < 3) {
|
|
600
|
+
return 'Please provide at least 3 characters of feedback';
|
|
601
|
+
}
|
|
602
|
+
return undefined; // Valid input
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
if (isCancel(feedback)) {
|
|
606
|
+
return { action: 'cancel' };
|
|
607
|
+
}
|
|
608
|
+
return { action: 'regenerate', feedback: feedback };
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
// Fallback to simple confirmation if prompts fail
|
|
612
|
+
console.log(chalk.yellow('ā Interactive prompts unavailable, using simple mode'));
|
|
613
|
+
const readline = await import('readline');
|
|
614
|
+
const rl = readline.createInterface({
|
|
615
|
+
input: process.stdin,
|
|
616
|
+
output: process.stdout,
|
|
617
|
+
});
|
|
618
|
+
return new Promise((resolve) => {
|
|
619
|
+
rl.question('Accept this commit? (y/N): ', (answer) => {
|
|
620
|
+
rl.close();
|
|
621
|
+
resolve({ action: answer.toLowerCase().startsWith('y') ? 'confirm' : 'cancel' });
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
}
|
|
417
625
|
}
|
|
418
626
|
/**
|
|
419
627
|
* Validate environment before processing
|
|
@@ -449,19 +657,17 @@ Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert
|
|
|
449
657
|
const pushMessage = hasUpstream
|
|
450
658
|
? `Pushing to ${currentBranch}`
|
|
451
659
|
: `Setting upstream and pushing to ${currentBranch}`;
|
|
452
|
-
const
|
|
660
|
+
const pushSpinner = createProcessingSpinner(pushMessage);
|
|
661
|
+
pushSpinner.start();
|
|
453
662
|
try {
|
|
454
663
|
await gitManager.pushToRemote(true); // Set upstream if needed
|
|
455
664
|
const successMessage = hasUpstream
|
|
456
665
|
? `Pushed to ${currentBranch}`
|
|
457
666
|
: `Upstream set and pushed to ${currentBranch}`;
|
|
458
|
-
|
|
459
|
-
console.log(chalk.green('ā Changes pushed successfully'));
|
|
667
|
+
pushSpinner.succeed(successMessage);
|
|
460
668
|
}
|
|
461
669
|
catch (error) {
|
|
462
|
-
|
|
463
|
-
console.log(chalk.red(`ā Error: ${error.message}`));
|
|
464
|
-
console.log(chalk.gray('š” You can try pushing manually: git push'));
|
|
670
|
+
pushSpinner.fail(`Push failed: ${error.message}`);
|
|
465
671
|
}
|
|
466
672
|
}
|
|
467
673
|
/**
|