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
package/src/index.mjs CHANGED
@@ -1,658 +1,154 @@
1
- import chalk from 'chalk'
1
+ #!/usr/bin/env node
2
2
  import { Command } from 'commander'
3
- import dayjs from 'dayjs'
4
- import isoWeek from 'dayjs/plugin/isoWeek.js'
5
- import fs from 'fs'
6
- import ora from 'ora'
7
- import path from 'path'
8
- import { fileURLToPath } from 'url'
9
- import { createProfiler } from 'wukong-profiler'
10
3
 
11
- import { parseOptions } from './cli/parseOptions.mjs'
12
- // eslint-disable-next-line no-unused-vars
13
- import { CLI_NAME } from './constants/index.mjs'
14
- import {
15
- exportExcel,
16
- exportExcelAuthorChangeStats,
17
- exportExcelPerPeriodSheets
18
- } from './excel.mjs'
19
- import { getGitLogsFast } from './git.mjs'
20
- import { handleServe } from './handlers/handleServe.mjs'
21
- import { renderAuthorChangesJson } from './json.mjs'
22
- import { setConfig } from './lib/configStore.mjs'
23
- import { createOvertimeStats } from './overtime/createOvertimeStats.mjs'
24
- import {
25
- renderOvertimeCsv,
26
- renderOvertimeTab,
27
- renderOvertimeText
28
- } from './overtime/overtime.mjs'
29
- import { renderAuthorMapText } from './renderAuthorMapText.mjs'
30
- import { startServer } from './server.mjs'
31
- import { renderChangedLinesText, renderText } from './text.mjs'
32
- import { checkUpdateWithPatch } from './utils/checkUpdate.mjs'
33
- import { handleSuccess } from './utils/handleSuccess.mjs'
34
- import {
35
- groupRecords,
36
- outputFilePath,
37
- writeJSON,
38
- writeTextFile
39
- } from './utils/index.mjs'
40
- import { logDev } from './utils/logDev.mjs'
41
- import { showVersionInfo } from './utils/showVersionInfo.mjs'
42
-
43
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
44
-
45
- const pkg = JSON.parse(
46
- fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf8')
47
- )
48
-
49
- dayjs.extend(isoWeek)
50
-
51
- const PKG_NAME = pkg.name
52
- const VERSION = pkg.version
53
-
54
- let profiler
55
-
56
- const autoCheckUpdate = async () => {
57
- // === CLI 主逻辑完成后提示更新 ===
58
- await checkUpdateWithPatch({
59
- pkg: {
60
- name: PKG_NAME,
61
- version: VERSION
62
- },
63
- force: true
64
- })
65
- }
66
-
67
- const version = async () => {
68
- showVersionInfo(VERSION)
69
- await autoCheckUpdate()
70
- process.exit(0)
71
- }
72
-
73
- /** 将 "2025-W48" → { start: '2025-11-24', end: '2025-11-30' } */
74
- export function getWeekRange(periodStr) {
75
- // periodStr = "2025-W48"
76
- const [year, w] = periodStr.split('-W')
77
- const week = parseInt(w, 10)
4
+ import { runGitPreflight, showGitInfo } from '#src/domain/git/index.mjs'
78
5
 
79
- const start = dayjs().year(year).isoWeek(week).startOf('week') // Monday
80
- const end = dayjs().year(year).isoWeek(week).endOf('week') // Sunday
81
-
82
- return {
83
- start: start.format('YYYY-MM-DD'),
84
- end: end.format('YYYY-MM-DD')
85
- }
86
- }
6
+ import { analyzeAction } from './app/analyzeAction.mjs'
7
+ import { exportAction } from './app/exportAction.mjs'
8
+ // import { initAction } from './app/initAction.mjs'
9
+ import { initActionWithTemp as initAction } from './app/initActionWithTemp.mjs'
10
+ import { journalAction } from './app/journalAction.mjs'
11
+ import { overtimeAction } from './app/overtimeAction.mjs'
12
+ import { serveAction } from './app/serveAction.mjs'
13
+ import { versionAction } from './app/versionAction.mjs'
14
+ import {
15
+ addAnalysisOptions,
16
+ addGitSourceOptions,
17
+ addOutputOptions,
18
+ addPerformanceOptions,
19
+ addServeOptions,
20
+ setupBaseProgram
21
+ } from './cli/defineOptions.mjs'
22
+ import { initI18n, t } from './i18n/index.mjs'
23
+ import { loadRcConfig } from './infra/configStore.mjs'
24
+
25
+ // 引入加载器
87
26
 
88
27
  const main = async () => {
89
- const program = new Command()
90
-
91
- program
92
- .name('git-commits')
93
- .version(pkg.version, '-v', 'show version')
94
- .description('Advanced Git commit log exporter.')
95
- .option('--author <name>', '指定 author 名')
96
- .option('--email <email>', '指定 email')
97
- .option('--since <date>', '起始日期')
98
- .option('--until <date>', '结束日期')
99
- .option('--limit <n>', '限制数量', parseInt)
100
- .option('--no-merges', '不包含 merge commit')
101
- .option('--export', '导出统计数据')
102
- .option('--json', '输出 JSON')
103
- .option('--format <type>', '输出格式: text | excel | json', 'text')
104
- .option('--group-by <type>', '按日期分组: day | month | week')
105
- .option('--stats', '输出每日统计数据')
106
- .option(
107
- '--gerrit <prefix>',
108
- '显示 Gerrit 地址,支持在 prefix 中使用 {{hash}} 占位符'
109
- )
110
- .option(
111
- '--gerrit-api <url>',
112
- '可选:Gerrit REST API 基础地址,用于解析 changeNumber,例如 `https://gerrit.example.com`'
113
- )
114
- .option(
115
- '--gerrit-auth <tokenOrUserPass>',
116
- '可选:Gerrit API 授权,格式为 `user:pass` 或 `TOKEN`(表示 Bearer token)'
117
- )
118
- .option('--overtime', '分析公司加班文化(输出下班时间与非工作日提交占比)')
119
- .option('--country <code>', '节假日国家:CN 或 US,默认为 CN', 'CN')
120
- .option(
121
- '--work-start <hour>',
122
- '上班开始小时,默认 9',
123
- (v) => parseInt(v, 10),
124
- 9
125
- )
126
- .option(
127
- '--work-end <hour>',
128
- '下班小时,默认 18',
129
- (v) => parseInt(v, 10),
130
- 18
131
- )
132
- .option(
133
- '--lunch-start <hour>',
134
- '午休开始小时,默认 12',
135
- (v) => parseInt(v, 10),
136
- 12
137
- )
138
- .option(
139
- '--lunch-end <hour>',
140
- '午休结束小时,默认 14',
141
- (v) => parseInt(v, 10),
142
- 14
143
- )
144
- .option(
145
- '--overnight-cutoff <hour>',
146
- '次日凌晨归并窗口(小时),默认 6',
147
- (v) => parseInt(v, 10),
148
- 6
149
- )
150
- .option('--out <file>', '输出文件名(不含路径)')
151
- .option(
152
- '--out-dir <dir>',
153
- '自定义输出目录,支持相对路径或绝对路径,例如 `--out-dir ../output-wukong`'
154
- )
155
- .option(
156
- '--out-parent',
157
- '将输出目录放到当前工程的父目录的 `output-wukong/`(等同于 `--out-dir ../output-wukong`)'
158
- )
159
- .option(
160
- '--per-period-formats <formats>',
161
- '每个周期单独输出的格式,逗号分隔:text,csv,tab,xlsx。默认为空(不输出 CSV/Tab/XLSX)',
162
- ''
163
- )
164
- .option(
165
- '--per-period-excel-mode <mode>',
166
- 'per-period Excel 模式:sheets|files(默认:sheets)',
167
- 'sheets'
168
- )
169
- .option(
170
- '--per-period-only',
171
- '仅输出 per-period(month/week)文件,不输出合并的 monthly/weekly 汇总文件'
172
- )
173
- .option(
174
- '--serve',
175
- '启动本地 web 服务,查看提交统计(将在 output-wukong/data 下生成数据文件)'
176
- )
177
- .option(
178
- '--port <n>',
179
- '本地 web 服务端口(默认 3000)',
180
- (v) => parseInt(v, 10),
181
- 3000
182
- )
183
- .option('--debug', 'enable debug logs')
184
- .option(
185
- '--serve-only',
186
- '仅启动 web 服务,不导出或分析数据(使用 output-wukong/data 中已有的数据)'
187
- )
188
- .option('--version', 'show version information')
189
- .option('--profile', '输出性能分析 JSON')
190
- .option('--verbose', '显示详细性能日志')
191
- .option('--flame', '显示 flame-like 日志')
192
- .option('--trace <file>', '生成 Chrome Trace')
193
- .option('--hot-threshold <n>', 'HOT 比例阈值', parseFloat, 0.8)
194
- .option('--fail-on-hot', 'HOT 时 CI 失败')
195
- .option('--diff-base <file>', '基线 profile.json')
196
- .option('--diff-threshold <n>', '回归阈值', parseFloat, 0.2)
197
- .parse()
198
-
199
- const opts = program.opts()
200
-
201
- const config = parseOptions(opts)
202
-
203
- profiler = createProfiler({
204
- enabled: opts.profile,
205
- verbose: opts.verbose,
206
- flame: opts.flame,
207
- traceFile: opts.trace,
208
- hotThreshold: opts.hotThreshold,
209
- failOnHot: opts.failOnHot,
210
- diffBaseFile: opts.diffBase,
211
- diffThreshold: opts.diffThreshold
212
- })
213
-
214
- // ❗只创建一次缓存实例
215
- const getOvertimeStats = createOvertimeStats(config)
216
-
217
- setConfig('debug', opts.debug === true)
218
-
219
- // compute output directory root early (so serve-only can use it)
220
- const outDir = opts.outParent
221
- ? path.resolve(process.cwd(), '..', 'output-wukong')
222
- : opts.outDir || undefined
223
-
224
- if (opts.version) {
225
- await version()
226
- return
28
+ // --- 第一步:参数预检 (Pre-flight) ---
29
+ const args = process.argv.slice(2)
30
+
31
+ // 1. 快速提取 --lang 参数(不依赖 Commander,避免混乱)
32
+ const langArgIndex = args.findIndex((a) => a === '--lang' || a === '-l')
33
+ const userLang = langArgIndex !== -1 ? args[langArgIndex + 1] : null
34
+
35
+ // 初始化字典
36
+ // 2. 初始化 i18n(如果有 --lang --lang,没有则内部调 osLocale)
37
+ // 这一步必须在定义子命令描述之前完成!
38
+ const finalLang = await initI18n(userLang)
39
+
40
+ // ---------------------------------------------------------
41
+ // 2. 环境准备
42
+ // ---------------------------------------------------------
43
+ const gitPreflightResult = await runGitPreflight()
44
+ if (args.includes('--info') || args.includes('-i')) {
45
+ console.log(`✔ Language: ${finalLang}`)
46
+ await showGitInfo(gitPreflightResult)
227
47
  }
228
- // if serve-only is requested, start server and exit
229
- if (opts.serveOnly) {
230
- try {
231
- await startServer(opts.port || 3000, outDir)
232
- } catch (err) {
233
- console.warn(
234
- 'Start server failed:',
235
- err && err.message ? err.message : err
236
- )
237
- process.exit(1)
238
- }
239
- return
48
+ // 【关键优化】在一切开始前,先异步加载 RC 配置
49
+ // 这样后续 parseOptions 内部的 cachedConfig 就有值了
50
+ await loadRcConfig()
51
+
52
+ // 拦截 --version 自定义逻辑
53
+ if (args.includes('--version')) {
54
+ await versionAction()
55
+ process.exit(0) // 彻底拦截,不走后面的子命令逻辑
240
56
  }
241
57
 
242
- const spinner = ora('Loading...').start()
58
+ // ---------------------------------------------------------
59
+ // 3. 定义 CLI 结构
60
+ // ---------------------------------------------------------
61
+ const program = new Command()
243
62
 
244
- const gitCommits = await getGitLogsFast(opts)
245
- let { commits: records } = gitCommits
246
- const { authorMap } = gitCommits
247
- // compute output directory root if user provided one or wants parent
63
+ // A. 挂载基础信息 (name, version, --lang, --debug)
64
+ setupBaseProgram(program)
248
65
 
249
- // --- Gerrit 地址处理(若提供) ---
250
- if (opts.gerrit) {
251
- const prefix = opts.gerrit
252
- // support optional changeNumber resolution via Gerrit REST API
253
- const { gerritApi, gerritAuth } = opts
254
- // create new array to avoid mutating function parameters (eslint: no-param-reassign)
255
- if (prefix.includes('{{changeNumber}}') && gerritApi) {
256
- // async mapping to resolve changeNumber using Gerrit API
257
- const cache = new Map()
258
- const headers = {}
259
- if (gerritAuth) {
260
- if (gerritAuth.includes(':')) {
261
- headers.Authorization = `Basic ${Buffer.from(gerritAuth).toString('base64')}`
262
- } else {
263
- headers.Authorization = `Bearer ${gerritAuth}`
264
- }
265
- }
266
- const fetchGerritJson = async (url) => {
267
- try {
268
- const res = await fetch(url, { headers })
269
- const txt = await res.text()
270
- // Gerrit prepends )]}' to JSON responses — strip it
271
- const jsonText = txt.replace(/^\)\]\}'\n/, '')
272
- return JSON.parse(jsonText)
273
- } catch (err) {
274
- return null
275
- }
276
- }
277
- const resolveChangeNumber = async (r) => {
278
- // try changeId first
279
- if (r.changeId) {
280
- if (cache.has(r.changeId)) return cache.get(r.changeId)
281
- // try `changes/{changeId}/detail`
282
- const url = `${gerritApi.replace(/\/$/, '')}/changes/${encodeURIComponent(r.changeId)}/detail`
283
- let j = await fetchGerritJson(url)
284
- if (j && j._number) {
285
- cache.set(r.changeId, j._number)
286
- return j._number
287
- }
288
- // fallback: query search
289
- const url2 = `${gerritApi.replace(/\/$/, '')}/changes/?q=change:${encodeURIComponent(r.changeId)}`
290
- j = await fetchGerritJson(url2)
291
- if (Array.isArray(j) && j.length > 0 && j[0]._number) {
292
- cache.set(r.changeId, j[0]._number)
293
- return j[0]._number
294
- }
295
- }
296
- // try commit hash
297
- if (r.hash) {
298
- if (cache.has(r.hash)) return cache.get(r.hash)
299
- const url3 = `${gerritApi.replace(/\/$/, '')}/changes/?q=commit:${encodeURIComponent(r.hash)}`
300
- const j = await fetchGerritJson(url3)
301
- if (Array.isArray(j) && j.length > 0 && j[0]._number) {
302
- cache.set(r.hash, j[0]._number)
303
- return j[0]._number
304
- }
305
- }
306
- return null
307
- }
308
- records = await Promise.all(
309
- records.map(async (r) => {
310
- const changeNumber = await resolveChangeNumber(r)
311
- const changeNumberOrFallback = changeNumber || r.changeId || r.hash
312
- const gerritUrl = prefix.replace(
313
- '{{changeNumber}}',
314
- changeNumberOrFallback
315
- )
316
- return { ...r, gerrit: gerritUrl }
317
- })
318
- )
319
- } else if (prefix.includes('{{changeNumber}}') && !gerritApi) {
320
- console.warn(
321
- 'prefix contains {{changeNumber}} but no --gerrit-api provided — falling back to changeId/hash'
322
- )
323
- records = records.map((r) => ({
324
- ...r,
325
- gerrit: prefix.replace('{{changeNumber}}', r.changeId || r.hash)
326
- }))
327
- } else {
328
- records = records.map((r) => {
329
- let gerritUrl
330
- if (prefix.includes('{{changeId}}')) {
331
- const changeId = r.changeId || r.hash
332
- gerritUrl = prefix.replace('{{changeId}}', changeId)
333
- } else if (prefix.includes('{{hash}}')) {
334
- gerritUrl = prefix.replace('{{hash}}', r.hash)
335
- } else {
336
- gerritUrl = prefix.endsWith('/')
337
- ? `${prefix}${r.hash}`
338
- : `${prefix}/${r.hash}`
339
- }
340
- return { ...r, gerrit: gerritUrl }
341
- })
342
- }
343
- }
66
+ // B. 注册子命令:按需组合参数模块
344
67
 
345
- // --- 分组 ---
346
- const groups = opts.groupBy ? groupRecords(records, opts.groupBy) : null
347
- profiler.step('load config')
68
+ // === 命令: Init ===
348
69
 
349
- // If serve mode is enabled, write data modules and launch the web server
350
- if (opts.serve) {
351
- handleServe({ opts, outDir, records, getOvertimeStats })
352
- }
353
-
354
- // --- Overtime analysis ---
355
- if (opts.overtime) {
356
- await profiler.stepAsync('getOvertimeStats', async () => {
357
- await getOvertimeStats(records)
70
+ program
71
+ .command('init')
72
+ .description(t('cmds.init'))
73
+ .option('-f, --force', t('options.force'))
74
+ .action(async (options) => {
75
+ // 如果你不想抽离到 app 层,也可以直接写这里
76
+ // 但推荐抽离以保持架构一致性
77
+ await initAction(options)
358
78
  })
359
- const stats = getOvertimeStats(records)
360
-
361
- profiler.step('load getOvertimeStats')
362
- // Output to console
363
- console.log('\n--- Overtime analysis ---\n')
364
- console.log(renderOvertimeText(stats))
365
-
366
- const authorMapText = renderAuthorMapText(authorMap)
367
- console.log('\n Developers:\n', authorMapText, '\n')
368
- writeTextFile(outputFilePath('authors.text', outDir), authorMapText)
369
-
370
- // if user requested json format, write stats to file
371
- if (opts.json || opts.format === 'json') {
372
- const file = opts.out || 'overtime.json'
373
- const filepath = outputFilePath(file, outDir)
374
- writeJSON(filepath, stats)
375
- logDev(`overtime JSON 已导出: ${filepath}`)
376
- }
377
- // Always write human readable overtime text to file (default: overtime.txt)
378
- const outBase = opts.out
379
- ? path.basename(opts.out, path.extname(opts.out))
380
- : 'commits'
381
- const overtimeFileName = `overtime_${outBase}.txt`
382
- const overtimeFile = outputFilePath(overtimeFileName, outDir)
383
- writeTextFile(overtimeFile, renderOvertimeText(stats))
384
- // write tab-separated text file for better alignment in editors that use proportional fonts
385
- const overtimeTabFileName = `overtime_${outBase}.tab.txt`
386
- const overtimeTabFile = outputFilePath(overtimeTabFileName, outDir)
387
- writeTextFile(overtimeTabFile, renderOvertimeTab(stats))
388
- // write CSV for structured data consumption
389
- const overtimeCsvFileName = `overtime_${outBase}.csv`
390
- const overtimeCsvFile = outputFilePath(overtimeCsvFileName, outDir)
391
- writeTextFile(overtimeCsvFile, renderOvertimeCsv(stats))
392
- logDev(`Overtime text 已导出: ${overtimeFile}`)
393
- logDev(`Overtime table (tabs) 已导出: ${overtimeTabFile}`)
394
- logDev(`Overtime CSV 已导出: ${overtimeCsvFile}`)
395
-
396
- // 按月输出 ... 保持原逻辑
397
- const perPeriodFormats = String(opts.perPeriodFormats || '')
398
- .split(',')
399
- .map((s) =>
400
- String(s || '')
401
- .trim()
402
- .toLowerCase()
403
- )
404
- .filter(Boolean)
405
- try {
406
- const monthGroups = groupRecords(records, 'month')
407
- const monthlyFileName = `overtime_${outBase}_monthly.txt`
408
- const monthlyFile = outputFilePath(monthlyFileName, outDir)
409
- let monthlyContent = ''
410
- const monthKeys = Object.keys(monthGroups).sort()
411
- monthKeys.forEach((k) => {
412
- const groupRecs = monthGroups[k]
413
- const s = getOvertimeStats(groupRecs)
414
- monthlyContent += `===== ${k} =====\n`
415
- monthlyContent += `${renderOvertimeText(s)}\n\n`
416
- // Also write a single file per month under 'month/' folder
417
- try {
418
- const perMonthFileName = `month/overtime_${outBase}_${k}.txt`
419
- const perMonthFile = outputFilePath(perMonthFileName, outDir)
420
- writeTextFile(perMonthFile, renderOvertimeText(s))
421
- logDev(`Overtime 月度(${k}) 已导出: ${perMonthFile}`)
422
- // per-period CSV / Tab format (按需生成)
423
- if (perPeriodFormats.includes('csv')) {
424
- try {
425
- const perMonthCsvName = `month/overtime_${outBase}_${k}.csv`
426
- writeTextFile(
427
- outputFilePath(perMonthCsvName, outDir),
428
- renderOvertimeCsv(s)
429
- )
430
- logDev(
431
- `Overtime 月度(CSV)(${k}) 已导出: ${outputFilePath(perMonthCsvName, outDir)}`
432
- )
433
- } catch (err) {
434
- console.warn(
435
- `Write monthly CSV for ${k} failed:`,
436
- err && err.message ? err.message : err
437
- )
438
- }
439
- }
440
- if (perPeriodFormats.includes('tab')) {
441
- try {
442
- const perMonthTabName = `month/overtime_${outBase}_${k}.tab.txt`
443
- writeTextFile(
444
- outputFilePath(perMonthTabName, outDir),
445
- renderOvertimeTab(s)
446
- )
447
- logDev(
448
- `Overtime 月度(Tab)(${k}) 已导出: ${outputFilePath(perMonthTabName, outDir)}`
449
- )
450
- } catch (err) {
451
- console.warn(
452
- `Write monthly Tab for ${k} failed:`,
453
- err && err.message ? err.message : err
454
- )
455
- }
456
- }
457
- } catch (err) {
458
- console.warn(
459
- `Write monthly file for ${k} failed:`,
460
- err && err.message ? err.message : err
461
- )
462
- }
463
- })
464
- if (!opts.perPeriodOnly) {
465
- writeTextFile(monthlyFile, monthlyContent)
466
- logDev(`Overtime 月度汇总 已导出: ${monthlyFile}`)
467
- }
468
- // per-period Excel (sheets or files)
469
- if (perPeriodFormats.includes('xlsx')) {
470
- const perPeriodExcelMode = String(opts.perPeriodExcelMode || 'sheets')
471
- if (perPeriodExcelMode === 'sheets') {
472
- try {
473
- const monthXlsxName = `month/overtime_${outBase}_monthly.xlsx`
474
- const monthXlsxFile = outputFilePath(monthXlsxName, outDir)
475
- await exportExcelPerPeriodSheets(monthGroups, monthXlsxFile, {
476
- stats: opts.stats,
477
- gerrit: opts.gerrit
478
- })
479
- logDev(`Overtime 月度(XLSX) 已导出: ${monthXlsxFile}`)
480
- } catch (err) {
481
- console.warn(
482
- 'Export month XLSX (sheets) failed:',
483
- err && err.message ? err.message : err
484
- )
485
- }
486
- } else {
487
- try {
488
- const monthKeys2 = Object.keys(monthGroups).sort()
489
- const tasks = monthKeys2.map((k2) => {
490
- const perMonthXlsxName = `month/overtime_${outBase}_${k2}.xlsx`
491
- const perMonthXlsxFile = outputFilePath(perMonthXlsxName, outDir)
492
- return exportExcel(monthGroups[k2], null, {
493
- file: perMonthXlsxFile,
494
- stats: opts.stats,
495
- gerrit: opts.gerrit
496
- }).then(() =>
497
- console.log(
498
- chalk.green(
499
- `Overtime 月度(XLSX)(${k2}) 已导出: ${perMonthXlsxFile}`
500
- )
501
- )
502
- )
503
- })
504
- await Promise.all(tasks)
505
- } catch (err) {
506
- console.warn(
507
- 'Export monthly XLSX files failed:',
508
- err && err.message ? err.message : err
509
- )
510
- }
511
- }
512
- }
513
- } catch (err) {
514
- console.warn(
515
- 'Generate monthly overtime failed:',
516
- err && err.message ? err.message : err
517
- )
518
- }
519
-
520
- // 周度输出保持原逻辑(略)
521
- try {
522
- const weekGroups = groupRecords(records, 'week')
523
- const weeklyFileName = `overtime_${outBase}_weekly.txt`
524
- const weeklyFile = outputFilePath(weeklyFileName, outDir)
525
- let weeklyContent = ''
526
- const weekKeys = Object.keys(weekGroups).sort()
527
- weekKeys.forEach((k) => {
528
- const groupRecs = weekGroups[k]
529
- const s = getOvertimeStats(groupRecs)
530
- weeklyContent += `===== ${k} =====\n`
531
- weeklyContent += `${renderOvertimeText(s)}\n\n`
532
- try {
533
- const perWeekFileName = `week/overtime_${outBase}_${k}.txt`
534
- const perWeekFile = outputFilePath(perWeekFileName, outDir)
535
- writeTextFile(perWeekFile, renderOvertimeText(s))
536
- // console.log(chalk.green(`Overtime 周度(${k}) 已导出: ${perWeekFile}`))
537
- logDev(`Overtime 周度(${k}) 已导出: ${perWeekFile}`)
538
-
539
- // eslint-disable-next-line no-shadow
540
- const perPeriodFormats = String(opts.perPeriodFormats || '')
541
- .split(',')
542
- // eslint-disable-next-line no-shadow
543
- .map((s) =>
544
- String(s || '')
545
- .trim()
546
- .toLowerCase()
547
- )
548
- .filter(Boolean)
549
- if (perPeriodFormats.includes('csv')) {
550
- try {
551
- const perWeekCsvName = `week/overtime_${outBase}_${k}.csv`
552
- writeTextFile(
553
- outputFilePath(perWeekCsvName, outDir),
554
- renderOvertimeCsv(s)
555
- )
556
- logDev(
557
- `Overtime 周度(CSV)(${k}) 已导出: ${outputFilePath(perWeekCsvName, outDir)}`
558
- )
559
- } catch (err) {
560
- console.warn(
561
- `Write weekly CSV for ${k} failed:`,
562
- err && err.message ? err.message : err
563
- )
564
- }
565
- }
566
- if (perPeriodFormats.includes('tab')) {
567
- try {
568
- const perWeekTabName = `week/overtime_${outBase}_${k}.tab.txt`
569
- writeTextFile(
570
- outputFilePath(perWeekTabName, outDir),
571
- renderOvertimeTab(s)
572
- )
573
- logDev(
574
- `Overtime 周度(Tab)(${k}) 已导出: ${outputFilePath(perWeekTabName, outDir)}`
575
- )
576
- } catch (err) {
577
- console.warn(
578
- `Write weekly Tab for ${k} failed:`,
579
- err && err.message ? err.message : err
580
- )
581
- }
582
- }
583
- } catch (err) {
584
- console.warn(
585
- `Write weekly file for ${k} failed:`,
586
- err && err.message ? err.message : err
587
- )
588
- }
589
- })
590
- writeTextFile(weeklyFile, weeklyContent)
591
- logDev(`Overtime 周度汇总 已导出: ${weeklyFile}`)
592
- } catch (err) {
593
- console.warn(
594
- 'Generate weekly overtime failed:',
595
- err && err.message ? err.message : err
596
- )
597
- }
598
- }
599
79
 
600
- // --- JSON/TEXT/EXCEL(保持原逻辑) ---
601
- if (opts.json || opts.format === 'json') {
602
- const file = opts.out || 'commits.json'
603
- const filepath = outputFilePath(file, outDir)
604
- writeJSON(filepath, groups || records)
605
- const jsonText = renderAuthorChangesJson(records)
606
- writeJSON(outputFilePath('author-changes.json', outDir), jsonText)
607
- logDev(`JSON 已导出: ${filepath}`)
608
- handleSuccess({ spinner })
609
- return
610
- }
611
-
612
- if (opts.format === 'text') {
613
- const file = opts.out || 'commits.txt'
614
- const filepath = outputFilePath(file, outDir)
615
- const text = renderText(records, groups, { showGerrit: !!opts.gerrit })
616
- writeTextFile(filepath, text)
617
- writeTextFile(
618
- outputFilePath('author-changes.txt', outDir),
619
- renderChangedLinesText(records)
620
- )
80
+ // # 命令: Analyze 核心分析(默认) 分析 git 提交记录 (最全参数)
81
+ const analyzeCmd = program
82
+ .command('analyze')
83
+ .description(t('cmds.analyze'))
84
+ .action((cmdOpts) => {
85
+ const globalOpts = program.opts()
86
+ const finalOpts = { ...globalOpts, ...cmdOpts }
87
+ analyzeAction(finalOpts)
88
+ })
621
89
 
622
- // console.log('\n Commits List:\n', text, '\n')
90
+ // 挂载 analyze 需要的参数组
91
+ addGitSourceOptions(analyzeCmd)
92
+ addAnalysisOptions(analyzeCmd)
93
+ addOutputOptions(analyzeCmd)
94
+ addPerformanceOptions(analyzeCmd)
95
+
96
+ // # 命令: Overtime 加班文化分析
97
+ const overtimeCmd = program
98
+ .command('overtime')
99
+ .description(t('cmds.overtime'))
100
+ .action((cmdOpts) => {
101
+ const globalOpts = program.opts()
102
+ const finalOpts = { ...globalOpts, ...cmdOpts }
103
+ overtimeAction(finalOpts)
104
+ })
105
+ addGitSourceOptions(overtimeCmd)
106
+ addAnalysisOptions(overtimeCmd) // 加班分析肯定需要上班时间配置
107
+
108
+ // # 命令: Export (专注导出) 导出(excel / csv / json)
109
+ const exportCmd = program
110
+ .command('export')
111
+ .description(t('cmds.export'))
112
+ // .option('-f, --format <type>', '导出格式') // 局部参数
113
+ .action((cmdOpts, command) => {
114
+ // globalOpts 拿到 author, since 等
115
+ const globalOpts = command.parent.opts()
116
+ // 合并全局和局部参数
117
+ const finalOpts = { ...globalOpts, ...cmdOpts }
118
+ exportAction(finalOpts)
119
+ })
120
+ addGitSourceOptions(exportCmd)
121
+ addOutputOptions(exportCmd)
122
+
123
+ // === 命令: Journal (日报) ===
124
+ const journalCmd = program
125
+ .command('journal')
126
+ .description(t('cmds.journal'))
127
+ .action((cmdOpts) => {
128
+ const globalOpts = program.opts()
129
+ const finalOpts = { ...globalOpts, ...cmdOpts }
130
+ journalAction(finalOpts)
131
+ })
623
132
 
624
- logDev(`文本已导出: ${filepath}`)
133
+ addGitSourceOptions(journalCmd)
625
134
 
626
- handleSuccess({ spinner })
135
+ addPerformanceOptions(journalCmd)
627
136
 
628
- return
629
- }
630
-
631
- if (opts.format === 'excel') {
632
- const excelFile = opts.out || 'commits.xlsx'
633
- const excelPath = outputFilePath(excelFile, outDir)
634
- const txtFile = excelFile.replace(/\.xlsx$/, '.txt')
635
- const txtPath = outputFilePath(txtFile, outDir)
636
- await exportExcel(records, groups, {
637
- file: excelPath,
638
- stats: opts.stats,
639
- gerrit: opts.gerrit
137
+ // === 命令: Serve (Web服务) ===
138
+ const serveCmd = program
139
+ .command('serve')
140
+ .description('Start web server')
141
+ .action((cmdOpts) => {
142
+ const globalOpts = program.opts()
143
+ const finalOpts = { ...globalOpts, ...cmdOpts }
144
+ serveAction(finalOpts)
640
145
  })
641
- await exportExcelAuthorChangeStats(
642
- records,
643
- outputFilePath('author_stats.xlsx', outDir)
644
- )
645
- const text = renderText(records, groups)
646
- writeTextFile(txtPath, text)
647
- logDev(`Excel 已导出: ${excelPath}`)
648
- logDev(`文本已自动导出: ${txtPath}`)
649
-
650
- handleSuccess({ spinner })
651
- }
146
+ // Serve 命令只需要端口,不需要 Git 作者之类的参数
147
+ addServeOptions(serveCmd)
148
+ program.parse(process.argv)
652
149
 
653
- await autoCheckUpdate()
654
-
655
- handleSuccess({ spinner })
150
+ // const opts = program.opts()
151
+ // console.log('✅ Cli Opts:', opts)
656
152
  }
657
153
 
658
154
  try {
@@ -661,18 +157,5 @@ try {
661
157
  console.error(err)
662
158
  process.exitCode = 1
663
159
  } finally {
664
- if (profiler) {
665
- const result = profiler.end('git-commits')
666
-
667
- // --profile 时输出 JSON
668
- if (process.argv.includes('--profile')) {
669
- const json = {
670
- command: 'git-commits',
671
- version: VERSION,
672
- timestamp: Date.now(),
673
- profile: result
674
- }
675
- console.log(JSON.stringify(json, null, 2))
676
- }
677
- }
160
+ /* empty */
678
161
  }