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.
Files changed (50) hide show
  1. package/.husky/pre-commit +2 -0
  2. package/README.md +306 -0
  3. package/biome.json +50 -0
  4. package/dist/ai/AIService.d.ts +51 -0
  5. package/dist/ai/AIService.d.ts.map +1 -0
  6. package/dist/ai/AIService.js +351 -0
  7. package/dist/ai/AIService.js.map +1 -0
  8. package/dist/config/ConfigManager.d.ts +49 -0
  9. package/dist/config/ConfigManager.d.ts.map +1 -0
  10. package/dist/config/ConfigManager.js +124 -0
  11. package/dist/config/ConfigManager.js.map +1 -0
  12. package/dist/errors/CustomErrors.d.ts +54 -0
  13. package/dist/errors/CustomErrors.d.ts.map +1 -0
  14. package/dist/errors/CustomErrors.js +99 -0
  15. package/dist/errors/CustomErrors.js.map +1 -0
  16. package/dist/git/GitService.d.ts +77 -0
  17. package/dist/git/GitService.d.ts.map +1 -0
  18. package/dist/git/GitService.js +219 -0
  19. package/dist/git/GitService.js.map +1 -0
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +48 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/orchestrator/MainOrchestrator.d.ts +63 -0
  25. package/dist/orchestrator/MainOrchestrator.d.ts.map +1 -0
  26. package/dist/orchestrator/MainOrchestrator.js +225 -0
  27. package/dist/orchestrator/MainOrchestrator.js.map +1 -0
  28. package/dist/types/index.d.ts +55 -0
  29. package/dist/types/index.d.ts.map +1 -0
  30. package/dist/types/index.js +2 -0
  31. package/dist/types/index.js.map +1 -0
  32. package/dist/ui/UIManager.d.ts +118 -0
  33. package/dist/ui/UIManager.d.ts.map +1 -0
  34. package/dist/ui/UIManager.js +369 -0
  35. package/dist/ui/UIManager.js.map +1 -0
  36. package/dist/validation/ConventionalCommitsValidator.d.ts +33 -0
  37. package/dist/validation/ConventionalCommitsValidator.d.ts.map +1 -0
  38. package/dist/validation/ConventionalCommitsValidator.js +114 -0
  39. package/dist/validation/ConventionalCommitsValidator.js.map +1 -0
  40. package/package.json +49 -0
  41. package/src/ai/AIService.ts +413 -0
  42. package/src/config/ConfigManager.ts +141 -0
  43. package/src/errors/CustomErrors.ts +176 -0
  44. package/src/git/GitService.ts +246 -0
  45. package/src/index.ts +55 -0
  46. package/src/orchestrator/MainOrchestrator.ts +263 -0
  47. package/src/types/index.ts +60 -0
  48. package/src/ui/UIManager.ts +420 -0
  49. package/src/validation/ConventionalCommitsValidator.ts +139 -0
  50. package/tsconfig.json +24 -0
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Base error class for Commit CLI
3
+ * Provides consistent error structure with user-friendly messages and suggestions
4
+ */
5
+ class CommitCLIError extends Error {
6
+ public readonly suggestion: string | null;
7
+
8
+ constructor(message: string, suggestion: string | null = null) {
9
+ super(message);
10
+ this.name = this.constructor.name;
11
+ this.suggestion = suggestion;
12
+ Error.captureStackTrace(this, this.constructor);
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Git repository related errors
18
+ * Thrown when Git operations fail or repository is invalid
19
+ */
20
+ export class GitRepositoryError extends CommitCLIError {
21
+ constructor(
22
+ message: string,
23
+ suggestion: string = 'Ensure you are in a valid Git repository or provide a correct path.'
24
+ ) {
25
+ super(message, suggestion);
26
+ }
27
+
28
+ static noRepositoryFound(path: string): GitRepositoryError {
29
+ return new GitRepositoryError(
30
+ `No Git repository found at: ${path}`,
31
+ 'Initialize a Git repository with "git init" or provide a valid repository path.'
32
+ );
33
+ }
34
+
35
+ static noCommitsFound(): GitRepositoryError {
36
+ return new GitRepositoryError(
37
+ 'Repository has no commits yet',
38
+ 'Create an initial commit with "git add . && git commit -m \'Initial commit\'"'
39
+ );
40
+ }
41
+
42
+ static noChanges(): GitRepositoryError {
43
+ return new GitRepositoryError(
44
+ 'No changes to commit',
45
+ 'Make some changes to your files or check "git status" to see the current state.'
46
+ );
47
+ }
48
+
49
+ static commitFailed(gitError: string): GitRepositoryError {
50
+ return new GitRepositoryError(
51
+ `Git commit failed: ${gitError}`,
52
+ 'Check the error message above and resolve any Git issues.'
53
+ );
54
+ }
55
+
56
+ static pathNotAccessible(path: string): GitRepositoryError {
57
+ return new GitRepositoryError(
58
+ `Path not accessible: ${path}`,
59
+ 'Ensure the path exists and you have permission to access it.'
60
+ );
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Configuration related errors
66
+ * Thrown when config file operations fail or configuration is invalid
67
+ */
68
+ export class ConfigurationError extends CommitCLIError {
69
+ constructor(
70
+ message: string,
71
+ suggestion: string = 'Try reconfiguring with the --reconfigure flag.'
72
+ ) {
73
+ super(message, suggestion);
74
+ }
75
+
76
+ static noApiKey(): ConfigurationError {
77
+ return new ConfigurationError(
78
+ 'No API key configured',
79
+ 'Run the CLI to set up your Gemini API key, or use --reconfigure to update it.'
80
+ );
81
+ }
82
+
83
+ static invalidApiKey(): ConfigurationError {
84
+ return new ConfigurationError(
85
+ 'Invalid API key format',
86
+ 'Ensure your Gemini API key is correct. Get one at https://makersuite.google.com/app/apikey'
87
+ );
88
+ }
89
+
90
+ static configFileCorrupted(): ConfigurationError {
91
+ return new ConfigurationError(
92
+ 'Configuration file is corrupted',
93
+ 'Delete ~/.commic/config.json and run the CLI again to reconfigure.'
94
+ );
95
+ }
96
+
97
+ static configSaveFailed(error: Error): ConfigurationError {
98
+ return new ConfigurationError(
99
+ `Failed to save configuration: ${error.message}`,
100
+ 'Check file system permissions for ~/.commic/ directory.'
101
+ );
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Gemini API related errors
107
+ * Thrown when API requests fail or return invalid responses
108
+ */
109
+ export class APIError extends CommitCLIError {
110
+ constructor(
111
+ message: string,
112
+ suggestion: string = 'Check your internet connection and try again.'
113
+ ) {
114
+ super(message, suggestion);
115
+ }
116
+
117
+ static requestFailed(error: Error): APIError {
118
+ return new APIError(
119
+ `Gemini API request failed: ${error.message}`,
120
+ 'Verify your API key is valid and you have internet connectivity.'
121
+ );
122
+ }
123
+
124
+ static rateLimitExceeded(): APIError {
125
+ return new APIError(
126
+ 'API rate limit exceeded',
127
+ 'Wait a few moments before trying again, or check your API quota at https://makersuite.google.com/'
128
+ );
129
+ }
130
+
131
+ static invalidResponse(): APIError {
132
+ return new APIError(
133
+ 'Received invalid response from Gemini API',
134
+ 'Try again. If the problem persists, the API might be experiencing issues.'
135
+ );
136
+ }
137
+
138
+ static authenticationFailed(): APIError {
139
+ return new APIError(
140
+ 'API authentication failed',
141
+ 'Your API key may be invalid or expired. Use --reconfigure to update it.'
142
+ );
143
+ }
144
+
145
+ static timeout(): APIError {
146
+ return new APIError('API request timed out', 'Check your internet connection and try again.');
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Commit message validation errors
152
+ * Thrown when generated messages don't meet Conventional Commits specification
153
+ */
154
+ export class ValidationError extends CommitCLIError {
155
+ constructor(
156
+ message: string,
157
+ suggestion: string = 'This is likely an internal error. Please try again.'
158
+ ) {
159
+ super(message, suggestion);
160
+ }
161
+
162
+ static invalidConventionalCommit(errors: string[]): ValidationError {
163
+ const errorList = errors.join(', ');
164
+ return new ValidationError(
165
+ `Generated commit message doesn't meet Conventional Commits spec: ${errorList}`,
166
+ 'Try generating new suggestions. If this persists, report it as a bug.'
167
+ );
168
+ }
169
+
170
+ static noValidSuggestions(): ValidationError {
171
+ return new ValidationError(
172
+ 'Could not generate valid commit message suggestions',
173
+ 'Try again with different changes, or check if your diff is too large.'
174
+ );
175
+ }
176
+ }
@@ -0,0 +1,246 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { type SimpleGit, simpleGit } from 'simple-git';
4
+ import { GitRepositoryError } from '../errors/CustomErrors.js';
5
+ import type { GitDiff, GitRepository } from '../types/index.js';
6
+
7
+ /**
8
+ * Handles all Git operations including repository discovery, diff retrieval, and commits
9
+ */
10
+ export class GitService {
11
+ /**
12
+ * Find Git repository by walking up the directory tree
13
+ * @param startPath Starting path to search from
14
+ * @returns GitRepository with path and root information
15
+ * @throws GitRepositoryError if no repository found or path not accessible
16
+ */
17
+ async findRepository(startPath: string): Promise<GitRepository> {
18
+ try {
19
+ // Normalize and resolve the path
20
+ const normalizedPath = this.normalizePath(startPath);
21
+ const resolvedPath = resolve(normalizedPath);
22
+
23
+ // Check if path exists and is accessible
24
+ try {
25
+ await fs.access(resolvedPath);
26
+ } catch {
27
+ throw GitRepositoryError.pathNotAccessible(resolvedPath);
28
+ }
29
+
30
+ // Walk up directory tree looking for .git folder
31
+ let currentPath = resolvedPath;
32
+ const root = '/';
33
+
34
+ while (currentPath !== root) {
35
+ const gitPath = join(currentPath, '.git');
36
+
37
+ try {
38
+ const stats = await fs.stat(gitPath);
39
+ if (stats.isDirectory()) {
40
+ // Found .git directory
41
+ return {
42
+ path: resolvedPath,
43
+ rootPath: currentPath,
44
+ };
45
+ }
46
+ } catch {
47
+ // .git not found at this level, continue up
48
+ }
49
+
50
+ // Move up one directory
51
+ const parentPath = dirname(currentPath);
52
+ if (parentPath === currentPath) {
53
+ // Reached root without finding .git
54
+ break;
55
+ }
56
+ currentPath = parentPath;
57
+ }
58
+
59
+ // No .git directory found
60
+ throw GitRepositoryError.noRepositoryFound(resolvedPath);
61
+ } catch (error) {
62
+ if (error instanceof GitRepositoryError) {
63
+ throw error;
64
+ }
65
+ throw GitRepositoryError.pathNotAccessible(startPath);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Normalize path by removing trailing slashes and resolving relative paths
71
+ * @param path Path to normalize
72
+ * @returns Normalized path
73
+ */
74
+ private normalizePath(path: string): string {
75
+ // Remove trailing slashes
76
+ let normalized = path.replace(/\/+$/, '');
77
+
78
+ // Handle empty string (current directory)
79
+ if (normalized === '') {
80
+ normalized = '.';
81
+ }
82
+
83
+ return normalized;
84
+ }
85
+
86
+ /**
87
+ * Check if repository has any commits
88
+ * @param repoPath Path to repository root
89
+ * @returns true if repository has commits, false if empty
90
+ * @throws GitRepositoryError if check fails
91
+ */
92
+ async hasCommits(repoPath: string): Promise<boolean> {
93
+ try {
94
+ const git: SimpleGit = simpleGit(repoPath);
95
+ const log = await git.log({ maxCount: 1 });
96
+ return log.total > 0;
97
+ } catch (_error) {
98
+ // If git log fails, repository likely has no commits
99
+ return false;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Get diff between HEAD and current working state
105
+ * @param repoPath Path to repository root
106
+ * @returns GitDiff with staged, unstaged changes and hasChanges flag
107
+ * @throws GitRepositoryError if diff retrieval fails
108
+ */
109
+ async getDiff(repoPath: string): Promise<GitDiff> {
110
+ try {
111
+ const git: SimpleGit = simpleGit(repoPath);
112
+
113
+ // Get staged changes (diff --cached)
114
+ const staged = await git.diff(['--cached']);
115
+
116
+ // Get unstaged changes (diff)
117
+ const unstaged = await git.diff();
118
+
119
+ const hasChanges = staged.length > 0 || unstaged.length > 0;
120
+
121
+ return {
122
+ staged,
123
+ unstaged,
124
+ hasChanges,
125
+ };
126
+ } catch (error) {
127
+ throw new GitRepositoryError(
128
+ `Failed to retrieve Git diff: ${(error as Error).message}`,
129
+ 'Ensure you are in a valid Git repository with proper permissions.'
130
+ );
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Stage all changes in the repository
136
+ * @param repoPath Path to repository root
137
+ * @throws GitRepositoryError if staging fails
138
+ */
139
+ async stageAll(repoPath: string): Promise<void> {
140
+ try {
141
+ const git: SimpleGit = simpleGit(repoPath);
142
+ await git.add('.');
143
+ } catch (error) {
144
+ throw new GitRepositoryError(
145
+ `Failed to stage changes: ${(error as Error).message}`,
146
+ 'Check if you have permission to modify the repository.'
147
+ );
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Execute git commit with the provided message
153
+ * @param repoPath Path to repository root
154
+ * @param message Commit message
155
+ * @returns Commit hash
156
+ * @throws GitRepositoryError if commit fails
157
+ */
158
+ async commit(repoPath: string, message: string): Promise<string> {
159
+ try {
160
+ const git: SimpleGit = simpleGit(repoPath);
161
+ const result = await git.commit(message);
162
+
163
+ // Extract commit hash from result
164
+ const commitHash = result.commit || 'unknown';
165
+
166
+ return commitHash;
167
+ } catch (error) {
168
+ throw GitRepositoryError.commitFailed((error as Error).message);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Get current branch name
174
+ * @param repoPath Path to repository root
175
+ * @returns Current branch name
176
+ */
177
+ async getCurrentBranch(repoPath: string): Promise<string> {
178
+ try {
179
+ const git: SimpleGit = simpleGit(repoPath);
180
+ const branch = await git.revparse(['--abbrev-ref', 'HEAD']);
181
+ return branch.trim();
182
+ } catch (_error) {
183
+ return 'unknown';
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Get repository name from path
189
+ * @param repoPath Path to repository root
190
+ * @returns Repository name (last directory in path)
191
+ */
192
+ getRepositoryName(repoPath: string): string {
193
+ const parts = repoPath.split('/').filter((p) => p.length > 0);
194
+ return parts[parts.length - 1] || 'unknown';
195
+ }
196
+
197
+ /**
198
+ * Get diff statistics (files changed, insertions, deletions)
199
+ * @param repoPath Path to repository root
200
+ * @returns Statistics object
201
+ */
202
+ async getDiffStats(repoPath: string): Promise<{
203
+ filesChanged: number;
204
+ insertions: number;
205
+ deletions: number;
206
+ }> {
207
+ try {
208
+ const git: SimpleGit = simpleGit(repoPath);
209
+ const diffSummary = await git.diffSummary();
210
+
211
+ return {
212
+ filesChanged: diffSummary.files.length,
213
+ insertions: diffSummary.insertions,
214
+ deletions: diffSummary.deletions,
215
+ };
216
+ } catch (_error) {
217
+ return {
218
+ filesChanged: 0,
219
+ insertions: 0,
220
+ deletions: 0,
221
+ };
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Get remote repository URL
227
+ * @param repoPath Path to repository root
228
+ * @param remoteName Remote name (default: 'origin')
229
+ * @returns Remote URL or null if no remote configured
230
+ */
231
+ async getRemoteUrl(repoPath: string, remoteName: string = 'origin'): Promise<string | null> {
232
+ try {
233
+ const git: SimpleGit = simpleGit(repoPath);
234
+ const remotes = await git.getRemotes(true);
235
+ const remote = remotes.find((r) => r.name === remoteName);
236
+
237
+ if (remote?.refs?.fetch) {
238
+ return remote.refs.fetch;
239
+ }
240
+
241
+ return null;
242
+ } catch (_error) {
243
+ return null;
244
+ }
245
+ }
246
+ }
package/src/index.ts ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { ConfigManager } from './config/ConfigManager.js';
5
+ import { GitService } from './git/GitService.js';
6
+ import { MainOrchestrator } from './orchestrator/MainOrchestrator.js';
7
+ import type { CLIOptions } from './types/index.js';
8
+ import { UIManager } from './ui/UIManager.js';
9
+
10
+ /**
11
+ * Main entry point for the Commit CLI
12
+ */
13
+ async function main() {
14
+ const program = new Command();
15
+
16
+ program
17
+ .name('commic')
18
+ .description('AI-powered Git commit message generator with Conventional Commits support')
19
+ .version('1.0.0')
20
+ .argument('[path]', 'Path to Git repository', '.')
21
+ .option('-r, --reconfigure', 'Reconfigure API key and model settings')
22
+ .action(async (path: string, options: { reconfigure?: boolean }) => {
23
+ try {
24
+ // Create service instances
25
+ const configManager = new ConfigManager();
26
+ const gitService = new GitService();
27
+ const uiManager = new UIManager();
28
+
29
+ // Create orchestrator
30
+ const orchestrator = new MainOrchestrator(configManager, gitService, uiManager);
31
+
32
+ // Build CLI options
33
+ const cliOptions: CLIOptions = {
34
+ path,
35
+ reconfigure: options.reconfigure,
36
+ };
37
+
38
+ // Execute workflow
39
+ await orchestrator.execute(cliOptions);
40
+ } catch (_error) {
41
+ // Top-level error handler
42
+ // Orchestrator already handles and displays errors
43
+ // This is just to ensure we exit with error code
44
+ process.exit(1);
45
+ }
46
+ });
47
+
48
+ await program.parseAsync(process.argv);
49
+ }
50
+
51
+ // Run the CLI
52
+ main().catch((error) => {
53
+ console.error('Fatal error:', error.message);
54
+ process.exit(1);
55
+ });