@entro314labs/ai-changelog-generator 3.0.5 → 3.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/CHANGELOG.md +92 -0
- package/README.md +22 -0
- package/package.json +10 -10
- package/src/application/orchestrators/changelog.orchestrator.js +469 -0
- package/src/application/services/application.service.js +13 -0
- package/src/infrastructure/cli/cli.controller.js +52 -0
- package/src/infrastructure/interactive/interactive-staging.service.js +313 -0
- package/src/infrastructure/validation/commit-message-validation.service.js +458 -0
- package/src/shared/constants/colors.js +28 -6
- package/src/shared/utils/utils.js +352 -0
|
@@ -298,4 +298,17 @@ export class ApplicationService {
|
|
|
298
298
|
throw error;
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
|
+
|
|
302
|
+
// Interactive commit workflow
|
|
303
|
+
async executeCommitWorkflow(options = {}) {
|
|
304
|
+
try {
|
|
305
|
+
await this.ensureInitialized();
|
|
306
|
+
|
|
307
|
+
// Delegate to orchestrator for the commit workflow
|
|
308
|
+
return await this.orchestrator.executeCommitWorkflow(options);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.error(colors.errorMessage('Commit workflow error:'), error.message);
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
301
314
|
}
|
|
@@ -38,6 +38,7 @@ export class CLIController {
|
|
|
38
38
|
this.commands.set('working-dir', new WorkingDirCommand());
|
|
39
39
|
this.commands.set('from-commits', new FromCommitsCommand());
|
|
40
40
|
this.commands.set('commit-message', new CommitMessageCommand());
|
|
41
|
+
this.commands.set('commit', new CommitCommand());
|
|
41
42
|
this.commands.set('providers', new ProvidersCommand());
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -125,6 +126,15 @@ export class CLIController {
|
|
|
125
126
|
|
|
126
127
|
// Utility commands
|
|
127
128
|
.command('commit-message', 'Generate a commit message for current changes.')
|
|
129
|
+
.command('commit', 'Interactive commit workflow with AI-generated messages.', (yargs) => {
|
|
130
|
+
yargs
|
|
131
|
+
.option('interactive', { alias: 'i', type: 'boolean', default: true, description: 'Use interactive staging (default).' })
|
|
132
|
+
.option('all', { alias: 'a', type: 'boolean', description: 'Automatically stage all changes.' })
|
|
133
|
+
.option('message', { alias: 'm', type: 'string', description: 'Use provided commit message (skip AI generation).' })
|
|
134
|
+
.option('dry-run', { type: 'boolean', description: 'Preview commit message without committing.' })
|
|
135
|
+
.option('editor', { alias: 'e', type: 'boolean', description: 'Open editor to review/edit commit message.' })
|
|
136
|
+
.option('model', { type: 'string', description: 'Override the default AI model.' });
|
|
137
|
+
})
|
|
128
138
|
.command('providers', 'Manage AI providers.', (yargs) => {
|
|
129
139
|
yargs
|
|
130
140
|
.command('list', 'List available providers.')
|
|
@@ -340,6 +350,48 @@ class CommitMessageCommand extends BaseCommand {
|
|
|
340
350
|
}
|
|
341
351
|
}
|
|
342
352
|
|
|
353
|
+
class CommitCommand extends BaseCommand {
|
|
354
|
+
async execute(argv, appService) {
|
|
355
|
+
console.log(colors.processingMessage('🚀 Starting interactive commit workflow...'));
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
// Process flags and model override
|
|
359
|
+
if (argv.model) appService.setModelOverride(argv.model);
|
|
360
|
+
|
|
361
|
+
// Execute the commit workflow
|
|
362
|
+
const result = await appService.executeCommitWorkflow({
|
|
363
|
+
interactive: argv.interactive !== false, // Default to true unless explicitly false
|
|
364
|
+
stageAll: argv.all || false,
|
|
365
|
+
customMessage: argv.message,
|
|
366
|
+
dryRun: argv.dryRun || false,
|
|
367
|
+
useEditor: argv.editor || false
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
if (result && result.success) {
|
|
371
|
+
if (argv.dryRun) {
|
|
372
|
+
console.log(colors.successMessage('✅ Commit workflow completed (dry-run mode)'));
|
|
373
|
+
console.log(colors.highlight(`Proposed commit message:\n${result.commitMessage}`));
|
|
374
|
+
} else {
|
|
375
|
+
console.log(colors.successMessage('✅ Changes committed successfully!'));
|
|
376
|
+
console.log(colors.highlight(`Commit: ${result.commitHash}`));
|
|
377
|
+
console.log(colors.dim(`Message: ${result.commitMessage}`));
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
console.log(colors.warningMessage('Commit workflow cancelled or no changes to commit.'));
|
|
381
|
+
}
|
|
382
|
+
} catch (error) {
|
|
383
|
+
console.error(colors.errorMessage(`Commit workflow failed: ${error.message}`));
|
|
384
|
+
|
|
385
|
+
// Provide helpful suggestions based on error type
|
|
386
|
+
if (error.message.includes('No changes')) {
|
|
387
|
+
console.log(colors.infoMessage('💡 Try making some changes first, then run the commit command.'));
|
|
388
|
+
} else if (error.message.includes('git')) {
|
|
389
|
+
console.log(colors.infoMessage('💡 Make sure you are in a git repository and git is properly configured.'));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
343
395
|
class ProvidersCommand extends BaseCommand {
|
|
344
396
|
async execute(argv, appService) {
|
|
345
397
|
const subcommand = argv._[1];
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import colors from '../../shared/constants/colors.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Interactive Staging Service
|
|
6
|
+
*
|
|
7
|
+
* Provides interactive git staging functionality similar to better-commits
|
|
8
|
+
* Handles file selection, staging, and unstaging operations
|
|
9
|
+
*/
|
|
10
|
+
export class InteractiveStagingService {
|
|
11
|
+
constructor(gitManager) {
|
|
12
|
+
this.gitManager = gitManager;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Show git status with staged and unstaged changes
|
|
17
|
+
*/
|
|
18
|
+
async showGitStatus() {
|
|
19
|
+
console.log(colors.processingMessage(' Checking Git Status '));
|
|
20
|
+
|
|
21
|
+
const statusResult = this.getDetailedStatus();
|
|
22
|
+
|
|
23
|
+
if (statusResult.staged.length > 0) {
|
|
24
|
+
console.log(colors.successMessage('Changes to be committed:'));
|
|
25
|
+
statusResult.staged.forEach(file => {
|
|
26
|
+
const icon = this.getStatusIcon(file.status);
|
|
27
|
+
console.log(colors.success(` ${icon} ${file.status} ${file.path}`));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (statusResult.unstaged.length > 0) {
|
|
32
|
+
console.log(colors.warningMessage('\nChanges not staged for commit:'));
|
|
33
|
+
statusResult.unstaged.forEach(file => {
|
|
34
|
+
const icon = this.getStatusIcon(file.status);
|
|
35
|
+
console.log(colors.warning(` ${icon} ${file.status} ${file.path}`));
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (statusResult.untracked.length > 0) {
|
|
40
|
+
console.log(colors.infoMessage('\nUntracked files:'));
|
|
41
|
+
statusResult.untracked.forEach(file => {
|
|
42
|
+
console.log(colors.dim(` ✨ ?? ${file.path}`));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return statusResult;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Interactive file selection for staging
|
|
51
|
+
*/
|
|
52
|
+
async selectFilesToStage(files = null) {
|
|
53
|
+
const { multiselect, confirm } = await import('@clack/prompts');
|
|
54
|
+
|
|
55
|
+
// Get current status if files not provided
|
|
56
|
+
if (!files) {
|
|
57
|
+
const status = this.getDetailedStatus();
|
|
58
|
+
files = [...status.unstaged, ...status.untracked];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (files.length === 0) {
|
|
62
|
+
console.log(colors.infoMessage('No files available for staging.'));
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Create choices for the multiselect prompt
|
|
67
|
+
const choices = files.map(file => ({
|
|
68
|
+
value: file.path,
|
|
69
|
+
label: `${this.getStatusIcon(file.status)} ${file.status} ${file.path}`,
|
|
70
|
+
hint: this.getStatusDescription(file.status)
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const selectedFiles = await multiselect({
|
|
75
|
+
message: 'Select files to stage for commit:',
|
|
76
|
+
options: choices,
|
|
77
|
+
required: false
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!selectedFiles || selectedFiles.length === 0) {
|
|
81
|
+
console.log(colors.infoMessage('No files selected for staging.'));
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Confirm the selection
|
|
86
|
+
const shouldStage = await confirm({
|
|
87
|
+
message: `Stage ${selectedFiles.length} file(s)?`,
|
|
88
|
+
initialValue: true
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (shouldStage) {
|
|
92
|
+
await this.stageFiles(selectedFiles);
|
|
93
|
+
console.log(colors.successMessage(`✅ Staged ${selectedFiles.length} file(s)`));
|
|
94
|
+
return selectedFiles;
|
|
95
|
+
} else {
|
|
96
|
+
console.log(colors.infoMessage('Staging cancelled.'));
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error.message.includes('cancelled')) {
|
|
101
|
+
console.log(colors.infoMessage('File selection cancelled.'));
|
|
102
|
+
} else {
|
|
103
|
+
console.error(colors.errorMessage(`Error during file selection: ${error.message}`));
|
|
104
|
+
}
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Interactive unstaging of files
|
|
111
|
+
*/
|
|
112
|
+
async selectFilesToUnstage() {
|
|
113
|
+
const { multiselect, confirm } = await import('@clack/prompts');
|
|
114
|
+
|
|
115
|
+
const status = this.getDetailedStatus();
|
|
116
|
+
const stagedFiles = status.staged;
|
|
117
|
+
|
|
118
|
+
if (stagedFiles.length === 0) {
|
|
119
|
+
console.log(colors.infoMessage('No staged files to unstage.'));
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const choices = stagedFiles.map(file => ({
|
|
124
|
+
value: file.path,
|
|
125
|
+
label: `${this.getStatusIcon(file.status)} ${file.status} ${file.path}`,
|
|
126
|
+
hint: 'Remove from staging area'
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const selectedFiles = await multiselect({
|
|
131
|
+
message: 'Select files to unstage:',
|
|
132
|
+
options: choices,
|
|
133
|
+
required: false
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (!selectedFiles || selectedFiles.length === 0) {
|
|
137
|
+
console.log(colors.infoMessage('No files selected for unstaging.'));
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const shouldUnstage = await confirm({
|
|
142
|
+
message: `Unstage ${selectedFiles.length} file(s)?`,
|
|
143
|
+
initialValue: true
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (shouldUnstage) {
|
|
147
|
+
await this.unstageFiles(selectedFiles);
|
|
148
|
+
console.log(colors.successMessage(`✅ Unstaged ${selectedFiles.length} file(s)`));
|
|
149
|
+
return selectedFiles;
|
|
150
|
+
} else {
|
|
151
|
+
console.log(colors.infoMessage('Unstaging cancelled.'));
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error.message.includes('cancelled')) {
|
|
156
|
+
console.log(colors.infoMessage('File unstaging cancelled.'));
|
|
157
|
+
} else {
|
|
158
|
+
console.error(colors.errorMessage(`Error during file unstaging: ${error.message}`));
|
|
159
|
+
}
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Stage all changes
|
|
166
|
+
*/
|
|
167
|
+
async stageAllChanges() {
|
|
168
|
+
try {
|
|
169
|
+
console.log(colors.processingMessage('Staging all changes...'));
|
|
170
|
+
execSync('git add .', { stdio: 'pipe' });
|
|
171
|
+
console.log(colors.successMessage('✅ All changes staged'));
|
|
172
|
+
return true;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error(colors.errorMessage(`Error staging all changes: ${error.message}`));
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Stage specific files
|
|
181
|
+
*/
|
|
182
|
+
async stageFiles(filePaths) {
|
|
183
|
+
try {
|
|
184
|
+
const files = Array.isArray(filePaths) ? filePaths : [filePaths];
|
|
185
|
+
|
|
186
|
+
for (const file of files) {
|
|
187
|
+
execSync(`git add "${file}"`, { stdio: 'pipe' });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return true;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error(colors.errorMessage(`Error staging files: ${error.message}`));
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Unstage specific files
|
|
199
|
+
*/
|
|
200
|
+
async unstageFiles(filePaths) {
|
|
201
|
+
try {
|
|
202
|
+
const files = Array.isArray(filePaths) ? filePaths : [filePaths];
|
|
203
|
+
|
|
204
|
+
for (const file of files) {
|
|
205
|
+
execSync(`git reset HEAD "${file}"`, { stdio: 'pipe' });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return true;
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error(colors.errorMessage(`Error unstaging files: ${error.message}`));
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get detailed git status (staged, unstaged, untracked)
|
|
217
|
+
*/
|
|
218
|
+
getDetailedStatus() {
|
|
219
|
+
try {
|
|
220
|
+
const output = execSync('git status --porcelain', { encoding: 'utf8' });
|
|
221
|
+
const lines = output.split('\n').filter(Boolean);
|
|
222
|
+
|
|
223
|
+
const staged = [];
|
|
224
|
+
const unstaged = [];
|
|
225
|
+
const untracked = [];
|
|
226
|
+
|
|
227
|
+
lines.forEach(line => {
|
|
228
|
+
if (line.length < 3) return;
|
|
229
|
+
|
|
230
|
+
const indexStatus = line.charAt(0);
|
|
231
|
+
const workTreeStatus = line.charAt(1);
|
|
232
|
+
const filePath = line.substring(3).trim();
|
|
233
|
+
|
|
234
|
+
// Handle different status combinations
|
|
235
|
+
if (indexStatus === '?' && workTreeStatus === '?') {
|
|
236
|
+
// Untracked file
|
|
237
|
+
untracked.push({ status: '??', path: filePath });
|
|
238
|
+
} else {
|
|
239
|
+
// Staged changes (index status)
|
|
240
|
+
if (indexStatus !== ' ') {
|
|
241
|
+
staged.push({ status: indexStatus, path: filePath });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Unstaged changes (work tree status)
|
|
245
|
+
if (workTreeStatus !== ' ') {
|
|
246
|
+
unstaged.push({ status: workTreeStatus, path: filePath });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return { staged, unstaged, untracked };
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error(colors.errorMessage(`Error getting git status: ${error.message}`));
|
|
254
|
+
return { staged: [], unstaged: [], untracked: [] };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get icon for status
|
|
260
|
+
*/
|
|
261
|
+
getStatusIcon(status) {
|
|
262
|
+
const icons = {
|
|
263
|
+
'M': '📝', // Modified
|
|
264
|
+
'A': '✨', // Added
|
|
265
|
+
'D': '🗑️', // Deleted
|
|
266
|
+
'R': '🔄', // Renamed
|
|
267
|
+
'C': '📋', // Copied
|
|
268
|
+
'U': '⚠️', // Unmerged
|
|
269
|
+
'??': '✨' // Untracked
|
|
270
|
+
};
|
|
271
|
+
return icons[status] || '📄';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get description for status
|
|
276
|
+
*/
|
|
277
|
+
getStatusDescription(status) {
|
|
278
|
+
const descriptions = {
|
|
279
|
+
'M': 'Modified file',
|
|
280
|
+
'A': 'New file',
|
|
281
|
+
'D': 'Deleted file',
|
|
282
|
+
'R': 'Renamed file',
|
|
283
|
+
'C': 'Copied file',
|
|
284
|
+
'U': 'Unmerged file',
|
|
285
|
+
'??': 'Untracked file'
|
|
286
|
+
};
|
|
287
|
+
return descriptions[status] || 'Changed file';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Check if there are any staged changes
|
|
292
|
+
*/
|
|
293
|
+
hasStagedChanges() {
|
|
294
|
+
try {
|
|
295
|
+
const output = execSync('git diff --cached --name-only', { encoding: 'utf8' });
|
|
296
|
+
return output.trim().length > 0;
|
|
297
|
+
} catch (error) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Check if there are any unstaged changes
|
|
304
|
+
*/
|
|
305
|
+
hasUnstagedChanges() {
|
|
306
|
+
try {
|
|
307
|
+
const output = execSync('git diff --name-only', { encoding: 'utf8' });
|
|
308
|
+
return output.trim().length > 0;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|