mihomo-cli 1.2.2 → 1.2.3

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,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.3] - 2026-04-07
4
+
5
+ ### 优化
6
+
7
+ - **简短帮助**:大幅精简,只列出最常用命令,增加 `mihomo help` 提示
8
+ - **性能**:
9
+ - settings 读取增加内存缓存,减少重复 JSON 解析
10
+ - 内核版本增加缓存,避免重复执行 `mihomo -v`
11
+ - `sleepSync()` 改用 `Atomics.wait` 而非 `execSync('sleep')`,减少子进程开销
12
+
13
+ ### 重构
14
+
15
+ - 精简模块导出接口,移除不必要的内部函数导出
16
+ - 新增 `formatProxySummary()` 复用函数(消除 3 处重复代码)
17
+ - `pickSingleSubscription()` 移除多余参数
18
+ - 统一代码风格:数组/条件表达式换行风格、`cmdUi` → `cmdUI` 命名一致性
19
+
20
+ ### 文档
21
+
22
+ - README 修复:GitHub 链接、重复段落、`profile-update-interval` 格式
23
+
24
+ ---
25
+
3
26
  ## [1.2.2] - 2026-04-05
4
27
 
5
28
  ### 优化
package/README.md CHANGED
@@ -28,7 +28,7 @@ npm install -g mihomo-cli
28
28
  ### 方式二:源码安装
29
29
 
30
30
  ```bash
31
- git clone https://github.com/yourname/mihomo-cli.git
31
+ git clone https://github.com/adaex/mihomo-cli.git
32
32
  cd mihomo-cli
33
33
  npm install
34
34
  npm link
@@ -79,49 +79,50 @@ mihomo ui yacd # YACD
79
79
 
80
80
  ### 核心命令
81
81
 
82
- | 命令 | 说明 |
83
- |------|------|
84
- | `mihomo start [tun\|mixed]` | 启动/重启/切换代理模式 |
85
- | `mihomo stop` | 停止代理 |
86
- | `mihomo status` | 查看运行状态 |
87
- | `mihomo log` | 实时查看日志 (`-o` 用系统编辑器打开) |
88
- | `mihomo logs` | 列出所有日志(当前 + 历史归档) |
89
- | `mihomo logs <编号>` | 查看指定日志(`0`=当前日志,`1+`=归档日志,支持 `-n N` 指定行数、`-o` 打开) |
82
+ | 命令 | 说明 |
83
+ | --------------------------- | ---------------------------------------------------------------------------- |
84
+ | `mihomo start [tun\|mixed]` | 启动/重启/切换代理模式 |
85
+ | `mihomo stop` | 停止代理 |
86
+ | `mihomo status` | 查看运行状态 |
87
+ | `mihomo log` | 实时查看日志 (`-o` 用系统编辑器打开) |
88
+ | `mihomo logs` | 列出所有日志(当前 + 历史归档) |
89
+ | `mihomo logs <编号>` | 查看指定日志(`0`=当前日志,`1+`=归档日志,支持 `-n N` 指定行数、`-o` 打开) |
90
90
 
91
91
  ### 订阅管理
92
92
 
93
- | 命令 | 说明 |
94
- |------|------|
95
- | `mihomo sub list` | 列出所有订阅(含流量、到期时间) |
96
- | `mihomo sub add <url> [name]` | 添加订阅 |
97
- | `mihomo sub update` | 更新所有订阅 |
98
- | `mihomo sub update <name>` | 更新指定订阅(支持模糊匹配) |
99
- | `mihomo sub use <name>` | 设置默认订阅(支持模糊匹配,自动重启) |
100
- | `mihomo sub web [name]` | 打开订阅页面(无参打开默认) |
93
+ | 命令 | 说明 |
94
+ | ----------------------------- | -------------------------------------- |
95
+ | `mihomo sub` | 列出所有订阅(含流量、到期时间) |
96
+ | `mihomo sub add <url> [name]` | 添加订阅 |
97
+ | `mihomo sub update` | 更新所有订阅 |
98
+ | `mihomo sub update <name>` | 更新指定订阅(支持模糊匹配) |
99
+ | `mihomo sub use <name>` | 设置默认订阅(支持模糊匹配,自动重启) |
100
+ | `mihomo sub web [name]` | 打开订阅页面(无参打开默认) |
101
101
 
102
102
  ### 覆写配置
103
103
 
104
- | 命令 | 说明 |
105
- |------|------|
104
+ | 命令 | 说明 |
105
+ | ------------------------------ | -------------------------- |
106
106
  | `mihomo ow` / `mihomo ow list` | 查看覆写配置状态和文件列表 |
107
- | `mihomo ow on` | 启用覆写配置(自动重启) |
108
- | `mihomo ow off` | 禁用覆写配置(自动重启) |
107
+ | `mihomo ow on` | 启用覆写配置(自动重启) |
108
+ | `mihomo ow off` | 禁用覆写配置(自动重启) |
109
109
 
110
110
  ### 其他命令
111
111
 
112
- | 命令 | 说明 |
113
- |------|------|
114
- | `mihomo kernel [镜像\|--no-mirror]` | 更新内核 |
115
- | `mihomo ui [zash\|dash\|yacd]` | 打开 Web UI |
116
- | `mihomo dir` | 显示数据目录位置 |
117
- | `mihomo dir open [target]` | 打开指定目录(`root`, `subs`, `logs`, `overwrites` 等) |
118
- | `mihomo reset [--full]` | 重置用户数据 (--full 同时删除内核) |
119
- | `mihomo version` | 显示版本信息 |
120
- | `mihomo help` | 显示帮助信息 |
112
+ | 命令 | 说明 |
113
+ | ----------------------------------- | ------------------------------------------------------- |
114
+ | `mihomo kernel [镜像\|--no-mirror]` | 更新内核 |
115
+ | `mihomo ui [zash\|dash\|yacd]` | 打开 Web UI |
116
+ | `mihomo dir` | 显示数据目录位置 |
117
+ | `mihomo dir open [target]` | 打开指定目录(`root`, `subs`, `logs`, `overwrites` 等) |
118
+ | `mihomo reset [--full]` | 重置用户数据 (--full 同时删除内核) |
119
+ | `mihomo version` | 显示版本信息 |
120
+ | `mihomo help` | 显示帮助信息 |
121
121
 
122
122
  ### 命令别名
123
123
 
124
124
  以下任意命令等效:
125
+
125
126
  - `mihomo-cli` (原名)
126
127
  - `mihomo`
127
128
  - `mmc`
@@ -204,12 +205,12 @@ mihomo kernel --no-mirror
204
205
 
205
206
  覆写配置支持以下特殊操作符:
206
207
 
207
- | 语法 | 作用 | 示例 |
208
- |------|------|------|
209
- | `key!` | 强制覆盖整个对象(不深度合并) | `dns!`: { ... } |
210
- | `+key` | 数组前置插入 | `+proxies`: [...] |
211
- | `key+` | 数组追加 | `rules+`: [...] |
212
- | `<+key>` | 键名以 `+` 开头时转义 | `<+.google.cn>`: ... |
208
+ | 语法 | 作用 | 示例 |
209
+ | -------- | ------------------------------ | -------------------- |
210
+ | `key!` | 强制覆盖整个对象(不深度合并) | `dns!`: { ... } |
211
+ | `+key` | 数组前置插入 | `+proxies`: [...] |
212
+ | `key+` | 数组追加 | `rules+`: [...] |
213
+ | `<+key>` | 键名以 `+` 开头时转义 | `<+.google.cn>`: ... |
213
214
 
214
215
  ### 示例
215
216
 
@@ -225,18 +226,18 @@ dns!:
225
226
 
226
227
  # 追加规则
227
228
  rules+:
228
- - "DOMAIN-SUFFIX,example.com,DIRECT"
229
+ - 'DOMAIN-SUFFIX,example.com,DIRECT'
229
230
  ```
230
231
 
231
232
  ## Web UI
232
233
 
233
234
  内置三个常用 Web UI:
234
235
 
235
- | 名称 | 地址 | 说明 |
236
- |------|------|------|
237
- | zash | https://board.zash.run.place | 现代简洁界面(默认) |
238
- | dash | https://metacubex.github.io/metacubexd | MetaCubeX 官方 UI |
239
- | yacd | https://yacd.metacubex.one | 经典 YACD 界面 |
236
+ | 名称 | 地址 | 说明 |
237
+ | ---- | ---------------------------------------- | -------------------- |
238
+ | zash | <https://board.zash.run.place> | 现代简洁界面(默认) |
239
+ | dash | <https://metacubex.github.io/metacubexd> | MetaCubeX 官方 UI |
240
+ | yacd | <https://yacd.metacubex.one> | 经典 YACD 界面 |
240
241
 
241
242
  ## 故障排除
242
243
 
@@ -261,6 +262,7 @@ sudo pkill -9 mihomo
261
262
  ### 端口被占用
262
263
 
263
264
  默认端口(取决于订阅配置):
265
+
264
266
  - Mixed 端口: `7890`
265
267
  - 外部控制器: `127.0.0.1:9090`
266
268
 
package/index.js CHANGED
@@ -33,7 +33,7 @@ process.on('SIGTERM', () => {
33
33
  process.exit(0);
34
34
  });
35
35
 
36
- process.on('uncaughtException', (e) => {
36
+ process.on('uncaughtException', e => {
37
37
  console.error('\n未捕获的异常: ' + e.message);
38
38
  if (e.stack) {
39
39
  console.error(e.stack.split('\n').slice(1).join('\n'));
@@ -41,80 +41,79 @@ process.on('uncaughtException', (e) => {
41
41
  process.exit(1);
42
42
  });
43
43
 
44
- process.on('unhandledRejection', (reason) => {
44
+ process.on('unhandledRejection', reason => {
45
45
  const msg = reason instanceof Error ? reason.message : String(reason);
46
46
  console.error('\n未处理的 Promise 拒绝: ' + msg);
47
47
  process.exit(1);
48
48
  });
49
49
 
50
50
  function printShortHelp() {
51
- console.log('\nmihomo-cli v' + VERSION);
52
- console.log('别名: mihomo, mmc, mh\n');
53
- console.log('命令:\n' +
54
- ' start [tun|mixed] 启动/切换代理\n' +
55
- ' stop 停止代理\n' +
56
- ' status 查看状态\n' +
57
- ' ui [zash|dash|yacd] Web 界面\n' +
58
- ' log 实时日志\n' +
59
- ' logs 日志列表\n' +
60
- ' subscription 订阅管理(别名 sub)\n' +
61
- ' overwrite [on|off] 覆写配置(别名 ow)\n' +
62
- ' directory 数据目录(别名 dir)\n' +
63
- ' kernel 更新内核\n' +
64
- ' reset 重置配置\n' +
65
- ' version 版本信息\n');
51
+ console.log('\nmihomo-cli v' + VERSION + ' (mihomo help 查看完整帮助)\n');
52
+ console.log(
53
+ '常用命令:\n' +
54
+ ' start [tun|mixed] 启动/切换代理\n' +
55
+ ' ui [zash|dash|yacd] 打开 Web UI\n' +
56
+ ' ow [on|off] 覆写配置\n' +
57
+ ' sub [use|update] 订阅管理\n',
58
+ );
66
59
  }
67
60
 
68
61
  function printHelp() {
69
- console.log('\nmihomo-cli v' + VERSION + '\n' +
70
- '\n' +
71
- '命令别名: mihomo, mmc, mh\n' +
72
- '\n' +
73
- '用法:\n' +
74
- ' mihomo <命令> [选项]\n' +
75
- '\n' +
76
- '控制:\n' +
77
- ' start [tun|mixed] 启动/切换代理 (默认 mixed)\n' +
78
- ' stop 停止代理\n' +
79
- ' status 查看状态\n' +
80
- '\n' +
81
- '界面:\n' +
82
- ' ui [zash|dash|yacd] 打开 Web UI (默认 zash)\n' +
83
- ' log [-o] 实时日志(-o 打开文件)\n' +
84
- ' logs [编号] [-n N] [-o] 日志列表(0=当前,1+=归档)\n' +
85
- '\n' +
86
- '订阅:\n' +
87
- ' subscription 列出所有订阅(别名 sub)\n' +
88
- ' subscription add <url> [name] 添加订阅\n' +
89
- ' subscription update [name] 更新订阅(无参更新所有)\n' +
90
- ' subscription use <name> 切换默认订阅\n' +
91
- ' subscription web [name] 打开订阅页面\n' +
92
- '\n' +
93
- '配置:\n' +
94
- ' overwrite 查看覆写状态(别名 ow)\n' +
95
- ' overwrite on|off 启用/禁用覆写配置\n' +
96
- ' directory 显示数据目录位置(别名 dir)\n' +
97
- ' directory open [target] 打开目录: root|subs|logs|overwrites|...\n' +
98
- '\n' +
99
- '系统:\n' +
100
- ' kernel [镜像|--no-mirror] 更新内核\n' +
101
- ' reset [--full] 重置用户数据 (--full 同时删除内核)\n' +
102
- ' help, -h 显示帮助\n' +
103
- ' version, -v 显示版本\n' +
104
- '\n' +
105
- '示例:\n' +
106
- ' mihomo start # 启动/重启 Mixed 模式\n' +
107
- ' mihomo start tun # 切换到 TUN 透明代理模式\n' +
108
- ' mihomo sub add <url> # 添加订阅 (sub 是 subscription 别名)\n' +
109
- ' mihomo ui # 打开 Web UI\n' +
110
- '\n' +
111
- '模式说明:\n' +
112
- ' mixed HTTP + SOCKS5 混合端口 (默认)\n' +
113
- ' tun 透明代理,全局自动路由,需要 sudo\n' +
114
- '\n' +
115
- '数据目录:\n' +
116
- ' 环境变量 MIHOMO_CLI_DIR 可自定义位置\n' +
117
- ' 默认: ' + config.USER_DATA_DIR + '\n');
62
+ console.log(
63
+ '\nmihomo-cli v' +
64
+ VERSION +
65
+ '\n' +
66
+ '\n' +
67
+ '命令别名: mihomo, mmc, mh\n' +
68
+ '\n' +
69
+ '用法:\n' +
70
+ ' mihomo <命令> [选项]\n' +
71
+ '\n' +
72
+ '控制:\n' +
73
+ ' start [tun|mixed] 启动/切换代理 (默认 mixed)\n' +
74
+ ' stop 停止代理\n' +
75
+ ' status 查看状态\n' +
76
+ '\n' +
77
+ '界面:\n' +
78
+ ' ui [zash|dash|yacd] 打开 Web UI (默认 zash)\n' +
79
+ ' log [-o] 实时日志(-o 打开文件)\n' +
80
+ ' logs [编号] [-n N] [-o] 日志列表(0=当前,1+=归档)\n' +
81
+ '\n' +
82
+ '订阅:\n' +
83
+ ' subscription 列出所有订阅(别名 sub)\n' +
84
+ ' subscription add <url> [name] 添加订阅\n' +
85
+ ' subscription update [name] 更新订阅(无参更新所有)\n' +
86
+ ' subscription use <name> 切换默认订阅\n' +
87
+ ' subscription web [name] 打开订阅页面\n' +
88
+ '\n' +
89
+ '配置:\n' +
90
+ ' overwrite 查看覆写状态(别名 ow)\n' +
91
+ ' overwrite on|off 启用/禁用覆写配置\n' +
92
+ ' directory 显示数据目录位置(别名 dir)\n' +
93
+ ' directory open [target] 打开目录: root|subs|logs|overwrites|...\n' +
94
+ '\n' +
95
+ '系统:\n' +
96
+ ' kernel [镜像|--no-mirror] 更新内核\n' +
97
+ ' reset [--full] 重置用户数据 (--full 同时删除内核)\n' +
98
+ ' help, -h 显示帮助\n' +
99
+ ' version, -v 显示版本\n' +
100
+ '\n' +
101
+ '示例:\n' +
102
+ ' mihomo start # 启动/重启 Mixed 模式\n' +
103
+ ' mihomo start tun # 切换到 TUN 透明代理模式\n' +
104
+ ' mihomo sub add <url> # 添加订阅 (sub 是 subscription 别名)\n' +
105
+ ' mihomo ui # 打开 Web UI\n' +
106
+ '\n' +
107
+ '模式说明:\n' +
108
+ ' mixed HTTP + SOCKS5 混合端口 (默认)\n' +
109
+ ' tun 透明代理,全局自动路由,需要 sudo\n' +
110
+ '\n' +
111
+ '数据目录:\n' +
112
+ ' 环境变量 MIHOMO_CLI_DIR 可自定义位置\n' +
113
+ ' 默认: ' +
114
+ config.USER_DATA_DIR +
115
+ '\n',
116
+ );
118
117
  }
119
118
 
120
119
  function printVersion() {
@@ -126,7 +125,7 @@ function printVersion() {
126
125
 
127
126
  function printStatus() {
128
127
  const status = processMgr.getStatus();
129
- const info = subscription.getConfigInfo();
128
+ const info = config.getConfigInfo();
130
129
  const owEnabled = overwrite.isOverwriteEnabled();
131
130
  const owFiles = overwrite.listOverwriteFiles().files;
132
131
  const activeSub = getActiveSubscription();
@@ -160,12 +159,7 @@ function printStatus() {
160
159
  if (activeSub) {
161
160
  let subLine = '订阅: ' + activeSub.name;
162
161
  if (info) {
163
- let parts = [];
164
- if (info.proxyGroups && info.proxyGroups > 0) {
165
- parts.push(info.proxyGroups + ' 组');
166
- }
167
- parts.push(info.proxies + ' 节点');
168
- subLine += ' (' + parts.join(', ') + ')';
162
+ subLine += ' (' + subscription.formatProxySummary(info) + ')';
169
163
  }
170
164
  console.log(subLine);
171
165
  } else {
@@ -213,7 +207,7 @@ function findSubscriptionFuzzy(subs, pattern) {
213
207
  return includes;
214
208
  }
215
209
 
216
- function pickSingleSubscription(subs, pattern, actionName) {
210
+ function pickSingleSubscription(subs, pattern) {
217
211
  if (subs.length === 0) {
218
212
  console.error('错误: 未找到匹配 "' + pattern + '" 的订阅');
219
213
  process.exit(1);
@@ -291,7 +285,7 @@ function viewLogWithTail(logPath, options) {
291
285
  const tail = spawn('tail', tailArgs, { stdio: 'inherit' });
292
286
 
293
287
  tail.on('close', () => process.exit(0));
294
- tail.on('error', (e) => {
288
+ tail.on('error', e => {
295
289
  console.error('无法读取日志: ' + e.message);
296
290
  process.exit(1);
297
291
  });
@@ -344,10 +338,7 @@ async function cmdStart(args) {
344
338
  }
345
339
 
346
340
  const modeLabel = targetMode === 'tun' ? 'TUN' : 'Mixed';
347
- const parts = [];
348
- if (cfgInfo.proxyGroups && cfgInfo.proxyGroups > 0) parts.push(cfgInfo.proxyGroups + ' 组');
349
- parts.push(cfgInfo.proxies + ' 节点');
350
- console.log([modeLabel, sub.name, parts.join(', ')].join(' · '));
341
+ console.log([modeLabel, sub.name, subscription.formatProxySummary(cfgInfo)].join(' · '));
351
342
 
352
343
  try {
353
344
  const result = await processMgr.start(targetMode);
@@ -377,7 +368,7 @@ async function cmdStop() {
377
368
  console.log('已停止');
378
369
  }
379
370
 
380
- function cmdUi(args) {
371
+ function cmdUI(args) {
381
372
  const uiName = args[1] || 'zash';
382
373
  const url = UI_URLS[uiName];
383
374
 
@@ -396,7 +387,6 @@ function cmdUi(args) {
396
387
  }
397
388
  }
398
389
 
399
-
400
390
  function cmdLog(args) {
401
391
  const logPath = processMgr.getLogPath();
402
392
 
@@ -419,7 +409,20 @@ function cmdLogs(args) {
419
409
  if (targetName === 'current' || targetName === '0') {
420
410
  logPath = processMgr.getLogPath();
421
411
  } else {
422
- logPath = processMgr.getLogPathByName(targetName);
412
+ // 纯数字 1+ 表示归档日志的位置(最新=1)
413
+ const parsedIdx = parseInt(targetName);
414
+ if (!isNaN(parsedIdx) && parsedIdx > 0 && String(parsedIdx) === targetName) {
415
+ const archiveLogs = processMgr.listLogs();
416
+ const archive = archiveLogs.archives[parsedIdx - 1];
417
+ if (!archive) {
418
+ console.error('错误: 未找到日志 "' + targetName + '"');
419
+ console.log('使用 "mihomo logs" 查看可用日志列表');
420
+ process.exit(1);
421
+ }
422
+ logPath = archive.path;
423
+ } else {
424
+ logPath = processMgr.getLogPathByName(targetName);
425
+ }
423
426
  }
424
427
 
425
428
  if (!logPath) {
@@ -454,8 +457,15 @@ function cmdLogs(args) {
454
457
  console.log('日志列表:');
455
458
  console.log('');
456
459
 
457
- all.forEach((log, idx) => {
458
- const num = log.isCurrent ? ' 0' : (idx < 10 ? ' ' + idx : '' + idx);
460
+ let archiveCounter = 0;
461
+ all.forEach(log => {
462
+ let num;
463
+ if (log.isCurrent) {
464
+ num = ' 0';
465
+ } else {
466
+ archiveCounter++;
467
+ num = archiveCounter < 10 ? ' ' + archiveCounter : '' + archiveCounter;
468
+ }
459
469
  const time = subscription.formatDate(log.mtime);
460
470
  const size = subscription.formatBytes(log.size);
461
471
  const name = log.isCurrent ? 'mihomo.log (当前运行中)' : log.name;
@@ -463,16 +473,16 @@ function cmdLogs(args) {
463
473
  console.log(' ' + num + '. ' + name);
464
474
  console.log(' 时间: ' + time + ' 大小: ' + size);
465
475
  if (!log.isCurrent) {
466
- console.log(' 查看: mihomo logs ' + idx + ' 或 mihomo logs -o ' + idx);
476
+ console.log(' 查看: mihomo logs ' + archiveCounter + ' 或 mihomo logs ' + archiveCounter + ' -o');
467
477
  }
468
478
  console.log('');
469
479
  });
470
480
 
471
481
  console.log('用法:');
472
482
  console.log(' mihomo logs 0 # 查看当前日志 (最后 100 行)');
473
- console.log(' mihomo logs 1 # 查看第 1 个归档日志');
483
+ console.log(' mihomo logs 1 # 查看第 1 个归档日志(最新)');
474
484
  console.log(' mihomo logs 1 -n 200 # 查看 200 行');
475
- console.log(' mihomo logs 1 -o # 用系统默认程序打开');
485
+ console.log(' mihomo logs 1 -o # 用系统默认程序打开');
476
486
  console.log('');
477
487
  }
478
488
 
@@ -542,11 +552,8 @@ async function cmdKernel(args) {
542
552
 
543
553
  console.log('\n可用镜像:');
544
554
  config.AVAILABLE_MIRRORS.forEach(m => {
545
- const isCurrent = effectiveMirror && (
546
- effectiveMirror.includes('//' + m + '/') ||
547
- effectiveMirror.includes('//' + m + ':') ||
548
- effectiveMirror.endsWith('//' + m)
549
- );
555
+ const isCurrent =
556
+ effectiveMirror && (effectiveMirror.includes('//' + m + '/') || effectiveMirror.includes('//' + m + ':') || effectiveMirror.endsWith('//' + m));
550
557
  console.log(' ' + m + (isCurrent ? ' (当前)' : ''));
551
558
  });
552
559
 
@@ -568,9 +575,9 @@ async function cmdKernel(args) {
568
575
  }
569
576
 
570
577
  console.log('\n正在下载...');
571
- const result = await kernel.downloadKernel((msg) => {
578
+ const result = await kernel.downloadKernel(msg => {
572
579
  console.log(msg);
573
- }, mirrorInfo.mirror); // 传递镜像参数(undefined = 用配置,null = 禁用)
580
+ }, mirrorInfo.mirror); // 传递镜像参数(undefined = 用配置,null = 禁用)
574
581
  console.log('已更新到 ' + result.version);
575
582
  } catch (e) {
576
583
  console.error('更新失败: ' + e.message);
@@ -625,6 +632,7 @@ async function printSubscriptionList() {
625
632
  console.log('切换默认: mihomo sub use <name>');
626
633
  console.log('更新订阅: mihomo sub update [name]');
627
634
  console.log('打开页面: mihomo sub web [name]');
635
+ console.log('新增订阅: mihomo sub add <url> [name]');
628
636
  console.log('');
629
637
  }
630
638
 
@@ -649,10 +657,7 @@ async function cmdSubscription(args) {
649
657
  try {
650
658
  config.addSubscription(url, name);
651
659
  const info = await subscription.downloadSubscription(url, name);
652
- const parts = [];
653
- if (info.proxyGroups && info.proxyGroups > 0) parts.push(info.proxyGroups + ' 组');
654
- parts.push(info.proxies + ' 节点');
655
- console.log('已添加 (' + parts.join(', ') + ')');
660
+ console.log('已添加 (' + subscription.formatProxySummary(info) + ')');
656
661
  } catch (e) {
657
662
  console.error('添加失败: ' + e.message);
658
663
  process.exit(1);
@@ -678,10 +683,7 @@ async function cmdSubscription(args) {
678
683
  results.forEach(r => {
679
684
  if (r.success) {
680
685
  ok++;
681
- const parts = [];
682
- if (r.proxyGroups && r.proxyGroups > 0) parts.push(r.proxyGroups + ' 组');
683
- parts.push(r.proxies + ' 节点');
684
- console.log('✓ ' + r.name + ': 已更新 (' + parts.join(', ') + ')');
686
+ console.log('✓ ' + r.name + ': 已更新 (' + subscription.formatProxySummary(r) + ')');
685
687
  } else {
686
688
  console.log('✗ ' + r.name + ': 失败 (' + r.error.split('\n')[0] + ')');
687
689
  }
@@ -693,15 +695,12 @@ async function cmdSubscription(args) {
693
695
  }
694
696
 
695
697
  const matches = findSubscriptionFuzzy(subs, name);
696
- const target = pickSingleSubscription(matches, name, '更新');
698
+ const target = pickSingleSubscription(matches, name);
697
699
 
698
700
  console.log('更新订阅: ' + target.name);
699
701
  try {
700
702
  const info = await subscription.downloadSubscription(target.url, target.name);
701
- const parts = [];
702
- if (info.proxyGroups && info.proxyGroups > 0) parts.push(info.proxyGroups + ' 组');
703
- parts.push(info.proxies + ' 节点');
704
- console.log('已更新 (' + parts.join(', ') + ')');
703
+ console.log('已更新 (' + subscription.formatProxySummary(info) + ')');
705
704
  } catch (e) {
706
705
  console.error('更新失败: ' + e.message);
707
706
  process.exit(1);
@@ -725,7 +724,7 @@ async function cmdSubscription(args) {
725
724
  }
726
725
 
727
726
  const matches = findSubscriptionFuzzy(subs, name);
728
- const target = pickSingleSubscription(matches, name, '切换');
727
+ const target = pickSingleSubscription(matches, name);
729
728
 
730
729
  // 检查是否已是当前默认订阅
731
730
  const currentDefault = getActiveSubscription();
@@ -775,7 +774,7 @@ async function cmdSubscription(args) {
775
774
  let target;
776
775
  if (name) {
777
776
  const matches = findSubscriptionFuzzy(subs, name);
778
- target = pickSingleSubscription(matches, name, '打开');
777
+ target = pickSingleSubscription(matches, name);
779
778
  } else {
780
779
  target = subs[0];
781
780
  }
@@ -833,11 +832,11 @@ async function cmdReset(args) {
833
832
  const readline = require('readline');
834
833
  const rl = readline.createInterface({
835
834
  input: process.stdin,
836
- output: process.stdout
835
+ output: process.stdout,
837
836
  });
838
837
 
839
838
  const answer = await new Promise(resolve => {
840
- rl.question('确认? (y/N) ', (a) => {
839
+ rl.question('确认? (y/N) ', a => {
841
840
  rl.close();
842
841
  resolve(a);
843
842
  });
@@ -943,14 +942,14 @@ async function cmdOverwrite(args) {
943
942
 
944
943
  // 目录目标映射(精确匹配)
945
944
  const DIRECTORY_TARGETS = {
946
- 'root': { path: null, label: '根目录' },
947
- 'subs': { path: config.DIRS.subscriptions, label: '订阅目录' },
948
- 'logs': { path: config.DIRS.logs, label: '日志目录' },
949
- 'data': { path: config.DIRS.data, label: 'mihomo 数据目录' },
950
- 'runtime': { path: config.DIRS.runtime, label: '运行时目录' },
951
- 'overwrites': { path: config.DIRS.overwrites, label: '覆写目录' },
952
- 'settings': { path: config.PATHS.settingsFile, label: '设置文件' },
953
- 'kernel': { path: config.DIRS.core, label: '内核目录' },
945
+ root: { path: null, label: '根目录' },
946
+ subs: { path: config.DIRS.subscriptions, label: '订阅目录' },
947
+ logs: { path: config.DIRS.logs, label: '日志目录' },
948
+ data: { path: config.DIRS.data, label: 'mihomo 数据目录' },
949
+ runtime: { path: config.DIRS.runtime, label: '运行时目录' },
950
+ overwrites: { path: config.DIRS.overwrites, label: '覆写目录' },
951
+ settings: { path: config.PATHS.settingsFile, label: '设置文件' },
952
+ kernel: { path: config.DIRS.core, label: '内核目录' },
954
953
  };
955
954
 
956
955
  function cmdDirectory(args) {
@@ -1055,7 +1054,7 @@ async function main() {
1055
1054
  cmdLogs(args);
1056
1055
  break;
1057
1056
  case 'ui':
1058
- cmdUi(args);
1057
+ cmdUI(args);
1059
1058
  break;
1060
1059
  case 'kernel':
1061
1060
  await cmdKernel(args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mihomo-cli",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
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
@@ -34,8 +34,6 @@ const DIRS = {
34
34
 
35
35
  const PATHS = {
36
36
  root: DIRS.root,
37
- data: DIRS.data,
38
- userDataDir: USER_DATA_DIR,
39
37
  mihomoBinary: path.join(DIRS.core, 'mihomo'),
40
38
  settingsFile: path.join(USER_DATA_DIR, 'settings.json'),
41
39
  subscriptionsCacheFile: path.join(DIRS.subscriptions, 'cache.json'),
@@ -77,36 +75,41 @@ function maskUrl(url) {
77
75
  }
78
76
  }
79
77
 
78
+ let _settingsCache = null;
79
+
80
80
  function readSettings() {
81
+ if (_settingsCache !== null) return _settingsCache;
81
82
  ensureDirs();
82
83
  if (fs.existsSync(PATHS.settingsFile)) {
83
84
  try {
84
85
  const content = fs.readFileSync(PATHS.settingsFile, 'utf8');
85
- return JSON.parse(content);
86
+ _settingsCache = JSON.parse(content);
87
+ return _settingsCache;
86
88
  } catch (e) {
87
- return {};
89
+ _settingsCache = {};
90
+ return _settingsCache;
88
91
  }
89
92
  }
90
- return {};
93
+ _settingsCache = {};
94
+ return _settingsCache;
91
95
  }
92
96
 
93
97
  function writeSettings(settings) {
94
98
  ensureDirs();
95
99
  const existing = readSettings();
96
100
  const merged = { ...existing, ...settings };
101
+ // undefined 值表示删除该键
102
+ for (const key of Object.keys(settings)) {
103
+ if (settings[key] === undefined) delete merged[key];
104
+ }
97
105
  fs.writeFileSync(PATHS.settingsFile, JSON.stringify(merged, null, 2), { mode: 0o600 });
106
+ _settingsCache = merged;
98
107
  return merged;
99
108
  }
100
109
 
101
110
  // GitHub 镜像配置
102
111
  const DEFAULT_GITHUB_MIRROR = 'https://v6.gh-proxy.org/';
103
- const AVAILABLE_MIRRORS = [
104
- 'v6.gh-proxy.org',
105
- 'gh-proxy.org',
106
- 'hk.gh-proxy.org',
107
- 'cdn.gh-proxy.org',
108
- 'edgeone.gh-proxy.org',
109
- ];
112
+ const AVAILABLE_MIRRORS = ['v6.gh-proxy.org', 'gh-proxy.org', 'hk.gh-proxy.org', 'cdn.gh-proxy.org', 'edgeone.gh-proxy.org'];
110
113
 
111
114
  function getGitHubMirror() {
112
115
  const settings = readSettings();
@@ -125,9 +128,7 @@ function setGitHubMirror(mirror) {
125
128
  // - null 或 undefined: 恢复默认
126
129
 
127
130
  if (mirror === null || mirror === undefined) {
128
- const settings = readSettings();
129
- delete settings.github_mirror;
130
- writeSettings(settings);
131
+ writeSettings({ github_mirror: undefined });
131
132
  return DEFAULT_GITHUB_MIRROR;
132
133
  }
133
134
 
@@ -239,24 +240,28 @@ function hasKernel() {
239
240
  return fs.existsSync(PATHS.mihomoBinary);
240
241
  }
241
242
 
243
+ let _kernelVersionCache = undefined;
244
+
242
245
  function getKernelVersion() {
243
246
  if (!hasKernel()) {
247
+ _kernelVersionCache = undefined;
244
248
  return null;
245
249
  }
250
+ if (_kernelVersionCache !== undefined) return _kernelVersionCache;
246
251
  try {
247
252
  const output = execSync('"' + PATHS.mihomoBinary + '" -v 2>&1 || true', {
248
253
  encoding: 'utf8',
249
254
  }).trim();
250
255
  if (output) {
251
256
  const match = output.match(/v?[\d]+\.[\d]+\.[\d]+/);
252
- if (match) {
253
- return match[0];
254
- }
255
- return output;
257
+ _kernelVersionCache = match ? match[0] : output;
258
+ return _kernelVersionCache;
256
259
  }
257
- return 'unknown';
260
+ _kernelVersionCache = 'unknown';
261
+ return _kernelVersionCache;
258
262
  } catch (e) {
259
- return 'unknown';
263
+ _kernelVersionCache = 'unknown';
264
+ return _kernelVersionCache;
260
265
  }
261
266
  }
262
267
 
@@ -377,13 +382,7 @@ function resetUserData(options) {
377
382
  if (options === undefined) options = {};
378
383
  const keepKernel = options.keepKernel !== false;
379
384
 
380
- const itemsToRemove = [
381
- PATHS.settingsFile,
382
- DIRS.subscriptions,
383
- DIRS.logs,
384
- DIRS.data,
385
- DIRS.runtime,
386
- ];
385
+ const itemsToRemove = [PATHS.settingsFile, DIRS.subscriptions, DIRS.logs, DIRS.data, DIRS.runtime];
387
386
 
388
387
  if (!keepKernel) {
389
388
  itemsToRemove.push(DIRS.core);
@@ -402,37 +401,35 @@ function resetUserData(options) {
402
401
  }
403
402
 
404
403
  ensureDirs();
404
+ _settingsCache = null;
405
405
  return removedCount;
406
406
  }
407
407
 
408
408
  module.exports = {
409
409
  PATHS,
410
410
  DIRS,
411
- PROJECT_ROOT,
412
411
  USER_DATA_DIR,
413
- IS_PKG,
414
412
  ensureDirs,
415
413
  readSettings,
416
414
  writeSettings,
417
415
  readSubscriptionsCache,
418
- writeSubscriptionsCache,
419
416
  saveSubscriptionCache,
420
417
  maskUrl,
421
418
  getSubscriptions,
422
419
  getSubscriptionsWithCache,
423
420
  addSubscription,
424
421
  setDefaultSubscription,
425
- getSubRawConfigPath,
426
422
  saveSubRawConfig,
427
423
  readSubRawConfig,
428
424
  hasKernel,
429
425
  getKernelVersion,
426
+ clearKernelVersionCache: () => {
427
+ _kernelVersionCache = undefined;
428
+ },
430
429
  getGitHubMirror,
431
430
  setGitHubMirror,
432
431
  DEFAULT_GITHUB_MIRROR,
433
432
  AVAILABLE_MIRRORS,
434
- TUN_CONFIG,
435
- BASE_CONFIG,
436
433
  parseYamlOrJson,
437
434
  buildConfig,
438
435
  writeMihomoConfig,
package/src/kernel.js CHANGED
@@ -31,8 +31,7 @@ function getArch() {
31
31
  function findMatchingAsset(assets, platform, arch) {
32
32
  const prefix = 'mihomo-' + platform + '-' + arch;
33
33
  const matchingAssets = assets.filter(a => {
34
- return (a.name.startsWith(prefix) && a.name.endsWith('.gz')) ||
35
- (a.name.startsWith(prefix + '-') && a.name.endsWith('.gz'));
34
+ return (a.name.startsWith(prefix) && a.name.endsWith('.gz')) || (a.name.startsWith(prefix + '-') && a.name.endsWith('.gz'));
36
35
  });
37
36
 
38
37
  if (matchingAssets.length === 0) {
@@ -67,11 +66,12 @@ async function getLatestRelease(repo) {
67
66
  throw new Error('无法获取版本信息');
68
67
  }
69
68
 
70
- const stableReleases = releases.filter(r =>
71
- !r.prerelease &&
72
- !r.tag_name.toLowerCase().includes('alpha') &&
73
- !r.tag_name.toLowerCase().includes('beta') &&
74
- !r.tag_name.toLowerCase().includes('prerelease')
69
+ const stableReleases = releases.filter(
70
+ r =>
71
+ !r.prerelease &&
72
+ !r.tag_name.toLowerCase().includes('alpha') &&
73
+ !r.tag_name.toLowerCase().includes('beta') &&
74
+ !r.tag_name.toLowerCase().includes('prerelease'),
75
75
  );
76
76
 
77
77
  if (stableReleases.length > 0) {
@@ -194,14 +194,18 @@ async function downloadKernel(progressCallback, mirror) {
194
194
  extractedBinary = outputPath;
195
195
  }
196
196
  } catch (e) {
197
- try { fs.unlinkSync(tempPath); } catch {}
197
+ try {
198
+ fs.unlinkSync(tempPath);
199
+ } catch {}
198
200
  throw new Error('解压失败: ' + e.message);
199
201
  }
200
202
 
201
203
  const foundBinary = extractedBinary || findBinaryInDir(extractPath);
202
204
 
203
205
  if (!foundBinary) {
204
- try { fs.unlinkSync(tempPath); } catch {}
206
+ try {
207
+ fs.unlinkSync(tempPath);
208
+ } catch {}
205
209
  throw new Error('解压后未找到可执行文件');
206
210
  }
207
211
 
@@ -223,6 +227,9 @@ async function downloadKernel(progressCallback, mirror) {
223
227
  fs.unlinkSync(tempPath);
224
228
  } catch (e) {}
225
229
 
230
+ // 内核已更新,清除版本缓存
231
+ config.clearKernelVersionCache();
232
+
226
233
  return {
227
234
  version: latest.tag_name,
228
235
  path: targetPath,
@@ -230,9 +237,6 @@ async function downloadKernel(progressCallback, mirror) {
230
237
  }
231
238
 
232
239
  module.exports = {
233
- getArch,
234
- getLatestRelease,
235
- findMatchingAsset,
236
240
  checkUpdate,
237
241
  downloadKernel,
238
242
  };
package/src/process.js CHANGED
@@ -3,6 +3,11 @@ const path = require('path');
3
3
  const { spawn, execSync } = require('child_process');
4
4
  const config = require('./config');
5
5
 
6
+ const _sharedBuf = new Int32Array(new SharedArrayBuffer(4));
7
+ function sleepSync(ms) {
8
+ Atomics.wait(_sharedBuf, 0, 0, ms);
9
+ }
10
+
6
11
  function clearRuntime() {
7
12
  if (fs.existsSync(config.DIRS.runtime)) {
8
13
  config.rmrf(config.DIRS.runtime);
@@ -47,7 +52,10 @@ function getAllMihomoPids() {
47
52
  encoding: 'utf8',
48
53
  }).trim();
49
54
  if (!output) return [];
50
- return output.split('\n').filter(Boolean).map(p => parseInt(p));
55
+ return output
56
+ .split('\n')
57
+ .filter(Boolean)
58
+ .map(p => parseInt(p));
51
59
  } catch {
52
60
  return [];
53
61
  }
@@ -208,9 +216,7 @@ function cleanupAll(forceSudo) {
208
216
 
209
217
  for (let i = 0; i < 50; i++) {
210
218
  if (getAllMihomoPids().length === 0) break;
211
- try {
212
- execSync('sleep 0.1', { stdio: 'ignore' });
213
- } catch (e) {}
219
+ sleepSync(100);
214
220
  }
215
221
 
216
222
  clearPid();
@@ -227,14 +233,25 @@ function createTunLaunchScript() {
227
233
  const configFile = config.PATHS.configFile;
228
234
  const logFile = config.PATHS.logFile;
229
235
  const pidFile = config.PATHS.pidFile;
230
- const dataDir = config.PATHS.data;
231
-
232
- const scriptContent = '#!/bin/bash\n' +
233
- 'BINARY="' + binary + '"\n' +
234
- 'CONFIG_FILE="' + configFile + '"\n' +
235
- 'LOG_FILE="' + logFile + '"\n' +
236
- 'PID_FILE="' + pidFile + '"\n' +
237
- 'DATA_DIR="' + dataDir + '"\n' +
236
+ const dataDir = config.DIRS.data;
237
+
238
+ const scriptContent =
239
+ '#!/bin/bash\n' +
240
+ 'BINARY="' +
241
+ binary +
242
+ '"\n' +
243
+ 'CONFIG_FILE="' +
244
+ configFile +
245
+ '"\n' +
246
+ 'LOG_FILE="' +
247
+ logFile +
248
+ '"\n' +
249
+ 'PID_FILE="' +
250
+ pidFile +
251
+ '"\n' +
252
+ 'DATA_DIR="' +
253
+ dataDir +
254
+ '"\n' +
238
255
  '\n' +
239
256
  '# 终止旧进程\n' +
240
257
  'pkill -9 -f "${BINARY}" 2>/dev/null || true\n' +
@@ -361,10 +378,7 @@ async function startMixedMode(staleState) {
361
378
  throw new Error('未找到配置文件,请先添加订阅并启动');
362
379
  }
363
380
 
364
- const args = [
365
- '-d', config.PATHS.data,
366
- '-f', configFile,
367
- ];
381
+ const args = ['-d', config.DIRS.data, '-f', configFile];
368
382
 
369
383
  const out = fs.openSync(logFile, 'a');
370
384
  const err = fs.openSync(logFile, 'a');
@@ -389,7 +403,12 @@ async function startMixedMode(staleState) {
389
403
  try {
390
404
  const logs = fs.readFileSync(logFile, 'utf8').slice(-3000);
391
405
  if (logs.trim()) {
392
- errorMsg += '\n最近的日志:\n' + logs.split('\n').map(l => ' ' + l).join('\n');
406
+ errorMsg +=
407
+ '\n最近的日志:\n' +
408
+ logs
409
+ .split('\n')
410
+ .map(l => ' ' + l)
411
+ .join('\n');
393
412
  }
394
413
  } catch {}
395
414
  }
@@ -427,14 +446,18 @@ async function startTunMode(staleState) {
427
446
  timeout: 60000,
428
447
  });
429
448
  } catch (e) {
430
- try { fs.unlinkSync(launchScript); } catch (e2) {}
449
+ try {
450
+ fs.unlinkSync(launchScript);
451
+ } catch (e2) {}
431
452
  if (e.status === 1) {
432
453
  throw new Error('密码错误或取消');
433
454
  }
434
455
  throw new Error(e.message);
435
456
  }
436
457
 
437
- try { fs.unlinkSync(launchScript); } catch (e) {}
458
+ try {
459
+ fs.unlinkSync(launchScript);
460
+ } catch (e) {}
438
461
 
439
462
  await new Promise(resolve => setTimeout(resolve, 500));
440
463
 
@@ -446,7 +469,7 @@ async function startTunMode(staleState) {
446
469
  return { success: true, pid: finalPid, mode: 'tun' };
447
470
  }
448
471
 
449
- function stop(wasTunMode) {
472
+ function stop(forceSudo) {
450
473
  const allPids = getAllMihomoPids();
451
474
  if (allPids.length === 0) {
452
475
  clearPid();
@@ -454,7 +477,7 @@ function stop(wasTunMode) {
454
477
  return { success: true, notRunning: true };
455
478
  }
456
479
 
457
- const result = cleanupAll(wasTunMode);
480
+ const result = cleanupAll(forceSudo);
458
481
 
459
482
  const remaining = getAllMihomoPids();
460
483
  if (remaining.length > 0) {
@@ -490,10 +513,7 @@ function rotateLog() {
490
513
  return null;
491
514
  }
492
515
 
493
- const timestamp = new Date().toISOString()
494
- .replace(/T/, '_')
495
- .replace(/:/g, '-')
496
- .replace(/\..+/, '');
516
+ const timestamp = new Date().toISOString().replace(/T/, '_').replace(/:/g, '-').replace(/\..+/, '');
497
517
 
498
518
  const rotatedName = `mihomo.${timestamp}.log`;
499
519
  const rotatedPath = path.join(config.DIRS.logs, rotatedName);
@@ -616,33 +636,6 @@ function getLogPathByName(name) {
616
636
  return null;
617
637
  }
618
638
 
619
- function readLog(lines) {
620
- if (lines === undefined) lines = 100;
621
- if (!fs.existsSync(config.PATHS.logFile)) {
622
- return '(暂无日志)';
623
- }
624
-
625
- try {
626
- const content = fs.readFileSync(config.PATHS.logFile, 'utf8');
627
- const allLines = content.split('\n');
628
- return allLines.slice(-lines).join('\n');
629
- } catch (e) {
630
- return '(读取日志失败: ' + e.message + ')';
631
- }
632
- }
633
-
634
- function clearLog() {
635
- if (fs.existsSync(config.PATHS.logFile)) {
636
- try {
637
- fs.writeFileSync(config.PATHS.logFile, '');
638
- return true;
639
- } catch (e) {
640
- return false;
641
- }
642
- }
643
- return true;
644
- }
645
-
646
639
  function openUrl(url) {
647
640
  try {
648
641
  spawn('open', [url], { stdio: 'ignore', detached: true });
@@ -653,21 +646,12 @@ function openUrl(url) {
653
646
  }
654
647
 
655
648
  module.exports = {
656
- getPid,
657
- isRunning,
658
- isProcessRunning,
659
649
  getAllMihomoPids,
660
- isProcessRoot,
661
- checkStaleState,
662
650
  cleanupAll,
663
651
  getStatus,
664
652
  start,
665
653
  stop,
666
654
  getLogPath,
667
- readLog,
668
- clearLog,
669
- rotateLog,
670
- cleanupOldLogs,
671
655
  listLogs,
672
656
  getLogPathByName,
673
657
  openUrl,
@@ -67,6 +67,13 @@ function formatDate(dateOrIso) {
67
67
  }
68
68
  }
69
69
 
70
+ function formatProxySummary(info) {
71
+ const parts = [];
72
+ if (info && info.proxyGroups > 0) parts.push(info.proxyGroups + ' 组');
73
+ parts.push(((info && info.proxies) || 0) + ' 节点');
74
+ return parts.join(', ');
75
+ }
76
+
70
77
  async function downloadSubscription(url, subName) {
71
78
  if (subName === undefined) subName = 'default';
72
79
 
@@ -159,7 +166,7 @@ function needsAutoUpdate(sub) {
159
166
  if (isNaN(lastUpdate)) return true;
160
167
  const intervalHours = sub.update_interval || DEFAULT_UPDATE_INTERVAL_HOURS;
161
168
  const intervalMs = intervalHours * 60 * 60 * 1000;
162
- return (Date.now() - lastUpdate) > intervalMs;
169
+ return Date.now() - lastUpdate > intervalMs;
163
170
  }
164
171
 
165
172
  async function tryUpdateOne(sub) {
@@ -193,10 +200,7 @@ async function autoUpdateStaleSubscriptions() {
193
200
  results.forEach(r => {
194
201
  if (r.success) {
195
202
  updatedCount++;
196
- const parts = [];
197
- if (r.proxyGroups && r.proxyGroups > 0) parts.push(r.proxyGroups + ' 组');
198
- parts.push(r.proxies + ' 节点');
199
- console.log('✓ ' + r.name + ': 已更新 (' + parts.join(', ') + ')');
203
+ console.log('✓ ' + r.name + ': 已更新 (' + formatProxySummary(r) + ')');
200
204
  } else {
201
205
  console.log('✗ ' + r.name + ': 失败 (' + r.error.split('\n')[0] + ')');
202
206
  }
@@ -213,11 +217,10 @@ module.exports = {
213
217
  DEFAULT_UPDATE_INTERVAL_HOURS,
214
218
  downloadSubscription,
215
219
  prepareConfigForStart,
216
- hasConfig: config.hasConfig,
217
- getConfigInfo: config.getConfigInfo,
218
220
  formatBytes,
219
221
  formatTimestamp,
220
222
  formatDate,
223
+ formatProxySummary,
221
224
  tryUpdateOne,
222
225
  autoUpdateStaleSubscriptions,
223
226
  };