wukong-gitlog-cli 1.0.38 → 1.0.40

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 (124) hide show
  1. package/.eslintrc +1 -0
  2. package/.prettierrc +2 -1
  3. package/CHANGELOG.md +103 -0
  4. package/README.md +93 -173
  5. package/README.zh-CN.md +85 -137
  6. package/doc//347/233/256/345/275/225/347/273/223/346/236/204.md +2871 -0
  7. package/package.json +33 -29
  8. package/rc/.wukonggitlogrc +53 -0
  9. package/scripts/compareHourlyCounts.mjs +42 -0
  10. package/scripts/compareLatest.mjs +106 -0
  11. package/src/app/analyzeAction.mjs +120 -0
  12. package/src/app/exportAction.mjs +215 -0
  13. package/src/app/exportActionProgress.mjs +37 -0
  14. package/src/app/helpers.mjs +292 -0
  15. package/src/app/initAction.mjs +110 -0
  16. package/src/app/initActionWithTemp.mjs +192 -0
  17. package/src/app/journalAction.mjs +117 -0
  18. package/src/app/overtimeAction.mjs +100 -0
  19. package/src/app/runProfileEnd.mjs +0 -0
  20. package/src/app/serveAction.mjs +73 -0
  21. package/src/app/versionAction.mjs +7 -0
  22. package/src/cli/defineOptions.mjs +209 -0
  23. package/src/cli/index.mjs +0 -0
  24. package/src/cli/parseOptions.mjs +126 -8
  25. package/src/constants/index.mjs +16 -2
  26. package/src/domain/author/analyze.mjs +6 -0
  27. package/src/domain/author/map.mjs +0 -0
  28. package/src/domain/export/exportAuthor.mjs +28 -0
  29. package/src/domain/export/exportAuthorChanges.mjs +27 -0
  30. package/src/domain/export/exportAuthorChangesJson.mjs +31 -0
  31. package/src/domain/export/exportByMonth.mjs +157 -0
  32. package/src/domain/export/exportByWeek.mjs +121 -0
  33. package/src/domain/export/exportCommits.mjs +26 -0
  34. package/src/domain/export/exportCommitsExcel.mjs +45 -0
  35. package/src/domain/export/exportCommitsJson.mjs +31 -0
  36. package/src/domain/export/index.mjs +91 -0
  37. package/src/domain/git/ensureGitAvailable.mjs +66 -0
  38. package/src/domain/git/ensureGitRepo.mjs +41 -0
  39. package/src/domain/git/getGitFeatures.mjs +59 -0
  40. package/src/domain/git/getGitLogs.mjs +326 -0
  41. package/src/domain/git/getGitUser.mjs +44 -0
  42. package/src/domain/git/getRepoRoot.mjs +32 -0
  43. package/src/domain/git/gitCapability.mjs +119 -0
  44. package/src/domain/git/index.mjs +96 -0
  45. package/src/domain/git/resolveGerrit.mjs +102 -0
  46. package/src/domain/overtime/analyze.mjs +48 -0
  47. package/src/domain/overtime/index.mjs +3 -0
  48. package/src/domain/overtime/perPeriod.mjs +15 -0
  49. package/src/domain/overtime/render.mjs +15 -0
  50. package/src/i18n/index.mjs +38 -0
  51. package/src/i18n/resources.mjs +252 -0
  52. package/src/index.mjs +132 -649
  53. package/src/infra/cache.mjs +0 -0
  54. package/src/infra/configStore.mjs +128 -0
  55. package/src/infra/fs.mjs +0 -0
  56. package/src/infra/path.mjs +0 -0
  57. package/src/output/csv/overtime.mjs +12 -0
  58. package/src/output/csv.mjs +0 -0
  59. package/src/output/data/readData.mjs +54 -0
  60. package/src/output/data/writeData.mjs +145 -0
  61. package/src/output/excel/commits.mjs +9 -0
  62. package/src/output/excel/outputExcelDayReport.mjs +92 -0
  63. package/src/output/excel/perPeriod.mjs +24 -0
  64. package/src/{excel.mjs → output/excel.mjs} +3 -2
  65. package/src/output/index.mjs +79 -0
  66. package/src/output/json/overtime.mjs +9 -0
  67. package/src/output/tab/overtime.mjs +12 -0
  68. package/src/output/tab.mjs +0 -0
  69. package/src/output/text/commits.mjs +9 -0
  70. package/src/output/text/index.mjs +3 -0
  71. package/src/output/text/outputTxtDayReport.mjs +74 -0
  72. package/src/output/text/overtime.mjs +18 -0
  73. package/src/output/utils/getEsmJs.mjs +10 -0
  74. package/src/output/utils/index.mjs +14 -0
  75. package/src/output/utils/outputPath.mjs +19 -0
  76. package/src/output/utils/writeFile.mjs +10 -0
  77. package/src/serve/index.mjs +0 -0
  78. package/src/{server.mjs → serve/startServer.mjs} +21 -3
  79. package/src/serve/writeData.mjs +0 -0
  80. package/src/utils/authorNormalizer.mjs +28 -2
  81. package/src/utils/buildAuthorChangeStats.mjs +44 -0
  82. package/src/utils/deepMerge.mjs +13 -0
  83. package/src/utils/getPackage.mjs +11 -0
  84. package/src/utils/getProfileDirFile.mjs +12 -0
  85. package/src/utils/{file.mjs → groupRecords.mjs} +8 -9
  86. package/src/utils/index.mjs +5 -2
  87. package/src/utils/logger.mjs +28 -17
  88. package/src/utils/profiler.mjs +0 -101
  89. package/src/utils/resolve.mjs +11 -0
  90. package/src/utils/showVersionInfo.mjs +6 -2
  91. package/src/utils/time.mjs +0 -0
  92. package/src/utils/wait.mjs +2 -0
  93. package/web/app.js +3233 -260
  94. package/web/index.html +175 -22
  95. package/web/revoke/alpha1/app.js +4324 -0
  96. package/web/revoke/alpha1/index.html +266 -0
  97. package/web/revoke/app.before.js +3139 -0
  98. package/web/revoke/index-before.html +181 -0
  99. package/web/static/style.css +155 -9
  100. package/src/git.mjs +0 -256
  101. package/src/handlers/handleServe.mjs +0 -203
  102. package/src/lib/configStore.mjs +0 -11
  103. package/src/lib/memoize.mjs +0 -14
  104. package/src/utils/analyzeOvertimeCached.mjs +0 -7
  105. package/src/utils/checkUpdate.mjs +0 -130
  106. package/src/utils/exitWithTime.mjs +0 -17
  107. package/src/utils/handleSuccess.mjs +0 -9
  108. package/src/utils/logDev.mjs +0 -19
  109. package/src/utils/output.mjs +0 -26
  110. package/src/utils/profiler/diff.mjs +0 -26
  111. package/src/utils/profiler/format.mjs +0 -11
  112. package/src/utils/profiler/index.mjs +0 -144
  113. package/src/utils/profiler/trace.mjs +0 -26
  114. package/src/utils/time/scopeTimer.mjs +0 -37
  115. package/src/utils/time/timer.mjs +0 -33
  116. package/src/utils/time/withTimer.mjs +0 -11
  117. package/src/utils/timer.mjs +0 -35
  118. /package/src/{overtime → domain/overtime}/createOvertimeStats.mjs +0 -0
  119. /package/src/{overtime → domain/overtime}/overtime.mjs +0 -0
  120. /package/src/{json.mjs → output/json.mjs} +0 -0
  121. /package/src/{renderAuthorMapText.mjs → output/renderAuthorMapText.mjs} +0 -0
  122. /package/src/{stats-text.mjs → output/stats-text.mjs} +0 -0
  123. /package/src/{stats.mjs → output/stats.mjs} +0 -0
  124. /package/src/{text.mjs → output/text.mjs} +0 -0
@@ -0,0 +1,59 @@
1
+ /**
2
+ * 检测 git 功能支持情况
3
+ */
4
+
5
+ import { execFile } from 'node:child_process'
6
+ import { promisify } from 'node:util'
7
+ import { getGitCapability } from './gitCapability.mjs'
8
+
9
+ const execFileAsync = promisify(execFile)
10
+
11
+ /**
12
+ * @returns {Promise<{
13
+ * numstat: boolean
14
+ * dateIsoLocal: boolean
15
+ * }>}
16
+ */
17
+ export async function getGitFeatures() {
18
+ await getGitCapability()
19
+
20
+ const features = {
21
+ numstat: false,
22
+ dateIsoLocal: false
23
+ }
24
+
25
+ /**
26
+ * --numstat 是很早就支持的
27
+ * 实际跑一次最靠谱
28
+ */
29
+ try {
30
+ await execFileAsync(
31
+ 'git',
32
+ ['log', '--numstat', '-1'],
33
+ {
34
+ windowsHide: true,
35
+ timeout: 5000,
36
+ maxBuffer: 1024 * 1024
37
+ }
38
+ )
39
+ features.numstat = false
40
+ } catch { /* empty */ }
41
+
42
+ /**
43
+ * --date=iso-local 是相对较新的格式
44
+ */
45
+ try {
46
+ await execFileAsync(
47
+ 'git',
48
+ ['log', '--date=iso-local', '-1'],
49
+ {
50
+ windowsHide: true,
51
+ timeout: 5000,
52
+ maxBuffer: 1024 * 1024
53
+ }
54
+ )
55
+ features.dateIsoLocal = true
56
+ } catch { /* empty */ }
57
+
58
+ return features
59
+ }
@@ -0,0 +1,326 @@
1
+ /* eslint-disable no-shadow */
2
+ /**
3
+ * 高性能 git log + numstat 获取 commit(无 shell / 无 WSL 版本)
4
+ *
5
+ * 特点:
6
+ * - 使用 execFile 直接调用 git(不经过 shell)
7
+ * - Windows / macOS / Linux 行为一致
8
+ * - 避免 zx / WSL / bash 相关问题
9
+ * - 支持大仓库(超大 stdout buffer)
10
+ */
11
+ import dayjs from 'dayjs'
12
+ import { execFile } from 'node:child_process'
13
+ import { promisify } from 'node:util'
14
+
15
+ import { createAuthorNormalizer } from '#utils/authorNormalizer.mjs'
16
+
17
+ const execFileAsync = promisify(execFile)
18
+
19
+ /**
20
+ * 获取 git commit 列表(高性能版)
21
+ */
22
+ export async function getGitLogsFast(opts = {}) {
23
+ /*
24
+ git: { merges: true, limit: undefined },
25
+ period: { groupBy: 'month', since: '2026-12-01', until: '2026-12-06' },
26
+ worktime: {
27
+ country: 'CN',
28
+ start: 9,
29
+ end: 18,
30
+ lunch: { start: 12, end: 14 },
31
+ overnightCutoff: 6
32
+ },
33
+ output: {
34
+ out: 'commits',
35
+ dir: 'output-wukong',
36
+ formats: 'text',
37
+ perPeriod: { enabled: true, excelMode: 'sheets', formats: [] }
38
+ },
39
+ author: { include: [ '杨琼,王欢庆' ] },
40
+ overtime: false,
41
+ gerrit: { prefix: undefined, api: undefined, auth: undefined },
42
+ serve: { port: 3000 },
43
+ profile: {
44
+ enabled: undefined,
45
+ flame: true,
46
+ traceFile: 'trace.json',
47
+ hotThreshold: 0.8,
48
+ diffThreshold: 0.2,
49
+ failOnHot: false,
50
+ diffBaseFile: 'baseline.json'
51
+ }
52
+ */
53
+ const { git = {}, period = {}, author, email } = opts
54
+ // 在运行时根据传入的 opts.authorAliases(或用户配置)创建 normalizer
55
+ const normalizer = createAuthorNormalizer(opts.authorAliases || {})
56
+ const { since, until } = period
57
+ const { limit, merges, numstat } = git
58
+
59
+ // 对于 CLI 传入的 author,只有当它是字符串时才传给 git
60
+ // 如果是对象(包含 include/exclude),我们在前端做更精细的过滤
61
+ const authorIsString = typeof author === 'string' && author.trim().length > 0
62
+
63
+ const pretty = `${[
64
+ '%H', // hash
65
+ '%an', // author name
66
+ '%ae', // email
67
+ '%ad', // date
68
+ '%s', // subject
69
+ '%B' // body
70
+ ].join('%x1f')}%x1e`
71
+
72
+ const args = [
73
+ 'log',
74
+ `--pretty=format:${pretty}`,
75
+ '--date=iso-local',
76
+ // '--numstat',
77
+ '--all'
78
+ ]
79
+
80
+ if (authorIsString) args.push(`--author=${author}`)
81
+ if (email && typeof email === 'string') args.push(`--author=${email}`)
82
+ // if (since) args.push(`--since=${until}`)
83
+ if (since) {
84
+ // 传给 git 绝对的 ISO 字符串,让 Git 自己去比对时间戳
85
+ args.push(`--since=${dayjs(since).startOf('day').toISOString()}`)
86
+ }
87
+ if (until) {
88
+ // 传给 git 绝对的 ISO 字符串,让 Git 自己去比对时间戳
89
+ args.push(`--until=${dayjs(until).endOf('day').toISOString()}`)
90
+ }
91
+ // numstat 是个性能杀手,默认不开启,除非用户明确要求,显示每次提交中更改的文件以及增删的行数统计
92
+ if (numstat) {
93
+ args.push('--numstat')
94
+ }
95
+ // if (until) args.push(`--until=${until}`)
96
+ if (!merges) args.push(`--no-merges`)
97
+ if (limit) args.push('-n', String(limit))
98
+
99
+ let stdout
100
+ try {
101
+ /**
102
+ * execFile 直接执行 git:
103
+ * - 不使用 shell
104
+ * - 不会触发 WSL / bash
105
+ * - maxBuffer 放大,防止大仓库 stdout 溢出
106
+ */
107
+ const result = await execFileAsync('git', args, {
108
+ maxBuffer: 1024 * 1024 * 200 // 200MB(Windows 大仓库友好)
109
+ })
110
+ stdout = result.stdout
111
+ } catch (err) {
112
+ /**
113
+ * 统一错误出口,方便 CLI 层捕获
114
+ */
115
+ const message = err?.stderr || err?.message || 'Failed to execute git log'
116
+ const error = new Error(message)
117
+ error.cause = err
118
+ throw error
119
+ }
120
+
121
+ /**
122
+ * Windows 下 git 输出可能带 \r
123
+ */
124
+ const raw = stdout.replace(/\r/g, '')
125
+ const commits = []
126
+
127
+ /**
128
+ * 匹配每个 commit header + body + numstat
129
+ */
130
+ const commitRegex =
131
+ // eslint-disable-next-line no-control-regex
132
+ /([0-9a-f]+)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([^\x1f]*)\x1f([\s\S]*?)(?=(?:[0-9a-f]{7,40}\x1f)|\x1e$)/g
133
+
134
+ // --- 关键改进 2: 引入内容指纹去重集 ---
135
+ const fingerPrintSet = new Set()
136
+
137
+ for (const match of raw.matchAll(commitRegex)) {
138
+ const [_, hash, authorName, emailAddr, date, subject, bodyAndNumstat] =
139
+ match
140
+
141
+ // 1. 统一作者名
142
+ const normalizedAuthor = normalizer.getAuthor(authorName, emailAddr)
143
+ // 2. 格式化日期(消除时刻差异,只看天)
144
+ const day = dayjs(date).format('YYYY-MM-DD')
145
+ // 3. 清理消息内容(去空格,取第一行)
146
+ const cleanMsg = subject.trim()
147
+
148
+ // 生成指纹:日期 + 统一后的作者 + 消息内容
149
+ // 这样即便 Hash 不同,分支不同,只要这三项一致,就视为同一项工作
150
+ const fingerPrint = `${day}_${normalizedAuthor}_${cleanMsg}`
151
+
152
+ if (fingerPrintSet.has(fingerPrint)) continue
153
+ fingerPrintSet.add(fingerPrint)
154
+
155
+ const [, changeId] =
156
+ bodyAndNumstat.match(/Change-Id:\s*(I[0-9a-fA-F]+)/) || []
157
+
158
+ const cherryPickMatch = bodyAndNumstat.match(
159
+ /\(cherry picked from commit\s+([0-9a-f]{7,40})\)/i
160
+ )
161
+
162
+ /**
163
+ * 解析 numstat
164
+ */
165
+ const commit = {
166
+ hash,
167
+ author: normalizer.getAuthor(authorName, emailAddr),
168
+ originalAuthor: authorName,
169
+ email: emailAddr,
170
+ date,
171
+ message: subject,
172
+ body: bodyAndNumstat,
173
+ changeId,
174
+
175
+ // ✅ 新增标记
176
+ isCherryPick: Boolean(cherryPickMatch),
177
+ cherryPickFrom: cherryPickMatch?.[1],
178
+
179
+ added: 0,
180
+ deleted: 0,
181
+ changed: 0,
182
+ files: []
183
+ }
184
+
185
+ const numstatRegex = /^(\d+)\s+(\d+)\s+(.+)$/gm
186
+ for (const m of bodyAndNumstat.matchAll(numstatRegex)) {
187
+ const added = parseInt(m[1], 10) || 0
188
+ const deleted = parseInt(m[2], 10) || 0
189
+ const file = m[3]
190
+
191
+ commit.added += added
192
+ commit.deleted += deleted
193
+ commit.changed += added + deleted
194
+ commit.files.push({ file, added, deleted })
195
+ }
196
+
197
+ commits.push(commit)
198
+ }
199
+
200
+ /**
201
+ * 最终统一覆盖 author
202
+ * 确保同一 email 使用同一个(中文)作者名
203
+ */
204
+ // 应用基于邮箱的最终映射
205
+ const finalMap = normalizer.getMap()
206
+ for (const c of commits) {
207
+ if (c.email && finalMap[c.email]) {
208
+ c.author = finalMap[c.email]
209
+ }
210
+ }
211
+
212
+ // 支持配置:author.include / author.exclude(可为数组或逗号分隔字符串)
213
+ const authorCfg = opts.author
214
+ function toList(v) {
215
+ if (!v) return null
216
+ if (Array.isArray(v)) return v.map((s) => String(s).trim()).filter(Boolean)
217
+ return String(v)
218
+ .split(',')
219
+ .map((s) => s.trim())
220
+ .filter(Boolean)
221
+ }
222
+
223
+ const include =
224
+ authorCfg && typeof authorCfg === 'object'
225
+ ? toList(authorCfg.include)
226
+ : typeof authorCfg === 'string'
227
+ ? toList(authorCfg)
228
+ : null
229
+ const exclude =
230
+ authorCfg && typeof authorCfg === 'object'
231
+ ? toList(authorCfg.exclude)
232
+ : null
233
+
234
+ let filteredCommits = commits
235
+
236
+ if (include || exclude) {
237
+ filteredCommits = commits.filter((c) => {
238
+ const name = (c.author || c.originalAuthor || '').trim().toLowerCase()
239
+ const mail = (c.email || '').trim().toLowerCase()
240
+
241
+ function matches(list) {
242
+ if (!list || !list.length) return false
243
+ return list.some((item) => {
244
+ const it = String(item).trim().toLowerCase()
245
+ if (it.includes('@')) return it === mail
246
+ return it === name
247
+ })
248
+ }
249
+
250
+ if (include && include.length) {
251
+ if (!matches(include)) return false
252
+ }
253
+
254
+ if (exclude && exclude.length) {
255
+ if (matches(exclude)) return false
256
+ }
257
+
258
+ return true
259
+ })
260
+ }
261
+
262
+ // 重新计算 authorMap / originalMap 以只包含筛选后的作者
263
+ const presentEmails = new Set(
264
+ filteredCommits.map((c) => c.email).filter(Boolean)
265
+ )
266
+ const filteredMap = {}
267
+ for (const [email, name] of Object.entries(finalMap)) {
268
+ if (presentEmails.has(email)) filteredMap[email] = name
269
+ }
270
+
271
+ const original = normalizer.getOriginalMap()
272
+ const filteredOriginal = {}
273
+ for (const [email, names] of Object.entries(original)) {
274
+ if (presentEmails.has(email)) filteredOriginal[email] = names
275
+ }
276
+
277
+ return {
278
+ commits: filteredCommits,
279
+ authorMap: filteredMap,
280
+ originalMap: filteredOriginal
281
+ }
282
+ }
283
+
284
+ /**
285
+ * 去重 cherry-pick commit
286
+ *
287
+ * 默认策略:
288
+ * - 按 Change-Id 去重
289
+ * - 优先保留非 cherry-pick
290
+ * - 多个 cherry-pick 时保留 first / last
291
+ */
292
+ export function dedupeCommits(
293
+ commits,
294
+ {
295
+ by = 'changeId', // 'changeId' | 'hash'
296
+ prefer = 'original' // 'original' | 'first' | 'last'
297
+ } = {}
298
+ ) {
299
+ const map = new Map()
300
+
301
+ for (const commit of commits) {
302
+ const key = by === 'changeId' ? commit.changeId || commit.hash : commit.hash
303
+
304
+ if (!map.has(key)) {
305
+ map.set(key, commit)
306
+ continue
307
+ }
308
+
309
+ const existing = map.get(key)
310
+
311
+ // 优先保留非 cherry-pick
312
+ if (prefer === 'original') {
313
+ if (!existing.isCherryPick && commit.isCherryPick) continue
314
+ if (existing.isCherryPick && !commit.isCherryPick) {
315
+ map.set(key, commit)
316
+ continue
317
+ }
318
+ }
319
+
320
+ if (prefer === 'last') {
321
+ map.set(key, commit)
322
+ }
323
+ }
324
+
325
+ return Array.from(map.values())
326
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * 获取 git user.name / user.email
3
+ */
4
+
5
+ import { execFile } from 'node:child_process'
6
+ import { promisify } from 'node:util'
7
+ import { getGitCapability } from './gitCapability.mjs'
8
+
9
+ const execFileAsync = promisify(execFile)
10
+
11
+ /**
12
+ * @returns {Promise<{ name: string, email: string }>}
13
+ */
14
+ export async function getGitUser() {
15
+ await getGitCapability()
16
+
17
+ const getConfig = async (key) => {
18
+ try {
19
+ const { stdout } = await execFileAsync(
20
+ 'git',
21
+ ['config', '--get', key],
22
+ {
23
+ windowsHide: true,
24
+ timeout: 3000,
25
+ maxBuffer: 1024 * 1024
26
+ }
27
+ )
28
+
29
+ return stdout.trim()
30
+ } catch {
31
+ return ''
32
+ }
33
+ }
34
+
35
+ const [name, email] = await Promise.all([
36
+ getConfig('user.name'),
37
+ getConfig('user.email')
38
+ ])
39
+
40
+ return {
41
+ name,
42
+ email
43
+ }
44
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 获取 git 仓库根目录
3
+ */
4
+
5
+ import { execFile } from 'node:child_process'
6
+ import { promisify } from 'node:util'
7
+ import { getGitCapability } from './gitCapability.mjs'
8
+
9
+ const execFileAsync = promisify(execFile)
10
+
11
+ /**
12
+ * @returns {Promise<string>} 仓库根路径
13
+ */
14
+ export async function getRepoRoot() {
15
+ await getGitCapability()
16
+
17
+ try {
18
+ const { stdout } = await execFileAsync(
19
+ 'git',
20
+ ['rev-parse', '--show-toplevel'],
21
+ {
22
+ windowsHide: true,
23
+ timeout: 5000,
24
+ maxBuffer: 1024 * 1024
25
+ }
26
+ )
27
+
28
+ return stdout.trim()
29
+ } catch {
30
+ throw new Error(`❌ 无法获取 Git 仓库根目录`)
31
+ }
32
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Git Capability Cache
3
+ *
4
+ * - CLI 生命周期内只检测一次
5
+ * - 不使用 shell
6
+ * - 不触发 WSL
7
+ */
8
+ import { execFile } from 'node:child_process'
9
+ import { promisify } from 'node:util'
10
+
11
+ const execFileAsync = promisify(execFile)
12
+
13
+ /**
14
+ * @type {null | {
15
+ * available: boolean
16
+ * version: string
17
+ * platform: string
18
+ * checkedAt: number
19
+ * }}
20
+ */
21
+ let cache = null
22
+
23
+ /**
24
+ * 内部:真正执行 git 检测
25
+ */
26
+ async function detectGit() {
27
+ const {platform} = process
28
+
29
+ const { stdout } = await execFileAsync('git', ['--version'], {
30
+ windowsHide: true,
31
+ timeout: 5000,
32
+ maxBuffer: 1024 * 1024
33
+ })
34
+
35
+ if (!stdout || !stdout.toLowerCase().includes('git version')) {
36
+ throw new Error(`Unexpected git output: ${stdout}`)
37
+ }
38
+
39
+ return {
40
+ available: true,
41
+ version: stdout.trim(),
42
+ platform,
43
+ checkedAt: Date.now()
44
+ }
45
+ }
46
+
47
+ /**
48
+ * 对外 API:获取 git capability(带缓存)
49
+ */
50
+ export async function getGitCapability() {
51
+ if (cache) {
52
+ return cache
53
+ }
54
+
55
+ try {
56
+ cache = await detectGit()
57
+ return cache
58
+ } catch (err) {
59
+ cache = {
60
+ available: false,
61
+ version: '',
62
+ platform: process.platform,
63
+ checkedAt: Date.now()
64
+ }
65
+
66
+ // eslint-disable-next-line no-use-before-define
67
+ const error = new Error(buildGitNotAvailableMessage())
68
+ error.cause = err
69
+ throw error
70
+ }
71
+ }
72
+
73
+ /**
74
+ * 是否已检测(通常你不需要)
75
+ */
76
+ export function isGitCapabilityCached() {
77
+ return Boolean(cache)
78
+ }
79
+
80
+ /**
81
+ * 生成跨平台提示信息
82
+ */
83
+ function buildGitNotAvailableMessage() {
84
+ const {platform} = process
85
+
86
+ if (platform === 'win32') {
87
+ return `
88
+ ❌ Git 不可用,CLI 无法继续运行
89
+
90
+ 请确认:
91
+ 1️⃣ 已安装 Git for Windows
92
+ https://git-scm.com/download/win
93
+
94
+ 2️⃣ 安装时勾选:
95
+ ✔ "Add Git to PATH"
96
+
97
+ 3️⃣ 关闭并重新打开终端后再试
98
+ `
99
+ }
100
+
101
+ if (platform === 'darwin') {
102
+ return `
103
+ ❌ Git 不可用,CLI 无法继续运行
104
+
105
+ 可通过以下方式安装 git:
106
+ xcode-select --install
107
+
108
+ brew install git
109
+ `
110
+ }
111
+
112
+ return `
113
+ ❌ Git 不可用,CLI 无法继续运行
114
+
115
+ 请使用系统包管理器安装 git,例如:
116
+ apt install git
117
+ yum install git
118
+ `
119
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @file: index.mjs
3
+ * @description:启动前环境检查
4
+ * @author: King Monkey
5
+ * @created: 2026-01-13 01:16
6
+ */
7
+ import { ensureGitRepo } from '#src/domain/git/ensureGitRepo.mjs'
8
+ import { getGitFeatures } from '#src/domain/git/getGitFeatures.mjs'
9
+ import { getGitUser } from '#src/domain/git/getGitUser.mjs'
10
+ import { getRepoRoot } from '#src/domain/git/getRepoRoot.mjs'
11
+ import { getGitCapability } from '#src/domain/git/gitCapability.mjs'
12
+
13
+ /**
14
+ * CLI 启动阶段 Git 运行环境预检(Preflight)
15
+ *
16
+ * @returns {Promise<{
17
+ * git: {
18
+ * version: string
19
+ * platform: string
20
+ * },
21
+ * repo: {
22
+ * root: string
23
+ * },
24
+ * user: {
25
+ * name: string
26
+ * email: string
27
+ * },
28
+ * features: {
29
+ * numstat: boolean
30
+ * dateIsoLocal: boolean
31
+ * },
32
+ * meta: {
33
+ * checkedAt: number
34
+ * }
35
+ * }>}
36
+ */
37
+ export const runGitPreflight = async () => {
38
+ // 1️⃣ git 可执行能力(带全局 cache)
39
+ // CLI 启动即检测(一次)
40
+ const gitCapability = await getGitCapability()
41
+
42
+ // 2️⃣ 必须在 git repo 内
43
+ await ensureGitRepo()
44
+
45
+ // 3️⃣ 并行获取上下文信息
46
+ const [root, user, features] = await Promise.all([
47
+ getRepoRoot(),
48
+ getGitUser(),
49
+ getGitFeatures()
50
+ ])
51
+
52
+ const result = {
53
+ git: {
54
+ version: gitCapability.version,
55
+ platform: gitCapability.platform
56
+ },
57
+
58
+ repo: {
59
+ root
60
+ },
61
+
62
+ user: {
63
+ name: user.name,
64
+ email: user.email
65
+ },
66
+
67
+ features,
68
+
69
+ meta: {
70
+ checkedAt: gitCapability.checkedAt
71
+ }
72
+ }
73
+
74
+ // 可选:打印
75
+ // console.log(`✔ Git detected: ${result.git.version}`)
76
+ // console.log(`✔ platform: ${result.git.platform}`)
77
+
78
+ // console.log('✔ root', result.repo.root)
79
+ // console.log('✔ user', result.user.name)
80
+ // console.log('✔ email', result.user.email)
81
+ // console.log('✔ features', result.features)
82
+ // console.log(`-`.repeat(50), '\n')
83
+
84
+ return result
85
+ }
86
+
87
+ export const showGitInfo = async (gitInfo) => {
88
+ console.log(`✔ Git detected: ${gitInfo.git.version}`)
89
+ console.log(`✔ platform: ${gitInfo.git.platform}`)
90
+
91
+ console.log('✔ root', gitInfo.repo.root)
92
+ console.log('✔ user', gitInfo.user.name)
93
+ console.log('✔ email', gitInfo.user.email)
94
+ console.log('✔ features', gitInfo.features)
95
+ console.log(`-`.repeat(50), '\n')
96
+ }