zen-gitsync 2.11.38 → 2.12.2

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.
@@ -1,1044 +1,1044 @@
1
- // const chalk = require('chalk');
2
- // const boxen = require('boxen');
3
- // const message = chalk.blue('git diff') + '\n' +
4
- // chalk.red('- line1') + '\n' +
5
- // chalk.green('+ line2') + '\n' +
6
- // chalk.cyan('@@ line diff @@');
7
- //
8
- // const options = {
9
- // padding: 1,
10
- // margin: 1,
11
- // borderStyle: 'round', // 可以选择 'single', 'double', 'round' 等边框样式
12
- // borderColor: 'yellow'
13
- // };
14
- //
15
- // const boxedMessage = boxen(message, options);
16
- // console.log(boxedMessage);
17
- import stringWidth from 'string-width';
18
- import Table from 'cli-table3';
19
- import chalk from 'chalk';
20
- import boxen from "boxen";
21
- import {exec, execSync} from 'child_process'
22
- import os from 'os'
23
- import ora from "ora";
24
- import readline from 'readline'
25
- import path from 'path'
26
- import fs from 'fs/promises'
27
- import config from '../config.js'
28
-
29
-
30
- const printTableWithHeaderUnderline = (head, content, style) => {
31
- // 获取终端的列数(宽度)
32
- const terminalWidth = process.stdout.columns || 100;
33
-
34
- // 计算表格的宽度,保证至少有 2 个字符留给边框
35
- const tableWidth = terminalWidth - 2; // 左右边框和分隔符的宽度
36
-
37
- // 计算每列的宽度
38
- const colWidths = [tableWidth]; // 只有一列,因此宽度设置为终端宽度
39
-
40
- if (!style) {
41
- style = {
42
- // head: ['cyan'], // 表头文字颜色为cyan
43
- border: [chalk.reset()], // 边框颜色
44
- compact: true, // 启用紧凑模式,去掉不必要的空白
45
- }
46
- }
47
- // 创建表格实例
48
- const table = new Table({
49
- head: [head], // 只有一个表头
50
- colWidths, // 使用动态计算的列宽
51
- style: style,
52
- wordWrap: true, // 启用自动换行
53
- // chars: {
54
- // 'top': '─',
55
- // 'top-mid': '┬',
56
- // 'bottom': '─',
57
- // 'mid': '─',
58
- // 'left': '│',
59
- // 'right': '│'
60
- // },
61
- // chars: {
62
- // 'top': '═', // 顶部边框使用长横线
63
- // 'top-mid': '╤', // 顶部连接符
64
- // 'top-left': '╔', // 左上角
65
- // 'top-right': '╗', // 右上角
66
- // 'bottom': '═', // 底部边框
67
- // 'bottom-mid': '╧', // 底部连接符
68
- // 'bottom-left': '╚',// 左下角
69
- // 'bottom-right': '╝',// 右下角
70
- // 'left': '║', // 左边框
71
- // 'left-mid': '╟', // 左连接符
72
- // 'mid': '═', // 中间分隔符
73
- // 'mid-mid': '╪', // 中间连接符
74
- // 'right': '║', // 右边框
75
- // 'right-mid': '╢', // 右连接符
76
- // 'middle': '│' // 中间内容的边界
77
- // }
78
- });
79
-
80
-
81
- content.forEach(item => {
82
- table.push([item]);
83
- })
84
-
85
- console.log(table.toString()); // 输出表格
86
- };
87
-
88
- // printTableWithHeaderUnderline();
89
-
90
- const colors = [
91
- '\x1b[31m', // 红色
92
- '\x1b[32m', // 绿色
93
- '\x1b[33m', // 黄色
94
- '\x1b[34m', // 蓝色
95
- '\x1b[35m', // 紫色
96
- '\x1b[36m', // 青色
97
- ];
98
-
99
- function getRandomColor() {
100
- return `\x1b[0m`;
101
- // const randomIndex = Math.floor(Math.random() * colors.length);
102
- // return colors[randomIndex];
103
- }
104
-
105
- function resetColor() {
106
- return '\x1b[0m';
107
- }
108
-
109
- const calcColor = (commandLine, str) => {
110
- let color = 'reset'
111
- switch (commandLine) {
112
- case 'git status':
113
- if (str.startsWith('\t')) {
114
- color = 'red'
115
- if (str.startsWith('new file:')) {
116
- color = 'red'
117
- }
118
- if (str.startsWith('modified:')) {
119
- color = 'green'
120
- }
121
- if (str.startsWith('deleted:')) {
122
- color = 'red'
123
- }
124
- }
125
- break;
126
- case 'git diff':
127
- // if (str.startsWith('---')) {
128
- // color = 'red'
129
- // }
130
- // if (str.startsWith('+++')) {
131
- // color = 'green'
132
- // }
133
- // if (str.startsWith('@@ ')) {
134
- // color = 'cyan'
135
- // }
136
- break;
137
- }
138
- return color
139
- }
140
- const tableLog = (commandLine, content, type) => {
141
- let handle_commandLine = `> ${commandLine}`
142
- let head = chalk.bold.blue(handle_commandLine)
143
- let style = {
144
- // head: ['cyan'], // 表头文字颜色为cyan
145
- border: [chalk.reset()], // 边框颜色
146
- compact: true, // 启用紧凑模式,去掉不必要的空白
147
- }
148
- switch (type) {
149
- case 'error':
150
- style.head = ['red'];
151
- content = content.toString().split('\n')
152
- head = chalk.bold.red(handle_commandLine)
153
- break;
154
- case 'common':
155
- style.head = ['blue'];
156
- content = content.split('\n')
157
- break;
158
- default:
159
- break;
160
- }
161
-
162
- // 限制输出内容
163
- const MAX_LINES = 10; // 最大行数
164
- const MAX_LINE_LENGTH = 200; // 每行最大字符数
165
- let isTruncated = false;
166
-
167
- if (content.length > MAX_LINES) {
168
- content = content.slice(0, MAX_LINES);
169
- isTruncated = true;
170
- }
171
-
172
- content = content.map(item => {
173
- let fontColor = calcColor(commandLine, item)
174
- let row = item.replaceAll('\t', ' ')
175
- // 截断过长的行
176
- if (row.length > MAX_LINE_LENGTH) {
177
- row = row.substring(0, MAX_LINE_LENGTH) + '...';
178
- }
179
- const result = chalk[fontColor](row)
180
- return result
181
- })
182
-
183
- // 如果内容被截断,添加提示
184
- if (isTruncated) {
185
- content.push(chalk.dim('... (输出内容过多,已省略)'));
186
- }
187
-
188
- printTableWithHeaderUnderline(head, content, style)
189
- }
190
- const coloredLog = (...args) => {
191
- // 获取参数内容
192
- const commandLine = args[0];
193
- const content = args[1];
194
- const type = args[2] || 'common';
195
- // console.log(`commandLine ==> `, commandLine)
196
- // console.log(`content ==> `, content)
197
- // console.log(`type ==> `, type)
198
- tableLog(commandLine, content, type);
199
- }
200
- const errorLog = (commandLine, content) => {
201
- // 使用 boxen 绘制带边框的消息
202
- let msg = ` FAIL ${commandLine}
203
- content: ${content} `
204
- const message = chalk.red.bold(msg);
205
- const box = boxen(message);
206
- console.log(box); // 打印带有边框的消息
207
- }
208
-
209
- function execSyncGitCommand(command, options = {}) {
210
- let {encoding = 'utf-8', maxBuffer = 30 * 1024 * 1024, head = command, log = true} = options
211
- try {
212
- let cwd = getCwd()
213
- const output = execSync(command, {
214
- env: {
215
- ...process.env,
216
- // LANG: 'en_US.UTF-8', // Linux/macOS
217
- // LC_ALL: 'en_US.UTF-8', // Linux/macOS
218
- GIT_CONFIG_PARAMETERS: "'core.quotepath=false'" // 关闭路径转义
219
- }, encoding, maxBuffer, cwd
220
- })
221
- if (options.spinner) {
222
- options.spinner.stop();
223
- }
224
- let result = output.trim()
225
- log && coloredLog(head, result)
226
- // 打印当前目录和时间信息
227
- if (log) {
228
- const currentTime = new Date().toLocaleString('zh-CN', { hour12: false });
229
- console.log(chalk.dim(`📁 目录: ${cwd} | ⏰ 时间: ${currentTime}`));
230
- }
231
- return result
232
- } catch (e) {
233
- // console.log(`执行命令出错 ==> `, command, e)
234
- log && coloredLog(command, e, 'error')
235
- throw new Error(e)
236
- }
237
- }
238
-
239
- // Add a command history array to store commands and their results
240
- const commandHistory = [];
241
- const MAX_HISTORY_SIZE = 100;
242
- const MAX_OUTPUT_LENGTH = 5000; // Limit the output length to avoid memory issues
243
-
244
- // 添加一个变量来保存Socket.io实例
245
- let ioInstance = null;
246
-
247
- // 提供注册Socket.io实例的函数
248
- function registerSocketIO(io) {
249
- ioInstance = io;
250
- }
251
-
252
- // 清空命令历史记录
253
- function clearCommandHistory() {
254
- // 清空数组
255
- commandHistory.length = 0;
256
-
257
- // 通过WebSocket广播历史已清空
258
- if (ioInstance) {
259
- ioInstance.emit('command_history_cleared');
260
- }
261
-
262
- return true;
263
- }
264
-
265
- function execGitCommand(command, options = {}) {
266
- return new Promise((resolve, reject) => {
267
- let {encoding = 'utf-8', maxBuffer = 30 * 1024 * 1024, head = command, log = true} = options
268
- let cwd = getCwd()
269
-
270
- // Record start time for command execution
271
- const startTime = Date.now();
272
-
273
- // setTimeout(() => {
274
- exec(command, {
275
- env: {
276
- ...process.env,
277
- // LANG: 'en_US.UTF-8', // Linux/macOS
278
- // LC_ALL: 'en_US.UTF-8', // Linux/macOS
279
- GIT_CONFIG_PARAMETERS: "'core.quotepath=false'" // 关闭路径转义
280
- },
281
- encoding,
282
- maxBuffer,
283
- cwd
284
- }, (error, stdout, stderr) => {
285
- if (options.spinner) {
286
- options.spinner.stop();
287
- }
288
-
289
- // Calculate execution time
290
- const executionTime = Date.now() - startTime;
291
-
292
- // Truncate long outputs
293
- let truncatedStdout = stdout;
294
- let truncatedStderr = stderr;
295
- let isStdoutTruncated = false;
296
- let isStderrTruncated = false;
297
-
298
- if (stdout && stdout.length > MAX_OUTPUT_LENGTH) {
299
- truncatedStdout = stdout.substring(0, MAX_OUTPUT_LENGTH) + '\n... (output truncated)';
300
- isStdoutTruncated = true;
301
- }
302
-
303
- if (stderr && stderr.length > MAX_OUTPUT_LENGTH) {
304
- truncatedStderr = stderr.substring(0, MAX_OUTPUT_LENGTH) + '\n... (error output truncated)';
305
- isStderrTruncated = true;
306
- }
307
-
308
- // Add command to history
309
- const historyItem = {
310
- command,
311
- stdout: truncatedStdout || '',
312
- stderr: truncatedStderr || '',
313
- error: error ? error.message : null,
314
- executionTime,
315
- timestamp: new Date().toISOString(),
316
- success: !error,
317
- isStdoutTruncated,
318
- isStderrTruncated
319
- };
320
-
321
- // Add to history (limited size)
322
- commandHistory.unshift(historyItem);
323
- if (commandHistory.length > MAX_HISTORY_SIZE) {
324
- commandHistory.pop();
325
- }
326
-
327
- // 通过WebSocket广播命令历史更新
328
- if (ioInstance) {
329
- ioInstance.emit('command_history_update', {
330
- newCommand: historyItem,
331
- fullHistory: commandHistory.slice(0, 10) // 只发送最近10条以减小数据量
332
- });
333
- }
334
-
335
- if (stdout) {
336
- log && coloredLog(head, stdout)
337
- }
338
- if (stderr) {
339
- log && coloredLog(head, stderr)
340
- }
341
- // 打印当前目录和时间信息
342
- if (log && (stdout || stderr)) {
343
- const currentTime = new Date().toLocaleString('zh-CN', { hour12: false });
344
- console.log(chalk.dim(`📁 目录: ${cwd} | ⏰ 时间: ${currentTime}`));
345
- }
346
- if (error) {
347
- log && coloredLog(head, error, 'error')
348
- // 错误情况也打印目录和时间
349
- if (log) {
350
- const currentTime = new Date().toLocaleString('zh-CN', { hour12: false });
351
- console.log(chalk.dim(`📁 目录: ${cwd} | ⏰ 时间: ${currentTime}`));
352
- }
353
- // 将 stdout 和 stderr 附加到 error 对象,以便上层可以获取完整输出
354
- error.stdout = stdout
355
- error.stderr = stderr
356
- reject(error)
357
- return
358
- }
359
- resolve({
360
- stdout,
361
- stderr
362
- })
363
- })
364
- // }, 1000)
365
- })
366
- }
367
-
368
- /**
369
- * 检查并尝试清理 Git 锁文件
370
- * @returns {Promise<boolean>} 是否清理成功
371
- */
372
- async function checkAndClearGitLock() {
373
- try {
374
- const cwd = getCwd();
375
- let gitRoot;
376
- try {
377
- // 使用 execSync 快速获取 Git 根目录
378
- const rootOutput = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' });
379
- gitRoot = rootOutput.trim();
380
- } catch (e) {
381
- gitRoot = cwd;
382
- }
383
-
384
- const lockFilePath = path.join(gitRoot, '.git', 'index.lock');
385
- try {
386
- await fs.access(lockFilePath);
387
- // 如果文件存在,尝试删除它
388
- await fs.unlink(lockFilePath);
389
- console.log(chalk.green(`✅ 已清理 Git 锁文件: ${lockFilePath}`));
390
- return true;
391
- } catch (e) {
392
- // 文件不存在,不需要清理
393
- return false;
394
- }
395
- } catch (error) {
396
- console.error(chalk.red('清理 Git 锁文件失败:'), error.message);
397
- return false;
398
- }
399
- }
400
-
401
- // Function to get command history
402
- function getCommandHistory() {
403
- return [...commandHistory];
404
- }
405
-
406
- // Function to manually add command to history (for commands not using execGitCommand)
407
- function addCommandToHistory(command, stdout = '', stderr = '', error = null, executionTime = 0) {
408
- const MAX_OUTPUT_LENGTH = 5000;
409
-
410
- // Truncate outputs if too long
411
- const isStdoutTruncated = stdout.length > MAX_OUTPUT_LENGTH;
412
- const isStderrTruncated = stderr.length > MAX_OUTPUT_LENGTH;
413
- const truncatedStdout = isStdoutTruncated ? stdout.substring(0, MAX_OUTPUT_LENGTH) + '...[truncated]' : stdout;
414
- const truncatedStderr = isStderrTruncated ? stderr.substring(0, MAX_OUTPUT_LENGTH) + '...[truncated]' : stderr;
415
-
416
- const historyItem = {
417
- command,
418
- stdout: truncatedStdout || '',
419
- stderr: truncatedStderr || '',
420
- error: error ? (typeof error === 'string' ? error : error.message) : null,
421
- executionTime,
422
- timestamp: new Date().toISOString(),
423
- success: !error,
424
- isStdoutTruncated,
425
- isStderrTruncated
426
- };
427
-
428
- // Add to history (limited size)
429
- commandHistory.unshift(historyItem);
430
- if (commandHistory.length > MAX_HISTORY_SIZE) {
431
- commandHistory.pop();
432
- }
433
-
434
- // Broadcast via WebSocket if available
435
- if (ioInstance) {
436
- ioInstance.emit('command_history_update', {
437
- newCommand: historyItem,
438
- fullHistory: commandHistory.slice(0, 10)
439
- });
440
- }
441
-
442
- return historyItem;
443
- }
444
-
445
- const getCwd = () => {
446
- const cwdArg = process.argv.find(arg => arg.startsWith('--path')) || process.argv.find(arg => arg.startsWith('--cwd'));
447
- if (cwdArg) {
448
- // console.log(`cwdArg ==> `, cwdArg)
449
- const [, value] = cwdArg.split('=')
450
- // console.log(`value ==> `, value)
451
- return value || process.cwd()
452
- }
453
- return process.cwd()
454
- }
455
- const judgePlatform = () => {
456
- // 判断是否是 Windows 系统
457
- if (os.platform() === 'win32') {
458
- try {
459
- // 设置终端字符编码为 UTF-8
460
- execSync('chcp 65001');
461
- execSync('git config --global core.autocrlf true');
462
- // 设置Git不转义路径(避免中文显示为八进制)
463
- execSync('git config --global core.quotepath false');
464
- } catch (e) {
465
- console.error('设置字符编码失败:', e.message);
466
- }
467
- }else{
468
- execSync('git config --global core.autocrlf input');
469
- }
470
- };
471
- const showHelp = () => {
472
- const helpMessage = `
473
- Usage: g [options]
474
-
475
- Options:
476
- -h, --help Show this help message
477
- --set-default-message=<msg> Set default commit message
478
- get-config Show current configuration
479
- -y Auto commit with default message
480
- -m <message> Commit message (use quotes if message contains spaces)
481
- -m=<message> Commit message (use this form without spaces around '=')
482
- --path=<path> Set custom working directory
483
- --cwd=<path> Set custom working directory
484
- --interval=<seconds> Set interval time for automatic commits (in seconds)
485
- log Show git commit logs
486
- --n=<number> Number of commits to show with --log
487
- --no-diff Skip displaying git diff
488
- addScript Add "g:y": "g -y" to package.json scripts
489
- addResetScript Add "g:reset": "git reset --hard origin/<current-branch>" to package.json scripts
490
- ui Launch graphical user interface (v2.0.0)
491
-
492
- File Locking:
493
- --lock-file=<path> Lock a file to exclude it from commits
494
- --unlock-file=<path> Unlock a previously locked file
495
- --list-locked List all currently locked files
496
- --check-lock=<path> Check if a file is locked
497
-
498
- --cmd="your-cmd" Execute custom cmd command (immediately, at a time, or periodically)
499
- --cmd-interval=<seconds> Execute custom cmd every N seconds
500
- --at="HH:MM" Execute custom cmd at a specific time (today) or --at="YYYY-MM-DD HH:MM:SS"
501
- --daily Repeat with --at every day at the same time
502
-
503
- Example:
504
- g --cmd="echo hello" --cmd-interval=5 # 每5秒执行一次echo hello
505
- g --cmd="echo at-time" --at=23:59 # 在23:59执行一次echo at-time
506
- g --cmd="echo daily" --at=23:59 --daily # 每天23:59执行一次echo daily
507
- g --cmd="echo now" # 立即执行一次echo now
508
- g --cmd="echo hi" --cmd-interval=10 --interval=60 # cmd和git自动提交并行
509
- g -m "Initial commit" Commit with a custom message
510
- g -m=Fix-bug Commit with a custom message (no spaces around '=')
511
- g -y Auto commit with the default message
512
- g -y --interval=600 Commit every 10 minutes (600 seconds)
513
- g --path=/path/to/repo Specify a custom working directory
514
- g log Show recent commit logs
515
- g log --n=5 Show the last 5 commits with --log
516
- g addScript Add auto commit script to package.json
517
- g addResetScript Add reset script to package.json
518
- g --lock-file=config.json Lock config.json file
519
- g --unlock-file=config.json Unlock config.json file
520
- g --list-locked List all locked files
521
-
522
- Add auto submit in package.json:
523
- "scripts": {
524
- "g:y": "g -y",
525
- "g:reset": "git reset --hard origin/<current-branch>"
526
- }
527
-
528
- Run in the background across platforms:
529
- Windows:
530
- start /min cmd /k "g -y --path=your-folder --interval=600"
531
-
532
- Linux/macOS:
533
- nohup g -y --path=your-folder --interval=600 > git-autocommit.log 2>&1 &
534
-
535
- Start GUI interface:
536
- g ui
537
-
538
- Stop all monitoring processes:
539
- Windows: Terminate the Node.js process in the Task Manager.
540
- Linux/macOS:
541
- pkill -f "g -y" # Terminate all auto-commit processes
542
- ps aux | grep "g -y" # Find the specific process ID
543
- kill [PID] # Terminate the specified process
544
- `;
545
-
546
- console.log(helpMessage);
547
- process.exit();
548
- };
549
-
550
- function judgeLog() {
551
- const logArg = process.argv.find(arg => arg === 'log');
552
- if (logArg) {
553
- printGitLog(); // 如果有 log 参数,打印 Git 提交记录
554
- // 打印完成后退出
555
- process.exit();
556
- }
557
- }
558
-
559
- function judgeHelp() {
560
- if (process.argv.includes('-h') || process.argv.includes('--help')) {
561
- showHelp();
562
- }
563
- }
564
-
565
- async function printGitLog() {
566
- let n = 20;
567
- let logArg = process.argv.find(arg => arg.startsWith('--n='));
568
- if (logArg) {
569
- n = parseInt(logArg.split('=')[1], 10);
570
- }
571
- // 使用 ASCII 记录分隔符 %x1E 作为字段分隔符
572
- const logCommand = `git log -n ${n} --pretty=format:"%C(green)%h%C(reset) %x1E %C(cyan)%an%C(reset) %x1E %C(yellow)%ad%C(reset) %x1E %C(blue)%D%C(reset) %x1E %C(magenta)%s%C(reset)" --date=format:"%Y-%m-%d %H:%M" --graph --decorate --color`
573
- try {
574
- const logOutput = await execGitCommand(logCommand, {
575
- head: `git log`
576
- });
577
- } catch (error) {
578
- console.error('无法获取 Git 提交记录:', error.message);
579
- }
580
- // 打印完成后退出
581
- process.exit();
582
- }
583
-
584
- function exec_exit(exit) {
585
- if (exit) {
586
- process.exit()
587
- }
588
- }
589
-
590
- function judgeUnmerged(statusOutput) {
591
- const hasUnmerged = statusOutput.includes('You have unmerged paths');
592
- if (hasUnmerged) {
593
- errorLog('错误', '存在未合并的文件,请先解决冲突')
594
- process.exit(1);
595
- }
596
- }
597
-
598
- async function exec_push({exit, commitMessage}) {
599
- // 执行 git push
600
- const spinner = ora('正在推送代码...').start();
601
- try {
602
- const {stdout, stderr} = await execGitCommand('git push', {
603
- spinner
604
- });
605
- await printCommitLog({commitMessage});
606
- return {stdout, stderr};
607
- } catch (error) {
608
- throw error;
609
- }
610
- }
611
-
612
- async function printCommitLog({commitMessage}) {
613
- try {
614
- // 获取项目名称(取git仓库根目录名)
615
- const projectRootResult = await execGitCommand('git rev-parse --show-toplevel', {log: false});
616
- const projectName = chalk.blueBright(path.basename(projectRootResult.stdout.trim()));
617
-
618
- // 获取当前提交hash(取前7位)
619
- const commitHashResult = await execGitCommand('git rev-parse --short HEAD', {log: false});
620
- const hashDisplay = chalk.yellow(commitHashResult.stdout.trim());
621
-
622
- // 获取分支信息
623
- const branchResult = await execGitCommand('git branch --show-current', {log: false});
624
- const branchDisplay = chalk.magenta(branchResult.stdout.trim());
625
-
626
- // 构建信息内容
627
- const message = [
628
- `${chalk.cyan.bold('Project:')} ${projectName}`,
629
- `${chalk.cyan.bold('Commit:')} ${hashDisplay} ${chalk.dim('on')} ${branchDisplay}`,
630
- `${chalk.cyan.bold('Message:')} ${chalk.reset(commitMessage)}`,
631
- `${chalk.cyan.bold('Time:')} ${new Date().toLocaleString()}`
632
- ].join('\n');
633
-
634
- // 使用boxen创建装饰框
635
- const box = boxen(message, {
636
- padding: 1,
637
- margin: 1,
638
- borderStyle: 'round',
639
- borderColor: 'green',
640
- title: chalk.bold.green('✅ COMMIT SUCCESS'),
641
- titleAlignment: 'center',
642
- float: 'left',
643
- textAlignment: 'left'
644
- });
645
-
646
- console.log(box);
647
- } catch (error) {
648
- // 异常处理
649
- const errorBox = boxen(chalk.red(`Failed to get commit details: ${error.message}`), {
650
- borderColor: 'red',
651
- padding: 1
652
- });
653
- console.log(errorBox);
654
- }
655
- }
656
-
657
- async function execPull() {
658
- try {
659
- // 检查是否需要拉取更新
660
- const spinner = ora('正在拉取代码...').start();
661
- await execGitCommand('git pull', {
662
- spinner
663
- })
664
- } catch (e) {
665
- console.log(chalk.yellow('⚠️ 拉取远程更新合并失败,可能存在冲突,请手动处理'));
666
- throw Error(e)
667
- }
668
- }
669
-
670
- function delay(timeout) {
671
- return new Promise(resolve => setTimeout(resolve, timeout));
672
- }
673
-
674
- async function judgeRemote() {
675
- const spinner = ora('正在检查远程更新...').start();
676
- try {
677
- // 检查是否有远程更新
678
- // 先获取远程最新状态
679
- await execGitCommand('git remote update', {
680
- head: 'Fetching remote updates',
681
- log: false
682
- });
683
- // 检查是否需要 pull
684
- const res = await execGitCommand('git rev-list HEAD..@{u} --count', {
685
- head: 'Checking if behind remote',
686
- log: false
687
- });
688
- const behindCount = res.stdout.trim()
689
- const { green, black, bgGreen, white } = chalk;
690
- // 如果本地落后于远程
691
- if (parseInt(behindCount) > 0) {
692
- try {
693
- spinner.stop();
694
- // const spinner_pull = ora('发现远程更新,正在拉取...').start();
695
- await execPull()
696
-
697
- // // 尝试使用 --ff-only 拉取更新
698
- // const res = await execGitCommand('git pull --ff-only', {
699
- // spinner: spinner_pull,
700
- // head: 'Pulling updates'
701
- // });
702
- // console.log(
703
- // bgGreen.white.bold(' SYNC ') +
704
- // green` ➔ ` +
705
- // chalk.blue.bold('远程仓库已同步') +
706
- // green(' ✔')
707
- // );
708
- const message = '已成功同步远程更新'.split('').map((char, i) =>
709
- chalk.rgb(0, 255 - i*10, 0)(char)
710
- ).join('');
711
-
712
- console.log(chalk.bold(`✅ ${message}`));
713
- } catch (pullError) {
714
- // // 如果 --ff-only 拉取失败,尝试普通的 git pull
715
- // console.log(chalk.yellow('⚠️ 无法快进合并,尝试普通合并...'));
716
- // await this.execPull()
717
- throw new Error(pullError)
718
- }
719
- } else {
720
- spinner.stop();
721
- const message = '本地已是最新'.split('').map((char, i) =>
722
- chalk.rgb(0, 255 - i*10, 0)(char)
723
- ).join('');
724
- console.log(chalk.bold(`✅ ${message}`));
725
- }
726
- } catch (e) {
727
- // console.log(`e ==> `, e)
728
- spinner.stop();
729
- throw new Error(e)
730
- }
731
- }
732
-
733
- async function execDiff() {
734
- const no_diff = process.argv.find(arg => arg.startsWith('--no-diff'))
735
- if (!no_diff) {
736
- await execGitCommand('git diff --color=always', {
737
- head: `git diff`
738
- })
739
- }
740
- }
741
-
742
- // 执行 git add 但排除锁定的文件
743
- async function execGitAddWithLockFilter() {
744
- try {
745
- // 获取锁定的文件列表
746
- const lockedFiles = await config.getLockedFiles();
747
-
748
- if (lockedFiles.length === 0) {
749
- // 如果没有锁定文件,直接执行 git add .
750
- await execGitCommand('git add .');
751
- return;
752
- }
753
-
754
- // 获取Git工作目录根路径,确保路径匹配的准确性
755
- let gitRoot;
756
- try {
757
- const gitRootResult = await execGitCommand('git rev-parse --show-toplevel', {log: false});
758
- gitRoot = path.normalize(gitRootResult.stdout.trim());
759
- } catch (error) {
760
- console.warn(chalk.yellow('⚠️ 无法获取Git根目录,使用当前工作目录'));
761
- gitRoot = path.normalize(process.cwd());
762
- }
763
-
764
- // 获取所有修改的文件(包括未跟踪文件)
765
- const statusResult = await execGitCommand('git status --porcelain --untracked-files=all', {log: false});
766
- const modifiedFiles = statusResult.stdout
767
- .split('\n')
768
- .filter(line => line.trim())
769
- .map(line => {
770
- // 解析 git status --porcelain 的输出格式
771
- // 格式: XY filename 或 XY "filename with spaces"
772
- const match = line.match(/^..\s+(.+)$/);
773
- if (match) {
774
- let filename = match[1];
775
- // 如果文件名被引号包围,去掉引号
776
- if (filename.startsWith('"') && filename.endsWith('"')) {
777
- filename = filename.slice(1, -1);
778
- // 处理转义字符
779
- filename = filename.replace(/\\(.)/g, '$1');
780
- }
781
- return filename;
782
- }
783
- return null;
784
- })
785
- .filter(Boolean);
786
-
787
- // 过滤掉锁定的文件,使用更严格的路径匹配逻辑
788
- const filesToAdd = modifiedFiles.filter(file => {
789
- // Git status 返回的是相对于Git根目录的路径
790
- const gitRelativeFile = path.normalize(file);
791
-
792
- const isLocked = lockedFiles.some(lockedFile => {
793
- // 处理锁定文件路径:可能是相对路径或绝对路径
794
- let normalizedLocked;
795
- if (path.isAbsolute(lockedFile)) {
796
- // 绝对路径:转换为相对于Git根目录的路径
797
- const absoluteLocked = path.normalize(lockedFile);
798
- if (absoluteLocked.startsWith(gitRoot)) {
799
- normalizedLocked = path.relative(gitRoot, absoluteLocked);
800
- } else {
801
- // 锁定文件不在当前Git仓库中,跳过
802
- return false;
803
- }
804
- } else {
805
- // 相对路径:直接使用
806
- normalizedLocked = path.normalize(lockedFile);
807
- }
808
-
809
- // 统一路径分隔符(Windows兼容性)
810
- const normalizedGitFile = gitRelativeFile.replace(/\\/g, '/');
811
- const normalizedLockedFile = normalizedLocked.replace(/\\/g, '/');
812
-
813
- // 精确匹配或目录匹配(双向检查)
814
- const isExactMatch = normalizedGitFile === normalizedLockedFile;
815
- const isFileInLockedDir = normalizedGitFile.startsWith(normalizedLockedFile + '/');
816
- const isLockedFileInDir = normalizedLockedFile.startsWith(normalizedGitFile + '/');
817
-
818
- return isExactMatch || isFileInLockedDir || isLockedFileInDir;
819
- });
820
-
821
- // 额外检查:如果是目录路径,检查该目录下是否有任何未锁定的文件
822
- if (!isLocked && file.endsWith('/')) {
823
- // 这是一个目录路径,检查是否该目录下所有文件都被锁定
824
- const dirPath = file.slice(0, -1); // 移除末尾的 '/'
825
- const hasUnlockedFilesInDir = modifiedFiles.some(otherFile => {
826
- if (otherFile === file) return false; // 跳过目录本身
827
-
828
- const normalizedOtherFile = path.normalize(otherFile).replace(/\\/g, '/');
829
- const normalizedDirPath = dirPath.replace(/\\/g, '/');
830
-
831
- // 检查文件是否在这个目录下
832
- if (normalizedOtherFile.startsWith(normalizedDirPath + '/')) {
833
- // 检查这个文件是否被锁定
834
- const isOtherFileLocked = lockedFiles.some(lockedFile => {
835
- let normalizedLocked;
836
- if (path.isAbsolute(lockedFile)) {
837
- const absoluteLocked = path.normalize(lockedFile);
838
- if (absoluteLocked.startsWith(gitRoot)) {
839
- normalizedLocked = path.relative(gitRoot, absoluteLocked);
840
- } else {
841
- return false;
842
- }
843
- } else {
844
- normalizedLocked = path.normalize(lockedFile);
845
- }
846
-
847
- const normalizedLockedFile = normalizedLocked.replace(/\\/g, '/');
848
- return normalizedOtherFile === normalizedLockedFile ||
849
- normalizedOtherFile.startsWith(normalizedLockedFile + '/') ||
850
- normalizedLockedFile.startsWith(normalizedOtherFile + '/');
851
- });
852
-
853
- return !isOtherFileLocked; // 如果文件未锁定,返回 true
854
- }
855
- return false;
856
- });
857
-
858
- // 如果目录下没有未锁定的文件,则跳过这个目录
859
- if (!hasUnlockedFilesInDir) {
860
- console.log(chalk.yellow(`🔒 跳过目录(所有文件都被锁定): ${file}`));
861
- return false;
862
- }
863
- }
864
-
865
- if (isLocked) {
866
- console.log(chalk.yellow(`🔒 跳过锁定文件: ${file}`));
867
- return false;
868
- }
869
- return true;
870
- });
871
-
872
- if (filesToAdd.length === 0) {
873
- console.log(chalk.blue('📝 所有修改的文件都被锁定,没有文件需要添加'));
874
- return;
875
- }
876
-
877
- // 逐个添加未锁定的文件
878
- for (const file of filesToAdd) {
879
- await execGitCommand(`git add "${file}"`, {
880
- head: `git add ${file}`,
881
- log: false
882
- });
883
- }
884
-
885
- const skippedCount = modifiedFiles.length - filesToAdd.length;
886
- console.log(chalk.green(`✅ 已添加 ${filesToAdd.length} 个文件到暂存区${skippedCount > 0 ? ` (跳过 ${skippedCount} 个锁定文件)` : ''}`));
887
-
888
- } catch (error) {
889
- console.error(chalk.red('执行 git add 时出错:'), error.message);
890
- throw error;
891
- }
892
- }
893
-
894
- async function execAddAndCommit({statusOutput, commitMessage, exit}) {
895
- // 检查 -m 参数(提交信息)
896
- const commitMessageArg = process.argv.find(arg => arg.startsWith('-m'));
897
- if (commitMessageArg) {
898
- if (commitMessageArg.includes('=')) {
899
- // 处理 -m=<message> 的情况
900
- commitMessage = commitMessageArg.split('=')[1]?.replace(/^['"]|['"]$/g, '');
901
- } else {
902
- // 处理 -m <message> 的情况
903
- const index = process.argv.indexOf(commitMessageArg);
904
- if (index !== -1 && process.argv[index + 1]) {
905
- commitMessage = process.argv[index + 1]?.replace(/^['"]|['"]$/g, '');
906
- }
907
- }
908
- }
909
-
910
- // 检查命令行参数,判断是否有 -y 参数
911
- const autoCommit = process.argv.includes('-y');
912
-
913
- if (!autoCommit && !commitMessageArg) {
914
- // 如果没有 -y 参数,则等待用户输入提交信息
915
- const rl = readline.createInterface({
916
- input: process.stdin,
917
- output: process.stdout
918
- })
919
-
920
- function rlPromisify(fn) {
921
- return async (...args) => {
922
- return new Promise((resolve, reject) => fn(...args, resolve, reject))
923
- }
924
- }
925
-
926
- const question = rlPromisify(rl.question.bind(rl))
927
- commitMessage = await question('请输入提交信息:') || commitMessage;
928
- rl.close(); // 关闭 readline 接口
929
- }
930
-
931
- // 使用带锁定文件过滤的 git add
932
- if (statusOutput.includes('(use "git add')) {
933
- await execGitAddWithLockFilter();
934
- }
935
-
936
- // 提交前二次校验(包括未跟踪文件)
937
- const checkStatus = await execGitCommand('git status --porcelain --untracked-files=all', {log: false});
938
- if (!checkStatus.stdout.trim()) {
939
- console.log(chalk.yellow('⚠️ 没有检测到可提交的变更'));
940
- // exec_exit(exit)
941
- return commitMessage; // 返回提交信息(即使没有提交)
942
- }
943
-
944
- // 执行 git commit
945
- if (statusOutput.includes('Untracked files:') || statusOutput.includes('Changes not staged for commit') || statusOutput.includes('Changes to be committed')) {
946
- await execGitCommand(`git commit -m "${commitMessage}"`)
947
- }
948
-
949
- // 返回实际使用的提交信息
950
- return commitMessage;
951
- }
952
-
953
- // 添加时间格式化函数
954
- function formatDuration(ms) {
955
- const totalSeconds = Math.floor(ms / 1000);
956
- const days = Math.floor(totalSeconds / (3600 * 24));
957
- const hours = Math.floor((totalSeconds % (3600 * 24)) / 3600);
958
- const minutes = Math.floor((totalSeconds % 3600) / 60);
959
- const seconds = totalSeconds % 60;
960
-
961
- return [
962
- days && `${days}天`,
963
- hours && `${hours}小时`,
964
- minutes && `${minutes}分`,
965
- `${seconds}秒`
966
- ].filter(Boolean).join('');
967
- }
968
-
969
- async function addScriptToPackageJson() {
970
- try {
971
- // 读取当前目录的 package.json
972
- const packagePath = path.join(process.cwd(), 'package.json');
973
- const packageJson = JSON.parse(await fs.readFile(packagePath, 'utf8'));
974
-
975
- // 确保有 scripts 部分
976
- if (!packageJson.scripts) {
977
- packageJson.scripts = {};
978
- }
979
-
980
- // 添加 g:y 命令
981
- if (!packageJson.scripts['g:y']) {
982
- packageJson.scripts['g:y'] = 'g -y';
983
- // 写回文件
984
- await fs.writeFile(packagePath, JSON.stringify(packageJson, null, 2));
985
- console.log(chalk.green('✓ 成功添加 g:y 脚本到 package.json'));
986
- } else {
987
- console.log(chalk.yellow('⚠️ g:y 脚本已存在'));
988
- }
989
- } catch (error) {
990
- if (error.code === 'ENOENT') {
991
- console.error(chalk.red('❌ 当前目录下未找到 package.json 文件'));
992
- } else {
993
- console.error(chalk.red('❌ 添加脚本失败:'), error.message);
994
- }
995
- process.exit(1);
996
- }
997
- }
998
-
999
- async function addResetScriptToPackageJson() {
1000
- try {
1001
- // 读取当前目录的 package.json
1002
- const packagePath = path.join(process.cwd(), 'package.json');
1003
- const packageJson = JSON.parse(await fs.readFile(packagePath, 'utf8'));
1004
-
1005
- // 确保有 scripts 部分
1006
- if (!packageJson.scripts) {
1007
- packageJson.scripts = {};
1008
- }
1009
-
1010
- // 获取当前分支名
1011
- const branchResult = await execGitCommand('git branch --show-current', {log: false});
1012
- const branch = branchResult.stdout.trim();
1013
-
1014
- // 添加 g:reset 命令
1015
- if (!packageJson.scripts['g:reset']) {
1016
- packageJson.scripts['g:reset'] = `git reset --hard origin/${branch}`;
1017
- // 写回文件
1018
- await fs.writeFile(packagePath, JSON.stringify(packageJson, null, 2));
1019
- console.log(chalk.green(`✓ 成功添加 g:reset 脚本到 package.json (重置到 origin/${branch})`));
1020
- } else {
1021
- console.log(chalk.yellow('⚠️ g:reset 脚本已存在'));
1022
- }
1023
- } catch (error) {
1024
- if (error.code === 'ENOENT') {
1025
- console.error(chalk.red('❌ 当前目录下未找到 package.json 文件'));
1026
- } else {
1027
- console.error(chalk.red('❌ 添加脚本失败:'), error.message);
1028
- }
1029
- process.exit(1);
1030
- }
1031
- }
1032
-
1033
- export {
1034
- coloredLog, errorLog, execSyncGitCommand,
1035
- execGitCommand, getCommandHistory, addCommandToHistory, // Add command history exports
1036
- clearCommandHistory,
1037
- checkAndClearGitLock,
1038
- registerSocketIO, // 导出注册Socket.io的函数
1039
- getCwd, judgePlatform, showHelp, judgeLog, printGitLog,
1040
- judgeHelp, exec_exit, judgeUnmerged, delay, formatDuration,
1041
- exec_push, execPull, judgeRemote, execDiff, execAddAndCommit,
1042
- execGitAddWithLockFilter, // 导出新的 git add 函数
1043
- addScriptToPackageJson, addResetScriptToPackageJson
1044
- };
1
+ // const chalk = require('chalk');
2
+ // const boxen = require('boxen');
3
+ // const message = chalk.blue('git diff') + '\n' +
4
+ // chalk.red('- line1') + '\n' +
5
+ // chalk.green('+ line2') + '\n' +
6
+ // chalk.cyan('@@ line diff @@');
7
+ //
8
+ // const options = {
9
+ // padding: 1,
10
+ // margin: 1,
11
+ // borderStyle: 'round', // 可以选择 'single', 'double', 'round' 等边框样式
12
+ // borderColor: 'yellow'
13
+ // };
14
+ //
15
+ // const boxedMessage = boxen(message, options);
16
+ // console.log(boxedMessage);
17
+ import stringWidth from 'string-width';
18
+ import Table from 'cli-table3';
19
+ import chalk from 'chalk';
20
+ import boxen from "boxen";
21
+ import {exec, execSync} from 'child_process'
22
+ import os from 'os'
23
+ import ora from "ora";
24
+ import readline from 'readline'
25
+ import path from 'path'
26
+ import fs from 'fs/promises'
27
+ import config from '../config.js'
28
+
29
+
30
+ const printTableWithHeaderUnderline = (head, content, style) => {
31
+ // 获取终端的列数(宽度)
32
+ const terminalWidth = process.stdout.columns || 100;
33
+
34
+ // 计算表格的宽度,保证至少有 2 个字符留给边框
35
+ const tableWidth = terminalWidth - 2; // 左右边框和分隔符的宽度
36
+
37
+ // 计算每列的宽度
38
+ const colWidths = [tableWidth]; // 只有一列,因此宽度设置为终端宽度
39
+
40
+ if (!style) {
41
+ style = {
42
+ // head: ['cyan'], // 表头文字颜色为cyan
43
+ border: [chalk.reset()], // 边框颜色
44
+ compact: true, // 启用紧凑模式,去掉不必要的空白
45
+ }
46
+ }
47
+ // 创建表格实例
48
+ const table = new Table({
49
+ head: [head], // 只有一个表头
50
+ colWidths, // 使用动态计算的列宽
51
+ style: style,
52
+ wordWrap: true, // 启用自动换行
53
+ // chars: {
54
+ // 'top': '─',
55
+ // 'top-mid': '┬',
56
+ // 'bottom': '─',
57
+ // 'mid': '─',
58
+ // 'left': '│',
59
+ // 'right': '│'
60
+ // },
61
+ // chars: {
62
+ // 'top': '═', // 顶部边框使用长横线
63
+ // 'top-mid': '╤', // 顶部连接符
64
+ // 'top-left': '╔', // 左上角
65
+ // 'top-right': '╗', // 右上角
66
+ // 'bottom': '═', // 底部边框
67
+ // 'bottom-mid': '╧', // 底部连接符
68
+ // 'bottom-left': '╚',// 左下角
69
+ // 'bottom-right': '╝',// 右下角
70
+ // 'left': '║', // 左边框
71
+ // 'left-mid': '╟', // 左连接符
72
+ // 'mid': '═', // 中间分隔符
73
+ // 'mid-mid': '╪', // 中间连接符
74
+ // 'right': '║', // 右边框
75
+ // 'right-mid': '╢', // 右连接符
76
+ // 'middle': '│' // 中间内容的边界
77
+ // }
78
+ });
79
+
80
+
81
+ content.forEach(item => {
82
+ table.push([item]);
83
+ })
84
+
85
+ console.log(table.toString()); // 输出表格
86
+ };
87
+
88
+ // printTableWithHeaderUnderline();
89
+
90
+ const colors = [
91
+ '\x1b[31m', // 红色
92
+ '\x1b[32m', // 绿色
93
+ '\x1b[33m', // 黄色
94
+ '\x1b[34m', // 蓝色
95
+ '\x1b[35m', // 紫色
96
+ '\x1b[36m', // 青色
97
+ ];
98
+
99
+ function getRandomColor() {
100
+ return `\x1b[0m`;
101
+ // const randomIndex = Math.floor(Math.random() * colors.length);
102
+ // return colors[randomIndex];
103
+ }
104
+
105
+ function resetColor() {
106
+ return '\x1b[0m';
107
+ }
108
+
109
+ const calcColor = (commandLine, str) => {
110
+ let color = 'reset'
111
+ switch (commandLine) {
112
+ case 'git status':
113
+ if (str.startsWith('\t')) {
114
+ color = 'red'
115
+ if (str.startsWith('new file:')) {
116
+ color = 'red'
117
+ }
118
+ if (str.startsWith('modified:')) {
119
+ color = 'green'
120
+ }
121
+ if (str.startsWith('deleted:')) {
122
+ color = 'red'
123
+ }
124
+ }
125
+ break;
126
+ case 'git diff':
127
+ // if (str.startsWith('---')) {
128
+ // color = 'red'
129
+ // }
130
+ // if (str.startsWith('+++')) {
131
+ // color = 'green'
132
+ // }
133
+ // if (str.startsWith('@@ ')) {
134
+ // color = 'cyan'
135
+ // }
136
+ break;
137
+ }
138
+ return color
139
+ }
140
+ const tableLog = (commandLine, content, type) => {
141
+ let handle_commandLine = `> ${commandLine}`
142
+ let head = chalk.bold.blue(handle_commandLine)
143
+ let style = {
144
+ // head: ['cyan'], // 表头文字颜色为cyan
145
+ border: [chalk.reset()], // 边框颜色
146
+ compact: true, // 启用紧凑模式,去掉不必要的空白
147
+ }
148
+ switch (type) {
149
+ case 'error':
150
+ style.head = ['red'];
151
+ content = content.toString().split('\n')
152
+ head = chalk.bold.red(handle_commandLine)
153
+ break;
154
+ case 'common':
155
+ style.head = ['blue'];
156
+ content = content.split('\n')
157
+ break;
158
+ default:
159
+ break;
160
+ }
161
+
162
+ // 限制输出内容
163
+ const MAX_LINES = 10; // 最大行数
164
+ const MAX_LINE_LENGTH = 200; // 每行最大字符数
165
+ let isTruncated = false;
166
+
167
+ if (content.length > MAX_LINES) {
168
+ content = content.slice(0, MAX_LINES);
169
+ isTruncated = true;
170
+ }
171
+
172
+ content = content.map(item => {
173
+ let fontColor = calcColor(commandLine, item)
174
+ let row = item.replaceAll('\t', ' ')
175
+ // 截断过长的行
176
+ if (row.length > MAX_LINE_LENGTH) {
177
+ row = row.substring(0, MAX_LINE_LENGTH) + '...';
178
+ }
179
+ const result = chalk[fontColor](row)
180
+ return result
181
+ })
182
+
183
+ // 如果内容被截断,添加提示
184
+ if (isTruncated) {
185
+ content.push(chalk.dim('... (输出内容过多,已省略)'));
186
+ }
187
+
188
+ printTableWithHeaderUnderline(head, content, style)
189
+ }
190
+ const coloredLog = (...args) => {
191
+ // 获取参数内容
192
+ const commandLine = args[0];
193
+ const content = args[1];
194
+ const type = args[2] || 'common';
195
+ // console.log(`commandLine ==> `, commandLine)
196
+ // console.log(`content ==> `, content)
197
+ // console.log(`type ==> `, type)
198
+ tableLog(commandLine, content, type);
199
+ }
200
+ const errorLog = (commandLine, content) => {
201
+ // 使用 boxen 绘制带边框的消息
202
+ let msg = ` FAIL ${commandLine}
203
+ content: ${content} `
204
+ const message = chalk.red.bold(msg);
205
+ const box = boxen(message);
206
+ console.log(box); // 打印带有边框的消息
207
+ }
208
+
209
+ function execSyncGitCommand(command, options = {}) {
210
+ let {encoding = 'utf-8', maxBuffer = 30 * 1024 * 1024, head = command, log = true} = options
211
+ try {
212
+ let cwd = getCwd()
213
+ const output = execSync(command, {
214
+ env: {
215
+ ...process.env,
216
+ // LANG: 'en_US.UTF-8', // Linux/macOS
217
+ // LC_ALL: 'en_US.UTF-8', // Linux/macOS
218
+ GIT_CONFIG_PARAMETERS: "'core.quotepath=false'" // 关闭路径转义
219
+ }, encoding, maxBuffer, cwd
220
+ })
221
+ if (options.spinner) {
222
+ options.spinner.stop();
223
+ }
224
+ let result = output.trim()
225
+ log && coloredLog(head, result)
226
+ // 打印当前目录和时间信息
227
+ if (log) {
228
+ const currentTime = new Date().toLocaleString('zh-CN', { hour12: false });
229
+ console.log(chalk.dim(`📁 目录: ${cwd} | ⏰ 时间: ${currentTime}`));
230
+ }
231
+ return result
232
+ } catch (e) {
233
+ // console.log(`执行命令出错 ==> `, command, e)
234
+ log && coloredLog(command, e, 'error')
235
+ throw new Error(e)
236
+ }
237
+ }
238
+
239
+ // Add a command history array to store commands and their results
240
+ const commandHistory = [];
241
+ const MAX_HISTORY_SIZE = 100;
242
+ const MAX_OUTPUT_LENGTH = 5000; // Limit the output length to avoid memory issues
243
+
244
+ // 添加一个变量来保存Socket.io实例
245
+ let ioInstance = null;
246
+
247
+ // 提供注册Socket.io实例的函数
248
+ function registerSocketIO(io) {
249
+ ioInstance = io;
250
+ }
251
+
252
+ // 清空命令历史记录
253
+ function clearCommandHistory() {
254
+ // 清空数组
255
+ commandHistory.length = 0;
256
+
257
+ // 通过WebSocket广播历史已清空
258
+ if (ioInstance) {
259
+ ioInstance.emit('command_history_cleared');
260
+ }
261
+
262
+ return true;
263
+ }
264
+
265
+ function execGitCommand(command, options = {}) {
266
+ return new Promise((resolve, reject) => {
267
+ let {encoding = 'utf-8', maxBuffer = 30 * 1024 * 1024, head = command, log = true} = options
268
+ let cwd = getCwd()
269
+
270
+ // Record start time for command execution
271
+ const startTime = Date.now();
272
+
273
+ // setTimeout(() => {
274
+ exec(command, {
275
+ env: {
276
+ ...process.env,
277
+ // LANG: 'en_US.UTF-8', // Linux/macOS
278
+ // LC_ALL: 'en_US.UTF-8', // Linux/macOS
279
+ GIT_CONFIG_PARAMETERS: "'core.quotepath=false'" // 关闭路径转义
280
+ },
281
+ encoding,
282
+ maxBuffer,
283
+ cwd
284
+ }, (error, stdout, stderr) => {
285
+ if (options.spinner) {
286
+ options.spinner.stop();
287
+ }
288
+
289
+ // Calculate execution time
290
+ const executionTime = Date.now() - startTime;
291
+
292
+ // Truncate long outputs
293
+ let truncatedStdout = stdout;
294
+ let truncatedStderr = stderr;
295
+ let isStdoutTruncated = false;
296
+ let isStderrTruncated = false;
297
+
298
+ if (stdout && stdout.length > MAX_OUTPUT_LENGTH) {
299
+ truncatedStdout = stdout.substring(0, MAX_OUTPUT_LENGTH) + '\n... (output truncated)';
300
+ isStdoutTruncated = true;
301
+ }
302
+
303
+ if (stderr && stderr.length > MAX_OUTPUT_LENGTH) {
304
+ truncatedStderr = stderr.substring(0, MAX_OUTPUT_LENGTH) + '\n... (error output truncated)';
305
+ isStderrTruncated = true;
306
+ }
307
+
308
+ // Add command to history
309
+ const historyItem = {
310
+ command,
311
+ stdout: truncatedStdout || '',
312
+ stderr: truncatedStderr || '',
313
+ error: error ? error.message : null,
314
+ executionTime,
315
+ timestamp: new Date().toISOString(),
316
+ success: !error,
317
+ isStdoutTruncated,
318
+ isStderrTruncated
319
+ };
320
+
321
+ // Add to history (limited size)
322
+ commandHistory.unshift(historyItem);
323
+ if (commandHistory.length > MAX_HISTORY_SIZE) {
324
+ commandHistory.pop();
325
+ }
326
+
327
+ // 通过WebSocket广播命令历史更新
328
+ if (ioInstance) {
329
+ ioInstance.emit('command_history_update', {
330
+ newCommand: historyItem,
331
+ fullHistory: commandHistory.slice(0, 10) // 只发送最近10条以减小数据量
332
+ });
333
+ }
334
+
335
+ if (stdout) {
336
+ log && coloredLog(head, stdout)
337
+ }
338
+ if (stderr) {
339
+ log && coloredLog(head, stderr)
340
+ }
341
+ // 打印当前目录和时间信息
342
+ if (log && (stdout || stderr)) {
343
+ const currentTime = new Date().toLocaleString('zh-CN', { hour12: false });
344
+ console.log(chalk.dim(`📁 目录: ${cwd} | ⏰ 时间: ${currentTime}`));
345
+ }
346
+ if (error) {
347
+ log && coloredLog(head, error, 'error')
348
+ // 错误情况也打印目录和时间
349
+ if (log) {
350
+ const currentTime = new Date().toLocaleString('zh-CN', { hour12: false });
351
+ console.log(chalk.dim(`📁 目录: ${cwd} | ⏰ 时间: ${currentTime}`));
352
+ }
353
+ // 将 stdout 和 stderr 附加到 error 对象,以便上层可以获取完整输出
354
+ error.stdout = stdout
355
+ error.stderr = stderr
356
+ reject(error)
357
+ return
358
+ }
359
+ resolve({
360
+ stdout,
361
+ stderr
362
+ })
363
+ })
364
+ // }, 1000)
365
+ })
366
+ }
367
+
368
+ /**
369
+ * 检查并尝试清理 Git 锁文件
370
+ * @returns {Promise<boolean>} 是否清理成功
371
+ */
372
+ async function checkAndClearGitLock() {
373
+ try {
374
+ const cwd = getCwd();
375
+ let gitRoot;
376
+ try {
377
+ // 使用 execSync 快速获取 Git 根目录
378
+ const rootOutput = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' });
379
+ gitRoot = rootOutput.trim();
380
+ } catch (e) {
381
+ gitRoot = cwd;
382
+ }
383
+
384
+ const lockFilePath = path.join(gitRoot, '.git', 'index.lock');
385
+ try {
386
+ await fs.access(lockFilePath);
387
+ // 如果文件存在,尝试删除它
388
+ await fs.unlink(lockFilePath);
389
+ console.log(chalk.green(`✅ 已清理 Git 锁文件: ${lockFilePath}`));
390
+ return true;
391
+ } catch (e) {
392
+ // 文件不存在,不需要清理
393
+ return false;
394
+ }
395
+ } catch (error) {
396
+ console.error(chalk.red('清理 Git 锁文件失败:'), error.message);
397
+ return false;
398
+ }
399
+ }
400
+
401
+ // Function to get command history
402
+ function getCommandHistory() {
403
+ return [...commandHistory];
404
+ }
405
+
406
+ // Function to manually add command to history (for commands not using execGitCommand)
407
+ function addCommandToHistory(command, stdout = '', stderr = '', error = null, executionTime = 0) {
408
+ const MAX_OUTPUT_LENGTH = 5000;
409
+
410
+ // Truncate outputs if too long
411
+ const isStdoutTruncated = stdout.length > MAX_OUTPUT_LENGTH;
412
+ const isStderrTruncated = stderr.length > MAX_OUTPUT_LENGTH;
413
+ const truncatedStdout = isStdoutTruncated ? stdout.substring(0, MAX_OUTPUT_LENGTH) + '...[truncated]' : stdout;
414
+ const truncatedStderr = isStderrTruncated ? stderr.substring(0, MAX_OUTPUT_LENGTH) + '...[truncated]' : stderr;
415
+
416
+ const historyItem = {
417
+ command,
418
+ stdout: truncatedStdout || '',
419
+ stderr: truncatedStderr || '',
420
+ error: error ? (typeof error === 'string' ? error : error.message) : null,
421
+ executionTime,
422
+ timestamp: new Date().toISOString(),
423
+ success: !error,
424
+ isStdoutTruncated,
425
+ isStderrTruncated
426
+ };
427
+
428
+ // Add to history (limited size)
429
+ commandHistory.unshift(historyItem);
430
+ if (commandHistory.length > MAX_HISTORY_SIZE) {
431
+ commandHistory.pop();
432
+ }
433
+
434
+ // Broadcast via WebSocket if available
435
+ if (ioInstance) {
436
+ ioInstance.emit('command_history_update', {
437
+ newCommand: historyItem,
438
+ fullHistory: commandHistory.slice(0, 10)
439
+ });
440
+ }
441
+
442
+ return historyItem;
443
+ }
444
+
445
+ const getCwd = () => {
446
+ const cwdArg = process.argv.find(arg => arg.startsWith('--path')) || process.argv.find(arg => arg.startsWith('--cwd'));
447
+ if (cwdArg) {
448
+ // console.log(`cwdArg ==> `, cwdArg)
449
+ const [, value] = cwdArg.split('=')
450
+ // console.log(`value ==> `, value)
451
+ return value || process.cwd()
452
+ }
453
+ return process.cwd()
454
+ }
455
+ const judgePlatform = () => {
456
+ // 判断是否是 Windows 系统
457
+ if (os.platform() === 'win32') {
458
+ try {
459
+ // 设置终端字符编码为 UTF-8
460
+ execSync('chcp 65001');
461
+ execSync('git config --global core.autocrlf true');
462
+ // 设置Git不转义路径(避免中文显示为八进制)
463
+ execSync('git config --global core.quotepath false');
464
+ } catch (e) {
465
+ console.error('设置字符编码失败:', e.message);
466
+ }
467
+ }else{
468
+ execSync('git config --global core.autocrlf input');
469
+ }
470
+ };
471
+ const showHelp = () => {
472
+ const helpMessage = `
473
+ Usage: g [options]
474
+
475
+ Options:
476
+ -h, --help Show this help message
477
+ --set-default-message=<msg> Set default commit message
478
+ get-config Show current configuration
479
+ -y Auto commit with default message
480
+ -m <message> Commit message (use quotes if message contains spaces)
481
+ -m=<message> Commit message (use this form without spaces around '=')
482
+ --path=<path> Set custom working directory
483
+ --cwd=<path> Set custom working directory
484
+ --interval=<seconds> Set interval time for automatic commits (in seconds)
485
+ log Show git commit logs
486
+ --n=<number> Number of commits to show with --log
487
+ --no-diff Skip displaying git diff
488
+ addScript Add "g:y": "g -y" to package.json scripts
489
+ addResetScript Add "g:reset": "git reset --hard origin/<current-branch>" to package.json scripts
490
+ ui Launch graphical user interface (v2.0.0)
491
+
492
+ File Locking:
493
+ --lock-file=<path> Lock a file to exclude it from commits
494
+ --unlock-file=<path> Unlock a previously locked file
495
+ --list-locked List all currently locked files
496
+ --check-lock=<path> Check if a file is locked
497
+
498
+ --cmd="your-cmd" Execute custom cmd command (immediately, at a time, or periodically)
499
+ --cmd-interval=<seconds> Execute custom cmd every N seconds
500
+ --at="HH:MM" Execute custom cmd at a specific time (today) or --at="YYYY-MM-DD HH:MM:SS"
501
+ --daily Repeat with --at every day at the same time
502
+
503
+ Example:
504
+ g --cmd="echo hello" --cmd-interval=5 # 每5秒执行一次echo hello
505
+ g --cmd="echo at-time" --at=23:59 # 在23:59执行一次echo at-time
506
+ g --cmd="echo daily" --at=23:59 --daily # 每天23:59执行一次echo daily
507
+ g --cmd="echo now" # 立即执行一次echo now
508
+ g --cmd="echo hi" --cmd-interval=10 --interval=60 # cmd和git自动提交并行
509
+ g -m "Initial commit" Commit with a custom message
510
+ g -m=Fix-bug Commit with a custom message (no spaces around '=')
511
+ g -y Auto commit with the default message
512
+ g -y --interval=600 Commit every 10 minutes (600 seconds)
513
+ g --path=/path/to/repo Specify a custom working directory
514
+ g log Show recent commit logs
515
+ g log --n=5 Show the last 5 commits with --log
516
+ g addScript Add auto commit script to package.json
517
+ g addResetScript Add reset script to package.json
518
+ g --lock-file=config.json Lock config.json file
519
+ g --unlock-file=config.json Unlock config.json file
520
+ g --list-locked List all locked files
521
+
522
+ Add auto submit in package.json:
523
+ "scripts": {
524
+ "g:y": "g -y",
525
+ "g:reset": "git reset --hard origin/<current-branch>"
526
+ }
527
+
528
+ Run in the background across platforms:
529
+ Windows:
530
+ start /min cmd /k "g -y --path=your-folder --interval=600"
531
+
532
+ Linux/macOS:
533
+ nohup g -y --path=your-folder --interval=600 > git-autocommit.log 2>&1 &
534
+
535
+ Start GUI interface:
536
+ g ui
537
+
538
+ Stop all monitoring processes:
539
+ Windows: Terminate the Node.js process in the Task Manager.
540
+ Linux/macOS:
541
+ pkill -f "g -y" # Terminate all auto-commit processes
542
+ ps aux | grep "g -y" # Find the specific process ID
543
+ kill [PID] # Terminate the specified process
544
+ `;
545
+
546
+ console.log(helpMessage);
547
+ process.exit();
548
+ };
549
+
550
+ function judgeLog() {
551
+ const logArg = process.argv.find(arg => arg === 'log');
552
+ if (logArg) {
553
+ printGitLog(); // 如果有 log 参数,打印 Git 提交记录
554
+ // 打印完成后退出
555
+ process.exit();
556
+ }
557
+ }
558
+
559
+ function judgeHelp() {
560
+ if (process.argv.includes('-h') || process.argv.includes('--help')) {
561
+ showHelp();
562
+ }
563
+ }
564
+
565
+ async function printGitLog() {
566
+ let n = 20;
567
+ let logArg = process.argv.find(arg => arg.startsWith('--n='));
568
+ if (logArg) {
569
+ n = parseInt(logArg.split('=')[1], 10);
570
+ }
571
+ // 使用 ASCII 记录分隔符 %x1E 作为字段分隔符
572
+ const logCommand = `git log -n ${n} --pretty=format:"%C(green)%h%C(reset) %x1E %C(cyan)%an%C(reset) %x1E %C(yellow)%ad%C(reset) %x1E %C(blue)%D%C(reset) %x1E %C(magenta)%s%C(reset)" --date=format:"%Y-%m-%d %H:%M" --graph --decorate --color`
573
+ try {
574
+ const logOutput = await execGitCommand(logCommand, {
575
+ head: `git log`
576
+ });
577
+ } catch (error) {
578
+ console.error('无法获取 Git 提交记录:', error.message);
579
+ }
580
+ // 打印完成后退出
581
+ process.exit();
582
+ }
583
+
584
+ function exec_exit(exit) {
585
+ if (exit) {
586
+ process.exit()
587
+ }
588
+ }
589
+
590
+ function judgeUnmerged(statusOutput) {
591
+ const hasUnmerged = statusOutput.includes('You have unmerged paths');
592
+ if (hasUnmerged) {
593
+ errorLog('错误', '存在未合并的文件,请先解决冲突')
594
+ process.exit(1);
595
+ }
596
+ }
597
+
598
+ async function exec_push({exit, commitMessage}) {
599
+ // 执行 git push
600
+ const spinner = ora('正在推送代码...').start();
601
+ try {
602
+ const {stdout, stderr} = await execGitCommand('git push', {
603
+ spinner
604
+ });
605
+ await printCommitLog({commitMessage});
606
+ return {stdout, stderr};
607
+ } catch (error) {
608
+ throw error;
609
+ }
610
+ }
611
+
612
+ async function printCommitLog({commitMessage}) {
613
+ try {
614
+ // 获取项目名称(取git仓库根目录名)
615
+ const projectRootResult = await execGitCommand('git rev-parse --show-toplevel', {log: false});
616
+ const projectName = chalk.blueBright(path.basename(projectRootResult.stdout.trim()));
617
+
618
+ // 获取当前提交hash(取前7位)
619
+ const commitHashResult = await execGitCommand('git rev-parse --short HEAD', {log: false});
620
+ const hashDisplay = chalk.yellow(commitHashResult.stdout.trim());
621
+
622
+ // 获取分支信息
623
+ const branchResult = await execGitCommand('git branch --show-current', {log: false});
624
+ const branchDisplay = chalk.magenta(branchResult.stdout.trim());
625
+
626
+ // 构建信息内容
627
+ const message = [
628
+ `${chalk.cyan.bold('Project:')} ${projectName}`,
629
+ `${chalk.cyan.bold('Commit:')} ${hashDisplay} ${chalk.dim('on')} ${branchDisplay}`,
630
+ `${chalk.cyan.bold('Message:')} ${chalk.reset(commitMessage)}`,
631
+ `${chalk.cyan.bold('Time:')} ${new Date().toLocaleString()}`
632
+ ].join('\n');
633
+
634
+ // 使用boxen创建装饰框
635
+ const box = boxen(message, {
636
+ padding: 1,
637
+ margin: 1,
638
+ borderStyle: 'round',
639
+ borderColor: 'green',
640
+ title: chalk.bold.green('✅ COMMIT SUCCESS'),
641
+ titleAlignment: 'center',
642
+ float: 'left',
643
+ textAlignment: 'left'
644
+ });
645
+
646
+ console.log(box);
647
+ } catch (error) {
648
+ // 异常处理
649
+ const errorBox = boxen(chalk.red(`Failed to get commit details: ${error.message}`), {
650
+ borderColor: 'red',
651
+ padding: 1
652
+ });
653
+ console.log(errorBox);
654
+ }
655
+ }
656
+
657
+ async function execPull() {
658
+ try {
659
+ // 检查是否需要拉取更新
660
+ const spinner = ora('正在拉取代码...').start();
661
+ await execGitCommand('git pull', {
662
+ spinner
663
+ })
664
+ } catch (e) {
665
+ console.log(chalk.yellow('⚠️ 拉取远程更新合并失败,可能存在冲突,请手动处理'));
666
+ throw Error(e)
667
+ }
668
+ }
669
+
670
+ function delay(timeout) {
671
+ return new Promise(resolve => setTimeout(resolve, timeout));
672
+ }
673
+
674
+ async function judgeRemote() {
675
+ const spinner = ora('正在检查远程更新...').start();
676
+ try {
677
+ // 检查是否有远程更新
678
+ // 先获取远程最新状态
679
+ await execGitCommand('git remote update', {
680
+ head: 'Fetching remote updates',
681
+ log: false
682
+ });
683
+ // 检查是否需要 pull
684
+ const res = await execGitCommand('git rev-list HEAD..@{u} --count', {
685
+ head: 'Checking if behind remote',
686
+ log: false
687
+ });
688
+ const behindCount = res.stdout.trim()
689
+ const { green, black, bgGreen, white } = chalk;
690
+ // 如果本地落后于远程
691
+ if (parseInt(behindCount) > 0) {
692
+ try {
693
+ spinner.stop();
694
+ // const spinner_pull = ora('发现远程更新,正在拉取...').start();
695
+ await execPull()
696
+
697
+ // // 尝试使用 --ff-only 拉取更新
698
+ // const res = await execGitCommand('git pull --ff-only', {
699
+ // spinner: spinner_pull,
700
+ // head: 'Pulling updates'
701
+ // });
702
+ // console.log(
703
+ // bgGreen.white.bold(' SYNC ') +
704
+ // green` ➔ ` +
705
+ // chalk.blue.bold('远程仓库已同步') +
706
+ // green(' ✔')
707
+ // );
708
+ const message = '已成功同步远程更新'.split('').map((char, i) =>
709
+ chalk.rgb(0, 255 - i*10, 0)(char)
710
+ ).join('');
711
+
712
+ console.log(chalk.bold(`✅ ${message}`));
713
+ } catch (pullError) {
714
+ // // 如果 --ff-only 拉取失败,尝试普通的 git pull
715
+ // console.log(chalk.yellow('⚠️ 无法快进合并,尝试普通合并...'));
716
+ // await this.execPull()
717
+ throw new Error(pullError)
718
+ }
719
+ } else {
720
+ spinner.stop();
721
+ const message = '本地已是最新'.split('').map((char, i) =>
722
+ chalk.rgb(0, 255 - i*10, 0)(char)
723
+ ).join('');
724
+ console.log(chalk.bold(`✅ ${message}`));
725
+ }
726
+ } catch (e) {
727
+ // console.log(`e ==> `, e)
728
+ spinner.stop();
729
+ throw new Error(e)
730
+ }
731
+ }
732
+
733
+ async function execDiff() {
734
+ const no_diff = process.argv.find(arg => arg.startsWith('--no-diff'))
735
+ if (!no_diff) {
736
+ await execGitCommand('git diff --color=always', {
737
+ head: `git diff`
738
+ })
739
+ }
740
+ }
741
+
742
+ // 执行 git add 但排除锁定的文件
743
+ async function execGitAddWithLockFilter() {
744
+ try {
745
+ // 获取锁定的文件列表
746
+ const lockedFiles = await config.getLockedFiles();
747
+
748
+ if (lockedFiles.length === 0) {
749
+ // 如果没有锁定文件,直接执行 git add .
750
+ await execGitCommand('git add .');
751
+ return;
752
+ }
753
+
754
+ // 获取Git工作目录根路径,确保路径匹配的准确性
755
+ let gitRoot;
756
+ try {
757
+ const gitRootResult = await execGitCommand('git rev-parse --show-toplevel', {log: false});
758
+ gitRoot = path.normalize(gitRootResult.stdout.trim());
759
+ } catch (error) {
760
+ console.warn(chalk.yellow('⚠️ 无法获取Git根目录,使用当前工作目录'));
761
+ gitRoot = path.normalize(process.cwd());
762
+ }
763
+
764
+ // 获取所有修改的文件(包括未跟踪文件)
765
+ const statusResult = await execGitCommand('git status --porcelain --untracked-files=all', {log: false});
766
+ const modifiedFiles = statusResult.stdout
767
+ .split('\n')
768
+ .filter(line => line.trim())
769
+ .map(line => {
770
+ // 解析 git status --porcelain 的输出格式
771
+ // 格式: XY filename 或 XY "filename with spaces"
772
+ const match = line.match(/^..\s+(.+)$/);
773
+ if (match) {
774
+ let filename = match[1];
775
+ // 如果文件名被引号包围,去掉引号
776
+ if (filename.startsWith('"') && filename.endsWith('"')) {
777
+ filename = filename.slice(1, -1);
778
+ // 处理转义字符
779
+ filename = filename.replace(/\\(.)/g, '$1');
780
+ }
781
+ return filename;
782
+ }
783
+ return null;
784
+ })
785
+ .filter(Boolean);
786
+
787
+ // 过滤掉锁定的文件,使用更严格的路径匹配逻辑
788
+ const filesToAdd = modifiedFiles.filter(file => {
789
+ // Git status 返回的是相对于Git根目录的路径
790
+ const gitRelativeFile = path.normalize(file);
791
+
792
+ const isLocked = lockedFiles.some(lockedFile => {
793
+ // 处理锁定文件路径:可能是相对路径或绝对路径
794
+ let normalizedLocked;
795
+ if (path.isAbsolute(lockedFile)) {
796
+ // 绝对路径:转换为相对于Git根目录的路径
797
+ const absoluteLocked = path.normalize(lockedFile);
798
+ if (absoluteLocked.startsWith(gitRoot)) {
799
+ normalizedLocked = path.relative(gitRoot, absoluteLocked);
800
+ } else {
801
+ // 锁定文件不在当前Git仓库中,跳过
802
+ return false;
803
+ }
804
+ } else {
805
+ // 相对路径:直接使用
806
+ normalizedLocked = path.normalize(lockedFile);
807
+ }
808
+
809
+ // 统一路径分隔符(Windows兼容性)
810
+ const normalizedGitFile = gitRelativeFile.replace(/\\/g, '/');
811
+ const normalizedLockedFile = normalizedLocked.replace(/\\/g, '/');
812
+
813
+ // 精确匹配或目录匹配(双向检查)
814
+ const isExactMatch = normalizedGitFile === normalizedLockedFile;
815
+ const isFileInLockedDir = normalizedGitFile.startsWith(normalizedLockedFile + '/');
816
+ const isLockedFileInDir = normalizedLockedFile.startsWith(normalizedGitFile + '/');
817
+
818
+ return isExactMatch || isFileInLockedDir || isLockedFileInDir;
819
+ });
820
+
821
+ // 额外检查:如果是目录路径,检查该目录下是否有任何未锁定的文件
822
+ if (!isLocked && file.endsWith('/')) {
823
+ // 这是一个目录路径,检查是否该目录下所有文件都被锁定
824
+ const dirPath = file.slice(0, -1); // 移除末尾的 '/'
825
+ const hasUnlockedFilesInDir = modifiedFiles.some(otherFile => {
826
+ if (otherFile === file) return false; // 跳过目录本身
827
+
828
+ const normalizedOtherFile = path.normalize(otherFile).replace(/\\/g, '/');
829
+ const normalizedDirPath = dirPath.replace(/\\/g, '/');
830
+
831
+ // 检查文件是否在这个目录下
832
+ if (normalizedOtherFile.startsWith(normalizedDirPath + '/')) {
833
+ // 检查这个文件是否被锁定
834
+ const isOtherFileLocked = lockedFiles.some(lockedFile => {
835
+ let normalizedLocked;
836
+ if (path.isAbsolute(lockedFile)) {
837
+ const absoluteLocked = path.normalize(lockedFile);
838
+ if (absoluteLocked.startsWith(gitRoot)) {
839
+ normalizedLocked = path.relative(gitRoot, absoluteLocked);
840
+ } else {
841
+ return false;
842
+ }
843
+ } else {
844
+ normalizedLocked = path.normalize(lockedFile);
845
+ }
846
+
847
+ const normalizedLockedFile = normalizedLocked.replace(/\\/g, '/');
848
+ return normalizedOtherFile === normalizedLockedFile ||
849
+ normalizedOtherFile.startsWith(normalizedLockedFile + '/') ||
850
+ normalizedLockedFile.startsWith(normalizedOtherFile + '/');
851
+ });
852
+
853
+ return !isOtherFileLocked; // 如果文件未锁定,返回 true
854
+ }
855
+ return false;
856
+ });
857
+
858
+ // 如果目录下没有未锁定的文件,则跳过这个目录
859
+ if (!hasUnlockedFilesInDir) {
860
+ console.log(chalk.yellow(`🔒 跳过目录(所有文件都被锁定): ${file}`));
861
+ return false;
862
+ }
863
+ }
864
+
865
+ if (isLocked) {
866
+ console.log(chalk.yellow(`🔒 跳过锁定文件: ${file}`));
867
+ return false;
868
+ }
869
+ return true;
870
+ });
871
+
872
+ if (filesToAdd.length === 0) {
873
+ console.log(chalk.blue('📝 所有修改的文件都被锁定,没有文件需要添加'));
874
+ return;
875
+ }
876
+
877
+ // 逐个添加未锁定的文件
878
+ for (const file of filesToAdd) {
879
+ await execGitCommand(`git add "${file}"`, {
880
+ head: `git add ${file}`,
881
+ log: false
882
+ });
883
+ }
884
+
885
+ const skippedCount = modifiedFiles.length - filesToAdd.length;
886
+ console.log(chalk.green(`✅ 已添加 ${filesToAdd.length} 个文件到暂存区${skippedCount > 0 ? ` (跳过 ${skippedCount} 个锁定文件)` : ''}`));
887
+
888
+ } catch (error) {
889
+ console.error(chalk.red('执行 git add 时出错:'), error.message);
890
+ throw error;
891
+ }
892
+ }
893
+
894
+ async function execAddAndCommit({statusOutput, commitMessage, exit}) {
895
+ // 检查 -m 参数(提交信息)
896
+ const commitMessageArg = process.argv.find(arg => arg.startsWith('-m'));
897
+ if (commitMessageArg) {
898
+ if (commitMessageArg.includes('=')) {
899
+ // 处理 -m=<message> 的情况
900
+ commitMessage = commitMessageArg.split('=')[1]?.replace(/^['"]|['"]$/g, '');
901
+ } else {
902
+ // 处理 -m <message> 的情况
903
+ const index = process.argv.indexOf(commitMessageArg);
904
+ if (index !== -1 && process.argv[index + 1]) {
905
+ commitMessage = process.argv[index + 1]?.replace(/^['"]|['"]$/g, '');
906
+ }
907
+ }
908
+ }
909
+
910
+ // 检查命令行参数,判断是否有 -y 参数
911
+ const autoCommit = process.argv.includes('-y');
912
+
913
+ if (!autoCommit && !commitMessageArg) {
914
+ // 如果没有 -y 参数,则等待用户输入提交信息
915
+ const rl = readline.createInterface({
916
+ input: process.stdin,
917
+ output: process.stdout
918
+ })
919
+
920
+ function rlPromisify(fn) {
921
+ return async (...args) => {
922
+ return new Promise((resolve, reject) => fn(...args, resolve, reject))
923
+ }
924
+ }
925
+
926
+ const question = rlPromisify(rl.question.bind(rl))
927
+ commitMessage = await question('请输入提交信息:') || commitMessage;
928
+ rl.close(); // 关闭 readline 接口
929
+ }
930
+
931
+ // 使用带锁定文件过滤的 git add
932
+ if (statusOutput.includes('(use "git add')) {
933
+ await execGitAddWithLockFilter();
934
+ }
935
+
936
+ // 提交前二次校验(包括未跟踪文件)
937
+ const checkStatus = await execGitCommand('git status --porcelain --untracked-files=all', {log: false});
938
+ if (!checkStatus.stdout.trim()) {
939
+ console.log(chalk.yellow('⚠️ 没有检测到可提交的变更'));
940
+ // exec_exit(exit)
941
+ return commitMessage; // 返回提交信息(即使没有提交)
942
+ }
943
+
944
+ // 执行 git commit
945
+ if (statusOutput.includes('Untracked files:') || statusOutput.includes('Changes not staged for commit') || statusOutput.includes('Changes to be committed')) {
946
+ await execGitCommand(`git commit -m "${commitMessage}"`)
947
+ }
948
+
949
+ // 返回实际使用的提交信息
950
+ return commitMessage;
951
+ }
952
+
953
+ // 添加时间格式化函数
954
+ function formatDuration(ms) {
955
+ const totalSeconds = Math.floor(ms / 1000);
956
+ const days = Math.floor(totalSeconds / (3600 * 24));
957
+ const hours = Math.floor((totalSeconds % (3600 * 24)) / 3600);
958
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
959
+ const seconds = totalSeconds % 60;
960
+
961
+ return [
962
+ days && `${days}天`,
963
+ hours && `${hours}小时`,
964
+ minutes && `${minutes}分`,
965
+ `${seconds}秒`
966
+ ].filter(Boolean).join('');
967
+ }
968
+
969
+ async function addScriptToPackageJson() {
970
+ try {
971
+ // 读取当前目录的 package.json
972
+ const packagePath = path.join(process.cwd(), 'package.json');
973
+ const packageJson = JSON.parse(await fs.readFile(packagePath, 'utf8'));
974
+
975
+ // 确保有 scripts 部分
976
+ if (!packageJson.scripts) {
977
+ packageJson.scripts = {};
978
+ }
979
+
980
+ // 添加 g:y 命令
981
+ if (!packageJson.scripts['g:y']) {
982
+ packageJson.scripts['g:y'] = 'g -y';
983
+ // 写回文件
984
+ await fs.writeFile(packagePath, JSON.stringify(packageJson, null, 2));
985
+ console.log(chalk.green('✓ 成功添加 g:y 脚本到 package.json'));
986
+ } else {
987
+ console.log(chalk.yellow('⚠️ g:y 脚本已存在'));
988
+ }
989
+ } catch (error) {
990
+ if (error.code === 'ENOENT') {
991
+ console.error(chalk.red('❌ 当前目录下未找到 package.json 文件'));
992
+ } else {
993
+ console.error(chalk.red('❌ 添加脚本失败:'), error.message);
994
+ }
995
+ process.exit(1);
996
+ }
997
+ }
998
+
999
+ async function addResetScriptToPackageJson() {
1000
+ try {
1001
+ // 读取当前目录的 package.json
1002
+ const packagePath = path.join(process.cwd(), 'package.json');
1003
+ const packageJson = JSON.parse(await fs.readFile(packagePath, 'utf8'));
1004
+
1005
+ // 确保有 scripts 部分
1006
+ if (!packageJson.scripts) {
1007
+ packageJson.scripts = {};
1008
+ }
1009
+
1010
+ // 获取当前分支名
1011
+ const branchResult = await execGitCommand('git branch --show-current', {log: false});
1012
+ const branch = branchResult.stdout.trim();
1013
+
1014
+ // 添加 g:reset 命令
1015
+ if (!packageJson.scripts['g:reset']) {
1016
+ packageJson.scripts['g:reset'] = `git reset --hard origin/${branch}`;
1017
+ // 写回文件
1018
+ await fs.writeFile(packagePath, JSON.stringify(packageJson, null, 2));
1019
+ console.log(chalk.green(`✓ 成功添加 g:reset 脚本到 package.json (重置到 origin/${branch})`));
1020
+ } else {
1021
+ console.log(chalk.yellow('⚠️ g:reset 脚本已存在'));
1022
+ }
1023
+ } catch (error) {
1024
+ if (error.code === 'ENOENT') {
1025
+ console.error(chalk.red('❌ 当前目录下未找到 package.json 文件'));
1026
+ } else {
1027
+ console.error(chalk.red('❌ 添加脚本失败:'), error.message);
1028
+ }
1029
+ process.exit(1);
1030
+ }
1031
+ }
1032
+
1033
+ export {
1034
+ coloredLog, errorLog, execSyncGitCommand,
1035
+ execGitCommand, getCommandHistory, addCommandToHistory, // Add command history exports
1036
+ clearCommandHistory,
1037
+ checkAndClearGitLock,
1038
+ registerSocketIO, // 导出注册Socket.io的函数
1039
+ getCwd, judgePlatform, showHelp, judgeLog, printGitLog,
1040
+ judgeHelp, exec_exit, judgeUnmerged, delay, formatDuration,
1041
+ exec_push, execPull, judgeRemote, execDiff, execAddAndCommit,
1042
+ execGitAddWithLockFilter, // 导出新的 git add 函数
1043
+ addScriptToPackageJson, addResetScriptToPackageJson
1044
+ };