@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.
@@ -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
+ }