zen-gitsync 2.15.11 → 2.15.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/package.json +3 -3
  2. package/src/gitCommit.js +475 -475
  3. package/src/ui/public/assets/AppVersionBadge-BaXEI9Aq.js +2 -0
  4. package/src/ui/public/assets/BranchSelector-DJhEF0y9.js +1 -0
  5. package/src/ui/public/assets/CommitForm-BfpcK-bz.js +6 -0
  6. package/src/ui/public/assets/ConsoleView-D838O0O6.js +17 -0
  7. package/src/ui/public/assets/CustomCommandManager-BcCK46df.js +1 -0
  8. package/src/ui/public/assets/EditorView-DnnqYNGF.js +0 -0
  9. package/src/ui/public/assets/{FlowExecutionViewer-CJgoxffE.js → FlowExecutionViewer-CSlMmcDS.js} +1 -1
  10. package/src/ui/public/assets/FlowOrchestrationWorkspace-UgHpZ3FR.js +21 -0
  11. package/src/ui/public/assets/LogList-uUgskwwp.js +7 -0
  12. package/src/ui/public/assets/MindmapView-CCmFMiHR.js +1 -0
  13. package/src/ui/public/assets/MonitorView-BxoqeX70.js +1 -0
  14. package/src/ui/public/assets/ProjectStartupButton-C3AwivIB.js +1 -0
  15. package/src/ui/public/assets/{RemoteRepoCard-CKQguRWi.js → RemoteRepoCard-BbxeVpje.js} +1 -1
  16. package/src/ui/public/assets/SourceMapView-DKv-Y6t_.js +3 -0
  17. package/src/ui/public/assets/{SvgIcon-DOkE0Qie.js → SvgIcon-sMM8XFgm.js} +1 -1
  18. package/src/ui/public/assets/TemplateManager-Dzj8rXVw.js +1 -0
  19. package/src/ui/public/assets/UserInputNode-BU7Trzq-.js +1 -0
  20. package/src/ui/public/assets/WorkbenchView-DTgRoOkv.js +12 -0
  21. package/src/ui/public/assets/{_plugin-vue_export-helper-DvV-QrsM.js → _plugin-vue_export-helper-8ryIYVvs.js} +1 -1
  22. package/src/ui/public/assets/{configStore-BRR4SloR.js → configStore-D77uNk8X.js} +1 -1
  23. package/src/ui/public/assets/element-plus-D8SQiesr.js +17 -0
  24. package/src/ui/public/assets/element-plus-DB_sOyH_.css +1 -0
  25. package/src/ui/public/assets/{flow-mindmap-BMvnvRl8.js → flow-mindmap-BLa-kSA7.js} +1 -1
  26. package/src/ui/public/assets/index-CpFreGqX.js +23 -0
  27. package/src/ui/public/assets/{socket-io-C2w3MxRu.js → socket-io-B1foIbJS.js} +1 -1
  28. package/src/ui/public/assets/{vendor--LaaJk9m.js → vendor-B9orL8iU.js} +37 -37
  29. package/src/ui/public/assets/vendor-Dm_yd-By.css +1 -0
  30. package/src/ui/public/assets/{vue-flow-MtbKS8yD.js → vue-flow-BY7LODit.js} +1 -1
  31. package/src/ui/public/favicon.svg +75 -75
  32. package/src/ui/public/index.html +27 -27
  33. package/src/ui/public/logo.svg +74 -74
  34. package/src/ui/server/middleware/requestLogger.js +53 -53
  35. package/src/ui/server/routes/branchStatus.js +105 -105
  36. package/src/ui/server/routes/fileOpen.js +309 -309
  37. package/src/ui/server/routes/fs.js +791 -791
  38. package/src/ui/server/routes/git/diff.js +426 -426
  39. package/src/ui/server/routes/git/diffUtils.js +149 -149
  40. package/src/ui/server/routes/git/stash.js +550 -550
  41. package/src/ui/server/routes/git/tags.js +176 -176
  42. package/src/ui/server/routes/git.js +181 -181
  43. package/src/ui/server/routes/gitOps.js +1211 -1211
  44. package/src/ui/server/routes/process.js +86 -86
  45. package/src/ui/server/routes/status.js +67 -67
  46. package/src/ui/server/utils/createSavePortToFile.js +48 -48
  47. package/src/ui/server/utils/instanceRegistry.js +282 -282
  48. package/src/ui/server/utils/startServerOnAvailablePort.js +116 -116
  49. package/src/utils/index.js +1168 -1168
  50. package/src/ui/public/assets/AppVersionBadge-CEhrATQo.js +0 -2
  51. package/src/ui/public/assets/BranchSelector-xLl3O-4I.js +0 -1
  52. package/src/ui/public/assets/CommitForm--izGtr_x.js +0 -6
  53. package/src/ui/public/assets/ConsoleView-BLYfnXAy.js +0 -17
  54. package/src/ui/public/assets/CustomCommandManager-D8Qd3b9m.js +0 -1
  55. package/src/ui/public/assets/EditorView-DG8tqp3L.js +0 -0
  56. package/src/ui/public/assets/FlowOrchestrationWorkspace-BAIqeASp.js +0 -21
  57. package/src/ui/public/assets/LogList-DjuL8nWB.js +0 -7
  58. package/src/ui/public/assets/MindmapView-DPomtQsu.js +0 -1
  59. package/src/ui/public/assets/MonitorView-gZhz528u.js +0 -1
  60. package/src/ui/public/assets/ProjectStartupButton-C8nDEd8N.js +0 -1
  61. package/src/ui/public/assets/SourceMapView-DfSRy6lc.js +0 -3
  62. package/src/ui/public/assets/TemplateManager-BJZloJIO.js +0 -1
  63. package/src/ui/public/assets/UserInputNode-DpRNQzrz.js +0 -1
  64. package/src/ui/public/assets/WorkbenchView-vaZsec5Z.js +0 -12
  65. package/src/ui/public/assets/element-plus-CL7fBdM_.js +0 -24
  66. package/src/ui/public/assets/element-plus-Cu9Y2raX.css +0 -1
  67. package/src/ui/public/assets/index-BAFCDhj2.js +0 -23
  68. package/src/ui/public/assets/vendor-CCbwTgrE.css +0 -1
  69. package/src/ui/server/.claude/codediff.txt +0 -8
package/src/gitCommit.js CHANGED
@@ -1,475 +1,475 @@
1
- #!/usr/bin/env node
2
- // Copyright 2026 xz333221
3
- //
4
- // Licensed under the Apache License, Version 2.0 (the "License");
5
- // you may not use this file except in compliance with the License.
6
- // You may obtain a copy of the License at
7
- //
8
- // http://www.apache.org/licenses/LICENSE-2.0
9
- //
10
- // Unless required by applicable law or agreed to in writing, software
11
- // distributed under the License is distributed on an "AS IS" BASIS,
12
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- // See the License for the specific language governing permissions and
14
- // limitations under the License.
15
- //
16
-
17
- import {
18
- coloredLog, errorLog, execGitCommand, showHelp,
19
- getCwd, judgePlatform, judgeLog, judgeHelp, exec_exit, judgeUnmerged, formatDuration,
20
- exec_push, execPull, judgeRemote, execDiff, execAddAndCommit, delay, addScriptToPackageJson, addResetScriptToPackageJson
21
- } from './utils/index.js';
22
- import readline from 'readline'
23
- import ora from 'ora';
24
- import chalk from 'chalk';
25
- import config from './config.js';
26
- import {
27
- registerCleanup, setupSigintHandler,
28
- } from './cli/cleanup.js';
29
- import { validateCustomCommand, isCmdStrictMode, runCustomCommand } from './cli/customCommand.js';
30
-
31
- // 延迟加载的重模块 — 这些只在特定子命令路径才需要,启动时加载会无谓拖慢
32
- // CLI 冷启动(其中 startUIServer 还会拉起 Express + Socket.IO + 全部 routes,
33
- // 一次约几十到上百 KB 依赖链)。
34
- // - startUIServer:仅 `g ui` 子命令使用
35
- // - boxen / boxenAdaptive / logUpdate / date-fns:仅定时/倒计时 UI 使用
36
- let startUIServerPromise = null;
37
- let boxenImportsPromise = null;
38
-
39
- async function loadStartUIServer() {
40
- if (!startUIServerPromise) {
41
- startUIServerPromise = import('./ui/server/index.js').then(m => m.default);
42
- }
43
- return startUIServerPromise;
44
- }
45
-
46
- async function loadBoxenImports() {
47
- if (!boxenImportsPromise) {
48
- boxenImportsPromise = (async () => {
49
- const [boxenMod, uiMod, logUpdateMod, dateFnsMod] = await Promise.all([
50
- import('boxen'),
51
- import('./cli/ui.js'),
52
- import('log-update'),
53
- import('date-fns/format')
54
- ]);
55
- return {
56
- boxen: boxenMod.default,
57
- boxenAdaptive: uiMod.boxenAdaptive,
58
- logUpdate: logUpdateMod.default,
59
- dateFormat: dateFnsMod.format
60
- };
61
- })();
62
- }
63
- return boxenImportsPromise;
64
- }
65
-
66
- let countdownInterval = null;
67
-
68
- async function startCountdown(interval) {
69
- let remaining = interval;
70
-
71
- // 清除旧的倒计时(同一时刻只允许一个倒计时可见)
72
- if (countdownInterval) {
73
- clearInterval(countdownInterval);
74
- countdownInterval = null;
75
- }
76
-
77
- // 懒加载 boxen/logUpdate/date-fns/boxenAdaptive — 这些只在倒计时 UI 用,
78
- // 单次 `g` 提交不触发,避免冷启动时无谓加载 ~150KB 依赖链。
79
- const { dateFormat, logUpdate, boxenAdaptive } = await loadBoxenImports();
80
-
81
- const render = () => {
82
- const nextTime = Date.now() + remaining;
83
- const formattedTime = dateFormat(nextTime, 'yyyy-MM-dd HH:mm:ss');
84
- const duration = formatDuration(remaining);
85
-
86
- const message = [
87
- `🕒 ${chalk.green.bold('倒计时')}`,
88
- `工作目录: ${chalk.green(getCwd())}`,
89
- `下次提交: ${chalk.blue(formattedTime)}`,
90
- `剩余时间: ${chalk.yellow(duration)}`,
91
- chalk.dim('按 Ctrl+C 终止进程')
92
- ].join('\n');
93
-
94
- const box = boxenAdaptive(message, {
95
- padding: 1,
96
- margin: 1,
97
- borderColor: 'cyan',
98
- borderStyle: 'round'
99
- });
100
-
101
- setTimeout(() => {
102
- logUpdate(box);
103
- }, 200);
104
- // logUpdate(box);
105
- };
106
-
107
- // 立即渲染一次
108
- render();
109
-
110
- // 每秒更新
111
- countdownInterval = setInterval(() => {
112
- remaining -= 1000;
113
-
114
- if (remaining <= 0) {
115
- clearInterval(countdownInterval);
116
- countdownInterval = null;
117
- logUpdate.clear();
118
- return;
119
- }
120
-
121
- render();
122
- }, 1000);
123
- // 注册到 SIGINT cleanup 表,Ctrl+C 时一并清掉,避免倒计时残影
124
- registerCleanup('countdown', () => {
125
- if (countdownInterval) {
126
- clearInterval(countdownInterval);
127
- countdownInterval = null;
128
- }
129
- try { logUpdate.clear(); } catch (_) {}
130
- });
131
- }
132
-
133
- const {loadConfig, saveConfig, handleConfigCommands} = config;
134
- const {defaultCommitMessage} = config
135
-
136
- let timer = null
137
-
138
- async function createGitCommit(options) {
139
- try {
140
- let statusOutput = null
141
- let exit = options ? !!options.exit : true
142
- const config = await loadConfig()
143
- let commitMessage = config.defaultCommitMessage
144
- let {stdout} = await execGitCommand(['status'])
145
- statusOutput = stdout
146
- judgeUnmerged(statusOutput)
147
- // 先检查本地是否有未提交的更改
148
- const hasLocalChanges = !statusOutput.includes('nothing to commit, working tree clean');
149
- if (hasLocalChanges) {
150
- // --ai 模式: 动态加载 AI 模块生成 Conventional Commits 提交信息
151
- // (懒加载,普通 g -y / g -m 不会触发,避免冷启动拖慢)
152
- const useAi = process.argv.includes('--ai');
153
- if (useAi) {
154
- const { generateAiCommitMessage } = await import('./aiCommit.js');
155
- commitMessage = await generateAiCommitMessage({ locale: config.locale });
156
- }
157
- // 检查是否有 --no-diff 参数
158
- await execDiff()
159
- // 获取实际使用的提交信息(可能是用户交互输入的)
160
- commitMessage = await execAddAndCommit({statusOutput, commitMessage, exit})
161
- statusOutput.includes('use "git pull') && await execPull()
162
-
163
- // 检查是否有远程更新
164
- await judgeRemote() // 等待 judgeRemote 完成
165
-
166
- await exec_push({exit, commitMessage})
167
- } else {
168
- if (statusOutput.includes('use "git push')) {
169
- // 获取最近一次提交的实际信息
170
- const lastCommitResult = await execGitCommand(['log', '-1', '--pretty=%B'], {log: false});
171
- const actualCommitMessage = lastCommitResult.stdout.trim();
172
- await exec_push({exit, commitMessage: actualCommitMessage || commitMessage})
173
- } else if (statusOutput.includes('use "git pull')) {
174
- await execPull()
175
- } else {
176
- await judgeRemote() // 等待 judgeRemote 完成
177
- exec_exit(exit);
178
- }
179
- }
180
- } catch (e) {
181
- console.error(chalk.red.bold('提交流程错误:'));
182
- console.error(chalk.dim(e.stack)); // 打印完整错误堆栈
183
- throw e; // 继续向上抛出错误
184
- }
185
- }
186
- // 处理文件锁定相关命令
187
- async function handleFileLockCommands() {
188
- // 锁定文件命令
189
- const lockFileArg = process.argv.find(arg => arg.startsWith('--lock-file='));
190
- if (lockFileArg) {
191
- const filePath = lockFileArg.split('=')[1];
192
- if (filePath) {
193
- await config.lockFile(filePath);
194
- } else {
195
- console.log(chalk.red('❌ 请指定要锁定的文件路径'));
196
- }
197
- process.exit();
198
- }
199
-
200
- // 解锁文件命令
201
- const unlockFileArg = process.argv.find(arg => arg.startsWith('--unlock-file='));
202
- if (unlockFileArg) {
203
- const filePath = unlockFileArg.split('=')[1];
204
- if (filePath) {
205
- await config.unlockFile(filePath);
206
- } else {
207
- console.log(chalk.red('❌ 请指定要解锁的文件路径'));
208
- }
209
- process.exit();
210
- }
211
-
212
- // 列出锁定文件命令
213
- if (process.argv.includes('--list-locked')) {
214
- await config.listLockedFiles();
215
- process.exit();
216
- }
217
-
218
- // 检查文件是否锁定命令
219
- const checkLockArg = process.argv.find(arg => arg.startsWith('--check-lock='));
220
- if (checkLockArg) {
221
- const filePath = checkLockArg.split('=')[1];
222
- if (filePath) {
223
- const isLocked = await config.isFileLocked(filePath);
224
- if (isLocked) {
225
- console.log(chalk.yellow(`🔒 文件已锁定: ${filePath}`));
226
- } else {
227
- console.log(chalk.green(`🔓 文件未锁定: ${filePath}`));
228
- }
229
- } else {
230
- console.log(chalk.red('❌ 请指定要检查的文件路径'));
231
- }
232
- process.exit();
233
- }
234
- }
235
-
236
- async function main() {
237
- judgePlatform()
238
-
239
- // 一次性注册 SIGINT 处理器 — 所有长跑流程(定时 commit / 自定义命令循环 /
240
- // 倒计时)都通过 registerCleanup 上报,SIGINT 时由统一处理器 drain。
241
- setupSigintHandler({
242
- onBeforeCleanup: () => {
243
- try { logUpdate.clear(); } catch (_) {}
244
- console.log(chalk.yellow('\n🛑 收到终止信号,清理中...'))
245
- },
246
- onAfterCleanup: () => {
247
- console.log(chalk.yellow('🛑 已清理,退出'))
248
- }
249
- })
250
-
251
- // 检查是否是UI命令
252
- if (process.argv.includes('ui')) {
253
- const startUIServer = await loadStartUIServer();
254
- await startUIServer(false, false); // 传入noOpen=false, savePort=false
255
- return;
256
- }
257
-
258
- // 检查是否是添加脚本命令
259
- if (process.argv.includes('addScript')) {
260
- await addScriptToPackageJson();
261
- return;
262
- }
263
-
264
- // 检查是否是添加重置脚本命令
265
- if (process.argv.includes('addResetScript')) {
266
- await addResetScriptToPackageJson();
267
- return;
268
- }
269
-
270
- // 检查是否有 log 参数
271
- judgeLog()
272
-
273
- // 检查帮助参数
274
- judgeHelp()
275
-
276
- await handleConfigCommands();
277
-
278
- // ========== 文件锁定功能 ==========
279
- await handleFileLockCommands();
280
-
281
- // ========== 新增:自定义cmd定时/定点执行功能 ==========
282
- const cmdArg = process.argv.find(arg => arg.startsWith('--cmd='));
283
- if (cmdArg) {
284
- // 使用正则表达式提取参数值,确保等号不会截断命令内容
285
- const cmdMatch = cmdArg.match(/^--cmd=(['"]?)(.*)\1$/);
286
- let cmd = cmdMatch ? cmdMatch[2] : '';
287
- cmd = cmd.replace(/%%/g, '%'); // 关键修复
288
-
289
- // 输入校验(SEC-CLI-1) — 本地 CLI,用户明确输入,所以检测而非阻止:
290
- // 拒绝对抗性输入(空 / - 开头 / 超长);危险模式打 warning。
291
- const cmdValidation = validateCustomCommand(cmd);
292
- if (!cmdValidation.ok) {
293
- console.error(chalk.red(`❌ --cmd 校验失败: ${cmdValidation.rejectReason}`));
294
- process.exit(2); // 2 = 命令行参数错误(Bash 惯例)
295
- }
296
- for (const w of cmdValidation.warnings) {
297
- console.warn(chalk.yellow(`⚠️ ${w}`));
298
- }
299
- // 显式 --cmd-strict 时打一条提示,让用户知道 shell 特性被禁用
300
- if (isCmdStrictMode(process.argv)) {
301
- console.log(chalk.dim('ℹ️ --cmd-strict 模式:将通过 execFile 拆 argv 执行,管道/重定向/通配符将不可用'));
302
- }
303
-
304
- const atArg = process.argv.find(arg => arg.startsWith('--at='));
305
- const intervalArg = process.argv.find(arg => arg.startsWith('--cmd-interval='));
306
-
307
- if (atArg) {
308
- const atMatch = atArg.match(/^--at=(['"]?)(.*)\1$/);
309
- const atTime = atMatch ? atMatch[2] : '';
310
- const repeatDaily = process.argv.includes('--daily') || process.argv.includes('--repeat=daily') || process.argv.includes('--at-repeat=daily');
311
-
312
- const runOnce = () => runCustomCommand(cmd);
313
-
314
- const getNextTarget = (now) => {
315
- if (/^\d{2}:\d{2}$/.test(atTime)) {
316
- const [h, m] = atTime.split(':').map((v) => parseInt(v, 10));
317
- if (!Number.isFinite(h) || !Number.isFinite(m)) return null;
318
- const base = new Date(now.getFullYear(), now.getMonth(), now.getDate(), h, m, 0, 0);
319
- if (base.getTime() > now.getTime()) return base;
320
- return new Date(base.getTime() + 24 * 60 * 60 * 1000);
321
- }
322
-
323
- const parsed = new Date(atTime);
324
- if (!Number.isFinite(parsed.getTime())) return null;
325
-
326
- if (!repeatDaily) return parsed;
327
-
328
- const base = new Date(now.getFullYear(), now.getMonth(), now.getDate(), parsed.getHours(), parsed.getMinutes(), parsed.getSeconds(), 0);
329
- if (base.getTime() > now.getTime()) return base;
330
- return new Date(base.getTime() + 24 * 60 * 60 * 1000);
331
- };
332
-
333
- let atTimer = null;
334
- const scheduleNext = () => {
335
- const now = new Date();
336
- const target = getNextTarget(now);
337
- if (!target) {
338
- console.error('无效的时间参数');
339
- return;
340
- }
341
-
342
- let delay = target.getTime() - now.getTime();
343
- if (!Number.isFinite(delay)) {
344
- console.error('无效的时间参数');
345
- return;
346
- }
347
-
348
- if (!repeatDaily && delay <= 0) {
349
- console.log('指定时间已过,不执行自定义命令');
350
- return;
351
- }
352
-
353
- if (delay < 0) delay = 0;
354
- console.log(`将在 ${target.toLocaleString()} 执行: ${cmd}${repeatDaily ? '(每日循环)' : ''}`);
355
- atTimer = setTimeout(() => {
356
- runOnce();
357
- if (repeatDaily) scheduleNext();
358
- }, delay);
359
- // 注册到 SIGINT cleanup 表 — Ctrl+C 时由统一处理器清理
360
- registerCleanup('atTimer', () => {
361
- if (atTimer) {
362
- clearTimeout(atTimer);
363
- atTimer = null;
364
- }
365
- });
366
- };
367
-
368
- scheduleNext();
369
- // 旧的内联 SIGINT handler 已被 setupSigintHandler 替代,这里不再重复注册
370
- } else if (intervalArg) {
371
- // 定时循环执行
372
- const intervalMatch = intervalArg.match(/^--cmd-interval=(['"]?)(.*)\1$/);
373
- const interval = intervalMatch ? parseInt(intervalMatch[2], 10) * 1000 : 0;
374
- if (interval > 0) {
375
- console.log(`每隔 ${interval/1000} 秒执行: ${cmd}`);
376
- const cmdIntervalId = setInterval(() => runCustomCommand(cmd, { silent: true }), interval);
377
- // 注册 cleanup,Ctrl+C 时清掉这个 interval(否则进程不退)
378
- registerCleanup('cmdInterval', () => clearInterval(cmdIntervalId));
379
- } else {
380
- console.error('无效的时间间隔参数');
381
- }
382
- } else {
383
- // 立即执行一次
384
- runCustomCommand(cmd);
385
- }
386
- }
387
- // ========== 新增功能结束 ==========
388
-
389
- // 判断是否需要执行git自动提交
390
- const hasGitTask = process.argv.some(arg =>
391
- arg.startsWith('--interval') ||
392
- arg === '-y' ||
393
- arg === '--ai' ||
394
- arg.startsWith('-m')
395
- );
396
- if (hasGitTask || !cmdArg) {
397
- judgeInterval();
398
- }
399
- }
400
-
401
- const showStartInfo = (interval) => {
402
- const cwd = getCwd();
403
- const intervalSeconds = interval / 1000;
404
- const startTime = new Date().toLocaleString();
405
-
406
- const head = `⏰ 定时提交任务已启动`;
407
-
408
- const message = chalk.green.bold([
409
- `开始时间: ${chalk.yellow(startTime)}`,
410
- `工作目录: ${chalk.cyan(cwd)}`,
411
- `提交间隔: ${chalk.magenta(formatDuration(interval))}`,
412
- ].join("\n"));
413
-
414
- coloredLog(head, message)
415
- }
416
- // 漂移自由调度的基线时间(由 judgeInterval 在启动时初始化一次)。
417
- // commitAndSchedule 每次循环 nextRunAt += interval,实际 setTimeout 延迟
418
- // = nextRunAt - Date.now()。
419
- let nextRunAt = 0;
420
-
421
- const commitAndSchedule = async (interval) => {
422
- try {
423
- await createGitCommit({exit: false});
424
- // await delay(2000)
425
- await startCountdown(interval); // 启动倒计时(内部已注册 cleanup)
426
-
427
- // 漂移自由的调度 — 每次循环把 nextRunAt 加上 interval,
428
- // 实际 setTimeout 延迟 = nextRunAt - now:
429
- // - 执行时间 < interval:延后到 nextRunAt,自然追平,不累积漂移
430
- // - 执行时间 > interval:延迟 = 0(可能 = -X,被 Math.max 钳到 0),立即跑下一轮
431
- // 之前 setTimeout(..., interval + 100) 在执行时间波动时会持续累积漂移,
432
- // 长时间运行后周期显著偏离用户配置的 interval。
433
- nextRunAt += interval;
434
- const delay = Math.max(0, nextRunAt - Date.now());
435
- timer = setTimeout(() => commitAndSchedule(interval), delay);
436
- // 注册到 SIGINT cleanup 表 — Ctrl+C 时统一清掉
437
- // 注意:cleanup 命名 'commitTimer',每次递归都覆盖上一次的 handler
438
- // (cleanupTasks.set 同名覆盖语义),保证只清当前 pending 那个 setTimeout
439
- registerCleanup('commitTimer', () => {
440
- if (timer) {
441
- clearTimeout(timer);
442
- timer = null;
443
- }
444
- });
445
- } catch (error) {
446
- console.error('提交出错:', error.message);
447
- clearTimeout(timer);
448
- clearInterval(countdownInterval);
449
- process.exit(1);
450
- }
451
- };
452
- const judgeInterval = async () => {
453
- const intervalArg = process.argv.find(arg => arg.startsWith('--interval'));
454
- if (intervalArg) {
455
- let interval = parseInt(intervalArg.split('=')[1] || '3600', 10) * 1000;
456
-
457
- showStartInfo(interval);
458
-
459
- // 漂移自由调度的基线 — 首次 commit 立即跑(与之前行为一致),
460
- // 之后 nextRunAt = startAt + N*interval, 每次循环 nextRunAt += interval
461
- nextRunAt = Date.now();
462
-
463
- try {
464
- await commitAndSchedule(interval);
465
- } catch (error) {
466
- console.error(chalk.red.bold('定时提交致命错误:'), error.message);
467
- process.exit(1);
468
- }
469
- // 旧的 inline SIGINT handler 已移除,改由 setupSigintHandler 统一处理
470
- } else {
471
- createGitCommit({exit: false});
472
- }
473
- };
474
-
475
- main()
1
+ #!/usr/bin/env node
2
+ // Copyright 2026 xz333221
3
+ //
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ //
8
+ // http://www.apache.org/licenses/LICENSE-2.0
9
+ //
10
+ // Unless required by applicable law or agreed to in writing, software
11
+ // distributed under the License is distributed on an "AS IS" BASIS,
12
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ // See the License for the specific language governing permissions and
14
+ // limitations under the License.
15
+ //
16
+
17
+ import {
18
+ coloredLog, errorLog, execGitCommand, showHelp,
19
+ getCwd, judgePlatform, judgeLog, judgeHelp, exec_exit, judgeUnmerged, formatDuration,
20
+ exec_push, execPull, judgeRemote, execDiff, execAddAndCommit, delay, addScriptToPackageJson, addResetScriptToPackageJson
21
+ } from './utils/index.js';
22
+ import readline from 'readline'
23
+ import ora from 'ora';
24
+ import chalk from 'chalk';
25
+ import config from './config.js';
26
+ import {
27
+ registerCleanup, setupSigintHandler,
28
+ } from './cli/cleanup.js';
29
+ import { validateCustomCommand, isCmdStrictMode, runCustomCommand } from './cli/customCommand.js';
30
+
31
+ // 延迟加载的重模块 — 这些只在特定子命令路径才需要,启动时加载会无谓拖慢
32
+ // CLI 冷启动(其中 startUIServer 还会拉起 Express + Socket.IO + 全部 routes,
33
+ // 一次约几十到上百 KB 依赖链)。
34
+ // - startUIServer:仅 `g ui` 子命令使用
35
+ // - boxen / boxenAdaptive / logUpdate / date-fns:仅定时/倒计时 UI 使用
36
+ let startUIServerPromise = null;
37
+ let boxenImportsPromise = null;
38
+
39
+ async function loadStartUIServer() {
40
+ if (!startUIServerPromise) {
41
+ startUIServerPromise = import('./ui/server/index.js').then(m => m.default);
42
+ }
43
+ return startUIServerPromise;
44
+ }
45
+
46
+ async function loadBoxenImports() {
47
+ if (!boxenImportsPromise) {
48
+ boxenImportsPromise = (async () => {
49
+ const [boxenMod, uiMod, logUpdateMod, dateFnsMod] = await Promise.all([
50
+ import('boxen'),
51
+ import('./cli/ui.js'),
52
+ import('log-update'),
53
+ import('date-fns/format')
54
+ ]);
55
+ return {
56
+ boxen: boxenMod.default,
57
+ boxenAdaptive: uiMod.boxenAdaptive,
58
+ logUpdate: logUpdateMod.default,
59
+ dateFormat: dateFnsMod.format
60
+ };
61
+ })();
62
+ }
63
+ return boxenImportsPromise;
64
+ }
65
+
66
+ let countdownInterval = null;
67
+
68
+ async function startCountdown(interval) {
69
+ let remaining = interval;
70
+
71
+ // 清除旧的倒计时(同一时刻只允许一个倒计时可见)
72
+ if (countdownInterval) {
73
+ clearInterval(countdownInterval);
74
+ countdownInterval = null;
75
+ }
76
+
77
+ // 懒加载 boxen/logUpdate/date-fns/boxenAdaptive — 这些只在倒计时 UI 用,
78
+ // 单次 `g` 提交不触发,避免冷启动时无谓加载 ~150KB 依赖链。
79
+ const { dateFormat, logUpdate, boxenAdaptive } = await loadBoxenImports();
80
+
81
+ const render = () => {
82
+ const nextTime = Date.now() + remaining;
83
+ const formattedTime = dateFormat(nextTime, 'yyyy-MM-dd HH:mm:ss');
84
+ const duration = formatDuration(remaining);
85
+
86
+ const message = [
87
+ `🕒 ${chalk.green.bold('倒计时')}`,
88
+ `工作目录: ${chalk.green(getCwd())}`,
89
+ `下次提交: ${chalk.blue(formattedTime)}`,
90
+ `剩余时间: ${chalk.yellow(duration)}`,
91
+ chalk.dim('按 Ctrl+C 终止进程')
92
+ ].join('\n');
93
+
94
+ const box = boxenAdaptive(message, {
95
+ padding: 1,
96
+ margin: 1,
97
+ borderColor: 'cyan',
98
+ borderStyle: 'round'
99
+ });
100
+
101
+ setTimeout(() => {
102
+ logUpdate(box);
103
+ }, 200);
104
+ // logUpdate(box);
105
+ };
106
+
107
+ // 立即渲染一次
108
+ render();
109
+
110
+ // 每秒更新
111
+ countdownInterval = setInterval(() => {
112
+ remaining -= 1000;
113
+
114
+ if (remaining <= 0) {
115
+ clearInterval(countdownInterval);
116
+ countdownInterval = null;
117
+ logUpdate.clear();
118
+ return;
119
+ }
120
+
121
+ render();
122
+ }, 1000);
123
+ // 注册到 SIGINT cleanup 表,Ctrl+C 时一并清掉,避免倒计时残影
124
+ registerCleanup('countdown', () => {
125
+ if (countdownInterval) {
126
+ clearInterval(countdownInterval);
127
+ countdownInterval = null;
128
+ }
129
+ try { logUpdate.clear(); } catch (_) {}
130
+ });
131
+ }
132
+
133
+ const {loadConfig, saveConfig, handleConfigCommands} = config;
134
+ const {defaultCommitMessage} = config
135
+
136
+ let timer = null
137
+
138
+ async function createGitCommit(options) {
139
+ try {
140
+ let statusOutput = null
141
+ let exit = options ? !!options.exit : true
142
+ const config = await loadConfig()
143
+ let commitMessage = config.defaultCommitMessage
144
+ let {stdout} = await execGitCommand(['status'])
145
+ statusOutput = stdout
146
+ judgeUnmerged(statusOutput)
147
+ // 先检查本地是否有未提交的更改
148
+ const hasLocalChanges = !statusOutput.includes('nothing to commit, working tree clean');
149
+ if (hasLocalChanges) {
150
+ // --ai 模式: 动态加载 AI 模块生成 Conventional Commits 提交信息
151
+ // (懒加载,普通 g -y / g -m 不会触发,避免冷启动拖慢)
152
+ const useAi = process.argv.includes('--ai');
153
+ if (useAi) {
154
+ const { generateAiCommitMessage } = await import('./aiCommit.js');
155
+ commitMessage = await generateAiCommitMessage({ locale: config.locale });
156
+ }
157
+ // 检查是否有 --no-diff 参数
158
+ await execDiff()
159
+ // 获取实际使用的提交信息(可能是用户交互输入的)
160
+ commitMessage = await execAddAndCommit({statusOutput, commitMessage, exit})
161
+ statusOutput.includes('use "git pull') && await execPull()
162
+
163
+ // 检查是否有远程更新
164
+ await judgeRemote() // 等待 judgeRemote 完成
165
+
166
+ await exec_push({exit, commitMessage})
167
+ } else {
168
+ if (statusOutput.includes('use "git push')) {
169
+ // 获取最近一次提交的实际信息
170
+ const lastCommitResult = await execGitCommand(['log', '-1', '--pretty=%B'], {log: false});
171
+ const actualCommitMessage = lastCommitResult.stdout.trim();
172
+ await exec_push({exit, commitMessage: actualCommitMessage || commitMessage})
173
+ } else if (statusOutput.includes('use "git pull')) {
174
+ await execPull()
175
+ } else {
176
+ await judgeRemote() // 等待 judgeRemote 完成
177
+ exec_exit(exit);
178
+ }
179
+ }
180
+ } catch (e) {
181
+ console.error(chalk.red.bold('提交流程错误:'));
182
+ console.error(chalk.dim(e.stack)); // 打印完整错误堆栈
183
+ throw e; // 继续向上抛出错误
184
+ }
185
+ }
186
+ // 处理文件锁定相关命令
187
+ async function handleFileLockCommands() {
188
+ // 锁定文件命令
189
+ const lockFileArg = process.argv.find(arg => arg.startsWith('--lock-file='));
190
+ if (lockFileArg) {
191
+ const filePath = lockFileArg.split('=')[1];
192
+ if (filePath) {
193
+ await config.lockFile(filePath);
194
+ } else {
195
+ console.log(chalk.red('❌ 请指定要锁定的文件路径'));
196
+ }
197
+ process.exit();
198
+ }
199
+
200
+ // 解锁文件命令
201
+ const unlockFileArg = process.argv.find(arg => arg.startsWith('--unlock-file='));
202
+ if (unlockFileArg) {
203
+ const filePath = unlockFileArg.split('=')[1];
204
+ if (filePath) {
205
+ await config.unlockFile(filePath);
206
+ } else {
207
+ console.log(chalk.red('❌ 请指定要解锁的文件路径'));
208
+ }
209
+ process.exit();
210
+ }
211
+
212
+ // 列出锁定文件命令
213
+ if (process.argv.includes('--list-locked')) {
214
+ await config.listLockedFiles();
215
+ process.exit();
216
+ }
217
+
218
+ // 检查文件是否锁定命令
219
+ const checkLockArg = process.argv.find(arg => arg.startsWith('--check-lock='));
220
+ if (checkLockArg) {
221
+ const filePath = checkLockArg.split('=')[1];
222
+ if (filePath) {
223
+ const isLocked = await config.isFileLocked(filePath);
224
+ if (isLocked) {
225
+ console.log(chalk.yellow(`🔒 文件已锁定: ${filePath}`));
226
+ } else {
227
+ console.log(chalk.green(`🔓 文件未锁定: ${filePath}`));
228
+ }
229
+ } else {
230
+ console.log(chalk.red('❌ 请指定要检查的文件路径'));
231
+ }
232
+ process.exit();
233
+ }
234
+ }
235
+
236
+ async function main() {
237
+ judgePlatform()
238
+
239
+ // 一次性注册 SIGINT 处理器 — 所有长跑流程(定时 commit / 自定义命令循环 /
240
+ // 倒计时)都通过 registerCleanup 上报,SIGINT 时由统一处理器 drain。
241
+ setupSigintHandler({
242
+ onBeforeCleanup: () => {
243
+ try { logUpdate.clear(); } catch (_) {}
244
+ console.log(chalk.yellow('\n🛑 收到终止信号,清理中...'))
245
+ },
246
+ onAfterCleanup: () => {
247
+ console.log(chalk.yellow('🛑 已清理,退出'))
248
+ }
249
+ })
250
+
251
+ // 检查是否是UI命令
252
+ if (process.argv.includes('ui')) {
253
+ const startUIServer = await loadStartUIServer();
254
+ await startUIServer(false, false); // 传入noOpen=false, savePort=false
255
+ return;
256
+ }
257
+
258
+ // 检查是否是添加脚本命令
259
+ if (process.argv.includes('addScript')) {
260
+ await addScriptToPackageJson();
261
+ return;
262
+ }
263
+
264
+ // 检查是否是添加重置脚本命令
265
+ if (process.argv.includes('addResetScript')) {
266
+ await addResetScriptToPackageJson();
267
+ return;
268
+ }
269
+
270
+ // 检查是否有 log 参数
271
+ judgeLog()
272
+
273
+ // 检查帮助参数
274
+ judgeHelp()
275
+
276
+ await handleConfigCommands();
277
+
278
+ // ========== 文件锁定功能 ==========
279
+ await handleFileLockCommands();
280
+
281
+ // ========== 新增:自定义cmd定时/定点执行功能 ==========
282
+ const cmdArg = process.argv.find(arg => arg.startsWith('--cmd='));
283
+ if (cmdArg) {
284
+ // 使用正则表达式提取参数值,确保等号不会截断命令内容
285
+ const cmdMatch = cmdArg.match(/^--cmd=(['"]?)(.*)\1$/);
286
+ let cmd = cmdMatch ? cmdMatch[2] : '';
287
+ cmd = cmd.replace(/%%/g, '%'); // 关键修复
288
+
289
+ // 输入校验(SEC-CLI-1) — 本地 CLI,用户明确输入,所以检测而非阻止:
290
+ // 拒绝对抗性输入(空 / - 开头 / 超长);危险模式打 warning。
291
+ const cmdValidation = validateCustomCommand(cmd);
292
+ if (!cmdValidation.ok) {
293
+ console.error(chalk.red(`❌ --cmd 校验失败: ${cmdValidation.rejectReason}`));
294
+ process.exit(2); // 2 = 命令行参数错误(Bash 惯例)
295
+ }
296
+ for (const w of cmdValidation.warnings) {
297
+ console.warn(chalk.yellow(`⚠️ ${w}`));
298
+ }
299
+ // 显式 --cmd-strict 时打一条提示,让用户知道 shell 特性被禁用
300
+ if (isCmdStrictMode(process.argv)) {
301
+ console.log(chalk.dim('ℹ️ --cmd-strict 模式:将通过 execFile 拆 argv 执行,管道/重定向/通配符将不可用'));
302
+ }
303
+
304
+ const atArg = process.argv.find(arg => arg.startsWith('--at='));
305
+ const intervalArg = process.argv.find(arg => arg.startsWith('--cmd-interval='));
306
+
307
+ if (atArg) {
308
+ const atMatch = atArg.match(/^--at=(['"]?)(.*)\1$/);
309
+ const atTime = atMatch ? atMatch[2] : '';
310
+ const repeatDaily = process.argv.includes('--daily') || process.argv.includes('--repeat=daily') || process.argv.includes('--at-repeat=daily');
311
+
312
+ const runOnce = () => runCustomCommand(cmd);
313
+
314
+ const getNextTarget = (now) => {
315
+ if (/^\d{2}:\d{2}$/.test(atTime)) {
316
+ const [h, m] = atTime.split(':').map((v) => parseInt(v, 10));
317
+ if (!Number.isFinite(h) || !Number.isFinite(m)) return null;
318
+ const base = new Date(now.getFullYear(), now.getMonth(), now.getDate(), h, m, 0, 0);
319
+ if (base.getTime() > now.getTime()) return base;
320
+ return new Date(base.getTime() + 24 * 60 * 60 * 1000);
321
+ }
322
+
323
+ const parsed = new Date(atTime);
324
+ if (!Number.isFinite(parsed.getTime())) return null;
325
+
326
+ if (!repeatDaily) return parsed;
327
+
328
+ const base = new Date(now.getFullYear(), now.getMonth(), now.getDate(), parsed.getHours(), parsed.getMinutes(), parsed.getSeconds(), 0);
329
+ if (base.getTime() > now.getTime()) return base;
330
+ return new Date(base.getTime() + 24 * 60 * 60 * 1000);
331
+ };
332
+
333
+ let atTimer = null;
334
+ const scheduleNext = () => {
335
+ const now = new Date();
336
+ const target = getNextTarget(now);
337
+ if (!target) {
338
+ console.error('无效的时间参数');
339
+ return;
340
+ }
341
+
342
+ let delay = target.getTime() - now.getTime();
343
+ if (!Number.isFinite(delay)) {
344
+ console.error('无效的时间参数');
345
+ return;
346
+ }
347
+
348
+ if (!repeatDaily && delay <= 0) {
349
+ console.log('指定时间已过,不执行自定义命令');
350
+ return;
351
+ }
352
+
353
+ if (delay < 0) delay = 0;
354
+ console.log(`将在 ${target.toLocaleString()} 执行: ${cmd}${repeatDaily ? '(每日循环)' : ''}`);
355
+ atTimer = setTimeout(() => {
356
+ runOnce();
357
+ if (repeatDaily) scheduleNext();
358
+ }, delay);
359
+ // 注册到 SIGINT cleanup 表 — Ctrl+C 时由统一处理器清理
360
+ registerCleanup('atTimer', () => {
361
+ if (atTimer) {
362
+ clearTimeout(atTimer);
363
+ atTimer = null;
364
+ }
365
+ });
366
+ };
367
+
368
+ scheduleNext();
369
+ // 旧的内联 SIGINT handler 已被 setupSigintHandler 替代,这里不再重复注册
370
+ } else if (intervalArg) {
371
+ // 定时循环执行
372
+ const intervalMatch = intervalArg.match(/^--cmd-interval=(['"]?)(.*)\1$/);
373
+ const interval = intervalMatch ? parseInt(intervalMatch[2], 10) * 1000 : 0;
374
+ if (interval > 0) {
375
+ console.log(`每隔 ${interval/1000} 秒执行: ${cmd}`);
376
+ const cmdIntervalId = setInterval(() => runCustomCommand(cmd, { silent: true }), interval);
377
+ // 注册 cleanup,Ctrl+C 时清掉这个 interval(否则进程不退)
378
+ registerCleanup('cmdInterval', () => clearInterval(cmdIntervalId));
379
+ } else {
380
+ console.error('无效的时间间隔参数');
381
+ }
382
+ } else {
383
+ // 立即执行一次
384
+ runCustomCommand(cmd);
385
+ }
386
+ }
387
+ // ========== 新增功能结束 ==========
388
+
389
+ // 判断是否需要执行git自动提交
390
+ const hasGitTask = process.argv.some(arg =>
391
+ arg.startsWith('--interval') ||
392
+ arg === '-y' ||
393
+ arg === '--ai' ||
394
+ arg.startsWith('-m')
395
+ );
396
+ if (hasGitTask || !cmdArg) {
397
+ judgeInterval();
398
+ }
399
+ }
400
+
401
+ const showStartInfo = (interval) => {
402
+ const cwd = getCwd();
403
+ const intervalSeconds = interval / 1000;
404
+ const startTime = new Date().toLocaleString();
405
+
406
+ const head = `⏰ 定时提交任务已启动`;
407
+
408
+ const message = chalk.green.bold([
409
+ `开始时间: ${chalk.yellow(startTime)}`,
410
+ `工作目录: ${chalk.cyan(cwd)}`,
411
+ `提交间隔: ${chalk.magenta(formatDuration(interval))}`,
412
+ ].join("\n"));
413
+
414
+ coloredLog(head, message)
415
+ }
416
+ // 漂移自由调度的基线时间(由 judgeInterval 在启动时初始化一次)。
417
+ // commitAndSchedule 每次循环 nextRunAt += interval,实际 setTimeout 延迟
418
+ // = nextRunAt - Date.now()。
419
+ let nextRunAt = 0;
420
+
421
+ const commitAndSchedule = async (interval) => {
422
+ try {
423
+ await createGitCommit({exit: false});
424
+ // await delay(2000)
425
+ await startCountdown(interval); // 启动倒计时(内部已注册 cleanup)
426
+
427
+ // 漂移自由的调度 — 每次循环把 nextRunAt 加上 interval,
428
+ // 实际 setTimeout 延迟 = nextRunAt - now:
429
+ // - 执行时间 < interval:延后到 nextRunAt,自然追平,不累积漂移
430
+ // - 执行时间 > interval:延迟 = 0(可能 = -X,被 Math.max 钳到 0),立即跑下一轮
431
+ // 之前 setTimeout(..., interval + 100) 在执行时间波动时会持续累积漂移,
432
+ // 长时间运行后周期显著偏离用户配置的 interval。
433
+ nextRunAt += interval;
434
+ const delay = Math.max(0, nextRunAt - Date.now());
435
+ timer = setTimeout(() => commitAndSchedule(interval), delay);
436
+ // 注册到 SIGINT cleanup 表 — Ctrl+C 时统一清掉
437
+ // 注意:cleanup 命名 'commitTimer',每次递归都覆盖上一次的 handler
438
+ // (cleanupTasks.set 同名覆盖语义),保证只清当前 pending 那个 setTimeout
439
+ registerCleanup('commitTimer', () => {
440
+ if (timer) {
441
+ clearTimeout(timer);
442
+ timer = null;
443
+ }
444
+ });
445
+ } catch (error) {
446
+ console.error('提交出错:', error.message);
447
+ clearTimeout(timer);
448
+ clearInterval(countdownInterval);
449
+ process.exit(1);
450
+ }
451
+ };
452
+ const judgeInterval = async () => {
453
+ const intervalArg = process.argv.find(arg => arg.startsWith('--interval'));
454
+ if (intervalArg) {
455
+ let interval = parseInt(intervalArg.split('=')[1] || '3600', 10) * 1000;
456
+
457
+ showStartInfo(interval);
458
+
459
+ // 漂移自由调度的基线 — 首次 commit 立即跑(与之前行为一致),
460
+ // 之后 nextRunAt = startAt + N*interval, 每次循环 nextRunAt += interval
461
+ nextRunAt = Date.now();
462
+
463
+ try {
464
+ await commitAndSchedule(interval);
465
+ } catch (error) {
466
+ console.error(chalk.red.bold('定时提交致命错误:'), error.message);
467
+ process.exit(1);
468
+ }
469
+ // 旧的 inline SIGINT handler 已移除,改由 setupSigintHandler 统一处理
470
+ } else {
471
+ createGitCommit({exit: false});
472
+ }
473
+ };
474
+
475
+ main()