@ksw8954/git-ai-commit 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 (77) hide show
  1. package/AGENTS.md +38 -0
  2. package/CRUSH.md +28 -0
  3. package/Makefile +32 -0
  4. package/README.md +145 -0
  5. package/dist/commands/ai.d.ts +35 -0
  6. package/dist/commands/ai.d.ts.map +1 -0
  7. package/dist/commands/ai.js +206 -0
  8. package/dist/commands/ai.js.map +1 -0
  9. package/dist/commands/commit.d.ts +17 -0
  10. package/dist/commands/commit.d.ts.map +1 -0
  11. package/dist/commands/commit.js +126 -0
  12. package/dist/commands/commit.js.map +1 -0
  13. package/dist/commands/config.d.ts +33 -0
  14. package/dist/commands/config.d.ts.map +1 -0
  15. package/dist/commands/config.js +141 -0
  16. package/dist/commands/config.js.map +1 -0
  17. package/dist/commands/configCommand.d.ts +20 -0
  18. package/dist/commands/configCommand.d.ts.map +1 -0
  19. package/dist/commands/configCommand.js +108 -0
  20. package/dist/commands/configCommand.js.map +1 -0
  21. package/dist/commands/git.d.ts +26 -0
  22. package/dist/commands/git.d.ts.map +1 -0
  23. package/dist/commands/git.js +150 -0
  24. package/dist/commands/git.js.map +1 -0
  25. package/dist/commands/loadEnv.d.ts +2 -0
  26. package/dist/commands/loadEnv.d.ts.map +1 -0
  27. package/dist/commands/loadEnv.js +11 -0
  28. package/dist/commands/loadEnv.js.map +1 -0
  29. package/dist/commands/prCommand.d.ts +16 -0
  30. package/dist/commands/prCommand.d.ts.map +1 -0
  31. package/dist/commands/prCommand.js +61 -0
  32. package/dist/commands/prCommand.js.map +1 -0
  33. package/dist/commands/tag.d.ts +17 -0
  34. package/dist/commands/tag.d.ts.map +1 -0
  35. package/dist/commands/tag.js +127 -0
  36. package/dist/commands/tag.js.map +1 -0
  37. package/dist/index.d.ts +3 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +23 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/prompts/commit.d.ts +3 -0
  42. package/dist/prompts/commit.d.ts.map +1 -0
  43. package/dist/prompts/commit.js +101 -0
  44. package/dist/prompts/commit.js.map +1 -0
  45. package/dist/prompts/pr.d.ts +3 -0
  46. package/dist/prompts/pr.d.ts.map +1 -0
  47. package/dist/prompts/pr.js +58 -0
  48. package/dist/prompts/pr.js.map +1 -0
  49. package/dist/prompts/tag.d.ts +3 -0
  50. package/dist/prompts/tag.d.ts.map +1 -0
  51. package/dist/prompts/tag.js +42 -0
  52. package/dist/prompts/tag.js.map +1 -0
  53. package/eslint.config.js +35 -0
  54. package/jest.config.js +16 -0
  55. package/package.json +51 -0
  56. package/src/__tests__/ai.test.ts +185 -0
  57. package/src/__tests__/commitCommand.test.ts +155 -0
  58. package/src/__tests__/config.test.ts +238 -0
  59. package/src/__tests__/git.test.ts +88 -0
  60. package/src/__tests__/integration.test.ts +138 -0
  61. package/src/__tests__/prCommand.test.ts +121 -0
  62. package/src/__tests__/tagCommand.test.ts +197 -0
  63. package/src/commands/ai.ts +266 -0
  64. package/src/commands/commit.ts +215 -0
  65. package/src/commands/config.ts +182 -0
  66. package/src/commands/configCommand.ts +139 -0
  67. package/src/commands/git.ts +174 -0
  68. package/src/commands/history.ts +82 -0
  69. package/src/commands/loadEnv.ts +5 -0
  70. package/src/commands/log.ts +71 -0
  71. package/src/commands/prCommand.ts +108 -0
  72. package/src/commands/tag.ts +230 -0
  73. package/src/index.ts +29 -0
  74. package/src/prompts/commit.ts +105 -0
  75. package/src/prompts/pr.ts +64 -0
  76. package/src/prompts/tag.ts +48 -0
  77. package/tsconfig.json +19 -0
@@ -0,0 +1,138 @@
1
+ import { loadEnv } from '../commands/loadEnv';
2
+
3
+ // Load environment variables from .env file before tests
4
+ loadEnv();
5
+
6
+ import { AIService } from '../commands/ai';
7
+ import { GitService } from '../commands/git';
8
+ import { ConfigService } from '../commands/config';
9
+
10
+ const hasApiKey = Boolean(
11
+ process.env.OPENAI_API_KEY ||
12
+ process.env.AI_API_KEY
13
+ );
14
+
15
+ describe('Integration Tests with Real API', () => {
16
+ let aiService: AIService;
17
+
18
+ beforeEach(() => {
19
+ if (!hasApiKey) {
20
+ return;
21
+ }
22
+
23
+ const config = ConfigService.getConfig();
24
+ ConfigService.validateConfig(config);
25
+ aiService = new AIService({
26
+ apiKey: config.apiKey!,
27
+ baseURL: config.baseURL,
28
+ model: config.model,
29
+ language: config.language
30
+ });
31
+ });
32
+
33
+ const describeIfApiKey = hasApiKey ? describe : describe.skip;
34
+
35
+ describeIfApiKey('Real API Integration', () => {
36
+ it('should generate commit message using real API', async () => {
37
+ // Sample git diff for testing
38
+ const sampleDiff = `diff --git a/package.json b/package.json
39
+ index 1a2b3c4..5d6e7f8 100644
40
+ --- a/package.json
41
+ +++ b/package.json
42
+ @@ -1,6 +1,6 @@
43
+ {
44
+ "name": "test-project",
45
+ - "version": "1.0.0",
46
+ + "version": "1.1.0",
47
+ "description": "Test project",
48
+ "main": "index.js"
49
+ }
50
+ diff --git a/src/index.ts b/src/index.ts
51
+ index abc123..def456 100644
52
+ --- a/src/index.ts
53
+ +++ b/src/index.ts
54
+ @@ -1,5 +1,8 @@
55
+ console.log('Hello World');
56
+ +
57
+ +// Add new feature
58
+ +console.log('New feature added');
59
+ `;
60
+
61
+ console.log('Testing with real API...');
62
+ console.log('Sample diff:', sampleDiff);
63
+
64
+ const result = await aiService.generateCommitMessage(sampleDiff);
65
+
66
+ console.log('API Response:', result);
67
+
68
+ expect(result.success).toBe(true);
69
+ expect(result.message).toBeDefined();
70
+ expect(result.message!.length).toBeGreaterThan(0);
71
+
72
+ // Check if the message follows conventional commit format
73
+ expect(result.message).toMatch(/^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\(.+\))?!?: .+/);
74
+ }, 30000); // 30 second timeout for real API call
75
+
76
+ it('should handle API errors gracefully', async () => {
77
+ // Test with empty diff to potentially trigger API error
78
+ const emptyDiff = '';
79
+
80
+ const result = await aiService.generateCommitMessage(emptyDiff);
81
+
82
+ console.log('Empty diff test result:', result);
83
+
84
+ // The API might still return a message, but if it fails, it should handle gracefully
85
+ if (!result.success) {
86
+ expect(result.error).toBeDefined();
87
+ expect(result.error!.length).toBeGreaterThan(0);
88
+ }
89
+ }, 60000);
90
+ });
91
+
92
+ describe('Git Service Integration', () => {
93
+ it('should get actual git diff if available', async () => {
94
+ const result = await GitService.getStagedDiff();
95
+
96
+ console.log('Git diff result:', result);
97
+
98
+ // If there are staged changes, success should be true
99
+ // If no staged changes, success should be false with appropriate error
100
+ if (result.success) {
101
+ expect(result.diff).toBeDefined();
102
+ expect(result.diff!.length).toBeGreaterThan(0);
103
+ } else {
104
+ expect(result.error).toBeDefined();
105
+ expect(result.error).toContain('No staged changes');
106
+ }
107
+ });
108
+ });
109
+
110
+ describe('Configuration Integration', () => {
111
+ const itIfApiKey = hasApiKey ? it : it.skip;
112
+
113
+ itIfApiKey('should load configuration from environment', () => {
114
+ const config = ConfigService.getConfig();
115
+ ConfigService.validateConfig(config);
116
+
117
+ console.log('Loaded config:', {
118
+ apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...${config.apiKey.slice(-4)}` : 'undefined',
119
+ baseURL: config.baseURL || 'undefined',
120
+ model: config.model || 'undefined',
121
+ language: config.language,
122
+ autoPush: config.autoPush
123
+ });
124
+
125
+ expect(config.apiKey).toBeDefined();
126
+ expect(config.apiKey!.length).toBeGreaterThan(0);
127
+
128
+ // Should have model from .env or default
129
+ expect(config.model).toBeDefined();
130
+ });
131
+
132
+ itIfApiKey('should validate configuration successfully', () => {
133
+ const config = ConfigService.getConfig();
134
+
135
+ expect(() => ConfigService.validateConfig(config)).not.toThrow();
136
+ });
137
+ });
138
+ });
@@ -0,0 +1,121 @@
1
+ import { PullRequestCommand } from '../commands/prCommand';
2
+ import { GitService } from '../commands/git';
3
+ import { AIService } from '../commands/ai';
4
+ import { ConfigService } from '../commands/config';
5
+
6
+ const mockGeneratePullRequestMessage = jest.fn();
7
+
8
+ jest.mock('../commands/git', () => ({
9
+ GitService: {
10
+ getBranchDiff: jest.fn()
11
+ }
12
+ }));
13
+
14
+ jest.mock('../commands/ai', () => ({
15
+ AIService: jest.fn().mockImplementation(() => ({
16
+ generatePullRequestMessage: mockGeneratePullRequestMessage
17
+ }))
18
+ }));
19
+
20
+ jest.mock('../commands/config', () => ({
21
+ ConfigService: {
22
+ getConfig: jest.fn(),
23
+ validateConfig: jest.fn()
24
+ }
25
+ }));
26
+
27
+ describe('PullRequestCommand', () => {
28
+ const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => undefined) as any);
29
+
30
+ beforeEach(() => {
31
+ jest.clearAllMocks();
32
+
33
+ (ConfigService.getConfig as jest.Mock).mockReturnValue({
34
+ apiKey: 'config-key',
35
+ baseURL: 'https://api.test',
36
+ model: 'test-model',
37
+ language: 'ko',
38
+ autoPush: false
39
+ });
40
+
41
+ (ConfigService.validateConfig as jest.Mock).mockReturnValue(undefined);
42
+
43
+ (GitService.getBranchDiff as jest.Mock).mockResolvedValue({
44
+ success: true,
45
+ diff: 'diff --git a/file b/file'
46
+ });
47
+
48
+ mockGeneratePullRequestMessage.mockResolvedValue({
49
+ success: true,
50
+ message: 'Add caching layer\n\n## Summary\n- ...\n\n## Testing\n- ...'
51
+ });
52
+ });
53
+
54
+ afterAll(() => {
55
+ exitSpy.mockRestore();
56
+ });
57
+
58
+ const createCommand = () => new PullRequestCommand();
59
+
60
+ it('prints generated pull request message on success', async () => {
61
+ const command = createCommand();
62
+ const logSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined);
63
+
64
+ try {
65
+ await (command as any).handlePullRequest({ base: 'main', compare: 'feature/cache' });
66
+
67
+ expect(ConfigService.validateConfig).toHaveBeenCalledWith({
68
+ apiKey: 'config-key',
69
+ language: 'ko'
70
+ });
71
+ expect(GitService.getBranchDiff).toHaveBeenCalledWith('main', 'feature/cache');
72
+ expect(mockGeneratePullRequestMessage).toHaveBeenCalledWith(
73
+ 'main',
74
+ 'feature/cache',
75
+ 'diff --git a/file b/file'
76
+ );
77
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Add caching layer'));
78
+ expect(exitSpy).not.toHaveBeenCalled();
79
+ } finally {
80
+ logSpy.mockRestore();
81
+ }
82
+ });
83
+
84
+ it('exits when diff retrieval fails', async () => {
85
+ (GitService.getBranchDiff as jest.Mock).mockResolvedValueOnce({
86
+ success: false,
87
+ error: 'diff error'
88
+ });
89
+
90
+ const command = createCommand();
91
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
92
+
93
+ try {
94
+ await (command as any).handlePullRequest({ base: 'main', compare: 'feature/cache' });
95
+
96
+ expect(errorSpy).toHaveBeenCalledWith('Error:', 'diff error');
97
+ expect(exitSpy).toHaveBeenCalledWith(1);
98
+ } finally {
99
+ errorSpy.mockRestore();
100
+ }
101
+ });
102
+
103
+ it('exits when AI generation fails', async () => {
104
+ mockGeneratePullRequestMessage.mockResolvedValueOnce({
105
+ success: false,
106
+ error: 'ai error'
107
+ });
108
+
109
+ const command = createCommand();
110
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
111
+
112
+ try {
113
+ await (command as any).handlePullRequest({ base: 'main', compare: 'feature/cache' });
114
+
115
+ expect(errorSpy).toHaveBeenCalledWith('Error:', 'ai error');
116
+ expect(exitSpy).toHaveBeenCalledWith(1);
117
+ } finally {
118
+ errorSpy.mockRestore();
119
+ }
120
+ });
121
+ });
@@ -0,0 +1,197 @@
1
+ import { TagCommand } from '../commands/tag';
2
+ import { GitService } from '../commands/git';
3
+ import { AIService } from '../commands/ai';
4
+ import { ConfigService } from '../commands/config';
5
+
6
+ jest.mock('../commands/git', () => ({
7
+ GitService: {
8
+ getLatestTag: jest.fn(),
9
+ getCommitSummariesSince: jest.fn(),
10
+ createAnnotatedTag: jest.fn(),
11
+ pushTag: jest.fn()
12
+ }
13
+ }));
14
+
15
+ const mockGenerateTagNotes = jest.fn();
16
+
17
+ jest.mock('../commands/ai', () => ({
18
+ AIService: jest.fn().mockImplementation(() => ({
19
+ generateTagNotes: mockGenerateTagNotes
20
+ }))
21
+ }));
22
+
23
+ jest.mock('../commands/config', () => ({
24
+ ConfigService: {
25
+ getConfig: jest.fn(),
26
+ validateConfig: jest.fn()
27
+ }
28
+ }));
29
+
30
+ describe('TagCommand', () => {
31
+ const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => undefined) as any);
32
+ let confirmSpy: jest.SpyInstance;
33
+
34
+ beforeEach(() => {
35
+ jest.clearAllMocks();
36
+
37
+ (ConfigService.getConfig as jest.Mock).mockReturnValue({
38
+ apiKey: 'env-key',
39
+ baseURL: 'https://env.test',
40
+ model: 'env-model',
41
+ language: 'ko',
42
+ autoPush: false
43
+ });
44
+
45
+ (ConfigService.validateConfig as jest.Mock).mockReturnValue(undefined);
46
+
47
+ // Confirm creation by default
48
+ jest
49
+ .spyOn(TagCommand.prototype as any, 'confirmTagCreate')
50
+ .mockResolvedValue(true);
51
+
52
+ // Do not push by default
53
+ confirmSpy = jest
54
+ .spyOn(TagCommand.prototype as any, 'confirmTagPush')
55
+ .mockResolvedValue(false);
56
+ });
57
+
58
+ afterEach(() => {
59
+ confirmSpy.mockRestore();
60
+ });
61
+
62
+ afterAll(() => {
63
+ exitSpy.mockRestore();
64
+ });
65
+
66
+ const getTagCommand = () => new TagCommand();
67
+
68
+ it('should create tag with provided message without invoking AI', async () => {
69
+ (GitService.createAnnotatedTag as jest.Mock).mockResolvedValue(true);
70
+
71
+ const command = getTagCommand();
72
+
73
+ await (command as any).handleTag('v1.2.3', { message: 'Manual release notes' });
74
+
75
+ expect(GitService.createAnnotatedTag).toHaveBeenCalledWith('v1.2.3', 'Manual release notes');
76
+ expect(AIService).not.toHaveBeenCalled();
77
+ expect(GitService.pushTag).not.toHaveBeenCalled();
78
+ expect(exitSpy).not.toHaveBeenCalled();
79
+ });
80
+
81
+ it('should generate tag notes using AI when message is not provided', async () => {
82
+ (GitService.getCommitSummariesSince as jest.Mock).mockResolvedValue({
83
+ success: true,
84
+ log: '- feat: add feature\n- fix: bug fix'
85
+ });
86
+ (GitService.createAnnotatedTag as jest.Mock).mockResolvedValue(true);
87
+
88
+ mockGenerateTagNotes.mockResolvedValue({
89
+ success: true,
90
+ notes: '- Added feature\n- Fixed bug'
91
+ });
92
+
93
+ const command = getTagCommand();
94
+
95
+ await (command as any).handleTag('v1.3.0', {
96
+ apiKey: 'test-key',
97
+ baseUrl: 'https://api.test',
98
+ model: 'gpt-test',
99
+ baseTag: 'v1.2.0'
100
+ });
101
+
102
+ expect(GitService.getCommitSummariesSince).toHaveBeenCalledWith('v1.2.0');
103
+ expect(AIService).toHaveBeenCalledWith({
104
+ apiKey: 'test-key',
105
+ baseURL: 'https://api.test',
106
+ model: 'gpt-test',
107
+ language: 'ko'
108
+ });
109
+ expect(mockGenerateTagNotes).toHaveBeenCalledWith('v1.3.0', '- feat: add feature\n- fix: bug fix', undefined);
110
+ expect(GitService.createAnnotatedTag).toHaveBeenCalledWith('v1.3.0', '- Added feature\n- Fixed bug');
111
+ expect(GitService.pushTag).not.toHaveBeenCalled();
112
+ });
113
+
114
+ it('should exit when commit history cannot be read', async () => {
115
+ (GitService.getCommitSummariesSince as jest.Mock).mockResolvedValue({
116
+ success: false,
117
+ error: 'No commits found'
118
+ });
119
+ (GitService.getLatestTag as jest.Mock).mockResolvedValue({
120
+ success: false,
121
+ error: 'No tags'
122
+ });
123
+
124
+ const command = getTagCommand();
125
+
126
+ await (command as any).handleTag('v2.0.0', { apiKey: 'test-key' });
127
+
128
+ expect(exitSpy).toHaveBeenCalledWith(1);
129
+ expect(GitService.createAnnotatedTag).not.toHaveBeenCalled();
130
+ expect(GitService.pushTag).not.toHaveBeenCalled();
131
+ });
132
+
133
+ it('should use environment config when API key is not provided', async () => {
134
+ (GitService.getCommitSummariesSince as jest.Mock).mockResolvedValue({
135
+ success: true,
136
+ log: '- chore: update deps'
137
+ });
138
+ (GitService.createAnnotatedTag as jest.Mock).mockResolvedValue(true);
139
+ mockGenerateTagNotes.mockResolvedValue({
140
+ success: true,
141
+ notes: '- Updated dependencies'
142
+ });
143
+ (ConfigService.getConfig as jest.Mock).mockReturnValue({
144
+ apiKey: 'env-key',
145
+ baseURL: 'https://env.test',
146
+ model: 'env-model',
147
+ language: 'ko',
148
+ autoPush: false
149
+ });
150
+ (GitService.getLatestTag as jest.Mock).mockResolvedValue({
151
+ success: true,
152
+ tag: 'v1.0.0'
153
+ });
154
+
155
+ const command = getTagCommand();
156
+
157
+ await (command as any).handleTag('v1.1.0', {});
158
+
159
+ expect(GitService.getLatestTag).toHaveBeenCalled();
160
+ expect(AIService).toHaveBeenCalledWith({
161
+ apiKey: 'env-key',
162
+ baseURL: 'https://env.test',
163
+ model: 'env-model',
164
+ language: 'ko'
165
+ });
166
+ expect(GitService.createAnnotatedTag).toHaveBeenCalledWith('v1.1.0', '- Updated dependencies');
167
+ expect(GitService.pushTag).not.toHaveBeenCalled();
168
+ });
169
+
170
+ it('should push tag when user confirms', async () => {
171
+ (GitService.createAnnotatedTag as jest.Mock).mockResolvedValue(true);
172
+ (GitService.pushTag as jest.Mock).mockResolvedValue(true);
173
+ confirmSpy.mockResolvedValueOnce(true);
174
+
175
+ const command = getTagCommand();
176
+
177
+ await (command as any).handleTag('v2.0.0', { message: 'Release notes' });
178
+
179
+ expect(GitService.createAnnotatedTag).toHaveBeenCalledWith('v2.0.0', 'Release notes');
180
+ expect(GitService.pushTag).toHaveBeenCalledWith('v2.0.0');
181
+ expect(exitSpy).not.toHaveBeenCalled();
182
+ });
183
+
184
+ it('should exit when tag push fails after confirmation', async () => {
185
+ (GitService.createAnnotatedTag as jest.Mock).mockResolvedValue(true);
186
+ (GitService.pushTag as jest.Mock).mockResolvedValue(false);
187
+ confirmSpy.mockResolvedValueOnce(true);
188
+
189
+ const command = getTagCommand();
190
+
191
+ await (command as any).handleTag('v3.0.0', { message: 'Release notes' });
192
+
193
+ expect(GitService.createAnnotatedTag).toHaveBeenCalledWith('v3.0.0', 'Release notes');
194
+ expect(GitService.pushTag).toHaveBeenCalledWith('v3.0.0');
195
+ expect(exitSpy).toHaveBeenCalledWith(1);
196
+ });
197
+ });