wukong-gitlog-cli 1.0.39 → 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 (125) hide show
  1. package/.eslintrc +1 -0
  2. package/.prettierrc +2 -1
  3. package/CHANGELOG.md +95 -0
  4. package/README.md +93 -173
  5. package/README.zh-CN.md +85 -137
  6. package/bin/wukong-gitlog-cli +0 -0
  7. package/doc//347/233/256/345/275/225/347/273/223/346/236/204.md +2871 -0
  8. package/package.json +32 -29
  9. package/rc/.wukonggitlogrc +53 -0
  10. package/scripts/compareHourlyCounts.mjs +42 -0
  11. package/scripts/compareLatest.mjs +106 -0
  12. package/src/app/analyzeAction.mjs +120 -0
  13. package/src/app/exportAction.mjs +215 -0
  14. package/src/app/exportActionProgress.mjs +37 -0
  15. package/src/app/helpers.mjs +292 -0
  16. package/src/app/initAction.mjs +110 -0
  17. package/src/app/initActionWithTemp.mjs +192 -0
  18. package/src/app/journalAction.mjs +117 -0
  19. package/src/app/overtimeAction.mjs +100 -0
  20. package/src/app/runProfileEnd.mjs +0 -0
  21. package/src/app/serveAction.mjs +73 -0
  22. package/src/app/versionAction.mjs +7 -0
  23. package/src/cli/defineOptions.mjs +209 -0
  24. package/src/cli/index.mjs +0 -0
  25. package/src/cli/parseOptions.mjs +126 -8
  26. package/src/constants/index.mjs +16 -2
  27. package/src/domain/author/analyze.mjs +6 -0
  28. package/src/domain/author/map.mjs +0 -0
  29. package/src/domain/export/exportAuthor.mjs +28 -0
  30. package/src/domain/export/exportAuthorChanges.mjs +27 -0
  31. package/src/domain/export/exportAuthorChangesJson.mjs +31 -0
  32. package/src/domain/export/exportByMonth.mjs +157 -0
  33. package/src/domain/export/exportByWeek.mjs +121 -0
  34. package/src/domain/export/exportCommits.mjs +26 -0
  35. package/src/domain/export/exportCommitsExcel.mjs +45 -0
  36. package/src/domain/export/exportCommitsJson.mjs +31 -0
  37. package/src/domain/export/index.mjs +91 -0
  38. package/src/domain/git/ensureGitAvailable.mjs +66 -0
  39. package/src/domain/git/ensureGitRepo.mjs +41 -0
  40. package/src/domain/git/getGitFeatures.mjs +59 -0
  41. package/src/domain/git/getGitLogs.mjs +326 -0
  42. package/src/domain/git/getGitUser.mjs +44 -0
  43. package/src/domain/git/getRepoRoot.mjs +32 -0
  44. package/src/domain/git/gitCapability.mjs +119 -0
  45. package/src/domain/git/index.mjs +96 -0
  46. package/src/domain/git/resolveGerrit.mjs +102 -0
  47. package/src/domain/overtime/analyze.mjs +48 -0
  48. package/src/domain/overtime/index.mjs +3 -0
  49. package/src/domain/overtime/perPeriod.mjs +15 -0
  50. package/src/domain/overtime/render.mjs +15 -0
  51. package/src/i18n/index.mjs +38 -0
  52. package/src/i18n/resources.mjs +252 -0
  53. package/src/index.mjs +132 -649
  54. package/src/infra/cache.mjs +0 -0
  55. package/src/infra/configStore.mjs +128 -0
  56. package/src/infra/fs.mjs +0 -0
  57. package/src/infra/path.mjs +0 -0
  58. package/src/output/csv/overtime.mjs +12 -0
  59. package/src/output/csv.mjs +0 -0
  60. package/src/output/data/readData.mjs +54 -0
  61. package/src/output/data/writeData.mjs +145 -0
  62. package/src/output/excel/commits.mjs +9 -0
  63. package/src/output/excel/outputExcelDayReport.mjs +92 -0
  64. package/src/output/excel/perPeriod.mjs +24 -0
  65. package/src/{excel.mjs → output/excel.mjs} +3 -2
  66. package/src/output/index.mjs +79 -0
  67. package/src/output/json/overtime.mjs +9 -0
  68. package/src/output/tab/overtime.mjs +12 -0
  69. package/src/output/tab.mjs +0 -0
  70. package/src/output/text/commits.mjs +9 -0
  71. package/src/output/text/index.mjs +3 -0
  72. package/src/output/text/outputTxtDayReport.mjs +74 -0
  73. package/src/output/text/overtime.mjs +18 -0
  74. package/src/output/utils/getEsmJs.mjs +10 -0
  75. package/src/output/utils/index.mjs +14 -0
  76. package/src/output/utils/outputPath.mjs +19 -0
  77. package/src/output/utils/writeFile.mjs +10 -0
  78. package/src/serve/index.mjs +0 -0
  79. package/src/{server.mjs → serve/startServer.mjs} +21 -3
  80. package/src/serve/writeData.mjs +0 -0
  81. package/src/utils/authorNormalizer.mjs +28 -2
  82. package/src/utils/buildAuthorChangeStats.mjs +44 -0
  83. package/src/utils/deepMerge.mjs +13 -0
  84. package/src/utils/getPackage.mjs +11 -0
  85. package/src/utils/getProfileDirFile.mjs +12 -0
  86. package/src/utils/{file.mjs → groupRecords.mjs} +8 -9
  87. package/src/utils/index.mjs +5 -2
  88. package/src/utils/logger.mjs +28 -17
  89. package/src/utils/profiler.mjs +0 -101
  90. package/src/utils/resolve.mjs +11 -0
  91. package/src/utils/showVersionInfo.mjs +6 -2
  92. package/src/utils/time.mjs +0 -0
  93. package/src/utils/wait.mjs +2 -0
  94. package/web/app.js +3197 -257
  95. package/web/index.html +171 -22
  96. package/web/revoke/alpha1/app.js +4324 -0
  97. package/web/revoke/alpha1/index.html +266 -0
  98. package/web/revoke/app.before.js +3139 -0
  99. package/web/revoke/index-before.html +181 -0
  100. package/web/static/style.css +116 -9
  101. package/src/git.mjs +0 -256
  102. package/src/handlers/handleServe.mjs +0 -203
  103. package/src/lib/configStore.mjs +0 -11
  104. package/src/lib/memoize.mjs +0 -14
  105. package/src/utils/analyzeOvertimeCached.mjs +0 -7
  106. package/src/utils/checkUpdate.mjs +0 -130
  107. package/src/utils/exitWithTime.mjs +0 -17
  108. package/src/utils/handleSuccess.mjs +0 -9
  109. package/src/utils/logDev.mjs +0 -19
  110. package/src/utils/output.mjs +0 -26
  111. package/src/utils/profiler/diff.mjs +0 -26
  112. package/src/utils/profiler/format.mjs +0 -11
  113. package/src/utils/profiler/index.mjs +0 -144
  114. package/src/utils/profiler/trace.mjs +0 -26
  115. package/src/utils/time/scopeTimer.mjs +0 -37
  116. package/src/utils/time/timer.mjs +0 -33
  117. package/src/utils/time/withTimer.mjs +0 -11
  118. package/src/utils/timer.mjs +0 -35
  119. /package/src/{overtime → domain/overtime}/createOvertimeStats.mjs +0 -0
  120. /package/src/{overtime → domain/overtime}/overtime.mjs +0 -0
  121. /package/src/{json.mjs → output/json.mjs} +0 -0
  122. /package/src/{renderAuthorMapText.mjs → output/renderAuthorMapText.mjs} +0 -0
  123. /package/src/{stats-text.mjs → output/stats-text.mjs} +0 -0
  124. /package/src/{stats.mjs → output/stats.mjs} +0 -0
  125. /package/src/{text.mjs → output/text.mjs} +0 -0
@@ -0,0 +1,292 @@
1
+ import dayjs from 'dayjs'
2
+
3
+
4
+ import { getWorkOvertimeStats } from '../domain/overtime/analyze.mjs'
5
+ import { getWeekRange } from '../utils/getWeekRange.mjs'
6
+ import { groupRecords } from '../utils/groupRecords.mjs'
7
+
8
+ /**
9
+ * 处理单条 commit message
10
+ * 如果冒号前是 feat-* 或 fix-*,则去掉前缀
11
+ */
12
+ function normalizeCommitMsg(message) {
13
+ // 匹配:feat-xxx: 或 fix-xxx:
14
+ const match = message.match(/^(feat|fix)-[^:]+:(.+)$/i)
15
+
16
+ if (match) {
17
+ return match[2].trim()
18
+ }
19
+
20
+ return message
21
+ }
22
+
23
+
24
+ export const getWorkTimeConfig = (opts) => {
25
+ // startHour = 9, endHour = 18, lunchStart = 12, lunchEnd = 14, country = 'CN'
26
+ return {
27
+ startHour: opts.worktime.start,
28
+ endHour: opts.worktime.end,
29
+ lunchStart: opts.worktime.lunch.start,
30
+ lunchEnd: opts.worktime.lunch.end,
31
+ country: opts.worktime.country,
32
+ overnightCutoff: opts.worktime.overnightCutoff
33
+ }
34
+ }
35
+
36
+ export const getOvertimeByWeek = (commits) => {
37
+ // 新增:每周趋势数据(用于前端图表)
38
+ const weekGroups = groupRecords(commits, 'week')
39
+ const weekKeys = Object.keys(weekGroups).sort()
40
+ const weeklySeries = weekKeys.map((k) => {
41
+ const s = getWorkOvertimeStats(weekGroups[k])
42
+ return {
43
+ period: k,
44
+ range: getWeekRange(k),
45
+ total: s.total,
46
+ outsideWorkCount: s.outsideWorkCount,
47
+ outsideWorkRate: s.outsideWorkRate,
48
+ nonWorkdayCount: s.nonWorkdayCount,
49
+ nonWorkdayRate: s.nonWorkdayRate
50
+ }
51
+ })
52
+ return weeklySeries
53
+ }
54
+
55
+ export const getOvertimeByMonth = (commits) => {
56
+ // 新增:每月趋势数据(用于前端图表)
57
+ const monthGroups = groupRecords(commits, 'month')
58
+ const monthKeys = Object.keys(monthGroups).sort()
59
+ const monthlySeries = monthKeys.map((k) => {
60
+ const s = getWorkOvertimeStats(monthGroups[k])
61
+ return {
62
+ period: k,
63
+ total: s.total,
64
+ outsideWorkCount: s.outsideWorkCount,
65
+ outsideWorkRate: s.outsideWorkRate,
66
+ nonWorkdayCount: s.nonWorkdayCount,
67
+ nonWorkdayRate: s.nonWorkdayRate
68
+ }
69
+ })
70
+ return monthlySeries
71
+ }
72
+
73
+ // 每日最晚提交小时(用于显著展示加班严重程度)
74
+ export const getLatestCommitByDay = ({ commits, opts }) => {
75
+ // 新增:每日最晚提交小时(用于显著展示加班严重程度)
76
+ const dayGroups2 = groupRecords(commits, 'day')
77
+ const dayKeys2 = Object.keys(dayGroups2).sort()
78
+
79
+ // 次日凌晨归并窗口(默认 6 点前仍算前一天的加班)
80
+ const overnightCutoff = Number.isFinite(opts.overnightCutoff)
81
+ ? opts.overnightCutoff
82
+ : 6
83
+ // 次日上班时间(默认按 workStart,若未指定则 9 点)
84
+ const workStartHour =
85
+ opts.workStart || opts.workStart === 0 ? opts.workStart : 9
86
+ const workEndHour = opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18
87
+
88
+ // 有些日期「本身没有 commit」,但第二天凌晨有提交要归并到这一天,
89
+ // 需要补出这些“虚拟日期”,否则 latestByDay 会漏掉这天。
90
+ const virtualPrevDays = new Set()
91
+ commits.forEach((r) => {
92
+ const d = new Date(r.date)
93
+ if (Number.isNaN(d.valueOf())) return
94
+ const h = d.getHours()
95
+ if (h < 0 || h >= overnightCutoff || h >= workStartHour) return
96
+ const curDay = dayjs(d).format('YYYY-MM-DD')
97
+ const prevDay = dayjs(curDay).subtract(1, 'day').format('YYYY-MM-DD')
98
+ if (!dayGroups2[prevDay]) {
99
+ virtualPrevDays.add(prevDay)
100
+ }
101
+ })
102
+
103
+ const allDayKeys = Array.from(
104
+ new Set([...dayKeys2, ...virtualPrevDays])
105
+ ).sort()
106
+
107
+ const latestByDay = allDayKeys.map((k) => {
108
+ const list = dayGroups2[k] || []
109
+
110
+ // 1) 当天「下班后」的提交:只统计 >= workEndHour 的小时
111
+ const sameDayHours = list
112
+ .map((r) => new Date(r.date))
113
+ .filter((d) => !Number.isNaN(d.valueOf()))
114
+ .map((d) => d.getHours())
115
+ .filter((h) => h >= workEndHour && h < 24)
116
+
117
+ // 2) 次日凌晨、但仍算前一日加班的提交
118
+ const nextKey = dayjs(k).add(1, 'day').format('YYYY-MM-DD')
119
+ const early = dayGroups2[nextKey] || []
120
+ const earlyHours = early
121
+ .map((r) => new Date(r.date))
122
+ .filter((d) => !Number.isNaN(d.valueOf()))
123
+ .map((d) => d.getHours())
124
+ // 只看 [0, overnightCutoff) 之间的小时,
125
+ // 并且默认认为 < workStartHour 属于「次日上班前」
126
+ .filter(
127
+ (h) =>
128
+ h >= 0 &&
129
+ h < overnightCutoff &&
130
+ // 保护性判断:若有人把 overnightCutoff 设得大于上班时间,
131
+ // 我们仍然只统计到上班时间为止
132
+ h < workStartHour
133
+ )
134
+
135
+ // 3) 计算「逻辑上的最晚加班时间」
136
+ // - 当天晚上的用原始小时(如 22 点)
137
+ // - 次日凌晨的用 24 + 小时(如 1 点 → 25)
138
+ const overtimeValues = [
139
+ ...sameDayHours.map((h) => h),
140
+ ...earlyHours.map((h) => 24 + h)
141
+ ]
142
+
143
+ if (overtimeValues.length === 0) {
144
+ // 这一天没有任何「下班后到次日上班前」的提交
145
+ return {
146
+ date: k,
147
+ latestHour: null,
148
+ latestHourNormalized: null
149
+ }
150
+ }
151
+
152
+ const latestHourNormalized = Math.max(...overtimeValues)
153
+
154
+ // latestHour 保留「当天自然日内」的最晚提交通常小时数,供前端需要时参考
155
+ const sameDayMax =
156
+ sameDayHours.length > 0 ? Math.max(...sameDayHours) : null
157
+
158
+ return {
159
+ date: k,
160
+ latestHour: sameDayMax,
161
+ latestHourNormalized
162
+ }
163
+ })
164
+ return latestByDay
165
+ }
166
+
167
+ /**
168
+ * @function getGitLogsDayReport
169
+ * @description 返回数据包含 git commit 的日期day (YYYY-MM-DD),msg(当天提交的所有合并到一个msg),author
170
+ * 用于 git log --all 的日报统计(Gerrit 友好)
171
+ *
172
+ * - 按 Change-Id 去重(同一 Change 只统计一次,取最新提交)
173
+ * - 按 day + author 聚合
174
+ * - msg:normalize 后 + 去重 + 合并
175
+ * - originalMsg:当天所有原始 commit message(不去重,便于排查)
176
+ * 返回数据包含:
177
+ * - day: YYYY-MM-DD
178
+ * - author
179
+ * - originalMsg: 当天所有原始 commit message
180
+ * - msg: 处理后的 message(去掉 feat-/fix- 冒号前缀)去重合并后的结果
181
+ * @param {Array} records
182
+ * @param {Object} opts
183
+ * @returns {Array<{ day: string, msg: string, author: string }>}
184
+ */
185
+ export const getGitLogsDayReport = async (records = [], opts = {}) => {
186
+ if (!Array.isArray(records) || records.length === 0) {
187
+ return []
188
+ }
189
+
190
+ const authorFilter = opts?.author
191
+ function toList(v) {
192
+ if (!v) return null
193
+ if (Array.isArray(v)) return v.map((s) => String(s).trim()).filter(Boolean)
194
+ return String(v)
195
+ .split(',')
196
+ .map((s) => s.trim())
197
+ .filter(Boolean)
198
+ }
199
+ const include = authorFilter && typeof authorFilter === 'object' ? toList(authorFilter.include) : (typeof authorFilter === 'string' ? toList(authorFilter) : null)
200
+ const exclude = authorFilter && typeof authorFilter === 'object' ? toList(authorFilter.exclude) : null
201
+
202
+ /* ---------------- 1️⃣ Change-Id 级别去重(取最新) ---------------- */
203
+
204
+ const changeMap = new Map()
205
+
206
+ for (const item of records) {
207
+ if (!item?.date || !item?.message) continue
208
+
209
+ const key = item.changeId || item.hash
210
+ const time = dayjs(item.date).valueOf()
211
+
212
+ const prev = changeMap.get(key)
213
+ if (!prev || time > dayjs(prev.date).valueOf()) {
214
+ changeMap.set(key, item)
215
+ }
216
+ }
217
+
218
+ const dedupedRecords = Array.from(changeMap.values())
219
+
220
+ /* ---------------- 2️⃣ Day + Author 聚合 ---------------- */
221
+
222
+ const map = {}
223
+
224
+ for (const item of dedupedRecords) {
225
+ const author =
226
+ item.author ||
227
+ item.originalAuthor ||
228
+ item.email ||
229
+ 'unknown'
230
+
231
+ // --author 过滤:支持字符串/数组 或 { include: [], exclude: [] }
232
+ if ((include && include.length) || (exclude && exclude.length)) {
233
+ const name = (item.author || item.originalAuthor || '').trim().toLowerCase()
234
+ const mail = (item.email || '').trim().toLowerCase()
235
+ const matches = (list) =>
236
+ list.some((it) => {
237
+ const v = String(it).trim().toLowerCase()
238
+ if (v.includes('@')) return v === mail
239
+ return v === name
240
+ })
241
+
242
+ if (include && include.length) {
243
+ if (!matches(include)) continue
244
+ }
245
+ if (exclude && exclude.length) {
246
+ if (matches(exclude)) continue
247
+ }
248
+ } else if (authorFilter && typeof authorFilter === 'string') {
249
+ // legacy: simple string match
250
+ if (author !== authorFilter) continue
251
+ }
252
+
253
+ const day = dayjs(item.date).format('YYYY-MM-DD')
254
+ const key = `${day}__${author}`
255
+
256
+ if (!map[key]) {
257
+ map[key] = {
258
+ day,
259
+ author,
260
+ originalMsgs: [],
261
+ msgSet: new Set()
262
+ }
263
+ }
264
+
265
+ const originalMsg = item.message.trim()
266
+ const handledMsg = normalizeCommitMsg(originalMsg)
267
+
268
+ map[key].originalMsgs.push(originalMsg)
269
+ map[key].msgSet.add(handledMsg)
270
+ }
271
+
272
+ /*
273
+ wukong-gitlog-cli journal --since 2026-02-01 可以得到2026-02-02日数据
274
+ wukong-gitlog-cli journal --since 2026-02-02 不能得到2026-02-02日数据
275
+ */
276
+
277
+ /* ---------------- 3️⃣ 输出 + 稳定排序 ---------------- */
278
+
279
+ return Object.values(map)
280
+ .map((item) => ({
281
+ day: item.day,
282
+ author: item.author,
283
+ originalMsg: item.originalMsgs.join('\n'),
284
+ msg: Array.from(item.msgSet).join('\n')
285
+ }))
286
+ .sort((a, b) => {
287
+ const dayDiff =
288
+ dayjs(b.day).valueOf() - dayjs(a.day).valueOf()
289
+ if (dayDiff !== 0) return dayDiff
290
+ return a.author.localeCompare(b.author, 'zh')
291
+ })
292
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * @file: initAction.mjs
3
+ * @description: 使用 @inquirer/prompts 初始化配置文件并维护 .gitignore
4
+ * @author: King Monkey
5
+ */
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import yaml from 'yaml';
9
+ import { select, confirm } from '@inquirer/prompts';
10
+ import { DEFAULT_CONFIG, RC_NAMES } from '../infra/configStore.mjs';
11
+ import { WUKONG_GITLOG_RC } from '#src/constants/index.mjs';
12
+
13
+
14
+ /**
15
+ * 自动将输出目录添加到 .gitignore
16
+ */
17
+ async function manageGitignore(outputDir) {
18
+ const gitignorePath = path.join(process.cwd(), '.gitignore');
19
+
20
+ if (!fs.existsSync(gitignorePath)) return;
21
+
22
+ try {
23
+ const content = fs.readFileSync(gitignorePath, 'utf8');
24
+
25
+ // 同时检查输出目录与常见配置文件名(从 configStore 导出),若全部存在则跳过
26
+ const configFiles = Array.isArray(RC_NAMES) ? RC_NAMES : [];
27
+
28
+ const hasOutput = content.includes(outputDir);
29
+ const hasAllConfigs = configFiles.length && configFiles.every((f) => content.includes(f));
30
+ if (hasOutput && hasAllConfigs) return;
31
+
32
+ const shouldAdd = await confirm({
33
+ message: `是否自动将报告目录 "${outputDir}/" 以及配置文件名添加到 .gitignore?`,
34
+ default: true
35
+ });
36
+
37
+ if (shouldAdd) {
38
+ const prefix = content.endsWith('\n') ? '' : '\n';
39
+ let entry = `${prefix}\n# Wukong GitLog Reports\n`;
40
+ if (!hasOutput) entry += `${outputDir}/\n`;
41
+
42
+ const missingConfigs = configFiles.filter((f) => !content.includes(f));
43
+ if (missingConfigs.length) {
44
+ entry += `\n# Wukong GitLog Config\n${ missingConfigs.map((f) => `${f}\n`).join('')}`;
45
+ }
46
+
47
+ fs.appendFileSync(gitignorePath, entry, 'utf8');
48
+ console.log(`✅ 已更新 .gitignore`);
49
+ }
50
+ } catch (err) {
51
+ if (err.name !== 'ExitPromptError') {
52
+ console.warn(`⚠️ 无法更新 .gitignore: ${err.message}`);
53
+ }
54
+ }
55
+ }
56
+
57
+
58
+ export async function initAction(options) {
59
+ console.log(`\n🚀 ${'Wukong GitLog'} 配置文件初始化\n`);
60
+
61
+ try {
62
+ const format = await select({
63
+ message: '请选择要生成的配置文件格式:',
64
+ choices: [
65
+ { name: 'ES Module (.mjs)', value: 'mjs' },
66
+ { name: 'JavaScript (.js)', value: 'js' },
67
+ { name: 'YAML (.yml)', value: 'yml' },
68
+ { name: 'JSON (.json)', value: 'json' },
69
+ { name: 'YAML 无后缀 (.wukonggitlogrc)', value: 'plain' }
70
+ ]
71
+ });
72
+
73
+ const fileName = format === 'plain' ? WUKONG_GITLOG_RC : `${WUKONG_GITLOG_RC}.${format}`;
74
+ const targetPath = path.join(process.cwd(), fileName);
75
+
76
+ if (fs.existsSync(targetPath) && !options.force) {
77
+ console.error(`\n❌ 错误: 当前目录已存在 ${fileName}`);
78
+ return;
79
+ }
80
+
81
+ let content = '';
82
+ const headerComment = `// Wukong GitLog Config\n// Generated at ${new Date().toLocaleString()}\n\n`;
83
+
84
+ switch (format) {
85
+ case 'mjs':
86
+ case 'js':
87
+ content = `${headerComment}export default ${JSON.stringify(DEFAULT_CONFIG, null, 2)};`;
88
+ break;
89
+ case 'yml':
90
+ case 'plain':
91
+ content = `# Wukong GitLog Config\n${yaml.stringify(DEFAULT_CONFIG)}`;
92
+ break;
93
+ case 'json':
94
+ content = JSON.stringify(DEFAULT_CONFIG, null, 2);
95
+ break;
96
+ default:
97
+ throw new Error('Unsupported format selected.');
98
+ }
99
+
100
+ fs.writeFileSync(targetPath, content, 'utf8');
101
+ console.log(`✅ 成功生成配置: ${fileName}`);
102
+
103
+ await manageGitignore(DEFAULT_CONFIG.output.dir);
104
+ console.log(`\n✨ 初始化完成!\n`);
105
+
106
+ } catch (err) {
107
+ if (err.name === 'ExitPromptError') console.log('\n👋 已取消');
108
+ else console.error(`\n❌ 失败: ${err.message}`);
109
+ }
110
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * @file: initActionWithTemp.mjs
3
+ * @description: 使用 @inquirer/prompts 初始化带详细注释的配置文件
4
+ * @author: King Monkey
5
+ */
6
+ import { confirm, select } from '@inquirer/prompts'
7
+ import chalk from 'chalk'
8
+ import fs from 'fs'
9
+ import path from 'path'
10
+
11
+ import { t } from '../i18n/index.mjs'
12
+ import { DEFAULT_CONFIG, RC_NAMES } from '../infra/configStore.mjs'
13
+
14
+ // 动态生成 YAML 模板
15
+ const getYamlTemplate =
16
+ () => `# ---------------------------------------------------------
17
+ # Wukong GitLog Config (.wukonggitlogrc.yml)
18
+ # ${t('template.generated_at')}: ${new Date().toLocaleString()}
19
+ # ---------------------------------------------------------
20
+
21
+ # ${t('template.author_config')}
22
+ author:
23
+ include: [] # ${t('template.author_include')}
24
+ exclude: [] # ${t('template.author_exclude')}
25
+
26
+ # ${t('template.git_config')}
27
+ git:
28
+ merges: true # ${t('template.git_merges')}
29
+ limit: 5000 # ${t('template.git_limit')}
30
+
31
+ # ${t('template.period_config')}
32
+ period:
33
+ groupBy: month # ${t('template.period_group')}
34
+ since: "" # ${t('template.period_since')}
35
+ until: "" # ${t('template.period_until')}
36
+
37
+ # ${t('template.gerrit_config')}
38
+ gerrit:
39
+ prefix: "" # Example: https://gerrit.xxx.com/c/{{changeNumber}}
40
+ api: "" # Gerrit API URL
41
+ auth: "" # "user:pass" or "TOKEN"
42
+
43
+ # ${t('template.worktime_config')}
44
+ worktime:
45
+ country: CN # ${t('template.worktime_country')}
46
+ start: 9 # ${t('template.worktime_start')} (0-23)
47
+ end: 18 # ${t('template.worktime_end')} (0-23)
48
+ lunch:
49
+ start: 12 # ${t('template.worktime_lunch')} start
50
+ end: 14 # ${t('template.worktime_lunch')} end
51
+ overnightCutoff: 6 # ${t('template.worktime_cutoff')}
52
+
53
+ # ${t('template.output_config')}
54
+ output:
55
+ dir: "output-wukong" # ${t('template.output_dir')}
56
+ formats: ["text", "excel"] # ${t('template.output_formats')}
57
+ perPeriod:
58
+ enabled: true # ${t('template.output_per_period')}
59
+ excelMode: "sheets" # sheets | files
60
+
61
+ # ${t('template.author_aliases')}
62
+ authorAliases: {}
63
+ `
64
+
65
+ // 动态生成 JS 模板
66
+ const getJsTemplate = () => `/**
67
+ * Wukong GitLog Config (.wukonggitlogrc.js)
68
+ * ${t('template.generated_at')}: ${new Date().toLocaleString()}
69
+ */
70
+ export default {
71
+ // ${t('template.author_config')}
72
+ author: {
73
+ include: [], // ${t('template.author_include')}
74
+ exclude: [] // ${t('template.author_exclude')}
75
+ },
76
+
77
+ // ${t('template.git_config')}
78
+ git: {
79
+ merges: true,
80
+ limit: 5000
81
+ },
82
+
83
+ // ${t('template.worktime_config')}
84
+ worktime: {
85
+ country: 'CN',
86
+ start: 9,
87
+ end: 18,
88
+ lunch: { start: 12, end: 14 },
89
+ overnightCutoff: 6
90
+ },
91
+
92
+ // ${t('template.author_aliases')}
93
+ authorAliases: {},
94
+
95
+ // ${t('template.output_config')}
96
+ output: {
97
+ dir: 'output-wukong',
98
+ formats: ['text', 'excel'],
99
+ perPeriod: { enabled: true, excelMode: 'sheets' }
100
+ }
101
+ };
102
+ `
103
+ async function manageGitignore(outputDir) {
104
+ const gitignorePath = path.join(process.cwd(), '.gitignore')
105
+ if (!fs.existsSync(gitignorePath)) return
106
+
107
+ try {
108
+ const content = fs.readFileSync(gitignorePath, 'utf8')
109
+
110
+ // 使用从 configStore 导出的 RC_NAMES
111
+ const configFiles = Array.isArray(RC_NAMES) ? RC_NAMES : []
112
+
113
+ const hasOutput = content.includes(outputDir)
114
+ const hasAllConfigs =
115
+ configFiles.length && configFiles.every((f) => content.includes(f))
116
+ if (hasOutput && hasAllConfigs) return
117
+
118
+ const shouldAdd = await confirm({
119
+ message: t('init.gitignore_ask'), // `是否自动将报告目录 "${outputDir}/" 以及配置文件名添加到 .gitignore?`,
120
+ default: true
121
+ })
122
+
123
+ if (shouldAdd) {
124
+ const prefix = content.endsWith('\n') ? '' : '\n'
125
+ let entry = `${prefix}\n# Wukong GitLog Reports\n`
126
+ if (!hasOutput) entry += `${outputDir}/\n`
127
+
128
+ const missingConfigs = configFiles.filter((f) => !content.includes(f))
129
+ if (missingConfigs.length) {
130
+ entry += `\n# Wukong GitLog Config\n${missingConfigs.map((f) => `${f}\n`).join('')}`
131
+ }
132
+
133
+ fs.appendFileSync(gitignorePath, entry, 'utf8')
134
+ console.log(`✅ ${t('init.gitignore_updated')}`)
135
+ }
136
+ } catch (err) {
137
+ if (err.name !== 'ExitPromptError') {
138
+ console.warn(`⚠️ ${t('init.gitignore_warn')} ${err.message}`)
139
+ }
140
+ }
141
+ }
142
+
143
+ export async function initActionWithTemp(options) {
144
+ console.log(`\n🚀 Wukong GitLog ${t('init.title')}\n`)
145
+
146
+ try {
147
+ const format = await select({
148
+ message: t('init.select_format'),
149
+ choices: [
150
+ { name: t('init.formats.mjs'), value: 'mjs' },
151
+ { name: t('init.formats.js'), value: 'js' },
152
+ { name: t('init.formats.yaml'), value: 'yaml' },
153
+ { name: t('init.formats.json'), value: 'json' },
154
+ { name: t('init.formats.plain'), value: 'plain' }
155
+ ]
156
+ })
157
+
158
+ const fileNameMap = {
159
+ mjs: '.wukonggitlogrc.mjs',
160
+ js: '.wukonggitlogrc.js',
161
+ yaml: '.wukonggitlogrc.yml',
162
+ json: '.wukonggitlogrc.json',
163
+ plain: '.wukonggitlogrc'
164
+ }
165
+
166
+ const fileName = fileNameMap[format]
167
+ const targetPath = path.join(process.cwd(), fileName)
168
+
169
+ if (fs.existsSync(targetPath) && !options.force) {
170
+ console.error(`\n❌ ${t('init.error_exists')} (${fileName})`)
171
+ return
172
+ }
173
+
174
+ let content = ''
175
+ if (format === 'js' || format === 'mjs') content = getJsTemplate()
176
+ else if (format === 'yaml' || format === 'plain')
177
+ content = getYamlTemplate()
178
+ else content = JSON.stringify(DEFAULT_CONFIG, null, 2)
179
+
180
+ fs.writeFileSync(targetPath, content, 'utf8')
181
+ console.log(`✅ ${t('init.success_created')} ${chalk.green(fileName)}`)
182
+
183
+ await manageGitignore(DEFAULT_CONFIG.output.dir)
184
+ console.log(`\n✨ ${t('init.complete')}\n`)
185
+ } catch (err) {
186
+ if (err.name === 'ExitPromptError') {
187
+ console.log(`\n👋 ${t('init.cancel')}`)
188
+ } else {
189
+ console.error(`\n❌ ${t('init.fail')} ${err.message}`)
190
+ }
191
+ }
192
+ }