@traisetech/autopilot 0.1.6 → 0.1.8

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.
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Google Gemini AI Integration for Autopilot
3
+ * Generates commit messages using the Gemini Pro model
4
+ */
5
+
6
+ const logger = require('../utils/logger');
7
+
8
+ const GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent';
9
+
10
+ /**
11
+ * Generate a commit message using Gemini API
12
+ * @param {string} diff - The git diff content
13
+ * @param {string} apiKey - Google Gemini API Key
14
+ * @returns {Promise<string>} Generated commit message
15
+ */
16
+ async function generateAICommitMessage(diff, apiKey) {
17
+ if (!diff || !diff.trim()) {
18
+ return 'chore: update changes';
19
+ }
20
+
21
+ // Truncate diff if it's too large (Gemini has token limits, though high)
22
+ // A safe limit might be around 30k chars for now to be safe and fast
23
+ const truncatedDiff = diff.length > 30000 ? diff.slice(0, 30000) + '\n...(truncated)' : diff;
24
+
25
+ const prompt = `
26
+ You are an expert software engineer.
27
+ Generate a concise, standardized commit message following the Conventional Commits specification based on the provided git diff.
28
+
29
+ Rules:
30
+ 1. Format: <type>(<scope>): <subject>
31
+ 2. Keep the subject line under 72 characters.
32
+ 3. If there are multiple changes, use a bulleted body.
33
+ 4. Detect breaking changes and add "BREAKING CHANGE:" footer if necessary.
34
+ 5. Use types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert.
35
+ 6. Return ONLY the commit message, no explanations or markdown code blocks.
36
+
37
+ Diff:
38
+ ${truncatedDiff}
39
+ `;
40
+
41
+ try {
42
+ const response = await fetch(`${GEMINI_API_URL}?key=${apiKey}`, {
43
+ method: 'POST',
44
+ headers: {
45
+ 'Content-Type': 'application/json',
46
+ },
47
+ body: JSON.stringify({
48
+ contents: [{
49
+ parts: [{
50
+ text: prompt
51
+ }]
52
+ }],
53
+ generationConfig: {
54
+ temperature: 0.2,
55
+ maxOutputTokens: 256,
56
+ }
57
+ })
58
+ });
59
+
60
+ if (!response.ok) {
61
+ const errorData = await response.json().catch(() => ({}));
62
+ throw new Error(`Gemini API Error: ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`);
63
+ }
64
+
65
+ const data = await response.json();
66
+
67
+ if (!data.candidates || data.candidates.length === 0 || !data.candidates[0].content) {
68
+ throw new Error('No response content from Gemini');
69
+ }
70
+
71
+ let message = data.candidates[0].content.parts[0].text.trim();
72
+
73
+ // Cleanup markdown if present (e.g. ```git commit ... ```)
74
+ message = message.replace(/^```[a-z]*\n?/, '').replace(/\n?```$/, '').trim();
75
+
76
+ return message;
77
+
78
+ } catch (error) {
79
+ logger.error(`AI Generation failed: ${error.message}`);
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Validate Gemini API Key
86
+ * @param {string} apiKey
87
+ * @returns {Promise<boolean>}
88
+ */
89
+ async function validateApiKey(apiKey) {
90
+ try {
91
+ // Simple test call with empty prompt
92
+ const response = await fetch(`${GEMINI_API_URL}?key=${apiKey}`, {
93
+ method: 'POST',
94
+ headers: { 'Content-Type': 'application/json' },
95
+ body: JSON.stringify({
96
+ contents: [{ parts: [{ text: "Hi" }] }],
97
+ generationConfig: { maxOutputTokens: 1 }
98
+ })
99
+ });
100
+ return response.ok;
101
+ } catch (e) {
102
+ return false;
103
+ }
104
+ }
105
+
106
+ module.exports = {
107
+ generateAICommitMessage,
108
+ validateApiKey
109
+ };
package/src/core/git.js CHANGED
@@ -1,154 +1,180 @@
1
- /**
2
- * Git helper module - Clean, testable Git operations
3
- * Built by Praise Masunga (PraiseTechzw)
4
- */
5
-
6
- const { execa } = require('execa');
7
-
8
- /**
9
- * Get current branch name
10
- * @param {string} root - Repository root path
11
- * @returns {Promise<string|null>} Branch name or null on error
12
- */
13
- async function getBranch(root) {
14
- try {
15
- const { stdout } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root });
16
- return stdout.trim();
17
- } catch (error) {
18
- return null;
19
- }
20
- }
21
-
22
- /**
23
- * Check if repository has uncommitted changes
24
- * @param {string} root - Repository root path
25
- * @returns {Promise<boolean>} True if there are changes
26
- */
27
- async function hasChanges(root) {
28
- try {
29
- const { stdout } = await execa('git', ['status', '--porcelain'], { cwd: root });
30
- return stdout.trim().length > 0;
31
- } catch (error) {
32
- return false;
33
- }
34
- }
35
-
36
- /**
37
- * Get porcelain status - parsed list of changed files
38
- * @param {string} root - Repository root path
39
- * @returns {Promise<{ok: boolean, files: string[], raw: string}>} Status object
40
- */
41
- async function getPorcelainStatus(root) {
42
- try {
43
- const { stdout } = await execa('git', ['status', '--porcelain'], { cwd: root });
44
- const raw = stdout.trim();
45
-
46
- if (!raw) {
47
- return { ok: true, files: [], raw: '' };
48
- }
49
-
50
- const files = raw
51
- .split(/\r?\n/)
52
- .map(line => line.slice(3).trim()); // Remove status prefix (XY + space)
53
-
54
- return { ok: true, files, raw };
55
- } catch (error) {
56
- return { ok: false, files: [], raw: error.message };
57
- }
58
- }
59
-
60
- /**
61
- * Stage all changes (git add -A)
62
- * @param {string} root - Repository root path
63
- * @returns {Promise<{ok: boolean, stdout: string, stderr: string}>} Result object
64
- */
65
- async function addAll(root) {
66
- try {
67
- const { stdout, stderr } = await execa('git', ['add', '-A'], { cwd: root });
68
- return { ok: true, stdout, stderr };
69
- } catch (error) {
70
- return { ok: false, stdout: '', stderr: error.message };
71
- }
72
- }
73
-
74
- /**
75
- * Commit staged changes
76
- * @param {string} root - Repository root path
77
- * @param {string} message - Commit message
78
- * @returns {Promise<{ok: boolean, stdout: string, stderr: string}>} Result object
79
- */
80
- async function commit(root, message) {
81
- try {
82
- const { stdout, stderr } = await execa('git', ['commit', '-m', message], { cwd: root });
83
- return { ok: true, stdout, stderr };
84
- } catch (error) {
85
- return { ok: false, stdout: '', stderr: error.message };
86
- }
87
- }
88
-
89
- /**
90
- * Fetch updates from remote
91
- * @param {string} root - Repository root path
92
- * @returns {Promise<{ok: boolean, stdout: string, stderr: string}>} Result object
93
- */
94
- async function fetch(root) {
95
- try {
96
- const { stdout, stderr } = await execa('git', ['fetch'], { cwd: root });
97
- return { ok: true, stdout, stderr };
98
- } catch (error) {
99
- return { ok: false, stdout: '', stderr: error.message };
100
- }
101
- }
102
-
103
- /**
104
- * Check if remote is ahead/behind
105
- * @param {string} root - Repository root path
106
- * @returns {Promise<{ok: boolean, ahead: boolean, behind: boolean, raw: string}>} Status object
107
- */
108
- async function isRemoteAhead(root) {
109
- try {
110
- const branch = await getBranch(root);
111
- if (!branch) return { ok: false, ahead: false, behind: false, raw: 'No branch' };
112
-
113
- // Ensure we have latest info
114
- await fetch(root);
115
-
116
- const { stdout } = await execa('git', ['rev-list', '--left-right', '--count', `${branch}...origin/${branch}`], { cwd: root });
117
- const [aheadCount, behindCount] = stdout.trim().split(/\s+/).map(Number);
118
-
119
- return {
120
- ok: true,
121
- ahead: aheadCount > 0,
122
- behind: behindCount > 0,
123
- raw: stdout.trim()
124
- };
125
- } catch (error) {
126
- return { ok: false, ahead: false, behind: false, raw: error.message };
127
- }
128
- }
129
-
130
- /**
131
- * Push changes to remote
132
- * @param {string} root - Repository root path
133
- * @param {string} branch - Branch to push
134
- * @returns {Promise<{ok: boolean, stdout: string, stderr: string}>} Result object
135
- */
136
- async function push(root, branch) {
137
- try {
138
- const { stdout, stderr } = await execa('git', ['push', 'origin', branch], { cwd: root });
139
- return { ok: true, stdout, stderr };
140
- } catch (error) {
141
- return { ok: false, stdout: '', stderr: error.message };
142
- }
143
- }
144
-
145
- module.exports = {
146
- getBranch,
147
- hasChanges,
148
- getPorcelainStatus,
149
- addAll,
150
- commit,
151
- fetch,
152
- isRemoteAhead,
153
- push,
154
- };
1
+ /**
2
+ * Git helper module - Clean, testable Git operations
3
+ * Built by Praise Masunga (PraiseTechzw)
4
+ */
5
+
6
+ const execa = require('execa');
7
+
8
+ /**
9
+ * Get current branch name
10
+ * @param {string} root - Repository root path
11
+ * @returns {Promise<string|null>} Branch name or null on error
12
+ */
13
+ async function getBranch(root) {
14
+ try {
15
+ const { stdout } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root });
16
+ return stdout.trim();
17
+ } catch (error) {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Check if repository has uncommitted changes
24
+ * @param {string} root - Repository root path
25
+ * @returns {Promise<boolean>} True if there are changes
26
+ */
27
+ async function hasChanges(root) {
28
+ try {
29
+ const { stdout } = await execa('git', ['status', '--porcelain'], { cwd: root });
30
+ return stdout.trim().length > 0;
31
+ } catch (error) {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Get porcelain status - parsed list of changed files
38
+ * @param {string} root - Repository root path
39
+ * @returns {Promise<{ok: boolean, files: string[], raw: string}>} Status object
40
+ */
41
+ async function getPorcelainStatus(root) {
42
+ try {
43
+ const { stdout } = await execa('git', ['status', '--porcelain'], { cwd: root });
44
+ const raw = stdout.trim();
45
+
46
+ if (!raw) {
47
+ return { ok: true, files: [], raw: '' };
48
+ }
49
+
50
+ const files = raw
51
+ .split(/\r?\n/)
52
+ .map(line => {
53
+ const status = line.slice(0, 2).trim();
54
+ const file = line.slice(3).trim();
55
+ return { status, file };
56
+ });
57
+
58
+ return { ok: true, files, raw };
59
+ } catch (error) {
60
+ return { ok: false, files: [], raw: error.message };
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Stage all changes (git add -A)
66
+ * @param {string} root - Repository root path
67
+ * @returns {Promise<{ok: boolean, stdout: string, stderr: string}>} Result object
68
+ */
69
+ async function addAll(root) {
70
+ try {
71
+ const { stdout, stderr } = await execa('git', ['add', '-A'], { cwd: root });
72
+ return { ok: true, stdout, stderr };
73
+ } catch (error) {
74
+ return { ok: false, stdout: '', stderr: error.message };
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Commit staged changes
80
+ * @param {string} root - Repository root path
81
+ * @param {string} message - Commit message
82
+ * @returns {Promise<{ok: boolean, stdout: string, stderr: string}>} Result object
83
+ */
84
+ async function commit(root, message) {
85
+ try {
86
+ const { stdout, stderr } = await execa('git', ['commit', '-m', message], { cwd: root });
87
+ return { ok: true, stdout, stderr };
88
+ } catch (error) {
89
+ return { ok: false, stdout: '', stderr: error.message };
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Fetch updates from remote
95
+ * @param {string} root - Repository root path
96
+ * @returns {Promise<{ok: boolean, stdout: string, stderr: string}>} Result object
97
+ */
98
+ async function fetch(root) {
99
+ try {
100
+ const { stdout, stderr } = await execa('git', ['fetch'], { cwd: root });
101
+ return { ok: true, stdout, stderr };
102
+ } catch (error) {
103
+ return { ok: false, stdout: '', stderr: error.message };
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Check if remote is ahead/behind
109
+ * @param {string} root - Repository root path
110
+ * @returns {Promise<{ok: boolean, ahead: boolean, behind: boolean, raw: string}>} Status object
111
+ */
112
+ async function isRemoteAhead(root) {
113
+ try {
114
+ const branch = await getBranch(root);
115
+ if (!branch) return { ok: false, ahead: false, behind: false, raw: 'No branch' };
116
+
117
+ // Ensure we have latest info
118
+ await fetch(root);
119
+
120
+ const { stdout } = await execa('git', ['rev-list', '--left-right', '--count', `${branch}...origin/${branch}`], { cwd: root });
121
+ const [aheadCount, behindCount] = stdout.trim().split(/\s+/).map(Number);
122
+
123
+ return {
124
+ ok: true,
125
+ ahead: aheadCount > 0,
126
+ behind: behindCount > 0,
127
+ raw: stdout.trim()
128
+ };
129
+ } catch (error) {
130
+ return { ok: false, ahead: false, behind: false, raw: error.message };
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Push changes to remote
136
+ * @param {string} root - Repository root path
137
+ * @param {string} [branch] - Branch to push (optional, defaults to current)
138
+ * @returns {Promise<{ok: boolean, stdout: string, stderr: string}>} Result object
139
+ */
140
+ async function push(root, branch) {
141
+ try {
142
+ const targetBranch = branch || await getBranch(root);
143
+ if (!targetBranch) throw new Error('Could not determine branch to push');
144
+
145
+ const { stdout, stderr } = await execa('git', ['push', 'origin', targetBranch], { cwd: root });
146
+ return { ok: true, stdout, stderr };
147
+ } catch (error) {
148
+ return { ok: false, stdout: '', stderr: error.message };
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Get diff of changes
154
+ * @param {string} root - Repository root path
155
+ * @param {boolean} staged - Whether to get staged diff (default: true)
156
+ * @returns {Promise<string>} Diff content
157
+ */
158
+ async function getDiff(root, staged = true) {
159
+ try {
160
+ const args = ['diff'];
161
+ if (staged) args.push('--cached');
162
+ args.push('-U3');
163
+ const { stdout } = await execa('git', args, { cwd: root });
164
+ return stdout || '';
165
+ } catch (error) {
166
+ return '';
167
+ }
168
+ }
169
+
170
+ module.exports = {
171
+ getBranch,
172
+ hasChanges,
173
+ getPorcelainStatus,
174
+ addAll,
175
+ commit,
176
+ fetch,
177
+ isRemoteAhead,
178
+ push,
179
+ getDiff
180
+ };