@mindbase/node-tools 1.1.0
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/LICENSE +21 -0
- package/bin/clear.js +74 -0
- package/bin/git-log.js +189 -0
- package/package.json +42 -0
- package/src/clear/cleaner.js +68 -0
- package/src/clear/config.js +130 -0
- package/src/clear/scanner.js +75 -0
- package/src/clear/ui.js +353 -0
- package/src/common/ui/ansi.js +49 -0
- package/src/common/ui/pagination.js +63 -0
- package/src/common/ui/screen.js +56 -0
- package/src/common/ui/single-select.js +80 -0
- package/src/common/ui/utils.js +80 -0
- package/src/git-log/ui.js +309 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Log UI 模块
|
|
3
|
+
* 使用 prompts + chalk + common/ui 模块实现
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const prompts = require('prompts');
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const fs = require('fs').promises;
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const readline = require('readline');
|
|
12
|
+
|
|
13
|
+
// Common UI 模块
|
|
14
|
+
const { singleSelect } = require('../common/ui/single-select.js');
|
|
15
|
+
const { enterAlternateScreen, exitAlternateScreen } = require('../common/ui/ansi.js');
|
|
16
|
+
const { ScreenRenderer } = require('../common/ui/screen.js');
|
|
17
|
+
const { PagedDisplay } = require('../common/ui/pagination.js');
|
|
18
|
+
|
|
19
|
+
// 配置文件路径
|
|
20
|
+
const CONFIG_PATH = path.join(os.homedir(), '.git-log-config.json');
|
|
21
|
+
|
|
22
|
+
// 加载保存的作者配置
|
|
23
|
+
async function loadConfig () {
|
|
24
|
+
try {
|
|
25
|
+
const content = await fs.readFile(CONFIG_PATH, 'utf-8');
|
|
26
|
+
const config = JSON.parse(content);
|
|
27
|
+
return config.lastAuthor || '';
|
|
28
|
+
} catch {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 保存作者配置
|
|
34
|
+
async function saveConfig (author) {
|
|
35
|
+
try {
|
|
36
|
+
let config = {};
|
|
37
|
+
try {
|
|
38
|
+
const content = await fs.readFile(CONFIG_PATH, 'utf-8');
|
|
39
|
+
config = JSON.parse(content);
|
|
40
|
+
} catch {}
|
|
41
|
+
config.lastAuthor = author;
|
|
42
|
+
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// 忽略保存错误
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 日期格式转换:YYYYMMDD -> YYYY-MM-DD
|
|
49
|
+
function formatDateForGit (dateStr) {
|
|
50
|
+
if (!dateStr || dateStr.length !== 8) return dateStr;
|
|
51
|
+
return `${dateStr.substring(0, 4)}-${dateStr.substring(4, 6)}-${dateStr.substring(6, 8)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 格式化日期为 YYYYMMDD
|
|
55
|
+
function toYYYYMMDD (date) {
|
|
56
|
+
const year = date.getFullYear();
|
|
57
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
58
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
59
|
+
return `${year}${month}${day}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 计算上周日到今天的日期范围
|
|
63
|
+
function getDefaultDateRange () {
|
|
64
|
+
const now = new Date();
|
|
65
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);
|
|
66
|
+
|
|
67
|
+
// 计算上周日
|
|
68
|
+
const dayOfWeek = now.getDay(); // 0 = 周日, 1 = 周一, ...
|
|
69
|
+
const daysToLastSunday = dayOfWeek + 7;
|
|
70
|
+
const lastSunday = new Date(now);
|
|
71
|
+
lastSunday.setDate(now.getDate() - daysToLastSunday);
|
|
72
|
+
lastSunday.setHours(0, 0, 0, 0);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
since: toYYYYMMDD(lastSunday),
|
|
76
|
+
until: toYYYYMMDD(today)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 输入筛选条件
|
|
82
|
+
* @param {Array} repos - 仓库列表 [{name, path}]
|
|
83
|
+
* @returns {Promise<object>} 过滤器条件 {author, since, until, selectedRepos}
|
|
84
|
+
*/
|
|
85
|
+
async function inputFilters (repos) {
|
|
86
|
+
// 加载保存的作者
|
|
87
|
+
const savedAuthor = await loadConfig();
|
|
88
|
+
|
|
89
|
+
// 获取默认日期范围
|
|
90
|
+
const defaultDates = getDefaultDateRange();
|
|
91
|
+
|
|
92
|
+
// 初始化过滤器
|
|
93
|
+
const filters = {
|
|
94
|
+
author: savedAuthor,
|
|
95
|
+
since: defaultDates.since,
|
|
96
|
+
until: defaultDates.until,
|
|
97
|
+
selectedRepos: repos.map(r => r.path) // 默认选中所有
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
// 仓库选择
|
|
102
|
+
const repoItems = [
|
|
103
|
+
{ title: `[全部] (${repos.length} 个仓库)`, value: 'all' },
|
|
104
|
+
...repos.map(r => ({ title: r.name, value: r.path }))
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
const selectedRepo = await singleSelect(repoItems, {
|
|
108
|
+
title: chalk.bold.white(`[Git 日志查看器] (找到 ${repos.length} 个仓库)`),
|
|
109
|
+
hint: '↑↓ 移动 | Enter 确认 | Esc 退出'
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (selectedRepo === null) {
|
|
113
|
+
return null; // 用户取消
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (selectedRepo === 'all') {
|
|
117
|
+
filters.selectedRepos = repos.map(r => r.path);
|
|
118
|
+
} else {
|
|
119
|
+
filters.selectedRepos = [selectedRepo];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 作者输入
|
|
123
|
+
const authorResponse = await prompts({
|
|
124
|
+
type: 'text',
|
|
125
|
+
name: 'author',
|
|
126
|
+
message: filters.author ? `作者 (当前: ${filters.author})` : '作者',
|
|
127
|
+
initial: filters.author,
|
|
128
|
+
hint: '留空则不筛选作者'
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (authorResponse.author === undefined) {
|
|
132
|
+
return null; // 用户取消
|
|
133
|
+
}
|
|
134
|
+
filters.author = authorResponse.author.trim();
|
|
135
|
+
|
|
136
|
+
// 开始日期输入
|
|
137
|
+
const sinceResponse = await prompts({
|
|
138
|
+
type: 'text',
|
|
139
|
+
name: 'since',
|
|
140
|
+
message: `开始日期 (前4位年,中间2位月,最后2位日,默认: ${filters.since})`,
|
|
141
|
+
initial: filters.since
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (sinceResponse.since === undefined) {
|
|
145
|
+
return null; // 用户取消
|
|
146
|
+
}
|
|
147
|
+
filters.since = sinceResponse.since.trim();
|
|
148
|
+
|
|
149
|
+
// 结束日期输入
|
|
150
|
+
const untilResponse = await prompts({
|
|
151
|
+
type: 'text',
|
|
152
|
+
name: 'until',
|
|
153
|
+
message: `结束日期 (前4位年,中间2位月,最后2位日,默认: ${filters.until})`,
|
|
154
|
+
initial: filters.until
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (untilResponse.until === undefined) {
|
|
158
|
+
return null; // 用户取消
|
|
159
|
+
}
|
|
160
|
+
filters.until = untilResponse.until.trim();
|
|
161
|
+
|
|
162
|
+
// 保存作者配置
|
|
163
|
+
if (filters.author) {
|
|
164
|
+
await saveConfig(filters.author);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return filters;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (error === 'CTRL_C') {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 等待日志导航按键
|
|
178
|
+
* @returns {Promise<string>} 导航动作
|
|
179
|
+
*/
|
|
180
|
+
function waitForLogNavigation () {
|
|
181
|
+
return new Promise((resolve) => {
|
|
182
|
+
readline.emitKeypressEvents(process.stdin);
|
|
183
|
+
process.stdin.resume();
|
|
184
|
+
process.stdin.setRawMode(true);
|
|
185
|
+
|
|
186
|
+
const handler = (_str, key) => {
|
|
187
|
+
if (key.name === 'n' || key.name === 'right' || key.name === 'down' || key.name === 'pagedown') {
|
|
188
|
+
cleanup();
|
|
189
|
+
resolve('next');
|
|
190
|
+
} else if (key.name === 'p' || key.name === 'left' || key.name === 'up' || key.name === 'pageup') {
|
|
191
|
+
cleanup();
|
|
192
|
+
resolve('prev');
|
|
193
|
+
} else if (key.name === 'f' || key.name === 'F') {
|
|
194
|
+
cleanup();
|
|
195
|
+
resolve('refilter');
|
|
196
|
+
} else if (key.name === 'q' || key.name === 'Q' || key.name === 'escape' || (key.name === 'c' && key.ctrl)) {
|
|
197
|
+
cleanup();
|
|
198
|
+
resolve('exit');
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const cleanup = () => {
|
|
203
|
+
process.stdin.removeListener('keypress', handler);
|
|
204
|
+
process.stdin.setRawMode(false);
|
|
205
|
+
process.stdin.pause();
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
process.stdin.on('keypress', handler);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 渲染日志页内容
|
|
214
|
+
* @param {object} page - 当前页数据
|
|
215
|
+
* @param {number} totalCount - 总记录数
|
|
216
|
+
* @returns {string} 渲染内容
|
|
217
|
+
*/
|
|
218
|
+
function renderLogPage (page, totalCount) {
|
|
219
|
+
const header = chalk.bold.white(
|
|
220
|
+
`[Git 日志] (共 ${totalCount} 条,第 ${page.pageIndex + 1}/${page.totalPages} 页)\n\n`
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const hint = chalk.dim(
|
|
224
|
+
'操作: ↑/↓/PageUp/PageDown/n/p 翻页 | f 重新筛选 | q 退出\n'
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// 格式化日志
|
|
228
|
+
const logContent = page.items.map(log => {
|
|
229
|
+
const date = new Date(log.date).toLocaleString('zh-CN');
|
|
230
|
+
const maxLength = 80;
|
|
231
|
+
const subject = log.subject.length > maxLength ? log.subject.substring(0, maxLength - 3) + '...' : log.subject;
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
chalk.yellow(`[${log.repo}]`) + '\n' +
|
|
235
|
+
chalk.bold.yellow(` 提交: ${log.hash.substring(0, 8)}`) + '\n' +
|
|
236
|
+
chalk.cyan(` 作者: ${log.authorName}`) + '\n' +
|
|
237
|
+
chalk.green(` 日期: ${date}`) + '\n' +
|
|
238
|
+
chalk.white(` 信息: ${subject}`) + '\n'
|
|
239
|
+
);
|
|
240
|
+
}).join('\n');
|
|
241
|
+
|
|
242
|
+
return header + hint + logContent;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 计算动态分页大小
|
|
247
|
+
* @returns {number} 每页显示条数
|
|
248
|
+
*/
|
|
249
|
+
function calculateDynamicPageSize () {
|
|
250
|
+
const terminalHeight = process.stdout.rows || 24;
|
|
251
|
+
const reservedLines = 6; // 头部3行 + 提示2行 + 底部1行
|
|
252
|
+
const availableLines = Math.max(terminalHeight - reservedLines, 10);
|
|
253
|
+
const linesPerLog = 5; // 每条日志约5行
|
|
254
|
+
return Math.floor(availableLines / linesPerLog);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 显示日志(支持重新筛选)
|
|
259
|
+
* @param {array} logs - 日志列表
|
|
260
|
+
* @param {function} onRefilter - 重新筛选的回调
|
|
261
|
+
* @returns {Promise<void>}
|
|
262
|
+
*/
|
|
263
|
+
async function displayLogs (logs, onRefilter) {
|
|
264
|
+
if (logs.length === 0) {
|
|
265
|
+
console.log(chalk.yellow('\n没有找到符合条件的日志记录'));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
enterAlternateScreen();
|
|
270
|
+
const renderer = new ScreenRenderer();
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
// 创建分页显示(动态计算每页条数)
|
|
274
|
+
const pageSize = calculateDynamicPageSize();
|
|
275
|
+
const pagedDisplay = new PagedDisplay(logs, pageSize);
|
|
276
|
+
|
|
277
|
+
// 主循环
|
|
278
|
+
while (true) {
|
|
279
|
+
const currentPage = pagedDisplay.getCurrentPage();
|
|
280
|
+
|
|
281
|
+
// 渲染当前页
|
|
282
|
+
const content = renderLogPage(currentPage, logs.length);
|
|
283
|
+
renderer.render(content);
|
|
284
|
+
|
|
285
|
+
// 等待导航
|
|
286
|
+
const action = await waitForLogNavigation();
|
|
287
|
+
|
|
288
|
+
if (action === 'next') {
|
|
289
|
+
pagedDisplay.nextPage();
|
|
290
|
+
} else if (action === 'prev') {
|
|
291
|
+
pagedDisplay.prevPage();
|
|
292
|
+
} else if (action === 'refilter') {
|
|
293
|
+
exitAlternateScreen();
|
|
294
|
+
onRefilter();
|
|
295
|
+
return;
|
|
296
|
+
} else if (action === 'exit') {
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} finally {
|
|
301
|
+
exitAlternateScreen();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
module.exports = {
|
|
306
|
+
inputFilters,
|
|
307
|
+
displayLogs,
|
|
308
|
+
formatDateForGit
|
|
309
|
+
};
|