commic 1.0.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/.husky/pre-commit +2 -0
- package/README.md +306 -0
- package/biome.json +50 -0
- package/dist/ai/AIService.d.ts +51 -0
- package/dist/ai/AIService.d.ts.map +1 -0
- package/dist/ai/AIService.js +351 -0
- package/dist/ai/AIService.js.map +1 -0
- package/dist/config/ConfigManager.d.ts +49 -0
- package/dist/config/ConfigManager.d.ts.map +1 -0
- package/dist/config/ConfigManager.js +124 -0
- package/dist/config/ConfigManager.js.map +1 -0
- package/dist/errors/CustomErrors.d.ts +54 -0
- package/dist/errors/CustomErrors.d.ts.map +1 -0
- package/dist/errors/CustomErrors.js +99 -0
- package/dist/errors/CustomErrors.js.map +1 -0
- package/dist/git/GitService.d.ts +77 -0
- package/dist/git/GitService.d.ts.map +1 -0
- package/dist/git/GitService.js +219 -0
- package/dist/git/GitService.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestrator/MainOrchestrator.d.ts +63 -0
- package/dist/orchestrator/MainOrchestrator.d.ts.map +1 -0
- package/dist/orchestrator/MainOrchestrator.js +225 -0
- package/dist/orchestrator/MainOrchestrator.js.map +1 -0
- package/dist/types/index.d.ts +55 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/ui/UIManager.d.ts +118 -0
- package/dist/ui/UIManager.d.ts.map +1 -0
- package/dist/ui/UIManager.js +369 -0
- package/dist/ui/UIManager.js.map +1 -0
- package/dist/validation/ConventionalCommitsValidator.d.ts +33 -0
- package/dist/validation/ConventionalCommitsValidator.d.ts.map +1 -0
- package/dist/validation/ConventionalCommitsValidator.js +114 -0
- package/dist/validation/ConventionalCommitsValidator.js.map +1 -0
- package/package.json +49 -0
- package/src/ai/AIService.ts +413 -0
- package/src/config/ConfigManager.ts +141 -0
- package/src/errors/CustomErrors.ts +176 -0
- package/src/git/GitService.ts +246 -0
- package/src/index.ts +55 -0
- package/src/orchestrator/MainOrchestrator.ts +263 -0
- package/src/types/index.ts +60 -0
- package/src/ui/UIManager.ts +420 -0
- package/src/validation/ConventionalCommitsValidator.ts +139 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { AIService } from '../ai/AIService.js';
|
|
2
|
+
import type { ConfigManager } from '../config/ConfigManager.js';
|
|
3
|
+
import { APIError, ConfigurationError, GitRepositoryError } from '../errors/CustomErrors.js';
|
|
4
|
+
import type { GitService } from '../git/GitService.js';
|
|
5
|
+
import type { CLIOptions, Config } from '../types/index.js';
|
|
6
|
+
import type { UIManager } from '../ui/UIManager.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Orchestrates the complete workflow of the Commit CLI
|
|
10
|
+
* Coordinates between configuration, Git operations, AI generation, and user interaction
|
|
11
|
+
*/
|
|
12
|
+
export class MainOrchestrator {
|
|
13
|
+
constructor(
|
|
14
|
+
private readonly configManager: ConfigManager,
|
|
15
|
+
private readonly gitService: GitService,
|
|
16
|
+
private readonly uiManager: UIManager
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Execute the complete commit workflow
|
|
21
|
+
* @param options CLI options from command line
|
|
22
|
+
*/
|
|
23
|
+
async execute(options: CLIOptions): Promise<void> {
|
|
24
|
+
try {
|
|
25
|
+
// Show welcome message
|
|
26
|
+
this.uiManager.showWelcome();
|
|
27
|
+
|
|
28
|
+
// Step 1: Handle configuration
|
|
29
|
+
const config = await this.handleConfiguration(options.reconfigure);
|
|
30
|
+
|
|
31
|
+
// Step 2: Find and validate repository
|
|
32
|
+
const repositoryPath = options.path || '.';
|
|
33
|
+
const repository = await this.findAndValidateRepository(repositoryPath);
|
|
34
|
+
|
|
35
|
+
// Step 3: Get Git diff
|
|
36
|
+
const diff = await this.getGitDiff(repository.rootPath);
|
|
37
|
+
|
|
38
|
+
// Step 4: Generate commit suggestions
|
|
39
|
+
const suggestions = await this.generateSuggestions(config, diff);
|
|
40
|
+
|
|
41
|
+
// Step 5: Let user select a commit message
|
|
42
|
+
const selectedIndex = await this.uiManager.promptForCommitSelection(suggestions);
|
|
43
|
+
|
|
44
|
+
// Handle cancellation
|
|
45
|
+
if (selectedIndex === -1) {
|
|
46
|
+
this.uiManager.showInfo('Commit cancelled. No changes were made.');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Step 6: Stage changes if needed
|
|
51
|
+
await this.stageChangesIfNeeded(repository.rootPath, diff);
|
|
52
|
+
|
|
53
|
+
// Step 7: Execute commit
|
|
54
|
+
const commitHash = await this.executeCommit(
|
|
55
|
+
repository.rootPath,
|
|
56
|
+
suggestions[selectedIndex].message
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Step 8: Show success with more details
|
|
60
|
+
const shortHash = commitHash.substring(0, 7);
|
|
61
|
+
const repoName = this.gitService.getRepositoryName(repository.rootPath);
|
|
62
|
+
const branch = await this.gitService.getCurrentBranch(repository.rootPath);
|
|
63
|
+
this.uiManager.showSuccess(
|
|
64
|
+
`Committed successfully!\n` +
|
|
65
|
+
` Repository: ${repoName}\n` +
|
|
66
|
+
` Branch: ${branch}\n` +
|
|
67
|
+
` Commit: ${this.uiManager.muted(shortHash)}`
|
|
68
|
+
);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
this.handleError(error);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Handle configuration loading or prompting
|
|
77
|
+
* @param forceReconfigure Force reconfiguration even if config exists
|
|
78
|
+
* @returns Configuration object
|
|
79
|
+
*/
|
|
80
|
+
private async handleConfiguration(forceReconfigure?: boolean): Promise<Config> {
|
|
81
|
+
try {
|
|
82
|
+
// Check if we need to reconfigure
|
|
83
|
+
if (forceReconfigure) {
|
|
84
|
+
this.uiManager.showInfo('Reconfiguring...');
|
|
85
|
+
const config = await this.configManager.promptForConfig(this.uiManager);
|
|
86
|
+
await this.configManager.save(config);
|
|
87
|
+
this.uiManager.showSuccess('Configuration saved!');
|
|
88
|
+
return config;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Try to load existing config
|
|
92
|
+
const existingConfig = await this.configManager.load();
|
|
93
|
+
|
|
94
|
+
if (existingConfig) {
|
|
95
|
+
return existingConfig;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// No config exists - prompt for first-time setup
|
|
99
|
+
this.uiManager.showInfo('First-time setup required');
|
|
100
|
+
const config = await this.configManager.promptForConfig(this.uiManager);
|
|
101
|
+
await this.configManager.save(config);
|
|
102
|
+
this.uiManager.showSuccess('Configuration saved!');
|
|
103
|
+
return config;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error instanceof ConfigurationError) {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
throw ConfigurationError.configSaveFailed(error as Error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Find and validate Git repository
|
|
114
|
+
* @param path Path to search for repository
|
|
115
|
+
* @returns Repository information
|
|
116
|
+
*/
|
|
117
|
+
private async findAndValidateRepository(path: string): Promise<{ rootPath: string }> {
|
|
118
|
+
const spinner = this.uiManager.showLoading('Finding Git repository...');
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const repository = await this.gitService.findRepository(path);
|
|
122
|
+
|
|
123
|
+
// Check if repository has commits
|
|
124
|
+
const hasCommits = await this.gitService.hasCommits(repository.rootPath);
|
|
125
|
+
|
|
126
|
+
if (!hasCommits) {
|
|
127
|
+
spinner.fail('Repository has no commits');
|
|
128
|
+
throw GitRepositoryError.noCommitsFound();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Get repository info
|
|
132
|
+
const repoName = this.gitService.getRepositoryName(repository.rootPath);
|
|
133
|
+
const branch = await this.gitService.getCurrentBranch(repository.rootPath);
|
|
134
|
+
const remoteUrl = await this.gitService.getRemoteUrl(repository.rootPath);
|
|
135
|
+
|
|
136
|
+
spinner.succeed('Repository found');
|
|
137
|
+
|
|
138
|
+
// Show repository info with better spacing
|
|
139
|
+
this.uiManager.newLine();
|
|
140
|
+
this.uiManager.showRepositoryInfo(repoName, branch, repository.rootPath, remoteUrl);
|
|
141
|
+
|
|
142
|
+
return repository;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
spinner.fail('Failed to find repository');
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get Git diff and validate changes exist
|
|
151
|
+
* @param repoPath Repository root path
|
|
152
|
+
* @returns Git diff
|
|
153
|
+
*/
|
|
154
|
+
private async getGitDiff(repoPath: string) {
|
|
155
|
+
const spinner = this.uiManager.showLoading('Analyzing changes...');
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const diff = await this.gitService.getDiff(repoPath);
|
|
159
|
+
|
|
160
|
+
if (!diff.hasChanges) {
|
|
161
|
+
spinner.fail('No changes to commit');
|
|
162
|
+
throw GitRepositoryError.noChanges();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Get diff statistics
|
|
166
|
+
const stats = await this.gitService.getDiffStats(repoPath);
|
|
167
|
+
|
|
168
|
+
spinner.succeed('Changes analyzed');
|
|
169
|
+
|
|
170
|
+
// Show change statistics with better spacing
|
|
171
|
+
this.uiManager.showChangeStats(stats);
|
|
172
|
+
|
|
173
|
+
return diff;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
spinner.fail('Failed to analyze changes');
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Generate commit message suggestions using AI
|
|
182
|
+
* @param config Configuration with API key
|
|
183
|
+
* @param diff Git diff
|
|
184
|
+
* @returns Array of suggestions
|
|
185
|
+
*/
|
|
186
|
+
private async generateSuggestions(config: Config, diff: any) {
|
|
187
|
+
this.uiManager.newLine();
|
|
188
|
+
this.uiManager.showSectionHeader('🤖 AI Generation');
|
|
189
|
+
this.uiManager.showAIGenerationInfo(config.model, 4);
|
|
190
|
+
|
|
191
|
+
const spinner = this.uiManager.showLoading(' Generating commit messages...');
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const aiService = new AIService(config.apiKey, config.model);
|
|
195
|
+
const suggestions = await aiService.generateCommitMessages(diff, 4);
|
|
196
|
+
|
|
197
|
+
spinner.succeed(` Generated ${suggestions.length} suggestions`);
|
|
198
|
+
this.uiManager.newLine();
|
|
199
|
+
|
|
200
|
+
return suggestions;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
spinner.fail(' Failed to generate suggestions');
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Stage changes if there are unstaged changes
|
|
209
|
+
* @param repoPath Repository root path
|
|
210
|
+
* @param diff Git diff
|
|
211
|
+
*/
|
|
212
|
+
private async stageChangesIfNeeded(repoPath: string, diff: any): Promise<void> {
|
|
213
|
+
if (diff.unstaged && diff.unstaged.length > 0) {
|
|
214
|
+
this.uiManager.newLine();
|
|
215
|
+
const spinner = this.uiManager.showLoading('Staging changes...');
|
|
216
|
+
try {
|
|
217
|
+
await this.gitService.stageAll(repoPath);
|
|
218
|
+
spinner.succeed('Changes staged');
|
|
219
|
+
this.uiManager.newLine();
|
|
220
|
+
} catch (error) {
|
|
221
|
+
spinner.fail('Failed to stage changes');
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Execute Git commit
|
|
229
|
+
* @param repoPath Repository root path
|
|
230
|
+
* @param message Commit message
|
|
231
|
+
* @returns Commit hash
|
|
232
|
+
*/
|
|
233
|
+
private async executeCommit(repoPath: string, message: string): Promise<string> {
|
|
234
|
+
this.uiManager.newLine();
|
|
235
|
+
const spinner = this.uiManager.showLoading('Committing changes...');
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const commitHash = await this.gitService.commit(repoPath, message);
|
|
239
|
+
spinner.succeed('Commit created successfully');
|
|
240
|
+
this.uiManager.newLine();
|
|
241
|
+
return commitHash;
|
|
242
|
+
} catch (error) {
|
|
243
|
+
spinner.fail('Commit failed');
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Handle errors with user-friendly messages
|
|
250
|
+
* @param error Error to handle
|
|
251
|
+
*/
|
|
252
|
+
private handleError(error: unknown): void {
|
|
253
|
+
if (
|
|
254
|
+
error instanceof GitRepositoryError ||
|
|
255
|
+
error instanceof ConfigurationError ||
|
|
256
|
+
error instanceof APIError
|
|
257
|
+
) {
|
|
258
|
+
this.uiManager.showErrorWithSuggestion(error.message, error.suggestion || '');
|
|
259
|
+
} else {
|
|
260
|
+
this.uiManager.showError('An unexpected error occurred', (error as Error).message);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration structure stored in ~/.commit-cli/config.json
|
|
3
|
+
*/
|
|
4
|
+
export interface Config {
|
|
5
|
+
apiKey: string;
|
|
6
|
+
model: string;
|
|
7
|
+
version: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Git repository information
|
|
12
|
+
*/
|
|
13
|
+
export interface GitRepository {
|
|
14
|
+
path: string;
|
|
15
|
+
rootPath: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Git diff information
|
|
20
|
+
*/
|
|
21
|
+
export interface GitDiff {
|
|
22
|
+
staged: string;
|
|
23
|
+
unstaged: string;
|
|
24
|
+
hasChanges: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Commit message suggestion
|
|
29
|
+
*/
|
|
30
|
+
export interface CommitSuggestion {
|
|
31
|
+
message: string;
|
|
32
|
+
type: 'single-line' | 'multi-line';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Validation result for commit messages
|
|
37
|
+
*/
|
|
38
|
+
export interface ValidationResult {
|
|
39
|
+
valid: boolean;
|
|
40
|
+
errors: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parsed commit message structure
|
|
45
|
+
*/
|
|
46
|
+
export interface ParsedCommitMessage {
|
|
47
|
+
type: string;
|
|
48
|
+
scope?: string;
|
|
49
|
+
breaking: boolean;
|
|
50
|
+
description: string;
|
|
51
|
+
body?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* CLI options from command line arguments
|
|
56
|
+
*/
|
|
57
|
+
export interface CLIOptions {
|
|
58
|
+
path?: string;
|
|
59
|
+
reconfigure?: boolean;
|
|
60
|
+
}
|