mihomo-cli 1.2.3 → 1.2.5

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
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.5] - 2026-04-07
4
+
5
+ ### 新增功能
6
+
7
+ - **update 命令**:新增 `mihomo update` 命令,执行 `npm install -g mihomo-cli` 快速更新 CLI 版本
8
+ - 支持别名:`update`、`upd`、`upgrade`
9
+
10
+ ---
11
+
12
+ ## [1.2.4] - 2026-04-07
13
+
14
+ ### 修复
15
+
16
+ - **路径安全**:`getLogPathByName()` 增加 `isPathUnderDir()` 校验,防止潜在的路径遍历风险
17
+ - **错误提示**:覆写配置文件解析失败时显示警告日志,不再静默忽略
18
+
19
+ ### 重构
20
+
21
+ - **代码结构**:工具函数(`sleepSync`、`formatBytes`、`isProcessRunning` 等)提取到 `utils.js` 模块,简化各模块依赖
22
+ - **命名规范**:统一函数命名为全称单数
23
+ - `autoUpdateStaleSubscriptions` → `autoUpdateStaleSubscription`
24
+ - `applyOverwrites` → `applyOverwrite`
25
+ - `loadOverwriteFiles` → `loadOverwriteFile`
26
+ - `listOverwriteFiles` → `listOverwriteFile`
27
+
28
+ ---
29
+
3
30
  ## [1.2.3] - 2026-04-07
4
31
 
5
32
  ### 优化
package/README.md CHANGED
@@ -112,6 +112,7 @@ mihomo ui yacd # YACD
112
112
  | 命令 | 说明 |
113
113
  | ----------------------------------- | ------------------------------------------------------- |
114
114
  | `mihomo kernel [镜像\|--no-mirror]` | 更新内核 |
115
+ | `mihomo update` | 更新 mihomo-cli (npm install -g) |
115
116
  | `mihomo ui [zash\|dash\|yacd]` | 打开 Web UI |
116
117
  | `mihomo dir` | 显示数据目录位置 |
117
118
  | `mihomo dir open [target]` | 打开指定目录(`root`, `subs`, `logs`, `overwrites` 等) |
package/index.js CHANGED
@@ -8,6 +8,7 @@ const kernel = require('./src/kernel');
8
8
  const subscription = require('./src/subscription');
9
9
  const processMgr = require('./src/process');
10
10
  const overwrite = require('./src/overwrite');
11
+ const utils = require('./src/utils');
11
12
 
12
13
  const VERSION = require('./package.json').version;
13
14
 
@@ -94,6 +95,7 @@ function printHelp() {
94
95
  '\n' +
95
96
  '系统:\n' +
96
97
  ' kernel [镜像|--no-mirror] 更新内核\n' +
98
+ ' update 更新 mihomo-cli (npm install -g)\n' +
97
99
  ' reset [--full] 重置用户数据 (--full 同时删除内核)\n' +
98
100
  ' help, -h 显示帮助\n' +
99
101
  ' version, -v 显示版本\n' +
@@ -127,7 +129,7 @@ function printStatus() {
127
129
  const status = processMgr.getStatus();
128
130
  const info = config.getConfigInfo();
129
131
  const owEnabled = overwrite.isOverwriteEnabled();
130
- const owFiles = overwrite.listOverwriteFiles().files;
132
+ const owFiles = overwrite.listOverwriteFile().files;
131
133
  const activeSub = getActiveSubscription();
132
134
 
133
135
  console.log('');
@@ -221,32 +223,6 @@ function pickSingleSubscription(subs, pattern) {
221
223
  process.exit(1);
222
224
  }
223
225
 
224
- function hasFlag(args, short, long) {
225
- return args && (args.includes(short) || args.includes(long));
226
- }
227
-
228
- function parseIntArg(args, short, long, defaultValue) {
229
- if (!args) return defaultValue;
230
- for (let i = 0; i < args.length; i++) {
231
- if (args[i] === short || args[i] === long) {
232
- if (i + 1 < args.length) {
233
- const val = parseInt(args[i + 1]);
234
- return isNaN(val) ? defaultValue : val;
235
- }
236
- }
237
- }
238
- return defaultValue;
239
- }
240
-
241
- function getNonFlagArg(args, startIdx) {
242
- if (!args) return null;
243
- for (let i = startIdx; i < args.length; i++) {
244
- if (!args[i].startsWith('-')) {
245
- return args[i];
246
- }
247
- }
248
- return null;
249
- }
250
226
 
251
227
  function openLogFile(logPath, label) {
252
228
  const displayLabel = label || logPath;
@@ -305,7 +281,7 @@ async function cmdStart(args) {
305
281
  process.exit(1);
306
282
  }
307
283
 
308
- await subscription.autoUpdateStaleSubscriptions();
284
+ await subscription.autoUpdateStaleSubscription();
309
285
 
310
286
  // 每次 start 都先确保完全干净的状态(停止进程 + 清理运行时文件)
311
287
  const status = processMgr.getStatus();
@@ -390,7 +366,7 @@ function cmdUI(args) {
390
366
  function cmdLog(args) {
391
367
  const logPath = processMgr.getLogPath();
392
368
 
393
- if (hasFlag(args, '-o', '--open')) {
369
+ if (utils.hasFlag(args, '-o', '--open')) {
394
370
  openLogFile(logPath);
395
371
  return;
396
372
  }
@@ -399,9 +375,9 @@ function cmdLog(args) {
399
375
  }
400
376
 
401
377
  function cmdLogs(args) {
402
- const targetName = getNonFlagArg(args, 1);
403
- const lines = parseIntArg(args, '-n', '--lines', 100);
404
- const openInViewer = hasFlag(args, '-o', '--open');
378
+ const targetName = utils.getNonFlagArg(args, 1);
379
+ const lines = utils.parseIntArg(args, '-n', '--lines', 100);
380
+ const openInViewer = utils.hasFlag(args, '-o', '--open');
405
381
 
406
382
  if (targetName) {
407
383
  let logPath;
@@ -466,8 +442,8 @@ function cmdLogs(args) {
466
442
  archiveCounter++;
467
443
  num = archiveCounter < 10 ? ' ' + archiveCounter : '' + archiveCounter;
468
444
  }
469
- const time = subscription.formatDate(log.mtime);
470
- const size = subscription.formatBytes(log.size);
445
+ const time = utils.formatDate(log.mtime);
446
+ const size = utils.formatBytes(log.size);
471
447
  const name = log.isCurrent ? 'mihomo.log (当前运行中)' : log.name;
472
448
 
473
449
  console.log(' ' + num + '. ' + name);
@@ -586,7 +562,7 @@ async function cmdKernel(args) {
586
562
  }
587
563
 
588
564
  async function printSubscriptionList() {
589
- const updateResult = await subscription.autoUpdateStaleSubscriptions();
565
+ const updateResult = await subscription.autoUpdateStaleSubscription();
590
566
  if (updateResult.total > 0) {
591
567
  console.log('');
592
568
  }
@@ -601,7 +577,7 @@ async function printSubscriptionList() {
601
577
  }
602
578
  console.log('订阅列表:');
603
579
  subs.forEach((s, i) => {
604
- const time = subscription.formatDate(s.updated_at);
580
+ const time = utils.formatDate(s.updated_at);
605
581
  const defaultMark = i === 0 ? ' [默认]' : '';
606
582
  const interval = s.update_interval || subscription.DEFAULT_UPDATE_INTERVAL_HOURS;
607
583
  console.log(' ' + (i + 1) + '. ' + s.name + defaultMark);
@@ -612,8 +588,8 @@ async function printSubscriptionList() {
612
588
  }
613
589
  if (s.download !== undefined || s.total !== undefined) {
614
590
  const used = (s.upload || 0) + (s.download || 0);
615
- const usedStr = subscription.formatBytes(used);
616
- const totalStr = subscription.formatBytes(s.total);
591
+ const usedStr = utils.formatBytes(used);
592
+ const totalStr = utils.formatBytes(s.total);
617
593
  let percentStr = '';
618
594
  if (s.total && s.total > 0) {
619
595
  const percent = Math.min((used / s.total) * 100, 100);
@@ -622,7 +598,7 @@ async function printSubscriptionList() {
622
598
  console.log(' 流量: ' + usedStr + ' / ' + totalStr + percentStr);
623
599
  }
624
600
  if (s.expire !== undefined) {
625
- console.log(' 到期: ' + subscription.formatTimestamp(s.expire));
601
+ console.log(' 到期: ' + utils.formatTimestamp(s.expire));
626
602
  }
627
603
  if (s.web_page_url) {
628
604
  console.log(' 页面: ' + s.web_page_url);
@@ -811,6 +787,27 @@ async function cmdSubscription(args) {
811
787
  process.exit(1);
812
788
  }
813
789
 
790
+ function cmdUpdate() {
791
+ console.log('更新 mihomo-cli...');
792
+ console.log('');
793
+
794
+ const npm = spawn('npm', ['install', '-g', 'mihomo-cli'], { stdio: 'inherit' });
795
+
796
+ npm.on('close', code => {
797
+ if (code === 0) {
798
+ console.log('');
799
+ console.log('更新完成');
800
+ } else {
801
+ process.exit(code);
802
+ }
803
+ });
804
+
805
+ npm.on('error', e => {
806
+ console.error('执行失败: ' + e.message);
807
+ process.exit(1);
808
+ });
809
+ }
810
+
814
811
  async function cmdReset(args) {
815
812
  const fullReset = args && (args.includes('--full') || args.includes('-f'));
816
813
  const skipConfirm = args && (args.includes('--yes') || args.includes('-y'));
@@ -853,7 +850,7 @@ async function cmdReset(args) {
853
850
  }
854
851
 
855
852
  function printOverwriteList() {
856
- const info = overwrite.listOverwriteFiles();
853
+ const info = overwrite.listOverwriteFile();
857
854
  console.log('状态: ' + (info.enabled ? '已启用' : '已禁用'));
858
855
  console.log('目录: ' + info.dir);
859
856
  console.log('');
@@ -1059,6 +1056,11 @@ async function main() {
1059
1056
  case 'kernel':
1060
1057
  await cmdKernel(args);
1061
1058
  break;
1059
+ case 'upd':
1060
+ case 'update':
1061
+ case 'upgrade':
1062
+ cmdUpdate();
1063
+ break;
1062
1064
  case 'sub':
1063
1065
  case 'subscription':
1064
1066
  case 'subscriptions':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mihomo-cli",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "description": "A terminal-based mihomo (Clash.Meta) client for macOS",
5
5
  "main": "index.js",
6
6
  "bin": {
package/src/config.js CHANGED
@@ -315,7 +315,7 @@ function buildConfig(subRawContent, mode) {
315
315
  const overwrite = require('./overwrite');
316
316
 
317
317
  // 应用覆写配置
318
- const withOverwrites = overwrite.applyOverwrites(baseConfig);
318
+ const withOverwrites = overwrite.applyOverwrite(baseConfig);
319
319
 
320
320
  // 合并 BASE_CONFIG(优先级高于覆写)
321
321
  const merged = { ...withOverwrites, ...BASE_CONFIG };
package/src/overwrite.js CHANGED
@@ -171,7 +171,7 @@ function setOverwriteEnabled(enabled) {
171
171
  * 读取 overwrites 目录下的所有 yaml 文件
172
172
  * 按文件名排序返回
173
173
  */
174
- function loadOverwriteFiles() {
174
+ function loadOverwriteFile() {
175
175
  const dir = getOverwritesDir();
176
176
 
177
177
  if (!fs.existsSync(dir)) {
@@ -197,7 +197,7 @@ function loadOverwriteFiles() {
197
197
  });
198
198
  }
199
199
  } catch (e) {
200
- // 忽略解析错误的文件
200
+ console.warn('警告: 覆写文件 "' + file + '" 解析失败: ' + e.message);
201
201
  }
202
202
  }
203
203
 
@@ -207,12 +207,12 @@ function loadOverwriteFiles() {
207
207
  /**
208
208
  * 应用所有覆写配置到基础配置
209
209
  */
210
- function applyOverwrites(baseConfig) {
210
+ function applyOverwrite(baseConfig) {
211
211
  if (!isOverwriteEnabled()) {
212
212
  return baseConfig;
213
213
  }
214
214
 
215
- const overwriteFiles = loadOverwriteFiles();
215
+ const overwriteFiles = loadOverwriteFile();
216
216
 
217
217
  if (overwriteFiles.length === 0) {
218
218
  return baseConfig;
@@ -230,8 +230,8 @@ function applyOverwrites(baseConfig) {
230
230
  /**
231
231
  * 列出覆写文件信息
232
232
  */
233
- function listOverwriteFiles() {
234
- const files = loadOverwriteFiles();
233
+ function listOverwriteFile() {
234
+ const files = loadOverwriteFile();
235
235
  const enabled = isOverwriteEnabled();
236
236
  const dir = getOverwritesDir();
237
237
 
@@ -249,6 +249,6 @@ function listOverwriteFiles() {
249
249
  module.exports = {
250
250
  isOverwriteEnabled,
251
251
  setOverwriteEnabled,
252
- applyOverwrites,
253
- listOverwriteFiles,
252
+ applyOverwrite,
253
+ listOverwriteFile,
254
254
  };
package/src/process.js CHANGED
@@ -2,11 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { spawn, execSync } = require('child_process');
4
4
  const config = require('./config');
5
-
6
- const _sharedBuf = new Int32Array(new SharedArrayBuffer(4));
7
- function sleepSync(ms) {
8
- Atomics.wait(_sharedBuf, 0, 0, ms);
9
- }
5
+ const utils = require('./utils');
10
6
 
11
7
  function clearRuntime() {
12
8
  if (fs.existsSync(config.DIRS.runtime)) {
@@ -27,21 +23,9 @@ function getPid() {
27
23
  }
28
24
  }
29
25
 
30
- function isProcessRunning(pid) {
31
- if (!pid) return false;
32
- try {
33
- const output = execSync('ps -p ' + pid + ' -o pid= 2>/dev/null || true', {
34
- encoding: 'utf8',
35
- }).trim();
36
- return output.length > 0;
37
- } catch (e) {
38
- return false;
39
- }
40
- }
41
-
42
26
  function isRunning() {
43
27
  const pid = getPid();
44
- return pid ? isProcessRunning(pid) : false;
28
+ return pid ? utils.isProcessRunning(pid) : false;
45
29
  }
46
30
 
47
31
  function getAllMihomoPids() {
@@ -61,17 +45,6 @@ function getAllMihomoPids() {
61
45
  }
62
46
  }
63
47
 
64
- function isProcessRoot(pid) {
65
- try {
66
- const uidOutput = execSync('ps -p ' + pid + ' -o uid= 2>/dev/null || true', {
67
- encoding: 'utf8',
68
- }).trim();
69
- return uidOutput === '0';
70
- } catch (e) {
71
- return false;
72
- }
73
- }
74
-
75
48
  function isPidFileOwnedByRoot() {
76
49
  if (!fs.existsSync(config.PATHS.pidFile)) {
77
50
  return false;
@@ -86,7 +59,7 @@ function isPidFileOwnedByRoot() {
86
59
 
87
60
  function checkStaleState() {
88
61
  const allPids = getAllMihomoPids();
89
- const hasRootProcess = allPids.some(p => isProcessRoot(p));
62
+ const hasRootProcess = allPids.some(p => utils.isProcessRoot(p));
90
63
  const hasRootPidFile = isPidFileOwnedByRoot();
91
64
 
92
65
  return {
@@ -184,7 +157,7 @@ function cleanupAll(forceSudo) {
184
157
  return { killed: 0, failed: 0, remaining: [] };
185
158
  }
186
159
 
187
- const hasRootProcess = pids.some(p => isProcessRoot(p));
160
+ const hasRootProcess = pids.some(p => utils.isProcessRoot(p));
188
161
  const hasRootPidFile = isPidFileOwnedByRoot();
189
162
  const needsSudo = hasRootProcess;
190
163
  const allowSudo = forceSudo || hasRootProcess || hasRootPidFile;
@@ -216,7 +189,7 @@ function cleanupAll(forceSudo) {
216
189
 
217
190
  for (let i = 0; i < 50; i++) {
218
191
  if (getAllMihomoPids().length === 0) break;
219
- sleepSync(100);
192
+ utils.sleepSync(100);
220
193
  }
221
194
 
222
195
  clearPid();
@@ -306,7 +279,7 @@ function getProcessInfo(pid) {
306
279
  pid,
307
280
  memory: rss ? (rss / 1024).toFixed(1) + ' MB' : '未知',
308
281
  cpu: pcpu ? pcpu.toFixed(1) + '%' : '未知',
309
- isRoot: isProcessRoot(pid),
282
+ isRoot: utils.isProcessRoot(pid),
310
283
  };
311
284
  } catch (e) {
312
285
  return { pid, memory: '未知', cpu: '未知', isRoot: false };
@@ -606,6 +579,12 @@ function listLogs() {
606
579
  return result;
607
580
  }
608
581
 
582
+ function isPathUnderDir(filePath, baseDir) {
583
+ const resolvedPath = path.resolve(filePath);
584
+ const resolvedBase = path.resolve(baseDir);
585
+ return resolvedPath === resolvedBase || resolvedPath.startsWith(resolvedBase + path.sep);
586
+ }
587
+
609
588
  function getLogPathByName(name) {
610
589
  const logsDir = config.DIRS.logs;
611
590
 
@@ -619,16 +598,19 @@ function getLogPathByName(name) {
619
598
  }
620
599
 
621
600
  const filePath = path.join(logsDir, targetName);
622
- if (fs.existsSync(filePath)) {
601
+ if (fs.existsSync(filePath) && isPathUnderDir(filePath, logsDir)) {
623
602
  return filePath;
624
603
  }
625
604
 
626
- // 尝试模糊匹配
605
+ // 尝试模糊匹配(readdirSync 返回的文件名已是安全的,但为了一致性仍校验)
627
606
  if (fs.existsSync(logsDir)) {
628
607
  const files = fs.readdirSync(logsDir);
629
608
  for (const file of files) {
630
609
  if (file.includes(name)) {
631
- return path.join(logsDir, file);
610
+ const candidatePath = path.join(logsDir, file);
611
+ if (isPathUnderDir(candidatePath, logsDir)) {
612
+ return candidatePath;
613
+ }
632
614
  }
633
615
  }
634
616
  }
@@ -1,5 +1,4 @@
1
1
  const axios = require('axios');
2
- const yaml = require('js-yaml');
3
2
  const config = require('./config');
4
3
 
5
4
  const DEFAULT_UPDATE_INTERVAL_HOURS = 12;
@@ -38,35 +37,6 @@ function parseUsernameFromContentDisposition(header) {
38
37
  return parts[parts.length - 1] || null;
39
38
  }
40
39
 
41
- function formatBytes(bytes) {
42
- if (bytes === undefined || bytes === null) return '未知';
43
- if (bytes === 0) return '0 B';
44
- const k = 1024;
45
- const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
46
- const i = Math.floor(Math.log(bytes) / Math.log(k));
47
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
48
- }
49
-
50
- function formatTimestamp(ts) {
51
- if (!ts) return '未知';
52
- try {
53
- return new Date(ts * 1000).toLocaleString('zh-CN');
54
- } catch {
55
- return '未知';
56
- }
57
- }
58
-
59
- function formatDate(dateOrIso) {
60
- if (!dateOrIso) return '未知';
61
- try {
62
- const d = dateOrIso instanceof Date ? dateOrIso : new Date(dateOrIso);
63
- if (isNaN(d.getTime())) return '未知';
64
- return d.toLocaleString('zh-CN');
65
- } catch {
66
- return '未知';
67
- }
68
- }
69
-
70
40
  function formatProxySummary(info) {
71
41
  const parts = [];
72
42
  if (info && info.proxyGroups > 0) parts.push(info.proxyGroups + ' 组');
@@ -178,7 +148,7 @@ async function tryUpdateOne(sub) {
178
148
  }
179
149
  }
180
150
 
181
- async function autoUpdateStaleSubscriptions() {
151
+ async function autoUpdateStaleSubscription() {
182
152
  const allSubs = config.getSubscriptionsWithCache();
183
153
  const staleSubs = allSubs.filter(needsAutoUpdate);
184
154
 
@@ -217,10 +187,7 @@ module.exports = {
217
187
  DEFAULT_UPDATE_INTERVAL_HOURS,
218
188
  downloadSubscription,
219
189
  prepareConfigForStart,
220
- formatBytes,
221
- formatTimestamp,
222
- formatDate,
223
190
  formatProxySummary,
224
191
  tryUpdateOne,
225
- autoUpdateStaleSubscriptions,
192
+ autoUpdateStaleSubscription,
226
193
  };
package/src/utils.js ADDED
@@ -0,0 +1,101 @@
1
+ const { execSync } = require('child_process');
2
+
3
+ const _sleepBuf = new Int32Array(1);
4
+
5
+ function sleepSync(ms) {
6
+ Atomics.wait(_sleepBuf, 0, 0, ms);
7
+ }
8
+
9
+ function formatBytes(bytes) {
10
+ if (bytes === undefined || bytes === null) return '未知';
11
+ const num = Number(bytes);
12
+ if (isNaN(num) || num < 0) return '未知';
13
+ if (num === 0) return '0 B';
14
+ const k = 1024;
15
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
16
+ const i = Math.floor(Math.log(num) / Math.log(k));
17
+ return parseFloat((num / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
18
+ }
19
+
20
+ function formatTimestamp(ts) {
21
+ if (ts === undefined || ts === null) return '未知';
22
+ try {
23
+ return new Date(ts * 1000).toLocaleString('zh-CN');
24
+ } catch {
25
+ return '未知';
26
+ }
27
+ }
28
+
29
+ function formatDate(dateOrIso) {
30
+ if (dateOrIso === undefined || dateOrIso === null) return '未知';
31
+ try {
32
+ const d = dateOrIso instanceof Date ? dateOrIso : new Date(dateOrIso);
33
+ if (isNaN(d.getTime())) return '未知';
34
+ return d.toLocaleString('zh-CN');
35
+ } catch {
36
+ return '未知';
37
+ }
38
+ }
39
+
40
+ function hasFlag(args, short, long) {
41
+ return args && (args.includes(short) || args.includes(long));
42
+ }
43
+
44
+ function parseIntArg(args, short, long, defaultValue) {
45
+ if (!args) return defaultValue;
46
+ for (let i = 0; i < args.length; i++) {
47
+ if (args[i] === short || args[i] === long) {
48
+ if (i + 1 < args.length) {
49
+ const val = parseInt(args[i + 1]);
50
+ return isNaN(val) ? defaultValue : val;
51
+ }
52
+ }
53
+ }
54
+ return defaultValue;
55
+ }
56
+
57
+ function getNonFlagArg(args, startIdx) {
58
+ if (!args) return null;
59
+ for (let i = startIdx; i < args.length; i++) {
60
+ if (!args[i].startsWith('-')) {
61
+ return args[i];
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+
67
+ function isProcessRunning(pid) {
68
+ if (!pid) return false;
69
+ try {
70
+ const output = execSync('ps -p ' + pid + ' -o pid= 2>/dev/null || true', {
71
+ encoding: 'utf8',
72
+ }).trim();
73
+ return output.length > 0;
74
+ } catch (e) {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ function isProcessRoot(pid) {
80
+ if (!pid) return false;
81
+ try {
82
+ const uidOutput = execSync('ps -p ' + pid + ' -o uid= 2>/dev/null || true', {
83
+ encoding: 'utf8',
84
+ }).trim();
85
+ return uidOutput === '0';
86
+ } catch (e) {
87
+ return false;
88
+ }
89
+ }
90
+
91
+ module.exports = {
92
+ sleepSync,
93
+ formatBytes,
94
+ formatTimestamp,
95
+ formatDate,
96
+ hasFlag,
97
+ parseIntArg,
98
+ getNonFlagArg,
99
+ isProcessRunning,
100
+ isProcessRoot,
101
+ };