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.
- package/README.md +226 -59
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +36 -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 +11 -1
- package/dist/modules/core.d.ts.map +1 -1
- package/dist/modules/core.js +391 -124
- 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 +17 -1
- package/dist/modules/git.d.ts.map +1 -1
- package/dist/modules/git.js +209 -13
- package/dist/modules/git.js.map +1 -1
- package/dist/modules/logger.d.ts +12 -0
- package/dist/modules/logger.d.ts.map +1 -1
- package/dist/modules/logger.js +40 -3
- package/dist/modules/logger.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 +16 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +10 -0
- package/dist/types/index.js.map +1 -1
- package/dist/utils/formatting.d.ts +57 -0
- package/dist/utils/formatting.d.ts.map +1 -0
- package/dist/utils/formatting.js +94 -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
|
/**
|
|
@@ -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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
108
|
-
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();
|
|
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: '
|
|
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,
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
176
|
-
return
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
206
|
-
return
|
|
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
|
-
|
|
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
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
349
|
+
rules += `\n- Generate a single-line commit message only`;
|
|
257
350
|
}
|
|
258
351
|
else {
|
|
259
|
-
|
|
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
|
-
|
|
355
|
+
rules += `\n- Limit description to ${options.descriptionLength} characters`;
|
|
263
356
|
}
|
|
264
357
|
if (options.emoji) {
|
|
265
|
-
|
|
358
|
+
rules += `\n- Include appropriate emoji at the start of the commit message`;
|
|
266
359
|
}
|
|
267
360
|
if (format === 'conventional') {
|
|
268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
367
|
+
rules += `\n\nRequired type: ${options.type}`;
|
|
291
368
|
}
|
|
292
369
|
if (options.scope) {
|
|
293
|
-
|
|
370
|
+
rules += `\nRequired scope: ${options.scope}`;
|
|
294
371
|
}
|
|
295
372
|
if (options.breaking) {
|
|
296
|
-
|
|
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
|
|
558
|
+
return { action: 'confirm' };
|
|
390
559
|
}
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
|
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
|
-
|
|
448
|
-
console.log(chalk.green('ā Changes pushed successfully'));
|
|
661
|
+
pushSpinner.succeed(successMessage);
|
|
449
662
|
}
|
|
450
663
|
catch (error) {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|