@mindbase/node-tools 1.1.0 → 1.3.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/src/clear/ui.js CHANGED
@@ -3,14 +3,12 @@
3
3
  * 使用 prompts + chalk + cli-table3 + treeify 实现
4
4
  */
5
5
 
6
- const prompts = require('prompts');
7
6
  const chalk = require('chalk');
8
- const Table = require('cli-table3');
9
7
  const treeify = require('treeify');
10
- const { enterAlternateScreen, exitAlternateScreen } = require('../common/ui/ansi.js');
11
- const { ScreenRenderer } = require('../common/ui/screen.js');
8
+ const { withScreenSession } = require('../common/ui/screen.js');
12
9
  const { PagedDisplay } = require('../common/ui/pagination.js');
13
- const { waitForAnyKey } = require('../common/ui/utils.js');
10
+ const { waitForAnyKey, waitForConfirm, waitForNavigation } = require('../common/ui/utils.js');
11
+ const { clean } = require('./cleaner.js');
14
12
  const readline = require('readline');
15
13
 
16
14
  /**
@@ -24,60 +22,12 @@ function formatItemName(item) {
24
22
  }
25
23
 
26
24
  /**
27
- * 等待导航按键(分页场景)
28
- */
29
- function waitForNavigation() {
30
- return new Promise((resolve) => {
31
- readline.emitKeypressEvents(process.stdin);
32
-
33
- if (process.stdin.isPaused()) {
34
- process.stdin.resume();
35
- process.stdin.setRawMode(true);
36
- }
37
-
38
- const handler = (_str, key) => {
39
- if (key.name === 'n' || key.name === 'down' || key.name === 'pagedown') {
40
- cleanup();
41
- resolve('next');
42
- } else if (key.name === 'p' || key.name === 'up' || key.name === 'pageup') {
43
- cleanup();
44
- resolve('prev');
45
- } else if (key.name === 'return') {
46
- cleanup();
47
- resolve('confirm');
48
- } else if ((key.name === 'c' && key.ctrl) || key.name === 'escape') {
49
- cleanup();
50
- resolve('exit');
51
- }
52
- };
53
-
54
- const cleanup = () => {
55
- process.stdin.removeListener('keypress', handler);
56
- process.stdin.setRawMode(false);
57
- process.stdin.pause();
58
- };
59
-
60
- process.stdin.on('keypress', handler);
61
- });
62
- }
63
-
64
- /**
65
- * 显示多选菜单
25
+ * 内部:显示多选菜单(接收 renderer)
26
+ * @param {Object} renderer - 屏幕渲染器
66
27
  * @param {Array} items - 可选项数组
67
28
  * @returns {Promise<Array>} 选中的项
68
29
  */
69
- async function selectItems(items) {
70
- if (items.length === 0) {
71
- console.log(chalk.yellow('没有找到可清理的内容'));
72
- return [];
73
- }
74
-
75
- // 进入备用屏幕
76
- enterAlternateScreen();
77
-
78
- // 创建渲染器
79
- const renderer = new ScreenRenderer();
80
-
30
+ async function selectItemsInternal(renderer, items) {
81
31
  // 分页显示
82
32
  const usePagination = items.length > 20;
83
33
  const pageSize = 15;
@@ -92,80 +42,83 @@ async function selectItems(items) {
92
42
  // 选中状态追踪(默认全选)
93
43
  const selectedIndices = new Set(items.map((_, idx) => idx));
94
44
 
95
- try {
96
- // 主循环
97
- while (true) {
98
- // 渲染当前页面
99
- const header = chalk.bold.white(
100
- `找到 ${items.length} 个可清理的项目 (已选: ${selectedIndices.size})\n`
101
- );
102
-
103
- const hint = chalk.dim(
104
- '↑↓ 移动 | 空格 选择 | Enter 确认' +
105
- (usePagination ? ' | PageUp/PageDown/n/p 翻页' : '') +
106
- ' | CTRL_C 退出\n'
107
- );
108
-
109
- // 构建选项列表
110
- const choices = currentPageItems.map((item) => {
111
- const globalIndex = items.indexOf(item);
112
- return {
113
- title: formatItemName(item),
114
- value: globalIndex,
115
- selected: selectedIndices.has(globalIndex),
116
- };
117
- });
45
+ // 主循环
46
+ while (true) {
47
+ // 渲染当前页面
48
+ const header = chalk.bold.white(
49
+ `找到 ${items.length} 个可清理的项目 (已选: ${selectedIndices.size})\n`
50
+ );
118
51
 
119
- // 使用 readline 实现自定义多选
120
- const result = await customMultiselect(choices, selectedIndices, header + hint);
52
+ const hint = chalk.dim(
53
+ '↑↓ 移动 | 空格 选择 | Enter 确认' +
54
+ (usePagination ? ' | PageUp/PageDown/n/p 翻页' : '') +
55
+ ' | CTRL_C 退出\n'
56
+ );
121
57
 
122
- // 处理退出
123
- if (result === 'exit') {
124
- exitAlternateScreen();
125
- return [];
126
- }
58
+ // 构建选项列表
59
+ const choices = currentPageItems.map((item) => {
60
+ const globalIndex = items.indexOf(item);
61
+ return {
62
+ title: formatItemName(item),
63
+ value: globalIndex,
64
+ selected: selectedIndices.has(globalIndex),
65
+ };
66
+ });
127
67
 
128
- // 更新选中状态
129
- result.newSelections.forEach((idx) => selectedIndices.add(idx));
130
- result.deselections.forEach((idx) => selectedIndices.delete(idx));
131
-
132
- // 分页处理
133
- if (usePagination) {
134
- const navigation = await waitForNavigation();
135
-
136
- if (navigation === 'next') {
137
- pagedDisplay.nextPage();
138
- currentPageItems = pagedDisplay.getCurrentPage().items;
139
- renderer.clear();
140
- continue;
141
- } else if (navigation === 'prev') {
142
- pagedDisplay.prevPage();
143
- currentPageItems = pagedDisplay.getCurrentPage().items;
144
- renderer.clear();
145
- continue;
146
- } else if (navigation === 'exit') {
147
- exitAlternateScreen();
148
- return [];
149
- } else if (navigation === 'confirm') {
150
- break;
151
- }
152
- } else {
68
+ // 使用 readline 实现自定义多选
69
+ const result = await customMultiselect(choices, selectedIndices, header + hint);
70
+
71
+ // 处理退出
72
+ if (result === 'exit') {
73
+ return [];
74
+ }
75
+
76
+ // 更新选中状态
77
+ result.newSelections.forEach((idx) => selectedIndices.add(idx));
78
+ result.deselections.forEach((idx) => selectedIndices.delete(idx));
79
+
80
+ // 分页处理
81
+ if (usePagination) {
82
+ const navigation = await waitForNavigation();
83
+
84
+ if (navigation === 'next') {
85
+ pagedDisplay.nextPage();
86
+ currentPageItems = pagedDisplay.getCurrentPage().items;
87
+ renderer.clear();
88
+ continue;
89
+ } else if (navigation === 'prev') {
90
+ pagedDisplay.prevPage();
91
+ currentPageItems = pagedDisplay.getCurrentPage().items;
92
+ renderer.clear();
93
+ continue;
94
+ } else if (navigation === 'exit') {
95
+ return [];
96
+ } else if (navigation === 'confirm') {
153
97
  break;
154
98
  }
99
+ } else {
100
+ break;
155
101
  }
102
+ }
156
103
 
157
- // 退出备用屏幕
158
- exitAlternateScreen();
104
+ // 返回选中项目
105
+ return Array.from(selectedIndices).map((idx) => items[idx]);
106
+ }
159
107
 
160
- // 返回选中项目
161
- return Array.from(selectedIndices).map((idx) => items[idx]);
162
- } catch (error) {
163
- exitAlternateScreen();
164
- if (error === 'CTRL_C') {
165
- return [];
166
- }
167
- throw error;
108
+ /**
109
+ * 显示多选菜单
110
+ * @param {Array} items - 可选项数组
111
+ * @returns {Promise<Array>} 选中的项
112
+ */
113
+ async function selectItems(items) {
114
+ if (items.length === 0) {
115
+ console.log(chalk.yellow('没有找到可清理的内容'));
116
+ return [];
168
117
  }
118
+
119
+ return withScreenSession(async (renderer) => {
120
+ return await selectItemsInternal(renderer, items);
121
+ });
169
122
  }
170
123
 
171
124
  /**
@@ -235,119 +188,145 @@ function customMultiselect(choices, currentSelected, titleText = '') {
235
188
  }
236
189
 
237
190
  /**
238
- * 显示删除预览
191
+ * 内部:显示删除预览(接收 renderer)
192
+ * @param {Object} renderer - 屏幕渲染器
239
193
  * @param {Array} items - 选中的项
240
194
  * @returns {Promise<boolean>} 是否确认删除
241
195
  */
242
- async function confirmDelete(items) {
196
+ async function confirmDeleteInternal(renderer, items) {
243
197
  if (items.length === 0) {
244
198
  return false;
245
199
  }
246
200
 
247
- enterAlternateScreen();
248
- const renderer = new ScreenRenderer();
249
-
250
- try {
251
- // 创建表格
252
- const table = new Table({
253
- head: [chalk.bold('类型'), chalk.bold('路径')],
254
- colWidths: [10, 70],
255
- wordWrap: true,
256
- });
257
-
258
- // 填充数据
259
- items.forEach((item) => {
260
- const icon =
261
- item.type === 'dir' ? chalk.cyan('📁 目录') : chalk.white('📄 文件');
262
- table.push([icon, item.relativePath]);
263
- });
201
+ // 构建简单列表
202
+ let content = chalk.bold.red(`\n即将删除以下 ${items.length} 项:\n\n`);
264
203
 
265
- // 渲染预览
266
- const header = chalk.bold.red(`\n即将删除以下 ${items.length} 项:\n`);
267
- const content = header + table.toString();
204
+ items.forEach((item) => {
205
+ const icon = item.type === 'dir' ? '📁' : '📄';
206
+ const coloredPath =
207
+ item.type === 'dir' ? chalk.cyan(item.relativePath) : chalk.white(item.relativePath);
208
+ content += ` ${icon} ${coloredPath}\n`;
209
+ });
268
210
 
269
- renderer.render(content);
211
+ // 操作提示
212
+ content += '\n' + chalk.dim('按 y/Enter 确认,n/Esc 取消...');
270
213
 
271
- // 确认
272
- const response = await prompts({
273
- type: 'confirm',
274
- name: 'confirm',
275
- message: chalk.yellow('确认删除?'),
276
- initial: false,
277
- });
214
+ renderer.render(content);
278
215
 
279
- exitAlternateScreen();
216
+ // 等待确认
217
+ return await waitForConfirm();
218
+ }
280
219
 
281
- return response.confirm === true;
282
- } catch (error) {
283
- exitAlternateScreen();
284
- if (error === 'CTRL_C') {
285
- return false;
286
- }
287
- throw error;
220
+ /**
221
+ * 显示删除预览
222
+ * @param {Array} items - 选中的项
223
+ * @returns {Promise<boolean>} 是否确认删除
224
+ */
225
+ async function confirmDelete(items) {
226
+ if (items.length === 0) {
227
+ return false;
288
228
  }
229
+
230
+ return withScreenSession(async (renderer) => {
231
+ return await confirmDeleteInternal(renderer, items);
232
+ });
289
233
  }
290
234
 
291
235
  /**
292
- * 显示删除结果
236
+ * 内部:显示删除结果(接收 renderer)
237
+ * @param {Object} renderer - 屏幕渲染器
293
238
  * @param {Object} stats - 删除统计
294
239
  */
295
- async function showResult(stats) {
296
- enterAlternateScreen();
297
- const renderer = new ScreenRenderer();
298
-
299
- try {
300
- // 统计表格
301
- const summaryTable = new Table({
302
- colWidths: [20, 10],
240
+ async function showResultInternal(renderer, stats) {
241
+ const total = stats.success + stats.failed;
242
+ let content =
243
+ chalk.bold.white('\n删除完成\n\n') +
244
+ ` 总处理数: ${chalk.bold(total)}\n` +
245
+ ` ${chalk.green('成功')}: ${chalk.bold.green(stats.success)}\n` +
246
+ ` ${chalk.red('失败')}: ${chalk.bold.red(stats.failed)}\n`;
247
+
248
+ // 失败详情
249
+ if (stats.failed > 0) {
250
+ const failedItems = stats.details.filter((d) => d.status === 'failed');
251
+
252
+ content += '\n' + chalk.bold.red('失败详情:\n');
253
+
254
+ // 使用 treeify 构建目录树
255
+ const tree = {};
256
+ failedItems.forEach((item) => {
257
+ const parts = item.relativePath.split(/[/\\]/);
258
+ let current = tree;
259
+
260
+ parts.forEach((part, idx) => {
261
+ if (!current[part]) {
262
+ current[part] = idx === parts.length - 1 ? null : {};
263
+ }
264
+ if (current[part] !== null) {
265
+ current = current[part];
266
+ }
267
+ });
303
268
  });
304
269
 
305
- summaryTable.push(
306
- [chalk.white('总处理数'), chalk.bold(stats.success + stats.failed)],
307
- [chalk.green('成功'), chalk.bold.green(stats.success)],
308
- [chalk.red('失败'), chalk.bold.red(stats.failed)]
309
- );
270
+ content += '\n' + chalk.red(treeify.asTree(tree, true)) + '\n';
271
+ }
310
272
 
311
- let content = chalk.bold.white('\n删除完成:\n\n') + summaryTable.toString();
273
+ // 操作提示
274
+ content += '\n' + chalk.dim('按任意键继续...');
312
275
 
313
- // 失败详情
314
- if (stats.failed > 0) {
315
- const failedItems = stats.details.filter((d) => d.status === 'failed');
276
+ renderer.render(content);
316
277
 
317
- content += '\n\n' + chalk.bold.red('失败详情:\n');
278
+ // 等待按键
279
+ await waitForAnyKey();
280
+ }
318
281
 
319
- // 使用 treeify 构建目录树
320
- const tree = {};
321
- failedItems.forEach((item) => {
322
- const parts = item.relativePath.split(/[/\\]/);
323
- let current = tree;
282
+ /**
283
+ * 显示删除结果
284
+ * @param {Object} stats - 删除统计
285
+ */
286
+ async function showResult(stats) {
287
+ await withScreenSession(async (renderer) => {
288
+ await showResultInternal(renderer, stats);
289
+ });
290
+ }
324
291
 
325
- parts.forEach((part, idx) => {
326
- if (!current[part]) {
327
- current[part] = idx === parts.length - 1 ? null : {};
328
- }
329
- if (current[part] !== null) {
330
- current = current[part];
331
- }
332
- });
333
- });
292
+ /**
293
+ * 运行清理流程(全程备用屏幕)
294
+ * @param {string} targetPath - 目标路径
295
+ * @param {Array} items - 扫描到的项目
296
+ * @returns {Promise<Object|null>} 删除统计,取消时返回 null
297
+ */
298
+ async function runCleanFlow(targetPath, items) {
299
+ return withScreenSession(async (renderer) => {
300
+ // 显示扫描信息
301
+ let content = chalk.bold.white(`扫描目录: ${targetPath}\n\n`);
302
+ content += `找到 ${chalk.bold(items.length)} 个可清理的项目\n\n`;
303
+ content += chalk.dim('按任意键继续...');
304
+ renderer.render(content);
305
+ await waitForAnyKey();
334
306
 
335
- content += '\n' + chalk.red(treeify.asTree(tree, true)) + '\n';
307
+ // 选择项目
308
+ const selected = await selectItemsInternal(renderer, items);
309
+ if (selected.length === 0) {
310
+ renderer.render(chalk.yellow('\n未选择任何内容\n按任意键退出...'));
311
+ await waitForAnyKey();
312
+ return null;
336
313
  }
337
314
 
338
- // 操作提示
339
- content += '\n' + chalk.dim('按任意键继续...');
315
+ // 确认删除
316
+ const confirmed = await confirmDeleteInternal(renderer, selected);
317
+ if (!confirmed) {
318
+ return null;
319
+ }
340
320
 
341
- renderer.render(content);
321
+ // 执行删除
322
+ renderer.render(chalk.bold('\n执行删除中...'));
323
+ const stats = clean(selected);
342
324
 
343
- // 等待按键
344
- await waitForAnyKey();
325
+ // 显示结果
326
+ await showResultInternal(renderer, stats);
345
327
 
346
- exitAlternateScreen();
347
- } catch (error) {
348
- exitAlternateScreen();
349
- throw error;
350
- }
328
+ return stats;
329
+ });
351
330
  }
352
331
 
353
- module.exports = { selectItems, confirmDelete, showResult };
332
+ module.exports = { selectItems, confirmDelete, showResult, runCleanFlow };
@@ -3,7 +3,14 @@
3
3
  * 负责屏幕的清屏、重绘等操作
4
4
  */
5
5
 
6
- const { CLEAR_SCREEN, MOVE_CURSOR_HOME, SAVE_CURSOR, RESTORE_CURSOR } = require('./ansi.js');
6
+ const {
7
+ CLEAR_SCREEN,
8
+ MOVE_CURSOR_HOME,
9
+ SAVE_CURSOR,
10
+ RESTORE_CURSOR,
11
+ enterAlternateScreen,
12
+ exitAlternateScreen
13
+ } = require('./ansi.js');
7
14
 
8
15
  class ScreenRenderer {
9
16
  constructor() {
@@ -53,4 +60,21 @@ class ScreenRenderer {
53
60
  }
54
61
  }
55
62
 
56
- module.exports = { ScreenRenderer };
63
+ /**
64
+ * 屏幕会话工厂函数
65
+ * 统一管理备用屏幕的生命周期
66
+ * @param {Function} fn - 在屏幕会话中执行的异步函数,接收 renderer 参数
67
+ * @returns {Promise<any>} fn 的返回值
68
+ */
69
+ async function withScreenSession(fn) {
70
+ enterAlternateScreen();
71
+ const renderer = new ScreenRenderer();
72
+
73
+ try {
74
+ return await fn(renderer);
75
+ } finally {
76
+ exitAlternateScreen();
77
+ }
78
+ }
79
+
80
+ module.exports = { ScreenRenderer, withScreenSession };
@@ -74,7 +74,38 @@ function waitForNavigation() {
74
74
  });
75
75
  }
76
76
 
77
+ /**
78
+ * 等待确认按键
79
+ * @returns {Promise<boolean>} true=确认, false=取消
80
+ */
81
+ function waitForConfirm() {
82
+ return new Promise((resolve) => {
83
+ readline.emitKeypressEvents(process.stdin);
84
+ process.stdin.resume();
85
+ process.stdin.setRawMode(true);
86
+
87
+ const handler = (str, key) => {
88
+ if (key.name === 'return' || str.toLowerCase() === 'y') {
89
+ cleanup();
90
+ resolve(true);
91
+ } else if (key.name === 'escape' || str.toLowerCase() === 'n') {
92
+ cleanup();
93
+ resolve(false);
94
+ }
95
+ };
96
+
97
+ const cleanup = () => {
98
+ process.stdin.removeListener('keypress', handler);
99
+ process.stdin.setRawMode(false);
100
+ process.stdin.pause();
101
+ };
102
+
103
+ process.stdin.on('keypress', handler);
104
+ });
105
+ }
106
+
77
107
  module.exports = {
78
108
  waitForAnyKey,
79
109
  waitForNavigation,
110
+ waitForConfirm,
80
111
  };
package/src/git-log/ui.js CHANGED
@@ -12,8 +12,7 @@ const readline = require('readline');
12
12
 
13
13
  // Common UI 模块
14
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');
15
+ const { withScreenSession } = require('../common/ui/screen.js');
17
16
  const { PagedDisplay } = require('../common/ui/pagination.js');
18
17
 
19
18
  // 配置文件路径
@@ -266,10 +265,7 @@ async function displayLogs (logs, onRefilter) {
266
265
  return;
267
266
  }
268
267
 
269
- enterAlternateScreen();
270
- const renderer = new ScreenRenderer();
271
-
272
- try {
268
+ await withScreenSession(async (renderer) => {
273
269
  // 创建分页显示(动态计算每页条数)
274
270
  const pageSize = calculateDynamicPageSize();
275
271
  const pagedDisplay = new PagedDisplay(logs, pageSize);
@@ -290,16 +286,13 @@ async function displayLogs (logs, onRefilter) {
290
286
  } else if (action === 'prev') {
291
287
  pagedDisplay.prevPage();
292
288
  } else if (action === 'refilter') {
293
- exitAlternateScreen();
294
289
  onRefilter();
295
290
  return;
296
291
  } else if (action === 'exit') {
297
292
  break;
298
293
  }
299
294
  }
300
- } finally {
301
- exitAlternateScreen();
302
- }
295
+ });
303
296
  }
304
297
 
305
298
  module.exports = {
@@ -0,0 +1,61 @@
1
+ /**
2
+ * 构建执行模块
3
+ * 检测和执行构建脚本
4
+ */
5
+
6
+ const { execaCommand } = require('execa');
7
+
8
+ /**
9
+ * 检测构建脚本
10
+ * @param {Object} scripts - package.json 中的 scripts 对象
11
+ * @returns {string|null} 找到的构建脚本名,未找到返回 null
12
+ */
13
+ function detectBuildScripts(scripts) {
14
+ // 按优先级检测构建脚本
15
+ const buildScriptNames = [
16
+ 'build',
17
+ 'compile',
18
+ 'prebuild',
19
+ 'prepublishOnly'
20
+ ];
21
+
22
+ for (const scriptName of buildScriptNames) {
23
+ if (scripts && scripts[scriptName]) {
24
+ return scriptName;
25
+ }
26
+ }
27
+
28
+ return null;
29
+ }
30
+
31
+ /**
32
+ * 执行构建脚本
33
+ * @param {string} pkgPath - 包路径
34
+ * @param {string} scriptName - 脚本名称
35
+ * @returns {Promise<Object>} 执行结果
36
+ */
37
+ async function runBuild(pkgPath, scriptName) {
38
+ try {
39
+ const result = await execaCommand(`npm run ${scriptName}`, {
40
+ cwd: pkgPath,
41
+ stdout: 'inherit',
42
+ stderr: 'inherit'
43
+ });
44
+
45
+ return {
46
+ success: true,
47
+ exitCode: result.exitCode
48
+ };
49
+ } catch (error) {
50
+ return {
51
+ success: false,
52
+ error: error.message,
53
+ exitCode: error.exitCode
54
+ };
55
+ }
56
+ }
57
+
58
+ module.exports = {
59
+ detectBuildScripts,
60
+ runBuild
61
+ };