wukong-gitlog-cli 1.0.5 → 1.0.7

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,20 @@
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.7](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.6...v1.0.7) (2025-11-29)
6
+
7
+
8
+ ### Features
9
+
10
+ * 🎸 charts 2 echarts ([6d6d780](https://github.com/tomatobybike/wukong-gitlog-cli/commit/6d6d780c053cd094a3a59ee58249d55bec504655))
11
+
12
+ ### [1.0.6](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.5...v1.0.6) (2025-11-29)
13
+
14
+
15
+ ### Features
16
+
17
+ * 🎸 auto open ([380e013](https://github.com/tomatobybike/wukong-gitlog-cli/commit/380e01386300342b5b4009f4ddf5463a19cf1be8))
18
+
5
19
  ### [1.0.5](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.4...v1.0.5) (2025-11-29)
6
20
 
7
21
  ### [1.0.4](https://github.com/tomatobybike/wukong-gitlog-cli/compare/v1.0.3...v1.0.4) (2025-11-29)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wukong-gitlog-cli",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.",
5
5
  "keywords": [
6
6
  "git",
package/src/server.mjs CHANGED
@@ -1,8 +1,9 @@
1
- import http from 'http';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import chalk from 'chalk';
5
- import { fileURLToPath } from 'url';
1
+ import chalk from 'chalk'
2
+ import { exec } from 'child_process'
3
+ import fs from 'fs'
4
+ import http from 'http'
5
+ import path from 'path'
6
+ import { fileURLToPath } from 'url'
6
7
 
7
8
  const mime = new Map([
8
9
  ['.html', 'text/html; charset=utf-8'],
@@ -14,74 +15,116 @@ const mime = new Map([
14
15
  ['.png', 'image/png'],
15
16
  ['.jpg', 'image/jpeg'],
16
17
  ['.jpeg', 'image/jpeg'],
17
- ['.map', 'application/json; charset=utf-8'],
18
- ]);
18
+ ['.map', 'application/json; charset=utf-8']
19
+ ])
20
+
21
+ // --------------------------------
22
+ // 打开浏览器(跨平台)
23
+ // --------------------------------
24
+ function openBrowser(url) {
25
+ const { platform } = process
26
+
27
+ let cmd
28
+ if (platform === 'win32') {
29
+ cmd = `start "" "${url}"`
30
+ } else if (platform === 'darwin') {
31
+ cmd = `open "${url}"`
32
+ } else {
33
+ cmd = `xdg-open "${url}"`
34
+ }
35
+
36
+ exec(cmd, (err) => {
37
+ if (err) {
38
+ console.log(chalk.yellow(`Failed to auto-open browser: ${err.message}`))
39
+ }
40
+ })
41
+ }
19
42
 
20
43
  // eslint-disable-next-line default-param-last
21
44
  export function startServer(port = 3000, outputDir) {
22
45
  // 解析包根目录,确保 web 资源在全局安装后也能找到
23
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
- const pkgRoot = path.resolve(__dirname, '..');
25
- const webRoot = path.resolve(pkgRoot, 'web');
26
- const dataRoot = outputDir ? path.resolve(outputDir) : path.resolve(process.cwd(), 'output');
46
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
47
+ const pkgRoot = path.resolve(__dirname, '..')
48
+ const webRoot = path.resolve(pkgRoot, 'web')
49
+ const dataRoot = outputDir
50
+ ? path.resolve(outputDir)
51
+ : path.resolve(process.cwd(), 'output')
27
52
 
28
53
  // warn if web directory or data directory doesn't exist
29
54
  if (!fs.existsSync(webRoot)) {
30
- console.warn(chalk.yellow(`Warning: web/ directory not found at ${webRoot}. Server will still run but no UI will be available.`));
55
+ console.warn(
56
+ chalk.yellow(
57
+ `Warning: web/ directory not found at ${webRoot}. Server will still run but no UI will be available.`
58
+ )
59
+ )
31
60
  }
32
61
  if (!fs.existsSync(dataRoot)) {
33
- console.warn(chalk.yellow(`Warning: output data directory not found at ${dataRoot}. Server will still run but data endpoints (/data/) may 404.`));
62
+ console.warn(
63
+ chalk.yellow(
64
+ `Warning: output data directory not found at ${dataRoot}. Server will still run but data endpoints (/data/) may 404.`
65
+ )
66
+ )
34
67
  }
35
68
 
36
69
  const server = http.createServer((req, res) => {
37
70
  try {
38
71
  // Normalize URL path
39
- const u = new URL(req.url, `http://localhost`);
40
- let pathname = decodeURIComponent(u.pathname);
72
+ const u = new URL(req.url, `http://localhost`)
73
+ let pathname = decodeURIComponent(u.pathname)
41
74
 
42
75
  // Serve data files under /data/* mapped to dataRoot/data/*
43
76
  if (pathname.startsWith('/data/')) {
44
- const relative = pathname.replace(/^\/data\//, '');
45
- const fileLocal = path.join(dataRoot, 'data', relative);
77
+ const relative = pathname.replace(/^\/data\//, '')
78
+ const fileLocal = path.join(dataRoot, 'data', relative)
46
79
  if (fs.existsSync(fileLocal) && fs.statSync(fileLocal).isFile()) {
47
- const ext = path.extname(fileLocal).toLowerCase();
48
- res.setHeader('Content-Type', mime.get(ext) || 'application/octet-stream');
49
- res.setHeader('Access-Control-Allow-Origin', '*');
50
- const stream = fs.createReadStream(fileLocal);
51
- stream.pipe(res);
52
- return;
80
+ const ext = path.extname(fileLocal).toLowerCase()
81
+ res.setHeader(
82
+ 'Content-Type',
83
+ mime.get(ext) || 'application/octet-stream'
84
+ )
85
+ res.setHeader('Access-Control-Allow-Origin', '*')
86
+ const stream = fs.createReadStream(fileLocal)
87
+ stream.pipe(res)
88
+ return
53
89
  }
54
90
  }
55
91
 
56
92
  // Resolve web assets
57
- if (pathname === '/') pathname = '/index.html';
58
- const fileLocal = path.join(webRoot, pathname);
93
+ if (pathname === '/') pathname = '/index.html'
94
+ const fileLocal = path.join(webRoot, pathname)
59
95
  if (fs.existsSync(fileLocal) && fs.statSync(fileLocal).isFile()) {
60
- const ext = path.extname(fileLocal).toLowerCase();
61
- res.setHeader('Content-Type', mime.get(ext) || 'application/octet-stream');
62
- const stream = fs.createReadStream(fileLocal);
63
- stream.pipe(res);
64
- return;
96
+ const ext = path.extname(fileLocal).toLowerCase()
97
+ res.setHeader(
98
+ 'Content-Type',
99
+ mime.get(ext) || 'application/octet-stream'
100
+ )
101
+ const stream = fs.createReadStream(fileLocal)
102
+ stream.pipe(res)
103
+ return
65
104
  }
66
105
 
67
106
  // file not found
68
- res.statusCode = 404;
69
- res.setHeader('Content-Type', 'text/plain; charset=utf-8');
70
- res.end('Not Found');
107
+ res.statusCode = 404
108
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8')
109
+ res.end('Not Found')
71
110
  } catch (err) {
72
- res.statusCode = 500;
73
- res.end('Server error');
111
+ res.statusCode = 500
112
+ res.end('Server error')
74
113
  }
75
- });
114
+ })
76
115
 
77
116
  return new Promise((resolve, reject) => {
78
- server.on('error', (err) => reject(err));
117
+ server.on('error', (err) => reject(err))
79
118
  server.listen(port, () => {
80
- console.log(chalk.green(`Server started at http://localhost:${port}`));
81
- console.log(chalk.green(`Serving web/ and output/data/`));
82
- resolve(server);
83
- });
84
- });
119
+ const url = `http://localhost:${port}`
120
+ console.log(chalk.green(`Server started at ${url}`))
121
+ console.log(chalk.green(`Serving web/ and output/data/`))
122
+
123
+ // ====== 自动打开浏览器 ======
124
+ openBrowser(url)
125
+ resolve(server)
126
+ })
127
+ })
85
128
  }
86
129
 
87
- export default startServer;
130
+ export default startServer
package/web/app.js CHANGED
@@ -1,4 +1,3 @@
1
- /* global Chart */
2
1
  /* eslint-disable import/no-absolute-path */
3
2
  const formatDate = (d) => new Date(d).toLocaleString();
4
3
 
@@ -19,38 +18,103 @@ async function loadData() {
19
18
  }
20
19
  }
21
20
 
22
- function renderCommitsTable(commits) {
21
+ let commitsAll = [];
22
+ let filtered = [];
23
+ let page = 1;
24
+ let pageSize = 10;
25
+
26
+ function renderCommitsTablePage() {
23
27
  const tbody = document.querySelector('#commitsTable tbody');
24
28
  tbody.innerHTML = '';
25
- commits.forEach((c) => {
29
+ const start = (page - 1) * pageSize;
30
+ const end = start + pageSize;
31
+ filtered.slice(start, end).forEach((c) => {
26
32
  const tr = document.createElement('tr');
27
33
  tr.innerHTML = `<td>${c.hash.slice(0, 8)}</td><td>${c.author}</td><td>${formatDate(c.date)}</td><td>${c.message}</td>`;
28
34
  tbody.appendChild(tr);
29
35
  });
30
36
  }
31
37
 
38
+ function updatePager() {
39
+ const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
40
+ if (page > totalPages) page = totalPages;
41
+ const pageInfo = document.getElementById('pageInfo');
42
+ pageInfo.textContent = `${page} / ${totalPages}`;
43
+ document.getElementById('prevPage').disabled = page <= 1;
44
+ document.getElementById('nextPage').disabled = page >= totalPages;
45
+ }
46
+
47
+ function applySearch() {
48
+ const q = document.getElementById('searchInput').value.trim().toLowerCase();
49
+ if (!q) {
50
+ filtered = commitsAll.slice();
51
+ } else {
52
+ filtered = commitsAll.filter((c) => {
53
+ const h = c.hash.toLowerCase();
54
+ const a = String(c.author || '').toLowerCase();
55
+ const m = String(c.message || '').toLowerCase();
56
+ const d = formatDate(c.date).toLowerCase();
57
+ return h.includes(q) || a.includes(q) || m.includes(q) || d.includes(q);
58
+ });
59
+ }
60
+ page = 1;
61
+ updatePager();
62
+ renderCommitsTablePage();
63
+ }
64
+
65
+ function initTableControls() {
66
+ document.getElementById('searchInput').addEventListener('input', applySearch);
67
+ document.getElementById('pageSize').addEventListener('change', (e) => {
68
+ pageSize = parseInt(e.target.value, 10) || 10;
69
+ page = 1;
70
+ updatePager();
71
+ renderCommitsTablePage();
72
+ });
73
+ document.getElementById('prevPage').addEventListener('click', () => {
74
+ if (page > 1) {
75
+ page -= 1;
76
+ updatePager();
77
+ renderCommitsTablePage();
78
+ }
79
+ });
80
+ document.getElementById('nextPage').addEventListener('click', () => {
81
+ const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
82
+ if (page < totalPages) {
83
+ page += 1;
84
+ updatePager();
85
+ renderCommitsTablePage();
86
+ }
87
+ });
88
+ }
89
+
32
90
  function drawHourlyOvertime(stats) {
33
- const ctx = document.getElementById('hourlyOvertimeChart').getContext('2d');
91
+ const el = document.getElementById('hourlyOvertimeChart');
92
+ const chart = echarts.init(el);
34
93
  const data = stats.hourlyOvertimeCommits || [];
35
94
  const labels = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0'));
36
- const chartHourly = new Chart(ctx, {
37
- type: 'bar',
38
- data: { labels, datasets: [{ label: 'Overtime commits', data }] },
39
- options: { responsive: true }
95
+ chart.setOption({
96
+ tooltip: {},
97
+ xAxis: { type: 'category', data: labels },
98
+ yAxis: { type: 'value' },
99
+ series: [{ type: 'bar', name: 'Overtime commits', data }]
40
100
  });
41
- return chartHourly;
101
+ return chart;
42
102
  }
43
103
 
44
104
  function drawOutsideVsInside(stats) {
45
- const ctx = document.getElementById('outsideVsInsideChart').getContext('2d');
105
+ const el = document.getElementById('outsideVsInsideChart');
106
+ const chart = echarts.init(el);
46
107
  const outside = stats.outsideWorkCount || 0;
47
108
  const total = stats.total || 0;
48
109
  const inside = Math.max(0, total - outside);
49
- const chartPie = new Chart(ctx, {
50
- type: 'pie',
51
- data: { labels: ['Inside work', 'Outside work'], datasets: [{ data: [inside, outside], backgroundColor: ['#36a2eb', '#ff6384'] }] },
110
+ chart.setOption({
111
+ tooltip: {},
112
+ series: [{ type: 'pie', radius: '55%', data: [
113
+ { value: inside, name: '工作时间内' },
114
+ { value: outside, name: '下班时间' }
115
+ ] }]
52
116
  });
53
- return chartPie;
117
+ return chart;
54
118
  }
55
119
 
56
120
  function drawDailyTrend(commits) {
@@ -61,42 +125,45 @@ function drawDailyTrend(commits) {
61
125
  });
62
126
  const labels = Array.from(map.keys()).sort();
63
127
  const data = labels.map(l => map.get(l));
64
- const ctx = document.getElementById('dailyTrendChart').getContext('2d');
65
- const chartDaily = new Chart(ctx, {
66
- type: 'line',
67
- data: { labels, datasets: [{ label: 'Commits per day', data, fill: true, tension: 0.3 }] },
128
+ const el = document.getElementById('dailyTrendChart');
129
+ const chart = echarts.init(el);
130
+ chart.setOption({
131
+ tooltip: {},
132
+ xAxis: { type: 'category', data: labels },
133
+ yAxis: { type: 'value' },
134
+ series: [{ type: 'line', name: '每日提交', data, areaStyle: {} }]
68
135
  });
69
- return chartDaily;
136
+ return chart;
70
137
  }
71
138
 
72
139
  function drawWeeklyTrend(weekly) {
73
140
  const labels = weekly.map(w => w.period);
74
141
  const dataRate = weekly.map(w => +(w.outsideWorkRate * 100).toFixed(1));
75
142
  const dataCount = weekly.map(w => w.outsideWorkCount);
76
- const ctx = document.getElementById('weeklyTrendChart').getContext('2d');
77
- const chartWeekly = new Chart(ctx, {
78
- type: 'line',
79
- data: {
80
- labels,
81
- datasets: [
82
- { label: '加班占比(%)', data: dataRate, borderColor: '#ff6384', yAxisID: 'y1' },
83
- { label: '加班次数', data: dataCount, borderColor: '#36a2eb', yAxisID: 'y2' },
84
- ]
85
- },
86
- options: {
87
- responsive: true,
88
- scales: {
89
- y1: { type: 'linear', position: 'left', min: 0, max: 100 },
90
- y2: { type: 'linear', position: 'right' }
91
- }
92
- }
143
+ const el = document.getElementById('weeklyTrendChart');
144
+ const chart = echarts.init(el);
145
+ chart.setOption({
146
+ tooltip: {},
147
+ xAxis: { type: 'category', data: labels },
148
+ yAxis: [
149
+ { type: 'value', min: 0, max: 100 },
150
+ { type: 'value' }
151
+ ],
152
+ series: [
153
+ { type: 'line', name: '加班占比(%)', data: dataRate, yAxisIndex: 0 },
154
+ { type: 'line', name: '加班次数', data: dataCount, yAxisIndex: 1 }
155
+ ]
93
156
  });
94
- return chartWeekly;
157
+ return chart;
95
158
  }
96
159
 
97
160
  (async function main() {
98
161
  const { commits, stats, weekly } = await loadData();
99
- renderCommitsTable(commits);
162
+ commitsAll = commits;
163
+ filtered = commitsAll.slice();
164
+ initTableControls();
165
+ updatePager();
166
+ renderCommitsTablePage();
100
167
  drawHourlyOvertime(stats);
101
168
  drawOutsideVsInside(stats);
102
169
  drawDailyTrend(commits);
package/web/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1" />
6
6
  <title>wukong-gitlog-cli — Overtime Dashboard</title>
7
7
  <link rel="stylesheet" href="/style.css" />
8
- <script src="./static/chart.js"></script>
8
+ <script src="./static/echarts.min.js"></script>
9
9
  </head>
10
10
  <body>
11
11
  <header>
@@ -15,28 +15,43 @@
15
15
  <section id="chartsHalf">
16
16
  <div class="chart-card">
17
17
  <h2>下班时间 vs 工作时间提交占比</h2>
18
- <canvas id="outsideVsInsideChart"></canvas>
18
+ <div id="outsideVsInsideChart" class="echart"></div>
19
19
  </div>
20
20
  <div class="chart-card"></div>
21
21
  </section>
22
22
  <section id="charts">
23
23
  <div class="chart-card">
24
24
  <h2>每小时加班分布 (小时 -> 提交数)</h2>
25
- <canvas id="hourlyOvertimeChart"></canvas>
25
+ <div id="hourlyOvertimeChart" class="echart"></div>
26
26
  </div>
27
27
 
28
28
  <div class="chart-card">
29
29
  <h2>按日提交趋势</h2>
30
- <canvas id="dailyTrendChart"></canvas>
30
+ <div id="dailyTrendChart" class="echart"></div>
31
31
  </div>
32
32
  <div class="chart-card">
33
33
  <h2>每周趋势(加班占比)</h2>
34
- <canvas id="weeklyTrendChart"></canvas>
34
+ <div id="weeklyTrendChart" class="echart"></div>
35
35
  </div>
36
36
  </section>
37
37
 
38
38
  <section class="table-card">
39
39
  <h2>提交清单</h2>
40
+ <div id="tableControls">
41
+ <input id="searchInput" type="search" placeholder="搜索作者/信息/Hash" />
42
+ <label for="pageSize">每页显示</label>
43
+ <select id="pageSize">
44
+ <option value="10">10</option>
45
+ <option value="20">20</option>
46
+ <option value="50">50</option>
47
+ <option value="100">100</option>
48
+ </select>
49
+ <div class="pager">
50
+ <button id="prevPage">上一页</button>
51
+ <span id="pageInfo"></span>
52
+ <button id="nextPage">下一页</button>
53
+ </div>
54
+ </div>
40
55
  <table id="commitsTable">
41
56
  <thead>
42
57
  <tr>