git-ai-shen 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.
@@ -0,0 +1,285 @@
1
+ const simpleGit = require('simple-git');
2
+ const fs = require('fs').promises;
3
+ const path = require('path');
4
+ const { exec } = require('child_process');
5
+ const util = require('util');
6
+ const execAsync = util.promisify(exec);
7
+
8
+ class GitOperations {
9
+ constructor(repoPath = process.cwd()) {
10
+ this.git = simpleGit(repoPath);
11
+ this.repoPath = repoPath;
12
+ }
13
+ /**
14
+ * 获取文件的详细变更信息(哪些行被修改)
15
+ * @param {string} filePath 文件路径
16
+ * @returns {Promise<Object>} 变更详情
17
+ */
18
+ async getFileDiff(filePath) {
19
+ try {
20
+ // 获取未暂存的变更
21
+ const diff = await this.git.diff([filePath]);
22
+
23
+ // 解析 diff 信息
24
+ return this.parseDiff(diff, filePath);
25
+ } catch (error) {
26
+ console.error(`获取 diff 失败: ${error.message}`);
27
+ return null;
28
+ }
29
+ }
30
+ /**
31
+ * 获取已暂存文件的详细变更信息
32
+ * @param {string} filePath 文件路径
33
+ * @returns {Promise<Object>} 变更详情
34
+ */
35
+ async getStagedDiff(filePath) {
36
+ try {
37
+ // 获取已暂存的变更
38
+ const diff = await this.git.diff(['--cached', filePath]);
39
+
40
+ // 解析 diff 信息
41
+ return this.parseDiff(diff, filePath);
42
+ } catch (error) {
43
+ console.error(`获取暂存 diff 失败: ${error.message}`);
44
+ return null;
45
+ }
46
+ }
47
+ /**
48
+ * 解析 git diff 输出
49
+ * @param {string} diff git diff 的输出
50
+ * @param {string} filePath 文件路径
51
+ * @returns {Object} 解析后的变更信息
52
+ */
53
+ parseDiff(diff, filePath) {
54
+ if (!diff) {
55
+ return {
56
+ file: filePath,
57
+ changes: [],
58
+ summary: '无变更'
59
+ };
60
+ }
61
+
62
+ const lines = diff.split('\n');
63
+ const changes = [];
64
+ let currentHunk = null;
65
+
66
+ // 解析 diff 的每一行
67
+ for (const line of lines) {
68
+ // 匹配 hunk 头信息,例如:@@ -1,4 +1,5 @@
69
+ const hunkMatch = line.match(/^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@/);
70
+ if (hunkMatch) {
71
+ currentHunk = {
72
+ oldStart: parseInt(hunkMatch[1]),
73
+ oldLines: parseInt(hunkMatch[2]) || 1,
74
+ newStart: parseInt(hunkMatch[3]),
75
+ newLines: parseInt(hunkMatch[4]) || 1,
76
+ changes: []
77
+ };
78
+ changes.push(currentHunk);
79
+ continue;
80
+ }
81
+
82
+ if (currentHunk) {
83
+ if (line.startsWith('+')) {
84
+ // 新增的行
85
+ currentHunk.changes.push({
86
+ type: 'add',
87
+ content: line.substring(1),
88
+ lineNumber: currentHunk.newStart + currentHunk.changes.filter(c => c.type !== 'delete').length
89
+ });
90
+ } else if (line.startsWith('-')) {
91
+ // 删除的行
92
+ currentHunk.changes.push({
93
+ type: 'delete',
94
+ content: line.substring(1),
95
+ lineNumber: currentHunk.oldStart + currentHunk.changes.filter(c => c.type !== 'add').length
96
+ });
97
+ } else if (!line.startsWith('\\')) {
98
+ // 上下文行
99
+ currentHunk.changes.push({
100
+ type: 'context',
101
+ content: line.substring(1)
102
+ });
103
+ }
104
+ }
105
+ }
106
+
107
+ // 生成变更摘要
108
+ const addCount = changes.flatMap(h => h.changes).filter(c => c.type === 'add').length;
109
+ const deleteCount = changes.flatMap(h => h.changes).filter(c => c.type === 'delete').length;
110
+
111
+ return {
112
+ file: filePath,
113
+ changes: changes,
114
+ summary: `新增 ${addCount} 行,删除 ${deleteCount} 行`,
115
+ addCount,
116
+ deleteCount
117
+ };
118
+ }
119
+ /**
120
+ * 获取所有变更文件的详细 diff
121
+ * @param {Array<string>} files 文件列表
122
+ * @param {boolean} staged 是否获取暂存区的变更
123
+ * @returns {Promise<Object>} 所有文件的 diff 信息
124
+ */
125
+ async getDiffs(files, staged = false) {
126
+ const diffs = {};
127
+
128
+ for (const file of files) {
129
+ if (staged) {
130
+ diffs[file] = await this.getStagedDiff(file);
131
+ } else {
132
+ diffs[file] = await this.getFileDiff(file);
133
+ }
134
+ }
135
+
136
+ return diffs;
137
+ }
138
+ async isGitRepo() {
139
+ try {
140
+ return await this.git.checkIsRepo();
141
+ } catch {
142
+ return false;
143
+ }
144
+ }
145
+
146
+ async getChangedFiles() {
147
+ try {
148
+ const status = await this.git.status();
149
+ return [
150
+ ...status.modified,
151
+ ...status.not_added,
152
+ ...status.created,
153
+ ...status.deleted
154
+ ];
155
+ } catch (error) {
156
+ return [];
157
+ }
158
+ }
159
+
160
+ async addFiles(files = ['.']) {
161
+ await this.git.add(files);
162
+ return true;
163
+ }
164
+
165
+ async commit(message) {
166
+ await this.git.commit(message);
167
+ return true;
168
+ }
169
+
170
+ async getCurrentBranch() {
171
+ const branch = await this.git.revparse(['--abbrev-ref', 'HEAD']);
172
+ return branch;
173
+ }
174
+
175
+ async pull(remote = 'origin', branch = null, sshPassword = null) {
176
+ try {
177
+ if (!branch) {
178
+ branch = await this.getCurrentBranch();
179
+ }
180
+
181
+ // 如果有 SSH 密码,使用 expect 脚本处理
182
+ if (sshPassword) {
183
+ return await this.pullWithPassword(remote, branch, sshPassword);
184
+ }
185
+
186
+ // 正常拉取
187
+ await this.git.pull(remote, branch);
188
+ return { success: true };
189
+
190
+ } catch (error) {
191
+ // 检查是否是 SSH 密钥密码错误
192
+ if (error.message.includes('Permission denied') ||
193
+ error.message.includes('authenticity') ||
194
+ error.message.includes('password')) {
195
+ return {
196
+ success: false,
197
+ needPassword: true,
198
+ error: error.message
199
+ };
200
+ }
201
+ return { success: false, error: error.message };
202
+ }
203
+ }
204
+
205
+ async pullWithPassword(remote, branch, password) {
206
+ try {
207
+ // 使用 expect 脚本自动输入 SSH 密码
208
+ const command = `
209
+ expect << 'EOF'
210
+ spawn git pull ${remote} ${branch}
211
+ expect {
212
+ "Enter passphrase for key" { send "${password}\r" }
213
+ "Password:" { send "${password}\r" }
214
+ }
215
+ expect eof
216
+ EOF
217
+ `;
218
+
219
+ await execAsync(command);
220
+ return { success: true };
221
+ } catch (error) {
222
+ return { success: false, error: error.message };
223
+ }
224
+ }
225
+
226
+ async push(remote = 'origin', branch = null, sshPassword = null) {
227
+ try {
228
+ if (!branch) {
229
+ branch = await this.getCurrentBranch();
230
+ }
231
+
232
+ // 如果有 SSH 密码,使用 expect 脚本处理
233
+ if (sshPassword) {
234
+ return await this.pushWithPassword(remote, branch, sshPassword);
235
+ }
236
+
237
+ await this.git.push(remote, branch);
238
+ return { success: true };
239
+
240
+ } catch (error) {
241
+ // 检查是否是 SSH 密钥密码错误
242
+ if (error.message.includes('Permission denied') ||
243
+ error.message.includes('authenticity') ||
244
+ error.message.includes('password')) {
245
+ return {
246
+ success: false,
247
+ needPassword: true,
248
+ error: error.message
249
+ };
250
+ }
251
+ return { success: false, error: error.message };
252
+ }
253
+ }
254
+
255
+ async pushWithPassword(remote, branch, password) {
256
+ try {
257
+ const command = `
258
+ expect << 'EOF'
259
+ spawn git push ${remote} ${branch}
260
+ expect {
261
+ "Enter passphrase for key" { send "${password}\r" }
262
+ "Password:" { send "${password}\r" }
263
+ }
264
+ expect eof
265
+ EOF
266
+ `;
267
+
268
+ await execAsync(command);
269
+ return { success: true };
270
+ } catch (error) {
271
+ return { success: false, error: error.message };
272
+ }
273
+ }
274
+
275
+ async getFileContent(filePath) {
276
+ try {
277
+ const fullPath = path.join(this.repoPath, filePath);
278
+ return await fs.readFile(fullPath, 'utf-8');
279
+ } catch {
280
+ return null;
281
+ }
282
+ }
283
+ }
284
+
285
+ module.exports = { GitOperations };
@@ -0,0 +1,171 @@
1
+
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const chalk = require('chalk');
6
+ const readline = require('readline');
7
+
8
+ class SimpleConfigManager {
9
+ constructor() {
10
+ this.configDir = path.join(os.homedir(), '.git-ai');
11
+ this.configFile = path.join(this.configDir, 'config.json');
12
+ this.config = this.loadConfig();
13
+ }
14
+
15
+ getDefaultConfig() {
16
+ return {
17
+ ai: {
18
+ provider: 'openai',
19
+ apiKey: 'ms-114c4bf4-ab3e-442b-a30c-6b355db26263',
20
+ apiUrl: 'https://api-inference.modelscope.cn/v1',
21
+ model: 'deepseek-ai/DeepSeek-V3.2',
22
+ // maxTokens: 1000,
23
+ // temperature: 0.3
24
+ },
25
+ git: {
26
+ autoAdd: true,
27
+ autoPull: true,
28
+ autoPush: true,
29
+ remote: 'origin'
30
+ },
31
+ review: {
32
+ enabled: true,
33
+ skipPatterns: [
34
+ '*.lock',
35
+ '*.log',
36
+ '*.min.js',
37
+ 'node_modules',
38
+ 'dist',
39
+ 'build',
40
+ '.git'
41
+ ],
42
+ maxFileSize: 1024 * 100,
43
+ checkSecrets: true,
44
+ checkTodos: true,
45
+ checkDebug: true
46
+ }
47
+ };
48
+ }
49
+
50
+ loadConfig() {
51
+ try {
52
+ if (!fs.existsSync(this.configDir)) {
53
+ fs.mkdirSync(this.configDir, { recursive: true });
54
+ }
55
+
56
+ if (fs.existsSync(this.configFile)) {
57
+ const data = fs.readFileSync(this.configFile, 'utf8');
58
+ return JSON.parse(data);
59
+ }
60
+ } catch (error) {
61
+ console.error(chalk.yellow(`⚠️ 读取配置文件失败: ${error.message}`));
62
+ }
63
+ return this.getDefaultConfig();
64
+ }
65
+
66
+ saveConfig() {
67
+ try {
68
+ if (!fs.existsSync(this.configDir)) {
69
+ fs.mkdirSync(this.configDir, { recursive: true });
70
+ }
71
+ fs.writeFileSync(this.configFile, JSON.stringify(this.config, null, 2), 'utf8');
72
+ return true;
73
+ } catch (error) {
74
+ console.error(chalk.red(`❌ 保存配置失败: ${error.message}`));
75
+ return false;
76
+ }
77
+ }
78
+
79
+ get(key) {
80
+ const keys = key.split('.');
81
+ let value = this.config;
82
+
83
+ for (const k of keys) {
84
+ if (value && typeof value === 'object' && k in value) {
85
+ value = value[k];
86
+ } else {
87
+ return undefined;
88
+ }
89
+ }
90
+ return value;
91
+ }
92
+
93
+ set(key, value) {
94
+ const keys = key.split('.');
95
+ let current = this.config;
96
+
97
+ for (let i = 0; i < keys.length - 1; i++) {
98
+ if (!(keys[i] in current)) {
99
+ current[keys[i]] = {};
100
+ }
101
+ current = current[keys[i]];
102
+ }
103
+
104
+ current[keys[keys.length - 1]] = value;
105
+ this.saveConfig();
106
+ }
107
+
108
+ getAll() {
109
+ return this.config;
110
+ }
111
+
112
+ init() {
113
+ console.log('\n' + chalk.cyan('='.repeat(50)));
114
+ console.log(chalk.cyan('🔧 初始化 Git AI 配置'));
115
+ console.log(chalk.cyan('='.repeat(50)) + '\n');
116
+
117
+ this.config = this.getDefaultConfig();
118
+
119
+ const rl = readline.createInterface({
120
+ input: process.stdin,
121
+ output: process.stdout
122
+ });
123
+
124
+ console.log(chalk.yellow('(可选) 配置 OpenAI API 密钥,用于AI代码审查和生成提交信息'));
125
+ console.log(chalk.gray('如果不配置,将只使用基础检查功能\n'));
126
+
127
+ const askApiKey = () => {
128
+ rl.question(chalk.cyan('OpenAI API密钥 (直接回车跳过): '), (apiKey) => {
129
+ if (apiKey && apiKey.trim()) {
130
+ this.config.ai.apiKey = apiKey.trim();
131
+ console.log(chalk.green('✅ API密钥已保存'));
132
+ } else {
133
+ console.log(chalk.yellow('⚠️ 跳过API密钥配置,将使用基础功能'));
134
+ }
135
+ askCustomUrl();
136
+ });
137
+ };
138
+
139
+ const askCustomUrl = () => {
140
+ rl.question(chalk.cyan('\n使用自定义API地址? (y/N): '), (customUrl) => {
141
+ if (customUrl.toLowerCase() === 'y' || customUrl.toLowerCase() === 'yes') {
142
+ rl.question(chalk.cyan('API地址: '), (url) => {
143
+ if (url && url.trim()) {
144
+ this.config.ai.apiUrl = url.trim();
145
+ console.log(chalk.green('✅ API地址已保存'));
146
+ }
147
+ finishInit();
148
+ });
149
+ } else {
150
+ finishInit();
151
+ }
152
+ });
153
+ };
154
+
155
+ const finishInit = () => {
156
+ if (this.saveConfig()) {
157
+ console.log(chalk.cyan('\n📋 当前配置:'));
158
+ console.log(JSON.stringify(this.config, null, 2));
159
+ console.log(chalk.green('\n✅ 配置初始化完成!'));
160
+ console.log(chalk.yellow('\n现在你可以在项目中使用: gai commit'));
161
+ } else {
162
+ console.log(chalk.red('\n❌ 配置保存失败!'));
163
+ }
164
+ rl.close();
165
+ };
166
+
167
+ askApiKey();
168
+ }
169
+ }
170
+
171
+ module.exports = { SimpleConfigManager };
package/lib/index.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 主入口模块
3
+ */
4
+
5
+ const { CommitCommand } = require('./commands/commit');
6
+ const { ConfigManager } = require('./core/config');
7
+ const { GitOperations } = require('./core/git-operations');
8
+ const { AIReviewer } = require('./core/ai-reviewer');
9
+ const { Logger } = require('./utils/logger');
10
+ const { FileUtils } = require('./utils/file-utils');
11
+
12
+ module.exports = {
13
+ CommitCommand,
14
+ ConfigManager,
15
+ GitOperations,
16
+ AIReviewer,
17
+ Logger,
18
+ FileUtils
19
+ };
@@ -0,0 +1,98 @@
1
+ /**
2
+ * 文件工具模块
3
+ */
4
+
5
+ const fs = require('fs').promises;
6
+ const path = require('path');
7
+ const glob = require('glob');
8
+
9
+ class FileUtils {
10
+ static async readFile(filePath) {
11
+ try {
12
+ return await fs.readFile(filePath, 'utf-8');
13
+ } catch (error) {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ static async writeFile(filePath, content) {
19
+ try {
20
+ await fs.writeFile(filePath, content, 'utf-8');
21
+ return true;
22
+ } catch (error) {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ static async fileExists(filePath) {
28
+ try {
29
+ await fs.access(filePath);
30
+ return true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ static async getFileSize(filePath) {
37
+ try {
38
+ const stats = await fs.stat(filePath);
39
+ return stats.size;
40
+ } catch {
41
+ return 0;
42
+ }
43
+ }
44
+
45
+ static getFileExtension(filePath) {
46
+ return path.extname(filePath).toLowerCase();
47
+ }
48
+
49
+ static getFileName(filePath) {
50
+ return path.basename(filePath);
51
+ }
52
+
53
+ static getFileDir(filePath) {
54
+ return path.dirname(filePath);
55
+ }
56
+
57
+ static async findFiles(pattern, options = {}) {
58
+ return new Promise((resolve, reject) => {
59
+ glob(pattern, options, (err, files) => {
60
+ if (err) reject(err);
61
+ else resolve(files);
62
+ });
63
+ });
64
+ }
65
+
66
+ static formatFileSize(bytes) {
67
+ const units = ['B', 'KB', 'MB', 'GB'];
68
+ let size = bytes;
69
+ let unitIndex = 0;
70
+
71
+ while (size >= 1024 && unitIndex < units.length - 1) {
72
+ size /= 1024;
73
+ unitIndex++;
74
+ }
75
+
76
+ return `${size.toFixed(2)} ${units[unitIndex]}`;
77
+ }
78
+
79
+ static async ensureDir(dirPath) {
80
+ try {
81
+ await fs.mkdir(dirPath, { recursive: true });
82
+ return true;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ static async deleteFile(filePath) {
89
+ try {
90
+ await fs.unlink(filePath);
91
+ return true;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+ }
97
+
98
+ module.exports = { FileUtils };
@@ -0,0 +1,71 @@
1
+ /**
2
+ * 日志工具模块
3
+ */
4
+
5
+ const chalk = require('chalk');
6
+ const ora = require('ora');
7
+
8
+ class Logger {
9
+ constructor(options = {}) {
10
+ this.spinner = null;
11
+ this.options = options;
12
+ }
13
+
14
+ info(message) {
15
+ console.log(chalk.blue('ℹ'), message);
16
+ }
17
+
18
+ success(message) {
19
+ console.log(chalk.green('✅'), message);
20
+ }
21
+
22
+ warning(message) {
23
+ console.log(chalk.yellow('⚠️'), message);
24
+ }
25
+
26
+ error(message) {
27
+ console.log(chalk.red('❌'), message);
28
+ }
29
+
30
+ startSpinner(text) {
31
+ if (this.options.showProgress) {
32
+ this.spinner = ora({
33
+ text,
34
+ color: 'cyan'
35
+ }).start();
36
+ }
37
+ }
38
+
39
+ stopSpinner(success = true) {
40
+ if (this.spinner) {
41
+ if (success) {
42
+ this.spinner.succeed();
43
+ } else {
44
+ this.spinner.fail();
45
+ }
46
+ this.spinner = null;
47
+ }
48
+ }
49
+
50
+ table(data) {
51
+ console.table(data);
52
+ }
53
+
54
+ divider() {
55
+ console.log(chalk.gray('─'.repeat(50)));
56
+ }
57
+
58
+ highlight(text) {
59
+ return chalk.cyan(text);
60
+ }
61
+
62
+ risk(text) {
63
+ return chalk.red(`[风险] ${text}`);
64
+ }
65
+
66
+ suggestion(text) {
67
+ return chalk.yellow(`[建议] ${text}`);
68
+ }
69
+ }
70
+
71
+ module.exports = { Logger };
package/node ADDED
File without changes
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "git-ai-shen",
3
+ "version": "1.0.0",
4
+ "description": "智能Git提交工具,支持行级代码审查和AI生成提交信息",
5
+ "main": "bin/git-ai.js",
6
+ "bin": {
7
+ "gai": "./bin/git-ai.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/git-ai.js"
11
+ },
12
+ "keywords": ["git", "ai", "code-review", "commit"],
13
+ "author": "shenzhichao",
14
+ "license": "MIT",
15
+ "dependencies": {
16
+ "simple-git": "^3.19.0",
17
+ "openai": "^4.0.0",
18
+ "chalk": "^4.1.2",
19
+ "inquirer": "^8.2.5",
20
+ "ignore": "^5.2.4"
21
+ },
22
+ "engines": {
23
+ "node": ">=14.0.0"
24
+ }
25
+ }