@mindbase/node-tools 1.3.8 → 1.3.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindbase/node-tools",
3
- "version": "1.3.8",
3
+ "version": "1.3.12",
4
4
  "description": "Node.js 开发工具集合:清理 node_modules、查看 Git 日志、发布包",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -28,7 +28,7 @@
28
28
  "cli-table3": "^0.6.3",
29
29
  "commander": "^11.0.0",
30
30
  "editor": "^1.0.0",
31
- "execa": "^9.6.1",
31
+ "execa": "^8.0.0",
32
32
  "glob": "^13.0.2",
33
33
  "prompts": "^2.4.2",
34
34
  "semver": "^7.7.4",
@@ -1,41 +1,22 @@
1
- const { spawnSync } = require("child_process");
2
- const { platform } = require("process");
1
+ const fs = require("fs");
3
2
 
4
- const isWin = platform === "win32";
5
- let rmAvailable = null;
6
-
7
- // 检测 rm 命令是否可用
8
- function checkRmAvailable() {
9
- if (rmAvailable !== null) return rmAvailable;
10
- const result = spawnSync("rm", ["--version"], { shell: true });
11
- rmAvailable = result.status === 0;
12
- return rmAvailable;
13
- }
14
-
15
- // 执行删除命令
3
+ // 删除目录
16
4
  function removeDir(path) {
17
- if (checkRmAvailable()) {
18
- const result = spawnSync("rm", ["-rf", path], { shell: true, stdio: "inherit" });
19
- return result.status === 0;
20
- } else if (isWin) {
21
- const result = spawnSync("rmdir", ["/S", "/Q", path], { shell: true, stdio: "inherit" });
22
- return result.status === 0;
23
- } else {
24
- const result = spawnSync("rm", ["-rf", path], { shell: true, stdio: "inherit" });
25
- return result.status === 0;
5
+ try {
6
+ fs.rmSync(path, { recursive: true, force: true });
7
+ return true;
8
+ } catch {
9
+ return false;
26
10
  }
27
11
  }
28
12
 
13
+ // 删除文件
29
14
  function removeFile(path) {
30
- if (checkRmAvailable()) {
31
- const result = spawnSync("rm", [path], { shell: true, stdio: "inherit" });
32
- return result.status === 0;
33
- } else if (isWin) {
34
- const result = spawnSync("del", [path], { shell: true, stdio: "inherit" });
35
- return result.status === 0;
36
- } else {
37
- const result = spawnSync("rm", [path], { shell: true, stdio: "inherit" });
38
- return result.status === 0;
15
+ try {
16
+ fs.rmSync(path, { force: true });
17
+ return true;
18
+ } catch {
19
+ return false;
39
20
  }
40
21
  }
41
22
 
@@ -58,6 +58,15 @@ class PagedDisplay {
58
58
  this.currentPage = page;
59
59
  }
60
60
  }
61
+
62
+ /**
63
+ * 跳转到指定索引位置的记录
64
+ * @param {number} index - 记录的绝对索引(从0开始)
65
+ */
66
+ goToIndex(index) {
67
+ const page = Math.floor(index / this.pageSize);
68
+ this.jumpToPage(page);
69
+ }
61
70
  }
62
71
 
63
72
  module.exports = { PagedDisplay };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * 通用单选菜单组件
3
- * 使用 readline 原生键盘处理
3
+ * 使用 readline 原生键盘处理,支持翻页
4
4
  */
5
5
 
6
6
  const readline = require('readline');
@@ -23,19 +23,43 @@ function singleSelect(items, options = {}) {
23
23
 
24
24
  let currentIndex = 0;
25
25
 
26
- // 显示选项
26
+ // 计算可见条目数(不超过终端高度)
27
+ function getVisibleCount() {
28
+ const terminalHeight = process.stdout.rows || 24;
29
+ const titleLines = options.title ? 2 : 0; // 标题 + 空行
30
+ const hintLines = options.hint ? 2 : 0; // 空行 + 提示
31
+ const reservedLines = titleLines + hintLines + 1; // 预留 1 行余量
32
+ return Math.max(3, terminalHeight - reservedLines);
33
+ }
34
+
35
+ // 显示选项(支持翻页窗口)
27
36
  function display() {
28
37
  console.clear();
29
38
  if (options.title) {
30
39
  console.log(options.title);
31
40
  console.log('');
32
41
  }
33
- items.forEach((item, idx) => {
34
- const isCurrent = idx === currentIndex;
42
+
43
+ const visibleCount = getVisibleCount();
44
+ // 计算窗口位置:确保当前项在可见区域内
45
+ let windowStart = 0;
46
+ if (items.length > visibleCount) {
47
+ // 当前项接近窗口底部时,向上滚动
48
+ windowStart = Math.max(0, currentIndex - Math.floor(visibleCount / 2));
49
+ // 不超过最大窗口起始位置
50
+ windowStart = Math.min(windowStart, items.length - visibleCount);
51
+ }
52
+ const windowEnd = Math.min(windowStart + visibleCount, items.length);
53
+
54
+ // 显示可见窗口内的选项
55
+ for (let i = windowStart; i < windowEnd; i++) {
56
+ const item = items[i];
57
+ const isCurrent = i === currentIndex;
35
58
  const prefix = isCurrent ? '> ' : ' ';
36
59
  const color = isCurrent ? chalk.green : chalk.gray;
37
60
  console.log(prefix + color(item.title));
38
- });
61
+ }
62
+
39
63
  if (options.hint) {
40
64
  console.log('');
41
65
  console.log(chalk.dim(options.hint));
@@ -55,6 +79,20 @@ function singleSelect(items, options = {}) {
55
79
  } else if (key.name === 'down') {
56
80
  currentIndex = Math.min(items.length - 1, currentIndex + 1);
57
81
  display();
82
+ } else if (key.name === 'pageup' || key.name === 'left') {
83
+ const visibleCount = getVisibleCount();
84
+ currentIndex = Math.max(0, currentIndex - visibleCount);
85
+ display();
86
+ } else if (key.name === 'pagedown' || key.name === 'right') {
87
+ const visibleCount = getVisibleCount();
88
+ currentIndex = Math.min(items.length - 1, currentIndex + visibleCount);
89
+ display();
90
+ } else if (key.name === 'home') {
91
+ currentIndex = 0;
92
+ display();
93
+ } else if (key.name === 'end') {
94
+ currentIndex = items.length - 1;
95
+ display();
58
96
  } else if (key.name === 'return') {
59
97
  cleanup();
60
98
  resolve(items[currentIndex].value);
@@ -67,13 +105,20 @@ function singleSelect(items, options = {}) {
67
105
  }
68
106
  };
69
107
 
108
+ // 终端大小变化时重新渲染
109
+ const resizeHandler = () => {
110
+ display();
111
+ };
112
+
70
113
  const cleanup = () => {
71
114
  process.stdin.removeListener('keypress', handler);
115
+ process.stdout.off('resize', resizeHandler);
72
116
  process.stdin.setRawMode(false);
73
117
  process.stdin.pause();
74
118
  };
75
119
 
76
120
  process.stdin.on('keypress', handler);
121
+ process.stdout.on('resize', resizeHandler);
77
122
  });
78
123
  }
79
124
 
package/src/git-log/ui.js CHANGED
@@ -172,13 +172,26 @@ async function inputFilters (repos) {
172
172
  }
173
173
  }
174
174
 
175
+ // 是否已初始化 keypress 事件
176
+ let keypressInitialized = false;
177
+
178
+ /**
179
+ * 确保输入流初始化(只执行一次)
180
+ */
181
+ function ensureInputStreamInitialized() {
182
+ if (!keypressInitialized) {
183
+ readline.emitKeypressEvents(process.stdin);
184
+ keypressInitialized = true;
185
+ }
186
+ }
187
+
175
188
  /**
176
- * 等待日志导航按键
177
- * @returns {Promise<string>} 导航动作
189
+ * 等待日志导航按键(支持 resize)
190
+ * @returns {Promise<string>} 导航动作或 'resize'
178
191
  */
179
192
  function waitForLogNavigation () {
180
193
  return new Promise((resolve) => {
181
- readline.emitKeypressEvents(process.stdin);
194
+ ensureInputStreamInitialized();
182
195
  process.stdin.resume();
183
196
  process.stdin.setRawMode(true);
184
197
 
@@ -198,13 +211,19 @@ function waitForLogNavigation () {
198
211
  }
199
212
  };
200
213
 
214
+ const resizeHandler = () => {
215
+ cleanup();
216
+ resolve('resize');
217
+ };
218
+
201
219
  const cleanup = () => {
202
220
  process.stdin.removeListener('keypress', handler);
203
- process.stdin.setRawMode(false);
204
- process.stdin.pause();
221
+ process.stdout.off('resize', resizeHandler);
222
+ // 不暂停 stdin,保持流处于活跃状态以便下次 resume
205
223
  };
206
224
 
207
225
  process.stdin.on('keypress', handler);
226
+ process.stdout.on('resize', resizeHandler);
208
227
  });
209
228
  }
210
229
 
@@ -243,18 +262,21 @@ function renderLogPage (page, totalCount) {
243
262
 
244
263
  /**
245
264
  * 计算动态分页大小
265
+ * 精确计算确保不超过终端高度
246
266
  * @returns {number} 每页显示条数
247
267
  */
248
268
  function calculateDynamicPageSize () {
249
269
  const terminalHeight = process.stdout.rows || 24;
250
- const reservedLines = 6; // 头部3行 + 提示2行 + 底部1行
251
- const availableLines = Math.max(terminalHeight - reservedLines, 10);
252
- const linesPerLog = 5; // 每条日志约5
270
+ // 头部:标题1行 + 空行1行 + 提示1行 = 3行
271
+ const reservedLines = 3;
272
+ const availableLines = Math.max(terminalHeight - reservedLines, 5);
273
+ // 每条日志:仓库1行 + 提交1行 + 作者1行 + 日期1行 + 信息1行 + 分隔空行1行 = 6行
274
+ const linesPerLog = 6;
253
275
  return Math.floor(availableLines / linesPerLog);
254
276
  }
255
277
 
256
278
  /**
257
- * 显示日志(支持重新筛选)
279
+ * 显示日志(支持重新筛选和 resize)
258
280
  * @param {array} logs - 日志列表
259
281
  * @param {function} onRefilter - 重新筛选的回调
260
282
  * @returns {Promise<void>}
@@ -267,8 +289,8 @@ async function displayLogs (logs, onRefilter) {
267
289
 
268
290
  await withScreenSession(async (renderer) => {
269
291
  // 创建分页显示(动态计算每页条数)
270
- const pageSize = calculateDynamicPageSize();
271
- const pagedDisplay = new PagedDisplay(logs, pageSize);
292
+ let pageSize = calculateDynamicPageSize();
293
+ let pagedDisplay = new PagedDisplay(logs, pageSize);
272
294
 
273
295
  // 主循环
274
296
  while (true) {
@@ -289,7 +311,22 @@ async function displayLogs (logs, onRefilter) {
289
311
  onRefilter();
290
312
  return;
291
313
  } else if (action === 'exit') {
314
+ // 退出时恢复 stdin 状态
315
+ process.stdin.setRawMode(false);
316
+ process.stdin.pause();
292
317
  break;
318
+ } else if (action === 'resize') {
319
+ // 终端大小改变,重新计算分页
320
+ const oldPageSize = pageSize;
321
+ pageSize = calculateDynamicPageSize();
322
+ if (oldPageSize !== pageSize) {
323
+ // 获取当前页第一条记录的绝对索引
324
+ const currentIndex = pagedDisplay.currentPage * pagedDisplay.pageSize;
325
+ // 重新创建分页器
326
+ pagedDisplay = new PagedDisplay(logs, pageSize);
327
+ // 尝试恢复到之前的位置
328
+ pagedDisplay.goToIndex(Math.min(currentIndex, logs.length - 1));
329
+ }
293
330
  }
294
331
  }
295
332
  });
@@ -3,7 +3,7 @@
3
3
  * 检测和执行构建脚本
4
4
  */
5
5
 
6
- const { execaCommand } = require('execa');
6
+ const { execa } = require('execa');
7
7
 
8
8
  /**
9
9
  * 检测构建脚本
@@ -36,7 +36,7 @@ function detectBuildScripts(scripts) {
36
36
  */
37
37
  async function runBuild(pkgPath, scriptName) {
38
38
  try {
39
- const result = await execaCommand(`npm run ${scriptName}`, {
39
+ const result = await execa(`npm`, ['run', scriptName], {
40
40
  cwd: pkgPath,
41
41
  stdout: 'inherit',
42
42
  stderr: 'inherit'
@@ -3,7 +3,7 @@
3
3
  * 支持发布到 npm 兼容的注册源
4
4
  */
5
5
 
6
- const { execaCommand } = require('execa');
6
+ const { execa } = require('execa');
7
7
  const { writeFileSync, unlinkSync, existsSync } = require('fs');
8
8
  const { join } = require('path');
9
9
  const { BaseAdapter } = require('./base.js');
@@ -28,7 +28,7 @@ class NpmAdapter extends BaseAdapter {
28
28
  if (tag) args.push('--tag', tag);
29
29
  if (dryRun) args.push('--dry-run');
30
30
 
31
- await execaCommand('npm ' + args.join(' '), {
31
+ await execa('npm', args, {
32
32
  cwd: pkg.path,
33
33
  stdout: 'inherit',
34
34
  stderr: 'inherit'
@@ -62,7 +62,7 @@ class NpmAdapter extends BaseAdapter {
62
62
  writeFileSync(tmpNpmrcPath, npmrcContent, 'utf-8');
63
63
 
64
64
  try {
65
- const result = await execaCommand('npm whoami', {
65
+ const result = await execa('npm', ['whoami'], {
66
66
  env: {
67
67
  ...process.env,
68
68
  npm_config_userconfig: tmpNpmrcPath
@@ -100,7 +100,7 @@ class NpmAdapter extends BaseAdapter {
100
100
  args.push('--registry', this.registry);
101
101
  }
102
102
 
103
- await execaCommand('npm ' + args.join(' '), {
103
+ await execa('npm', args, {
104
104
  env: {
105
105
  ...process.env,
106
106
  npm_config_registry: this.registry
package/src/publish/ui.js CHANGED
@@ -423,6 +423,18 @@ async function showPublishResults(renderer, results, registries) {
423
423
  * 运行发布流程
424
424
  */
425
425
  async function runPublishFlow(targetPath, options) {
426
+ const { existsSync } = require('fs');
427
+ const { join } = require('path');
428
+
429
+ // 检查 package.json 是否存在
430
+ const packageJsonPath = join(targetPath, 'package.json');
431
+ if (!existsSync(packageJsonPath)) {
432
+ console.error(chalk.red('\n错误: 当前目录不是 npm 包或 npm 工作空间'));
433
+ console.error(chalk.yellow(`\n未找到 package.json 文件: ${packageJsonPath}`));
434
+ console.error(chalk.gray('\n请确保在正确的 npm 项目目录下执行此命令\n'));
435
+ process.exit(1);
436
+ }
437
+
426
438
  return withScreenSession(async (renderer) => {
427
439
  // 1. 扫描预览页
428
440
  const scanResult = await showScanPreview(renderer, targetPath);