wukong-gitlog-cli 0.0.14 → 1.0.1

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [1.0.1](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v0.0.15...v1.0.1) (2025-11-28)
6
+
7
+
8
+ ### Features
9
+
10
+ * 🎸 show version ([4a81b72](https://github.com/tomatobybike/wukong-gitlog-cli/commit/4a81b7222100521dcacda4d1d8ff96db2d669a3c))
11
+ * 🎸 version ([8fa81d6](https://github.com/tomatobybike/wukong-gitlog-cli/commit/8fa81d64e1a6c44e4b4327fbb6a27559ed2b5372))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * 🐛 eslint ([17701b2](https://github.com/tomatobybike/wukong-gitlog-cli/commit/17701b248802532a63d5b5896cf331de29e07c07))
17
+
18
+ ### [0.0.15](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v0.0.14...v0.0.15) (2025-11-28)
19
+
20
+
21
+ ### Features
22
+
23
+ * 🎸 chart pie ([3f56a70](https://github.com/tomatobybike/wukong-gitlog-cli/commit/3f56a70d0d4fb9974a6e1a918d936ea9bade366b))
24
+
5
25
  ### [0.0.14](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v0.0.13...v0.0.14) (2025-11-28)
6
26
 
7
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wukong-gitlog-cli",
3
- "version": "0.0.14",
3
+ "version": "1.0.1",
4
4
  "description": "Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.",
5
5
  "keywords": [
6
6
  "git",
@@ -90,13 +90,15 @@
90
90
  ]
91
91
  },
92
92
  "dependencies": {
93
- "chalk": "5.6.0",
94
93
  "commander": "12.1.0",
95
94
  "dayjs": "1.11.19",
96
95
  "date-holidays": "2.1.1",
97
96
  "string-width": "5.1.2",
98
97
  "exceljs": "4.4.0",
99
- "zx": "7.2.4"
98
+ "zx": "7.2.4",
99
+ "is-online": "12.0.1",
100
+ "boxen": "8.0.1",
101
+ "chalk": "5.6.2"
100
102
  },
101
103
  "devDependencies": {
102
104
  "@trivago/prettier-plugin-sort-imports": "5.2.2",
package/src/cli.mjs CHANGED
@@ -1,14 +1,58 @@
1
- import { Command } from 'commander';
2
- import chalk from 'chalk';
3
- import path from 'path';
4
- import { getGitLogs } from './git.mjs';
5
- import { renderText } from './text.mjs';
6
- import { analyzeOvertime, renderOvertimeText, renderOvertimeTab, renderOvertimeCsv } from './overtime.mjs';
7
- import { startServer } from './server.mjs';
8
- import { exportExcel, exportExcelPerPeriodSheets } from './excel.mjs';
9
- import { groupRecords, writeJSON, writeTextFile, outputFilePath } from './utils/index.mjs';
1
+ import chalk from 'chalk'
2
+ import { Command } from 'commander'
3
+ import fs from 'fs'
4
+ import path from 'path'
5
+ import { fileURLToPath } from 'url'
10
6
 
11
- const program = new Command();
7
+
8
+ import { CLI_NAME } from './constants/index.mjs'
9
+ import { exportExcel, exportExcelPerPeriodSheets } from './excel.mjs'
10
+ import { getGitLogs } from './git.mjs'
11
+ import {
12
+ analyzeOvertime,
13
+ renderOvertimeCsv,
14
+ renderOvertimeTab,
15
+ renderOvertimeText
16
+ } from './overtime.mjs'
17
+ import { startServer } from './server.mjs'
18
+ import { renderText } from './text.mjs'
19
+ import { checkUpdateWithPatch } from './utils/checkUpdate.mjs'
20
+ import {
21
+ groupRecords,
22
+ outputFilePath,
23
+ writeJSON,
24
+ writeTextFile
25
+ } from './utils/index.mjs'
26
+ import { showVersionInfo } from './utils/showVersionInfo.mjs'
27
+
28
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
29
+
30
+
31
+ const pkg = JSON.parse(
32
+ fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf8')
33
+ )
34
+
35
+ const PKG_NAME = pkg.name
36
+ const VERSION = pkg.version
37
+
38
+ const autoCheckUpdate = async () => {
39
+ // === CLI 主逻辑完成后提示更新 ===
40
+ await checkUpdateWithPatch({
41
+ pkg: {
42
+ name: PKG_NAME,
43
+ version: VERSION
44
+ },
45
+ force: true
46
+ })
47
+ }
48
+
49
+ const version = async () => {
50
+ showVersionInfo(VERSION)
51
+ await autoCheckUpdate()
52
+ process.exit(0)
53
+ }
54
+
55
+ const program = new Command()
12
56
 
13
57
  program
14
58
  .name('git-commits')
@@ -23,383 +67,558 @@ program
23
67
  .option('--format <type>', '输出格式: text | excel | json', 'text')
24
68
  .option('--group-by <type>', '按日期分组: day | month | week')
25
69
  .option('--stats', '输出每日统计数据')
26
- .option('--gerrit <prefix>', '显示 Gerrit 地址,支持在 prefix 中使用 {{hash}} 占位符')
27
- .option('--gerrit-api <url>', '可选:Gerrit REST API 基础地址,用于解析 changeNumber,例如 `https://gerrit.example.com`')
28
- .option('--gerrit-auth <tokenOrUserPass>', '可选:Gerrit API 授权,格式为 `user:pass` `TOKEN`(表示 Bearer token)')
70
+ .option(
71
+ '--gerrit <prefix>',
72
+ '显示 Gerrit 地址,支持在 prefix 中使用 {{hash}} 占位符'
73
+ )
74
+ .option(
75
+ '--gerrit-api <url>',
76
+ '可选:Gerrit REST API 基础地址,用于解析 changeNumber,例如 `https://gerrit.example.com`'
77
+ )
78
+ .option(
79
+ '--gerrit-auth <tokenOrUserPass>',
80
+ '可选:Gerrit API 授权,格式为 `user:pass` 或 `TOKEN`(表示 Bearer token)'
81
+ )
29
82
  .option('--overtime', '分析公司加班文化(输出下班时间与非工作日提交占比)')
30
83
  .option('--country <code>', '节假日国家:CN 或 US,默认为 CN', 'CN')
31
- .option('--work-start <hour>', '上班开始小时,默认 9', (v) => parseInt(v, 10), 9)
84
+ .option(
85
+ '--work-start <hour>',
86
+ '上班开始小时,默认 9',
87
+ (v) => parseInt(v, 10),
88
+ 9
89
+ )
32
90
  .option('--work-end <hour>', '下班小时,默认 18', (v) => parseInt(v, 10), 18)
33
- .option('--lunch-start <hour>', '午休开始小时,默认 12', (v) => parseInt(v, 10), 12)
34
- .option('--lunch-end <hour>', '午休结束小时,默认 14', (v) => parseInt(v, 10), 14)
91
+ .option(
92
+ '--lunch-start <hour>',
93
+ '午休开始小时,默认 12',
94
+ (v) => parseInt(v, 10),
95
+ 12
96
+ )
97
+ .option(
98
+ '--lunch-end <hour>',
99
+ '午休结束小时,默认 14',
100
+ (v) => parseInt(v, 10),
101
+ 14
102
+ )
35
103
  .option('--out <file>', '输出文件名(不含路径)')
36
- .option('--out-dir <dir>', '自定义输出目录,支持相对路径或绝对路径,例如 `--out-dir ../output`')
37
- .option('--out-parent', '将输出目录放到当前工程的父目录的 `output/`(等同于 `--out-dir ../output`)')
38
- .option('--per-period-formats <formats>', '每个周期单独输出的格式,逗号分隔:text,csv,tab,xlsx。默认为空(不输出 CSV/Tab/XLSX)', '')
39
- .option('--per-period-excel-mode <mode>', 'per-period Excel 模式:sheets|files(默认:sheets)', 'sheets')
40
- .option('--per-period-only', '仅输出 per-period(month/week)文件,不输出合并的 monthly/weekly 汇总文件')
41
- .option('--serve', '启动本地 web 服务,查看提交统计(将在 output/data 下生成数据文件)')
42
- .option('--port <n>', '本地 web 服务端口(默认 3000)', (v) => parseInt(v, 10), 3000)
43
- .option('--serve-only', '仅启动 web 服务,不导出或分析数据(使用 output/data 中已有的数据)')
44
- .parse();
104
+ .option(
105
+ '--out-dir <dir>',
106
+ '自定义输出目录,支持相对路径或绝对路径,例如 `--out-dir ../output`'
107
+ )
108
+ .option(
109
+ '--out-parent',
110
+ '将输出目录放到当前工程的父目录的 `output/`(等同于 `--out-dir ../output`)'
111
+ )
112
+ .option(
113
+ '--per-period-formats <formats>',
114
+ '每个周期单独输出的格式,逗号分隔:text,csv,tab,xlsx。默认为空(不输出 CSV/Tab/XLSX)',
115
+ ''
116
+ )
117
+ .option(
118
+ '--per-period-excel-mode <mode>',
119
+ 'per-period Excel 模式:sheets|files(默认:sheets)',
120
+ 'sheets'
121
+ )
122
+ .option(
123
+ '--per-period-only',
124
+ '仅输出 per-period(month/week)文件,不输出合并的 monthly/weekly 汇总文件'
125
+ )
126
+ .option(
127
+ '--serve',
128
+ '启动本地 web 服务,查看提交统计(将在 output/data 下生成数据文件)'
129
+ )
130
+ .option(
131
+ '--port <n>',
132
+ '本地 web 服务端口(默认 3000)',
133
+ (v) => parseInt(v, 10),
134
+ 3000
135
+ )
136
+ .option(
137
+ '--serve-only',
138
+ '仅启动 web 服务,不导出或分析数据(使用 output/data 中已有的数据)'
139
+ )
140
+ .option('--version', 'show version information')
141
+ .parse()
45
142
 
46
- const opts = program.opts();
143
+ const opts = program.opts()
47
144
  // compute output directory root early (so serve-only can use it)
48
145
  const outDir = opts.outParent
49
146
  ? path.resolve(process.cwd(), '..', 'output')
50
- : opts.outDir || undefined;
147
+ : opts.outDir || undefined
51
148
 
52
- (async () => {
149
+ ;(async () => {
150
+ if (opts.version) {
151
+ await version()
152
+ return
153
+ }
154
+ await autoCheckUpdate()
53
155
  // if serve-only is requested, start server and exit
54
156
  if (opts.serveOnly) {
55
157
  try {
56
- await startServer(opts.port || 3000, outDir);
158
+ await startServer(opts.port || 3000, outDir)
57
159
  } catch (err) {
58
- console.warn('Start server failed:', err && err.message ? err.message : err);
59
- process.exit(1);
160
+ console.warn(
161
+ 'Start server failed:',
162
+ err && err.message ? err.message : err
163
+ )
164
+ process.exit(1)
60
165
  }
61
- return;
166
+ return
62
167
  }
63
168
 
64
- let records = await getGitLogs(opts);
169
+ let records = await getGitLogs(opts)
65
170
 
66
171
  // compute output directory root if user provided one or wants parent
67
172
 
68
173
  // --- Gerrit 地址处理(若提供) ---
69
174
  if (opts.gerrit) {
70
- const prefix = opts.gerrit;
175
+ const prefix = opts.gerrit
71
176
  // support optional changeNumber resolution via Gerrit REST API
72
- const { gerritApi, gerritAuth } = opts;
177
+ const { gerritApi, gerritAuth } = opts
73
178
  // create new array to avoid mutating function parameters (eslint: no-param-reassign)
74
179
  if (prefix.includes('{{changeNumber}}') && gerritApi) {
75
180
  // async mapping to resolve changeNumber using Gerrit API
76
- const cache = new Map();
77
- const headers = {};
181
+ const cache = new Map()
182
+ const headers = {}
78
183
  if (gerritAuth) {
79
184
  if (gerritAuth.includes(':')) {
80
- headers.Authorization = `Basic ${Buffer.from(gerritAuth).toString('base64')}`;
185
+ headers.Authorization = `Basic ${Buffer.from(gerritAuth).toString('base64')}`
81
186
  } else {
82
- headers.Authorization = `Bearer ${gerritAuth}`;
187
+ headers.Authorization = `Bearer ${gerritAuth}`
83
188
  }
84
189
  }
85
190
  const fetchGerritJson = async (url) => {
86
191
  try {
87
- const res = await fetch(url, { headers });
88
- const txt = await res.text();
192
+ const res = await fetch(url, { headers })
193
+ const txt = await res.text()
89
194
  // Gerrit prepends )]}' to JSON responses — strip it
90
- const jsonText = txt.replace(/^\)\]\}'\n/, '');
91
- return JSON.parse(jsonText);
195
+ const jsonText = txt.replace(/^\)\]\}'\n/, '')
196
+ return JSON.parse(jsonText)
92
197
  } catch (err) {
93
- return null;
198
+ return null
94
199
  }
95
- };
200
+ }
96
201
  const resolveChangeNumber = async (r) => {
97
202
  // try changeId first
98
203
  if (r.changeId) {
99
- if (cache.has(r.changeId)) return cache.get(r.changeId);
204
+ if (cache.has(r.changeId)) return cache.get(r.changeId)
100
205
  // try `changes/{changeId}/detail`
101
- const url = `${gerritApi.replace(/\/$/, '')}/changes/${encodeURIComponent(r.changeId)}/detail`;
102
- let j = await fetchGerritJson(url);
206
+ const url = `${gerritApi.replace(/\/$/, '')}/changes/${encodeURIComponent(r.changeId)}/detail`
207
+ let j = await fetchGerritJson(url)
103
208
  if (j && j._number) {
104
- cache.set(r.changeId, j._number);
105
- return j._number;
209
+ cache.set(r.changeId, j._number)
210
+ return j._number
106
211
  }
107
212
  // fallback: query search
108
- const url2 = `${gerritApi.replace(/\/$/, '')}/changes/?q=change:${encodeURIComponent(r.changeId)}`;
109
- j = await fetchGerritJson(url2);
213
+ const url2 = `${gerritApi.replace(/\/$/, '')}/changes/?q=change:${encodeURIComponent(r.changeId)}`
214
+ j = await fetchGerritJson(url2)
110
215
  if (Array.isArray(j) && j.length > 0 && j[0]._number) {
111
- cache.set(r.changeId, j[0]._number);
112
- return j[0]._number;
216
+ cache.set(r.changeId, j[0]._number)
217
+ return j[0]._number
113
218
  }
114
219
  }
115
220
  // try commit hash
116
221
  if (r.hash) {
117
- if (cache.has(r.hash)) return cache.get(r.hash);
118
- const url3 = `${gerritApi.replace(/\/$/, '')}/changes/?q=commit:${encodeURIComponent(r.hash)}`;
119
- const j = await fetchGerritJson(url3);
222
+ if (cache.has(r.hash)) return cache.get(r.hash)
223
+ const url3 = `${gerritApi.replace(/\/$/, '')}/changes/?q=commit:${encodeURIComponent(r.hash)}`
224
+ const j = await fetchGerritJson(url3)
120
225
  if (Array.isArray(j) && j.length > 0 && j[0]._number) {
121
- cache.set(r.hash, j[0]._number);
122
- return j[0]._number;
226
+ cache.set(r.hash, j[0]._number)
227
+ return j[0]._number
123
228
  }
124
229
  }
125
- return null;
126
- };
230
+ return null
231
+ }
127
232
  records = await Promise.all(
128
233
  records.map(async (r) => {
129
- const changeNumber = await resolveChangeNumber(r);
130
- const changeNumberOrFallback = changeNumber || r.changeId || r.hash;
131
- const gerritUrl = prefix.replace('{{changeNumber}}', changeNumberOrFallback);
132
- return { ...r, gerrit: gerritUrl };
234
+ const changeNumber = await resolveChangeNumber(r)
235
+ const changeNumberOrFallback = changeNumber || r.changeId || r.hash
236
+ const gerritUrl = prefix.replace(
237
+ '{{changeNumber}}',
238
+ changeNumberOrFallback
239
+ )
240
+ return { ...r, gerrit: gerritUrl }
133
241
  })
134
- );
242
+ )
135
243
  } else if (prefix.includes('{{changeNumber}}') && !gerritApi) {
136
- console.warn('prefix contains {{changeNumber}} but no --gerrit-api provided — falling back to changeId/hash');
137
- records = records.map((r) => ({ ...r, gerrit: prefix.replace('{{changeNumber}}', r.changeId || r.hash) }));
244
+ console.warn(
245
+ 'prefix contains {{changeNumber}} but no --gerrit-api provided — falling back to changeId/hash'
246
+ )
247
+ records = records.map((r) => ({
248
+ ...r,
249
+ gerrit: prefix.replace('{{changeNumber}}', r.changeId || r.hash)
250
+ }))
138
251
  } else {
139
252
  records = records.map((r) => {
140
- let gerritUrl;
253
+ let gerritUrl
141
254
  if (prefix.includes('{{changeId}}')) {
142
- const changeId = r.changeId || r.hash;
143
- gerritUrl = prefix.replace('{{changeId}}', changeId);
255
+ const changeId = r.changeId || r.hash
256
+ gerritUrl = prefix.replace('{{changeId}}', changeId)
144
257
  } else if (prefix.includes('{{hash}}')) {
145
- gerritUrl = prefix.replace('{{hash}}', r.hash);
258
+ gerritUrl = prefix.replace('{{hash}}', r.hash)
146
259
  } else {
147
- gerritUrl = prefix.endsWith('/') ? `${prefix}${r.hash}` : `${prefix}/${r.hash}`;
260
+ gerritUrl = prefix.endsWith('/')
261
+ ? `${prefix}${r.hash}`
262
+ : `${prefix}/${r.hash}`
148
263
  }
149
- return { ...r, gerrit: gerritUrl };
150
- });
264
+ return { ...r, gerrit: gerritUrl }
265
+ })
151
266
  }
152
267
  }
153
268
 
154
269
  // --- 分组 ---
155
- const groups = opts.groupBy ? groupRecords(records, opts.groupBy) : null;
270
+ const groups = opts.groupBy ? groupRecords(records, opts.groupBy) : null
156
271
 
157
272
  // --- Overtime analysis ---
158
273
  if (opts.overtime) {
159
274
  const stats = analyzeOvertime(records, {
160
275
  startHour: opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
161
276
  endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
162
- lunchStart: opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
277
+ lunchStart:
278
+ opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
163
279
  lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
164
- country: opts.country || 'CN',
165
- });
280
+ country: opts.country || 'CN'
281
+ })
166
282
  // Output to console
167
- console.log('\n--- Overtime analysis ---\n');
168
- console.log(renderOvertimeText(stats));
283
+ console.log('\n--- Overtime analysis ---\n')
284
+ console.log(renderOvertimeText(stats))
169
285
  // if user requested json format, write stats to file
170
286
  if (opts.json || opts.format === 'json') {
171
- const file = opts.out || 'overtime.json';
172
- const filepath = outputFilePath(file, outDir);
173
- writeJSON(filepath, stats);
174
- console.log(chalk.green(`overtime JSON 已导出: ${filepath}`));
287
+ const file = opts.out || 'overtime.json'
288
+ const filepath = outputFilePath(file, outDir)
289
+ writeJSON(filepath, stats)
290
+ console.log(chalk.green(`overtime JSON 已导出: ${filepath}`))
175
291
  }
176
292
  // Always write human readable overtime text to file (default: overtime.txt)
177
- const outBase = opts.out ? path.basename(opts.out, path.extname(opts.out)) : 'commits';
178
- const overtimeFileName = `overtime_${outBase}.txt`;
179
- const overtimeFile = outputFilePath(overtimeFileName, outDir);
180
- writeTextFile(overtimeFile, renderOvertimeText(stats));
293
+ const outBase = opts.out
294
+ ? path.basename(opts.out, path.extname(opts.out))
295
+ : 'commits'
296
+ const overtimeFileName = `overtime_${outBase}.txt`
297
+ const overtimeFile = outputFilePath(overtimeFileName, outDir)
298
+ writeTextFile(overtimeFile, renderOvertimeText(stats))
181
299
  // write tab-separated text file for better alignment in editors that use proportional fonts
182
- const overtimeTabFileName = `overtime_${outBase}.tab.txt`;
183
- const overtimeTabFile = outputFilePath(overtimeTabFileName, outDir);
184
- writeTextFile(overtimeTabFile, renderOvertimeTab(stats));
300
+ const overtimeTabFileName = `overtime_${outBase}.tab.txt`
301
+ const overtimeTabFile = outputFilePath(overtimeTabFileName, outDir)
302
+ writeTextFile(overtimeTabFile, renderOvertimeTab(stats))
185
303
  // write CSV for structured data consumption
186
- const overtimeCsvFileName = `overtime_${outBase}.csv`;
187
- const overtimeCsvFile = outputFilePath(overtimeCsvFileName, outDir);
188
- writeTextFile(overtimeCsvFile, renderOvertimeCsv(stats));
189
- console.log(chalk.green(`Overtime text 已导出: ${overtimeFile}`));
190
- console.log(chalk.green(`Overtime table (tabs) 已导出: ${overtimeTabFile}`));
191
- console.log(chalk.green(`Overtime CSV 已导出: ${overtimeCsvFile}`));
304
+ const overtimeCsvFileName = `overtime_${outBase}.csv`
305
+ const overtimeCsvFile = outputFilePath(overtimeCsvFileName, outDir)
306
+ writeTextFile(overtimeCsvFile, renderOvertimeCsv(stats))
307
+ console.log(chalk.green(`Overtime text 已导出: ${overtimeFile}`))
308
+ console.log(chalk.green(`Overtime table (tabs) 已导出: ${overtimeTabFile}`))
309
+ console.log(chalk.green(`Overtime CSV 已导出: ${overtimeCsvFile}`))
192
310
 
193
311
  // If serve mode is enabled, write data modules and launch the web server
194
312
  if (opts.serve) {
195
313
  try {
196
- const dataCommitsFile = outputFilePath('data/commits.mjs', outDir);
197
- const commitsModule = `export default ${JSON.stringify(records, null, 2)};\n`;
198
- writeTextFile(dataCommitsFile, commitsModule);
199
- const dataStatsFile = outputFilePath('data/overtime-stats.mjs', outDir);
200
- const statsModule = `export default ${JSON.stringify(stats, null, 2)};\n`;
201
- writeTextFile(dataStatsFile, statsModule);
314
+ const dataCommitsFile = outputFilePath('data/commits.mjs', outDir)
315
+ const commitsModule = `export default ${JSON.stringify(records, null, 2)};\n`
316
+ writeTextFile(dataCommitsFile, commitsModule)
317
+ const dataStatsFile = outputFilePath('data/overtime-stats.mjs', outDir)
318
+ const statsModule = `export default ${JSON.stringify(stats, null, 2)};\n`
319
+ writeTextFile(dataStatsFile, statsModule)
202
320
 
203
321
  // 新增:每周趋势数据(用于前端图表)
204
- const weekGroups = groupRecords(records, 'week');
205
- const weekKeys = Object.keys(weekGroups).sort();
322
+ const weekGroups = groupRecords(records, 'week')
323
+ const weekKeys = Object.keys(weekGroups).sort()
206
324
  const weeklySeries = weekKeys.map((k) => {
207
325
  const s = analyzeOvertime(weekGroups[k], {
208
- startHour: opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
326
+ startHour:
327
+ opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
209
328
  endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
210
- lunchStart: opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
329
+ lunchStart:
330
+ opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
211
331
  lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
212
- country: opts.country || 'CN',
213
- });
332
+ country: opts.country || 'CN'
333
+ })
214
334
  return {
215
335
  period: k,
216
336
  total: s.total,
217
337
  outsideWorkCount: s.outsideWorkCount,
218
338
  outsideWorkRate: s.outsideWorkRate,
219
339
  nonWorkdayCount: s.nonWorkdayCount,
220
- nonWorkdayRate: s.nonWorkdayRate,
221
- };
222
- });
223
- const dataWeeklyFile = outputFilePath('data/overtime-weekly.mjs', outDir);
224
- const weeklyModule = `export default ${JSON.stringify(weeklySeries, null, 2)};\n`;
225
- writeTextFile(dataWeeklyFile, weeklyModule);
226
- console.log(chalk.green(`Weekly series 已导出: ${dataWeeklyFile}`));
340
+ nonWorkdayRate: s.nonWorkdayRate
341
+ }
342
+ })
343
+ const dataWeeklyFile = outputFilePath(
344
+ 'data/overtime-weekly.mjs',
345
+ outDir
346
+ )
347
+ const weeklyModule = `export default ${JSON.stringify(weeklySeries, null, 2)};\n`
348
+ writeTextFile(dataWeeklyFile, weeklyModule)
349
+ console.log(chalk.green(`Weekly series 已导出: ${dataWeeklyFile}`))
227
350
 
228
- startServer(opts.port || 3000, outDir).catch(() => {});
351
+ startServer(opts.port || 3000, outDir).catch(() => {})
229
352
  } catch (err) {
230
- console.warn('Export data modules failed:', err && err.message ? err.message : err);
353
+ console.warn(
354
+ 'Export data modules failed:',
355
+ err && err.message ? err.message : err
356
+ )
231
357
  }
232
358
  }
233
359
 
234
360
  // 按月输出 ... 保持原逻辑
235
- const perPeriodFormats = String(opts.perPeriodFormats || '').split(',').map(s => String(s || '').trim().toLowerCase()).filter(Boolean);
361
+ const perPeriodFormats = String(opts.perPeriodFormats || '')
362
+ .split(',')
363
+ .map((s) =>
364
+ String(s || '')
365
+ .trim()
366
+ .toLowerCase()
367
+ )
368
+ .filter(Boolean)
236
369
  try {
237
- const monthGroups = groupRecords(records, 'month');
238
- const monthlyFileName = `overtime_${outBase}_monthly.txt`;
239
- const monthlyFile = outputFilePath(monthlyFileName, outDir);
240
- let monthlyContent = '';
241
- const monthKeys = Object.keys(monthGroups).sort();
370
+ const monthGroups = groupRecords(records, 'month')
371
+ const monthlyFileName = `overtime_${outBase}_monthly.txt`
372
+ const monthlyFile = outputFilePath(monthlyFileName, outDir)
373
+ let monthlyContent = ''
374
+ const monthKeys = Object.keys(monthGroups).sort()
242
375
  monthKeys.forEach((k) => {
243
- const groupRecs = monthGroups[k];
376
+ const groupRecs = monthGroups[k]
244
377
  const s = analyzeOvertime(groupRecs, {
245
- startHour: opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
378
+ startHour:
379
+ opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
246
380
  endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
247
- lunchStart: opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
381
+ lunchStart:
382
+ opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
248
383
  lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
249
- country: opts.country || 'CN',
250
- });
251
- monthlyContent += `===== ${k} =====\n`;
252
- monthlyContent += `${renderOvertimeText(s)}\n\n`;
384
+ country: opts.country || 'CN'
385
+ })
386
+ monthlyContent += `===== ${k} =====\n`
387
+ monthlyContent += `${renderOvertimeText(s)}\n\n`
253
388
  // Also write a single file per month under 'month/' folder
254
389
  try {
255
- const perMonthFileName = `month/overtime_${outBase}_${k}.txt`;
256
- const perMonthFile = outputFilePath(perMonthFileName, outDir);
257
- writeTextFile(perMonthFile, renderOvertimeText(s));
258
- console.log(chalk.green(`Overtime 月度(${k}) 已导出: ${perMonthFile}`));
390
+ const perMonthFileName = `month/overtime_${outBase}_${k}.txt`
391
+ const perMonthFile = outputFilePath(perMonthFileName, outDir)
392
+ writeTextFile(perMonthFile, renderOvertimeText(s))
393
+ console.log(
394
+ chalk.green(`Overtime 月度(${k}) 已导出: ${perMonthFile}`)
395
+ )
259
396
  // per-period CSV / Tab format (按需生成)
260
397
  if (perPeriodFormats.includes('csv')) {
261
398
  try {
262
- const perMonthCsvName = `month/overtime_${outBase}_${k}.csv`;
263
- writeTextFile(outputFilePath(perMonthCsvName, outDir), renderOvertimeCsv(s));
264
- console.log(chalk.green(`Overtime 月度(CSV)(${k}) 已导出: ${outputFilePath(perMonthCsvName, outDir)}`));
399
+ const perMonthCsvName = `month/overtime_${outBase}_${k}.csv`
400
+ writeTextFile(
401
+ outputFilePath(perMonthCsvName, outDir),
402
+ renderOvertimeCsv(s)
403
+ )
404
+ console.log(
405
+ chalk.green(
406
+ `Overtime 月度(CSV)(${k}) 已导出: ${outputFilePath(perMonthCsvName, outDir)}`
407
+ )
408
+ )
265
409
  } catch (err) {
266
- console.warn(`Write monthly CSV for ${k} failed:`, err && err.message ? err.message : err);
410
+ console.warn(
411
+ `Write monthly CSV for ${k} failed:`,
412
+ err && err.message ? err.message : err
413
+ )
267
414
  }
268
415
  }
269
416
  if (perPeriodFormats.includes('tab')) {
270
417
  try {
271
- const perMonthTabName = `month/overtime_${outBase}_${k}.tab.txt`;
272
- writeTextFile(outputFilePath(perMonthTabName, outDir), renderOvertimeTab(s));
273
- console.log(chalk.green(`Overtime 月度(Tab)(${k}) 已导出: ${outputFilePath(perMonthTabName, outDir)}`));
418
+ const perMonthTabName = `month/overtime_${outBase}_${k}.tab.txt`
419
+ writeTextFile(
420
+ outputFilePath(perMonthTabName, outDir),
421
+ renderOvertimeTab(s)
422
+ )
423
+ console.log(
424
+ chalk.green(
425
+ `Overtime 月度(Tab)(${k}) 已导出: ${outputFilePath(perMonthTabName, outDir)}`
426
+ )
427
+ )
274
428
  } catch (err) {
275
- console.warn(`Write monthly Tab for ${k} failed:`, err && err.message ? err.message : err);
429
+ console.warn(
430
+ `Write monthly Tab for ${k} failed:`,
431
+ err && err.message ? err.message : err
432
+ )
276
433
  }
277
434
  }
278
435
  } catch (err) {
279
- console.warn(`Write monthly file for ${k} failed:`, err && err.message ? err.message : err);
436
+ console.warn(
437
+ `Write monthly file for ${k} failed:`,
438
+ err && err.message ? err.message : err
439
+ )
280
440
  }
281
- });
441
+ })
282
442
  if (!opts.perPeriodOnly) {
283
- writeTextFile(monthlyFile, monthlyContent);
284
- console.log(chalk.green(`Overtime 月度汇总 已导出: ${monthlyFile}`));
443
+ writeTextFile(monthlyFile, monthlyContent)
444
+ console.log(chalk.green(`Overtime 月度汇总 已导出: ${monthlyFile}`))
285
445
  }
286
446
  // per-period Excel (sheets or files)
287
447
  if (perPeriodFormats.includes('xlsx')) {
288
- const perPeriodExcelMode = String(opts.perPeriodExcelMode || 'sheets');
448
+ const perPeriodExcelMode = String(opts.perPeriodExcelMode || 'sheets')
289
449
  if (perPeriodExcelMode === 'sheets') {
290
450
  try {
291
- const monthXlsxName = `month/overtime_${outBase}_monthly.xlsx`;
292
- const monthXlsxFile = outputFilePath(monthXlsxName, outDir);
293
- await exportExcelPerPeriodSheets(monthGroups, monthXlsxFile, { stats: opts.stats, gerrit: opts.gerrit });
294
- console.log(chalk.green(`Overtime 月度(XLSX) 已导出: ${monthXlsxFile}`));
451
+ const monthXlsxName = `month/overtime_${outBase}_monthly.xlsx`
452
+ const monthXlsxFile = outputFilePath(monthXlsxName, outDir)
453
+ await exportExcelPerPeriodSheets(monthGroups, monthXlsxFile, {
454
+ stats: opts.stats,
455
+ gerrit: opts.gerrit
456
+ })
457
+ console.log(
458
+ chalk.green(`Overtime 月度(XLSX) 已导出: ${monthXlsxFile}`)
459
+ )
295
460
  } catch (err) {
296
- console.warn('Export month XLSX (sheets) failed:', err && err.message ? err.message : err);
461
+ console.warn(
462
+ 'Export month XLSX (sheets) failed:',
463
+ err && err.message ? err.message : err
464
+ )
297
465
  }
298
466
  } else {
299
467
  try {
300
- const monthKeys2 = Object.keys(monthGroups).sort();
301
- const tasks = monthKeys2.map(k2 => {
302
- const perMonthXlsxName = `month/overtime_${outBase}_${k2}.xlsx`;
303
- const perMonthXlsxFile = outputFilePath(perMonthXlsxName, outDir);
304
- return exportExcel(monthGroups[k2], null, { file: perMonthXlsxFile, stats: opts.stats, gerrit: opts.gerrit })
305
- .then(() => console.log(chalk.green(`Overtime 月度(XLSX)(${k2}) 已导出: ${perMonthXlsxFile}`)));
306
- });
307
- await Promise.all(tasks);
468
+ const monthKeys2 = Object.keys(monthGroups).sort()
469
+ const tasks = monthKeys2.map((k2) => {
470
+ const perMonthXlsxName = `month/overtime_${outBase}_${k2}.xlsx`
471
+ const perMonthXlsxFile = outputFilePath(perMonthXlsxName, outDir)
472
+ return exportExcel(monthGroups[k2], null, {
473
+ file: perMonthXlsxFile,
474
+ stats: opts.stats,
475
+ gerrit: opts.gerrit
476
+ }).then(() =>
477
+ console.log(
478
+ chalk.green(
479
+ `Overtime 月度(XLSX)(${k2}) 已导出: ${perMonthXlsxFile}`
480
+ )
481
+ )
482
+ )
483
+ })
484
+ await Promise.all(tasks)
308
485
  } catch (err) {
309
- console.warn('Export monthly XLSX files failed:', err && err.message ? err.message : err);
486
+ console.warn(
487
+ 'Export monthly XLSX files failed:',
488
+ err && err.message ? err.message : err
489
+ )
310
490
  }
311
491
  }
312
492
  }
313
493
  } catch (err) {
314
- console.warn('Generate monthly overtime failed:', err && err.message ? err.message : err);
494
+ console.warn(
495
+ 'Generate monthly overtime failed:',
496
+ err && err.message ? err.message : err
497
+ )
315
498
  }
316
499
 
317
500
  // 周度输出保持原逻辑(略)
318
501
  try {
319
- const weekGroups = groupRecords(records, 'week');
320
- const weeklyFileName = `overtime_${outBase}_weekly.txt`;
321
- const weeklyFile = outputFilePath(weeklyFileName, outDir);
322
- let weeklyContent = '';
323
- const weekKeys = Object.keys(weekGroups).sort();
502
+ const weekGroups = groupRecords(records, 'week')
503
+ const weeklyFileName = `overtime_${outBase}_weekly.txt`
504
+ const weeklyFile = outputFilePath(weeklyFileName, outDir)
505
+ let weeklyContent = ''
506
+ const weekKeys = Object.keys(weekGroups).sort()
324
507
  weekKeys.forEach((k) => {
325
- const groupRecs = weekGroups[k];
508
+ const groupRecs = weekGroups[k]
326
509
  const s = analyzeOvertime(groupRecs, {
327
- startHour: opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
510
+ startHour:
511
+ opts.workStart || opts.workStart === 0 ? opts.workStart : 9,
328
512
  endHour: opts.workEnd || opts.workEnd === 0 ? opts.workEnd : 18,
329
- lunchStart: opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
513
+ lunchStart:
514
+ opts.lunchStart || opts.lunchStart === 0 ? opts.lunchStart : 12,
330
515
  lunchEnd: opts.lunchEnd || opts.lunchEnd === 0 ? opts.lunchEnd : 14,
331
- country: opts.country || 'CN',
332
- });
333
- weeklyContent += `===== ${k} =====\n`;
334
- weeklyContent += `${renderOvertimeText(s)}\n\n`;
516
+ country: opts.country || 'CN'
517
+ })
518
+ weeklyContent += `===== ${k} =====\n`
519
+ weeklyContent += `${renderOvertimeText(s)}\n\n`
335
520
  try {
336
- const perWeekFileName = `week/overtime_${outBase}_${k}.txt`;
337
- const perWeekFile = outputFilePath(perWeekFileName, outDir);
338
- writeTextFile(perWeekFile, renderOvertimeText(s));
339
- console.log(chalk.green(`Overtime 周度(${k}) 已导出: ${perWeekFile}`));
521
+ const perWeekFileName = `week/overtime_${outBase}_${k}.txt`
522
+ const perWeekFile = outputFilePath(perWeekFileName, outDir)
523
+ writeTextFile(perWeekFile, renderOvertimeText(s))
524
+ console.log(chalk.green(`Overtime 周度(${k}) 已导出: ${perWeekFile}`))
340
525
  // eslint-disable-next-line no-shadow
341
- const perPeriodFormats = String(opts.perPeriodFormats || '').split(',').map(s => String(s || '').trim().toLowerCase()).filter(Boolean);
526
+ const perPeriodFormats = String(opts.perPeriodFormats || '')
527
+ .split(',')
528
+ // eslint-disable-next-line no-shadow
529
+ .map((s) =>
530
+ String(s || '')
531
+ .trim()
532
+ .toLowerCase()
533
+ )
534
+ .filter(Boolean)
342
535
  if (perPeriodFormats.includes('csv')) {
343
536
  try {
344
- const perWeekCsvName = `week/overtime_${outBase}_${k}.csv`;
345
- writeTextFile(outputFilePath(perWeekCsvName, outDir), renderOvertimeCsv(s));
346
- console.log(chalk.green(`Overtime 周度(CSV)(${k}) 已导出: ${outputFilePath(perWeekCsvName, outDir)}`));
537
+ const perWeekCsvName = `week/overtime_${outBase}_${k}.csv`
538
+ writeTextFile(
539
+ outputFilePath(perWeekCsvName, outDir),
540
+ renderOvertimeCsv(s)
541
+ )
542
+ console.log(
543
+ chalk.green(
544
+ `Overtime 周度(CSV)(${k}) 已导出: ${outputFilePath(perWeekCsvName, outDir)}`
545
+ )
546
+ )
347
547
  } catch (err) {
348
- console.warn(`Write weekly CSV for ${k} failed:`, err && err.message ? err.message : err);
548
+ console.warn(
549
+ `Write weekly CSV for ${k} failed:`,
550
+ err && err.message ? err.message : err
551
+ )
349
552
  }
350
553
  }
351
554
  if (perPeriodFormats.includes('tab')) {
352
555
  try {
353
- const perWeekTabName = `week/overtime_${outBase}_${k}.tab.txt`;
354
- writeTextFile(outputFilePath(perWeekTabName, outDir), renderOvertimeTab(s));
355
- console.log(chalk.green(`Overtime 周度(Tab)(${k}) 已导出: ${outputFilePath(perWeekTabName, outDir)}`));
556
+ const perWeekTabName = `week/overtime_${outBase}_${k}.tab.txt`
557
+ writeTextFile(
558
+ outputFilePath(perWeekTabName, outDir),
559
+ renderOvertimeTab(s)
560
+ )
561
+ console.log(
562
+ chalk.green(
563
+ `Overtime 周度(Tab)(${k}) 已导出: ${outputFilePath(perWeekTabName, outDir)}`
564
+ )
565
+ )
356
566
  } catch (err) {
357
- console.warn(`Write weekly Tab for ${k} failed:`, err && err.message ? err.message : err);
567
+ console.warn(
568
+ `Write weekly Tab for ${k} failed:`,
569
+ err && err.message ? err.message : err
570
+ )
358
571
  }
359
572
  }
360
573
  } catch (err) {
361
- console.warn(`Write weekly file for ${k} failed:`, err && err.message ? err.message : err);
574
+ console.warn(
575
+ `Write weekly file for ${k} failed:`,
576
+ err && err.message ? err.message : err
577
+ )
362
578
  }
363
- });
364
- writeTextFile(weeklyFile, weeklyContent);
365
- console.log(chalk.green(`Overtime 周度汇总 已导出: ${weeklyFile}`));
579
+ })
580
+ writeTextFile(weeklyFile, weeklyContent)
581
+ console.log(chalk.green(`Overtime 周度汇总 已导出: ${weeklyFile}`))
366
582
  } catch (err) {
367
- console.warn('Generate weekly overtime failed:', err && err.message ? err.message : err);
583
+ console.warn(
584
+ 'Generate weekly overtime failed:',
585
+ err && err.message ? err.message : err
586
+ )
368
587
  }
369
588
  }
370
589
 
371
590
  // --- JSON/TEXT/EXCEL(保持原逻辑) ---
372
591
  if (opts.json || opts.format === 'json') {
373
- const file = opts.out || 'commits.json';
374
- const filepath = outputFilePath(file, outDir);
375
- writeJSON(filepath, groups || records);
376
- console.log(chalk.green(`JSON 已导出: ${filepath}`));
377
- return;
592
+ const file = opts.out || 'commits.json'
593
+ const filepath = outputFilePath(file, outDir)
594
+ writeJSON(filepath, groups || records)
595
+ console.log(chalk.green(`JSON 已导出: ${filepath}`))
596
+ return
378
597
  }
379
598
 
380
599
  if (opts.format === 'text') {
381
- const file = opts.out || 'commits.txt';
382
- const filepath = outputFilePath(file, outDir);
383
- const text = renderText(records, groups, { showGerrit: !!opts.gerrit });
384
- writeTextFile(filepath, text);
385
- console.log(text);
386
- console.log(chalk.green(`文本已导出: ${filepath}`));
387
- return;
600
+ const file = opts.out || 'commits.txt'
601
+ const filepath = outputFilePath(file, outDir)
602
+ const text = renderText(records, groups, { showGerrit: !!opts.gerrit })
603
+ writeTextFile(filepath, text)
604
+ console.log(text)
605
+ console.log(chalk.green(`文本已导出: ${filepath}`))
606
+ return
388
607
  }
389
608
 
390
609
  if (opts.format === 'excel') {
391
- const excelFile = opts.out || 'commits.xlsx';
392
- const excelPath = outputFilePath(excelFile, outDir);
393
- const txtFile = excelFile.replace(/\.xlsx$/, '.txt');
394
- const txtPath = outputFilePath(txtFile, outDir);
610
+ const excelFile = opts.out || 'commits.xlsx'
611
+ const excelPath = outputFilePath(excelFile, outDir)
612
+ const txtFile = excelFile.replace(/\.xlsx$/, '.txt')
613
+ const txtPath = outputFilePath(txtFile, outDir)
395
614
  await exportExcel(records, groups, {
396
615
  file: excelPath,
397
616
  stats: opts.stats,
398
617
  gerrit: opts.gerrit
399
- });
400
- const text = renderText(records, groups);
401
- writeTextFile(txtPath, text);
402
- console.log(chalk.green(`Excel 已导出: ${excelPath}`));
403
- console.log(chalk.green(`文本已自动导出: ${txtPath}`));
618
+ })
619
+ const text = renderText(records, groups)
620
+ writeTextFile(txtPath, text)
621
+ console.log(chalk.green(`Excel 已导出: ${excelPath}`))
622
+ console.log(chalk.green(`文本已自动导出: ${txtPath}`))
404
623
  }
405
- })();
624
+ })()
@@ -0,0 +1,3 @@
1
+ export const CLI_NAME = 'wukong-gitlog-cli'
2
+
3
+ export const GIT_HUB ='https://www.npmjs.com/package/wukong-gitlog-cli'
@@ -0,0 +1,130 @@
1
+ import boxen from 'boxen'
2
+ import fs from 'fs'
3
+ import isOnline from 'is-online'
4
+ import os from 'os'
5
+ import path from 'path'
6
+ import semver from 'semver'
7
+
8
+ import { colors } from './colors.mjs'
9
+
10
+ const CONFIG_DIR = path.join(os.homedir(), '.config', 'configstore')
11
+
12
+ const getCacheFile = (pkg) =>
13
+ path.join(CONFIG_DIR, `update-notifier-${pkg.name}.json`)
14
+
15
+ async function fetchLatestVersion(pkgName, timeout = 1500) {
16
+ const url = `https://registry.npmjs.org/${pkgName}/latest`
17
+ const controller = new AbortController()
18
+ const timer = setTimeout(() => controller.abort(), timeout)
19
+
20
+ try {
21
+ const res = await fetch(url, { signal: controller.signal })
22
+ clearTimeout(timer)
23
+ if (!res.ok) throw new Error('fetch fail')
24
+ const data = await res.json()
25
+ return data.version
26
+ } catch (e) {
27
+ clearTimeout(timer)
28
+ // 超时或其他错误都返回 null,防止程序卡死
29
+ return null
30
+ }
31
+ }
32
+
33
+ /**
34
+ * 格式化升级提示信息
35
+ */
36
+ export function formatUpdateMessage(current, latest, name) {
37
+ const arrow = colors.dim('→')
38
+ const currentVer = colors.dim(current)
39
+ const latestVer = colors.green(latest)
40
+
41
+ const message =
42
+ `${colors.dim('Update available')}` +
43
+ ` ${currentVer} ${arrow} ${latestVer}\n` +
44
+ `${colors.dim('Run')} ${colors.cyanish(`npm i -g ${name}`)} ${colors.dim(' to update')}`
45
+
46
+ return boxen(message, {
47
+ padding: 1,
48
+ margin: 1,
49
+ borderStyle: 'round',
50
+ borderColor: 'yellow'
51
+ })
52
+ }
53
+
54
+ /**
55
+ * 检查更新(带缓存 + 支持 patch 更新)
56
+ */
57
+ export async function checkUpdateWithPatch({
58
+ pkg,
59
+ interval = 24 * 60 * 60 * 1000,
60
+ // interval = 6 * 1000,
61
+ force = false
62
+ } = {}) {
63
+ const online = await isOnline()
64
+ // console.log('online', online)
65
+ if (!online) return null
66
+ const now = Date.now()
67
+ const CACHE_FILE = getCacheFile(pkg)
68
+
69
+ let cache = {}
70
+ if (fs.existsSync(CACHE_FILE)) {
71
+ try {
72
+ cache = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')) || {}
73
+ // eslint-disable-next-line no-empty
74
+ } catch {}
75
+ }
76
+
77
+ // console.log('now - cache.lastCheck', now - cache.lastCheck, interval)
78
+ if (!force && cache.lastCheck && now - cache.lastCheck < interval) {
79
+ return cache.updateInfo || null
80
+ }
81
+
82
+ const latest = await fetchLatestVersion(pkg.name)
83
+ if (!latest) {
84
+ // console.log('无法获取最新版本')
85
+ return null
86
+ }
87
+
88
+ if (semver.lt(pkg.version, latest)) {
89
+ // 构造 update 对象,兼容 update-notifier
90
+ const updateInfo = {
91
+ current: pkg.version,
92
+ latest,
93
+ type: semver.diff(pkg.version, latest) || 'patch',
94
+ name: pkg.name,
95
+ // 这里官方还会有 message 字段
96
+ // 用官方的默认消息格式生成,方便notify打印
97
+ message: `\nUpdate available ${pkg.version} → ${latest}\nRun npm i -g ${pkg.name} to update\n`
98
+ }
99
+
100
+ // 缓存
101
+ try {
102
+ fs.mkdirSync(CONFIG_DIR, { recursive: true })
103
+ fs.writeFileSync(
104
+ CACHE_FILE,
105
+ JSON.stringify(
106
+ {
107
+ lastCheck: now,
108
+ updateInfo
109
+ },
110
+ null,
111
+ 2
112
+ )
113
+ )
114
+ } catch (e) {
115
+ console.error('缓存写入失败:', e)
116
+ }
117
+
118
+ const { current } = updateInfo
119
+ console.log(formatUpdateMessage(current, latest, pkg.name))
120
+ return updateInfo
121
+ }
122
+ // 无更新,刷新缓存时间
123
+ try {
124
+ fs.mkdirSync(CONFIG_DIR, { recursive: true })
125
+ fs.writeFileSync(CACHE_FILE, JSON.stringify({ lastCheck: now }, null, 2))
126
+ } catch (e) {
127
+ console.error('缓存写入失败:', e)
128
+ }
129
+ return null
130
+ }
@@ -0,0 +1,56 @@
1
+ import { Chalk } from 'chalk'
2
+
3
+ const chalk = new Chalk({ level: 3 }) // 强制开启 truecolor chalk v5
4
+ // 自定义灰色,适合透明和深色背景,避免看不清
5
+ const betterGray = chalk.hex('#BBBBBB')
6
+
7
+ // 统一颜色模块
8
+ export const colors = {
9
+ // 标题、版本号
10
+ title: chalk.bold.cyan,
11
+
12
+ // 命令名
13
+ command: chalk.green,
14
+
15
+ // 选项标记
16
+ option: chalk.magenta,
17
+
18
+ // 描述文字,替代默认灰色
19
+ desc: betterGray,
20
+
21
+ // Usage、Commands、Options、示例等小标题
22
+ header: chalk.yellow,
23
+
24
+ // 示例文字,普通白色
25
+ example: chalk.white,
26
+
27
+ // 警告红色
28
+ error: chalk.bold.red,
29
+
30
+ // 成功绿色
31
+ success: chalk.bold.green,
32
+
33
+ // 信息蓝色
34
+ info: chalk.blue,
35
+
36
+ // 可用更柔和的调暗白色,替代 dim gray
37
+ dim: chalk.white.dim,
38
+ bold: chalk.bold,
39
+ boldBlue: chalk.bold.blue,
40
+ gray: chalk.bold.gray,
41
+ green: chalk.green,
42
+ cyanish: chalk.hex('#57a097'),
43
+ white: chalk.white,
44
+ }
45
+
46
+ export const c = {
47
+ title: chalk.bold.cyan,
48
+ label: chalk.yellow,
49
+ value: chalk.white,
50
+ dim: chalk.hex('#BBBBBB'),
51
+ success: chalk.green,
52
+ green: chalk.green,
53
+ error: chalk.red,
54
+ red: chalk.red,
55
+ link: chalk.blue.underline
56
+ }
@@ -0,0 +1,10 @@
1
+ import os from 'os'
2
+ import process from 'process'
3
+ import { colors } from './colors.mjs'
4
+
5
+
6
+ export function showVersionInfo(VERSION) {
7
+ console.log(colors.title(`\nwukong-deploy v${VERSION}\n`))
8
+ console.log(`${colors.bold('Node.js')}: ${process.version}`)
9
+ console.log(`${colors.bold('Platform')}: ${os.platform()} ${os.arch()}`)
10
+ }
package/web/index.html CHANGED
@@ -1,49 +1,61 @@
1
1
  <!doctype html>
2
2
  <html lang="zh-CN">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width,initial-scale=1" />
6
- <title>wukong-gitlog-cli — Overtime Dashboard</title>
7
- <link rel="stylesheet" href="/style.css" />
8
- <script src="./static/chart.js"></script>
9
- </head>
10
- <body>
11
- <header>
12
- <h1>Wukong Gitlog Overtime Dashboard</h1>
13
- </header>
14
- <main>
15
- <section id="charts">
16
- <div class="chart-card">
17
- <h2>每小时加班分布 (小时 -> 提交数)</h2>
18
- <canvas id="hourlyOvertimeChart"></canvas>
19
- </div>
20
- <div class="chart-card">
21
- <h2>下班时间 vs 工作时间提交占比</h2>
22
- <canvas id="outsideVsInsideChart"></canvas>
23
- </div>
24
- <div class="chart-card">
25
- <h2>按日提交趋势</h2>
26
- <canvas id="dailyTrendChart"></canvas>
27
- </div>
28
- <div class="chart-card">
29
- <h2>每周趋势(加班占比)</h2>
30
- <canvas id="weeklyTrendChart"></canvas>
31
- </div>
32
- </section>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>wukong-gitlog-cli — Overtime Dashboard</title>
7
+ <link rel="stylesheet" href="/style.css" />
8
+ <script src="./static/chart.js"></script>
9
+ </head>
10
+ <body>
11
+ <header>
12
+ <h1>Wukong Gitlog Overtime Dashboard</h1>
13
+ </header>
14
+ <main>
15
+ <section id="chartsHalf">
16
+ <div class="chart-card">
17
+ <h2>下班时间 vs 工作时间提交占比</h2>
18
+ <canvas id="outsideVsInsideChart"></canvas>
19
+ </div>
20
+ <div class="chart-card"></div>
21
+ </section>
22
+ <section id="charts">
23
+ <div class="chart-card">
24
+ <h2>每小时加班分布 (小时 -> 提交数)</h2>
25
+ <canvas id="hourlyOvertimeChart"></canvas>
26
+ </div>
33
27
 
34
- <section class="table-card">
35
- <h2>提交清单</h2>
36
- <table id="commitsTable">
37
- <thead>
38
- <tr><th>Hash</th><th>Author</th><th>Date</th><th>Message</th></tr>
39
- </thead>
40
- <tbody></tbody>
41
- </table>
42
- </section>
43
- </main>
44
- <footer>
45
- <small>Local dashboard generated by wukong-gitlog-cli. Data served from <code>/data/</code>.</small>
46
- </footer>
47
- <script type="module" src="/app.js"></script>
48
- </body>
28
+ <div class="chart-card">
29
+ <h2>按日提交趋势</h2>
30
+ <canvas id="dailyTrendChart"></canvas>
31
+ </div>
32
+ <div class="chart-card">
33
+ <h2>每周趋势(加班占比)</h2>
34
+ <canvas id="weeklyTrendChart"></canvas>
35
+ </div>
36
+ </section>
37
+
38
+ <section class="table-card">
39
+ <h2>提交清单</h2>
40
+ <table id="commitsTable">
41
+ <thead>
42
+ <tr>
43
+ <th>Hash</th>
44
+ <th>Author</th>
45
+ <th>Date</th>
46
+ <th>Message</th>
47
+ </tr>
48
+ </thead>
49
+ <tbody></tbody>
50
+ </table>
51
+ </section>
52
+ </main>
53
+ <footer>
54
+ <small
55
+ >Local dashboard generated by wukong-gitlog-cli. Data served from
56
+ <code>/data/</code>.</small
57
+ >
58
+ </footer>
59
+ <script type="module" src="/app.js"></script>
60
+ </body>
49
61
  </html>
package/web/style.css CHANGED
@@ -1,7 +1,8 @@
1
1
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; margin: 0; padding: 0; background: #fafafa; color: #222; }
2
2
  header { background: #0f172a; color: white; padding: 12px 20px; }
3
3
  main { display: flex; flex-direction: column; gap: 12px; padding: 20px; max-width: 1200px; margin: 20px auto; }
4
- #charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; }
4
+ #charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(640px, 1fr)); gap: 16px; }
5
+ #chartsHalf { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; }
5
6
  .chart-card { background: white; border-radius: 8px; padding: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
6
7
  .table-card { background: white; padding: 12px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
7
8
  table { width: 100%; border-collapse: collapse; }